Как организовать запись данных из WebSocket в базу данных и кэш
Что проверяют
Умеете ли вы построить конвейер, который не развалится под нагрузкой: с ограничением параллелизма, пакетной записью и понятным поведением при сбоях. Наивное «горутина на сообщение и INSERT» интервьюер ждёт, чтобы спросить, что будет при десяти тысячах сообщений в секунду и при падении базы.
Как отвечать
- чтение соединения: одна горутина на соединение читает сообщения, с
SetReadLimit, дедлайнами и ping/pong, чтобы мёртвые клиенты не висели вечно. Библиотеки:gorilla/websocketилиgithub.com/coder/websocket; - развязка через канал: читатель кладёт разобранное сообщение в буферизованный канал, а пул воркеров фиксированного размера его обрабатывает. Горутина на каждое сообщение означает неограниченный рост памяти и соединений к базе;
- обратное давление: когда канал полон, читатель блокируется и клиент замедляется. Если блокировать нельзя, сообщения отбрасывают с метрикой или закрывают соединение с кодом ошибки;
- пакетная запись: воркер копит пачку по размеру или по таймеру (скажем, 500 сообщений или 100 мс) и пишет одним
CopyFromилиpgx.Batch. Это намного дешевле отдельныхINSERT; - сначала база, потом кэш: после фиксации в базе обновляем или инвалидируем ключ в Redis (
github.com/redis/go-redis/v9). Кэш не источник истины; - идемпотентность: у сообщения есть идентификатор, вставка через
ON CONFLICT DO NOTHING, и повтор после сбоя не создаёт дубль. Если важен порядок по ключу, воркер выбирается по хэшу ключа; - остановка: по сигналу отменяем
context, перестаём читать, дописываем содержимое каналов и только потом выходим.
Если терять данные нельзя совсем, между WebSocket и базой ставят брокер с подтверждением (Kafka, NATS JetStream), а в базу пишет отдельный потребитель.
Где спотыкаются
Запускают go processMessage(msg) на каждое сообщение. Порядок теряется, а под нагрузкой сервис упирается в max_connections базы.
Пишут сначала в кэш, потом в базу. При ошибке базы кэш показывает данные, которых нет.
Везде используют context.Background() и не могут корректно остановить сервис: при деплое теряется всё, что было в памяти.