Linux下出口过滤的性能基准分析

2020-09-13 00:23:32

流量过滤是用于保护网络和主机免受恶意活动攻击的常用技术。可以在传入和传出流中进行过滤,也就是更广为人知的入口和出口过滤。入口过滤有很多用例:减轻DDoS攻击、避免垃圾邮件、阻止访问特定地理区域的服务等。

出口过滤的一个常见应用是阻止应用程序和用户访问远程主机。这类过滤器通常基于较大的IP地址范围拒绝列表,如信誉阻止列表。用于执行此任务的技术必须能够处理大量受限主机,在某些情况下可达数百万台。

我们在SAP的朋友要求我们对这项任务可用的常见Linux技术进行基准测试。这篇博客文章介绍了对一些Linux过滤技术进行基准测试的方法和结果:eBPF、IP集和iptables。

评估Linux网络堆栈中的不同机制,以过滤出大量IP地址,并在要阻止的IP范围达到100万时评估其可扩展性。

我们的目标是了解在大范围的IP范围内根据目的IP执行出站过滤的成本。

我们考虑的场景由通过IP网络通信的客户端和服务器计算机组成。出站过滤在客户端机器上执行,在服务器端不执行过滤。

出口过滤器必须执行匹配操作,以了解数据包是否可以继续传输或必须丢弃。如果阻止的IP数量很多,则此操作的成本可能会很高。这种过滤器会影响IP连接的吞吐量,它会消耗额外的CPU并增加延迟。我们决定在以下情况下考虑这些指标。

Linux内核提供了不同的机制来过滤网络数据包。在这个基准测试中,我们考虑了最知名的测试。Iptables是Linux中历史悠久且可能更为人所知的过滤实用程序。IP集支持特定类型集的高速匹配,例如IP范围。EBPF非常灵活和可定制。以下部分详细介绍了我们如何在基准测试中使用这些机制。

EBPF是构建在Linux内核中的虚拟机。在网络范围内,它允许用户加载每次数据包到达或从网络接口发出时执行的程序。这些eBPF程序可以修改、重定向或丢弃分组。

有不同类型的联网eBPF程序,用于按组过滤的cgroup_skb,用于流量控制级别过滤的SCHED_CLS和SCHED_ACT,以及用于网络接口级别过滤的XDP。

在我们的场景中,我们希望应用相同的过滤,而不考虑生成流量的应用程序的cgroup。来自非本地应用程序的流量或转发的流量也应该被过滤。这使得套接字级别的过滤不适合我们的场景。XDP程序只能连接到入口1路径,在我们的测试中也不考虑它。通过剔除过程,我们只考虑附加到交通控制层的eBPF程序。

考虑到我们希望使测试尽可能简单,我们决定在eBPF中实现我们自己的过滤程序,该程序将使用clsact qdisk附加到流量控制层。Clsact qdisk是在Linux4.5中引入的,它是一个伪qdisk,允许使用直接操作模式在入口和出口附加eBPF程序。

我们的过滤器实现使用LPM映射来保存要阻止的IP地址列表。EBPF使用数字树实现LPM算法。

我们通过向筛选表中的输出链添加丢弃规则来测试标准iptables实现。我们使用的规则具有以下格式。

在实现此筛选器时,我们发现单独附加每个规则太慢,必须使用iptables-restore实用程序来完成。

我们还使用IP SET框架进行了测试。我们使用了hash:net类型的IP集,并使用以下规则将其链接到iptables:

-a output-o eth0-m set--Match-set myset dst-m Comment--Comment Benchmark-j Drop。

要检查的IP地址被散列,并且散列的结果被用作散列表中的索引。哈希表的每个桶都包含该哈希的网络数组。当对照类型为“hash:net”的IP集检查IP地址时,不知道匹配的网络大小。IP集实施会对所有可能的网络大小的IP地址进行散列。例如,如果要匹配的IP是1.2.3.4,则会查找1.2.3.4/32,然后查找1.2.3.4/31,依此类推,直到测试完所有32种可能的网络大小。

我们使用iPerform3来测量TCP、UDP和CPU使用率的性能。使用标准ping实用程序测量延迟。我们使用10、100、1k、10k、100k和1M规则进行测试,每次测试运行5次,以获得统计稳健性。

我们使用两台在Packet上运行的裸机服务器作为客户端和服务器机器。此类服务器的规格为:

吞吐量图显示测试正在达到10Gbps的线速,因为我们使用的是单个连接,因此在大多数情况下只使用绑定上的一个接口。只有规则超过10k的iptables才无法处理该性能。从这项测试中我们可以得出结论,iptables在100k之后不能很好地扩展,但不幸的是,我们无法从其他测试中得出任何结论,因为网络性能是瓶颈。

此测试的目标是使CPU饱和。我们使用UDP数据包和-b 10G参数尝试达到线速。该图显示没有一种情况达到10Gbps的线速,这对此测试很有好处,因为这意味着网络不再是瓶颈。

我们可以看到,iptables不能扩展到超过1k个规则,并且IP集比具有大量规则的clsact qdisk上的eBPF要快一点。

我们使用目标带宽为1Gbps(-b1G)的iperf来避免CPU饱和,并能够比较不同过滤器的CPU使用情况。我们采用iperf报告的CPU使用率,其中包括主机上运行的所有应用程序。从上图中我们可以看到,当使用超过1k的规则时,iptables的CPU使用率会增加。EBPF和IP集过滤器的CPU使用率几乎保持不变,在eBPF情况下显示了更高的使用率。

我们使用标准的Linux ping实用程序通过-i 0.001和-c1000参数测量延迟,以1毫秒的间隔发送1000个ping。上图显示,延迟不受eBPF和IP集过滤器中规则数量的影响,在这些情况下约为27µs。另一方面,对于iptables情况,它随着规则数量的增加而线性增加。

此测试旨在测量在系统上安装过滤规则所需的时间。它没有考虑加载eBPF程序、创建IP集等固定时间,而是专注于测量更新规则本身所需的时间,以及它如何随规则数量变化。

上图显示,在所有筛选器中设置规则所需的时间随着规则数量的增加而线性增加。

在100.000个IP的情况下,设置时间仍然不到1秒。对于100万个IP,设置时间不到10秒。这表明在实践中,所有3种方法都有一个可接受的设置时间。

设置时间在很大程度上取决于基准实现,而不是过滤方法固有的。如果认为有必要,可以在基准代码中优化每种过滤方法。

对于bpf过滤器:基准测试为每个IP执行一次bpf()系统调用。从Linux5.6开始,可以使用单个bpf()调用在eBPF映射中执行多个更新。

对于iptables和IP集筛选器,基准测试执行外部命令,并通过管道单独馈送每个IP范围。我们可以通过直接使用Netlink协议将列表传递给内核来避免这种情况。有一些尝试使用vishvananda/NetLink库在Golang中实现IP集支持。

因此,读者在比较一个滤波器与另一个滤波器的设置时间时应谨慎。这不应该被解释为一种方法比另一种方法更好,而是为了显示预期的设置性能,并且设置时间对于任何过滤方法都不太可能是问题。

无论eBPF和IP集过滤器的规则数量如何,测试一和测试二中的吞吐量几乎相同。我们想知道这种行为是否会随着更高的数字规则(1000万和1亿)而改变。

我们发现,在eBPF中运行100M条目的测试是不可能的,因为LPM映射需要超过4 GB的内存,而且Linux内核2不允许这样做。我们决定使用10M和50M规则进行测试。

从上面的图表可以看出,吞吐量没有太大差异,同样的趋势在10M和50M规则下保持不变。

如果您想要更多地了解每个实现并阅读相关的源代码,一个不错的入门方法是分析CPU使用情况,找出哪些函数占用的CPU时间最多。

在UDP测试中,CPU饱和,无法达到10Gbps的线速。我们可以使用密件抄送配置文件工具来获取在测试中为每种过滤方法执行的函数列表。

我们必须使用-K参数运行该工具以从内核获取跟踪。

调用的顺序是从下到上,我们可以看到nf_hooklow函数是如何以迭代的方式调用ipt_do_table的。它是一个简单的“for”循环,迭代每个规则,解释性能随规则数量线性下降的原因。

在本例中,第一个重要的调用是tcf_classfy,然后在_bpf_prog_run中运行bpf程序,并在trie_lookup_elem和length_prefix_match中执行LPM中的查找。它正在走弯路。这解释了更好的性能,因为使用IPv4地址遍历Trie最多需要32步。

对于IP集的情况,我们可以看到堆栈的第一部分如何与iptables相同,nf_hook_low->;iptable_filter_hook->;ipt_do_table。然后是set_match_v4,并且在其执行特定的IP设置逻辑:ip_set_test、hash_net4_kadt和hash_net4_test之后,查找给定IP的所有可能的网络大小。

吞吐量、CPU使用率和延迟测试表明,即使在100万个IP的情况下,IP集和eBPF过滤器的伸缩性也很好。另一方面,iptables的伸缩性不是很好,即使规则数量很少,如1k或10k,也会显示出与其他筛选器相当大的差异。这是预期的,因为iptables在规则列表上执行线性搜索。

IP set在clsact qdisk上使用LPM映射略优于eBPF,它在UDP测试中的吞吐量略高,并且使用的CPU更少。eBPF由于Linux内核中的限制,无法处理超过89M个条目。

我们建议,决定使用哪种工具不应严格基于此基准测试的结果,而应考虑其他方面,例如该技术是否已在主机上使用、某些技术是否可以提供其他所需的功能(现在或将来)、向监控工具公开流量统计数据的方式等。

请记住,该基准测试只测试一个特定的用例:过滤出口上的IP地址范围的大型拒绝列表。更真实的集群应该有其他过滤器,包括入口处的过滤器。

有一些建议要添加对出口路径的支持,但是在撰写本文时它们还没有合并。↩︎。

在我们的例子中,LPM映射上的每个元素需要48个字节(来自sizeof(Struct LPM_Trie_Node)的40个字节+来自attr->;value_size的4个字节和来自trie->;data_size的4个字节)。这意味着地图中可能的元素的最大数量是(2^32/48)~89M。↩︎