© Сериал "Настоящий детектив"
© Сериал "Настоящий детектив"

Привет! Меня зовут Алексей, я работаю в команде Антиспама Почты Mail.ru, и сегодня хочу поделиться интересным опытом нашей команды по поиску проблем на бою. 

Многие из нас любят детективные истории: интрига, драйв, захватывающий сюжет, который держит в напряжении до самого конца, когда раскрывается загадка. Ровно такие же ощущения испытываешь, когда разбираешься с проблемами на проде. 

Этот пост — детектив со всеми его элементами: загадочная история с превышением времени до таймаута, круг подозреваемых (среди которых сервисы, прокси и сеть), команда разработчиков и админов - сыщиков, собирающих и изучающих улики и ставящих следственные эксперименты, и непредсказуемая развязка.  

Завязка

Представляю вам главных героев нашей истории — сервис А и сервис Б. Отношения между ними можно описать следующим образом: сервис А получает от клиента сообщение, дальше через локальную прокси (стоит на той же машине, что и сервис А) отправляет его сервису Б по сети и с нетерпением ожидает ответа в течение установленного таймаута. Сообщения от сервиса А могут быть различного объема — от сотен килобайт до десятков мегабайт. Ответ же от сервиса Б всегда скудный и сухой, и укладывается в несколько десятков килобайт. 

Все было гладко в этом общении, пока в какой-то момент не сработал алерт, что сервис Б начал запаздывать с ответом и перестал укладываться в таймаут. С течением времени такие алерты стали появляться все чаще и чаще. Взволнованный сервис А обратился в детективное агентство с целью провести расследование, почему сервис Б не хочет с ним общаться.  

Ложный след

Итак, мы представили действующих лиц и описали завязку. Настало время кого-то подозревать. Первым очевидным подозреваемым стал сервис Б. 

Опишем его портрет: Б — большой сервис (в простонародье – «монолит»). Его деятельность можно сравнить с обязанностями рецензента научной работы: принять «работу», проанализировать (запуск бизнес-логики) и выдать свое заключение по ней. 

Был проведен допрос сервиса Б через grep логов. Обнаружилось, что в подавляющем большинстве случаев подозреваемый работает корректно, но иногда наблюдались отклонения в поведении — аномалии, связанные со временем работы. Детальное изучение аномалий показало, что проблема проявляется только на сообщениях, объем которых измеряется десятками мегабайт. На первый взгляд, самая очевидная причина — сервис зависает где-то в недрах бизнес-логики и не укладывается в отведенный таймаут. 

И каково же было наше удивление, когда мы все так же по логам обнаружили, что львиная доля времени тратится на чтение сообщения от сервиса А. Это было первым и далеко не последним сюрпризом в нашем расследовании. 

Число подозреваемых стало увеличиваться:  

  • Сервис Б некорректно читает сообщения;

  • Прокси некорректно отправляет данные;

  • Проблема с сетью.

Проблемы во взаимодействии между сервисом А и прокси мы сразу отмели: логи подтверждали, что на этом участке цепи тратится минимум времени.

Допрос подозреваемых

Мы решили до конца разобраться с сервисом Б, прежде чем переходить к другим подозреваемым. Для этого нам пришлось более плотно познакомится с его серверной частью. 

Сервис Б написан на С++, серверная часть однопоточная, для приема сообщений использует под капотом boost::asio. Сервер принимает соединение от клиента, в течение отведенного таймаута асинхронно вычитывает сообщение (сообщение считываем chunk-ами и складываем их в tmpfs, чтобы долго не блокироваться и давать возможность вычитывать данные из других клиентов). После этого сообщение передается на обработку воркеру. 

Было выдвинуто несколько гипотез, но лишь одна казалась состоятельной: размер буфера выбран неэффективно. Это вызывает множество прерываний и загружает процессор, что в свою очередь сказывается на производительности системы и приводит к росту таймаутов.  

На dev мы не смогли воспроизвести исходную проблему, поэтому попытались сделать это на машине в песочнице в изолированной боевой сети. Все было тщетно: мы никак не могли увидеть торможение сервиса Б на чтении больших сообщений. 

Тогда мы взяли изолированный пул боевых машин (песочница) и на них стали увеличивать размер буфера для считывания chunk-a — но время считывания значимо не изменялось. Смущал еще и тот факт, что прокси с тем же серверным механизмом бодро считывала сообщение от сервиса А. Поэтому сервис Б был вычеркнут из списка подозреваемых.

Следующей на очереди была прокси, но ее код был настолько тривиальным, что не вызывал подозрений, и интерес следствия к ней быстро остыл. 

Оставалась сеть.

Загадка сети

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

Сервис А получает сообщение от клиента по сети и отправляет его в локальную прокси, которая, как мы помним, стоит на той же машине, что и сервис А. И вот самый напряженный момент: прокси отправляет сообщение в сервис Б. Сервис Б расположен в разных ДЦ, при этом часть его инстансов крутилась на железе, а часть работала в K8S, что вносило еще больше энтропии в расследование. (К слову, сейчас сервис Б к нашему превеликому восторгу уже целиком расположился в k8s: 1200 подов сплошного наслаждения, раскатить которые можно по щелчку пальцев).

Но вернемся к делу. Сейчас читатель может взять паузу и подумать, что бы он сделал в этой ситуации. 

Сеть. Сужение круга подозреваемых

Итак, мы принялись локализовывать проблему.

  1. По логам выяснили, что проблема общая. Неважно, крутится ли сервис в k8s или на железе: мы видим торможение на считывании сообщения и там, и там. Это значит, что из рассмотрения можно выкинуть оверхед виртуализации на работу с сетью. 

  2. Проверили, нет ли корреляции проблемы с ДЦ. Корреляции обнаружено не было: проблема была во всех ДЦ, где был сервис Б. 

  3. Убедились, что есть сервера, на которых проблема воспроизводится (серверов с сервисом А около сотни, с Б — и того больше, так что было важно найти «проблемные сервера»).  

После всех этих этапов мы решили взять сервер с сервисом А, сервер с сервисом Б, на котором воспроизводится проблема, и начать более детальное исследование. 

Подчеркну важный момент, который был пропущен при тестировании в песочнице. Важно выбирать только сервера, на которых есть проблема. Впоследствии выяснилось, что сервера, которые были в песочнице, не были подвержены проблеме, именно поэтому мы на них ничего и не могли воспроизвести. 

Итак, в этой точке начинается новая глава нашего расследования. 

Отчаяние

Мы взяли два сервера, на которых наблюдали проблему, и начали думать, что делать. Первое, за что мы взялись, воспользовавшись iperf/iperf3, — это проверка скорости передачи данных между серверами. Результаты запуска не показали никаких проблем, скорость и канал были в норме. 

Мы еще раз вернулись к изучению логов и графиков, чтобы понять, не происходят ли торможения в определенное время (кучно) — например, большие пакеты приходят одномоментно в большом количестве, и мы упираемся в канал. Но и тут логи были неумолимы: проблема происходит постоянно, нет никаких закономерностей. Залезли в графики node_exportera и по ним убедились, что запас канала у нас достаточный. Проверили, что никаких аномалий с tcp тоже не наблюдается. 

Мы были близки к отчаянию.

Кульминация

В этот момент пришла идея вернуться к истокам и более детально изучить флоу тормозящего пакета. И я начал считать. Получалось следующее: сообщение в 20 МБ от сервиса А до прокси проходило почти мгновенно (еще бы, localhost), а вот от прокси до сервиса Б шло порядка двух секунд. То есть скорость нашего канала должна быть 10 МБ/сек или 80 Мбит/сек — и это в 21 веке, при пропускной способности 1 Гбит/сек…. Что-то тут не так. И это при том, что наши удачные запуски iperf проблем не обнаружили.

Следующим ключевым звеном в расследовании стало подтверждение гипотезы, что даже маленькие письма — например, в 1 МБ — будут так же медленно передаваться. Стало уже почти очевидно, что проблема не связана с размером передаваемого сообщения (на маленьких сообщениях мы укладывались в таймаут, и поэтому не видели проблему). 

Чтобы окончательно вычеркнуть из подозреваемых прокси и сервис Б, вместе с нашими коллегами-админами мы провели эксперимент: сгенерировали файл размером в 20 МБ и заслали его с одного сервера на другой с помощью nc. 

В этот момент я еще раз убедился, что неправильно проведенный эксперимент дорого обходится: сначала мы допустили ошибку, сохранив файл на жесткий диск, и при отправке уперлись в скорость диска, поэтому получили неверные цифры. Обнаружив эту оплошность, мы исправили ее, записав файл в shm (данная схема максимальна близка к той по которой происходит взаимодействие между нашими сервисами), и отправляли его уже оттуда. И вот он, момент истины: сервис Б точно не виноват — сообщение в 20 МБ передавалось почти две секунды.

Оставалось понять, что же не так с сетью. Мы начали снимать tcpdump и отфильтровали пакеты, которые относились к одному сообщению. Проанализировав tcpdump с помощью wireshark мы подтвердили, что скорость действительно низкая.  Параллельно изучая tcpdump, админы начали анализировать настройки TCP стека и заметили, что на машине, на которой крутился сервис А, была выключена опция “tcp_window_scaling” (на серверах Б она была включена), и включили ее. Мы повторили тест — сообщение передавалось почти мгновенно. Мы ликовали — тайна была раскрыта.

Немного про опцию “tcp_window_scaling”: она позволяет более эффективно утилизировать пропускную способность. По умолчанию размер окна не может превысить 65535 байт из-за ограничения TCP заголовка. Опция “tcp_window_scaling” позволяет работать с окном больше 65 КБ за счет добавления коэффициента «масштабирования» (масштабирование окна происходит за счет умножения коэффициента масштабирования на размер окна). Так, например, можно «отмасштабировать» окно вплоть до 1 ГБ. Чтобы «масштабирование» работало, нужно, чтобы опция была включена на стороне отправителя и получателя. Подробнее о том, на что именно влияет опция и как с ней работать, можно почитать тут: https://en.wikipedia.org/wiki/TCP_window_scale_option

wireshark. График window_size с отключенным tcp_window_scaling
wireshark. График window_size с отключенным tcp_window_scaling
wireshark. График window_size с включенным tcp_window_scaling
wireshark. График window_size с включенным tcp_window_scaling

Оставалось лишь включить эту ручку на всех серверах, что мы и сделали. В результате исправления проблемы сервис А начал получать от нашего «рецензента» — сервиса Б — на 55 тысяч больше «заключений» в сутки .

В заключение

  • Хочется поблагодарить всех, кто был причастен к расследованию — было реально интересно и драйвово!

  • Выводы для себя: пытайся воспроизвести проблему в dev, если не получилось — всегда бери машины, где ты видишь проблему. 

  • Iperf не всегда показывает проблему. Проведя исследования, мы смогли заметить на iperf деградацию, только когда сервис оказывался в сильно удаленных ДЦ, где был высокий rtt. 

Комментарии (24)


  1. acordell
    24.08.2022 11:15

    Спасибо, интересно было почитать! Не очень только уловил отчего же в самом начале не вылезла корреляции проблемы с определенными серверами сервиса А, где, собственно, и не была включена tcp_window_scaling?


    1. bralexey Автор
      24.08.2022 11:59

      Спасибо!

      В первую очередь начали исследовать сервисы, сервис А редко изменяется, а вот сервис Б обновляется довольно часто - поэтому сначала пошли в ту сторону. В сторону "железа" не смотрели до последнего, потому что изменения в конфигурации происходят крайне редко и по истории изменений никаких обновлений не было. Также в момент проблемы у нас было довольно много энтропии в системе: переход в кубер, постоянное обновления сервисов и т.п.

      Когда я рефлексировал на тему того как можно было быстрее выйти на исходную проблему у меня пришло в голову только одно - правильно ставить эксперименты и более пристально проверять гипотезы.


  1. Yokozuna
    24.08.2022 11:15

    Захватывающая история! А сколько по времени заняло решение проблемы?


    1. bralexey Автор
      24.08.2022 11:21

      Спасибо! Суммарно ушло порядка 2 недель с учетом походов по ложным следам. Когда плотно сели за исследование, проблему нашли где-то за 3 дня.


  1. logost
    24.08.2022 11:33
    +1

    Правильный вывод то так и не сделан - сервера должны разворачиваться тулами с заранее определённой конфигурацией. Никакой ручной конфигурации, тем более на prod серверах.


    1. bralexey Автор
      24.08.2022 12:04
      +2

      Конфигурирование через тулы - это тоже ручное действие (имею ввиду написание самого конфига). У нас все сервера наливаются через автоматику и ручной кастомизации на серверах не происходит (только на момент экспериментов, с принудительном откатом до оригинальной конфигурации). В данном случае опция была прописана некорректно в самом конфигурационном файле, который разливался автоматикой на сервера.

      P.S. Замечу, что конфигурация серверов не однородна по ряду причин, поэтому на части серверов были одни настройки, на части другие. После того как нашли проблему прошли по всем серверам и актуализировали опцию


    1. sunnybear
      24.08.2022 12:09
      +1

      Автоматика была когда то давно неверно настроена. Это и обнаружили


  1. QuAzI
    24.08.2022 13:37
    +1

    Не раскрыто кто садовник и доколе


    1. bralexey Автор
      24.08.2022 15:28

      Личность садовника не так важна :)

      Про проблему - имела место быть, сейчас все работает штатно.


  1. mvv-rus
    24.08.2022 21:02

    В давнее время сталкивался с проблемой медленной работы сети из-за фуркции масштабирования окна. Правда обстоятельства были несколько другие: масштабирование массово пошло на клиенты, начиная с Win7 (точнее, Vista), для которых MS переписала стек TCP/IP, а в Win2K3 (под которым тогда были основные серверы) старый стек имел ошибку, мешавшую ему нормально работать. Обновление MS выпустила оперативно, но пока его распространили, обходным решением было отключать масштабирование окна TCP на проблемных клиентах. Благо сеть была гигабитной локалкой, больших задержек в ней не было, доступ был все больше для чтения с диска/записи на диск (файл-сервер чаще всего), а диски были не столь скоростными, потому ограничение размера окна на реальную работу не влияло.
    Так что мысль начет окна передачи была бы одной из первых (где-то после проверки сети на потрею пакетов — скорость TCP к этому чувствительна).


  1. diver66
    25.08.2022 08:57

    Если проблема была в отключенном tcp_window_scaling, то почему вы не увидели того, что при тестировании через iperf скорость также не растет?


    1. Chupaka
      25.08.2022 09:04

      Возможно, iperf был многопоточным? Суммарно много медленных потоков показывают хорошую картинку — идём дальше...


    1. blind_oracle
      25.08.2022 09:17

      Вообще такое поведение больше похоже на небольшое начальное окно - при отправке небольшого пакета данных окно не успевает вырасти, а при проверке через iperf TCP-сессия держится дольше и окно увеличивается достаточно.


    1. bralexey Автор
      25.08.2022 13:00

      Этот момент у меня не получилось до конца раскопать, хотя я совершал несколько заходов. Возможно кто-то сможет подсказать здесь. Ниже приведу вывод iperf для разных конфигурация scaling. Как я писал, из того что получилось заметить, что iperf начинает подсвечивать наличие проблемы на географически удаленных ДЦ.

      Запуск iperf3 на серверах в билзлежащих ДЦ c tcp_window_scaling = 1

      [hostA~]# net.ipv4.tcp_window_scaling = 1 
      [hostA~]# iperf3 -c hostB -V 
      ....
      TCP MSS: 1468 (default) 
      [  4] local hostA port 38440 connected to hostB port 5201 Starting Test: protocol: TCP, 1 streams, 131072 byte blocks, omitting 0 seconds, 10 second test 
      Test Complete. Summary Results: 
      [ ID] Interval           Transfer     Bandwidth       Retr 
      [  4] 0.00-10.00  sec  1022 MBytes   857 Mbits/sec    0             sender 
      [  4] 0.00-10.00  sec  1019 MBytes   854 Mbits/sec                  receiver CPU Utilization: local/sender 5.1% (0.2%u/4.9%s), remote/receiver 6.6% (0.5%u/6.1%s)

      Запуск iperf3 на серверах в билзлежащих ДЦ c tcp_window_scaling = 0

      [hostA ~]# sysctl  net.ipv4.tcp_window_scaling=0 
      net.ipv4.tcp_window_scaling = 0 
      [hostA ~]# iperf3 -c hostB -V 
      ...
      TCP MSS: 1468 (default) [
      [ 4] local hostA port 52814 connected to hostB port 5201 Starting Test: protocol: TCP, 1 streams, 131072 byte blocks, omitting 0 seconds, 10 second test
      Test Complete. Summary Results: 
      [ ID] Interval           Transfer     Bandwidth       Retr 
      [  4]   0.00-10.00  sec   913 MBytes   766 Mbits/sec    0             sender 
      [  4]   0.00-10.00  sec   913 MBytes   766 Mbits/sec                  receiver

      Как видно, незначительная деградация наблюдается, но цифры не бьются с реальной скоростью передачи.


      1. diver66
        25.08.2022 13:41

        У меня два предположения:

        1. iperf3 в обход системных настроек и все-таки включает tcp window scaling

        2. iperf3 выставляет начальное значение окна, достаточное чтобы прокачать гигабитный линк даже без scaling

        Это можно проверить если включить дебаг в iperf3 (он покажет какое окно выставляет) и/или посмотреть в дампе что там на самом деле улетает в сеть.


        1. diver66
          25.08.2022 13:50
          +1

          Нашел еще такое на просторах:

          There are no per-process APIs for sockets at all, just per-socket APIs and global kernel configurations.

          But you don't need to modify the scale settings directly. You just need to set the socket receive buffer size you want prior to connecting. Then the appropriate window scale is negotiated during the connect handshake. If you want mo window scaling! make sure your socket receive buffer is < 64k before connecting. In the case of accepted sockets, that is set on the listening socket.

          Это тоже можно увидеть в выхлопе дебага айперфа и косвенно в дампе.


        1. bralexey Автор
          25.08.2022 15:22

          Спасибо большое за идеи! Постараюсь в ближайшее время проверить эти гипотезы.


  1. blind_oracle
    25.08.2022 09:13

    Интересно как вы умудрились её выключить - если она уже лет 20 как включена по умолчанию...


    1. bralexey Автор
      25.08.2022 11:13

      Вот этот момент похоже останется тайной, покрытой мраком


  1. serge-sb
    25.08.2022 10:33

    Простая ошибка, когда таймаут обработки запускается сразу после того, как данные попали в процедуру отправки, а не реально были переданы. Нужно либо запускать таймаут после того, как пакет полностью уйдёт (типа flush перед взведением) или учитывать время, необходимое для передачи (или что-то ещё - всё индивидуально). Если я правильно понял (поправьте, если нет), вы подковырнули настройки передачи, чтобы время отправки опустить в рамки погрешности.


    1. bralexey Автор
      25.08.2022 11:33

      Не до конца уловил идею про "момент" запуска таймаута. В нашем случае таймаут ограничивает разрешенное время передачи всего пакета (например, на случай тупящей сети) - если этого не сделать, сервис начнет деградировать в производительности (уменьшится rps) потому что будет стоять в отправку данных.

      Если я правильно понял (поправьте, если нет), вы подковырнули настройки передачи, чтобы время отправки опустить в рамки погрешности. - в общем правильно. Подтюнили настройки TCP, чтобы "разогнать" пропускную способность.


  1. 3263927
    25.08.2022 17:43

    спасибо интересно! не знал о таком


  1. StSav012
    26.08.2022 08:52

    До меня так и не дошло, почему проблема проявлялась не на всех серверах с Б, хотя причиной были серверы без Б с А.


    1. bralexey Автор
      26.08.2022 11:25

      На самом деле проблема проявлялась на всех серверах с сервисом Б, но в момент начала исследования это было не очевидно, поскольку тайматили произвольные пары A и Б и таймауты происходили не массово (так как больших сообщений много меньше чем маленьких).