Производительность etcd-кластера со множеством объектов — головная боль команд, которые любят и ценят Kubernetes. И вот почему: чаще всего для роста производительности кластера используют горизонтальное скалирование, а это приводит к нагрузке на кластер из-за увеличения времени согласования записи данных. В результате вместо шустрого кластера получается неповоротливый тяжеловес. 

Меня зовут Алексей Волков. Я менеджер продукта Cloud Containers в VK Cloud. В этой статье я расскажу о кейсе одного из наших клиентов: с каким запросом он пришел и с какими сложностями сталкивался, как мы провели тюнинг etcd-кластера и какие настройки нужны, чтобы повысить производительность Kubernetes. 

Статья написана по мотивам моего доклада «Не всемогущий etcd, или Почему он не тянет большие нагрузки могучего Kubernetes» на конференции HighLoad++.

Постановка задачи

VK Cloud — безопасная и технологичная платформа с широким набором облачных сервисов для эффективной разработки и работы с данными. Наряду со стеком облачных сервисов мы предоставляем и профессиональную поддержку экспертов для решения задач разной сложности.

Одним из наших клиентов стала компания, которая планировала запуск нового инструмента для создания отчетов бизнес-аналитики на основе Apache Superset 4.0, в котором можно создавать аналитические отчеты на основе данных из базы данных.

В качестве целевой платформы для развертывания 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:

Выбор в пользу переноса/events рационален по двум причинам:

  1. Их легко перенести в продакшен-кластер.

  2. Ивенты часто пишутся и читаются, то есть их перенос позволяет существенно снизить нагрузку на основной 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. 

Это произошло из-за того, что команда использовала 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

С очередным вызовом команда клиента столкнулась в тот момент, когда количество подов снизилось до 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.

Комментарии (1)


  1. kWatt
    22.05.2025 11:37

    Могу ошибаться.
    Helm в secrets кладёт весь свой chart в заархивированном состоянии, ну там для версионирования деплоймента.
    Я бы начал дебаг не с etcd, а с того, что же такого в этом secrets положил helm на ~2GB.