- ステップ1: 新しいインスタンスの設定のサポート
- ステップ2:新しいインスタンスへの書き込みと読み込みのサポート
- ステップ3:データのマイグレーション
- ステップ4: マイグレーション後のクリーンアップ
新しいRedisインスタンスの追加
GitLabは複数のRedisインスタンスを利用することができます。これらのインスタンスは機能的にパーティショニングされているので、たとえばCIトレースのチャンクをあるRedisインスタンスに保存しつつ、セッションを別のRedisインスタンスに保存することができます。
時々、新しいRedisインスタンスを追加したくなるかもしれません。通常、これはキャッシュや共有状態のような既存のインスタンスの1つから分割された機能パーティションになります。このドキュメントでは、既存のデータを処理する新しいRedisインスタンスを追加するためのアプローチを、過去の例に基づいて説明します:
このドキュメントでは、新しいRedisインスタンスの準備と設定の運用面については詳しく説明しませんが、エピック例にはこれまでのアプローチに関する情報が含まれています。
ステップ1: 新しいインスタンスの設定のサポート
新しいインスタンスを使用するように機能を切り替える前に、インスタンスの設定とコードベースでの参照をサポートする必要があります。主なインストールタイプをサポートしなければなりません:
フォールバックインスタンス
アプリケーションコードでは、新しいインスタンスが設定されていない場合に備えてフォールバックインスタンスを定義する必要があります。例えば、GitLabインスタンスがすでに別の共有状態のRedisを設定しており、その共有状態のRedisからデータをパーティショニングしている場合、新しいインスタンスの設定は共有状態のRedisが存在しないときのものにデフォルト設定されるべきです。そうしないと、新しいRedisインスタンスが利用可能になるとすぐに設定しないインスタンスを壊してしまう可能性があります。
Gitlab::Redis::Wrapper
(すべてのRedisインスタンスの基本クラス)に.config_fallback
メソッド を定義して、このインスタンスが設定されていない場合に使用するインスタンスを定義することができます。SharedState
にフォールバックするFoo
インスタンスを追加する場合は、このようにします:
module Gitlab
module Redis
class Foo < ::Gitlab::Redis::Wrapper
# The data we store on Foo used to be stored on SharedState.
def self.config_fallback
SharedState
end
end
end
end
このフォールバックが正しく機能するように、trace_chunks_spec.rb
のような仕様も追加する必要があります。
ステップ2:新しいインスタンスへの書き込みと読み込みのサポート
新しいインスタンスにマイグレーションする場合、データがオンになっているケースを考慮する必要があります:
- 古い’(元の)インスタンス。
- 今回サポートが追加された新しいインスタンス。
その結果、何らかの条件によっては、両方のインスタンスからの読み込みと、両方のインスタンスへの書き込みをサポートする必要があるかもしれません。
どのような条件を使用するかは、マイグレーションするデータによって異なります。上記のトレースチャンクのケースでは、データがどこに保存されているかを示すデータベースカラムがすでにありました(Redis以外にもストレージオプションがあるため)。
データのライフタイムが非常に短く(せいぜい数分)、クリティカルではない場合、このステップは適用されないかもしれません。その場合、少量のデータ損失が発生しても問題ないと判断し、設定のみで切り替えることができます。
データがどこに保存されているかを示すより自然な方法がない場合は、機能フラグを使うのが便利でしょう:
- アプリケーションを再起動する必要はありません。
- すべてのアプリケーションインスタンス(Sidekiq、API、Webなど)に同時に適用されます。
- インクリメンタルなロールアウトをサポートし、理想的にはアクター(プロジェクト、グループ、ユーザーなど)ごとに、エラーを監視して簡単にロールバックできるようにします。
ステップ3:データのマイグレーション
新しいインスタンスを GitLab.com の本番環境とステージング環境に設定します。うまくいけば、この変更をステージング環境で効果的にテストすることができます。
それが終わったら、本番環境にこの変更をロールアウトします。機能フラグに関する標準的なインクリメンタルロールアウトのドキュメントに従って、インクリメンタルな方法で行うのが理想的です。
しばらく本番環境で新しいインスタンスを100%使用し、イシューがなければ、次に進みます。
提案するソリューションフォールバック戦略でMultiStoreを使用してデータをマイグレーションします。
UXの観点から不便を感じることなくユーザーを新しいRedisストアにマイグレーションする方法が必要です。また、新しいインスタンスで何か問題が発生した場合、「古い」Redisインスタンスにフォールバックする機能も必要です。
マイグレーション要件:
- ダウンタイムなし
- データを保存するためのTTLが切れるまで、保存されたデータが失われることはありません。
- 機能フラグまたはENVバーを使用した部分的なロールアウト。
- スイッチの監視
- Prometheusメトリクスを導入。
- 新しいインスタンスやロジックが期待通りに動作しない場合に、ダウンタイムなしで簡単にロールバックできます。
ゼロダウンタイムのDBテーブルのリネームと多少似ています。両方のRedisインスタンス(新旧)にデータを書き込む必要があります。新しいインスタンスから読み込みますが、失敗した新しい専用Redisインスタンスからプリフェッチするときは古いインスタンスにフォールバックする必要があります。新しいインスタンスでイシューや例外が発生した場合でも、古いインスタンスにフォールバックしてログに記録する必要があります。
提案するマイグレーション戦略は、MultiStoreを実装して使用することです。セッションキー専用の新しいRedisインスタンスを追加して、このアプローチを使用しました。また、MultiStoreには対応する仕様が用意されています。
MultiStoreはredis-rb ::Redis
インスタンスのように見えます。
ステップ 1 で追加した新しい Redis インスタンス・クラスで、Redisメソッドを::Gitlab::Redis::Wrapper
module Gitlab
module Redis
class Foo < ::Gitlab::Redis::Wrapper
...
def self.redis
# Don't use multistore if redis.foo configuration is not provided
return super if config_fallback?
primary_store = ::Redis.new(params)
secondary_store = ::Redis.new(config_fallback.params)
MultiStore.new(primary_store, secondary_store, store_name)
end
end
end
end
MultiStore は、プライマリストアとして新しい Redis インスタンスを、セカンダリストアとして古い (fallback-instance)Redis インスタンスを提供することで初期化されます。3番目の引数はstore_name
。これは、MultiStoreの実装を異なるRedisストアに同時に使用する場合に、ログ、メトリクス、機能フラグ名に使用します。
デフォルトでは、MultiStoreはデフォルトのRedisストアからのみ読み書きします。デフォルトのRedisストアはsecondary_store
(古いフォールバックインスタンス)です。これにより、デフォルトの動作を変更することなくMultiStoreを導入することができます。
MultiStoreは2つの機能フラグを使って実際のマイグレーションを制御します:
use_primary_and_secondary_stores_for_[store_name]
use_primary_store_as_default_for_[store_name]
例えば、新しいRedisインスタンスがGitlab::Redis::Foo
という名前の場合、次のように実行することで2つの機能フラグを作成できます:
bin/feature-flag use_primary_and_secondary_stores_for_foo
bin/feature-flag use_primary_store_as_default_for_foo
use_primary_and_secondary_stores_for_foo
機能フラグを有効にすると、Gitlab::Redis::Foo
、MultiStore
新しいRedisインスタンスと古いインスタンス(フォールバックインスタンスMultiStore
)の両方にMultiStore
書き込みを MultiStore
行います。MultiStore
すべての読み取りコマンドは、機能use_primary_store_as_default_for_foo
フラグを use_primary_store_as_default_for_foo
使用して制御されるデフォルトのストアにのみ実行されます。機能フラグをuse_primary_store_as_default_for_foo
有効に use_primary_store_as_default_for_foo
MultiStore
すると、primary_store
(新しいインスタンス) をデフォルトの Redis ストアとして使用します。
pipelined
コマンド(pipelined
およびmulti
)については、両方のストアでオペレーション全体を実行し、結果を比較します。結果が異なる場合は、Gitlab::Redis::MultiStore:PipelinedDiffError
エラーを発生させ、gitlab_redis_multi_store_pipelined_diff_error_total
Prometheus カウンターで追跡します。
新しいストアにデータが格納されるまでの時間が経過したら、外部検証を実行して両方のストアの状態を比較します。検証の結果が満足のいくものであれば、トラフィックを新しいRedisストアに移動しても問題ないでしょう。use_primary_and_secondary_stores_for_foo
機能フラグを無効にすることができます。これにより、MultiStoreはプライマリRedisストア(新しいストア)からのみ読み書きできるようになり、すべてのトラフィックが新しいRedisストアに移動します。
すべてのトラフィックをプライマリストアに移動したら、データのマイグレーションは完了です。MultiStoreの実装を安全に削除し、新しく導入したRedisストアインスタンスを引き続き使用することができます。
実装の詳細
MultiStoreはRedisコマンドの読み込みと書き込みを別々に実装しています。
読み取りコマンド
get
mget
smembers
-
scard
- ‘exists’
- ‘exists?
- ‘get’
- ‘hexists’
- ‘hget’
- ‘hgetall’
- ‘hlen’
- ‘hmget’
- ‘hscan_each’
- ‘mapped_hmget’
- ‘mget’
- ‘scan_each’
- ‘scard’
- ‘sismember’
- ‘smembers’
- ‘sscan’
- ‘sscan_each’
- ‘ttl’
- ‘zscan_each’
コマンドの書き込み
- ‘del’
- ‘eval’
- ‘expire’
- ‘flushdb’
- ‘hdel’
- ‘hset’
- ‘incr’
- ‘incrby’
- ‘mapped_hmset’
- ‘rpush’
- ‘sadd’
- ‘set’
- ‘setex’
- ‘setnx’
- ‘srem’
- ‘unlink’
pipelined
コマンド
注意:これらのコマンドに渡されるRubyブロックは、各ストアごとに1回ずつ、合計2回実行されます。したがって、実行されるRedisのオペレーションを除けば、ブロックはべき等であるべきです。
pipelined
multi
サポートされているリスト以外のコマンドが使用されると、method_missing
、古いRedisインスタンスにそれを渡し、それを追跡します。これにより、予期しないものがあっても以前のように動作するようになります。開発者やテスト環境では、早期発見のためにエラーが発生します。
gitlab_redis_multi_store_method_missing_total
カウンターとGitlab::Redis::MultiStore::MethodMissingError
を追跡することで、開発者はマイグレーションを進める前に、見つからない Redis コマンドの実装を追加する必要があります。エラー
エラー | メッセージ |
---|---|
Gitlab::Redis::MultiStore::PipelinedDiffError |
pipelined コマンドは両ストアで正常に実行されましたが、結果は両ストアで異なっていました。 |
Gitlab::Redis::MultiStore::MethodMissingError | メソッドがありません。Redis セカンダリ・ストアのメソッド実行にフォールバックします。 |
メトリクス
メトリクス名 | 種類 | ラベル | 説明 |
---|---|---|---|
gitlab_redis_multi_store_pipelined_diff_error_total | Prometheus カウンタ |
command ,instance_name
| Redis MultiStorepipelined ストア間の差分コマンド |
gitlab_redis_multi_store_method_missing_total | Prometheus カウンタ |
command ,instance_name
| クライアント側のRedis MultiStoreメソッドの合計がありません。 |
ステップ4: マイグレーション後のクリーンアップ
マイグレーションパスを残すか削除するかは、セルフマネージドインスタンスがマイグレーションを行うことを想定しているかどうかによります。その場合のように、セルフマネージドインスタンスがこの機能パーティションなしで対処することを期待するのであれば、マイグレーションコードのサポートにかかるメンテナンスコストの方が、セルフマネージドインスタンスがシームレスにマイグレーションを実行できるようにするメリットよりも高いと判断することになるかもしれません。
マイグレーションコードを維持すると決めた場合:
- マイグレーション手順を文書化すべきです。
- 機能フラグを使用する場合、opsタイプの機能フラグであることを確認すべきです。
そうでなければ、フラグを削除してプロジェクトを終了します。