Дайджесты новостей
Концептуальная 3D-визуализация кольца консистентного хэширования прокси-сервера Pingora с оптимизацией виртуальных узлов и экономией оперативной памяти.

Как математика и Rust помогли Cloudflare сэкономить 100 ТБ оперативной памяти

Представь ситуацию: у тебя глобальная сеть из тысяч серверов, через которую ежесекундно проходят десятки миллионов HTTP-запросов. Память на узлах буквально горит — кеширующие демоны забивают оперативку до предела, а мониторинг показывает, что огромная доля RAM уходит даже не на пользовательские страницы или медиафайлы, а на служебные структуры данных самой маршрутизации. Первая реакция многих команд — пойти к руководству, запросить бюджет и закупить новые планки памяти. Но инженеры Cloudflare пошли другим путем: они взяли формулы теории вероятностей, открыли исходники прокси-сервера Pingora на Rust и сэкономили 100 терабайт оперативной памяти без единого цента затрат на железо.

Меня в этой истории зацепило то, как красивое математическое рассуждение в связке с компилятором Rust способно перевернуть архитектуру огромного продакшена. Давай разберём эту механику по винтикам.

Где прятались сотни терабайт: сервис PBR и кольцо Ketama

Чтобы понять суть проблемы, нужно заглянуть в архитектуру кеширования Cloudflare. Когда запрос приходит на пограничный сервер, системе важно сделать так, чтобы один и тот же URL внутри дата-центра всегда обрабатывался одним и тем же внутренним узлом. Это базовое правило: если пять одинаковых запросов попадут на пять разных машин, мы получим пять дубликатов в памяти и пятикратную нагрузку на сервер-источник (origin).

За выбор конкретного сервера отвечает сервис Pingora Backend Router (PBR) — прокси на Rust, заменивший устаревший NGINX. Для стабильной адресации внутри PBR используется библиотека pingora-ketama, реализующая алгоритм консистентного хэширования (consistent hashing).

Суть алгоритма Ketama: представь замкнутое кольцо хэшей от 0 до 2^32 - 1. Физические серверы кластера хэшируются по именам или IP-адресам и занимают точки на этом кольце. Запрос тоже хэшируется по кеш-ключу, попадает в точку окружности и направляется к ближайшему серверу по часовой стрелке.

Схема классическая, но во время аудита производительности инженер Иван Бабров (Ivan Babrou) зафиксировал пугающую цифру: структуры pingora-ketama внутри PBR на отдельных узлах отъедали до 6 ГБ RAM! В масштабах тысяч серверов это выливалось в сотни терабайт чистой памяти, занятой исключительно кольцами маршрутизации.

Парадокс виртуальных узлов и плато эффективности

Почему кольцо хэширования занимает столько памяти? Всё дело в случайном распределении. Если разместить 100 серверов на кольце по одной точке на узел, математика преподнесёт неприятный сюрприз: для 100 серверов коэффициент вариации длин сегментов превышает 98%. Один сервер захлёбывается от двойного потока трафика, а сосед простаивает.

Исторический рецепт решения проблемы — виртуальные узлы (хэш-реплики). Каждому физическому серверу сопоставляют десятки псевдослучайных меток на кольце. В NGINX и ранних версиях Pingora базовое значение составляло 160 реплик. А с учётом разной ёмкости дисков число хэш-точек на одну машину переваливало за десятки тысяч.

Инженеры Cloudflare построили график зависимости погрешности балансировки от количества виртуальных узлов и обнаружили закон убывающей отдачи:

  • Первые сотни хэшей сбивают ошибку распределения с катастрофических 99% до 5–8%.
  • Но дальше начинается плато: отрезок от 10 000 до 100 000 хэшей (девяносто тысяч точек!) снижает неравномерность всего на 0.7%.
  • При 32-битном пространстве хэшей на больших кластерах срабатывал парадокс дней рождения: хэши виртуальных узлов сталкивались, выбивая друг друга из кольца и внося шум.

Продакшен годами оплачивал миллионами гигабайт памяти мизерную долю процента балансировки. Команда приняла решение сократить число генерируемых хэшей на 90% (в 10 раз). На реальной нагрузке бэкендов это не сказалось, а объём структур данных в RAM рухнул на порядок.

Как Rust выравнивает память и где потерялись 25%

Дальше исследователи заглянули в бинарное представление данных в памяти. В исходной реализации крейта pingora-ketama каждая точка кольца описывалась структурой:

struct Point {
    hash: u32,
    index: u32,
}

Поле hash — 4 байта (позиция на кольце), index — порядковый номер сервера в массиве бэкендов (тоже 4 байта). Инженер Зайдун Абд Аль Хади (Zaidoon Abd Al Hadi) подметил: ни в одном дата-центре Cloudflare не бывает больше 65 536 серверов. Для индекса достаточно 16-битного числа u16.

Но прямая замена типа на u16 в Rust не даёт выигрыша: компилятор выравнивает поля по границе максимального типа (4 байта для u32) и неявно добавляет 2 байта padding. Структура как занимала 8 байтов, так и продолжала занимать 8 байтов.

Директива #repr(packed) здесь опасна: невыровненный доступ к памяти на серверах грозит просадкой скорости чтения. Поэтому инженеры упаковали точку в компактный сырой массив байтов с инлайн-методами:

struct Point([u8; 6]);

impl Point {
    #[inline]
    fn hash(&self) -> u32 {
        u32::from_ne_bytes(self.0[0..4].try_into().unwrap())
    }

    #[inline]
    fn index(&self) -> u16 {
        u16::from_ne_bytes(self.0[4..6].try_into().unwrap())
    }
}

Этот ход честно сжал размер структуры ровно до 6 байтов, дав ещё 25% экономии памяти без потерь в скорости бинарного поиска.

Канареечная миграция без всплеска нагрузки на origin

Если в один момент переключить алгоритм хэширования на всем парке машин, все хэш-точки на кольце сместятся. Запросы полетят на другие серверы, где страниц в кеше нет. Произойдёт сброс кеша (cache churn), миллионы запросов одновременно пробьют защиту и полетят напрямую на клиентские бэкенды (cache stampede).

Команда развернула аккуратную канареечную миграцию:

  1. PBR одновременно держал в памяти старые кольца v1 и компактные кольца v2.
  2. Выбор версии кольца происходил детерминированно по хэшу запроса, исключая метание между серверами.
  3. Изменения включили сначала на изолированных тестовых точках, затем на небольших региональных дата-центрах и лишь затем распространили глобально.
  4. В реальном времени отслеживались задержки ответов, трафик на origin и потребление памяти процессов (RSS).

После полного перевода трафика на кольца v2 старые структуры v1 выгрузили из памяти. Итог оптимизации — более 100 терабайт освобождённой оперативной памяти по всему миру. Если тебе интересны подобные инженерные кейсы оптимизации инфраструктуры, взгляни также на наш материал про оптимизацию UEFI-загрузки bare-metal серверов в Cloudflare.

Инженерный урок для распределённых систем

Не спеши масштабировать инфраструктуру «в ширину» при росте аппетитов сервиса. Профайлинг структур данных в оперативной памяти и базовая математика теории вероятностей нередко позволяют найти резервы производительности на сотни терабайт там, где все привыкли доверять дефолтным значениям библиотек.