В одной из предыдущих статей мы рассказывали, как Netflix использует eBPF для масштабного сбора журналов TCP-потоков, чтобы лучше понимать состояние облачной сети. В этом материале мы разберём, как Netflix решил ключевую задачу: корректно сопоставлять IP-адреса в потоках с идентичностями рабочих нагрузок.
Краткий обзор
FlowExporter — сайдкар-контейнер, который запускается рядом со всеми рабочими нагрузками Netflix в облаке AWS. Он использует eBPF и точки трассировки TCP (TCP tracepoints), чтобы отслеживать изменения состояния TCP-сокетов. Когда TCP-сокет закрывается, FlowExporter формирует запись в журнале потока («flow logs»), куда входят IP-адреса, порты, метки времени и дополнительные метрики по сокету. В среднем генерируется около 5 миллионов записей в секунду.
В облачных средах IP-адреса перераспределяются между различными рабочими нагрузками по мере создания и завершения их экземпляров, поэтому одних IP-адресов недостаточно, чтобы понять, какие нагрузки взаимодействуют. Чтобы журналы потоков были полезны, каждый IP-адрес нужно отнести к соответствующей идентичности рабочих нагрузок. FlowCollector — это бэкенд-сервис, который собирает журналы потоков от всех экземпляров FlowExporter во флоте, выполняет атрибуцию IP-адресов и отправляет атрибутированные записи потоков в Netflix Data Mesh для последующей потоковой и пакетной обработки.
Журналы потоков eBPF дают целостное представление о топологии сервисов и «здоровье» сети по всему флоту микросервисов Netflix — независимо от используемого языка программирования, RPC-стека или протокола прикладного уровня в отдельных нагрузках. Это особенно полезно там, где наш service mesh ещё не покрывает инфраструктуру.
Проблема неверной атрибуции
Корректно сопоставлять IP-адреса потоков с идентичностями рабочих нагрузок было серьёзным вызовом с момента запуска журналов потоков на eBPF.
Изначально мы опирались на Sonar — внутренний сервис учета IP-адресов, который генерирует событие каждый раз, когда IP-адрес в VPC AWS Netflix назначается рабочей нагрузке или освобождается. FlowCollector потребляет поток событий о смене IP-адресов из Sonar и использует эти данные, чтобы выполнять атрибуцию IP-адресов потоков в реальном времени.

Фундаментальный недостаток этого метода в том, что он может приводить к неверной атрибуции. В распределённых системах неизбежны задержки и сбои, из-за чего события о смене IP-адресов могут поздно доходить до FlowCollector. Например, IP-адрес мог сначала быть назначен нагрузке X, а затем переназначен нагрузке Y. Однако если событие о переназначении задержится, FlowCollector будет продолжать считать, что IP-адрес принадлежит X, и в итоге появятся неверно атрибутированные потоки. К тому же метки времени у событий могут быть неточными в зависимости от способа их фиксации.
Неверная атрибуция делает данные о потоках ненадёжными для принятия решений. Пользователи часто полагаются на журналы сетевых потоков, чтобы подтверждать зависимости рабочих нагрузок, но неверная атрибуция вносит путаницу. Без экспертного понимания ожидаемых зависимостей пользователям трудно обнаружить или подтвердить факт неверной атрибуции. Более того, она часто возникала у критически важных сервисов с большим числом экземпляров (частыми сменами IP-адресов). В целом это делает анализ зависимостей по всему флоту практически непригодным.
Мы ввели задержку (буфер) 15 минут перед атрибуцией, чтобы дождаться запоздалых событий о смене IP. Этот подход снизил количество неверной атрибуции, но не устранил её полностью. К тому же ожидание снижало «оперативность» данных и их ценность для анализа в реальном времени.
Полностью устранить неверную атрибуцию критически важно, поскольку достаточно одного неверно атрибутированного потока, чтобы породить ошибочную зависимость между рабочими нагрузками. Пришлось пересобрать подход с нуля. За последний год Netflix разработал новый метод атрибуции, который, как описано далее, наконец устранил неверную атрибуцию.
Атрибуция локальных IP-адресов
У каждого сокета два IP-адреса: локальный и удалённый. Раньше мы использовали один и тот же метод атрибуции для обоих. Однако атрибуция локального IP-адреса должна быть проще, поскольку локальный IP-адрес принадлежит тому экземпляру, на котором FlowExporter зафиксировал сокет. Поэтому FlowExporter должен определить локальную идентичность рабочей нагрузки на основе своего окружения и выполнить атрибуцию локального IP-адреса до отправки потока в FlowCollector.
Это просто для нагрузок, работающих напрямую на экземплярах EC2: Metatron в Netflix выдаёт сертификаты идентичности рабочей нагрузки каждому экземпляру EC2 при загрузке. FlowExporter может просто прочитать эти сертификаты с локального диска, чтобы определить локальную идентичность рабочей нагрузки.
Атрибуция локальных IP-адресов для контейнерных нагрузок, работающих на контейнерной платформе Netflix Titus, сложнее. FlowExporter работает на уровне хоста контейнеров, где каждый хост управляет несколькими контейнерными нагрузками с разными идентичностями. Когда eBPF-программы FlowExporter получают событие по сокету из точек трассировки TCP в ядре, сокет мог быть создан одним из контейнеров или самим хостом. Поэтому FlowExporter должен определить, к какой нагрузке отнести локальный IP-адрес сокета. Чтобы решить эту задачу, мы задействовали IPMan — сервис назначения IP-адресов для контейнеров в Netflix. IPManAgent, демон, работающий на каждом хосте контейнеров, отвечает за назначение и освобождение IP-адресов. По мере запуска контейнерных нагрузок IPManAgent записывает соответствие «IP-адрес → ID рабочей нагрузки» в карту eBPF, которую затем используют eBPF-программы FlowExporter, чтобы по локальному IP-адресу сокета найти связанный ID рабочей нагрузки.

Ещё одной задачей было поддержать механизм трансляции IPv6 в IPv4 на Titus. Чтобы упростить миграцию на IPv6, Netflix разработал механизм, позволяющий контейнерам, работающим только по IPv6, общаться с узлами по IPv4 без накладных расходов NAT64. Этот механизм перехватывает системные вызовы connect и заменяет использующийся сокет на сокет с общим IPv4-адресом, назначенным хосту контейнеров. Это ломает привязку на стороне FlowExporter: ядро сообщает один и тот же локальный IPv4-адрес для сокетов, создаваемых разными контейнерными нагрузками. Чтобы их различать, нужна еще и информация о локальном порте. Мы доработали Titus так, чтобы при каждом перехвате системного вызова connect он записывал в карту eBPF соответствие «(локальный IPv4-адрес, локальный порт) → ID рабочей нагрузки». Затем eBPF-программы FlowExporter используют эту карту, чтобы корректно атрибутировать сокеты, созданные механизмом трансляции.
С учётом решённых проблем мы теперь можем точно атрибутировать локальный IP-адрес каждого потока.
Атрибуция удалённых IP-адресов
После того как проблема с атрибуцией локального IP-адреса решена, становится возможной точная атрибуция удалённых IP-адресов. Теперь каждый поток, который отправляется FlowExporter, содержит локальный IP-адрес, идентичность локальной рабочей нагрузки и метки времени начала и окончания соединения. По мере получения этих потоков FlowCollector может определять интервалы времени, в течение которых каждая нагрузка владела данным IP-адресом. Например, если FlowCollector видит поток с локальным IP-адресом 10.0.0.1, связанным с нагрузкой X, который начинается в t1 и заканчивается в t2, он может сделать вывод, что 10.0.0.1 принадлежал нагрузке X в интервале от t1 до t2. Поскольку Netflix использует Amazon Time Sync по всему флоту, метки времени (зафиксированные FlowExporter) надёжны.
Кластер сервиса FlowCollector состоит из множества узлов. Каждый узел должен уметь атрибутировать любой удалённый IP-адрес и, следовательно, ему необходимы сведения обо всех IP-адресах нагрузок и их недавней истории владения. Для представления этих сведений каждый узел поддерживает в памяти хеш-таблицу, которая отображает IP-адрес в список временных интервалов, как показано следующими структурами на Go:
type IPAddressTracker struct {
ipToTimeRanges map[netip.Addr]timeRanges
}
type timeRanges []timeRange
type timeRange struct {
workloadID string
start time.Time
end time.Time
}
Чтобы заполнить хеш-таблицу, FlowCollector извлекает из каждого полученного потока локальный IP-адрес, идентичность локальной рабочей нагрузки, время начала и окончания соединения и создаёт или расширяет соответствующие временные интервалы в этой map. Временные интервалы для каждого IP-адреса отсортированы по возрастанию и не пересекаются, поскольку один IP-адрес не может одновременно принадлежать двум разным нагрузкам.
Поскольку каждый поток отправляется только на один узел FlowCollector, каждый узел должен делиться полученными на основе полученных потоков интервалами с остальными узлами. Мы реализовали механизм широковещания (fan-out) через Kafka: каждый узел публикует выученные интервалы остальным узлам в кластере. Хотя существуют более эффективные реализации широковещания, подход с Kafka прост и хорошо зарекомендовал себя.
Теперь FlowCollector может атрибутировать удалённые IP-адреса, выполняя их поиск в своей хеш-таблице (map), которая возвращает список временных интервалов. Затем он использует метку времени начала потока, чтобы определить соответствующий интервал и связанную с ним идентичность рабочей нагрузки. Если момент начала не попадает ни в один интервал, FlowCollector повторит попытку позже и в итоге прекратит попытки, если атрибуция так и не удалась. Такие сбои возможны, когда потоки теряются или широковещательные сообщения приходят с задержкой. Для наших сценариев допустимо оставить небольшой процент потоков без атрибуции, но любая неверная атрибуция недопустима.

Новый метод обеспечивает точную атрибуцию благодаря непрерывным heartbeat-сообщениям, каждое из которых привязано к надёжному временному интервалу принадлежности IP-адреса. Он устойчив к кратковременным сбоям: несколько задержанных или потерянных heartbeat-сообщений не приводят к неверной атрибуции. В отличие от этого, прежний метод опирался только на дискретные события назначения и освобождения IP-адресов. Без heartbeat-сообщений приходилось предполагать, что IP-адрес остаётся назначенным до получения уведомления об обратном (что может занять часы или дни), из-за чего задержки уведомлений приводили к неверной атрибуции.
Один нюанс: когда FlowCollector получает поток, он не может сразу атрибутировать его удалённый IP-адрес, поскольку для этого нужны самые свежие наблюдавшиеся интервалы для удалённого IP-адреса. Поскольку FlowExporter отправляет потоки пакетами раз в минуту, FlowCollector должен дождаться пакета потоков за последнюю минуту от удалённой рабочей нагрузки, который мог ещё не прийти. Чтобы решить это, FlowCollector временно сохраняет полученные потоки на диск на одну минуту перед атрибуцией их удалённых IP-адресов. Это добавляет задержку в 1 минуту, что значительно меньше 15 минут в предыдущем подходе.
Помимо точности, новый метод также экономичен за счёт простоты и поиска в памяти. Поскольку состояние в памяти можно быстро восстановить при запуске узла FlowCollector, постоянное хранилище не требуется. Используя 30 экземпляров c7i.2xlarge, мы обрабатываем ≈5 млн потоков/с по всему флоту Netflix.
Атрибуция межрегиональных IP-адресов
Ранее мы опустили детали разделения по регионам для простоты. Облачные микросервисы Netflix работают в нескольких регионах AWS. Чтобы оптимизировать отчётность по потокам и минимизировать межрегиональный трафик, кластер FlowCollector развёрнут в каждом крупном регионе, а агенты FlowExporter отправляют потоки в соответствующий региональный FlowCollector. Когда FlowCollector получает поток, его локальный IP-адрес гарантированно принадлежит этому региону.
Чтобы уменьшить межрегиональный трафик, механизм широковещания ограничен узлами FlowCollector внутри одного региона. В результате in-memory хеш-таблица (map) интервалов для IP-адресов содержит адреса только этого региона. Однако у межрегиональных потоков удалённый IP-адрес находится в другом регионе. Для атрибуции таких потоков принимающий узел FlowCollector форвардит эти потоки в региональный кластер FlowCollector. Регион для удалённого IP-адреса определяется по префиксному дереву (trie), построенному из всех CIDR-подсетей VPC Netflix. Такой подход эффективнее, чем рассылать обновления временных интервалов IP-адресов по всем регионам, поскольку лишь 1% потоков Netflix являются межрегиональными.
Атрибуция IP-адресов, не относящихся к рабочим нагрузкам
До сих пор FlowCollector умеет точно атрибутировать IP-адреса, принадлежащие облачным рабочим нагрузкам Netflix. Однако не все IP-адреса в потоках относятся к этой категории. Например, значительная доля потоков проходит через балансировщики AWS ELB. В таких потоках удалённые IP-адреса соответствуют самим ELB, где запустить FlowExporter мы не можем. Следовательно, FlowCollector не в состоянии вывести их идентичности просто по наблюдаемым потокам. Чтобы атрибутировать такие удалённые IP-адреса, мы по-прежнему используем события смены IP-адресов из Sonar, который обходит ресурсы AWS и выявляет изменения в назначениях IP. Хотя у этого потока данных метки времени могут быть неточными, а сами события — запаздывать, риск неверной атрибуции невелик, поскольку переназначение IP-адресов у ELB происходит крайне редко.
Проверка корректности
Подтвердить, что новый метод полностью устранил неверную атрибуцию, сложно, так как нет окончательного «источника истины» по зависимостям рабочих нагрузок, с которым можно было бы сверить журналы потоков; в конечном счёте сами журналы потоков призваны быть таким источником. Чтобы получить достаточную уверенность, мы проанализировали журналы потоков крупного сервиса с хорошо известными зависимостями. Нужен большой масштаб, поскольку неверная атрибуция чаще проявляется у сервисов с множеством экземпляров, и при этом должна существовать надёжная возможность определить зависимости без опоры на журналы потоков.
Облачный шлюз Netflix — Zuul — идеально подошёл для этой цели благодаря своему масштабу (обрабатывает весь входящий облачный трафик), большому числу downstream-зависимостей и возможности вывести его зависимости из конфигураций маршрутизации в качестве «источника истины» для сравнения с журналами потоков. За двухнедельный период мы не обнаружили ни одной неверной атрибуции для потоков, проходящих через Zuul. Это вселило серьёзную уверенность, что новый метод действительно устранил неверную атрибуцию. В прежнем подходе примерно 40% зависимостей Zuul, определённых по журналам потоков, были атрибутированы неверно.
Заключение
После устранения неверной атрибуции журналы потоков eBPF теперь дают надёжную, охватывающую весь флот картину топологии сервисов и состояния сети Netflix. Это открывает множество новых возможностей — от аудита зависимостей сервисов и анализа безопасности до триажа инцидентов — и помогает инженерам Netflix лучше понимать наши постоянно эволюционирующие распределённые системы.
Если тема сетей, межрегионального трафика и наблюдаемости для вас актуальна, вот куда идти дальше — уже руками: разберёте BGP/OSPF/IS-IS на боевых стендах, соберёте отказоустойчивую инфраструктуру под нагрузку и наведёте порядок в метриках/логах без «зоопарка» тулов. Открыт набор на курсы:
Network Engineer. Professional — BGP, OSPF/IS-IS/EIGRP, IPv4/IPv6, VPN/IPSec, системный траблшутинг. Практикум на ресурсах Yandex Cloud. Вступительный тест.
Инфраструктура высоконагруженных систем — кластеризация и оркестрация (Proxmox/KVM/oVirt/Xen, Pacemaker, k8s/Nomad), дисковые кластеры (Ceph/Gluster/Linstore), веб-стек на базе Nginx. Вступительный тест.
Observability: мониторинг, логирование, трейсинг — Prometheus, Alertmanager, Grafana, Thanos/VictoriaMetrics, ELK/EFK, Kafka, Loki, Tempo/Jaeger, Vector, Logstash. Вступительный тест.
Начните со входного теста (ссылки выше) — он поможет оценить уровень и понять, подойдёт ли вам курс.