Долгий путь низкоуровневого дебага, чтобы найти закомментаренную строку кода, случайно ушедшую в продакшн

от автора

«Здравствуйте. Меня зовут Вячеслав, мне 22 года и я пхп программист. Я сижу на PHP с 18 лет.»… (копирайт не знаю чей, я не Вячеслав и мне давно не 22 года)
Да, у нас проект на PHP и мы столкнулись с тем, что на MySQL сервере стало появляться большое количество коннектов в состоянии Sleep.
А виноват в этом оказался код, работаюший с кроликом (RabbitMQ).
Ну а как связаны MySQL, RabbitMQ и закомментаренная строка вы узнаете код катом.

Архитектура нашей системы достаточно стандартная. Основная часть данных в MySQL. Имеется масштабируемый пул worker’ов на NodeJS. PHP процессы через RabbitMQ ставят задачи этим worker’ам и из того же кролика получает назад результаты.
В какой-то момент у нас стали возникать проблемы с тем, что PHP процессы (у наc Nginx + php-fpm) иногда не могут подключиться к MySQL. Есть проблема — нужно ее решать!
Стали смотреть на MySQL. Из аномального заметили, что show processlist показывает чрезмерно большое количество коннектов в состоянии sleep.

mysql> show processlist; +----------+--------------+---------------------+-----------------+-------------+--------+ | Id       | User         | Host                | db              | Command     | Time   | +----------+--------------+---------------------+-----------------+-------------+--------+ ............... | 16941988 | new_search   | 172.16.10.214:59412 | parts_nm_new    | Sleep       |  19667 | | 16941993 | new_search   | 172.16.10.213:58562 | parts_nm_new    | Sleep       |  19666 | ............... 

Это только часть вывода команды. Подобных процессов мы обнаружили более 100 штук и их количество со временем росло. Причем по Time видно, что в Sleep процессы висят очень давно.

Такого раньше не было и быть не должно.
Хорошо, тут мы видим IP PHP серверов (а PHP серверов у нас порядка 10 штук), с которых висели подозрительные коннекты.
Выбираем для исследований сервер 172.16.10.214 и идем смотреть, что там творится.
pm.max_requests у нас имеет не самое большое значение и по логике вещей, мы должны найти php-fpm процесс, который стартовал достаточно давно, т.к. в штатном режиме php-fpm процессы регулярно рестартуются мастер процессом.

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND ................ nginx     6627  3.6  2.6 961232 103940 ?       S    22:29   0:09 php-fpm: pool main_socket nginx     8434  2.7  2.5 956224 99416 ?        D    22:30   0:07 php-fpm: pool main_socket nginx     8449  2.9  2.0 958124 79312 ?        S    22:30   0:07 php-fpm: pool main_socket nginx    10798  0.0  1.9 954268 75696 ?        S    16:59   0:06 php-fpm: pool main_socket nginx    11412  0.0  2.6 961384 104128 ?       S    17:48   0:06 php-fpm: pool main_socket nginx    12173  0.0  2.0 960452 79664 ?        S    17:48   0:06 php-fpm: pool main_socket nginx    13260  0.0  2.6 955920 102360 ?       S    17:00   0:03 php-fpm: pool main_socket nginx    13663  0.0  1.1 953040 44324 ?        S    17:01   0:00 php-fpm: pool main_socket root     23213  0.0  0.1 939640  7768 ?        Ss   15:41   0:01 php-fpm: master process (/opt/php5/etc/php-fpm.main.conf) ................ 

И мы видим “залипшый” процесс (на самом деле их несколько) по достаточно старому времени запуска (START). Например, процесс с PID 10798 висит уже несколько часов.
Нам конечно же интересно понять, чем он там занимается. Цепляемся к нему strace’ом и видим:

strace -p 10798 Process 10798 attached - interrupt to quit recvfrom(10, 

ну а дальше тишина. Процесс чего-то от кого-то ждет. Как по дескриптору 10 понять, с чем от связан (файл, сокет) я не знаю. Наверное как-то можно. Буду благодарен, если в комментах подскажете, как это сделать. В будущем пригодится.
Изучив еще несколько подозрительных процессов на других серверах было выявлено, что все они висят на recvfrom.
Копаем дальше. Смотрим, какие коннекты держит процесс с PID 10798:

netstat -ap|grep 10798 tcp        0      0 v-php-ws2:47736             mongodb01:27017             ESTABLISHED 10798/php-fpm tcp        0      0 v-php-ws2:55686             memcached-vip:memcache      ESTABLISHED 10798/php-fpm tcp        0      0 v-php-ws2:59412             mysql-master:mysql          ESTABLISHED 10798/php-fpm tcp        0      0 v-php-ws2:54630             rabbitmq:amqp               ESTABLISHED 10798/php-fpm tcp        0      0 v-php-ws2:58901             mongodb02:27017             ESTABLISHED 10798/php-fpm udp        0      0 *:43387                     *:*                                     10798/php-fpm unix  2      [ ]         STREAM     CONNECTED     20141894 10798/php-fpm       /var/run/php-fpm-main.socket 

Что мы видим. Коннект с MySQL сервером ( mysql-master:mysql ) — это мы и так знаем. Именно этот коннект в состоянии Sleep мы видели на MySQL сервере.
Коннект к memcache сервером. Там у нас хранятся сессии пользователей.
Коннект с MongoDB — там у нас часть закешированных данных лежит. А два коннекта, т.к. кластер MongoDB из трех серверов, один из которых арбитр.
И коннект к кролику (RabbitMQ).
Исходя из логики приложения к кролику коннект происходит позже всего. И если бы мы зависли на другом соединении, то скорее всего до кролика дело бы не дошло. Поэтому основным подозреваемым стал коннект именно к кролику.
Идем на RabbitMQ и ищем подозреваемый коннект (54630 — это исходящий порт коннекта к кролику проблемного php-fpm, см. предыдущий вывод netstat):

netstat -anp|grep 54630 tcp        0      0 ::ffff:172.16.10.218:5672   ::ffff:172.16.10.214:54630  ESTABLISHED 2364/beam 

Что-то умнее, чем пробовать этот коннект убить с помощью killcx.sourceforge.net в голову не приходит, т.к. tcpdump говорит, что данные в этом коннекте не ходят:

killcx 172.16.10.214:54630 killcx v1.0.3 - (c)2009-2011 Jerome Bruandet - http://killcx.sourceforge.net/  [PARENT] checking connection with [172.16.10.214:54630] [PARENT] found connection with [172.16.10.218:5672] (ESTABLISHED) [PARENT] forking child [CHILD]  interface not defined, will use [eth1] [CHILD]  setting up filter to sniff ACK on [eth1] for 5 seconds [PARENT] sending spoofed SYN to [172.16.10.218:5672] with bogus SeqNum [CHILD]  hooked ACK from [172.16.10.218:5672] [CHILD]  found AckNum [2073273429] and SeqNum [1158333446] [CHILD]  sending spoofed RST to [172.16.10.218:5672] with SeqNum [2073273429] [CHILD]  sending RST to remote host as well with SeqNum [1158333446] [CHILD]  all done, sending USR1 signal to parent [8658] and exiting [PARENT] received child signal, checking results...          => success : connection has been closed !  

И тут случилось чудо, зависший php-fpm (по strace стало видно) весело побежал дальше работать и зависший коннект с MySQL сервером исчез!

Результат расследований выявил проблемный участок — почему-то PHP чего-то долго ждет (он же в recvfrom системной вызове висит) от кролика.
У нас логика приложения такая, что если от кролика нет ответа более 5 секунд, то ждать уже нет смысла. А тут зависания навечно.
Пошли смотреть в код и вот оно!!! Закомментированная строка, которая случайно ушла в продакшен:

       $cnn = new \AMQPConnection($conn); //        $cnn->setReadTimeout(5); 

Теперь мы знаем, как нам починиться. Но почему NodeJS иногда не отправляют в кролик результаты расчетов за 5 секунд? Нужно масштабироваться? Нет, там еще запас прочности гигантский.
И я знаю, почему NodeJS иногда не присылает результаты, но не знаю, из-за чего это происходит.
Если посмотреть логи RabbitMQ, то у нас там есть ошибки, которые мы давно не можем понять, из-за чего они происходят. Вот эти ошибки:

=ERROR REPORT==== 26-Nov-2015::06:03:11 === Error on AMQP connection <0.16535.3527> (172.16.10.95:44482 -> 172.16.10.218:5672, vhost: '/', user: 'php', state: running), channel 1: {amqp_error,frame_error,             "type 1, first 16 octets = <<0,60,0,40,0,0,28,80,97,114,116,75,111,109,77,97>>: {invalid_frame_end_marker,\n                                                                                2}",             none}  =ERROR REPORT==== 26-Nov-2015::06:20:12 === Error on AMQP connection <0.18878.3534> (172.16.10.95:44683 -> 172.16.10.218:5672, vhost: '/', user: 'php', state: running), channel 512: {amqp_error,frame_error,             "type 206, all octets = <<>>: {frame_too_large,16777216,131064}",             none} 

Вот эти два типа ошибок иногда, примерно пару раз в час возникают при работе NodeJS c RabbitMQ. На NodeJS возникает Exception и результат в очередь не записывается. Ну и соответственно PHP его не дожидается. PHP ждет 5 сек и идет дальше. Мы можем себе позволить потерять ответ (это не банковские транзакции). Но мы пытаемся понять природу этих ошибок. К сожалению, пока безуспешно.

Мораль: зачем я это все написал?

Это не первый случай, когда мне приходится опускаться до уровня strace. И мне иногда кажется, что могут быть более высокоуровневые инструменты выявления подобных проблем. При тестировании проблема не воспроизводится. Ловится она только на продакшене. Как бы вы поступили в моей ситуации?
Ну и вторая причина. Вдруг кто-то уже сталкивался с подобной проблемой RabbitMQ (см. ошибки выше)? Если знаете природу ошибок, то дайте знать. Можно в личку.

ссылка на оригинал статьи http://habrahabr.ru/post/271807/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *