graceful restart できない daemon の再起動時のダウンタイムを HAProxy でリトライして救う

とあるネットワーク(Web)サーバがありまして。

  • graceful restart できない
  • graceful stop はできる
    • 処理中のものは全て処理し終わってから終了する
  • 再起動は数秒で完了する

という性質のものを、稼働中に再起動するとダウンタイムがでてしまうのをなんとか誤魔化したかったのです。

最初は nginx から proxy しているところで、繋がらなかったら何秒かおいて何回までリトライ、みたいな設定ができないか探したんですが見つからず、HAProxy を調べたら以下のような設定でできたのでメモ。

frontend absorber
    mode tcp
    default_backend upstream
    bind *:9999

backend upstream
    mode tcp
    server localhost localhost:8888 weight 1
    retries 10

retries 10 で、接続できなかった場合には 1秒間隔(1秒以下のtimeout connect が指定されていればその間隔) で10回までリトライする、という挙動になります。これで10秒以内に再起動できれば救済可能です。

適当に ab でリクエストを流し続けながら再起動してみてもエラーはでなかったので、大丈夫でしょう。

(2018-01-15 18:15 redispatch は関係なく、再試行間隔は timeout connect に依存していたので修正しました)

Mackerel と連携する外形監視エージェント maprobe でマネージドサービスのメトリック収集を自動化する

Mackerel Advent Calendar 2018 22日目の記事です。

ちょっと前に maprobe という、Mackerel のホスト情報と連携して外形監視を行うエージェントツールを作りました。

sfujiwara.hatenablog.com

maprobe は指定した Mackerel のサービス、ロールに存在しているホストに対して外形監視を行った結果をホストメトリックとして投稿するものです。定義にしたがって外から pingTCP, HTTP の疎通を確認した結果を投稿できるのですが、mackerel-agent のメトリックプラグインと互換性のある出力をするコマンドを実行する機能もあります。

今回は AWS インテグレーションで管理されているマネージドサービスのホストに、maprobe でメトリックプラグインで取得したメトリックを追加することで、運用負荷を下げる手法を紹介します。

AWS インテグレーションで管理されたホストにカスタムメトリックを投稿する方法(おさらい)

メトリックプラグインによって得られたメトリックを AWS インテグレーションで管理されているホストに送信することは、mackerel-agent でも可能です。

例として ElastiCache Redis に対してメトリック収集を行いたい場合、どこかのホストで実行されている mackerel-agent の設定に以下のような記述をします。

[plugin.metrics.redis-001]
command = [
  "mackerel-plugin-redis",
  "-host=redis-001.xxxx.apne1.cache.amazonaws.com",
  "-tempfile=/tmp/mackerel-plugin-redis-001",
  "-config-command=",
]
custom_identifier = "redis-001.xxxx.0001.apne1.cache.amazonaws.com"
  • ElastiCache では CONFIG コマンドが実行できないので、-config-command= の設定で CONFIG による値取得を無効化しています
  • -tempfile の指定は、同一 agent で複数の ElastiCahce を監視する場合に状態管理ファイルを分離するために必要です

ポイントは custom_identifier で、ここに mkr hosts --verbose で確認できる customIdentifier の値を指定します。

これによって AWS インテグレーションで認識された ElastiCache のホストに、mackerel-plugin-redis によって取得したメトリックを投稿することができます。

mackerel-agent でメトリック収集する場合の運用負荷

このように maprobe を使わずとも mackerel-agent でもメトリック収集自体は可能なのですが、運用上面倒な点があります。

  • custom_identifier を調べる必要がある
    • 何らかの方法 (mkr コマンドや API) で調べた値を、config に静的に書き込む必要があります
  • ホストが増減した時の設定更新が必要
    • AWS 上でインテグレーション対象のホストを増減した場合、config の記述もそれに合わせて増減する必要があります
    • 具体的には、設定ファイルを再生成して mackerel-agent の reload が必要になります

maprobe で解決すること

maprobe は指定したサービス、ロールのホストを Mackerel API から収集し、その時点で存在している全てのホストに対して外形監視、コマンド実行を行います。メトリック投稿には API で得られた Mackerel のホストIDを使用するので、custom_identifier を調べる必要はありません。

ホストが増減した場合には自動的に追従するため、設定ファイルの変更は必要ありません。

設定ファイルが変更されている場合には自動で再読み込みを行うため、新しい監視設定を追加しても再起動は必要ありません。

maprobe の設定例

先に記述した、ElastiCache Redis にカスタムメトリックを投稿する例は maprobe では以下のようになります。(v0.2.3で command に配列を渡せるようになりました)

probes:
  - service: production
    role: ElastiCache
    command:
      command:
        - "mackerel-plugin-redis"
        - "-host={{.Host.CustomIdentifier}}"
        - "-tempfile=/tmp/mackerel-plugin-redis-{{.Host.ID}}"
        - "-config-command="
      graph_defs: true

graph_defs: true を指定すると、プラグインから得たグラフ定義の投稿 (MACKEREL_AGENT_PLUGIN_META=1 で得られる結果) を自動的に投稿するため、Mackerel 上での見た目は mackerel-agent でメトリック収集したものと同一になります。

さらに maprobe では、ping, TCP, HTTP による外形監視 (check-{tcp,http} とほぼ同等機能) も行えるため、Redis プロトコルPING, PONG による外形監視も追加できます。

probes:
  - service: production
    role: ElastiCache
    tcp:
      host: "{{ .Host.CustomIdentifier }}"
      port: 6379
      send: "PING\n"
      expect_pattern: "PONG"
    command:
      command:
        - "mackerel-plugin-redis"
        - "-host={{.Host.CustomIdentifier}}"
        - "-tempfile=/tmp/mackerel-plugin-redis-{{.Host.ID}}"
        - "-config-command="
      graph_defs: true

Mackerel 本体で提供されている外形監視は、現時点ではグローバルな HTTP(S) に対するもののみです。マネージドサービスのホストでは mackerel-agent が稼働しないため、Unreachable なアラートも上げられません。そのためダウン検知がむずかしいのですが、maprobe では外形監視によりホストのダウンを素早く検知できます。

まとめ

maprobe は Go で実装された OSS です。Mackerel API を利用して、簡単な設定で指定したサービス、ロールのホストに対する外形監視とメトリック収集、投稿を行えます。

github.com

DockerHub でイメージも公開 しているため、コンテナ環境での利用も容易です。

マネージドサービスのホストが増減しても、特に監視設定の変更を意識せずにメトリック収集、外形監視を追従できるため、運用負荷を下げることができるでしょう。

WEB+DB PRESS vol.108 に18周年記念エッセイを寄稿しました

いつもお世話になっている WEB+DB PRESS誌の vol.108 に、18周年記念エッセイを寄稿しました。2018年12月22日発売です。創刊18周年おめでとうございます。

WEB+DB PRESS Vol.108

これまで WEB+DB PRESS には Perl Hackers Hub (vol. 71) と、特集 (vol.94) で技術記事を書かせていただきましたが、まさかエモい文章を求められるとは。どういう経緯での人選なのかは知るよしもありませんが…

普段カンファレンスなどでも技術の話しかしないので、引き受けようかどうしようかちょっと迷ったのですが、(中を読んでいただければ分かるとおり) こういうお誘いには乗っておくべきだということで快諾しました。

いただいたお題が「私が限界を感じたとき」というもので、普段は「人生なるようにしかならないよ」と適当に生きているのでちょっと困りました。WEB+DB PRESS & 限界というキーワードで思い浮かんだのが20年近く前、WEB+DB PRESS の創刊当初の昔話だったため、完全におじさんの昔語りになっています。ご笑覧ください。

f:id:sfujiwara:20181220233548j:plain

「こんな仕事のしかたをしていては死ぬな」というタイトルは編集の方に提案していただいたもので、特に他にいい案も思い浮かばなかったのでじゃあそれで、と決めたものです。他の3名の方々のエッセイを読み、自分の語りがなんだかちょっと恥ずかしくなったので、今後も基本的には真面目に技術の話をしていきたいと思います…

今号の特集は、スキーマ駆動 Web API 開発 (最近はもう必須ですね)、久々の PostgreSQL 大特集 (いまは MySQL ばかりですが昔使ってて実は PostgreSQL 大好きです)、ZOZOのリプレースノウハウ大公開 (泥臭い、けど価値がある話で大好物) と大変おもしろく読ませていただきました。今号からは Kindle 版も用意されるようで、より多くの環境で読みやすくなりますね。ぜひどうぞ。

WEB+DB PRESS Vol.108

WEB+DB PRESS Vol.108

  • 作者: 大和田純,中野暁人,山本浩平,曽根壮大,ZOZOTOWNリプレースチーム,権守健嗣,茨木暢仁,松井菜穂子,新多真琴,laiso,豊田啓介,藤原俊一郎,牧大輔,向井咲人,大島一将,上川慶,末永恭正,久保田祐史,星北斗,池田拓司,竹馬光太郎,粕谷大輔,WEB+DB PRESS編集部
  • 出版社/メーカー: 技術評論社
  • 発売日: 2018/12/22
  • メディア: 単行本
  • この商品を含むブログを見る

エッセイ中にも出てくる自分にとって転機になったカンファレンス、YAPC が来年1月に東京で開催されます。

yapcjapan.org

自分も分散トレーシングと AWS X-Ray の話をするので、こちらも是非よろしくお願いします。チケットもまだ発売中です!(2019年1月6日までの予定です)

今年書いた & update したCPANモジュールたち

Perl Advent Calendar 2018 11日目のエントリです。

最近、新しく書きはじめるもので Perl を使うことはめっきり減っているのですが、それでも仕事のいくつかのプロジェクトのメイン言語が Perl だったり、リリースした CPAN モジュールでまだまだ需要があるものはメンテが必要です。

ということでちょくちょく Perl も書いているので、今年自分がリリースした CPAN モジュールと update の変更点をまとめておきたいと思います。

Fluent::Logger

おなじみ Fluentd へログを送信するためのモジュール。2011年にリリース以来、継続的にメンテナンスしています。2018年の変更は、エラー時に即座にリトリライするためのオプション追加でした。

  • 0.27 2018-06-19T07:52:56Z
    • Add retry_immediately option (#28)

これは、とあるプロジェクトで AWS NLB の配下に fluentd を配置していて、NLB が 360秒間通信がないとコネクションを切断してしまうので (普通の fluentd はそういう挙動はしない)、その場合のリトライが次のログ送信時まで遅延してしまうのを救済する意図で追加したものでした。

Redis::Setlock

Redis を使って排他ロックを取得するためのモジュールです。こんなエントリも書きました。 sfujiwara.hatenablog.com

社内の Perl のプロジェクトではよく使われていて、そこでの要求からいくつかの機能追加があります。

  • 0.12 2018-07-09T08:50:57Z
    • Set expire to a wait queue. (#6)
  • 0.11 2018-05-24T05:04:58Z
    • Add blocking option to lock_guard (#5)
  • 0.10 2018-05-24T04:23:50Z
    • Change an algorithm to build key name for $WAIT_QUEUE. (#4)
  • 0.09 2018-05-23T07:33:17Z
    • Add $WAIT_QUEUE package variable. (#3) When $WAIT_QUEUE is true, wait for a lock released using BLPOP command instead of polling

このモジュールは Redis のある key を SET NX で排他取得し、必要なくなったらその key を削除することでロックを解放します。

ロックが競合した場合、あとにロックを取得しようとしたプロセスは 500ms 間隔のリトライで再度取得を試みるのが従来の実装でしたが、それでは解放待ちに時間が掛かりすぎるという課題がでました。そこで BLPOP を利用した解放待ち queue を用意して、解放時に queue へメッセージを送信することで、待たされているプロセスは速やかにロックを取得しにいく、という挙動ができるようになりました。

Katsubushi::Client

memcached protocol でアクセスできる分散ID採番機 katsubushi へのクライアントモジュールです。

  • 0.2 2018-07-25T03:10:29Z
    • fork safe (#4)

fork safe、(作者からすると)嫌な (使う側とすると)嬉しい fix ですね。

とあるプロジェクトでベンチマークをしていたところ、妙に katsubushi からのID取得に時間が掛かることが発覚しました。その原因を追及したところ、内部で使っている Cache::Memcached::Fast は fork safe ではない!ということに気づいて解決したものです。

一度接続したあと fork した場合、fork したプロセスがひとつの接続を共有してしまいます。そのため、自分が投げたリクエストの結果を誰かに横取りされてしまって、なかなかレスポンスが返ってこない、というような現象が発生するのでした。

fork safe ではないことは、Cache::Memcached::Fast のドキュメント にも、ちゃんと書いてあります。

disconnect_all

$memd->disconnect_all;

Closes all open sockets to memcached servers. Must be called after "fork" in perlfunc if the parent process has open sockets to memcacheds (as the child process inherits the socket and thus two processes end up using the same socket which leads to protocol errors.)

AWS::XRay, Plack::Middleware::XRay, Devel::KYTProf::Logger::XRay

これは今年の新作です。

AWS X-Ray という分散トレーシングのマネージドサービスに、Perl のアプリケーションからトレースデータを送信するためのものです。

公式の Perl SDK は提供されていないため、じゃあ自分で書くしかないな、ということでえいやと書きました。

f:id:sfujiwara:20181027013112p:plain

sfujiwara.hatenablog.com

ちなみに Katsubushi::Client が妙に遅い(ことがある)、というのは X-Ray の解析結果から気がついたものでした。DB へのクエリなどとは違って通常遅くなることが考えにくい部分なので、X-Ray がなかったら原因にたどり着くのに相当時間が掛かっただろうなあという実感があります。おすすめです。

X-Ray 関係のモジュールについては、来年1月に開催される YAPC::Tokyo で、 「Perlでも分散トレーシングしたい! - AWS::XRayによる解析とその実装」というタイトルで 40分の発表をする予定です。 YAPC::Tokyo 2019 - TALKS

12月16日まで チケットも発売中のようです ので、興味のあるかたは是非聞きに来てください!

まとめ

あらためて振り返ってみて気がついたのですが、実はこれらの修正はすべて、とあるプロジェクトの構築中に負荷試験で明らかになった問題を解決したり、負荷試験結果を解析するために作られたものでした。必要は発明の母ですね。

それでは、良いお年を。

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日間の長丁場で、主催、スタッフ、スポンサー、スピーカーの皆様には大変お世話になりました。楽しいカンファレンスをありがとうございました!