Использование PostStart хука при запуске пода в Kubernetes-кластере

Dec 8, 2020 08:37 · 776 words · 4 minute read kubernetes k8s redis

После написания статьи о вставке данных в redis при запуске контейнера в кластере Kubernetes Александр Косенко вполне резонно заметил, что для решения такой задачи можно использовать PostStart хук, который предоставляется “из коробки” для управления жизненным циклом контейнера. Давайте разберемся!

Хуки дают возможность получать информацию о жизненном цикле управления контейнерами и выполнять код, реализованный в обработчике (handler), при срабатывании определенного хука.

Для каждого контейнера в поде хуки определяются отдельно. Существуют два типа хуков - PostStart и PreStop. Первый является асинхронным и выполняется сразу же при создании контейнера, однако нет никакой гарантии, что данный хук будет выполнен до запуска инструкции ENTRYPOINT контейнера. Стоит отметить, что если выполнение PostStart хука занимает очень много времени (или зависает), то контейнер не может перейти в состояние Running.

Хук PreStop, как видно из его названия, выполняется перед тем как контейнер будет остановлен (terminated) - будь то API-запрос или другое событие (например, неудачная liveness probe, “выдавливание” пода с узла кластера, перебор используемых ресурсов). Этот вызов синхронный, а это значит, что он обязательно должен быть завершен до того, как будет отправлен сигнал остановки контейнера.

Для хуков в жизненном цикле контейнеров предусмотрено два варианта обработчиков (handlers):

  • Exec - выполняет определенную команду (скрипт) в пространстве имен контейнера. Ресурсы, которые используются данной командой также учитываются в используемых ресурсах контейнера (важно при определении памяти и CPU);
  • HTTP - выполняет HTTP-запрос на определенный эндпоинт контейнера.

Если какой-то из хуков PostStart или PreStop завершается с ошибкой, то контейнер также будет остановлен. Логи хуков недоступны при выполнении команды kubectl logs <pod_name>, но если по какой-то причине они выполнились неудачно, то происходит событие FailedPostStartHook или FailedPreStopHook соответственно. Эти события можно увидеть выполнив команду kubectl describe pod <pod_name>.

Итак, мы вполне можем использовать PostStart хук для вставки данных в Redis при старте контейнера.

Идея состоит в следующем: с помощью ConfigMap мы добавим файл(ы) внутрь контейнера, причем названием ключа в редисе будет имя, а значением - содержимое этого файла. Далее, используя PostStart хук, мы “обработаем” каждый из файлов и вставим соответствующие данные в БД Redis.

Манифест, содержащий в себе все необходимые объекты Kubernetes, будет выглядеть так:

apiVersion: v1
kind: Service
metadata:
  name: ads-redis-test
  namespace: default
spec:
  selector:
    app: ads-redis-test
  ports:
  - name: redis
    port: 6379
  clusterIP: None
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: ads-redis-test
  namespace: default
data:
  flow-rules-key: |
    [{
      "resource": "loopme.grpc.ssp.v0.AdsTxtRecordService/GetAdsTxtRelationships",
      "count": 100.0,
      "grade": "THREAD",
      "limit-app": "default"
    },{
      "resource": "loopme.grpc.ssp.v0.PublisherAccountService/GetPublisherById",
      "count": 5.0,
      "grade": "THREAD",
      "limit-app": "default"
    },{
      "resource": "loopme.grpc.ssp.v1.PublisherAccountService/GetPublisherById",
      "count": 5.0,
      "grade": "THREAD",
      "limit-app": "default"
    },{
      "resource": "loopme.grpc.ssp.v0.BundleLegacyService/GetBundleByKey",
      "count": 20.0,
      "grade": "THREAD",
      "limit-app": "default"
    },{
      "resource": "loopme.lsm.ssp.v0.BundleService/GetBundleById",
      "count": 20.0,
      "grade": "THREAD",
      "limit-app": "default"
    },{
      "resource": "loopme.lsm.ssp.v0.BundleService/QueryBundle",
      "count": 20.0,
      "grade": "THREAD",
      "limit-app": "default"
    },{
      "resource": "loopme.grpc.ssp.v0.AppLegacyService/GetAppById",
      "count": 10.0,
      "grade": "THREAD",
      "limit-app": "default"
    },{
      "resource": "loopme.grpc.ssp.v0.AppLegacyService/GetAppIdByKey",
      "count": 10.0,
      "grade": "THREAD",
      "limit-app": "default"
    },{
      "resource": "loopme.grpc.ssp.v0.AppLegacyService/GetAppIdByContainerKey",
      "count": 16.0,
      "grade": "THREAD",
      "limit-app": "default"
    },{
      "resource": "loopme.grpc.ssp.v0.AppLegacyService/GetAppByContainerKey",
      "count": 10.0,
      "grade": "THREAD",
      "limit-app": "default"
    },{
      "resource": "ExchangeThrottleRateService/GetThrottleRatesByKeys",
      "count": 20.0,
      "grade": "THREAD",
      "limit-app": "default"
    },{
      "resource": "dsp-fetcher",
      "count": 25.0,
      "grade": "THREAD",
      "limit-app": "default"
    },{
      "resource": "exchange-fetcher",
      "count": 300.0,
      "grade": "THREAD",
      "limit-app": "default"
    },{
      "resource": "kafka_dmp_ads_requests_info",
      "count": 500.0,
      "grade": "QPS",
      "limit-app": "default"
    }]    
  degrade-rules-key: |
    [{
      "resource": "analytics.AnalyticsApiService/AnalyzeCall",
      "count": 10.0,
      "grade": "EXCEPTION_COUNT",
      "time-window": 10,
      "min-request-amount": 100,
      "stat-interval-ms": 20000,
      "slow-ratio-threshold": 0.6
    },{
      "resource": "analytics.AnalyticsApiService/AnalyzeCall",
      "count": 10.0,
      "grade": "EXCEPTION_RATIO",
      "time-window": 10,
      "min-request-amount": 100,
      "stat-interval-ms": 20000,
      "slow-ratio-threshold": 0.6
    }]    
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ads-redis-test
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ads-redis-test
  template:
    metadata:
      labels:
        app: ads-redis-test
    spec:
      containers:
      - name: redis
        image: redis:6.0.9
        ports:
        - name: redis
          containerPort: 6379
        resources:
          limits:
            cpu: "0.5"
            memory: 1Gi
          requests:
            cpu: "0.5"
            memory: 1Gi
        lifecycle:
          postStart:
            exec:
              command: ["/bin/bash", "-c", "cd /script/ && for FILE in *key; do cat ${FILE} | redis-cli -n 2 -x set ${FILE}; done"]
        livenessProbe:
          exec:
            command:
            - sh
            - -c
            - redis-cli -h $(hostname) ping
          initialDelaySeconds: 5
          periodSeconds: 3
        readinessProbe:
          exec:
            command:
            - sh
            - -c
            - redis-cli -h $(hostname) ping
          initialDelaySeconds: 5
          timeoutSeconds: 3
        volumeMounts:
        - mountPath: /script
          name: script
      volumes:
      - name: script
        configMap:
          name: ads-redis-test

Вся “магия” заключается в команде, которая определена в postStart хуке:

command: ["/bin/bash", "-c", "cd /script/ && for FILE in *key; do cat ${FILE} | redis-cli -n 2 -x set ${FILE}; done"]

Здесь для каждого файла в каталоге /script, который заканчивается на key выполняется следующее:

  • с помощью команды cat выводится содержимое файла в STDOUT;
  • через конвейер | передаются следующей команде - консольной утилите redis-cli (здесь в STDIN попадает содержимое STDOUT из предыдущего шага);
  • redis-cli выполняет вставку данных во вторую БД (ключ -n 2) с помощью команды SET.

Примечание Именем ключа будет значение переменной ${FILE} (имя файла), а значением - данные из STDIN (об этом заботится ключ -x).

На этом все, мы успешно вставили данные в Redis при запуске контейнера используя “родной” функционал Kubernetes, а именно PostStart хук управления жизненным циклом контейнера.

tweet Share