RubyConf 2018に登壇するためにL.Aまで行ってきた

RubyConf 2018のRubyKaigi関連トラックに採択されたので、登壇するためにロサンゼルスまで行ってきました。 RubyKaigiトラックということで、内容はRubyKaigiの再演でした。

学生の時にめっちゃ貧乏だったのもあって、今迄海外に行く機会が全く無かったので、実はこれが初の海外旅行であり、当然ながら英語で発表するのも初めてだったので大分緊張しましたが、本番ではそれなりにウケたという手応えがあったので、今迄の登壇経験が上手いこと活きたんだろうと思っています。(後で松田さんとそらはーさんに、めっちゃ発音突っ込まれたけど……)

共同登壇者であるtagomorisさんには、トーク以外でも色々と滞在中にお世話になったので改めて感謝を!ありがとうございます!

f:id:joker1007:20181119163501j:plain f:id:joker1007:20181119163515j:plain f:id:joker1007:20181119163525j:plain

録画はあるらしいので、もうちょっとしたらyoutubeとかに上がるだろうと思います。

発表の仕方については、流石に普段英語全然喋ってないので、テンパって飛ぶとヤバイだろうと事前に原稿をある程度書いて発表者ノートにまとめておくというスタイルでやりました。プレゼンの原稿ちゃんと書いたのは、これが初めて。 一応、そのまんま読むのは止めようと、イメトレして何パターンか作っておいたので、読みながら適宜合わせるぐらいの事は一応できたと思う。

質疑応答にめっちゃ身構えてたんですが、結果的にはRyan Davisに"Good Job"って言われて嬉しかったという、テンパらずに済む形になって助かりました。 登壇後にも何人かにTerrible Workとか、Amazingとか言われたので、通じなかったってことは無さそうで安心しました。 初の海外登壇としては、成功だったと言って良いでしょう!

初のUSAについて

全体的な印象としては、やっぱ色々雑な国だなという感じですw (というか日本がキッチリし過ぎというか)

トイレで流すボタンが反応しなかったり、シャワーの出し方が分からなかったり、水量と水温を一つのレバーで操作しようとする謎UIだったり、自販機で売ってるものの値段が全く分からなくて1ドル札しか使えなかったり売り切れ状態が分からなかったりする。

一方で、フリーWiFiが本当にあちこちにある。これは素晴らしい。

後、Uber/Lyftが真の力を発揮できて、どこに行くにもサクっとタクシーに乗れて決済も簡単に済むというのはめっちゃ便利だった。

そして、とりあえず何でもでかい。

人もでかいし、歩道も通りもでかい。 交差点間の距離がめっちゃ長い上に単位がフィートなんで全然感覚分からなくて、コンビニが2ブロック向こうかーと思って歩いたら15分ぐらいかかったりする。日本じゃ長くて3分だぞ……。

食べ物

肉とビールに関しては最高だった。特にロサンゼルスはローカルブルワリーが一杯あるみたいで、ビール好きにとっては最高の街だと思います。

しかし、何でもかんでも油入ってる感じだし、マッシュポテトやアスパラがもう完全にバターの味だったw 後、海産物系は全体的に選択に失敗した感じで、大体駄目でした。

昼間っから自家醸造したクラフトビール飲めるのは最高!しかも安い!

f:id:joker1007:20181119165208j:plain

ステーキは美味いし、物価の差を考えるとかなり安い!

f:id:joker1007:20181119165220j:plain

美味いと評判だったThe CounterのBTOバーガー。実際めっちゃ美味かった。

f:id:joker1007:20181119165516j:plain

フィッシュマーケットみたいな所でシュリンプカクテル頼んだけど、クソ辛かった……。

f:id:joker1007:20181119165751j:plain

今回のRubyConfの会場近くにKarl Straussという最高に良いブルワリーがあって、カンファレンス中は毎日そこに通っていたし、大体面子がAsakusa.rbだったので、最早Karl StraussはAsakusa.rbのLA支部と言って差し支えないレベルだと思う。本当に最高だったのでLAに行く機会がある人は、是非行くべきだと思う。

f:id:joker1007:20181119170210j:plain

観光

カンファレンス後に1日空けておいたので、その日にロングビーチまで観光に行きました。 一番の目的は、アイオワ級戦艦のネームシップであるアイオワを観に行くことです。中がミュージアムになっていて色々見て回れる様になっている。 確か映画「バトルシップ」に出てきた戦艦はアイオワ級ミズーリで兄弟艦にあたるはずですが、実際見るとまんま映画で見た奴だ!ってなりました。

f:id:joker1007:20181119170655j:plain f:id:joker1007:20181119170717j:plain

後は、役目を終えてホテルになってる豪華客船クイーンメリー号と近くにある潜水艦スコーピオンを見に行きました。

なんというか、アイオワもそうだったがでか過ぎる!

f:id:joker1007:20181119170900j:plain

後はこいつに乗りました!電動で動くキックボードみたいなやつ。

f:id:joker1007:20181119171106j:plain

実際乗ってみたら、めっちゃ楽しかったけど、思いの外加速が凄くてアクセス全開にしたら30km/hぐらい出るし、車輪がすげー小さいので段差とかあるとめっちゃ危ない。 こんなもん国土がクソ広くて道が無駄に広い国じゃないと危なくて乗れたもんじゃないし、絶対日本の公道で乗るのは無理だと思う。 というかアメリカでも十分に危険だと思うので、その内規制されるんじゃなかろうか……。今の内に乗れて良かったと思う。

まとめ

というわけで、RubyConfで登壇しにロサンゼルスまで行き、ついでに観光してきた訳ですが、全体としてはとても良い経験になりました。 アメリカを一部でも経験できたし、美味いビールと美味い肉は食えたし、英語で登壇するという経験ができたので、RubyKaigiでも英語で話せるかもなあという自信が付きました。(やるかは分からんけど……。そもそもCFP通るか分からんけど……) しかし、リスニングは結構苦労するというか、周りがごちゃごちゃしてると全然分からんし、早口で喋られると何言ってるか分からなくなる。ギリギリコミュニケーションは取れなくもないという感じでしたね。 もうちょい慣らさんとなあ……。

まあ色々ありましたが、今回こういう機会を得ることができたのも、トークに採択してくれた松田さんとRubyConfのおかげです。本当にありがとうございました!

本番環境でcassandraの運用を開始してみた

職場でcassandraの運用を開始したので、選定理由とか運用してみて得た知見や所感等を書いてみようと思う。 (まだ十分に知見を得たとは良い難いので、間違った印象を得ていたり、より良いオペレーションに気付いていない可能性があります。)

cassandraの採用理由

そもそもの目的はホットデータの書き込み先としての利用です。要件としては以下の様なものがあります。

  • エンドユーザー端末の数に比例するので、書き込みのスケーラビリティが必要
  • 書き込みデータは更新される
  • 一件単位、もしくは数秒単位の短かいサイクルでバッファチャンクを書き込む必要がある
  • prestoで読み込める (最悪connectorは書いてもいいが、できれば避けたい)
  • 一度に数百万件ぐらいまで読み込む可能性があるので、それなりに読み込みもスケールして欲しいし、レンジスキャンにもある程度効率が求められる。

元々、データ加工で潰しが効く様にkafkaの利用も検討してたんですが、データが頻繁に更新される可能性があるので一意のキーでデータが特定できてupsertが可能であり、書き込みレイテンシを小さく保つことができそうということでcassandraに倒しました。

その他、Apache Kuduとかも調査はしましたが、ちょっと新し過ぎるのとprestoとの接続性に不安があったので、見送りました。

cassandraの構築方法と現在のクラスタ規模

ディスクとネットワークにかなりのI/Oがかかるので、コンテナは使わずにpackerでAMIを生成し、EC2で普通に立ててます。

cassandraにはseedという概念があり、クラスタの情報を提供するための初期ノードが必要になります。 なので、seedは固定でローカルIPを降って3台立ててあり、他のノードはAutoscalingで起動できる様にしてあります。 クラスタ全体では10台で運用しています。テーブルは現時点で3種類あり、一番大きいテーブルで50GB程のデータが入ってます。 今後利用範囲を広げていく予定です。

現時点での利用スタイル

利用開始当初は、アプリケーション本体のコードを弄らずに実験するために、fluentdを経由してデータを投入していました。 元々fluentdにデータを流していたので、copyして軽く加工するだけで出力先をコントロールできる様になっています。 で、データを投入するためにfluentdのプラグインを書きました。(いくつか既存のものがあったんですが古かった) ; github.com

cassandraは挿入は一件単位で行うことになるので、fluentdの様にbufferチャンクを溜めて処理するのは余り相性が良くないとは思いますが。 というのもfluentdはバッファチャンク単位でリトライすることになるので、チャンクの中の一行がエラーになった場合にどうリトライするかを考えるのが面倒くさい。 とりあえず無視するか全体リトライするかの二択を選べる様にはしましたが……。

現在は利用スタイルが固まったので、rubyのcassandra-driverを使ってアプリケーションから直接データ投入する様に変更中です。

メトリック収集

会社ではdatadogを利用しており、datadogにcassandraのインテグレーションがあるのでそれを利用しています。 実態はdatatdog-agentがJMXを介してmbeanから値を取得する形になります。

とりあえず以下の値を収集してます。

  • CPU利用率
  • 読み書きのレイテンシ
  • ディスク利用量 (デバイス全体と、テーブル毎のディスク利用量)
  • 書き込み件数
  • sstableの数
  • keycache hit ratio
  • drop message rate
  • pending task count
  • JVM系のメトリック (heap, nonheap, gc等)

加えて、プロセス監視もdatadogでやってます。

cassandraの特性についての理解

アーキテクチャの細かい解説は、datastaxのドキュメントを読むのが分かり易いと思いますが、cassandraは大まかには以下の様に動きます。

書き込み時

  • レコードのパーティションキーからcoodinatorを決定する
  • coordinatorに書き込みをしにいく
  • まずmemtableというオンメモリのテーブルにデータを書き込む
  • 一定時間、一定データ量毎にflushされてsstableと呼ばれるファイルに書き出される
  • sstable内のデータは不変
  • データが肥大化しない様に定期的にコンパクションが実行されてsstableがマージされる
  • 更新なり削除なりが発生した時はtombstoneマーカーが付与されて、コンパクションタイミングで削除される
  • 整合性レベルに応じた応答が返ってきたら、クライアントにレスポンスを返す

読み込み時

  • パーティションキーを元にcoordinatorにデータを要求する
  • memtableを確認し、あれば取得する
  • 行キャッシュを確認し、あれば取得する
  • ブルームフィルタとkey cacheを使ってsstableの読み出し先を特定する
  • それでも見つからない場合は、sstableに含まれるパーティションインデックスからデータの場所を特定し読み込む
  • 整合性レベルに応じた結果が集まったら、レスポンスを返す

書き込みは基本的にメモリに書かれるし追記しか起きないのでスケールさせやすい。現在のうちの利用規模だと一件辺りの書き込みレイテンシは100μsぐらいで完了してます。(ネットワークレイテンシは別)

しかし、読み込みの時はパーティションを特定してクエリするのが必須になるため、クエリのワークロードに合わせてパーティションキーを設計する必要がある。

実際、パーティションキーを指定しない形でクエリすることは基本できない様になっている。 この点で、読み込みに関してはデータ構造がリッチなKVSに近い。

オリジナルのデータが同じでも参照方法に合わせて、テーブルを作る方が良いとされている様です。 その分書き込みの数が増えるが、書き込みはスケールさせやすいので読み込み形式に合わせた構造を優先した方が良いということだと思います。

prestoからの読み込み

パーティションキーを指定しない場合は全パーティションからデータを読むことになる。 パーティションキーを指定すれば必要なデータだけ取ってくれる。うまくパーティションが分散できれば、大量に読み込んでもそれなりのスピードがでる。クエリパターンに合わせたテーブル設計が大事。 パーティションキー設計や必要なカラムの絞り込みが上手くできないと10倍から100倍ぐらい負荷が変わる。

prestoノード30台、cassandraノード10台で300万件のuser_idを取得してもクエリ自体は3秒ぐらいで終わるし、負荷もかなり軽い。 ただ、1行辺りのデータ量が増えるとsstableの読み出しに結構な負荷がかかるので、読み込みが詰まる傾向があった。

また、キーキャッシュやchunk cache (sstableのキャッシュ)にちゃんとデータを載せておいて、実際のディスクI/Oが発生しない様にしておかないと読み込みが安定しない。

セカンダリインデックスについて

ノード毎にインデックスが作成される、パーティションキーを指定しなくてもクエリ可能になるが、大量のパーティションをスキャンすると異様に遅くなるので、少数のパーティションの中でデータを更に絞り込むのには役に立つが、パーティションキーの代わりに使える様な便利なものではない。

余りデータのカーディナリティが低くても高くても駄目で、程々に共通して存在するデータが有効らしい。また更新が頻発するカラムだとindex更新に負荷がかかるので、利用を避けるべきです。 別途パーティションキーの異なるテーブルを作って、用途に合わせてデータを投入していく方が良さそうです。

cassandraの運用

ノード追加

設定ファイルにseed情報とSnitchの設定を書いて起動すると自動でクラスタに所属します。 起動時にbootstrapと呼ばれる処理が走り、自動でデータの再分散が行われます。

同時に複数台追加しようとすると、bootstrapが競合するので、追加は1台づつでないと事故ったり止まったりします。 急激にクラスタを増強できないので、多少の余裕を見てハンドリングする必要がある。

ノードの削除

正常起動しているノードをクラスターから削除する場合は、nodetool decomissionコマンドを削除したいノードで実行する。 データの再分散が行われて完了したらクラスタから情報が削除されます。

ノード故障

ノードが故障した場合は色々考慮する自体が増えます。

書き込みに関してはhinted handoffという機能により、故障ノードへの書き込み要求をcoordinatorが一時的に受け取ることができます。 ノードが復帰したら、hintファイルに蓄積された書き込みデータが同期される。hintが溜まり過ぎるとノードの復帰に時間がかかる。

ノードが復帰不能の場合は、nodetool removenodeコマンドにnode idを渡してノード情報をクラスタから削除する。この時点でhintファイルが溜まっていれば削除されるはずです。 ノードを同数に戻す場合は代わりに新しく1台追加します。

起動時にJVMオプションを渡して、あるIPのノードを置き換えるという指定をすることで、そのノードの役目を引き継ぐことができる様ですが、オペレーションが複雑化するので諦めて、故障した奴をクラスタから削除し、新規にノードを追加しなおして再分散した方が楽に思える。 ただし、これはクラスタが持ってるデータ規模等に左右されると思います。

seedノード故障

seedノードが故障するとめちゃくちゃ面倒なことになる。

seedノードは起動時に特別扱いされており、bootstrapが行われない。そのため、一回通常ノードとして構成してbootsrapを完了してからseedノードとして起動しなおすことになる。 同一IPで新しくノードを構成しなおそうとすると、非常にややこしいので諦めて新しくノードを追加しbootstrapが終わった時点で、seedにしたいノードのIPを各ノードの設定ファイルに追加し、順番にcassandraを再起動するというのが一番マシな手順ではないかと思います。

同じIPで構成しなおしたい場合は、一回seedsリストから除外してcassandraクラスタ全体を再起動した後、ノードを追加してbootstrapが終わった後に、再度seedsリストにIPを追加して、またクラスタ全体を再起動する必要があるようです。

クラスタを構成するためにseedノードにかなり依存しているので、ここが本当にややこしい。同時にseedノードが全部死んだ時にどうやって復帰すればいいのかは良く分かっていない。生き残っているノードを新しいseedとして指定した上で、クラスタ全体を再起動し、新しくノードを復活させて設定ファイルをこまごま調整すれば元に戻ると思いますが。

今は10数台ぐらいの規模なのでどうってことは無いのですが、これが100台規模になってくるとseedzeノード故障は死ぬ程面倒なのでは、と思うので大規模クラスタ運用している人の知見が欲しい所です。

全体的に見て、ノード自体が無事でプロセスが短時間死んだ程度ならどうとでもなりそうですが、ノードごと死ぬとなるとかなりオペレーションが面倒だったり再分散の負荷がかかるので、作業タイミングに注意した方が良さそうです。

全体感

10台ぐらいで構築するなら、それ程苦労はしなかった。(ある程度インフラの自動構成やイメージ作成に知見があるからですが) モニタリングもdatadogと組み合わせれば割と簡単にdashboardが構築できました。監視さえしっかりしていれば、落ち着いて運用できそうです。

問題はやはりハードウェア故障でノードごと突然死する場合で、そういう場合にデータロストしない様にかつ過剰な負荷がかからない様にオペレーションするのはかなり面倒な気配がビンビンします。

書き込みは本当に早いし、スケールするので安心感がある。読み込みはとにかくクエリパターンを事前にちゃんと考えて、それに合わせたパーティション設計をすることが必須です。まずクエリありきでテーブル構造考えないと、読み込みにめっちゃ負荷がかかることになります。パーティションキーとクエリパターンを中心にしたデータ設計は慣れるまで苦労しそうです。

マテビューが入ったことで、クエリパターンに合わせた柔軟なテーブル構成が実現できそうな気配がありますが、実験してみた所書き込みプロセスが不安定になってOOMで落ちたりしたので、もうちょい知見を積むなりリソース調整をマスターする必要がある。後、現時点ではprestoから読み込む対象として利用できないのが、うちの環境だと辛い点でした。

とりあえず、ホットデータを利用しやすくしつつスケーラビリティを確保する、という当初の目的は達成できそうなので、しばらく運用を継続して様子を見ていこうと思います。

Asakusa.rbとは私にとってどんな場所なのか

Asakusaの方面から来ました

今回、RubyKaigiに来た人は何度かこの画像を目にしたかもしれません。

f:id:joker1007:20180606213950j:plain

昔からAsakusa.rbに良く参加している人が発表資料を作る時に、良く使われている画像です。 いかにもシンボリックでアングルが良い写真なので、これが定番として使われています。私とモリスさんも今回のRubyKaigiで使わせてもらいました。

最近のAsakusa.rbに良く参加していて、今回のRubyKaigiに登壇していたメンバーは私が覚えている限りで以下の通り。(id表記 敬称略)

  • @tagomoris & me
  • @hsbt
  • @koic
  • @spikeolaf
  • @youchan
  • @aycabta
  • LT Speakers
    • @tad
    • @284km
    • @youchan

全員の資料を見てないので、全ての発表でこの画像が出てたかは分からないですが、めっちゃ多いですね。1日二人以上のペース。(抜けてたらマジでごめんなさい)

(2018/6/7 1:26 追記) 正確にはAsakusa.rbは通常のmeetupに一度でも参加したことがある人がメンバーであり、その定義から行くとAsakusa.rbと関係が無いトークの方が少ないレベルですね。また、参加したことがあるメンバーの居住地がAsakusaと呼ばれるというローカルルールがあります。そのため、世界各国にAsakusaがあるのですw (浅草のAsakusaに住んでいる俺)

そんなこんなで、Asakusa.rbというコミュニティの存在感がかなり示された結果、懇親会で何度も質問を受けることになりました。「Asakusa.rbって普段どういうことしてるんですか?」と。 後、Asakusa.rbがヤバイハッカー集団っぽくて怖いという話も良く聞きますね。

直接質問くれた人には色々と自分が思っていることを話しましたが、なんか需要ありそうだしせっかくのタイミングなので私にとってのAsakusa.rbをブログに書いておくことにしました。

Asakusa.rbはmeetup

Asakusa.rbは毎週火曜日に、大体神田から上野辺りの範囲で会議室やコミュニティスペースを借りて開かれているmeetupです。 ググるesaの公開ページが出てくるので、そこにメンバー登録して、毎週のmeetupの開催案内に「参加します」とコメントすれば、後は当日にフラっと行くだけです。 仕事が忙しい時は参加できなかったり、体調不良だったりする時もあるので、私も常に毎週参加している訳ではないですが、良く参加するメンバーが居たり、たまに参加するメンバーが居たり、色々です。

基本的には場所と時間のお知らせだけ書いてあり、テーマはありません。 各自、何かやることや話したいこと、相談したいことを適当に持ち込みます。コメントに「今日はgemのメンテやります」とか「資料作成やる予定です」とか書くことが多い。 とは言え、別に何も無くてもブラっと参加して良い会です。毎週時間になったら集まれる場所がある、というのがAsakusa.rbです。

meetupとは

Asakusa.rbはいわゆる勉強会ではありません。勉強する時間を確保するための場所として使っても全然OKだと思いますが、Rubyに関わることを中心にして人と人が会って話をする場所がAsakusa.rbです。

持ち込んだことや最近コード書いてて思ったこと等をベースに、雑談するのがメインと言っても良いでしょう。

初めてAsakusaに来る場合は、相談事があると一番入っていきやすいんじゃないかなと思ってます。皆色々教えてくれますし、これ以上ないぐらいに詳しい人から話を聞くチャンスがあります。 最近、仕事でこういう問題があったからgem作ってみたんだけどどうですかねーとか、こういう時どうやってデバッグしてるんですかーとか、そういう話があると「オッ」ってなった人が教えてくれたり、それは分からんなー(分からんもんは分からんw)ってなったりします。

他の人同士が話している内容でも、自分が興味ありそうなら首を突っ込んで話に混じっていくのが一番楽しめると思います。そういう事を切っ掛けに普段からある疑問なり困り事を質問するのも良さそうです。

とは言え、毎週技術の話ばかりをしている訳ではなく、どこかの美味い食事処の話やゲームの話、ガジェットの話をしてたりする時間も良くあります。

Asakusa.rbと飲み会

最近のAsakusa.rbには、主催の松田さんを筆頭にお酒と美味しいご飯が好きなメンバーが大勢居ます。なので終わった後に飲んで帰りますかーという日もしばしばあります。(毎回ではない) ただ結構meetupの終わりが遅いので、22:30ぐらいから飲みに行ったりすることがあります。 Asakusa.rbは地域コミュニティなので近所に住んでる人が多く、良い店にあたると終電という概念が存在しなくなるので、そちらにも参加する場合はタイムマネジメントが重要です。他の人の終電を気にする人は余り居ないと思われますw

こんな感じでお酒を飲んでワイワイ喋るのが好きな人も多いです。

RubyKaigiとAsakusa.rb

何故、RubyKaigiでこんなにAsakusa.rbメンバーの登壇者が多いのかというと、メンバーに高いレベルでコードと向き合ってる人が多いというのもあると思いますが、大きな要因は毎週松田さんと会う機会があるという点にあると私は思っています。

毎週、松田さんがRubyKaigiのために奔走している姿をチラチラ目にしている中で、CFP出して欲しいという話を毎度聞くことになる訳で、こりゃ是非とも出してKaigiを盛り上げる力になりたいと思う訳です。更にRubyKaigiがどういう思いを持って運営されているか、どういう発表内容が望まれているかという点について、質問したり聞き易い立場に居ます。 なので、技術力という点を抜きにしても、RubyKaigiを楽しんでいく上で恵まれたコミュニティであり、そういう部分が表われているのかなあと思います。

(ただ、RubyKaigiのトーク審査は鬼厳しいので、それだけで通る程甘くはない……。)

Asakusa.rbは怖い?

現時点の私としては全く怖くなくて、ただ単に楽しい集まりなのですが、かつての自分を思い返すと、そういう気持ちも分からなくはないという感じです。 私が初めてAsakusa.rbに行ったのはもう4年から5年前ぐらい(良く覚えてない)だと思いますが、初めての頃はちょっとビビりながら参加していたと思います。 その感覚をより正確に言うなら「畏れ多い」とかでしょうか。端的に表現すると「怖い」ってなるのは分からなくはない。

ただ、実際の所、Rubyが好きな人が集まってワイワイ雑談してるのがメインの会であり、本当に怖いことなんか特に何も無い訳です。バリバリコードレビューしている訳でもなく、全員が常にRubyをハックしている訳でもないです。私も仕事が終わってなくて普通に仕事しながら回りの雑談を聞いている、みたいな時がよくあります。

もし、コードを書いている様に見せかけたり、言い方が悪いかもしれませんが日々の活動がハッタリっぽい人だと話に入っていきづらいかもしれませんが。そういう振舞いは嫌いな人が多いと思います。

何だかんだでコミュニティって、継続的に参加しているメンバーがいて、ある程度話がしやすい関係性というものが既に構築されている場所です。新しいメンバーを歓迎する工夫があったとしても、やっぱり慣れない集団に参加していこうとするのは勇気が必要な行為だと思います。(特に我々はコミュ力に難のある側の人間なので……)

Asakusa.rbに限らず、何度も参加して入れそうな話題に首を突っ込んで、ちょっとづつ慣れていくというのはどこのコミュニティでも必要なことじゃないかと思います。ただ、皆Rubyが好きでコードを書くのが好きでそれに関する話をするのも好きだ、という前提があるのでそこを取っ掛かりに話をしやすいのは、どこのRubyコミュニティでも共通のことだと思います。Asakusa.rbももちろんそうです。

Asakusa.rbでは新しいメンバーが来ると、全員自己紹介して普段何をしているのかとか今日はこういう話がしたくて来た、という時間を取っています。そこを切っ掛けに声をかけにいくのも良いと思います。 しかし、その時間は松田さんが来たタイミングで始まるので、たまにその時間がえらく遅くなる時があるのがハードルの一つかもしれないw

後、飲み会が不定期でしかも時間がかなり遅いので、そこから交流するのもハードルが高いかもしれない……。

Asakusa.rbと海外からのゲスト

松田さんを始め、海外カンファレンスで登壇経験があるメンバーも多いので、たまに海外から日本に遊びに来た人がmeetupに参加してくれることがあります。そういう時は常に英語って訳ではないですが、英語比率が上がりますし、自己紹介も英語になります。(私は余り上手くないです……)

英語が得意でない場合は、初参加のタイミングとしては、ちょっとハードルがあるかもしれません。ただRubyKaigiで多少なりとも海外から来たRubyistと交流をするための練習としては良い経験が得られると思います。私もじわじわ慣れてきたのかなーという感じです。毎年のRubyKaigiでちょっとづつ海外から来た方と話をする時間を増やすことが出来ているのはAsakusa.rbのおかげもあると思います。

ちなみに、Asakusa.rbに海外ゲストが来る時は交流の比率を上げるために、早めの時間から飲み会に移行することが多いですね。

Wellcome Asakusa.rb

色々と書かせていただきましたが、これらは私が感じているAsakusa.rbです。これが正しいという訳でも無いですし、回によってイメージが変わることもあります。何度も参加していればきっと楽しくなるし得られるものもあると私は思っていますが、全ての人にとってそうでないこともあるでしょう。

私としてはとても楽しい会なので、これからも定期的に参加するつもりだし、新しく色々な人が参加してくれて刺激が得られるのも嬉しいことなので、RubyKaigiやこのエントリ等を通じて興味を持った人がいれば、是非気楽にAsakusa.rbに参加してみてください。もちろんAsakusa.rbに限らず、他にも多数存在する地域.rbにも興味があれば是非参加してみてください。それぞれ特色ある集まりです。楽しんで刺激が得られる空間が見つかるかもしれません。

やっぱり仲間が居ると楽しいものだし、ただでさえ最高に楽しいRubyKaigiがもっと楽しくなります。

Wellcome Ruby Community !!

俺史上最高のRubyKaigi 2018

仙台で行われたRubyKaigi 2018に参加してきました。 RubyKaigiは毎年最高のイベントなのですが、今年は総合的に今迄で最も良い体験ができたRubyKaigiでした。

実績解除

今回のRubyKaigiで初めてメインスピーカーとして登壇することができました。 私が初めてRubyKaigiに参加したのが、確か2011年で1stシーズンのFINALとして開催された時でした。 そして、その頃からずっとThe RubyKaigiのメインスピーカーというのは憧れの場であったのですが、7年の歳月を経て辿り着くことができたことを本当に嬉しく思っています。

f:id:joker1007:20180604172956j:plain

今回の発表内容は「Hijacking Ruby Syntax in Ruby」というタイトルで話をさせていただきました。

Asakusa.rbで雑談してたのが切っ掛けで、@tagomoris さんと盛り上がってRubyのダイナミックな機能を活用した様々なハックに手を出す機会があり、その内容を元に @tagomorisさんと二人で発表しました。 TracePointやRefinementsやBinding、そしてRubyに存在する様々なフックをこれでもかという程使った内容になっているので、それらの内容に興味がある人には是非資料を見ていただきたいですね。

会場の客足も良くて椅子が足りなくなるぐらいだったし、TLや後の懇親会での評判を聞く限りではかなり楽しんでもらえた様で、自分としても大成功だったと言って良いでしょう。 特に嬉しかったのが、Refinementsの作者であるShugo Maedaさんがブログで印象に残った発表として紹介してくれたことです。これはマジでめっちゃ嬉しい。

一緒に発表してくれた @tagomoris さんにも超感謝しております。ありがとうございました! & お疲れ様でした! (追記: モリスさんのエントリ)

その他のセッションについて

@maciejmensfeld さんのkarafkaについての発表は、普段の業務的にも気になる発表で、とても良い話でした。発表後の飲み会で質問が出来たのも良かった。 Karafka - Ruby Framework for Event Driven Architecture - RubyKaigi 2018

また、羽角さんのmruby/cについての発表は内容もさることながら、私としては憧れざるを得ないCOOLさがあって最高でした。RubyKaigiという場所で、私も大好きな日本酒である十旭日の話をするのはカッコ良過ぎですね。実際にmruby/cという新しい技術を使って旭日酒造さんとビジネス事例を生み出しているという点でも凄い話だと思います。(29BYはRubyが酒造に活用された十旭日らしいので、是が非でも飲まなければならない) Firmware programming with mruby/c - RubyKaigi 2018

後、金子さんのASTにカラム情報を入れて正確な位置情報を取れる様にするという仕事は可能性が色々広がりそうで、聞いていて楽しかった。 RNode with code positions - RubyKaigi 2018

仙台という場所

魚介メインで大分美味しいものが食べられた。ホヤ、牡蠣、穴子、どれも美味かった。 そしてお酒も美味い。日本の地方都市開催は多少お金はかかるかもしれないが、めっちゃ楽しめるので素晴らしいと思う。

f:id:joker1007:20180531213342j:plain f:id:joker1007:20180531213107j:plain f:id:joker1007:20180602232539j:plain f:id:joker1007:20180602224943j:plain

ジョジョ

仙台、つまりS市 杜王町ジョジョを愛して止まない自分としては、この町はRubyKaigiの場所として最高だった。 浮かれて3日間、ジョジョTシャツを毎日着てたぐらいです。ちなみに上着はSPW財団のジャケットを着てました。

f:id:joker1007:20180603123817j:plain

ちゃんと、吉良吉影の領収書も貰った!

RubyKaigi 2018 IN 仙台

RubyKaigiそのものの経験としても、初スピーカー実績解除できて最高だったし、仙台観光もできて美味しいものも食べれて最高だったし、むかでやで領収書も貰えて最高だった。 つまり、最高だった訳です。(語彙)

ありがとうRubyKaigi。来年も楽しみにしてます。

f:id:joker1007:20180604183426j:plain f:id:joker1007:20180604183415j:plain

Professional Rails on ECS (rails developer meetup 2017)

このエントリはRails developer meetup 2017で発表した内容をブログとして書き出したものです。 サンプルのスニペットが多いので資料の代わりにエントリとして公開します。 スライド用のmarkdownを元に起こしたものなので、少し読み辛いかもしれませんがご容赦ください。

ECSとは

  • Dockerコンテナを稼動するためのクラスタを管理してくれるサービス
  • 使えるリソースを計測し、自動でコンテナの配置先をコントロールしてくれる
  • kubernetesではない。最近、kubernetesが覇権取った感があって割と辛い
  • 今はEC2が割とバックエンドに透けて見えるのだが、Fargateに超期待
  • ECS or EKS :tired_face:

RailsアプリのDockerize

オススメの構成

  • 実際にデプロイするimageは一つにする
    • 例えばstagingやproduction等のデプロイ環境の違いはイメージでは意識しない
  • 手元で開発する様のDockerfileは分ける

FROM ruby:2.4.2

ENV DOCKER 1

# install os package
RUN <package install>

# install yarn package
WORKDIR /yarn
COPY package.json yarn.lock /yarn/
RUN yarn install --prod --pure-lockfile && yarn cache clean

# install gems

WORKDIR /app
COPY Gemfile Gemfile.lock /app/
RUN bundle install -j3 --retry 6 --without test development --no-cache --deployment

# Rails app directory
WORKDIR /app
COPY . /app
RUN ln -sf /yarn/node_modules /app/node_modules && \
  mkdir -p vendor/assets tmp/pids tmp/sockets tmp/sessions && \
  cp config/unicorn.rb.production config/unicorn.rb

ENTRYPOINT [ \
  "prehook", "ruby -v", "--", \
  "prehook", "ruby /app/docker/setup.rb", "--" ]

CMD ["bundle", "exec", "unicorn_rails", "-c", "config/unicorn.rb"]

ARG git_sha1 # どのコミットなのか中から分かる様にする

RUN echo "${git_sha1}" > revision.log
ENV GIT_SHA1 ${git_sha1}

docker build


assets:precompile

RailsのDocker化における鬼門の一つ

  • S3 or CDNを事前に整備しておくこと
  • ビルド時に解決するがビルド自体とは独立させる
  • docker buildした後で、docker runで実行する

  • ビルドサーバーのボリュームをマウントし、assets:precompileのキャッシュを永続化する
  • キャッシュファイルが残っていれば、高速にコンパイルが終わる
  • manifestをRAILS_ENV毎にrenameしてS3に保存しておく この時、コミットのSHA1を名前に含めておく。(build時にargで付与したもの)
docker run --rm \
  -e RAILS_ENV=<RAILS_ENV> -e RAILS_GROUPS=assets \
  -v build_dir/tmp:/app/tmp app_image_tag \
  rake \
    assets:precompile \
    assets:sync \
    assets:manifest_upload

prehook

ENTRYPOINTで強制的に実行する処理で環境毎の差異を吸収する

  • ERBで設定ファイル生成
  • 秘匿値の準備
  • assets manifestの準備
    • さっきRAILS_ENV毎に名前付けてuploadしてたのをDLしてくる

秘匿値の扱い

  • 設定ファイル自体を暗号化してイメージに突っ込む
    • 環境変数で直接突っ込むとECSのconsoleに露出する
    • 値の種類が多いと環境変数管理する場所が必要になる
  • コンテナ起動時に起動環境の権限で複合化できると良い
    • prehookで複合化処理を行う

yaml_vault

https://github.com/joker1007/yaml_vault

  • Rails5で入った、encrypted secrets.ymlの拡張版
  • AWS-KMS, GCP-KMSに対応している
  • KMSを利用すると秘匿値にアクセスできる権限をIAMで管理できる
  • クラスタに所属しているノードのIAM Roleで複合化
  • 設定をファイルに一元化しつつ安全に管理できる
  • Railsの場合、secrets.ymlをメモリ上で複合化して起動できる
    • ファイルに展開後の値が残らない

開発環境

docker-composeとディレクトリマウントで工夫する


version: "2"
services:
  datastore:
    image: busybox
    volumes:
      - mysql-data:/var/lib/mysql
      - vendor_bundle:/app/vendor/bundle
      - bundle:/app/.bundle

  app:
    build:
      context: .
      dockerfile: Dockerfile-dev
    environment:
      MYSQL_USERNAME: root
      MYSQL_PASSWORD: password
      MYSQL_HOST: mysql
    depends_on:
      - mysql
    volumes:
      - .:/app
    volumes_from:
      - datastore
    tmpfs:
      /app/tmp/pids


  mysql:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - '3306:3306'
    volumes_from:
      - datastore

Macの場合

  • ちなみにボリュームマウントが死ぬ程遅いので、何らかの工夫が必要
  • dinghyかdocker-syncで頑張る
    • どっちも辛い
  • Mac捨てるのがオススメ

俺の開発スタイル

  • 開発用Dockerfileでzshや各種コマンドを入れておく
  • docker-compose run --service-ports app zsh
  • シェルスクリプトで自分の.zshrcやpeco等をコピーしdocker exec zsh
  • ファイルの編集だけはホストマシンで行い、後は基本的にコンテナ内で操作する

set -e

container_name=$1

cp_to_container()
{
  if ! docker exec ${container_name} test -e $2; then
    docker cp -L $1 ${container_name}:$2
  fi
}

cp_to_container ~/.zshrc /root/.zshrc
if ! docker exec ${container_name} test -e /usr/bin/peco; then
  docker exec ${container_name} sh -c "curl -L -o /root/peco.tar.gz https://github.com/peco/peco/releases/download/v0.4.5/peco_linux_amd64.tar.gz && tar xf /root/peco.tar.gz -C /root && cp /root/peco_linux_amd64/peco /usr/bin/peco"
fi

docker exec -it ${container_name} sh -c "export TERM=${TERM}; exec zsh"

デプロイの前に

ECSの概念について


TaskDefinition

  • 1つ以上のコンテナ起動定義のセット
    • イメージ、CPUのメモリ使用量、ポート、ボリューム等
    • 物理的に同じノードで動作する
  • docker-composeの設定一式みたいなもの
  • kubernetesでいうPod

Task

  • TaskDefinitionから起動されたコンテナ群
  • 同一のTaskDefinitionから複数起動される

Service

  • Taskをどのクラスタでいくつ起動するかを定義する
  • ECSが自動でその数になるまで、コンテナを立てたり殺したりする
  • コンテナの起動定義はTaskDefinitionを参照する
  • コンテナが起動したノードをALBと自動で紐付ける
  • kubernetesにも似た概念がある

ECSへのデプロイの基本

  1. TaskDefinitionを更新
  2. Serviceを更新
  3. 後はECSに任せる

ecs_deploy

https://github.com/reproio/ecs_deploy

  • capistrano plugin
  • TaskDefinitionとServiceの更新を行う
  • Service更新後デプロイ状況が収束するまで待機する
  • 更新したTaskDefinitionのrevisionを他のタスクで参照できる
  • TaskDefinitionやServiceの定義はRubyのHashで行う
    • Hash化できれば何でも良いので、YAMLでもJSONでも

Why use Capistrano

  • 既存の資産が多数ある
    • slack通知のフックとか
  • デプロイのコマンドが変化しない
  • 設定ファイルの場所や定義も大きく変化しない

個人別ステージング環境

  • アプリサーバーだけで良いなら容易に実現可能
  • RDB等のデータストアを個別に持つなら色々難しい
  • 弊社はアプリサーバーだけ個別にデプロイ可能
  • データストアを弄る場合はフルセットの環境を使い、そこを占有する

インフラの準備

terraform等で以下のものを準備する

  • ALBを一つ用意する
  • 個人別のサブドメインをRoute53に定義
  • ALBのTarget Groupを個人別に定義
  • ALBのホストベースルーティングを定義

その後capistranoにmemberという環境を定義し、各メンバーが自分の名前でtarget_ groupやTaskDefinitionの名前を使ってデプロイ出来る様に諸々を変数化する


terraformの例

module "acm" {
  source = "../acm"
}

resource "aws_alb" "developers" {
  name = "developers"
  internal = false
  subnets = ["your-subnet-id"]
  security_groups = ["your-security-group-id"]
  idle_timeout = 120
}

resource "aws_alb_target_group" "per_developer" {
  count = "${length(var.users)}"

  name = "${element(var.users, count.index)}"
  port = 8080
  protocol = "HTTP"
  vpc_id = "your-vpc-id"
  deregistration_delay = 0

  health_check {
    path                = "/health_check"
    interval            = 45
    matcher             = 200
    unhealthy_threshold = 8
  }
}

resource "aws_alb_listener" "developers" {
  load_balancer_arn = "${aws_alb.developers.arn}"
  port = 443
  protocol = "HTTPS"
  ssl_policy = "ELBSecurityPolicy-2016-08"
  certificate_arn = "your-certificate-arn"

  default_action {
    target_group_arn = "${element(aws_alb_target_group.per_developer.*.arn, 0)}"
    type = "forward"
  }
}

resource "aws_alb_listener_rule" "per_developer" {
  count = "${length(var.users)}"

  listener_arn = "${aws_alb_listener.developers.arn}"
  priority     = "${count.index + 1}"

  action {
    target_group_arn = "${element(aws_alb_target_group.per_developer.*.arn, count.index)}"
    type = "forward"
  }

  condition {
    field  = "host-header"
    values = ["${element(var.users, count.index)}-*.example.com"]
  }
}

resource "aws_route53_record" "per_developer_app" {
  count = "${length(var.users)}"

  zone_id = "${var.zone_id}"
  name = "${element(var.users, count.index)}-app.${var.domain_name}"
  type = "A"
  alias {
    name = "${aws_alb.developers.dns_name}"
    zone_id = "${aws_alb.developers.zone_id}"
    evaluate_target_health = true
  }
}

デプロイ定義の例

set :rails_env, :staging
set :branch, ENV["GIT_SHA1"] || (`git rev-parse HEAD`).strip
set :slackistrano, false

raise "require DEVELOPER env" unless ENV["DEVELOPER"]

target_group_arn = Aws::ElasticLoadBalancingV2::Client.new.describe_target_groups(names: [ENV["DEVELOPER"]]).target_groups[0].target_group_arn

set :ecs_services, [
  {
    cluster: "staging-per-member",
    name: "#{ENV["DEVELOPER"]}-#{fetch(:rails_env)}",
    task_definition_name: "#{ENV["DEVELOPER"]}-staging",
    desired_count: 1,
    deployment_configuration: {maximum_percent: 100, minimum_healthy_percent: 0},
    load_balancers: [
      target_group_arn: target_group_arn,
      container_port: 8080,
      container_name: "nginx",
    ],
  },
]

Autoscale (近い内に不要になる話)

Fargate Tokyoリージョンはよ!

  • 現時点でECS ServiceのスケールとEC2のスケールは独立している
  • Service増やしてもEC2のノードを増やさないとコンテナを立てるところがない
  • 増やすのは簡単だが減らす時の対象をコントロールできない

というわけでデフォルトで良い方法がない。


ecs_deploy/ecs_auto_scaler

https://github.com/reproio/ecs_deploy

  • CloudWatchをポーリングして自分でオートスケールする :cry:
  • Serviceの数を制御し、EC2の数はServiceの数に合わせて自動で収束させる
  • スケールインの際は、コンテナが動作していないノードを検出して落とす
  • コンテナが止まるまではEC2のノードは落とさない

polling_interval: 60

auto_scaling_groups:
  - name: ecs-cluster-nodes
    region: ap-northeast-1
    buffer: 1 # タスク数に対する余剰のインスタンス数

services:
  - name: app-production
    cluster: ecs-cluster
    region: ap-northeast-1
    auto_scaling_group_name: ecs-cluster-nodes
    step: 1
    idle_time: 240
    max_task_count: [10, 25]

    scheduled_min_task_count:
      - {from: "1:45", to: "4:30", count: 8}
    cooldown_time_for_reach_max: 600
    min_task_count: 0
    upscale_triggers:
      - alarm_name: "ECS [app-production] CPUUtilization"
        state: ALARM
    downscale_triggers:
      - alarm_name: "ECS [app-production] CPUUtilization (low)"
        state: OK

ecs_auto_scaler自体もコンテナに

  • ecs_auto_scalerはシンプルなforegroundプロセス
  • 簡単なDockerfileでコンテナ化可能
  • こいつ自身もECSにデプロイする

まあ、Fargateで不要になると思う


コマンド実行とログ収集

ECSにおいて特定のノードにログインするというのは負けである rails runnerやrakeをSSHで実行とかやるべきではない そのサーバーの運用管理をしなければならなくなる


wrapbox

https://github.com/reproio/wrapbox

  • ECS用のコマンドRunner
    • 半端に汎用性を持たせようとしたんでコードが微妙に……
  • TaskDefinitionを生成、登録し、即起動する
  • 終了までステータスをポーリングし待機する
  • タスク起動権限はIAMでクラスタ単位で管理できる
  • 慣れるとSSHとかデプロイが不要でむしろ楽

default:
  region: ap-northeast-1
  container_definition:
    image: "<ecr_url>/app:<%= ENV["GIT_SHA1"]&.chomp || ENV["RAILS_ENV"] %>"
    cpu: 704
    memory: 1408
    working_directory: /app
    entry_point: ["prehook", "ruby /app/docker/setup.rb", "--"]
    environment:
      - {name: "RAILS_ENV", value: "<%= ENV["RAILS_ENV"] %>"}

wrapboxで実行したコマンドログの取得

  • papertrailにログを転送し、別スレッドでポーリングしてコンソールに流すことができる
  • 原理的に他のログ集約サービスでも実現可能だが、現在papertrailしか実装はない

default:
  # 省略
  log_fetcher:
    type: papertrail # Use PAPERTRAIL_API_TOKEN env for Authentication
    group: "<%= ENV["RAILS_ENV"] %>"
    query: wrapbox-default
  log_configuration:
    log_driver: syslog
    options:
      syslog-address: "tcp+tls://<papertrail-entrypoint>"
      tag: wrapbox-default

db:migrate

capistranoのhookを利用しwrapboxで実行する

def execute_with_wrapbox(executions)
  executions.each do |execution|
    runner = Wrapbox::Runner::Ecs.new({
      cluster: execution[:cluster],
      region: execution[:region],
      task_definition: execution[:task_definition]
    })
    parameter = {
      environments: execution[:environment],
      task_role_arn: execution[:task_role_arn],
      timeout: execution[:timeout],
    }.compact
    runner.run_cmd(execution[:command], **parameter)
  end
end

desc "execution command on ECS with wrapbox gem (before deployment)"
task :before_deploy do
  execute_with_wrapbox(Array(fetch(:ecs_executions_before_deploy)))
end

set :ecs_executions_before_deploy, -> do
  # ecs_deployの結果からTaskDefを取得
  rake = fetch(:ecs_registered_tasks)["ap-northeast-1"]["rake"]
  raise "Not registered new task" unless rake

  [
    {
      cluster: "app",
      region: "ap-northeast-1",
      task_definition: {
        task_definition_name: "#{rake.family}:#{rake.revision}",
        main_container_name: "rake"
      },
      command: ["db:ridgepole:apply"],
      timeout: 600,
    }
  ]
end

db:migrate -> ridgepole

  • migrateのupとdownがめんどい
  • 特に開発者用ステージング
  • ridgepoleならデプロイ時に実行するだけで、ほぼ収束する
  • エラーが起きたらslackにログを出して、手動で直す
  • productionはdiffがあればリリースを停止させる
  • diffを見てリリース担当者が手動でDDLを発行する

テストとCI

以下を参照。 https://speakerdeck.com/joker1007/dockershi-dai-falsefen-san-rspechuan-jing-falsezuo-rifang


コンテナを真面目に運用するためには、結構色々考えることがある

何かしら参考になれば幸いです

如何にしてAsakusaから来た面々はISUCON7の予選に敗北したのか

とてもとても悲しいので、とりあえずやったことと言い訳を書いて気を紛らわせることにする。 敗北した身でグダグダ言うのが格好悪いことは百も承知だが、人間には魂の救済が必要であることをご理解いただきたい。

序盤〜方針決定

最初パスワードのコピペミス等でサーバーからガンガンBANされて、そもそもログインできなくなる。これで10分から20分ぐらい無駄にした気がする。 テザリングにIPを切り替えたり、他のノードから入ったりして、何とか公開鍵でログインできる環境を整える。

適当にベンチ流してスコアを取る前に、nginxのログ設定や構成を確認しalpを使って集計できる準備を整えた。デフォルト実装とRuby実装でベンチを流す。その裏で実装を一通り読む。

f:id:joker1007:20171023022330j:plain

ざっくり図を書いて、相談。とにかく/iconsを何とかしないと話が進まないので、静的ファイルとして書き出してCache-Controlだよね、までは即決。

モリスさんの発案により全てのノードにPOSTが来た時点でPUTリクエストを転送し、WebDAVで全てのノードにファイルを書き出して、後はnginxでサーブすればいいという方針で行こうということになる。この決定は後になって思うと危険な判断ではあった。

そこに多少の準備がかかるので、その間にSQLやアプリケーションの実装周りで明らかに非効率な所を潰すことにした。俺はRubyコードの全般を直し、やんちゃさんにDB周りのインデックスやSQLを見てもらった。

中盤

/iconsの静的ファイル化が完了し、nginxにより直接サーブが可能になる。この時点でスコアが3万前後。画像ファイルのキャッシュとかは一切してないので、周りのスコアとかを見ても、まあこんなもんだろうと思う。

ここで一つ目の壁にぶち当たる。nginxにexpiresを追加してCache-Controlが動作する様にしたのだが、スコアが伸びない。この時点でベンチマーカーが一体何であるのかを考えることになる。

ここで我々のチームは、ベンチマーカーがCDNかキャッシュプロキシの様なものからのリクエストである、ということをすぐに想定できなかった。少なくとも自分はブラウザかAPIのクライアントの様なものしかすぐに想定できなかった。

ただ、ネットワークがサチっていることはメトリックから明らかであり、周りのチームとのスコア差を考えると、304かCache-Controlによるキャッシュしか方法が無いだろうというのは分かっていた。

その間に裏でワーカーの数やらキャッシュを調整し、スコアを45000ぐらいまで伸ばす。やったのは大体以下の様な感じ。

  • チャンネルは作ったら変更されないので、起動時に全部Redisのハッシュにストア
  • チャンネルが保持しているメッセージ数もRedisにハッシュでストアしメッセージが入ったらhincrbyする
  • db.prepareを全てmysql2-cs-bindのxqueryに置き換える
  • RedisとMySQLのコネクションをコネクションプール化し起動時に接続済みにしておく
  • nginxとpumaのプロセス数を2にして、スレッド数やwocker_connection数を調整
  • カーネルのsomaxconnが128で少なかったので数を増やす
  • SQLのクエリワークロードに合わせてインデックスを張る (そんなにやることはなかった)
  • N+1をJOINに変えて潰す

ちなみに、pubsubとか使えそうかなとは考えたが、3台でそれやってもなあというのと、短時間で実装するのが無理っぽかったので止めた。

しばらく悩んだ後にモリスさんが気付いてCache-Controlにpublicを突っ込んだらスコアが倍ぐらいになり、どうもCDNっぽいという話が出てきた。

終盤

Cache-Controlがある程度効いているのだが、やっぱりネットワークがサチってスコアが10万辺りで止まる。304を返して何とかする方法があるはずだ、と思ってパケットキャプチャをしてクライアントの動きを見たのだが、どうもIf-Modified-SinceやらIf-None-Matchが見当たらなかった。(本当は少数ログに残ってた可能性はあるが気付かなかった)

で、どうやって304で返すかで色々と迷走することになる。この時にベンチマークの仕様について確信が持てなかったため、肝心なことに気付けなかった。EtagやLast-Modifiedが入ってて、何故ネットワークが先にサチるのかが分からなかった。

その間も裏でアプリケーションのキャッシュ可能な場所を探したり、nginxのopen_file_cacheやらTCP周りのカーネルチューニングをやって11万ぐらいは安定して出る様になっていた。

しかし、最終的に最後まで304でレスポンスを返すことができず、アプリケーションやDBにかかっている負荷は5割に満たないまま時間切れとなった。

アプリケーションサーバー側に来ていたトラフィックは遅くても100msec、POSTでも200msecぐらいで、半数以上は数十msecで返せていたのでウェブアプリケーションとしては充分な水準だったとは思う。

最終的な敗因

終わった後の情報からの推測によると、一番致命的だったのは静的なファイルを返す様にした際に各ノードに初期画像を撒いたのだが、その時にファイルの更新時間を揃えるのを忘れたという点だと思われる。

ブラウザだったら、Cache-Controlで一旦ローカルにキャッシュされてしまえば、If-Modified-Sinceが多少異なっていても、そもそもサーバーにアクセスしないので、何も問題は無い。

しかし、後から推測するにベンチマーカーはCDNからのリクエストを想定しているものだと思われる。なのでレスポンスヘッダの状態をちゃんとチェックしてキャッシュの削除をしていたんじゃないだろうか。

もし、ファイルの更新時間が異なる同一ファイルが各ノードにあった場合、あるノードから戻ってきたLast-Modifiedが自分がリクエストしているIf-Modified-Sinceより前になる場合、というのが発生する。ここの値が一致しないとnginxはデフォルトでは普通にコンテンツを返すはず。

ここで、一つ気付いていながら確信が持てず試さなかったのが、nginxのif_modified_since beforeである。もしサーバーが持っているファイルの更新時間より未来のIf-Modified-Sinceが送られてきても、nginxは304を返すことができる設定がある。恐らくうちの場合はこれでほとんどのコンテンツに対して304を返すことができた可能性がある。

一方でEtagもレスポンスヘッダに含まれており、ファイルの更新時刻が異なる場合それも異なってしまうので、最終的にはnginxの実装に依る様な気もする。どっちにしても駄目だったかもしれないが、そこまでの知識はない。

何にせよ、上記の問題によって、初期画像に対するリクエストのほとんどが304を返すことができなかった。

そのためネットワークがサチって負荷が上がりきらずスコアが伸びなかった。3台分の帯域をフルに使ってWeb,DB共にリソースの半分以上の余力を残して11万点だったので、304がまともに返せていれば、20万後半から30万ぐらいは行けたんじゃないかとは思うのだが、完全に別の世界線の話になってしまったので、我々には本当のところは分からない……。

ちなみに、WebDAVでファイルを複製して各ノードに配置したことが危険だったのは、ファイルを書き込んだタイミングが1秒以上ズレると上記と同じ問題が発生するところだった。

そもそもファイルの更新時間という点に気付いていなかったので、ここも致命的だったのかどうかは定かではない。ローカルネットワークは帯域もレイテンシも全然余裕であり、書き込みが終わるまでに全体で0.1秒以下ぐらいしかかかってないので、よっぽど運が悪くない限りはそうそうズレることは無いと思っていたのだが、ぶっちゃけ各ノードの時計が秒単位で一致してたかというと、はなはだ怪しい。恐らくこれも影響があったのではないだろうか。

言い訳

実際、適当にいくつかの画像にアクセスした限りでは複数のノードでもEtagもLast-Modifiedも一致していたのも気付けなかった点の一つではある。

しかし、今回私にとってとてもハードルが高かったのは恐らくCDNというものを想定したベンチマーカーだったこと、そしてその挙動特性を知る経験が無かったことだ。

一応、数年Webに関する仕事をしてきて、今CTOなんぞをやっている訳だが、CDNが必要になったことなど今迄一度も無かった。ずっとB2Bで仕事をしていたのでPOSTの数が多いとかやたら複雑だとか一回のGETでめちゃめちゃ複雑な計算結果を返さなきゃいけないことは一杯あるのだが、GETはそんなに世界のあちこちから来ないし、1分に数件とか処理できれば余裕な仕事がほとんどだった。

というわけで、CDN等は遥か遠い世界の話だった。一応メルカリの事件とかは知ってて、ああやべえなCache-Control周り、とかは思ってはいたのだが、所詮対岸の火事であるという認識で、必要になった時に調べられる様に覚えておこう、ぐらいのものだった。

この辺りのハイトラフィックWebに対する地力の無さが敗因だったと言える。

一方で、運が悪かったというか問題の特性によって敗北した点もあると思っている。いくらいきなり無茶なパフォーマンス改善が降ってきたとして、そのサービスがCDNを使っているかどうかと、どういうCDNを使っているかの情報すら無いままサーバーサイドの改善にかかることはまずあり得ないだろうと思う。(まあ、この業界は想像の斜め下とか余裕で越えてくるので分からんけど)

またCDNの導入がされててS3やGCSに類するオブジェクトストレージが使えないというのも、イマイチ考えにくい。今回、ローカルのファイルシステムから画像をサーブした結果ファイルの更新時刻の罠にハマってしまったが、仕事で関わったサービスで画像をサーブするのにローカルのファイルシステムを使ったことは多分一度もなかったと思う。CDNを使うぐらいまで成長したサービスが、そういう環境が無いってのはそんなに多くはないだろう。(オンプレで開発してきて場当たり的な改善とCDNでギリギリ何とかしながら成長してしまったサービスとかならあり得ないとも言えない気はする)

というわけで、今回の問題は本当に世界レベルからのアクセスがあるB2CのハイトラフィックWebと向き合ってきた人間がやはり有利だったと思う。まあ、それがWebサービスの現実であり地力の違いである、と言われればグウの音も出ないのだが……。あー辛い……。

今回の我々のチームは全員がB2B業であり、ミドルウェアやら集計SQLやら分散処理システムやらワークフローを見るのがメインみたいな人間ばかりで、これは完全に最近のWebの現実というものに立ち向かえていなかったと言える。CDN使っててS3の無い世界なんて知らねえよww

まあ、私の様な人間はクラウドのぬるま湯に漬かって生きているのだなあ、ということを実感した1日ではあった。

今回得た教訓は、CDNの仕様は厳密に確認しなければならないということと、ローカルファイルシステムからコンテンツをサーブする時はファイルの更新時間を揃えなければいけない、ということだ。まあ後者の知見はよっぽどでないと使う機会は無さそうだが。(未来のISUCONとかぐらいっぽい)

いやー、S3って本当便利ですよね。

データ分析基盤構築入門の献本をいただいた

先日、著者のデータ分析基盤構築入門の著者の一人である@yoshi_kenさんより、献本をいただきました。

http://gihyo.jp/book/2017/978-4-7741-9218-5

f:id:joker1007:20171006045932j:plain

この本は、Fluentdと、ElasticSearch, Kibanaを中心にしたデータの可視化と分析を行える基盤を実際に構築する手順やその意義等が詳細に書かれた本です。

少し遅くなってしまったのですが、やっと一通り目を通すことができました。

序盤はデータ分析を行うことの意義や、分析を行うためのデータ設計、ETLやデータフローの構築が何故必要なのか丁寧に解説されています。最近私も似た様な分野で仕事をしているので、その通りだなあと頷くことが多い内容でした。

中盤はfluentdの解説、後半はElasticSearchとKibanaについての解説が中心です。かなりconfigサンプルが多いので始めてこういうミドルウェアを触る人にとっては助けになる内容だと思います。

fluentdはv0.12系で書かれていて、現在でも安定板はまだ0.12系の扱いですが、ちょうどv0.14系への移り変わり時期と被ってしまっているのが、残念な所です。軽くフォローはされていますが、基本的な内容はv0.12系をベースに書かれています。私も技術書を書いたことがあるので想像できますが、こういうバージョン移行期の執筆は書く側としてももどかしい思いをする所だと思います。しかし、ほとんどの内容はそのまま活用できますし、fluentdのアーキテクチャや構成パターン、運用TIPS等、幅広く網羅されていて、とても良い内容でした。

そして、最後にAppendixとしてfluentdのプラグインまとめとembulk, digdagについての解説があるんですが、Appendixが異様に分厚くて、これは普通にパート1つ分ぐらいある様なボリュームでしたw 多分、日本語の書籍でfluentd, embulk, digdagについてまとめて解説してる本は貴重だと思います。

embulkの解説の中で、私が作ったembulk-filter-ruby_procを活用している例が紹介されていて、なるほど、ちゃんと誰かの役に立ってるんだと嬉しくなりました。ご紹介いただきありがとうございます!

プラグインまとめでは、私がメンテしてたり作ったりしているプラグインもいくつか紹介されています。この本を読んだ人にとって使えそうなものがあったら幸いですね。

私の仕事でも、fluentdやembulkはかなり活用しており、この本のfluentdの解説はとても手厚いので同僚にオススメしたい感じでした。

今回は、献本していただきありがとうございました!

(そういえば、技術書の献本をいただいたのは初めてかも)