ラベル redis の投稿を表示しています。 すべての投稿を表示
ラベル redis の投稿を表示しています。 すべての投稿を表示

2026年2月9日月曜日

外部の Redis にアクセスする場合はちゃんと pipeline を使おう

外部の Redis にアクセスする場合はちゃんと pipeline を使おう

概要

ループごとに GET するような処理はネットワークの負荷が重くなり結果としてアプリのレスポンス遅延にもつながるので可能な限り1回でほしい結果はすべて取得するようにしましょう

環境

  • Python 3.12.11
  • redis-py 7.1.0
  • redis 8.4.0

ダメなコード

def get_index_pairs(redis_client, hand_images: dict, ai_images: dict) -> list:
    profiles = get_profiles()
    pairs = []
    for num in sorted(
        set(hand_images.keys()) | set(ai_images.keys()),
        key=lambda x: int(x),
        reverse=True,
    ):
        hand_like_key = f"{num}_hand"
        ai_like_key = f"{num}_ai"
        hand_like = int(redis_client.get(hand_like_key) or 0)  # type: ignore
        ai_like = int(redis_client.get(ai_like_key) or 0)  # type: ignore
        pair = {
            "num": num,
            "hand": hand_images.get(num),
            "ai": ai_images.get(num),
            "hand_like": hand_like,
            "ai_like": ai_like,
            "name_hand": profiles.get(hand_like_key, {}).get("name", "名無し"),
            "character_hand": profiles.get(hand_like_key, {}).get(
                "character", "考え中"
            ),
            "name_ai": profiles.get(ai_like_key, {}).get("name", "名無し"),
            "character_ai": profiles.get(ai_like_key, {}).get("character", "考え中"),
            "winner": (
                "hand" if hand_like > ai_like else "ai" if ai_like > hand_like else None
            ),
        }
        pairs.append(pair)
    return pairs

良いコード

def get_index_pairs(redis_client, hand_images: dict, ai_images: dict) -> list:
    profiles = get_profiles()
    sorted_nums = sorted(
        set(hand_images.keys()) | set(ai_images.keys()),
        key=lambda x: int(x),
        reverse=True,
    )

    # --- Pipeline の開始 ---
    pipe = redis_client.pipeline()
    for num in sorted_nums:
        pipe.get(f"{num}_hand")
        pipe.get(f"{num}_ai")

    # 1回の通信で全データを取得
    raw_results = pipe.execute()
    # --- Pipeline の終了 ---

    pairs = []
    # 取得した結果を 2 つずつ取り出す
    for i, num in enumerate(sorted_nums):
        hand_like_bytes = raw_results[i * 2]
        ai_like_bytes = raw_results[i * 2 + 1]

        # decode_responses=False の場合を考慮
        hand_like = int(hand_like_bytes.decode() if hand_like_bytes else 0)
        ai_like = int(ai_like_bytes.decode() if ai_like_bytes else 0)

        pair = {
            "num": num,
            "hand": hand_images.get(num),
            "ai": ai_images.get(num),
            "hand_like": hand_like,
            "ai_like": ai_like,
            "name_hand": profiles.get(f"{num}_hand", {}).get("name", "名無し"),
            "character_hand": profiles.get(f"{num}_hand", {}).get(
                "character", "考え中"
            ),
            "name_ai": profiles.get(f"{num}_ai", {}).get("name", "名無し"),
            "character_ai": profiles.get(f"{num}_ai", {}).get("character", "考え中"),
            "winner": (
                "hand" if hand_like > ai_like else "ai" if ai_like > hand_like else None
            ),
        }
        pairs.append(pair)
    return pairs

比較

ネットワーク・レイテンシの削減

これが最も大きな改善点です。

ダメなコード(逐次処理): ループの中で毎回 redis_client.get() を呼び出しています。もし num が100個あれば、200回の通信(リクエストとレスポンスの往復)が発生します。

良いコード(パイプライン): すべての get コマンドを一旦溜めてから、pipe.execute() で 1回だけ Redisサーバーに送ります。通信回数が1回(往復1回)で済むため、ネットワークの遅延(レイテンシ)の影響を最小限に抑えられます。

スループット(処理能力)の向上

Redisサーバー側の負荷も軽減されます。

ダメなコード: サーバーは「リクエスト受信 → 解析 → 実行 → レスポンス送信」というサイクルを200回繰り返します。

良いコード: サーバーは「大きなリクエストを1つ受信 → まとめて実行 → 大きなレスポンスを1つ送信」という挙動になります。これにより、コンテキストスイッチやパケット処理のオーバーヘッドが減り、全体のスループットが向上します。

型の安全性と堅牢な実装

細かい部分ですが、データの扱いも丁寧になっています。

デコード処理: hand_like_bytes.decode() if hand_like_bytes else 0 のように、Redisから返ってくる値が None(キーが存在しない)場合や、バイト列(bytes型)である可能性を考慮して明示的にデコードしています。

マジックナンバーの排除: i * 2 と i * 2 + 1 を使ってパイプラインの結果から正しくデータを取り出しており、インデックス管理が明確です。

最後に

開発初期ではデータが少なく問題にならないですがデータが増えてくるとアプリの挙動がおかしくなるので注意しましょう

gunicorn などを使っていると Redis の応答よりもワーカーが先に死亡し以下のようなエラーになります

Traceback (most recent call last):
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/gunicorn/workers/sync.py", line 142, in handle
    self.handle_request(listener, req, client, addr)
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/gunicorn/workers/sync.py", line 185, in handle_request
    respiter = self.wsgi(environ, resp.start_response)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/flask/app.py", line 1536, in __call__
    return self.wsgi_app(environ, start_response)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/werkzeug/middleware/proxy_fix.py", line 183, in __call__
    return self.app(environ, start_response)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
    response = self.full_dispatch_request()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
    rv = self.dispatch_request()
         ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)  # type: ignore[no-any-return]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/data/repo/ai-gallery/app.py", line 162, in index
    pairs = get_pairs()
            ^^^^^^^^^^^
  File "/Users/username/data/repo/ai-gallery/app.py", line 143, in get_pairs
    pairs = get_index_pairs(redis_client, hand_images, ai_images)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/data/repo/ai-gallery/lib/pair.py", line 45, in get_index_pairs
    ai_like = int(redis_client.get(ai_like_key) or 0)  # type: ignore
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/commands/core.py", line 1923, in get
    return self.execute_command("GET", name, keys=[name])
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/client.py", line 657, in execute_command
    return self._execute_command(*args, **options)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/client.py", line 668, in _execute_command
    return conn.retry.call_with_retry(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/retry.py", line 116, in call_with_retry
    return do()
           ^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/client.py", line 669, in <lambda>
    lambda: self._send_command_parse_response(
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/client.py", line 640, in _send_command_parse_response
    return self.parse_response(conn, command_name, **options)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/client.py", line 691, in parse_response
    response = connection.read_response()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/connection.py", line 1133, in read_response
    response = self._parser.read_response(disable_decoding=disable_decoding)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/_parsers/resp2.py", line 15, in read_response
    result = self._read_response(disable_decoding=disable_decoding)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/_parsers/resp2.py", line 25, in _read_response
    raw = self._buffer.readline()
          ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/_parsers/socket.py", line 115, in readline
    self._read_from_socket()
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/_parsers/socket.py", line 65, in _read_from_socket
    data = self._sock.recv(socket_read_size)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/gunicorn/workers/base.py", line 198, in handle_abort
    sys.exit(1)
SystemExit: 1

2025年8月5日火曜日

go-redis を v9 にアップグレードする方法

go-redis を v9 にアップグレードする方法

概要

v9 からインストール時のパスが変わっているので go get -u などしてもアップグレードされません
直接 go.mod ファイルなどを編集することでアップグレードできます

環境

  • macOS 15.5
  • golang 1.24.5
  • go-redis 9.11

go-redis v9 のインストール

  • go get github.com/redis/go-redis/v9

go-redis v6 のアンインストール

  • vim go.mod
require (
	github.com/beego/beego/v2 v2.3.8
	github.com/stretchr/testify v1.10.0
	golang.org/x/crypto v0.40.0
)

もともとあった github.com/go-redis/redis v6.15.9+incompatible の行を削除します

コード内の import 文の修正

"github.com/go-redis/redis"

の箇所を

"github.com/redis/go-redis/v9"

に書き換えます

コードの修正

以下主な修正点を紹介します
基本的には各メソッドで何かしらのコンテキストを受け取る形式になっているのでそこを修正します

  • context の import
import (
	"context"
	"github.com/redis/go-redis/v9"
)
  • インタフェースの修正
type RedisClient interface {
	LRange(ctx context.Context, key string, start, stop int64) *redis.StringSliceCmd
	LPush(ctx context.Context, key string, values ...interface{}) *redis.IntCmd
	Del(ctx context.Context, keys ...string) *redis.IntCmd
	Get(ctx context.Context, key string) *redis.StringCmd
	Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd
	Close() error
}
  • 各メソッドの修正
ctx := mc.Ctx.Request.Context()
ret, err := cli.Del(ctx, key).Result()
  • テスト用のモックの修正
func (m *MockRedisClient) Del(ctx context.Context, keys ...string) *redis.IntCmd {
	for _, key := range keys {
		delete(m.data, key)
	}
	return redis.NewIntResult(1, nil)
}

動作確認

あとはテストなどを実行すれば OK です

  • go fmt ./...
  • go test ./...
  • go build

最後に

golang の場合非互換のバージョンアップがある場合は新規にパスを切るケースがほとんどなどでそれを知らずに放置しておくと最新バージョンからかなり遅れることになってしまいます
パスを新しくしたらあとは go get -u && go mod tidy でバージョンアップすることができます

参考サイト

2023年3月27日月曜日

redis-server で Warning: Could not create server TCP listening socket #:6379: nodename nor servname provided, or not known

redis-server で Warning: Could not create server TCP listening socket #:6379: nodename nor servname provided, or not known

概要

Mac 上の Redis で bind するアドレスを 127.0.0.1 以外に変更しようとした際に発生したので対処方法を紹介します

環境

  • macOS 11.7.4
  • redis-server 7.0.9

解決方法: bind に必ず 127.0.0.1 を付与する

  • vim /usr/local/etc/redis.conf
bind 0.0.0.0 127.0.0.1

おまけ: プロテクションモードを無効にする

  • CONFIG SET protected-mode no

これをしないと celery.flower で DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. が発生します

最後に

redis のバージョンを 7 に上げたら発生したので最新バージョンの仕様なのかもしれません

2021年12月24日金曜日

Ruby で redis に画像ファイルを保存する方法

Ruby で redis に画像ファイルを保存する方法

概要

Redis はバイナリセーフなので画像デーも保存できます
今回は Ruby から保存/取得する方法を紹介します

環境

  • macOS 11.6.2
  • Ruby 3.0.3
  • redis 4.5.1

画像データの保存

test.png という画像ファイルを Redis に保存します

  • vim set.rb
require 'redis'

path = "test.png"
File.open(path, 'rb') do |file|
  redis = Redis.new
  redis.set('image', file.read)
end

画像データの取得

Redis に保存されているデータを元に画像データを生成します

  • vim get.rb
require 'redis'

path = "test.png"
File.open(path, 'wb') do |file|
  redis = Redis.new
  image = redis.get('image')
  file.write(image)
end

最後に

File.open の mode は rb と wb を使うのがポイントかなと思います

2021年7月26日月曜日

RedisGraph を使ってみた

RedisGraph を使ってみた

概要

RedisGraph は Redis 上で GQL を使うことができる redis の拡張モジュールです 今回はインストール方法から簡単 GQL の発行まで行ってみました

環境

  • Redis with RedisGraph 6.2.3
  • RedisInsight 1.10.1

RedisGraph モジュールが有効な redis を起動する

docker イメージがあるのでそれを使うと簡単に立ち上げることができます すでに redis が立ち上がっている場合は一旦停止してから redisgraph を起動してください

  • docker run -p 6379:6379 -d redislabs/redisgraph

データの登録

ではデータを登録してみましょう Graph データを登録するには redis-cli で GRAPH.QUERY を使います

127.0.0.1:6379> GRAPH.QUERY MotoGP "CREATE (:Rider {name:'Valentino Rossi'})-[:rides]->(:Team {name:'Yamaha'}), (:Rider {name:'Dani Pedrosa'})-[:rides]->(:Team {name:'Honda'}), (:Rider {name:'Andrea Dovizioso'})-[:rides]->(:Team {name:'Ducati'})"
1) 1) "Labels added: 2"
   2) "Nodes created: 6"
   3) "Properties set: 6"
   4) "Relationships created: 3"
   5) "Cached execution: 0"
   6) "Query internal execution time: 4.416402 milliseconds"

MotoGP というキーに graphdata を登録します 少し複雑に見えますが一つずつ見ていくと単純な構文になっています

データを登録する場合は CREATE を使います SQL で言うところの INSERT になります デーは括弧で囲いリレーションはハイフンを使います

上記のデータは

  • Valentino Rossi はチーム Yamaha に所属
  • Dani Pedrosa はチーム Honda に所属
  • Andrea Dovizioso はチーム Ducati に所属

という 3 つのデータを登録しています グラフデータはある属性がある属性に紐付いているというデータを登録することで可視化や検索を行うことができます

データベースの一覧を取得する

今回は MotoGP というデータベースに登録しています RedisGraph ではデータベースは単純な key として登録されています なのでデータベースの一覧を取得する場合は keys で OK です

127.0.0.1:6379> keys *
1) "MotoGP"

データの検索

例えばライダーの名前だけを取得してみます SQL で言うところの SELECT 文は GQL では MATCH という構文になります

127.0.0.1:6379> GRAPH.QUERY MotoGP "MATCH (r:Rider) RETURN r.name"
1) 1) "r.name"
2) 1) 1) "Valentino Rossi"
   2) 1) "Dani Pedrosa"
   3) 1) "Andrea Dovizioso"
3) 1) "Cached execution: 0"
   2) "Query internal execution time: 0.189014 milliseconds"

(r:Rider) でノードを検索できます 先頭の小文字の r はエイリアスでその後に続くクエリで r で検索したノードを参照できます 今回はすべてのノードを取得してその中の properties の name だけを表示しています

おまけ: RedisInsight で動作確認

RedisInsight を使えばグラフデータを可視化することができます

すでに RedisInsight が構築されているのであれば左メニューにある「RedisGraph」をクリックします そしてデータベースを選択して GQL を入力すれば OK です

ライダーのノード一覧が表示されたらノードをダブルクリックしてみましょう すると紐付いているチームの情報がグラフ上に展開され確認することができます もし紐付きが複数ある場合はいろいろなノードがリアルタイムで展開されていくのでノードの関連性が簡単に確認することができます

最後に

ある属性とある属性が紐付き関係性がグラフデータのような構造のデータがある場合に RedisGraph に入れて可視化すると簡単に関係性を確認することができるようになります

テーブルデータは SQL、グラフデータは GQL というふうに使い分けできるようになると強力なツールになると思います

参考サイト

2021年7月23日金曜日

Redis の管理画面の RedisInsight を試してみた

Redis の管理画面の RedisInsight を試してみた

環境

Redis の管理画面に RedisInsight というのがあります これを使うと画面からキーの登録や削除、取得などができるようになります redis-cli を直接叩いたりすることもできます

環境

  • macOS 11.4
  • Redis 4.0.9
  • RedisInsight 1.10.1

インストール

今回は docker 上で動作させます 以下のコマンドで起動できます

  • docker run -d -v redisinsight:/db -p 8001:8001 redislabs/redisinsight:latest

これでブラウザで 8001 にアクセスすると管理画面が確認できます 一番始めに EULA に同意する必要があるのでトグルボタンを変更して CONFIRM をクリックしましょう

設定

次に接続する Redis を選択します 今回は VM 上で動作している既存の redis があるのでそれを使います その場合は「I already have a database」を選択します

「Connect to Redis Database」を選択します

Redis を追加する画面になるので IP アドレスとポートと好きなデータベース名を入力しましょう

追加が完了すると以下のように一覧に表示されます

動作確認

試しに key/value を登録してみましょう 「ADD KEY」からキーを登録できます 今回は文字列を登録しています

登録が完了すると以下のように一覧に表示されます

最後に

編集機能まで使えると危険なことがあるので slave を用意してそっちを登録するとビューワーとして使うことができると思います (おそらく readonly ユーザは作成できないはず)

参考サイト

2021年5月2日日曜日

bitnami の redis helm chart を使ってみた

bitnami の redis helm chart を使ってみた

概要

bitnami の redis helm chart を使ってみました
いくつかのデプロイ方式があるのでそれぞれ試しています

環境

  • kubernetes 1.20.4
  • helm 3.5.2
  • bitnami redis helm chart 14.0.1

事前準備

何でもいいので pv を用意しましょう
local を使う方法はこちら
default の storageClass を用意しても OK です
nfs で default の storageClass を作成する方法はこちら

リポジトリインストール

  • helm repo add bitnami https://charts.bitnami.com/bitnami
  • helm repo update

初期デプロイ

  • helm install test bitnami/redis

これで secret もデプロイされるのでこのパスワードを使って redis の構成を変更していきます

パスワードは環境変数に設定しておきます

  • export REDIS_PASSWORD=$(kubectl get secret --namespace default test-redis -o jsonpath="{.data.redis-password}" | base64 --decode)

Master-Slave 構成をデプロイする

まずは Master-Slave 構成を試してみます
アクセスするのは NodePort を使います

  • helm upgrade test bitnami/redis --set master.service.type=NodePort --set master.service.nodePort=30700 --set replica.service.type=NodePort --set replica.service.nodePort=30800 --set auth.password=$REDIS_PASSWORD

Master へのアクセスは 30700 ポートを使い Slave へのアクセスは 30800 を使います

デプロイした起動しているか確認しましょう
Master が 1 つ Slave が 3 つ起動していれば OK です

  • kubectl get pod
NAME                                               READY   STATUS    RESTARTS   AGE
nfs-subdir-external-provisioner-78cc5b4979-tnnnj   1/1     Running   0          19d
test-redis-master-0                                1/1     Running   0          97s
test-redis-replicas-0                              1/1     Running   0          7s
test-redis-replicas-1                              1/1     Running   0          48s
test-redis-replicas-2                              1/1     Running   0          68s
  • kubectl get svc
NAME                  TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
kubernetes            ClusterIP   10.96.0.1       <none>        443/TCP          19d
test-redis-headless   ClusterIP   None            <none>        6379/TCP         6m45s
test-redis-master     NodePort    10.97.246.247   <none>        6379:30700/TCP   6m45s
test-redis-replicas   NodePort    10.105.78.89    <none>        6379:30800/TCP   6m45s

動作確認

redis-cli を使って動作確認してみましょう
まずは Master にアクセスしてみます
ちゃんと Wirte できることが確認できます

  • export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
  • redis-cli -h $NODE_IP -p 30700 -a $REDIS_PASSWORD
NODE_IP:30700> set name hawksnowlog
OK

次に Slave にアクセスします

  • redis-cli -h $NODE_IP -p 30800 -a $REDIS_PASSWORD

Slave では Write できないのが確認できます
また Master で set した値がちゃんと参照できるのが確認できます

NODE_IP:30800> set age 10
(error) READONLY You can't write against a read only replica.
NODE_IP:30800> get name
"hawksnowlog"

failover を試してみる

基本は failover せず StatefullSet によって Pod が復帰してくる感じになります

  • kubectl delete pod test-redis-master-0

で Master を削除すると再度上がってきます
再起動中は Master にアクセスできなくなります

Sentinel 構成をデプロイしてみる

次に Sentinel 構成をデプロイしてみます

  • helm install test-sentinel bitnami/redis --set auth.password=$REDIS_PASSWORD --set sentinel.enabled=true --set sentinel.service.type=NodePort --set sentinel.service.nodePorts.redis=30700 --set sentinel.service.nodePorts.sentinel=30800

Redis へのアクセスは 30700 ポートを使い Sentinel へのアクセスは 30800 を使います

Sentinel への書き込みや読み込みはできないので基本的には 30700 番を使うことになります

動作確認

  • export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
  • redis-cli -h $NODE_IP -p 30700 -a $REDIS_PASSWORD

で書き込みと読み込むができることを確認しましょう
また Sentinel では get/set ができないことも確認します

# redis-cli -h $NODE_IP -p 30700 -a $REDIS_PASSWORD
NODE_IP:30700> set name hawksnowlog
OK
NODE_IP:30700> exit
# redis-cli -h $NODE_IP -p 30800 -a $REDIS_PASSWORD
NODE_IP:30800> get name
(error) ERR unknown command `get`, with args beginning with: `name`,

また注意点として Redis の構成は Master-Slave 構成になっています
何度か繰り返すとわかりますが 30700 番にアクセスして set するとできるときとできないときがあるのがわかると思います
これは NodePort 経由で Redis クラスタにアクセスした際にアクセスする Pod はバランシングされているためそうなります
本来であれば常に Master にアクセスしたり常に Slave にアクセスすることになると思います
その場合は Sentinel で現在の Master を調べてその Master にアクセスするようにしましょう

# redis-cli -h $NODE_IP -p 30800 -a $REDIS_PASSWORD
NODE_IP:30800> sentinel get-master-addr-by-name mymaster
1) "10.233.2.232"
2) "6379"
NODE_IP:30800> exit

# kubectl exec --stdin --tty test-sentinel-redis-node-0 -- redis-cli -h 10.233.2.232 -p 6379 -a $REDIS_PASSWORD
Defaulting container name to redis.
Use 'kubectl describe pod/test-sentinel-redis-node-0 -n default' to see all of the containers in this pod.
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
10.233.2.232:6379> set age 10
OK

failover を試してみる

Sentinel があるので Master の Redis がダウンした場合には Slave が昇格して Master が変わるはずです
Bitnami の Redis Pod の構成の場合 Sentinel + Redis ノードのサイドカー構成で 1 Pod になっているので Pod 内の Redis コンテナのみを削除することで failover を発生させてみます

  • kubectl exec test-sentinel-redis-node-0 -c redis -- /bin/sh -c "kill 1"

これで redis コンテナのみ再起動します
しばらくて redis コンテナが起動したら Master が切り替わっていることを確認してみます

まずはそれぞれの Pod にアクセスして role の情報を確認してみます

# kubectl exec test-sentinel-redis-node-0 -c redis -- redis-cli -a $REDIS_PASSWORD info | grep role
role:slave

# kubectl exec test-sentinel-redis-node-1 -c redis -- redis-cli -a $REDIS_PASSWORD info | grep role
role:master

# kubectl exec test-sentinel-redis-node-2 -c redis -- redis-cli -a $REDIS_PASSWORD info | grep role
role:slave

先程は test-sentinel-redis-node-0 が Master だったのに対して test-sentinel-redis-node-1 に failover していることが確認できます

次に Sentinel にも Master 情報を確認してみましょう

# redis-cli -h $NODE_IP -p 30800 -a $REDIS_PASSWORD
NODE_IP:30800> sentinel get-master-addr-by-name mymaster
1) "10.233.1.200"
2) "6379"

ちゃんと failover しているのが確認できると思います
Master にアクセスして Write できるかも確認してみましょう

ubectl exec --stdin --tty test-sentinel-redis-node-0 -- redis-cli -h 10.233.1.200 -p 6379 -a $REDIS_PASSWORD
Defaulting container name to redis.
Use 'kubectl describe pod/test-sentinel-redis-node-0 -n default' to see all of the containers in this pod.
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
10.233.1.200:6379> set age 10
OK

問題なさそうです

おまけ: リソース削除

helm uninstall test

for i in `kubectl get pvc -o json | jq -r '.items[].metadata.name'`; do kubectl delete pvc $i; done

最後に

簡単に Sentinel 構成もデプロイできるのでかなり使えそうです
values はある程度理解して使えるようにならないとダメかもしれません

failover させる際に Master の Sentinel も一緒に削除すると Sentinel Cluster が壊れて Pod が上がってこなくなりました
具体的には以下のコマンドで failover のテストをすると Pod が上がってこないのが確認できると思います

  • kubectl delete pod test-sentinel-redis-node-0

ちなみに Slave の Pod を削除してもクラスタは元の状態に戻ります
ダメそうなのは Master の Pod が削除された場合のみのようです

参考サイト

2021年4月5日月曜日

Heroku の Redis Addon が End of Life になったのでアップグレードしてみた

Heroku の Redis Addon が End of Life になったのでアップグレードしてみた

概要

Heroku から連絡があり使っている redis-addon のバージョンが古いからバージョンアップしてくれとの連絡が来ました
なのでアップグレードしてみました

環境

  • macOS 11.2.3
  • heroku 7.51.0
  • redis 4.0.14 -> 6.0

現状の確認

まずは現在使っている redis の情報を確認しましょう
Version が 4.0.14 で End of Life になっているのが確認できます

  • heroku redis:info -a your-app-name
=== redis-silhouetted-79757 (REDIS_URL)
Plan:                   Hobby Dev
Status:                 available
Created:                2019-06-28 03:22 
Version:                4.0.14 (End of Life)
Timeout:                300
Maxmemory:              noeviction
Maintenance:            not required
Maintenance window:     Mondays 20:30 to Tuesdays 00:30 UTC
Persistence:            None
HA Status:              Unavailable
Requires TLS:           No
Keyspace Notifications: Disabled

REDIS_URL の確認

上記の REDIS_URL を確認します
今回は新しい redis にデータを移行する方式を取ります

  • heroku config:get REDIS_URL -a your-app-name

ここで表示される URL はメモしておきましょう

既存の redis のデータを元に新規で redis を作成する

先程確認した REDIS_URL を元に新規で redis を作成します
addons:create--fork オプションを使います

  • heroku addons:create heroku-redis:hobby-dev --fork redis://h:xxxxxxxxx@ec2-xxxxxxxxx:12345 -a your-app-name
Creating heroku-redis:hobby-dev on ⬢ your-app-name... free
Your add-on should be available in a few minutes.
! WARNING: Data stored in hobby plans on Heroku Redis are not persisted.
redis-silhouetted-79757 is being created in the background. The app will restart when complete...
Use heroku addons:info redis-silhouetted-79757 to check creation progress
Use heroku addons:docs heroku-redis to view documentation

作成状況の確認

すぐに新規の redis は払い出されないので状況を逐一確認しましょう
Status: available になれば新規作成+データの移行が完了しています
fork in progress になっている場合はまだデータの移行中になるので古い redis など削除しないようにしましょう

  • heroku redis:info redis-silhouetted-79757 -a your-app-name

アプリが使用する redis を変更する

作成が完了したら使用する redis を変更します
redis:promote を使います
新規で作成された redis を指定しましょう

  • heroku redis:promote redis-amorphous-58530 -a your-app-name

動作確認

アプリにアクセスして正常に動作する確認しましょう

古い redis を削除する

古いバージョンの redis は削除しましょう
addons:destroy で古い redis を指定すれば OK です

  • heroku addons:destroy redis-silhouetted-79757

おまけ: データを移行しないでアップグレードする場合は

いきなり destroy しても良いですが丁寧なのは create -> promote -> destroy かなと思います

  • heroku addons:create heroku-redis:hobby-dev -a your-app-name2
  • heroku redis:promote redis-yyyy-12345 -a your-app-name2
  • heroku addons:destroy redis-xxxxx-12345

参考サイト

2020年10月13日火曜日

Ruby で redis にあるデータをオブジェクトにバインドする方法

概要

Ruby で Redis を扱う場合にデータ構造が決まっている場合はオブジェクトにしたい場合があります
そんな場合は redis-objects というライブラリを使うと簡単にオブジェクト操作できます

環境

  • macOS 10.15.7
  • Ruby 2.7.1p83
    • redis-objects 1.5.0

インストール

  • bundle init
  • vim Gemfile
gem "redis-objects"
  • bundle install

接続

内部的には redis ライブラリを使っているので初期化方法などは redis ライブラリと同じように行えます

  • vim app.rb
require 'redis'

Redis.current = Redis.new(
  :host => '127.0.0.1',
  :port => 6379
)

データを保存する

データを保存する場合も一度 redis-objects のオブジェクトを作成してから保存します
redis のオブジェクトとして操作するクラスは必ず Redis::Objects を include する必要があります

今回は単純な文字列を保存してみます
Redis::Objects には様々なタイプが用意されており文字列を保存したい場合には value を使って宣言します
そしてキーを一意に特定するために必ず id というメソッドを実装する必要があります

  • vim app.rb
require 'redis'
require 'redis/objects'

Redis.current = Redis.new(
  :host => '127.0.0.1',
  :port => 6379
)

class User
  include Redis::Objects
  value :name
  def id
    1
  end
end

user = User.new
user.name = "hawk"

これで実行すると redis 側には以下のようなデータが保存されているのが確認できると思います

127.0.0.1:6379> keys *
1) "user:1:name"
127.0.0.1:6379> get user:1:name
"hawk"

データを取得する

次に保存したデータを取得してみましょう
とは言ってもやることはオブジェクトを作成するだけです
データを参照する場合は .value を使います

  • vim app.rb
require 'redis'
require 'redis/objects'

Redis.current = Redis.new(
  :host => '127.0.0.1',
  :port => 6379
)

class User
  include Redis::Objects
  value :name
  def id
    1
  end
end

user = User.new
puts user.name.value # => hawk

カウンタを使う

先程は value というタイプを使いました
次は counter というタイプを使ってみます
これは数字の情報を redis で管理する他にカウントアップするための専用のメソッドが用意されています
increment を呼び出すと値を 1 つプラスしてくれます

  • vim app.rb
require 'redis'
require 'redis/objects'

Redis.current = Redis.new(
  :host => '127.0.0.1',
  :port => 6379
)

class User
  include Redis::Objects
  counter :my_posts
  def id
    1
  end
end

user = User.new
user.my_posts.increment
user.my_posts.increment
user.my_posts.increment
puts user.my_posts.value # 3
user.my_posts.reset
puts user.my_posts.value # 0
user.my_posts.reset 5
puts user.my_posts.value # 5
127.0.0.1:6379> keys *
1) "user:1:name"
2) "user:1:my_posts"
127.0.0.1:6379> type user:1:my_posts
string
127.0.0.1:6379> get user:1:my_posts
"5"

配列を使う

配列も扱えます
list を使ってキーを宣言します

require 'redis'
require 'redis/objects'

Redis.current = Redis.new(
  :host => '127.0.0.1',
  :port => 6379
)

class User
  include Redis::Objects
  list :favorites
  def id
    1
  end
end

user = User.new
['ruby', 'swift', 'python'].each do |lang|
  user.favorites << lang
end
127.0.0.1:6379> LLEN user:1:favorites
(integer) 3
127.0.0.1:6379> LRANGE user:1:favorites 0 -1
1) "ruby"
2) "swift"
3) "python"

ハッシュを使う

ハッシュも扱えます
hask_key を使ってキーを宣言します

  • vim app.rb
require 'redis'
require 'redis/objects'

Redis.current = Redis.new(
  :host => '127.0.0.1',
  :port => 6379
)

class User
  include Redis::Objects
  hash_key :score
  def id
    1
  end
end

user = User.new
user.score['japanese'] = 10
user.score['arithmetic'] = 20
user.score['science'] = 30
127.0.0.1:6379> hgetall user:1:score
1) "japanese"
2) "10"
3) "arithmetic"
4) "20"
5) "science"
6) "30"

直接 redis オブジェクトを使う

get や set といった redis のコマンドを直接実行することもできます
ただその場合はキーの指定も直接行う必要があります
クラス名、id メソッド、キー名を使って自動生成される redis のキーを指定します

  • vim app.rb
require 'redis'
require 'redis/objects'

Redis.current = Redis.new(
  :host => '127.0.0.1',
  :port => 6379
)

class User
  include Redis::Objects
  hash_key :score
  def id
    1
  end
end

score = User.redis.hgetall('user:1:score')
puts score

=> {"japanese"=>"10", "arithmetic"=>"20", "science"=>"30"}

最後に

redis-objects を使って redis にあるデータをオブジェクトにシリアライズしてから使用する方法を紹介しました
コード上でどういったデータが redis に入っているのか一目で確認することができるのも嬉しい点かなと思います

こちらのほうがオブジェクト指向っぽく書けますがデータ構造が変わった場合はクラス側も修正必要があるので手間であります
今回は基本的なシリアライズ/デシリアライズの方法しか紹介しませんでしたがカスタムシリアライザも作成できるので複雑なデータのオブジェクト化も自作すれば可能になります

参考サイト

2020年2月17日月曜日

redis でリストからインデックスを指定して値を削除する方法

概要

redis ではリストから値を削除するのにインデックスを指定して削除する方法が直接的にはありません
少し工夫することで可能なので紹介します

環境

  • macOS 10.15.2
  • redis 5.0.5

コマンド

  • INDEX=0 && redis-cli lrem hoge 1 "$(redis-cli lindex hoge $INDEX)"

INDEX に削除したいインデックスを指定します
hoge というキーのリストから値を削除します
lindex というインデックスを取得できるコマンドがあるのでそれを使って値を取得することで lrem しています

参考サイト

2019年6月29日土曜日

redis-rb でコネクションがどう扱われているか簡単に確認してみた

概要

Redis.new したオブジェクトをインスタンス変数として保持した場合と毎回生成するような処理を比べた場合に redis-server に対するコネクションがどうなっているか挙動を確認してみました

環境

  • macOS 10.14.5
  • Ruby 2.6.2p47
    • redis-rb 4.1.2
  • Redis 5.0.5

準備

  • bundle init
  • vim Gemfile
gem "redis"
  • bundle install --path vendor

インスタンス変数にした場合

Redis.new したオブジェクトをインスタンス変数にしてインスタンスメソッドで使う回すようにします
おそらくこれが最も一般的な使い方かなと思います

  • vim test.rb
require 'redis'

class CRedis
  def initialize
    @client = Redis.new
  end

  def keys
    @client.keys
  end

  def list
    @client.client('list')
  end
end

client = CRedis.new
3.times do
  client.list.each { |c| p c['id'] }
  client.keys.length
  sleep 10
end

key の数をチェックしているだけです
チェックしたあとにクライアントの状態が変化しているか確認します
redis-server は接続しているクライアントそれぞれに id を振り識別できるようにしています
id はクライアントの接続/切断があるたびにインクリメントしていきます
試しに redis-cli client list をたくさん実行するとどんどん id が増えていくのが確認できると思います

これを実行して見ましょう

  • bundle exec ruby test.rb
"49"                                                  
"49"                         
"49"

結果は同じ id が表示され続けました
つまりインスタンス変数にした場合は redis-rb では同じコネクションを使って接続していることがわかります (当然と言えば当然)

ちなみに上記を実行中に別のターミナルで確認コマンドとして以下を実行してみると以下のような結果が返ってきました

  • while true; do redis-cli client list; sleep 1; done
id=49 addr=127.0.0.1:59174 fd=8 name= age=5 idle=5 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
id=50 addr=127.0.0.1:59175 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
id=49 addr=127.0.0.1:59174 fd=8 name= age=6 idle=6 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
id=51 addr=127.0.0.1:59176 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
id=49 addr=127.0.0.1:59174 fd=8 name= age=7 idle=7 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
id=52 addr=127.0.0.1:59177 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
id=49 addr=127.0.0.1:59174 fd=8 name= age=8 idle=8 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
id=53 addr=127.0.0.1:59178 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
id=49 addr=127.0.0.1:59174 fd=8 name= age=9 idle=9 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
id=54 addr=127.0.0.1:59179 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
id=49 addr=127.0.0.1:59174 fd=8 name= age=10 idle=10 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
id=55 addr=127.0.0.1:59180 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
id=49 addr=127.0.0.1:59174 fd=8 name= age=11 idle=1 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
id=56 addr=127.0.0.1:59181 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
id=49 addr=127.0.0.1:59174 fd=8 name= age=12 idle=2 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
id=57 addr=127.0.0.1:59182 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
id=49 addr=127.0.0.1:59174 fd=8 name= age=13 idle=3 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
id=58 addr=127.0.0.1:59183 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
id=49 addr=127.0.0.1:59174 fd=8 name= age=14 idle=4 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys
id=59 addr=127.0.0.1:59184 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client

id=49 が redis-rb のクライアントになります
これの age フィールドが接続から切断までの時間になります
そして idel フィールドが接続はされているがコマンドが一切実行されていない時間です
確認用のスクリプトは 10 秒おきにコマンドを実行しているので idel=10 になると一度カウントがリセットされているのが確認できると思います

redis-server は idel 中のプロセスを kill しないのか

ここで疑問に思ったのは「この idel フィールドが無限に増え続けた場合に redis-server は切断したりするのか」ですが、どうやらデフォルトではしないようです

By default recent versions of Redis don't close the connection with the client if the client is idle for many seconds: the connection will remain open forever.

詳しく読んでいないので理由は不明ですがおそらくクライアント側はちゃんとルールを守って使ってくれるであろうという暗黙的な了解があるんだと思います
なので当然コネクションを張りっぱなしにしていると Too many open files や Out of memory エラーなどのカーネルエラーが発生するはずです

タイムアウトを設定してみる

とは言え運用するに辺りそういったクライアントが 100% 登場しないとは言い切れません
なのでちゃんとタイムアウトを設定することができます
timeout という config があるのでこれを設定します

  • redis-cli config get timeout
    • => 0
  • redis-cli config set timeout 5
    • => OK

これで再度先程のスクリプトを実行してみます
すると今度は id がカウントアップしているのが確認できると思います

"72"
"73"
"74"

idel が 5 秒以上あると強制的に切断されるので redis-rb は内部的に新しいコネクションを作成して実行しているようです
コネクション切断エラーが出ると思ったのですがそうではないようです

ちなみに切断の条件が idel だと確認したのはタイムアウトの時間を 15 などにして実行してもすべて同じ id が表示されたからです
もし age の時間がタイムアウトの条件であれば 3 回目に表示される id は別の id が表示されることになります

実行するたびに Redis.new する場合では

次にインスタンス変数ではなく module メソッド内で毎回 Redis.new するような場合の挙動を確認してみます

  • vim test2.rb
require 'redis'

module MRedis
  def keys
    client = Redis.new
    client.keys
  end

  def list
    client = Redis.new
    client.client('list')
  end

  module_function :keys, :list
end

3.times do
  MRedis.list.each { |c| p c['id'] }
  MRedis.keys.length
  sleep 10
end

keys, client list を実行するたびに Redis.new するようにしてみます
実行して挙動を確認しましょう

  • bundle exec ruby test2.rb
"85"
"86"
"87"
"88"
"89"

結果は上記のようになりました
1 回目は client list 分の id が表示されます
2 回目以降は keysclient list の 2 つ分が表示されます
2 回目以降に 2 つ表示されるのは前回の keys のコネクションが残っており次の keys が実行されるまでは破棄されないためです
つまりメソッド内で Redis.new している場合はそのメソッドが再度コールされるまではコネクションが保持され続けることになります

そんな場合のために意図的に close できます
なのでメソッド内で client を作成している場合などはちゃんと close をコールしてあげるのがお作法かなと思います

module MRedis
  def keys
    client = Redis.new
    ret = client.keys
    client.close
    ret
  end

  def list
    client = Redis.new
    ret = client.client('list')
    client.close
    ret
  end

  module_function :keys, :list
end

3.times do
  MRedis.list.each { |c| p c['id'] }
  MRedis.keys.length
  sleep 10
end

これで実行するとちゃんと keys コール後にコネクションが切断されており結果に表示されるのは client list の id のみになるが確認できると思います

"131"
"133"
"135"

おまけ: クライアントに名前を付与するには

client list の結果を見るとわかりますがクライアントには id 以外に name も付与できます
アプリを特定したい場合に便利です

  • vim test3.rb
client = Redis.new(:id => "hoge")
client.keys
sleep 5
  • bundle exec ruby test3.rb

でプロセスが実行中にクライアントを確認すると name が付与されているのが確認できます
:name ではなく :id を使うのが少し分かりづらいですが、、

  • redis-cil client list
id=139 addr=127.0.0.1:59480 fd=8 name=hoge age=1 idle=1 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=keys

最後に

redis-rb のクライアントのコネクションの挙動を確認してみました
基本はインスタンス変数として使うのが良いと思いますが場合によっては redis-cli のようにコマンドごとに接続してもいいのではと思います

Web アプリのように session があり特定の時間コネクションをつないでいたほうがリソースを節約できる場合はインスタンス変数にし、バッチなど単発で処理しすぐに破棄する場合はコマンドごとに接続しても問題ないと思います
ただ接続/切断を短時間で頻繁に繰り返すような処理は避けたほうが良いと思います

ちゃんとやるのであれば redis-server 側でコネクション数と idel 時間を監視しておかしなクライアントがいた場合は修正する感じにするのが良いかなと思います

2019年6月28日金曜日

Firebase のデータを Redis に格納する Ruby スクリプト

概要

メモがてら残しておきます
Firebase の認証はレガシーな Database Secret を使っています

環境

  • macOS 10.14.5
  • Ruby 2.6.2p47

準備

  • bundle init
  • vim Gemfile
gem "firebase"
gem "redis"
  • bundle install --path vendor

アプリ

  • vim app.rb
require './lib/my_fb.rb'
require './lib/my_redis.rb'

p MyRedis.store(MyFB.fetch_all)

firebase の処理

  • vim lib/my_fb.rb
require 'firebase'

module MyFB
  BASE_URL = 'https://product_id.firebaseio.com/'
  SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

  def fetch_all
    client = Firebase::Client.new(BASE_URL, SECRET)
    ret = {}
    ret.store('category1', client.get('db1/category1').body)
    ret.store('category2', client.get('db1/category2').body)
    ret
  end

  module_function :fetch_all
end

Redis の処理

  • vim lib/my_redis.rb
require 'redis'
require 'json'

module MyRedis

  def store(ret)
    client = Redis.new
    sum = 0
    ret.each do |key, ary|
      ary.each_with_index do |p, i|
        client.set("#{key}_#{i}", p.to_json)
      end
      client.set("#{key}", ary.length)
      sum += ary.length
    end
    sum
  end

  module_function :store
end

Redis には category1_0 のような key で登録します
後からインデックスで検索、ループ処理しやすいように各カテゴリごとのデータのサイズを category1 という key に登録しています

実行

  • bundle exec ruby app.rb

動作確認

redis-cli get category1_0 で JSON の値が取得できれば OK
また redis-cli get category1 でコンテンツの数が取得できれば OK

最後に

サブカテゴリみたいに更に階層が深い場合にはループを増やせば OK です
Redis も localhost を前提にしているので外部なのであれば URL を指定してください

2019年6月1日土曜日

Redis Cluster を k8s 上に構築する

概要

前回 k8s 上に Redis Sentinel 環境を構築するところまでやってみました
今回は Redis Cluster に挑戦したいと思います
StatefulSet と ConfigMap を使って構築しています

環境

  • macOS 10.14.5
  • minikube v0.28.2
  • Redis 5.0.5

ConfigMap

  • vim redis_cluster_configmap.yml
apiVersion: v1
kind: ConfigMap
metadata:
  name: redis-cluster
data:
  redis.conf: |+
    port 6379
    cluster-enabled yes
    cluster-config-file /data/nodes.conf
    cluster-node-timeout 5000
    appendonly yes
  • kubectl apply -f redis_cluster_configmap.yml

StatefulSet

  • vim redis_cluster_sts.yml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis-cluster
spec:
  selector:
    matchLabels:
      app: redis
  serviceName: "redis"
  replicas: 6
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis
        command: ["redis-server", "/conf/redis.conf"]
        ports:
        - containerPort: 6379
          name: client
        - containerPort: 16379
          name: gossip
        volumeMounts:
        - name: data
          mountPath: /data
        - name: conf
          mountPath: /conf
      volumes:
      - name: conf
        configMap:
          name: redis-cluster
          defaultMode: 0755
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi
  • kubectl apply -f redis_cluster_sts.yml
  • kubectl exec redis-cluster-0 redis-cli info cluster
# Cluster
cluster_enabled:1

Headless Service

あとからわかったのですが redis-cli --cluster create はホスト名では使えないようです
なので Headless Service のデプロイは不要です

  • vim redis_cluster_service.yml
apiVersion: v1
kind: Service
metadata:
  name: redis
  labels:
    app: redis
spec:
  ports:
  - port: 6379
    name: client
  - port: 16379
    name: gossip
  clusterIP: None
  selector:
    app: redis
  • kubectl apply -f redis_cluster_service.yml
  • kubectl run -i --tty --image busybox:1.28 dns-test --restart=Never --rm
/ # nslookup redis-cluster-0.redis
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      redis-cluster-0.redis
Address 1: 172.17.0.7 redis-cluster-0.redis.default.svc.cluster.local

クラスタ作成

kubectl exec -it redis-cluster-0 -- \
redis-cli --cluster create \
172.17.0.7:6379 \
172.17.0.8:6379 \
172.17.0.9:6379 \
172.17.0.10:6379 \
172.17.0.11:6379 \
172.17.0.12:6379 \
--cluster-replicas 1
  • kubectl exec redis-cluster-0 -- redis-cli --cluster info 172.17.0.7:6379
172.17.0.7:6379 (922c7825...) -> 0 keys | 5461 slots | 1 slaves.
172.17.0.9:6379 (189ed07f...) -> 0 keys | 5461 slots | 1 slaves.
172.17.0.8:6379 (a7d9f418...) -> 0 keys | 5462 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.
  • kubectl exec redis-cluster-0 -- redis-cli cluster nodes
189ed07f5f7d736d623e5da3b384193ac620f777 172.17.0.9:6379@16379 master - 0 1559107652051 3 connected 10923-16383
a7d9f418035595e68231b2baf079c65064a43a96 172.17.0.8:6379@16379 master - 0 1559107651000 2 connected 5461-10922
922c7825f518a6af2d29ce934c09f43d72f97951 172.17.0.7:6379@16379 myself,master - 0 1559107650000 1 connected 0-5460
8c6c41a4179261abad6f521ce0e21a6c05aed317 172.17.0.10:6379@16379 slave 189ed07f5f7d736d623e5da3b384193ac620f777 0 1559107651347 4 connected
7534abbae8a6ffe4183eb45b294507e19fed5516 172.17.0.11:6379@16379 slave 922c7825f518a6af2d29ce934c09f43d72f97951 0 1559107651548 5 connected
a000022e75dbbeaadecd48b21c2a9322bad73c28 172.17.0.12:6379@16379 slave a7d9f418035595e68231b2baf079c65064a43a96 0 1559107651000 6 connected

failover 動作確認

  • kubectl delete po redis-cluster-0
  • kubectl exec redis-cluster-1 -- redis-cli --cluster info 172.17.0.8:6379
172.17.0.9:6379 (189ed07f...) -> 0 keys | 5461 slots | 1 slaves.
172.17.0.11:6379 (7534abba...) -> 0 keys | 5461 slots | 1 slaves.
172.17.0.8:6379 (a7d9f418...) -> 0 keys | 5462 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.
  • kubectl exec redis-cluster-1 -- redis-cli cluster nodes
a000022e75dbbeaadecd48b21c2a9322bad73c28 172.17.0.12:6379@16379 slave a7d9f418035595e68231b2baf079c65064a43a96 0 1559107872000 6 connected
8c6c41a4179261abad6f521ce0e21a6c05aed317 172.17.0.10:6379@16379 slave 189ed07f5f7d736d623e5da3b384193ac620f777 0 1559107873547 4 connected
189ed07f5f7d736d623e5da3b384193ac620f777 172.17.0.9:6379@16379 master - 0 1559107872541 3 connected 10923-16383
922c7825f518a6af2d29ce934c09f43d72f97951 172.17.0.7:6379@16379 slave 7534abbae8a6ffe4183eb45b294507e19fed5516 0 1559107873000 7 connected
a7d9f418035595e68231b2baf079c65064a43a96 172.17.0.8:6379@16379 myself,master - 0 1559107871000 2 connected 5461-10922
7534abbae8a6ffe4183eb45b294507e19fed5516 172.17.0.11:6379@16379 master - 0 1559107872843 7 connected 0-5460

ノード追加

  • vim redis_cluster_sts.yml

replicas: 6 -> replicas: 8

  • kubectl apply -f redis_cluster_sts.yml
  • kubectl get po -o wide
NAME              READY   STATUS    RESTARTS   AGE   IP            NODE
redis-cluster-0   1/1     Running   0          36m   172.17.0.7    minikube
redis-cluster-1   1/1     Running   0          1h    172.17.0.8    minikube
redis-cluster-2   1/1     Running   0          1h    172.17.0.9    minikube
redis-cluster-3   1/1     Running   0          1h    172.17.0.10   minikube
redis-cluster-4   1/1     Running   0          1h    172.17.0.11   minikube
redis-cluster-5   1/1     Running   0          1h    172.17.0.12   minikube
redis-cluster-6   1/1     Running   0          31m   172.17.0.13   minikube
redis-cluster-7   1/1     Running   0          1m    172.17.0.14   minikube
  • kubectl exec -it redis-cluster-1 -- redis-cli --cluster add-node 172.17.0.13:6379 172.17.0.8:6379

=> cdea61f96f575df26eb4828dfa5ab937a3f9f27f

  • kubectl exec -it redis-cluster-1 -- redis-cli --cluster add-node 172.17.0.14:6379 172.17.0.8:6379 --cluster-slave --cluster-master-id cdea61f96f575df26eb4828dfa5ab937a3f9f27f

=> 040da0752197c7bf366f398df1653927f4ff1bef

  • kubectl exec redis-cluster-1 -- redis-cli --cluster info 172.17.0.8:6379
172.17.0.9:6379 (189ed07f...) -> 0 keys | 5461 slots | 1 slaves.
172.17.0.11:6379 (7534abba...) -> 0 keys | 5461 slots | 1 slaves.
172.17.0.8:6379 (a7d9f418...) -> 0 keys | 5462 slots | 1 slaves.
172.17.0.13:6379 (cdea61f9...) -> 0 keys | 0 slots | 1 slaves.
[OK] 0 keys in 4 masters.
0.00 keys per slot on average.

reshared

  • kubectl exec -it redis-cluster-1 -- redis-cli --cluster reshard 172.17.0.8:6379

インタラクティブモードになるので以下の順番で入力します

  • 4096
  • cdea61f96f575df26eb4828dfa5ab937a3f9f27f
  • all
  • yes

スロットが正常に reshard されているか確認します

  • kubectl exec redis-cluster-1 -- redis-cli --cluster info 172.17.0.8:6379
172.17.0.8:6379 (a7d9f418...) -> 0 keys | 4096 slots | 1 slaves.
172.17.0.13:6379 (cdea61f9...) -> 0 keys | 4096 slots | 1 slaves.
172.17.0.9:6379 (189ed07f...) -> 0 keys | 4096 slots | 1 slaves.
172.17.0.11:6379 (7534abba...) -> 0 keys | 4096 slots | 1 slaves.
[OK] 0 keys in 4 masters.
0.00 keys per slot on average.

ノード削除

  • kubectl exec -it redis-cluster-1 -- redis-cli --cluster reshard 172.17.0.8:6379

インタラクティブモードになるので以下の順番で入力します

  • 4096
  • a7d9f418035595e68231b2baf079c65064a43a96
  • cdea61f96f575df26eb4828dfa5ab937a3f9f27f
  • done
  • yes

reshard 後削除します

  • kubectl exec -it redis-cluster-1 -- redis-cli --cluster del-node 172.17.0.13:6379 cdea61f96f575df26eb4828dfa5ab937a3f9f27f
  • kubectl exec redis-cluster-1 -- redis-cli --cluster info 172.17.0.8:6379
172.17.0.8:6379 (a7d9f418...) -> 0 keys | 8192 slots | 2 slaves.
172.17.0.9:6379 (189ed07f...) -> 0 keys | 4096 slots | 1 slaves.
172.17.0.11:6379 (7534abba...) -> 0 keys | 4096 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.
  • kubectl exec -it redis-cluster-1 -- redis-cli --cluster del-node 172.17.0.14:6379 040da0752197c7bf366f398df1653927f4ff1bef

rebalance

  • kubectl exec -it redis-cluster-1 -- redis-cli --cluster rebalance 172.17.0.8:6379
  • kubectl exec redis-cluster-1 -- redis-cli --cluster info 172.17.0.8:6379
172.17.0.8:6379 (a7d9f418...) -> 0 keys | 5462 slots | 1 slaves.
172.17.0.9:6379 (189ed07f...) -> 0 keys | 5461 slots | 1 slaves.
172.17.0.11:6379 (7534abba...) -> 0 keys | 5461 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.

Tips

一度削除したノードがある場合は Pods の他に PersistentVolume も削除しないと前の nodes.conf が残っているため再度 add-node できません

[ERR] Node 172.17.0.13:6379 is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0.

一度 StatefulSet でスケールダウンさせ PersistentVolume を削除するか Pod からアンマウントして PersistentVolume を削除してからスケールアウトして新規で Pod を作成する必要がある
もしくは強制的に pvc -> pv -> pod を削除します

  • kubectl delete pvc data-redis-cluster-6
  • kubectl delete pv pvc-2b491981-81d3-11e9-b396-08002719d78a
  • kubectl delete po redis-cluster-6

で再度 add-node できます

あとしまつ

  • kubectl delete sts redis-cluster
  • for i in {0..7}; do kubectl delete pvc data-redis-cluster-$i; done
  • kubectl delete svc redis

最後に

k8s 上で StatefulSet を使って RedisCluster を構築してみました
Headless Service が使えないので、正直 StatefulSet である必要はないかもしれませんが公式の使用パターンでも推奨しているので特に理由がなければ StatefulSet でいいかなと思います

それよりも Redis Cluster でノードを追加するときにホスト名ベースで追加できるように対応してほしいなと感じました
そうすれば Headless Service が使えるのでもっとキレイにできるようになると思います

参考サイト