В PayPal мы недавно начали тестировать возможности Kubernetes. Большинство наших рабочих нагрузок выполняются на Apache Mesos, и в рамках этой миграции нам нужно было понять несколько аспектов производительности кластеров, на которых работает Kubernetes, со специфичной для PayPal плоскостью управления. Главным среди этих аспектов является понимание масштабируемости платформы, а также определение возможностей для улучшения путем настройки кластера.

В отличие от Apache Mesos, который может масштабироваться до 10 000 узлов из коробки, масштабирование Kubernetes является сложной задачей. Масштабируемость Kubernetes ограничивается не только количеством узлов и модулей, но и несколькими аспектами, такими как количество созданных ресурсов, количество контейнеров на модуль, общее количество сервисов и пропускная способность развертывания модуля. В этом посте описаны некоторые проблемы, с которыми мы столкнулись при масштабировании, и то, как мы их решили.

Топология кластера

У нас есть кластеры разного размера в производстве, охватывающие тысячи узлов. Наша установка состояла из трех главных узлов и внешнего кластера etcd из трех узлов, работающих на Google Cloud Platform (GCP). Плоскость управления была размещена за балансировщиком нагрузки, и все узлы данных принадлежали к той же зоне, что и плоскость управления.

Нагрузка

Для тестирования производительности мы использовали генератор рабочей нагрузки с открытым исходным кодом k-bench, модифицированный для наших случаев использования. Объектами ресурсов, которые мы использовали, были простые модули и развертывания. Мы развертывали их как пакетами, так и последовательно с различными размерами пакетов и временем между развертываниями.

Шкала

Мы начали с небольшого количества модулей и небольшого количества узлов. В ходе стресс-тестирования мы обнаружили возможности для улучшения и продолжили масштабирование кластера, поскольку наблюдали повышение производительности. Каждый рабочий узел имел четыре ядра ЦП и мог содержать до 40 модулей. Мы масштабировались примерно до 4100 узлов. Приложение, использованное для эталонного тестирования, представляло собой службу без сохранения состояния, работающую на 100 миллиядрах с гарантированным качеством обслуживания (QoS).

Мы начали с 2000 стручков на 1000 узлов, затем 16 000 стручков, а затем 32 000 стручков. После этого мы подскочили до 150 000 стручков на 4100 узлах, а затем до 200 000 стручков. Нам пришлось увеличить количество ядер на каждом узле, чтобы разместить больше модулей.

API-сервер

Сервер API оказался узким местом, когда несколько подключений к серверу API вернули 504 тайм-аута шлюза в дополнение к регулированию на уровне локального клиента (экспоненциальная отсрочка). Они увеличивались в геометрической прогрессии во время разгона:

I0504 17:54:55.731559       1 request.go:655] Throttling request took 1.005397106s, request: POST:https://<>:443/api/v1/namespaces/kbench-deployment-namespace-14/pods..
I0504 17:55:05.741655       1 request.go:655] Throttling request took 7.38390786s, request: POST:https://<>:443/api/v1/namespaces/kbench-deployment-namespace-13/pods..
I0504 17:55:15.749891       1 request.go:655] Throttling request took 13.522138087s, request: POST:https://<>:443/api/v1/namespaces/kbench-deployment-namespace-13/pods..
I0504 17:55:25.759662       1 request.go:655] Throttling request took 19.202229311s, request: POST:https://<>:443/api/v1/namespaces/kbench-deployment-namespace-20/pods..
I0504 17:55:35.760088       1 request.go:655] Throttling request took 25.409325008s, request: POST:https://<>:443/api/v1/namespaces/kbench-deployment-namespace-13/pods..
I0504 17:55:45.769922       1 request.go:655] Throttling request took 31.613720059s, request: POST:https://<>:443/api/v1/namespaces/kbench-deployment-namespace-6/pods..

Размер очереди, которая управляет ограничением скорости на сервере API, был обновлен с помощью max-mutating-requests-inflight и max-requests-inflight. Эти два флага на сервере API определяют, как функция Приоритет и справедливость, представленная в виде бета-версии в версии 1.20, распределяет общий размер очереди между своими классами очередей. Например, запросы на выборы лидера имеют приоритет над запросами pod. В рамках каждого приоритета существует справедливость с настраиваемыми очередями. В будущем есть возможность для дальнейшей тонкой настройки с помощью объектов API PriorityLevelConfiguration и FlowSchema.

Менеджер контроллера

Controller Manager отвечает за предоставление контроллеров для собственных ресурсов, таких как набор реплик, пространства имен и т. д., с большим количеством развертываний (которые управляются наборами реплик). Скорость, с которой менеджер контроллера мог синхронизировать свое состояние с сервером API, была ограничена. Для настройки этого поведения использовались несколько ручек:

  • kube-api-qps —количество запросов, которые диспетчер контроллера может отправить на сервер API в данную секунду.
  • kube-api-burst — пакет диспетчера контроллера, который представляет собой дополнительные одновременные вызовы над kube-api-qps.
  • concurrent-deployment-syncs — Параллелизм в вызове синхронизации для таких объектов, как развертывание, набор реплик и т. д.

Планировщик

При независимом тестировании в качестве независимого компонента планировщик может поддерживать высокую пропускную способность до 1000 pod в секунду. Однако при развертывании планировщика в действующем кластере мы заметили реальное снижение пропускной способности. Медленный экземпляр etcd вызывал увеличение задержки привязки для планировщика, что приводило к увеличению размера очереди ожидания порядка тысяч pod. Идея заключалась в том, чтобы во время тестовых прогонов это число не превышало 100, поскольку более высокое значение влияет на задержку запуска модуля. В итоге мы также настроили параметры выбора лидера, чтобы они были устойчивы к ложным перезапускам недолговечных сетевых разделов или к перегрузке сети.

и т. д.

etcd — самая важная часть кластера Kubernetes. Это видно из множества проблем, которые etcd может вызвать во всем кластере, проявляющихся по-разному. Требовалось тщательное исследование, чтобы выявить основные причины и масштабировать etcd, чтобы справиться с предполагаемым масштабом.

При наращивании многие предложения Raft начали терпеть неудачу:

В результате исследования и анализа мы определили, что GCP ограничивает пропускную способность диска PD-SSD примерно до 100 МБ в секунду (как показано ниже) на нашем диске размером 100 ГБ. GCP не дает возможности увеличить лимит пропускной способности — он только увеличивается с размером диска. Несмотря на то, что узел etcd занимает всего ‹ 10 ГБ пространства, мы сначала попробовали его с 1 ТБ PD-SSD. Однако даже большой диск стал узким местом, когда все 4k узлов одновременно присоединились к плоскости управления Kubernetes. Мы решили использовать локальный SSD, у которого очень высокая пропускная способность за счет чуть более высокой вероятности потери данных в случае сбоев, так как он не постоянный.

После перехода на локальный SSD мы не увидели ожидаемой производительности от самого быстрого SSD. Некоторые бенчмарки делались прямо на диске с FIO и цифры там были ожидаемые. Однако тесты etcd показали другую картину для одновременной записи всех участников:

LOCAL SSD
Summary:
 Total: 8.1841 secs.
 Slowest: 0.5171 secs.
 Fastest: 0.0332 secs.
 Average: 0.0815 secs.
 Stddev: 0.0259 secs.
 Requests/sec: 12218.8374

PD SSD
Summary:
 Total: 4.6773 secs.
 Slowest: 0.3412 secs.
 Fastest: 0.0249 secs.
 Average: 0.0464 secs.
 Stddev: 0.0187 secs.
 Requests/sec: 21379.7235

Локальный SSD показал себя хуже! После более глубокого расследования это было связано с фиксацией кэша барьера записи файловой системы ext4. Поскольку etcd использует ведение журнала с упреждающей записью и вызывает fsync каждый раз, когда фиксирует в журнале raft, можно отключить барьер записи. Кроме того, у нас есть задания резервного копирования БД на уровне файловой системы и на уровне приложений для аварийного восстановления. После этого изменения показатели с локальным SSD улучшились по сравнению с PD-SSD:

LOCAL SSD
Summary:
  Total: 4.1823 secs.
  Slowest: 0.2182 secs.
  Fastest: 0.0266 secs.
  Average: 0.0416 secs.
  Stddev: 0.0153 secs.
  Requests/sec: 23910.3658

Эффект от этого улучшения был виден в длительности синхронизации ведения журнала с опережающей записью (WAL) etcd и задержках серверной фиксации, которые уменьшились более чем на 90% примерно на отметке времени 15:55, как показано ниже:

Размер базы данных MVCC по умолчанию в etcd составляет 2 ГБ. Это было увеличено до максимума 8 ГБ после срабатывания аварийных сигналов о нехватке места в БД. Используя около ~60% этой БД, мы смогли масштабировать до 200 000 pod без сохранения состояния.

Со всеми вышеупомянутыми оптимизациями кластер был намного более стабильным в предполагаемом масштабе, однако мы сильно отставали в SLI для задержек API.

Сервер etcd по-прежнему время от времени перезагружается, и всего один перезапуск может испортить результаты тестов, особенно числа P99. При ближайшем рассмотрении выяснилось, что для версии 1.20 в YAML есть баг в etcd живости. Чтобы исправить это, был применен обходной путь путем увеличения счетчика порога отказа.

После исчерпания всех способов масштабирования etcd по вертикали, в основном с точки зрения ресурсов (процессор, память, диск), мы обнаружили, что запросы диапазона влияют на производительность etcd. Etcd не работает хорошо, когда есть много запросов диапазона, и записи в журнал Raft страдают, тем самым замедляя задержки кластера. Ниже указано количество запросов диапазона на ресурс Kubernetes, которые влияют на производительность в одном из тестовых прогонов:

etcd$ sudo grep -ir "events" 0.log.20210525-035918 | wc -l
130830
etcd$ sudo grep -ir "pods" 0.log.20210525-035918 | wc -l
107737
etcd$ sudo grep -ir "configmap" 0.log.20210525-035918 | wc -l
86274
etcd$ sudo grep -ir "deployments" 0.log.20210525-035918 | wc -l
6755
etcd$ sudo grep -ir "leases" 0.log.20210525-035918 | wc -l
4853
etcd$ sudo grep -ir "nodes" 0.log.20210525-035918 | wc -l

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

Полученные результаты

После оптимизации и настройки различных компонентов Kubernetes мы заметили значительное улучшение задержек. На следующих диаграммах показан прирост производительности, достигнутый с течением времени для достижения SLO. Рабочая нагрузка здесь составляет всего 150 тыс. модулей с 250 репликами на развертывание на 10 одновременных рабочих процессах. Пока задержки P99 для запуска пода не превышают пяти секунд в соответствии с SLO Kubernetes, все в порядке.

На следующей диаграмме показаны задержки вызовов API в пределах SLO при 200 тыс. модулей в кластере.

Мы также добились задержки запуска модуля P99 около пяти секунд для модулей 200 000 при гораздо более высокой скорости развертывания модулей, чем то, что тесты K8s для узлов 5 000 заявляют при 3000 модулей/мин.

Заключение

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