Фото Todd Diemer, Unsplash.com В 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 вернули ошибку 504 (gateway timeouts), кроме того, при каждой повторной отправке запроса на уровне локального клиента добавляются дополнительные задержки (прим. переводчика: подробнее об алгоритмах лимитирования повторных исходящих сетевых запросов можно прочитать в статье Экспоненциальная выдержка или exponential backoff). Задержки, добавляемые этими ограничениями, росли экспоненциально по мере развёртывания: Размер очереди, которая управляет ограничениями (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 может синхронизировать своё состояние с сервером API, была ограничена. Для оптимизации этих ограничений использовалось несколько параметров: При независимом тестировании в качестве независимого компонента шедулер может поддерживать высокую пропускную способность до 1 000 подов в секунду. Однако при развёртывании шедулера в реальном кластере мы заметили снижение пропускной способности. Медленный экземпляр etcd привел к увеличению задержек процесса binding для шедулера, что повлекло за собой увеличение длины очереди ожидания до нескольких тысяч подов. Идея состояла в том, чтобы во время тестовых запусков это число было меньше 100, так как большее количество влияет на задержки при запуске подов. Мы также выполнили настройку параметров выбора лидера для устойчивости к ложным перезапускам при кратковременной сетевой изоляции или перегрузкам в сети. etcd является, пожалуй, наиболее важной частью кластера Kubernetes. Это видно из множества проблем, которые могут возникнуть по всему кластеру и проявить себя различными способами. Потребовалось тщательное расследование, чтобы выявить первопричины и масштабировать etcd под наши нужды. При увеличении нагрузки, большое количество Raft proposals стали завершаться ошибками:  Проведя расследование и анализ, мы определили, что GCP снизила пропускную способность диска PD-SSD примерно до 100 Мб в секунду (как показано ниже) при нашем размере диска 100 ГБ. GCP не предоставляет способа увеличить ограничение пропускной способности — оно увеличивается только с размером диска. Несмотря на то, что нода etcd занимает < 10 ГБ места, мы сначала попробовали использовать 1 ТБ PD-SSD. Однако даже больший диск стал узким местом, когда все 4 000 нод одновременно подключились к Kubernetes control plane. Мы решили использовать локальный SSD-накопитель, который обладает очень высокой пропускной способностью за счет чуть более высокой вероятности потери данных в случае сбоев, поскольку сохранение данных на нём не гарантировано.  После перехода на локальный SSD-накопитель, мы не увидели ожидаемой от самого быстрого SSD накопителя производительности. Некоторые тесты были сделаны непосредственно на диске с помощью утилиты FIO, и цифры там были ожидаемыми. Однако тесты etcd показали другую картину для одновременных записей всех участников кластера etcd: Локальный SSD-накопитель работал хуже! После более тщательного расследования выяснилось, что это было связано с ограничением записи кэша файловой системы ext4. Поскольку etcd использует журнал с упреждением записи (write-ahead logging) и вызывает fsync каждый раз, когда пишет в журнал raft, можно отключить это ограничение записи. Кроме того, у нас есть задания резервного копирования БД на уровне файловой системы и на уровне приложений для катастрофоустойчивости (DR). После этого изменения показатели с локальным твердотельным накопителем улучшились по сравнению с показателями PD-SSD: Эффект этого улучшения выразился в продолжительности синхронизации журнала с упреждением записи (WAL sync) etcd и задержек подтверждения записи в backend, которые уменьшились более чем на 90% около временной отметки 15:55, как показано ниже:   Размер базы данных 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 backend. После выделения ресурса events в отдельный сегмент etcd (прим. переводчика: это очень хороший трюк, который позволяет преодолеть проблемы в производительности etcd для высоконагруженных кластеров. Обратите внимание на порядок значений для events, pods и configmap по отношению к другим ресурсам), мы увидели улучшение стабильности кластера при большом количестве одновременно запускаемых подов. В будущем есть возможность для дальнейшей сегментации кластера etcd для ресурса pods. Сервер API можно легко сконфигурировать для обращения к соответствующему инстансу etcd для взаимодействия с сегментированным ресурсом. После оптимизации и настройки различных компонентов Kubernetes мы наблюдали значительное уменьшение задержек. Следующие диаграммы демонстрируют прирост производительности, достигнутый с течением времени для достижения SLO. Рабочая нагрузка здесь составляет в общей сложности 150 000 подов с 250 репликами на деплоймент на десяти параллельно запущенных воркерах. Пока задержки P99 для запуска подов находятся в пределах пяти секунд, согласно Kubernetes SLOs, у нас все хорошо.  На диаграмме ниже показаны задержки вызовов API в пределах SLO при 200 000 подах в кластере.  Kubernetes — это сложная система, и для того, чтобы знать, как масштабировать каждый компонент, необходимо глубокое понимание control plane. Мы многому научились благодаря этому опыту и будем продолжать оптимизировать наши кластеры. Редактор