Масштабируем Kubernetes до 4000+ нод и 200 000 подов

Kate

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


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

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


Мы используем кластеры разного размера, охватывающие тысячи нод. Конфигурация кластера состояла из трех мастеров и внешнего кластера etcd из трех узлов, все они работали на Google Cloud Platform (GCP). Control plane размещалась за балансировщиком нагрузки, и все узлы данных принадлежали к той же зоне, что и control plane.


Нагрузка​


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


Масштаб​


Мы начали с небольшого количества подов и небольшого количества нод. Благодаря стресс-тестам мы нашли возможности для улучшения и продолжили расширять кластер, наблюдая за улучшением производительности. Каждая воркер нода имела четыре ядра и поддерживала запуск максимум 40 подов. Мы масштабировались примерно до 4 100 нод. Приложение, используемое для сравнительного анализа, представляло собой stateless сервис, которому выделено 0.1 ядра CPU гарантированных ресурсов (QoS).


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


API сервер​


Сервер API оказался узким местом, когда несколько подключений к серверу API вернули ошибку 504 (gateway timeouts), кроме того, при каждой повторной отправке запроса на уровне локального клиента добавляются дополнительные задержки (прим. переводчика: подробнее об алгоритмах лимитирования повторных исходящих сетевых запросов можно прочитать в статье Экспоненциальная выдержка или exponential backoff). Задержки, добавляемые этими ограничениями, росли экспоненциально по мере развёртывания:


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..

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


Controller Manager​


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


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

Шедулер​


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


etcd​


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


При увеличении нагрузки, большое количество Raft proposals стали завершаться ошибками:


image



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


image



После перехода на локальный SSD-накопитель, мы не увидели ожидаемой от самого быстрого SSD накопителя производительности. Некоторые тесты были сделаны непосредственно на диске с помощью утилиты FIO, и цифры там были ожидаемыми. Однако тесты etcd показали другую картину для одновременных записей всех участников кластера 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 использует журнал с упреждением записи (write-ahead logging) и вызывает fsync каждый раз, когда пишет в журнал raft, можно отключить это ограничение записи. Кроме того, у нас есть задания резервного копирования БД на уровне файловой системы и на уровне приложений для катастрофоустойчивости (DR). После этого изменения показатели с локальным твердотельным накопителем улучшились по сравнению с показателями 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 sync) etcd и задержек подтверждения записи в backend, которые уменьшились более чем на 90% около временной отметки 15:55, как показано ниже:


image



image



Размер базы данных MVCC (multiversion concurrency control) в etcd по умолчанию составляет 2 ГБ. Этот объем был увеличен до максимума в 8 ГБ после отключения предупреждений о недостаточном количестве места в DB (DB out of space). При использовании около ~ 60% этой базы данных мы смогли масштабироваться до 200 000 stateless подов.


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


Сервер etcd все равно перезапускался время от времени, и всего один перезапуск мог испортить результаты нагрузочного тестирования, особенно значения P99. При более детальном рассмотрении выяснилось, что существует баг liveness probe в YAML манифесте etcd версии v1.20. Для устранения этой проблемы было применено обходное решение – увеличение порогового значения отказов.


После того как мы исчерпали все возможности вертикального масштабирования 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 backend. После выделения ресурса events в отдельный сегмент etcd (прим. переводчика: это очень хороший трюк, который позволяет преодолеть проблемы в производительности etcd для высоконагруженных кластеров. Обратите внимание на порядок значений для events, pods и configmap по отношению к другим ресурсам), мы увидели улучшение стабильности кластера при большом количестве одновременно запускаемых подов. В будущем есть возможность для дальнейшей сегментации кластера etcd для ресурса pods. Сервер API можно легко сконфигурировать для обращения к соответствующему инстансу etcd для взаимодействия с сегментированным ресурсом.


Результаты​


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


image



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


Мы также достигли значения P99 около пяти секунд для задержек запуска подов при 200 000 подах при гораздо более высокой скорости развёртывания, чем в тестах K8s для 5 000 нод при 3 000 подов в минуту.
image



Заключение​


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

 
Сверху