Featured image of post Redis Sentinel Watchdog

Redis Sentinel Watchdog

Костыли к Sentinel Redis кластеру, если приложение не умеет в Sentinel

Введение

Очень люблю чарты от Bitnami, хорошо написаны и довольно стабильны. Среди прочего, всегда ставлю Redis из их чарта. Всё бы хорошо, когда тебе нужен просто Redis без всякого там High Availability, но так бывает не всегда.

В чём сложность поднять Redis HA?

Тут дело в том, что драйверы подключения к Redis в сервисах-клиентах должны поддерживать Redis Sentinel. Когда мы поднимаем кластер с Redis, так же поднимается по контейнеру с Sentinel - это своего рода контроллер для кластера, который сделает замену мастера если текущий пропадёт. Очень похоже на Mongo Arbiter. Так вот механизм подключения из-за этого самого Sentinel меняется.

  1. Подключаемся к Sentinel
  2. Запрашиваем адрес мастера
  3. Подключаемся к мастеру
  4. Если мастер недоступен - опять идём к 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
All rights reserved
Создано при помощи Hugo
Тема Stack, дизайн Jimmy