Event-Driven Architecture: Event Loop implementation details / RU


0. Введение

todo

1. EDA. определение.

Первые упоминаний event-driven architecture уходят в 1970тые будет свое начало Интересно заметить что описание на wiki звучит: "Событие можно определить как «существенное изменение состояния»[1]. Например, когда покупатель приобретает автомобиль, состояние автомобиля изменяется с «продаваемого» на «проданный». Системная архитектура продавца автомобилей может рассматривать это изменение состояния как событие, создаваемое, публикуемое, определяемое и потребляемое различными приложениями в составе архитектуры."

Ничего не напоминает? использованием в определении "state", т.е. не так давно появившихся подходов state-management. Интересно что использование state-management'а появилось только сейчас.

Интересно еще отметить "по структуре более ориентированы на непредсказуемые и асинхронные окружения", т.е. это

Системы посторнные по EDA выделяются наличием:

  1. Генератор событий
  2. Канал событий
  3. Механизм обработки событий
  4. Последующее действие, управляемое событиями

на язык фронтенда это можно перевести как:

??? 1. action ??? 2. - ??? 3. handlers ??? 4. reactions

2. Немного истории

2.1 apache

Давным давно в далекой далекой галактике появился apache HTTP server (созданный в 1995 году), так как это первый проект apache foundation, его обычно называют просто apache. Важную роль в apache выполняет модуль MPM (Multi-Processing Module), который, грубо говоря, отвечает обработку нескольких запросов несколькими процессами. Один из наиболее частых режимов использования которого mpm_prefork. Суть его в том, что на 1 запрос обрабатывает отдельный процесс с одним потоком. В этом режиме может быть запущено определенное количество X процессов, их количество, по большей части ограничивается RAM на сервере. И как вы думаете, что произойдет если придет X+1 запрос? Верно, он будет ждать пока освободится процесс.

TODO: либо demo, либо анимацию с примерами.

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

2.2 проблема C10k

В 1999 администратор популярного публичного FTP_сервера Simtel Ден Кегель обнаружил, что по аппаратным показателем средний узел его сети был готов держать 10k соединение, но программное обеспечение не могло выдать такой результат. [http://kegel.com/c10k.html]

2.3 nginx

NB: здесь, кстати, в 2002-2003 году начали вестись разговоры про SPA.

Но! через 5 лет после обозначения этой проблемы (в 2004 году) вышел первый релиз такого веб-сервера как nginx. nginx изначально был спроектирован на базе асинхронных неблокирующих event-driven алгоритмов. Каждый запрос помещается в event loop. В этом цикле события обрабатываются асинхронно, позволяя обрабатывать задачи в неблокирующей манере. Когда соединение закрывается оно удаляется из цикла.

https://www.eyerys.com/sites/default/files/nginx-apache-10kreqs.png

TODO: либо demo, либо анимацию с примерами.

А еще через 5 лет что появилось? Кто угадает?) Правильно платформа Node.js

2.4 libuv (Node.js)

В осонову платформы node.js легла библиотека libuv с реализацией event-loop. Что она на себя берет:

  1. во-первых, кроссплатформенность, так как она абстарагиерует работу с epoll kqueue и прочими.
  2. предоставляет API хендлеров и реквестов (к диску, сети, DNS резолв и пр.).
  3. собственно event loop (в дефолтной конфигурации один, в такой конфигурации его используется node.js, но так же можно создавать и несколько).

2.5 select poll epoll

Еще один важный момент измененный за это время это способ работы с "внешним миром для процессора", таким как файлы, сеть и т.д. В POSIX системах все является "файлом" будь то socket, file etc. И для работы с этим используются файловые дескрипторы. Изначать был select() он реализован еще в 1980ых годах когда сокеты назывались "сокетами Беркли" и никто, видимо, не продполагаю что в будущем появится сильная необходимость писать многопоточные приложения. более новый poll() стал более производительным при использовании многопоточности и на 1000 fd был быстрее select в ~40 раз. В epoll() стало еще лучше, нам сразу видно какие дескрипторы ждут, чтобы из них прочитали или в них записали данные, но здесь добавляется немного больше системных вызовов через poll() /TODO проверить, вот эта https://it.wikireading.ru/3323 статья все опровергает/и по сути, выйгрыш в производительности виден только на большом количестве "не активных" дескрипторов (порядка 10к). node.js в linux (простите, в gnu/linux) работает через epoll.

https://monkey.org/~provos/libevent/libevent-benchmark.jpg https://monkey.org/~provos/libevent/libevent-benchmark2.jpg

2.6 TCP соединения

Еще есть ньюанс что каждый браузер при открытии страницы делать разное количество TCP соединений и через разные промежутки это обрывает. Есть флаг keep-alive через который это можно более менее регульровать.

2.7 Demo libuv server

TODO пример реализации сервера на libuv:

int main() {
    loop = uv_default_loop();

    uv_tcp_t server;
    uv_tcp_init(loop, &server);

    uv_ip4_addr("0.0.0.0", DEFAULT_PORT, &addr);

    uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);
    int r = uv_listen((uv_stream_t*) &server, DEFAULT_BACKLOG, on_new_connection);
    if (r) {
        fprintf(stderr, "Listen error %s\n", uv_strerror(r));
        return 1;
    }
    return uv_run(loop, UV_RUN_DEFAULT);
}

3. event loop

3.1 анимированный представления

Про event loop очень много хороших графических представлений, как они работают и как отображаются. Возможно их было бы меньше будь у нас инструменты для отладки и мониторига состояния event-loop, так как сейчас они очень скудны. Но давайте рассмотрим пару интересных отсылок. https://www.youtube.com/watch?v=cCOL7MC4Pl0

3.2 реализации

3.2.1 libuv TODO

TODO: дать описание на основеоснове доки
http://docs.libuv.org/en/v1.x/_images/architecture.png

3.2.2 tokio

Сделаль сравнение с libuv
https://tokio.rs/docs/overview/

Как по мне, так там принципы очень похожи, единственная разница в "подстраивании под язык", libuv - C, tokio - rust. + в tokio из коробки идет реализаций future.

3.3 event loop в браузерах

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

The event loop is the mastermind that orchestrates: what JavaScript code gets executed when does it run when do layout and style get updated render: when do DOM changes get rendered It is formally specified in whatwg's HTML standard.

3.3.1 браузеры и отличие event loop от серверного.

TODO: https://stackoverflow.com/questions/25750884/are-there-significant-differences-between-the-chrome-browser-event-loop-versus-t

3.3.2 event loop спецификация.

У whatwg есть описание как должен выглядеть event loop.
Но, соотвествено реализации в браузерах расходятся и 
TODO расширить из https://github.com/atotic/event-loop

3.3.3 chromium

TODO из https://github.com/atotic/event-loop

3.4 puzlers

1. https://cdn.rawgit.com/atotic/event-loop/caa3cfd4/rendering-events.html
2. https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
3. пример из доклада арчибальда про click из браузера и из кода.

4. problems

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

4.1 отравление обработчика

TODO: анимированное представление. если мы добавили в очередь тысячу событий, и 500ое будет выполняться 1 минуту, то у всех событий с 501 до 1000 задержка будет не меньше 1 минуты. Поэтому очень важно следить за тем, что именно представляют собой обработчики.

4.2 redos

Один из интереснейших примеров таких уязвимостей это Regular expression Denial of Service [ReDOS].

Суть ее заключается в том, что регулярные выражения по своей природе рекурсивны из-за чего можно найти определенную строку, которая, при разборе ее регуляркой, сможет "съесть" много ресурсов. Для того кто знаком со временной сложностью алгоритмов, "O" большое для разбора по регулярке - экспоненциально, т.е. O(c^n), что очень плохо. Если вас это не пугает, то вот вам картинка, на ней экспоненцианалое время оранжевое (из класических примеров хуже только факториальное) https://twitter.com/jsunderhood/status/1169947400804425732/photo/1

А вот и на эту тему науч статья) "Freezing the Web: A Study of ReDoS Vulnerabilities in JavaScript-based Web Servers" https://usenix.org/system/files/conference/usenixsecurity18/sec18-staicu.pdf

Но если вам скучно читать это полотно текста, то вот есть примеры: https://github.com/sola-da/ReDoS-vulnerabilities Эта подборка примеров ReDoS уязвимостей в популярных библиотеках.

Там есть и lodash, и moment, и underscore.string. Благо что большинство этих проблем поправлены. И вот разобор от snyk на эту тему - https://snyk.io/blog/redos-and-catastrophic-backtracking/ В ней все с примерами доходчиво объясняется.

4.3 Упадет воркер - потеряются все коннекты с воркера

Еще один важный ньанс, касающийся Node.js. Если окажется так, что один из обработчиков повлечет за собой падение приложения. То все события находящиеся в очереди будут потеряны.

Давайте разберем пример. Представим что у нас есть сервер принимающий финансовые транзакции по http. TODO возможно стоит придумать более интересный пример..

TODO Demo показать пример в консоле.

4.4 Нет инструментов для анализа того что присходит в eventloop

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

4.4.1 Node.js

В Node.js не так давно велась разработка по предоставлению API для трассировки event loop TODO раскрыть детали: feature request: a way to inspect what's in the event loop https://github.com/nodejs/node/issues/1128 https://github.com/nodejs/node/issues/19063 https://github.com/nodejs/node/issues/19158 https://github.com/libuv/libuv/pull/1764 https://github.com/nodejs/node-report

4.5 Нет управляемой приоритизацией

как мы рассмотрели в libuv и с примерами в пазлерах, процесс обхода event loop является синхронной функцией, которая выполняет задачи по заранее заданной последовательности и это нельзя изменить. Но есть определенные хаки который рассмотрим чуть позже.

5. нахождение проблем

Как находить такие проблемы

5.1 devtools

В devtools основной инструмент для этого это timeline, на котором можно отслеживать время выполнения той или иной функции. TODO: demo с devtools в котором можно рассмотреть скрытые ньюансы.

5.2 Нахождение проблем, fuzzing

TODO: взять отсюда интересные моменты если они там есть http://people.cs.vt.edu/dongyoon/papers/EUROSYS-17-NodeFz.pdf

5.3 snyk

snyk. Следить за обновлениями и блабла Так же можно встроиться в CI. Показать пример.

5.4 добавление проверок в CI

TODO: статистика с devtools TODO: статистика со snyk

6. решение проблем

Какие же есть решения?

6.1 высвобождение основного потока

все handlers должны быть максимально "тонкие" все остальное выполнять вне основного потока.

6.2 fibers

TODO пример с fibers

7. Выводы