KubernetesでRedisを構築した。
構成決定まで紆余曲折があったのだけど、それは置いておくとして、最終的には以下の素朴な構成に落ち着いた。
StatefulSetで4Replicasで構成する。それぞれのRedisはStandaloneで動くので、メモリ情報はPod間で共有しない。今回の要件では特に共有は不要だったのでこのようなシンプルな構成にした。
それぞれのPodがPersistentVolumeClaim(PVC)をマウントする。ApplicationはRedisへはService Resourceを介してアクセスする。なお、メモリの容量は1GiBにした。
StatefulSetを構築してわかったこと
StatefulSetの構築は初めてだったのだけれど、ひとつ新しく知ったのはStatefulSetごとに別のPVCをマウントする場合、StatefulSetの中にvolumeClaimTemplatesというパラメータを設定すると1対1でマウントするPVCを自動で作ってくれる。しかもPVCのResourceを別途作成する必要もない。こんな感じ。
volumeClaimTemplates: - metadata: name: data spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 1Gi storageClassName: volume-storage-class
初めて知ったのだけれどめちゃくちゃ便利だ。 ref: StatefulSet | Kubernetes
これで以下のような構成になる。
% k get statefulset -n redis NAME READY AGE redis 4/4 2d18h % k get pod -n redis NAME READY STATUS RESTARTS AGE redis-0 1/1 Running 0 19h redis-1 1/1 Running 0 19h redis-2 1/1 Running 0 19h redis-3 1/1 Running 0 19h % k get pvc -n redis NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE data-redis-0 Bound pvc-8a2080c6-edb5-4c25-8366-7ea8d4f958f2 1Gi RWO volume-storage-class 2d18h data-redis-1 Bound pvc-5af5bad2-2b84-49bc-9b63-9368aaea9648 1Gi RWO volume-storage-class 2d18h data-redis-2 Bound pvc-17ad4657-bd7d-4f4a-a7d9-c2da90a01423 1Gi RWO volume-storage-class 2d18h data-redis-3 Bound pvc-7b797afe-570f-4e19-9ebc-da30372aedee 1Gi RWO volume-storage-class 2d18h
Redisのパラメータ調整
StatefulSetでRedisが起動した後、設定をながめているとデフォルト値ではまずいな、と思ったことがひとつあった。
redis-svc.redis.svc.cluster.local:6379> INFO Memory <snip> maxmemory:0 maxmemory_human:0B maxmemory_policy:noeviction <snip>
これはつまりメモリの格納量が飽和するとそれ以上格納できなくなることを示している。ref: Key eviction | Redis
なのでメモリのサイズを使い切ったら使用頻度の低いキーを削除するポリシーに変えた。
apiVersion: v1 kind: ConfigMap metadata: name: redis-config namespace: redis data: redis.conf: | maxmemory 1gb maxmemory-policy allkeys-lfu
メモリ限界検証の試験をした
- StatefulSetのリソース設定でメモリのLimitを1Giにした
- Redisの設定で使えるメモリも1GBとした
- 使われないキーから削除する
これらの設定でちゃんと動作するのかが不安だったのでメモリ限界検証の試験をした。
RedisのPodに向けて、キーを格納しまくる。(DEBUGコマンドがあるようだがkeyをsetするほうがしっくりくるのでシェルで回した)
% k exec -it redis-pod -- /bin/bash root@redis-pod:/data# for i in {1..1000000}; do redis-cli -h 10.233.134.136 set test$i "$(for l in {1..10000}; do echo 1234567890;done)" > /dev/null; done
これを実施してしばらくしたら、Redisのメモリが1024Mで頭打ちになった。
root@redis-pod:/data# redis-cli -h 10.233.134.136 info memory | grep used_memory_human used_memory_human:1023.81M root@redis-pod:/data# redis-cli -h 10.233.134.136 info memory | grep used_memory_human used_memory_human:1023.83M root@redis-pod:/data# redis-cli -h 10.233.134.136 info memory | grep used_memory_human used_memory_human:1023.86M
キーの数は減っていくような動作に見えた
root@redis-pod:/data# redis-cli -h 10.233.134.136 info Keyspace # Keyspace db0:keys=123803,expires=0,avg_ttl=0 root@redis-pod:/data# redis-cli -h 10.233.134.136 info Keyspace # Keyspace db0:keys=123541,expires=0,avg_ttl=0 root@redis-pod:/data# redis-cli -h 10.233.134.136 info Keyspace # Keyspace db0:keys=123280,expires=0,avg_ttl=0
evicted_keysはガンガン増えていったので、消されていっていることがわかる
root@redis-pod:/data# redis-cli -h 10.233.134.136 info stats | grep evicted_keys evicted_keys:83580
Podをtopでみるとこちらも1024Miで頭打ちになっていた。
2023年 4月21日 金曜日 15時55分40秒 JST NAME CPU(cores) MEMORY(bytes) redis-2 57m 1023Mi 2023年 4月21日 金曜日 15時55分45秒 JST NAME CPU(cores) MEMORY(bytes) redis-2 57m 1023Mi
この状態で新規にキーバリューを格納したら正常に動作することを確認できた。
root@redis-pod:/data# redis-cli -h 10.233.134.136 set check_testdata1 "check_testdata_put1" OK root@redis-pod:/data# redis-cli -h 10.233.134.136 get check_testdata1 "check_testdata_put1"
検証してわかったこと
Redisはオンメモリデータベースなので、Podのメモリ使用量もRedisで設定するmaxmemory
の値は使うことを意識しないといけない。
つまり、
- RedisでStatefulSetを構築するときはメモリのリソース設定が必須。1Gi使いたい場合はrequest / limitともに1Gi以上の値を入れる必要がある。
- 自分の場合はRedisのmaxmemory、StatefulSetのRequest、StatefulSetのLimitをすべて1Giに合わせた。これでちゃんと動いてそうだったので。
- maxmemory-policy はちゃんと設定しましょう
ということがわかった。
なお、CPUリソースの設定値がまだよくわからないのでLimitだけ多めに設定して、あとはVPAを入れて運用しながら調整することにした。
参考までに、自分がつくったyamlファイルを置いておく。
redis yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: redis-config
namespace: redis
data:
redis.conf: |
maxmemory 1gb
maxmemory-policy allkeys-lfu
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
namespace: redis
spec:
serviceName: redis
replicas: 4
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
nodeSelector:
storage: redis-storage
tolerations:
- key: "type"
operator: "Equal"
value: "redis-storage"
effect: "NoExecute"
containers:
- name: redis
image: redis:7.0.11
command: ["redis-server", "/redis-standalone/redis.conf"]
resources:
requests:
memory: "1Gi"
limits:
memory: "1Gi"
cpu: "1"
env:
- name: ALLOW_EMPTY_PASSWORD
value: "yes"
volumeMounts:
- name: data
mountPath: /data
- name: config
mountPath: /redis-standalone
volumes:
- name: config
configMap:
name: redis-config
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
storageClassName: volume-storage-class
---
apiVersion: v1
kind: Service
metadata:
name: redis-svc
namespace: redis
spec:
ports:
- port: 6379
targetPort: 6379
name: redis
clusterIP: None
selector:
app: redis
---
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: redis
namespace: redis
spec:
targetRef:
apiVersion: apps/v1
kind: StatefulSet
name: redis
updatePolicy:
updateMode: "Off"