LAppS: Пол миллиона 1KB-WebSocket сообщений в секунду с TLS на одном CPU +17



Для тех кто не в курсе: LAppS — Lua Application Server, это почти как nginx или apache, но только для WebSocket протокола, вместо HTTP.


HTTP в нём поддерживается только на уровне Upgrade запроса.


LAppS изначально затачивался на высокую нагрузку и вертикальную масштабируемость, и сегодня достиг пика своих возможностей на моём железе (ну почти, можно и дальше оптимизировать, но это будет долгий и упорный труд).


Самое главное, LAppS по производительности WebSocket стека, превзошёл библиотеку uWebSockets, которая позиционируется как самая быстрая WebSocket имплементация.


Заинтересованных прошу под кат.


С последней моей статьи про LAppS прошло уже пару месяцев, заинтересованности та статья не вызвала. Надеюсь эта статья покажется хабровчанам интересней. LAppS за это время проделал довольно сложный путь к версии 0.7.0, оброс функционалом и вырос в плане производительности (что и было обещано ранее).


Одна из появившихся фич: подгружаемый модуль с реализацией клиентской части протокола WebSocket, — cws.


Благодаря этому модулю, я наконец-то смог выжать все возмозжное из моего домашнего компа, и нагрузить LAppS по настоящему.


Ранее тестирование производилось с помощью echo клиента библиотеки websocketpp (подробнее можно посмотреть на github странице проекта), которая мало того, что медленная, так ещё и трудно параллелизуемая. Тесты выполнялись просто: стартовалась пачка клиентов, результаты от каждого клиента собирались с помощью awk и несложная арифметика выдавала результаты производительности. Результаты были такими:


Сервер Кол-во клиентов RPS сервера RPS на клиента payload (bytes)
LAppS 0.7.0 240 84997 354.154 128
uWebSockets (latest) 240 74172.7 309.053 128
LAppS 0.7.0 240 83627.4 348.447 512
uWebSockets (latest) 240 71024.4 295.935 512
LAppS 0.7.0 240 79270.1 330.292 1024
uWebSockets (latest) 240 66499.8 277.083 1024
LAppS 0.7.0 240 51621 215.087 8192
uWebSockets (latest) 240 45341.6 188.924 8192

В данном тесте как и в последующих кол-во пакетов на смом деле вдвое выше, т.к. замер производится на on_message и в методе on_message клиента производится отправка нового пакета того-же размера. Т.е. запрос клиента и ответ сервера имеют одинаковый размер, и если считать объём трафика обработанного сервером, то нужно удваивать результат RPS умножать на payload и пренебрегая заголовками можно получить примерный объём трафика в байтах.


Очевидно, что при одновременной работе 240 процессов клиентов, самому LAppS-у (как и uWebSockets) остаётся не так уж и много ресурсов ЦПУ.


Я просмотрел несколько клиентских реализаций для WebSocket под Lua, и к сожалению не нашёл простого и достаточно производительного модуля, с помощью которого, я-бы мог как следует нагрузить LAppS. Поэтому, как обычно сделал свой велосипед.


Модуль имеет довольно простой интерфейс и иммитирует поведение браузерного WebSocket API


Простой пример того как работать с этим модулем (сервис для получения сделок с BitMEX):


Скрытый текст
bitmex={}
bitmex.__index=bitmex

bitmex.init=function()
end

-
bitmex.run=function()
-- подключаемся к BitMEX
local websocket,errmsg=cws:new(
  "wss://www.bitmex.com/realtime",
  {
    ["onopen"]=function(handler)
    -- после установления WebSocket соединения отправляем запрос 
      local result, errstr=cws:send(handler,[[{"op": "subscribe", "args": ["orderBookL2:XBTUSD"]}]],1);
      -- Тип отправляемого сообщения 1 (OpCode 1 - ТЕХТ)
      if(not result) -- если отравка сообщения была неудачной, - обрабатываем
      then
        print("Error on websocket send at handler "..handler..": "..errstr);
      end
    end,
    ["onmessage"]=function(handler,message,opcode)
      print(message) -- выводим на экран сообщения BitMEX по запрошенному топику.
    end,
    ["onerror"]=function(handler, message) -- обрабатываем ошибки соединения
      print(message..". Socket FD:  "..handler);
    end,
    ["onclose"]=function(handler) -- реагируем на закрытие сокета
      print("WebSocket "..handler.." is closed by peer.");
    end
  });

  if(websocket == nil) -- если не удалось подключиться
  then
   print(errmsg)
  else
    while not must_stop()
    do
      cws:eventLoop(); -- poll событий
    end
  end
end

return bitmex;

Сразу предупреждаю, модуль появился только сегодня и он слабо оттестирован.


Для тестирования я написал простой сервис для LAppS и назвал его так-же незатейливо benchmark.


Этот сервис на старте создаёт 100 соединений к эхо серверу WebSocket (не важно какому), и при удачном соединении отправляет 1кб сообщение. При приёме сообщения от сервера, он отправляет его назад.


Мой домашний комп: Intel® Core(TM) i7-7700 CPU @ 3.60GHz, microcode 0x5e
Память: DIMM DDR4 Synchronous Unbuffered (Unregistered) 2400 MHz (0,4 ns), Kingston KHX2400C15/16G


Всё тестирование проводилось на этом локалхосте.


Конфигурация эхо сервиса в LAppS:


  "echo": {
      "auto_start": true,
      "instances": 2,
      "internal": false,
      "max_inbound_message_size": 16777216,
      "preload": null,
      "protocol": "raw",
      "request_target": "/echo"
    }

Параметр instances требует от LAppS старта двух параллельных эхо сервисов.


Конигурация бенчмарк-сервиса (клиента):


  "benchmark" : {
    "auto_start" : true,
    "instances": 4,
    "internal": true,
    "preload" : [ "cws", "time" ]
  }

T.e. при старте создаётся 4 экземпляра сервиса-бенчмарка


Результат с включенным TLS


Сервер Кол-во клиентов RPS сервера RPS на клиента payload (bytes)
LAppS 0.7.0-Upstream 400 257828 644.57 1024
nginx &lua-resty-websocket 4 workers 400 33788 84.47 1024
websocketpp 400 9789.52 24.47 1024

uWebSockets оттестировать пока не удалось, — TLS handshake ругается на SSLv3 (мой клиент использует TLSv1.2 и в используемом мной libreSSL SSLv3 вырезан).


Результат без TLS


Сервер Кол-во клиентов RPS сервера RPS на клиента payload (bytes)
LAppS 0.7.0-upstream 400 439700 1099.25 1024
uWebSockets-upstream 400 247549 618.87 1024

Почему в заголовке "полмилиона" сообщений, а в тесте 257828? Потому-что сообщений вдвое больше (как и было разъяснено выше).


uWebsockets, показывает незавидные результаты в этом тесте, только потому, что он работает на 1-м ядре, многопоточная версия uWebSockets из репозитория проекта, на самом деле не работает и при включении TLS имеет data-race в OpenSSL стэке.


Если представить, что uWebSockets прекрасно работает на 2-х ядрах (как 2 эхо-сервиса LAppS), то ему условно можно будет зачесть 495098 RPS (просто удвоив результат из таблицы).


Но нужно учитывать, что эхо сервер (uWebSockets) ничего с полученными данными не делает, а сразу отправляет назад. LAppS-же передаёт данные в Lua стек, соответствующему сервису.


Что ещё нового появилось в LAppS


  • Модуль аутентификации ч-з PAM: pam_auth
  • Модуль очередей сообщений: mqr — для обмена сообщениями между сервисами в рамках одного сервера LAppS (для мултисерверного обмена нужно использовать, что-то уже существующее, например: RabbitMQ, mosquitto, etc)
  • ACL сетевых соединений

Со всем этим можно ознакомиться на wiki странице проекта.


Ну и на закуску, для ценителей, чем же собственно LAppS занимается во время этого тестирования.


Без TLS


Скрытый текст
Очевидный лидер iptables.
     4.98%  lapps    [ip_tables]             [k] ipt_do_table

Возврат из системных вызовов
     3.80%  lapps    [kernel.vmlinux]        [.] syscall_return_via_sysret

Это передача данных между сервером и Lua сервисами
     3.52%  lapps    libluajit-5.1.so.2.0.5  [.] lj_str_new

Парсинг потока данных WebSocket сервером
     1.96%  lapps    lapps                   [.] WSStreamProcessing::WSStreamServerParser::parse

Обращения к системным вызовам
     1.88%  lapps    [kernel.vmlinux]        [k] copy_user_enhanced_fast_string
     1.81%  lapps    [kernel.vmlinux]        [k] __fget
     1.61%  lapps    [kernel.vmlinux]        [k] tcp_ack
     1.49%  lapps    [kernel.vmlinux]        [k] _raw_spin_lock_irqsave
     1.48%  lapps    [kernel.vmlinux]        [k] sys_epoll_ctl
     1.45%  lapps    [xt_tcpudp]             [k] tcp_mt

Воркеры LAppS
     1.35%  lapps    lapps                   [.] LAppS::IOWorker<false, true>::execute

Клиент бенчмарка
     1.28%  lapps    lapps                   [.] cws_eventloop
...
     1.27%  lapps    [nf_conntrack]          [k] __nf_conntrack_find_get.isra.11
     1.14%  lapps    [kernel.vmlinux]        [k] __inet_lookup_established
     1.14%  lapps    libluajit-5.1.so.2.0.5  [.] lj_BC_TGETS

Эхо серверы взгляд со стороны C++
     1.01%  lapps    lapps                   [.] LAppS::Application<false, true, (abstract::Application::Protocol)0>::execute

...
     0.98%  lapps    [kernel.vmlinux]        [k] ep_send_events_proc
     0.98%  lapps    [kernel.vmlinux]        [k] tcp_recvmsg
     0.96%  lapps    libc-2.26.so            [.] __memmove_avx_unaligned_erms
     0.93%  lapps    libc-2.26.so            [.] malloc
     0.92%  lapps    [kernel.vmlinux]        [k] tcp_transmit_skb
     0.88%  lapps    [kernel.vmlinux]        [k] sock_poll
     0.85%  lapps    [nf_conntrack]          [k] nf_conntrack_in
     0.83%  lapps    [nf_conntrack]          [k] tcp_packet
     0.79%  lapps    [kernel.vmlinux]        [k] do_syscall_64
     0.78%  lapps    [kernel.vmlinux]        [k] ___slab_alloc
     0.78%  lapps    [kernel.vmlinux]        [k] _raw_spin_lock_bh
     0.73%  lapps    libc-2.26.so            [.] _int_free
     0.69%  lapps    [kernel.vmlinux]        [k] __slab_free
     0.66%  lapps    libcryptopp.so.5.6.5    [.] CryptoPP::Rijndael::Base::UncheckedSetKey
     0.66%  lapps    [kernel.vmlinux]        [k] tcp_write_xmit
     0.65%  lapps    [kernel.vmlinux]        [k] sock_def_readable
     0.65%  lapps    [kernel.vmlinux]        [k] tcp_sendmsg_locked
     0.64%  lapps    libc-2.26.so            [.] vfprintf

Собственно отправка сообщений клиентом (сервисом - bemchmark)
     0.64%  lapps    lapps                   [.] LAppS::ClientWebSocket::send
...
     0.64%  lapps    [kernel.vmlinux]        [k] tcp_v4_rcv
     0.63%  lapps    [kernel.vmlinux]        [k] __alloc_skb
     0.61%  lapps    lapps                   [.] std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release
     0.61%  lapps    [kernel.vmlinux]        [k] _raw_spin_lock
     0.60%  lapps    libc-2.26.so            [.] __memset_avx2_unaligned_erms
     0.60%  lapps    [kernel.vmlinux]        [k] kmem_cache_alloc_node
     0.59%  lapps    libluajit-5.1.so.2.0.5  [.] lj_tab_get
     0.59%  lapps    [kernel.vmlinux]        [k] __local_bh_enable_ip
     0.58%  lapps    [kernel.vmlinux]        [k] __dev_queue_xmit
     0.57%  lapps    [kernel.vmlinux]        [k] nf_hook_slow
     0.55%  lapps    [kernel.vmlinux]        [k] ep_poll_callback
     0.55%  lapps    [kernel.vmlinux]        [k] skb_release_data
     0.54%  lapps    [kernel.vmlinux]        [k] native_queued_spin_lock_slowpath
     0.54%  lapps    libc-2.26.so            [.] cfree@GLIBC_2.2.5
     0.53%  lapps    [kernel.vmlinux]        [k] ip_finish_output2
     0.49%  lapps    libluajit-5.1.so.2.0.5  [.] lj_BC_RET
     0.49%  lapps    libc-2.26.so            [.] __strlen_avx2
     0.48%  lapps    [kernel.vmlinux]        [k] _raw_spin_unlock_irqrestore

С найдём 10 отличий при работе с TLS


Скрытый текст
    3.73%  lapps    [kernel.vmlinux]        [k] syscall_return_via_sysret
     3.49%  lapps    libcrypto.so.43.0.1     [.] gcm_ghash_clmul
     3.42%  lapps    libcrypto.so.43.0.1     [.] aesni_ctr32_encrypt_blocks
     2.74%  lapps    [ip_tables]             [k] ipt_do_table
     2.17%  lapps    libluajit-5.1.so.2.0.5  [.] lj_str_new
     1.41%  lapps    libpthread-2.26.so      [.] __pthread_mutex_lock
     1.34%  lapps    libssl.so.45.0.1        [.] tls1_enc
     1.32%  lapps    [kernel.vmlinux]        [k] __fget
     1.16%  lapps    libcrypto.so.43.0.1     [.] getrn
     1.06%  lapps    libc-2.26.so            [.] __memmove_avx_unaligned_erms
     1.06%  lapps    lapps                   [.] WSStreamProcessing::WSStreamServerParser::parse
     1.05%  lapps    [kernel.vmlinux]        [k] tcp_ack
     1.02%  lapps    [kernel.vmlinux]        [k] copy_user_enhanced_fast_string
     1.02%  lapps    [nf_conntrack]          [k] __nf_conntrack_find_get.isra.11
     0.98%  lapps    lapps                   [.] cws_eventloop
     0.98%  lapps    [kernel.vmlinux]        [k] native_queued_spin_lock_slowpath
     0.93%  lapps    libcrypto.so.43.0.1     [.] aead_aes_gcm_open
     0.92%  lapps    lapps                   [.] LAppS::IOWorker<true, true>::execute
     0.91%  lapps    [kernel.vmlinux]        [k] tcp_recvmsg
     0.89%  lapps    [kernel.vmlinux]        [k] sys_epoll_ctl
     0.88%  lapps    libcrypto.so.43.0.1     [.] aead_aes_gcm_seal
     0.84%  lapps    [kernel.vmlinux]        [k] do_syscall_64
     0.82%  lapps    [kernel.vmlinux]        [k] __inet_lookup_established
     0.82%  lapps    [kernel.vmlinux]        [k] tcp_transmit_skb
     0.79%  lapps    libpthread-2.26.so      [.] __pthread_mutex_unlock_usercnt
     0.77%  lapps    [kernel.vmlinux]        [k] _raw_spin_lock_irqsave
     0.76%  lapps    [xt_tcpudp]             [k] tcp_mt
     0.71%  lapps    libcrypto.so.43.0.1     [.] aesni_encrypt
     0.70%  lapps    [kernel.vmlinux]        [k] _raw_spin_lock
     0.67%  lapps    [kernel.vmlinux]        [k] ep_send_events_proc
     0.66%  lapps    libcrypto.so.43.0.1     [.] ERR_clear_error
     0.63%  lapps    [kernel.vmlinux]        [k] sock_def_readable
     0.62%  lapps    lapps                   [.] LAppS::Application<true, true, (abstract::Application::Protocol)0>::execute
     0.61%  lapps    libc-2.26.so            [.] malloc
     0.61%  lapps    [nf_conntrack]          [k] nf_conntrack_in
     0.58%  lapps    libssl.so.45.0.1        [.] ssl3_read_bytes
     0.58%  lapps    libluajit-5.1.so.2.0.5  [.] lj_BC_TGETS
     0.57%  lapps    [kernel.vmlinux]        [k] tcp_write_xmit
     0.56%  lapps    libssl.so.45.0.1        [.] do_ssl3_write
     0.55%  lapps    [kernel.vmlinux]        [k] __netif_receive_skb_core
     0.54%  lapps    [kernel.vmlinux]        [k] ___slab_alloc
     0.54%  lapps    libc-2.26.so            [.] __memset_avx2_unaligned_erms
     0.51%  lapps    [kernel.vmlinux]        [k] _raw_spin_lock_bh
     0.51%  lapps    libcrypto.so.43.0.1     [.] gcm_gmult_clmul
     0.51%  lapps    [kernel.vmlinux]        [k] sock_poll
     0.48%  lapps    [nf_conntrack]          [k] tcp_packet
     0.48%  lapps    libc-2.26.so            [.] cfree@GLIBC_2.2.5
     0.48%  lapps    libssl.so.45.0.1        [.] SSL_read
     0.46%  lapps    [kernel.vmlinux]        [k] copy_user_generic_unrolled
     0.45%  lapps    [kernel.vmlinux]        [k] tcp_sendmsg_locked
     0.45%  lapps    lapps                   [.] LAppS::ClientWebSocket::send
     0.44%  lapps    libc-2.26.so            [.] _int_free
     0.44%  lapps    libssl.so.45.0.1        [.] ssl3_read_internal
     0.43%  lapps    [kernel.vmlinux]        [k] futex_wake
     0.42%  lapps    libluajit-5.1.so.2.0.5  [.] lj_tab_get
     0.42%  lapps    libc-2.26.so            [.] vfprintf
     0.41%  lapps    [kernel.vmlinux]        [k] tcp_v4_rcv

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (8):

  1. Moxa
    /#19037979

    интересная штука, а можно подробную инструкцию как запускать бенчмарки? просто ./benchmark/runBenchmark.sh у меня не работает

    • thatsme
      /#19038277

      Это старые бенчмарки, они с websocketpp сделаны. По идее ./runBenchmark.sh должен скомпилить из исходников 2 файла ./benchmark_tls и benchmark_nontls. Они по сути и запускаются.
      Что-бы runBenchmark.sh работал, вы должны находиться в том-же каталоге, в котором расположены 3 файла: runBenchmark.sh, echo_client_tls.cpp и echo_client.cpp
      При запуске нужно указывать кол-во процессов, размер payload и uri сервера WebSocket.

      Запускать так:
      ./runBenchmark.sh 240 1024 wss://localhost:5083/target

      Естественно вместо wss:// мозно использовать ws://, вместо localhost можно подставить IP конкретного сервера WebSocket с запущенным echo-сервисом. /target требуется только если сервер знает что делать с таргетом.

      — Если хотите использовать LAppS для тестов, то нужно поднять эхо сервис:

      В файле /opt/lapps/etc/conf/lapps.json добавить ехо сервис:

      {
        "directories": {
          "app_conf_dir": "etc",
          "applications": "apps",
          "deploy": "deploy",
          "tmp": "tmp",
          "workdir": "workdir"
        },
        "services": {
          "echo": {
            "auto_start": true,
            "instances": 2,
            "internal": false,
            "max_inbound_message_size": 16777216,
            "preload": null,
            "protocol": "raw",
            "request_target": "/echo",
            "extra_headers" : {
              "Service" : "echo",
              "Strict-Transport-Security" : "max-age=31536000; includeSubDomains"
            }
          }
        }
      }
      

      Потом запустить lapps в режиме демона: /opt/lapps/bin/lapps -d

      Потом перейти в директорию где лежит ./runBenchmark.sh и запустить его. В URI обязательно указывать таргет /echo — т.к сервис поднят именно для этого таргета (см конфиг сервиса выше)

      Новый бенчмарк работает как сервис LAppS, т.е. его нужно задеплоить. И да, это только после сборки из гита.

      P.S: демон можно остановить с помощью kill -15 pid-LappS. Тесты прерывать по Ctrl-C бесполезно, — процессы в фоне будут крутиться. После стопа LappS-a клиентские процессы отвалятся.

  2. thatsme
    /#19038251

    не туда

  3. trapwalker
    /#19038547

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

    • thatsme
      /#19038845

      Я честно не могу представить, что «змейка» должна делать в онлайне. Давайте так: вы делаете фронтенд, и говорите мне что нужно от бэкенда, думаю осилим вдвоём. Пишите в личку.

      • trapwalker
        /#19040255

        Змеек может быть много и каждой управляет отдельный человек. Что-то вроде slither.io. Такие задачи действительно часто упираются в логистику сообщений. Если делать в лоб, то там квадратичная зависимость.
        На счет скооперироваться=) так я ж тоже бэкендщик=)

  4. tgz
    /#19039979

    На одном CPU — это сколько потоков?

    • thatsme
      /#19041009

      Вообще на этом CPU 4-e ядра, если s Hyperthreading то 8.

      Параллельно работающих IOWorkers в тестах — 4
      Параллельных инстансов echo сервиса: 2
      Параллельныйх инстансов нагрузки: 4

      Load при 5-мин исполнении: 3.97
      Использование CPU LAppS-ом (это и клиентом и сервером) 6.37 CPU
      Involuntary context switches: 0
      Voluntary context switches: 14

      Из железа выжато всё.

      Кстати на ноуте пробовал запустить (AMD A8) — в 10-15 раз медленнее, но там и частота вдвое ниже и память тормозная (Хотя 4-е честных ядра).