Всем привет, меня зовут Андрей Клюев, я разработчик. Недавно я столкнулся с такой задачей – создать интерактивный сервис, где пользователь может получать быстрые бонусы за свои действия. Дело осложнялось тем, что в проекте были довольно высокие требования по нагрузке, а сроки были крайне невелики.
В этой статье я расскажу, как выбирал решение для реализации websocket-сервера под непростые требования проекта, с какими проблемами столкнулся в процессе разработки, а также скажу несколько слов о том, как в достижении вышеуказанных целей может помочь конфигурирование ядра Linux.
В конце статьи приведены полезные ссылки на инструменты разработки, тестирования и мониторинга.
Задачи и требования
Требования к функционалу проекта:
- сделать возможным отслеживание присутствия пользователя на ресурсе и трекинг времени просмотра;
- обеспечить быстрый обмен сообщениями между клиентом и сервером, так как время на получение пользователем бонуса строго ограничено;
- создать динамичный интерактивный интерфейс с синхронизацией всех действий при работе пользователя с сервисом через несколько вкладок или устройств одновременно.
Требования к нагрузке:
- Приложение должно выдерживать не менее 150 тысяч пользователей онлайн.
Срок реализации – 1 месяц.
Выбор технологии
Сопоставив задачи и требования проекта, я пришел к выводу, что для его разработки целесообразнее всего использовать технологию WebSocket. Она обеспечивает постоянное соединение с сервером, избавляя от оверхеда на новое соединение при каждом сообщении, которое присутствует в реализации на технологиях ajax и long-polling. Это позволяет получить необходимую высокую скорость обмена сообщениями в сочетании с адекватным потреблением ресурсов, что очень важно при высоких нагрузках.
Также благодаря тому, что установка и разрыв соединения – это два четких события, появляется возможность с высокой точностью отслеживать время присутствия пользователя на сайте.
Учитывая достаточно ограниченные сроки проекта, я принял решение вести разработку с помощью WebSocket-фреймворка. Изучил несколько вариантов, самыми интересными из которых мне показались PHP ReactPHP, PHP Ratchet, Node.JS websockets/ws, PHP Swoole, PHP Workerman, Go Gorilla, Elixir Phoenix. Их возможности в плане нагрузки тестировал на ноутбуке с процессором Intel Core i5 и 4 ГБ оперативной памяти (таких ресурсов было вполне достаточно для исследования).
PHP Workerman – асинхронный событийно-ориентированный фреймворк. Его возможности исчерпываются простейшей реализацией websocket-сервера и умением работать с библиотекой libevent, нужной для обработки асинхронных оповещений о событиях. Код находится на уровне PHP 5.3 и не соответствует никаким стандартам. Для меня главным минусом стало то, что фреймворк не позволяет реализовывать высоконагруженные проекты. На тестовом стенде разработанное приложение уровня «Hello World» не смогло удержать и тысячи коннектов.
ReactPHP и Ratchet по своим возможностям в целом сопоставимы с Workerman. Ratchet внутри зависит от ReactPHP, работает также через libevent и не позволяет создать решение для высоких нагрузок.
Swoole – интересный фреймворк, написанный на C, подключается как расширение для PHP, имеет средства для параллельного программирования. К сожалению, я обнаружил, что фреймворк недостаточно стабилен: на тестовом стенде он обрывал каждый второй коннект.
Далее я рассмотрел Node.JS WS. Этот фреймворк показал неплохие результаты – около 5 тысяч коннектов на тестовом стенде без дополнительных настроек. Однако мой проект подразумевал заметно более высокие нагрузки, поэтому я остановил свой выбор на фреймворках Go Gorilla + Echo Framework и Elixir Phoenix. Эти варианты тестировались уже более детально.
Нагрузочное тестирование
Для тестирования использовались такие инструменты как artillery, gatling и сервис flood.io.
Целью тестирования было изучение потребления ресурсов процессора и памяти. Характеристики машины были такими же – процессор Intel iCore 5 и 4 ГБ оперативной памяти. Тесты проводились на примере простейших чатов на Go и Phoenix:
Вот такое простое приложение чата нормально функционировало на машине указанной мощности при нагрузке 25-30 тысяч пользователей:
config:
target: "ws://127.0.0.1:8080/ws"
phases
-
duration:6
arrivalCount: 10000
ws:
rejectUnauthorized: false
scenarios:
-
engine: “ws”
flow
-
send “hello”
-
think 2
-
send “world”
Class LoadSimulation extends Simulation {
val users = Integer.getInteger (“threads”, 30000)
val rampup = java.lang.Long.getLong (“rampup”, 30L)
val duration = java.lang.Long.getLong (“duration”, 1200L)
val httpConf = http
.wsBaseURL(“ws://8.8.8.8/socket”)
val scn = scenario(“WebSocket”)
.exes(ws(“Connect WS”).open(“/websocket?vsn=2.0.0”))
.exes(
ws(“Auth”)
sendText(“““[“1”, “1”, “my:channel”, “php_join”, {}]”””)
)
.forever() {
exes(
ws(“Heartbeat”).sendText(“““[null, “2”, “phoenix”, “heartbeat”, {}]”””)
)
.pause(30)
}
.exes(ws(“Close WS”).close)
setUp(scn.inject(rampUsers(users) over (rampup seconds)))
.maxDuration(duration)
.protocols(httpConf)
Тестовые запуски показали, что все спокойно работает на машине указанной мощности при нагрузке 25-30 тысяч пользователей.
Потребление ресурсов процессора:
Phoenix
![image](https://habrastorage.org/webt/gt/9i/a8/gt9ia8elxtq2xqigrfrvewl9djk.jpeg)
Gorilla
![image](https://habrastorage.org/webt/mj/yu/gt/mjyugtobdjld1ha2qkxw-bbsvys.jpeg)
Потребление оперативной памяти при нагрузке в 20 тысяч соединений доходило до 2 ГБ в случае с обоими фреймворками:
Phoenix
![image](https://habrastorage.org/webt/ra/8b/lc/ra8blc1rtvxrxuchjr72f0od1ta.jpeg)
Gorilla
![image](https://habrastorage.org/webt/4v/5t/6q/4v5t6q5isuiklpw42z16bl0c16w.jpeg)
При этом Go даже опережает Elixir по производительности, однако Phoenix Framework при этом предоставляет намного больше возможностей. На графике ниже, который показывает потребление сетевых ресурсов, можно заметить, что в тесте Phoenix передается в 1,5 раза больше сообщений. Это связано с тем, что у этого фреймворка уже в изначальной «коробочной» версии есть механизм heartbeat’ов (периодических синхронизирующих сигналов), который в Gorilla придется реализовывать самостоятельно. В условиях ограниченных сроков любая дополнительная работа была весомым аргументом в пользу Phoenix.
Phoenix
![image](https://habrastorage.org/webt/_l/pg/3g/_lpg3gebhazhy2b9el-ecu3l_qu.jpeg)
Gorilla
![image](https://habrastorage.org/webt/wv/5n/lu/wv5nluzykygyi99-f5u56mfzi7o.jpeg)
О Phoenix Framework
Phoenix – это классический MVC-фреймворк, достаточно похожий на Rails, что не удивительно, так как одним из его разработчиков и создателем языка Elixir является Хосе Валим – один из основных создателей Ruby on Rails. Некоторое сходство можно увидеть даже в синтаксисе.
Phoenix:
defmodule Benchmarker.Router do
use Phoenix.Router
alias Benchmarker.Controllers
get "/:title", Controllers.Pages, :index, as: :page
end
Rails:
Benchmarker::Application.routes.draw do
root to: "pages#index"
get "/:title", to: "pages#index", as: :page
end
Mix – автоматизирующая утилита для Elixir-проектов
При использовании Phoenix и языка Elixir значительная часть процессов выполняется посредством утилиты Mix. Это инструмент для сборки (build tool), который решает множество разных задач по созданию, компиляции и тестированию приложения, по управлению его зависимостями и по некоторым другим процессам.
Mix – ключевая часть любого Elixir-проекта. Эта утилита ничем не уступает и ничем не превосходит аналоги из других языков, но справляется со своей работой на отлично. А благодаря тому, что Elixir-код выполняется на виртуальной машине Erlang, появляется возможность добавлять в зависимости любые библиотеки из мира Erlang. Помимо этого вместе с Erlang VM вы получаете удобный и безопасный параллелизм, а также высокую отказоустойчивость.
Проблемы и решения
При всех достоинствах у Phoenix есть и свои недостатки. Один из них заключается в сложности решения такой задачи, как отслеживание активных пользователей на сайте в условиях высокой нагрузки.
Дело в том, что пользователи могут подключаться к разным нодам приложения, и каждая нода будет знать лишь о собственных клиентах. Чтобы вывести список активных пользователей, придется опрашивать все ноды приложения.
Для решения этих проблем в Phoenix’е существует модуль Presence, который дает разработчику возможность отслеживать активных пользователей буквально в три строки кода. Он использует механизм хартбитов и бесконфликтной репликации внутри кластера, а также PubSub-сервер для обмена сообщениями между нодами.
![image](https://habrastorage.org/webt/zh/ts/ur/zhtsurf7ndsi-twjyd5wqudvgs8.png)
Звучит хорошо, но на деле получается примерно следующее. Сотни тысяч подключающихся и отключающихся пользователей порождают миллионы сообщений на синхронизацию между нодами, из-за чего потребление ресурсов процессора переходит все допустимые пределы, и даже подключение Redis PubSub не спасает ситуацию. Список пользователей дублируется на каждой ноде, и расчет дифа при каждом новом подключении становится все дороже и дороже – и это учитывая, что расчет проводится на каждой из действующих нод.
![image](https://habrastorage.org/webt/ds/jf/rk/dsjfrkjuzpuezbv_ae1bjt61ao0.png)
В такой ситуации отметка даже в 100 тысяч клиентов становится недостижимой. Других готовых решений для данной задачи найти не удалось, поэтому я решил поступить следующим образом: возложить обязанность по мониторингу присутствия пользователей онлайн на БД.
На первый взгляд, это хорошая идея, в которой нет ничего сложного: достаточно хранить в базе поле last activity и периодически его обновлять. К сожалению, для проектов с высокой нагрузкой это не выход: когда количество пользователей достигнет нескольких сотен тысяч, система не справится с миллионами приходящих от них хартбитов.
Я выбрал менее тривиальное, но более производительное решение. При подключении пользователя для него создается уникальная строка в таблице, которая хранит в себе его идентификатор, точное время входа и список нод, к которым он подключен. Перечень нод хранится в JSONB-поле, и при конфликте строк достаточно его обновить.
create table watching_times (
id serial not null constraint watching_times_pkey primary key,
user_id integer,
join_at timestamp,
terminate_at timestamp,
nodes jsonb
);
create unique index watching_times_not_null_uni_idx
on watching_times (user_id, terminate_at)
where (terminate_at IS NOT NULL);
create unique index watching_times_null_uni_idx
on watching_times (user_id)
where (terminate_at IS NULL);
Вот такой запрос отвечает за вход пользователя:
INSERT INTO watching_times (
user_id,
join_at,
terminate_at,
nodes
)
VALUES (1, NOW(), NULL, '{nl@192.168.1.101”: 1}')
ON CONFLICT (user_id)
WHERE terminate_at IS NULL
DO UPDATE SET nodes = watching_times.nodes ||
CONCAT(
'{nl@192.168.1.101:',
COALESCE(watching_times.nodes->>'nl@192.168.1.101', '0')::int + 1,
'}'
)::JSONB
RETURNING id;
Список нод при этом выглядит так:
![image](https://habrastorage.org/webt/lc/jb/lm/lcjblm_rukdhxcnf8qoeti3v9xw.png)
Если пользователь открывает сервис во втором окне или на другом устройстве, он может попасть на другую ноду, и тогда она тоже добавится в список. Если он попадет на ту же ноду, что и в первом окне, число напротив названия этой ноды в списке увеличится. Это число отражает количество активных подключений пользователя к конкретной ноде.
Вот так выглядит запрос, который идет в БД при закрытии сессии:
UPDATE watching_times
SET nodes
CASE WHEN
(
CONCAT(
'{“nl@192.168.1.101”: ',
COALESCE(watching_times.nodes ->> 'nl@192.168.1.101', '0') :: INT - 1,
'}'
)::JSONB ->>'nl@192.168.1.101'
)::INT <= 0
THEN
(watching_times.nodes - 'nl@192.168.1.101')
ELSE
CONCAT(
'{“nl@192.168.1.101”: ',
COALESCE(watching_times.nodes ->> 'nl@192.168.1.101', '0') :: INT - 1,
'}'
)::JSONB
END
),
terminate_at = (CASE WHEN ... = '{}' :: JSONB THEN NOW() ELSE NULL END)
WHERE id = 1;
Список нод:
![image](https://habrastorage.org/webt/q-/kd/9q/q-kd9qj3nwtpnkksu6mrzwex69e.png)
При закрытии сессии на определенной ноде счетчик подключений в БД уменьшается на единицу, а при достижении нуля нода убирается из списка. Когда список нод окончательно опустеет, этот момент будет зафиксирован как окончательное время выхода пользователя.
Данный подход дал возможность не только отслеживать присутствие пользователя онлайн и время просмотра, но и фильтровать эти сессии по различным критериям.
Во всем этом остается лишь один недостаток – если нода падает, все ее пользователи «зависают» в онлайне. Для решения данной проблемы у нас есть демон, который периодически чистит БД от таких записей, но до сих пор этого не требовалось. Анализ нагрузки и мониторинг работы кластера, проведенные после выхода проекта в продакшн, показали, что падений нод не было и данный механизм не использовался.
Были и другие трудности, но они более специфичны, поэтому стоит перейти к вопросу отказоустойчивости приложения.
Конфигурирование ядра Linux для повышения производительности
Написать хорошее приложение на производительном языке – это только половина дела, без грамотных DevOps достигнуть хоть сколько-нибудь высоких результатов невозможно.
Первой преградой на пути к целевой нагрузке стало сетевое ядро Linux. Потребовалось произвести некоторые настройки, чтобы добиться более рационального использования его ресурсов.
Каждый открытый сокет – это файловый дескриптор в Linux, а их число ограничено. Причина лимита в том, что для каждого открытого файла в ядре создается C-структура, которая занимает unreclaimable-память ядра.
Чтобы использовать память по максимуму, я выставил очень высокие значения размеров буферов приема и передачи, а также увеличил размер буферов TСP сокетов. Значения здесь выставляются не в байтах, а в страницах памяти, обычно одна страница равна 4 кБ, и на максимальное количество открытых сокетов, ожидающих соединения для высоконагруженных серверов, я поставил значение 15 тысяч.
Лимиты файловых дескрипторов:
#!/usr/bin/env bash
sysctl -w 'fs.nr_open=10000000' # Максимальное количество открытых файловых дескрипторов
sysctl -w 'net.core.rmem_max=12582912' # Максимальный размер буферов приема всех типов
sysctl -w 'net.core.wmem_max=12582912' # Максимальный размер буферов передачи всех типов
sysctl -w 'net.ipv4.tcp_mem=10240 87380 12582912' # Объем памяти TCP сокета
sysctl -w 'net.ipv4.tcp_rmem=10240 87380 12582912' # размер буфера приема
sysctl -w 'net.ipv4.tcp_wmem=10240 87380 12582912'# размер буфера передачи
<code>sysctl -w 'net.core.somaxconn=15000' # Максимальное число открытых сокетов, ждущих соединения
Если вы используете nginx перед cowboy-сервером, то стоит также задуматься об увеличении его лимитов. За это отвечают директивы worker_connections и worker_rlimit_nofile.
Вторая преграда не столь очевидна. Если запустить подобное приложение в распределенном режиме, можно заметить резкий рост потребления ресурсов процессора при увеличении количества коннектов. Проблема в том, что Erlang по умолчанию работает с системными вызовами Poll. В версии 2.6 ядра Linux существует Epoll, который может предоставить более эффективный механизм для приложений, обрабатывающих большое количество одновременно открытых соединений — со сложностью O(1) в отличие от Poll, обладающего сложностью O(n).
К счастью, режим Epoll включается одним флагом: +K true, также рекомендую увеличить максимальное количество процессов, порождаемых вашим приложением, и максимальное количество открытых портов с помощью флагов +P и +Q соответственно.
Poll vs. Epoll
#!/usr/bin/env bash
Elixir --name ${MIX_NODE_NAME}@${MIX_HOST} --erl “-config sys.config -setcookie ${ERL_MAGIC_COOKIE} +K true +Q 500000 +P 4194304” -S mix phx.server
Третья проблема более индивидуальна, и не каждый может с ней столкнуться. На данном проекте был организован процесс автоматического деплоя и динамического скейлинга с помощью Сhef и Kubernetes. Kubernetes позволяет быстро разворачивать Docker-контейнеры на большом количестве хостов, и это очень удобно, однако заранее узнать ip-адрес нового хоста нельзя, а если не прописать его в конфиг Erlang, подключить новую ноду к распределенному приложению не получится.
К счастью, для решения этих проблем существует библиотека libcluster. Общаясь с Kubernetes по API, она в режиме реального времени узнает о создании новых нод и регистрирует их в кластере erlang.
config :libcluster,
topologies: [
k8s: [
strategy: Cluster.Strategy.Kubernetes,
config: [
kubernetes_selector: “app=my -backend”,
kubernetes_node_basename: “my -backend”]]]
Итоги и перспективы
Выбранный фреймворк в сочетании с правильной настройкой серверов позволил достичь всех целей проекта: в поставленные сроки (1 месяц) разработать интерактивный веб-сервис, который общается с пользователями в режиме реального времени и при этом выдерживает нагрузки от 150 тысяч коннектов и выше.
После запуска проекта в продакшн был проведен мониторинг, который показал следующие результаты: при максимальном количестве коннектов до 800 тысяч потребление ресурсов процессора доходит до 45%. Среднее значение загрузки составляет 29% при 600 тысячах соединений.
![image](https://habrastorage.org/webt/w2/h0/ms/w2h0msuts0dky-2omau9hmku8lk.png)
На этом графике – потребление памяти при работе в кластере 10 машин, каждая из которых имеет по 8 ГБ оперативной памяти.
![image](https://habrastorage.org/webt/hr/tn/h1/hrtnh1ysbf_c0nnc_1qiei00ucq.jpeg)
![image](https://habrastorage.org/webt/d9/ph/96/d9ph96p9f0foley0jqww7-oq3sm.jpeg)
Что же касается основных рабочих инструментов в этом проекте, Elixir и Phoenix Framework, у меня есть все основания полагать, что в ближайшие годы они станут такими же популярными, как в свое время Ruby и Rails, так что есть смысл начинать их освоение уже сейчас.
Спасибо за внимание!
Ссылки
Разработка:
elixir-lang.org
phoenixframework.org
Нагрузочное тестирование:
gatling.io
flood.io
Мониторинг:
prometheus.io
grafana.com
Комментарии (53)
apapacy
12.03.2018 19:11Спасибо и за пример и за реальную статистику. Чисто теоретически я вобщем-то был уверен в том что решения на основе Erlang это наиболее удачное решение для веб-сокетов. То что Go показал сравнимую производительность в тестах это еще не гарантирует сравнимую производительность на проде (и не исключает ее). Хотелось бы посмотреть на реальную статистику реальных приложений на Go, если она у кого-нибудь имеется.
AterCattus
13.03.2018 00:04habrahabr.ru/company/mailru/blog/331784 к примеру.
Да и как раз на проде сложно представить реальную задачу, где Erlang составил бы сильную конкуренцию Go. Разве что какие очень развесистые деревья или подобные структуры.apapacy
13.03.2018 00:25Спасибо интересная статья. Правда как я понял из контекста это все же не работет на продакшине, а то что работает (как я понял тоже Go только без оптимизации) нет конкретной статистики. Про erlang конкретно я имел в виду исключительно решение с большим количеством сокетов. (см. phoenixframework.org/blog/the-road-to-2-million-websocket-connections ).
AterCattus
13.03.2018 09:17Как раз таки работает.
Но в любом случае такие технические статьи на любом ЯП интересно почитать.
absurdil
12.03.2018 21:59Была ли проверка с mnesia in memory в качестве базы данных на отдельной ноде? Теоретически это позволило бы мониторить упавшую ноду и обновлять цифры.
KirEv
12.03.2018 22:49много обовсем,
Тесты проводились на примере простейших чатов на Go и Phoenix
мне, например, непонятно что за такие простейшие чаты и откуда,
хороший тон: публиковать тестируемый код, иначе остается словам верить.mediasoft Автор
14.03.2018 12:28Чаты — те что идут в официальных примерах фреймворков
Для Gorilla — github.com/gorilla/websocket/tree/master/examples/chat
Для Phoenix — github.com/chrismccord/phoenix_chat_example
В тот момент не планировали что-либо публиковать, и многое из тестируемого кода просто не сохранилось
vazir
13.03.2018 00:56Для 3х сценариев ( 1)-минимум подключенных клиентов (например два), (2)-среднее кол-во (например 150К и (3) тестируемый макс — например 800к) сл. вопросы:
— Скольку удавалось получить new-connections-per-second (создание нового соединения — процесс очень дорогой)
— Какая messages-per-second
— Какое latency при передаче сообщения клиент-клиент
— Какое время регистрации нового клиента
mediasoft Автор
14.03.2018 13:05Отвечу вам цифрами с нашего последнего нагрузочного тестирования на уже готово продукте.
Порядка 220 новых соединений в секунду, 200к ботов.
Время регистрации нового клиента не превышает 50ms.
В пике около 6000 сообщений в секунду летает между сервером и клиентами.
У нас бизнес логика не предполагает общения клиент-клиент, NDA не позволит дать конкретики, абстрактно схема следующая:
1. Событие на сервере -> Уникальные сообщения для большой группы пользователей
2. Реакция пользователя -> Сообщение серверу о том что пользователь подтвердил получение сообщения 1
Hixon10
13.03.2018 01:01Ого, вот это у вас темп работы — разработка за месяц сервиса для 150 тыс. людей онлайн — круто!
Вот интересно, вы, как Тимлид проекта, когда выбирали Элексир vs Go — рассматривали ли вопрос кадров? Кажестся, что у нас куда проще найти Go-разработчика, чем Erlang/Elixir.
alek0585
13.03.2018 02:49Код nodejs сервера, который у вас не выдержал 5000 соединений
в студиюна гитхаб!apapacy
13.03.2018 16:41Ну уж раз так то список сайтов на node.js где это работет на 150 тыс. соединенй (хотя бы ) тута же.
alek0585
13.03.2018 18:50Я не утверждаю, что 150 000 соединений невозможны или возможны. Автор статьи же наоборот утверждает, что 5000 невозможно.
Кстати, случайно не подскажете где ссылка на сайт автора в продакшене?apapacy
13.03.2018 19:37Далее я рассмотрел Node.JS WS. Этот фреймворк показал неплохие результаты – около 5 тысяч коннектов на тестовом стенде без дополнительных настроек
Возможно 5000. Ссылки на сайт автора у меня нет.
По поводу использования конерктно nodе.js и конерктно для веб-сокетов очень большие вопросы есть. Если бы был реальный пример то можно было бы пробовать реализовать в нагруженном (хотя бы) проекте.AterCattus
13.03.2018 19:54Вот тут автор уверяет, что еще в 2015 поднял на node.js на амазоновском EC2 600-620к вебсокет соединений. Пусть и с небольшими подпорками, но, видимо, реально.
Сам я в ноде практически 0, так что хз.apapacy
13.03.2018 20:19Вобщем-то автор не скрывает что это были лишь тесты со «своим клиентом». Цель была присоединиться и держать соединение на что не использщуется основной процесс в коором віполняется JavaScript а только средства NIO. Что будет когда все эти коннеты «заговорят» и потребуют свою долю от движка JavaScript. Большой плюс автора обсуждаемй статьи на Хабре это то что он сделал реальное приложение и показал нам графики реальной загрузки реальной конфигураци сервера.
wouter
April 13, 2015 at 20:37 /
What did you use to generate 620k connections?
And did you send a message over every connection every x seconds or was is just a silent connection?
Daniel Kleveros
April 14, 2015 at 08:11 /
I created my own client with the same websocket lib websockets/ws which I then deployed on a cluster of M1.Large instances.
It was silent connections but to keep the connections alive, if you are behind a elastic load balancer, you need to check the idletimeout setting on the loadbalancer and send a ping before that time expires. If not the load balancer will start to drop your connections.
pyur
13.03.2018 06:52не холивара ради, простое любопытство: почему не СИ?
я набросал работающий фреймворк за день. но у меня уже было понимание, как устроен протокол. разобрался когда пытался, несколько лет назад, делать сервер на CLI PHP.OnYourLips
13.03.2018 11:50Потому что нет готовых инструментов, зато есть ограничения по времени и уровню поддержки.
maxood
13.03.2018 22:38Почему не C — надеюсь автор ответит.
А вот работающие «фреймворки» давно написаны и вряд ли можно написать что-то более производительное, чем связка libwebsocket и libevent.
mediasoft Автор
14.03.2018 12:34Были мысли и про СИ, остановило отсутствие готовых инструментов, общая сложность разработки и то что СИ, в целом, не особо ориентирован на WEB.
pyur
14.03.2018 12:47мне кажется, вы как-то через-чур преувеличиваете сложность СИ, не так страшен чёрт как его малюют. пишете некое CORE (ядро) для общей обработки Websockets, а для разных функций — разные подключаемые DLL модули. и не придётся перекомпилять всё то и дело.
а вообще, я уверен, что-то уже есть. вот люди советуют вам посмотреть libwebsocket и libevent, посмотрите, хотябы и обзорно. хотябы и после того, как «горящий» проект сдадите в прод. я думаю такие вещи имеют высокую повторяемость в потребности, и время на изучение, проработку вопроса не будет потрачено без пользы. оно себя с лихвой окупит в будущем.mediasoft Автор
14.03.2018 12:52+1Проект уже давно ушёл в прод, и успешно выполняет задачи.
В мир C/C++ обязательно загляну в свободное время. Но «продать» заказчику реализацию на С, по моему мнению, будет ещё сложнее чем на Erlang. Увы, все хотят дешёвых и популярных технологий, таких как PHP, Ruby, Pyton
akadex
13.03.2018 22:38Приветствую всех!
Ws действительно интересная тема. Недавно была задача для crm-bpm системы сделать event-сервер. Я остановился на node.js. У меня, служба работающая под pm2, является клиентом ws для asterisk. Ловит и обрабатывает все события АТС и раздает авторизованным пользователям системы, подключенным по wss, уведомления о входящих звонках, проверяя соответствия номера клиента с наличием в базе данных. Также от пользователей приходят комманды на уведомления других пользователей при постановке задачи, состояние он-лайн, офф-лайг, а также работа по API с сайтами при поступлении POST запросов. То есть, одновременно это клиент и сервер.
Пользователей он-лайн около 200, события от АТС идут постоянно (один только звонок порождает около 6000 тыс. строк), их около 400 в день. С сайтов приходят запросы, но немного, один-два в час.
Так к чему я это все рассказывал. Честно говоря не могу понять как протестировать нагрузку, чтобы можно было смело говорить о каких-то конкретных цифрах.
Я конечно понимаю, что видимо знаний мало, но тут и запросы к базе и активность клиентов системы очень варьируется, и данные по API разный объем имеют. Плюс-минус километр если, то на 200 пользователей при 400 звонках в день и 300 задачах в системе + 40 отправленных сообщений по API в сутки, пиковая нагрузка по RAM была 81mb. По процессору не более 10%. Можно смело об этом говорить, что эти показатели не превышались никогда, так как PM2 в этом случае перезапускал бы службу (ну должен это делать во всяком случае) и фиксировал это событие.
Интересно было бы узнать о каких-то системах расчета нагрузки, потому как мои средние наблюдения и "простые чаты" это как-то из разряда средней температуры по больнице…
За статью спасибо! Я, к сожалению до Go не дошел в этом вопросе, а PHP отмел по причине неудобной, ИМХО реализации по работе с сокета и.
NachtRitter
13.03.2018 22:38Так много вариантов со стороны PHP (не совсем очевидного выбора о котором думают в первую очередь говоря о вебсокетах), а со стороны node.js один только ws. Хотя можно было ещё и socket.io рассмотреть — было бы интересно посмотреть на его результаты.
akadex
13.03.2018 23:07socket.io — смотрел, пробовал. Это было чуть раньше, когда он был незаменим в связи со слабой поддержкой сокетов браузерами. Сейчас все и так хорошо.
Сам большой поклонник PHP, но сдался в связи в удобством реализации на Node.
Могу сказать только что socket.io многократно больше потребляет траффика… или я «котов не умею готовить» :)apapacy
13.03.2018 23:20т. Плюс-минус километр если, то на 200 пользователей при 400 звонках в день и 300 задачах в системе + 40 отправленных сообщений по API в сутки, пиковая нагрузка по RAM была 81mb
На ноде такая загрузка была? Конечно одновременных 200 соединений это почти ничего но все равно я предполагал большую цифру.
wormen
13.03.2018 22:38хотелось ты посмотреть код на NodeJS, почему был такой маленький лимит у вас в запросах
akadex
13.03.2018 23:17Поверьте, вот все по мануалу из:
www.npmjs.com/package/ws
Там банальный небольшой скрипт процедурный. Никакого специализированного фреймворка. Все время/трудозатраты в основном на тесты и отладку ушли. Те же event`ы различных диалпланов Астериска парсить. А лимиты такие сугубо из показателей мониторинга. Как только PM2 начнет ребутить, то будет повод посмотреть логи, подумать о том, что надо ресурсы увеличить, или увидеть очередной объект, который надо за`delete`ить.
apapacy
13.03.2018 23:19Если посмотреть на код можно наверное сказать почему был лимит 5000. Но это не ответит на вопрос какой может быть реальный лимит и какие при этом будут затраты (сколько ядер, сколько памяти, сколько нодов. У Вас есть реаьные данные по nodejs как у атора этой статьи есть реальные данные по фениксу? Расскажите, это интересно.
wormen
13.03.2018 23:26у меня есть проект в проде, и там показатели в разы выше
вот и хотел код глянуть
с фениксом не работал, тут нечего сказать
интерес исключительно по NodeJS
по готовым примерам… вот готовый модуль, лаботающий на сокетах, использующий нативный модуль
www.npmjs.com/package/ipc-socket
максимальный лимит который был достигнут на моей машине, 35к/сек
этот модуль у нас так же используется в проде, по нему гоняем логи с нескольких проектов в единое хранилищеapapacy
14.03.2018 00:45То есть у Вас одновременно около 1 миллиона одновременно подключившихся активных клиентов? Икакие при этом параметры по серверу/серверам (количество серверов/ядер/объем памяти)?
wormen
14.03.2018 01:28на серверах замеры не производил
замерял только на своей локальной машине
замер производился на конфигурации
i7
16G RAM
у имеющихся двух серверов, только оперативы в 2 раза больше, остальные параметры примерно те жеapapacy
14.03.2018 02:34Ага у Вас свой нативный модуль. А как с fallback если веб-скоеті на поддерживаются и актуальнно ли сейчас делать fallback? Говорят что например в некоторых странах (Китай) и некоторые провайдеры (мобилного интернета) эти веб-сокеты не поддерживают. Также поддержка веб-сокетов из соображений скажем безопасности моет быть отключна администраторами корпоративной сети для доступа к внешним сверерам/сайтам.
wormen
14.03.2018 03:59этот модуль рассчитан только на работу в серверном коде
а там TCP сокеты никто никогда не отменялapapacy
14.03.2018 04:17А какой модуль работет с 1 млн. одновременных клиентов-браузеров онлайн? Мы же насколько я понял это сейчас обсуждаем?
wormen
14.03.2018 04:20изначально, я у автора просто спросил код, и ничего обсуждать не собирался
написание кода под высокие нагрузки, имхо, это вопрос прямизны рук
wormen
14.03.2018 04:05Также поддержка веб-сокетов из соображений скажем безопасности моет быть отключна администраторами корпоративной сети для доступа к внешним сверерам/сайтам.
не слышал ни одного такого случая
если что-то закрывается с целью безопасности, то уже сразу целым доменом/IP
apapacy
14.03.2018 13:10Я наконец понял в чем Ваш вопрос. Автор не говорит что у него было 5000 запрсов в секунду. И для вебсокетов (не в серверном варианте) высокая нагрузка совсем другое подразумевает. Это когда у Вас 5 или 150 тысяч одновременно пользователей открыли соединения из браузера и все они работают не секунду а час или например цеоый день у них приложение открыто и нужно держать постоянно соединение. Я-то думаю откуда у Вас на ноде такие показатели на слабых девайсах.
Или если перейти на серверы то это у вас со всего мира 150 000 серверов к Вами присоединились и держат коннекты. А если у Вас пусть даже миллион сообщений в секунду но от сотни серверов то это как раз сто содеинений а не сто миллионов соединений.
TokiTori
13.03.2018 22:38И это ещё cowboy версии 1. Жду не дождусь, когда cowboy 2 интегрируют в феникс.
Есть сторонний хэндлер для cowboy 2: github.com/voicelayer/phoenix_cowboy2.
Было бы интересно взглянуть на графики из статьи ещё раз, но уже с cowboy 2
Lordz
13.03.2018 22:39Причитал статью и вспомнил высказывание: "- Вы не любите котов!? — Вы просто не умеете их правильно готовить!"
Как чужой опыт, статья конечно имеет право быть, но ИМХО выглядит она как хвалебная песня в честь Linux, Go и Erlang.
Можно было просто сказать, что наша команда знает Linux, Go и Erlang и поэтому в условиях дефицита времени для достижения цели выбрали в качестве инструмента, то что мы лучше всего знаем. (И это единственно правильное решение).
Но недостатки других решений или преимущества (Linux, Go и Erlang) совсем не очевидны.
Чем плох PHP и в частности PHP Workerman не понятно!
«Poll vs. Epoll»… А где kqueue/kevent?
Или kqueue настолько не эфективен что не заслужил упоминания? Да,… не кросплатформенно… да, с этим умеют работать только BSD системы и некоторые комерческие системы используют подобный подход.
Но согласитесь, что это проблема Linux, а не PHP Workerman!
NGINX + PHP-FPM умеют использовать эфективный kqueue и вместе они прекрасно масштабируются «по самое не могу»!
«PHP Workerman — Код находится на уровне PHP 5.3 и не соответствует никаким стандартам.» — простите, а каким конкретно стандартам он должен соответствовать?!
У меня код работал на PHP 7.1, но если автор тестировал на сервере с PHP 5.3, то это даже не проблема настроек PHP… и уж тем более не проблема PHP Workerman. И из этого не следует, что он не позволяет реализовывать высоконагруженные проекты…
Скажем прямо, просто Вы выбрали другой путь… свой путь.apapacy
13.03.2018 23:15У меня код работал на PHP 7.1
Не сочтите за наезд. Я просто реально хочу получить реальные данные о реальных серверах (как у автора этой статьи). Какая нагрузка была на приложение? Какие были затраты по памяти/процессорам? Как мониторилась доступнасть веркеров? Сколько было запущено веркеров? Каки были организованы сессии в смысле как отслеживалось чтобы лиибо было соединение с одинм и тем же веркером или же данные расшарвались между веркерами? Расскажите. Это интересно.
mediasoft Автор
14.03.2018 12:47Можно было просто сказать, что наша команда знает Linux, Go и Erlang
Это было-бы великолепно, но увы это был первый опыт работы с Elixir
А где kqueue/kevent?
Нельзя объять необъятное, BSD системы не рассматривали, хотя возможно стоило
а каким конкретно стандартам он должен соответствовать
Ну для начала PSR
Moxa
спасибо за статью, всегда интересно читать про использование вебсокетов =) я тоже как-то давно развлекался поднятием 1кк коннектов на домашнем компе, выдерживало 800к, сообщения ходили, все было ок, старенький amd на 4 ядра и 16гб оперативы, java. Тестировать это было интересно, пришлось поднять 20 виртуалок по 40к коннектов
BigDflz
как получил 40к коннектов? почему именно 40к? что за сервер?
если на одном сервере сделать 20 http портов слушающих ws и на каждом по 40к коннектов?
это вместо 20 виртуалок.
Moxa
Ну всего лимит 65к коннектов с одного клиента в один порт, минус занятые файловые дескрипторы, можно и с одного клиента 1кк коннектов открыть, если в разные порты и лимиты настроить, но про разные порты я не подумал, а с лимитами в lxc было что-то мутно. Кстати как балансировать 20 разных портов?
BigDflz
про разные порты — мысль пришла из твоего опыта. вот тут tootallnate.github.io/Java-WebSocket это вполне можно реализовать, там просто «слушатель» порта. он отдельно. а вот если использовать либу из tomcat — там так просто порты не добавить…
как балансировать по портам — чисто теоретически — массив — свободных, при подключении/открытии страницы брать из него свободный и подставлять… и убирать из массива…
для начала как бы проверить вообще такую возможность — несколько http портов вместо машин.
и вроде для ws подключений нет ограничений для количества портов. они вроде как отдельно от портов http? ( имеется в виду все 65к)
Moxa
ws идет поверх http, для балансировки нужно будет как-то сообщать клиенту в какой порт стучаться, но да, всё реализуемо
BigDflz
если клиент браузер — то всё просто, а если приложение — то несколько усложняется.