Upgrade to Pro — share decks privately, control downloads, hide ads and more …

SREの視点で挑む Redis Stream 無停止移行

Avatar for GO Inc. dev GO Inc. dev
January 27, 2026
12

SREの視点で挑む Redis Stream 無停止移行

GO TechTalk #32 タクシーアプリ『GO』の最強SREチーム で発表した資料です。

■ YouTube
https://www.youtube.com/watch?v=JKqmWhSqusg
■ connpass
https://jtx.connpass.com/event/379672/

Avatar for GO Inc. dev

GO Inc. dev

January 27, 2026
Tweet

More Decks by GO Inc. dev

Transcript

  1. © GO Inc. GO株式会社 技術戦略部 SRE / 前田 恭男 タクシーアプリ『MOV』および『GO』の開発に

    従事。 iOSアプリからバックエンドへと専門性を広 げ、現在はSREとして『GO』のシステム信頼 性向上や基盤整備を担当。 自己紹介 2
  2. © GO Inc. あるサービスでGKE上に構築されたRedisが利用されており 運用観点からマネージドなRedisに移行する事になった 該当サービスはリアルタイムなイベント配信基盤として Redisを利用しており以下の機能を持つ • 配信されたイベントは即時に配信できること •

    複数人が同じイベントを受信できること • 受信者ごとに既読管理ができること 上記の機能をRedis Streamを使って実現しており 通常のKVSとは異なるRedisの移行方法を考える必要がある きっかけ 3
  3. © GO Inc. Redisのデータ構造の1つ 書き込み側(Producer)と読み取り(Consumer)が独立して管理されており、読み取り側では未読 /既読と いった状態管理が行われる Redis Pub/Subも近い機能を持つが、最も大きな違いはデータの永続性にあります。 •

    Redis Pub/Sub: Publishした瞬間にSubscribeしている接続先のみが取得できる • Redis Stream: Stream側で永続化されており、Consumer Group経由で後から取得することが可 能 Redis Streamとは 4
  4. © GO Inc. 以下の要素を持つ • Entry ◦ 登録したメッセージ、発行毎にID(デフォルトは時刻ベース)を持つ • Stream

    ◦ Producer側のEntry格納先、追記型のデータ構造 • Consumer Group ◦ Consumer側がStreamからどこまでEntryを読み込んだのか何を既読にしたかを管理 ◦ 特徴として複数のConsumerが並列、かつ非同期にStreamからデータを取得可能 • PEL(Pending Entries List) ◦ Consumer Group内で読み出されたが既読になっていない Entryのリスト • ACK ◦ EntryのIDを指定してConsumer Group内のPELから除外し、既読扱いにする Redis Streamとは 5
  5. © GO Inc. 一般的な以下の3つの手法が使えない • アプリケーション側でダブルライト • SLAVEOFコマンドでデータを移行先Redisインスタンスにレプリケーションさせる • サービス自体をメンテナンスモードにして旧

    Redisデータを新Redisにコピーする Redis Streamの移行が難しいところ Entry IDは {ミリ秒}-{index}で自動採番 登録タイミングによっては、新旧 Redisで同一 IDではなくなる → ACKする際に、ダブルライトしたデータを 特定できなくなるため手法としては難しい 12
  6. © GO Inc. 一般的な以下の3つの手法が使えない • アプリケーション側でダブルライト • SLAVEOFコマンドでデータを移行先Redisインスタンスにレプリケーションさせる • サービス自体をメンテナンスモードにして旧

    Redisデータを新Redisにコピーする Redis Streamの移行が難しいところ マネージド側の制約でSLAVEOF 系が利用 できないため、そもそもこの手法ではできない 13
  7. © GO Inc. 一般的な以下の3つの手法が使えない • アプリケーション側でダブルライト • SLAVEOFコマンドでデータを移行先Redisインスタンスにレプリケーションさせる • サービス自体をメンテナンスモードにして旧

    Redisデータを新Redisにコピーする Redis Streamの移行が難しいところ 切り戻し手順が複雑になる 「新→旧へ戻す」際にそのまま旧Redisに向 き先を変えると新Redisで配信されたEntryが 欠損してしまうため、手法としては難しい 14
  8. © GO Inc. 今回の割り切り(設計判断) 要件として落としたこと • 順序保証 ◦ 本来は重要だが、イベント数がそこまで多くないので不要と判断 •

    旧RedisにConsumerによってACKされないまま残り続けるイベントをどうするか ◦ 今回のイベント配信は一定時間受信されなかったものは不要になるため、移行期間を一定時 間設けて旧Redisに残り続けるイベントを無視して移行を進めることにした • 移行中に全Consumer GroupのPEL件数を監視する ◦ 一括で取得できるコマンドがなく Consumer Group毎にRedisコマンド叩いて確認する必要が あり、今回はConsumerの数が多くRedis負荷が高くなる危険性があるため実施せず 25
  9. © GO Inc. ビジネスロジック側でRedis構造体に直接 参照されており、抽象化されていなかった このままだと移行ロジックを直接 Redis構 造体に書く羽目に。。。 アプリケーションコード側で修正 //

    ビジネスロジック type EventLogic struct { redis *Redis // Redis構造体を直接参照 } func NewEventLogic(redis *Redis) *EventLogic { return &EventLogic{ redis: redis, } } type (el *EventLogic) Add(ctx context.Context, key string, value string) error { return el.redis.XAdd(ctx, key, value) } type (el *EventLogic) Read(ctx context.Context, key string, id string) (string, error) { return el.redis.ReadMessage(ctx, key, id) } type (el *EventLogic) Acknowledge(ctx context.Context, key string) error { return el.redis.Ack(ctx, key) } 27 既存のコード
  10. © GO Inc. 辛いので interfaceにして処理の抽象化 logic側でもinterfaceを利用することで、 移行ロジックを既存ロジックに混ぜる必 要がなくなり、移行ロジックにフォーカス した構造体を作れるようになった アプリケーションコード側で修正

    // RedisStreamでイベントを管理するためのインターフェース type EventStreamer interface { XAdd(ctx context.Context, key string, value string) error ReadMessage(ctx context.Context, key string, id string) (string, error) Ack(ctx context.Context, key string) error } // ビジネスロジック type EventLogic struct { streamer *EventStreamer // interfaceにして処理の抽象化 } func NewEventLogic(streamer *EventStreamer) *EventLogic { return &EventLogic{ streamer: streamer, } } 28 移行のために変えたコード