Pull to refresh
72.09
Rating

О новом push-сервере «1С-Битрикс»

Битрикс24 corporate blog Node.JS *
Recovery mode


Некоторое время назад у нас назрела необходимость в разработке нового push-сервера для сервиса «Битрикс24». Предыдущий вариант, реализованный на базе модуля для Nginx, имел ряд особенностей, которые доставляли нам немало хлопот. В результате мы поняли — пора делать push-сервер. Здесь мы хотим рассказать о том, как это происходило.

Push-сервер (он же pulling-сервер, он же сервер мгновенных сообщений) предназначен для быстрого обмена сообщениями между пользователями, которые заходят на портал через браузер или подключаются с помощью настольного или мобильного приложений. И браузеры, и приложения устанавливают и держат постоянное соединение с push-сервером. Обычно это делается с помощью WebSocket, а если эта технология не поддерживается браузером, то используется Long Polling — постоянный долгий опрос. Это Ajax-запрос, который в течение 40 секунд ждет ответа от сервера. В случае получения ответа или при наступлении таймаута, запрос повторяется. Сейчас большинство наших клиентов сидят на WebSocket.



Pollings — количество соединений по технологии long-polling.
Websockets — количество соединений по технологии WebSockets.
Channels — количество каналов.

Что нас не устраивало


Предыдущий сервер работал следующим образом: система публиковала в nginx-модуль сообщения для пользователей, а он уже отвечал за их хранение и отправку адресатам. Если нужный пользователь был в онлайне, то получал послание сразу же. Если же пользователь отсутствовал, то push-сервер ждал его появления, чтобы переслать накопившиеся сообщения. Однако nginx-модуль часто падал, и при этом терялись все неотправленные сообщения. Это было бы полбеды, но после каждого падения модуля сильно возрастала нагрузка на PHP-бэкенд.

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

Чтобы канал был создан, портал должен его инициализировать, отправив сообщение. И когда nginx-модуль падал, то все каналы обнулялись и все порталы начинали одновременно создавать новые каналы в push-сервере. Получался своеобразный DDoS: PHP на бекенде, где работают порталы, переставал отвечать. Это была серьезная проблема.

Обнуление накопленных на push-сервере сообщений было не слишком большой проблемой, ведь они дублируются на порталах, и пользователь все равно их получал при обновлении страницы. А вот DDoS была проблемой куда серьезнее, ведь из-за него могли оказаться недоступны порталы.

Новый push-сервер


Новый сервер решено было написать с нуля. В качестве среды разработки мы решили взять Node.js. До этого мы не работали с этой платформой, но нас подкупило то, что с ее помощью можно создать сервис, который будет держать очень много соединений. Кроме того, у нас много разработчиков, использующих JavaScript, поэтому было кому поддерживать новую систему.

Важным условием было сохранение совместимости с протоколом, по которому работали с предыдущим сервером. Это позволяло не переписывать исполняемую в браузере клиентскую часть, реализованную на JavaScript. Также можно было оставить нетронутой и PHP-часть бэкенда, которая транслирует сообщения в push-сервер.

Сначала был сделан небольшой прототип, чтобы проверить работоспособность. Функциональность его была ограничена, и все сообщения он хранил прямо в памяти процесса Node.js. Поскольку push-сервер должен держать десятки тысяч соединений одновременно, мы внедрили в свою разработку поддержку кластеризации. Сейчас в российском сегменте работает push-сервер, состоящий из шести процессов нашего Node.js-приложения, обслуживающих входящие соединения, и двух процессов, отвечающих за публикацию сообщений.

Хранить сообщения в памяти, как это было сделано в прототипе, в готовом продукте было нельзя. Допустим, на портале находятся несколько пользователей, чьи запросы обрабатываются разными процессами. Если эти пользователи находятся в общем чате, то все сообщения из него должны одновременно рассылаться всем участникам. А поскольку область памяти каждого процесса изолирована, то хранить сообщения в памяти push-сервера нельзя. Конечно, можно было бы сделать что-то наподобие shared memory, но этот подход не слишком надежен и удобен в реализации, поэтому мы решили хранить все сообщения в Redis. Это NoSQL key-value хранилище, наподобие memcache, только более продвинутое. В нем могут храниться не только ключ-значение, но и ключ-словарь, ключ-список, то есть более сложные структуры данных. Поэтому мы используем Redis для хранения всех сообщений, статистики по каналам и состояние онлайна.

Также в Redis есть полезная функция — подписка на канал. То есть все наши восемь процессов Node.js-приложения имеют постоянное соединение с Redis. И если в него записывается какое-то сообщение, то оно потом приходит всем процессам, подписанным на добавление сообщений. Общая схема такая: пользователь пишет какое-то сообщение на портале, оно идет на бэкенд в PHP, там оно сохраняется, а потом отправляется в Node.js-приложение. В самом сообщении содержатся все атрибуты данных, то есть автор, адресат и т.д. Один из двух процессов, отвечающих за публикацию сообщений, принимает этот запрос, обрабатывает его и публикует в Redis. Тот записывает сообщение и сообщает другим шести процессам о том, что поступило новое сообщение и его можно разослать подписчикам.

Для работы с WebSocket мы использовали open source модуль Node.js под названием ws.

Возникшие трудности


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

Как выяснилось, подстраховались мы не зря. После развертывания выяснилось, что в ряде случаев, когда нагрузка существенно возрастала, происходило падение системы. С одной стороны, тесты подтвердили способность нашего сервера держать очень большую нагрузку. Но все же это не «настоящая» нагрузка: пользователи заходят с разных IP, каналов, браузеров, у них по-разному реализована поддержка WebSocket.

Нам так и не удалось выяснить, почему происходило падение. Проблема возникала на этапе установки TCP-соединения, поэтому решили перевести его обработку на хорошо знакомый и умеющий держать очень много соединений nginx-сервер. При этом Node.js-процессы стали выступать в роли backend-серверов. В новой схеме мы убрали несколько звеньев, а именно:

  • Утилиту PM2, использовавшуюся для запуска процессов Node.js-приложения в кластерной конфигурации. Она следит за состоянием процессов, показывает красивые графики использования СPU и памяти, умеет перезапускать упавшие процессы. Эту утилиту мы заменили собственными скриптами.
  • Модуль кластера, входящий в Node.js и помогающий запускать несколько процессов одного приложения. Балансировкой запросов теперь занимается сам nginx.
  • Модуль обработки HTTPS-соединений. Сейчас этот протокол обрабатывается самим nginx.

В результате схема работы сервера стала выглядеть следующим образом:



Внесенные улучшения


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

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

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

Также мы улучшили безопасность работы с push-сервером. Когда пользователь присоединяется, то получает уникальный идентификатор канала — это случайная строка из 32 символов. Но если ее перехватить, то можно слушать чужие сообщения. Поэтому мы добавили специальную подпись, уникальную для этого конкретного идентификатора канала. Сами каналы регулярно меняются, как и их идентификаторы.

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

* * *

После всех проведенных доработок и оптимизаций мы получили новый стабильно работающий push-сервер, выдерживающий высокие нагрузки. Однако его доработка на этом не закончена, есть еще ряд вещей, которые мы планируем реализовать и оптимизировать. Но это уже история на будущее.
Tags:
Hubs:
Total votes 23: ↑16 and ↓7 +9
Views 19K
Comments Comments 41

Information

Founded
2012
Location
Россия
Website
www.bitrix24.ru
Employees
201–500 employees
Registered
Representative