Cloudflare Just Saved 100TB of Memory

BBetter Stack
Computing/SoftwareInternet Technology

Transcript

00:00:00Во сколько вам на самом деле обходится один байт?
00:00:02Подумать только: тратить всего один байт на запись при масштабах Cloudflare
00:00:04означает потерю более 250 гигабайт памяти
00:00:06во всем их парке серверов.
00:00:08А недавно они умудрились сократить объем памяти на запись вдвое,
00:00:11высвободив 100 терабайт памяти.
00:00:13В нынешней экономической ситуации это огромные деньги.
00:00:15Они добились этого всего с помощью пяти довольно простых изменений
00:00:17в своей системе кэширования,
00:00:18и заодно ускорили работу DNS.
00:00:21Давайте разберем, как им это удалось.
00:00:27Возможно, вы уже слышали про 1.1.1.1.
00:00:30Это публичный DNS-резолвер от Cloudflare,
00:00:32который принимает домен, куда вы хотите зайти,
00:00:34например betterstack.com,
00:00:35и выясняет реальный IP-адрес для этого домена.
00:00:39Сам компонент, выполняющий эту работу,
00:00:40называется Big Pineapple и написан на Rust.
00:00:42Как вы, наверное, догадываетесь,
00:00:43это невероятно загруженная программа,
00:00:46которая хранит в любой момент времени более 250 миллиардов записей DNS-кэша.
00:00:50Это гарантирует, что если кто-то захочет перейти на betterstack.com
00:00:52через две секунды после другого пользователя,
00:00:53системе не придется заново обходить всю иерархию DNS.
00:00:56Она просто держит ответ в памяти и сразу выдает его обратно.
00:00:59Но, как я уже упоминал во вступлении,
00:01:01250 миллиардов записей в кэше означают, что один потерянный байт на запись
00:01:04обходится примерно в 250 гигабайт ОЗУ.
00:01:07Именно поэтому эти пять изменений оказали столь масштабный эффект.
00:01:10Во-первых, поговорим о стоимости емкости.
00:01:12Вот как раньше выглядела запись в кэше.
00:01:14Метка времени, время жизни (TTL), счетчики обращений
00:01:16и целая куча векторов.
00:01:17Вектор записей ответа (answer),
00:01:18вектор авторитетных записей (authority),
00:01:20вектор дополнительных записей (additional),
00:01:21вектор ошибок
00:01:22и многое другое.
00:01:23Для тех, кто не знаком с Rust,
00:01:25вектор — это стандартный тип данных для списка с изменяемым размером,
00:01:27и в памяти он состоит из трех элементов.
00:01:29Указатель, длина и емкость.
00:01:31Емкость показывает, сколько места зарезервировано на рост,
00:01:32чтобы не выполнять перераспределение памяти при каждом добавлении.
00:01:35Это очень полезно, если объект действительно увеличивается.
00:01:38Но они изучили реальные записи DNS-кэша
00:01:40и поняли, что всё просто записывается в кэш
00:01:42и больше никогда не изменяется.
00:01:44Они только читались,
00:01:45так что эти векторы на самом деле никогда не росли.
00:01:47Это означает наличие перераспределенного в куче пространства,
00:01:49которое просто так простаивает.
00:01:50Если вектор имеет емкость на восемь элементов,
00:01:52но хранит только пять,
00:01:53остается три неиспользуемых слота,
00:01:55а само поле емкости имеет тип usize,
00:01:57что составляет восемь байт памяти,
00:01:58которые им абсолютно не нужны.
00:02:00Исправление оказалось невероятно простым.
00:02:02Просто заменить вектор на box,
00:02:04поскольку размер box фиксируется при создании,
00:02:07так что ему не нужно поле емкости,
00:02:08и он хранит ровно то, что есть.
00:02:10Ему не нужно выделять лишнюю память на будущее.
00:02:13Они также поняли, что могут применить ту же самую экономию
00:02:15и к строковым полям,
00:02:16поскольку они по сути являются вектором U8,
00:02:18чтобы текст мог расти или уменьшаться.
00:02:20Но если вам это не нужно,
00:02:21можно использовать boxed string slice,
00:02:23то есть строка гарантированно не изменится.
00:02:26В итоге в каждой записи кэша
00:02:27было восемь векторных и строковых полей,
00:02:29поэтому замена их на box сэкономила по восемь байт на поле,
00:02:31то есть 64 байта на запись,
00:02:33а также устранила избыточное пространство в куче,
00:02:35резервируемое вектором на будущее,
00:02:36что в масштабе 250 миллиардов записей дало общую экономию
00:02:39более чем в 15 терабайт.
00:02:42И всё это благодаря простой смене типов данных.
00:02:45Что касается следующего изменения,
00:02:46они присмотрелись к некоторым из этих списков
00:02:48и задались вопросом: а нужны ли они нам вообще?
00:02:50DNS-ответ содержит три типа записей:
00:02:52ответ (answer), авторитетную (authority) и дополнительную (additional).
00:02:54И как мы видели ранее,
00:02:55они кэшировались по мере поступления
00:02:57в виде трех отдельных списков.
00:02:58и несмотря на то, что мы удалили поле вместимости в первом изменении,
00:03:01каждый список по-прежнему содержит указатель и длину,
00:03:03то есть (8 + 8) умножить на 3,
00:03:05что в сумме составляет 48 байт
00:03:07и три отдельных выделения памяти в куче.
00:03:09Но когда они проанализировали,
00:03:10как эти списки используются на практике,
00:03:12они поняли, что те всегда читаются вместе,
00:03:13всегда записываются вместе
00:03:14и всегда находятся в абсолютно одинаковом порядке.
00:03:16Поэтому вместо трех списков
00:03:17можно было использовать один список с двумя разделителями внутри.
00:03:20Таким образом, три списка превращаются в один список записей
00:03:22плюс два небольших смещения, которые указывают: здесь заканчиваются ответы,
00:03:25а здесь заканчивается авторитетная секция.
00:03:26И поскольку DNS-ответ никогда не будет содержать четырех миллиардов записей,
00:03:2916-битного беззнакового целого числа будет вполне достаточно для смещений,
00:03:33что составляет всего по два байта на каждое.
00:03:34Это значит, что в общей сложности
00:03:35они заменили два полных 16-байтовых заголовка двумя 2-байтовыми числами,
00:03:39сэкономив 28 байт на запись.
00:03:41Это также позволило Rust удалить лишние отступы между полями,
00:03:44сделав структуру меньше простой суммы размеров удаленных полей.
00:03:47Они даже пошли дальше в этой концепции
00:03:49и объединили несколько булевых полей в единый битовый флаг.
00:03:52Переходя к изменению номер три:
00:03:53что если перестать указывать одно и то же имя дважды?
00:03:55Каждая запись DNS содержит владельца (owner),
00:03:57то есть доменное имя, которому принадлежит эта запись.
00:04:00Поэтому, когда вы запрашиваете betterstack.com и получаете свои записи,
00:04:03на каждой из них написано betterstack.com.
00:04:05Но эта информация в некотором роде избыточна.
00:04:08Запрос сделали вы,
00:04:09так что доменное имя уже известно.
00:04:11И Cloudflare уже сохраняла домен запроса в качестве ключа кэша,
00:04:14зачем же им сохранять его снова в качестве владельца?
00:04:17Конечно, незачем.
00:04:17Поэтому Cloudflare просто заменила это поле на опциональное boxed-имя,
00:04:20где если владелец совпадает с запросом,
00:04:22хранится none,
00:04:23но если он действительно отличается
00:04:24(что бывает в некоторых случаях вроде цепочек CNAME),
00:04:27владелец сохраняется, как и раньше.
00:04:29Благодаря этому в обычных случаях, когда они совпадают,
00:04:32экономится целое выделение памяти в куче на каждую запись.
00:04:34Эта оптимизация свелась к анализу имеющихся в кэше данных
00:04:37и обнаружению дублирующихся значений.
00:04:39Для оптимизации номер четыре рассмотрим 144-байтный IP-адрес.
00:04:43Данные их записей представляли собой перечисление (enum) Rust для A, AAAA, TXT,
00:04:47SVCB и NAPTR записей — один тип, объединяющий их все.
00:04:51Но в чем проблема перечислений?
00:04:52Они всегда имеют размер своего наибольшего варианта.
00:04:54Каждое значение этого типа занимает одинаковый объем памяти,
00:04:57нужно оно ему или нет.
00:04:58В таком случае NAPTR — самое большое значение, занимающее 136 байт,
00:05:03а если добавить тег и заполнение к перечислению, получается 144 байта.
00:05:07Если сравнить это с тем, что требуется для A-записи, представляющей собой простой IPv4-адрес,
00:05:12то для него нужно всего четыре байта.
00:05:13Это значит, что каждая A-запись в этом кэше находилась в 144-байтовом контейнере,
00:05:17используя всего четыре из них,
00:05:19а A и AAAA-записи составляют основную часть реального трафика.
00:05:22В бенчмарке Cloudflare на A-записи приходится 56%, на AAAA — 25%, так что бо́льшая часть кэша состояла просто из заполнения.
00:05:29Решением этой проблемы стал наш надежный бокс.
00:05:32Они поместили большие варианты в кучу, а A и AAAA оставили встроенными, так как они мелкие и частые,
00:05:36поэтому теперь text, SVCB и NAPTR находятся за указателем,
00:05:40так что перечисление должно быть размером лишь с оставшийся максимум — 16-байтовый IPv6-адрес.
00:05:45В итоге для каждой A или AAAA-записи экономится по 120 байт.
00:05:50Но у этого изменения есть и свой недостаток.
00:05:52Когда вы помещаете вариант в кучу, его данные перемещаются из элемента кэша
00:05:55в совершенно другую область памяти,
00:05:58и это влечет за собой две новые проблемы.
00:06:00Первая — это аллокатор.
00:06:02Cloudflare использует Jemalloc, а он не выдает точное число запрошенных байт,
00:06:06а группирует выделения памяти в блоки фиксированного размера и округляет до ближайшего.
00:06:10Поэтому текстовая запись, запрашивающая 32 байта, попадает в 32-байтовый блок и ничего не тратит впустую,
00:06:15а MX-запись запрашивает 40, округляется до 48 байт, незаметно теряя 8 байт.
00:06:21Вторая проблема — локальность.
00:06:22До помещения в кучу все данные записей элемента находились в одном непрерывном блоке памяти,
00:06:27а после — каждая хранится в другом месте, и чтение требует перехода по указателю,
00:06:31и если этот указатель оказывается далеко от остальной части записи,
00:06:34процессору приходится загружать совершенно новую строку кэша только для ее чтения.
00:06:37Так что, хотя помещение в кучу решило проблему с заполнением, оно создало собственную,
00:06:41и тогда в Cloudflare подумали: а что если вообще не хранить их как типы Rust?
00:06:45Что ж, это пятое изменение.
00:06:46В Cloudflare описали это как компромисс: оставить остальную часть кэша в виде обычных структурированных
00:06:50полей, но сами данные записей хранить как сырые байты.
00:06:54Поэтому вместо перечисления или бокса на каждую запись весь элемент получает один массив байт (u8),
00:07:00где каждая запись записывается с 2-байтовым префиксом длины, за которым следуют ее данные.
00:07:03Это устраняет обе проблемы, о которых мы только что говорили.
00:07:06Все эти отдельные выделения памяти схлопываются в одно для всех данных записей,
00:07:10так что больше нет округления каждой записи до блока Jemalloc,
00:07:13и все снова упаковано непрерывно, возвращая локальность кэша, которую отобрало помещение в кучу.
00:07:18И в качестве бонуса это также ускоряет поиск.
00:07:21Раньше при каждом кэшировании в памяти находилась разобранная запись,
00:07:25и ее нужно было сериализовать полем за полем обратно в формат DNS-сообщений перед отправкой куда-либо.
00:07:30Теперь же она уже в сетевом формате DNS, поэтому большинство типов записей копируются прямо из буфера
00:07:35в исходящее сообщение.
00:07:37Единственное, что все еще требует разбора — это записи, содержащие доменные имена,
00:07:40то есть CNAME, NS, MX и SOA,
00:07:43и то лишь потому, что сжатие имен в DNS требует в любом случае их перезаписывать.
00:07:47Так что это финальное изменение сокращает объем памяти и убирает массу работы с критического пути.
00:07:51Вот так, это 5 низкоуровневых изменений кэша,
00:07:54которые в комбинации уменьшают размер одного элемента с 953 байт до 420,
00:08:00то есть на 56%, а реально выделяемая память на элемент сократилась с 1,1 килобайта
00:08:05до 461 байта, то есть уменьшилась на 58%.
00:08:09Вдобавок к этому пропускная способность записи выросла на 43%,
00:08:12с 625 000 элементов в секунду до 893 000,
00:08:17а поиск стал быстрее на 19%: время сократилось с 828 наносекунд до 670.
00:08:23Они внедрили эти изменения в продакшене в текущем году,
00:08:25и их резидентная память на уровне P99 снизилась с 9,3 гигабайт до 5,3,
00:08:29что дает сокращение на 43% на реальном трафике,
00:08:32что по всему парку серверов составляет примерно 100 терабайт освобожденной памяти,
00:08:35или объем оперативной памяти 130 таких серверов 13-го поколения.
00:08:38Теперь они планируют использовать это дополнительное пространство для увеличения кэша,
00:08:41ускорив работу еще сильнее.
00:08:43Мне очень нравится эта статья, потому что в ней поднимается решение, которое придется принимать и нам.
00:08:46Стоило ли нам до создания всего этого продумать все оптимизации
00:08:50или это была бы преждевременная оптимизация?
00:08:52И я полагаю, тогда, в 2018 году, когда все это создавалось,
00:08:55стоимость оперативной памяти для Cloudflare была не столь значительной, как сегодня.
00:08:59Полная статья — отличный материал, так что я оставлю ссылку на нее в описании.
00:09:02Дайте мне знать, что вы думаете об этом в комментариях,
00:09:03а заодно подпишитесь,
00:09:04и как всегда, увидимся в следующем видео.
00:09:05Увидимся в следующем видео.

Key Takeaway

Пять точечных оптимизаций структур данных в Rust-приложении сократили потребление памяти кэша на 58% и высвободили 100 терабайт ОЗУ в инфраструктуре Cloudflare.

Highlights

  • Система DNS-резолвера Big Pineapple от Cloudflare хранит в памяти более 250 миллиардов записей кэша.

  • Пять низкоуровневых изменений в кэше уменьшили размер одного элемента с 953 до 420 байт.

  • Резидентная память сервиса на уровне P99 снизилась с 9,3 до 5,3 гигабайт, освободив 100 терабайт памяти по всему парку серверов.

  • Пропускная способность записи выросла на 43%, увеличившись с 625 000 до 893 000 элементов в секунду.

  • Время поиска сократилось на 19% и составило 670 наносекунд вместо 828.

Timeline

Масштаб проблемы и архитектура кэша

  • Публичный DNS-резолвер 1.1.1.1 использует компонент Big Pineapple, написанный на Rust.
  • Система поддерживает более 250 миллиардов записей DNS-кэша в оперативной памяти.
  • Потеря всего одного байта на одну запись в масштабах Cloudflare приводит к расходу 250 гигабайт ОЗУ.

Публичный DNS-резолвер принимает доменные имена и возвращает их реальные IP-адреса, избегая повторных обходов иерархии DNS. Огромный объем хранимых данных делает критически важным каждый потерянный байт. Из-за масштаба инфраструктуры экономия даже небольшой доли памяти на каждой записи дает огромный эффект в масштабе всего парка серверов.

Замена векторов на боксы и удаление лишних списков

  • Замена векторов на box-типы устранила избыточное поле емкости размером восемь байт для неизменяемых данных.
  • Объединение трех отдельных списков ответа, авторитетной и дополнительной секций в один массив с двумя смещениями сократило размер записи на 28 байт.
  • Замена строковых векторов на boxed string slice предотвратила выделение лишнего пространства в куче.

Анализ показал, что записи DNS-кэша после создания никогда не изменяются и только читаются. Стандартные векторы в Rust резервируют лишнюю память для возможного роста, которая простаивает без надобности. Замена векторов и строк на фиксированные box-структуры убирает поле емкости и лишние аллокации. Кроме того, слияние трех списков в один общий список с 16-битными смещениями уменьшило количество указателей и выделений памяти в куче.

Устранение дублирования и оптимизация вариантов перечислений

  • Замена поля владельца записи на опциональный бокс устранила дублирование доменного имени, если оно совпадает с ключом запроса.
  • Перечисление Rust для разных типов записей занимало 144 байта по размеру наибольшего варианта NAPTR.
  • Перенос крупных вариантов в кучу позволил маленьким A и AAAA-записям не использовать избыточное заполнение.

Каждая запись DNS содержала имя владельца, которое уже дублировало ключ самого запроса. Замена этого поля на опциональное значение сохраняет имя только тогда, когда оно отличается от запроса, экономя память в большинстве случаев. Перечисления в Rust всегда выделяют объем под самый большой элемент, поэтому частые IPv4 и IPv6-адреса тратили много памяти на выравнивание и заполнение.

Переход на сырые байты и финальные результаты

  • Хранение данных записей в виде единого массива сырых байтов u8 вернуло локальность кэша и устранило проблему аллокатора Jemalloc.
  • Новый формат устранил необходимость постоянной сериализации записей при отправке сетевых сообщений.
  • Показатель резидентной памяти на уровне P99 снизился с 9,3 до 5,3 гигабайт.

Помещение крупных вариантов в кучу создало проблемы с аллокатором Jemalloc и ухудшило локальность данных в процессоре из-за прыжков по указателям. Переход на единый массив сырых байтов с префиксом длины решил эти проблемы, восстановив непрерывное размещение в памяти. Это уменьшило средний размер элемента кэша до 420 байт, ускорило поиск на 19% и высвободило 100 терабайт памяти в продакшене.

Community Posts

No posts yet. Be the first to write about this video!

Write about this video