Редирект блокирует посетителя. Он нажимает короткую ссылку, браузер ждёт, и ничего больше не загружается, пока не вернётся 302. Поэтому у services/edge-redirect есть бюджет задержки, а не ощущение "достаточно быстро": p50 5 мс и p95 15 мс на попадании в кэш.
Читайте это как цель, а не результат. Бюджет записан в README сервиса и в runbook дежурного, и на нём построена наша планка приёмки нагрузочного теста. Зафиксированного бенчмарка или файла результатов нагрузочного теста, подтверждающего перцентиль по региону, у нас нет, поэтому этот материал его не печатает. Он описывает по коду, что делает горячий путь и, не менее полезно, от чего он отказывается.
Кратко
- 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 документирует поток в начале файла. По порядку:
- Привести хост к нижнему регистру и взять слаг из пути.
resolver.Resolveпроходит L1, затем L2, затем origin.- Короткое замыкание по состоянию: приостановленное рабочее пространство, истёкшая ссылка, ссылка под паролем (посетитель идёт на страницу разблокировки, и клик не отправляется), ссылка, которую url-scanner пометил как
blocked(451), и хост назначения из административного блок-листа. - Отправить событие клика, не дожидаясь его.
- Ответить
302, если ссылка не требует301или307.
Всё в шаге 3 - это поле в кэшированной записи ссылки или поиск в наборе в памяти. Например, сопоставитель блок-листа синхронизируется из api-core в фоне, поэтому проверка стоит пары обращений к map, а не вызова базы данных.
Превью ссылок (слаг, оканчивающийся на +), запросы AI-агентов, просящих JSON, и ссылки со сквозным шифрованием идут другими ветками до редиректа. Ни одна из них не обращается к Postgres.
Три уровня поиска
Каждый уровень небольшой намеренно, и каждый, кроме последнего, работает по принципу best-effort.
L1, LRU внутри процесса. internal/cache/lru.go оборачивает hashicorp/golang-lru/v2/expirable. Размер L1Size: 100_000 записей, TTL L1TTL: 60s, оба заданы в internal/config/config.go. TTL существует, чтобы отключённая или удалённая ссылка не могла вечно отдаваться одним узлом. Ключ - host + "/" + slug, поэтому один слаг на двух доменах не конфликтует.
L2, общий кэш. internal/cache хранит ссылку как JSON с ключом из хоста и slug и L2TTL: 5m. Он общий для edge-экземпляров. Nil-клиент допустим: методы кэша возвращают промах, и edge продолжает работать.
Origin, gRPC к api-core. internal/origin/origin.go поддерживает два транспорта. REST (GET /v1/lookup) используется по умолчанию, если ничего иного не настроено. gRPC (elido.v1.LookupService.Resolve) применяется, когда задан GRPC_ORIGIN_ADDR, и это выбор для продакшена. Вызов ограничен OriginTimeout, 1.5 секунды.
Резолвер достаточно короткий, чтобы процитировать важную часть:
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; если он это делает, резолвер повторяет запрос один раз с родительским хостом с подстановкой (abc.example.com становится *.example.com) для пользовательских доменов с подстановочным знаком.
Чего в этом дизайне нет
Более ранняя версия этого материала описывала вещи, которых код не делает. Скажем явно:
- Группы
singleflightнет. Два одновременных промаха по одному холодному слагу оба доходят до 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. Runbook упоминает другие имена метрик (edge_cache_*, edge_origin_grpc_duration), которые мы не нашли зарегистрированными в текущем коде, поэтому не документируем их.
Что остаётся вне горячего пути
Хранение кликов. click.Producer.Emit сериализует событие и передаёт его продюсеру franz-go, а обработчик вызывает его как go h.clicks.Emit(...). Callback продюсера только пишет ошибку в журнал. Если шина событий медленная, посетитель этого не замечает; цена в том, что клик может потеряться, что является обычным компромиссом fire-and-forget. Запись кликов в аналитическое хранилище - задача 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 сервиса. Сравнительного бенчмарка нашего обработчика мы не публиковали, так что это довод проектирования, а не измеренный результат.
Как измерить самому
В репозитории есть runbook по нагрузочному тестированию (docs/runbooks/19-load-testing.md) на основе k6. Его предзапусковая планка приёмки для горячего пути: 1.5k запросов/с в течение 5 минут при p95 15 мс без 5xx и 5k запросов/с в течение 1 минуты при p95 30 мс с долей 5xx меньше 0.1%. Это пороги, а не результаты.
Тест с ближнего генератора нагрузки измеряет сервис. Тест с далёкого хоста измеряет вашу сеть. Для задержки со стороны посетителя зондируйте снаружи синтетическим монитором, как в материале мониторинг редиректов ссылок через 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 аналитики кликов по ссылкам - куда попадают события кликов.
Частые вопросы
15 мс p95 - это измеренное число?
Нет. 15 мс p95 (при p50 5 мс) на попадании в кэш - это бюджет задержки сервиса edge-redirect, и именно на нём написаны наши алерты и планка приёмки нагрузочного теста. Этот материал не публикует измеренные перцентили по регионам, потому что у нас нет зафиксированного бенчмарка или файла результатов, на который можно сослаться.
Как редирект Elido определяет слаг?
Edge сначала смотрит в LRU внутри процесса, затем в общем кэше в памяти и только потом обращается к api-core по gRPC. Каждый слой кэша работает по принципу best-effort, поэтому авторитетный ответ «не найдено» может вернуть только 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 ссылок в месяц.
