るいすのブログ

オタクエンジニアの雑記

RDS Proxy を使うとパフォーマンスは悪くなる

前回の続き
blog.luispc.com

目的

RDS Proxy 良いじゃん と前回記事を書いたけど、パフォーマンスは落ちると思ったので
どれぐらい落ちるかを超簡単にベンチマークをする。

HammerDB

hammerdb.com

HammerDB の使い方は atsuizo さんのを参考にしました。
atsuizo.hatenadiary.jp

TPC-C を流す

設定

hammerdb>print dict
Dictionary Settings for MySQL
connection {
 mysql_host = hasegawa.cluster.ap-northeast-1.rds.amazonaws.com
 mysql_port = 3306
}
tpcc       {
 mysql_count_ware       = 1
 mysql_num_vu           = 1
 mysql_user             = root
 mysql_pass             = ZokWAWywPwQtO7xr
 mysql_dbase            = tpcc
 mysql_storage_engine   = innodb
 mysql_partition        = false
 mysql_total_iterations = 1000000
 mysql_raiseerror       = false
 mysql_keyandthink      = false
 mysql_driver           = timed
 mysql_rampup           = 2
 mysql_duration         = 5
 mysql_allwarehouse     = false
 mysql_timeprofile      = false
 mysql_async_scale      = false
 mysql_async_client     = 10
 mysql_async_verbose    = false
 mysql_async_delay      = 1000
}
hammerdb>print vuconf
Virtual Users = 4
User Delay(ms) = 500
Repeat Delay(ms) = 500
Iterations = 1
Show Output = 1
Log Output = 0
Unique Log Name = 1
No Log Buffer = 0
Log Timestamps = 1

結果

5回計測した、平均 TPM

without RDS Proxy

TEST RESULT : System achieved 29349 MySQL TPM at 9754 NOPM
TEST RESULT : System achieved 23279 MySQL TPM at 7660 NOPM
TEST RESULT : System achieved 30078 MySQL TPM at 9929 NOPM
TEST RESULT : System achieved 30173 MySQL TPM at 9892 NOPM
TEST RESULT : System achieved 30125 MySQL TPM at 9940 NOPM

平均 TPM: 28,601

with RDS Proxy

TEST RESULT : System achieved 29353 MySQL TPM at 9709 NOPM
TEST RESULT : System achieved 20001 MySQL TPM at 6526 NOPM
TEST RESULT : System achieved 23056 MySQL TPM at 7553 NOPM
TEST RESULT : System achieved 20105 MySQL TPM at 6551 NOPM
TEST RESULT : System achieved 26769 MySQL TPM at 8721 NOPM

平均 TPM: 23,857

まとめ

RDS Proxy を使うとパフォーマンスは落ちる。

RDS Proxy を使うとフェイルオーバー時のコネクションプーリング問題が良い感じになるのでは?

目的

フェイルオーバー時のエラーレートを下げたい

RDS Proxy の公式ドキュメントに書かれている

Doesn't drop idle connections during failover, which reduces the impact on client connection pools

を試す

aws.amazon.com

環境

  • Aurora MySQL 2.08.1
  • Lambda (Go)

やり方

Lambda から DB(もしくは RDS Proxy) に対して 0.5秒間隔で Ping を打つ
3分間起動している間に 5回フェイルオーバーをする

コード

package main

import (
	"fmt"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
	"log"
	"os"
	"time"
)

type PingErr struct {
	Time time.Time
}

const (
	executeTimeSec = 300

	DBMaxOpenConn = 100
	DBMaxIdleConn = 10
	DBMaxLifeTime = time.Second * 10
)

func main() {
	lambda.Start(realMain)
}

func realMain() {
	time.Local = time.FixedZone("JST", 9*60*60)

	log.Println("initializing")
        // direct
	rw, err := gorm.Open("mysql", "root:ZokWAWywPwQtO7xr@tcp(hasegawa.cluster.ap-northeast-1.rds.amazonaws.com:3306)/mysql?charset=utf8mb4&parseTime=True&loc=Local")
        // proxy
	// rw, err := gorm.Open("mysql", "root:ZokWAWywPwQtO7xr@tcp(hasegawa.proxy.ap-northeast-1.rds.amazonaws.com:3306)/mysql?charset=utf8mb4&parseTime=True&loc=Local")
	defer rw.Close()
	if err != nil {
		panic(err)
	}

	rw.DB().SetMaxOpenConns(DBMaxOpenConn)
	rw.DB().SetMaxIdleConns(DBMaxIdleConn)
	rw.DB().SetConnMaxLifetime(DBMaxLifeTime)
	log.Println("finish initialized")

	now := time.Now().Unix()

	var pingErrs []PingErr
	for {
		diff := time.Now().Unix() - now

		if diff >= executeTimeSec {
			if len(pingErrs) != 0 {
				log.Println("ping error detected.", "count: ", len(pingErrs))

				for _, v := range pingErrs {
					fmt.Println(v.Time)
				}
			}

			log.Println("finish")
			os.Exit(0)
		}

		check(rw, &pingErrs)

		time.Sleep(time.Second / 2)
	}
}

func check(db *gorm.DB, pingErrs *[]PingErr) {
	err := db.DB().Ping()
	if err != nil {
		*pingErrs = append(*pingErrs, PingErr{Time: time.Now()})
	}
}

RDS Proxy を”使わない"場合

f:id:rarirureluis:20200701225305p:plain

46回 Ping でエラー

RDS Proxy を”使った"場合

f:id:rarirureluis:20200701225449p:plain

8回 Ping でエラー

もっとクエリ数を増やす

SELECT 1 を並列で流す。

SetConnMaxLifetime(DBMaxLifeTime) は直接つないだ時で使う。
RDS Proxy を経由するときはここを指定せずに全コネクションを永遠に使い回す設定にして試す。

package main

import (
	"context"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
	"log"
	"sync"
	"time"
)

type PingErr struct {
	Time time.Time
}

const (
	timeout    = 120 // sec
	goroutines = 100 // db.r5.large = 1000
	queryCount = 1000000

	DBMaxOpenConn = 900
	DBMaxIdleConn = 900
	DBMaxLifeTime = time.Second * 10
	DBQuery       = "SELECT 1"
)

var (
	wg sync.WaitGroup
)

func main() {
	lambda.Start(realMain)
	// realMain()
}

func realMain() {
	time.Local = time.FixedZone("JST", 9*60*60)

	log.Println("initializing")
	// rw, err := gorm.Open("mysql", "root:ZokWAWywPwQtO7xr@tcp(hasegawa.cluster.ap-northeast-1.rds.amazonaws.com:3306)/mysql?charset=utf8mb4&parseTime=True&loc=Local")
	rw, err := gorm.Open("mysql", "root:ZokWAWywPwQtO7xr@tcp(hasegawa.proxy.ap-northeast-1.rds.amazonaws.com:3306)/mysql?charset=utf8mb4&parseTime=True&loc=Local")
	defer rw.Close()
	if err != nil {
		panic(err)
	}

	rw.DB().SetMaxOpenConns(DBMaxOpenConn)
	rw.DB().SetMaxIdleConns(DBMaxIdleConn)
	rw.DB().SetConnMaxLifetime(DBMaxLifeTime)
	log.Println("finish initialized")

	var pingErrs []PingErr

	ctx, cancel := context.WithCancel(context.Background())
	q := make(chan *gorm.DB)
	for i := 0; i < goroutines; i++ {
		wg.Add(1)
		go check(ctx, q, &pingErrs)
	}

	now := time.Now().Unix()

	for i := 0; i < queryCount; i++ {
		diff := time.Now().Unix() - now
		if diff >= timeout {
			log.Println("timed out: ", timeout)
			break
		}
		q <- rw
	}

	cancel()
	wg.Wait()

	if len(pingErrs) != 0 {
		log.Println("ping error detected: ", len(pingErrs))
	}

	log.Println("finish")
}

func check(ctx context.Context, q chan *gorm.DB, pingErrs *[]PingErr) {
	for {
		select {
		case <-ctx.Done():
			wg.Done()
			return
		case db := <-q:
			err := db.Exec(DBQuery).Error
			if err != nil {
				*pingErrs = append(*pingErrs, PingErr{Time: time.Now()})
			}
		}
	}
}

f:id:rarirureluis:20200703162822p:plain

最大 16,000/qps が流れる。

RDS Proxy を”使わない”場合

f:id:rarirureluis:20200703162934p:plain

ping error になってるけど、実際は query error
RDS Proxy を使わない場合は 15,4056 が失敗

RDS Proxy を"使う”場合

f:id:rarirureluis:20200703163111p:plain

742

まとめ

RDS Proxy は月 1vCPU 辺り $0.018/h なので、案外安い
SetConnMaxLifetime これをこっちで意識しなくてよくなるし、フェイルオーバーの速さと、フェイルオーバー時のエラーレートも下がるので SLA/SLO が厳しい要件では入れおいたほうが良さそう。

RDS Proxy を通すことでホップ数が増えるのと、RDS Proxy 内部でも処理が走るので少なからずパフォーマンスが落ちるはず。

MySQL EXPLAIN の結果は良い感じなのに、何故か遅いクエリの原因を調べる

1回目は7秒かかるクエリが、2回目は速い。
実行計画を見ても、Slow Query を見てもインデックスは使われてそうなイキフンを感じる。

前置き

新しく Aurora MySQL を作成し、検証用のクエリ以外は流れないようにする。
遅いクエリは 7.5秒, 2回目実行すると 20ms とかで返ってくる。

クエリ

SELECT c1, c2, c3, c4, c5
FROM t1
WHERE ((c3 = 1489930231868609 and c4 in (7, 1169) and c2 between '2018-05-29 10:33:35.495' and '2020-05-29 10:33:35.495'))
ORDER BY c2 desc, c1 desc;

テーブル

mysql> show create table t1 \G;
*************************** 1. row ***************************
       Table: t1
Create Table: CREATE TABLE `t1` (
  `c1` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `c2` datetime NOT NULL,
  `c3` bigint(20) unsigned NOT NULL,
  `c4` bigint(20) unsigned NOT NULL,
  `c5` varchar(255) NOT NULL,
  `c6` varchar(255) NOT NULL,
  `c7` varchar(255) NOT NULL,
  `c8` varchar(255) DEFAULT NULL,
  `c9` datetime NOT NULL,
  `c10` datetime NOT NULL,
  PRIMARY KEY (`c1`,`c2`),
  KEY `idx_c3_c2` (`c3`,`c2`),
  KEY `c2_c4_idx` (`c2`,`c4)
) ENGINE=InnoDB AUTO_INCREMENT=2081930928 DEFAULT CHARSET=utf8
/*!50500 PARTITION BY RANGE  COLUMNS(c2)
(PARTITION part_201501 VALUES LESS THAN ('2015-01-01 00:00:00') ENGINE = InnoDB,
 ...
 PARTITION part_202010 VALUES LESS THAN ('2020-10-01 00:00:00') ENGINE = InnoDB,
 ... */

EXPLAIN

id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE t1 range idx_c3_c2, c2_c4_idx idx_c3_c2 13 NULL 21 Using where

Slow Query

# Query_time: 6.397257  Lock_time: 1.034133 Rows_sent: 0  Rows_examined: 1914

調べる

考えられる原因として、I/O に時間がかかってると予想し、
とりあえず Performance Schema を有効にする。

Performance Schema

mysql> SHOW VARIABLES LIKE 'per%';
mysql> UPDATE performance_schema.setup_instruments SET ENABLED = 'YES', TIMED = 'YES';
mysql> UPDATE performance_schema.setup_consumers SET ENABLED = 'YES';

file_summary_by_instance

ファイルごとに I/O の統計情報が見れる。

table_io_waits_summary_by_table という便利なテーブルもあるけど、何故か Aurora MySQL だと見れなかった。
今回は検証用の DB なので他のクエリは流れないのでこれで行く。

純粋にテーブルを見ても良いし、ps-top を見ても良い。
もし、I/O が発生していたら Buffer Pool に載っておらず、Buffer Pool への取り込むのに時間がかかっていると予測できる。

github.com

MySQL 5.7 で EXPLAIN をする

MySQL 5.7 では EXPLAIN の情報が増えていて、partitions というカラムがある。
パーティショニングされたテーブルだと、舐めるであろうパーティションが見れる

id|select_type|table          |partitions                                                                                                                                                                                                                                                     |type |possible_keys                                 |key               |key_len|ref|rows|filtered|Extra                             |
--|-----------|---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----|----------------------------------------------|------------------|-------|---|----|--------|----------------------------------|
 1|SIMPLE     |t1|part_201806,part_201807,part_201808,part_201809,part_201810,part_201811,part_201812,part_201901,part_201902,part_201903,part_201904,part_201905,part_201906,part_201907,part_201908,part_201909,part_201910,part_201911,part_201912,part_202001,part_202002,par|range|idx_account_number,execute_date_partner_id_idx|idx_account_number|13     |   |1914|    20.0|Using index condition; Using where|

結果

Buffer Pool が空の状態と、2回目の file_summary_by_instance を比較する

1回目

f:id:rarirureluis:20200615222043p:plain

2回目

f:id:rarirureluis:20200615222057p:plain

1回目でクエリが返ってくるまで 7.5秒ぐらいあったので、ほとんどが I/O による時間であることが予測できる。
2回目は Buffer Pool にデータが載ってるので I/O が発生せずに数ミリ秒で返ってくる。

このテーブルは数億行のテーブルで、頻繁にアクセスもないようなので Buffer Pool に常に載っているわけではないと予想。

解決方法

教えてください。

ふと思いつくのは

  • innodb_file_per_table を付ける(今回はついてた)
  • 不要なレコード数を削除して、Buffer Pool に乗る量を減らす(不要なレコードはない)
  • クエリを見直して舐める行数を減らす(舐める行数は2000行ぐらいしかないけど、パーティショニングの範囲が広すぎるせい)
  • それでもダメなら Buffer Pool を増やす(これが一番?)

ぐらいなのかなぁ。と思ったけどパーティションを20以上も見てるということはこれ以上 I/O は削減できなさそう。
いらないレコード削除できるだけでも良いかもしれないけどこれは履歴を保存してるのでそれは厳しそう。

今んとこの解決方法としてはメモリを増やす ということしか思いつかば無いんですが
他にいい案があったら教えてください。

EXPLAIN と実際の舐める行数が剥離する理由

WHERE 句によってその時どきで結果は変わるけど、今回の WHERE 句には不変な値しか指定していないのにも関わらず
EXAPLAIN では rows 21、Slow Query 読みだと 1914行と90倍の差がある。

EXPLAIN はインデックスのカーディナリティから推測する。
もし、オプティマイザがベストなインデックスを選択していれば実際に舐める行数(今回で言う Slow Query)と差異があっても
何も問題ないよう。

勉強になりました。

8.0.19 以降ならもっと便利に

EXPLAIN ANALYZE

Aurora MySQL でスロークエリの調査をするのはダルい

Survivable Cache Warming という、機能のせいで DB インスタンスを再起動してもキャッシュが残るので
空にするには逐一 DB を作り直さないといけない。
docs.aws.amazon.com

Pulumi を使えば Infrastructure as Code の本来の目的が果たせると思う

IaC は DevOps の中で重要な立ち位置に居て、インフラ専門部隊だけではなく、バックエンドの開発者も柔軟に構成が変更できるためには
今までの Terraform や Ansible では敷居が高かった(ツール独自のループの書き方とか色々)。だけど Pulumi や aws cdk の登場により好きな言語でリソース管理ができるならその敷居はもっと下がって無駄なコストを減らせると思う(リソースの変更を願いせずとも PR 出せばいいとか)

Pulumi

好きな言語で Infrastructure as Code を実現できるフレームワーク?ツール?
AWS だけではなく、GCP、Azure、Kubernetes、その他 SaaS にも対応してる。
対応言語は Go, Python, Node.js, .NET.Core

仕組みは Terraform とほぼ一緒。

www.pulumi.com

Pulumi の良いところ

Terraform や Ansible といった独自言語(HCL)、もしくはツールに対しての知識は不要で好きな言語で IaC を実現できるのでインフラ専属じゃない人でも気軽にリソースを弄ることができる。(Terraform のバージョン追従みたいので辛い思いしなくていいし、loop の書き方をいちいち調べなくても良い)
また、Pulumi は Go にも対応してるので aws-cdk にはないメリット。

www.pulumi.com

そして、Pulumi の provider は Terraform の provider から生成されてるので tf2pulumi とかいうマイグレツールもある。
github.com

Aurora Cluster を作ってみる

CLI のインストールや、state ファイルの指定などは公式ドキュメントへ。

全貌

func createAuroraSecurityGroup(ctx *pulumi.Context) (*ec2.SecurityGroup, error) {
	name := fmt.Sprintf("aurora-%s-%s", env, prj)

	args := &ec2.SecurityGroupArgs{
		VpcId: pulumi.String("vpc-0ccbc720526afbcff"),
		Ingress: ec2.SecurityGroupIngressArray{
			ec2.SecurityGroupIngressArgs{
				CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")},
				FromPort:   pulumi.Int(3306),
				ToPort:     pulumi.Int(3306),
				Protocol:   pulumi.String("TCP"),
			},
		},
		Egress: ec2.SecurityGroupEgressArray{
			ec2.SecurityGroupEgressArgs{
				CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")},
				FromPort:   pulumi.Int(0),
				ToPort:     pulumi.Int(0),
				Protocol:   pulumi.String("-1"),
			},
		},
		Name: pulumi.String(name),
		Tags: tags,
	}

	return ec2.NewSecurityGroup(ctx, name, args)
}

func createAuroraSubnetGroup(ctx *pulumi.Context) (*rds.SubnetGroup, error) {
	args := &rds.SubnetGroupArgs{
		Name:      pulumi.String(fmt.Sprintf("%s", prj)),
		SubnetIds: pulumi.StringArray{pulumi.String("subnet-082199a2239986516"), pulumi.String("subnet-00e5e8eda3f4cdef1")},
		Tags:      tags,
	}

	return rds.NewSubnetGroup(ctx, fmt.Sprintf("%s", prj), args)
}

func createAuroraClusterParameterGroup(ctx *pulumi.Context) (*rds.ClusterParameterGroup, error) {
	timeZone := rds.ClusterParameterGroupParameterArgs{
		Name:  pulumi.String("time_zone"),
		Value: pulumi.String("Asia/Tokyo"),
	}

	args := &rds.ClusterParameterGroupArgs{
		Family:     pulumi.String("aurora-mysql5.7"),
		Parameters: rds.ClusterParameterGroupParameterArray{timeZone},
		Tags:       tags,
	}

	return rds.NewClusterParameterGroup(ctx, fmt.Sprintf("%s-%s", env, prj), args)
}

func createAuroraCluster(ctx *pulumi.Context, clusterParameterGroupName, subnetGroupName pulumi.StringPtrInput, securityGroupID pulumi.IDOutput) (*rds.Cluster, error) {
	name := fmt.Sprintf("%s-%s-cluster", env, prj)

	args := &rds.ClusterArgs{
		ApplyImmediately:            pulumi.Bool(true),
		ClusterIdentifier:           pulumi.String(name),
		DbClusterParameterGroupName: clusterParameterGroupName,
		DbSubnetGroupName:           subnetGroupName,
		Engine:                      pulumi.String("aurora-mysql"),
		EngineMode:                  pulumi.String("provisioned"),
		EngineVersion:               pulumi.String("5.7.mysql_aurora.2.07.2"),
		MasterPassword:              pulumi.String(c.Require("aurora_master_password")),
		MasterUsername:              pulumi.String(c.Require("aurora_master_username")),
		Tags:                        tags,
		VpcSecurityGroupIds:         pulumi.StringArray{securityGroupID},
	}

	return rds.NewCluster(ctx, name, args)
}

func createAuroraClusterInstance(ctx *pulumi.Context, cluster *rds.Cluster) error {
	count := c.RequireInt("aurora_instances")

	for i := 0; i < count; i++ {
		name := fmt.Sprintf("%s-%s-instance-%d", env, prj, i)

		args := &rds.ClusterInstanceArgs{
			ApplyImmediately:           pulumi.Bool(true),
			ClusterIdentifier:          cluster.ID(),
			Engine:                     pulumi.String("aurora-mysql"),
			EngineVersion:              pulumi.String("5.7.mysql_aurora.2.07.2"),
			Identifier:                 pulumi.String(name),
			InstanceClass:              pulumi.String(c.Require("aurora_instance_class")),
			PerformanceInsightsEnabled: pulumi.Bool(false), // 2020/05/26 5.7.mysql_aurora.2.07.2 is not supported
			PubliclyAccessible:         pulumi.Bool(false),
			Tags:                       tags,
		}

		_, err := rds.NewClusterInstance(ctx, name, args)
		if err != nil {
			return err
		}
	}

	return nil
}

Tips

使ってみた感じ

configuration variable

外部から注入する変数みたいなもので

$ pulumi config set env stg

env 変数を作って、コードで取得する際は c.Require("env") とするだけ
この変数もスタック(dev 用とか、stg 用とかで分けれる)ごとに分かれる。

configuration variable を取得する際にエラーハンドリングをしなくても落ちてくれる
Diagnostics:
  pulumi:pulumi:Stack (test-pulumi-stg):
    panic: fatal: A failure has occurred: missing required configuration variable 'test-pulumi:project'; run `pulumi config` to set
env = c.Require("env")
if env == "" {
	return fmt.Errorf("'env' variable not defined")
}

こんなことしなくて OK

既存のリソースに対しての変更もちゃんとできる

pulumi で作ったリソースのコードにタグを追加した結果 ↓

Previewing update (stg):
     Type                 Name                Plan       Info
     pulumi:pulumi:Stack  test-pulumi-stg             1 message
 ~   └─ aws:ecs:Cluster   stg-test         update     [diff: ~tags]

Diagnostics:
  pulumi:pulumi:Stack (test-pulumi-stg):
    &{CustomResourceState:{ResourceState:{urn:{OutputState:0xc000207c00} providers:map[] aliases:[] name:stg-test transformations:[]} id:{OutputState:0xc000207b90}} Arn:{OutputState:0xc0002078f0} CapacityProviders:{OutputState:0xc000207960} DefaultCapacityProviderStrategies:{OutputState:0xc0002079d0} Name:{OutputState:0xc000207a40} Settings:{OutputState:0xc000207ab0} Tags:{OutputState:0xc000207b20}}

エラー内容も結構分かりやすい

Previewing update (stg):
     Type                      Name                Plan     Info
     pulumi:pulumi:Stack       test-pulumi-stg
     └─ aws:ec2:SecurityGroup  aurora-stg-test           3 errors

Diagnostics:
  aws:ec2:SecurityGroup (aurora-stg-test):
    error: aws:ec2/securityGroup:SecurityGroup resource 'aurora-stg-test' has a problem: "ingress.0.from_port": required field is not set
    error: aws:ec2/securityGroup:SecurityGroup resource 'aurora-stg-test' has a problem: "ingress.0.to_port": required field is not set
    error: aws:ec2/securityGroup:SecurityGroup resource 'aurora-stg-test' has a problem: "ingress.0.protocol": required field is not set

手で消したリソースを pulumi にも反映させる方法

$ pulumi state delete urn:pulumi:stg::test-pulumi::aws:rds/cluster:Cluster::stg-test-cluster
 warning: This command will edit your stack's state directly. Confirm? Yes
 Multiple resources with the given URN exist, please select the one to edit: "tf-20200525154551533900000001" (Pending Deletion)
Resource deleted successfully

俺的監視ベストプラクティス

入門監視を読んで、社内のドキュメントに書いたものをブログにも書く。
今回のは VM を対象。

監視俺的ベストプラクティス(VM編)

前提

  • 監視ツールは1つに絞らなくていい
  • クリティカルなものはサービス影響が出るものだけ
  • 障害時に参考になりそうなものはグラフ化する
  • できるだけユーザーに近いところを監視する
  • アラートには絶対メモをつける
    • 影響、解決方法

warning と critical の使い分け

❌ warning を @here で通知する

warning 見たって対応しないでしょ? アラート部屋に通知だけして、メンションは無しが良さそう

❌メールで通知受ける

Slack で良いじゃん?

メールでのアラートをやめたほうがいい5つの理由 | PagerDuty by Digital Stacks

CPU 使用率と、ディスク容量について

❌ Load Average をトリガーにする

LA は CPU の使用率を見るものとしては正しいが負荷がどうとかで見るのは間違い。 実行中のプロセスが多ければ必然的に LA も高くなる。 参考:https://www.techscore.com/blog/2017/12/08/how_is_load_average_calculated/

ディスク容量の監視

80% ≥ warning にするとあとで良いやってなる。 じゃあ 95% ≥ critical はどうでしょう。あと 5% でその VM は障害になるかもしれない!

DB は例外で考えても良いかも DELETE + ALTER TABLE で容量を確保する際に、ALTER でコピー分の容量も必要なため 80 ≥ critical ぐらいが良さそう (スレーブで一時的に停止できるなら、停止してからボリュームの拡張でも良さそう)

自動復旧について

例えばプロセスが落ちたトリガーのフックに、プロセスの再起動を仕込んだりする。 人間の手が不要で勝手にプロセスが立ち上がってくれる。

MySQL が OOM で該当プロセスが死んだ場合は注意 OOM は SIGKILL を発行するが、もし MySQL の設定が sync_binlog = 0 や doublewrite ≠ 1 の場合はプロセスは起動できてもレプリが死んでたりするので MySQL に関しては自動復旧しないことをオススメする。

アラートメッセージは大事

何故これを監視して、どう影響を与えて、どうやって復旧するのか

をアラートメッセージに書いとくのが重要。アラートを見た人(サービスの担当者が見ても対応ができる)

監視すべきメトリクス

1. レスポンスタイム 95 パーセンタイルで 5秒超え

コメント例

95パーセンタイルでレスポンスタイムが5秒を超えてる。 レスポンスタイムの影響は大きいよ。 何が原因かは調べてください。

平均値でレスポンスタイムを計測するのは間違い

2. ステータスコード 5xx が n分間に m%

コメント例

Apache(Tomcat)が HTTP 5xx を 1分間で 2% 以上レスポンスを返してる。 DB や Memcached、負荷等、何かが要因になってる可能性有り。

3. プロセス死活監視

mysql とか Apache とかサービスに影響あるもの

例えば API サーバーの1台のプロセスが死んだだけでサービスにクリティカルな影響を与えるとは限らないため、API サーバーのうち n/m が死んだらクリティカルにするっていうのも良い。もしくは Critical ではなく Warning で出すとか。ちなみに、Mackerel だとできない。

4. ディスク容量

≠ DB warning ≥ 90 critical ≥ 95

コメント例

空き容量が残り 5% しかない 対応しないと障害になる可能性有り 主な対応方法は不要なファイルを削除していく、コマンド例は下記を参照 まずはルートから辿っていく du -sh /* | sort -hr | head -n 10 更にそのディレクトリを辿ってく du -sh /directory/* | sort -hr | head -n 10

= DB warning ≥ 70 critical ≥ 80

ただ EC2 にしろ、OpenStack(Cinder)にしろ容量の拡張ができるので DB も 95 クリティカルでも良いかもしれない。

コメント例

DB の空き容量が残り 20% しかない DELETE + ALERT TABLE ではコピー分の容量も必要なため 80 >= critical にしてる メンテ無しでやるなら レコードを削除して ALTER TABLE t1 ENGINE=InnoDB; メンテが挟めるなら 適当にテーブル作って、select insert -> rename -> drop 詳細 https://bit.ly/31WIZLg

監視俺的ベストプラクティス(VM編)

5. MySQL の監視

5.1 レプリ遅延

critical ≥ 3 ※ Mackerel でも取れるけど、Mackerel の計測間隔は最低1分なのでここを許容できるかどうか

コメント例

レプリ遅延が3秒以上遅れてる。 この閾値は3分間の平均値が3回連続でトリガーされる値で、継続的にレプリ遅延が発生中。 データの取得に不整合がある可能性有り。 今すぐできることはあらず。 解消されない場合、メンテを入れることも視野に入れてください。

5.2 スロークエリ

warning ≥ 10 (critical にしないのは、レスポンスタイムで賄えるから) ※ Mackerel でも取れるけど、Mackerel の計測間隔は最低1分なのでここを許容できるかどうか

コメント例

スロークエリが直近1分間で10クエリ以上有り。 Table Locks Waited も多い場合、ロックの競合が起きてる可能性有り。クエリの見直しや、他の要因も考えられる。

5.3 コネクション数の割合

warning ≥ 70 critical ≥ 80

コメント例

Max Connections の使用率が 70% / 80% を超えた。 これが高負荷時でなら問題はなく閾値の調整を。 通常時に起きている場合は Max Connections の引き上げを検討して。 もし、Max Connections に当たると新規コネクションが貼れなくなるので普通に障害になる。

SPA は SSR を使わなくても Google 検索で1位取れる

巷では SPA ( not SSR ) は SEO に不利とか、SEO 専門の人がそう言ってたりする。
Google で「SPA SEO」とかで検索すると、トップページはできるだけ SSR にしたほうがいい とか見られる。

けど現に SPA ( not SSR ) で作った Webサービスは2つのキーワードで SEO 1位を取ってる。
uploader.xzy.pw

一部に SSR を使ったりせずに本当にピュア?な SPA で出来てます。

Googleクローラーは昨年末に Chrome のバージョンが上がり、SPA のレンダリングができるようになったけど、
Yahoo 検索は心配かと想いきや、Yahoo 検索でも1位取れてる。

あんまり気にしなくても良いかもしれない。

f:id:rarirureluis:20200522212441p:plain

f:id:rarirureluis:20200522212603p:plain

f:id:rarirureluis:20200522212610p:plain

まだ S3 + CloudFront で消耗してるの?Cloudflare + Backblaze の方が安いよ

Web サービスを提供してたり、アセットの配布で S3 + CloudFront を使ってる人は多いと思います。
が、この度 Backblaze が S3 Compatible API をリリースしたので Cloudflare と組み合わせればマイグレーションのコストを少なく(AWS SDK も使えるし、AWS CLI も使える)Backblaze へ移行できる。
blog.cloudflare.com
www.backblaze.com

コストの比較

10TB のファイルを毎月 10TB を送信し、1000万回 GETを想定すると

S3 + CloudFront

S3

ストレージ:0.025/GB * 10,000GB = $250
送信:$0

CloudFront

転送量:0.114/GB * 10,000GB = $1,140
リクエスト:0.0090/1万 * 1000 = $9

合計
$250 + $1,140 = $1,399 / 月

Backblaze + Cloudflare

Backblaze

ストレージ:0.005/GB * 10,000GB = $50
送信:0.01/GB * 10,000GB = $1,100

Cloudflare

転送量:$0
リクエスト:$0

合計
$50 + $1,100 = $1,150 / 月

差額
$1,399 - $1,150 = $249 / 月

Backblaze + Cloudflare の方が毎月 $249 お得だった。
思ったほどインパクトはなかった、、、。

12 * $249 = $2,988
年計算なら、毎年 30万以上は節約できる。

こんな臭いタイトルにしてそんなに良い結果ではなくて申し訳ありません。

注意

Backblaze の S3 Compatible API はリリースされた間もないことも有り
Server Side Encryption と CORS に対応していない。
www.backblaze.com

uploader.xzy.pw

これを Backblaze へ移行しようと思ったけど、CORS も SSE も対応してなかったのでできなかった。

AWS EC2 m6g, c6g, r6g は本当に速くなったのか

m系だけで比較 UnixBench

AMI
m6g.large: amzn2-ami-hvm-2.0.20200406.0-arm64-gp2
m5.large: amzn2-ami-hvm-2.0.20200406.0-x86_64-gp2

# yum update -y
# yum groupinstall '@Development Tools'
# wget https://github.com/kdlucas/byte-unixbench/archive/master.zip
# unzip master.zip

m5.large

2 CPUs in system; running 1 parallel copy of tests

Dhrystone 2 using register variables       38850809.4 lps   (10.0 s, 7 samples)
Double-Precision Whetstone                     4363.1 MWIPS (9.1 s, 7 samples)
Execl Throughput                               4709.3 lps   (30.0 s, 2 samples)
File Copy 1024 bufsize 2000 maxblocks        594108.7 KBps  (30.0 s, 2 samples)
File Copy 256 bufsize 500 maxblocks          154550.5 KBps  (30.0 s, 2 samples)
File Copy 4096 bufsize 8000 maxblocks       1970248.9 KBps  (30.0 s, 2 samples)
Pipe Throughput                              747070.6 lps   (10.0 s, 7 samples)
Pipe-based Context Switching                  66250.9 lps   (10.0 s, 7 samples)
Process Creation                              11919.4 lps   (30.0 s, 2 samples)
Shell Scripts (1 concurrent)                   7624.2 lpm   (60.0 s, 2 samples)
Shell Scripts (8 concurrent)                   1214.2 lpm   (60.0 s, 2 samples)
System Call Overhead                         412715.6 lps   (10.0 s, 7 samples)

System Benchmarks Index Values               BASELINE       RESULT    INDEX
Dhrystone 2 using register variables         116700.0   38850809.4   3329.1
Double-Precision Whetstone                       55.0       4363.1    793.3
Execl Throughput                                 43.0       4709.3   1095.2
File Copy 1024 bufsize 2000 maxblocks          3960.0     594108.7   1500.3
File Copy 256 bufsize 500 maxblocks            1655.0     154550.5    933.8
File Copy 4096 bufsize 8000 maxblocks          5800.0    1970248.9   3397.0
Pipe Throughput                               12440.0     747070.6    600.5
Pipe-based Context Switching                   4000.0      66250.9    165.6
Process Creation                                126.0      11919.4    946.0
Shell Scripts (1 concurrent)                     42.4       7624.2   1798.2
Shell Scripts (8 concurrent)                      6.0       1214.2   2023.7
System Call Overhead                          15000.0     412715.6    275.1
                                                                   ========
System Benchmarks Index Score                                        1021.9

------------------------------------------------------------------------
Benchmark Run: 月  5月 18 2020 09:31:17 - 09:59:20
2 CPUs in system; running 2 parallel copies of tests

Dhrystone 2 using register variables       55968401.9 lps   (10.0 s, 7 samples)
Double-Precision Whetstone                     7327.2 MWIPS (9.3 s, 7 samples)
Execl Throughput                               6701.5 lps   (30.0 s, 2 samples)
File Copy 1024 bufsize 2000 maxblocks        789430.5 KBps  (30.0 s, 2 samples)
File Copy 256 bufsize 500 maxblocks          204553.5 KBps  (30.0 s, 2 samples)
File Copy 4096 bufsize 8000 maxblocks       2679537.8 KBps  (30.0 s, 2 samples)
Pipe Throughput                             1012333.1 lps   (10.0 s, 7 samples)
Pipe-based Context Switching                 282192.2 lps   (10.0 s, 7 samples)
Process Creation                              19656.0 lps   (30.0 s, 2 samples)
Shell Scripts (1 concurrent)                   8858.5 lpm   (60.0 s, 2 samples)
Shell Scripts (8 concurrent)                   1224.7 lpm   (60.0 s, 2 samples)
System Call Overhead                         540338.4 lps   (10.0 s, 7 samples)

System Benchmarks Index Values               BASELINE       RESULT    INDEX
Dhrystone 2 using register variables         116700.0   55968401.9   4795.9
Double-Precision Whetstone                       55.0       7327.2   1332.2
Execl Throughput                                 43.0       6701.5   1558.5
File Copy 1024 bufsize 2000 maxblocks          3960.0     789430.5   1993.5
File Copy 256 bufsize 500 maxblocks            1655.0     204553.5   1236.0
File Copy 4096 bufsize 8000 maxblocks          5800.0    2679537.8   4619.9
Pipe Throughput                               12440.0    1012333.1    813.8
Pipe-based Context Switching                   4000.0     282192.2    705.5
Process Creation                                126.0      19656.0   1560.0
Shell Scripts (1 concurrent)                     42.4       8858.5   2089.3
Shell Scripts (8 concurrent)                      6.0       1224.7   2041.2
System Call Overhead                          15000.0     540338.4    360.2
                                                                   ========
System Benchmarks Index Score                                        1523.2

m6g.large

2 CPUs in system; running 1 parallel copy of tests

Dhrystone 2 using register variables       40582507.5 lps   (10.0 s, 7 samples)
Double-Precision Whetstone                     5912.0 MWIPS (9.6 s, 7 samples)
Execl Throughput                               7006.5 lps   (30.0 s, 2 samples)
File Copy 1024 bufsize 2000 maxblocks       1041850.6 KBps  (30.0 s, 2 samples)
File Copy 256 bufsize 500 maxblocks          291910.6 KBps  (30.0 s, 2 samples)
File Copy 4096 bufsize 8000 maxblocks       2952450.1 KBps  (30.0 s, 2 samples)
Pipe Throughput                             1790930.7 lps   (10.0 s, 7 samples)
Pipe-based Context Switching                 134169.8 lps   (10.0 s, 7 samples)
Process Creation                              11434.1 lps   (30.0 s, 2 samples)
Shell Scripts (1 concurrent)                   8528.6 lpm   (60.0 s, 2 samples)
Shell Scripts (8 concurrent)                   1658.2 lpm   (60.0 s, 2 samples)
System Call Overhead                        1733100.7 lps   (10.0 s, 7 samples)

System Benchmarks Index Values               BASELINE       RESULT    INDEX
Dhrystone 2 using register variables         116700.0   40582507.5   3477.5
Double-Precision Whetstone                       55.0       5912.0   1074.9
Execl Throughput                                 43.0       7006.5   1629.4
File Copy 1024 bufsize 2000 maxblocks          3960.0    1041850.6   2630.9
File Copy 256 bufsize 500 maxblocks            1655.0     291910.6   1763.8
File Copy 4096 bufsize 8000 maxblocks          5800.0    2952450.1   5090.4
Pipe Throughput                               12440.0    1790930.7   1439.7
Pipe-based Context Switching                   4000.0     134169.8    335.4
Process Creation                                126.0      11434.1    907.5
Shell Scripts (1 concurrent)                     42.4       8528.6   2011.5
Shell Scripts (8 concurrent)                      6.0       1658.2   2763.6
System Call Overhead                          15000.0    1733100.7   1155.4
                                                                   ========
System Benchmarks Index Score                                        1649.2

------------------------------------------------------------------------
Benchmark Run: 月  5月 18 2020 09:35:22 - 10:03:18
2 CPUs in system; running 2 parallel copies of tests

Dhrystone 2 using register variables       81093723.6 lps   (10.0 s, 7 samples)
Double-Precision Whetstone                    11816.9 MWIPS (9.6 s, 7 samples)
Execl Throughput                              11173.2 lps   (30.0 s, 2 samples)
File Copy 1024 bufsize 2000 maxblocks       1311901.0 KBps  (30.0 s, 2 samples)
File Copy 256 bufsize 500 maxblocks          405826.8 KBps  (30.0 s, 2 samples)
File Copy 4096 bufsize 8000 maxblocks       3190465.5 KBps  (30.0 s, 2 samples)
Pipe Throughput                             3586570.9 lps   (10.0 s, 7 samples)
Pipe-based Context Switching                 638429.7 lps   (10.0 s, 7 samples)
Process Creation                              22963.7 lps   (30.0 s, 2 samples)
Shell Scripts (1 concurrent)                  12549.6 lpm   (60.0 s, 2 samples)
Shell Scripts (8 concurrent)                   1706.7 lpm   (60.0 s, 2 samples)
System Call Overhead                        2563990.0 lps   (10.0 s, 7 samples)

System Benchmarks Index Values               BASELINE       RESULT    INDEX
Dhrystone 2 using register variables         116700.0   81093723.6   6948.9
Double-Precision Whetstone                       55.0      11816.9   2148.5
Execl Throughput                                 43.0      11173.2   2598.4
File Copy 1024 bufsize 2000 maxblocks          3960.0    1311901.0   3312.9
File Copy 256 bufsize 500 maxblocks            1655.0     405826.8   2452.1
File Copy 4096 bufsize 8000 maxblocks          5800.0    3190465.5   5500.8
Pipe Throughput                               12440.0    3586570.9   2883.1
Pipe-based Context Switching                   4000.0     638429.7   1596.1
Process Creation                                126.0      22963.7   1822.5
Shell Scripts (1 concurrent)                     42.4      12549.6   2959.8
Shell Scripts (8 concurrent)                      6.0       1706.7   2844.5
System Call Overhead                          15000.0    2563990.0   1709.3
                                                                   ========
System Benchmarks Index Score                                        2775.8

価格も m5.large より安いしパフォーマンスも良いなら、m6g 使います(ARM ベースで問題ないなら)

InnoDB Cluster のアップグレードをやる

8.0.19 -> 8.0.20

1. MySQL Shell を最新にする

2. MySQL Router を最新にする

3. メタデータを最新にする

MySQL  db01:33060+ ssl  JS > dba.upgradeMetadata()
NOTE: Installed metadata at 'db02.luis.local:3306' is up to date (version 2.0.0).
Metadata state is consistent and a restore is not necessary.

今回は必要なかった。
メタデータの互換性がないアップグレードで、MySQL Shell のアップグレードを忘れると、InnoDB Cluster の操作ができなくなるので注意

4. MySQL Server を最新にする

普通にアップグレードすれば OK
スレーブから、最後にプライマリー

もし、アップグレード後の検証が必要なら set persist group_replication_start_on_boot=false; をしてから停止させたほうが吉

 MySQL  db02:33060+ ssl  JS > c.status()
{
    "clusterName": "main",
    "defaultReplicaSet": {
        "name": "default",
        "primary": "db03.luis.local:3306",
        "ssl": "REQUIRED",
        "status": "OK",
        "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
        "topology": {
            "db01.luis.local:3306": {
                "address": "db01.luis.local:3306",
                "mode": "R/O",
                "readReplicas": {},
                "replicationLag": null,
                "role": "HA",
                "status": "ONLINE",
                "version": "8.0.20"
            },
            "db02.luis.local:3306": {
                "address": "db02.luis.local:3306",
                "mode": "R/O",
                "readReplicas": {},
                "replicationLag": null,
                "role": "HA",
                "status": "ONLINE",
                "version": "8.0.20"
            },
            "db03.luis.local:3306": {
                "address": "db03.luis.local:3306",
                "mode": "R/W",
                "readReplicas": {},
                "replicationLag": null,
                "role": "HA",
                "status": "ONLINE",
                "version": "8.0.20"
            }
        },
        "topologyMode": "Single-Primary"
    },
    "groupInformationSourceMember": "db03.luis.local:3306"
}

CircleCI で GitHub のラベルを見て自動でタグをプッシュする

流れ

1. PR を作る、ラベルで major, minor, patch のいずれをつける
2. master へマージする。
3. CircleCI がラベルを見て新しいタグをプッシュする

CircleCI

GHE 環境なのでところどころ読み直してください。
雰囲気で伝わってほしい。

push_tag:
    <<: *build
    steps:
      - checkout
      - add_ssh_keys
      - run:
          name: push new version tag
          command: |
            PR_NUMBER=$( git log ${CIRCLE_SHA1} --oneline -1 | awk '{print $5}' | sed -s 's/#//' )
            label=$( curl -s -u u:p https://hoge/pulls/${PR_NUMBER} | jq -r '.labels[0].name' )
            latest_version=$( curl -s -u u:p https://hoge/tags | jq -r '.[0].name' )
            new_version=$( bash .circleci/push_tag.sh "${label}" "${latest_version}" )
            git tag ${new_version}
            git push --tags

push_tag.sh

新しいタグ(バージョン)を作って、GHE にプッシュする

#!/usr/bin/env bash

set -eu

readonly semantic_version="${1}"
readonly latest_version="${2}"

# v2.15.1 -> 2.15.1
version=$( echo "${latest_version}" | sed -e 's/v//g' )
# 2.15.1 -> [ 2, 15, 1 ]
version_split=( ${version//./ } )

new_major_version="${version_split[0]}"
new_minor_version="${version_split[1]}"
new_patch_version="${version_split[2]}"

case "${semantic_version}" in
    "major" ) new_major_version=$(( new_major_version + 1 )) ;;
    "minor" ) new_minor_version=$(( new_minor_version + 1 )) ;;
    "patch" ) new_patch_version=$(( new_patch_version + 1 )) ;;
    * ) echo "please add a label 'major', 'minor', 'patch'"
        exit 1 ;;
esac

new_version=$( echo "v${new_major_version}.${new_minor_version}.${new_patch_version}" )

echo "${new_version}"
luis@ubuntu ~selected/.circleci (fix/datadog●●)$ bash push_tag.sh patch v2.15.1
v2.15.2
luis@ubuntu ~selected/.circleci (fix/datadog●●)$ bash push_tag.sh minor v2.15.1
v2.16.1
luis@ubuntu ~selected/.circleci (fix/datadog●●)$ bash push_tag.sh major v2.15.1
v3.15.1

問題点

  • 複数のラベルがあって、最初に major, minor, patch がないとエラーになる。
  • patch を2つ上げたいけど、1つしか上げられない

気が向いたら直す。

これでタグのプッシュのし忘れとか無くなって(・∀・)イイネ!!