
Производительность etcd-кластера со множеством объектов — головная боль команд, которые любят и ценят Kubernetes. И вот почему: чаще всего для роста производительности кластера используют горизонтальное скалирование, а это приводит к нагрузке на кластер из-за увеличения времени согласования записи данных. В результате вместо шустрого кластера получается неповоротливый тяжеловес.
Меня зовут Алексей Волков. Я менеджер продукта Cloud Containers в VK Cloud. В этой статье я расскажу о кейсе одного из наших клиентов: с каким запросом он пришел и с какими сложностями сталкивался, как мы провели тюнинг etcd-кластера и какие настройки нужны, чтобы повысить производительность Kubernetes.
Статья написана по мотивам моего доклада «Не всемогущий etcd, или Почему он не тянет большие нагрузки могучего Kubernetes» на конференции HighLoad++.
Постановка задачи
VK Cloud — безопасная и технологичная платформа с широким набором облачных сервисов для эффективной разработки и работы с данными. Наряду со стеком облачных сервисов мы предоставляем и профессиональную поддержку экспертов для решения задач разной сложности.
Одним из наших клиентов стала компания «1С-Битрикс» — разработчик CMS «1С-Битрикс: Управление сайтом» и сервиса «Битрикс24». В «1С-Битрикс» к тому моменту планировали запуск BI-конструктора — нового инструмента для создания отчетов бизнес-аналитики на основе Apache Superset 4.0, в котором можно создавать аналитические отчеты на основе данных из «Битрикс24» пользователя.
В качестве целевой платформы для развертывания BI-конструктора компания выбрала Kubernetes, но им нужна была помощь в подготовке среды оркестратора и решении отдельных архитектурных задач.
Изначально сервис планировали запускать как MVP с тестовой нагрузкой 1 000 – 2 000 подов, с планами расширения до 5 000 подов через 6 месяцев — именно с этой задачей мы стартовали.
Для создания и удаления аккаунтов, то есть изолированных инсталляций приложения для каждого клиента BI-конструктора, был реализован довольно типовой подход.
Создание нового аккаунт сводится к выделению Namespace, внутри которого создаются поды, репликасеты, секреты, лимиты и другие сущности. Все устанавливается через Helm. И такой типовой набор создается для каждого последующего клиента.

Алгоритм на случай блокировки аккаунтов несколько проще. Так, если клиенты долго не заходят и не используют свои ресурсы, количество реплик пода уменьшается до нуля. Это помогает минимизировать неоправданный простой мощностей.
Эпизод 1: 5 000 подов, большие задержки в etcd
Кластер, развернутый для BI-конструктора, достиг размера в 5 000 подов в первый месяц, а не через полгода, как планировалось изначально. Одновременно с таким ростом клиент начал фиксировать задержки etcd, из-за чего Kubernetes стал работать нестабильно.
В связи с этим возникла гипотеза, что повысить производительность и отказоустойчивость кластера можно, увеличив количество мастер-нод. Так, изначально у клиента на кластере было 5 мастер-нод, а мы хотели увеличить их количество до 7.
Но проверка гипотезы показала, что с добавлением дополнительных мастер-нод отказоустойчивость увеличивается, но с задержками ситуация только ухудшается: 7 etcd создают задержку в согласовании записи/чтения.

Соответственно, нужно было копать глубже.
Тут стоит подробнее остановиться на том, как работает etcd.
Погружение в etcd
Etcd — это распределенное key/value-хранилище, обеспечивающее линеаризуемые операции GET/SET/CAS с данными.
Примечание: Линеаризуемость — свойство распределенных систем, которое гарантирует, что все операции (например, чтение или запись данных) выглядят так, будто они выполняются мгновенно и в строгом порядке, даже если на самом деле это не так. Это нужно, чтобы все участники системы видели данные согласованно, без противоречий.
Линеаризуемость создает иллюзию работы с единственной копией данных. При этом, чтобы обеспечить выполнение гарантий в части линеаризуемости и реплицирования данных в etcd, используется кворум.
Примечание: Кворум — это минимальное количество узлов кластера, которые должны быть доступны, чтобы система могла принимать решения (например, записывать данные или выбирать лидера). Это гарантирует, что etcd сохраняет согласованность данных и работает надежно, даже если часть узлов выходит из строя.
Основная сложность использования кворума — необходимость опросить все etcd и одновременно получить одинаковый ответ от большинства. Соответственно, чем больше мастер-нод, тем больше операций приходится выполнять.
Примечание: Здесь можно провести аналогию с назначением командного созвона: троим всегда легче выбрать удобные для всех дату и время звонка, чем семерым.
Аналогичная ситуация и с чтением. Может показаться, что, если есть 7 мастер-нод, можно сходить на любую и получить нужную информацию, что повышает производительность чтения. В действительности это не так, поскольку в основе реализации чтения из etcd лежит алгоритм RAFT. То есть:
при любом количестве мастер-нод всегда есть всего один лидер (Leader);
все запросы на чтение проходят через Leader;
если запрос пришел на Follower, то он пересылается на Leader;
данные на Follower реплицируются через Leader.
Таким образом, увеличение числа нод в кластере не приводит к масштабированию операций чтения.
Примечание: По нашему опыту, 3–5 мастер-нод достаточно для обеспечения как отказоустойчивости, так и оптимизации.
Поскольку добавление мастер-нод оказалось неэффективным методом, мы решили прибегнуть к шардированию etcd.
Мы создали новый etcd, выделили для него высокопроизводительный диск, как и для основного etcd, подкорректировали kube-api-конфиг и вынесли из основного etcd /events:
--etcd-servers overrides= /events#
https://127.0.0.1:3379…
data-dir: /var/lib/etcd events/default.etcd
Выбор в пользу переноса/events рационален по двум причинам:
Их легко перенести в продакшен-кластер.
Ивенты часто пишутся и читаются, то есть их перенос позволяет существенно снизить нагрузку на основной etcd.
Дополнительно, чтобы не отказываться от повышенной отказоустойчивости, достигнутой увеличением количества мастер-нод, было принято решение увеличить диск etcd до 500 Гб для снижения задержек (в облаке производительность зависит от размера диска).
Таким образом мы одновременно и снизили задержки, и повысили отказоустойчивость кластера.
Эпизод 2: 10 000 подов, квота на ETCD
На втором месяце использования кластера количество подов выросло уже до 10 000. В результате мы уперлись в квоту, из-за чего все действия с etcd были заблокированы, а кластер не работал.
В логах kube-apiserver была очевидная ошибка:
…rpc error: code = ResourceExhausted desc = etcdserver: mvcc: database space exceeded"}…
Решение оказалось очевидным — расширить квоту. При этом мы исходили из того, что в etcd для Kubernetes квота по умолчанию 2 Гб, а разработчики etcd рекомендуют не устанавливать квоту более 8 Гб. Причем превышение квоты чревато негативным влиянием на многие параметры, в том числе:
время запуска (сильно увеличивается время запуска самой ноды, если она перезапустилась по какой-то причине);
потребление памяти;
производительность индексов;
время завершения операции;
требования к скорости сети (так как надо переносить снапшоты с лидера на Follower большего размера);
скорость восстановления Follower после сбоя.
Поэтому мы выставили размер квоты для основного etcd на уровне 8 Гб и 2 Гб для events. Это помогло справиться с текущей ситуацией.
Эпизод 3: 25 000 подов, переполнение переменной
В течение трех месяцев количество подов достигло 25 000.
Поскольку кластер изначально не был рассчитан на такую нагрузку, постепенно начались трудности с переполнением переменной.
Так, специалисты компании использовали Helm для установки релизов для каждого клиента и применяли команду, которая была необходима в бизнес-процессе для подсчета количества установленных релизов. Но со временем начали получать ошибку с Helm, указывающую на сбои в работе gRPC:
$ helm list -A| wc -l
Error: list: failed to list: rpc error: code = ResourceExhausted desc = grpc: trying to send message larger than max (2150939364 vs. 2147483647)
Но причина ошибки была непонятна.
Логи kube-apiserver тоже ситуацию не прояснили:
{"level":"warn","ts":"2024-01-15T10:31:11.267Z","logger":"etcd-client","caller":"v3/retry_interceptor.go:62","msg":"retrying of unary invoker failed","target":"etcd-endpoints://0xc00213ae00/127.0.0.1:2379","attempt":0,"error":"rpc error: code = ResourceExhausted desc = grpc: trying to send message larger than max (2150939365 vs. 2147483647)"}
Таким образом, для поиска первопричины сбоев нам также пришлось опускаться на уровень логики работы команды helm list -A.
Принцип работы helm list -A прост:
По умолчанию Helm хранит релизы в etcd в <namespace>/secrets.
При запуске Helm запрашивает в kube-apiserver api/v1/secrets и labelSelector=owner=helm.
Далее kube-apiserver запрашивает данные из etcd, после чего etcd отдает все ключ/значения из /secrets.
Следом kube-apiserver фильтрует ключ/значения по labelSelector, потому что вся информация об объекте хранится в Value, а etcd не умеет фильтровать данные, хранимые Value. При этом запросы с фильтром по полю на множестве объектов приводят к повышенному потреблению памяти kube-apiserver.
После этого kube-apiserver отдает результат в Helm.
После анализа всех этапов выполнения команды стало очевидно, что в нашем случае сбой произошел в момент, когда etcd должен отдавать все ключ/значения из /secrets.
Проблема оказалась в том, что в переменной из библиотеки grpc для сервера etcd было прописано ограничение размера ответа — не более 2 Гб.
# MaxSendMsgSize returns a ServerOption
# to set the max message size in bytes
# the server can send. If this is not
# set, gRPC uses the default math.MaxInt32.
const (
grpcOverheadBytes = 512 * 1024
maxSendBytes = math.MaxInt32
)
В теории можно было перекомпилировать библиотеку на лету. Но это помогло бы избавиться только от «симптома», но не от самой проблемы. Поэтому мы пошли дальше.
Дальнейшее изучение кластера и бизнес-процессов показало, что компания выкатывает релизы для своего продукта не один раз в месяц, как планировалось, а 2–3 раза в неделю. В результате у пользователей BI-конструктора собралось около 7–8 релизов, хранимых в etcd.
Это произошло из-за того, что команда «1С-Битрикс» использовала Helm upgrade без указания параметра количества релизов (значение по умолчанию — 10).
--history-max int limit the maximum number of revisions saved per release. Use 0 for
no limit (default 10)
При развертывании единичных инсталляций это было бы не столь критично, но у компании было 25 000 пользователей, у каждого из которых было накоплено до 7–8 релизов. То есть даже при небольших размерах самих релизов суммарно получался огромный объем хранимых секретов в кластере.
Нюанс в том, что просто уменьшить количество хранимых релизов нельзя. Поэтому нам пришлось искать другие подходы. Одним из таких стало удаление одного из старых тестовых аккаунтов (в том числе его Namespace и секретов) и обновление истории релизов:
helm upgrade --history-max=2 <release> <chart>
Это позволило нам очистить историю релизов от ненужных данных, решить проблему с переполнением переменной и разгрузить etcd.
Эпизод 4: 17 000 подов, перекос в данных etcd
С очередным вызовом команда «1С-Битрикса» столкнулась в тот момент, когда количество подов снизилось до 17 000. Здесь примечательно, что после уменьшения количества подов с 25 000 до 17 000 проблем и нагрузок в теории должно было стать меньше.
Но оказалось не так.
Мы заметили, что etcd продолжил деградировать из-за перекоса в хранимых данных etcd.
Так, мы сделали два снапшота с разницей в несколько дней и увидели, что количество подов уменьшилось более чем на 2 000.
При этом количество других объектов, хранимых в etcd, увеличилось на 8 000. А общее количество объектов, хранимых в etcd, почти достигло полумиллиона.

При подробном изучении стало понятно, что причина в том, что блокировка аккаунтов осуществлялась методом уменьшения количества подов до нуля. Более того, из-за ошибки в скрипте старые данные из проектов не удалялись.
Но исправление этих нюансов не решило проблему.
Поэтому мы начали изучать размеры объектов разных типов. Оказалось, что общий объем данных в etcd равен 2 706 Мб и 1 645 Мб из них приходится на секреты, то есть secrets в etcd кластера занимают 63% от всего размера etcd.

При этом, согласно рекомендациям Google и AWS, размер каждого типа ресурса не должен превышать 800 Мб.
В нашем же случае кластер и все его основные сущности были заняты обслуживанием работы Helm.
Чтобы исправить ситуацию, мы приняли решение создать скрипт и мигрировать секреты Helm в БД Postgres. После этого кластер заработал в штатном режиме — проблема была решена.
Что в итоге
На основе описанного кейса можно сделать несколько выводов.
Kubernetes — де-факто инфраструктурный стандарт. Но для получения всех преимуществ работы с оркестратором нужно иметь достаточную экспертизу. Без этого даже небольшие проблемы, которые неизбежно возникнут при выпуске приложения в прод, могут стать непреодолимыми барьерами.
Есть 1 000 и 1 компонент Kubernetes, влияющие на успешность запуска высоконагруженного проекта. Нюанс и в том, что даже обнаруженная неисправность или ошибка может оказаться не основной первопричиной всех проблем.
Архитектура сервиса, запущенного в K8s, также влияет на стабильность и производительность кластера. То есть если приложение ведет себя стабильно под текущей нагрузкой, нельзя гарантировать, что ситуация не изменится при масштабировании. Поэтому архитектуру приложений, стратегии их развития и подходы к поддержке надо прорабатывать заранее.
При этом порог входа в работу с Kubernetes по-прежнему остается довольно низким. Этому способствует то, что часть задач при работе с оркестратором, в которых недостаточно экспертизы, можно делегировать профильным специалистам. Например, в подобных сценариях могут помочь команда Managed Kubernetes aaS Cloud Containers и команда Professional Services VK Cloud.
kWatt
Могу ошибаться.
Helm в secrets кладёт весь свой chart в заархивированном состоянии, ну там для версионирования деплоймента.
Я бы начал дебаг не с etcd, а с того, что же такого в этом secrets положил helm на ~2GB.