リリカルメール配信機能のアーキテクチャ

はじめに

RubyのMongoDBドライバには不具合があります。
ある条件でfind and modifyを使用すると失敗するというものです。
リリカルメールの配信機能を実装していた時にこれに気付いてチケットを作ったのですが、次のver 1.9.0で解消されるとのことでした。
実装していたのは去年の9月で大分前の話ですが、この知らせを受けて思い出したので、配信のアーキテクチャについて書こうと思い立ちました。

配信機能

ユーザーが指定した条件に一致する投稿をリアルタイムでメール送信するのが配信機能です。
例えば同年代の人の投稿のみ受け取りたければ20代と指定しておくと、20歳から29歳までの人の投稿が自動的に届くようになります。
便利な機能ですがリリカルメールの中で最も負荷が高い機能です。
例えば秒間10投稿あるとして、配信に登録しているユーザーが1万人いるとすると、条件に一致するかの検索を秒間10万回行う必要があります。

リリカルメールの検索はタグがツリー構造を取っているために、完全一致の検索よりは重い処理になります。具体的には、タグのツリー構造をマテリアライズドパスで表現していて、正規表現の前方一致による検索を行っています。

そのためスケール可能な構成にする必要があり、リリカルメールではキュー・ワーカーモデルの構成をとっています。
投稿があるとキューに貯めて、ワーカーがキューに対して検索を行うというものです。ワーカーは2種類いて、検索ワーカーと、メール送信ワーカーです。
検索ワーカーが条件にヒットした投稿を別のキューに貯めて、そのキューからメール送信ワーカーがメールをユーザーへ送信します。
キューというとRabbitMQやZeroMQを思い浮かべる方が多いと思いますが、リリカルメールはMongoDBを使っています。
MongoDBはRDBMSの代替として使っていて、ミドルウェアを増やして複雑にしたくなかったので、キューについても利用しようという考えです。
ただ、MongoDBにはトランザクションがありません。それでどうやってキューを実現するかというと、先ほどのfind and modifyというMongoDBのコマンドです。
このコマンドは参照と更新を一貫性をもって実行することができます。MongoDBでトランザクションっぽい操作ができるコマンドです。
これを各ワーカーがMongoDBに対して実行し、各ユーザーの検索条件を取得して検索済みの時刻を設定するということをやっています。

なお、MongoDBはレプリカセットを使って負荷分散しています。つまりセカンダリ優先の読み込み設定としているのですが、書き込みはマスターにしか行えないのにも関わらず、ドライバはセカンダリに書きにいこうとして失敗するというのが最初に書いた内容です。

実のところ、スケールアウトする必要性は今のところないのですが、一人でやっている以上は必要に迫られてから実装するのでは遅いので、サーバーを追加すればスループットが上がるというのを意識して開発をしています。