tech.kayac.com

新人研修担当の長田です。

今年も新人研修の締めとして社内ISUCONを行いました。

昨年はプログラミング基礎の講師をやったのですが、 今年はその実績を買われて(?)社内ISUCONの出題を担当することになりました。

過去のISUCON準備の様子を傍から見ていた身としては、 準備を始める前から「とにかく大変そうだ・・・」というイメージを持っていました。 問題を作りこむ以上、どうしてもISUCON当日ぎりぎりまでかかってしまうのでしょう。

ぎりぎりになるのはまあ準備する人が頑張ればいいとして、 ぎりぎりになった結果競技自体の進行が危ぶまれるのは避けたい! ということで、いくつか効率化という名の妥協策をとることにしました。

効率化できるところは?

毎回新規に出題するのはしんどい!

社内ISUCONは過去2回実施していますが、 どちらも新規に高速化対象のWebアプリケーションを作成していました。 アプリケーションが異なるということは難易度調整も毎回行わなければいけません。 ベンチマーカーの動作確認も毎回行わなければいけません。

確認事項が増えるということは確認漏れが発生する可能性も増えるわけで、 重大な漏れがあった場合には最悪競技続行不可能になってしまうかもしれません。

社内ISUCONは新人研修の一環として行います。 新人・・・ということは毎回別の人が競技を行うということです。 つまり問題は変えなくてもみんな新鮮な気持ちでトライできる! ということで問題の作りこみは昨年のものに多少手を加えるにとどめました

競技にゲスト参加している先輩社員が有利にはなってしまいますが、 まー1年も経てば内容なんて忘れているでしょう。 大事なのは「研修を受けた新人が成果を確認できる」ことです。

できるだけ楽にセットアップしたい!

ISUCONを実施する上で準備しなければならないものはざっくり以下のとおりです。

  • チーム数分の競技サーバー
  • ベンチマーク実行サーバー
  • ベンチマーク結果集計サーバー

3つのアプリケーションを同時に用意しななければならないわけです。 これは手慣れていないとなかなか厳しい。

「競技サーバー」は減らすことはできません。 競技の性質からそこは仕方がありません。 減らせるとすれば運用用である「ベンチマーク実行サーバー」と「ベンチマーク結果集計サーバー」です。

と、いうわけで今回は以下のように各サービスで置き換えることにしました。

  • ベンチマーク実行サーバー -> AWS Lambda
  • ベンチマーク結果集計サーバー -> Mackerel

この置換えを行うことで、競技運用に必要なサーバーが必要なくなりました

ベンチマーク渋滞を無くしたい!

複数のチームが同時にベンチマーク開始をリクエストした場合、 どちらかのチームの処理を先に行い、もう一方のチームの処理は順番待ち状態になってしまいます。 かと言ってベンチマーク処理を並列実行すると、 ベンチマーカーのリソース管理をうまくやらないとベンチマークサーバーの状態によってスコアが変動してしまいます。 チーム数分のベンチマークサーバーを用意するのが理想ですが、 ベンチマークを実行するサーバーはそれなりのスペックが必要で、それなりのスペックのサーバーをチーム数分用意するのはそれなりにお金がかかります。 第一そんな数のサーバーをセットアップ&動作確認するのはしんどいのでやりたくありません。

今回ベンチマークの実行をLambdaで行うことにしたため、 副作用としてベンチマークをいくらでも並列実行できるようになりました。

なんだか夢の様な仕組みですが、デメリットがなかったわけではありません。 改善点についてはこの後書きます。

ベンチマーカーについて

ベンチマークの実行

競技対象のWebアプリケーションが昨年のものと同じなので、 ベンチマーカーもおおよそ昨年と同じものを使いまわしています。 代わりにLambda上で動かすために手を加えています。

ベンチマーカー構成

ベンチマーカーはGoで書き、 Lambda functionの管理にはapexを利用しました。 apexを使うとGo以外の言語を一切書かずにLambdaでGoバイナリを動かすことができます。

ひとつのworkerは20並列で競技サーバーにベンチマークをかけるようになっています。 問題アプリケーションの初期状態では20並列のリクエストをさばくだけでCPUを使い切るようになっていますが、 チューニングを加えていくことで20並列ではマシンパワーが余るようになってきます。 より高い負荷が必要な場合はkickerからベンチリクエストを送る際に-workloadというパラメータを指定すると、 指定した数だけのworkerが実行されるようになっています。

得点配分

スコアは以下の式で計算しました。


スコア = pv / (0.3 + 通信量(GB) * 0.14)

式中に出てくるマジックナンバーの出自はというと、

  • 0.3
    • もろもろの固定費(USD)
  • 0.14
    • Amazon EC2の通信料(USD)を丸めたもの
    • 東京リージョンでひと月内に1GB〜10TBの範囲で通信した場合の料金

つまり、スコアは「$1でさばけるpv数」を表していました。 アプリケーションのチューニングによって運用コストがいかに下げられるのかを示した計算方法となっています。

社内ISUCONの結果

スコア変遷

初期スコアは1700程度、優勝チームは50355(グラフ中の水色)、模範解答は103379(グラフ中の抹茶)でした。 以下各チームのスコア


          boko :   62793 (fail)
  yokohama-zoo :   50963
  moulin-rouge :   42336
chatzmers-mini :   33126
 yokohama-rare :   23579
      sukiyaki :   16106
       ninjari*:   14823
           nil :   14309 (インターン+アドバイザーの先輩混合チーム)
          neck*:    3469
   haagen-dazs*:    3296
    takeiteasy*:    1679
      yurufuwa*:       0 (fail)
           333*:       0 (fail)

     acidlemon :  103379 (模範解答)

*が付いているのは新卒チーム

今年も先輩チームは面目を保ったようです。 スコアだけで見ると1位のチーム「boko」ですが、最後に攻めすぎたせいでfailしてしまったようです。

基本的な改善点(ループ内でのクエリ発行、適切なインデックス作成)を潰していけば、 スコア10000程度は余裕で超えられるようになっていたので、新卒諸氏にはもうちょっと頑張ってもらいたかったな〜という感想です。

改善点

-workloadの値で試行錯誤しちゃう問題

過去のベンチマーカーの実装に倣って、ベンチマーク実行時に-workloadというパラメータを設定できるようにしていました。 下手にいじれる要素があることで、そこをいじってスコアを上げようとしてしまうチームが出てきてしまいました。 残念ながらというか必然というか、この方法を試みたチームはことごとくfailしていたようです。

本来-workloadはアプリケーションのチューニングが充分にされた状態で、 負荷を上げる以外にスコアを上げる方法がない場合の最後の手段として指定するものです。 これが競技者に充分伝わっていなかったために余計な試行錯誤の時間をとらせてしまった感があります。

理想としては-workloadパラメータなしで、常に対象アプリケーションに限界の負荷がかかり、 かつ負荷のかけ過ぎでfailしないベンチマーク方法になっていればいいのですが・・・。

なお、-workloadは本家ISUCO3で導入されたパラメータですが、 これは負荷を上げすぎてしまうと初期状態の対象アプリケーションがfailしてしまう、という問題を回避する苦肉の策とのことです。 (by 当時の出題者のfujiwaraさん)

スコアが伸び悩んでいるチームへの手助け

2週間の研修を受けたとはいえ、新卒チームはサーバーでの操作に慣れていない初心者です。 各チームのスコアは随時確認していたので、これを見ながら伸び悩んでいるチームには助言をしてもよかったように思いました。 何をすればいいのかわからず競技時間が過ぎてしまうのはもったいないですし、なにより楽しくありません。

ベンチマーカーにスコアに応じてヒントを出す機能をつけたり、 予めチェックするべき要素リストを作って渡しておくなど、 新人研修用にアレンジを加える必要があるように思いました。

Lambda function実行ホストのTIME_WAIT

仮想化されているとはいえ、コードを実行するのは実在するサーバーです。 ベンチマーク対象のアプリケーションがkeep aliveを有効にしていない場合、 ベンチマーカー側でTIME_WAITが大量に溜まってしまう、という問題がありました。 実サーバー上でベンチマークを行なっている場合は調整のしようがあるのですが、 managedなサービスであるLambdaでは手が出せません。

最終計測はTIME_WAITが無くなった頃を見計らって、ひとチームずつ順に行いました。 Lambdaだから全チーム同時に最終計測ができるぞー!と妄想していた頃がぼくにもありました。

名前解決の失敗

ベンチマークを繰り返すうちに、ベンチマーカー上で以下のようなエラーが発生することが有りました。


lookup {競技ホスト名} on 10.153.0.2:53: dial udp 10.153.0.2:53: socket: too many open files

どうやら名前解決に失敗しているようだということで、 ベンチマーク開始時に一度だけ名前解決を行い以降はIPアドレス+Hostヘッダーでリクエストを行うよう手を加えました。

当日は出題者もヒマじゃない!

競技の準備は前日までに(といっても深夜までかかりましたが・・・)無事完了したので、 当日はゆうゆうと観戦できるんじゃないかと思っていたのですがそんなことはありませんでした。 誰かがfailするたびに「ベンチマーカーが原因なんじゃないか・・・」と原因を探り、 「どこでfailしたのかが知りたいんだけどー」というリクエストに答えてログ表示機能を追加し、 あるいは前項のようなエラーに対応し・・・、 と常時ベンチマーカーの改善を行っていたので全然暇にはなりませんでした。

社内ISUCONでこれだけベンチマーカーの挙動にいちいちヒヤヒヤするのだから、 本家ISUCONの出題者の方々には頭がさがる思いでいっぱいです。

来年へ

Amazon Lambdaでベンチマークを実行するという方法は、 セットアップなど諸々の作業や問題をスキップできるナイスな選択だったと思います。 次回はベンチマーカーのチューニングは程々に、対象アプリケーションの作り込みに時間をかけられるようになる・・・のかな? まだ出題者が誰になるかはわかりませんが、今回作った仕組みが生かせれば幸いです。

今年の社内ISUCONで悔しい思いをした新卒たちが、 1年後に入ってくる2017年の新卒チームをバッタバッタとなぎ倒してくれることでしょう。

関連エントリ

カヤックではISUCONに興味があるエンジニアも募集中です!

Lobiはメインの言語としてPerlを採用しています。 サーバーサイドで使用するコードは、Webアプリケーションから手動実行用のスクリプトまで、 ほとんどがPerlで書かれています。

languages.png

(なぜかPerl6のコードがあることになっていますが、さすがにまだ使ってません)

が、そこは適材適所。 Goの方が適していると判断した部分では積極的に利用しています。

Goの使いどころ

単機能を高いパフォーマンスで実現する必要がある場合はGoの出番です。 バイナリひとつを配置すれば動作するというポータビリティも魅力的です。 これらのツール・アプリは単独で実行され、一部はアプリケーションの要求に応じてその機能を提供します。

  • spam-filter
  • maintainer
  • gunfish
  • katsubushi
  • stretcher
  • rin
  • fluent-agent-hydra
  • nuko

それでは各ツール・アプリについて簡単に説明していきましょう。 なお、GithubリポジトリへのURLがないものは非公開のプロダクトとなっています。

spam-filter

Lobiの主たる機能はチャットコミュニティです。 ユーザーがメッセージを投稿するわけですが、その中にはスパムや荒らしも含まれています。 これらの投稿は単純なNGワードで防ぐことは困難です。 対策として、すべての投稿をGoで実装されたベイジアンフィルタに通してチェックしています。

spam-filter.png

スパムメッセージの教示は人間が行う必要が有るため、 spam-filterが判断に迷ったメッセージの蓄積と人力によるスパム判定を専用の管理画面から行なっています。 spam-filter自体は教示を受け付けるインターフェイスしか持たず、管理画面とは完全に独立しています。

Lobiに投稿されるすべての投稿をspam-filterが1プロセスでチェックしています。

maintainer

サービスを提供しているからにはいつでも安定稼働することが理想ですが、 やむをえずメンテナンスを実施することがあります。 メンテナンス中にもユーザーからのアクセスは発生します。 リバースプロキシとして使用しているnginxから静的ファイルを返せば事足り・・・ればいいのですが、 クエリパラメータなどでレスポンスのフォーマットを指定できるAPIがあるため、そうはいきません。 パラメータを解釈して適切なフォーマットでメンテナンス情報を返す必要があります。

関連するnginx.confの設定箇所を抜き出すと以下のようになります。


upstream maintainer {
    server 127.0.0.1:8090;
}

set $maintenance "false"

// 各種メンテ状態判定
if (...) {
    $maintenance = "true";
}

...

// メンテ有効であればmaintainerにreverse proxy
if ($maintenance = "true") {
    proxy_pass http://$maintainer;
    break;
}

ngx_http_lua_moduleを使ってluaで実装することもできますが、 使用言語をむやみに増やしてもメンテナンスコストが上がるだけなので Goで専用アプリケーションを書くことにしました。

平常時は1台のアプリケーションサーバーにつき数十のPerlプロセスがリクエストをさばいていますが、 メンテナンス時は同サーバー上のmaintainerが1プロセスでメンテナンス情報を返しています。

(そもそもこんな仕組みを用意しなくてもいいように、 API設計時にメンテナンス中のレスポンスフォーマットを統一のものとして定義しておくのが理想でしょう)

Gunfish

2016-05-12追記

公開しました! https://github.com/kayac/Gunfish

追記ここまで

iOSのpush通知はAnyEvent::APNSで送っていました。 メインのアプリケーションからHTTPで通知専用アプリケーションにリクエストを送り、 それを元に非同期でAPNsに通知内容を送信する、という仕組みになっていました。 しかし、昨年発表されたHTTP/2を使ったAPNs Provider APIには対応していません。

Goで書かれたpush通知送信ツールとしては、gaurunなどがありますが、 こちらも当時はAPNs Provider APIには対応していませんでした。 (本記事公開時点では対応したPull Rquestがマージされているようです)

APNs Provider APIに対応しつつ、Lobi内で使用していた既存のフォーマットに互換のあるAPNs通知送信ツールとして実装したのがGunfishです。

gunfish.png

Gunfishについて詳しくは以下の発表資料を参照下さい。

AnyEvent::APNsからGunfishに置き換えることで、 全ユーザーに対して送信する場合に300分程度かかっていた所要時間を90分程度まで短縮することができました。

katsubushi

https://github.com/kayac/go-katsubushi

katsubushiはTwiter社のsnowflakeのアルゴリズムを採用したid発番器です。 主にチャットIDの発番に使用しています。

タイムスタンプ、マシンID、シーケンスIDを元に発番するので、 各アプリケーションサーバーにマシンIDを一意に割り振れば 同期の必要なくユニークなIDを得ることができます。

daemonとして常駐し、idを必要とするアプリケーションとは memcachedプロトコルを使って通信を行います。 既存のプロトコルを流用し対応したライブラリを用いることで、 アプリケーション側のコード追加を最小限に抑えることができます。

katsubushi.png

stretcher

https://github.com/fujiwara/stretcher

stretcherはデプロイツールです。 特に初回のデプロイでは、依存するライブラリ等がある場合それらもインストールする必要がありますが、 Goで書いた場合はビルドして作られたバイナリを配置するだけなので非常にお手軽です。

stretcherを用いたデプロイについては以下のエントリーを参照下さい。

rin

https://github.com/fujiwara/rin

rinはAmazon SQSからの通知をトリガーにAmazon Redshiftにデータを投入するツールです。 Lobiではfluentdで集約したアクセスログをRedshiftに取り込むために使用しています。

rin.png

詳しくは以下を参照下さい。

fluent-agent-hydra

https://github.com/fujiwara/fluent-agent-hydra

fluent-agent-hydraはログファイルを監視してfluentdに送信するagentです。 Lobiでは一日に数十GBのログが生成されており、 これらすべてを各アプリケーションサーバーに配置されたfluent-agent-hydraがfluentdに送信しています。

fluent-agent-hydra.png

詳しくは以下を参照下さい。

nuko

おまけ。

nuko(ぬこ)はGoで書かれたSlack botです。 Go習得のためにいままでnodejs(hubot)で書かれていたbotを移植したもので、以下の機能があります。

  • Github issue番号の展開
  • レビュー依頼のランダム振り分け

シンプルなbotですがどれも開発に必須な機能ということでチーム内で日々利用されています。 レビュー依頼をbotに任せると、レビューさせちゃって申し訳ない感が薄れるのでおすすめ!

次回

今回はLobiで実際に使用しているGoプロダクトについて紹介しました。 次回はさらっと紹介したGunfishについて詳しく書こうと思います。

LobiではGoを書きたいエンジニアも募集しています!

Lobiチームの長田です。

今回はLobiで使用しているデータベースの構成・運用について紹介します。

TL;DR

  • メインのデータベースとしてMySQLを使用
  • 一般的なmaster-slave構成
  • HAProxyとMHAでDBのfailoverを自動化
  • HAProxyでslaveへの接続を分散・死活監視
  • MHAで障害時のfailover・ENIを付け替えてmaster切り替え

で、何が起こるの?

サービスが提供できなくなります。 ユーザーの認証情報等、サービス提供に必要不可欠なデータを管理しているため、一時的なサービス停止は免れません。 ユーザー体験的にもビジネス的にも、大変厳しい状態です。

この「一時的」な時間を可能な限り短くするために障害復旧の一次対応を自動化しています。 自動化のメリットとして、単純にサービス停止時間が短くなることはもちろん、 復旧処理を行う際の人的なエラーを未然に防ぐことで二次災害の可能性を減らすことができます。

それでは実際にLobiで採用されている自動化の手段について紹介していきましょう。

DB構成

LobiではサーバーインフラとしてAmazon Web Services(AWS)を利用しています。 Amazon EC2上でMySQLを動作させ、これをサービスのメインのデータベースとして使用しています。

一般的なmaster-slave構成を採用しており、 また一部のテーブルは水平分割されているため、master-slave構成が複数存在することになります。

アプリケーションからmasterへの接続はElastic Network Interface(ENI)を通して、 slaveへの接続はHAProxyを通して行っています。

db_cluster.png

slave障害発生時

正常時は以下のようになっています。 図中では省略していますが、HAProxyとMySQLの間はHTTPで接続を受け付けMySQLのチェックを行うヘルスチェッカーが仲介しています。

slave_failover_1.png

HAProxyが指定の間隔で行っているヘルスチェックが失敗すると、 そのホストには新規接続が回されなくなります。

slave_failover_2.png

アプリケーションはHAProxy通していれば常に正常なslaveに接続することができます。

slave_failover_3.png

slaveは負荷的に余裕を持った台数が稼働しているため、このままでもサービス運用は可能です。 この状態からさらにslaveがダウンすると余剰分がなくなるため迅速にに新しいslaveを追加する必要があります。 HAProxyによるfailoverはあくまでも一次対応の自動化です。

HAProxyにはmasterを含めた全てのデータベースがserverとして定義されています。 言い換えれば、HAProxyはどのデータベースがmasterかは感知しません。 ヘルスチェックを行う際にmasterかどうかを確認し、masterであればチェックに失敗するようにしています。 ヘルスチェックに失敗したserverには接続が発生しないため、masterはslaveとして接続を受けることはありません。


# HAProxyの設定例
listen  mysql-slave
        bind    0.0.0.0:3307
        mode    tcp
        balance roundrobin

        # MySQLが起動しているインスタンスの指定したポート(この例では13307番)にhttpでヘルスチェックをかける
        option httpchk

        # db01はmasterだがヘルスチェックが失敗するようになっているのでslaveへの接続が振り分けられることはない
        server db01 db-server01:3306 weight 100 check port 13307 inter 5000 rise 3 fall 3

        # アプリケーションからの接続は以下の2台に振り分けられる
        server db02 db-server02:3306 weight 100 check port 13307 inter 5000 rise 3 fall 3
        server db03 db-server03:3306 weight 100 check port 13307 inter 5000 rise 3 fall 3

DBホストに常駐しているヘルスチェッカを手動でダウンさせることで、安全にslaveを切り離すことができます。 ホストであるEC2インスタンスのScheduled Reboot対応(つらい)や、不具合発生時の調査などに利用できます。

httpによるヘルスチェックについては以下を参照ください。

master障害発生時

MHA managerが各master候補を監視しています。 この図ではDB01がmaster、02・03はslaveとして稼働しています。

master_failover_1.png

masterで障害が検知されると、MHA nodeによるbinlogの同期が行われます。

master_failover_2.png

binlogの同期が完了すると、ENIが新しいmasterに付け替える処理が行われます。 アプリケーションから見ると接続先は変わりません。

master_failover_3.png

もともとslaveとして稼働していたDB02ですが、 masterとして稼働し始めた時点でHAProxy用のヘルスチェックが失敗するようになり、 前述のslave障害発生時と同様にslaveとしての新規接続は振り分けられなくなります。

ENIの付け替えにはMHA::AWSを使用しています。 MHA::AWSは弊社藤原作のfailover支援ツールです。 詳しくは以下の記事を参照ください。

MHAのmaster_ip_failover_scriptにMHA::AWSが提供するコマンドを設定することで、 failover後にENIが新masterにつけ変わるようになっています。 slaveからmasterに役割が変わることによってHAProxy用のヘルスチェックが失敗するようになり、 slaveへの接続は新masterを除いた各slaveに振り分けられるようになります。 これによりアプリケーションの変更をすること無くmasterデータベースのfailoverを行うことができます。

以下はMHA managerの設定例です。


[server default]
secondary_check_script=masterha_secondary_check -s some_host --user=root --master_host=db-master
master_ip_failover_script=mhaws master_ip_failover --interface_id=eni-123*****
master_ip_online_change_script=mhaws master_ip_online_change --interface_id=eni-123*****
shutdown_script=mhaws shutdown --interface_id=eni-123*****

[server1]
hostname=db-01
candidate_master=1

[server2]
hostname=db-02
candidate_master=1

[server3]
hostname=db-03
candidate_master=1

MHAそのものについてはMHAのドキュメントを参照ください。

自動化最高!

影響範囲の大きい操作が自動化されていると安心感が段違いです。 導入には検証も含めてそれなりのコストがかかりますが、その価値は充分にあります。 便利なツールを提供してくれている作者の方々に感謝 :pray:

次回

Lobiで実際に使用しているgo製アプリ・ツールについて紹介します。