Netflix 如何精确地归属 eBPF Flow Logs
[中文正文内容]
在之前的博客文章中,我们描述了 Netflix 如何使用 eBPF 大规模捕获 TCP flow logs,以增强网络洞察力。在这篇文章中,我们将深入探讨 Netflix 如何解决一个核心问题:如何准确地将 flow IP 地址归属到工作负载身份。
简要回顾
FlowExporter 是一个伴随所有 Netflix 工作负载运行的 sidecar。它使用 eBPF 和 TCP tracepoints 监控 TCP socket 状态的变化。当一个 TCP socket 关闭时,FlowExporter 会生成一个 flow log 记录,其中包含 IP 地址、端口、时间戳和额外的 socket 统计信息。平均而言,每秒会产生 500 万条记录。
在云环境中,IP 地址会随着工作负载实例的创建和终止而被重新分配给不同的工作负载,因此仅靠 IP 地址无法提供关于哪些工作负载正在通信的洞察。为了使 flow logs 有用,每个 IP 地址都必须归属到其对应的工作负载身份。FlowCollector 是一种后端服务,它从整个集群中的 FlowExporter 实例收集 flow logs,归属 IP 地址,并将这些归属后的 flows 发送到 Netflix 的 Data Mesh,以进行后续的流式和批量处理。
无论各个工作负载使用哪种编程语言、RPC 机制或应用层协议,eBPF flow logs 都能全面地展示 Netflix 广泛的微服务集群的服务拓扑和网络健康状况。
错误归属的问题
自 eBPF flow logs 引入以来,准确地将 flow IP 地址归属到工作负载身份一直是一个巨大的挑战。
正如我们在之前的博客文章中提到的,我们最初的归属方法依赖于 Sonar,这是一种内部 IP 地址跟踪服务,每当 Netflix 的 AWS VPC 中的一个 IP 地址被分配或取消分配给一个工作负载时,它就会发出一个事件。FlowCollector 消费来自 Sonar 的 IP 地址变更事件流,并使用这些信息来实时地归属 flow IP 地址。
这种方法的根本缺陷是它可能导致错误归属。在分布式系统中,延迟和故障是不可避免的,这可能会延迟 IP 地址变更事件到达 FlowCollector。例如,一个 IP 地址可能最初被分配给工作负载 X,但后来被重新分配给工作负载 Y。然而,如果这个重新分配的变更事件被延迟,FlowCollector 将继续假设该 IP 地址属于工作负载 X,从而导致错误归属的 flows。此外,事件时间戳可能会因其捕获方式而变得不准确。
错误归属使得 flow 数据对于决策制定来说变得不可靠。用户经常依赖 flow logs 来验证工作负载依赖关系,但是错误归属会造成混乱。如果没有关于预期依赖关系的专业知识,用户将很难识别或确认错误归属。此外,由于频繁的 IP 地址变更,对于具有大量足迹的关键服务,错误归属的发生频率更高。总而言之,错误归属使得全集群的依赖关系分析变得不切实际。
作为一种变通方法,我们让 FlowCollector 在归属之前将接收到的 flows 保持 15 分钟,以便为延迟的 IP 地址变更事件留出时间。虽然这种方法减少了错误归属,但并没有消除它。此外,等待期使得数据的新鲜度降低,从而降低了其用于实时分析的效用。
完全消除错误归属至关重要,因为仅仅一个错误归属的 flow 就会产生不正确的工作负载依赖关系。解决这个问题需要对我们的方法进行彻底的反思。在过去的一年中,Netflix 开发了一种新的归属方法,该方法最终消除了错误归属,详细信息将在本文的其余部分中介绍。
归属本地 IP 地址
每个 socket 都有两个 IP 地址:一个本地 IP 地址和一个远程 IP 地址。以前,我们使用相同的方法来归属两者。但是,归属本地 IP 地址应该是一项更简单的任务,因为本地 IP 地址属于 FlowExporter 捕获 socket 的实例。因此,FlowExporter 应该从其环境中确定本地工作负载身份,并在将 flow 发送到 FlowCollector 之前归属本地 IP 地址。
对于直接在 EC2 实例上运行的工作负载来说,这很简单,因为 Netflix 的 Metatron 在启动时会将工作负载身份证书 provision 给每个 EC2 实例。FlowExporter 可以简单地从本地磁盘读取这些证书来确定本地工作负载身份。
对于在 Netflix 的容器平台 Titus 上运行的容器工作负载,归属本地 IP 地址更具挑战性。FlowExporter 在容器主机级别运行,每个主机管理多个具有不同身份的容器工作负载。当 FlowExporter 的 eBPF 程序从内核中的 TCP tracepoints 接收到 socket 事件时,该 socket 可能由其中一个容器工作负载或主机本身创建。因此,FlowExporter 必须确定将 socket 的本地 IP 地址归属到哪个工作负载。为了解决这个问题,我们利用了 IPMan,Netflix 的容器 IP 地址分配服务。IPManAgent 是一个在每个容器主机上运行的守护程序,负责分配和取消分配 IP 地址。当容器工作负载启动时,IPManAgent 会将 IP 地址到工作负载 ID 的映射写入一个 eBPF map,然后 FlowExporter 的 eBPF 程序可以使用该 map 查找与 socket 本地 IP 地址关联的工作负载 ID。
另一个挑战是适应 Netflix 在 Titus 上的 IPv6 to IPv4 转换机制。为了促进 IPv6 迁移,Netflix 开发了一种机制,使仅支持 IPv6 的容器能够与 IPv4 目标通信,而不会产生 NAT64 开销。该机制会拦截 connect syscalls,并将底层 socket 替换为使用分配给容器主机的共享 IPv4 地址的 socket。这会使 FlowExporter 感到困惑,因为内核会为不同容器工作负载创建的 sockets 报告相同的本地 IPv4 地址。为了消除歧义,还需要本地端口信息。我们修改了 Titus,以便每当 connect syscall 被拦截时,将(本地 IPv4 地址,本地端口)到工作负载 ID 的映射写入一个 eBPF map。然后,FlowExporter 的 eBPF 程序使用该 map 来正确地归属由转换机制创建的 sockets。
解决了这些问题,我们现在可以准确地归属每个 flow 的本地 IP 地址。
归属远程 IP 地址
一旦解决了本地 IP 地址归属问题,准确地归属远程 IP 地址就变得可行了。现在,FlowExporter 报告的每个 flow 都包含本地 IP 地址、本地工作负载身份以及连接开始/结束时间戳。当 FlowCollector 接收到这些 flows 时,它可以了解到每个工作负载拥有给定 IP 地址的时间范围。例如,如果 FlowCollector 看到一个本地 IP 地址为 10.0.0.1 的 flow 与工作负载 X 相关联,该 flow 在 t1 时刻开始,在 t2 时刻结束,那么它可以推断出 10.0.0.1 在 t1 到 t2 时刻属于工作负载 X。由于 Netflix 在其整个集群中使用 Amazon Time Sync,因此时间戳(由 FlowExporter 捕获)是可靠的。
FlowCollector 服务集群由许多节点组成。每个节点都必须能够归属任意远程 IP 地址,因此需要了解所有工作负载 IP 地址及其最近的所有权记录。为了表示这些知识,每个节点都维护一个内存中的 hashmap,该 hashmap 将 IP 地址映射到时间范围列表,如下图 Go structs 所示:
type IPAddressTracker struct { ipToTimeRanges map[netip.Addr]timeRanges}type timeRanges []timeRangetype timeRange struct { workloadID string start time.Time end time.Time}
为了填充 hashmap,FlowCollector 从接收到的每个 flow 中提取本地 IP 地址、本地工作负载身份、开始时间和结束时间,并在 map 中创建/扩展相应的时间范围。每个 IP 地址的时间范围按升序排序,并且它们是不重叠的,因为一个 IP 地址不能同时属于两个不同的工作负载。
由于每个 flow 仅发送到一个 FlowCollector 节点,因此每个节点都必须与其他节点共享其从接收到的 flows 中获知的时间范围。我们使用 Kafka 实施了一个广播机制,其中每个节点将获知的时间范围发布到所有其他节点。虽然存在更有效的广播实现,但基于 Kafka 的方法很简单,并且对我们来说效果很好。
现在,FlowCollector 可以通过在填充的 map 中查找远程 IP 地址来归属它们,该 map 返回一个时间范围列表。然后,它使用 flow 的开始时间戳来确定相应的时间范围和关联的工作负载身份。如果开始时间未落在任何时间范围内,FlowCollector 将在延迟后重试,如果重试失败,最终将放弃。当 flows 丢失或广播消息延迟时,可能会发生此类故障。对于我们的用例来说,留下少量未归属的 flows 是可以接受的,但是任何错误归属都是不可接受的。
这种新方法通过连续的心跳来实现准确的归属,每个心跳都与可靠的 IP 地址所有权时间范围相关联。它可以优雅地处理瞬时问题 - 几个延迟或丢失的心跳不会导致错误归属。相比之下,以前的方法仅依赖于离散的 IP 地址分配和取消分配事件。由于缺少心跳,它必须假定 IP 地址保持分配状态,直到另行通知(这可能是几个小时或几天后),这使得它在通知延迟时容易受到错误归属的影响。
一个细节是,当 FlowCollector 接收到 flow 时,它无法立即归属其远程 IP 地址,因为它需要远程 IP 地址的最新观察到的时间范围。由于 FlowExporter 每分钟批量报告 flows,因此 FlowCollector 必须等到它从远程工作负载 FlowExporter 接收到最后一分钟的 flow 批次,这可能尚未到达。为了解决这个问题,FlowCollector 在归属远程 IP 地址之前,会将接收到的 flows 临时存储在磁盘上一分钟。这引入了 1 分钟的延迟,但这比以前方法中的 15 分钟延迟要短得多。
除了产生准确的归属之外,由于其简单性和内存查找,新方法也是经济高效的。由于内存中的状态可以在 FlowCollector 节点启动时快速重建,因此不需要持久性存储。使用 30 个 c7i.2xlarge 实例,我们可以在整个 Netflix 集群中每秒处理 500 万个 flows。
归属跨区域 IP 地址
为了简单起见,到目前为止,我们忽略了一个主题:区域化。Netflix 的云微服务跨多个 AWS 区域运行。为了优化 flow 报告并最大限度地减少跨区域流量,每个主要区域都运行一个 FlowCollector 集群,并且 FlowExporter 代理将其 flows 发送到其对应的区域 FlowCollector。当 FlowCollector 接收到 flow 时,保证其本地 IP 地址位于该区域内。
为了最大限度地减少跨区域流量,广播机制仅限于同一区域内的 FlowCollector 节点。因此,IP 地址时间范围 map 仅包含来自该区域的 IP 地址。但是,跨区域 flows 具有不同区域中的远程 IP 地址。为了归属这些 flows,接收 FlowCollector 节点会将它们转发到相应区域中的节点。FlowCollector 通过查找从所有 Netflix VPC CIDR 构建的 trie 来确定远程 IP 地址的区域。这种方法比跨所有区域广播 IP 地址时间范围更新更有效,因为只有 1% 的 Netflix flows 是跨区域的。
归属非工作负载 IP 地址
到目前为止,FlowCollector 可以准确地归属属于 Netflix 云工作负载的 IP 地址。但是,并非所有 flow IP 地址都属于此类别。例如,很大一部分 flows 通过 AWS ELB。对于这些 flows,它们的远程 IP 地址与 ELB 相关联,我们无法在这些 ELB 上运行 FlowExporter。因此,FlowCollector 无法通过简单地观察接收到的 flows 来确定它们的身份。为了归属这些远程 IP 地址,我们继续使用来自 Sonar 的 IP 地址变更事件,Sonar 会抓取 AWS 资源以检测 IP 地址分配的变化。虽然此数据流可能包含不准确的时间戳并被延迟,但错误归属并不是主要问题,因为 ELB IP 地址重新分配的发生频率非常低。
验证正确性
由于缺少用于验证 flow logs 的工作负载依赖关系的明确事实来源,因此验证新方法是否消除了错误归属具有挑战性;毕竟,flow logs 本身旨在充当此事实来源。为了建立信心,我们分析了一个具有良好理解的依赖关系的大型服务的 flow logs。有大量的足迹是必要的,因为错误归属在具有大量实例的服务中更为普遍,并且必须有一种可靠的方法来确定此服务的依赖关系,而无需依赖 flow logs。
Netflix 的云网关 Zuul 完美地实现了这一目的,因为它具有广泛的足迹(处理所有云入口流量)、大量的下游依赖关系,并且我们能够从其路由配置中推断其依赖关系,作为与 flow logs 比较的事实来源。我们发现 Zuul 的 flows 在两周的窗口期内没有错误归属。这提供了强烈的信心,即新的归属方法消除了错误归属。在以前的方法中,flow logs 报告的 Zuul 大约 40% 的依赖关系被错误归属。
结论
随着错误归属的解决,eBPF flow logs 现在可以提供可靠的、全集群的洞察力,帮助我们了解 Netflix 的服务拓扑和网络健康状况。这一进步在服务依赖性审计、安全分析和事件分类等领域释放了许多令人兴奋的机会,同时帮助 Netflix 工程师更好地了解我们不断发展的分布式系统。
鸣谢
我们要感谢 Martin Dubcovsky, Joanne Koong, Taras Roshko, Nabil Schear, Jacob Meyers, Parsha Pourkhomami, Hechao Li, Donavan Fritz, Rob Gulewich, Amanda Li, John Salem, Hariharan Ananthakrishnan, Keerti Lakshminarayan, 以及其他杰出的同事,感谢他们的反馈、灵感和对这项工作的成功所做的贡献。