Настройка core dump в Docker
Цель этого поста — дать общее руководство по включению и сбору core dumps для приложений, работающих в docker контейнере.
Настраиваем хост для сохранения дампов
Для начала надо сконфигурировать хостовую машину, чтобы сохранять такие дампы в нужное место. Нет, не в жопу.
ЧИТАТЬ ПЕРВЫМ В ТЕЛЕГРАМУниверсальный способ — задать шаблон core pattern. Шаблон определяет путь и информацию о процессе который наебнулся.
echo '/tmp/core.%e.%p' | sudo tee /proc/sys/kernel/core_pattern
Кратенько:
%e
— имя процесса
%p
— pid процесса
Более подробно о конфигурации core pattern можешь почитать в man-странице ядра Linux.
Как вариант, можно настроить host изнутри контейнера через CMD или ENTRYPOINT. Но контейнер в этом случае должен запускаться в privileged mode, что хуева с точки зрения безопасности.
Пример хуёвого приложения
#include <cstdlib>
void foo() {
std::abort();
}
int main() {
foo();
return 0;
}
После компиляции и запуска, приложение наебнется с ошибкой.
Пишем Dockerfile для этого приложения
FROM ubuntu:22.04
# Install tools
RUN apt update \
&& apt -y install \
build-essential \
gdb \
&& rm -rf /var/lib/apt/lists/*
# Build the application
COPY ./ /src/
WORKDIR /src/
RUN g++ main.cpp -o app
CMD ["/src/app"]
Тот кто в теме, сможет его прочитать и понять. Если не понятно, гугли и разбирай, качнешь свой девопсовый скиллз.
Запускаем контейнер с нужными опциями:
docker run \
--init \
--ulimit core=-1 \
--mount type=bind,source=/tmp/,target=/tmp/ \
application:latest
Разбираем опции:
--init
— гарантирует корректную обработку сигналов внутри контейнера
--ulimit
— устанавливает неограниченный размер core dump для процессов внутри контейнера
--mount
— монтирует /tmp
из хоста в контейнер, чтобы дампы, создаваемые внутри контейнера, были доступны после его остановки или удаления
Здесь важно: путь source
на хосте должен совпадать с тем, который задан в шаблоне core pattern.
После того как приложение наебнется, core dump будет сохранён на хостовой машине в директории /tmp
.
ls /tmp/core*
# /tmp/core.app.5
Анализируем дамп с помощью gdb
Такс, мы получили core dump и он у нас лежит на хостовой машине, но рекомендуется открыть его внутри контейнера. Контейнер должен быть из того же образа, в котором компилилось приложение.
Это поможет убедиться, что все зависимости (библиотеки и прочая хуитень) доступны.
docker run \
-it \
--mount type=bind,source=/tmp/,target=/tmp/ \
application:latest \
bash
Если в образе нет исходного кода, можно допом смаунтить исходники:
docker run \
-it \
--mount type=bind,source=/tmp/,target=/tmp/ \
--mount type=bind,source=<путь_к_исходникам_на_хосте>,target=/src/ \
application:latest \
bash
Теперь внутри контейнера запускаем:
gdb app /tmp/core.app.5
После загрузки дампа можно выполнить команду bt
(backtrace), чтобы увидеть трассировку стека:
(gdb) bt
#0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
#1 0x00007f263f378921 in __GI_abort () at abort.c:79
#2 0x000055f9a9d16653 in foo() ()
#3 0x000055f9a9d1665c in main ()
Команда поможет определить, где именно произошел факап.
Давай быстренько разберем ошибку.
#0
и #1
показывают, что процесс получил сигнал 6 (SIGABRT) и завершился через abort()
#2
— вызов произошёл внутри функции foo()
#3
— main()
вызвал foo()
В исходнике было:
void foo() {
std::abort();
}
То есть ошибка здесь не баг компиляции или рантайма, а намеренно вставленный вызов std::abort()
, который и приводит к аварийному завершению и генерации core dump.
Если у тебя docker-compose, то все флаги (--init
, --ulimit
, --mount
и т.д.) применимы и для него. То есть отладку можно легко адаптировать.
Хуй знает чё еще написать, завтра тему дебага продолжим, чет в конце года много траблшутинга навалило разнообразного.