Re[13]: WA: 3 млн tcp соединений на одном сервере
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 06.08.20 13:33
Оценка: 5 (1)
Здравствуйте, Pzz, Вы писали:

Pzz>А что, у ерланга вынимание из очереди пробегается по всей памяти очереди? Потому что иначе трудно понять, почему временное достижение очередью размера кэша настолько замедляет разгребание очереди, причем делает это "навсегда", что программа уже не восстанавливается.


У него это выглядит так: оператор receive может получать шаблон или нет.
Если шаблона нет, просто хватается сообщение из головы очереди.
Если шаблон есть, идёт перебор начиная с головы пока не встретится сообщение, соответствующее шаблону. Точнее, шаблонов может быть несколько, они проверяются по приоритетам.
Если такого сообщения (не соответствующего ни одному из предложенных шаблонов) в очереди нет, то процесс переходит в спячку, а при поступлении новых пробуждается на проверку поступивших. У спячки может быть задан таймаут.
Можно посмотреть, например, здесь.

Нормальный цикл процесса выглядит примерно так: я переписываю для ясности на C-подобный синтаксис:

work() {
  Message = receive(); // из головы очереди, вариантов нет
  if (Message LIKE первый шаблон) { ... }
  else if (Message LIKE второй шаблон) { ... }
  ...
  work(); // циклов в коде нет, всё пишется через рекурсию, интерпретатор умеет хвостовую рекурсию
}


в этом варианте всё ещё относительно шустро (с поправкой на динамику).

Проблемы начинаются, если делается синхронный вызов. Каждый такой вызов это:

1. Клиент заворачивает свой запрос — далее Request — в кортеж с тегом "это синхронное сообщение от меня"; в его синтаксисе это, чуть упрощая, Message переводится в {'$call', pid(), Request, make_ref()} и это отправляется серверу. make_ref() это функция генерации уникального значения (считай GUIDʼом, но конструируется иначе).

2. Сервер, получив нечто соответствующее такому шаблону {'$call', Client, Request, Ref}, исполняет запрос и отдаёт свой ответ, посылая процессу Client сообщение {'$reply', Ref, Reply}, где Reply — его ответ.

3. Клиент в это время находится в ожидании ответа, выполняя код типа

Ref = make_ref();
send(ServerPid, {'$call', pid(), Request, Ref});
Resp = receive(       //  <-- вот тут сидим в ожидании ответа
  {'$reply', Ref, *}, //  <-- это шаблон для поиска в очереди
  5000);              //  <-- таймаут, по умолчанию это 5 секунд, пишется в миллисекундах


И вот тут начинаются проблемы. Представим себе, что в очереди 10000 сообщений. receive() всегда работает одинаково: начинает перебирать с головы очереди. Как-то ему сказать, что сейчас ещё точно ответа нет? Не положено. Значит, послали запрос и начинаем перебирать эти 10000 сообщений... хотя твёрдо известно, что в этом нет смысла.
В худшем случае, если делается один синхронный вызов на каждое входящее сообщение, время работы становится O от квадрата длины очереди — что мы и наблюдали в полный рост.

В общем случае это решается несколькими вариантами:

1. Перед посылкой запроса узнать позицию хвоста очереди, а в receive() передать её, чтобы итерирование в поиске ответа начиналось с этой позиции.
Это оптимизация на частный случай, и она сделана, кажется, в 14-м релизе, но она 1) завязана на получение этого самого Ref, а поэтому не работает, например, при отправке в TCP порт (это то, что как раз обходится залезанием в потроха в RabbitMQ), и вообще для всех случаев за пределами стандартного вызова стиля gen_call; 2) делается не программистом, а компилятором на анализе кода, и поэтому отклонение от идеального шаблона make_ref() — send() — receive() сразу ломает логику и выключает оптимизацию. Дать возможность программисту управлять этим они отказались.

2. Сделать таки хотя бы 2 очереди, честные FIFO, и пусть обычный send() отправляет в 0, а спецпосылки и ответы на синхронные запросы — в 1; пусть синтаксис receive не менять, но чтобы очередь 1 всегда проверялась первой. Это даст универсальное решение для таких ожиданий и не только; я писал в предыдущем, что у нас при такой перегрузке даже нормальный call() тормозил и выбивался по таймауту из-за перегрузки входа (обычный разбор идёт строго последовательно).

N>>2) Если ему хоть иногда надо делать синхронные вызовы (gen_server:call) и соответственно сразу ждать отвёт — всё, суши вёсла: из накопления входной очереди выше 10-20K сообщений он не способен уже выйти. Граница неточная, но, похоже, связана с размером кэша процессора. Временное переполнение, безвредное в других условиях, становится фатальным (надо только рубить процесс).


Pzz>Вот по таким оговоркам сразу видно профессионала. Любителю такая гипотеза в голову не придет (это искренний комплимент, а не скрытая ирония).


Спасибо на самом деле я уже за давностью не помню, это я предположил или кто-то из команды, но я запомнил и оно, по экспериментам, подтвердилось.
При такой квадратичной зависимости от длины очереди дополнительный рывок торможения в 10-15 раз за счёт вылета из кэша оказался смертельным для задачи.
The God is real, unless declared integer.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.