Redis開発ガイドライン

Redisインスタンス

GitLabはRedisを以下のような明確な目的で使用します:

  • キャッシュ (主にRails.cache 経由)。
  • Sidekiqによるジョブ処理キューとして。
  • 共有アプリケーションの状態を管理します。
  • CI トレースチャンクの保存。
  • ActionCableのPub/Subキューバックエンドとして。
  • 状態保存のレート制限。
  • セッション

ほとんどの環境(GDKを含む)では、これらはすべて同じRedisインスタンスを指しています。

GitLab.comでは、別々のRedisインスタンスを使っています。設定の詳細についてはRedis SRE ガイドを参照ください。

どのアプリケーションプロセスも同じRedisサーバーを使うように設定されているので、PostgreSQLが適切でない場合にプロセス間通信に使うことができます。例えば、一時的な状態や、読み込むよりも書き込む頻度が高いデータなどです。

Geoが有効になっている場合、各 Geo ノードはそれぞれ独立した Redis データベースを取得します。

新しい Redis インスタンスの追加に関する開発者向けドキュメントがあります。

キーの命名

Redisは階層のないフラットな名前空間なので、衝突を避けるためにキーの名前に注意を払う必要があります。一般的には、コロンで区切られた要素を使用して、アプリケーション・レベルでの構造の体裁を整えます。例えば、projects:1:somekey

GitLab.com のような可用性の高い設定では、Redis の利用を目的別に分けて別々の Redis サーバーにマッピングしますが、デフォルトの Omnibus と GDK ではひとつの Redis サーバーを共有します。つまり、キーはすべてのカテゴリで常にグローバルに一意でなければなりません。

通常、Redisのキー名には不変の識別子、例えばフルパスではなくプロジェクトIDを使う方がよいでしょう。フルパスを使用すると、プロジェクトの名前が変更されたときにキーが参照されなくなります。キーの内容が名前の変更によって無効になる場合は、キーの変更に依存するのではなく、エントリを期限切れにするフックを含めるほうがよいでしょう。

マルチキーコマンド

GitLabはエピック823で導入されたRedisレート制限タイプでのみRedisクラスターをサポートします。

GitLabが複数のキーを同じRedisサーバーに保持させるオペレーションを行う場合、例えばRedisに保持されている2つのセットを差分する場合、キーは変更可能な部分を中かっこで囲む必要があります。例えば

project:{1}:set_a
project:{1}:set_b
project:{2}:set_c

set_aset_b は同じ Redis サーバーにあることが保証されますが、set_c は保証されません。

現在、cacheshared_state Redisインスタンスで有効になっているRedisClusterValidator、開発者とテスト環境で検証しています。

開発者は、より多くのRedisタイプで将来Redisクラスターを採用しやすくするために、必要に応じてハッシュタグを使用することを強く推奨します。たとえば、Namespaceモデルは設定キャッシュキーにハッシュタグを使用しています。

構造化ロギングにおけるRedis

GitLabチームメンバーの方へ:基本的な GitLab.comのRedis構造化ロギング・フィールドの使い方を紹介する上級者向けのビデオがあります。

ウェブリクエストと Sidekiq ジョブの構造化ロギングには、Redis インスタンスごとの継続時間、コールカウント、書き込みバイト数、読み込みバイト数のフィールドと、すべての Redis インスタンスの合計が含まれます。特定のリクエストでは、次のようになります:

項目
json.queue_duration_s0.01
json.redis_cache_calls1
json.redis_cache_duration_s0
json.redis_cache_read_bytes109
json.redis_cache_write_bytes49
json.redis_calls2
json.redis_duration_s0.001
json.redis_read_bytes111
json.redis_shared_state_calls1
json.redis_shared_state_duration_s0
json.redis_shared_state_read_bytes2
json.redis_shared_state_write_bytes206
json.redis_write_bytes255

これらのフィールドはすべてインデックス化されているため、本番環境でのRedisの使用状況を調査するのは簡単です。たとえば、キャッシュから最も多くのデータを読み込んだリクエストを見つけるには、redis_cache_read_bytes で降順に並べ替えます。

遅いログ

note
GitLab.comにスローログ(GitLab内部)の見方のビデオがあります。

GitLab.com では、Redis のスローログのエントリがredis.slowlog タグpubsub-redis-inf-gprd* インデックスに表示されます。これは、長い時間がかかり、パフォーマンスが懸念されるコマンドを示しています。

fluent-plugin-redis-slowlog プロジェクトは、Redis からslowlog エントリを取り出し、Fluentd(そして最終的には Elasticsearch)に渡す役割を担っています。

鍵空間全体の分析

Redis Keyspace Analyzerプロジェクトには、Redisインスタンスの完全なキーリストとメモリ使用量をダンプし、結果から潜在的にセンシティブなデータを除外しながらそれらのリストを分析するためのツールが含まれています。最も頻繁に使用されるキーパターンや、最もメモリを使用するキーパターンを見つけるために使用できます。

現在のところ、これはGitLab.comのRedisインスタンスでは自動的には実行されず、必要に応じて手動で実行されます。

N+1コールの問題

spec/support/helpers/redis_commands/recorder.rb 経由で内部導入。f696f670

RedisCommands::Recorder はテストから Redis の N+1 コールの問題を検出するツールです。

Redis はしばしばキャッシュ目的で使用されます。通常、キャッシュの呼び出しは軽量で、Redis インスタンスに影響を与えるほどの負荷は発生しません。しかし、そうとは知らずに高価なキャッシュの再計算をトリガーしてしまう可能性はあります。このツールを使ってRedisコールを分析し、予想される制限を定義します。

テストの作成

ActiveSupport::Notifications instrumenterとして実装されています。

テスト可能なコードが Redis を 1 回だけ呼び出すことを検証するテストを作成できます:

it 'avoids N+1 Redis calls' do
  control = RedisCommands::Recorder.new { visit_page }

  expect(control.count).to eq(1)
end

または、特定の Redis 呼び出しの回数を検証するテストを作成します:

it 'avoids N+1 sadd Redis calls' do
  control = RedisCommands::Recorder.new { visit_page }

  expect(control.by_command(:sadd).count).to eq(1)
end

特定の Redis 呼び出しのみをキャプチャするパターンを指定することもできます:

it 'avoids N+1 Redis calls to forks_count key' do
  control = RedisCommands::Recorder.new(pattern: 'forks_count') { visit_page }

  expect(control.count).to eq(1)
end

また、特別なマッチャーexceed_redis_calls_limitexceed_redis_command_calls_limit を使って、Redis 呼び出し回数の上限を定義することもできます:

it 'avoids N+1 Redis calls' do
  control = RedisCommands::Recorder.new { visit_page }

  expect(control).not_to exceed_redis_calls_limit(1)
end
it 'avoids N+1 sadd Redis calls' do
  control = RedisCommands::Recorder.new { visit_page }

  expect(control).not_to exceed_redis_command_calls_limit(:sadd, 1)
end

これらのテストは、Redis呼び出しに関連するN+1の問題を特定し、その修正が期待通りに動作することを確認するのに役立ちます。

こちらもご覧ください

ユーティリティクラス

特定のユースケースを支援するために、追加のクラスをいくつか用意しています。これらは主にRedisの使い方を細かく制御するためのものなので、Rails.cache ラッパーと組み合わせて Rails.cache使うことはありませんRails.cacheRails.cache

私たちはRails.cache 、将来Railsに施される最適化の恩恵を享受できるようにしたいと考えています。RubyオブジェクトはRedisに書き込まれるときにマーシャルされるので、巨大なオブジェクトや信頼できないユーザーの入力を保存しないように注意しなければなりません。

通常、これらのクラスは次のうち少なくとも1つが当てはまる場合にのみ使用します:

  1. 非キャッシュ Redis インスタンスでデータを操作したい場合。
  2. Rails.cache は実行したいオペレーションをサポートしていません。

Gitlab::Redis::{Cache,SharedState,Queues}

これらのクラスはRedisのインスタンスを(Gitlab::Redis::Wrapper を使って)ラップし、直接操作できるようにします。典型的な使い方は、.with を呼び出すことです。 は Redis 接続を返すブロックを受け取ります。例えば

# Get the value of `key` from the shared state (persistent) Redis
Gitlab::Redis::SharedState.with { |redis| redis.get(key) }

# Check if `value` is a member of the set `key`
Gitlab::Redis::Cache.with { |redis| redis.sismember(key, value) }

Gitlab::Redis::Boolean

Redis では、すべての値は文字列です。Gitlab::Redis::Boolean は、ブーリアンが一貫してエンコードおよびデコードされるようにします。

Gitlab::Redis::HLL

RedisのPFCOUNTPFADDPFMERGE コマンドはHyperLogLogsでオペレーションします。HyperLogLogsは、少ないメモリ使用量でユニークな要素の数を推定できるデータ構造です。詳細については、RedisのHyperLogLogsを参照してください。

Gitlab::Redis::HLL はHyperLogLogsに値を追加したりカウントしたりするための便利なインターフェースを提供します。

Gitlab::SetCache

アイテムがアイテムのグループ内にあるかどうかを効率的にチェックする必要がある場合、Redisセットを使用することができます。Gitlab::SetCache は、SISMEMBER コマンドを使用する#include? メソッドと、セット内のすべてのエントリをフェッチする#read を提供しています。

これはRepositorySetCache 、ブランチ名などのリポジトリデータをキャッシュするためにセットを使用する便利な方法を提供するために使用されます。

バックグラウンドマイグレーション

Redisベースのマイグレーションでは、SCAN コマンドを使用して、特定のキーパターンについてRedisインスタンス全体をスキャンします。大規模なRedisインスタンスの場合、マイグレーションは通常のマイグレーションやデプロイ後のマイグレーションの制限時間を超える可能性があります。RedisMigrationWorker 、バックグラウンドマイグレーションとして長時間実行されるRedisマイグレーションを実行します。

クラスを作成してバックグラウンドマイグレーションを実行するには、次のようにします:

module Gitlab
  module BackgroundMigration
    module Redis
      class BackfillCertainKey
        def perform(keys)
        # implement logic to clean up or backfill keys
        end

        def scan_match_pattern
        # define the match pattern for the `SCAN` command
        end

        def redis
        # define the exact Redis instance
        end
      end
    end
  end
end

デプロイ後のマイグレーションによってワーカーを起動する場合:

class ExampleBackfill < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!

  MIGRATION='BackfillCertainKey'

  def up
    queue_redis_migration_job(MIGRATION)
  end
end