ISUCON10予選問題で EC2 c6g.2xlarge vs c5.2xlarge をやってみる
少し前にISUCON10予選問題のスコアアップに取り組んでいたので、その成果を使い、EC2 Graviton2 インスタンスとIntel CPUのインスタンスでスコアを比較してみます。
ISUCON10予選問題チャレンジをしていた時のスレはこちら
ISUCON10予選のベンチマーク、Macbook Pro上で、23000まででるようになった。Xeon Gold 6148 x2上だと37000。
— kazeburo. (@kazeburo) 2020年10月28日
ここではISUCON10予選時のように複数台用意するのではなく、1台のサーバでGoのアプリケーションとMySQLを動作させ、同じサーバ上からベンチマークを実行しています。
準備
初期データを作る際のdockerコンテナでarm64対応していないものがあるため、Intel CPUのインスタンスで作成してscpする方法を取りました。
まず、 c5.2xlarge
のインスタンスを起動して準備していきます
# 依存のインストール sudo apt-get update sudo apt-get install -y apt-transport-https ca-certificates curl gnupg-agent software-properties-common python3-pip git curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - # Dockerをいれる sudo add-apt-repository \ "deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) \ stable" sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io sudo gpasswd -a $USER docker # 一度logout & login # docker-compose を pip でインストール sudo pip3 install -U docker-compose # Goのインストール curl -fsSL https://golang.org/dl/go1.15.6.linux-$(dpkg --print-architecture).tar.gz | sudo tar -C /usr/local -xzf - export PATH=$PATH:/usr/local/go/bin echo 'export PATH=$PATH:/usr/local/go/bin' | sudo tee /etc/profile.d/go.sh # 初期データ作成で使うコマンドのインストール go get github.com/orisano/wayt export PATH=$PATH:$HOME/go/bin echo 'export PATH=PATH=$PATH:$HOME/go/bin' | tee -a ~/.bashrc # ISUCON10 予選問題をgit cloneして初期データ作成 git clone https://github.com/isucon/isucon10-qualify.git cd isucon10-qualify/initial-data pip3 install -r requirements.txt make
初期データ作成時に一度止まってしまいましたが、Ctrl-Cしてもう一度実行したらうまく動きました。
MySQLをインストールして、ユーザ作成をします。簡単なパスワードになるので気をつけましょう。
sudo apt install mysql-server sudo mysql > CREATE USER 'isucon'@'%' IDENTIFIED BY 'isucon'; > GRANT ALL ON *.* TO `isucon`@`%`; > CREATE DATABASE isuumo;
ここまでできたら c6g.2xlarge
側の準備をします。
c6g.2xlarge
でインスタンスを起動し、
sudo apt-get update sudo apt-get install -y mysql-server git make export PATH=$PATH:/usr/local/go/bin echo 'export PATH=$PATH:/usr/local/go/bin' | sudo tee /etc/profile.d/go.sh sudo mysql > CREATE USER 'isucon'@'%' IDENTIFIED BY 'isucon'; > GRANT ALL ON *.* TO `isucon`@`%`; > CREATE DATABASE isuumo;
このあと、Intel CPUのインスタンスから、isucon10-qualify
ディレクトリを scpで Graviton2 のインスタンスにコピーすれば準備完了です。
初期状態でのスコア比較
まず、Intel CPUである c5.2xlarge での初期スコア
isuumoアプリを make
して起動
cd isucon10-qualify/webapp/go make ./isuumo
別ターミナルを開き、ベンチマークを実行します。
cd isucon10-qualify/bench make ./bench
初期スコア
初期状態ではタイムアウトも多くますが、最終的に以下のスコアになりました。
c5.2xlarge
での初期スコア
2020/12/23 13:26:06 bench.go:102: 最終的な負荷レベル: 8 {"pass":true,"score":1335,"messages":[{"text":"POST /api/estate/nazotte: リクエストに失敗しました (タイムアウトしました)","count":34}],"reason":"OK","language":"go"}
c6g.2xlarge でも同じようにターミナルを2つ開いてアプリケーションとベンチマークを実行します。
c6g.2xlarge
での初期スコア
2020/12/23 14:18:14 bench.go:102: 最終的な負荷レベル: 9 {"pass":true,"score":1552,"messages":[{"text":"POST /api/estate/nazotte: リクエストに失敗しました (タイムアウトしました)","count":48}],"reason":"OK","language":"go"}
初期状態では、c6g.2xlarge
の方が 16%
ほどスコアがよいという結果になりました。
ベンチマーク時の負荷の様子を top
コマンドでみると次のようになっておりました。
c5.2xlarge
top - 14:15:39 up 10 min, 3 users, load average: 7.18, 2.64, 0.96 Tasks: 146 total, 2 running, 144 sleeping, 0 stopped, 0 zombie %Cpu(s): 85.6 us, 3.2 sy, 0.0 ni, 10.3 id, 0.5 wa, 0.0 hi, 0.4 si, 0.0 st MiB Mem : 15549.2 total, 12448.3 free, 929.9 used, 2171.0 buff/cache MiB Swap: 0.0 total, 0.0 free, 0.0 used. 14411.4 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 777 mysql 20 0 2989500 506064 37208 S 638.9 3.2 9:38.23 mysqld 1331 ubuntu 20 0 1526204 34796 7164 S 44.9 0.2 0:36.05 isuumo 1486 ubuntu 20 0 1521292 139536 7692 S 30.0 0.9 0:07.15 bench
c6g.2xlarge
top - 14:18:03 up 16 min, 3 users, load average: 5.98, 2.22, 1.13 Tasks: 189 total, 2 running, 187 sleeping, 0 stopped, 0 zombie %Cpu(s): 92.9 us, 4.0 sy, 0.0 ni, 1.7 id, 0.3 wa, 0.0 hi, 1.1 si, 0.0 st MiB Mem : 15709.3 total, 11525.9 free, 880.3 used, 3303.1 buff/cache MiB Swap: 0.0 total, 0.0 free, 0.0 used. 14661.9 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 3516 mysql 20 0 3049328 490156 34860 S 707.0 3.0 11:29.75 /usr/sbin/mysqld 4896 ubuntu 20 0 1598624 40468 6208 S 39.5 0.3 0:46.90 ./isuumo 5193 ubuntu 20 0 1661656 153140 6860 S 33.9 1.0 0:13.45 ./bench
明らかにMySQLがボトルネックですが、c6g.2xlarge の方がidleが少なくCPUをよく使えているようにみえます。
感想戦ブランチでのスコア比較
次に、私のブランチへ切り替えてスコア比較をしてみます。
ブランチはこちら
効果があまりなかったものもありますが、主な変更点は次のようになります。
- nazotte検索に空間インデックスを利用
- 検索用テーブルと、データ用テーブルを分離してMySQLの負荷を削減
- 検索用インデックスの追加。
- 検索時にrangeクエリにならないようrangeIDカラムを作成しtriggerにてデータ投入
- 検索時にSQL_CALC_FOUND_ROWSを利用。結果をキャッシュしておき、同条件での検索時はSQL_CALC_FOUND_ROWSをつけずにキャッシュを利用する
- chairとestateは全てインメモリキャッシュ。検索時は
SELECT id
をする - 検索結果やchairとestateをキャッシュする際は、JSON化したものをキャッシュする
- bot判定を最適化
- fiber (fasthttp) へのフレームワーク変更
- MySQLに秘伝のタレを入れる
いれたMySQLに秘伝のタレ
disable-log-bin innodb_doublewrite = 0 innodb_flush_log_at_trx_commit = 0 innodb_flush_method = O_DIRECT innodb_adaptive_hash_index = 0
感想戦ブランチでのスコア
c5.2xlarge
での感想戦ブランチスコア
$ ./bench 2020/12/23 14:34:27 bench.go:78: === initialize === 2020/12/23 14:34:32 bench.go:90: === verify === 2020/12/23 14:34:32 bench.go:100: === validation === 2020/12/23 14:34:34 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:34:36 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:34:37 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:34:37 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:34:37 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:34:38 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:34:38 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:34:38 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:34:38 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:34:39 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:34:39 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:35:32 bench.go:102: 最終的な負荷レベル: 11 {"pass":true,"score":41462,"messages":[],"reason":"OK","language":"go"}
初期状態では負荷レベルが最大の11までには届きませんでしたが、ここでは7秒ほどで最大に達し、最終スコアは4万を超えてきました。これはMacbook Pro上でやっていたときより若干良い結果
c6g.2xlarge
での感想戦ブランチスコア
ubuntu@ip-10-0-11-141:~/isucon10-qualify/bench$ ./bench 2020/12/23 14:36:20 bench.go:78: === initialize === 2020/12/23 14:36:30 bench.go:90: === verify === 2020/12/23 14:36:31 bench.go:100: === validation === 2020/12/23 14:36:33 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:36:34 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:36:35 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:36:36 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:36:36 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:36:36 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:36:37 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:36:37 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:36:37 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:36:37 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:36:38 load.go:181: 負荷レベルが上昇しました。 2020/12/23 14:37:31 bench.go:102: 最終的な負荷レベル: 11 {"pass":true,"score":39460,"messages":[],"reason":"OK","language":"go"}
Intel CPUに比べると5%ほど低いスコアです。何回か実行してみましたが、4万に届くことはありませんでした。
ベンチマーク実行中の負荷をtopコマンドでみると次のようになっていました。
c5.2xlarge
top - 14:45:24 up 39 min, 3 users, load average: 3.68, 2.66, 2.15 Tasks: 147 total, 2 running, 145 sleeping, 0 stopped, 0 zombie %Cpu(s): 57.6 us, 4.3 sy, 0.0 ni, 36.8 id, 0.0 wa, 0.0 hi, 1.3 si, 0.0 st MiB Mem : 15549.2 total, 10447.3 free, 2096.7 used, 3005.3 buff/cache MiB Swap: 0.0 total, 0.0 free, 0.0 used. 13346.3 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 3879 ubuntu 20 0 1663700 163836 7724 S 277.3 1.0 1:00.39 bench 1691 mysql 20 0 3335408 651500 37268 S 189.7 4.1 22:25.35 mysqld 3650 ubuntu 20 0 2572288 1.0g 15200 R 45.0 6.7 2:13.37 isuumo
c6g.2xlarge
top - 14:40:43 up 39 min, 3 users, load average: 1.44, 1.86, 1.69 Tasks: 185 total, 1 running, 184 sleeping, 0 stopped, 0 zombie %Cpu(s): 58.2 us, 3.6 sy, 0.0 ni, 37.0 id, 0.0 wa, 0.0 hi, 1.2 si, 0.0 st MiB Mem : 15709.3 total, 9707.4 free, 2011.2 used, 3990.7 buff/cache MiB Swap: 0.0 total, 0.0 free, 0.0 used. 13669.2 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 7423 ubuntu 20 0 1810080 205848 6788 S 268.8 1.3 0:59.13 ./bench 5417 mysql 20 0 3724652 652440 35008 S 203.0 4.1 18:10.73 /usr/sbin/mysqld 7352 ubuntu 20 0 2499408 986528 14492 S 38.9 6.1 0:50.62 ./isuumo
感想戦ブランチでは全体のCPU使用率が下がっています。大きな差ではありませんが、Intel CPUの方がMySQLのCPU使用率が小さくベンチマークとisuumoアプリのCPU使用率が大きいようにみえます。
感想など
tasksetコマンドを使い、benchコマンドが使うCPUを1個に絞った結果も合わせたグラフが次ようになります。
Graviton2はIntel CPUのインスタンスに比べてシングルスレッドのパフォーマンスが低めというベンチマーク結果もありますが、
この結果からも初期状態ではMySQLでのマルチスレッド性能が、感想戦ブランチではベンチマーカーのシングルスレッド性能がスコアに影響しているのかなと考えております。
感想戦ブランチではGraviton2のインスタンスの方がスコアが低いという結果になってしまいましたが、インスタンスのコストはGraviton2の方が20%ほど低く、スコアをコストで割った数値ではGraviton2の方が高くなるでしょう。
armなインスタンスの利用ではミドルウェアのパッケージやdockerのイメージが用意されていないことがあるなど、いくつか困難にぶつかることはありますが、パフォーマンスがよくコストメリットも大きいのでやはり使っていくといいんじゃないかと思いますね~
Re: 結局、Go言語をやめる理由はなかった件
すみません、ISUCONのアレに火がついてしまったので..。 Advent Calendarとはとくに関係がありません。
こちらの記事をみて、気になってしまったので interpolateParams
の追加とMySQLチューニングをしてベンチマークを回してみました。
ベンチマークの環境が公開されているのは素晴らしいですね。
8vCPU/16GB Memのc5a.2xlargeなEC2のインスタンスを起動して、以下の手順にしたがってdockerとdocker-composeをインストールしました。OSはUbuntu 20.04を使いました。
上記のbenchmarkのrepositoryをgit cloneし、compose build && compose upします。
$ git clone https://github.com/okdyy75/bench-docker.git $ sudo docker-compose build $ sudo docker-compose up -d
まず何も変更しない状態でのスコア結果は、
$ sudo docker-compose run --rm python sh -c 'cd python; pip install -r ./requirements.txt && time python bench.py' 平均秒数:9.670429 real 1m 36.73s user 0m 6.25s sys 0m 2.79s
Go言語
$ sudo docker-compose run --rm golang sh -c 'cd go; go build . && time ./go' 平均秒数:11.70815524 (0x0,0x0) real 1m 57.08s user 0m 3.84s sys 0m 10.83s
たしかに、Go言語の結果の方が少し悪くなっています。
ここで元記事にてmethaneさんから指摘のあった interpolateParams
の設定をいれます。
diff --git a/www/go/bench.go b/www/go/bench.go index 5c56714..a4b86d0 100644 --- a/www/go/bench.go +++ b/www/go/bench.go @@ -37,7 +37,7 @@ func getDB() *sql.DB { } dsn := fmt.Sprintf( - "%s%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true", + "%s%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true&interpolateParams=true", user, password, host, port, name) db, err := sql.Open("mysql", dsn)
そうすると、結果は
平均秒数:10.540287 real 1m 45.40s user 0m 2.99s sys 0m 6.26s
となり、pythonに追いつかないまでも、改善はしています。
ところで、ベンチマークを何回か動かしながら気になったのは、スコアが振れが大きく、iowaitがそれなりでているとところです。(Maki-Daisukeさんはioの影響を抑えるためNVMe SSDのついたインスタンスを使われておりました)
$ vmstat 1 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 0 1 0 11964048 140004 3405756 0 0 0 7872 8115 13468 1 2 95 2 0 0 0 0 11964048 140004 3405932 0 0 0 7656 6939 11857 1 2 93 4 0 0 1 0 11956236 140004 3414672 0 0 0 16284 7068 12074 1 1 94 4 0 0 1 0 11956236 140004 3414672 0 0 0 7768 8375 14349 1 2 94 3 0 1 0 0 11956236 140004 3414672 0 0 0 7464 7463 12275 2 1 96 2 0 0 1 0 11955480 140004 3415720 0 0 0 8780 7971 12701 2 1 97 0 0 0 1 0 11955480 140004 3415720 0 0 0 7712 8561 13845 1 2 97 0 0 0 1 0 11955480 140004 3415720 0 0 0 7780 7966 13032 1 1 96 1 0 0 1 0 11954472 140004 3416496 0 0 0 8788 7051 12944 2 1 90 7 0 0 1 0 11964536 140004 3405576 0 0 0 9592 9176 16006 2 1 92 4 0 2 0 0 11964284 140004 3405832 0 0 0 8220 7737 13731 1 1 92 5 0 1 0 0 11964252 140004 3405928 0 0 0 8056 8078 12893 1 1 97 0 0 0 1 0 11955416 140004 3414756 0 0 0 16132 6565 11829 1 1 91 7 0 2 0 0 11955416 140004 3414756 0 0 0 7056 7333 12482 1 1 94 3 0
また、CPUも全然使えてません。
これはMySQL側がボトルネックになっている可能性が高いとみて、対ISUCON用秘伝のタレを投入して、再度実行してみました。
my.cnf を作成。4つのパラメータを追加
$ cat docker/mysql/my.cnf [mysqld] innodb_doublewrite = 0 innodb_flush_log_at_trx_commit = 2 innodb_flush_method = O_DIRECT_NO_FSYNC innodb_adaptive_hash_index = 0
compose.ymlに追記して、mysqlを再起動
$ git diff diff --git a/docker-compose.yml b/docker-compose.yml index 94b3dc7..e39de9b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,7 @@ services: volumes: - "./.data/mysql:/var/lib/mysql" - "./log/mysql:/var/log/mysql" + - "./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf" ports: - "3306:3306" # php-fpm ################################################ $ sudo docker-compose restart mysql Restarting bench-docker_mysql_1 ... done
結果は
平均秒数:1.518821 real 0m 15.21s user 0m 6.12s sys 0m 1.69s
Go言語(interpolateParams=false)
平均秒数:1.770360 real 0m 17.70s user 0m 3.04s sys 0m 7.85s
Go言語(interpolateParams=true)
平均秒数:1.196632 real 0m 11.96s user 0m 2.26s sys 0m 4.95s
interpolateParamsが有効のGo言語が最速になりました。最初の10倍近い高速化ですね。
MySQLが高速化したことで順位が入れ替わるところはもう少し深堀してもいいかと思いますが、Pythonにしたってかなり速くなってますので、これくらいなら正直どの言語でやってもいいのではないかと思います。仲良くしましょう
Go言語を実行中のvmstatは以下のようになりました。
vmstat 1 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 1 0 0 12075152 140028 3363636 0 0 10 453 441 22 1 1 98 1 0 0 0 0 12075152 140028 3363636 0 0 0 0 139 257 0 0 100 0 0 0 1 0 12015176 140028 3387952 0 0 0 25772 1950 2773 5 1 93 1 0 1 0 0 11968016 140028 3391408 0 0 0 12732 31447 60372 9 7 84 1 0 1 0 0 11967732 140028 3391376 0 0 0 4660 42484 86361 7 8 85 0 0 1 0 0 11967748 140028 3391360 0 0 0 14592 35933 73675 6 6 87 1 0 1 0 0 11967256 140028 3391228 0 0 0 6964 38245 77879 7 7 86 0 0 2 0 0 11966928 140028 3391416 0 0 0 13028 36928 75262 6 7 87 0 0 2 0 0 11966816 140028 3391440 0 0 0 7352 37506 76638 7 7 86 0 0 1 0 0 11968984 140028 3391440 0 0 0 14120 32657 71858 8 7 85 0 0 1 0 0 11969976 140028 3391468 0 0 0 15564 33490 69433 7 7 86 1 0 2 0 0 11969976 140028 3391468 0 0 0 3392 40593 82689 7 8 85 0 0 1 1 0 11971740 140028 3391440 0 0 0 15568 35473 72960 6 7 86 1 0 2 0 0 11973520 140028 3391476 0 0 0 5420 36698 76628 8 7 85 0 0 1 0 0 11973344 140028 3391476 0 0 0 13000 37315 77527 6 7 87 0 0 1 0 0 11973392 140028 3391536 0 0 0 8284 34950 71476 6 8 86 0 0 2 0 0 11973408 140028 3391536 0 0 0 13840 38778 79834 6 7 86 1 0 1 0 0 11974132 140028 3391544 0 0 0 8692 37283 74883 7 7 86 0 0 3 0 0 11974132 140028 3391544 0 0 0 10444 37224 77928 6 8 86 0 0 2 0 0 11973108 140028 3391608 0 0 0 15688 34599 71052 6 7 86 1 0 0 0 0 11980788 140028 3391728 0 0 0 6868 33311 72294 7 8 84 0 0 0 0 0 12069692 140028 3363668 0 0 0 2588 2979 4393 1 1 98 0 0
iowaitが小さくなり、user, systemが以前より高くあがるようになりました。
topだと
top - 07:43:15 up 2:16, 2 users, load average: 0.43, 0.11, 0.05 Tasks: 169 total, 1 running, 168 sleeping, 0 stopped, 0 zombie %Cpu(s): 6.2 us, 3.9 sy, 0.0 ni, 85.9 id, 0.4 wa, 0.0 hi, 3.6 si, 0.0 st MiB Mem : 15827.2 total, 11689.1 free, 689.4 used, 3448.7 buff/cache MiB Swap: 0.0 total, 0.0 free, 0.0 used. 14833.8 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 44962 systemd+ 20 0 1377796 216128 20612 S 77.4 1.3 1:29.73 mysqld 47046 root 20 0 710064 10828 3836 S 60.5 0.1 0:06.25 go
ようになり、1vCPU 使い切りそうなぐらいまできてました。
ちなみに、MySQLのデータディレクトリをみんな大好きtmpfsにおくと、
Go言語(interpolateParams=true)
平均秒数:1.106469 real 0m 11.06s user 0m 2.45s sys 0m 5.07s
平均秒数:1.394543 real 0m 13.97s user 0m 6.15s sys 0m 1.70s
もう少しだけ速くなります。
まとめ
ミドルウェア含めたベンチマークを行う時は、ミドルウェアやOSの設定なども気を配るのがいいですね。広い視点を保つためもにもISUCONは役に立つ。
コマンドを実行しその終了ステータスコードメトリクス化するMackerelカスタムプラグイン
Mackerel Advent Calendar 2020 15日目の参加記事です。
キャッシュの更新やなんらかの同期を行うコマンドなどをcronにて実行している場合、メンテナンスや一時的なネットワークの不調によりコマンドがエラーになっても問題ないが、一定時間以上エラーが続くと困る、みたいなことはあると思われます。
mackerelでは、cronのコマンドがエラーになった際に、通知する方法としては mkr wrap が利用できます。
crontabに
* * * * * mkr wrap -- /path/to/your-batch ...
とすることで、コマンドが失敗した際に、アラートをあげることができます。ただmkr wrapでは1度でもエラーとなるとアラートとなってしまうため、前述したような一定時間エラーになっても問題ないものでもアラートが上がってしまいます。
そこで、mackerel-agentをcrondのように利用し、コマンドが終了ステータスコードをメトリクス化するカスタムプラグインを作成し、ホストのメトリック監視にてエラーが続いている状況がないかを確認する方法を考えました。
mackerel-plugin-command-status
できたのが、こちらのmackerelカスタムプラグインです
元々Perlでさっと作ったものでしたが、この記事を書くためにGoで書き直しました。mkr plugin install
で導入が可能です。
使い方
メトリクスの名前を指定し、ハイフン2つのあとに実行するコマンドを書きます。
mackerel-agent.conf で実行する場合はこんな形になります。
[plugin.metrics.update-cache] command = "/path/to/mackerel-plugin-command-status --name update-cache --timeout 10s -- /path/to/cmd-fetch-cache"
そして、5回以上(5分以上失敗し続けている場合)アラートをあげたいという場合は
こんな形で閾値を設けることで、監視ができるのではないかと思います。
あとがき
Mackerelで監視されていないとあるサーバがサービスに投入されてしまっていたことで障害に気づくのが遅れた問題に対して、1分毎にcronでホストのステータスをmackerelのapiから取得しておいて、consulサービスのチェックスクリプトからそれを確認し、workingでなかったらサービスから自動で外れるという対策を行っておりました。
ところが、mackerelの障害・メンテナンス時にcronのエラー通知が大量にきてあたふた対応が必要になってしまっていたので、なんとかそれを避けるために考えた仕組みです。ただし、これでもmackerelがメンテナンス終了するタイミングでアラートが来てしまうのが悩ましいところ。
作ってみたもののこれ以外の用途には使ってない感じですが、いい使い道があればどうぞー。
cgoを利用した Go project を GitHub Actions でクロスコンパイル&リリースを行う
check-lastlogというmackerel監視pluginを作ってるのですが、
こちらは /var/log/lastlog
を読むために一部cgoを使っています。mackerelのpluginなので一つ前の記事で紹介した方法で
リリースしたいところなのですが、GoReleaserでは、cgoへの依存があるプロジェクトのクロスコンパイルがサポートされていないため、ひと工夫必要になります。
check-lastlogではこちらのxgoを使う方法でクロスコンパイルし、GoReleaserでGitHub Releaseにアップロードをしています。
出来上がったGitHub Actionsのワークフローはこちら
https://github.com/kazeburo/check-lastlog/blob/master/.github/workflows/release.yml
xgoでのクロスコンパイル
xgo はGoのクロスコンパイルの環境がはいったDockerのイメージと各環境をターゲットとしたビルドを行うコマンドを提供しています。
check-lastlogではGitHub Actionが用意されているこちら
を使いました。こちらはforkしたxgoを使っているようです。
check-lastlogのワークフロー、.github/workflows/release.yml
の前半ではxgoを使ってクロスコンパイルしています。
name: release on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+" jobs: goreleaser: runs-on: ubuntu-latest env: BIN_NAME: check-lastlog steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set tag to environment variable id: set-tag run: echo ::set-output name=version::${GITHUB_REF#refs/*/} - name: Build with xgo uses: crazy-max/ghaction-xgo@v1 with: xgo_version: latest go_version: 1.15.x dest: build prefix: ${{ env.BIN_NAME }} targets: linux/amd64,linux/arm64 v: true x: false ldflags: -s -w -X main.version=${{ steps.set-tag.outputs.version }} - name: Check build dir run: ls -lR build
ghaction-xgo のパラメータは次のようになってます。
- dest: ビルドしたバイナリを格納するディレクトリ
- prefix: バイナリのファイル名。後ろに
-${OS}-${Arch}
がつきます - targets: クロスコンパイルするターゲット。カンマで区切る
- v: ビルドしたファイル名の表示
- x: ビルドの際のコマンドなどの表示
また、GoReleaser ではldflagsをデフォルトでいくつか設定してくれますが、xgoはそうではないので、-w -s
と -X main.version
を追加しています。
これでビルドが完了すると、dest
で指定したディレクトリにバイナリができます。
> Run ls -lR build build: total 3428 -rwxr-xr-x 1 runner docker 1806024 Dec 2 09:12 check-lastlog-linux-amd64 -rwxr-xr-x 1 runner docker 1703448 Dec 2 09:12 check-lastlog-linux-arm64
アーカイブ作成とGitHub Releaseへのアップロード
あとは、GitHub Releaseへのアップロードだけではあるのですが、check-lastlog はmackerel監視プラグインなので、mkr plugin install
対応をさせます。
mkr plugin install
対応するためにはファイル名が {{ .ProjectName }}_{{ .Os }}_{{ .Arch }}.zip
でzipでアーカイブする必要があります。なのでShellで頑張ってzipをつくりました。
- name: Zip binaries run: | cd build for file in ./* ; do mkdir $(echo ${file}|awk -F- '{print "${{ env.BIN_NAME }}_"$(NF-1)"_"$(NF)}') && cp ${file} $(echo ${file}|awk -F- '{print "${{ env.BIN_NAME }}_"$(NF-1)"_"$(NF)}')/${{ env.BIN_NAME }} && zip $(echo ${file}|awk -F- '{print "${{ env.BIN_NAME }}_"$(NF-1)"_"$(NF)}').zip -j $(echo ${file}|awk -F- '{print "${{ env.BIN_NAME }}_"$(NF-1)"_"$(NF)}')/${{ env.BIN_NAME }} ../README.md ../LICENSE; done shasum -a 256 *.zip > ${{ env.BIN_NAME }}_${{ steps.set-tag.outputs.version }}_checksums.txt ls -lR ./
バイナリだけではなく、README.mdやLICENSEファイルもアーカイブに同梱しています。また、checksum用のファイルも作っています。GoReleaserのchecksumがsha256なので、同じようにshasumコマンドを使ってみました。
そして最後にGitHub Releaseにアップロードするのですが、CHANGESも作ってくれるGoReleaserを使います。
まず、GoReleaserの設定ファイル .goreleaser.yml
# Use for generating CHANGELOG builds: - binary: check-lastlog skip: true release: github: owner: kazeburo name: check-lastlog extra_files: - glob: "build/*.zip" - glob: "build/*_checksums.txt"
ビルド(builds)は skip:true で止めることができました。成果物がない場合、archiveの作成も行われないようでした。GoReleaserで作ったパッケージのかわりに、xgoとShellで作ったzipとchecksumファイルをextra_filesに指定してアップロードしてもらいます。
GitHub Actionsのワークフローは goreleaser/goreleaser-action
を使いました。
- name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 with: version: latest args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GoReleaser 便利
まとめ
xgoを使うとGoReleaserのみでクロスコンパイルする場合にくらべて実行時間は伸びてしまいますが、cgoを使っているGo Projectでもlinuxのamd64, arm64に対応したパッケージを作ることができました。
GitHub ActionsとGoReleaserでmkr plugin install対応mackerel pluginをリリースする
Mackerel Advent Calendar 2020 9日目の参加記事です。隙間で書きました。
最近 mackerel pluginをリリースするときは、ほぼ全てGitHub ActionsとGoReleaserを使ってビルドとリリースしています。
GoReleaserはこちら
GoReleaserはGoのprojectをクロスプラットフォームでビルドし、GitHub Releaseなどにアップロードしてくれるツールです。これのおかげでかなりリリース作業が楽になりました。
準備
mackerel pluginのrepositoryに .goreleaser.yml
というファイルを作ります。
builds: - binary: mackerel-plugin-linux-process-status goos: - linux goarch: - amd64 - arm64 archives: - format: zip name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" release: github: owner: kazeburo name: mackerel-plugin-linux-process-status
こちらは mackerel-plugin-linux-process-status という plugin の設定ファイルです。linuxのみサポートなので、linux/amd64およびlinux/arm64用にバイナリをビルドします。
mkr plugin install
に対応したpluginはアーカイブのファイル名が(REPOSITORY_NAME)(GOOS)(GOARCH).zipという命名規則になっていることが必要です。
なので、archivesの name_templateに {{ .ProjectName }}_{{ .Os }}_{{ .Arch }}
を指定します。
これで、GoReleaserの準備は完了で、手元で、goreleaser --rm-dist
を実行すると、最新のタグを取得してビルドとGitHub Releaseへのアップロードを行ってくれます。楽。
また、GoReleaserは以下のような簡単なCHANGESを作ってくれるのもとても気に入っています。
GitHub Acitons を使う
ここまでできていたら、tagがつけられたらGitHub Acitonsでビルドとリリースを自動化するだけ。
.github/workflows/release.yml
などファイルを作成し
name: release on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+" jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 0 - name: Setup Go uses: actions/setup-go@v2 with: go-version: 1.15 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 with: version: latest args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GoReleaser用のActionが用意されているので、これを使うだけ
上記はサンプルそのままですな。
これで、
git tag v${VERSION} git push origin v${VERSION}
するだけで自動でパッケージの作成とリリースができるようになりました。簡単。
後書き
最近、公開してあったたくさんのmackerel pluginを一気にGitHub Actionsを利用するようにしたり、arm64での動作確認を行ってました。 その過程でtag追加を楽にするコマンドを作ったり、cgo含むprojectのGitHub Actionsでのクロスコンパイルをやってたりした話はまた今度できたらと思います。
GoでMySQLの監視コマンドを作成するのが楽になるライブラリ
本エントリはMySQL Advent Calendar 2020 の9日目の記事になります。
これまでいくつものMySQLの監視コマンドをつくってきたのですが、毎回同じような処理を書いているので、それらをまとめるライブラリを作ってみました
主な機能は
の2つです。
使い方 (コマンドラインオプション)
まずコマンドオプションの処理
オプションを定義した構造体に mysqlflags.MyOpts
を追加します
type opts struct { mysqlflags.MyOpts Timeout time.Duration `long:"timeout" default:"10s" description:"Timeout to connect mysql"` Version bool `short:"v" long:"version" description:"Show version"` } opts := opts{} psr := flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash) _, err := psr.Parse()
これだけで、helpを表示させると以下のようになります。
Application Options: --defaults-extra-file= path to defaults-extra-file --mysql-socket= path to mysql listen sock -H, --host= Hostname (default: localhost) -p, --port= Port (default: 3306) -u, --user= Username (default: root) -P, --password= Password --database= database name connect to --timeout= Timeout to connect mysql (default: 10s) -v, --version Show version
MySQLの defaults-extra-file、socket、ホスト、ポート、ユーザなどのコマンドオプションが追加されてますね。
この構造体を利用してデータベースに接続するには次のようにします。
db, err := mysqlflags.OpenDB(opts.MyOpts, opts.Timeout, debug) if err != nil { log.Printf("couldn't connect DB: %v", err) return 1 } defer db.Close()
DSNだけ欲しい場合は、mysqlflags.CreateDSN()` を使います。
mysqlflagsはDSNを作る際に、my_print_defaults -s client
コマンドを実行して my.cnf
などに書かれた値を取得します。
% my_print_defaults -s client --host=127.0.0.1 --user=xxx --password=xxx
これらをデフォルト値として利用するので、$HOME/.my.cnf
が設定されていればコマンドラインオプションが未指定でもコマンドが動きます。
また、接続する際のparameterを追加したい場合は、MySQLDSNParams
にmapを渡します。
params := map[string]string{ "readTimeout": timeout.String(), } opts.MySQLDSNParams = params
使い方 (SHOWステートメントの構造体へのマッピング)
MySQLのSHOW STATUSやVARIABLESでは列方向に情報が並べられています。行によってstringやint、boolっぽいものが混じります。
mysql> SHOW VARIABLES; +-----------------------------+-------------------------------+ | Variable_name | Value | +-----------------------------+-------------------------------+ | activate_all_roles_on_login | OFF | | admin_address | | | admin_port | 33062 | | admin_tls_version | TLSv1,TLSv1.1,TLSv1.2,TLSv1.3 | ..
これらを良い感じにintやstringに自動で変換して構造体にマッピングしてくれると、監視コマンドのコードは短くなり、とても楽になります
また、SHOW SLAVE(REPLICA) STATUSではカラムが増え、行方向に情報が追加されています。情報が縦方向なのか横方向なのかあまり気にせず同じコードで必要とするカラムだけ良い感じにとれると嬉しいところです。
これをやるのが、mysqlflags.Query().Scan()
です。内部では mapstructure というライブラリを利用しています。
thread関連の値をSHOW VARIABLES/SHOW GLOBAL STATUSから取得するのは以下のように書けます
type threads struct { Running int64 `mysqlvar:"Threads_running"` Connected int64 `mysqlvar:"Threads_connected"` Cached int64 `mysqlvar:"Threads_cached"` } type connections struct { Max int64 `mysqlvar:"max_connections"` CacheSize int64 `mysqlvar:"thread_cache_size"` } var threads threads err = mysqlflags.Query(db, "SHOW GLOBAL STATUS").Scan(&threads) if err != nil { log.Printf("couldn't fetch status: %v", err) return 1 } var connections connections err = mysqlflags.Query(db, "SHOW VARIABLES").Scan(&connections) if err != nil { log.Printf("couldn't fetch variables: %v", err) return 1 }
構造体のメタ情報のタグに、mysqlvar:"取得する変数名"
を入れると自動的にマッピングを行います。
レプリケーションの情報を取得する際は次のようにします。
type replica struct { IORunning bool `mysqlvar:"Slave_IO_Running"` // Replica_IO_Running SQLRunning bool `mysqlvar:"Slave_SQL_Running"` // Replica_SQL_Running LastSQLError string `mysqlvar:"Last_SQL_Error"` } var replica replica err := mysqlflags.Query(db, "SHOW SLAVE(REPLICA) STATUS").Scan(&replica) f !replica.IORunning || !replica.SQLRunning { fmt.Errorf("something wrong in replication with %s", replica.LastSQLError); }
bool へのマッピングは、値が Yes や On だった場合に true になり、それ以外は false になります。
Slave_IO_Running(Replica_IO_Running) は Connecting
という文字列にもなりますが、こちらは false 扱いです。
もし、真偽値判定を楽にしたいけど、元の文字列も取得したいときは、mysqlflags.Bool
型が使えます。
type replica struct { IORunning mysqlflags.Bool `mysqlvar:"Slave_IO_Running"` SQLRunning mysqlflags.Bool `mysqlvar:"Slave_SQL_Running"` ChannelName *string `mysqlvar:"Channel_Name"` // ポインタ型はオプショナルになる Behind int64 `mysqlvar:"Seconds_Behind_Master"` } var replicas []replica err := mysqlflags.Query(db, "SHOW SLAVE(REPLICA) STATUS").Scan(&replicas) for _, replica := replicas { f !replica.IORunning.Yes() || !replica.SQLRunning.Yes() { if replica.ChannelName == nil { *replica.ChannelName = "-" } fmt.Errorf("something wrong in replication Channel:%s IO:%s SQL:%s Error:%s", *replica.ChannelName, replica.IORunning, replica.SQLRunning, replica.LastSQLError); } }
Yes()
メソッドがbool値を返し、String()
で元の文字列を返します。
使用例
いくつかのMackerelプラグイン、checkプラグインはこのライブラリを使って実装しています。
- GitHub - kazeburo/go-mackerel-plugin-mysql-lite: lite means Yet Another
- GitHub - kazeburo/go-check-mysql-slave-sql-error
- GitHub - kazeburo/go-mackerel-plugin-msr: mackerel plugin for mysql multi source replication rag
- GitHub - kazeburo/go-check-mysql-msr: check multi source replication
- GitHub - kazeburo/go-check-mysql-uptime
mkr plugin installでインストールできるようになってますので、ニーズが合えばご利用ください。
ライブラリや監視コマンドの機能追加要望などありましたらお気軽にどうぞ〜
mackerel plugin で瞬間的な負荷を捉える mackerel-plugin-maxcpu
とあるAPIに対するアクセスが、
回数 時間 1833 time:27/Oct/2020:21:32:25 1657 time:27/Oct/2020:21:32:26 1616 time:27/Oct/2020:21:32:27 2584 time:27/Oct/2020:21:32:28 14958 time:27/Oct/2020:21:32:29 6619 time:27/Oct/2020:21:32:30 1725 time:27/Oct/2020:21:32:31 1796 time:27/Oct/2020:21:32:32 1713 time:27/Oct/2020:21:32:33 1932 time:27/Oct/2020:21:32:34 1797 time:27/Oct/2020:21:32:35 1841 time:27/Oct/2020:21:32:36
という感じで、一瞬だけ急増し、1秒後にはほぼ戻るようなことがあり、この瞬間にCPUが尽きることで全体のレスポンスタイムに影響してしまうようなことがありました。
とりあえず台数を増やしてスケールアウトで対応してるわけですが、大元の原因の解決するまで、CPUの使用率が100%に近くなる瞬間がないか監視しておきたいということで、新しく mackerel plugin を作成しました。
CPU使用率の計算
Linuxの場合、よくあるツールではCPUの使用率を /proc/stat
のファイルを読んで計測します。
$ cat /proc/stat |head cpu 4102976 0 548407 3258934935 6723 0 115819 0 0 0 cpu0 115620 0 14370 81452406 84 0 2527 0 0 0 cpu1 123341 0 12898 81453921 748 0 2527 0 0 0 cpu2 109211 0 16473 81465621 82 0 2505 0 0 0 cpu3 109706 0 14273 81467250 216 0 2513 0 0 0
cpuの行の数値は、user, nice, system, idle, iowait, irq, softirq.. と各モードに入っていた時間(USER_HZ)、CPU使用率を出すツールは一定周期でこのファイルを確認し、それぞれの数値の増分を使ってCPU使用率を計算しています。
通常のmackerel pluginの場合、1分周期でpluginが実行されるため、1分間の平均CPU使用率しか計算ができません。1分のあいだの最大値は取得できません。そこで、maxcpuでは初回plugin実行時に、1秒毎にCPU使用率を計測するbackgroundプロセスを作成し、次の起動タイミングからはbackgroundプロセスに前回取得タイミングからのCPU使用率の最大・最小・平均・パーセンタイル値を取得しています。
backgroudプロセスとの通信はgRPC over UNIXドメインソケットで行ってますが、gRPC(http含む)ではバイナリサイズが大きくなってしまうので変更するべきかもしれません。
maxcpuの使い方
gRPC over UNIXドメインソケットに使う --socket
だけ指定します。
[plugin.metrics.maxcpu] command = "/usr/local/bin/mackerel-plugin-maxcpu --socket /var/run/maxcpu.sock"
maxcpuでは運用がしやすいように、バイナリが入れ替わった際にbackgroundプロセスは自動で再起動(新しいプロセスを作成し、自プロセスを停止)します。このためpluginのアップデート時に手動にて再起動する必要はありません。この機能を実現するため、SO_REUSEPORT
を使っています。
また、一定時間gRPCの通信がないと停止して、余計なリソースを使い続けることはないようにしています。
作成されたグラフは以下のようになります。何回かCPUが跳ねているのがわかります。
ニッチなpluginですが、ニーズが合えばお使いください。