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Увидимся в следующем видео.
Community Posts
No posts yet. Be the first to write about this video!
Write about this video