Irbisa · Шпаргалка
Backend system design
Кэширование, очереди, идемпотентность, rate limiting, консистентность, outbox, пагинация, SLA
Чем отличаются cache-aside, read-through, write-through и write-behind, и когда какой выбирать?
Чем строгая консистентность отличается от eventual consistency, и что на самом деле дают CAP и PACELC при проектировании сервиса?
Что такое консистентное хеширование (consistent hashing), почему
hash(key) % Nпереносит почти все ключи при добавлении узла и где его используют?// Ring maps keys to nodes. Each node sits at many points (virtual nodes),// so the load evens out and a removed node's keys spread over all the others.type Ring struct { points []uint64 // sorted positions of every virtual node owner map[uint64]string // position -> node}…Чем различаются rate limiting по фиксированному окну, скользящему окну, token bucket и leaky bucket, и что бы вы выбрали?
Продюсеры кладут задачи быстрее, чем консьюмеры успевают их обработать. Что такое backpressure, и где его применять в Go-сервисе и в очереди?
// A bounded hand-off between the handler and a fixed number of workers.var jobs = make(chan Job, 1000) func enqueue(w http.ResponseWriter, r *http.Request) { job, err := decodeJob(r) if err != nil {…Как реализовать keyset-пагинацию (по курсору), если колонка сортировки не уникальна, и что вы теряете по сравнению с
OFFSET?// The cursor is the sort key of the last row returned; id breaks ties between equal timestamps.type cursor struct { CreatedAt time.Time `json:"t"` ID int64 `json:"i"`}…UUIDv4, UUIDv7 или
bigintidentity в качестве первичного ключа: что каждый из них делает с B-tree индексом в Postgres и когда нужны id в стиле Snowflake?Найдите баг: два ретрая одного платежа приходят с разницей в 5 мс, и оба списывают деньги с карты.
func (s *Service) Pay(ctx context.Context, key string, req PayRequest) ([]byte, error) { var saved []byte err := s.db.QueryRowContext(ctx, `SELECT response FROM idempotency_keys WHERE key = $1`, key).Scan(&saved) if err == nil { return saved, nil // seen this key before: replay the stored response } resp, err := s.psp.Charge(ctx, req) if err != nil { return nil, err } _, err = s.db.ExecContext(ctx, `INSERT INTO idempotency_keys (key, response) VALUES ($1, $2)`, key, resp) return resp, err}Ваш API последовательно вызывает четыре зависимости, у каждой SLO 99,9%. Какую доступность вы можете обещать, и как обещать больше, чем дают они?
Когда ставить in-process кэш перед Redis, и как не дать локальным копиям на 40 подах устаревать или падать под одним горячим ключом?
Пользователи иногда часами видят старую цену после обновления, хотя каждая запись удаляет ключ кэша. Какие гонки к этому приводят, и как их закрыть?
Пользователь сохраняет профиль, перезагружает страницу и видит старое имя: чтения идут на асинхронную реплику. Как дать read-your-writes, не отправляя все чтения на primary?
// After a write, hand the primary's WAL position back to the client (cookie or header).func (s *Store) Rename(ctx context.Context, userID int64, name string) (lsn string, err error) { if _, err = s.primary.ExecContext(ctx, `UPDATE profiles SET name = $2 WHERE user_id = $1`, userID, name); err != nil { return "", err }…Как выбрать таймаут для вызова зависимости, и когда hedged requests помогают с хвостовой латентностью?
// hedged calls once and, if there is no answer after delay (about the dependency's p95),// sends a second copy. The first success wins; returning cancels the other attempt.func hedged[T any](ctx context.Context, delay time.Duration, call func(context.Context) (T, error)) (T, error) { ctx, cancel := context.WithCancel(ctx) defer cancel()…Relay вашего outbox опрашивает таблицу каждые 100 мс. Когда стоит заменить его на CDC, например Debezium, и какие новые отказы приносит слот репликации?
На флеш-распродаже 100 единиц товара и 50 000 покупателей в первую секунду. Как не продать лишнего и не сделать строку остатков узким местом?
// The check and the write are one statement, run under the row lock.func (r *Repo) Reserve(ctx context.Context, productID int64, qty int) error { res, err := r.db.ExecContext(ctx, ` UPDATE inventory SET stock = stock - $2 WHERE product_id = $1 AND stock >= $2`, productID, qty) if err != nil {…Спроектируйте леджер для денежных переводов между счетами: двойная запись, идемпотентность, балансы и сверка.
CREATE TABLE accounts ( id bigint PRIMARY KEY, currency char(3) NOT NULL, balance bigint NOT NULL DEFAULT 0 -- minor units; a cache of SUM(entries.amount));…Спроектируйте сокращатель ссылок вроде bit.ly: 100 миллионов новых ссылок в месяц, чтений намного больше, чем записей. Расскажите про API, генерацию ID, хранение и редиректы.
const alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" // encode turns a unique id into a short code: ids below 62^7 ≈ 3.5 trillion fit in 7 chars; larger ids give longer codes.func encode(id uint64) string { if id == 0 { return "0"…Спроектируйте счётчик просмотров для видео, которые получают 50 000 инкрементов в секунду: где считать, как сохранять и что можно потерять?
// Views adds views up in memory: however many Inc calls arrive,// each pod writes each video at most once a second.type Views struct { mu sync.Mutex deltas map[int64]int64 // video id -> views since the last flush; make it before use // flush writes one batch: a Redis pipeline of INCRBY, or one Kafka message.…Спроектируйте сервис уведомлений, который отправляет email, SMS и push миллионам пользователей. Как справляться со всплесками, ретраями, дублями и настройками пользователей?
Спроектируйте бэкенд чата: доставка онлайн- и офлайн-пользователям, порядок внутри диалога, отметки о прочтении и хранение.
CREATE TABLE conversations ( id bigint PRIMARY KEY, last_seq bigint NOT NULL DEFAULT 0 -- the next message gets last_seq + 1); CREATE TABLE messages (…Спроектируйте ленту новостей: fan-out on write против fan-out on read, и что меняется для аккаунта с десятью миллионами подписчиков?
// Feed merges the user's precomputed feed (fan-out on write) with the recent// posts of the celebrities they follow (fan-out on read). Post ids are// time-ordered (Snowflake-style), so sorting ids sorts by time.func (s *Service) Feed(ctx context.Context, userID int64, limit int) ([]Post, error) { ids, err := s.feeds.Recent(ctx, userID, limit) // pushed in when authors posted if err != nil {…Спроектируйте rate limiter для публичного API на 50 репликах, 100 запросов в секунду на API-ключ. Где живёт состояние, и что происходит, когда Redis тормозит или лежит?
// Token bucket per key, refilled lazily from Redis's own clock, atomic inside Redis.var tokenBucket = redis.NewScript(`local rate, burst = tonumber(ARGV[1]), tonumber(ARGV[2])local t = redis.call('TIME')local now = t[1] * 1000 + math.floor(t[2] / 1000)local s = redis.call('HMGET', KEYS[1], 'tokens', 'ts')…