Моё внимание привлекла статья «Асинхронность в программировании» — это расшифровка доклада Ивана Пузыревского, преподавателя школы анализа данных Яндекса, внутри есть и видео.
Общая концепция асинхронного программирования и выводы изложены весьма доступно и грамотно.
Пересказывать всю статью в этом выпуске подкаста не буду, повторю за Иваном несколько основных тезисов и выскажу свои мысли на тему асинхронного программирования в PHP.
Пользуясь случаем, приглашаю всех на конференцию PHP Russia 2019, 17 мая в Москве – обсудим все эти животрепещущие темы. В частности там будет доклад про асинхронный PHP срывающий все покровы!
В первую очередь, зачем нам нужно асинхронное программирование? Какую проблему оно решает? Производительность!
Давайте разберёмся поподробнее. У нас есть приложение на сервере, будь то классический PHP или даже небольшое веб-приложение, написанное на коленке на C++. Приложение принимает запрос от пользователя, дальше мы идём в базу данных, чтобы загрузить данные, получаем ответ от СУБД, формируем HTML страницу, отправляем пользователю. Если мы пишем простой и понятный синхронный код, то в момент запроса к базе данных наш процесс, наша программа, останавливается и просто ждёт ответа от базы. И это понятно, не получив данные из базы, мы не можем перейти к следующему шагу формирования HTML страницы.
Запрос в СУБД и получение ответа от СУБД – это операция ввода-вывода и она у нас блокирующая, наш процесс, наше приложение заблокировалось в ожидании данных от СУБД. В этот промежуток времени наш процессор (CPU) простаивает. Нагрузка переместилась на сервер СУБД, там сейчас идёт активная работа по поиску и выборке данных, но наш сервер приложений оказывается не загружен.
А что если в этот момент на сайт приходит новый посетитель, т.е. мы получаем новый входящий HTTP запрос по сети? Мы можем его принять и обработать, наш процессор простаивает без дела, но кто займётся новым посетителем? Чтобы конкурентно обрабатывать много параллельных запросов от пользователей, мы можем запустить много процессов нашего приложения, например, php-fpm запускает несколько воркеров (максимально кол-во которых можно отрегулировать в конфиге php-fpm параметром pm.max_children). А дальше в дело вступает ядро операционной системы, в котором есть планировщик задач, он жонглирует нашими процессами, так что при достаточно хорошем потоке посетителей мы видим отличную утилизацию процессора. Т.е. пока одни php-fpm воркеры зависли в блокирующей операции ввода-вывода, другие активно занимаются полезной работой – склеиванием строк в результирующий HTML. Все при деле, все довольны, где проблема?
Проблема в эффективности ядра операционной системы – работа планировщика и процесс переключения между процессами не бесплатны.
Иван приводит в своём докладе цифры: переключение контекста из нашего приложения в ядро стоит единицы микросекунд. Дальне не сложный подсчёт: допустим переключение контекста стоит нам 5 микросекунд. К нам на сайт приходит 20 тысяч запросов в секунду и, чтобы их обслужить мы решили запустить 20 тысяч процессов. На 20 тысяч переключений контекста ядро ОС потратит 100 000 микросекунд (по 5 микросекунд на каждое). Микросекунда – это одна миллионная, значит 100 000 микросекунд – это одна десятая секунды, это накладные расходы ядра. Если мы хотим уложиться в секунду, т.е. обслужить всех внезапно пришедших 20 тысяч пользователей, у нас остаётся 9/10 секунды на реальную работу бизнес логики приложения. При таком подходе влоб, когда мы запускаем по процессу на каждое входящее соединение, на 20 тыс входящих соединений получаем что ядро ОС съедает 10% CPU. А если придёт 40 тыс пользователей и мы запустим 40 тыс процессов, то ядро ОС будет потреблять уже 20% CPU. И так далее.
Что делать? А давайте запустим всего один процесс (или небольшое их число, по числу ядер процессора), чтобы не утруждать ядро ОС переключением контекстов. А чтобы не было простоя процессора в периоды блокирующих операций ввода-вывода, наша программа должна сама уметь жонглировать задачами на обработку входящих запросов, иметь собственный планировщик и некую систему для запуска и приостановки пользовательского кода, кода бизнес-логики. По сути, мы хотим создать мини операционную систему, но такую, чтобы она не съедала 10% CPU на накладные расходы при 20 тыс запросов в секунду. Удастся ли нам?
Многим удалось. Например, веб-серверу Nginx или платформе Node.js (хотя это в первую очередь заслуга библиотеки libuv, которая рулит задачами под капотом ноды). И одним из удобных способов является реализация так называемого Event Loop.
Но возникает неприятный побочный эффект. Если раньше, когда процессами рулило ядро операционной системы, мы могли писать тупой синхронный код, инструкция за инструкцией. Теперь же, когда у нас есть собственная реализация Event Loop, в нашем коде появляются callbacks, promises и coroutines.
Лично мне это ощущается как протекающие абстракции. Следите за ходом событий: ради производительности мы переизобрели часть ядра операционной системы, реализовали Event Loop при планировщик задач, накладные расходы на CPU уменьшились, полезная нагрузка увеличилась, КПД вырос, но теперь стало сложнее писать пользовательский код. Об асинхронности всё время нужно думать. Я не просто делаю запрос к MySQL и получаю результат, я получаю этот результат потом, в callback функции. Или в Promise. Самые продвинуты разработчики скажут мне: «чувак, используй async/await» — который, кажется, с недавних пор заработал в Node.js и уже давно был, например, в C#. Код выглядит практически как синхронный, но это не избавляет меня от необходимости думать об асинхронном коде: «нужно ли мне перед вызовом данной функции ставить await или не нужно?». Ответ вроде простой, ибо достаточно взглянуть на сигнатуру функции и проверить, есть ли там ключевое слово async, но я бы хотел вообще об этом не думать!
Производительность, это, конечно, хорошо, но вся индустрия разработки программного обеспечения эволюционно шла по модели увеличения числа абстракций, чтобы сфокусировать программиста на решении его конкретных бизнес задач. В частности, если мы делаем веб-сервис, интернет-магазин или какой-то другой продукт для конечных пользователей, мы хотим думать в терминах этого бизнеса: пользователь, корзина, товары, транзакции по оплате. Я не хочу думать о том, асинхронно я сохраняю корзину в базу или синхронно! Пусть об этом будмает рантайм или ядро ОС – мне не важно. Если ядро ОС жрёт 10%, значит это та цена, которую я готов заплатить за удобные абстракции. Мы же не сидим и не упариваемся по ассемблеру для реализации страницы профиля пользователя в нашем сайте.
Безусловно, есть разные уровни разработки, кто-то пишет не конечные продукты, а инфраструктурные сервисы, тот же веб-сервер Nginx– там асинхронная модель хоть и привнесла сложности в код, но в этом и соль самого продута: очень быстрый, высокопроизводительный веб-сервер! Также как есть место и ассемблерным вставкам при решении определённых задач.
Возвращаясь к веб-приложениям. Можно ли писать удобно (без callbacks, promises и async/await) и при этом, чтобы runtime твоего языка был достаточно эффективным, например, с неблокирующими сетевыми запросами? Можно – язык Go. Все функции горутинные, не нужно писать await перед сетевыми вызовами, runtime сам аккуратно уберёт твою горутину в фон, пока данные идут по сети, предоставив процессорное время другой горутине. Тут, конечно, можно меня подколоть, мол мне нравится отсутствие await, но в этом Go нужно через строчку проверять if err!= nil, да и других сложностей хватает и не так уж он удобен для написания развесистых веб-приложений, которые мы пишем на PHP. Недостатки и достоинства Go — это отдельная большая тема которую я не буду затрагивать, но, надеюсь, я смог донести общую претензию к асинхронному программированию: в некоторых языках об асинхронном программировании нужно постоянно думать, приходится думать, потому что оно просачивается даже в языковые конструкции и синтаксис.
Справедливости ради, есть моменты, когда асинхронное программирование оправдано на том прикладном уровне, на котором мы обычно работаем в PHP. Если я хочу сделать несколько запросов в разные API, причём эти запросы независимы, т.е. хотелось бы их сделать параллельно, чтобы как можно быстрее отдать результат пользователю. В PHP есть инструменты для этого – используем curl_multi, создаём каналы, потом перебираем их в цикле с помощью curl_multi_select – всё очень просто и понятно, никаких Promise.all, и, кстати, весьма похоже на Go с его каналами и select’ом. Да тут по большому счёту и нет никакого асинхронного программирования, торчащего ушами наружу – всю магию делает curl.
Подведём итог, какие выводы я делаю:
- асинхронное программирование нужно для улучшения производительности приложения;
- улучшение производительности особенно заметно на приложениях, которые активно занимаются вводом-выводом, и менее заметно на приложениях, которые сами что-то вычисляют, нагружая CPU (почему так, подробности в исходной статье);
- но асинхронное программирование уменьшает производительность самого программиста, это дополнительная когнитивная нагрузка;
- мы привыкли снимать когнитивную нагрузку, повышая уровень абстракции: от ассемблера к Си, от ручного управления памятью к сборке мусора, от простых библиотек к фреймворкам, от простых SQL запросов к ORM. Впрочем, иногда это заходит слишком далеко и мы возвращаемся на шаг назад к QueryBuilder’ам;
- стоит ли когнитивная нагрузка от асинхронного программирования той выгоды (той производительности) которую получаем в результате — решать вам, проекты разные, задачи разные, нагрузки разные. Ровно, как и со сборкой мусора, есть задачи, где ей не место, но кажется, большая часть кода, который пишется в наши дни, пишется именно на языках со сборкой мусора;
- для своих бизнес-приложений я сознательно выбираю синхронное программирование на PHP, у меня нет каких-то невероятных нагрузок. А если начнут появляться, я буду в первую очередь наращивать мощность сервера, а не переписывать код в асинхронном стиле;
- наконец, если появятся супер-нагруженные вводом-выводом участки, там, где реально могло бы помочь асинхронное программирование, я перепишу эти фрагменты на Go;
- а в высокоуровневом бизнес коде я говорю НЕТ асинхронному программированию!
Источник: 5minphp.ru