Расскажу, как мы в 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 (шлюз не отвечает), а одновременно ситуация осложнялась дросселированием API на уровне локального клиента (экспоненциальная выдержка).
Дросселирование при таких операциях наращивания увеличивалось экспоненциально:
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 управляют тем, как именно вводится возможность равноправного доступа к API (Priority and Fairness), определённая как бета-версия в релизе 1.20. Эта возможность позволяет разделить имеющуюся очередь на более мелкие очереди по классам. Например, запросы на выбор лидера получают приоритет над запросами, касающимися подов. В рамках любого приоритета конфигурируемые очереди обслуживаются равноправно. Возможно, в будущем появится пространство и для более тонкой настройки при помощи объектов API PriorityLevelConfiguration и FlowSchema.
❯ Менеджер контроллеров
Менеджер контроллеров отвечает за предоставление контроллеров для предоставления нативных ресурсов, таких, как набор реплик (Replica Set), пространств имён (Namespaces), т.д., при наличии большого количества развёрнутых инстансов (этими инстансами управляют наборы реплик). Скорость, с которой менеджер контроллеров может синхронизировать собственное состояние с сервером API – ограничена. Для настройки такого поведения использовались различные рычаги:
- kube-api-qps — количество запросов, которые менеджер контроллеров может направить серверу API за данную секунду.
- kube-api-burst— разгонный механизм для менеджера контроллеров, обеспечивающий дополнительные конкурентные вызовы сверх kube-api-qps.
- concurrent-deployment-syncs — конкурентность при синхронизации вызовов, направляемых к объектам – например, к развёрнутым инстансам, наборам реплик, т.д.
❯ Планировщик
Когда планировщик независимо тестируется на независимом компоненте, он может поддерживать высокую пропускную способность – до 1000 подов в секунду. Однако, как только планировщик развёрнут в действующем кластере, отмечалось заметное снижение реальной производительности. Медленный инстанс etcd вызывал повышение задержки связывания у планировщика, что приводило к увеличению размера необработанной очереди, порядка тысяч подов. При тестовых прогонах мы пытались добиться, чтобы это количество не превышало 100. Чем выше данное количество подов, тем выше задержка при запуске отдельных подов. Кроме того, нам удалось настроить параметры, действующие при выборе лидера, благодаря чему система приобрела устойчивость к мнимым перезапускам короткоживущих сетевых сегментов или к перегрузке сети.
❯ etcd
etcd – это наиважнейшая часть кластера Kubernetes. Это очевидно хотя бы из того, сколько разнообразных проблем etcd может спровоцировать в рамках всего кластера, и насколько по-разному они проявляются. Требуется тщательно исследовать симптомы, чтобы выявлять коренные причины и масштабировать etcd так, чтобы она справлялась с актуальными для нас нагрузками.
При наращивании нагрузок переставали выполняться Raft-предложения, одно за другим:
Расследовав и проанализировав эти проблемы, мы обнаружили, что GCP ограничивает пропускную способность диска PD-SSD примерно 100 МиБиБ в секунду (как показано ниже) при работе с диском, имеющим размер 100 ГБ. В GCP не предусмотрен способ вручную увеличить пороговую пропускную способность — она возрастает только вместе с размером диска. Хотя, на узел etcd затрачивается < 10 ГБ дискового пространства, сначала мы попробовали диск PD-SSD на один терабайт. Но даже сравнительно крупный диск превращался в узкое место, когда все 4000 узлов одновременно подключались к плоскости управления Kubernetes. Мы решили воспользоваться локальным 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
Это улучшение явно прослеживается в том, что в etcd значительно быстрее стала проходить синхронизация логирования, опережающего запись (WAL) и сократились задержки при записи коммитов на бэкенде. Длительность этих процессов сократилась более чем на 90% и теперь находится на отметке около 15:55, как показано ниже:
По умолчанию размер базы данных MVCC в etcd равен 2 ГБ. Его можно максимально нарастить до 8 ГБ, если отключить тревожные оповещения о расходе дискового пространства под базу данных. Задействовав примерно 60% этой базы данных, мы смогли отмасштабироваться до 200k подов, не сохраняющих состояния.
После всех вышеперечисленных оптимизаций кластер интересующего нас масштаба стал гораздо стабильнее, но мы далеко не выполняли заданные SLI (индикаторы уровня обслуживания) для задержек API.
Иногда сервер etcd спонтанно перезагружается, а всего из-за одной такой перезагрузки можно испортить результаты бенчмаркинга, и особенно числа по P99. Более тщательный анализ показал, что в версии etcd 1.20 есть баг в работе разметки YAML, проявляющийся при проверке работоспособности (liveness probe). Мы так вышли из ситуации: увеличили порог чувствительности отказов.
Исчерпав все варианты вертикального масштабирования etcd, в основном, связанные с грамотным использованием ресурсов (ЦП, память, диск), мы обнаружили, что на производительности etcd сказываются запросы с поиском по диапазону значений (range queries). Etcd начинает испытывать проблемы с производительностью, когда таких запросов много, а также страдают записи в raft-log, и из-за этого задержки в кластерах увеличиваются. Далее показано, сколько запросов к диапазону значений в пересчёте на ресурс 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 по виду событий, стабильность кластера сразу улучшилась, особенно в случаях высокой конкуренции за поды. В дальнейшем можно ещё детальнее шардировать кластер, на этот раз по ресурсам подов. Легко сконфигурировать сервер API так, чтобы он сам обращался к etcd по поводу взаимодействия с шардированным ресурсом.
❯ Результаты
Оптимизировав и настроив различные компоненты Kubernetes, мы добились огромного прогресса в сокращении задержек. На следующих графиках показано, какой выигрыш в производительности мы со временем наработали с целью соответствия уровням качества обслуживания SLO. Общая рабочая нагрузка здесь составляет 150k подов с 250 репликами на каждый развёрнутый инстанс при одновременном конкурентном выполнении 10 рабочих потоков. Пока задержки P99 при запуске пода не превышают пяти секунд, можно считать, что с SLO мы справляемся.
На следующей схеме показаны задержки при вызовах API, которые также хорошо укладываются в SLO, когда в кластере развёрнуто 200k подов.
Также нам удалось уложиться примерно в пятисекундную задержку (P99) при 200k развёрнутых подов, но при значительно более высоком темпе их развёртывания, чем заявлено в тестах K8s на 5k узлов, когда, якобы, развёртывание идёт со скоростью 3000 подов в минуту.
❯ Заключение
Kubernetes – сложная система, и при работе с ним требуется глубокое понимание плоскости управления. Только так можно грамотно масштабировать каждый из компонентов. Этот опыт многому нас научил, и пока мы продолжаем оптимизировать наши кластеры.