Введение ¶
Очень люблю чарты от Bitnami, хорошо написаны и довольно стабильны. Среди прочего, всегда ставлю Redis из их чарта. Всё бы хорошо, когда тебе нужен просто Redis без всякого там High Availability, но так бывает не всегда.
В чём сложность поднять Redis HA?
Тут дело в том, что драйверы подключения к Redis в сервисах-клиентах должны поддерживать Redis Sentinel. Когда мы поднимаем кластер с Redis, так же поднимается по контейнеру с Sentinel - это своего рода контроллер для кластера, который сделает замену мастера если текущий пропадёт. Очень похоже на Mongo Arbiter. Так вот механизм подключения из-за этого самого Sentinel меняется.
- Подключаемся к Sentinel
- Запрашиваем адрес мастера
- Подключаемся к мастеру
- Если мастер недоступен - опять идём к Sentinel
Очень часто приложения такое не поддерживают. Что же делать в таком случае? Я решил что мне поможет, как всегда, инкостыляция.
Ниже представлен фрагмент values.yaml, с которыми я запускаю bitnami/redis используя Helmfile.
Что там происходит?
Среди прочего, я создаю дополнительный sidecar контейнер, который каждые N секунд запрашивает у Sentinel адрес мастера и если он отличается от такового в созданном Service типа ExternalName - обновляет сервис. Таким образом, мы можем просто направить приложения, которые с Sentinel не умеют общаться, на этот сервис и всё будет работать, так как сервис всегда будет указывать на правильный под.
Потребляет такой sidecar контейнер ~65 mCPU и 1 MiB. Можно конечно собрать образ и включить туда redis-cli чтобы подключаться напрямую, но неохота поддерживать и хранить этот образ, обновлять его.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
| # ┬─┐┬─┐┬─┐o┐─┐
# │┬┘├─ │ ││└─┐
# ┘└┘┴─┘┘─┘┘──┘
{{- $name := "redis" }}
{{- $masterServiceName := printf "%s-master" $name }}
nameOverride: {{ $name }}
fullnameOverride: {{ $name }}
architecture: replication
auth:
enabled: true
password: {{ quote .Values.secrets.redisPassword }}
replica:
replicaCount: 3
resources:
limits:
memory: 75Mi
requests:
cpu: 50m
memory: 25Mi
persistence:
enabled: true
storageClass: standard
size: 1Gi
podAntiAffinityPreset: hard
sidecars:
- name: master-service-watchdog
image: '{{`{{ printf "bitnami/kubectl:%s.%s" .Capabilities.KubeVersion.Major .Capabilities.KubeVersion.Minor }}`}}'
imagePullPolicy: IfNotPresent
command: ["/bin/sh", "-ec"]
args:
- |
while sleep 5; do
MASTER_ADDRESS="$(kubectl exec -c sentinel "${POD_NAME}" -- sh -c "REDISCLI_AUTH='$REDISCLI_AUTH' redis-cli -p '${REDIS_SENTINEL_PORT}' SENTINEL get-master-addr-by-name this" | head -1)"
if ! kubectl get svc "${MASTER_SERVICE}" -o jsonpath='{.spec.externalName}' | grep -qF "${MASTER_ADDRESS}"; then
echo "info: master address has changed to ${MASTER_ADDRESS}"
jq -Mnr --arg address "${MASTER_ADDRESS}" '[{op:"replace",path:"/spec/externalName",value:$address}]' | \
xargs -0 kubectl patch svc "${MASTER_SERVICE}" --type=json --patch
fi
done
env:
- name: MASTER_SERVICE
value: {{ quote $masterServiceName }}
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: REDISCLI_AUTH
valueFrom:
secretKeyRef:
name: '{{`{{ template "common.names.fullname" . }}`}}'
key: redis-password
- name: REDIS_SENTINEL_PORT
value: '{{`{{ .Values.sentinel.containerPorts.sentinel }}`}}'
resources:
limits:
cpu: 100m
memory: 50Mi
requests:
cpu: 100m
memory: 50Mi
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
privileged: false
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
seccompProfile:
type: RuntimeDefault
serviceAccount:
create: true
sentinel:
enabled: true
masterSet: this
automateClusterRecovery: true
downAfterMilliseconds: 2000
resources:
limits:
memory: 50Mi
requests:
cpu: 50m
memory: 50Mi
networkPolicy:
enabled: true
extraIngress:
- ports:
- port: 6379
from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ .Release.Namespace }}
rbac:
create: true
rules:
# Allow watchdog update our service
- apiGroups: [""]
resources: ["services"]
verbs: ["get", "update", "patch"]
resourceNames: [{{ quote $masterServiceName }}]
# Allow exec into Redis pods
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
pdb:
create: true
raw:
Service:
apiVersion: v1
kind: Service
metadata:
name: {{ $masterServiceName }}
spec:
type: ExternalName
# 1.1.1.1 is just a stub value that is used for initial deployment
# It will be overridden as soon as first watchdog will be ready
externalName: 1.1.1.1
ports:
- port: 6379
protocol: TCP
targetPort: 6379
name: redis
|