Эта статья была первоначально опубликована на 99designs Tech Blog
На 99designs мы активно используем Varnish, чтобы сделать наше приложение очень быстрым, а также для выполнения простых и простых задач, не вызывая наш тяжелый контрастный стек PHP. В результате наша конфигурация Varnish довольно сложна, она содержит более 1000 строк VCL и нетривиальное количество встроенных C.
Когда мы начали видеть регулярные segfaults, это было довольно безопасное предположение, что один из нас глупо писал код на C. Итак, как вы можете отследить временную ошибку в системе, подобной Varnish? Присоединяйтесь к нам по кроличьей норе …
Получить дамп ядра
Первым шагом является изменение вашей производственной среды, чтобы предоставить вам полезные дампы ядра. В этом есть несколько шагов:
-
Прежде всего, настройте ядро для предоставления дампов ядра, установив несколько sysctl:
echo 1> / proc / sys / kernel / core_uses_pid echo 2> / proc / sys / fs / suid_dumpable mkdir / mnt / cores chmod 777 / mnt / cores echo / mnt / cores / core> / proc / sys / kernel / core_pattern
По порядку это:
- Говорит ядру добавлять pid к файлам ядра, чтобы упростить объединение ядер с журналами
- Сообщает ядру, что suid-двоичным файлам разрешено выгружать ядро
- Создает место для хранения ядер в эфемерном хранилище AWS (если вы, как и мы, используете EC2)
- Сообщает ядру записать туда файлы ядра
-
Сделав это, и пока нет известного способа вызвать ошибку, поиграйте в игру ожидания.
-
Когда лак взрывается, время показа. Скопируйте файл ядра вместе с общим объектом, который испускает лак из компиляции VCL (находится в
/var/lib/varnish/$HOSTNAME
), в экземпляр разработки и позвольте начать отладку.
Найдите место аварии
Если у вас есть доступ к отличному LLDB из проекта LLVM, используйте его. В нашем случае, чтобы заставить его работать на Ubuntu 12.04, необходимо обновить половину системы, что приведет к созданию среды, слишком непохожей на рабочую.
Если вы проводите много времени в отладчике, вы, вероятно, захотите использовать помощника, такого как gdbinit или voltron для fG !, Чтобы сделать вашу жизнь проще. Я использую voltron, но из-за некоторой неуклюжести в gdb API, сразу же столкнулся с некоторыми ошибками .
Наконец, среда отладки работает, пора копаться в аварии. Ваша ситуация будет отличаться от нашей, но вот как мы недавно отладили такую проблему:
Отладка дампа ядра с помощью вольтрона
Как вы можете видеть на панели code
, ошибочная инструкция — mov 0x0(%rbp),%r14
, пытающаяся загрузить значение, на которое указывает RBP
в r14
. Глядя в регистр, мы видим, что RBP
равен NULL.
Изучив источник, мы видим, что ошибочная подпрограмма является встроенной, и что компилятор похитил RBP (базовый указатель для текущего стекового кадра), чтобы использовать его в качестве хранилища аргументов для встроенной подпрограммы.
Оскорбительный код сборки
Особый интерес представляет эта часть:
0x000000000045a7c9 <+ 265 >: mov 0x223300 (% rip ),% rbp # 0x67dad0 <pagesize_mask>
0x000000000045a7d0 <+ 272 >: not % rbp 0x000000000045a7d3 <+ 275 >: and 0x10 (% r14 ),% rbp 0x000000000045a7d7 <+ 279 >: cmpb $0x0 , 0x223303 (% rip ) # 0x67dae1 <opt_junk>
=> 0x000000000045a7de <+ 286 >: mov 0x0 (% rbp ),% r14 0x000000000045a7e2 <+ 290 >: mov 0x28 (% r14 ),% r15
Который простым языком:
- Загружает относительный адрес
rip
вrbp
(pagesize_mask) - Инвертирует
rbp
поразрядно - Выполняет поразрядно и против 16 байтов в структуре, на которую указывает
r14
, (mapelm->bits
) - Бессмысленно проверяет, является ли pagesize_mask
NULL
- Пытается загрузить адрес, на который указывает
rbp
вr14
, чтоrbp
r14
.
Который испускается:
static inline void arena_dalloc_small(arena_t *arena, arena_chunk_t *chunk, void *ptr, arena_chunk_map_t *mapelm) { arena_run_t *run; arena_bin_t *bin; size_t size; run = (arena_run_t *)(mapelm->bits & ~pagesize_mask); assert(run->magic == ARENA_RUN_MAGIC); bin = run->bin; // XXX KABOOM size = bin->reg_size;
Теперь мы знаем, что ошибка вызвана структурой mapelm
с членом bits
установить на ноль; но почему мы получаем эту сломанную структуру с мусором в
Это?
Копаем глубже
Так как эта функция объявлена встроенной, она фактически свернута в вызывающий фрейм. Единственная причина, по которой он фактически отображается как в обратном следе, заключается в том, что в данных отладки DWARF присутствует место вызова.
Мы можем определить значение, определив его местоположение из вышестоящей сборки, но проще перейти к следующему вышестоящему кадру и проверить, что:
(gdb) frame 1 #1 arena_dalloc (arena=0x7f28c4000020, ptr=0x7f28c40008c0, chunk=0x7f28c4000000) at jemalloc_linux.c:3939 3939 in jemalloc_linux.c (gdb) info locals pageind = <optimized out> mapelm = 0x7f28c4000020 (gdb) p *mapelm $3 = {link = {rbn_left = 0x300000001, rbn_right_red = 0x100002fda}, bits = 0} (gdb)
Так что это похоже на элемент в красном черном дереве с двумя соседями и нулем для элемента bits
. Давайте дважды проверим:
(gdb) ptype *mapelm type = struct arena_chunk_map_s { struct { arena_chunk_map_t *rbn_left; arena_chunk_map_t *rbn_right_red; } link; size_t bits; } (gdb) ptype arena_run_t type = struct arena_run_s { arena_bin_t *bin; unsigned int regs_minelm; unsigned int nfree; unsigned int regs_mask[1]; } (gdb)
Чего ждать?
Оглядываясь назад, чтобы получить наши ориентиры:
run = (arena_run_t *)(mapelm->bits & ~pagesize_mask);
Код пытается сгенерировать указатель на эту структуру прогона арены, используя количество битов в структуре mapelm И против обратного pageizeize_mask, чтобы найти начало страницы. Поскольку биты равны нулю, это начало нулевой страницы ; нулевой указатель.
Этого достаточно, чтобы увидеть, как он рушится, но не дает нам большого понимания, почему. Пойдем копать.
Оглядываясь назад на фрагмент кода, мы видим утверждение, что magic
член структуры arena_run_t является правильным, поэтому с этим известным мы можем искать другие структуры в памяти. Быстро появляется grep:
./lib/libjemalloc/malloc.c:# define ARENA_RUN_MAGIC 0x384adf93
pagesize_mask
— это просто размер страницы -1, что означает, что любой адрес побитового И против инверсии pageize_mask даст вам адрес в начале этой страницы.
Поэтому мы можем просто найти на каждой записываемой странице в памяти магическое число с правильным смещением.
.. или мы можем?
typedef struct arena_run_s arena_run_t; struct arena_run_s { #ifdef MALLOC_DEBUG uint32_t magic; # define ARENA_RUN_MAGIC 0x384adf93 #endif /* Bin this run is associated with. */ arena_bin_t *bin; ...
Магическое число и магический член структуры (удобно расположенные как первые 4 байта каждой страницы) существуют только при наличии отладочной сборки.
Кроме того: можем ли мы злоупотреблять LD_PRELOAD
для получения прибыли?
На этом этапе все признаки указывают либо на двойное освобождение в реализации пула потоков лака, что приводит к пустому сегменту ( bits
== 0), либо на ошибку в его библиотеке выделения памяти jemalloc.
Теоретически, должно быть довольно легко исключить jemalloc путем замены другой реализации библиотеки malloc. Мы могли бы сделать это, поставив, скажем, tcmalloc, перед его разрешающей способностью символов, используя LD_PRELOAD
:
Мы добавим:
export LD_PRELOAD=/usr/lib/libtcmalloc_minimal.so.0
в /etc/varnish/default
и сбросить лак. Затем уберите все старые файлы ядра, подождите (и сравните результаты!)
Однако в нашем плане есть недостаток. Более старые версии лака (помните, что мы находимся в дистрибутиве Ubuntu LTS) продают копию jemalloc и статически связывают ее, что означает, что символы free
и malloc
разрешаются во время компиляции, а не во время выполнения. Это означает, что для нас нелегко выполнить предварительную загрузку.
Восстановление лака
Простое решение не сработает, поэтому давайте сделаем неловкое: восстановите лак!
apt-get source varnish
Возьмите копию источника лака и свяжите его с tcmalloc. Однако перед этим я удалил lib/libjemalloc
и использовал grep, чтобы удалить все ссылки на jemalloc из кодовой базы (в основном это были лишь некоторые изменения в скрипте configure и make-файлах)
и затем добавьте -ltcmalloc_minimal
в CFLAGS
перед -ltcmalloc_minimal
. Кроме того, пакеты ubuntu для tcmalloc поставляют /usr/lib/libtcmalloc_minimal.so.0
но не /usr/lib/libtcmalloc_minimal.so
, что означает, что компоновщик не может их найти. Мне пришлось вручную создать символическую ссылку.
С этим новым лаком в производстве мы еще не видели того же сбоя, поэтому кажется, что это была ошибка в jemalloc, вероятно, неприятное взаимодействие между libpthread и libjemalloc (сбой был последовательно при инициализации потока).
Попробуй сам?
Будем надеяться, что нет. Но если вы делаете много взлома Varnish с помощью пользовательских расширений, то можно ожидать случайных ошибок C. В этом посте вы познакомились с хитрой ошибкой Varnish, в которой вы познакомились с инструментами и хитростями, связанными с отладкой похожих волосатых ошибок.
Если вы возитесь с voltron, вы можете найти мою конфигурацию voltron и скрипт tmux, которые я использую для настройки своей среды, полезной отправной точкой.