激闘90日!RDSmultiAZ化プロジェクトの軌跡
タイトルの日数は適当です。
前回の記事のブコメに「インフラパズル」に言及したものがあって結構嬉しいので、先日やっと終わった一連のインフラ改善の話を紹介しようと思います。
ことのはじまり
そもそもRDSのSingleAZ->MultiAZってボタン一発無停止適用じゃねえのという話ですが、まあ色々経緯があります。
1. 半年前ぐらい
(僕が現職に入社する前の話ですが)RDSのマスターノードに障害があり、replicaが昇格することになりました。
master | replica | |
---|---|---|
before | db-master | db-repl |
after | db-repl | db-repl2 |
db-masterは破棄され、db-repl2が新設されて体制は維持されました。 しかし障害復旧を優先したため、db-replはSingleAZのままな上、"repl"なのにmasterというややこしい状態で、このまま恒久運用はできません。
2. 同時期
僕が入社した時点でdb-replを修繕するissueは作られていましたが、rename + multiAZ化を無停止でやるというのは字面より複雑な作業で
- rename時には再起動が発生する
- この再起動でmultiAZのfailoverが起こるかは確証なし
- 事前にhostnameではなくipで参照するようにアプリを設定しておくことで、rename時の接続断時間を最小限にできる
- multiAZが適用されるとfailoverによってipが変わってしまう可能性があるため、上記の方法を使うならrename > multiAZという順で作業する必要がある
このようなことがまとめられていました。決済サービスの性質上、たとえ計画であっても停止は極力避けなくてはならないため、いちいち話がややこしくなります。
renameの停止時間は相当短い(実験した限りでは2秒程度)ですが0ではなく、ユーザーに踏まれてしまうと500が出てしまうため、いい方法が思いつくまで塩漬けということになりました。
他の課題たち
RDSの問題はそれとしてほかにも課題はたくさんありました。
SPOFをなくす
幾つかSPOFになっている場所が残っているので、HAProxyの導入によってそれをなくそうというものです。具体的には以下のミドルウェアが対象でした。
- LDAP
- proxy
- インターネットに出るためのhttpプロキシが1台しか構成されておらず、2台目を構築して多重化する必要がある
- http_proxy環境変数などでは多重化やフェイルオーバーの仕組みは実現できない
どちらもHAProxyの導入で解決できますが、HAProxyをどこに置くかが問題です。独立したHAProxyサーバーを作ったとしても、そこがSPOFになってはあまり意味がありません。
pgbouncerサーバーを作る
pgbouncerが各アプリケーションサーバーのローカルにそれぞれインストールされていて非効率なので、専用のサーバーを立ててそこに接続させるようにするというものです。
導入当時はpgbouncerの安定性が評価できていなかったのと、(障害にケツを蹴られての)突貫での導入だったため、とりあえずの対策 + 安定性評価期間としてこのような構成になったと古文書に書いていました。
古文書によれば1台のサーバーで運用しようということでしたが、pgbouncerの安定性に問題はなくともインスタンスがふっとんだときに困るという話はあり、これも半塩漬け状態でした。
つながる点と点
これらは独立した問題から生じた課題で、当初は個別に対処を考えていましたが、ある日のissue棚卸し中に出た
「HAProxyを各サーバーに置けばいいんじゃね?」
という一言から一気に解決に向かいます。
- HAProxyを各サーバーに置けば、"HAProxyサーバーの障害"はなくなる
- SPOF問題解決
- 今後多重化が必要なミドルウェアが増えたとしても、各サーバーのHAProxyに適切な設定をばら撒けばいかようにも対応できる
- スゴイ
- pgbouncerも2台立ててHAProxyでroundrobinすればいいのでは?
- pgbouncerサーバーSPOF問題も同時に解決
- pgbouncerのPAUSE/RESUME機能を使えばクエリをせき止めることができる
- 現行でも可能だが、全アプリサーバーのpgbouncerにPAUSEを打たなくてはならず微妙だった
- 2台なら手でもやれる
- PAUSEしてる間にRDSのリネームを行えば、再起動時にクライアントをぶった切ることはない
- 完全無停止RDSリネームの目処がたった!
- RDSのリネームが終われば、あとはmultiAZ化するだけ!
こうして、3つの大きな問題を一挙に解決するプロジェクトが始まりました。
それから
ここからの実作業の話は、ansibleをごちゃごちゃいじった ぐらいしか話すことがないので割愛します。
改修とリリースは足掛け2ヶ月ぐらいかけて少しずつ行っていきました。もちろん全部無停止です。
- haproxyの配備(参照なし)
- アプリケーションがhaproxyを参照するように変更
- pgbouncerサーバーの作成
- haproxyにpgbouncerプロキシ設定を追加配備
- 外部サービス向けでないサーバーから少しずつ向けて様子見しながら段階的にリリース
- pgbouncerからRDSへの参照をipに変更
- PAUSE/RESUMEを使ってクエリをせき止めた状態でRDSをrename
- RDSをmultiAZ化する
最後のmultiAZ化をapplyしたときの様子が以下になります。
multiAZをオンにするだけでこんなに感動できる日が来るとは思いませんでした。
なぜ無停止にこだわるのか
ぶっちゃけ5分停止すればそれで済む修正をなぜここまでして無停止にこだわるか不思議に思う人もいるかもしれません。
まず、サービスを停止する難易度はサービスによって千差万別です。システム自体を停止することは雑でよければなんとでもなりますが、商用サービスである以上は利用者とか関係者との調整が絡んできます。
僕が見てきただけでも、Skypeで「ちょっと瞬断します」と5分前に連絡するだけで済むものから、親サービスの月イチメンテナンス日以外は基本的に不可能なものまで様々でした。決済はそれらよりさらに面倒なので、全力で回避する必要があります。
もう一つ大きな理由として、メンテナンス停止を伴うリリースには必然的に制限時間が設定されてしまうというものあります。
時間に追われて焦って作業すると、人間はすぐミスをします。倍の時間がかかるとしても無停止手順を組むことで、想定外のことが起こっても落ち着いて対処できます。
おわり
自分で考えたアーキテクチャがきれいにハマる瞬間は楽しいものです。単一障害点が減れば結果的に自分やチームを助けることにもなります。
去年末にかけては他にも幾つか大きなアーキテクチャ変更をやっているので、その話も気が向けばやろうと思います。
インフラをやってきた話
この記事はpyspa advent calendarの6日目として書かれました
どうもfeizです。前回の投稿日が2013年とかでちょっと引きました。
好きな牛丼屋は吉野家です。
去る7月より、20台の大半を過ごした株式会社ビープラウドを退職し、BASE株式会社に入社していました。
現在はPAY.JPというサービスでインフラをメインに担当しています。
長いこと勤めた会社を辞めて一区切りついたところで、とりとめもなくポエムでも書いてみようと思います。
§1
ここ数年、webサービスのインフラの仕事を担当することが増えた。増えたどころか、ほぼ全案件インフラ担当を兼任していたように思う。
新卒の頃から数えても、「サーバーをインフラの人に頼んで工面してもらう」みたいな経験自体があんまりなく、大体自分で作ったり管理したりしていた。小さなチームでばかり仕事をしていればまあ当然かもしれない。
§2
そんな環境に置かれていると、どうしてもインフラに真剣に取り組まざるを得なくなる。サービスが落ちたとき治すのは当然自分だし、インフラが壊れたときの被害はコードのバグの比ではないからだ。
ぱっと思いつくだけでも、mysqlのdatadirとreplicationがぶっこわれて15時間ぶっ続けでリカバリしたり、本番系インスタンスが14台中10台ぐらい壊れて死にそうになりながら直した記憶がよみがえる。もちろんどちらも朝までコースだった。
こんなことばかりしていては早晩カロウシしてしまうわけで、落ちないインフラについて考え始めることになる。自分が死なないために。
§3
一言に落ちないインフラと言っても、実現するのは難しい。ソースコードの文字が勝手に変わることはないが、インフラは平気で経年劣化したり何もしてないのに突然プロセスが死んだりする。
これを並列化してあれにバックアップを用意して…などと考えているだけで日が暮れる。
コードなんか触ってる場合ではない。コードを書くのは好きなのだが、それ以上に夜安心して眠れるようにしたかった。
考えたインフラ構成を実際に作るのも大変なので、必然にfabricやらansibleやらのプロビジョニングツールにどっぷり浸かることになったりして、どんどんインフラに関わる時間が増えていった。
§4
ある程度詳しくなってくると、チームを組んでも大抵インフラ仕事をやることになる。
自分の勉強にはいいのだが、逆にそれ以外の人がアプリの人(というかnotインフラの人)になってしまうという問題があったように思う。
かといって無理やり誰かにやらせても良い結果にならないのは明白なので、どんどんインフラの人化が加速していくことになる。
今思えば、あのあたりで技術継承の一つもできていればよかったのだろうなと思う。
§5
そういう仕事を続けた結果、feizはインフラの人になった。
必要に駆られて取り組み始めたことではあるが、なんやかんやで結構楽しんでやってはいる。
耐障害性の確保やら無停止メンテナンスの計画作りはパズルめいた面白さがある。
コードを書く時間は減ってしまった(今はほぼ0)が、書いたときに出せるものの質は上がったように思う。
インフラの限界がどこかを分かってコードを書くと、障害を未然に防げたり、パフォーマンス問題で詰んだりしなくなる。
何より、サービスのあらゆる場所を把握して手出しができるというのは、ストレスが無くて良い。
§6
今は決済代行サービスのインフラという、字面だけ見れば結構シビアなポジションに就いているが、まだ成長途中ということもあってなんとかやっていけている。
課題は山積みだが、おかげでしばらくは飯の種と勉強のネタには困らなさそうだ。
何年後かに続く。明日は岡野先輩です
Chromeデベロッパーツール拡張の話
艦これ向けChrome Extension 「艦これインスペクタ」を作りました - logiqboard の続き
艦これインスペクタを作る際に、devtools拡張の日本語情報がなくて結構苦労したので、調べたことをまとめておきます。元ドキュメントを読む際の理解の助けになれば幸いです。
実装例として feiz / kancolle_inspector / source / — Bitbucket も参照するとよいと思います。
基本的なこと
Chrome extensionにはデベロッパーツールを拡張するためのAPI chrome.devtools.* が用意されています。
Extending DevTools - Google Chrome
デベロッパーツールにパネル(タブ?)を追加したり、インスペクト対象のウィンドウにcontent scriptを送り込んだり、webRequestでは取れないHTTPレスポンス内容を取得したりできます。
デベロッパーツールを真面目に拡張する人は中々いないと思いますが、艦これインスペクタのようにchrome extensionでHTTPレスポンスの内容を覗き見したい場合は実質devtools拡張しか無いと思います。*1
devtools_page
devtools APIは通常のbackground_pageやcontent script上では参照できず、devtools_pageという"デベロッパーツールのバックグラウンドページ"みたいなものの中でのみ参照できます。
逆にこのコンテキスト内ではchrome.devtools.*とchrome.runtime以外のAPIが参照できません。
それらのAPIを使うときは、background側にchrome.runtime.postMessageでデータを投げ*2、向こう側で処理してもらう必要があります。
また必然的に、動作させるためにはデベロッパーツールが起動している必要があります。なので、違和感なく使えるツールにするのはなかなか難しいです。
特定のwebサイト用のdevtools拡張
manifestのドキュメントを見た限りではcontent_scriptのような「特定のサイト上でdevtoolsを起動した時だけ読ませる」設定はないようです。したがって、どんなページでデベロッパーツールを起動してもdevtools_pageは読み込まれてしまいます。
インスペクトしているページのURLは以下のようなコードで判別できるので、艦これインスペクタでは以下のようにしてパネルの追加を制御しています。
chrome.devtools.inspectedWindow.eval('document.baseURI', function(page_url) { if (!is_kancolle_page(page_url)){ return; } chrome.devtools.panels.create("艦隊情報", 'icon/icon.png', 'html/panel.html'); });
デバッグ
以下のとおり。MacではCmd+Shift+Iです。
@kyo_ago ウィンドウ状態のデベロッパーツールを選択してShift+Ctrl+J(Windows)すると、デベロッパーツールを対象にしたデベロッパーツールが起動するのですが、これは試されましたか? pic.twitter.com/O3oYVQI586
— syoichi (@syoichi) 2013, 2月 25
番外: extension内でevalする
(devtoolsだけでなく)chrome.*が使える場所ではevalやFunctionが使えません。テンプレートエンジンとかを組み込むときは面倒ですが
- manifestでsandbox化するhtmlのパスを指定
- 指定したhtmlをsandboxを使いたいページにiframeで読み込む
- evalを使うコードはsandboxページの中で処理するようにし、外部とのやりとりはwindow.postMessageで行う
という手順が必要です。
まとめ
- レスポンスを読みたいなら
Firefoxを使えdevtools拡張をつかう - なにをするにもpostMessage地獄なので覚悟する
艦これ向けChrome Extension 「艦これインスペクタ」を作りました
全国100万の提督の皆様こんばんは、feizです。
夏頃に始めたカタカナ鯖のぼくも秋イベではE4E5と戦うレベルになってきました。
さて、艦これのイベントはいかにして資材を効率よく貯めるかが一つのポイントですが、そのためのキラ付け計算が果てしなくめんどくさい。
なんとかコンディション値を簡単に見たい…ということで、そういうextensionを作ってみました。
Firefoxのエクステンションと名前が被ってるのはきにしない。
機能
インストールして艦これページ上でインスペクタを開くと艦隊情報ってタブが増えてます。
タブを開いて艦これを遊んでいるといつの間にか艦隊情報っぽいものが表示されてます。
艦名が表示されるべきところにはIDっぽいものが表示されていますが、今のとこ仕様です。並び順は合っているので、位置で判断してください。
なぜ作ったか
艦これ用extensionといえば 艦これウィジェット がありますが、コンディション値などを表示する機能がありません。
理由は 加賀さんと僕(実装編)〜艦これウィジェットの課題と実装〜 に詳しいですが、要は通常のChrome Extensionではレスポンスの内容が取れないということのようです。いきなり詰んでます。
同ウィジェットではその問題を解決するために暗黒魔術OCRに手を染めているようですが、闇の魔術が使いこなせるほどの魔力はぼくにはありませんでした。
真面目に考えてもキラキラを画像認識は相当つらい上に数字が分からないと意味がない
どうやって実装するか
これを使って艦これウィジェットに機能追加してみようかとも考えましたが、devtoolsを常に開いていないといけないので、既存のUIと合わない。
色々考えましたが、@monjudoh の「もうインスペクタに表示しちゃえば?」の一言で拡張機能パネルに表示する案を思いついて開発開始。のべ1週間ぐらいで開発して公開に至ります。
今後
ぶっちゃけcondの表示ができた時点で個人的な目的は達成されてますが、折角公開したのでもうちょっとマシにしてみようと思います。
スペシャルサンクス
- ネタ元: https://github.com/kageroh
- 実装参考: @otiai10
- アイデア: @monjudoh
2年使って気づいた、fabricでサーバーセットアップすることの微妙さ
fabricでサーバーセットアップすんの、使い込めば使い込むほど愚策だったな
— 沖縄スライス (@feiz) 2013, 7月 22
@everes @isoparametric (chefとかが台頭してきたからこその感想ではあるけど)冪等に書くのたいへんだし、冪等に書けないとfabricの@rolesがうまく働かないですね。
— 沖縄スライス (@feiz) 2013, 7月 22
このへんの話。
結論はツイートの通り、冪等性の確保が大変すぎるというところ。んでサーバーセットアップ系のスクリプトは冪等に書けないといろいろ困るわけです。
rolesと冪等性
普通はサーバー構成はスケールアウト前提で設計するので、同一の役割をこなすサーバーが複数置かれることになります。こういうのの管理にはいわゆるrole機能が便利です。
env.roledefs = { 'app': [ '192.168.0.1', '192.168.0.2', ], 'db': [ '192.168.0.3', '192.168.0.4', ], # ... }
特定のサーバーに対して実行するのは面倒なので、折角なら以下のように使えればいいんですが
@task @roles('app') def setup(): # setup
このsetup関数が冪等じゃない場合、setupが安心して使えるのは初回セットアップの時のみです。
appを1台増やしたら、setup叩いとけばおわり とはいきません。新規のサーバー以外にどんな影響があるかわかったもんじゃないので。
仕方ないので以下のようにして回避したりしました。
@task def setup_one(): setup()
role消えちゃってます。代わりにプロンプトでホスト名を入れることで、setupを単体に対して実行できるようになってます。
こうなるとroleで纏めるより、単体狙いのタスク(上でいうsetup_one)のみを用意したほうがマシという本末転倒な状態になってしまいます。
適したツールを使おう
もちろんsetup関数が完璧に冪等にかけるならfabでも十分です。
でも手書きで冪等に書くのは骨が折れるし、そこに労力掛ける必要もきょうび無いと思うので、こういう作業の自動化にはchefなりansibleなりのCMツールを使ったほうが良いと思います。
だからと言ってfabricとかcapistranoがお払い箱かといえばそうでもなくて、デプロイとか再起動とかのサーバー操作にはやはりfab/capが鉄板です。chefでやろうとするとできなくはないけどもやっとします。
適材適所というやつです。
djangoのQuerySetで大量のデータを扱う
メモリに載り切らないような大量のデータを扱うときは何かしらの工夫が必要で、djangoのQuerySetも例外ではありません。
簡単な例として、以下のコードをメモリが多くない環境で実行すると、ループに入る前にメモリをドカ食いして落ちます。
many_many_objs = MyDBLog.objects.all() # 1億件ぐらいあるとする for log in many_many_objs: print(log.body)
どうする
件数が大きくなりえるQuerySetを扱う場所では、QuerySetを分割して扱うようにします。
QuerySetはスライスが使えるので、うまく活用しましょう。
def chunked(queryset, chunk_size=1000): start = 0 while True: chunk = queryset[start:start + chunk_size] for obj in chunk: yield obj if len(chunk) < chunk_size: raise StopIteration start += chunk_size
これを最初のコードのqueryset利用箇所に仕込めばOKです。
for log in chunked(many_many_objs): print(log.body)
QuerySetの評価時に何が起こるのか
QuerySetは評価されると、自分が保持している条件に応じてクエリを投げ、ヒットしたすべてのデータをオブジェクトに変換してQuerySetオブジェクト内のキャッシュ領域に保持します。
__iter__を含むQuerySetの大概のインターフェースは、このキャッシュされたオブジェクトリストを使う(無ければlen(self)で無理やり評価してキャッシュを作る)実装になっているので、普通に使っている限りは必ず全件オブジェクト化されてしまうと考えたほうがよいです。
どうしてもキャッシュをかわしたい場合は、QuerySet.iteratorというメソッドがあります。このメソッドはクエリの結果を1行ずつfetchしてオブジェクトに変換するイテレータで、ここからオブジェクトを得る分にはキャッシュされません*1
QuerySetだけじゃない
最初は、QuerySet.iterator使えば万事解決と思ってましたが、そもそも件数が多いとDBクライアント*2に保持される生のクエリ結果自体が大量になるので、オブジェクト化される前にやっぱり落ちます。
python-mysqldbでは結果をクライアントに保持するか、サーバー側に置いておいて少量ずつクライアントでfetchするかは選べるようですが、サーバー側のメモリを食うようになると今度は落ちる箇所がDBサーバーになってしまうので、より酷いことになります。
結局
色々調べた結果、スライスする方法に落ち着きました。
djangoを使う上では、QuerySetの評価時になにが起こるのかと、どういうタイミングで評価されるのかは、ソースを読んでよく把握しておかないと結構ハマります。
"ドキュメントを部分的に公開/非公開にしてビルドする"の実用例
Sphinx Advent Calendar 2012: 18日目
おひさしぶりのfeizです。久しぶりついでにはてブロに移行してみました。
先日、こんなcookbookを投稿しました。
ドキュメントを部分的に公開/非公開にしてビルドする :: ドキュメンテーションツール スフィンクス Sphinx-users.jp
仕事でお客さん向けのドキュメントを書いてると結構需要のある話なんじゃないかなーと思ってます。
ただ、cookbookには実際の利用例みたいなものをあまり入れられなかったので、この機会に実用例や設計手順をまとめてみようと思います。
前提とするシチュエーション
以下のようなシチュエーションを想定します。
- とあるB2BサービスのAPIサーバーを書いている
- 自社(開発)→サービス元(サービス運用)→エンドユーザー(複数社)
- サービス元とエンドユーザーと自社内の3ロールにそれぞれドキュメントを提供しないといけない
ドキュメント間の関係の設計
内容を考え始める前に、各ドキュメントがどういう関係になるかを設計しておきます。
大抵の場合、自社向けドキュメントには全部載せの内容が出るのが望ましく、自社から離れるに従って内部仕様が削られていくような構造になってほしいはずです。
つまり
自社向けドキュメント⊃サービス元向けドキュメント⊃エンドユーザー向けドキュメント
のような構造です。
これにしたがってビルド時のタグ付けを決めます。
自社向け | タグなし |
サービス元向け | service |
エンドユーザー向け | user |
更に、エンドユーザーには複数社あるので、"user"に加えて各社それぞれにタグを割り当てておきます。
A社 | Asha |
B社 | Bsha |
ドキュメントの構造設計
まずは全部込みの構造を考えてみます。
/spec/ => サービス概要/用語説明など /admin/ => 管理ツールの使い方 /api/ => API毎の詳細仕様 index.rst
こんなかんじにしましょう。
ここから、更に各出力設定によってどういう風に出し/消ししたいかを考えます。
1. /api/
user向けには内部APIは出してはいけないので、userタグがついた場合にはそれらが消えるべきです。
/api/developer/ディレクトリを掘り、内部APIに関するドキュメントは全てそこに纏めることにします。
後は、conf.pyでdeveloperディレクトリのexclude設定を書けばOKです (参照)
if 'user' in tags: exclude_patterns += ['/api/developer/*']
ここで、"/api/developer/*"の代わりに"*/developer/*"という指定にしておくと、どんな場所でもエンドユーザーに見せたくないものは全部developerディレクトリを掘って入れるだけで消し去れます。
以降はこの設定をしたものとして進めます。
2. /service/
サービス概要は、文書丸ごとの出し分けよりはセンテンス単位での出し分けが多そうです。
センテンス毎の出し分けは、onlyディレクティブで実現できます。(参照)
ただし、参照記事にもあるようにセクションの出し分けはできません。
装飾をつけた文字で代用するなどの工夫が必要です。
/service/grossary.rst
:`みんな死ぬしか`: ないじゃない .. only:: not user :`ユーザーのみんなには`: ナイショだよ
3. /admin/
serviceとほぼ同じですが、各ユーザー向けの情報(管理画面のログインIDなど)を載せる必要があるかもしれません。
このような場合に各社毎のタグ定義が役に立ちます。
admin.rst
管理画面ログインIDについて ============================= .. only:: Asha or not user :ログインID: asha_1234 .. only:: Bsha or not user :ログインID: bsha_1234
このような記述にしておくと、非ユーザー向けドキュメントには全ログインIDが表示され、ユーザー向けドキュメントには対象のユーザー向けのものしか出ないという挙動になります。
書く
出し分けの構造設計までできれば、あとは書くだけです。
ひたすら書いて下さい。
まとめと注意点
- 設計重点
この手法をうまく適用するには、事前の設計がとても重要です。
適当に書いていると、excludeしたドキュメントにリンクが必要になったり、セクションを消す必要が出てきたりしてスムーズに進められません。
- 動作確認する
表示非表示のロジックを仕込み始めると、当然バグを仕込む可能性が出てきます。
adminの項で例に出したログインIDの出し分けなどは、間違って出てしまうとえらいことになります。
動作チェック/出力内容チェックのワークフローは用意しておくべきでしょう。
- 頒布物にソースを含めない
「絶対に見えてはいけない」情報を出し分ける場合には、必ずconf.pyでhtml_copy_sourceをFalseに設定しておくようにしましょう。
ここをTrueにしたままだと、ソースのrstファイルが配布物に含まれてしまうため、onlyで隠しているつもりの情報が見えてしまいます。