Добавить в корзинуПозвонить
Найти в Дзене

Две настройки Kubernetes вызвали скрытые OOM-сбои в Spark

Иногда для падения пайплайна не нужен ни баг в коде, ни экзотический edge case. Достаточно двух «невинных» настроек: в одном случае Spark на Kubernetes начал складывать временные данные не на диск, а в оперативную память, в другом все executor оказались прижаты к одному узлу. Результат предсказуемый: OOMKilled с кодом 137, ложный след в сторону тюнинга heap и потерянные часы команды, которая чинит не ту проблему. Об этом сообщает InfoQ в разборе инженера Pranav Bhasker, описавшего инцидент после миграции batch-пайплайнов на Azure Kubernetes Service. Снаружи все выглядело как классическая история про «Spark не хватает памяти». Один из крупных job, который раньше годами работал on-premises без подобных сбоев, после переезда в облако начал стабильно падать на shuffle-heavy стадиях. Команда сначала делала то, что обычно делают в таких случаях: увеличивала память executor, меняла их количество, перезапускала джобу. Ничего не помогло. Сам кейс показателен именно деталями. Речь шла о producti

Иногда для падения пайплайна не нужен ни баг в коде, ни экзотический edge case. Достаточно двух «невинных» настроек: в одном случае Spark на Kubernetes начал складывать временные данные не на диск, а в оперативную память, в другом все executor оказались прижаты к одному узлу. Результат предсказуемый: OOMKilled с кодом 137, ложный след в сторону тюнинга heap и потерянные часы команды, которая чинит не ту проблему.

Об этом сообщает InfoQ в разборе инженера Pranav Bhasker, описавшего инцидент после миграции batch-пайплайнов на Azure Kubernetes Service. Снаружи все выглядело как классическая история про «Spark не хватает памяти». Один из крупных job, который раньше годами работал on-premises без подобных сбоев, после переезда в облако начал стабильно падать на shuffle-heavy стадиях. Команда сначала делала то, что обычно делают в таких случаях: увеличивала память executor, меняла их количество, перезапускала джобу. Ничего не помогло.

Сам кейс показателен именно деталями. Речь шла о production-пайплайнах крупного американского финансового института. Каждый день они обрабатывали файл примерно на 3 ГБ, но сам размер входа здесь был обманчив. Это был fixed-width flat file с несколькими перемешанными типами записей, поэтому job читала один и тот же файл несколько раз разными парсерами, строила промежуточные DataFrame и потом объединяла их через union. Для Spark это означает много shuffle-операций и резкий рост промежуточных данных. На старой инфраструктуре такой профиль нагрузки переживался нормально, потому что spill уходил на локальный диск. После миграции контракт инфраструктуры незаметно изменился.

Первая проблемная настройка выглядела так: spark.kubernetes.local.dirs.tmpfs=true. В Kubernetes-версии Spark это означает, что локальные scratch-директории, включая пути для shuffle spill, размещаются в tmpfs, то есть фактически в RAM узла, а не на диске. Вторая настройка была не менее токсичной: жесткое правило podAffinity required, которое заставляло все executor располагаться на одном node. По отдельности эти параметры уже спорные. Вместе они образовали хороший способ быстро сжечь память узла во время shuffle. Плюс к этому и tmp-volume, и workdir были ограничены всего 1 GiB, что для job с тяжелым shuffle оказалось просто несерьезно.

Диагностика заняла несколько дней еще и потому, что симптомы подталкивали к неверным выводам. На этапе первичной проверки команда увеличила spark.executor.memory с 8g до 10g. Затем попробовала добавить executor, чтобы лучше распределить нагрузку. Ожидаемого эффекта не было: падения повторялись ровно в тех же стадиях. Настоящая подсказка появилась только на уровне Kubernetes и мониторинга. В Datadog было видно, что во время shuffle память узла скачком уходила выше 90%. По логам AKS просматривался churn executor: вместо штатных четырех процессов джоба начинала бесконечно поднимать новые, пытаясь заменить убитые, и в одном из прогонов дошла примерно до пятидесятого executor перед окончательным фейлом. Память узла, по данным InfoQ, подскакивала примерно с 42 до более чем 58 ГБ за считаные секунды, после чего kubelet и ядро делали свою работу.

Это и есть неприятная особенность Spark на Kubernetes: стандартная логика расследования часто концентрируется на heap, skew, partitioning и количестве executor, хотя проблема уже давно живет не в Spark-конфиге, а в планировщике и storage semantics платформы. Если spill уходит в RAM, а не на диск, увеличение executor memory не лечит причину. Если все executor сидят на одном узле, горизонтальное масштабирование тоже мало помогает: вы расширяете job логически, но физически продолжаете биться в тот же memory ceiling одного node. В on-premise-кластерах такие вещи нередко «настроены по умолчанию правильно» и потому не попадают в чек-лист миграции. В облаке эта самоуверенность дорого обходится.

Исправление в кейсе оказалось на удивление прозаичным, что только добавляет истории педагогической ценности. Команда отключила RAM-backed scratch, выставив spark.kubernetes.local.dirs.tmpfs=false, увеличила лимиты tmp-volume и workdir с 1 до 10 GiB и заменила жесткое co-location правило на podAntiAffinity preferredDuringSchedulingIgnoredDuringExecution. Иными словами, executor перестали насильно прижимать друг к другу и вернули временное хранилище туда, где ему и место при серьезном shuffle, на диск. После этого OOM-сбои прекратились сразу и, по словам автора, не повторялись в течение шести месяцев.

Для русскоязычных команд здесь несколько практических выводов. Во-первых, lift-and-shift для data workloads почти никогда не бывает настоящим lift-and-shift: совпадение CPU и RAM не гарантирует совпадения поведения. Во-вторых, входной объем данных сам по себе мало что говорит о реальном memory profile job. Файл на 3 ГБ может генерировать тяжелый shuffle, если формат кривой, парсинг многопроходный, а логика строится на materialization и union. В-третьих, при переносе Spark на Kubernetes стоит отдельно валидировать три вещи: где физически живут local dirs, как scheduler размещает executor и соответствует ли размер scratch-volume реальному профилю spill в проде. Если хотя бы один из этих пунктов проверяется «на глаз», расследование первого крупного инцидента почти гарантировано превратится в спор о heap size.

История из InfoQ хорошо показывает, что для платформенных команд главный риск миграции не в очевидных несовместимостях, а в тихой смене инфраструктурных предположений. Spark на Kubernetes давно перестал быть экзотикой, но чем больше организаций переносят зрелые on-premise пайплайны в managed-кластеры, тем важнее проверять не только код и ресурсы, но и поведение временного хранения, affinity-правил и node-level памяти под реальной production-нагрузкой.

The post Две настройки Kubernetes вызвали скрытые OOM-сбои в Spark appeared first on iTech News.