この記事には、Kaigi on Rails 2024 の Day2 に聞いたセッションの感想と記録を書きたいと思います。
セッション会場は2つあったのですが、2日目は特にどちらの発表も面白そうでどちらを聞くか悩ましかったです。
また、発表の中で、1日目や他の方の発表内容に言及していたりしているのも、おお、繋がっている!!となりました。
( イベントで思ったことと、Day1 のセッションについては別記事を書いています)
- 作って理解する RDBMSのしくみ
- Cache to Your Advantage: フラグメントキャッシュの基本と応用
- 推し活のハイトラフィックに立ち向かうRailsとアーキテクチャ
- 入門「状態」
- 約9000個の自動テストの時間を50分から10分に短縮、偽陽性率(Flakyテスト)を1%以下に抑えるまでの道のり
- Sidekiq vs Solid Queue
- Importmapを使ったJavaScriptの読み込みとブラウザアドオンの影響
- Tuning GraphQL on Rails
- 30万人が利用するチャットをFirebase Realtime DatabaseからActionCableへ移行する方法
- サイロ化した金融システムを、packwerk を利用して無事故でリファクタリングした話
- omakaseしないためのrubocop.yml のつくりかた
- Identifying User Identity
- 基調講演: WHOLENESS, REPAIRING, AND TO HAVE FUN
作って理解する RDBMSのしくみ
Yudai Takada さん
Day2 の朝は少し出遅れてしまったので、1つ目のセッションは結論しか聞けませんでした 。アーカイブが出たら見たいと思います。
Cache to Your Advantage: フラグメントキャッシュの基本と応用
SonicGarden の方の発表でした。Sendagaya.rb を主催されているとのこと。
フラグメントキャッシュの基本についてと、それを応用して作成中の gem(ページ全体にフラグメントキャッシュを広げるもの)についての発表でした。
Rails のキャッシュの機構について分かっていなかったので、基本の説明がありがたかったです。
基本:フラグメントキャッシュとは
- キャッシュとは、貯蔵所(近くに貯めておく場所)
- フラグメントとは、断片(ページの一部分)
- 架空のECサイトを例示しながら説明
- erbでフラグメントキャッシュを使うには、以下のようにcacheでdo endで囲むだけ
<% cache @product do %> <% end %>
- キャッシュデータの保持はキーバリュー
- キャッシュ事故の不安
- DBのレコードを更新したが、キャッシュを消すのを忘れて更新されない
- 特定ユーザーしか見られない情報を誤ってキャッシュしてしまって他のユーザーに見えてしまう
- 古いキャッシュどうする?
- キャッシュの失効を手動で行うのは、非常に面倒でミスが発生しやすい、もっと良い方法「キーベースのキャッシュ失効」と呼ばれるものがある from DHH
キーベースのキャッシュ失効
- ActiveRecordインスタンス単体の場合
- productsが更新される
- [モデル名]/[id]-[updated_at] でキーが変わる
- ActiveRecord::Relaction(複数)の場合
- ビューによる追加のキー
- ビューテンプレートからキーを生成
- [ビューテンプレート名]:[ハッシュ値]
- すべてのテンプレートとパーシャルのハッシュ
- ex. /app/views/products/show.html.erb
views/products/show:7a1156131a6928cb0026877f8b749ac/products/5-20240918083000000000
- ページを見ると以下に分かれる
- 静的コンテンツ
- 動的コンテンツ
- キャッシュNG: 個人ごとに変わる部分があるのでキャッシュしてはいけない部分
応用:フラグメントキャッシュをページ全体に広げる rendering_caching gem
- 作成中のgem: rendering_caching gem
- ページ全体をキャッシュさせる
- Controllerに、
cached_rendering
と書く- 自動的に
cache @product
を書いたのと同じ
- 自動的に
class ProductsController caches_rendering :index, :show end
- 他のユーザーのデータを見れないようにする
- sessionに依存する他のデータを使わない
- gemで、自動的に session, cookiesが無効になるようにしている
- CSRFトークンを使わない方法
- Originを確認
- SameSite Lax/Strictを設定
- Fetch Metadataも確認
- 1日目のcoroちゃんさんの発表がすごく参考になる
- Rails APIモードのためのシンプルで効果的なCSRF対策 | Kaigi on Rails 2024
- 残ったキャッシュNG部分は、Turbo Frames(洗練されたframeのようなもの)で分離させる
まとめ
- フラグメントキャッシュは ActiveRecord モデルを渡せばおまかせでスピードアップできるよ
- rendering_caching gem ならページ全体にもできる
- ただしキャッシュ内で session, cookies を使わないように注意
- rendering_caching gem なら自動的に無効
推し活のハイトラフィックに立ち向かうRailsとアーキテクチャ
株式会社 TwoGate さんの CTO、奥本さんの発表でした。
ライブ会場などの物販でものすごい量の決済リクエスト(秒間400件超!)を捌くOEM型モバイルオーダーアプリ(Caravan)のアーキテクチャについて語ってくださいました。
TwoGate さん、めちゃくちゃかっこいいと思いました。パフォーマンス関連の話は痺れます。
例として、在庫を更新する処理がロックを取ることでロック競合して進まなくなり、ハイトラフィックな決済リクエストを捌けなくなってしまうという問題をデータの持ち方を変更して解決した話をしてくださったのですが、自分は在庫管理のソリューションに携わっているので、こんなDBテーブル構造で解決したのか!と目から鱗でした。
FOR UPDATE SKIP LOCKED
という、行ロックした結果をスキップしてデータを返してくれる構文も初めて知りました。
前提
アプリの特徴
- サーバサイドはマルチテナント
- アプリはOEM型、1アーティスト=1アプリ
- 短期間での提供:内製向けローコード化
- 1クリックでAppStoreに審査出すところまで作り込んでいる
- フロントエンドは、Angularとionic、アプリもWebで作っているとのこと
インフラアーキテクチャ
- Fastly -> ALB - nginx -> Rals App -> Aurora PostgreSQL / ElastiCache Redis(Cache/Queue)
- Sidekiq
- Amazon ECS
- 結構シンプルな構成、モノリシックな Rails
ピークトラフィック
推し活 × ハイトラフィック
ハイトラフィックに挑む設計
-スロークエリを起こさない実装 - CDNでのキャッシュを活用 - アプリケーションキャッシュ(Redis)の活用 - CDNでのキャッシュが難しい時
起きてくる問題
- DBレイヤーでのボトルネックから始まる
- インデックスの不足・クエリが悪い・テーブル設計が悪い
- 実行計画が変わって急に遅くなる
- 各種外部APIのレートリミットに引っかかる
- ALBのスケーリングに間に合わずエラー発生、事前に暖気したり
対処
- Performanc Insight / APMによる分析
- アプリケーションログの分析、特定条件だけ遅い:ある商品だけ在庫が非常に多い
- 実行計画の解析:ChatGPTに投げると便利!とのこと
- 負荷試験
本発表で扱うプロダクトの機能
- 予告した日時に販売開始、在庫限りの販売、受け取り時間枠選択->商品選択
- ユーザーが一同にアクセスし、在庫の奪い合い!
1. 高スループット & 正確な在庫確保のアーキテクチャ
- 在庫確保をする簡易な実装
在庫テーブル
商品ID | 在庫数 | 販売数 |
---|---|---|
P1 | 100 | 20 |
P2 | 150 | 10 |
- 同時に購入者がいると、在庫数を確認したタイミングと更新するまでの間に更新が走れば、在庫数を超過してしまう・・・
- 解決策:行ロックを導入する
BEGIN; SELECT * FROM 在庫テーブルWHERE 商品ID = P1 FOR UPDATE; UPDATE 在庫テーブル SET 販売数 = 販売数 - 10 WHERE 商品ID = P1; COMMIT;
- 行ロックを掛けることで正確な数量の販売ができるようになるが、数百RPSでの在庫確保のワークロードでは全然進まない
- 同じ行を更新するユーザーが多数、ロックの奪い合いでロック待ち・デッドロックが頻発
どうやって行を奪い合わないようにすればいい??
1行1在庫のテーブルに変更
- 順番に排出できれば競合を回避できるはず
在庫ID | 商品ID | ユーザID |
---|---|---|
Z1 | P1 | |
Z2 | P1 | |
Z3 | P1 |
FOR UPDATE SKIP LOCKED
で実装- 行ロックされている行をスキップした結果を応答してくれる
- PostgreSQL9.5- MySQL8.0- ロックがないものを取得できる
BEGIN; SELECT * FROM 在庫テーブル WHERE 商品ID = P1 LIMIT 4 FOR UPDATE SKIP LOCKED; UPDATE 在庫テーブル SET ユーザID = Ua WHERE 商品ID = P1; COMMIT;
- 行ロックされていない行を必ず返答する保証がある
- 競合することがなくなり、数百TPSでの処理が可能に
- (余談)Solid QueueはあDBをバックエンドにしているが、FOR UPDATE SKKP LOCKEDを使って実現してる!!!
2. 外部決済APIのレートリミットとの向き合い方
- クレカ決済には外部のペイメントプロバイダを利用
- 非通過、非保持化のため
- 決済関連20-30rps程度
- 決済できず宙に浮く在庫をなくすため、オーソリとってから在庫確保する
- 売り切れた在庫が復活すると、クレーム発生につながることもあるため
- 決済プロバイダのレートは上げることはできない
- 正しく諦める必要がある
- 決済システムの限界よりもリクエストを受け付けない、レートリミットを導入
- CDN/WAFで対応する?正確性や条件制御が単純なので不採用
- Railsのレイヤでレートリミットを Redis で自作した
- 限界に対して正しく向き合うことは大事
- 負荷試験で外部APIをモックする時はレートリミット時の挙動も考慮すること
入門「状態」
しんくう さん
「状態」についてのお話でした。状態がどんなときに辛くなるのか、その解決方法についてコード例を交えながら解説してくださいました。
今、自分が開発しているコードで、ユーザーの状態がコードの様々な箇所に散見されているように感じていていて、なにか改善するヒントはないかと思って聞きました。状態の扱い方をわかりやすくまとめてくださっていて参考になりました。
オブジェクトと「状態」
- 電球 LightBulb クラス
@is_on
: 電球が点いているか@brightness
: 明るさ
- オブジェクトは、複数のインスタンス変数の組み合わせで「状態」の表現を増やすことができる
attr_accesor :is_on, :brightness
「状態」がどのような時につらくなるのか
- 変化を繰り返すと追いづらくなる
- 状態の変化が多いと、最終的な状態を把握するのに労力がかかる
- インスタンス変数を何度も書き換えている
- 変化の幅が大きいと考慮点が増える
つらさ抑制
- 「状態」を可能な限り変化させない
- 再代入をさせない
- 「状態」の変化幅を限定的にする
- 値を変更する専用のメソッドを用意してあげて、変更方法をメソッド経由のみにする
- 「状態」をシンプルに少なく保つ
- 変数を見て状態を読み取らせるのをやめ、状態を読み取るためのメソッドを用意する
- 無数のパターンではなく、用意されたメソッドから限られたパターンだけ
- つらさの抑制まとめ
- 役目を終えるまで変化をさせないようにする
- 変化のバリエーションも数が多くならないようシンプルに保つ
Rails実装でのつらい「状態」のいなし方
- Controller でのインスタンス変数再代入
- 変数に代入するのは1回のみに
- 変数の値を決定するためのメソッドを用意する
- メソッドを見ればいいので状態を把握しやすくなる
- View内でのインスタンス変数を利用した分岐
- モデルに状態をわかるようなメソッドを用意してViewから呼ぶ
- 「状態」としてメソッドが定義されることで読みやすさが明快になる
def before_open? open_at > Time.zone.now end
- 用途が迷子になったControlerインスタンス変数
- 必要なインスタンス変数以外削除しておく、必要以上にインスタンス変数を増やさない
- インスタンス変数が増えるほと状態を把握する手間が多くなる
- 状態はなるべく変えず、シンプルに保つと良い!!
約9000個の自動テストの時間を50分から10分に短縮、偽陽性率(Flakyテスト)を1%以下に抑えるまでの道のり
hatsu さん
「状態」のお話と同じ時間帯のこちらの自動テスト改善の発表もめちゃくちゃ参考になりそうで聞きたかったです。
E2Eテスト改善するヒントがたくさん散りばめられていました。
やり遂げているのが素晴らしいです。
Sidekiq vs Solid Queue
Railsのバックグラウンドワーカーの変遷の歴史を紐解きながら、現在よく使われている Sidekiq と Rails 8.0から標準となる Solid Queue を比較した発表でした。
Solid Queue についてキャッチアップできてなかったのでありがたかったです。
ginza.rb を主催している方とのことでした。地域ruby 主催している方多くていいですね。
バックグラウンドワーカーの話も、今回結構多かった気がします。
そして、TwoGate さんの発表で出てきた SKIP LOCKED がこのセッションでも早速出てきました・・!
バックグラウンドワーカー
- Sidekiq も Solid Queue もバックグラウンドワーカー
- Railsにおけるバックグラウンドワーカーは Active Job
- アダプタ経由で使う(ラッパー)
- ActiveJobを使わない方が良いという説もある(直接アダプター使いましょう)
- 圧倒的に Sidekiq が使われている、2024年のデファクトは Sidekiq
- Rails 8.0 のデフォルトは Solid Queue
- 何を前提にバックグラウンドワーカーを選定してどのように使うといいのか
Railsのバックグラウンドワーカーの歴史
Active Job以前
- BackgrounDRb(2006-)
- Delayed::Job(2008-)
- RDB使ってる
- ジョブ取得時のロックの競合、実行可能な列をブロック、並列実行に難あり
- Resque(2009-)
- GitHub社
- Redisを使ってる
- Sidekiq(2012-)
- Redis
- OSS版と、商用版 Pro/Exterprise
- ActiveJob (2014-)
- Rails4.2から
- バックグラウンドワーカー処理への共通インタフェースができた
- deliver_later, Active Storageから使える
「ActiveJob経由ではなくSidekiqを直接使う」説
- Active Job以前に作られたライブラリは独自のインタフェースと機能を持つ
- ライブラリの機能を完全に使えないこともある
- 重複している機能もある(ActiveJobとSidekiqのリトライ機構)
Active Job以降、Rails8.0以前
- GoodJob(2020-)
- ActiveJobのアダプタとして動かす前提
- ストレージはPostgreSQL限定
- Delayed::Jobと同様のロックの問題は、Advisory Locksを利用して回避している
pg_try_advisary_locks
: ロックを取得できたらtrue、できなかったらfalseを返す関数
Rails8.0
- Solid Queue(2023-)
- Rails 8.0からのデフォルト
- ストレージはActive Record経由
- GoodJob 同様 ActiveJob 経由が前提
Solid Queue誕生の秘話
- HEY(DHHがCTO)ではResqueを長年使っていた
- resquenの他に6つのプラグインのgemをforkしたり自作したり、辞めたいというモチベーション
- Solic Trifecta
- Solid Queue, Solid Cache, Solid Cable に共通する話
- RedisやめてRDBにしようぜ
- ディスクの性能が上がっている、ディスクの方がいいじゃん、安い、特にキャッシュ使うときメリット
- Redisの要素なくなるので開発楽、One Person Framework
- SKIP LOCKED
- MySQL8.0.1、PostgreSQL9.5から利用可能
- すでに取得されているロックを飛ばして残りをロックする
- これにより、RDBでも大量のジョブを扱えるように
FOR UPDATE SKIP LOCKED
- これまでの知見の積みと2024年の状況を踏まえ Solid Queue が生まれた
バックグラウンドワーカーの選定
Sidekiq の機能の深掘り
- 仕事で使うにはSidekiq Proを使うのが前提
- 2種類の信頼性を高める機能がProにはある!
- エンキュー時にRedisにアクセス失敗したらメモリ上に貯めて次のエンキューに再送してくれる
- ジョブ実行中に何らかの要因でプロセスが死んでもジョブの情報を失わずに再実行できる
- 実際には、Enterpriceを推奨
- cron等の便利な機能を公式サポート付きで利用可能
Sidekiq とSolid Queue を比較
- SidekiqはEntreprise利用前提での話
- ストレージ
- Redis or RDB
- Solid Queue はジョブ専用の DB を持つ前提、相乗りもできる
- 大量のジョブを扱うサービスであればSidekiq (Redis)に優位性がある
- HEYはSolid Queueで2000万ジョブ/日を扱っている実績あり
- Sidekiqは2万ジョブ/秒
- インタフェース
- Solid QueueはActiveJob経由なので迷いにくい
- 機能
- Solid Queue は現時点では HEY が求めている機能を持っている(cron、同時実行制御)
- Sidekiqはもっと色々ある(wiki: https://github.com/sidekiq/sidekiq/wiki )
Sidekiq にあって Solid Queue になる機能
- Batch
- 複数のジョブをバッチという単位でまとめて管理し、バッチのジョブ全体がおわったことをトリガにできる
- Rate Limiting
- 特定の処理の実行数に制限をかける
within_limit
で、同時実行制御できる
- Encryption
- Redisに渡すデータの一部を自動的に暗号化→複合
- SidekiqのWeb UIで引数の情報を見れるので、個人情報見れないように
sidekiq_options encrypt: true
を宣言しておくと、perform_async
の最後の引数が暗号化される
SecretJob.perform_async(1, 2, {"mynumber" => "12345678901"})
今使っている Sidekiq から Solid Queue に移行する必要はありますか?
- 移行のメリットと移行のコストを比較して決めましょう
- 移行は基本的にメリットが薄い、機能が増えているわけでない、Sidekiqで困っていることが Solid Queue で解決するわけではない
- 小規模サービスで Sidekiq を Solid Queueに変更して運用コストを減らすのはありかも
rails newするのであればどちらを採用する?
- ジョブどれくらい使うか次第
- ジョブをあまり使わないサービスは SolidQueue、ジョブをたくさん使うサービスはSidekiq
- 中間のサービスは Sidekiq 特有の機能の中で便利なものがあるか探して決める
- あれば Sidekiq
- なければ Solid Queue
- サービス育った結果、Solid Queue からSidekiq への移行するのはできなくはないのでは
- 個人の意見
Importmapを使ったJavaScriptの読み込みとブラウザアドオンの影響
shu_numata さん
Rails7 で標準となった Importmap とブラウザアドオンのAdBlockによる問題のお話でした。AdBlock が JS の読み込みをブロックしていた問題があり、その解決までの道のりを話してくださいました。
Importmapについて知りたくてお聞きしました。
import mapsの概要
- Rails 6 -> Rails 7 へのアップデート
- Webpacker がリタイア(Rails 6でデフォルトになって 7でリタイア)
- 移行先はいずれか
- js-budling: Webpack や esbuild を使うもの
- importmap-rails
- Rails 7でデフォルトとなった importmap-rails ってなに?
- 今まではJS モジュールを CDN から node_modules へダウンロード、node_modules にあるものを import する
- Webpack や esbuild などのビルドツールで application.js などにビルドしていた
- import_map は node_modules を使わず、ブラウザ上でモジュールを読み込める、ビルドしなくていい
- モダンブラウザで使える -importmap-rails というgem
なぜ Rails 7 からデフォルトになったのか
- Babelでのトランスパイルが不要になった
- HTTP/2が当たり前に使われるようになったので、1つのJSファイルにまとめなくていい
- HTTP/2 は、複数のリクエストを一つのコネクションで同時に送信できるので、ファイルを小分けにしても遅延しなくなった
importmap-rails使う
- 自分たちのプロジェクトでビルドツールいらない? -> いらない
- TS使ってなかった、Vue.jsちょっと使ってたけど Stimulus に移行する方針に決まった
- がっつり TS や Vue.js や React 使ってると、ビルド捨てるの難しい
- Rails World で NO BUILD という方針が出ていた
- プロダクトでは、JS を no build にして importmap-rails でやっていくことになった
しばらくして起きたエラー、原因と解決法
- サーバーサイドに奇妙なエラー
- ログを調査すると、TURBO_STREAM 形式でリクエストが来るはずが HTML 形式で来てるようなメッセージ
- 直接リクエストしているわけでもなく、Turbo も動いている、ユーザー環境も最新のブラウザ・・Addonなのか?という疑い
- よく使われるAddon? -> 翻訳やAdblock
- Adblock入れるとエラーになる
- このエラーになると、アプリケーションが使っているJSが全て動かなくなる
- Adblockが使っているフィルターリスト easylistの中に、EasyPrivacy の中に関係あるものを発見
- EasyPrivacy を有効化するか無効化するかというオプションのせいだった
- importmap-railsに移行して起きた問題
- import-rails で使用するモジュールをダウンロードした
- importmap-railsで起きた時使用していたのは、v1.2
- download オプションを使用して、/venderにダウンロードする
- Sprockets か Propshaht でフィンガープリントがつき、発生しなくなった
- importmap-rails v2ではvendorへのダウンロードがデフォルトの挙動になった
- 引き続き外部CDNの使用もしている
- JSのライブラリの中には1つのエントリポイントに機能がまとまっていない場合がある
- importmap-railsでは1つのエントリファイルしかダウンロードしない(依存しているものはダウンロードされない)
- 外部CDNを使う場合は、Adblock にブロックされるかも
- 対応のPRが出てるがマージされていない
- 似たような問題が起きた時、気づけるようにしたい
- 主要な機能での Turbo Stream のリクエストチェックをして気づけるようにした
まとめ
- ビルドの手間が改善できて良かった
- Adblockによってブロックされるケースある
- 最新の import-rails はデフォルトで vendor へ JSモジュールがダウンロードされる
- JS モジュールが Adblock にブロックされることは今後もあるかも、気づけるようにしたい
- おまけ:nodelessにできていない
- ESLintはいらないの?Playwright は使いたい
Tuning GraphQL on Rails
ペパボ在籍時にとてもお世話になっていた、pyama さんの発表でした。
GraphQL APIの中の N+1 を数多く潰してくださっていたことについてでした。GraphQL でN+1を解決する方法をわかっていなかったので、勉強になりました。
爆速で様々な改善を進めてくださっていたことを思い出し、感謝しつつ聴いていました。
サービスのダッシュボードを週次など定期的にみんなで見る活動はいいなあと思っていました。なかなか当時参加できなかったのですが・・・
(自分は全然 GraphQL 周りわかってなかったな・・・と個人的な反省もありつつ、発表内容を振りかえりました)
現場紹介
- minne ハンドメイドマーケットサービス
- モノリス構成なRails、ひとつのモノリスだったのをフロントを Next.js で切り出し
- k8sのpodは分けて運用してる
- オンプレミスと AWS のマルチクラウド構成
- AWS EKS(batch)、Elastic Service、RDS etc
- データセンター間に Direct Connect
- データセンター持ってるので価格を抑えて調達できる、コスト低いけど運用は大変
- AWSは運用が楽、コストは・・・
発生していた課題
どのように改善したか
- GraphQLでよくあるN+1なクエリ
- 各注文(order)は1つの顧客(customer)に紐付いている
- ordersを取得して、それぞれのorderに紐付いているcustomerを取得する
- ordersが5000件だった場合、それぞれのorderに紐づくcustomerを取得しようといてN+5000のSQLが発行される(と理解した)
query { orders { id customer { name } } }
SELECT * FROM orders; SELECT * FROM customers WHERE customer_id = 1; SELECT * FROM customers WHERE customer_id = 2; ... SELECT * FROM customers WHERE customer_id = 5000;
2つの改善方法
- lookahead を利用する
- バッチロードを利用
- Shopify/graphql-batch
- GraphQL::Dataloader
--- IN句で解決 SELECT * FROM orders; SELECT * FROM customers WHERE customer_id IN(1, 2...5000);
lookaheadを利用した先読み
- (lookaheadはGraphQLのクエリ内容を解析し、どのフィールドがリクエストされているかをリゾルバの中で事前に確認する手法とのこと)
- 先読みできる
- lookaheadがシンプルに書けるが、増えてくると辛い
graphql-batch
- GitHub - Shopify/graphql-batch: A query batching executor for the graphql gem
- promise.rbというライブラリを使っている(非同期処理)
- Ruby の Promises/A++準拠のライブラリとのこと
- 条件付きの proc オブジェクト
require 'promise' promise = Promise.new promise.then { |value| puts "Fulfilled: #{value}" } puts "Pending: #{promise.state}" promise.fulfill('Hello, World')
# thenに渡されたブロックを遅延実行
% ruby example.rb
Pending: pending
Fulfilled: Hello, World!
- どこを遅延実行させるか
- idをまとめて集めて、クエリは後から遅延実行して投げたい
module GraphQL::Batch class Loader def load(key) cache[cache_key(key)] ||= begin queue << key ::Promise.new.tap { |promise| promise.source = self } end end end end class OrderType < Types::BaseObject def customer Loaders::AssociationLoader.for(Order, :customer).load(object) end end
- loadに渡された引数(customer_id)を queue に append していく
queue = [1, 2, 3, ... 5000]
- Promise を返却
- load(object)にどんどん customer_id が追加されていく
- Promise.sync(Promiseの実行)で、上から順番に浅い順に Promise.sync がコールされる
query { orders { # 1. Orderのプロミスが実行される id customer { # 2. Customerのプロミスが実行される name } } }
# recordsに渡されたActiveRecordモデルのインスタンスに対し、 # 関連データを一括でプリロードし、それぞれのレコードに対応するデータを設定する def perform(records) # <= recordsには、loadでqueueに溜まっていた値が渡るので、遅延実行対象のレコードをまとめて preload preload_association(records) records.each do |record| fulfill(record, read_association(record)) # <= レコードごとに結果を分割して埋め込む end end # RailsのActiveRecord::Associations::Preloaderを使って # recordsに関連するデータをまとめてロード def preload_association(records) ::ActiveRecord::Associations:Preloader. new(records: records, associations: @association_name).call end def read_association(record) record.public_send(@association_name) end
ActiveRecord::Associations::Preloader
で IN 句にまとめられる
実践N+1解消
- インスタンスメソッドは、ARメソッドが個別に実行するので、N+1が発生する
query { orders { id customer { name isLoyal } } }
def loyal? sales.exists? end
- 遅延実行した ↓
module Types class CustomerType < Types::BaseObject def is_loyal Loaders::AssociationLoader.for(Customer, :sales).load(object) do |sales| object.loyal? end end end end
- Loaders::AssociationLoaderを利用して、関連データ(sales)を一括でロード
- 各Customerごとに個別のSQLクエリが発行されることを防いでる
- 太客が過ぎる(1つのCustomerに、5,000,000,000,000件のSalesデータ)
- Loaderを書く
customers.each do |customer| customer_sale = result.find { |row| row["customer_id"] == customer.id } fullfill(customer, customer_sale.present?) # <= クエリの結果を Promiseの戻り値に設定 end customers.each { |customer| fulfill(customer, nil) unless fulfilled?(customer) } module Types class CustomerType < Types::BaseObject def is_loyal Loaders::LoyalCustomerLoader.load(object) end end end
Loaderのメリット・デメリット
- Loaderはすっきり書ける
- Loadler乱立すると、Loaderにビジネスロジックが埋め込まれてモデルと差が生じてしまう、バグの温床に
パフォーマンス問題の探し方
改善の実績
- オンプレミス 120VM -> 30VMに減少
- AWSも 20-80VM -> 20VM(スケールアウト不要に)
- メトリクスも大幅に改善
日々の運用
- 日々サービスは遅くなる
- 新機能が増える=新しいクエリが増える
- SLI指標とSLO目標を決める
- SLIとSLOは、サービスのKPIからブレイクダウンして決める
- レスポンスタイムとカートインの相関関係の図
- モニタリング手法は、APMで可観測性を上げる
- SaaS(New Relic/ Data Dog/ Sentry)
- OpenTelemetry(Grafana Stack):規格を標準化することでテレメトリデータの標準化
- ダッシュボードを作るのがおすすめ、レイテンシ
- チームで定期的(週次)で見れると良い
今日話したこと
- N+1の問題は、graphql-batchのAssociationLoader または GraphQL::DataLoaderに準ずる実装で解決できる
- 安易に先読みすると、レコード数の多いテーブルを大量にロードしてしまう
- この場合、自作のLoaderを書く
- Loader乱立すると、モデルとの実装差異が生まれることがあるので、一貫性を持つ実装にすること
- 日々のモニタリングでパフォーマンスの劣化に対処する
30万人が利用するチャットをFirebase Realtime DatabaseからActionCableへ移行する方法
pato というエンタメスキルシェアサービスのチャット機能を、Firebase Realtime DatabaseからMySQLに移行し、ActionCableを使って実現するように移行したお話でした。
アーキテクチャを移行作業、すごいです。
それからお話を聴いて、Ruby の Feature Flags で、flipper という gem があることを知りました。どんな感じか使ってみたいなと思いました。
発表した方は、Roppongi.rb のオーガナイザの方でした。
抱えていた問題
リージョンがus-central
- リージョンがus-central、2017年当時はus-centralしか選択できなかった
- 一斉送信のような大量の書き込みすると顕著に現れる
- 1,000リクエスト/秒という制約
- firebase-ruby メンテナンスされていない、PR投げたが反応ない -乗り換えることに
ボトルネックと技術選定
- Firestore
- 書き込みスケーラビリティが良い
- 料金が高い
- Cloud Pub/Sub
- フルマネージドでメンテナンスが不要
- 7日間のストレージ機能、再送機能
- Pub/Subによる料金がかかるけどデメリットになるほどではない
- Action Cable
- npmパッケージあり、フロントの実装も楽
- インフラレイヤのメンテが必要
- ベンダーロック嫌だ、ActionCableが良さそう?
- Action Cableの大規模なチャットサービスでの事例は聞かない、事例作りたい!
リリースまでのロードマップ
- データ移行 Firebase -> MySQL
- Firebase Realtime Databaseの全データをBigQueryに吐き出してどんなデータがあるか見てみた
- よくわからないKeyがめちゃくちゃ生えてた
- BQで整形することが可能になた
- talksテーブルへのinsertはやめた(ダウンタウンが発生してしまうから)
- 本番環境下で検証するために、Feature flags として flipper gemを使用
- 本番環境で起こった事案
- GKEを利用していたので、コネクションが30秒で切れてしまう
- 連続でメッセージを送るとメッセージがロストしてしまう
- かといってタイムアウトとの時間を伸ばすとコストが上がってしまう・・
- 再送時に足りないデータを再送するように
- ギフト機能も、チャットとして送信される
- 即時決済が走るギフト機能
- 送信者は Action Cable 経由でなく、Post リクエストで対応
- 同時接続の安定性
- Thread または Web concurrency
- Thread: Pumaのプロセスが処理できる並列リクエストの数
- Web Concurrency: Pumaのプロセス数(ワーカー数)。各プロセスは独立、それぞれスレッドプールを持っている
- Threadの方がメモリ効率が良い
残された課題
- INSERT時にRETURNINGが使えないため、関連テーブルの一括登録ができない
- 運用コスト
- スケール時のインフラコストが課題
サイロ化した金融システムを、packwerk を利用して無事故でリファクタリングした話
Kota Kusama さん
コインチェックのエンジニアの方が、モノリシックなシステムを packwerk を使って様々な箇所から使われている認可ロジックを切り出してリファクタリングしたお話です。
ちょうど、今自分の会社でも、モノリスからモジュラーモノリスに移行できないかと試行を進めようとしており、この内容は持ち帰りたいと思って聞きました。
packwerk を使うと、安全に機能をパッケージ化できそうですし、試してすぐ戻すということもできそうで良さそうだと思いました。
1. バックエンド部分の現状と課題
- 暗号資産取引サービス Coincheck
- 10年経過、プロダクトコードの規模はどんどん膨らんでいる
- 複雑でモノリシックなコード
- 適切に統一されたレイヤー化ができていない、サービス横串の仕様整理が難しい、ロジックに対するコードオーナーの境界線が曖昧
- コインチェックのプロダクトは大きく分けても10を超えている
- プロダクト1つ1つについて、ユーザーステータスに応じた取引可否の判定が必要だが判定条件がとても複雑になっている
- 取引可否の判定箇所が、プロダクト毎やModel/Controllerに、40箇所以上
課題:認可ロジックの散在に対する課題
- この1つの課題を取り上げたお話
- サービス横串の仕様がわかりにくい
- 認可ロジックの妥当性を確認するコストが高い
- ドメインロジックが複雑化する
解決方針
- IFの統一
- 認可ロジックの集約
どう集約する?
packwerkを採用
- 実態は静的解析なので本番に影響を与えない
- 依存関係の検知ができる
- コード移動のみでモジュールか実現できて認知負荷が低め
packwerk とは
- Railsアプリケーションのモジュラーモノリス化けを支援する静的解析ツール
bundle exec packwerk check
- Shopify が作った
- package.ymlが置かれたディレクトリは以下を1つのパッケージと認識
- デフォルトでは、packsを切ってディレクトリを置いて、その中にrailsのパッケージをおく
- package.ymlで依存が認められていなければ、違反として検知する
- packwerk 自身は、依存関係の違反を検知する
packwerk-extensions
- メインはプライバシーチェッカー
- packwerkで検知する違反を拡充させるgem
- パッケージ内にパブリック領域/パブリッククラスの定義を行い、非パブリック領域への参照を違反として検知
- インタフェースを統一する上で、認可ロジックへの直接参照を制限するために有効化した
packs-rails
- パッケージ化したコード類を自動的に、zeitwerk のオートロードパスに乗せてくれる
- デフォルトで packs 配下のパッケージをパスに追加してくれる
やったこと
- 認可ロジックを集約した認可パッケージの作成を行うことで課題を解決
- ユーザーが特定の取引を実施できるか否かを返却するクラスを作成、IFを1つに統一
- 引数にユーザーIDと取引種別を受け取る
- 認可ロジックは、非パブリックなクラスに集約
- 非パブリック領域への参照を違反として検知する設定
- CIで違反を検知、違反があるとマージできない
導入後の評価
- 既存動作に極力影響を与えない構造
- 静的解析のツールなので本番環境に影響を与えない
- パッケージするにあたってディレクトリ構造を変える必要はある
- packs-rails のコードの中身を見ると、Railtieクラスが定義されている
- これにより、zeitwerk によるオートロード対象になり、パッケージ配下のファイルが読み込まれる
- packs-rails を併用することで、ディレクトリ構造を変更するだけでモジュラーモノリス構造、パッケージ化けを安全にできる
防ぎ切れない観点
今後の展望
omakaseしないためのrubocop.yml のつくりかた
Shu Oogawara さん
packwerkの話と同じ時間に、rubocop のお話もありました。
私は今の会社に入って最初に rubocop の導入をして rubocop.todo.yml に残る既存の違反を無視するための設定を減らしていきたいと考えていたので、このお話はとても聞きたいと思っていました。この時間もどちらを聞くかとても迷った・・・
スライド見たら、チームで毎週、コードルールをどうするか定期的に議題にあげて話しつつ進めててとてもいい取り組みだと思いました。後でアーカイブ見ないとです。
Identifying User Identity
多くの Rails アプリケーションに存在する、利用者を表す User
モデルについての考察でした。お話を聞きながら、User の設計についてどんどん整理されていって、聴くうちにどんどん頭がすっきり整理されていって気持ちよかったです。おおお!となりました。整理された設計・考え方は気持ち良いのだなと思いました。
また、スライド下部に書いてあるちょっととした Tips のような話がおもしろくて、見たい!と思いつつ、発表中は前の人の頭とかで見れなかったので、手元で資料見てたりしました。
発表会場のホールは、前の方に moro さんのお知り合い(ファン?)の方がいて、わいわいたのしそうな雰囲気になっていました。
はじめに: ユーザーとはなにか
- 同じサービスでもいろいろなユーザーがいる
- そのサービス上で「一人」の主体として識別したい = identify したい単位がユーザーである
架空のユーザー管理機能やることリスト
- ユーザーとして、サービスに登録したい
- ユーザーとして、サービスを利用したい
- ユーザーとして、ログインしたい
- ユーザーとして、サービスを退会したい
ユーザーとして、サービスを利用したい
- ActiveRecord モデル User を定義する
- 基本的には主キー id だけで良い!!!!
- 人間が読み出す代理キーの human_id、created_at
id だけのテーブルが表現するもの
- いること = identityがあること、そのものを表現
- identity があると、その持ち物であるデータを作れる(サービスを使ってもらえる)
- 名前やメールアドレスはbelongs_to :userな別テーブルに保存する
- user_profiles
- user_credentials: 第3者にばらされたくないようなデータ
本当に大丈夫…?
- サービスの主要機能を使っているときに、メアドやプロフイールを参照するシーンは実は少ない
- ActiveRecordがやってくれる
- 今までの経験から大丈夫でした
分けても大丈夫としてメリットは?
- サービス利用開始時点で、個人情報取得の離脱を減らせる
- 秘匿性の高い情報を分析環境から見えなくするのとか楽にできる 🙌
- Rails 8, generate authentication 🤔
- email_address, :password_digestついてきちゃう・・
ログインした状態でサービスの機能を使える
- ログインしているユーザーを判別できる
- リクエストの操作主体が、このアイデンティティを持った存在である、ことがわかればOK
- current_userのよくある実装
- session[:user_id]にusers.idが入っている
- APIでも考え方は同じ、JWT や BearerトークンにユーザーIDを判別できる情報が載っている
- current_userができること
- 自分の所有物にアクセスできる
- ユーザーとして、サービスを利用できている
- この設計だと、ログイン機能を作り込まなくても「ユーザーIDを指定してsessionに入れる」仮実装ができていれば、機能開発ができる!
ユーザーとして、ログインしたい
- ログイン操作そのもの
- ActiveRecord標準なら、has_secure_password
- 確認できたユーザーのIDをセッションに保存する
- IDaaSやライブラリも使える
- IDaaSの識別子を入れるテーブルをbelongs_to :userで用意しておく
- OmniAuth
- OmniAuthの概念圧縮の発表もあった!(聞きたい)
- OmniAuthから学ぶOAuth 2.0 | Kaigi on Rails 2024
- https://speakerdeck.com/ykpythemind/omniauthkaraxue-buoauth2-dot-0-kaigi-on-rails-2024
- もちろん自分で実装することもできる、ご安全に!
- 確認できたユーザーIDをセッションに保存する
ユーザーとして、サービスに登録したい
- サービスにおけるユーザーのアイデンティティが生まれるのはいつなのか
- 登録フローを完了したときに、アイデンティティが生まれるという考え方
- 登録している段階は、usersレコードはできない
- 登録しようとしていることを表す UserRegistration モデル
- belngs_to :user, optional: true
- メールアドレスや事前聴取のアンケートなども保存できる
登録完了したらユーザーができる
- UserRegistration から User以下一連のデータグラフを作成する
- ここではじめて、そのサービスにおけるユーザーとしてのアイデンティティが生まれる
- Userテーブルは今いる人だけ、登録完了したユーザのみ
- 登録中テーブルも複雑に育ちうる、それらをUserから分離できてうれしい
ユーザーとして、退会したい
- 退会した場合も「いたこと」の事実は残す必要がある
- 存在した、というアイデンティティは消えない
- 実装上は、usersを参照する外部キー制約として現れる
- 持ち物を全部消さない限り、存在した事実は消せない
- usersレコードは消さないが、user_credeintialsは消せる、個人情報は消すことができる
- credentialsを消せばログインもできなくなる
- 副次的に「有効なメアド/電話番号」にユニーク制約をかけられる
- 「論理削除するなの会」の会員としては、論理削除だとユニーク制約かけられなくて辛い
管理スタッフが特別な権限でログインしたい
- 管理スタッフとユーザーとして、アイデンティティをすっかり分けておくと良いのでは
- サービスを利用する目的や利用方法が異なる場合は、アイデンティティが根本的に違うと理解する
- 実装も完全に分ける
- アイデンティティプール(DBのテーブル)も分けると良い
まとめ
- ユーザーが「いること」それ自体を表現するのが最も大事
- ログイン状態とは、リクエストの主体たるユーザーが識別可能であること。ログインは識別可能にすること
- 登録フローの途中では、まだユーザーとして存在していない
- 退会しても「いたこと」の事実は残る
- 管理スタッフは「ユーザーとして」いるのではない、アイデンティティプールを分けるべき
そうするとなにが嬉しいのか
- ユーザー管理周りは、いろんな複雑さが存在する
- 原則に沿って整理できる。不合理でないし、かっこいいコードにできて面白い
- いい感じに保てると、サービスの目玉機能を伸ばす役にも立てる 🥳
- ER図を自動生成すると太陽が2つある図になりがち
- 機能の主たるテーブルと、usersテーブル
- 大事なエンティティなので、よく考えていきたい
- かっこいいコード、ユーザー状態をstatus列なしで表できる
UserRegistration.scope :succeeded, -> { joins(:user) } UserRegistration.scope :expired, -> { where.missing(:user).where(created_at: ..N.days.ago) } User.scope :active, -> { joins(:credentials) } User.scope :withdrawn, -> { where.missing(:credential) }
ちなみに発表の中で触れられていた、OmniAuthの発表スライドはこちらでした。 speakerdeck.com
基調講演: WHOLENESS, REPAIRING, AND TO HAVE FUN
島田浩二さん
札幌のえにしテック代表で、様々な本を翻訳されている島田さんのお話でした。
このお話を現地で聞くことができて良かったです。胸に響くものがありました。
「修復しながら変化に適応し続ける」「DON'T FORGET TO HAVE FUN!」というメッセージを受け取りました。
Kaigi on Rails の締めくくりに最高の基調講演でした。ありがとうございました。
書籍「RailsによるアジャイルWebアプリケーション開発」
今日、伝えたいこと
- ソフトウェアを設計する際に大事だと考えていること
- 全体が機能するように設計する
- 変化に向けて設計する
1. 全体が機能するように設計する
- Web開発していく中で学んだこと
- システムでユーザーに価値を提供する意識
- システム全体が自分たちの問題を解く形にうまく構成されていくことが大事
①システムコンポーネントを知る
- 良い設計には物事の正確な観察が必要
- 適切な場所で適切な問題を解けるように
- アプリケーションだけでなんでも解こうとしてない?
②設計のパターンを知る
- Webシステムのよくある問題と、それに対するよくある解決策を知る
- ex. リクエストを複数のサーバーで処理したい -> サーバーの手前にロードバランサーを置く などなど
- 設計とは、現実世界の問題を1つずつ解いていくことの積み重ね
- その結果現れるのがシステムの構造、最初に全体構造があるわけでなく、そうなるもの
どこから始めると良いか
- Web技術、データベース技術、計算機システムなど、自分のアプリケーションを取り巻く要素につい勉強していく
2. 変化に向けて設計する
- 現実の難しさ
- 設計の開始時点はもっとも何も分かっていないタイミング、不確実性
- システムの前提が分かりきっていない中でも設計を進めていかなくてはならない
オプションを手にいれる
- オプションとは、将来的な選択肢を買う権利
Tidy First?
というケントベックの書籍(英語だけ->近々日本語でも出版されるよう)
ソフトウェア設計は、振る舞いの変更への準備だ。今日行う設計は、将来振る舞いを変更する「オプション」に支払うプレミアムのようなものだ。
- 将来変更するためのオプションを買っている
オプションのための設計
- 何にでも対応できるような構図やしくみを作る? → ×
- 選択肢を広げたかったのに、実際には狭めて待ち構えていることになってしまう
- どの方向にも進めるようにシンプルに作る、選択肢を狭めないことが大事
- そのためのアプローチ: できるだけ標準の道具、標準の方法で、必要十分に作る
オプションのためのWebアプリケーション設計
- Railsはオプションのための設計をサポートしてくれるフレームワーク
- Ruby on Rails scales from HELLO WORLD to IPO
- 標準的な道具で、最小の最善構成を用意してくれている
The menu is omakase
- 本当に必要になるまで複雑な仕組みを避けられる
- オプションのための設計のためには、Railsを使いこなす!
- Railsの思想に沿って標準の道具を十分に活用する
本当の難しさ
- シンプルさを維持し続けること
- 機能変更や追加が続いていく中でうまくやらないと将来の変更に向けた設計が破られてしまう
- 「時を超えた建設の道」 24章 修復のプロセスを拠り所にしている
- 「元の状態に戻す」のではなく、「変わった後の全体と再度調和させる」
- とてもクリエイティブな活動
- 修復し続けることで変化に適応し続ける
- 手本は、Ruby on Rails
- ずっと Ruby on Rails
- 手触りがずっとRails、でも変わっていっている
- Web標準が変わっていくから、Railsも変わっていく(DHH)
- 修復が必要な箇所に気づくには?
Process Feel
- Process Feel という考え方がヒントになるのでは?
- SRE conference
- 複雑なシステムの状態を直感的に理解する
- ケントベック先輩「感じる」といっている
- Process Feelを養うには?
- 複雑性によりオペレーターへの認知負荷をいかに減らせるか
- 概念圧縮
- The One Person Framework
- システム全体への感知能力を増やすことに価値を置いたフレームワーク
- Rails Way は導きの星
- One Person Framework で修復箇所を完治する能力を高めシステムを修復し続ける
どこから始めるといいか
- Railsの上手な使い方を学んでいく
- 書籍で
- コードで(once)
- コミュニティで
- Kaigi on Rails!!
- Railsの上手な使い方を持ち寄ってみんなで学んでく
- DHHは、たのしそう
- 楽しさにはビジネス価値があります by Martin Fowler
- たのしいが大事
- DON'T FORGET TO HAVE FUN!