Облака обещают магическую экономию и бесконечное масштабирование. На практике Kubernetes‑кластеры в AWS и Яндекс.Облаке легко превращаются в бездонную чёрную дыру для бюджета. Мы в «Антиплагиате» научились готовить споты, научили шедулер и дешедулер работать правильно и выжали из кубов максимум надежности и производительности при минимуме затрат. В этой статье — конкретные шаги, которые позволили сэкономить миллионы рублей.

Казалось бы, задай вопрос любой популярной LLM, она выдаст набор рецептов на любой вкус. То, что будет в этой статье, LLM не расскажут. Я проверял. В этой статье, только реализованные и действительно работающие рецепты с плюсами, минусами и подводными камнями.
Привет, Хабр! Меня зовут Андрей Ивахненко, я отвечал за инфраструктуру и работу сервиса, руководил отделом внедрения и эксплуатации компании «Антиплагиат» более 10 лет. В этой статье я расскажу о том, с чем мы столкнулись на пути внедрения и оптимизации кластеров K8s с 2022 по 2024 годы. Статья является немного причесанным транскриптом моего доклада на Saint Highload++ 2025.
Что такое Антиплагиат
Антиплагиат — это поисковик, на вход которого подаётся документ, а на выходе он размечается блоками. Для каждого размеченного блока система дает ссылку на источник, в котором присутствует ровно этот или сильно похожий (с точностью до перефразирования или перевода) на него текст.

Антиплагиат ищет не только прямое совпадения текста, но и с точностью до перевода (на 100 популярных языков), перефразирования, признаки сгенерированного ИИ текста. Еще система ищет совпадения изображений с точностью до вращения, кадрирования, отражений и тому подобных мелких изменений. Используем для этого наукоёмкие алгоритмы, которые сами и создали. У нас есть R&D, много машинного обучения и его эксплуатации.
Как это работает

От пользователя приходит документ.
Документ попадает в бизнес‑логику (кабинет, авторизация).
Документ разбирается на части.
Не зря на схеме изображена мясорубка, документ действительно превращается в своеобразный фарш: текст, изображения, формулы, таблицы, форматирование — всё отдельно, по частям. Так документ перестаёт быть документом.
Дальше всё это отправляется в нужных пропорциях на поиск совпадений.
Отдельно изображения, отдельно способы обхода системы, отдельно выявление искусственно сгенерированного текста, для которого помимо текста внезапно нужно и форматирование.
Затем все результаты поисков собираются обратно в единое целое.
Готовится отчет.
Результат показывается пользователю.
Разница в нагрузке
Антиплагиату 20 лет. Поэтому у нас даже остался кусочек монолита — внимание! — на Винде. То есть, мы — своего рода совершеннолетний стартап, который всё ещё дорабатывается. Нам осталось пара лет до полного исключения этого легаси.
За 20 лет у нас проиндексировано более 1,5 млрд документов. Это большие тексты, примерно 23 википедии, причём целиком и на всех языках. Если все эти документы распечатать и сложить стопкой, то она достанет до луны.

Мы ежемесячно индексируем больше двух миллионов новых документов. А ещё проверяем, что старое лежит на том же месте, а если, вдруг, исчезло, то ищем, куда именно.
Мы ежегодно проверяем больше 12 миллионов входящих документов пользователей. До 150 документов ежеминутно (во время пиков). А на проверку одного документа у нас уходит от 5 до 30 минут.
При этом лет 5–7 назад мы успевали за 15 секунд найти совпадения среди 1,5 млрд документов‑источников. Но теперь все поиски стали гораздо сложнее. Поиск переводных заимствований, детекция сгенерированного ИИ текста, поиск совпадений изображений, поиск в живом интернете с помощью поисковиков — все это очень тяжелые вычислительно пои��ки. Поэтому время поиска неизбежно увеличилось и сейчас в норме составляет 5–7 минут на документ.
Для нашего сервиса характерна дневная сезонность. На изображении ниже — не самые нагруженные сутки. Здесь ночью на проверку отправляли по 10 документов в минуту, а днём — более 110.

А ещё есть годовая сезонность:

В пиковый день (обычно это какой‑то день около 12 июня) мы проверяем в 50 раз больше документов, чем в самый незагруженный (обычно это последняя суббота июля) день в году.
Ещё есть недельная сезонность. Разница между субботой и понедельником в два раза. То есть в субботу проверяем 50 тыс., а в понедельник — 100 тыс.
Масштабируемая архитектура в облаке
При такой сезонности и колебаниях надо масштабироваться, достаточно быстро как вверх, так и вниз. А значит идти в облака через наш любимый Kubernetes. Мы прикинули затраты эксплуатации на каждый элемент в пайплайне обработки документа в процентах:

Как и ожидалось, основные расходы — это поиск (90%). Он разделен на две части — 60% (вычислительные операции в нескольких кластерах Kubernetes) + 30% (хранение: цифровые слепки, индексы и так далее). Хранение оптимизировать сложно, так как это данные, которые лежат на дисках и объемы хранения только растут. Поэтому сосредоточимся на 60% расходов на вычисления.
Максимальные суммарные значения объемов кластеров в пиковые периоды в 2025 году:
7200 подов.
7700 ядер.
15 ТБ памяти.
6,52,7 млн рублей в месяц затрат.
Заметьте, 60% — это сейчас, когда мы уже 3 года что‑то оптимизируем, улучшаем, удешевляем. Просто так взять и откатить все оптимизации к состоянию 3 года назад что бы оценить стоимость до оптимизаций — невозможно. Во‑первых, если откатить все оптимизации, то оно просто не будет работать (производительность), во‑вторых, это просто очень трудозатратно. Поэтому я попытался подсчитать сколько бы это стоило 3 года назад с помощью ряда допущений. Получилась оценка примерно в 6,5 млн рублей, что составило бы более 80% от всей стоимости эксплуатации.
Давайте рассмотрим динамику стоимости одного из кластеров в Amazon:

Мы начали активно расти примерно в мае 2024. До этого момента все улучшения компенсировались ростом количества подов. С 2022 года мы начали переезд в Kubernetes, распил монолита, перетаскивание сервисов из автоскейлинг‑групп. Параллельно внедряя различные оптимизации из списка ниже. Поэтому рост числа сервисов, перенесенных в K8S, компенсировался оптимизациями и стоимость не росла. В 2024 году, мы сделали волевое усилие и сосредоточившись на этой задаче завезли в кубы все что могли, поэтому расходы начали расти.
У нас больше нет автоскейлинг‑групп. Почти всё, что работает на Linux, размещено в K8s, а в сторонке — виндовый монолитик и несколько сервисов которые его обслуживают.
Споты – серебряная пуля
Первое, о чём думают при оптимизации стоимости K8s — споты. Так в AWS называют инстансы которые могут в любой момент отозвать. В Яндексе они тоже есть и называются прерываемые инстансы. Из‑за такого ограничения в использовании они стоят дешевле в 2–3 раза по сравнению с обычными инстансами (on‑demand). K8s управляют нодами, нода упала, ноду отозвали, K8s поднял новую, класс! K8S и спотовые инстансы — созданы друг для друга!
Рассмотрим подробнее:

Действительно, экономия выглядит потрясающе. При этом у Amazon цена изменчивая и может отличаться от типа к типу. В Яндексе таких проблем нет. В AWS мы используем разные типы машин, и по максимуму распределяем нагрузку среди них по соотношению CPU/RAM. А в Яндексе используем один размер машин и одно соотношение CPU/RAM. Так можно задавать любое целое соотношение. В частности, мы используем 1 к 3, что лучше всего подходит к нашему распределению CPU и RAM. В Амазоне, чтобы достичь 1 к 3, нужно смешать инстансы с соотношениями 1 к 2 и 1 к 4.
Но при использовании есть неочевидные нюансы. Размер машины для ноды – баланс между накладными расходами и неиспользованными ресурсами на последней пока не загруженной полностью ноде.

Например, в AWS нужно использовать как можно больше различных семейств инстансов. Потому что иногда кто‑то хочет взять себе, например, все C5A машины, и выметает их с рынка полностью. Если у вас весь кластер на этих машинах, весь кластер — хоп! — и свернулся. А если даже не весь, то половина. Поэтому мой совет: распределяйте среди максимального количества разных типов и во всех 3 зонах доступности.
Неожиданностью стало и то, что машина 16xlarge как‑то раз внезапно оказалась на 6−8% дешевле, чем 4x 4xlarge. Потом этот дисбаланс прошёл. Но я рекомендую отслеживать, потому что иногда такое бывает. Оптимальный размер машины для ноды надо подбирать исходя из размеров вашего кластера. Большая машина имеет как плюсы, так и минусы. К плюсам можно отнести, меньше накладных расходов, а к минусам — если её вдруг отзовут, больше сервисов станет недоступно одномоментно. Соответственно, нужно подбирать размер из борьбы жадности бережливости и рисков.
Первый подход к запуску Kubernates мы начали с развертывания ванильного кластера и настройке всего самостоятельно. Kubernetes как конструктор — можно решать одни и те же задачи разными инструментами. В команде были жаркие споры что лучше. В итоге что‑то заработало, но поддерживать было сложно. Следующая идея была — а давайте используем managed решения от облачных провайдеров. Мы наткнулись на то, что разные провайдеры имеют разные api, а нам бы хотелось, чтобы код был одинаков. Тогда мы нашли компанию, которая занимается внедрением и поддержкой Kubernates кластеров. Получилось managed решение, где мы брали от облаков только инстансы, а вся логика кластеров была одинакова.
Нам казалось, что для всех облаков будет один код, что удобно. Но нам пришлось поплатиться за это решение дальнейшими проблемами с кастомизацией. Не все элементы managed K8s были хороши. В частности, в AWS скейлинг был просто отвратительным, поэтому мы сменили его на Karpenter. Karpenter входит в стандартную поставку managed K8s от Amazon. Это хорошее решение, которое самостоятельно следит за ценами на рынке спотовых инстансов, и всё делает за вас.
В Яндекс спотовые инстансы называют прерываемыми. Они гарантированно выключаются раз в сутки. ��ыключение по некоторым данным должно быть равномерно распределенным с 22 до 24 часов после включения. Однако, мы столкнулись с проблемой массового отзыва машин, когда машины отключаются так же, как и включались примерно одновременно. Это было неприятно, поэтому мы перестали надеется на рандом от YC и начали планово освобождать машины через 20 часов после старта. Говорим дешедулеру что бы он освободил машину, а потом удаляем ее из кластера. Это решение существенно улучшило user experience. В остальном, если нет ситуативных проблем со скейлингом, Яндекс работает великолепно и без перерывов. В Amazon спотовые инстансы могут работать неделями без каких‑либо проблем.
Масштабирование
Буквально каждую минуту у нас что‑то отзывается, а что‑то заезжает, масштабирование происходит на дневном тренде утром, а вечером — даунскейлинг. Всё нужно делать быстро!
Но не всё так просто. Расскажу, с какими проблемами мы столкнулись.
Шедулер и дешедулер

Шедулер у нас в Kubernetes пытается найти трейдоф:
В ванильном кубере он пытается поселить новый под на наименее загруженную ноду. Нам это не понравилось. Мы стараемся загружить под на наиболее нагруженный узел для того, чтоб�� нод было меньше.
При этом стараемся распределить реплики равномерно по зонам доступности и типам инстансов. Для этого мы завели внутри достаточно сложную скоринговую систему, которая анализирует, куда этот под поместить. Мы стараемся сделать так, чтобы разные реплики подов одних и тех же сервисов максимально разъехались по зонам доступности и типам инстансов. Это позволяет минимизировать вероятность потери сразу всех экземпляров. Скоринговая система выставляет баллы, куда лучше поместить, выбирая самую загруженную ноду.
Дешедулер тоже старается одновременно сделать противоположное:
Освободить наиболее свободную ноду, чтобы отключить её и не платить.
Не убить молодые поды. Потому что часто бывает пограничная ситуация — ещё +1 экземпляр сервиса и нужно скейлиться вверх, а через 5 минут оказывается, что уже и ненужно. А если нужно ещё и новую ноду завести, то вот это «подняли‑опустили» повторяется много раз. Это долго и дорого. Поэтому мы делаем петлю гистерезиса, то есть стараемся нарастить, чтобы не так быстро откатываться и убивать.
Тюнинг VPA

Вместо пересчёта рекомендаций раз в сутки — вычисляем их раз в минуту.
Снизили минимальные лимиты: до 0,1 CPU и100MB RAM. Это минимально необходимые параметры, чтобы наши сервисы запустились и прошли Liveness Probe и Readiness Probe.
Выдаём на 40% больше рекомендаций VPA. Это связано с нашей спецификой. Документы приходят разного размера: 20Kb, 50Kb, 300Kb, 2Mb. У нас всегда отведён небольшой запас на случай редких больших документов.
Если случился OOM, то выдаем +50% к рекомендации.
Если сервис видит, что пришло больше, чем он может обработать, то он сразу делает OOM. Это сигнал для VPA, который сразу говорит — «+50% этому господину». Таким образом мы достаточно быстро набираем память или процессоры, чтобы обработать больших чёрных лебедей, которые приходят в неожиданный момент. У нас есть от 10 до 30 минут на проверку документа, которые входят в нормальный user experience. Пользователь понимает, что он отдал нам документ, значит, может пойти выпить чаю, скушать тортик, а через 10–30 минут всё будет проверено. Часто, вообще, история асинхронная — одни пользователи загружают документы, другие через два часа проходят посмотреть результат.
Минимум реплик = 1
Если свободной реплики сейчас нет, сервис отвечает resource exhausted, мы подождём. Через две минуты она появится, и мы всё посчитаем. Задачи на проверку у нас параллелятся, и поэтому редкая задержка в 2 минуты не сильно влияет на общее время проверки.
Минимум 20 минут жизни пода до применения рекомендаций.
По тем же самым причинам первые 20 минут под только осматривается, понимает, что происходит, вырабатывает характеристики.
Тюнинг HPA
Метрика: сколько запросов обрабатывается из возможных. Сначала мы перешли от стандартных измерений CPU и сети к отслеживанию очередей. Но и это работало не очень хорошо. Поэтому мы добились того, что каждый сервис знает, сколько запросов он может обработать. Например, способен на 4 параллельных запроса и тогда, если мы видим, что он обрабатывает 3 из 4 — понимаем, что всё ок.
Возвращать максимум метрики, если данных нет. Мы столкнулись с проблемой: когда приходят большие документы, «чёрные лебеди», по 2MB, сервис уходит с головой в работу, Liveness Probe и Readiness Probe не отдают метрики — сервис не оставил ресурсов для проб. Мы в этот момент возвращаем максимум метрики. Стандартная история — если ничего не вернул, система понимает, что нужно масштабироваться, добавить ещё экземпляров. Частая ошибка интерпретировать отсутствие результатов как последнее значение или вообще 0. Такая интерпретация влечет скейлинг не вверх, а наоборот вниз.
Прогрев и масштабирование сервисов при обновлении. При обновлении сервиса, когда появляется новая версия, мы прогреваем её, делаем столько копий, сколько в текущей версии, и отдаём готовое облако новых версий. Можно менять и по одному экземпляру, но в этом случае, если пользователь любит перепроверять, то в нескольких проверках получает разное значение и обращается в техподдержу. Поэтому мы стараемся заменять сразу весь сервис целиком на новый.
Как выглядит включение HPA. Это один из немногих графиков, которые мне удалось достать:

Тайминг графика — приблизительно с 22:00 до 6 утра. Слева то, что было (много) до 22:00, далее запустили HPA, система пересчитала, и пошёл стандартный спад — уменьшение активности вечером. Уже при старте экономия получилась около 40%, а к вечеру и того больше. То есть вместо 40 CPU, получилось 10 — хорошая эффективность.
Диски. Как устроена Дискотека
Специфика нашего сервиса в том, что у нас «толстые» модели. Под может притащить 1 гигабайт данных, а может и 4 или даже 10 гигабайт. Больше 10 мы стараемся не делать, но такое тоже возможно. Расскажу, что с этим делать.
Данные моделей у нас хранятся в S3, образы — в Nexus. Данные из S3 забираем медленно, потому что у S3 есть лимиты, а модели большие. Допустим надо скейлится и под с данными модели заезжает сразу на 3–4 ноды. Надо выкачать с S3 несколько гигабайт в 3 потока. Получается очень медленно — проблема.
Чтобы решить эту проблему несколько раз в сутки мы упаковываем для ноды стартовый образ диска с часто используемыми сервисами и данными моделей, при этом оставляем около 50% свободного резерва. При запуске машины, что в Amazon, что в Яндекс, происходит загрузка образа с S3, но в отличии от простой загрузки тут у каждой машины лимиты свои и они отдельные от общей пропускной способности S3. Благодаря этому запуск происходит гораздо быстрее.
Текущие размеры дисков у нод — 93-186GB. Тем, кто работает с Яндексом, знакомы эти цифры. В YC есть быстрые нереплицируемые диски, которые подаются кусками по 93GB. В Amazon мы решили сделать также, просто для идентичности.
Для моделей мы используем Statefulset с синхронизацией S3 при старте. Мы отслеживаем, появилась ли новая версия данных в S3, с момента создания образа могли залить новые модели. Мы смотрим на хэш‑суммы, если они совпадают, работаем дальше, если нет — обновляем данные.
После остановки и удаления ноды мы не всегда удаляем и диск ноды. Мы оставляем около 30% неиспользуемых дисков в резерве. Если понадобится новая нода, мы просто используем один из свободных дисков, нода поднимается быстро, диск уже готов, не нужно ничего скачивать или прогревать. Всё работает очень быстро.
Трафик
Первое, что мы сделали с трафиком — переселили Istio с мастеров на спотовые воркер‑ ноды, и стали меньше платить за мастеров (они on‑demand, по понятным причинам).

На графике видно, как сильно уменьшилось потребление CPU на мастерах после переноса Istio. Теперь мы можем уменьшить on‑demand мастера в два раза, заменив их таким же или меньшим объемом спотовых ресурсов.
Если видим, что трафик от Istio гигантский, то с этим надо что‑то делать.
Мы строим ограниченные карты для всех сервисов. Каждый сервис у нас знает свои зависимости. Это драматически сократило трафик (карта существенно меньше) и ускорило процесс подъема подов (карта строится быстрее).

Вот пример с dev-кластера. Здесь время на старт сервисов упало с 20 секунд до миллисекунд. На проде было 90 секунд на старт сервиса — стало 1 секунда. Есть ещё и феноменальное сокращение трафика. В Amazon, кстати, трафик платный, в Яндексе пока нет — но ключевое тут «пока».
Сжатие трафика
Мы работаем с текстами, yaml, xml. Эти форматы прекрасно сжимаются. Степень сжатия до 80%, а то и выше при незначительных затратах CPU. Включаем сжатие трафика.
Для этого мы сделали:
Прозрачное сжатие трафика http/1 на уровне Istio envoyfilter.
Включили поддержку компрессии на серверах и клиентах служб.net и Python.
Это оказалось не тривиальной задачей, потому что по умолчанию было не включено. Разработчики потратили несколько спринтов на согласования, чтобы клиенты и сервера это поддерживали.
Проверили, что включено сжатие gRPC.
Мониторинг
Логи и OpenSearch
Для начала логи. Здесь тоже есть место для оптимизации. Мы перешли с Elastic на OpenSearch.
В кластере храним два последних часа логов, чтобы разработчики могли быстро их просмотреть. При этом бывали случаи, когда нужно понять, когда начался инцидент. Для этого хотелось иметь историю хотя бы за месяц. Хранение таких объёмов на дисках виртуальных машин обходится дорого. Поэтому мы храним месяц логов на S3 и используем в таком виде как источник данных для OpenSearch.
Для данных хранящихся в S3 скорость поиска меньше, но для эпизодических исследований раз в месяц или полгода — подходит, и, самое главное, обходится существенно дешевле. В результате мы дешевле и больше храним то, что используется редко и не требует скорости.
Дашборд эффективности
Графики — хорошая визуализация для временных серий событий. Хорошо сделанный дашборд помогает понять, что происходит прямо сейчас и находить новые кейсы для оптимизации. Именно благодаря графикам, задавая вопрос «А почему так?» мы нашли сэкономили не мало денег. Самые простые графики, показывающие сколько ресурсов оплачено, заказано и утилизировано творят чудеса при пытливом уме.

Сейчас мы далеки от 100% утилизации, но отношение заказанных и оплаченных ресурсов у нас в районе 95−98%. Это нормальная ситуация как по памяти, так и по CPU.
Интегральный показатель
Интегральный показатель — это усреднённое значение между памятью и CPU:

Доска позора
Мы наблюдаем за сервисами, которые много заказывают, но мало потребляют. К сожалению, это возможно в силу нашей специфики: к нам могут прилететь «чёрные лебеди» на 2MB посреди спокойного озера текстов в 30KB.

У нас есть сервисы, с которыми мы иногда миримся, а иногда нет. В частности, пресловутый парафраз по идее должен быть скоро переписан целиком и полностью, поэтому пока живёт с такой жуткой неэффективностью. На графике показано, что 17,4% CPU от всего кластера не утилизируются из‑за этого сервиса.
Изменения неизбежны
Что мы поменяли в требованиях к разработке:
1. Все сервисы должны соответствовать 12 факторам (TFA), мы за этим следим.
2. У сервисов есть минимальный ресурс для старта.
3. Smoke‑тесты нужно проводить сразу после развёртывания, чтобы проверить, корректно ли прошел деплой, работают ли они вообще.
Это базовая проверка. Естественно, она идёт после тестирования и нужна, чтобы не выкатить в прод что‑то, где не работают зависимости или есть другие проблемы.
Изменения в мировоззрении
Самое сложное — это изменение в мировоззрении разработчиков.
● Сервис может умереть в любой момент. Поэтому он должен разбивать свою работу на части, сохранять чек‑поинты.
● Если сервис недоступен, то нужно ретраить.
● Нужно проверять, живы ли зависимости сервиса. Если база была недоступна ещё секунду назад, но уже доступна — значит, можно и нужно продолжать работать.
→ Мы сейчас находимся здесь. Это следующее точка для оптимизации.
● Все данные должны обрабатываться одинаковыми фрагментами. Грубо говоря, «чёрные лебеди» в 2MB должны биться на куски по 50 KB и обрабатываться по частям. К сожалению, прийти к этому очень непросто, потому что в основы системы заложен иной подход. Совсем недавно мы ограничили размер входного текста двумя мегабайтами, раньше работали с десятью мегабайтами и хвастались — любой текст обработаем, не проблема! Но тут вступает в игру не программистская гордость за алгоритмы, а cost effective. Десять мегабайт — слишком дорого для нас. Нам нужно держать большие резервы, чтобы раз в день обрабатывать такой большой документ. Дальнейшее уменьшение максимального размера документа приведет к снижению потребления, но тут уже бизнес не готов к таким жертвам, и мы идем к тому, чтобы разбивать большие тексты на чанки для обработки.
Резюме: как мы экономим до 3,8 млн рублей в месяц
Набор рецептов, что мы сделали и что вы можете попробовать сделать у себя в проекте:
Определите реальные рамки скорости реакции и downtime — сколько времени для вас составляет допустимое время недоступности сервиса. Например, 10−30 минут дают нам достаточно большую свободу. Если у вас задача отвечать за миллисекунды, то, конечно, этот вариант вам не подойдёт.
Споты — 50-70% экономии, но готовы ли вы к частым сменам нод? Это серьёзная задача. У нас она заняла больше года. Столько понадобилось, чтобы научиться по‑настоящему хорошо обрабатывать отзыв нод и быстро масштабироваться.
Постройте дашборд эффективности и задавайте себе вопросы: а почему график такой? Это максима — если вы начнёте наблюдать за параметром, он гарантированно начнёт улучшаться.
Сократите область видимости карт сервисов. Это достаточно дёшево, хотя может быть нетривиально. Но если предпримете организационные усилия — всё заработает.
Хотел бы поблагодарить коллег за помощь в подготовке доклада для Saint Highload++ и этой статьи на его основе:
Александра Кильдякова
Павла Бордукова, TeamLead DevOps
Команды DevOps и SRE
И всех остальных моих коллег
Без этих людей мы не смогли бы всё это реализовать. Также большое спасибо разработчикам, которые стойко выдержали все наши требования.
