Chatwork Creator's Note

ビジネスチャット「チャットワーク」のエンジニアとデザイナーのブログです。

ビジネスチャット「Chatwork」のエンジニアとデザイナーのブログです。

読者になる

3週間で出来たEvent SourcingでCQRSなアーキテクチャ上でのHBaseのupgrade

プロジェクトのリーダーである、cw-sakamotoがこの超重要プロジェクトを終え、軽い燃え尽き症候群になっているので、代わりにcw-tomitaが代筆しております。
マラソンガチ勢(ベストタイム3時間17分)であるcw-sakamotoによって、Carbon X(50マイルでの世界記録を出したランナーが履いていたランニングシューズの名前)と名付けられた、このHBase upgradeプロジェクト、

dogsorcaravan.com

その名に恥じない、プロジェクト開始から終了まで3週間という短納期で完遂することができました。今回はこのプロジェクトに関して書きました。

tl;dr

  • Event SourcingでCQRSなシステムの上で動いてるHBaseのversion upをblue-green形式で実施した。
  • 計画から実行まで3週間程度で完了するというスピード感で、大きな事故もなく凄く上手くいって、これまでの運用では経験できなかった新しい感覚だったので、是非シェアしたい :)

Databaseのupgrade

Databaseのupgradeは基本とても難しいし、本番データベースのオペレーションはめちゃめちゃ(他の本番オペレーションの100倍くらい)緊張します。この感覚はやったことない人には伝わりづらいかもですが、実際にやったことある人は99.9%の人が同意してくれると思います。また、コストがかかるしリスクも大きい割には、多くの場合はビジネス的に新しい価値をもたらすものではないので、どうしても後手後手になりやすい部分だったりもします。一方で、Databaseがアンタッチャブルなものになってしまうのは、非常に大きなビジネスリスクなので、ここをどのようにハンドリングしていくかは、多くのシステム開発/運用者にとって、普段は隠れているけれども、とても大きな問題であり続けていると思います。

Upgradeの手法

一般的に、2つの手法があります。

rolling upgrade

1つ目はrolling upgradeです。一台ずつ順番にversion upgradeを行っていくアプローチです。 以下の記事によい説明があったので、もし、この言葉の意味がピンとこない場合は、こちらを参照してください。

https://www.quora.com/What-is-meant-by-a-rolling-upgrade-in-software-development

  • 長所

    • オリジナルのデータが1箇所にしか存在しない。このため、後述のblue-green方式と違って、データをどのようにsyncするか、どのようにsyncされていることを保証するのか?ということを気にしないで良い。
  • 短所

    • upgradeが終わったらrevertする方法がない。このため、もしupgradeによって事前のテストで検知できなかったパフォーマンス問題や、クリティカルなバグが発生しても、サクッと元の状態に戻すということができない。
    • Databaseサーバは長く動いていることが多く、過去の緊急オペレーションの残骸などの影響で、テスト環境で完全に状態を再現しづらい/できない状態になっていることがある。このため、テスト環境で完璧に動作したコマンドがエラーになったりすることもあり、起こった問題に対してはその場で対処していく必要がある。また、その可能性があること自体が、担当者の精神的負荷を大きく上げる一因ともなる。

f:id:cw-tomita:20190701225250p:plain
rolling-upgrade

blue-greenデプロイメント

もう1つの手法はblue-greenデプロイです。新しいversionのdatabaseを別に立てて、アプリケーションの向き先をエイっと切り替える方式です。 もし、この言葉の意味がピンとこない場合は、こちらの記事を参照してください。

BlueGreenDeployment

この手法はwebアプリケーションのデプロイの文脈でよく使われれていて、上の記事でもその文脈ですが、この記事の"router"という単語を"application"に、"web server"という単語を"databae"に置き換えると、同じアプローチを取ることができます。

  • 長所

    • 稼働中のdatabaseサーバには触らなくてよい。これによってrolling upgradeに比べて、精神的負荷がだいぶ小さくなる。
    • テストで発見しきれなかった予期せぬ問題が発生した時、サクッと元のdatabaseに戻すことができる。また、段階リリースが可能なので、不備があった時の影響範囲を最小限にとどめやすい。(ただし、"短所"に記載してあるように、これを実現するには、比較的技術的に難しい、新しいcluserから元のclusterへのデータ同期が動いていることが前提となります。)
    • ↑により、事前の負荷試験をある程度圧縮できるので、移行作業をスピーディに進めやすい
  • 短所

    • databaseが両方のclusterで確実に正しく同期できていなければならない。upgrade後の切り戻しを可能にするには、元のclusterから新clusterだけでなく、新clusterから元のclusterへの双方向の同期が必要となるが、これは多くの場合、かなり難しい。アプリケーションで常に両方に書き込むということも可能ではあるが、単純に実装すると片っぽだけオペレーションが失敗した時にデータ不整合が起きるので、ここのハンドリングをしっかり作り込む必要があるが、それもかなり難しい。
    • しばらくの間、倍のサーバリソースが必要となるのでコストがかさむ。また、オンプレミスのシステムの場合、そもそも実現が難しいこともある。

f:id:cw-tomita:20190701230448p:plain
blue-green

今回採用した方法

基本的には、blue-greenデプロイ方式になります。一点違いとして、Kafkaがevent sourcingのハブとして存在しているので、blue-green デプロイの"短所"にあげた、データ同期の問題への対処が大幅に簡単になりました。

現在のアーキテクチャ

まず、現在のアーキテクチャの簡単な説明です。社内で、このチャットメッセージ部分のサブシステムを"Falcon"と呼んでいるので、隼のアイコンが図の中に登場しています。

  1. ユーザがメッセージを投稿すると、write-apiがKafkaにメッセージを積む
  2. read-model-updater (略して"rmu")が、Kafkaからメッセージをfetchし、HBase用のread-modelに変換し、HBaseに保存する
  3. ユーザがメッセージを読むときは、read-apiがHBaseからメッセージを取り出す

f:id:cw-tomita:20190701215258p:plain
falcon-architecture

この記事の中ではなぜ我々がCQRSを採用したかという部分の説明は行いません。ご興味ある方は、以下に過去の登壇資料のリンクを記載していますので、こちらをご覧いただければと思います。

ChatWorkの新メッセージングシステムを支える技術 - Speaker Deck

Worldwide Scalable and Resilient Messaging Services by CQRS and Event…

Database upgradeの流れ

このアーキテクチャをベースに、どのようにHBaseのupgradeを行ったのか紹介していきたいと思います。

Step1: 新しいclusterを準備し、定期的に取得しているbackupから初期データのrestoreを行う

f:id:cw-tomita:20190701220107p:plain
step1

Step2: 別のKafka consumerを準備(図の中では"rmu2")し、Kafkaから新しいclusterへのデータ同期を行う。Kafkaに積まれている古いイベントを再実行することで、初期データとの差分を埋めます。このとき、初期データと重複したメッセージを処理することもあるので冪等性 (=同じメッセージが複数回処理されても結果が同じである状態にする)はアプリケーション側で保証する必要があります。

f:id:cw-tomita:20190701220606p:plain
step2

Step3: 新しいconsumer(rmu2)が最新の状態に追いついたら、新しいclusterを向いたread-api(図の中の"read-api2")を準備します。そして、段階的に新しいread-apiにリクエストを送り込み、問題が発生しないかを確認します。

f:id:cw-tomita:20190701220827p:plain
step3

Kafkaからのイベント反映は全て非同期で行われているため、ミリ秒単位で反映が完了しているとしても、瞬間瞬間では2つのclusterの間では状態に差があります。私たちも、この状態差によってread-apiの並行稼働中に問題に遭遇しました。この状態差分がどういう問題を引き起こすかははアプリケーションロジック依存になるので、どういう影響が出そうかを事前に予測し、理想としてはその問題が計測可能にし、明確な切り戻し基準が準備できると良いと思います。また、私たちはDNSでのround robinでの振り分けを行ったのですが、read-apiの手前にNginxやEnvoy等の別レイヤーがいて、ユーザ属性等に応じてリクエストの送り先を制御できるのであれば、それによって問題の発生をなくせるかもしれません。

f:id:cw-tomita:20190701222404p:plain
step3-follow-up

また、このPJの振り返りをやりながら気づきましたが、リクエストのミラーリングができれば、ユーザ影響を気にせずに、本番環境の負荷をかけて新しいcluster + アプリケーションのパフォーマンスを確認することも可能そうです。

f:id:cw-tomita:20190703114324p:plain
step3-follow-up2

Step4: 切り替えて大丈夫と確信が持てたら、新しいread-apiに100%切り替えて、古いcluterを廃棄する。

f:id:cw-tomita:20190701222707p:plain
step4

このアプローチのどこがいいのか?

改めて、通常のblue-greenとの違いを説明したいと思います。blue-greenでは、両database clusterの間でデータが同期できていることを保証しなければならない、かつ、切り戻しを考えると相互に同期させなければならないという難しさがありました。通常、データ同期はdatabaseが提供するレプリケーション機能を使って実現することが多いかと思いますが、このアプローチでは、databaseが提供するレプリケーション機能ではなく、自分たちが実装したアプリケーションを複数走らせて、結果的にデータが同期した状態を作りだしています。この違いが大きなメリットをもたらすと感じています。

まず、単純に2系統のアプリケーションが動いているだけなので、どういうレプリケーション(旧 → 新 or 新 → 旧 or 旧 ⇄ 新)が動いているのかを気にする必要がありません。片方向レプリケーションはそこまで難しくないことが多いですが、upgrade後の切り戻しを考えて、途中でレプリケーションの方向を変えたり、双方向レプリケーションをするとなると、だいたい難易度がグッとあがりますが、このアプローチでは独立した2つのアプリケーションを走らせればOKです。新clusterへの切り替え後も、元のKafka consumerを動かし続けておくことでデータは反映され続けるので、切り戻しを行いたいときはアプリケーションの向き先を変えるだけです。

f:id:cw-tomita:20190701224552p:plain

次に、cluster間のversionの互換性を気にする必要がありません。databaseが提供しているレプリケーションのための機能を利用する場合、バージョンの制限が存在しますし、また、場合によってバージョン間の相性問題のようなものに悩まされたりすることもあるかもしれません。しかし、このアプローチでは、それぞれのdatabaseにデータを投入する、独立したアプリケーションを準備すればOKです。大体の場合、普段滅多に使わないレプリケーションの機能を頑張るよりは、データの投入ロジックを変更したアプリケーションを準備する方が、コストが小さく済むのではないかと思います。また、今回の我々のケースでは同じ種類のdatabaseのupgradeでしたが、理論的には全く別の種類のdatabase(e.g. DynamoDB)への切り替えも同じアプローチで行うことができます。その場合、初期データを投入するためのdata migrationのバッチプログラムも別途準備する必要がありますが、それを踏まえても、現実的、かつ、実行コストの低いdatabaseの移行プランが計画できるのではないかと感じています。

f:id:cw-tomita:20190701223452p:plain

まとめ

CQRSやevent sourcingというのは、ソフトウェアのアーキテクチャの文脈で出てくることが多い単語です。一方、システムを運用する立場から見たときは、単純なCRUDのシステムと比べるとレイヤーが1つ多くなるので、インフラの複雑性(構成、モニタリング)は増し、運用コストも大きくなります。私は立場的に後者の観点からシステムを見ることが多く、正直、果たしてこの構成はペイしているのだろうか?と時々心の中でモヤモヤしている所がありました。でも、今回、CQRSがシステム運用にもたらすメリットを享受し、今後発生しうるdatabase関連のオペレーションの精神的負荷を大きく下げてくれるものであると認識し、この構成がだいぶ好きになりました :)

次のチャレンジ

今回はEvent busであるKafkaを利用することで、後ろにいるdatabaseのupgradeを、リスクを抑えてできたよっていう話だったけど、Kafkaのupgradeはどうするの?という疑問を感じている方がいらっしゃるのではないかと思います。そして、それこそが我々の次のチャレンジになりそうです。単純なrolling upgradeも1つの手ではありそうですが、よりリスクを抑えた、万が一の際にもサービス影響の出ないやり方を模索していきたいです。

そして、Chatworkでは、ミドルウェアのupgradeが大好き!というエンジニアを絶賛大募集中です!

corp.chatwork.com