サービス無停止でRedisを移行した話

# はじめに

自分の組織ではRedisのアップグレードに伴って、VPCを移行するニーズがありました。
アップグレードするためだけなら、色々方法はあります。
AWSが公式で推奨する方法は別にありますし、Redisの公式も別です。
この記事では、Rediｓをサービス無停止でのRedis移行に関してをまとめたいと思います。

# この記事の想定読者

別のネットワークのRedisにリアルタイムで同期してサービス無停止で切り替えを行いたい人

つまり、

- AWSからオンプレミスへ
- AWSからGCPなど別のクラウドサービスへ
- NonManagedなRediｓからElastiCacheのようなマネージドサービスへ

サービス無停止で切り替えができることと同義かとおもいます。

AWSが提供しているデータ同期サービスDMSは
AWS -> xxx のデータ同期ができないです。
なので、サービスで行いたい場合は参考になるかと思います。

# きっかけ

自分が開発に関わっている組織ではRedis3系が使われていました。
AWSのElastiCacheのRedisのEOLが７月末に行われることになり、アップグレードする必要がありました。
https://docs.aws.amazon.com/ja_jp/AmazonElastiCache/latest/red-ug/deprecated-engine-versions.html
ElastiCacheのVPCを移行したいというニーズもあったことから、アップグレードを機にVPCも移行する方針になりました。

# Redisはサービス無停止移行ができたか

自分が移行を行ったサービスでは一切のエラーなく、問い合わせ０件、社内からのエスカレーションなく移行できました。

ただ、完全にデータの一致を担保しなければならないのなら、難しそうです。
Redisで扱うデータが少し誤差が合っても問題ないと判断できるならいけます。

事前の調査として、Redisの使われ方を理解しておいたほうが良いかもしれないです。
（概ねどのような使い方のキーがあって、どのようなデータ型で、どの頻度で更新されるのか）

つまづきポイントがたくさんでてくると思うので、余裕のあるスケジュールが必要かもしれないです。
自分の場合はPjとして、概ね２ヶ月かかってしまいました。

# サービスの概要

ざっくりとわかる程度に雑にサービスの説明します。

AWSを使っています。
複数のバックエンドアプリケーションからサービスが構成されています。
全アプリケーションがRails on Rubyです。
そこそこのリクエストが毎分きます。
分散型のアプリケーションになっています。


# サービス無停止移行の方法

サービス無停止移行はアプリケーション側のRedisの参照を切り替えることで移行しました。

下図は移行元のRediｓをV3（version 3x）移行先のRedisをV6（version 6x）としてます。
Appというのは何種類かのアプリケーションが複数台のサーバです。

![Screen Shot 2023-07-05 at 0.30.26 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315979/e78b0d88-8617-e962-0e54-badb6e7f07ba.png)


## 事前準備

上記の移行方法を実現するために、いくつか事前準備が必要でした。

1. 両書き込みする
    1. アプリケーション側でRedisに対して書き込み等する処理をオーバーラップして、２台のRedisに書き込みできるようにする（同じタイミングで同じコマンド叩くようにする）
1. データ一致させる
    1. RedisV3とRedisV6のデータをttl含め、一致させる
1. 両Redisのデータ一致を確認する
    1. 全キーを総なめして全データを確認する
    1. 全データ一致が確認できたと判断できたら、切り替え作業する

`両書き込み` はアプリケーション側の実装漏れや、デプロイ漏れがあって、苦戦しました。
`データ一致` は書き込み等のデータ更新が高頻度で行われていて、なかなか一致していないようにみえて、かなり苦戦しました。
`データ一致確認` は高頻度で更新されるもの・更新頻度高く、データ量が大きいもの　のデータ一致の確認にてこづりました。

# データ同期の具体的な話

データをするためには

- app側の修正
    - アプリケーション側の書き込み処理でV3とV6へ同時に書き込み行くようにする
- マイグレーションツール作成
- データ確認ツール作成

が必要です

## 今回の対象となるRedisの使われ方

最初はアプリケーション側でちゃんと両書き込みできるようになっていれば困らないと思っていたのですが、
ちゃんと知っておいたほうが良かったです。
なかなかうまく動機が取れなかったので、結局アプリケーションのコード全部読んで、データも分析しました。

この記事で扱うRedisは２種類です。

- sessionを管理するRedis
- アプリケーションでキャッシュ使うときにとりあえず使うRedis（こっちが厄介）

それぞれのRedisの特徴

- sesion
    - command count: 11M/h
    - items(dbsize): 13M (avg)
    - data type は主にstring
- other
    - command count: 20M/h
    - items(dbsize): 7M (avg)
    - data type は list, string, sroted set, set, hash
        - 一部listのデータが2Mあった
        - 一部string型のデータはbyteだった
        - 一部list型のデータの要素はbyteだった

メトリクスはAWSのコンソールから確認しました。
データ型はアプリケーションのコードを読むのと、Redisからに当該のキー郡をTYPEコマンドで確認しました。

## アプリケーション側の修正

session管理してるクラスはgemつかっていたので関連処理はソース読みました。

### session

https://railsguides.jp/configuring.html#config-session-store

```diff

< config.session_store :my_custom_store
>  config.session_store :patch_my_custom_store

```

```ruby
class MyCustomStore # sessionの管理してるクラス
end

class PatchMyCustomStore < MyCustomStore
    def initialize(*arg)
        @bg = Redis.new({ xxx })
        super(*arg)
    end

    def write_sesion(xxx)
        begin
            # かきこみ
        rescue StandardError => e
            # 同期取れていない原因になるので、logだす
            # 既存処理に影響与えないようにちゃんとにぎりつぶす
        end
        super(xxx)
    end

    def delete_session # same with write_sesion
    end
end
```

### other

組織として生のRedis classではなくて、wrapしているものをつかっていたので、そいつにパッチあてました

```diff
< class OtherRedis < Redis
> class OtherRedis < PatchRedis

> class PatchRedis < Redis
>    def initialize(*ag)
>        @bg = Redis.new({ xx })
>        super(*ag)
>    end
>    %i[対象となる関数].each do |method| # 自分の場合はめんどくさかったので、RedisのAPIDocに書いてある更新系の処理を全部かいた
>    define_method(method) do |*args|
>        begin
>            @bg.send(method, *args)
>        rescue E => e
>            # log
>        end
>    end
> end
```

## データ同期ツール

OSS色々あると思いますが、うまく最新の状態を動悸するのができなかったので、自作しました。

高頻度で更新があるようなRedisのデータ同期はつらいです。
一致するまで、上書きする処理をループすると解決しました。

データ量も膨大で、処理にかなり時間かかるのでJavaでマルチスレッドで書きました。
データの使い方にもよると思うのですが、Rubyで 各Redisのデータ型をbinaryで扱えるので、Javaで型にキャストするときにデータが変わってしまいます。ハマりました。

### データが変わる例

string型だけでなくすべてのデータ型でありえます

```ruby
# from rails console
r = Redis.new(xx)
key = 'hoge'
r.set(key, binary_data)
```

```java
Jedis r = new Jedis(xxx);
String hoge_data = r.get("hoge"); // hoge_dataはrails consoleから設定した値と異なる。
```

### 解決法

https://redis.io/commands/dump/

https://javadoc.io/doc/redis.clients/jedis/latest/redis/clients/jedis/Jedis.html#dump-byte:A-

```java
Jedis v3Redis = v3JedisPool.getResource();
Jedis v6Redis = v6JedisPool.getResource();
String key = "hoge";
Long ttl = v3Redis.ttl(key);
v6Redis.restore(key, ttl == -1 ? 0 : v3Redis.ttl(key) * 1000, v3Redis.dump(key));
```

## データ一致確認

- 各データ型でパターンマッチさせる
- 念の為、１０回くらい間隔を開けてrecheckする
- データ量・更新頻度が高いlist型のデータは更新頻度の方（先頭か末尾）から lrange, rrange で１万件くらい取得して、loopで確認する

と一致することが確認できました。
更新頻度が高いと、一致したか判断するのが難しいです。

# 移行のタイミング

ElastiCacheのメトリクスをみて、コネクションが少ないタイミングだと、更新頻度がすくないので、切り替えのタイミングにデータ差分がないことを担保しやすそうです。

ちなみに、自分の場合はRedisの切り替えを段階を踏んで行いました。

![Screen Shot 2023-07-05 at 0.30.26 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315979/5642521e-a048-df32-b266-00a290ba0b54.png)

実行時はホストの切り替え後にもデータ差分を確認して、差分があれば、適切な値に書き換えるオペレーションを行うことを計画していました。
なので、ユーザ影響が少なさそうな深夜早朝の時間帯で行いました。

結果的には差分がなかったです。
ビジネス要件が許してくれるなら日中でも良いと思います。

# さいごに

この記事は個人で書いたものなので、色々と濁しました。
質問等あれば、メッセージいただけると。
ネット上に情報が少なかったので、だれかのなにかの助けになれば幸いです。

協力してくださったチームメンバーと会社に感謝