diff --git a/README.md b/README.md index aeab7f5..15fd6e7 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,8 @@ Superset, создаёт администратора и импортирует и `test_kafka`, метаданные и подключение Superset. Первый пробник создаёт таблицы на обеих нодах и читает через `Distributed` на ноде 2 строку из локальной таблицы ноды 1. Второй пишет в Kafka и читает свой маркер. В конце -проверка спрашивает у Docker, не убивало ли ядро какой-нибудь долгоживущий -контейнер за нехватку памяти и не включалась ли политика перезапуска: убитый +проверка спрашивает у Docker, не убивало ли ядро что-нибудь в долгоживущих +контейнерах за нехватку памяти и не включалась ли политика перезапуска: убитый контейнер Docker поднимает сам, и проверка состояния об этом промолчит. Временный топик проверки с машины и запуски DAG удаляются; постоянный топик пробника сохраняется, а старые записи чистит Kafka. diff --git a/docs/adr/0001-stand-services.md b/docs/adr/0001-stand-services.md index 49309fc..0282380 100644 --- a/docs/adr/0001-stand-services.md +++ b/docs/adr/0001-stand-services.md @@ -1,6 +1,7 @@ # ADR 0001. Сервисы каркаса стенда -Дата: 30 июля 2026 года. Статус: принято. +Дата: 30 июля 2026 года. Статус: принято; ресурсный довод отозван +[ADR 0004](0004-resource-limits.md). ## Решение @@ -50,6 +51,13 @@ Prometheus читает встроенные точки метрик двух с компьютера. Отказ от triggerer, второго Postgres и внешних сборщиков удерживает стенд в пределе 3,4 ГБ. +Ресурсный довод предыдущего абзаца отозван +[ADR 0004](0004-resource-limits.md): предела 3,4 ГБ у стенда нет, вместо него +объявлено требование к машине. Сами решения остаются в силе по остальным +основаниям, названным выше. Отказ от внешних сборщиков вдобавок частично +пересмотрен [ADR 0002](0002-monitoring-scope.md): сборщик метрик Kafka нужен +ради отставания чтения. + Тома `clickhouse_*_data` хранят данные keeper и двух нод ClickHouse. `kafka_data`, `postgres_metadata_data`, `superset_home`, `prometheus_data` и `grafana_data` хранят состояние своих сервисов. `airflow_logs` хранит журналы, diff --git a/docs/adr/0004-resource-limits.md b/docs/adr/0004-resource-limits.md index bb20093..648ec58 100644 --- a/docs/adr/0004-resource-limits.md +++ b/docs/adr/0004-resource-limits.md @@ -13,12 +13,13 @@ Проверка суммарного потребления памяти уходит из `scripts/stand-smoke.sh`. Вместе с ней уходят зависящая от её сообщения проверка в `tests/docs-guards.sh` и описание порога в README. Взамен `make smoke` спрашивает у Docker, не убивало -ли ядро какой-нибудь долгоживущий контейнер за нехватку памяти и не включалась -ли политика перезапуска. Это не бюджет в новой одежде: порога у проверки нет, -она отвечает «да или нет». +ли ядро что-нибудь в долгоживущих контейнерах за нехватку памяти и не включалась +ли политика перезапуска. Прежний бюджет она не заменяет: числа, с которым +что-то сравнивают, у неё нет вовсе — ответ либо «убивало», либо «нет». -Нодам ClickHouse — по 4 ГиБ вместо гигабайта. Остальные `mem_limit` не -меняются. +Нодам ClickHouse — по 4 ГиБ вместо гигабайта. Дальше в этом тексте предел +памяти контейнера (`mem_limit`) называется коробкой: сервер живёт внутри неё и +о самой машине ничего не знает. Остальные `mem_limit` не меняются. Числа, выставленные ради прежнего бюджета, остаются в силе, но лишаются статуса решения: `KAFKA_HEAP_OPTS`, `shared_buffers`, `max_connections`, @@ -51,35 +52,47 @@ своей коробки, сумма при этом была в норме — 3181 МиБ из 3242,5, — а красным стенд сделал пробник. Механизм оповещения работает и без неё. -Замена не возвращает бюджет через заднюю дверь. Дыра, которую она закрывает, -другая: убитый за память контейнер Docker поднимает сам, через полминуты его -проверка состояния снова зелёная, и `make smoke` докладывает, что всё хорошо — -хотя нода умирала и потеряла всё, что держала в памяти. Новая проверка -спрашивает у Docker факт, а не величину, и потому нечего подгонять и незачем -пересматривать при росте стенда. +Новая проверка — не тот же бюджет под другим именем. Она закрывает другую дыру: +убитый за память контейнер Docker поднимает сам, через полминуты его проверка +состояния снова зелёная, и `make smoke` докладывает, что всё хорошо — хотя нода +умирала и потеряла всё, что держала в памяти. Проверка спрашивает у Docker +факт, а не величину, поэтому её нечего подгонять и незачем пересматривать, +когда стенд растёт. Гигабайт на ноду был неработоспособен, и причина не та, что предполагал -issue #21. Праздная нода держит `VmRSS` 846 МиБ, из которых 530 МиБ — страницы -её собственного бинарника: файл `clickhouse` весит 790 МБ, и ядро подгружает -куски по мере обращения к ним. Учёта памяти в этом почти нет, `MemoryTracking` -всего 113 МиБ, но отказ по превышению сравнивается с RSS, куда чистые страницы -кода входят. Значит из гигабайта ноде оставалось около 350 МиБ рабочего места, -а RSS полз вверх просто потому, что каждый новый прогон пробника трогал новый -код: 531 → 774 МиБ за сессию. Рестарт возвращал число назад, и это выглядело -как утечка. Заводские кэши тут ни при чём: в коробке на гигабайт ClickHouse сам -понижает каждый до 512 МиБ, а на стенде без данных они и вовсе пусты. +issue #21. У праздной ноды около 530 МиБ RSS — это страницы её собственного +бинарника: файл `clickhouse` весит 790 МБ, ядро подгружает куски по мере +обращения к ним, а отказ по превышению памяти сравнивается с RSS, куда такие +страницы входят. Учёта сервера в них почти нет: `MemoryTracking` при этом +всего 113 МиБ. Это постоянная часть расхода: меньше неё нода не занимает +никогда. Из гигабайта на саму работу оставалось около 350 МиБ. -Лимит для ClickHouse — не потолок, а вход в самонастройку. Сервер берёт 0,9 от -того, что видит, под общий предел памяти и 0,5 — под размеры кэшей и пороги -сброса на диск. Поэтому «поднять потолок ничего не стоит» верно для девяти -сервисов, которые лимит не читают, и неверно для двух нод: от коробки зависит, -где у них срабатывают предохранители. Снять лимит с нод по той же причине -нельзя — без коробки каждая считает свои доли от всей машины, и память у -машины кончится раньше, чем сервер решит экономить. Панель «не упёрлись ли -ноды в память» из ADR 0002 тоже требует знаменателя. Сумма всех потолков после -правки больше восьми гигабайт, и это не противоречие: `mem_limit` — потолок, а -не бронь, Docker ничего не резервирует, в покое стенд занимает около 3,2 ГБ. -Восемь гигабайт нужны не сумме потолков, а работе с запасом. +Дальше нода набирает рабочую память, и вместе с постоянной частью та перестаёт +помещаться. За обычную сессию стенда замерено: страницы бинарника 523 → 531 +МиБ, то есть стоят на месте, а рабочая память 489 → 723 МиБ. Инцидент 31 июля +имел ту же форму — 531 → 774 МиБ: та же постоянная часть плюс наросшая работа. +Рестарт возвращал число назад, и это выглядело как утечка. Отчего именно растёт +рабочая память, здесь не разбирается — для решения хватает того, что одна +только постоянная часть занимала больше половины коробки. Заводские кэши тут +ни при чём: в коробке на гигабайт +ClickHouse сам понижает каждый до 512 МиБ, а на стенде без данных они и вовсе +пусты. + +Для ClickHouse коробка — не просто верхняя граница: сервер настраивает себя по +её размеру. При запуске он читает её и берёт 0,9 под общий предел собственной +памяти, а 0,5 — под размеры кэшей и под порог, после которого промежуточные +данные запроса уходят на диск. Поэтому «поднять предел ничего не стоит» верно +для девяти сервисов, которые своей коробки не читают, и неверно для двух нод: от +её размера зависит, когда сервер начинает себя ограничивать. Снять лимит с нод +по той же причине нельзя — без коробки каждая нода считает свои доли от всей +машины, и память кончится у машины раньше, чем сервер решит экономить. Панель +«не упёрлись ли ноды в память» из ADR 0002 без коробки тоже не работает: +упираться становится не во что. + +Сумма всех пределов после правки больше восьми гигабайт, и это не противоречие. +`mem_limit` — верхняя граница, а не резерв: Docker ничего не откладывает +заранее, и в покое стенд занимает около 3,2 ГБ. Восемь гигабайт нужны не сумме +пределов, а работе с запасом. Остальные девять лимитов не меняются, потому что ни один из них ни разу не сработал. Снять их скопом — то же изменение без свидетельств, каким они были @@ -111,8 +124,8 @@ has limited RAM`; `cache_size_to_ram_max_ratio` = 0,5; праздный серв `max_bytes_ratio_before_external_sort` = 0,5. Тяжёлый `GROUP BY` в коробке не получает отказ, а уходит на диск и досчитывается; отказ по памяти остаётся для того, что на диск не сбрасывается. Учебный сюжет «тяжёлый запрос — симптом на -графике памяти» из ADR 0002 выглядит полкой и дисковым вводом-выводом, а не -обрывом. +графике памяти» из ADR 0002 выглядит ровной линией у верхней границы и +дисковой нагрузкой, а не обрывом. cgroup v2: `docker stats` показывает `memory.current` минус неактивные файловые страницы. На праздном контейнере — 1056 МБ при 779 МБ файловых и 234 МБ @@ -120,8 +133,8 @@ cgroup v2: `docker stats` показывает `memory.current` минус не `apache/kafka:4.3.1`: `kafka-server-start.sh` при пустой `KAFKA_HEAP_OPTS` ставит `-Xmx1G -Xms1G`, а по размеру коробки кучу не подбирает. Поэтому -переменную не убираем: без неё куча стала бы больше, а не меньше, и с нынешней -коробкой на гигабайт это был бы прямой OOM-kill. +переменную не убираем: без неё куча стала бы больше, а не меньше, и в нынешней +коробке Kafka на гигабайт ядро убивало бы контейнер за нехватку памяти. На стенде с новой коробкой в 4 ГиБ то же поведение подтвердилось: `max_server_memory_usage` = 3,60 ГиБ, в журнале ноды `Lowered mark cache size diff --git a/scripts/stand-smoke.sh b/scripts/stand-smoke.sh index 468cf92..2e14ef1 100755 --- a/scripts/stand-smoke.sh +++ b/scripts/stand-smoke.sh @@ -592,15 +592,18 @@ check_superset() { # Контейнер, убитый ядром за нехватку памяти, Docker поднимает сам, и через # полминуты его проверка состояния снова зелёная: о смерти она не расскажет. -# Поэтому спрашиваем у Docker два факта — убивало ли контейнер ядро и включалась -# ли политика перезапуска. Ручной `docker compose restart` счётчик не трогает, -# так что документированный перезапуск нод проверку не роняет. Порога здесь -# нет: это «да или нет», а не бюджет памяти (ADR 0004). +# Поэтому спрашиваем у Docker два факта — убивало ли ядро что-нибудь в контейнере +# за память и включалась ли политика перезапуска. Ручной `docker compose restart` +# счётчик не трогает, так что документированный перезапуск нод проверку не +# роняет. Порога здесь нет: это «да или нет», а не бюджет памяти (ADR 0004). +# +# Два факта берутся одним `--format` и разбираются образцом — тем же приёмом, +# что и состояние с проверкой здоровья выше. check_containers_survived() { local container_id local service local state - local hurt=0 + local problems=0 for service in "${LONG_LIVED_SERVICES[@]}"; do container_id="$(compose ps --all --quiet "$service" 2>/dev/null || true)" @@ -611,22 +614,24 @@ check_containers_survived() { state="$(docker inspect --format '{{.State.OOMKilled}}/{{.RestartCount}}' "$container_id" 2>/dev/null || true)" case "$state" in false/0) ;; + # OOMKilled поднимается и когда ядро убило процесс внутри живого + # контейнера, поэтому говорим про процесс, а не про контейнер. true/*) - fail "контейнер ${service} был убит из-за нехватки памяти" - hurt=1 + fail "в контейнере ${service} ядро убило процесс из-за нехватки памяти" + problems=$((problems + 1)) ;; false/*) fail "контейнер ${service} перезапускался, счётчик Docker — ${state#*/}" - hurt=1 + problems=$((problems + 1)) ;; *) fail "Docker не рассказал о состоянии контейнера ${service}" - hurt=1 + problems=$((problems + 1)) ;; esac done - if [[ "$hurt" -eq 0 ]]; then - pass 'ни один долгоживущий контейнер не был убит по памяти и не перезапускался сам' + if [[ "$problems" -eq 0 ]]; then + pass 'ни в одном долгоживущем контейнере ядро не убивало процессы за память, и никто не перезапускался сам' fi }