MicroAd Developers Blog

マイクロアドのエンジニアブログです。インフラ、開発、分析について発信していきます。

redis-pyでRedis Clusterの全キーを取得する

はじめに

こんにちは、マイクロアドでサーバーサイドエンジニアをしている高橋です。 主にDigdag1とPythonを使用したETL処理のバッチを開発しています。

今回はredis-pyというPythonライブラリを使ってRedis Clusterに登録されている全キーを取得する方法を共有します。

経緯

マイクロアドでは広告配信のデータベースとして速度が求められる一部の処理でRedisを使用しています。 一方で、速度よりも検索性が求められるような場面ではRedisはあまり適しておらず、クエリで検索できるようなデータベースが望ましいです。 マイクロアドではRedis以外のデータベースとしてCDHも利用しており、上記の理由からやむを得ずRedisのデータをCDHに全件転送する処理も行っています。

今まではこのRedisをシャーディング構成で使用していましたが、最近ではRedis Clusterへの移行を進めている状態です。 Redis Clusterへの移行に伴い全件転送の処理もRedis Clusterに対応する必要があったため、redis-pyでの全件取得の方法を調査しました。

RedisやCDHをはじめとするマイクロアドで採用しているデータ基盤やログ蓄積については別の記事で詳しく書かれています。 興味がありましたらぜひご一読ください。

developers.microad.co.jp developers.microad.co.jp

redis-py とは

その名の通りRedisを操作するためのPythonライブラリで、Redis社によって開発が行われています。

redis-pyを採用した理由としては以下の2点が挙げられます。

  • 既存のレプリケーション構成用の処理でもredis-pyを使用していた
  • redis-pyがredis-py-clusterというライブラリを取り込む形で4.1.0から正式にRedis Clusterに対応した2

また、redis-pyの処理を高速化するためにhiredis-py3も導入します。 hiredis-pyはredis-pyと同様にRedis社によって開発が行われているライブラリで、パーサ部分をC言語で実装したhiredisをPythonで使用するためのラッパーです。

Redis関連のライブラリは以下のバージョンを使用しています。

redis==4.3.4
hiredis==2.0.0

Redis Clusterへの接続

接続にはホスト名とポート番号が必要です。

以下ではクラスタ内の各ノードをClusterNodeクラスのインスタンスとして定義し、そのリストを渡してRedisClusterクラスのインスタンスを作成しています。

from redis.cluster import RedisCluster, ClusterNode

nodes = [
    {"host": "redis-cluster-host01", "port": 6379},
    {"host": "redis-cluster-host02", "port": 6379}
]

cluster_nodes = [ClusterNode(**node) for node in nodes]
redis_cluster = RedisCluster(startup_nodes=cluster_nodes, read_from_replicas=True)

RedisClusterクラスを定義する際の引数として、startup_nodes以外にもread_from_replicasなど複数の項目があります。 read_from_replicasはデフォルトでFalseですが、Trueにするとread系のコマンドでマスタノード以外からも取得できるようになります。

問題なく接続できていれば、RedisCluster.get_nodes()で以下のようなノードのインスタンスの情報が取得できます。 startup_nodesでは2ノードを指定しましたが、以下では3ノード分出力されておりクラスタ内の全てのノードが取得できています。

>>> redis_cluster.get_nodes()
[[host=192.0.2.1,port=6379,name=192.0.2.1:6379,server_type=primary,redis_connection=Redis<ConnectionPool<Connection<host=192.0.2.1,port=6379,db=0>>>], [host=192.0.2.2,port=6379,name=192.0.2.2:6379,server_type=primary,redis_connection=Redis<ConnectionPool<Connection<host=192.0.2.2,port=6379,db=0>>>], [host=192.0.2.3,port=6379,name=192.0.2.3:6379,server_type=primary,redis_connection=Redis<ConnectionPool<Connection<host=192.0.2.3,port=6379,db=0>>>]]

Redisのコマンドでノードの取得をする場合はRedisCluster.cluster_nodes()などで取得できます。

全キーの取得

Redis Clusterの全キー取得の方針として以下の3つが挙げられます。

  1. KEYS
  2. SCAN
  3. CLUSTER GETKEYSINSLOT

キーが取得できたら取得したいデータの型に合わせてGETHGETなどを行えば全件の内容が取得できます。

KEYS

KEYSは正規表現のパターンに一致するキーを全て取得するコマンドです。 これを使用した全キー取得のシンプルな方法としてKEYS *が考えられますが、公式ドキュメントで推奨されていないコマンドでもありレコードが多い場合には危険です。

Don't use KEYS in your regular application code.

KEYSの危険性についてはこちらの記事がわかりやすかったです。

qiita.com

SCAN

カーソルによって分割して参照していくSCANを使う方法もあります。 ただし、以下の通りredis-cliとredis-pyで挙動が異なっているので注意が必要です。

SCANに該当する操作 挙動
redis-cli SCAN 特定のノードのキーの取得
redis-py RedisCluster.scan()
RedisCluster.scan_iter()
クラスタ内の全ノードのキーの取得

どちらもキーを分割して取得できますが、redis-cliではクラスタ内の全ノードに対して個別にSCANを行わなければ全キーの取得ができません。

CLUSTER GETKEYSINSLOT

今回の記事で使用するのはクラスタのスロット内のキーを取得するCLUSTER GETKEYSINSLOTです。 スロット・取得件数を引数として実行するコマンドで、指定した件数のキーが取得できます。 これとスロットのキー数を取得するCLUSTER COUNTKEYSINSLOTを組み合わせることでスロット内の全キー取得を実現します。 redis-pyだとRedisCluster.cluster_get_keys_in_slot()RedisCluster.cluster_countkeysinslot()がそれぞれのコマンドに対応しています。 デフォルトの設定ではRedis Clusterは16384スロット存在しているので、スロット数分繰り返せば全スロットの全キーの取得が可能です。

for slot in range(16384):
    num_keys = redis_cluster.cluster_countkeysinslot(slot)
    keys = redis_cluster.cluster_get_keys_in_slot(slot, num_keys)

特定のスロットで取得した結果が以下になります。

>>> slot = 15033
>>> num_keys = redis_cluster.cluster_countkeysinslot(slot)
>>> num_keys
1
>>> keys = redis_cluster.cluster_get_keys_in_slot(slot, num_keys)
>>> keys
['key_name']

備考

RedisCluster.scan_iter()RedisCluster.cluster_get_keys_in_slot()の違いは多数ありますが、そのうち一点を紹介します。 RedisCluster.scan_iter()はbytes型、RedisCluster.cluster_get_keys_in_slot()はstr型でキーを取得するという点です。

>>> type(next(redis_cluster.scan_iter(match='*')))
<class 'bytes'>
>>> type(redis_cluster.cluster_get_keys_in_slot(slot, num_keys)[0])
<class 'str'>

おわりに

redis-pyでのRedis Clusterに登録されている全キーの取得について紹介しました。 全キーの取得に関する記事はいくつかありましたが、クラスタ構成のものはなさそうだったので参考になれば幸いです。 接続時のオプションなどまだまだ改善すべき部分は多そうですが、機会があれば記事を書きます。


  1. Digdagとはマイクロアドで採用しているワークフローエンジンです。
    詳しくは過去記事を参照ください。https://developers.microad.co.jp/archive/category/Digdag
  2. https://github.com/redis/redis-py/releases/tag/v4.1.0rc1
  3. https://github.com/redis/hiredis-py