Читай реальный конфиг и код четырёх кейсов, затем выбирай сеньорную починку: цикл ретраев без дедупа, лента на offset, чат-хендлер с ack до персиста и автодополнение, ранжирующее на пути чтения.
SDCSenior◷ 14 min
Уровень
ОсновыJuniorMiddleSenior
Баги соцсистем прячутся в нескольких строках: ретрай без проверки дедупа, OFFSET в запросе ленты, ack до записи, вызов базы на пути чтения. Читай каждый сниппет, прослеживай, что происходит на масштабе, и выбирай изменение, которое закоммитит сеньор.
Отрабатывай цикл дизайн-ревью: находи несущую строку, рассуждай, что она делает на миллионах событий, и выбирай починку, что адресует режим отказа, а не замазывает его.
Сниппет 1 — цикл ретраев уведомлений
def deliver(msg): for attempt in range(5): try: provider.send(msg) # может таймаутить / 429 return except Retryable: sleep(2 ** attempt) # экспоненциальный backoff dead_letter.put(msg)
Викторина
Completed
Здесь есть ограниченные ретраи, backoff и DLQ. После восстановления провайдера пользователи всё ещё получают дубли push. Чего не хватает?
Heads-up Джиттер растекает ретраи ради избегания thundering herd на восстановившемся провайдере — стоит добавить — но не останавливает дубли. Переотправленное или повторно хлынувшее сообщение всё равно шлётся дважды без проверки идемпотентности перед отправкой.
Heads-up Больше ретраев дают БОЛЬШЕ дублей, не меньше. Дубль идёт от переотправки без стража дедупа; потолок управляет тем, когда сдаться в DLQ, а не тем, дублирует ли переотправка.
Heads-up Ретрай постоянных ошибок (невалидный токен, hard bounce) тратит отправки и спотыкается об abuse-лимиты — надо ретраить МЕНЬШЕ типов ошибок, не больше. И это всё равно не адресует дубли, которым нужен ключ идемпотентности.
Сниппет 2 — запрос ленты
SELECT post_id FROM feedWHERE user_id = :uidORDER BY created_at DESCLIMIT 20 OFFSET :n; -- n растёт по мере прокрутки
Викторина
Completed
Глубокие прокрутки медленны, и пользователи сообщают, что посты появляются дважды или пропускаются. В чём причина и починка?
Heads-up Индекс помогает сортировке, но OFFSET n всё равно сканирует и выбрасывает n строк, так что медленнее с глубиной независимо, и всё ещё сдвигается при конкурентных вставках. Структурная починка — курсор, а не индекс.
Heads-up Нижележащий список сдвигается при вставке постов, так что закешированные страницы всё равно пропускают/дублируют, а персонализированная лента молотит кеш. Курсорная пагинация чинит и стоимость, и корректность.
Heads-up OFFSET n всё равно пропускает n строк как ни режь, а глубокая прокрутка копит большой n; он всё ещё пропускает/дублирует при вставках. Привяжись к последнему виденному элементу курсором.
В чём баг корректности и как переупорядочить хендлер?
Heads-up Галочка 'отправлено' значит, что сервер долговечно держит сообщение, а не что получатель принял (это отдельная галочка 'доставлено'). Починка — персист-затем-ack; привязка ack к доставке получателю смешивает две разные галочки.
Heads-up Эта скорость — ровно ловушка: если процесс падает после оптимистичного ack и до персиста, сообщение тихо потеряно — единственное, что чат никогда не должен делать. Персист до ack.
Heads-up Баг — в порядке, не в конкурентности: ack происходит до долговечной записи. Персист первым чинит это; лок не адресует окно потери сообщения при крахе между ack и persist.
Сниппет 4 — эндпойнт автодополнения
def suggest(prefix): rows = db.query( "SELECT q FROM search_log WHERE q LIKE %s GROUP BY q ORDER BY count(*) DESC LIMIT 10", prefix + "%") # бежит на каждое нажатие return [r.q for r in rows]
Викторина
Completed
Это ранжирует совпадающие запросы вживую на каждое нажатие. Тормозит на 600 мс и перегружает кластер. В чём починка?
Heads-up Индекс ускоряет скан LIKE, но ты всё равно гоняешь GROUP BY / ORDER BY агрегацию на каждое нажатие по огромному логу — слишком медленно для десятков мс, сотни тысяч раз в секунду. Ранжирование не должно быть на пути чтения вовсе.
Heads-up Наивный кеш на префикс всё равно постоянно промахивается на длинном хвосте и устаревает без пайплайна обновления; структурный ответ — предвычисленный trie с top-k на узле, обновляемый офлайн-сборкой.
Heads-up Меньшее окно — всё равно живая агрегация на каждое нажатие — быстрее, но всё ещё на пути чтения и всё ещё шторм нажатий. Предвычисли ранжирование офлайн и отдавай O(1) лукап trie.
Вспомните перед уходом
01
Почему цикл ретраев с backoff и DLQ всё равно дублирует и почему OFFSET неверен для ленты?
02
Почему чат-хендлер обязан персистить до ack и почему автодополнение не может ранжировать на пути чтения?
Итог
Четыре сниппета прячут по однострочному отказу. Цикл ретраев уведомлений имеет backoff и DLQ, но нет ключа идемпотентности, так что повторно хлынувший бэклог дублирует — ретраи at-least-once, дедуп обязан сторожить отправку. Запрос ленты использует OFFSET, что сканирует n строк (медленно на глубине) и сдвигается при вставках (пропуски/дубли); починка — курсорная пагинация. Хендлер чатаподтверждает до персиста, так что крах теряет сообщение под уверенной галочкой; сначала персист (и назначь seq), затем ack. Эндпойнт автодополнения ранжирует вживую на нажатие по логу, что не уместит бюджет или нагрузку; предвычисли top-k на узле trie офлайн и отдавай O(1) чтение. Сеньорная привычка: найди несущую строку, рассуди о ней на масштабе и приложи починку, которой требует режим отказа.