Привет, Хабр! Меня зовут Дмитрий Иванов, я начальник отдела эксплуатации IT-инфраструктуры RUTUBE, что на наши деньги переводится как SRE-тимлид. В этой статье разберу задачу доставки контента и расскажу о решениях, которые помогают нам в RUTUBE. 

Дано: с одной стороны у нас 17,7 млн ежедневных пользователей, а с другой — 400 млн единиц контента. Оба эти показателя постоянно увеличиваются, а география присутствия пользователей расширяется.

Требуется: показывать всем нашим пользователям видео из библиотеки быстро, надежно и эффективно.

Как взаимосвязаны эти приоритеты и в чём проблема: 

  • Раздавать видео по всей нашей большой стране и за её пределы из одного хранилища, очевидно, не быстро — пинг от Москвы до Владивостока около 100 мс. 

  • Некоторые видео популярнее других, то есть одновременно их хотят посмотреть очень много пользователей. Если показывать такие видео напрямую из видеохранилища, это не будет надежно — потому что вся нагрузка пойдет на один сервер, серверу может стать плохо и «кина не будет».

  • Чтобы раздавать контент быстро, надо его разместить как можно ближе к зрителю. Но с учётом размера нашей библиотеки мы не можем её всю разложить по всем точкам присутствия пользователей — это будет неэффективно

Решением этой задачи, как понятно из названия, является CDN — Content Delivery Network. Далее в статье сначала определим особенности нашего контента и то, какие требования к CDN в связи с этим возникают, а потом будем искать способы им соответствовать.  

Контент на RUTUBE — что нужно доставлять

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

Количество единиц контента на RUTUBE за последние два года
Количество единиц контента на RUTUBE за последние два года

Рост хранилища и пользовательской базы имеет большое значение в разрезе масштабирования, о чем подробнее можно прочитать в статье об архитектуре RUTUBE. В рамках темы CDN нам важно, что библиотека постоянно расширяется, и в ней есть более и менее популярные видео. Если построить график, где по оси абсцисс будут единицы контента, а по оси ординат — просмотры, то получится гипербола примерно как на рисунке ниже. 

Схематичный график распределения просмотров по библиотеке контента. Красным обозначен горячий, популярный контент, голубым — холодный
Схематичный график распределения просмотров по библиотеке контента. Красным обозначен горячий, популярный контент, голубым — холодный

У нас есть небольшой процент видео, которые смотрят часто, и длинный хвост контента с гораздо меньшим количеством просмотров (например, видео со встречи одноклассников или обзор товаров в магазине у дома). В соответствии с этим мы можем поделить контент на горячий, то есть востребованный в данный момент, и холодный — интересный ограниченной аудитории. 

Два уровня кэширования

Для эффективного и быстрого доступа к горячему и холодному контенту соответственно выделим в CDN два вида кэша: L1 для холодного и L2 для горячего. 

Исходный контент находится в видеохранилище. Далее разместим L1-кэш — уровень, который предназначен для холодного контента, ближе к хранилищу и с прямым доступом к нему. А L2, то есть горячий кэш, расположим как можно ближе к зрителям, в том числе в регионах. При этом, как показано на схеме, у региональных CDN-серверов с горячим контентом нет доступа в основное видеохранилище, вместо этого используется L1 для подкачки на уровень L2. 

Тут можно было бы прокинуть VPN с L2 сразу в хранилище или сделать отдельный канал, но это была бы более сложная и дорогая схема. Каждая стрелочка на схеме не бесплатная с точки зрения ресурсов, перемещая контент как можно ближе к зрителю, мы экономим сетевой трафик.  

Ниже в таблице характеристики серверов для раздачи горячего и холодного контента. Для горячего L2-кэша в среднем не требуются очень производительные серверы и скорости отдачи в 20-40 ГБит/с обычно достаточно, зато их нужно много. L1 же прокачивает через себя больше контента, так как является промежуточным звеном и обеспечивает доступ к тому самому длинному хвосту из миллионов видео. Поэтому холодные серверы обычно мощнее и оснащены сетевыми картами на 100 или 200 Гбит/с. 

При этом скорость раздачи сервера определяется не только сетевой картой, но и нашим договором с оператором связи и тем, сколько местный провайдер может раздавать трафика.

20 Gbit/s | 40 Gbit/s

100 Gbit/s | 200 Gbit/s

Chassis

1-2U

1-2U

CPU

AMD EPYC 9xxx Series не менее 48 ядер

2x AMD EPYC 9xxx Series не менее 48 ядер

RAM

DDR5 ECC/REG не менее 512Gb и не менее 4800MHz

DDR5 ECC/REG не менее 1Tb и не менее 4800MHz

Storage

2x SSD минимум 480Gb под ОС (Hardware RAID), не менее SSD SATA 3.84Tb (JBOD)

2x SSD минимум 480Gb под ОС (Hardware RAID), не менее 16х SSD NVME 7.68Tb (JBOD)

RAID

не менее контроллер LSI/Broadcom MegaRAID SAS (JBOD)

не менее контроллер NVME/SAS

Network

2x Mellanox 2 port SFP+, 10GbE

1) 2x Mellanox 2 port SFP28, 25GbE

2) 1x Mellanox 2 port QSFP28, 100GbE

Как раздать 200 Гбит/с с одного сервера

Если просто взять сервер, вставить в него сетевую карту на 200 Гбит/с, то, к сожалению, без дополнительных усилий скорость отдачи будет намного ниже. Чтобы утилизировать всю пропускную способность сетевой карты, нужно знать, что такое NUMA — Non-Unified Memory Access.  

Идея состоит в следующем: когда на сервере несколько процессоров, то скорость их доступа к разным областям памяти отличается. Потому что часть RAM для процессора будет локальной и соответственно более быстрой, а к другой он будет обращаться через шину (это называется interconnect), то есть медленнее. Ниже иллюстрация принципа работы NUMA.

Схема работы с памятью на многопроцессорных серверах
Схема работы с памятью на многопроцессорных серверах

Для того, чтобы раздавать с одного сервера все 200 Гбит/с, нужно привязывать прерывание сетевых карт к ядрам того процессора, который их обрабатывает, чтобы не обращаться к памяти через шину.  

К счастью, для сетевых карт Mellanox, которые мы используем, уже есть специальные скрипты, которые позволяют настроить NUMA (см. github.com/Mellanox/mlnx-tools). Скрипты имеют возможность выбора профиля: minimum latency предназначен для оптимизации задержек, а high throughput — для максимальной пропускной способности. Нас в данном случае интересует максимальная пропускная способность. 

Кэширование

Разберём, как разложить контент по кэширующим серверам. Теоретически можно, например, с помощью rsync копировать видео из хранилища. Это даже будет работать, но придется городить целую схему, чтобы обеспечить эффективность и надёжность. Поэтому у нас кэширование организовано в обратную сторону: вместо того чтобы нам закачивать на CDN-серверы какие-то файлы, CDN сам скачивает нужные данные из хранилища по мере надобности при помощи обратного проксирования. 

Мы используем nginx proxy_pass для скачивания и, что логично, делаем и proxy_cache, чтобы сервер складывал то, что скачал, в свой кэш. Также мы разделяем контент на чанки по 4 секунды, то есть скачиваются не сразу гигабайтные видеоролики, а чанки в несколько мегабайт.

Такая схема может генерировать достаточно большую нагрузку на диски, но её можно регулировать с помощью директивы proxy_cache_min_uses.

В зависимости от этого параметра контент будет попадать на диск только после нескольких обращений за ним. 

Распределенное кэширование

На CDN-серверы под кэш мы ставим обычно 8-32 диска, но не объединяем их в общий RAID. Потому что, если вдруг RAID развалится, то потребуется долгая процедура rebuild. Вместо этого можно использовать такой трюк:

proxy_cache_path /media/d0 keys_zone=d0:512m levels=2:2:1 use_temp_path=off;
proxy_cache_path /media/d1 keys_zone=d1:512m levels=2:2:1 use_temp_path=off;
proxy_cache_path /media/d2 keys_zone=d2:512m levels=2:2:1 use_temp_path=off;
proxy_cache_path /media/d3 keys_zone=d3:512m levels=2:2:1 use_temp_path=off;
proxy_cache_path /media/d4 keys_zone=d4:512m levels=2:2:1 use_temp_path=off;
proxy_cache_path /media/d5 keys_zone=d5:512m levels=2:2:1 use_temp_path=off;

split_clients $request_uri $disk_cache {
  16.66%  d0;
  16.66%  d1;
  16.66%  d2;
  16.66%  d3;
  16.66%  d4;
  16.66%  d5;
  *   d0;
}

Здесь на каждый диск, который есть в сервере, определяется своя зона кэширования и через директиву split_clients разделяются запросы, которые приходят за контентом в кэш (подробнее см. документацию). Таким образом мы равномерно размазываем весь контент по всем дискам сервера. 

Если какой-то диск выйдет из строя, что на наших масштабах случается регулярно, то достаточно его исключить и пересчитать директиву split_clients, и не нужно будет делать rebuild.  

Кроме того, нужно учесть подключение серверов к операторам связи. Чтобы не дублировать контент на одной площадке, то есть не хранить одни и те же видео на серверах, стоящих в одном CDN-узле, но подключенных к разным операторам, у нас организовано распределенное кэширование. 

Распределенное кэширование устроено так, что каждый запрос, попадающий в nginx, проходит две стадии:

  • балансировка с consistent-hash, 

  • непосредственно кэширование. 

Таким образом, даже если запросы приходят от разных серверов, они попадают всегда на один и тот же кэш-сервер и дублирование не происходит. Сплошное пространство кэширования помогает эффективнее использовать ресурсы и обеспечивать большой объем закэшированного контента. Например, на площадке в Москве мы рассчитываем получить 10 Пбайт кэша. 

На самом деле в качестве веб-сервера мы используем не nginx, а Angie — форк nginx от его бывших разработчиков. В Angie из коробки есть полезный нам расширенный мониторинг, а также там уже собраны некоторые сторонние модули, которых нам не хватает для nginx. Подробнее о том, что мониторим, какие модули используем и как мигрировали, рассказали в статье

Поиск ближайшего сервера

После того, как мы разложили видео по горячим и холодным кэшам, нужно адресовать запрос от пользователя на правильный сервер так, чтобы:

  • это был ближайший сервер, то есть чтобы было быстро

  • сбалансировать нагрузку, чтобы это было надёжно;

  • и при этом эффективно расходовать сетевые ресурсы. 

Для выбора ближайшего сервера существует BGP Anycast.

По Anycast один и тот же IP-адрес анонсируется из разных мест. Когда запрос идет по такому адресу, он автоматически попадает на ближайший сервер с точки зрения топологии сети. 

Сделать Anycast достаточно несложно, нужно: 

  • купить сеть, которую мы будем анонсировать;

  • поставить на сервер демон маршрутизации, мы используем bird;

  • всё, profit.  

Это будет работать, но если что-то в сломается, то починить будет трудно, потому что везде транслируется один и тот же IP-адрес, а значит, трудно локализовать проблему. 

Кроме проблемы надёжности в нештатных ситуациях, с Anycast есть еще одна проблема: с точки зрения Anycast, ближайший сервер не всегда ближайший с точки зрения географии. Например, из Новосибирска ближайший сервер может оказаться в Санкт-Петербурге. 

Чтобы определять ближайший к пользователю сервер с учётом физического расстояния, существует другая технология — GeoIP. GeoIP — это база данных производства компании MaxMind — по сути бинарный файл, который загружается в nginx-модуль. С его помощью в ответ на запрос с IP-адресом nginx возвращает: регион местоположения пользователя, город, а также, возможно, информацию об операторе. 

Однако в GeoIP-базе MaxMind есть неточности, причём количество этих неточностей растет, поэтому нам приходится накладывать на неё правки.

Ниже представлен пример корректировки GeoIP-базы: 

  • заводим список, где пишем IP-адреса и то, к какому региону их нужно относить;

  • через директиву geo формируем переменную, которая в ответ на IP-запрос возвращает расположение;

  • делаем map на приоритетное использование региона из нашего списка. 

# geoip_corrections.conf example
198.51.100.0/24 Moscow; # RFC 5737 TEST-NET-2

# nginx.conf
geo $region_name_from_geo {
	# The variable will be empty if IP address does not match.
	include geoip_corrections.conf;
}

# Now we combine region names.
map $region_name_from_geo $region_name {
	# If there is no correction.
	''  	$geoip2_region_name;
	# And if there is.
	default $region_name_from_geo;
}

Это было бы не такой уж большой проблемой, если бы не влияло на пользователей. Но, к сожалению, о необходимости поправок мы обычно узнаём из обращений зрителей. Как это происходит? Неправильный GeoIP приводит к двум нежелательным ситуациям:

  • Если определить неправильный регион, то можно направить зрителя не на ближайший сервер. Зритель скорее всего этого не поймёт и не будет жаловаться, но мы же хотели так не делать. 

  • Если неверно определить страну, то из-за локальных лицензионных ограничений контент может вовсе стать недоступен для пользователя. Тогда, возможно, он обратится в поддержку, мы всё исправим, но зритель на какое-то время останется без любимого шоу и будет расстроен. 

Есть другой вариант GeoIP-базы, но с ними тоже есть свои особенности. Во-первых, в базе MaxMind 32 млн записей, а в geoip.noc.gov.ru — 4,5 млн записей. Допустим, это не так страшно, потому что это записи о сетях и они могут быть разного размера. Во-вторых, в базах различаются названия регионов, например, Chuvashia вместо Chuvash Republic. И третье, что нам действительно мешает перейти на эту базу: порядка 5 млн IP-адресов со всей страны в ней приписаны к Москве. То есть она опять не позволит всегда корректно определять ближайшие кэш-серверы, некоторым зрителям из регионов по-прежнему придется ждать лишнее время, чтобы посмотреть видео на RUTUBE. 

Балансировка

Все запросы зрителей за видео идут не напрямую к одному из CDN-серверов, с сначала попадают в наше самописное ПО, которое называется видеобалансер. 

Видеобалансер: 

  • определяет, где находится зритель, по GeoIP-базе;

  • учитывает оператора, чтобы раздавать контент по возможности минуя меж-операторские стыки;

  • считает температуру видео, то есть горячий это контент или холодный;

  • мониторит нагрузку на серверы CDN;

  • распределяет зрителей на ближайший свободный сервер, где есть нужный контент.

Ниже скриншот из видеобалансера — у него утилитарный инженерный интерфейс, не самый красивый, но рабочий. 

Интерфейс нашего видеобалансера
Интерфейс нашего видеобалансера

На скриншоте каждому серверу соответствует три строки, потому что балансер развернут в нескольких ЦОДах для отказоустойчивости. Цветами обозначен уровень загрузки: красному серверу уже хватит, жёлтый ещё может выдержать нагрузку, а зеленый явно недогружен. 

Итоговая схема раздачи приведена на схеме ниже.

Когда пользователь открывает плеер RUTUBE происходит следующее: 

  • клиент отправляет запрос за видео в видеобалансер; 

  • балансер по GeoIP определяет ближайший к пользователю CDN; 

  • балансер знает температуру запрашиваемого видео, он мониторит CDN-серверы и в соответствие с этими данными возвращает мастер-манифест на клиент со ссылками на плейлисты с чанками запрашиваемого видео;

  • по манифесту зритель уже направляется на конкретные CDN-серверы (с учётом всех уровней кэширования, которые мы рассмотрели ранее). 

Edgestat на схеме выше — это наш сервис мониторинга, с помощью которого балансер узнаёт о состоянии серверов. Edgestat написан на Go и напрямую из псевдо-файловой системы /proc собирает параметры производительности процессора, сети и пр. Ниже пример файла, который модуль мониторинга возвращает в балансер:

{
  "net": {
	"tx_bytes": 3070389798640361
  },
  
  "cpu": {
	"user": 821038789,
	"nice": 7206904,
	"system": 1068090496,
	"idle": 8891017069,
	"iowait": 140402173,
	"irq": 183963736,
	"softirq": 1899188205,
	"steal": 0,
	"guest": 0,
	"guest_nice": 0
  },
  
  "tcp": {
	"out": 4009339463073,
	"retrans": 107337952956
  },
  
  "pressure": {
	"cpu": 0.42,
	"io": 1.77,
	"memory": 0.77,
	"irq": 1.7
  }
}

Разберем подробнее блок "pressure". Pressure Stall Information — подсистема внутри Linux-ядра, которая отслеживает задержки в работе системы, связанные с нехваткой железных ресурсов, таких как CPU, устройств ввода-выводы, память.

По меркам ядра PSI появилась относительно недавно, около 7 лет назад. А также буквально пару лет назад — совсем новинка — в неё добавили отслеживание задержек по обработке прерываний — IRQ (значения в микросекундах). 

Мониторить irq особенно полезно на серверах, обрабатывающих много трафика, так как в таких условиях обработка прерываний от сетевой карты уже может вносить существенный вклад в общую производительность. 

$ ls /proc/pressure
cpu  io  irq  memory
$ cat /proc/pressure/io
some avg10=2.50 avg60=2.38 avg300=2.34 total=13968383315
full avg10=2.35 avg60=2.22 avg300=2.15 total=12888830571
$ cat /proc/pressure/irq
full avg10=0.64 avg60=0.36 avg300=0.24 total=7294258624
$

На серверах, прокачивающих большой трафик, например, как у нас в Тбит/с, показатель irq pressure часто скачет и сообщает нам, что сервер перегружен, даже если этого не видно по другим характеристикам. Внедрение PSI помогло нам окончательно избавить CDN от тормозов — повысить скорость

Для обеспечения дополнительной надёжности в схеме раздачи в мастер-манифесте содержится не один, а два CDN-сервера: основной и резервный. Мастер-манифест — это по сути плейлист из плейлистов, выглядит примерно так: 

#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=478000, FRAME-RATE=25, CODECS="avc1.42c01f, mp4a.40.2", RESOLUTION=256x144
https://cdn-server
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=478000, FRAME-RATE=25, CODECS="avc1.42c01f, mp4a.40.2", RESOLUTION=256x144
https://backup-cdn

Наличие резервного CDN-сервера в манифесте позволяет клиенту без дополнительных запросов и лишнего ожидания переключиться с одного сервера, с которым по какой-то причине проблемы, на другой. Причём бэкапный сервер определяется по Anycast — то есть мы резервируем ещё и способы определения подходящего CDN-сервера. 

Сеть

Работа с сетью — также неотъемлемая часть эффективной реализации CDN, ведь на таких расстояниях и объемах трафика сеть легко может стать узким местом. 

Ниже неполный вывод команда ip, который показывает список сетевых интерфейсов одного из наших CDN-серверов.

$ ip -c a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
	link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
	inet 127.0.0.1/8 scope host lo
   	valid_lft forever preferred_lft forever
2: aggr0: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond0 state UP group default qlen 1000
	link/ether xx:xx:xx:xx:xx:x1 brd ff:ff:ff:ff:ff:ff
3: aggr1: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond0 state UP group default qlen 1000
	link/ether xx:xx:xx:xx:xx:x2 brd ff:ff:ff:ff:ff:ff
4: aggr2: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond1 state UP group default qlen 1000
	link/ether xx:xx:xx:xx:xx:x3 brd ff:ff:ff:ff:ff:ff
5: aggr3: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond1 state UP group default qlen 1000
	link/ether xx:xx:xx:xx:xx:x4 brd ff:ff:ff:ff:ff:ff
6: anycast2: <BROADCAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default qlen 1000
	link/ether xx:xx:xx:xx:xx:xa brd ff:ff:ff:ff:ff:ff
	inet 198.51.100.79/24 brd 89.248.230.255 scope global anycast2
   	valid_lft forever preferred_lft forever
7: bond0: <BROADCAST,MULTICAST,MASTER,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
	link/ether xx:xx:xx:xx:xx:xb brd ff:ff:ff:ff:ff:ff
	inet 192.0.2.1/24 brd 192.0.2.0 scope global bond0
   	valid_lft forever preferred_lft forever
8: bond1: <BROADCAST,MULTICAST,MASTER,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
	link/ether xx:xx:xx:xx:xx:xc brd ff:ff:ff:ff:ff:ff
	inet 203.0.113.2/24 brd 203.0.113.0 scope global bond1
   	valid_lft forever preferred_lft forever
$

То есть на каждом из десятков CDN стоят серверы с десятками же сетевых интерфейсов — всем этим нужно эффективно и надёжно управлять. Например, везде должны быть правильные согласованные с операторами настройки. Использовать для этого штатный NetworkManager не очень удобно, поэтому мы перешли на systemd-networkd

systemd-networkd — это демон для настройки сети. Как и всё в systemd, все настройки в нём хранятся в текстовых файлах, что удобно для автоматизации, например, Ansible. 

Ниже пример файла .link, который настраивает физические сетевые устройства. В нём задаётся максимальный размер буферов для эффективной раздачи больших объемов трафика. 

# /etc/systemd/network/10-aggr0.link

[Match]
MACAddress=xx:xx:xx:xx:xx:xx

[Link]
Name=aggr0
RxBufferSize=max
TxBufferSize=max

В файле .netdev описываются виртуальные сетевые устройства, например, bond — объединение двух физических сетевых устройств в одно логическое для большей отказоустойчивости или скорости отдачи. 

# /etc/systemd/networkd/bond0.netdev
[NetDev]
Name=bond0
Kind=bond

[Bond]
Mode=802.3ad
MIIMonitorSec=100ms
TransmitHashPolicy=layer3+4

Другой пример полезной настройки в .netdev — dummy-интерфейс. По сути это loopback, который мы используем для того, чтобы на него назначать Anycast-адрес. Можно назначить этот Anycast-адрес как второй ip-адрес на сетевой интерфейс, но тогда, если сетевой интерфейс перестанет работать, то и Anycast-адрес перестанет быть доступным.

# /etc/systemd/network/anycast2.netdev

[NetDev]
Name=anycast2
Kind=dummy

Третий вид файлов systemd-networkd это .network. Они описывают стандартные настройки: IP-адреса, gateway, маршруты и т.д. 

# /etc/systemd/network/aggr0.network
[Match]
Name=aggr0
[Network]
Bond=bond0

# /etc/systemd/network/bond0.network
[Match]
Name=bond0

[Network]
Address=12.34.56.78/28
Gateway=12.34.56.79

Подключение операторов

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

Проблема в том, что при подключении двух операторов к одному серверу «в лоб» получается асимметричная маршрутизация. Её эффект показан на иллюстрации слева: 

  • на anycast-адрес второго оператора приходит запрос, 

  • ответ на этот запрос направляется default gateway, который скорее всего будет указывать в первого оператора.  

Получится медленнее и менее надёжно из-за сетевых стыков и более длинного маршрута. Очевидно, что схема как на иллюстрации справа лучше. 

Как сделать, чтобы ответ на запрос уходил тому же оператору, от которого он пришел? При помощи fwmark — меток сетевых пакетов. Это виртуальные метки, которые ставятся на сетевые пакеты и при этом существуют в рамках одного Linux-хоста. Они позволяют сделать такой трюк: 

chain prerouting {
	type filter hook prerouting priority mangle; policy accept;
	iifname "bond0" ip daddr 198.51.100.0/24 meta mark set 0x00000010 accept comment "isp1 fwmark"
	iifname "bond1" ip daddr 198.51.100.0/24 meta mark set 0x00000020 accept comment "isp2 fwmark"
}

Выше представлены правила nftables: на запросы, приходящие через сетевой интерфейс "bond0", ставим на него метку 0x00000010 (неважно какую, важно, что не ноль), а на "bond1" ставим другую метку. 

В соответствие с этим прописываем и исходящие правила маршрутизации: 

$ ip rule
0:  	from all lookup local
110:	from 198.51.100.0/24 fwmark 0x10 lookup 100 proto static
120:	from 198.51.100.0/24 fwmark 0x20 lookup 101 proto static
32766:  from all lookup main
32767:  from all lookup default
$

Здесь в приоритете 110 написано: если запрос с Anycast-адреса и есть метка 0x10, то нужно посмотреть в таблицу номер 100, где будет default gateway для bond0; а если метка другая — то надо смотреть в другую таблицу, где уже будет default gateway для bond1. 

Воедино входящие правила фаервола и исходящие правила маршрутизации связывают несколько sysctl:

net.ipv4.tcp_fwmark_accept=1
net.ipv4.fwmark_reflect=1
net.ipv4.conf.all.src_valid_mark=1

Здесь написано следующее: если на сетевой пакет, который пришел на данный сетевой интерфейс, есть метка, то её нужно скопировать на весь сокет и выставлять на все исходящие пакеты в рамках соединения. 

Player events

Мы мониторим состояние серверов, но кроме того, для надёжности, плеер пользователя в случае каких-либо проблем научен отправлять специальные сообщения, которые называются player events. Если на клиенте, в вебе, в мобильном или Smart TV приложении RUTUBE возникнут какие-то проблемы с доступом к CDN, то мы узнаем об это по резервному маршруту. Например, сбои в каком-то сегменте сети, дадут всплеску player events и наш мониторинг тут же отреагирует. 

И последняя в этой статье оптимизация: простая, но важная. Мы уже говорили, что используем демон маршрутизации BIRD, чтобы анонсировать Anycast. Логично, что тогда должен быть запущен и nginx, чтобы он что-то раздавал. Если запустить bird, но не запустить nginx (а у нас такое бывало), то зрители придут и попадут в черную дыру. Чтобы такого избежать, можно использовать такой трюк:

# /etc/systemd/system/bird.service.d/requisite_nginx.conf
[Unit]
# If nginx is stopped, bird will be stopped as well.
# If nginx is not running, bird will refuse to start.
# If bird is stopped, nginx is not affected.
# If nginx is restarted, bird is not affected.
Requisite=nginx.service

Главное тут в последней строчке: если nginx запущен, то bird работает; если nginx выключить, то bird погаснет; если nginx не запущен, то и bird не запустится.

Итоги

В этой статье мы рассмотрели архитектуру CDN RUTUBE: как кэшировать, балансировать нагрузку, обеспечивать отказоустойчивость или, в крайнем случае, деградировать незаметно для зрителей. Как сделать CDN, чтобы показывать нашим пользователям видео быстро, надежно и эффективно.

Вот три основных момента, на которые нужно обратить внимание, если вы строите свой CDN:

  • Если вы только начинаете строить CDN, то начните с BGP Anycast как наиболее простого варианта. Но не ограничивайтесь им и подключайте узконаправленные средства балансировки, подходящие для вашего профиля трафика и нагрузки. 

  • Используйте разные источники мониторинга: и изнутри системы, и снаружи.

  • Pressure Stall Information — это ваш союзник в борьбе с тормозами. 

Ещё больше секретов архитектуры, высоконагруженных систем, профессиональных лайфхаков и реальных кейсов можно увидеть на Saint HighLoad++ 2025. Программа и другая полезная информация — на официальном сайте конференции.

А если хотите знать больше о создании медиасервисов, подписывайтесь на канал Смотри за IT: в нём инженеры Цифровых активов «Газпром-Медиа Холдинга» таких, как PREMIER, RUTUBE, Yappy делятся своим опытом и тонкостями разработки видеоплатформ.

Комментарии (7)


  1. Lazhu
    18.06.2025 10:26

    17,7 млн ежедневных пользователей

    опрос на сайте mail.ru показал, что 100% населения пользуется интернетом


    1. contuild
      18.06.2025 10:26

      а опрос на ютубе показал что более 80% не пользуются