Асинхронное программирование в веб-проектах

Согласно каноническому определению, асинхронное программирование (event-driven) – это когда результат функции вы получаете не сразу, а “когда-нибудь потом”. В этом посте я не буду говорить о мультипоточном выполнении программного кода. Речь о другом: вместо того, чтобы бороться с нагрузкой на систему путём масштабирования или тупой оптимизации кода и архитектуры, – нагрузка просто “размазывается” во времени.

Не смотря на то, что пользователь привык ожидать отклика от сайта практически мгновенно, между действием человека и реакцией системы всегда есть промежуток, фактически игнорируемый сознанием человека. Его можно и нужно заполнять ресурсоёмкими действиями, но выполняя их не одновременно.

Например, если у вас сервер “дохнет” при превышении значений – 100 одновременных запросов по 10 миллисекунд каждый, вы можете “размазать” их на протяжении секунды реального времени – выполнить последовательно – и получить тем самым снижение нагрузки до 100 раз. Без заметного снижения комфорта работы пользователей. Без масштабирования системы, апгрейда серверов, оптимизации кода и всего прочего.

Эта простая мысль требует, в общем-то, довольно простой реализации – которая называется “очередь задач”. Более того, очередь позволяет не просто снижать нагрузку, но и выполнять ресурсоёмкие задачи “в фоновом режиме”, позволяя человеку продолжать работать с сайтом. А когда задача выполнится – он получит уведомление.

Для проектов на php есть готовые решения, но для понимания самой сути дела я расскажу как реализовать очередь своими руками. Вам потребуется три компонента:

– постановщик задач в очередь;
– исполнитель (обработчик) задач;
– система отклика (обратной связи).

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

Обработчик работает в бесконечном цикле и берёт по одной задаче строго в порядке очередности (далее рассмотрим исключения из этого правила), затем выполняет её.

Система отклика сообщает пользователю о факте выполнения задачи (или возникновения ошибки). Это может быть уведомление на экране, сообщение в icq, письмо на e-mail, или постановка следующей задачи.

Иногда само по себе выполнение задачи может порождать новую задачу. Например, когда вы сохраняете страницу в админке CMS, в очередь можно поставить задачу по обновлению контента этой страницы в поисковом индексе сайта. В свою очередь обновление индекса может вызвать, например, необходимость перестроить информацию об этой странице в xml-карте сайта (sitemap.xml) – это должно быть поставлено как отдельная задача в ту же очередь.

Иногда ресурсоёмкие задачи настолько глобальны, что постановка их в очередь целиком может вызывать опасения. Логичное решение – поставить в очередь несколько задач, таким образом как-бы решая крупную задачу “по шагам”. Но как обеспечить их строго поочерёдное выполнение? Можно заставить обработчик выполнять очередь строго по порядку (как было сказано ранее), но тогда мелкие задачи будут неоправданно долго висеть и ждать. Решение – использование “previous_id”, идентификатора предыдущей задачи. Когда обработчик берёт очередную задачу и видит, что у неё есть “невыполненный предок”, он просто завершает текущую итерацию без выполнения. Следующая итерация обработчика возьмёт уже другую задачу, и если у неё такового предка нет (или он просто не указан) – задача выполнится. Таким образом решаются две цели – мелкие “одиночные” задачи выполняются равномерно с крупными “пошаговыми”.

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

Важный аспект работы с очередями – это минимальный интервал между событиями одного типа. Допустим, когда вам нужно разослать 1500 писем своим клиентам, легко поставить в очередь 1500 задач и разослать письма за полчаса. Но если вы живёте на недорогом VDS, ваш хостер наверняка помянет вас недобрым словом за такой “спам”. Вам необходимо предусмотреть интервалы между событиями одного и того же типа, чтобы избежать таких ситуаций.

Ещё один важный аспект – дублирование задач. Как было сказано в примере ранее, обновление страницы должно ставить задачу по её переиндексации внутренней поисковой машиной сайта. Однако если эта переиндексация не успела произойти, а страницу отредактировали вновь, имеет ли смысл ставить вторую задачу? Очевидно, нет. Следует предотвратить избыточное дублирование одинаковых задач.

Некоторые задачи требуется выполнять в окружении конкретного пользователя. Например, обновлять какие-то данные в его профайле, списке его заказов, или в других его персональных данных. Можно было бы записывать логин пользователя, но есть более интересное решение: записывать идентификатор сессии. Тогда в обработчике задачи можно просто поднять ту же сессию и получить систему фактически в состоянии “запущенной от имени этого пользователя”. Но при этом следует учесть, что если сессия устареет или будет окончена пользователем принудительно – такая задача не выполнится. Как вариант решения, при попытке выхода из системы пользователя можно предупреждать о текущих незавершённых задачах.