$30 off During Our Annual Pro Sale. View Details »

ニジエチューニング2023-12

 ニジエチューニング2023-12

ニジエインフラ

December 04, 2023
Tweet

More Decks by ニジエインフラ

Other Decks in Programming

Transcript

  1. あんただれ • 名前 ◦ ٩( ᐛ )( ᐖ )۶とか₍₍⁽⁽(◌ી( ・◡・

    )ʃ)とか ◦ 匿名ボードだとインちゃんと呼ばれてる • インフラ・バックエンドのボランティアスタッフです • バックエンドを手伝ってくれる人が増えて嬉しいです • 2014/03/18にJoin • 絵上手くなりたいです • ニジエ手伝って9年経過してるのマジ驚く • nijieinfraのXアカウントにも転職しませんか?みたいなのが来て驚く ◦ 企業?アカウントにも送ってくるんだ・・・というか本業でない • そろっと婚活したいきもする(ておくれ おひさしぶりです
  2. ニジエで使ってるMWなど • OS・管理・監視 ◦ Ubuntu, teleport, Ansible, Prometheus, grafana, statsD,

    fluentd • HTTP/Sトラフィック周辺 ◦ hitch, Varnish, Apache, Nginx, keepalived • App関連 ◦ PHP, Python • DataStore ◦ MySQL, Memcached, Meilisearch • メール関連 ◦ Postfix • 外部サービス ◦ Github, Let’sEncrypt, slack, newrelic APM, GSLB, S3, R2, Cloudflare • などなど 新しいなかま(MW)が増えた
  3. なぜクラウドやコンテナではないのか • 現状で月数万程度のインフラコストで動いている • モダンな構成にすること自体は簡単だが様々な検討が必要 ◦ コストが極端に上がらないことが必要 ◦ コストが上がったとしても見合うメリットが必要 •

    コストで問題がないか? ◦ リソース(CPU/RAM/STORAGE/EGRESSなど)のバランスを見て選定が必要 ▪ 特にニジエはトラフィックヘビーであり、 egressが高いとそれだけで辛い ◦ 昨今の円安傾向から外貨建てのサービスは可能な限り避けたい ◦ といったことを考えると積極的に載せ替える動機とならない • メリットがあるか? ◦ アダルトということもあり、突然 BANされても復旧が容易である必要がある ▪ クラウド特有のマネージドサービスを使った場合でも他で動かせる検討が必要 ◦ 負荷の増減で素早く ASができる/しやすいメリットがあるがキャッシュで吸収している ◦ IaCについてもAnsibleで実現できており運用負荷を加味しても VM運用がバランスが良い なのでしばらくは現状構成でいく(一部はコンテナ使うかも )
  4. 直近?やっていたこと • ストレージの構成変更 • DB改善 • 全文検索導入 • bot対策 •

    認証改修 • サムネ生成改修 • コスト削減 • CDN連携 • 監視改善 • などなど 他にもいろいろあるんですが
  5. 直近?やっていたこと • ストレージの構成変更 • DB改善 • 全文検索導入 • bot対策 •

    認証改修 • サムネ生成改修 • コスト削減 • CDN連携 • 監視改善 • などなど 今回はこの3つを紹介します
  6. ストレージの歴史 • そもそもニジエが運用している各サービスは画像投稿を主とするのでどう保存するかは重要 • そのため数回大規模な改修があり、ざっくり区分けすると4つに分けられる • Gen1 試行錯誤時期(ごく初期) ◦ ローカルに保存したり、s3のみとしたり、別建ての画像サーバ(以下pic)と試行錯誤していた ◦

    バックアップはない • Gen2 pic+backup(S3)構成の確立 ◦ picに保存しつつバックアップとしてS3、参照はpicのみ ▪ 正直バックアップは信頼性がない(後述) ◦ 投稿時にSFTPでpicに書き込み、同時にサムネ生成、S3書き込みをおこなっていた ◦ 途中でサムネの動的生成ができるようになりサムネ生成を停止(Gen2.5ぐらい) ▪ このあたりから自分が手伝い始めた • Gen3 書き込み速度の改善・コード整理・信頼性向上 ◦ バックアップの信頼性が担保できたので障害時にS3にrerouteするように ◦ SFTPからwebdav、S3への書き込みを非同期に変更し書き込み速度を改善 • Gen4 運用コスト削減・コード整理(現行) ◦ S3からR2に変更 ◦ 増え続けるpicを何とかするためにストレージクラスをHot/Cold/Backupに整理 ▪ pic(Hot/RW)+R2(Cold/RW)+M-DISC(Backup/W) ◦ 必ず2か所にデータがあるようにすることで信頼性と低コストを両立(pic+R2 / R2+M-DISC) ◦ ファイルに関する処理を集約
  7. ストレージの歴史 • そもそもニジエが運用している各サービスは画像投稿を主とするのでどう保存するかは重要 • そのため数回大規模な改修があり、ざっくり区分けすると4つに分けられる • Gen1 試行錯誤時期(ごく初期) ◦ ローカルに保存したり、s3のみとしたり、別建ての画像サーバ(以下pic)と試行錯誤していた ◦

    バックアップはない • Gen2 pic+backup(S3)構成の確立 ◦ picに保存しつつバックアップとしてS3、参照はpicのみ ▪ 正直バックアップは信頼性がない(後述) ◦ 投稿時にSFTPでpicに書き込み、同時にサムネ生成、S3書き込みをおこなっていた ◦ 途中でサムネの動的生成ができるようになりサムネ生成を停止(Gen2.5ぐらい) ▪ このあたりから自分が手伝い始めた • Gen3 書き込み速度の改善・コード整理・信頼性向上 ◦ バックアップの信頼性が担保できたので障害時にS3にrerouteするように ◦ SFTPからwebdav、S3への書き込みを非同期に変更し書き込み速度を改善 • Gen4 運用コスト削減・コード整理(現行) ◦ S3からR2に変更 ◦ 増え続けるpicを何とかするためにストレージクラスをHot/Cold/Backupに整理 ▪ pic(Hot/RW)+R2(Cold/RW)+M-DISC(Backup/W) ◦ 必ず2か所にデータがあるようにすることで信頼性と低コストを両立(pic+R2 / R2+M-DISC) ◦ ファイルに関する処理を集約
  8. 最初に強調しておきたいこと • 個人的な意見ですが、動いているサービスにおいては常に改修が必要です • 様々な制約によって改修のレベルがパッチ当てから式年遷宮のようなものまであります • サービスに様々な機能が追加されていくにつれて他の改修のスピードについていけなかったパッチだ らけのコードが陳腐化していき (あまり好きな言葉ではないのですが) 技術的負債と呼ばれる

    ◦ 一般的な話ですが、過去の経緯を無視してダメというのは喧嘩になるのでやめよう • これらを踏まえてGen2は試行錯誤していた Gen1を整理したものであったがそのあとの改修に取り残 されて技術的負債となりつつあった • Gen3も改修した後にもコスト等の問題があり、やはり技術的負債となりつつあった • 改修後に当時は見えなかった問題や、優先度で先送りした問題が気になってくるのは当然 過去の経緯を無視せず敬意を持とう
  9. Gen2の問題 • 大規模改修のきっかけは投稿が重いという問い合わせからだった ◦ picへの書き込みがSFTP ▪ プロトコル的に重いのもそうですが、 1ファイル毎に接続を開いて ...(そりゃ遅い) ◦

    S3へのバックアップが投稿と同時 (sync) ▪ リモートなので遅い ◦ 特に漫画投稿などの枚数が多いと顕著に遅い ▪ PHPなので並列化が困難 (最新だと状況変わってますが ) • 他にも問題点はいろいろ存在 ◦ バックアップが信頼できない ▪ 呼び出し元がpic/S3の書き込みを直ハンドルするため S3が漏れているパスがある ◦ 動的サムネも途中からなのでサムネ生成が投稿時に同時に行われているパスがある ▪ 余計重い 問題たくさん
  10. Gen3への改修 • picへの書き込みをSFTPからwebdavへ変更 ◦ プロトコル的に軽い • S3へのバックアップは直接書きこまずにローカルに一旦保存して cronでアップロード ◦ そもそもバックアップなので即時である必要がない

    ◦ ローカルに保存しても pic+ローカルの2か所で保存されており片方死んでも復帰可能 • 呼び出し元にあった各処理を全サービスに共通コードで提供 ◦ 形式チェックや投稿処理を共通化して呼び出すように変更 ◦ これで投稿の品質を一定に担保 Gen3の最大の成果はローカルを使った非同期書き込み
  11. ローカルを使った非同期書き込み • 現在のところ3種類に分けて管理している ◦ put リモート書きこみ ◦ delete リモート削除 ◦

    async_unlink pic削除 • pic削除も非同期にしているのは即時に削除すると不都合があるものがあるため ◦ 例えばプロファイル画像を消しても、旧画像参照が即止まるわけではない ▪ 様々なページをESIキャッシュしておりその期間は残しておく必要がある ◦ 退会時などに一気に削除しようとすると時間がかかる • 実行タイミングはmtimeで管理 ◦ 書き込み途中にflushされても困るのでmtimeから1分経過したファイルを対象としている ◦ 削除する場合はmtimeに予定時刻を入れておいて、経過したタイミングで削除 • 仮に失敗した場合でもそこで breakするので次のタイミングで再実行 • 再起動などのタイミングでは強制で flushがかかるように • 空のディレクトリが大量にできるので cronで定期削除している 仕組みは単純だけどこの規模だと安定している(Gen4も同じ仕組み ) 削除予定時刻(未来) 現在時刻
  12. 共通コード化と参照reroute • ファイルを書きこみをする際にはざっくりこんな処理がある ◦ 形式チェック(投稿サイズ /フォーマットなど) ◦ 書き込み先選定 ◦ パス作成

    ◦ 書き込み ◦ バックアップ • 今まではこの処理をそれぞれの投稿処理で行っていたのでそれは漏れても仕方ない • 書き込みとバックアップを同時に実行するコードを作成し既存コードを置き換えた ◦ 先の非同期と合わせてバックアップが信頼できるようになった • チェックロジックも共通化してブレが無いように変更 • バックアップが信頼できるようになったことから例えば障害やサーバの入れ替えにも 参照をs3にrerouteすることでサービスの継続性も高まった バックアップも信頼できて基盤として安定してきた
  13. そしてGen4へ • Gen3でも意識はしていたが改修コストが高く積み残した問題が多くあった (BPS的超法規的措置 ) • また長く運用してきた中で新しく問題となったのが増え続ける picサーバ ◦ CPUがついたストレージと考えれば格安でサムネ生成と兼用していた

    ◦ とはいえ台数が増え CPUが余ってしまいコストメリットが薄くなってきた ▪ 純粋にストレージとして考えると s3より当然高い ◦ 増設は運用コストが高く、また減らせないので費用面でも重しとなる ◦ 要はサムネ生成で必要な台数を保持して、増減させない形で使いたい ▪ 直s3書き込みは遅いので採用はできなく、サムネ生成でインスタンスは必要 ◦ コストバランスが崩壊した • そこで出てきたのが r2 ◦ 検証で参照を本格的に流しても問題なく、コストも予測可能であった (egress無料など) 本格的に参照をr2に流そう
  14. cloudflare r2検証 • Gen3運用中にcloudflareからr2が発表されベータのころから検証 ◦ 保存コストが安いのもそうだが何より egressが無料、APIのリクエストも無料枠が大きい ◦ ベータは遅く使えなかったが、 GAになりリージョンが追加され速くなった

    • まずバックアップをs3からr2に切り替えて書き込みが詰まったりしないかを見ていたが安定 • 一時的に参照リクエストも流してみたりもしたが許容範囲で使えると判断 • (ちなみにですが)s3はsnapshot/log保存に利用しておりこちらは使い続けるつもり ◦ 現時点はwrite onlyのAPI発行ができない(ACL) ◦ 参照をしないのであれば s3 Glacierのほうが安い(コスト) 本当に助かるサービス
  15. ストレージクラス • Gen4で最初に決まった方針がストレージクラスを Hot/Cold/Backupにわけることでした • Hotは比較的参照がされるデータ (直近投稿)でpicを利用 • Coldはあまり参照がされないデータ (ある程度経過した投稿

    )でr2を利用 ◦ Hotがある場合はBackupを兼用 • Backupは緊急時のみ使うデータで M-DISCを利用 ◦ 最初はs3 glacierも検討したがBAN回避を考えてM-DISC(まだ迷ってるので glacierにするかも) ▪ コスト的には5,6年でトントンぐらい ▪ 回転メディアなので個別にファイルを書くと無限に時間がかかるので isoに固めて焼くというtarを思い出す運用 • データをColdに変更するタイミングで同時に M-DISCを作る運用 ◦ 運用コストは発生するがそこまで頻繁ではなく増設より楽なので今のところ許容範囲 ストレージクラスの方針は決まったが問題があった
  16. どうやってクラス判定をするのか? • 当時の投稿画像のパスは「 https://pic.nijie.net/01/nijie_picture/foo.jpg」といった形 ◦ 本当に初期(Gen1)の投稿だと投稿ファイル名がそのまま出ているなど非常に自由闊達 ▪ 新規(1).jpgみたいなのがあった ◦ リクエストをpic/r2のどちらかに振り分けすればいいか

    proxyで判定できない • URLの格納にも問題があった ◦ pic.nijie.net/01/nijie_picture/foo.jpgの場合 ▪ DBに格納されているのは pic01を表すhostindex(これはOK)とfoo.jpg(!?)だけ ▪ /nijie_picture/はテンプレート(!?)と書き込みコードにべた書きされている (!?!?) • つまり ◦ パスの抜本的な変更が必要 ▪ 今まで書き込み側で生成していたパスのハンドルが必要になる ▪ proxyでパスを元にクラス判定させたいので全画像のリバランスが必要 ◦ テンプレートにパスの一部が入ってるのを辞めて DBに寄せる ▪ 恐ろしい数のテンプレート /コードの修正 地獄が見える 覚悟は決めてやり切ったけどほんと辛かった
  17. Gen4 現行世代 • この時点で把握しているストレージの問題を覚悟をもって精算する • クラスによって画像の有無が変わるので「配信サービス」として高度な統合が必要 ◦ https://pic.nijie.net/03/nijie/23m12/…foo.jpg ▪ 23m12の投稿年月でHot/Coldのルーティングを決める(パス変更が必須)

    ◦ 各サービスとの責任分界点を明確に決める必要がある • Gen3では共通コードにまとめたがパスの生成など大部分の処理は呼び出す元 ◦ 保存したいと投げればいい感じに採番して URLだけ返ってくるサービスが必要 ◦ 処理の委譲 • ということから当初からサービスであることを意識して設計(内部名は StorageService#v1) ◦ 呼び出し側の各プロダクトでドライバとなる共通クラスを用意して Gen4を呼び出す ◦ 比較的マイクロサービスに近いものができたと思う ▪ 単独デプロイ可能、interfaceも明確、自身で配信もやってるし・・・ ▪ 不要なのでやってないが RPC経由にすることも可能 とにかく処理を巻き取る
  18. Gen4まとめ • 一気にpicを3台まで減らすことができ、今後もほぼ増えないので安心感がある ◦ ごく一部全部Hotであるプロファイルのようなデータはあるがしばらくは問題ない ▪ 問題になったらこれも r2に移すのも良い(複数枚ではないので重さは許容範囲) ◦ 減らしたサーバの一部を後述の全文検索に利用

    • r2のコストも想定内 ◦ ストレージコストが安いが、何も気にせず GETをしまくるとAPIコストがかかる ◦ GETは10MReq(1000万)無料、月間平均で3.85RPSを越えると無料枠を越える ▪ キャッシュ構成を工夫することで無料範囲に収めている (下画像の通り月平均1.56RPS) • 今後も何かしら改善点がでてくるだろうがサービスとして分界点も定義できたので安心 大成功 R2へのRPS
  19. DBやばい • DBが原因でピークタイムや特定のアクションで刺さることが増えた • 原因は3点 ◦ 大量の更新 ◦ サーチクエリ ◦

    抜いた系クエリ(レコメンド) • 今までも逐次改善は行っていた ◦ 検索では複数のページ分を一括取得してキャッシュ ▪ で、offset取得してページ構築 ◦ swrで見え方として遅くならないように ◦ とはいえこれは小手先の対応だった • 小手先の対応でできる範囲が少なくなり複雑度も許容範囲を越えたため抜本的対策が必要に DB改善やるぞ
  20. 大量の更新を何とかする • 調べてみて驚いたが 7割弱が広告IMPのUPDATE ◦ 正直これを見た時勝ったな・・・と思った(フラグ) • 直DBを辞めアクセスログからの登録も検討したが sqlite3を使った ◦

    同じ広告は複数クライアントから見られるので 1リクエスト毎にDBに登録するのではなくある程 度まとめて+10とかにしたかった ◦ 他にもリアルタイム更新が不要なものがあるので応用が利くものが欲しかった ◦ 要はgroup byできるキュー的な仕組みが欲しかった • 仕組みは単純 1. ローカルのsqlite3にinsert 2. cronで1秒前までに挿入されたデータを group by + count(*) 3. ある程度まとまった状態で dbに登録 4. 登録成功したら削除 • 狙い通り7割弱の更新リクエストが消えた これで多少は持つか・・・
  21. 多少の改善にはなったが • ピークタイムに刺さりやすいのは変わらず抜本的な解決が必要 • サーチクエリと抜いた系クエリ(レコメンド)をなんとかするしかない • なぜサーチクエリが遅いのか ◦ 要はLIKE検索を行っていたがユーザ規模の増加で DBが耐えられなくなった

    ▪ %word%はインデックスが効かないので full scanとなる • 前方一致だと効くんですがそれだと無意味 ◦ LIKEを否定してるわけではなく小規模ならお手軽なのでいいと思います • 全文検索を導入するしかない・・・が何を入れるのか ◦ DB拡張のFTSを検証したがしっくりこない ◦ そもそも非力な環境で使える FTSエンジンは少ない・・・ • 抜いた系については更にデータの刈り込みを行って一旦様子見 ◦ そもそもがグラフ構造なのでグラフ DBを検討中 餅は餅屋に
  22. ユーザ・イラスト検索に導入 • 全文検索を行っているのはイラストとユーザ • ユーザが増えてきたこともあり運が悪いとユーザ検索が動くだけで全体が刺さることがあった • イラストはタグやソート順など条件が複雑なのでひとまずユーザ検索に入れてみることに ◦ 特に問題なく導入出来て爆速になり刺さる件数が減った •

    イラスト検索にも適用しようと思ったが機能が足りずしばらく足踏み・・・ ◦ v1.3でattributesToSearchOnが導入され検索対象のフィールドを指定できるように • 機能も揃ったのでイラストに適用・・・ ◦ とはいえ今までの負荷対策でロジックが複雑になっていてかなり辛かった 導入で一気に負荷が減った
  23. 検索ワードをquoteしないと厳しい • キャラ名などで意図しない結果となる ◦ 画像例はデストロイの typoと判定された?(推測) ▪ 意味的にはあってそうな気がする (𝑭𝒂𝒕𝒂𝒍𝒊𝒕𝒚...) ◦

    辞書にないので仕方ない • 元がLIKE検索なので曖昧な検索はいらないと 割り切ってquoteして解消 • v1.4でカスタム辞書が使えるようになったので 将来的にタグを登録してなどは検討している
  24. 漢字のみの検索でヒットしなくなる • 導入後に検索がおかしいとの問い合わせを受ける ◦ issueは認識してたが対応ビルドをいれ忘れた ◦ テストはカタカナで行っており気付かなかった ◦ 問い合わせありがとうございます! •

    Meilisearchは複数言語に対応しており CJKも行ける • 漢字のみだと中国語判定されることがあり 日本語ドキュメントが検索対象外となる • ひとまずは日本語強制でビルドして入れ替え ◦ cargo build --release --no-default-features --features "analytics mini-dashboard japanese" • オプションは変わる可能性があるのでこちらを参照 ◦ Japanese specialized Meilisearch Docker Image #3882 • そして根本対応も検討されていて期待 ◦ Define languagues in settings #702 • なんとなく昔の文字コード判定で使われた美乳を 思い出しました
  25. doc登録の工夫 • doc登録は割とi/o負荷が高い ◦ これはRAMが少ないからかもしれない ◦ とはいえ登録中も快適に検索可能 • そのため先ほどDB改善で利用したsqlite3を利用 ◦

    更新されたイラストやユーザ IDを登録 ◦ 指定idのデータを抽出して doc登録 ◦ 全台登録できたらsqlite3から該当IDを削除 • meilisearchのvupなどで一時的にflushを止めることもで きるので便利 • なお、全更新を行っても普通に検索ができるので 割と気軽にできます ◦ 全更新も全idをsqlite3に入れるだけ 全更新
  26. データ構造(イラスト) • イラスト検索では以下が指定可能 ◦ タイトル・本文の部分一致 ◦ タグの部分/完全一致 ◦ 投稿時間 ◦

    抜かれた数 ◦ いいね数 ◦ 期間指定 ◦ イラストタイプ • また内部機能で ◦ 最後に抜かれた時間 ◦ 最後にブックマークされた時間 ◦ ブックマーク数 • パラメータは結構多い
  27. 大変だったタグ検索 • タグ検索には4種類存在 ◦ 部分一致(AND) / 完全一致(AND) / 部分一致(OR) /

    完全一致(OR) • タグが[AABB]の場合 ◦ AAで部分一致=Hit ◦ AAで完全一致=Miss (AABBでHitする) • 問題はこれのAND/ORをどう表現するかで苦労しました ◦ meilisearchのqueryはANDでORが存在しません(現時点では) ▪ matchingStrategyはlast/allしかなくallはANDとして使えるがOR相当がない ◦ filter機能はIN句を使うことでORを表現できますが、検索文字列は完全一致となります ▪ 実現するためのコードはあるのですがパフォーマンスに影響があるらしく未マージ ▪ Experimental feature:CONTAINS and Prefix / Suffix filter operators #544 • タグが[AABB] [CCDD]の場合クエリ ◦ 部分一致(AND/OR): q=”AABB” “CCDD” (ORは実現できないので ANDに変更) ◦ 完全一致(AND): filter[tag_id =AABBのID, tag_id=CCDDのID] ◦ 完全一致(OR): filter[tag_id IN [AABBのID, CCDDのID]] フィルタを使うことでOR検索もできるが制限がある
  28. クエリ関連で工夫した点 • ranking-ruleの設定 ◦ デフォルトのランキングルールは以下の通り ▪ words > typo >

    proximity > attribute > sort > exactness • https://www.meilisearch.com/docs/learn/core_concepts/relevancy ◦ 今回は投稿順などの様々な sortを使うことから変更 ▪ sort > words > typo > proximity > attribute > exactness • filterable/sortable-attributeの変更 ◦ いいね件数などでsortしたり、イラストタイプで filterしたりするのでそれを追加 ◦ これらの更新はindexの再構築が走るのである程度考えて設定したほうがいいです ▪ 多少時間がかかる • 開発中に頻繁に項目を変更したのですが、 docがどのバージョンなのかを把握しづらく バージョン管理するようにしました( FMTVER) ◦ 全更新かけるときに FMTVERでfilterして・・・といったこともできて便利 • データに見られたら困るものを入れないのが第一ですが、 attributesToSearchOnで検索対象は絞ったほうが良いでしょう meilisearchおすすめです クラウド版もそんな高くないので使ってみるとよいと思います