Редирект блокує відвідувача. Він клікає на коротке посилання, браузер чекає, і нічого більше не завантажується, доки не повернеться 302. Саме тому в services/edge-redirect є бюджет затримки, а не відчуття «досить швидко»: p50 5 мс і p95 15 мс при влученні в кеш.
Читайте це як ціль, а не результат. Бюджет записано в README сервісу та в ранбуку чергового, і на ньому збудовано наш поріг приймання навантажувальних тестів. У нас немає закоміченого бенчмарку чи файлу результатів навантажувального тесту, що доводив би перцентиль по регіону, тож цей допис його не друкує. Він описує за кодом, що робить гарячий шлях і, не менш корисно, від чого відмовляється.
Коротко
- Edge - це сервіс на Go із низькоалокаційною HTTP-бібліотекою Go, що сьогодні працює на одному продакшн-хості за зворотним проксі, а мультирегіональний edge запланований.
- Пошук проходить LRU у процесі (100 000 записів, TTL 60 секунд), потім спільний кеш у пам'яті (TTL 5 хвилин), потім api-core через gRPC з тайм-аутом 1,5 секунди.
- Подія кліку створюється з окремої горутини. Відвідувач ніколи на неї не чекає.
- Бюджет: p50 5 мс, p95 15 мс при влученні в кеш. Це цілі, і виміряних перцентилів тут не публікується.
Навіщо потрібен бюджет
Час редиректу додається до всього, що йде після нього. Якщо кампанійне посилання веде трафік на цільову сторінку, редирект входить у час до першого байта цієї сторінки. Бюджет змушує ухвалювати рішення щоразу, коли хтось хоче додати роботу на цей шлях: вона вміщується або переїжджає до воркера.
Числа з services/edge-redirect/README.md:
| Перцентиль | Влучання в кеш | Промах кешу |
|---|---|---|
| p50 | 5 мс | 40 мс |
| p95 | 15 мс | 100 мс |
| p99 | 30 мс | 200 мс |
Це проєктна ціль. Якщо вам потрібні виміряні числа для перевірки вендора, запустіть сценарій k6, описаний наприкінці цього допису, на власних посиланнях і збережіть вивід.
Шлях запиту крок за кроком
internal/handler/redirect.go документує потік на початку файлу. По порядку:
- Привести host до нижнього регістру й узяти slug зі шляху.
resolver.Resolveпроходить L1, потім L2, потім origin.- Коротке замикання за станом: призупинений робочий простір, прострочене посилання, посилання під паролем (відвідувач іде на сторінку розблокування, і клік не надсилається), посилання, яке url-scanner позначив як
blocked(451), і хост призначення зі списку блокування адміністратора. - Надіслати подію кліку, не чекаючи на неї.
- Відповісти
302, якщо посилання не вказує301або307.
Усе в кроці 3 - це поле в кешованому записі посилання або пошук у множині в пам'яті. Наприклад, зіставлювач списку блокування синхронізується з api-core у фоні, тож його перевірка коштує кілька пошуків у map, а не виклик бази даних.
Прев'ю посилань (slug, що закінчується на +), запити AI-агентів, які просять JSON, і наскрізно зашифровані посилання йдуть іншими гілками перед редиректом. Жодна з них не торкається Postgres.
Три рівні пошуку
Кожен рівень навмисно малий, і кожен, крім останнього, працює за принципом найкращих зусиль.
L1, LRU у процесі. internal/cache/lru.go обгортає hashicorp/golang-lru/v2/expirable. Розмір - L1Size: 100_000 записів, а TTL - L1TTL: 60s, обидва задано в internal/config/config.go. TTL існує, щоб вимкнене чи видалене посилання не могло вічно віддаватися одним вузлом. Ключ - host + "/" + slug, тож однаковий slug на двох доменах не конфліктує.
L2, спільний кеш. internal/cache зберігає посилання як JSON, ключем слугують хост і slug, з L2TTL: 5m. Його спільно використовують edge-екземпляри. Нульовий клієнт допустимий: методи кешу повертають промах, а edge продовжує працювати.
Origin, gRPC до api-core. internal/origin/origin.go підтримує два транспорти. REST (GET /v1/lookup) - типовий, коли нічого іншого не налаштовано. gRPC (elido.v1.LookupService.Resolve) використовується, щойно задано GRPC_ORIGIN_ADDR, і це вибір для продакшену. Виклик обмежений OriginTimeout, 1,5 секунди.
Resolver достатньо короткий, щоб навести важливу частину:
if v, ok := r.l1.Get(key); ok {
return v, nil
}
if v, ok := r.l2.Get(ctx, key); ok {
r.l1.Set(key, v)
return v, nil
}
link, err := r.origin.Resolve(ctx, host, slug)
if err != nil {
return nil, err
}
r.l1.Set(key, link)
r.l2.Set(ctx, key, link)
return link, nil
Влучання в L2 поповнює L1. Промах в обох іде до api-core і поповнює обидва. Лише origin може сказати «не знайдено»; якщо так, resolver один раз повторює спробу з wildcard-батьківським хостом (abc.example.com стає *.example.com) для wildcard-власних доменів.
Чого в цьому дизайні немає
Попередня версія цього допису описувала речі, яких код не робить. Щоб було явно:
- Немає групи
singleflight. Два одночасні промахи для того самого холодного slug обидва доходять до origin. - На edge немає підписки pub/sub. Коли посилання змінюється, api-core видаляє ключ L2 (purger у
services/api-core/internal/linkmod/adapters.go). У L1 нічого не надсилається, тож змінене посилання може віддаватися з пам'яті вузла до 60 секунд. - L1 - це LRU з обмеженням за TTL, а не кеш із контролем допуску, і його розмір вимірюється кількістю записів, а не байтами.
- Немає кінцевої точки
cache-status, заголовка відповіді по рівнях і гістограми затримки, що експортується edge.
60-секундне вікно - причина, чому редаговане посилання має використовувати тимчасовий редирект, і чому 301 проти 302 важливе для браузерного боку тієї самої проблеми. Дизайн кешу розглянуто глибше в стратегії кешу для редиректів URL.
Єдина метрика Prometheus, на яку ми можемо вказати в коді edge, - elido_edge_redirect_total з мітками fleet і outcome. Ранбук згадує інші назви метрик (edge_cache_*, edge_origin_grpc_duration), яких ми не знайшли зареєстрованими в поточному коді, тож їх не документуємо.
Що лишається поза гарячим шляхом
Зберігання кліків. click.Producer.Emit серіалізує подію та передає її продюсеру franz-go, а обробник викликає його як go h.clicks.Emit(...). Зворотний виклик продюсера лише логує помилку. Якщо шина подій повільна, відвідувач цього не помічає; ціна в тому, що клік може бути втрачено, а це звичайний компроміс «вистрілив і забув». Запис кліків у сховище аналітики - робота click-ingester, який живить API аналітики кліків.
Випробування для ботів. На редиректі немає JavaScript-випробування. Виявлення ботів - це список підрядків user-agent, що вирішує, чи зараховувати клік, тож краулери та завантажувачі прев'ю посилань усе одно отримують редирект, але не роздувають аналітику. Це грубий фільтр за задумом.
Сканування. url-scanner публікує вердикт на посилання під час створення та за розкладом. Edge лише читає збережений вердикт (clean, suspect, blocked), і редирект зупиняє тільки blocked.
Обчислення правил. Розумні посилання подорожують із записом посилання, тож правила за країною, пристроєм тощо виконуються над кешованою копією, а пошук GeoIP робиться в процесі з локальної бази. Влучання в кеш із правилами не потребує мережевого виклику до api-core.
Налаштування сервера скромні. cmd/server/main.go налаштовує сервер із тайм-аутом читання й запису 2 секунди, тайм-аутом простою 30 секунд і обмеженням тіла запиту 64 КБ. Ми обрали його замість net/http через меншу алокацію на запит, як зазначає README сервісу. Прямого порівняльного бенчмарку для нашого обробника ми не публікували, тож це проєктна причина, а не виміряний результат.
Як виміряти самому
У репозиторії є ранбук навантажувального тестування (docs/runbooks/19-load-testing.md) на основі k6. Його передзапусковий поріг приймання для гарячого шляху - 1,5 тис. запитів/с протягом 5 хвилин при p95 15 мс без жодної 5xx і 5 тис. запитів/с протягом 1 хвилини при p95 30 мс із менш ніж 0,1% 5xx. Це пороги, а не результати.
Тест із близького генератора навантаження вимірює сервіс. Тест із далекого хоста вимірює вашу мережу. Для затримки з боку відвідувача проводьте зондування ззовні синтетичним монітором, як у моніторингу редиректів із Sentry та Datadog.
Чесно кажучи, я б поки не називав клієнту 15 мс як вимір. Прогалини, які ми закрили б перед друком таблиці по регіонах: гістограма затримки на edge, singleflight на холодних промахах, push-інвалідація L1 і закомічений бенчмарк за цифрою 15 мс.
Marius Voß - DevRel та edge-інфраструктура в Elido. Джерела: services/edge-redirect, services/api-core/internal/linkmod/adapters.go і docs/runbooks/19-load-testing.md; тверджень зовнішніх вендорів не робиться.
Читайте також у блозі
- Стратегія кешу для редиректів URL - дизайн L1 і L2 докладніше.
- Редиректи 301 і 302 - чому редаговані посилання використовують тимчасовий редирект.
- Розумні посилання: пояснення - правила, що виконуються над кешованим записом.
- Моніторинг редиректів посилань із Sentry та Datadog - вимірювання ззовні.
- API аналітики кліків за посиланнями - куди потрапляють події кліків.
Поширені запитання
Чи є p95 15 мс виміряним числом?
Ні. p95 15 мс (з p50 5 мс) при влученні в кеш - це бюджет затримки сервісу edge-redirect, і саме проти нього написано наші алерти та поріг приймання навантажувальних тестів. Цей допис не публікує виміряних перцентилів по регіонах, бо в нас немає закоміченого бенчмарку чи файлу результатів, на який можна посилатися.
Як редирект Elido розв'язує slug?
Edge дивиться спочатку в LRU у процесі, потім у спільному кеші в пам'яті і лише тоді звертається до api-core через gRPC. Кожен рівень кешу працює за принципом найкращих зусиль, тож лише origin може повернути авторитетну відповідь «не знайдено». Влучання в нижчому рівні копіюється вгору в рівні над ним.
Що лишається поза шляхом редиректу?
Зберігання кліків, аналітика та перевірки url-scanner. Edge надсилає подію кліку в шину подій в окремій горутині й відповідає відвідувачу, не чекаючи на неї. Фільтрація ботів - це перевірка user-agent, що вирішує, чи зараховувати клік, а не випробування, яке відвідувач має пройти.
Чому Elido типово використовує 302?
302 каже браузерам не вважати призначення постійним, тож змінене посилання набирає чинності з наступного візиту. Для посилання можна окремо задати 301 або 307. Компроміс розглянуто в нашому дописі про 301 проти 302.
Де розташовані edge-вузли POP?
Сьогодні продакшн - це один хост, на якому працює edge-сервіс за зворотним проксі. Мультирегіональний edge (Європа, Північна Америка та Південно-Східна Азія, у кожному той самий edge-бінарник) запланований, але ще не працює. Основні бази даних і шина подій лишаються в ЄС, а за цим задумом поруч з edge-вузлами лежали б лише дані кешу редиректів, як описує ADR-0014.
Чи може змінене посилання віддаватися застарілим?
Недовго. Коли посилання змінюється, api-core видаляє його запис у спільному кеші, але копія в процесі edge-вузла живе, доки не мине її TTL у 60 секунд. Тож зміна призначення може доходити до всіх вузлів до хвилини, і це одна з причин, чому тимчасові редиректи підходять для редагованих посилань.
Спробуйте Elido
Вставте URL - отримайте коротке посилання
Без реєстрації. Посилання живе 30 днів. Зареєструйтесь, щоб зберегти назавжди.
Без реєстрації. Зареєструйтеся безкоштовно і отримайте 50 посилань на місяць.
