Real-Time Backend
Design: Uber/Lyft
Нажатие кнопки 'Вызвать' в Uber запускает цепочку из дюжины распределённых сервисов. Через 30 секунд на экране - водитель, маршрут и точная минута прибытия. Это не магия - это конкретная архитектура с конкретными трейдофами.
- Uber обрабатывает более 2 млрд поездок в год и 100 000 ETA-запросов в секунду в пиковые часы
- Время матчинга водитель-пассажир составляет в среднем менее 30 секунд при 6 млн+ активных водителей
- Location Service принимает 1.5 млн обновлений координат в минуту только от водителей - без специализированного геоиндекса это невозможно
- Lyft, DiDi, Grab строят идентичные системы - паттерны ride-hailing стали индустриальным стандартом для геораспределённых realtime-платформ
Архитектура Uber
Uber обрабатывает более 2 млрд поездок в год. На платформе зарегистрировано свыше 6 млн водителей в 70+ странах. В пиковые часы система принимает десятки тысяч запросов в секунду. Монолит с таким трафиком невозможен физически.
Архитектура разбита на домены: поездки (Trip Service), геолокация (Location Service), матчинг (Dispatch), ETA, оплата, уведомления. Каждый домен - отдельная группа сервисов с независимым деплоем и масштабированием.
Uber использует Apache Kafka как центральную шину событий. Каждое изменение состояния поездки публикуется в топик - ETA, Dispatch, Payment и Notifications подписаны независимо. Это позволяет добавлять новые потребители без изменения продюсеров.
Хранилище данных разделено по доменам. Trip Service использует MySQL с горизонтальным шардированием по trip_id. Location Service - in-memory Riak или Redis для хранения последних координат водителей. Поиск по координатам требует специализированного геоиндекса.
| Сервис | Хранилище | Причина |
|---|---|---|
| Trip Service | MySQL (sharded) | ACID, история поездок |
| Location Service | Redis / Riak | Sub-millisecond обновления координат |
| Dispatch | In-memory + Kafka | Stateful matching, скорость важнее персистентности |
| ETA Service | DynamoDB / Cassandra | Высокая read-нагрузка, eventual consistency OK |
Почему Uber использует отдельный сервис для геолокации вместо хранения координат в Trip Service?
Driver-Rider Matching
Dispatch Service - самая чувствительная часть Uber. Среднее время до принятия заказа водителем составляет менее 30 секунд. Алгоритм матчинга запускается каждые несколько секунд и перебирает кандидатов в радиусе от точки подачи.
Первый этап - пространственный поиск. Geohash делит поверхность Земли на иерархические ячейки. Precision 6 (около 1.2 км x 0.6 км) дает приемлемый баланс - в каждой ячейке обычно несколько доступных водителей. Поиск начинается с текущей ячейки и расширяется на соседние, если кандидатов недостаточно.
Второй этап - скоринг кандидатов. Расстояние важно, но не единственный фактор. Uber учитывает: рейтинг водителя, тип автомобиля (UberX / Black / XL), текущую загруженность водителя, историю принятия/отклонения заказов и время ожидания конкретного пассажира.
- Proximity score: линейная функция от расстояния, вес ~40%
- Driver rating: нормализованный рейтинг 1-5, вес ~20%
- Acceptance rate: водители с высоким процентом принятия получают приоритет
- Vehicle match: тип запрошенного сервиса (Pool, X, Black, XL)
- Supply/demand: в условиях дефицита водителей расширяется радиус и снижаются пороги
Предложение поездки отправляется одному водителю за раз, не всем сразу. Если водитель не отвечает в течение ~15 секунд - заказ переходит к следующему кандидату. Параллельный оффер нескольким водителям создаёт race condition: несколько водителей едут к пассажиру, один "побеждает", остальные теряют время.
| Проблема | Решение | Трейдоф |
|---|---|---|
| Водитель не отвечает | Таймаут 15с, следующий кандидат | Растёт время ожидания пассажира |
| Несколько запросов для одного водителя | Distributed lock (Redis SETNX) на driver_id | Небольшая латентность локинга |
| Водитель принял заказ, пока система думала | Optimistic locking + версионирование состояния | Повторные попытки при конфликте |
| Зоны с дефицитом водителей | Surge pricing + расширение радиуса поиска | Пассажир ждёт дольше или платит больше |
Почему Uber предлагает поездку водителям последовательно (один за раз), а не параллельно всем кандидатам сразу?
ETA Calculation
ETA - не просто расстояние, делённое на скорость. Uber вычисляет время прибытия с учётом реального трафика, историческихпаттернов по времени суток и дню недели, текущих аварий и дорожных событий, предпочтений маршрута водителя.
Архитектурно ETA Service строится поверх графа дорог. Вершины - перекрёстки, рёбра - отрезки дорог с динамическим весом (время проезда). Алгоритм Дейкстры или A* находит кратчайший путь по времени, а не по расстоянию.
Uber заранее прогревает кэш ETA для популярных зон. В Нью-Йорке или Сан-Франциско тысячи пар (origin, destination) запрашиваются ежеминутно. Ответ за 50-100 мс достигается через предвычисленные shortest-path деревья от ключевых точек (аэропорты, вокзалы, центры деловых районов).
Для масштаба: Uber обрабатывает пиковую нагрузку около 100 000 ETA-запросов в секунду глобально. Граф дорог только для США содержит десятки миллионов вершин и рёбер. Обход такого графа в реальном времени требует агрессивной сегментации: каждый город/регион имеет изолированный подграф.
- Дейкстра — Гарантирует оптимальный путь, обходит весь граф от источника. Подходит для офлайн-прекомпутации. Для реального времени слишком медленен на больших графах.
- A* с эвристикой — Направляет поиск в сторону цели через heuristic (геодезическое расстояние). В 3-10x быстрее Дейкстры на практике. Используется Uber для real-time routing.
- Contraction Hierarchies — Предобрабатывает граф, добавляя shortcut-рёбра через важные узлы. Запросы в 1000x быстрее Дейкстры. Google Maps и HERE используют этот подход.
Почему ETA в Uber не равна расстоянию / среднюю_скорость?
Live Map и Routing
Живая карта в приложении - это не просто картинка. Каждый водитель отправляет GPS-координаты каждые 4 секунды. 6 млн активных водителей = 1.5 млн событий в минуту только от координат. Location Service должен принять, обработать и раздать эти данные заинтересованным пассажирам в реальном времени.
Архитектура потока координат строится на нескольких слоях. Водитель отправляет HTTP/WebSocket на Location Service. Сервис обновляет Redis GEORADIUS с координатами и публикует событие в Kafka. Dispatch Service и пользовательские WebSocket-соединения подписаны на изменения через Kafka Consumer Groups.
Тайлы карты - отдельная задача. Uber использует собственные векторные тайлы поверх OpenStreetMap и собственных данных о дорогах. Тайлы кешируются на CDN (Cloudfront/Fastly) по координатам и zoom-уровню. Клиент запрашивает только тайлы текущего viewport.
- Векторные тайлы (.pbf / Mapbox Vector Tiles) - в 10x меньше растровых PNG при том же качестве
- CDN кеш: тайлы по zoom 14-16 для крупных городов прогреваются заранее
- Dead reckoning: клиент интерполирует позицию водителя между обновлениями для плавной анимации
- Bearing-aware rendering: иконка машины поворачивается плавно по направлению движения
WebSocket vs HTTP polling: Uber использует persistent WebSocket для активных поездок. Polling каждые 4 секунды создаёт 15 запросов/минуту с головой в 60-100 байт HTTP header на каждый. WebSocket после handshake передаёт чистые данные без overhead. При миллионах одновременных поездок разница в нагрузке существенна.
Для показа живой карты достаточно периодически запрашивать координаты через REST API
Живая карта требует WebSocket для push-уведомлений о движении, dead reckoning для плавной анимации и CDN-кешированных векторных тайлов - три независимых механизма
REST polling создаёт постоянную задержку в N/2 секунд (половина интервала опроса), видимые рывки в движении иконки и лишние HTTP-соединения под нагрузкой. Каждый из трёх механизмов решает отдельную проблему.
Зачем клиент Uber использует dead reckoning для позиции водителя на карте?
Итоги
- Микросервисная декомпозиция по доменам (Trip, Location, Dispatch, ETA) - каждый сервис масштабируется независимо и использует оптимальное хранилище
- Геохеш + Redis GEORADIUS - стандартный паттерн для поиска водителей в радиусе; последовательный оффер предотвращает thundering herd среди водителей
- ETA строится на A* по графу дорог с динамическими весами рёбер от реального трафика - расстояние / скорость принципиально недостаточно
- Live map требует трёх независимых механизмов: WebSocket push, dead reckoning на клиенте и CDN-кешированные векторные тайлы
Связанные темы
Система Uber пересекается с несколькими фундаментальными областями системного дизайна.
- WebSocket и realtime коммуникация — Live map и уведомления водителю строятся на persistent WebSocket соединениях
- Geospatial индексы — Geohash и Redis GEORADIUS - основа для поиска водителей в радиусе
- Графовые алгоритмы — A* и Contraction Hierarchies для построения маршрутов и расчёта ETA
- Apache Kafka — Шина событий для координации между Dispatch, Location, ETA и Notifications
Вопросы для размышления
- Как изменится архитектура матчинга, если Uber захочет поддерживать carpooling с несколькими пассажирами на одном маршруте?
- Какие данные нужно собирать и как обрабатывать, чтобы ETA была точной в незнакомом для алгоритма городе с мало исторических данных?
- При отказе Kafka весь Dispatch встаёт или деградирует частично - как спроектировать fallback для критичного пути матчинга?