Читаем реальный гео-код, конфиг маршрутизации pub/sub, настройку движка маршрутизации и команды отсортированного множества Redis, затем рассуждаем: баг одной ячейки, взрыв вещания, ловушка пересжатия и запрос ранга O(n).
SDCSenior◷ 14 min
Уровень
ОсновыJuniorMiddleSenior
Баги этих систем живут в коде, не в прозе: запрос, читающий одну ячейку, публикация, идущая всем, настройка маршрутизации, пере-предобрабатывающая на каждый тик трафика, запрос ранга, который сканирует. Читай каждый сниппет, прогоняй в голове и выбирай ответ, на который подписался бы senior-инженер.
Практикуй петлю дизайн-ревью: найди паттерн доступа в коде, сопоставь его с верной структурой и выбери изменение, которое арифметика/архитектура реально поддерживают.
Сниппет 1 — запрос близости
def nearby(lat, lng, radius_m): cell = h3.geo_to_h3(lat, lng, res=9) # собственная ячейка пользователя candidates = store.get_by_cell(cell) # лишь эта одна ячейка return [p for p in candidates if haversine(lat, lng, p.lat, p.lng) <= radius_m]
Викторина
Completed
В чём баг nearby() и как его починить?
Heads-up Ячейка — грубая корзина, не круг — она содержит места вне радиуса и исключает те, что прямо за краем. Нужны И запрос соседей (чтобы ничего близкого не пропустить), И фильтр точного расстояния.
Heads-up Огрубление ячейки грубо хватает далёкие места и всё равно асимметрично пропускает случай границы. Верный фикс — запрашивать соседние ячейки (kRing) на разрешении под радиус, затем фильтровать по точному расстоянию.
Heads-up Математика расстояния в порядке; структурный баг — чтение одной ячейки. ORDER BY distance по всем строкам — это скан O(n), которого индекс ячейки и существует, чтобы избежать.
Сниппет 2 — публикатор локаций
def on_ping(user_id, lat, lng): store.set(user_id, (lat, lng), ttl=30) for conn in all_connections: # каждый подключённый клиент conn.send({"user": user_id, "lat": lat, "lng": lng})
Викторина
Completed
При 2 млн пингов/с этот цикл плавит кластер. Структурный фикс?
Heads-up Фоновый поток всё равно шлёт каждому соединению — общая работа тот же квадратичный взрыв, лишь вне потока приёма. Фикс — НЕ слать всем: маршрутизировать по каналу региона и фильтровать на краю.
Heads-up Батчинг снижает syscalls, но всё равно доставляет каждый пинг каждому соединению — проблема в множестве получателей, не в каденции флаша. Публикуйте по региональной ячейке, чтобы получали лишь близкие подписчики.
Heads-up TTL управляет истечением, не тем, кто получает пинг. Плавление — фанаут на все соединения; чините его pub/sub по региону плюс фильтр дружбы/радиуса на краю.
Сниппет 3 — настройка движка маршрутизации
def update_traffic(edge_id, new_travel_time): graph.set_weight(edge_id, new_travel_time) graph.build_contraction_hierarchy() # бежит на каждое обновление трафика return graph# вызывается ~тысячи раз в секунду из потока зондов
Викторина
Completed
Почему это катастрофа и какая архитектура верна?
Heads-up Кэш не помогает, когда веса меняются на каждом вызове — каждое обновление инвалидирует кэш иерархии. Надо развязать структуру и веса, чтобы живой трафик накладывался без перестройки.
Heads-up Это жертвует скоростью, нужной на планетарном QPS. Держите предобработку, но партиционируйте по скорости изменения: структура предобрабатывается редко, веса накладываются живьём.
Heads-up Ни одна машина не сжимает континентальный граф за миллисекунды между тиками трафика. Архитектурный фикс — разделение структуры и весов, не более быстрое железо.
Сниппет 4 — запрос ранга
-- ранг игрока на лидербордеSELECT COUNT(*) + 1 AS rankFROM scoresWHERE score > (SELECT score FROM scores WHERE player_id = :id);-- вызывается на игрока, миллионы опрашивают свой ранг
Викторина
Completed
Почему этот запрос плохо масштабируется и что его заменяет?
Heads-up Индекс помогает найти строки, но COUNT(*) по всем высшим очкам всё равно O(n) на запрос. ZREVRANK у ZSET честно O(log n) — суммирует предвычисленные span'ы вместо счёта строк.
Heads-up Еженочное представление делает ранг устаревшим (очки меняются вживую) и O(n) на обновление для всех игроков, большинство из которых не смотрят. ZREVRANK считает любой ранг свежим за O(log n) на чтение.
Heads-up Обновление хранимого ранга на смене очков может сдвинуть ранг всех ниже — O(n) на запись. Отсортированное множество держит ранг неявно в структуре, так что чтения O(log n) и записи O(log n).
Вспомните перед уходом
01
Два кодовых запаха, сигналящих о баге близости или фанаута, и их фиксы?
02
Почему пере-сжатие на каждое обновление трафика неверно, и почему SQL-запрос ранга медлен — что заменяет каждое?
Итог
Каждый сбой этого юнита виден в коде и структурен, не деталь тюнинга. Запрос близости, читающий одну ячейку, пропускает место за границей — запрашивай кольцо соседей и фильтруй по точному расстоянию. Обработчик локаций, шлющий каждый пинг всем соединениям — квадратичное вещание — публикуй по региональной ячейке и фильтруй на краю-подписчике. Движок маршрутизации, перестраивающий contraction hierarchy на каждый тик трафика — невозможно на континентальном масштабе — раздели структуру и веса, чтобы трафик накладывался живьём. А SQL-запрос ранга (COUNT(*) WHERE score > mine) — это скан O(n) на опрашивающего — заменяй отсортированным множеством, чей ZREVRANK — O(log n). Senior-привычка одна каждый раз: читай паттерн доступа из кода, затем выбирай структуру данных или маршрутизацию под него — никогда версию, что прячет O(n) или пересчёт на горячем пути.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.