はじめに
QiitaにAdvent Calendarを書くのも5年ぶり(前回の記事)です、お久しぶりです。
さて、今年を振り返ってみると、SSDを5PBほど買ったなあとか、ハードの思い出もなくはないのですが、5年前とは趣向を変えて、マンガアプリのAPIの話をしようと思います。

Solidigm P5336 122TBを沢山入手した記念写真
マンガアプリのAPIのお話
さて、皆さんマンガアプリをお使いでしょうか?早速ですが、弊社が得意とする、基本無料のマンガアプリだと「ある程度使っている」ユーザーは、大体、「100作品1万話」読んでいるようなイメージです。もうめちゃくちゃざっくりですし、勿論ヘヴィに使われている方は文字通り桁違いの量読まれています。
で、それら閲覧履歴から、各種画面の作品を並べる順番とか、特に作品数が多いアプリではパーソナライズしたいわけです。
普通に考えたら、タイトルごとの集約テーブルを持ってっていう話になるんですが、マンガって後から書き足されて1話が2話に分裂したり、期間限定の話が途中でいなくなったり、途中から課金限定になったり・・・。まあ色んなことが起こりまして、パーソナライズする際に、話のデータまで持ってこないと、細かい制御できないよねー。っていうことが良くあります。
で、話ごとのデータを全部取ろうとすると、1万行かー、みたいな感じになります。弊社のAPIは基本的に「画面とAPIが1対1」で対応しているし、起動直後のホーム画面には大体数百タイトル並んでいるんですよね。業界ルールの「50ms or die」と合わせると、「キャッシュゼロから1万行読み込んで、数百タイトル選んで並び替えて、50msでAPI返せやー」という話になります。どうしましょうかね?
というのを、とあるマンガアプリのケースでお話ししましょう。
とあるマンガアプリの環境
| CPU | メモリ | SSD |
|---|---|---|
| AMD EPYC 9655P(96C/192T) | DDR5-6400 2,304GB | 11個刺さってた |
| Webサーバー | API | DB | キャッシュ |
|---|---|---|---|
| Nginx | Vert.x | Percona XtraDB Cluster | JavaのCaffeine |
Percona XtraDB ClusterはマルチマスターのDBで、どのノードにもR/Wできますが、弊社では1ノードだけをアクティブにしています。
しかし、創業当初のマンガアプリはDBがお名前.comのメモリ2GBのVPSだったことを考えると、メモリは1,000倍!創業当初は本当にお金なかったんだよな。意外とメモリが2GBでHDDでも数十億行のテーブルからリアルタイムでSELECTできるので、MySQLってよくできてるよね、ほんと。
どうやって捌くか?
ちなみに、閲覧履歴のテーブル構造はこんな感じです。
| ユーザーID | 話のID | 最後にアイテムを使って読んだ閲覧時刻 | 最終閲覧時刻 |
|---|---|---|---|
| UInt32 | UInt32 | DateTime | DateTime |
| Primary Key | Primary Key |
ここから、作品ごとに並列に読み出します。システムによってはあえて、作品のIDをPrimary Keyに挟んでいるものもありますが、意外とそこまで速度差はありません。クエリにするとこんな感じ。
SELECT * FROM 閲覧履歴
WHERE 話のID IN (SELECT 話のID FROM 話のマスター WHERE 作品ID=?)
AND ユーザーID=?
冗長なので「*」にしましたが、一応カラム列挙でSELECTした方が良いと思います。INのサブクエリ!って思うかもですが、最近のMySQLだとJOINにしてもあんまり変わらないです。
さて、引っ張っておいてなんなんですが、並列にクエリを打つしかない気がします!というわけで、このアプリでは1つのAPIリクエストあたり数十並列でクエリを発行することがあります。ここら辺は条件次第ですが、50並列に若干届かないぐらいがピークになります。Vert.x全体では1,000コネクションぐらいプールしています。
捌き方終わり・・・。
・・・こうなることは書く前からわかってたんですが、「大量のデータを低レイテンシでDBから読み出したいときは、並列にクエリを発行すればいい。」っていうのを実例付で上げてくれている人があんまりいなかったので書いてみました。
・・・Q&Aでもやりますか。
Q&A
ピークでどのくらいクエリ捌いてるの?
日にもよりますが、このアプリはコンテンツ更新のタイミングで多い日には5万クエリ/秒、イベントとかやると10万クエリ/秒っていうイメージです。
CPUどのくらい食ってるの?
実のところ、ピーク時でもtopコマンドで見ていて、MySQLの使用量は1,000%行ってないです。物理コアが96コアあることを考えれば、かなり余裕があります。多分MySQLそのものはPrimary KeyヒットするSELECTなら、軽く10倍は出せると思います。正直、コア数をもうちょっと減らして、クロックを高くした方が良かったんじゃないかとか、思わなくも・・・。
既読履歴のテーブルのサイズってどのくらい?
リアルタイムに変化しているので、何ともですが、しばらくは、100億行から1,000億行の間を旅していると思います。
パーティション切ってる?
切ってません。パーティションってログを定期的にDROPする以外で、あんまり嬉しい使い方がわかってない。
Vert.xで困ったことあった?
vertx-sql-clientで、MySQLとのコネクションプールの再接続処理に問題があったので、弊社からissueを上げたよ。1週間もしないうちに直してくれたよ。
DBにSSD使って大丈夫?
むしろ、DB「だけ」に使う分には大丈夫。マンガアプリに使ってよいかと言われると、微妙。マンガアプリのように、「SSDが暇になる瞬間がない」「画像やログなど長期間放置のファイルがある」の双方の条件を満たすと、結構バコバコ壊れます。導入したロットの半分が1年ぐらいで壊れたこともあります。メーカーの技術者の方にもご来社いただいてしまうぐらい、弊社のマンガアプリは微妙な使い方をするようです。なので、同様の使い方をされる場合は頭の片隅にでも入れておくと良いやも。
条件は僕の推測なので正しくないかもです。後「壊れる」の定義はRMAされる、つまり「良品交換」or「返金」の対象となることを指しています。
さいごに
いやー、ここまで書いておいて何ですが、実はAPIのレスポンスタイムで言うと50ms~100msの間に入っていることが多くて、実は「orの条件の後ろの方」になっちゃいそうなんですよね。
おっと、誰か来たようだ。