Привет! Я хочу показать, что завести gRPC на PHP — это нормальное боевое решение, которое пишется быстро, легко разворачивается и может быть для вас проще, чем сокеты.



Сначала все работало на REST и работало хорошо, но начался рост…


Skyeng постоянно взаимодействует с учениками: периодически нам надо позвонить — чтобы подтвердить запись на пробный урок или, например, уточнить, все ли хорошо, если человек пропустил занятие.

На заре школы работа со звонками велась вручную, но бизнесу быстро захотелось как-то автоматизировать и анализировать работу операторов. Это помог сделать Voximplant. Мы до сих пор пользуемся их технологиями — удобно.

Чтобы операторы не слушали гудки, не тратили время на прозвон недоступных номеров и так далее, у ребят есть PDS (predictive dialing system) — система автоматического дозвона. Она берет два пула — операторов и клиентов, и по ходу прозвона вычисляет контактность базы, задавая скорость дальнейшего набора номеров. Идея в том, чтобы операторы и клиенты как можно меньше ждали на линии.
Долгое время все работало по примерно такой схеме.

Например, мы загружаем PDS список из 1000 номеров — и знаем, что сейчас у нас 50 операторов. Начинаем прозванивать первые 100.

  • Номер недоступен — обрабатываем сценарий на JS, отправляем на endpoint номер и его статус, пишем в базу факт неудачного звонка.
  • Срабатывает автоответчик: все то же самое, но со статусом «автоответчик»?.
  • Происходит дозвон до клиента — он берет трубку и говорит «Алло»?. В это время из пула операторов срочно подыскивается первый подходящий: мы знаем, что если держать человека «на проводе»? больше двух секунд, он просто положит трубку.

При росте начались сложности. PDS вычисляет контактность базы — и набирает еще сколько-то номеров. А параллельно считает длительность текущих разговоров и обновляет статистику, чтобы балансировать время ожидания операторов и клиентов. Упор всегда делается на удобство клиента (те самые пара секунд), но время простоя оператора то не должно затягиваться. Нормально, если это 30-40 секунд. На практике случалось, что утилизация времени операторов достигала почти 30% — они долго сидели в ожидании, это было критично.

Также мы стали сталкиваться с кейсами двойного прозвона. С утра мы собирали большой колл-лист на тысячи номеров и загружали его в систему. Но параллельно через сайт приходили новые потенциальные ученики — опять же, опытным путем бизнес установил, что их надо прозвонить в приоритетном порядке в ближайшие часы после заявки. Опции «докинуть номеров»? у PDS не было. Поэтому мы останавливали ее, обновляли лист и снова запускали его в работу. Но на момент остановки часть номеров могла уйти в набор — а вот их статусы в базу еще не пришли. Они не помечались как прозвоненные и, бывало, что с человеком поговорили, а через две минуты ему набирает следующий оператор… Мы не могли пометить, что номер отдан в прозвон, на своей стороне: так как работали сразу с тысячами номеров, это вызывало большие тормоза на нашей базе.

Тогда ребята из Voximplant дали нам нам прототип своей новой PDS — более продвинутого решения, которое получило название PDS2. И нам надо было как-то подключиться к нему.

Почему выбрали gRPC? И почему не подошел клиент на Go


Ох, у gRPC много классных фич:

  • Protobuf как инструмент описания сериализации типов данных — мы описываем протокол в протофайл, это быстро.

Вот типичный protobuf-файл — очень похож на JSON, всё достаточно просто:

syntax = "proto3" ;

message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3
}

  • Есть gRPC-плагин, который позволяет сгенерировать весь необходимый код из протофайла — сервис, типы, PDS-клиент, в нашем случае. Единственное, что он не генерит, это серверную часть на PHP.
  • HTTP/2 в качестве транспорта — можно прекратить выполнение запроса на сервере, а еще переиспользовать один cокет для нескольких параллельных запросов.
  • Вместо набора «domain.com/сервис/коллекция/ресурс/запрос? параметр=значение», как в REST, есть только сервис. Все остальное описывается через Protobuf в терминах нашей модели и ее событий.

Однако, выбрали не из-за фич: просто выбора не было — PDS2 общалась только по gRPC.


А вот с Go мы попробовали. Прототип клиента от разработчиков из Voximplant какое-то время крутился в проде, но его тяжело было поддерживать. PHP основной язык в Skyeng, на нем написано почти все. И мы поняли, что надо тащить в Go-клиент много кода, а затем поддерживать его и в клиенте, и PHP-части. Например, у нас были проблемы с таймзонами — и было решение, но на PHP. И это всё приходилось уносить на тот Go-клиент.

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

Что делать, если PHP не завезли? Написать свое решение — это (почти) просто


Я следовал рекомендациям с gRPC.io для PHP. В принципе, там описано все, что нужно.

Был лишь один забавный нюанс. Пару дней искал решение, как сгенерировать код с неймспейсами в нашем протофайле. Все сгенерил, все нормально, только их не хватает. В итоге засел перечитывать всю документацию. Оказалось, оно называется packages.

Так что, если тоже зададитесь вопросом, всё достаточно просто: пишем в файле

package foo.bar;
message MyMessage {}

И это сгенерирует вот такой namespace.

Foo\Bar\MyMessage

Детали по ссылке.

Как это работает. При генерации мы задаем необходимые параметры подключения к Voximplant, стартуем, и у нас получается бесконечный цикл, который постоянно слушает наш стрим. Наш клиент — по факту, обычный демон.

Вот пример от Voximplant. Наш бандл показать не могу: он сильно разросся за счет сложной и специфичной для нас логики.

Что в итоге


Наш клиент вместе с supervisorD крутится на проде с января, он стабилен. В сочетании c супервизором это почти демон — если что, супервизор поднимет и запишет падение к себе.

Проблемы роста мы решили.

Благодаря демону мы отгружаем номера по запросу, динамически, маленькими порциями — по 50 за раз. И теперь, если у нас появляются какие-то «горячие» номера с морды сайта, он уже знает, что эти номера имеют самый высокий приоритет — когда от Voximplamt приходит новый запрос, отправляет их. У нас появилась гибкость.

А еще, время ожидания операторов сократилось примерно до 20 секунд, — но это уже чисто за счет лучших алгоритмов самой PDS2, которую писали не мы.