現代のフォローリスト

数日前の話、同僚からサポートissueの調査について相談を受けた。内容は「フォローリストが正しく表示されない(数人足りない)」というものだった。

最初にうちの会社の環境を紹介しておくと、メイン事業はモノリシックで巨大なrailsで書かれている。最近は機能や開発を担当するチームごとに切り崩してマイクロサービス化される事例も増えてきた。今回調査したフォローシステムを提供するアプリケーションもメインのrailsアプリケーションとは別に小さなrailsとして動いていた。

事前調査していた同僚から「DB上のソーシャルグラフは正常だった事」と「名前のないユーザーはフォローリストに表示されない事」を教えてもらった。今回のサポートissueに挙がってきた事例もユーザー名が無いことが原因の様で、それはシステム上意図してない挙動だった。

調査の手始めにユーザー名の管理について調べた。DBはアプリケーション毎に別のものを参照していた。ユーザー名はメインのrailsとフォローシステムのrailsで二重管理されていて、メインのUserモデルに変更があると内製のPub/Subシステムを経由して変更内容が同期されるようになっていた。

Pub/SubシステムはQueueを持っていて、イベントがpublishされるとQueueにどんどん積んでいく。積まれたイベントは非同期にデキューされ、紐付けられたDockerイメージが立ち上がり事前に設定したスクリプトがその中で実行される。

デキュー後の処理はごく稀に失敗することを把握していたので今回もそこが原因だろうと考えた。

ところが影響範囲を調べてみると数十万人(直近数日でも数百人)の規模で起こっていることが判明して読みが外れていることに気づいた。デキュー後の処理が失敗するのは一日数件程度なので、もっと根本的なバグがありそうだった。

調査を再開してUserモデル内の該当イベントがpublishされている箇所を調べた。最近入った何らかの変更でイベントが飛ばなくなったみたいな話はいかにもありそうだ。そこで該当のイベント名でgrepした見たところやはり見当たらなかった。

「おっエンバグの線で当たりかな..」と思ったがこれも間違いだった。Userレコードが書き換わったときに該当イベントのpublishをチェックするユニットテストが見つかった。 もしイベントがpublishされなくなっていたらこのテストケースで気付くことができる。しかしmaster CIはずっとgreenのままだったし、当たり前だけれど手元でテストを実行してももちろん成功した。ここでエンバグによってpublishイベントが消えたという線はなくなった。

やや脇道に逸れるけど一体どんな仕組みで該当イベントをpublishしているのかどうしても気になったので該当モデルのコードをもう少し読んでみた。すると何かのイベントをpublishしている処理を見つけた。注意深くコードを追うとイベント名がメタプログラミングチックに組み立てられていることに気づいた。「なるほどだからさっきのgrepでは引っかからなかったのか」と納得が入った。一方で「なんてgrepビリティが低いコードなんだ。こんなに処理を汎化する意味はあるんだろうか...これだからrubyは..」と悪態をつきたくなった。(もちろんほとんど冗談の文脈。 rubyは別に悪くないし grepしさすさを重視するかは個人の好みに分かれそう)

イベントのpublishまでは正しく行われていることがわかった。次に疑うべきはイベントが流れる経路とsubscribe側の処理なのだけどここは疑う余地がほとんど無かった。他のPub/Subシステムを利用したサービスは正常に動いていたし、さらに言えば内製のPub/SubシステムはほとんどAWSのサービスのラッパーみたいなものでAWS側で障害が起こったりしない限り原因になりえそうに無かったからだ。仮に障害が起こっていたら技術基盤かインフラチームの人がいち早く気づいて共有してくれると思うのでこのあたりを疑うのはすぐにやめた。

こうなってくると手詰まりで「申し訳ないけどちょっと原因がわからないな ちゃんと時間を確保して調べてみないとダメそうですね」という話をした。

他の作業も残っているので一旦調査をやめようかと思っていたのだけど、ふと昨年関わった仕事のことを思い出した。 そういえばユーザー登録基盤はメインのrailsから引き剥がされてマイクロサービス化されていたのだ。

「ユーザー登録基盤のrailsはメインのrailsとDBを共有していて、ユーザー登録基盤のrailsからDBを書き換えるとイベントが発火しないのでは」という仮説が偶然思いついた。結論だけ言うと大体合っていた。正確にはほとんどのテーブルは共有してなかったがusersテーブルのみ直接参照/書き込みしていた。 (あまり詳しくないので間違っているかもしれない)

railsチュートリアルに登場するフォローリストなら中間テーブルから一覧を引いてきて、ユーザー情報を列挙するだけのシンプルな実装で十分だけど、現実のフォローリストには無名のユーザーが表示されないロジックがあったり、ユーザー名はpub/subで渡ってきて登録されていたり、とても複雑な構成になっていた。

たまたま思いついた雑な推測から原因が特定できて問題は解決したのだけど、現代のフォローリストは複雑なんだなーと思ったのが印象的でブログに調査ログを書いてみた。

今回の調査でアプリケーション間でDBの共有すると状況を把握するのが大変になるので、(しょうがないケースも多いけど) 出来るだけ避けたいですね。という学びがあった。アンチパターンなことは知ってはいたけれど、どういうケースで困るのか理解できていなかった。