AWS X-Ray による ISUCON8 本選問題の解析

ISUCON8 の本選問題は、競技者がコントロールできない外部 API 呼び出しを多数含んだ出題内容でした。

講評では、

サービスの特性を適切に分析した上で、まとめるところはまとめたり、遅延させるところは遅延させるなど

……とさらっと書かれていますが、実際そんなことを短時間で分析することは可能なのかよ!という話題が競技後の懇親会でもあったので、それ AWS X-Ray でできるよ、というエントリをまとめておきたいと思います。

今回の解析は Perl 版の初期実装に対して行ったものですが、なぜ Perl かというと AWS の公式 SDK にない X-Ray 関連の CPAN モジュールを自分が書いているので、その宣伝も兼ねています。(blogエントリ書いてなかった)

Go, Java, Node.JS, Ruby, .NET については、公式 SDK が提供されています。正直うらやましい。

どのように解析するのか

X-Ray のトレーシングをするためにソースコードに多少の手を加えてからベンチマークを実行すると、AWS console の X-Ray の画面で GUI で可視化ができます。

これは文章で説明するのが大変なので、画面キャプチャを貼りながら進めていきます。X-Ray へアプリケーションからどのようにしてデータ(トレース) を送信するかは最後にまとめます。また、競技時の初期実装では静的ファイルをアプリケーションが処理していてノイズが多いので、静的ファイルは nginx が配信するように変更した状態からはじめます。

まず、X-Ray へトレースを送信してしばらくすると、このようにサービス (丸いやつ) がグラフになった画面が出てきます。

f:id:sfujiwara:20181027005805p:plain

Clients から、isucoin というサービスに矢印が伸び、そこから Isubank, Isulogger, Isucoin::Model, DBI, DBI::st, Furl::HTTP というサービスが呼び出されているのが可視化されています。

f:id:sfujiwara:20181027010128p:plain

isucoin サービスをクリックすると、このサービスのレスポンスタイムがヒストグラムになって出てきます。 そのまま「トレースの表示」をクリックすると…

f:id:sfujiwara:20181027011110p:plain

URLごとに平均レスポンスタイム、呼び出し回数などがサマライズされています。

ここで平均 1.6 sec といういかにもヤバそうな http://isucoin:5000/info?cursor=0 をクリックすると、その URL のみでフィルタリングされたトレースのリストが下に表示されます。

f:id:sfujiwara:20181027010533p:plain

一番上の 1.3 sec 掛かっているものを開くと…

f:id:sfujiwara:20181027010848p:plain

レスポンスタイム 1.3 sec のうち、どのような処理にどれぐらいの時間が掛かったのかがタイムラインとして表示されます。 495msec 掛かっている DB へのクエリ (DBI::st) があるので詳細を見てみましょう。

f:id:sfujiwara:20181027010950p:plain f:id:sfujiwara:20181027011005p:plain f:id:sfujiwara:20181027011017p:plain

SELECT * FROM trade ORDER BY id DESC でテーブル全件23万行を読み取っている、問題のクエリが出てきます。これは結果的に1行しか使われないので、LIMIT 1 を付けるべきやつですね。

このクエリの親 (ひとつ上のサブセグメント) の詳細を見ると、Isucoin::Modelget_latest_trade メソッドから呼ばれていることが分かります。

f:id:sfujiwara:20181027011540p:plain

ということで、当該メソッドのコードを修正し、LIMIT 1 を付けて再度ベンチを実行したあとに X-Ray を見ると

f:id:sfujiwara:20181027012118p:plain

レスポンスタイムの分布が最初とは変わりました。1秒ぐらいにかけて盛り上がっていた山が消えています。

先ほどと同様に http://isucoin:5000/info?cursor=0 を選択してひとつのトレースを見てみると、

f:id:sfujiwara:20181027012405p:plain

最初のクエリは 9.2 msec になっていました。

f:id:sfujiwara:20181027012446p:plain f:id:sfujiwara:20181027012456p:plain

LIMIT 1 したことにより、23万行読み取っていたのが1行になっているのが確認できます。

外部 API 呼び出しの解析

ここまでは外部ではなく DB へのクエリの解析だったので、X-Ray によらずとも slow query log の解析でも把握できるものでした。

次に、外部 API のうちのひとつ、ISUBANK への API 呼び出しを解析してみましょう。

f:id:sfujiwara:20181027012939p:plain

Isubank サービスを選択すると、そのサービスのみのレスポンスタイムの分布が表示されます。 100ms 以下のものが大半ですが、300ms 前後にも山があるのが観測できます。

f:id:sfujiwara:20181027013103p:plain

300ms付近を選択してズーム

f:id:sfujiwara:20181027013112p:plain

トレースを表示すると…

f:id:sfujiwara:20181027013410p:plain

service("Isubank") { responsetime >= 0.215 AND responsetime <= 0.561 } という条件で抽出されたトレースのみが表示されています。 300ms前後の時間が掛かる Isubank の呼び出しは POST /orders からしか実行されていないことも分かります。

f:id:sfujiwara:20181027013553p:plain

このトレースはいろいろ突っ込み所が多いのですが、今は 300ms 前後の Isubank 呼び出しを目的にしているのでそれを見ると…

f:id:sfujiwara:20181027013804p:plain f:id:sfujiwara:20181027013841p:plain

commit であることが分かります。 このようにしていくつかのトレースを見ていくと、bank で 300ms かかるやつは commit だけだな…ということも見えてくるはずです。

丸見えですね。

アプリケーションへの組み込み

これまで見てきたようなトレース情報は、アプリケーションから直接 AWSX-Ray API へ送信されているわけではありません。

アプリケーションからは UDPX-Ray daemon というプロセスへトレース情報を送信し、X-Ray daemon が適宜バッファリングして AWS API に送信する、という作りになっています。UDP で送信するため、X-Ray daemon になにか問題が発生しても、アプリケーションはブロックせずに動作できる仕組みです。

X-Ray daemon の用意

ということで、まず X-Ray daemon をどこかで動かす必要がありますが、今回の初期実装は docker-compose なので、そこで動作させましょう。公式の Docker image は特に用意されていないようなので、ドキュメントに従って適当に Dockerfile を用意します。

webapp/xray/Dockerfile

FROM amazonlinux
RUN yum install -y unzip
RUN curl -o daemon.zip https://s3.dualstack.us-east-2.amazonaws.com/aws-xray-assets.us-east-2/xray-daemon/aws-xray-daemon-linux-2.x.zip
RUN unzip daemon.zip && cp xray /usr/bin/xray
ENTRYPOINT ["/usr/bin/xray", "-b", "0.0.0.0:2000", "--local-mode"]
EXPOSE 2000/udp

docker-compose.yml

services:
  isucoin:
    environment:
       # 略
      AWS_XRAY_DAEMON_ADDRESS: 'xray:2000'
    links:
      - mysql
      - xray
  xray:
    build: xray
    environment:
      AWS_REGION: "ap-northeast-1"
      AWS_ACCESS_KEY_ID: "xxxxx"
      AWS_SECRET_ACCESS_KEY: "zzzz"

AWS 外で動かす場合は access key を使用することになりますが、ISUCON のような他者が触れる環境で使用する場合は付与する権限に細心の注意を払ってください。X-Ray への write 権限のみの IAM user を作成し、その key を使用して、必要がなくなったら速やかに無効化するのをお薦めします。

Perl 実装への AWS::XRay 組み込み

Plack の entrypoint である app.psgi で、Plack::Middleware::XRay を有効にします。また、DBI, Furl などのトレースは Devel::KYTProf を利用し、Logger を置き換える形で取得するのでその設定もします。

# app.psgi

use Devel::KYTProf::Logger::XRay;
use AWS::XRay;
Devel::KYTProf->logger("Devel::KYTProf::Logger::XRay");

# (中略)

builder {
    enable 'XRay',
        name => 'isucoin';
# (後略)

これだけ (5行追加) で、Devel::KYTProf でプロファイルが取得できるものについてはすべてトレースが取得されるようになります。

さらに今回は、Isucoin::Model, Isulogger, Isubank の各モジュールのメソッド呼び出しもトレースするため、以下のコードも app.psgi に追加します (builder の前に)。長く見えますが、やっていることは AWS::XRay->add_capture() に各 package のメソッド名を渡しているだけです。

my $captures = {
    Isubank   => [qw/ check reserve commit cancel /],
    Isulogger => [qw/ send send_bulk /],
    "Isucoin::Model" => [qw/
       init_benchmark set_setting get_setting send_log
       user_signup user_login get_user_by_id get_user_by_id_with_lock
       get_orders_by_user_id get_orders_by_user_id_and_last_trade_id
       get_open_order_by_id get_order_by_id get_order_by_id_with_lock
       get_lowest_sell_order get_highest_buy_order fetch_order_relation
       add_order delete_order cancel_order get_trade_by_id
       get_latest_trade get_candletick_data has_trade_chance_by_order
       reserve_order commit_reserved_order
       try_trade run_trade
   /],
};
for my $package (keys %$captures) {
    AWS::XRay->add_capture($package, @{$captures->{$package}});
}

Class::Inspector->methods などを使ってすべてのメソッドを自動で追加することもできますが、export された encode_json など、トレースに不要なメソッドまで追加されてしまうので列挙することにしました。

この記事の前半で行った解析に必要なことはこれだけです。この状態でアプリケーションを実行すると X-Ray へトレースデータが送られ、可視化されることになります。

なお、この状態ではすべてのトレースを送信するので、料金が問題になる可能性があります。Plack::Middleware::XRay ではサンプリングレートの設定、条件付きサンプリング (結果のステータスコードやレスポンスタイムの条件を見て、一致した場合だけトレースを送信する) こともできるので、詳しくは Plack::Middleware::XRayのドキュメント を参照してください。

トレース取得に伴うオーバーヘッド

トレースの取得処理は、対象の関数を wrap して実行時間などを計測するコードを挿入するだけのため、実アプリでのオーバーヘッドは大きくありません。

実測では、1リクエストにつき100個のトレースを取得した場合でも2ms程度のCPU時間消費で収まっているため、レスポンスタイムが平均50〜100msというような実アプリケーションにおいてはほぼ気にならないと思われます。

ISUCON では極限性能を求めるのに邪魔になる可能性は高いですが、その場合はチューニングが進んで不要になったらさっくり削除すればいいわけですし。

AWS へのトレースの送信しすぎによる課金増には、各自お気を付けください。条件サンプリングの不等号の向きを逆に書いてしまった結果、 1%だけ取得するつもりが99%取得してしまった事故がありました…

まとめ

ISUCONのレギュレーションでは競技中の外部リソースの使用は禁じられていますが、監視やモニタリングについての利用は認められています。 また、本番稼働中のアプリケーションについても常時トレースしておくことで、問題発生時の切り分けが容易になるでしょう。

AWS::XRay では Devel::KYTProf という既存の素晴らしいプロファイラを利用することにより、HTTP や DB 呼び出しについてのトレースを取得するだけであれば、既存アプリケーションに数行の追加で実装できます。どうぞご利用ください。

ISUCON8 本選出題記 あるいはISUCONベンチマーカー負荷調整の歴史

ISUCON 8 の本選出題を同僚の @ken39arg と担当しました。参加された皆様、運営にご協力して頂いたすべての関係者の方々にお礼申し上げます。

問題についての講評は公式の ISUCON8 本選問題の解説と講評 をご覧頂くとして、こちらでは今回、出題に導入された新要素である「シェア機能」について、どういう経緯で導入されたのか、裏話的なことを書いておきたいと思います。

ベンチマークの負荷を自分で決めるのも、自動で際限なく負荷が上昇するのも実際のアプリケーションとは違うよね?」というところから思いついた機構なのですが、経緯についてはいろいろな前提と、歴史の理解が必要になります。結果的に長文になってしまいました。

ISUCONベンチマーカーとは

ISUCON という競技は、あらかじめ与えられた所定のアプリケーションに対して、運営が用意したベンチマーカーと呼ばれるホストから (HTTPなどの) リクエストが送信され、そのリクエストをどれだけ処理できたのかによってスコアが決定します。

ベンチマーカーのリクエストに一定時間応答できなかったり、アプリケーション的な不整合を検出したり、エラーを多数返したりすると、ベンチマークは失敗します。その状態を「fail」と呼び、スコアは 0 となります。

初期状態のアプリケーションには様々な問題が(ほぼ意図的に)仕込まれていて、それを解消したり、よりよいコード、構成、OSやミドルウェアの設定変更を行うことでスコアを上げていく、というのが競技のメイン要素です。

チューニングによりどれぐらいスコアが向上するのかは回によって異なりますが、今年の本選の場合は初期状態でのスコア 500 程度から、競技中の最高スコアで 50,000 程度、あらかじめ検証した時点では 100,000 以上まで、つまり200倍以上に上げることが可能です。

ここで難しいのは、「極端に遅い初期状態」と「チューニングが進んで100倍高速化された状態」のどちらに対しても、成功(pass)してスコアが残るベンチマークを行う必要がある、ということです。

初期の遅い状態のアプリケーションに多くのリクエストを送りすぎるとタイムアウト等で fail しますし、かといって高速なアプリケーションに送るリクエストが少なすぎると、スコアの差が出なくなる可能性があります。

原理的には初期でスコアが出ない状態で競技を開始することも可能ですが、競技者が行った変更が有効だったのかは基本的にスコアで判断するしかなく、初期スコアが 0 の場合は何が有効なのかを暗中模索することなります。これは非常にストレスが掛かる状態のため、初期状態でスコアが記録され、かつ何らかの有効な手を打つとスコアが向上する、というベンチマークを作成することが望ましいことになります。

ベンチマーカーの負荷調整の歴史

ここで歴史を紐解きます。

ISUCON1, ISUCON2 の時代は予選がなく、本戦であらかじめ複数台に配置されたアプリケーションで競技を行っていました。この時点では、ベンチマーカーは特に動的に負荷を増やすということはしていなかったと記憶しています。

workload 指定時代

ISUCON3 の出題を自分が担当することになり、予選が導入されました。

この当時の予選(4まで)は、AWS (Amazon Web Services)上で各競技者がアプリケーションとベンチマーカー(Goで実装されたバイナリのみ)が配置されたインスタンスを1台起動し、ローカルホスト上でベンチマークを実行する形態で行われました。

ベンチマークコマンドに -workload という引数があり、その引数を変えることで負荷を増加させることができるようになっていました。

これは、チューニング後のアプリケーションに対しても十分な負荷を供給するためのギミックだったのですが、いくつかの問題がありました。

  • 競技者が十分に高速化されていないアプリケーションに対して workload 値の試行錯誤をしてしまう
    • workload=1 でスコアが出た上で、CPU等のリソースに余裕があるようならば値を増加させていく想定でした
    • その意図を読み取れない競技者が、workload の調整で時間を無駄にしてしまうことがありました
  • 逆に、workload の指定に気がつかない競技者が、負荷を増やせないのでスコアを上げられない
  • ISUCON4 の予選においては workload に異常に大きな値を指定することにより、ベンチマークが規定時間になっても終了せず、不正に高いスコアを出すことが可能でした

それを受けて、ISUCON5 では workload が廃止され、競技者が任意の負荷を与える形ではなくなりました。

ちなみに自分は 5 で優勝をしているのですが、どのように初期でもチューニング後でも通る調整がなされていたのか、全く理解していません…このへんは出題した @tagomoris さん、@kamipo さんに今度教えてもらいたいところです。

自動負荷増加時代

ISUCON6 予選には、5と同様に負荷調整機構がありません。ただし、初期言語実装の7種類のうち、4言語でスコアが出ない状態で提供されていました。

ISUCON6 本選から、自動的に負荷が増加する機構が導入されました。

isucon6-final/matsuri.go at 6b7e659f3de2f455b1f76bf27531534a20f0f79f · isucon/isucon6-final · GitHub

// watcherIncreaseInterval秒おきに、 (まだ退室していないwatcherの数 - 既に退室したwatcherの数) の人数が入室する

タイムアウトするとユーザー(ベンチマーカーが与える負荷)が減り、5秒おきにユーザー(=負荷)が増えていく、という機構です。これは特に競技者へは伝えられていなかった仕様ですが、負荷の様子を観察していれば認識できるものでした。

その後、ISUCON7では1秒おきに、エラーが発生したり、レスポンスタイムが遅くなったりしていなければ負荷が増加するという機構になります。これはユーザに示されるベンチマーク実行結果にも「負荷レベルが上昇しました」「エラーが発生したため負荷レベルを上げられませんでした」というログにより、明示されるようになりました。

今回の ISUCON8 予選は、基本的に 7 の仕様が踏襲されていました。

自動負荷増加の問題

今回8本選の出題にあたり、自動で負荷が際限なく上がることについての問題意識が自分の中にありました。

レスポンスを返せば返すほど負荷が上昇してしまうと、最終的にはアプリケーションは負荷に耐えきれずに fail する状態になってしまいます。 ベンチマーク走行時間が 60 秒であれば、60秒ギリギリでも死なない (しかしたとえば70秒走らせたら死ぬ) 状態に持っていくのがベストということになりますが、そのために意図的にエラーを返したり、レスポンスを遅らせるというのはかなり本末転倒感があります。

また、8の予選マニュアルには以下のような記述があります。

各ステップで失敗が見付かった場合にはその時点で停止します。 ただし、負荷走行中のエラーについては、タイムアウトや500エラーを含む幾つかのエラーについては無視され、ベンチマーク走行が継続します。

負荷が際限なく上昇するために、負荷走行中のタイムアウトやエラーを原因として失敗させることができなくなってしまうのです。

ベンチマーカー(=ユーザー)に対してタイムアウトや500エラーをいくら返しても咎められない、というのは、アプリケーションを提供する側の意識としては望ましくないのでは、と思ったわけですね。

そのあたりを解決する方法は何かないか……

SNS シェア機能の発明

負荷を何らかの方法で競技者にある程度コントロールさせたい、けど明示的に数値を与えるのは違う、というのを解決する立て付けを考えて、予選の競技終了後の運営打ち上げの場でふと思いついたのが「SNSシェア機能」でした。

  • アプリケーションの機能により、特定の条件下でSNSでシェアが行われ、新規ユーザーが増加することで負荷が増加する
  • 増加率はシェア機能の on/off を AB テスト的にコントロールすることで調整可能
  • ある時点でシェアを無効にすることで、際限のない負荷上昇を防ぐことが可能

ABテスト的に一部のユーザだけ(もしくは確率的に)有効にする、というのが思いつかない人も多かったようですが、一部の人は気づけていたので、そこは発想と経験ということで競技の要素になっていたと思います。

チューニングが進んだ状態での最適解は、まずシェアを全力で行い急速にユーザを呼び込みつつ、アクセスしているユーザ数や負荷をモニタリングし、負荷的に耐えきれる限界の人数になったらシェアを無効化する、というものになります。現状のリソースで耐えられるところを見極める、というモニタリング要素も競技要素としてありかなと思います。

また、今回のアプリケーションは基本的に JSON API のみのアプリケーションなので、以下のようなエラーの扱いをしています。

  • 5xx エラーはクライアントがリトライするため減点しない
  • ただし、最初のリクエストから10秒以上成功しなかった場合はタイムアウトで失敗
    • エラーとして減点、エラーが一定数以上で fail
    • そのユーザは去ってしまうので結果的に負荷が下がる

サービス提供側で何らかの原因で 5xx エラーが発生することは稀にあることとして、クライアントのリトライで救えるレベルであればアプリケーションとしては問題ない、ただしユーザーが耐えきれなくて離脱してしまうようでは問題である、という基準を置いています。

最後に

ISUCON 3 の時に自分が軽い気持ちで導入した workload について、その後の経緯をみるにつけ、これは何らかの結論を出す必要があると思っていました。

今回の出題で、やっと自分の中で成仏させられた気がします。

繰り返しになりますが、ISUCON 8 へ参加、運営に関わっていたすべての皆様にお礼申し上げます。

builderscon 2018 に行ってきました

bulderscon 2018 tokyo に参加してきました。

今年は前後に CEDEC(登壇) と ISUCON(出題) があったので、発表する余裕はなさそうだなということで応募しなかったんですが、やっぱりカンファレンスは聞いているだけだと発表したくなりますね。来年は応募したい。

名札

結局会期中には画像転送しかできなかったので後日何とかしたいと思います。(酔って帰宅後に自宅Wifi繋ぐところでうまくいかないなと思ったら寝落ちしていた) 気象ビーコンからデータ引っ張ってきて電子ペーパーに表示したい!

聞いたトーク

Envoy internals deep dive

Envoy、まだ若いプロジェクトなのに一気に広まっててすごい。内部構造の話、いかにロックしないでパフォーマンスを出すかの話が面白かった。

Algorithms in React

React は4年ぐらい前に流行はじめたところで小さいアプリをひとつ書いてそれっきり、だったので興味深く聞きました。 同期非同期を使い分けて処理をキャンセルできるところとできないところを区別する、なるほどーというかんじ。

知らなかった、時に困るWebサービスのセキュリティ対策

セキュリティ事故、実際起こった身としては思い出したくもないということが多いんですが、それを組織としてリソースを割いて向き合って改善していこうという姿勢が真摯だなと。

発表にあったTLS太郎のような脆弱性のscan、うちは Zabbix でスクリプトを実行して TLS version とか Heartbleed の検知とかを証明書の有効期限チェックとまとめてやってたんですが、なかなか新規のを追加して維持するのが面倒なので、なにかいいサービスはないかなあ。

機械学習を用いず数学でゲーム内の需要予測をする

会社の同僚の発表。数式をできる限り抑えたところが、途中で数学愛が溢れて板書をはじめちゃったところが面白すぎた。

Understanding Microservices with Distributed Tracing

最近話題の分散トレーシングの話。自分も Perl から X-Ray を使うために AWS::XRay とか Plack::Middleware::XRay をこの前書いたので興味深いところ。(エントリ書いてない…)

あらゆる外部への通信を Envoy 経由にすることで、言語を問わず tracing するのは賢い。HTTP, gRPC みたいな通信じゃなく、たとえば MySQLMemcached みたいな TCP の通信でどうやって trace-id を認識するのかがちょっと分からなかったので調べよう。

lld − 開発ツールの主要コンポーネントの1つをスクラッチから作成した話

似たようだけどちょっとだけ違うコードを無理に抽象化しないほうがよい、二度目は一度目よりよく書ける。長年プログラミングしているとほんとそうですね、という話なんだけど、実例が圧倒的なパフォーマンスを出しているので説得力が半端ない。

次世代通信プロトコルにおけるセキュリティ・プライバシー保護・パフォーマンス

kazuho さんの安定の素晴らしい話。いままで QUIC とか正直よく分からん、という感じだったんですが、内部構造の話が分かりやすくて最高です。

全人類に使われるプロトコルがまさに策定されている最前線で戦っているのは格好いいですね。

あなたの知らないデータベースのロギングの世界

セキュリティ事故を受けて DB へのクエリを全部ロギングしたい話。ProxySQL は知らなかったんですが、既存アプリケーションにあとからロギングのためだけに入れるにはアプリケーションへの影響が結構大きくて大変そう、という感想。

自分ならどうするか。単純な TCP Proxy を書いてそこで MySQL へ行く通信だけすべて記録して、解析はあとで何とかするとか、それこそ Envoy みたいなやつを通したらそこで capture できたりしないかな、とかいろいろ考えるところがありました。

業務時間で書いたパッチは誰のもの? OSS 活動にまつわる罠

サイボウズさんの OSS ポリシー策定の話。カヤックOSS は勝手にやっていいよ、というスタンスなんですが、権利関係とかいろいろ取り決めて個人にも会社にも不利益ないようにしていかないとな、と思っていたところなので大変ためになりました。参考にさせて頂きます。

Building Self-Hosted Kubernetes

kubernetes という単語が何回出てきたか分からないぐらい連呼して噛まないのがすごい。Self-Hosted は大変そうだけど、オンプレでやるならやるしかないところですね。自宅で全台落ちるとクラスタ崩壊、つらい。

1日約70万ビルド: DockerとNomadが支えるCI/CDプラットフォーム

CircleCI 2.0 でスケジューラとして Nomad を採用したという話。いつもお世話になっております。

Hashicorp プロダクト、複雑すぎず、システムとして見通しがよいところが好きです。しかしクラスタはいつか崩壊する定めなんでしょうね。

懇親会とか

前夜祭、前夜祭のあとHUB、懇親会、アフターパーティーでHUB、とよく飲みました。ひとり源泉徴収票ナイト(正確には h29syotoku.pdf ナイト)を開催してしまったのでいつか他の人のも見せてもらわないと…

水曜日のスピーカーディナーから含めると4日間の長丁場で、主催、スタッフ、スポンサー、スピーカーの皆様には大変お世話になりました。楽しいカンファレンスをありがとうございました!

ハッカーズチャンプルーに参加して Goとコンテナで作るWebアプリケーションベンチマーカー という話をしてきた #hcmpl

ハッカーズチャンプルー2018 で Go と ISUCON について話してもらえませんか? というお誘いをいただきまして、20年ぶりに沖縄に渡っております (注 まだ帰ってないので現在進行形)。

発表資料はこちらです。

speakerdeck.com

前日〜前夜祭

早めの飛行機で昼前に着いたので、瀬長島という空港から車で15分ぐらいの島へ渡って、沖縄らしいもの…ということでハンバーガーを。

氾濫バーガー チムフガス というお店の「氾濫バーガー」すごい。1.5cm厚のほぼ脂身なベーコンとパティ2枚、40過ぎの人間が食べるものではないのでは…と思ったけど意外としつこくなくて完食。

寝不足の状態で沖縄の凶悪な太陽を浴びたため、その後ホテルで昼寝して、起きたら前夜祭の時間でした。

時間になっても10人ぐらいしかいなくて、なるほどこれがウチナータイム、と思いつつビールを飲んでいると、だんだん人が集まってきて LT 開始、飛び入り LT が引きも切らず、いいコミュニティだなあ…とひたすらにビールを開けてました。

カンファレンス当日

朝、台風の影響か南国らしい土砂降りの洗礼を受けつつ、スタッフの方に会場まで車で送ってもらいました。

チャンプルーの名の通り、特定の技術要素によらないトーク、LT があって飽きないですね。とくに motemen さんのいい話、あーいい話だなーって聞いてました。人に歴史あり。

懇親会でオリオンビールを無限にのみ、沖縄の若者に ISUCON を仕込んでいる さぼ (@saboyutaka) さんと話せたのもよかったですね。

台風で帰れない

既にカンファレンス中から台風の影響で日曜日の飛行機が飛ばなくて帰れなそう、という話がでしたが案の定欠航が決まり、月曜日に振り替えようとしたけど既に満席で、火曜まで帰れないことが確定…

懇親会二次会中に欠航を知り、この時点で日月の宿を確保しておくべきだったのだけど (飛行機は振り替えできた)、眠くて寝てしまって翌朝。

既に那覇のホテルがほとんどすべて埋まっていて、booking.com でひたすらリロードして空き部屋 (おそらく飛行機が飛ぶ人がキャンセルしたんでしょう) を見つけたら争奪戦を何回か挑んでやっと確保して現在に至ります。

「ブログを書くまでがハッカーズチャンプルーです」ということなのですが、帰宅できない人たちでこれから後夜祭があるようなので行ってきます。まだハッカーズチャンプルーが終わりません!

maprobe - Mackerel のホスト情報と連携する外形監視エージェントを作った

監視を Zabbix から Mackerel に移行しています。そこで困ったことを OSS を書いて解決しようシリーズのお時間です。

ホストのダウン検知を早くしたい

Mackerel の監視は Push 型と呼ばれるもので、mackerel-agent が Mackerel サーバに対してメトリクスを送信する形態です。そのため、agent を稼働させているホストがダウンしたという事象は、「一定時間サーバに対して情報が送られてこない」ことによって、組み込みのアラート "connectivity" として検知されます。

これによって困ることとしては、以下があります。

  • ホストが実際にダウンしてから7分程度経過しないとアラートが上がらない
    • あまり短時間で判断してしまうと誤検知が増えるからでしょうか
    • もうちょっと早く検知したいです
  • "connectivity" アラートは Critical レベルしかなく、設定変更不可
    • 大抵のホストは多重化してあるため、1台ダウンするのは大きな問題にならない
    • アラートレベルの設定指針として、「即対応しないとサービスに影響が出るものは Critical」「翌営業日に確認・対処でいいものは Warning」としているので、Critical でない事象で Critical を上げたくない

内部リソースに対する外形監視をしたい

Mackerel の監視の基本は Push 型ですが、URL外形監視機能 というものがあり、これは Mackerel のサーバから HTTP(S) で対象にアクセスして監視する機能です。

外部に公開している HTTP のリソースについてはこれで問題はないのですが、足りないところとしては以下があります。


ということで、そのあたりを補完する外形監視エージェント、maprobe を作りました。

github.com

maprobe がやること

maprobe は次のように動作します。

  1. Mackerel API を叩いてホスト情報を取得
    • Service, Role でフィルタリング
  2. 各ホストに対して、probe(pingtcp、http、command)を実行
  3. 得られた結果をホストメトリックとして Mackerel に送信
  4. 60秒ごとに繰り返し

組み込みで ping, tcp, http の監視機能と、Mackerel agent plugin 形式で出力するコマンドを実行する機能があります。

監視対象のアドレス等は、Go の text/template 形式で、mackerel-client-go#Host の持っている値を展開することで指定します。

設定ファイルの例

probes:
  - service: production
    role: server
    ping:
      address: '{{ .Host.IPAddresses.eth0 }}'

  - service: production
    role: InternalELB
    http:
      url: 'http://{{ .Host.CustomIdentifier }}/api/healthcheck'
      post: POST
      headers:
        Content-Type: application/json
      body: '{"hello":"world"}'
      expect_pattern: 'ok'

  - service: production
    role: redis
    tcp:
      host: '{{ .Host.IPAddress.eth0 }}'
      port: 6379
      send: "PING\n"
      expect_pattern: "PONG"
      quit: "QUIT\n"
    command:
      command: "mackerel-plugin-redis -host {{ .Host.IPAddress.eth0 }} -tempfile /tmp/redis-{{ .Host.ID }}"

このように、複数の Service / Role に所属しているホストに対して外形監視を行い、ホストメトリックを送信することができます。 (なお、チェック監視ではなくメトリック形式なのは、チェック監視のような 0/1/2 でアラートを上げる形は過去の遺産でしかなく、今後はなるべく使いたくないという思想によるものです)

f:id:sfujiwara:20180420175809p:plain

たとえば ping であればこのようなグラフが得られるので、この値に対して適宜アラートを設定する、ということになります。

Critical がなく Warning だけのアラートも設定できますし、式機能を使えば特定の Role に所属するホストの 10% 以上に ping が疎通できなくなったらアラート、というような設定も可能になります。

コマンド実行の応用例

特定の Service / Role に存在するホスト(情報)に対して、任意のコマンドを実行できるため、「Mackerel から退役に失敗して残ってしまっているが、既に EC2 側にはインスタンスが存在しないホスト」の掃除をする例です。

probes:
  - service: production
    role: EC2
    command:
      command: 'cleanup.sh {{.Host.ID}} {{index .Host.Meta.Cloud.MetaData "instance-id"}}'

Mackerel のホストIDと EC2 のインスタンス ID を引数にして、既に terminate されているインスタンスなら mkr retire を実行して退役処理を行う script を実行します。

#!/bin/bash
set -u
host_id="$1"
instance_id="$2"
exec 1> /dev/null # dispose stdout
result=$(aws ec2 describe-instance-status --instance-id "${instance_id}" 2>&1)
if [[ $? == 0 ]]; then
  exit
elif [[ $result =~ "InvalidInstanceID.NotFound" ]]; then
   mkr retire --force "${host_id}"
fi

なお、このような処理は毎分実行するには負荷が大きい場合があるので、maprobe once というサブコマンドで、処理を一回だけ実行できるようにもしてあります。このような掃除だったら、1時間に1回 cron で実行する、でも十分でしょう。

コンテナ環境への対応

DockerHub にイメージを用意してあります。https://hub.docker.com/r/fujiwara/maprobe/

maprobe のように複雑な設定ファイルが必要なものをコンテナにすると、設定ファイルをコンテナ内に配置するためだけにいちいち自前のイメージを作成する必要があって面倒です。

なので、-config 引数の値はローカルファイルだけではなく、HTTP(S)と Amazon S3 の URL を処理できるようにしました。 適当なところに設定ファイルを配置し、環境変数 CONFIG にその URL を指定すれば、公式イメージをそのまま動作させられて便利ですね。

config は更新を検知して自動で再読み込みするので、maprobe agent の再起動も不要です。

どうぞご利用ください。

sardine で mackerel-plugin の出力をサービスメトリックとして投稿する

全国三千万 Mackerel ユーザーの皆様こんにちは。

mackerel-plugin で生成した値を、サービスメトリックとして投稿したいと思ったことはないでしょうか。ありますよね。でも mackerel-agent ではホストメトリックしか投稿できません。

ということで、拙作の sardine に Mackerel のサービスメトリックとして投稿する機能を追加しました。(Thanks for @jet_zousan)

sardine についてはこちらをご覧ください。

sfujiwara.hatenablog.com github.com

mackerel-plugin 互換の出力を CloudWatch のメトリックとして投稿する agent です。 おもにコンテナ環境で、mackerel-agent を全部に立てたくはないけど使い慣れた mackerel-plugin で収集した値を集約して把握することを目的としています。Go で書かれていてバイナリ1つで動作するため、コンテナに同梱して動かすのも簡単です。

以下のような設定ファイルで起動すると

[plugin.metrics.ping]
command = "mackerel-plugin-ping -count 2 -host 8.8.8.8,8.8.4.4"
service = "Home"
destination = "Mackerel"

サービスメトリックを投稿して

$ sudo MACKEREL_APIKEY="XXX" sardine -config test.conf -debug
2018/03/27 13:05:36 [plugin.servicemetrics.ping] starting
2018/03/27 13:05:38 putToMackerel: {"Service":"Home","MetricValues":[{"name":"ping.rtt.8_8_8_8","time":1522123536,"value":3.379283},{"name":"ping.rtt.8_8_4_4","time":1522123536,"value":2.739579}]}
2018/03/27 13:06:38 putToMackerel: {"Service":"Home","MetricValues":[{"name":"ping.rtt.8_8_8_8","time":1522123596,"value":2.716042},{"name":"ping.rtt.8_8_4_4","time":1522123596,"value":3.67368}]}

こんな感じにグラフができます。便利ですね。

f:id:sfujiwara:20180327131535p:plain

また、interval という設定で実行間隔を制御できるため、毎分取るほどでもない値を任意の間隔で投稿することもできます。(mackerel-agent では metric plugin の実行間隔は 1分で固定になっていて、現状変更できません)

どうぞご利用ください。

ECS のデプロイツール ecspresso と、環境変数を展開して YAML/JSON/TOML を読み込む go-config について

OSS紹介 Advent Calendar 2017 - Qiita 18日目の記事です。(一週間遅れ)

Amazon ECS へのデプロイツール ecspresso と、そこで使っている環境変数を展開しつつ複数の YAML/JSON/TOML を読み込む config loader である go-config の紹介をします。

ecspresso

github.com

エスプレッソ」と読みます。Go で書かれた Amazon ECS 用のデプロイツールです。以下の3つのファイルを用いて ECS へのサービス、タスク定義作成、入れ換えを行います。

  • YAML の設定ファイル
  • タスク定義のための JSON (aws ecs describe-task-definition 出力と互換)
  • サービス定義のための JSON (オプション。aws ecs describe-services 出力の services セクションと互換`)
region: ap-northeast-1
cluster: default
service: app
task_definition: taskdef.json
service_definition: service.json
timeout: 10m

create (サービス作成)、deploy (新しいタスク定義を作成しサービスに対して入れ換える)、status (deployments, events をみる)、rollback (一つ前のタスク定義に差し替えてデプロイすることでロールバックする) の機能があります。

元々 aws-cli を shell script から叩いていたデプロイ script を実装し直したもので (一番最初のバージョンは Go から aws-cli をコマンド起動するものでした)、「それ ○○ (他の ECS に対応したデプロイツール)とどう違うの」といわれると答えに窮する代物ですが…

無理矢理特徴を挙げると

  • Go で実装してあるのでシングルバイナリで環境依存少なく動作する
  • タスクとサービスの定義ファイルには後述の go-config による、環境変数展開機能がある

ぐらいでしょうか。

go-config

github.com

こちらは、複数の YAML / JSON / TOML をマージしつつ、Go の text/template の記法で環境変数を展開して読み込むことができるパッケージです。もともと社内のとあるアプリケーションで使用するために開発されたものですが、ecspresso で使いたいがために (便利なので) 公開しました。

コンテナでアプリケーションやミドルウェアを動作させる場合、設定ファイル自体はコンテナに同梱しておくが、その中の値は環境変数で指定したい、というニーズがあります。ファイルにハードコードされていると設定値を書き換えるたびにコンテナのビルドが必要になりますが、環境変数であればコンテナの起動時に動的に指定することができるため、便利になりますね。

foo: {{ env "FOO" "default_foo" }}

このような YAML を、環境変数 FOObar が設定されている状態で読み込むと

foo: bar

となりますし、FOO が未設定であればデフォルト値として

foo: default_foo

として読み込まれます。

また、環境変数が設定されていなければ panic することで、読みこみ時に確実に設定されていることを強要する {{ must_env "FOO" }} という記法もあります。

やっていることは単純で、先に Go の text/template環境変数を展開した上で YAML / JSON / TOML のパーサを通しているだけなので、ファイル自体が各フォーマットとして不正であっても展開後に正しい状態であれば、問題なく読み込むことができます。

とはいえエディタでの編集時にそれぞれのフォーマットとして正しく扱えてくれた方が楽なので、" " でクォートされた内部に展開する場合は、template の記法の方でバッククォートを用いるのがお薦めです。

{
  "foo": "{{ env `FOO` `default_foo` }}"
}

先述の ecspresso では、タスクとサービスの定義ファイル (JSON) の読みこみ時に go-config による環境変数展開が行われます。

{
  "taskDefinition": {
    "containerDefinitions": [
      {
         "image": "example.com/myapp:{{ must_env `IMAGE_TAG` }}",
         "environment": [
           {
             "name": "APP_SECRET",
             "value": "{{ must_env `APP_SECRET` }}"
           },
           {
             "name": "ENDPOINT",
             "value": "{{ env `ENDPOINT` `example.com` }}"
           }
         ]
      }
    ]
  }
}

典型的には、以下のような値を展開することを想定しています。

  • デプロイごとに変わる可能性が高い値
    • Docker イメージのタグなど
  • リポジトリには生で入れたくないクレデンシャルの類

特にクレデンシャル類は、direnv 等で使用される .envrc というファイルに記述した上で暗号化してリポジトリに保存し、デプロイ時に復号、環境変数に設定した上でデプロイを行うと便利に扱えると思います。

# .envrc
export APP_TOKEN="raw_value_of_token"

AWS KMS で暗号化した値を .envrc.encrypted として出力、リポジトリにはこれをコミット。

$ aws kms encrypt --key-id mykey \
       --plaintext fileb://.envrc \
       --output text --query CiphertextBlob \
       --output text \
    | base64 --decode > .envrc.encrypted

デプロイ直前に KMS で .envrc を復号し、環境変数に設定してデプロイ。

$ aws kms decrypt --ciphertext-blob fileb://.envrc.encrypted \
        --output text
        --query Plaintext \
     | base64 --decode > .envrc
$ source .envrc
$ ecspresso --config app.yaml deploy

まとめ

  • Amazon ECS 用のデプロイツール ecspresso を書きました
  • go-config は実行時に環境変数を展開しつつ設定ファイルを読み込めて便利です