Поступь хаоса: почему Apache не закрывает WebSocket

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

Вобщем, как и предсказывал утром - хаос моей жизни вносит свою лепту в моё существование. СПАСИБО ему за это! :)

Прикинулся сисадмином (вместо ерунды всякой типа "поработать").

На слабом серваке с мозжечком в 3.8 гига!

Процессы apache2 растут медленно и тихо. Сначала 10, потом 50. Потом 100, потом сервераку плохеет. Клиенты отваливаются, а на 3.8 ГБ RAM дело доходит до OOM-killer. И вот тогда смотрим в ss и видим картину маслом. Десятки соединений в состоянии CLOSE-WAIT. Все - на порт Mattermost.

CLOSE-WAIT означает: Mattermost отправил FIN. Apache получил его. Но сам ещё не вызвал close. Процесс как вампир - помереть не может, а жизнь высасывает.

Почему это происходит? Причин несколько и они складываются.

Первая

WebSocket - это долгоживущее соединение. Mattermost активно использует его для realtime. В Apache с MPM prefork каждый процесс обслуживает ровно одно соединение. Одно WebSocket - 1 процесс Apache. Занятый постоянно. При 350 активных клиентах Mattermost нужно 350 процессов Apache. Каждый около 22 МБ. Итого примерно 7.5 ГБ RAM только на Apache. При том что "на руках" 3.8 ГБ.

Вторая

Prefork и mod_proxy_wstunnel плохо закрывают соединения. Когда клиент уходит в офлайн - закрыл вкладку, потерял сеть, или Mattermost закрывает сессию - mod_proxy_wstunnel не всегда вызывает close на сокете. Процесс остаётся в CLOSE-WAIT и никогда не умирает. Потому что Timeout Apache - это таймаут на I/O-операцию, а не на всё соединение. Пока клиент шлёт WebSocket ping каждые 30 секунд, таймер сбрасывается.

Третья

Нехватка RAM ускоряет деградацию. При нехватке памяти ядро не может выделить буферы. Apache не успевает обработать FIN. Процессы копятся быстрее. Замкнутый круг.

Что не помогает:

Timeout 30 - он не работает для активных WebSocket потому, что ping сбрасывает таймер. KeepAliveTimeout 5 влияет только на HTTP keep-alive, а не на WebSocket. ProxyPassMatch вместо RewriteRule. Не решает проблему закрытия. MaxConnectionsPerChild 50. Помогает слабо. Одно WebSocket - одно соединение.

Правильное решение - переход на MPM event.

Ключевой момент.

Prefork архитектурно не предназначен для долгоживущих соединений. MPM event использует потоки и обслуживает сотни соединений небольшим числом процессов. Закрытие WebSocket в event-режиме не блокирует процесс и поток освобождается сразу.

Условие

PHP-сайты на том же сервере должны ходить через php-fpm. А не через mod_php. Потому что mod_php блокирует переход на event. Если PHP-сайты ещё ходят через mod_php - сначала переводим их на php-fpm. Обычно php-fpm уже настроен и надо просто отключить старый модуль.

Настройка mpm_event - 3 процесса, 25 потоков на старте. Потолок 400. С запасом на 350 WebSocket плюс HTTP. RAM - 3-10 процессов по 30 метров. 150-400 метров вместо гигов.

Что изменилось

Процессов Apache было 100+. Стало 5-15. RAM на Apache было 2+ гига. Стало 150-400 метров. 350 WebSocket было невозможно. Стало нормой. CLOSE-WAIT накапливались. Перестали занимать процессы. PHP-сайты работали. Работают, но через fpm. Mattermost деградировал. Стабилен.

Если сервер маленький — добавь swap. Чтобы пережить пики нагрузки и свои психи ночью! :)

Резюме.

Проблема - не баг Mattermost, и не ошибка конфига. Это - архитектурное ограничение MPM prefork, который плохо работает с долгоживущими WebSocket-соединениями.

Однозначное решение - перейти на MPM event. Это стандартный путь для всех, кто проксирует WebSocket через Apache. И он занимает пару минут.

Такой вот хаос в моей жизни случается. Просто решил поделиться примером что я понимаю под этим словом. Ну и кейс дать заодно - вдруг пригодится кому во времена, когда с мессенджерами чехарда происходит.

Но вот что главное стоит отметить - хаос не приходит внезапно. Он приходит тогда, когда мы перестаёшь смотреть.

А список команд я приложу в коммент под статьёй - авось пригодится кому?