Фон

Ранее в этом году Lyft запустила функцию, позволяющую пользователям сравнивать режимы велосипеда / скутера с режимами совместного использования и общественного транспорта. Целью этого проекта было предоставить пользователям всю предварительную информацию, необходимую для планирования поездки на велосипеде или скутере, включая расчетное время прибытия, информацию о маршрутах и ​​ценах. После предоставления этой функции нашим пользователям мы заметили явную проблему в UX - режимы велосипеда и самоката часто мерцали, что сбивало с толку конечных пользователей.

В этом посте рассказывается, как мы использовали наши знания gevent из постов 1, 2 и 3 для устранения причины и устранения проблемы. Этот пост актуален для всех, кто использует чувствительные к задержкам микросервисы Python со смешанными рабочими нагрузками ЦП и ввода-вывода.

Обзор проблемы с высоты 10 000 футов

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

Наши серверные службы вычисляют «предложения» для каждого режима Lyft параллельно - мы установили жесткое ограничение в 500 мс на время, необходимое для создания каждого предложения, чтобы гарантировать быстрое взаимодействие с конечным пользователем. Если предложение не может быть сгенерировано в течение этих ограничений по времени ответа, предложение игнорируется, и те, которые были успешно сгенерированы, возвращаются пользователям. Клиенты опрашивают наши серверные службы каждые пару секунд, чтобы этот список предложений оставался актуальным для пользователя.

Мы обнаружили, что формирование предложений велосипедов и самокатов в некоторых случаях занимало более 500 мс, что означало, что в некоторых опросах клиентов наши серверные службы предоставляли предложения велосипедов и самокатов, тогда как в последующих опросах предложения байков и самокатов не срабатывали в течение времени ответа. ограничения и были проигнорированы.

Чтобы дать вам представление о том, насколько серьезна эта проблема, 0,5% предложений велосипедов / скутеров не были созданы из-за тайм-аутов, что дает нам 2,5 девятки надежности - не очень подходит для одного из самых ценных пользовательских интерфейсов в приложении Lyft.

Наши задержки p95 были довольно хорошими и стабильно ниже 450 мс.

Однако в некоторых регионах Lyft наши задержки p99 достигли 1 секунды (вдвое больше задержки p95s).

Задержки p999 при создании предложений велосипедов и самокатов составили поразительные 1,5 секунды (что более чем в 3 раза превышает нашу задержку p95).

После изучения этих данных возник вопрос: Почему существует такая большая разница между нашими задержками p95, p99 и p999? »

Чтобы ответить на этот вопрос, давайте подробнее рассмотрим, как создается предложение на байк или самокат. Внутренняя служба, отвечающая за создание предложений Lyft (назовем ее служба предложений), вычисляет каждое предложение параллельно. Чтобы вычислить цены на велосипеды и скутеры, offerservice, микросервис Go, отправляет восходящий запрос к pricingservice микросервису Python.

Вот диаграмма потока данных в кодировке ASCII, показывающая, как создается предложение на байк и самокат:

Мобильный клиент (iOS / Android) - ›offerservice (Go) -› pricingservice (Python) - ›восходящие микросервисы (Python + Go)

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

Основная причина

Изучая задержки запросов восходящей службы pricingservice, мы заметили, что некоторые вызовы, как сообщалось, занимали ~ 110 мс восходящей службой, но инструментарий pricingservice сообщил о задержках до 5,3 секунды, расхождение ~ 35 раз.

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

Используя нашу распределенную трассировку, мы обнаружили, что восходящие запросы от pricingservice были поставлены в очередь и долго ждали. Мы подозревали, что это могло иметь какое-то отношение к тому, как веб-сервер pricingservice планировал запросы.

Как упоминалось ранее, pricingservice - это микросервис Python, который использует Flask в качестве среды своего веб-приложения. Gunicorn - это веб-сервер, используемый для запуска Flask, а Gunicorn использует gevent для планирования нескольких запросов одновременно.

Напомним, что глобальная блокировка интерпретатора Python эффективно делает любую программу Python, привязанную к процессору, однопоточной. Итак, как же Gunicorn и gevent обслуживают тысячи входящих запросов в секунду с этим ограничением? gevent запускает гринлет (также известный как зеленый поток) для каждого запроса, полученного микросервисом. Планируется, что эти гринлеты будут запускаться с использованием цикла событий с использованием структуры совместной многозадачности, о которой мы говорили в Части 1.

Типичный гринлет для запроса (назовем этот гринлет A) может выполнять некоторую работу с ЦП, выполнять вызов восходящей службы (который включает в себя блокировку ввода-вывода) и, наконец, выполнять некоторую дополнительную работу с ЦП. При выполнении блокирующего ввода-вывода, то есть сетевого запроса в этом случае, гринлет передаст управление циклу событий.

Затем Gevent может найти еще одну гринлетку (назовем ее B) для планирования своего цикла событий. После завершения гринлета B, если гринлет A завершил свой блокирующий ввод-вывод, цикл событий планирует повторное выполнение гринлета A. Greenlet A завершает свою работу ЦП и выходит из цикла обработки событий. Цикл обработки событий теперь находит следующую гринлетку для запуска.

Ключевой вывод заключается в том, что когда гринлет выполняет сетевой вызов, мы ждем не только завершения вызова, но и цикла событий, чтобы снова запустить наш гринлет. Если мы запускаем слишком много одновременных гринлетов, работа ЦП для других запросов может «заблокировать» выполнение нашего запроса.

Давайте проиллюстрируем это на примере (представьте, что каждая ячейка представляет временной интервал 10 мс). Наша сетевая инфраструктура будет измерять задержку сетевого вызова Request 7 как 70 мс, однако приложение измеряет ее как 180 мс, поскольку мы ждали еще 110 мс, чтобы запланировать цикл событий. Работа ЦП для других запросов "блокирует" выполнение нашего запроса.

В приведенном выше примере выполнение 3 одновременных запросов было бы почти идеальным:

Теперь, как это связано с услугой ценообразования и большим временем отклика, которое мы наблюдали ранее?

pricingservice включает сотни различных конечных точек с разными уровнями рабочих нагрузок ЦП и операций ввода-вывода. Некоторые конечные точки интенсивно используют ЦП, тогда как некоторые (например, конечные точки, которые предлагают цены на велосипеды и скутеры) требуют большого количества операций ввода-вывода (поскольку они вызывают несколько вышестоящих сервисов).

Гринлеты для конечных точек с интенсивным использованием ЦП в pricingservice блокировали выполнение гринлетов для тяжелых запросов ввода-вывода. Это приводило к тому, что гринлеты конечной точки ценообразования на велосипеды и скутеры ставились в очередь и дольше ожидали, чтобы их запланировал цикл событий.

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

Используя инструменты, созданные нашими командами по инфраструктуре, мы смогли запустить наши конечные точки ценообразования на велосипеды и скутеры в выделенных процессах и направили последующие сервисы на эти выделенные хосты для всех запросов ценообразования на велосипеды и скутеры. Как только мы внедрили эти изменения, мы получили именно те результаты, на которые надеялись!

Таймауты для запросов к нашей конечной точке ценообразования на велосипеды и скутеры немедленно прекратились, что привело к мерцанию, которое мы описали ранее. Надежность запросов на ценообразование на велосипеды и скутеры подскочила в среднем с 2,5 до 4,5 девятки - огромное улучшение!

Выводы

Ключевой вывод для читателей заключается в том, что запуск службы Python с высокой пропускной способностью (которая использует gevent), которая запускает смешанные рабочие нагрузки ЦП и ввода-вывода, может привести к интенсивным запросам ЦП и нехватке запросов с интенсивным вводом-выводом. Мы рекомендуем использовать распределенную трассировку, чтобы изучить жизненные циклы запросов, и использовать рекомендации из этой серии gevent, чтобы улучшить задержки и надежность вашего сервиса Python.

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

Эта публикация была бы невозможна без помощи Роя Уильямса, Дэвида Куэйда и Джастина Филлипса. Кроме того, огромное спасибо Гаррету Хилу, Илье Константинову, Даниэлю Мецу и Ятину Чопре за их советы и за просмотр этого поста.

Lyft нанимает! Если вы заинтересованы в улучшении жизни людей с помощью лучшего в мире транспорта, присоединяйтесь к нам!