Что делать, когда кластер превращается в тыкву?
Когда эволюция платформы dBrain.cloud дошла до заявленных и считающихся нормальными в публичных облаках пределов, справляться с возникшими при росте объемов кластеров проблемами пришлось самостоятельно. Ведь компании-гиганты, эксплуатирующие Kubernetes, эти тайны не раскрывают.
Например, для кластеров EKS (Elastic Kubernetes Service) на AWS (Amazon Web Services) официальное ограничение объема хранилища составляет 8ГБ. При этом разработчики EKS предлагают пользователям только лишь мониторить размер etcd (метрика apiserver_storage_db_total_size_in_bytes) и не выходить за этот предел.
Согласно политике GCP (Google Cloud Platform) для кластеров GKE (Google Kubernetes Engine), максимальный размер базы данных etcd — 6 ГБ, а общий размер всех объектов каждого типа ресурса не должен превышать 800 МБ. Например, можно создать 750 МБ экземпляров подов и 750 МБ секретов, но нельзя создать 850 МБ секретов. Существуют и другие ограничения.
На сайте Kubernetes указано, что он способен работать на кластерах до 5 тысяч хостов и до 150 тысяч подов. Это значения, при которых в сообществе проводились тестирования, но нигде не сказано, как добиться таких цифр.
Даже на кластерах меньшего объема мы столкнулись с проблемами из-за того, что etcd и kube-api подвисали, происходила частая смена лидера etcd, а api-серверы потребляли все выделенные для них ресурсы, пытаясь кэшировать лавину запросов. В результате на кластере медленно создавались поды, а соединения к api-серверам постоянно рвались.
Все запросы, которые летят к kube-api, идут к одному и тому же кластеру etcd, что замедляет работу и уменьшает производительность. Поэтому первым шагом dBrain на пути к построению кластеров большого объема стало разделение etcd. Что это значит?
Разделение
По умолчанию в деплоях Kubernetes используется один etcd. Архитектуру Kubernetes мы все знаем.
Суть такая: api-серверы Kubernetes ходят в etcd и тут же хранят все данные.
etcd — распределенное key-value-хранилище, производительность которого зависит от времени ответа записи на диск. Это значит Qps (объем запросов) зависит от размера ключей, пропускной способности и времени ответа. Решить проблему с помощью масштабирования путем добавления реплик нельзя: чем больше реплик, тем медленнее запись в etcd, т.к. каждая реплика из-за механизма Raft должна подтвердить свою запись. Соответственно, если реплик больше, то и подтвердить запись нужно большему их количеству. Это приводит к издержкам, потому что реплики находятся на разных серверах и общаются по сети друг с другом. С одной базой etcd, даже на быстрых выделенных NVME локальных дисках, у нас дошло до того, что api-серверы отвечали на некоторые запросы по 20 секунд и обрабатывали при этом не все.
В etcd есть ограничение на размер базы данных (по умолчанию 8 Гб). Но в больших кластерах на базу данных приходится очень много объектов, которые занимают больше установленного объема, даже если выставить revisionHistoryLimit=1. Это не значит, что база перестает работать, но делать это все же не рекомендуется, т.к.etcd не может нормально кэшировать информацию и все время читает с диска.
Транзакции, которые идут в etcd от api-сервера, содержат в себе много данных. Помимо того, что значение по ключу само по себе большое, etcd держит у себя всю историю ключей до момента последнего компакшена.
На небольших сетапах бороться с этим довольно просто: необходимо вынести etcd на отдельный физический диск. etcd все записи делает последовательно — в один поток, а скорость зависит от времени ожидания записи на диск. Чтобы обеспечить минимальное время ожидания, для записи, помимо etcd, выделяются отдельные диски. Но когда кластеры становятся больше, это не помогает: etcd все равно упирается в свою запись, а следовательно увеличивается время записи и ответа.
В api-сервере мы используем специальную опцию etcd-servers-overrides, которая указывает какие объекты Kubernetes в каком etcd хранить. (К сожалению, это работает только для стандартных ресурсов, Custom Resource Definitions (CRD) таким способом вынести не получится). Таким образом, можно переопределить хранение деплойментов в другие etcd.
В итоге в dBrain мы пришли к архитектуре, когда у нас есть три кластера etcd по три реплики etcd в каждом. Первый etcd остался основным, в нем хранятся почти все объекты Kubernetes. Во второй мы вынесли ивенты и leases Kubernetes (это классическая рекомендация). Leases используются как механизм, благодаря которому компоненты Kubernetes (и не только) «договариваются» кто будет лидером, например, controller-manager, scheduler. Для оперативного переключения этот эндпоинт должен быть максимально быстрым. В третий etcd мы вынесли весь workload — стандартные абстракции Kubernetes для рабочей нагрузки: pods, deployments, statefullset, replicaset, daemonset, и т.д.
Мы распределили нагрузку с одного на три кластера etcd, каждый из которых может использовать отдельные выделенные диски и/или ноды. Мы провели своеобразное горизонтальное масштабирование etcd путем распределения общего пула данных на разные etcd. Таким образом увеличилась общая параллельная пропускная способность. Да, деплойменты читаются с той же скоростью, запись не ускорилась, но теперь эти действия друг с другом не коррелируют, не конфликтуют, одни запросы не ждут других.
Проблемы миграции
Чтобы переехать с одного etcd на три, к сожалению, нельзя просто выставить флажок api-сервера (etcd-servers-overrides).
Поэтому мы создали утилиту, которая забирает из etcd-источника указанные ключи и перекладывает в другой etcd. Обновления получаются относительно безболезненные: отключаются api-серверы, чтобы не портить консистентность данных, потом запускается утилита, которая переливает данные из основного etcd в два новых. Финальный этап — запуск api-серверов новой ревизии.
Конечно, путь к большим кластерам не ограничился разделением etcd. В следующих статьях мы расскажем, как провели операцию по разделению обязанностей controller-manager. Кроме того, у нас для вас есть еще один кейс — автоскейлинг api-серверов.