PLAY DEVELOPERS BLOG

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

Docker を使った Redis Cluster 開発環境を構築し Rails などのアプリから接続する方法

こんにちは、OTTサービス技術部 開発第2グループの榊です。 今回は、当社でRuby on RailsでのRedisクライアントのクラスターモード対応を行った際の事例についてお話しします。

私が担当しているプロジェクトでは、これまでAmazon ElastiCache with Redis OSS compatibility(以下、ElastiCache (Redis OSS) と記載します)をクラスターモード無効の状態で運用しており、この設定ではシャード1のリードレプリカが最大5つまでと制限があります。徐々にデータ量が増加してきて今回ノード数の増強が必要になったため、クラスターモードを有効にすることにしました。

ElastiCache (Redis OSS)のクラスターモード有効と無効の違いについて

ElastiCache (Redis OSS)の基本的な用語とクラスターモード有効と無効で違う部分について記載しております。

用語 説明
Redisクラスター 単一または複数のシャードで構成されるグループ
シャード プライマリノード+ レプリカノードで構成されるノードグループ
プライマリノード 読み取り/書き込み両方可能なノード
レプリカノード 読み取りのみ可能なノード

クラスターモード無効

シャード数は1で固定となり、シャードもレプリカノードが最大5となるため、クラスタモード有効と比べてスケーラビリティの制限がある。 Redisクラスターにアクセスするためのエンドポイントとして、書き込み用途のプライマリエンドポイントと、読み取り用途のリーダーエンドポイントの2種類が提供される。

クラスターモード有効

マルチシャード構成が可能でデータを複数のシャードに分割して保存する。 データの自動分散により、データ容量やリード・ライト性能の面で大幅にスケーラビリティが向上する。 注意点としてはdbが0でデータが入る前提となっている。

詳しい説明についてはAmazon Web Services ブログを参照ください。

Ruby on Railsの対応

使用パッケージ

Redisクライアントとしてredis.rb(v4.8.1)を使用してRedisへの接続を行っております。詳しくはドキュメントを参照ください。

github.com

クラスターモード無効の場合

ElastiCache (Redis OSS)に接続する際、書き込み用と読み込み用でエンドポイントが分かれているため、用途に応じてそれぞれのエンドポイントを使い分ける必要があります。基本的には、ドキュメント通りの設定で問題ありません。以下にサンプルコードを示します。

docker-compose.yml

version: '3.8'

services:
  node-1:
    image: redis:6.2
    ports:
      - "7001:6379"

Redisクライアントの使用方法

r = Redis.new(host: "127.0.0.1", port: 7001, db: 1)
=> #<Redis client v4.8.1 for redis://127.0.0.1:7001/0>

クラスターモード有効の場合

ElastiCache (Redis OSS)に接続する際は、設定したエンドポイントを指定するだけで基本的に接続可能です。ただし、利用目的に応じてオプション(例: replicaなど)を設定する必要があります。

以下は開発環境構築時のコード例です。Docker上に構築したRedisクラスターに接続する際に少し工夫が必要でした。 PC上のRedisクライアントからDocker上のRedisクラスターへポートフォワードを使用してアクセスを試みましたが、クラスター情報の取得がうまくいかずエラーとなりました。そこで、RailsアプリをRedisクラスターと同一のネットワーク内に配置し、接続確認を行いました。以下にサンプルコードを示します。

docker-compose.yml

version: '3.8'

services:
  node-1:
    image: redis:6.2
    command: ["redis-server", "--port", "6379", "--cluster-enabled", "yes", "--cluster-config-file", "nodes.conf", "--cluster-node-timeout", "5000", "--appendonly", "yes"]
    ports:
      - "7001:6379"
    networks:
      - redis-cluster-network

  node-2:
    image: redis:6.2
    command: ["redis-server", "--port", "6379", "--cluster-enabled", "yes", "--cluster-config-file", "nodes.conf", "--cluster-node-timeout", "5000", "--appendonly", "yes"]
    ports:
      - "7002:6379"
    networks:
      - redis-cluster-network

  node-3:
    image: redis:6.2
    command: ["redis-server", "--port", "6379", "--cluster-enabled", "yes", "--cluster-config-file", "nodes.conf", "--cluster-node-timeout", "5000", "--appendonly", "yes"]
    ports:
      - "7003:6379"
    networks:
      - redis-cluster-network

  node-4:
    image: redis:6.2
    command: ["redis-server", "--port", "6379", "--cluster-enabled", "yes", "--cluster-config-file", "nodes.conf", "--cluster-node-timeout", "5000", "--appendonly", "yes"]
    ports:
      - "7004:6379"
    networks:
      - redis-cluster-network

  node-5:
    image: redis:6.2
    command: ["redis-server", "--port", "6379", "--cluster-enabled", "yes", "--cluster-config-file", "nodes.conf", "--cluster-node-timeout", "5000", "--appendonly", "yes"]
    ports:
      - "7005:6379"
    networks:
      - redis-cluster-network

  node-6:
    image: redis:6.2
    command: ["redis-server", "--port", "6379", "--cluster-enabled", "yes", "--cluster-config-file", "nodes.conf", "--cluster-node-timeout", "5000", "--appendonly", "yes"]
    ports:
      - "7006:6379"
    networks:
      - redis-cluster-network

  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/app
    ports:
      - 3000:3000
    depends_on:
      - db
    tty: true
    stdin_open: true
    networks:
      - redis-cluster-network

  db:
    image: postgres:12
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: redis_cluster_development

networks:
  redis-cluster-network:
volumes:
  postgres_data:

docker-compose.ymlファイルではRedisの立ち上げのみを行っています。次に、redis-cluster-networkネットワーク上のコンテナIPを取得し、webコンテナのIPを除外してRedisノードのリストを構築します。

①ネットワーク上のRedisノードIP取得

REDIS_NETWORKに指定したネットワーク名から、RedisノードのIPアドレスを取得します。取得したIPに:6379ポートを追加し、Redisクラスターの作成に使用します。

$ REDIS_NETWORK=redis_cluster_redis-cluster-network
$ docker network inspect ${REDIS_NETWORK} | jq -r '.[0].Containers | .[].IPv4Address' | sed -e 's/\/.*/:6379/' | tr '\n' ' '

# 出力例
# 192.168.32.2:6379 192.168.32.4:6379 192.168.32.7:6379 192.168.32.8:6379 192.168.32.5:6379 192.168.32.3:6379 192.168.32.6:6379
②Redisクラスターの作成

取得したIPリストを使って、Redisクラスターを作成します。redis-cli --cluster createコマンドでクラスターを構成し、各ノードにレプリカ数を設定します。

$ REDIS_SERVICE_NAME=node-1
$ CLUSTER_REPLICAS=1
$ NODES="192.168.32.2:6379 192.168.32.4:6379 192.168.32.7:6379 192.168.32.5:6379 192.168.32.3:6379 192.168.32.6:6379"
$ docker-compose exec ${REDIS_SERVICE_NAME} bash -c "yes yes | redis-cli --cluster create ${NODES} --cluster-replicas ${CLUSTER_REPLICAS}"
③Redisクラスターの情報を確認

redis-cliのオプション-cを使用して接続し、cluster nodesでクラスターの情報を確認します。 masterが3個、各masterに1個のslaveがあることが確認出来ました。

❯ redis-cli -p 7001 -c
127.0.0.1:7001> cluster nodes
9addcba1379514fac09fa32a040d33e8c30057e4 192.168.32.2:6379@16379 master - 0 1730642472091 1 connected 0-5460
2ec30498bb095f540c3bffbc02b291ac1462cb7b 192.168.32.6:6379@16379 slave 227750c873121b1cf7eeb51b9d13b328d56c9f5b 0 1730642470575 2 connected
c499e97ae99b9cc3b611d3bcb618cbd03ae733b6 192.168.32.5:6379@16379 slave e5bc0a0ed23ac38c684bbbea8dabbc4b4e04427c 0 1730642472000 3 connected
e5bc0a0ed23ac38c684bbbea8dabbc4b4e04427c 192.168.32.7:6379@16379 master - 0 1730642472091 3 connected 10923-16383
227750c873121b1cf7eeb51b9d13b328d56c9f5b 192.168.32.4:6379@16379 master - 0 1730642472091 2 connected 5461-10922
b2aeeb53be865192e56c3a0da1c7b7fc6c097f79 192.168.32.3:6379@16379 myself,slave 9addcba1379514fac09fa32a040d33e8c30057e4 0 1730642470000 1 connected
④RailsのRedisクライアントからの接続方法

Redisクライアントのクラスターモードへの接続方法ですが、バージョンv4.1.1から対応しており、バージョンによって取得方法が異なります。 今回はv4.1.1〜v4.8.1で使用したケースを記載しており、v5.0.0以降はgemのredis-clusteringを使用する方法に変わってますので、v5.0.0以降を使用する場合はこちらを参考にお願いします。

masterノードのみに接続したい場合

# 一部のノードを指定するだけでCLUSTER NODESをコマンドで不足しているノードも検出してくれる
r = Redis.new(cluster: %w[redis://192.168.32.2:6379])
=> #<Redis client v4.8.1 for redis://192.168.32.2:6379/0 redis://192.168.32.4:6379/0 redis://192.168.32.7:6379/0>

読み込みのときはreplica、書き込みのときはmasterを参照する場合

r = Redis.new(cluster: %w[redis://192.168.32.2:6379], replica: true)
=> #<Redis client v4.8.1 for redis://192.168.32.2:6379/0 redis://192.168.32.3:6379/0 redis://192.168.32.4:6379/0 redis://192.168.32.5:6379/0 redis://192.168.32.6:6379/0 redis://192.168.32.7:6379/0>

クラスターモード有効化時の注意点

  • スロットを跨いだアクセスが出来なくなります。mget、pipelineなどを使用している場合は一件ずつ取得するように修正が必要となります。
  • Redisのdb: 0以外を使用している場合はdb: 0へデータを移行する必要があります。

最後に

今回は、Ruby on RailsでのRedisクライアントのクラスターモード対応について解説しました。クラスターモードに対応したdocker-compose.ymlを作成したものの、Redisクラスターと同じネットワークに入れる必要があり、手間がかかる構成になってしまいました。次回は、これを改善する方法についても検討したいと思います。