(Без)болезненный NGINX Ingress

в 6:00, , рубрики: devops, ingress, k8s, kubernentes, nginx, Блог компании Southbridge, Серверное администрирование, системное администрирование

(Без)болезненный NGINX Ingress - 1

Итак, у вас есть кластер Kubernetes, а для проброса внешнего трафика сервисам внутри кластера вы уже настроили Ingress-контроллер NGINX, ну, или пока только собираетесь это сделать. Класс!

Я тоже через это прошел, и поначалу все выглядело очень просто: установленный NGINX Ingress-контроллер был на расстоянии одного helm install. А затем оставалось лишь подвязать DNS к балансировщику нагрузки и создать необходимые Ingress-ресурсы.

Спустя несколько месяцев весь внешний трафик для всех окружений (dev, staging, production) направлялся через Ingress-серверы. И все было хорошо. А потом стало плохо.

Все мы отлично знаем, как это бывает: сначала вы заинтересовываетесь этой новой замечательной штукой, начинаете ей пользоваться, а затем начинаются неприятности.

Мой первый сбой в Ingress

Сперва позвольте предупредить: если вы еще не обеспокоены переполнениями очереди приема (accept queue overflows), начинайте беспокоиться.

(Без)болезненный NGINX Ingress - 2

Не забывайте об очередях

Случилось вот что: стоящее за NGINX приложение начало отвечать с большими задержками, что, в свою очередь, привело к заполнению NGINX listen backlog. Из-за этого NGINX начал сбрасывать соединения, в том числе и те, что пытался установить Kubernetes для проверки работоспособности сервиса (liveness/readiness probes).

А что происходит, когда под не отвечает на такие запросы? Kubernetes думает, что что-то пошло не так, и перезапускает его. Проблема в том, что это одна из тех ситуаций, когда перезапуск пода только усугубляет ситуацию: очередь приема продолжает переполняться, Kubernetes продолжает перезапускать поды, и в итоге их затягивает в водоворот падений и последующих рестартов.

(Без)болезненный NGINX Ingress - 3

Переполнения очереди TCP listen, согласно netstat

Какие уроки можно извлечь из этой ситуации?

  • Изучите конфигурацию своего NGINX до последней буквы. Выясните, что там должно быть и чего не должно. Не нужно слепо доверять настройкам по умолчанию.
  • Большинство Linux-дистрибутивов из коробки не настроены на работу в качестве высоконагруженных веб-серверов. Перепроверьте значения соответствующих параметров ядра с помощью sysctl -a.
  • Измеряйте задержку своих сервисов и устанавливайте соответствующие таймауты на основе ожидаемого максимального значения + запас на небольшие отклонения.
  • Настраивайте свои приложения таким образом, чтобы в случае перегрузки они начинали отклонять запросы или аккуратно снижать нагрузку. Например, в приложениях на NodeJS увеличения задержек в цикле сообщений могут означать, что сервер уже с трудом справляется с обработкой текущего трафика.
  • Используйте более одного NGINX Ingress-контроллера.

Важность мониторинга

Мой совет №0: никогда не запускайте в production Kubernetes-кластер (или что-то аналогичное), не настроив качественный мониторинг его работы. Сам по себе мониторинг от проблем не избавит, но собранная телеметрия значительно облегчает нахождение первопричин сбоев, что позволяет исправлять их в процессе дальнейшей работы.

(Без)болезненный NGINX Ingress - 4

Несколько полезных метрик из `nodenetstat`*

Если и вы поддались повальному увлечению Prometheus, для сбора метрик уровня ноды можете воспользоваться node_exporter. Это удобный инструмент, который позволяет выявлять в том числе и только что описанные проблемы.

(Без)болезненный NGINX Ingress - 5

Некоторые метрики, получаемые с NGINX Ingress-контроллера

NGINX Ingress-контроллер и сам способен генерировать метрики для Prometheus. Не забудьте наладить их сбор.

Знай свой конфиг

Красота Ingress-контроллера том, что вы можете положиться на эту замечательную программу в вопросе генерации и перезагрузки конфигурации прокси-сервера и больше по этому поводу не беспокоиться. Вам даже не обязательно быть знакомым с нижележащей технологией (NGINX в данном случае). Правда? Нет!

Если вы этого еще не сделали, обязательно посмотрите на сгенерированную для вас конфигурацию. Для NGINX Ingress-контроллера можно получить содержимое /etc/nginx/nginx.conf с помощью kubectl.

$ kubectl -n <namespace> exec <nginx-ingress-controller-pod-name> -- cat /etc/nginx/nginx.conf > ./nginx.conf

# $ cat ./nginx.conf
daemon off;

worker_processes auto;
pid /run/nginx.pid;

worker_rlimit_nofile 1047552;
worker_shutdown_timeout 10s ;

events {
    multi_accept        on;
    worker_connections  16384;
    use                 epoll;
}

http {
    real_ip_header      X-Forwarded-For;
    # ...
}

# ...

Теперь попробуйте найти что-нибудь несовместимое с вашей установкой. Хотите пример? Давайте начнем с worker_processes auto;

Оптимальное значение зависит от множества факторов, включая (но не ограничиваясь ими) число процессорных ядер, число жестких дисков с данными и картину нагрузок. Если затрудняетесь в выборе правильного значения, можно начать с установки его равным числу процессорных ядер (значение “auto” пытается определить его автоматически).

Первая проблема: в настоящий момент (будет ли это когда-либо исправлено?) NGINX ничего не знает о cgroups, а значит, в случае auto будет использовано значение количества физических ядер CPU хоста, а не количества «виртуальных» процессоров, как определено в Kubernetes resource requests/limits.

Проведем эксперимент. Что будет, если мы попробуем загрузить следующий файл конфигурации NGINX на двухъядерном сервере в контейнере, ограниченном только одним CPU? Сколько рабочих процессов будет запущено?

# $ cat ./minimal-nginx.conf

worker_processes auto;

events {
  worker_connections 1024;
}

http {
  server {
    listen 80;
    server_name localhost;

    location / {
      root  html;
      index index.html index.htm;
    }
  }
}

$ docker run --rm --cpus="1" -v `pwd`/minimal-nginx.conf:/etc/nginx/nginx.conf:ro -d nginx
fc7d98c412a9b90a217388a094de4c4810241be62c4f7501e59cc1c968434d4c

$ docker exec fc7 ps -ef | grep nginx
root         1     0  0 21:49 pts/0    00:00:00 nginx: master process nginx -g daemon off;
nginx        6     1  0 21:49 pts/0    00:00:00 nginx: worker process
nginx        7     1  0 21:49 pts/0    00:00:00 nginx: worker process

Таким образом, если вы планируете ограничить ресурсы процессора, которые доступны NGINX Ingress, не стоит позволять nginx создавать большое количество рабочих процессов в одном контейнере. Лучше всего явно указать их необходимое количество с помощью директивы worker_processes.

Теперь рассмотрим директиву listen. Значение параметра backlog явно не указано и по умолчанию для Linux равно 511. Если параметр ядра net.core.somaxconn равен, скажем, 1024, то backlog нужно присвоить соответствующее значение. Другими словами, убедитесь в том, что конфигурация nginx настроена с учетом параметров ядра.

Но на этом не останавливайтесь. Такое упражнение необходимо провести для каждой строчки сгенерированного файла конфигурации. Только посмотрите на все те параметры, которые позволяет поменять Ingress-контроллер. Исправляйте без колебаний все, что не подходит для вашего случая. Большинство параметров NGINX могут быть настроены с помощью записей и/или аннотаций ConfigMap.

Параметры ядра

С Ingress или без него, всегда проверяйте и настраивайте параметры нод в соответствии с ожидаемой нагрузкой.

Это достаточно сложная тема, поэтому я не планирую здесь подробно ее раскрывать. Дополнительные материалы по этом вопросу можно найти в разделе Ссылки.

Kube-Proxy: Таблица Conntrack

Тем, кто использует Kubernetes, думаю, не нужно объяснять, что такое Сервисы и для чего они предназначены. Однако полезно будет рассмотреть некоторые особенности их работы.

В каждой ноде Kubernetes-кластера выполняется kube-proxy, который отвечает за реализацию виртуального IP для Сервисов типа, отличного от ExternalName. В Kubernetes v1.0 прокси выполнялся исключительно в пространстве пользователя. В Kubernetes v1.1 был добавлен iptables-прокси, однако это не был режим по умолчанию. Начиная с Kubernetes v1.2, iptables-прокси используется по умолчанию.

Другими словами, отправленные на IP сервиса пакеты направляются (напрямую или через балансировщик) на соответствующий Endpoint (пары address:port подов, которые соответствуют label selector сервиса) с помощью правил iptables, управляемых kube-proxy. Соединения с IP-адресами сервиса отслеживаются ядром с помощью модуля nf_conntrack, и эта информация хранится в RAM.

Поскольку различные параметры conntrack должны быть согласованы друг с другом (например, nf_conntrack_maxw и nf_conntrack_buckets), kube-proxy, начиная работу, устанавливает разумные значения по умолчанию.

$ kubectl -n kube-system logs <some-kube-proxy-pod>
I0829 22:23:43.455969       1 server.go:478] Using iptables Proxier.
I0829 22:23:43.473356       1 server.go:513] Tearing down userspace rules.
I0829 22:23:43.498529       1 conntrack.go:98] Set sysctl 'net/netfilter/nf_conntrack_max' to 524288
I0829 22:23:43.498696       1 conntrack.go:52] Setting nf_conntrack_max to 524288
I0829 22:23:43.499167       1 conntrack.go:83] Setting conntrack hashsize to 131072
I0829 22:23:43.503607       1 conntrack.go:98] Set sysctl 'net/netfilter/nf_conntrack_tcp_timeout_established' to 86400
I0829 22:23:43.503718       1 conntrack.go:98] Set sysctl 'net/netfilter/nf_conntrack_tcp_timeout_close_wait' to 3600
I0829 22:23:43.504052       1 config.go:102] Starting endpoints config controller
...

Это неплохой вариант, но вам может потребоваться увеличить значения этих параметров, если мониторинг показывает, что у вас заканчивается место, выделенное для conntrack. Однако необходимо помнить, что увеличение значений этих параметров ведет к повышенному потреблению памяти, так что поаккуратнее там.

(Без)болезненный NGINX Ingress - 6

Мониторинг использования conntrack

Делиться (не) значит заботиться

До недавнего времени у нас был только один экземпляр NGINX Ingress, ответственный за проксирование запросов ко всем приложениям во всех окружениях (dev, staging, production). Я убедился на собственном опыте, что это плохая идея. Не складывайте все яйца в одну корзину.

Думаю, то же самое может быть сказано и по поводу использования одного кластера для всех окружений, однако мы выяснили, что такой подход приводит к более эффективному расходованию ресурсов. Мы запускали dev/staging-поды на уровне QoS с негарантированной доставкой (best-effort QoS tier), используя таким образом ресурсы, оставшиеся от production-приложений.

Обратной стороной медали здесь является то, что мы оказываемся ограничены в действиях, которые можно выполнять по отношению к кластеру. Например, если нам потребуется провести нагрузочное тестирование staging-сервиса, придется быть очень осторожными, чтобы не повлиять на боевые сервисы, запущенные в этом же кластере.

Несмотря на то что контейнеры дают в общем случае хороший уровень изоляции, они по-прежнему зависят от разделяемых ресурсов ядра, которые являются объектом злоупотреблений.

Одна установка Ingress на каждое окружение

Мы уже говорили о том, что нет причин, по которым не стоит использовать по одному Ingress-контроллеру на каждое окружение. Это дает дополнительный уровень защиты на тот случай, если с сервисами в dev и/или staging начнутся проблемы.

Некоторые преимущества этого подхода:

  • Появляется возможность использовать различные настройки для разных окружений.
  • Позволяет тестировать обновления Ingress перед применением их в production.
  • Можно избежать раздувания конфигурации NGINX записями множества вышестоящих серверов и сервисов, связанных с эфемерными и/или нестабильными окружениями.
  • Ускоряются перезагрузки конфигурации и сокращается количество этих событий в течение дня (позже мы обсудим, почему нужно стремиться к минимизации количества перезагрузок).

Ingress-классы в помощь

Одним из способов заставить разные ingress-контроллеры управлять разными Ingress-ресурсами является использование разных имен классов ingress для каждой отдельной установки ingress, а затем аннотирование Ingress-ресурсов с целью указать, кто кем должен управлять.

# Ingress controller 1
apiVersion: extensions/v1beta1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - args:
            - /nginx-ingress-controller
            - --ingress-class=class-1
            - ...

# Ingress controller 2
apiVersion: extensions/v1beta1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - args:
            - /nginx-ingress-controller
            - --ingress-class=class-2
            - ...

# This Ingress resource will be managed by controller 1
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: class-1
spec:
  rules: ...

# This Ingress resource will be managed by controller 2
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: class-2
spec:
  rules: ...

Проблемы с перезагрузками Ingress

К этому моменту у нас был запущен выделенный ingress-контроллер для production-окружения. Все было хорошо до тех пор, пока мы не решили перенести одно WebSocket-приложение на Kubernetes + ingress.

Скоро я заметил странную тенденцию в использовании памяти production-подами ingress.

(Без)болезненный NGINX Ingress - 7

Что здесь, черт побери, происходит?!

По какой причине потребление памяти так сильно выросло? С помощью kubectl exec я зашел в один из ingress-контейнеров и обнаружил группу рабочих процессов, висящих в статусе shutting down.

root     17755 17739  0 19:47 ?        00:00:00 /usr/bin/dumb-init /nginx-ingress-controller --default-backend-service=kube-system/broken-bronco-nginx-ingress-be --configmap=kube-system/broken-bronco-nginx-ingress-conf --ingress-class=nginx-ingress-prd
root     17765 17755  0 19:47 ?        00:00:08 /nginx-ingress-controller --default-backend-service=kube-system/broken-bronco-nginx-ingress-be --configmap=kube-system/broken-bronco-nginx-ingress-conf --ingress-class=nginx-ingress-prd
root     17776 17765  0 19:47 ?        00:00:00 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
nobody   18866 17776  0 19:49 ?        00:00:05 nginx: worker process is shutting down
nobody   19466 17776  0 19:51 ?        00:00:01 nginx: worker process is shutting down
nobody   19698 17776  0 19:51 ?        00:00:05 nginx: worker process is shutting down
nobody   20331 17776  0 19:53 ?        00:00:05 nginx: worker process is shutting down
nobody   20947 17776  0 19:54 ?        00:00:03 nginx: worker process is shutting down
nobody   21390 17776  1 19:55 ?        00:00:05 nginx: worker process is shutting down
nobody   22139 17776  0 19:57 ?        00:00:00 nginx: worker process is shutting down
nobody   22251 17776  0 19:57 ?        00:00:01 nginx: worker process is shutting down
nobody   22510 17776  0 19:58 ?        00:00:01 nginx: worker process is shutting down
nobody   22759 17776  0 19:58 ?        00:00:01 nginx: worker process is shutting down
nobody   23038 17776  1 19:59 ?        00:00:03 nginx: worker process is shutting down
nobody   23476 17776  1 20:00 ?        00:00:01 nginx: worker process is shutting down
nobody   23738 17776  1 20:00 ?        00:00:01 nginx: worker process is shutting down
nobody   24026 17776  2 20:01 ?        00:00:02 nginx: worker process is shutting down
nobody   24408 17776  4 20:01 ?        00:00:01 nginx: worker process

Чтобы понять, что произошло, надо сделать шаг назад и взглянуть на то, как в NGINX реализован процесс перезагрузки конфигурации.

Получив сигнал, главный процесс проверяет правильность синтаксиса нового конфигурационного файла и пытается применить конфигурацию, содержащуюся в нем. Если это ему удается, главный процесс запускает новые рабочие процессы и отправляет сообщения старым рабочим процессам с требованием завершиться. В противном случае, главный процесс откатывает изменения и продолжает работать со старой конфигурацией. Старые рабочие процессы, получив команду завершиться, прекращают принимать новые запросы и продолжают обслуживать текущие запросы до тех пор, пока все такие запросы не будут обслужены. После этого старые рабочие процессы завершаются.

Напомню, что мы проксируем WebSocket-соединения, которые по своей природе являются долгоживущими. WebSocket-соединение может поддерживаться часами, а то и днями, в зависимости от приложения. NGINX-сервер не знает, можно ли разорвать соединение во время перезагрузки, мы должны облегчить ему эту работу. (Например, можно применить стратегию принудительного завершения соединений, бездействующих определенное количество времени, как на клиенте, так и на сервере. Не оставляйте такие вещи на потом.)

Вернемся к нашей проблеме. Если у нас так много рабочих процессов в стадии завершения, значит, конфигурация ingress перезагружалась много раз, а рабочие процессы не могли завершить работу из-за долгоживущих соединений.

Так на самом деле и было. Мы выяснили, что NGINX Ingress-контроллер периодически генерировал различные файлы конфигурации по причине изменения очередности вышестоящих серверов и IP-адресов серверов.

I0810 23:14:47.866939       5 nginx.go:300] NGINX configuration diff
I0810 23:14:47.866963       5 nginx.go:301] --- /tmp/a072836772 2017-08-10 23:14:47.000000000 +0000
+++ /tmp/b304986035 2017-08-10 23:14:47.000000000 +0000
@@ -163,32 +163,26 @@

     proxy_ssl_session_reuse on;

-    upstream production-app-1-80 {
+    upstream upstream-default-backend {
         # Load balance algorithm; empty for round robin, which is the default
         least_conn;
-        server 10.2.71.14:3000 max_fails=0 fail_timeout=0;
-        server 10.2.32.22:3000 max_fails=0 fail_timeout=0;
+        server 10.2.157.13:8080 max_fails=0 fail_timeout=0;
     }

-    upstream production-app-2-80 {
+    upstream production-app-3-80 {
         # Load balance algorithm; empty for round robin, which is the default
         least_conn;
-        server 10.2.110.13:3000 max_fails=0 fail_timeout=0;
-        server 10.2.109.195:3000 max_fails=0 fail_timeout=0;
+        server 10.2.82.66:3000 max_fails=0 fail_timeout=0;
+        server 10.2.79.124:3000 max_fails=0 fail_timeout=0;
+        server 10.2.59.21:3000 max_fails=0 fail_timeout=0;
+        server 10.2.45.219:3000 max_fails=0 fail_timeout=0;
     }

     upstream production-app-4-80 {
         # Load balance algorithm; empty for round robin, which is the default
         least_conn;
-        server 10.2.109.177:3000 max_fails=0 fail_timeout=0;
         server 10.2.12.161:3000 max_fails=0 fail_timeout=0;
-    }
-
-    upstream production-app-5-80 {
-        # Load balance algorithm; empty for round robin, which is the default
-        least_conn;
-        server 10.2.21.37:9292 max_fails=0 fail_timeout=0;
-        server 10.2.65.105:9292 max_fails=0 fail_timeout=0;
+        server 10.2.109.177:3000 max_fails=0 fail_timeout=0;
     }

     upstream production-app-6-80 {
@@ -201,61 +195,67 @@
     upstream production-lap-production-80 {
         # Load balance algorithm; empty for round robin, which is the default
         least_conn;
-        server 10.2.45.223:8000 max_fails=0 fail_timeout=0;
+        server 10.2.21.36:8000 max_fails=0 fail_timeout=0;
         server 10.2.78.36:8000 max_fails=0 fail_timeout=0;
+        server 10.2.45.223:8000 max_fails=0 fail_timeout=0;
         server 10.2.99.151:8000 max_fails=0 fail_timeout=0;
-        server 10.2.21.36:8000 max_fails=0 fail_timeout=0;
     }

-    upstream production-app-7-80{
+    upstream production-app-1-80 {
         # Load balance algorithm; empty for round robin, which is the default
         least_conn;
-        server 10.2.79.126:3000 max_fails=0 fail_timeout=0;
-        server 10.2.35.105:3000 max_fails=0 fail_timeout=0;
-        server 10.2.114.143:3000 max_fails=0 fail_timeout=0;
-        server 10.2.50.44:3000 max_fails=0 fail_timeout=0;
-        server 10.2.149.135:3000 max_fails=0 fail_timeout=0;
-        server 10.2.45.155:3000 max_fails=0 fail_timeout=0;
+        server 10.2.71.14:3000 max_fails=0 fail_timeout=0;
+        server 10.2.32.22:3000 max_fails=0 fail_timeout=0;
     }

-    upstream production-app-8-80 {
+    upstream production-app-2-80 {
         # Load balance algorithm; empty for round robin, which is the default
         least_conn;
-        server 10.2.53.23:5000 max_fails=0 fail_timeout=0;
-        server 10.2.110.22:5000 max_fails=0 fail_timeout=0;
-        server 10.2.35.91:5000 max_fails=0 fail_timeout=0;
-        server 10.2.45.221:5000 max_fails=0 fail_timeout=0;
+        server 10.2.110.13:3000 max_fails=0 fail_timeout=0;
+        server 10.2.109.195:3000 max_fails=0 fail_timeout=0;
     }

-    upstream upstream-default-backend {
+    upstream production-app-9-80 {
         # Load balance algorithm; empty for round robin, which is the default
         least_conn;
-        server 10.2.157.13:8080 max_fails=0 fail_timeout=0;
+        server 10.2.78.26:3000 max_fails=0 fail_timeout=0;
+        server 10.2.59.22:3000 max_fails=0 fail_timeout=0;
+        server 10.2.96.249:3000 max_fails=0 fail_timeout=0;
+        server 10.2.32.21:3000 max_fails=0 fail_timeout=0;
+        server 10.2.114.177:3000 max_fails=0 fail_timeout=0;
+        server 10.2.83.20:3000 max_fails=0 fail_timeout=0;
+        server 10.2.118.111:3000 max_fails=0 fail_timeout=0;
+        server 10.2.26.23:3000 max_fails=0 fail_timeout=0;
+        server 10.2.35.150:3000 max_fails=0 fail_timeout=0;
+        server 10.2.79.125:3000 max_fails=0 fail_timeout=0;
+        server 10.2.157.165:3000 max_fails=0 fail_timeout=0;
     }

-    upstream production-app-3-80 {
+    upstream production-app-5-80 {
         # Load balance algorithm; empty for round robin, which is the default
         least_conn;
-        server 10.2.79.124:3000 max_fails=0 fail_timeout=0;
-        server 10.2.82.66:3000 max_fails=0 fail_timeout=0;
-        server 10.2.45.219:3000 max_fails=0 fail_timeout=0;
-        server 10.2.59.21:3000 max_fails=0 fail_timeout=0;
+        server 10.2.21.37:9292 max_fails=0 fail_timeout=0;
+        server 10.2.65.105:9292 max_fails=0 fail_timeout=0;
     }

-    upstream production-app-9-80 {
+    upstream production-app-7-80 {
         # Load balance algorithm; empty for round robin, which is the default
         least_conn;
-        server 10.2.96.249:3000 max_fails=0 fail_timeout=0;
-        server 10.2.157.165:3000 max_fails=0 fail_timeout=0;
-        server 10.2.114.177:3000 max_fails=0 fail_timeout=0;
-        server 10.2.118.111:3000 max_fails=0 fail_timeout=0;
-        server 10.2.79.125:3000 max_fails=0 fail_timeout=0;
-        server 10.2.78.26:3000 max_fails=0 fail_timeout=0;
-        server 10.2.59.22:3000 max_fails=0 fail_timeout=0;
-        server 10.2.35.150:3000 max_fails=0 fail_timeout=0;
-        server 10.2.32.21:3000 max_fails=0 fail_timeout=0;
-        server 10.2.83.20:3000 max_fails=0 fail_timeout=0;
-        server 10.2.26.23:3000 max_fails=0 fail_timeout=0;
+        server 10.2.114.143:3000 max_fails=0 fail_timeout=0;
+        server 10.2.79.126:3000 max_fails=0 fail_timeout=0;
+        server 10.2.45.155:3000 max_fails=0 fail_timeout=0;
+        server 10.2.35.105:3000 max_fails=0 fail_timeout=0;
+        server 10.2.50.44:3000 max_fails=0 fail_timeout=0;
+        server 10.2.149.135:3000 max_fails=0 fail_timeout=0;
+    }
+
+    upstream production-app-8-80 {
+        # Load balance algorithm; empty for round robin, which is the default
+        least_conn;
+        server 10.2.53.23:5000 max_fails=0 fail_timeout=0;
+        server 10.2.45.221:5000 max_fails=0 fail_timeout=0;
+        server 10.2.35.91:5000 max_fails=0 fail_timeout=0;
+        server 10.2.110.22:5000 max_fails=0 fail_timeout=0;
     }

     server {

По этой причине NGINX Ingress-контроллер перезагружал конфигурацию несколько раз в минуту, забивая память завершающимися рабочими процессами до тех пор, пока под не становился жертвой OOM killer.

После того как я обновил NGINX Ingress-контроллер на версию с исправлением и указал параметр командной строки --sort-backends=true, дела пошли на поправку.

(Без)болезненный NGINX Ingress - 8

Количество ненужных перезагрузок после установки исправленной версии упало до нуля.

Спасибо @aledbf за помощь в нахождении и исправлении этой ошибки!

Продолжаем минимизировать перезагрузки конфигурации

Важно помнить, что перезагрузки конфигурации — операции затратная, и их лучше избегать, особенно при работе с WebSocket-соединениями. По этой причине мы решили установить отдельный Ingress-контроллер специально для долгоживущих соединений.

В нашем случае изменения в WebSocket-приложениях происходят намного реже, чем во всех остальных, и отдельный контроллер позволяет нам не трогать эти долгоживущие соединения лишний раз.

Отдельная установка также позволяет нам по-разному настраивать Ingress-контроллеры, оптимизируя их под тип обслуживаемых соединений.

Тонкая настройка автомасштабирования подов

Поскольку в NGINX Ingress вышестоящие серверы прописываются в виде IP-адресов подов, каждый раз при изменении списка конечных точек сервиса Ingress-конфигурация генерируется заново и перезагружается. Поэтому если при нормальной нагрузке часто происходят автомасштабирования (autoscaling), может понадобиться изменение HorizontalPodAutoscalers.

(Без)болезненный NGINX Ingress - 9

Horizontal pod autoscaler в работе на пиковых нагрузках

Еще одна вещь, которую люди часто не осознают, — это то, что у horizontal pod autoscaler есть специальный таймер, который не позволяет масштабироваться несколько раз в течение короткого времени.

Name:                                                   <app>
Namespace:                                              production
Labels:                                                 <none>
Annotations:                                            <none>
CreationTimestamp:                                      Fri, 23 Jun 2017 11:41:59 -0300
Reference:                                              Deployment/<app>
Metrics:                                                ( current / target )
  resource cpu on pods  (as a percentage of request):   46% (369m) / 60%
Min replicas:                                           8
Max replicas:                                           20
Conditions:
  Type                  Status  Reason                  Message
  ----                  ------  ------                  -------
  AbleToScale           False   BackoffBoth             the time since the previous scale is still within both the downscale and upscale forbidden windows
  ScalingActive         True    ValidMetricFound        the HPA was able to succesfully calculate a replica count from cpu resource utilization (percentage of request)
  ScalingLimited        True    TooFewReplicas          the desired replica count was less than the minimum replica count
Events:
  FirstSeen     LastSeen        Count   From                            SubObjectPath   Type            Reason                  Message
  ---------     --------        -----   ----                            -------------   --------        ------                  -------
  14d           10m             39      horizontal-pod-autoscaler                       Normal          SuccessfulRescale       New size: 10; reason: cpu resource utilization (percentage of request) above target
  14d           3m              69      horizontal-pod-autoscaler                       Normal          SuccessfulRescale       New size: 8; reason: All metrics below target

Согласно значению по умолчанию для флага --horizontal-pod-autoscaler-upscale-delay в kube-controller-manager минимальный интервал между событиями масштабирования приложения равняется 3 минутам.

Таким образом, если нагрузка на ваше приложение действительно сильно возросла, может потребовать примерно 4 минуты (3 минуты задержки autoscaler + примерно 1 минута на синхронизацию метрик), прежде чем autscaler отреагирует на это, чего может оказаться вполне достаточно для серьезного ухудшения работы сервиса.

Ваше мнение?

Можете что-нибудь посоветовать? Буду рад!

Ссылки

Автор: Игорь Олемской

Источник

Поделиться

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