Kubernetes в production: сервисы

в 14:34, , рубрики: devops, kubernetes, load balancing, Блог компании okmeter.io, отказоустойчивость, распределенные системы, Серверное администрирование, системное администрирование

Kubernetes в production: сервисы - 1Полгода назад мы закончили миграцию всех наших stateless сервисов в kubernetes. На первый взгляд задача достаточно простая: нужно развернуть кластер, написать спецификации приложений и вперед. Из-за одержимости в вопросе обеспечения стабильности в работе нашего сервиса пришлось сразу начать разбираться с тем, как работает k8s и тестировать различные сценарии отказов. Больше всего вопросов у меня возникало ко всему, что касается сети. Один из таких "скользких" моментов — работа сервисов (Services) в kubernetes.

В документации нам говорят:

  • выкатите приложение
  • задайте liveness/readiness пробы
  • создайте сервис
  • дальше все будет работать: балансировка нагрузки, обработка отказов итд.

Но на практике все несколько сложнее. Давайте посмотрим, как оно работает на самом деле.

Немного теории

Далее я подразумеваю, что читатель уже знаком с устройством kubernetes и его терминологией, вспомним лишь, что такое сервис.

Сервис — сущность k8s, которая описывает совокупность подов и методов доступа к ним.

Например, мы запустили наше приложение:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp
spec:
  selector:
    matchLabels: 
      app: webapp
  replicas: 2
  template:
    metadata: 
      labels: 
        app: webapp
    spec:
      containers:
      - name: webapp
        image: defaultxz/webapp
        command: ["/webapp", "0.0.0.0:80"]
        ports:
        - containerPort: 80
        readinessProbe:
          httpGet: {path: /, port: 80}
          initialDelaySeconds: 1
          periodSeconds: 1

$ kubectl get pods -l app=webapp
NAME                      READY     STATUS    RESTARTS   AGE
webapp-5d5d96f786-b2jxb   1/1       Running   0          3h
webapp-5d5d96f786-rt6j7   1/1       Running   0          3h

Теперь чтобы обратиться к нему, мы должны создать сервис, в котором определяем к каким именно подам мы хотим иметь доступ (selector) и по каким портам:

kind: Service
apiVersion: v1
metadata:
  name: webapp
spec:
  selector:
    app: webapp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80

$ kubectl get svc webapp
NAME      TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
webapp    ClusterIP   10.97.149.77   <none>        80/TCP    1d

Теперь мы с любой машины кластера можем обратиться к нашему сервису:

curl -i http://10.97.149.77
HTTP/1.1 200 OK
Date: Mon, 24 Sep 2018 11:55:14 GMT
Content-Length: 2
Content-Type: text/plain; charset=utf-8

Как это все работает

Kubernetes в production: сервисы - 2

Очень упрощенно:

  • вы сделали kubectl apply спецификации Deployment
  • происходит магия, детали которой не важны в данном контексте
  • в результате на каких-то нодах оказались работающие поды приложения
  • раз в интервал kubelet (агент k8s на каждой ноде) выполняет liveness/readiness пробы всех запущенных на его ноде подов, результаты он отправляет в apiserver (интерфейс к мозгам k8s)
  • kube-proxy на каждой ноде получает уведомления от apiserver о всех изменениях сервисов и подов, которые участвуют в сервисах
  • kube-proxy все изменения отражает в конфигурации нижележащих подсистем (iptables, ipvs)

Для простоты рассмотрим дефолтный способ проксирования — iptables. В iptables у нас для нашего виртуального ip 10.97.149.77:

-A KUBE-SERVICES -d 10.97.149.77/32 -p tcp -m comment --comment "default/webapp: cluster IP" -m tcp --dport 80 -j KUBE-SVC-BL7FHTIPVYJBLWZN

трафик уходит в цепочку KUBE-SVC-BL7FHTIPVYJBLWZN, в которой распределяется между 2 другими цепочками

-A KUBE-SVC-BL7FHTIPVYJBLWZN -m comment --comment "default/webapp:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-UPKHDYQWGW4MVMBS
-A KUBE-SVC-BL7FHTIPVYJBLWZN -m comment --comment "default/webapp:" -j KUBE-SEP-FFCBJRUPEN3YPZQT

это уже наши поды:

-A KUBE-SEP-UPKHDYQWGW4MVMBS -p tcp -m comment --comment "default/webapp:" -m tcp -j DNAT --to-destination 10.244.0.10:80
-A KUBE-SEP-FFCBJRUPEN3YPZQT -p tcp -m comment --comment "default/webapp:" -m tcp -j DNAT --to-destination 10.244.0.11:80

Тестируем отказ одного из подов

Мое тестовое приложение webapp умеет переключаться в режим "сыпь ошибками", для этого нужно сделать дернуть урл "/err".

Результаты ab -c 50 -n 20000 в середине теста дернули "/err" на одном из подов:

Complete requests:      20000
Failed requests:        3719

Дело тут не в конкретном количестве ошибок (их количество будет меняться в зависимости от нагрузки), а в том, что они есть. В целом мы выкинули "плохой" под из балансировки, но в момент переключения клиент сервиса получал ошибки. Причину ошибок достаточно легко объяснить: readiness пробы выполняются kubelet раз в секунду + еще небольшое время на распространение информации о том, что под не ответил на пробу.

Поможет ли IPVS бэкенд для kube-proxy (experimental)?

На самом деле нет! Он решает задачу оптимизации проксирования, предлагает настраиваемый алгоритм балансировки, но никак не решит проблему обработки отказов.

Как быть

Данную проблему может решить только балансировщик, который умеет повторные попытки (retries). Другими словами, для http нам нужен L7 балансировщик. Такие балансировщики для kubernetes уже во всю используются либо в виде ingress (подразумевался как точка в хода в кластер, но по большому счету делает ровно то, что нужно), либо как реализацию отдельного слоя — service mesh, например istio.

У себя в production мы не стали пока использовать ни ingress, ни service mesh из-за дополнительной сложности. Подобные абстракции, на мой взгляд, помогают в тех случаях, когда нужно часто конфигурировать большое количество сервисов. Но при этом вы "платите" управляемостью и простой инфраструктуры. Вы будете тратить дополнительное время, чтобы понять, как настроить рертаи, таймауты для конкретного сервиса.

Как делаем мы

Мы используем headless сервисы k8s. У таких сервисов нет виртуального ip и соответственно в их работе kube-proxy и iptables не участвует. Для каждого такого сервиса можно получить список живых подов либо через DNS, либо через API.

Для приложений, которые взаимодействуют с другими сервисами мы делаем sidecar контейнер с envoy. Evoy периодически получает актуальных список подов для всех нужных сервисов через DNS, и самое главное умеет делать повторные попытки запросов на другие поды в случае ошибки. Можно запустить его в виде DaemonSet на каждой ноде, но тогда при отказе этого инстанса, перестали бы работать все приложения, которые его используют. Так как потребление ресурсов этим прокси достаточно небольшое, мы решили использовать его именно в варианте sidecar контейнера.

Это по сути ровно то, что делает istio, но в нашем случае баланс сместился в сторону простоты (не нужно изучать istio, нарываться на его баги). Возможно этот баланс изменится, и мы начнем использовать что-нибудь типа istio.

У нас в okmeter.io kubernetes определенно прижился, и мы верим в его дальнейшее распространение. Поддержка мониторинга k8s в нашем сервисе на подходе, следите за новостями!

Автор: NikolaySivko

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js