読者です 読者をやめる 読者になる 読者になる

でこてっくろぐ ねお

でこてっくろぐ(http://dekokun.github.io/)から進化しました。でこらいふろぐ(http://dekolife.hatenablog.com/)の姉妹版。デコテックログ(deko tech log)である

Varnishによる一貫性を考慮した積極的キャッシュ戦略実験

この記事は、はてなエンジニアアドベントカレンダー2016の9日目の記事です。昨日は id:cockscomb による Swift 3 の Data とポインタ使いこなし術 - Hatena Developer Blogでした。今日はISUCON 6の予選問題とVarnishを組み合わせてみた話を書きます。

はじめに

みなさんは今年の夏に行われたISUCON6には参加しましたか?私は参加しましたが予選落ちでした。また、最近、私は業務でVarnishというHTTPキャッシュサーバに触ることが多いです。 今回、ISUCON6が終わった後、Varnishで何ができるかの検証がてらISUCON6の予選問題で少しトリッキーな実装をした話を書きます。

実装の方針は以下のとおりです。

  • キャッシュに必要な実装以外は出来る限りアプリケーションには手を入れない
  • 全てのリクエストをキャッシュサーバを通す
  • 更新は即座に反映させつつ出来る限りキャッシュヒット率を高くする

本記事では、どのようにすればその作戦が可能になるのか、実際に行ってみてどうだったかなどをまとめます。

ISUCON6 予選問題について

どのようなアプリケーションかについて詳しくは ISUCON6 予選問題の解説と講評 を読むと分かるのですが、Wikipediaのようなアプリケーションでした。 複数のキーワードとその解説が載っており、キーワードの解説内に別のキーワード名が出現している場合は、そのキーワードへのリンクが自動的に貼られます(以下この記事では"キーワードの自動リンク機能"と呼びます)。 また、ユーザはログインすればキーワードとその解説の投稿が可能であり、更にはてなスターのような、投稿に対してスターをつけるような機能がありました。

ISUCONで最初に用意された実装ではこのキーワードの自動リンク機能が非常に遅く、ここをどのように対処するかが勝負の分かれ道でした。

リポジトリ

運営が用意したリポジトリをforkして以下で作業をしていました。

GitHub - dekokun/isucon6-qualify

今回の記事で扱うのは以下ファイルです。

  • アプリケーションコードはwebapp/go以下
  • キャッシュサーバの設定はwebapp/config/default.vcl

作戦

最初にも書きましたが、以下3点の作戦で進めました

  • キャッシュに必要な実装以外は出来る限りアプリケーションには手を入れない
  • 全てのリクエストをキャッシュサーバを通す
  • 更新は即座に反映させつつ出来る限りキャッシュヒット率を高くする

proxyサーバアプリケーションサーバの間にVarnishを入れ、全ての参照リクエストについてキャッシュが存在すればそれを返し、存在しなければVarnishがアプリケーションサーバにリクエストしてその結果を返します。 なお、この方針ではISUCONのベンチマークの得点は極めて低いです。理由なども以下に記載しています

なぜキャッシュで進めようとするのか

  • 最近仕事でよくVarnishに触れていることもあり、Varnishを活用すると何がどこまで出来るのかを実際に実装して試してみたくなったため
  • 上記に関連して、一度、"参照リクエストをキャッシュしつつキャッシュデータを適切に破棄して更新結果は即座に反映されるようにする"というのをやってみたかったから
    • “いつかはやるぞ"と思いつつ、なかなか始められていなかったところにちょうど手頃そうな規模感なISUCONの予選問題がふってきたから

構成

以下のような3段構成になっています。実際のISUCON予選問題はマイクロサービス化しておりもう少し複雑なのですが、この記事を読む上では以下の構成を認識していれば十分です。ISUCON予選を解いた人のためにもう少し解説すると、今回は説明の単純化のためにIsudaの話しかしません。この記事を読む上ではIsutarやIsupamはなかったこととして考えて頂いて問題ありません。 nginxからのリクエストは全てVarnishを通り、VarnishはリクエストがGETリクエストかつキャッシュがあればそれを返し、それ以外の場合はAppにリクエストを渡すという構成です。

     |
  [nginx]
     |
 [Varnish]
     |
 [App(golang)]

正確にはESIでのVarnishからAppの呼び出しがあるので上記以外のHTTPの流れもあるのですが、そちらについては以下で説明します。

レスポンスをキャッシュさせるとどのような問題が起きるか

動的なサービスにおいてレスポンスをキャッシュさせるには色々と考えなくてはいけないことがあります。今回のISUCON予選のアプリケーションでは以下2つの大きな問題を解決する必要がありました。

  • ページが更新された時にどのように即座に新しいデータを反映させるか?
  • ページ全てをキャッシュできない

以下で問題の詳細及び解決策の案、今回はどのように解決したかについて記述します

ページが更新された時にどのように新しいデータを反映させるか?

ページをキャッシュするということは何もしなければキャッシュ期間中は同じページが出続けることになります。今回のISUCONでは、データの更新が反映されていないと点数が下がってしまう作りになっていたため、キーワードの更新があった場合はそのページのキャッシュを破棄することが必要です。 実際のサービスでも、更新直後に更新対象のページにアクセスした際に更新結果が表示されていないとユーザが違和感を覚える場合が多いかと思います。

更新内容の反映は更新があったキーワードのページのキャッシュを破棄するだけでいいかというとそれだけではありません。 今回のISUCON予選問題では新たにキーワードが投稿されたタイミングで既存のページでもそのキーワード文字列を含むページでは自動的にリンクが貼られる必要があるため、適切にそれらを探し出してキャッシュデータを破棄する必要があります。

ページが更新された時にどのように新しいデータを反映させるか?解決策案

  1. キャッシュ期間を短めにしてデータを更新してもキャッシュは破棄しない
  2. 更新があった際に全キャッシュを破棄する
  3. 更新があった際に更新に関係するページのキャッシュを破棄する

1. については、キャッシュ期間を短くするとダイレクトにキャッシュヒット率が低下するため今回は採用しません。実際のサービスにキャッシュを入れることを考えた場合はこの方式が実装は一番簡単になるので、必要な要件とキャッシュヒット率とを考えた上でこの方式を採用する場合は多いかなと思います。 2. は更新が多いとキャッシュヒット率が極端に低下するため、あまりうれしくはなさそうです。更新頻度の低いサービスであれば採用する余地はありそうです。 3. は、適切にこれを実装出来るのであればこの方式が一番キャッシュヒット率が上がり嬉しそうです。今回はこちらを採用しています。

ページ全てをキャッシュできない

ISUCON予選のアプリケーションにはログイン機能が存在し、ログインすると全ページにおいて"Hello, ○○(ユーザ名)“という表記が出ます。 これにより普通にページ全てをキャッシュしてしまうと"ログインした際に別のユーザ名が表示される"等の問題が起きることになります。 このような問題は一般的なサービスでも、ログイン機能がある場合よく起きるのではないかなと思います。

f:id:dekokun:20161209112806p:plain

ページ全てをキャッシュできない 解決案

  1. ユーザ名の表示したさとキャッシュのしたさとその他の実装の難易度を天秤にかけて各ページのユーザ名は表示しなくて良いことに決める
  2. ユーザ名などはJS等で別途読み込む
  3. Cookieを見てログインしていたらキャッシュを返さない
  4. Cookie毎にキャッシュを保持
  5. VarnishのESIによるテンプレート分割(ISUCON予選問題だと、"Hello, ○○さん" の部分だけ別のテンプレートとして扱い、メインのページでそのテンプレートを呼び出す記述を書きVarnishがそれぞれを合体させる)してそれぞれキャッシュ

1.2. はHTMLの構造が変わってしまいISUCONのベンチマーカのチェックが通らなくなるので今回は採用できません。実際にこの方針が採用できる場合、キャッシュ周りの実装は極めてシンプルになるのでお薦めです。 3.についてはログインユーザにキャッシュが返せずキャッシュヒット率が下がってしまいます。が、全参照ユーザの中でログインユーザの割合の少ないようなサービスでは有効です。 4. は、1ページ1ユーザ毎に1つのキャッシュを用意する必要が出てきて、キャッシュヒット率が下がってしまいます。こちらも 3. と同様にログインユーザの割合が少ない場合や、ログインユーザが非常に高頻度に同じページにアクセスするような状況では有効な手立てになります。 5. については、複雑な構成になってしまい運用に難が出る、アプリケーションサーバへのリクエスト数が増えがちではありますが、とにかく必要な部分を必要な分だけキャッシュするという戦略が取れるため、今回はこの方式を選択しています。

実装

今回、キャッシュサーバとしてVarnishを使用しているため、Varnishではどのような実装で上記戦略を実施することができるのかを解説していきます。

必要な部分でキャッシュを行い、それ以外ではキャッシュを行わない

リクエストの全てがVarnishを通る構成であるため、どのページはキャッシュ可能でどのページはキャッシュしないかをアプリケーションサーバが指示する必要があります。

今回は、キャッシュするかどうかだけ指定したい状態でエンドポイント毎の細かなキャッシュの生存期間制御などは行わず、キャッシュさせたくないエンドポイントではHTTPヘッダに Cache-Control: no-cache を付与し、キャッシュさせたいエンドポイントでは Cache-Control: public, max-age=86400 を付与しています

https://github.com/dekokun/isucon6-qualify/blob/dba81a180ca7b722e45727f11278c19587e9d103/webapp/go/isuda.go#L49-L55

更新時の適切なキャッシュの破棄

更新時のキャッシュの破棄のためには、以下が必要です。

  1. キャッシュに対するタグ付け
  2. データの更新時にタグを指定してのキャッシュの破棄

キャッシュに対するタグ付け

Varnishはアプリケーションサーバからのレスポンスヘッダをタグとして使用することができます。今回は、KEYWORDという名前のHTTPヘッダでそのキーワードのIDを指定しています。

https://github.com/dekokun/isucon6-qualify/blob/dba81a180ca7b722e45727f11278c19587e9d103/webapp/go/isuda.go#L145

   w.Header().Set("KEYWORD", strconv.Itoa(e.ID))

データの更新時にタグを指定してのキャッシュの破棄

データの更新が走った際にタグを指定して関連するページのキャッシュの破棄を行います。 今回は、関連するページはMySQLのFULLTEXT インデックスを使用して取得しています。この記事の本筋とは関係ないため説明は省略しますが、詳しくはこのあたりを見るといいかなと思います。 https://github.com/dekokun/isucon6-qualify/blob/dba81a180ca7b722e45727f11278c19587e9d103/webapp/go/isuda.go#L58

rows, err := db.Query(`select id from entry where match(description) against(?);`, keyword)

FULLTEXT インデックス使用のためのALTER文は以下のとおりです。

alter table entry add fulltext( description ) WITH PARSER ngram;

キャッシュの削除は、VarnishのBans という機能を利用しています。VarnishのVCLに以下のように書いておくと、VarnishにINVALIDATEメソッドでHTTPリクエストを送ることで必要なタグをBANすることができます。

https://github.com/dekokun/isucon6-qualify/blob/dba81a180ca7b722e45727f11278c19587e9d103/webapp/config/default.vcl#L32-L33

  if (req.method == "INVALIDATE") {
ban("obj.http.KEYWORD = " + req.http.X-INVALIDATED-KEYWORD);

例えば、以下のようなcurlリクエストを送ることでで、アプリケーションサーバがレスポンスを返す際に Keyword: 1 というHTTPヘッダを付与していたキャッシュが消えることになります。

curl varnishhost -XINVALIDATE --header 'X-INVALIDATED-KEYWORD: 1'

後は、以下のようにデータの更新時に必要なタグを付与してVarnishにリクエストを送るようにアプリケーションを改修すればOKです。現在はキャッシュの破棄が必要なタグの数だけHTTPリクエストを送っていますが、VarnishのVCLを操作して複数のタグを許容するようにすれば1リクエストで済むと思います(未検証)

https://github.com/dekokun/isucon6-qualify/blob/dba81a180ca7b722e45727f11278c19587e9d103/webapp/go/isuda.go#L72-L78

VarnishのESIによるテンプレート分割

ESI とはEdge Side Includesの略で、VarnishでのESIとは、キャッシュサーバがアプリケーションサーバから返されるデータを読んでその中のタグを解釈し、必要に応じて別のデータを取りに行くような機能のことです。これだけでは意味がわからないと思いますので以下でもう少し簡単に説明します。

ESIの簡単な例

今回のISUCON予選の問題を簡略化して説明します。

/ のアプリケーションサーバのレスポンス

(前略)
    <div class="container">
      <p>Hello <span class="isu-account-name">dekokun</span></p>


<esi:include src="/index_keyword?page=1" /> (<-ここに注目!ここでESIを使用してます)

    </div>
(後略)

/index_keyword?page=1アプリケーションサーバのレスポンス

<article>
  <h1><a href="/keyword/%e3%81%a7%e3%81%93%e3%81%8f%e3%82%93">でこくん</a></h1>
  <div><a href="http://13.78.91.40/keyword/%E3%81%A7%E3%81%93%E3%81%8F%E3%82%93">でこくん</a>は、いつも元気に働いている社会人です。
(後略)

/ のVarnishのレスポンス。まずアプリケーションの/を叩き、その後 /index_keyword?page=1 のキャッシュでesi:includeの部分を埋めて返す。これにより、 Hello dekokun の部分は動的に生成しつつ重いキーワードの自動リンク機能はキャッシュを返すことが可能。

(前略)
    <div class="container">
      <p>Hello <span class="isu-account-name">dekokun</span></p>


<article>
  <h1><a href="/keyword/%e3%81%a7%e3%81%93%e3%81%8f%e3%82%93">でこくん</a></h1>
  <div><a href="http://13.78.91.40/keyword/%E3%81%A7%E3%81%93%E3%81%8F%E3%82%93">でこくん</a>は、いつも元気に働いている社会人です。
(中略)

    </div>
(後略)

ESIについて詳しくは以下をご参照下さい。

Content composition with Edge Side Includes — Varnish version 4.0.4 documentation

ESIの設定

VarnishはデフォルトではESIが有効になっていないので、VCLで以下のように有効にする必要があります。

https://github.com/dekokun/isucon6-qualify/blob/dba81a180ca7b722e45727f11278c19587e9d103/webapp/config/default.vcl#L67

set beresp.do_esi = true;

ISUCON予選問題でのESIの実装

上記例に出したようにユーザによって動的に表示が変わる部分と、キャッシュをしたいキーワード部分を別テンプレートに切り出す作業を行います。

基本的にキャッシュしつつ動的な部分は正確にキャッシュしないようにしたところテンプレートを非常に細かく分割する必要が発生し、ESIで呼び出されたHTMLの中から更にESIで別のHTMLが呼び出されるような多段のESIがいたるところに出現し、最高で4段のネストしたESI呼び出しが発生しました。

以下の赤枠で囲った1つ1つが、テンプレートを分割してESIでVarnishから呼び出されている部分になります。つまり、Varnishへの1リクエストが4リクエストに増幅しているわけで、今回のISUCON問題のように、"とてつもなく重い機能(キーワードの自動リンク機能)を徹底的にキャッシュしたい。そのためには同時リクエスト数は増えても構わない" という場合以外はこの方式はむしろ性能が劣化する原因になるので注意です。

f:id:dekokun:20161209114759p:plain

結果

上記の対応を行うことで全てのページにおいて一番重いキーワード生成部分は完全にキャッシュされ、データの更新時には必要なキャッシュだけ破棄されるような環境は整ったのですが、如何せんそもそもキーワードの自動リンク機能部分のキャッシュの生成にかかるコストが高すぎて、"キャッシュに必要な実装以外は出来る限りアプリケーションには手を入れずに" という制約の上ではISUCONのベンチマークをかけても全くスコアが出ませんでした。 ISUCONのベンチマーカーはPOSTリクエストの割合が高く、キャッシュがかなり頻繁に破棄される状態だというのも点数が低い原因になっています。

今後行いたいこと

以下行いたいですね。

  • ISUCONアプリケーションをFastlyで配信する
    • VarnishからFastlyに載せ替える みたいなブログを書きたい
    • 実際は既に配信はできる状態になっているのですが、ここに書くには余白が足りない
  • アプリケーションに手を入れてキーワード生成部分を高速化し、どれくらいの点数が出るのか見てみる
  • 今回はデータの更新で即座にキャッシュを破棄していたため、その次にアクセスするユーザには長時間待たせることになってしまっていたが、、Varnishに古いデータを返させつつアプリケーションサーバにリクエストを送り裏でキャッシュを生成させるような実装にしたい
    • その場合は新しいキャッシュができるまで古いデータが返ることになるのでISUCONのベンチマークツールにはエラーとして認識されそうではある

今回使ったVarnishの各機能に対する感想

キャッシュにタグ付けを行いタグ指定でのPurge

非常に夢がある機能です。これによって動的なアプリケーションでも出来る限りVarnishにキャッシュさせつつ更新は即座に反映することができます。タグ付けを適切に行えば、更新データに関連するキャッシュを全て綺麗に瞬時に消し去ることができます。

ESI

ESIによって実現される、"ページの一部だけキャッシュされて残りはキャッシュされていない"というような状況は、ただでさえ人間の頭に負担になる"キャッシュ"というものが更に複雑なものになってわけがわからなくなるなと強く感じました。基本的には使うべき機能ではないと感じます。開発時にVarnishがないとアプリケーションが動かないという状況にもなり、動作確認が行いづらくそちらでもあまり好ましくないなと感じます。 ESIを使うのと同等のことは、JSでAPIを叩いてDOM書き換えを行うことでできる(ただし、ESIと比較してブラウザからアプリケーションサーバまでのレイテンシは余計にかかりますが…)ので、そちらを採用したほうがアプリケーションを書く側としても一般的で分かりやすく良さそうです。

最後に

ここ1年ほど仕事でVarnishを触ることが多く、VarnishのタグPurge機能やESL機能に触れれば触れるほど"これなら全ての参照リクエストへのレスポンスをキャッシュさせつつリアルタイムなアプリケーションが作れる!そういうコンセプト実装をしたいなぁ"と感じていたところにちょうどまさにそういう構成が力を発揮しそうなアプリケーションがISUCONという形で与えられたため、是非やってみようと思ってやってみました。 実際にやってみたところで、そのような構成が実際に可能なこと、ELSの難解さやタグPurgeの強力さを肌で感じることができ、とても良い経験になったなと思います。

最近はFastly等のキャッシュPurgeが高速なCDNも登場しており、全てをCDNにキャッシュさせるような動的ページも作っていきたいなと夢が広がりますね。

明日のアドベントカレンダーid:Windymelt です!!!

最近AWSのENIの付け替え速度がチョッパヤになってる気がしたので測ってみた

AWSにおいてサーバの冗長構成をとるための様々な手法の中の一つとして、ENIを移動させることでIPを移動させる方法もあるかと思います。

そのENIの付け替えにかかる時間が最近異様に素早くなってきている気がしたので、すごく雑に測ってみました。及び素早く付け外しするコツも記載してあります。

手動でENIを付け替えたバージョンと、grabeniでENIを付け替えたバージョンの両方があります。

なお、grabeniとは、ちょうど今日書かれた以下エントリにも登場している、go製のENI操作CLIツールです。keepalivedとgrabeniを組み合わせて冗長構成をとるなど、最近便利に使ってます。 developer.hatenastaff.com

結論

  • 手でもツールを使っても、30秒以内くらいでENIを付け替えられそうな感じ
  • 昔はもっと時間がかかっていた気がするけど昔のデータはないので比較できない。次回に期待
    • 昔は2〜3分かかってた気がするという証言が得られた
  • grabeni便利

測り方や環境等

  • pingを打ちながらENIを外して別サーバにつけ、pingが失敗していた秒数を測る
  • pingが疎通していない時間の長さで時間を測る
  • 「だいたい何秒くらい」という情報がわかっていればそれである程度十分だし、特に手でつけたり外したりするの結構面倒なので、それぞれ1回勝負。平均をとったりはしない
  • allow-hotplugの設定をしているため、ENIを付与するだけで勝手にインターフェースが上がってくる環境

手動バージョンの測定

26秒です。

AWSのマネジメントコンソールから手でつけ外しした場合。 EC2サーバが大量に存在する環境にて素早く付け外しするコツがある。そのコツとは、外したENIを付与する際は、ENIからattachするEC2サーバを探すのではなく(サーバが多い環境だと大量のEC2サーバがリストに表れて非常に探しづらい)、attachするEC2サーバにて"ネットワーキング" -> "ネットワークインターフェース" の順で操作することによって、現在使われていないENI(大抵EC2サーバの数より少ないのではないかと思っているがどうでしょうか)から選べば良くなるため、圧倒的に早く済むというものである。

pingの状況

64 bytes from xx.xx.xx.xx: icmp_seq=98 ttl=59 time=xxxx ms
Request timeout for icmp_seq 99
Request timeout for icmp_seq 100
Request timeout for icmp_seq 101
Request timeout for icmp_seq 102
Request timeout for icmp_seq 103
Request timeout for icmp_seq 104
Request timeout for icmp_seq 105
Request timeout for icmp_seq 106
Request timeout for icmp_seq 107
Request timeout for icmp_seq 108
Request timeout for icmp_seq 109
Request timeout for icmp_seq 110
Request timeout for icmp_seq 111
Request timeout for icmp_seq 112
Request timeout for icmp_seq 113
Request timeout for icmp_seq 114
Request timeout for icmp_seq 115
Request timeout for icmp_seq 116
Request timeout for icmp_seq 117
Request timeout for icmp_seq 118
Request timeout for icmp_seq 119
Request timeout for icmp_seq 120
Request timeout for icmp_seq 121
Request timeout for icmp_seq 122
Request timeout for icmp_seq 123
Request timeout for icmp_seq 124
64 bytes from xx.xx.xx.xx: icmp_seq=125 ttl=59 time=xxxx ms

grabeni使用バージョンの測定

18秒。

1回勝負の測定結果なので、優位な差があるかと言われるとそんなにないと思うが、まぁ、手でやるよりはやはり早いよねという結果になった。

なお、その時に打ったgrabeniコマンドは以下のとおりである。ENIを付与したいサーバ上で以下1コマンドを叩くだけでよく大変便利。オプションの-i 1の部分が重要で、grabeniはループ内でiオプションの秒数だけsleepしながらENIのdetachを待ち、detachが完了したらattachするという動きになっているため、この数を減らすことによって、ENIの付け替えを高速化させることができる。デフォルト値は2となっているが、私はいつも1で使っている。

$ grabeni grab -d 1 -i 1 -n 40 eni-xxxxxxxxx

pingの状況

64 bytes from xx.xx.xx.xx: icmp_seq=10 ttl=59 time=xxxx ms
Request timeout for icmp_seq 11
Request timeout for icmp_seq 12
Request timeout for icmp_seq 13
Request timeout for icmp_seq 14
Request timeout for icmp_seq 15
Request timeout for icmp_seq 16
Request timeout for icmp_seq 17
Request timeout for icmp_seq 18
Request timeout for icmp_seq 19
Request timeout for icmp_seq 20
Request timeout for icmp_seq 21
Request timeout for icmp_seq 22
Request timeout for icmp_seq 23
Request timeout for icmp_seq 24
Request timeout for icmp_seq 25
Request timeout for icmp_seq 26
Request timeout for icmp_seq 27
Request timeout for icmp_seq 28
64 bytes from xx.xx.xx.xx: icmp_seq=29 ttl=59 time=xxxx ms

感想

  • 昔はもっと遅かった気がするけどどうだっけなぁ。
  • grabeni便利。
  • この前やった時は10秒くらいで出来た気がしたんだけど勘違いかなぁ。当時のログが残っていない。

nginxでproxy_hide_header, proxy_set_header, add_headerを書く時にはまりがちな罠

reverse proxyサーバとしてよく使われているnginxですが、意外にハマりやすい罠があったりします。

今回は、proxy_hide_header, proxy_set_header, add_header等で設定内容を複数コンテキストに分割する際にはまりがちな点を紹介します。

概要

  • 上記3つのディレクティブについては、基本的には現在のコンテキストにそのディレクティブがない時に限って上位のコンテキストで設定された値が継承される
    • 他にもaccess_logディレクティブなどでも同様の動きをするようだが、まだどのディレクティブがそのように動くかの一覧は出せていない

どのようなときにハマるのかストーリー

上記説明だと、具体的に何がどうなるのかがわかりづらいため、ストーリー仕立てで説明します。 nginx - app のように、リバースプロキシの後ろにアプリケーションサーバがいるような一般的な形を考えます。

このアプリケーションサーバには、nginxでHTTPヘッダを付与する必要があったとします。今回のストーリでは以下のようにX-HogeとX-Fugaヘッダを付与します。

server {
    listen       80;
    server_name  nginx;

    proxy_set_header X-Hoge hogevalue;
    proxy_set_header X-Fuga fugavalue;
    location / {
        proxy_pass http://app;
    }
}

さて、この状態は特に問題ありません。アプリケーションサーバでX-HogeとX-Fugaの値をロギングするようにした状態でnginxにアクセスを行うと、ログから以下のように正しく値がセットされていることが分かります。

X-Hoge:hogevalue        X-Fuga:fugavalue

ここで、やんごとなき事情によって/test/以下へのアクセスのみ、X-Hoge, X-Fuga以外に、X-Piyoヘッダも付与する必要が出てきたとします。直感的に以下のように設定を変更すれば良さそうな感じがします

server {
    listen       80;
    server_name  nginx;

    proxy_set_header X-Hoge hogevalue;
    proxy_set_header X-Fuga fugavalue;
    location / {
        proxy_pass http://app;
    }
    location /test/ {
        proxy_set_header X-Piyo piyovalue; # これで大丈夫そう!
        proxy_pass http://app;
    }
}

うまく動きそうですね。/test/にきたら、X-HogeもX-FugaもX-Piyoも付与されていることが期待できます。では、/test/以下にアクセスしてみます。

$ curl http://nginx/test/index.html

ログは以下の通り

X-Hoge:-        X-Fuga:-        X-Piyo:piyovalue

…X-HogeとX-Fugaが設定されていませんね…

この場合は、以下のように必要な設定を全部入れてあげる必要があります。

server {
    listen       80;
    server_name  nginx;

    proxy_set_header X-Hoge hogevalue;
    proxy_set_header X-Fuga fugavalue;
    location / {
        proxy_pass http://app;
    }
    location /test/ {
        proxy_set_header X-Hoge hogevalue; # ここにも書く
        proxy_set_header X-Fuga fugavalue; # ここにも書く
        proxy_set_header X-Piyo piyovalue;
        proxy_pass http://app;
    }
}

そうすると当然ながら以下のように必要なヘッダが全て渡っていることが分かります。

X-Hoge:hogevalue        X-Fuga:fugavalue        X-Piyo:piyovalue

同様の動きをする他のヘッダ達

上記ストーリーではproxy_set_headerを例に出しましたが、proxy_hide_header, add_header等他にも同様の動きをするディレクティブが存在します(が、詳しくは追えていない。ドキュメントには一覧はなさそう?)。 以下にも記載しましたが、ディレクティブによっては、そのディレクティブの説明でこのような動きをする旨記載してあるものもあります。

ドキュメント上ではどのように扱われているのか

proxy_set_headerやadd_headerのドキュメントには、以下のような記述がありました。

These directives are inherited from the previous level if and only if there are no proxy_set_header directives defined on the current level.

Module ngx_http_proxy_module

ただ、proxy_hide_headerのドキュメントにはなさそう?

more_set_headersはまた違う動きをする

openrestyのheaders-more-nginx-moduleを使うと、add_headerをもう少し強力にしたようなディレクティブとしてmore_set_headersを使うことができるようになります。 add_headerと同様にクライアントに返すHTTPヘッダを追加することができるディレクティブなのですが、add_headerとの違いは、add_headerは既に存在するHTTPヘッダを上書きすることはできませんが、more_set_headersを使うと上書きすることができます。

ただ、それ以外に、add_headersとは、この記事で書いている挙動をするかしないかという点で、異なる動きをします。

以下で、実際にどのように異なる動きをするのかを解説します。

まず、以下のようにadd_headerとmore_set_headersでそれぞれヘッダを付与していたとします。

server {
    listen       80;
    server_name  nginx;

    add_header X-Add-Header-1 add-header-1;
    add_header X-Add-Header-2 add-header-2;
    more_set_headers 'X-More-Set-Headers-1: more-set-headers-1';
    more_set_headers 'X-More-Set-Headers-2: more-set-headers-2';

    location / {
        proxy_pass http://app;
    }
    location /test/ {
        proxy_pass http://app;
    }
}

curlでリクエストを投げると以下のようにどちらで設定したヘッダも出力されています(なお、curlのオプションの-Iというのはヘッダを出力するオプションです。ただ、-Iを付与するとGETではなくHEADリクエストになってしまうのでそれを阻止するために-XGETオプションも一緒に指定しています。)

$ curl -IXGET http://nginx/test/index.html
HTTP/1.1 200 OK
Server: openresty/1.9.7.4
Date: Sun, 22 May 2016 13:38:14 GMT
Content-Type: text/html
Content-Length: 12
Connection: keep-alive
Last-Modified: Sun, 22 May 2016 12:52:29 GMT
ETag: "5741ab8d-c"
Accept-Ranges: bytes
X-More-Set-Headers-1: more-set-headers-1
X-More-Set-Headers-2: more-set-headers-2
X-Add-Header-1: add-header-1
X-Add-Header-2: add-header-2

ここで、以下のように、/test/以下にアクセスがあった場合にさらにいろいろヘッダを付与するような設定をしたとします。

server {
    listen       80;
    server_name  nginx;

    add_header X-Add-Header-1 add-header-1;
    add_header X-Add-Header-2 add-header-2;
    more_set_headers 'X-More-Set-Headers-1: more-set-headers-1';
    more_set_headers 'X-More-Set-Headers-2: more-set-headers-2';

    location / {
        proxy_pass http://app;
    }
    location /test/ {
        proxy_pass http://app;
        add_header X-Add-Header-3 add-Header-3;
        more_set_headers 'X-More-Set-Headers-3: more-set-headers-3';
    }
}

すると、curlの結果は、以下のように、more_set_headersで付与したHTTPヘッダはそのまま上位のコンテキストで設定されたものに下位のコンテキストで設定した結果を付け足していくような動きになっていますが、add_headerは、上記で説明したproxy_set_headerと同様に、上位のコンテキストで設定したHTTPヘッダは付与されていません。難しいですね。確実に、両方同時に使わない方が良いです。

$ curl -IGET http://nginx/test/index.html
HTTP/1.1 200 OK
Server: openresty/1.9.7.4
Date: Sun, 22 May 2016 13:40:47 GMT
Content-Type: text/html
Content-Length: 12
Connection: keep-alive
Last-Modified: Sun, 22 May 2016 12:52:29 GMT
ETag: "5741ab8d-c"
Accept-Ranges: bytes
X-More-Set-Headers-1: more-set-headers-1
X-More-Set-Headers-2: more-set-headers-2
X-More-Set-Headers-3: more-set-headers-3
X-Add-Header-3: add-Header-3

あるディレクティブがそのように動くかどうかどのように調べればいいか

ドキュメントを読む、実際に試験をしてみる、実装を読むしかなさそうです。 まだ実装を読んでないですがまた読んだらブログにまとめたいです。

あとがき

最近nginxをよく触っているのですが、ここで紹介したproxy_hide_header, proxy_set_header, add_headerの罠に私は既に2回ハマっております。恥ずかしい。また周りでもちょくちょくハマっている姿が見られるため、そのような不幸な人が1人でも減ることを願って、この記事を執筆しました。

よいnginxライフを!

この記事によって今後書きたくなった記事

  • nginxのドキュメントへのコントリビュート
    • hgでコントリビュートするらしいというところまで見て今回は土日が終わってタイムアウト
  • ディレクティブの実装がどのような場合に上記のような動きをするがについてまとめる

技術ブログを月に1回書くことにした

最近技術ブログを書いてないな、書きたいな。しかし実際は書いてないなという思いが強くて、じゃあどうすればいいんだと思いました。

以前はなぜ書いていたのかと考えたら、自分のブログの"DevLOVE Conference 2012 に参加した後、これからの私にとって最も大切なこと"で「ブログを書く(頻度とかを考えてなかった。。最低、月1回くらいでかな)」という宣言をしていたからだ!と思い至り、今回も宣言してみることにしました。

(なお、前回宣言時は2年間毎月ブログを書き続けられました。その時は諸事情により途絶えてしまいましたが、今回はもっと続けたいですね!)

はてなに入社して経験したMySQL4系のオンラインでのmaster切り替え

このエントリははてなエンジニアアドベントカレンダー2015の12日目です。

developer.hatenastaff.com

昨日は id:daiksy によるこちらのエントリでした。

daiksy.hatenablog.jp

現在、私は東京のオフィスで働いているのですが、京都のエンジニアと共に仕事をする場面が多いです。しかし、上記エントリに記載されている様々な取り組みのおかげで遠隔地でもかなり働きやすい環境だなというのを感じています。これからもより皆が働きやすい環境になるように私もいろいろと工夫をしていけたらなと思います。

このエントリでは、今年の8月にはてなに入社した私が最近経験したMySQL4系のmaster切り替え手法について説明します。

背景・概要

はてなの運用するWebサービスではデータストアとしてMySQLが使われる場面が多いです。それにより、入社してから私もMySQLのmasterサーバを切り替える必要があるようなメンテナンスを何度か行いました。

masterサーバを切り替えるようなメンテナンスを行う理由としては様々なものが考えられます。例えば

  • サーバのディスク使用量が増えてきておりこのままでは近日ディスク使用量が100%になってしまいサービスが停止してしまう(からディスク容量の多い別のサーバに切り替えよう)
  • サービスの成長に伴いサーバのCPU使用率が高まってきており放っておくとそのうちシステムが高負荷に陥ってサービスに影響が出てしまう(から性能の良い別のサーバに切り替えよう)

などがパッと思いつくのではないでしょうか

もちろん、上に挙げた問題への対策はサーバの切り替え以外にもいろいろ考えられます。ディスク使用量が多ければ、不要なデータを消す、データを圧縮するなどで対処することが可能な場合がありますし、CPU使用率が上がっている場合はインデックスやクエリ、キャッシュ戦略を見直すなどにより対処することが可能な場合もあります。 しかしそれらの手を打ったとしても、どうしてもサーバを切り替える必要が出てくる場合もあります。また、話は変わりますが、サーバを切り替えるためにはサービスを止めてDBへのアクセスがない状態にして切り替えるのが最も安全ではありますが、可能であればサービスを止めない状態のまま切り替えたいですよね。

このエントリでは、MySQLにおいて、サービスを止めずにmasterサーバを手動で別のサーバへ切り替える作業について説明します。

特に、今回ははてなで行われている「MySQL4」系のmaster切り替え手法、その中でもMySQL4系特有の「masterサーバを切り替える際にどのように旧masterのslaveサーバ群を新masterのslaveにするか」に重点を置いて説明していきます。

MySQL5.0が世に出てからもう10年になります。今ではMySQLのバージョンに関する世間の耳目は5.6や5.7、もしくは、MySQLではないですが互換性のあるDBとしてAmazon AuroraやMariaDBに集まっていますね。そんな中では、MySQL4系を運用している人はかなり少ないのではないかと思います。 はてなでも古いバージョンのMySQLは新しいバージョンのMySQLに切り替えていっているところです。しかし、まだ一部でMySQL4系も使われています。先日私ははてなのサービス運用を行っている中で稀にあるMySQL4系のmasterサーバの切り替えを、人生において初めて行いました。そこで行った作業内容が比較的面白い感じでしたので、その内容をお伝えしたいと思いこの記事を書いています。

具体的にどのように面白いと感じたかと言いますと、以下のようなものが挙げられます。

  • 昔の人はこんなことをやったりしたのか、という歴史を体験するような面白さ
  • MySQLの管理関連のツールの充実していないMySQL4系においてツールを使わずに作業することによって、改めてMHAやmk-slave-moveなどのツールが内部で何を行っているのかについて実感することができた
  • こんな職人芸っぽい作業をしたぞ、という自己満足感
    • これは本来はエンジニアとしてはよくない感情ですね。もっと如何に楽するか、職人芸を撲滅するかを考えなくては。とは思うのですが、一方で、「こんな変なことやってやったぜ」のような喜びはやはりあるなぁと思っています

ですので、読む皆様も上記のような気持ちを感じながら読むと、「俺はMySQL4系なんか触ったこともないし触る予定もない」という人も楽しく読むことができるのではないかと思います。

MySQL5系のmasterサーバの切り替えに関しては、最後におまけとしてほんの少しだけ(本当にほんの少しだけですが)載せてありますのでそちらもご参照ください。

前提知識としてのはてなでのMySQLの冗長構成と、本ブログ内での用語

はてなではMySQLのmasterサーバは以下の図にありますように相互レプリケーションによるactive-standby構成で運用されています。以下図では、太い矢印の向きがレプリケーションにおけるbinlogの流れを表しています。また、以下では「active master」が、現在サービスに使用されているmasterサーバを表し、「standby master」が、active masterに障害が起きた際/もしくは手動でサーバを切り替える際の切り替え役として待機しているサーバを表すこととします。

このブログ内で「master切り替え」といった場合は、active masterとstandby masterを切り替える(旧active masterをstandby masterとしてサービスから外し、旧standby masterをactive masterとしてサービスから外す)ことを表します。

f:id:dekokun:20151210232746p:plain

master切り替えのステップ

さて、手動でmaster切り替えを行うためには、以下2つのステップを実行する必要があります。

  1. 旧active masterにぶら下がっているslaveを新active masterにぶら下げる f:id:dekokun:20151211012501p:plain
  2. Webサーバからの接続を旧active masterから新active masterに向ける f:id:dekokun:20151211012629p:plain

"1."がこのエントリの最大のテーマとなっています。 MySQL5系ですと、MHAという便利なツールがあり、そちらに任せてしまえば、設定さえちゃんとしていれば上記"1.", "2."を機械がよしなに実施してくれるので大変ありがたいのですが、MHAはMySQL5.0以上しか対応していません。

"2." については、一般的にはなんらかの形でDB接続に使用されているIPアドレスを旧active masterサーバから新active masterサーバに付け替える、もしくはDB接続の際にDNSを使用して接続先IPを特定している場合はDNSレコードの書き換えによって接続先のIPを変更する、などの方法でWebサーバからのアクセス先を切り替えている場合が多いかと思います。 はてなでもkeepalivedによるIP付与及びgarpの送信や、(AWSであれば)ENIの付け替えによって、Webサーバからの接続に使用しているIPを新active masterに付与することでWebサーバからの接続を新active masterに向けるようにしています。切り替えサーバが別のネットワークに所属している場合はまた別の方法で…などもありますが、こちらにつきましては、MySQL5でも4でも特に行っていることは変わらないです。詳細は割愛させていただきます。

それでは、実際にMySQL4系において"1."で行うべきであることを知るべく、以下で"1."を更に細かいステップに分けていきましょう。

旧active masterにぶら下がっているslaveを新active masterにぶら下げるステップ

MySQLにおいて、MySQL5.6に搭載されるGTIDを使えない場合、slaveを別のmasterに付け替える際は、slaveの現在のbinlogのポジションが新masterではどのポジションに対応するかを把握してそのポジションに対してCHANGE MASTER TOを打つ必要があります。 実際に行う必要があるのは以下です。

  1. active masterの更新をロックする
  2. 切り替え対象の全slave、及び新active masterの、レプリケーションが最後に実行したクエリのactive master上のbinlogのポジション(説明が難しいですね…Exec_Master_Log_Posのことです)がactive masterの現在のbinlogのポジションに到達するのを待つ
  3. 新active masterの現在のbinlogのポジションを把握する
  4. 切り替え対象の全slaveにて、レプリケーションを止めた後に"3."で把握したポジションへCHANGE MASTER TOを使って切り替え、レプリケーションを再開する
  5. active masterの更新ロックを解除する

うーん複雑ですね。ただ複雑であるだけならいいのですが、active masterの更新をロックしているがために、上記を高速で実施する必要があるというのがまた難しいところです。

これらを自動的に行ってくれているMHAのありがたさが身にしみます。

旧active masterにぶら下がっているslaveを新active masterにぶら下げるステップ(更新ロックを行わないver)

私が作業を行った際は上記の手順で行ったのですが、後から更新ロックが不要な手順が存在すると発覚したのでした。。。

まだ私はこの更新ロックを行わない手順ではslaveの切り替えを行ったことはないのですが、参考までにステップを書いておきます。

イデアとしては、上記更新ロックを行う方法では「切り替え対象のslaveの現在のポジションが新active masterのどのポジションに対応するかが分からないために、active masterの更新を止めることで新active masterと切り替え対象のslaveの更新を止めて、現在のslaveのポジションに対しての新active masterのポジションを把握する」という考えだったのですが、更新ロックを行わない方法は、「切り替え対象のslaveのレプリケーションを止め、mysqlbinlogコマンドなどを使用して最後のクエリをbinlogから特定した後に、新active masterのbinlogからそのクエリに対応するクエリのポジションを把握する」という考えです。

  1. 切り替え対象のslaveのレプリケーションを止める
  2. mysqlbinlogコマンドで最後のクエリを特定する
  3. 新active masterにて、上記切り替え対象のslaveの最後のクエリに対応するポジションをmysqlbinlogコマンドから特定する
  4. 上記ポジションに対してCHANGE MASTER TOを使って切り替え、Slaveを再開する

この手順ですと、基本的にslaveは1台ずつ操作していく形になり、更に確実に操作対象のslaveが遅延するために対象のslaveをサービスから外しながら実施する必要がありその分時間はかかりますが、masterサーバの更新を止めずに済むというのは非常に大きいため、なるべくこちらの手順で行ったほうが良さそうでしたね…次回から気をつけます…

手順

では、(更新ロックを行う方の)コマンド等を書いた手順のお披露目です。職人芸が必要となるっぽい感じがしますね。私が実際に行った手順ははてなお手製スクリプトによってもう少しだけ自動化されてはいるのですが、ほぼ以下のとおりです。

前準備

tmux-cssh などを使用し、複数のサーバに同時にコマンドを投げられるような環境を作っておく

active masterの更新をロック

mysql> FLUSH TABLES WITH READ LOCK;

切り替え対象のslaveのExec_Master_Log_Posがactive masterの現在のbinlogのポジションに到達するのを待つ

切り替え対象の全slave、及び新active masterにて以下を打ち、Exec_Master_Log_Posを確認

mysql> SHOW SLAVE STATUS\G

active masterにて以下を打ってPositionを確認

mysql> SHOW MASTER STATUS\G

両者が一致するまで待つ

新active masterの現在のポジションを把握する

新active masterにて

mysql> SHOW MASTER STATUS\G

ここで、FileとPositionからCHANGE MASTER TO構文を構築する

CHANGE MASTER TO master_host='xx.xx.xx.xx', master_user='xxx', master_log_file='xxxxxxxx-bin.xx', master_log_pos=x;

切り替え対象の全slaveにて、レプリケーションを止めた後に"3."で把握したポジションへCHANGE MATER TOを使って切り替え、レプリケーションを再開する

切り替え対象の全slaveにて、以下を実施

mysql> STOP SLAVE;
mysql> CHANGE MASTER TO master_host='xx.xx.xx.xx', master_user='xxx', master_log_file='xxxxxxxx-bin.xx', master_log_pos=x;
mysql> START SLAVE;
mysql> SHOW SLAVE STATUS\G     // slaveがちゃんと動いているか確認

active masterの更新を解除

mysql> UNLOCK TABLES;

重要なのは、上記を出来る限り素早く(可能であれば5秒以内くらいで)行うことです。

はい、大変ですね!ちなみに、上記手順以外でも細かいことをいうと新active masterでFLUSH LOGSを打つことによってCHANGE MASTER TOを事前に用意しておいたりできるようにしたりという小技もあるのですが、まぁだいたい上記のような感じのことを先日行ったという話でした。

練習なども行った後に実際に作業を行ってみると結構さくっとできるようになったのですが、もうあまり行いたくない類の作業ですね。

感想など

「背景・概要」のところにも記載してありますが、このエントリは、「先日行ったMySQL4系のmaster切り替えの手順が面白かったから共有しよう」という思いの元に書かれたもので、実際に皆様のMySQLの運用にはほぼ立たないかと思いますが、楽しんでいただけましたでしょうか。もし他にもMySQL4系の運用を行っている方がいらっしゃいましたらお友達になりたいなぁと思います。

このエントリを書きながら「この作業は自動化しなくてはあかんやつや」という思いを受けもしましたが、そもそもはてなにおいて現在MySQL4系はだいぶ減ってきているため、自動化も大切ですが、とにかくMySQL4系を撲滅することに力を割かなくてはいけないなというのを強く感じました。 また、このような作業を行うとMHAなどの自動化ツールの重要性が改めてよく分かるなというのは感じました。

本題とは関係ない話なのですが、この記事を書くことによって、これまでなんとなくしか分かっていなかったことが明確になり、非常に勉強になったなというのも感じます。私は前職でアプリケーションエンジニアだったのですがはてなでインフラエンジニアになり、知識が足りないところが非常に多いのですが、そんな私こそブログにいろいろ書いていくことで学んでいかなくてはならないということもまた強く思いました。

おまけ MySQL5系におけるmaster切り替え手法

slaveの付け替えにつきましては、上記でも少し単語を出していますがMHA (MySQL5以上対応)を使うのが最も良いのではないでしょうか。

また、MySQL5系のslaveの付け替えについては私はmk-slave-moveというコマンドをいつも使っており皆さんも使うといいと思います。mk-slave-moveはIt was retired because it didn't work consistently.ということでPercona Toolkitに含まれていないのが非常に残念です。なお、このブログを書くまで私は「mk-slave-moveは、内部でSTART SLAVE UNTIL という構文を使用しており、MySQL5以上でないと使えない」と思っていたのですが、START SLAVE UNTIL構文はMySQL4.1.1から対応されているようでして、もしかしたらMySQL4.1.1以降でしたらmk-slave-moveを使うことができるかもしれません。そちらはまだ検証を行ってはいないため、どなたか使ったことのある方などいらっしゃいましたら教えていただけたらと思います。

他にも、上記に少し書きましたようにMySQL 5.6の場合はGTIDを使用しても楽にslaveの付け替えを行うことができるという話は聞いていますが、私はGTIDについては詳しくないので今後の研究課題としておきます。

最後に

はてなでは、様々なversionのミドルウェアを触ることが大好き(だけどなるべく新しいバージョンにしていきたいという思いを持つ)メンバーを募集しています。

hatenacorp.jp

アドベントカレンダーですが、明日はid:hatz48 です!どんな内容になるのか楽しみです!皆様もお楽しみに!!!!

PHP使いとScala使いとPerl使いが集まって #ISUCON 予選敗北した話と雑な決勝内容予想

タイトルの通りで、PHP使いとScala使いとPerl使いが集まって死力を尽くしたのですがISUCONに敗北したのでした。ISUCON当日までの流れと当日の反省点などを記録していきたいと思います。特に技術的内容はない。

 

ついでに、最後に雑な決勝の予想を載せておきました。

 

ブログ書かねばと言いながら延び延びになっていていつのまにかISUCON決勝が明日に迫っていたので慌てて公開。


チームビルディング

かれこれ2年連続PHPでISUCONに出場し、2年連続で「優勝してPHPの優秀さを証明してみせる」と言っては決勝までは進むけれども決勝では惨敗する というのがいつものパターンだった私ですが、今年は夏に、はてなという名前のPHPのピの字もないPerlScalaの会社にインフラ担当として転職しておりまして、「はてなの社員とISUCONに出た場合、PerlScalaになるだろうしPHPの優秀さは示せそうにないのが極めて残念ではあるが、 それはそれとして今年こそ優勝するぞ」と強い意気込みをもって過ごしていたのでした。

そして優勝するために社内のいろいろな人に声をかけつつチームビルディングを行っていたのですがそこで誤算発生。なんと、ISUCON決勝のある10/31は私の大学時代の友人の結婚式の日でありまして、 皆がISUCON決勝を頑張っている中、私ははるか遠く九州まで出掛けていることが発覚したのでした。

流石に決勝に参加できないのは申し訳ないということで当時声をかけていたメンバーには一度チーム解散宣言をしたうえで、「決勝には出られないのですがISUCON予選だけでも出たいです。 誰か一緒に参加していただけませんか」と弱気に社内で呼びかけていたところ、超若手が参加してくれることになりました。彼は社内でScalaを書いており ました。私はPHP使いながらもScalaJVMは学ばなくてはいけないと思っていたので、「よしScalaで予選を戦おう」と決めたのは大変自然な流 れだったのでした。そして、更に「Scalaで戦うのですがScala使いでもScalaを勉強したい人でも、あと1人メンバー募集中です。」と社内で声をかけたところ、若手のホープ新卒2人目、Perl使いが手を挙げてくれまして、無事にISUCON予選には3人チームで参加できることになったのでした。

このような流れで、PHP使いとScala使いとPerl使いが1同に会すというドリームメンバーでISUCONを戦うことになったのでした。

決勝に出られない私がチームを組めたというのは大変ありがたく、嬉しく思った次第で「(私は参加できないけど)若い2人に絶対に決勝を体験してもらいたい」と強く決意した次第でした。

なお、インフラ担当は私であとの2人はアプリケーションエンジニアとなっております。また、前々から「ISUCONで若者潰す」みたいなことを言っていたのですが、超若い2人とチームを組んだことにより平均年齢が劇的に下がったため方針を転換し「おっさん潰す」という感じで頑張ることにしました。

 

なお、1人は京都から参加で残り2人は東京から参加ということで、遠隔でやってました。


チーム名決め


チーム名は大切です。これによって当日のテンションがかなり変わってきます。


scala祭り」「常勝」「殺戮摂理(ラジカルサーカス)」「二重言語(ヒステリックリボルバー)」など色々案を出しましたが、結局参加者各位のIDを混ぜたようなチーム名に落ち着きました。


事前準備


Scalaで行く」と言ったもののインフラ担当の私がJVMの知識が全くないこともあり、必死でJVMの勉強を行いました。具体的には、社内でJVMのインフラを見ている方に「JVM勉強したいんですがいい本ないっすか」と聞いて教えてもらえた「Javaパフォーマンス」をひたすら読んでいました。その結果、JavaGCについては結構詳しくなった感じがしました。

 

O'Reilly Japan - Javaパフォーマンス



また、最初に設定したいカーネルパラメータの値や入れておきたいツールについてまとめたりをしていました。


当日開始前、再度の使用言語決め


私たちはISUCONの2日目に参加したのですが「Twitterを見ている限り、ISUCON1日目のPHP実装は何か変なことがあったっぽい」「ISUCON1日目はNode実装がなかったらしい」という情報を見て、「Web業界ではそこまでメジャーではないScala実装も何かしら問題がある可能性がある、最悪実装自体が存在しない可能性がある」という結論に達し、もしscalaがダメだった場合の使用言語の選定方法を決めることにしたのでした。

そこで再度3人の使用可能言語などを調査したところ、PHP, Scala, Perlの超得意言語の他にも「3人ともJSは書ける」「@stefafafanはPHPを読んだことがある」「goなら3人とも大丈夫?」などという話が出たのですが、結果として、「特に何事もなければScalaでいく」「もしScalaに何かあった場合は@stefafafanがまだ読め、私が超得意なPHPでいく」という結論になったのでした。

今思えばこれが失敗でした。インフラ担当の得意なPHPでそのままいくのではなく、アプリケーションエンジニアが得意な言語に合わせるべきで「ScalaがダメだったらPerlでいく」というのが正解だったなぁと今になっては思います。

 

当日、開始

 

開始して早々「Scala実装では初回のベンチマークは通りません」という話が出てきたため、Scalaはやめて上記決めていた方針通りPHPでいくことに決定しました。

 

アプリケーションエンジニア2人にはコードを読んでもらいつつ私はとにかくアプリケーションを動かすというのに注力しながら頑張っていき、一旦アプリケーションを動かして最初のベンチマークが通ったのでした。めでたかった。

 

当日、その後

 

アクセスログから遅いエンドポイントを特定し、スロークエリログから遅いクエリを特定し、というのを行いながら15時くらいに3位になってめっちゃテンション上がったりしながらも、得点の伸びがそこで止まり、その後は特に何もできず終わったのでした。

 

なお、my.cnfが別の場所にあることに気づかなかったインフラ担当の私は本当に死すべき存在として名前を刻まれるべきでしょう。

 

最後の10分くらいでなぜかベンチマークがfailし始めて、最終的に得点が出ませんでした。謎。

 

反省点

 

上にも記載したのですが、私がインフラ担当だったのに私が一番得意(他の2人はそんなに得意ではない)な言語を選択してしまったというのが最大の失敗で、前半の私のボトルネックっぷりが半端なくその結果様々な場面で問題が発生した感がありました。

 

私のせいで優秀な2人の足を引っ張ってしまったなぁと反省しきり。

 

私がボトルネックになったことで(というかむしろそれによって私がかなりテンパったことで優先度付けが崩壊し)最大にやばかったのが以下2点。

 

my.cnfが別の場所にあるのに気づかなかった

 

ISUCON開始直後、「my.cnfを書き換えてスロークエリログを出そう」となったわけですが、なぜかmy.cnfを書き換えてもログが出ません。あれあれおかしいなと思いながら、set globalでログを出すようになり、その直後に全然別の理由でmysqlが起動しなくなるなど事件がありそのままmy.cnfの件は忘れ去っていまし た。

最後の方で「あ、my.cnfの設定まだしてないじゃん」となりinnodb buffer poolの設定などをしたのですが「あれ、buffer pool増やしたのになんかOSがメモリ使ってないなぁ。DBデータのファイル量的にそんなことはないはずだが…なんかおかしいなぁ」と思いつつ、その後別の何かにまた忙殺されてそのままになっていたのでした。ひどかった…

 

各人の開発環境を作れなかった

 

毎年のように各人の開発環境をサーバ上に別ポートで立ち上げてそこで開発しようと思っていたのですが、どうもGCEでファイアウォール設定をしても動かない。ローカルの開発環境を作るのもPHPはちょっと面倒だなぁという感じで、最終的に、アプリエンジニアの1人はサーバ上のプログラムを直接編集し、もう1人はPHPを書いたことないにも関わらずローカルで適当に書き換えてはpushしてサーバ上で動作を見てみるという感じになり、かなりめちゃくちゃだった。

 

それにもかかわらずでかいパッチをポンと作ってそいつがほぼそのままパッと動かせることができた彼はスゲェなと思った限りでした。

 

良かったこと

 

とにかく、私が決勝に出られない状態にもかかわらずチームを結成できて予選に参加できたことは本当に嬉しかったです。また、同じチームだった若者2人はこの経験によって1年後、ものすごい人材になり来年の弊社の層は更に厚くなりいい感じだなという思い(若者に負けないようにもちろん私も頑張りますとも!)

 

あと、15時くらいにn+1問題解決して突如3位に躍り出たのは普通にテンション上がった。嬉しかった。

 

決勝予想

 

以下のようなナウい感じのワードが飛び交う

 

不必要なマイクロサービス

 

「マイクロサービス」などと言って1台のサーバ内に不必要に複数のdockerコンテナが(もしくは別ポートでHTTPサーバが何台も)立っておりそれぞれがお互いのAPIを叩きまくっている世界観。それらを分解する(マイクロサービスをやめるか、そのまま別サーバへ持っていくか)ところから全てが始まる。

 

ビッグ(?)データ

 

アクセス情報を貯めて最後にそのアクセス情報を集計させる。スループットが上がってベンチマーク中のアクセス数が増えるほど最後の集計でタイムアウトしやすくなる。

 

最初は同期的にログ書き込み及び集計をしているので、fluentd(等)を使って非同期にしたり集計を後でまとめてやるようにしたりすることで飛躍的に点数上昇が見込まれる(…のか…?)、等(出題陣から、やはりfluentdが活躍する何かが出てくるのではないかという予想がある)

 

最後に

 

来年こそは優勝したい(その頃にはPerlもしくはScalaの裏側に習熟しているように頑張ります)

 

というわけで、九州行ってきます!皆さんISUCON決勝頑張って!!!

 

併せて読みたい

 

t-kyt.hatenablog.com

 

stefafafan.hatenablog.com

 

 

YAPC Asia 2015の感想やわからなかったことを調べたことなど #yapcasia

YAPC::Asia Tokyo 2015に参加してきた。

各発表で思うところがあったものをまとめてみる。

なお、以下は対象の発表を聞いたかもしくは発表資料を読んだ人くらいしか理解できない内容です。発表資料を読みましょう。

前夜祭

技術ブログを書くことについて語るときに僕の語ること

「これまで自分が書いたエントリを振り返ってみて、『この方向でいいのだろうか』みたいなのを考える」みたいなの、極めて参考になった。

私も過去のブログを振り返ってみて、「これまではこの方向で楽しかったけど、少なくとも今後はこうじゃないな、じゃあどうなんだろう」という思いに駆られた。

さて、じゃあどうなんだろう。考えなくてはいけない。y_uukiさんのお陰で考えることができて感謝感謝である。圧倒的感謝。

1日目

メリークリスマス!

ホビット指輪物語Perl 6について。

「Perl6はクリスマスに出す予定、ただ、何が起きるかはわからないよね」という話を聞いて以下を思い出した。

Gitのプロジェクトにて、すでに存在するオブジェクトと同じSHA-1を持つコミットをしてしまう可能性があるという不安に対して、

それよりも「あなたの所属する開発チームの全メンバーが、同じ夜にそれぞれまったく無関係の事件で全員オオカミに殺されてしまう」可能性のほうがよっぽど高いことでしょう。

参照:Git - リビジョンの選択

あと、指輪物語は映画で観ていたけどホビットは映画も観てなければ小説も読んでないので、どっちかで中身を知っておかなくてはいけないなと思った。

世界展開する大規模ウェブサービスのデプロイを支える技術

Miiverseのデプロイについて。

以前、Mamiyaを見て、「これが新しいデプロイの形…!!!」という感動を覚えたものだったが、同じコンセプトで、fujiwara/stretcher · GitHub というのも作られたんですね。知らなかった。

「なんかよくわからない事態になっても、消してから再度pushすれば普通に動くようになっている」みたいな話があって、そういう、状態を他からもらってくることによって簡単に復旧できるような環境にできているのはとてもよいですねという感じだった。

Consulと自作OSSを活用した100台規模のWebサービス運用

このYAPCではよくConsulの話を聞いたが、一時期よく聞いていたSerfはどうなったのかなという感じがある。

Serfはもう当たり前のものになったか、機能がある程度似ているConsulに皆の注目を奪われてSerfはあまり使われなくなったのかどちらかなのであろう。

というか、私自身ConsulとSerfの違いをそこまでよく知っているわけではない(「SerfにKVSとか高機能なヘルスチェックがついたのがConsulでしょ」くらいのイメージ)のでConsul vs. Serf - Consul by HashiCorpを読んでみた。

「SerfにKVSとか高機能なヘルスチェックがついたのがConsulでしょ」以外に以下のような感じらしい

  • 両方ともゴシッププロトコルで情報伝播
  • ConsulのほうがWANを経由してもSerfほど性能が落ちないらしい
  • CAP定理でいうと、ConsulはCPでSerfはAPとのこと
    • Consulは中央サーバがquorumを維持できなくなるとオペレーションできなくなるので
  • あと、上記URLには記載がなかったが、Consulはサーバ/クライアント型でSerfは全員が同等というのと、ConsulにはDNS Interfaceがないというのが結構大きいかなと今回の発表を聞いていて思いました

Consul、stale modeで使っていればかなり信頼性高く使っていけそうな気配を感じた

Conway's Law of Distributed Work

リモートワークについて

  • 今、私は東京で京都のメンバーとリモートワークをしているのですが、これからも積極的にチャット上で話していこうと思いました
  • 「オフィスにいれば偶然コミュニケーション出来る機会があるが、リモートではそうではないので積極的に!」という感じですので
  • 私がリモートワークで意識していることは正しかったなぁという感じで安心した

Electron: Building desktop apps with web technologies

esa.io - 趣味から育てたWebサービスで生きていく

  • プロダクトへの強い愛を感じた
  • よく寝るのは極めて大切

2日目

Google Cloud Platformの謎テクノロジーを掘り下げる

BigQueryの100億行のフルスキャン及び正規表現マッチが10秒で結果を返すとか、Nearlineというデータアーカイブサービスは安いにもかかわらずデータロードがかなり早いとか、コンテナをいい感じに配置することでハードの性能をかなり高いレベルで使い切ることができているとかそういう感じで結構面白さは伝わった

全体的に、「これはすげぇぇ!!」もしくは「すごいんだろうけどよくわからん!!!」のオンパレードだった

個人的にはロードバランサのIPアドレスTCP anycastを使用して常に1つだけ(ELBはDNSラウンドロビン)とか「最近はディスクIOが高価なので、コンテナアーカイブ用のNearlineは安くできる」とかの話しが結構面白いよねという感じがした。

我々はどのように冗長化を失敗したのか

これだけ考えて構築したConsulを本番環境で動かせなかったというのは極めて無念だっただろうなぁという思いが強い。「Consulによって最初はDNSラウンドロビンされていたがConsulの冗長性に不安を抱いてhostsも使うようにした結果ラウンドロビンされなくなっていた」とか「Public層からPrivate層への接続が日本を駆け巡っていた」とか「なぜか同じクエリでもえらいスロークエリになる場合があってDBのホストを移行したらなおった」とか、忙しい中だとちゃんと確認せずについついはまりがちな失敗談が、良かった。

LVSなどの枯れた技術を使った冗長性の担保とConsulを使った冗長性の担保があり、「手に馴染んだ道具を使おう」という話だったが、LVSよりもConsulのほうがある程度それ自身の障害に強いなどあり得ると思うが、大丈夫今回手に馴染んだLVSではなくConsulを使って、「Consulのほうがここが強いぜ」みたいなのはあったか」みたいな質問をしたら褒められたのでよかった

答えは、やはり「それ自身の障害への強さだろう」とのことだった。

ソーシャルゲームにおける AWS 移行事例

とにかく「すぐに役に立つぜ!」的な実践的内容が詰まっていてすごかった。便利。

「それまでのデータセンターでの運用だと、プールしているサーバ費用については全車共通費用についていてプロジェクト側の費用が見かけ上安くなっていたが、AWSになって少し高くなった」みたいな話しも極めて重要なところだしそういうのも含めて話されているのがすごく良かった。

HTTP2 時代の Web

私は「理解した 導入する」の層に行きたい!!ある程度理解していたが再度理解を深められた。 「RTTを減らすか、光はこれ以上早くならないのでそもそもRTの回数自体を減らすか」 我々も社内ツールにh2oをリバースプロキシとして導入して検証するくらいはできそうだしやってみたいと思った。

サーバサイドプッシュで、せっかくキャッシュがあっても送りつけるというのはなんとかならんものか…(cookieを使って対処する実装案などがあるという話ではあった)

nginxさんのHTTP/2実装が楽しみ!!

最後に

超面白かった。最近なんかもやもや感あったのが吹っ切れた感じがする。

うぉぉぉぉぉぉ!!!!!