Slide 1

Slide 1 text

コロプラ社の Redis 事情 尾山貴康 2022年4月27日(水)

Slide 2

Slide 2 text

自己紹介 ● 尾山貴康 ● 入社6年目(業界10年目) ● サーバー基盤グループ ○ 技術検証 ○ 社内ライブラリのメンテ ○ 開発チームのサポート

Slide 3

Slide 3 text

もくじ ● Redis とは ● アーキテクチャー紹介 ● 今のアーキテクチャーになった経緯 ● 独自拡張した機能の紹介 ● 利用事例

Slide 4

Slide 4 text

Redis とは

Slide 5

Slide 5 text

2009年に Salvatore Sanfilippo が開発 メモリ上で動作する Key Value Store Remote Dictionary Server の略らしい 特徴 ● インメモリーなので高速(ミリ秒未満) ● 主にデータの共有キャッシュとして使われる ● Key Value Store だけじゃない ○ Sorted Set (ランキングなどに使う) ○ Pubsub(Queueなどに使う) ○ Streams(チャットなどに使う) Redis とは

Slide 6

Slide 6 text

最新のアーキテクチャー紹介

Slide 7

Slide 7 text

Administration Redis Enterprise Cloud Durable Redis Enterprise Cloud Volatile Redis Enterprise Cloud ロストしたくないデータ用(カウンターなど) シャーディング あり データ永続化 あり レプリカ あり ロストしてもいいデータ用(キャッシュなど) シャーディング あり データ永続化 なし レプリカ なし Kubernetes Cluster アーキテクチャー API Servers GKE Pods Twemproxy 内部管理用(管理ツール → Podへの通知に使う) シャーディング なし データ永続化 なし レプリカ なし

Slide 8

Slide 8 text

今のアーキテクチャーになった経緯

Slide 9

Slide 9 text

Redis Array (Client Sharding) on VM API Server PHP PHP PHP … 第1世代(~2017) ● アプリ側で分散ロジックを実装 ● 接続先はアプリ側の設定ファイルで管理

Slide 10

Slide 10 text

Redis (Client Sharding) on VM 問題 ● IPアドレスをアプリの設定ファイルにベタ打ちで更新が大変 ● 一台でも落ちるとマニュアル復旧 ● スケールインやアウトするのが大変 ● スケールするとデータが消える 第1世代(~2017)

Slide 11

Slide 11 text

Redis Cluster on K8s ● ダウンタイムなしでスケールが可能に ● Sharding のロジックが Cluster 側に移行によりアプリ側のメンテナンス性 UP Cluster Mode API Servers GKE Pods PHP PHP PHP … 第2世代(2017~2019)

Slide 12

Slide 12 text

Redis Cluster on K8s ops / sec nodes 理想 現実 request / sec connections 第2世代(2017~2019) 独自調査 Redis Benchmark (https://redis.io) 問題 ● Node 数を増やしてもパフォーマンスが線形に伸びない(左の図) ● Connection 数が増えるとパフォーマンスが下がる(右の図)

Slide 13

Slide 13 text

Twemproxy + GCP Memory Store 第3世代(2019~2020) Twemproxy API Servers GKE Pods PHP PHP PHP …

Slide 14

Slide 14 text

Twemproxy + GCP Memory Store 第3世代(2019~2020) GCP Memory Store とは ● Google Cloud 社のマネージド Redis ● 自動フェイルオーバー ● 簡単レプリケーション

Slide 15

Slide 15 text

Twemproxy + GCP Memory Store Twemproxy とは ● Twitter 社が開発した Redis Proxy ● 機能 ○ Sharding ○ Connection Pooling ○ Command Pipelining Twemproxy API Servers GKE Pods PHP PHP PHP … 第3世代(2019~2020)

Slide 16

Slide 16 text

第3世代(2019~2020) Twemproxy イメージ Twemproxy PHP PHP PHP PHP コネクション数 x3 コネクション数 x1 PHP GET k1 GET k2 INCR k3 PHP GET k1 GET k2 INCR k3 BEFORE AFTER − 1 Pod 1 Connection − 1 Process 1 Connection localhost:6379 K8s Pod K8s Pod

Slide 17

Slide 17 text

Twemproxy + GCP Memory Store 問題 ダウンタイムなしでスケールできない Memory Store が Failover するのに数秒〜数分かかる 第3世代(2019~2020)

Slide 18

Slide 18 text

Twemproxy + Redis Enterprise Cloud Twemproxy localhost:6379 API Servers GKE Pods PHP Redis Enterprise Cloud PHP PHP … 第4世代(2021~) ● Failover が速い(最大1分と書いてあるが実際は1秒以内) ● スケールしてもデータが消えない(約1秒ダウンする)

Slide 19

Slide 19 text

残ってる問題 ● Redis Enterprise Cloud ○ 日本語のサポートがない ○ ポート割り振りがランダムで使いにくい ● Twemproxy ○ 2016 以降メンテされてない(最近メンテナーが現れたが、更新は少なめ) ○ PhpRedis との相性が悪い ■ Twemproxy には一部独自エラーがあるが、 PhpRedis はそれを受け取っても 握りつぶしてしまう(false を返す) ● 現在は Twemproxy 側を改造して対応 ● PhpRedis に修正プルリク提出中(次のバージョンで入れてくれるかも?) https://github.com/phpredis/phpredis/pull/1832 第4世代(2021~)

Slide 20

Slide 20 text

Administration Redis Enterprise Cloud Durable Redis Enterprise Cloud Volatile Redis Enterprise Cloud Kubernetes Cluster アーキテクチャー API Servers GKE Pods Twemproxy

Slide 21

Slide 21 text

独自拡張した機能の紹介

Slide 22

Slide 22 text

● Laravel Framework の PhpRedis のラッパークラス ● 欲しい機能がいくつか足りなかったので拡張 1. Twemproxy 対応 2. 切断時の自動リトライ 3. ちゃんとしたエラーハンドリング PhpRedisConnection の拡張

Slide 23

Slide 23 text

1)Twemproxy 対応 ● Twemproxy 経由だと、FLUSH / KEYS / SCAN が使えない ● Twemproxy 専用のアダプターを作成して以下の実装を加える a. 上記コマンドに実行時はTwemproxy の API を叩いて接続先一覧を取得 b. 一台一台、Twemproxy を経由せず、単体の Redis として接続 してコマンド実行 ● どれも頻繁に使うコマンドではないのでパフォーマンスは気にしない Twemproxy PHP-FPM

Slide 24

Slide 24 text

2)切断時の自動リトライ ● Redis Enterprise は Failover 時に数秒切断するがすぐ復旧する ● なので基本的には Retry すれば通る ● ということで、Exponential Backoff + Jitter な Retry を実装 ※ 最近 PhpRedis 側に同等の機能が実装されたので廃止予定 https://github.com/phpredis/phpredis#retry-and-backoff

Slide 25

Slide 25 text

3)ちゃんとしたエラーハンドリング ● PhpRedis は一部のエラーを throw してくれない ● なぜか false を返す ● 本当のエラーは Redis::getLastError() の中に入っている ● コマンド実行部分を拡張してエラーがないかチェックするコードを追加 >>> $redis->set('abc', '_'); => trueる >>> $redis->incr('abc'); => false >>> $redis->getLastError(); => "ERR value is not an integer or out of range\0"

Slide 26

Slide 26 text

利用事例

Slide 27

Slide 27 text

レスポンスキャッシング

Slide 28

Slide 28 text

レスポンスキャッシング レスポンスがタイムアウトするとめんどくさい問題 POST /battle/finish ユーザー API サーバー バトルインスタンス削除 200 OK timeout サーバー側では処理が終わっている クライアント側はエラる どうする?

Slide 29

Slide 29 text

レスポンスキャッシング(過去の対応) 個別に対応 ↓ 実装、テストの工数増加 ユーザーをタイトルに戻す ↓ ユーザーイライラ 離脱率上昇 😭 機会損失 😭

Slide 30

Slide 30 text

レスポンスキャッシング Timeout はそこそこ頻繁に発生する ● トイレに入った時 ● エレベーターに乗った時 ● WiFi → Cellular (家を出た時など) ● 公共 WiFi (駅など) 大体 Request の 0.01%(1万リクエストに1回) はリトライ処理

Slide 31

Slide 31 text

レスポンスキャッシング ということで以下の方法で対応 クライアントは Timeout したらとりあえずリトライする サーバーはGET以外のレスポンスを Redis にまるごと保存しておき、 同じリクエストがリトライされてきたら キャッシュされている情報をそのまま返す

Slide 32

Slide 32 text

レスポンスキャッシング(シーケンス図) POST /battle/finish Idempotency-Key: クライアント API サーバー Redis SET : POST /battle/finish Idempotency-Key: Retry-Count: 1 ❌ GET : Retry-Count 検知 キャッシュ参照 タイムアウト 思い届かず 200 OK 208 Already Reported Idempotency-Key 検知 レスポンスを キャッシュに保存 リトライ!

Slide 33

Slide 33 text

レスポンスキャッシング Redis をフルに活用することによって、 ユーザーが快適にプレイでき、 エンジニアにも負担が少ない実装を実現することができました。

Slide 34

Slide 34 text

まとめ コロプラでは ● Redis Enterprise Cloud を使っています ○ Failover が高速で障害にならないから ○ データを維持したスケーリングができるから ● Twemproxy を経由して Redis に接続しています ○ Redis のコネクション数を減らすため ● 上記を快適に使えるようにフレームワークを改造しました ○ Twemproxy 対応、リトライ処理 など ● 上記があることで Redis をフル活用した開発ができています ○ レスポンスキャッシュ など