F-Stack ff_rss_check()优化介绍

1. 概述

本文档旨在介绍 F-Stack 中对 ff_rss_check() 函数的一项重要优化。该优化通过引入一个预计算的静态端口查找表,显著提升了应用程序作为客户端主动发起大量短连接时的性能,解决了原 ff_rss_check() 函数在高并发场景下可能成为性能瓶颈的问题。

核心优化提交:e54aa4317b5d81f9f8643e491d8ec0ec1e72282a

2. 背景与问题分析

ff_rss_check() 函数在 F-Stack 中负责一个关键任务:当应用程序作为客户端发起新的 TCP 连接时,为其分配合适的本地源端口。

  • 原有实现机制: 每次需要建立新连接时,都会动态调用 ff_rss_check()。该函数会根据目标服务的 IP 和端口(4元组信息),通过一个哈希计算来选择一个 RSS(Receive Side Scaling)友好的本地端口,以确保数据包能被高效地分发到正确的 CPU 核心上处理。
  • 性能瓶颈: 在需要频繁创建短连接(例如,处理 HTTP 请求且未开启 keep-alive)的场景下,每次连接建立都需要执行一次或多次端口选择和冲突检查。这个过程涉及多次的toeplitz_hash计算(平均每次耗时约300+tsc,平均次数至少为该端口的总队列(进程)数),在高并发压力下,会消耗可观的 CPU 资源,并成为限制连接建立速率的瓶颈。

3. 优化方案:静态 RSS 端口表

为了克服上述性能问题,优化方案的核心思想是:变“动态计算”为“静态查找”

  1. 预初始化静态表
    • 在应用程序启动阶段(ff_init() 时),根据配置文件中的预定义规则,预先计算并初始化一个静态的端口查找表(ff_rss_tbl),。
    • 该表包含了预先调用ff_rss_check()计算好的哪些本地端口发出去的包可以返回本进程对应的网卡队列,表中包含 {{远程地址,远程端口}:{本地地址, [所有可用本地端口]}}组合,以及一些辅助数据结构,如可用端口的起始、结束索引,上次选择的到的端口索引等。
  2. 高效的端口选择
    • 当需要建立新连接时,首先尝试从这个预先生成的静态表中进行查找。
    • 如果能找到一个与当前连接目标地址/端口匹配且未被占用的本地端口,则直接使用它。这个过程几乎是无锁且开销极低的(平均耗时约100-250tsc)
    • 最重要的是,静态查找表无需多次选择并计算RSS,只需要查找一次本地端口即可保证远程回包可以回到本进程的队列进行处理,极大提升了选择源端口的效率,进而提升总体的QPS性能。
  3. 优雅降级
    • 如果静态表中所有符合条件的端口都已被占用或该四元组未配置静态查找表,系统会自动降级到原有的 ff_rss_check() 动态计算流程,确保功能的正确性。

4. 配置说明

此功能需要通过配置文件(例如 config.ini)手动开启和定义。相关配置段如下:

# 启用或禁用静态 ff_rss_check 表。
# 若启用,F-Stack 将在 APP 启动时初始化该表。
# 此后当 APP 作为客户端连接服务器时,会首先尝试从该表中选择本地端口。
[ff_rss_check_tbl]
# 启用开关:0-禁用,1-启用。默认为 0。
enable = 1
# 定义端口分配规则(4元组)。
# 格式:<网卡端口ID> <本地地址 daddr> <远程地址 saddr> <远程端口 sport>
# 单个四元组内用空格分隔,多个四元组之间用分号分隔。
# 注意:saddr/sport 二元组最多支持 16 个,同一 saddr/sport 下最多支持 4 个 daddr。
# 因此,最多支持 64 种组合,超出的配置将被忽略。
rss_tbl=0 192.168.1.10 192.168.2.10 80;0 192.168.1.10 192.168.2.11 80;0 192.168.1.10 192.168.2.12 80

配置参数详解

  • enable: 总开关,必须设置为 1 才能启用此优化功能。
  • rss_tbl: 定义端口分配规则。每一条规则指定了网卡端口ID用于获取网卡的队列配置,对于特定的目标服务(saddr + sport),使用哪个预先分配好的本地地址(daddr)。

5. 性能提升

根据内部测试数据,该优化带来了显著的性能提升:

  • 测试场景: 客户端(wrk)向F-Stack Nginx发起http请求,使用长连接。F-Stack Nginx设置反向代理,作为客户端,频繁向多个远程服务器发起短 TCP 连接(未使用 keep-alive)。示例图如下所示:
  • 测试数据:如下表所示
  • 常规 QPS 提升约 2-6%左右,且提升的比列会随着进程数的增加而增加,因为进程数越多,原有的ff_rss_check()需要计算的平均次数就越多。
  • 某些特殊场景下提升可以达到35%以上,主要是因为在某些进程数的配置下,原始动态计算方式,随机选择源端口的次数会远超数学期望的总进程数+少量次数,导致需要大量额外调用ff_rss_check()进行计算,消耗了大量CPU资源。
    • 【注意】不同的upstream服务器配置(数量、IP、端口等)可能会导致不同的进程数存在类似问题,在本测试场景下配置8或16进程时,会出现该问题。导致该问题的具体原因则尚未完全明确,查看相关静态表、端口是否正在被占用和请求测试都正常。
    • 如下表所示分别为8和12进程Nginx调用ff_rss_check()次数的统计,可以看到8核时的随机选择次数明显超出了数学期望的(应该在8-12次左右),16核同理。
      • 其中randomtime表示内核参数net.inet.ip.portrange.randomtime的设置,因为F-Stack框架的特性,作为客户端选择源端口时此时完全随机选择效果更好,且F-Stack对随机性要求并不高,早前已经替换了性能好好很多的伪随机函数。
  • perf top截图

8进程间歇性随机(大部分不随机)时ff_rss_check()调用次数太多,热点很高

8进程完全随机,ff_rss_check()调用次数热点有一定降低

12进程,ff_rss_check()的热点则大幅下降

  • 8进程间歇随机和完全随机选择源端口QPS性能对比

6. 其他参数优化调整

F-Stack对应调整了部分参数,具体如下,其他业务如何配置可以根据自己业务的具体特点灵活调整

[freebsd.sysctl]
net.inet.tcp.delayed_ack=1 # 可以提升大并发的吞吐量
net.inet.tcp.fast_finwait2_recycle=1
net.inet.tcp.finwait2_timeout=5000
net.inet.tcp.maxtcptw=128 # 尽快释放TIME_WAIT状态的端口,增加空闲端口,减少in_pcblookup_local()的调用次数,进而减少ff_rss_check()的调用次数
net.inet.ip.portrange.randomized=1
# Always do random while connect to remote server.
# In some scenarios of F-Stack application, the performance can be improved to a certain extent, ablout 5%.
net.inet.ip.portrange.randomtime=0 # 对某些特定配置条件下原动态计算ff_rss_check()方式有一定性能提升

7. 总结

本次对 ff_rss_check() 的优化是 F-Stack 追求性优化能的一个典型例子。它通过以下方式解决了核心瓶颈:

  1. 空间换时间: 使用预计算的静态表换取运行时动态计算的开销。
  2. 减少调用次数: 极大降低了端口选择时的ff_rss_check()和in_pcblookup_local()的重试调用次数,实现总体系统2-6%,特殊极限场景35%以上的总体系统性能提升。
  3. 保证兼容: 通过降级机制保障了在任何情况下的功能正确性。

建议启用此功能的场景: 所有使用 F-Stack 作为客户端、需要高频率创建新连接(特别是短连接)的应用程序。启用后,只需在配置文件中预先定义好常用的目标服务地址,即可获得显著的性能收益。

【AI使用声明】本文初始版本使用DeepSeek-V3.1生成,然后进行人工调整;示意图片由元宝根据人工提供的提示词生成和修改

递归DNS如何选择权威DNS的IP-SRTT优化及实践

本文主要介绍递归DNS的SRTT本身机制及如何进行更进一步的优化,来达到更好的递归效果。都是字,不想弄图、表和数字,格式也有点乱,不想整理了,凑合看吧

1. SRTT本身机制

本节仅对SRTT进行简单的介绍供了解,不进行特别深入和详细的分析。

SRTT(Smoothed Round-Trip Time,平滑往返时延)是在递归DNS系统里,用加权平均算法计算出的上游DNS服务器的“响应时延”估值,它用于优化解析性能和选择最快的上游服务器。

延伸阅读

BIND的SRTT机制总体做的还是相对比较好的,可以参考 https://developer.aliyun.com/article/622410。

而Unbound社区版的SRTT做的则不够好,基本就是可用权威IP完全随机,递归的平均延迟非常高,所以在实际业务使用前必须进行优化,具体算法优化可以参考相关论文或BIND的实现等。

虽说递归DNS选择权威服务器时都希望选择延迟更低,但在最大限度降低递归时延的前提下,也要遵循一定的规则,如

  • 需要有一定的随机性选择非延迟最低的权威IP来自适应权威IP的RTT的波动和服务器的突然故障。
    • 如某个权威IP正常RTT很低,但偶发丢包被惩罚延迟后,需要能够逐步平滑降低其RTT。
    • 如果低RTT的权威IP突然故障,除了丢包惩罚延迟外,还需要随机性来选择其他权威DNS IP,防止单个IP不可用导致整个域名解析异常。
  • 标记不可用的IP也需要定时发送DNS请求。
    • 不可用的IP恢复可用后需要使用该机制探活标记为恢复可用
    • BIND和UNBOUND都会在15或30分钟完全重置一次权威IP的相关状态,其中包括RTT,以及IP的EDNS支持状态等。
    • 也可以低频率的对惩罚到标记不可用的IP做并发异步的DNS探活请求。
  • 不能向单一权威IP发送过于集中的请求,降低被权威封禁递归IP的概率。
    • 一个可探讨的方案介绍:如果本次不进行随机选择递归IP,选择最低RTT的IP时候再进行二次选择,1/2直接选择最低RTT的权威服务器,1/2在最低RTT差距一定范围内(如minrtt + 20ms,也认为有比较低的延迟)的其他权威服务器IP中随机选择。
    • 国内的各大权威DNS服务商的服务只要不是真实的攻击,常规的SRTT优化很是很难触发递归IP被封禁的场景的,但是一些单位、机构等自建的权威DNS,如果单个IP请求量较大则比较容易触发,所以这里也是递归DNS必须要考虑的一种场景。

2. SRTT的配套机制优化

只有SRTT本身的选择机制对一个递归DNS是不够的,还必须配合多个其他配套系统来使用,而这些配套系统对递归DNS的整体递归时延也是非常重要的。

2.1 某个权威DNS IP的SRTT生效范围

在实际业务中,存在以下两种场景会使多个域名使用相同的权威DNS IP

  • 不同的域名会使用相同的NameServer(NS)
  • 不同的域名使用的NS不同,但NS对应的IP相同(如腾讯云解析/DNSPod提供的个性化NS功能)

在我们的常规认知中,既然权威DNS IP相同,那么统一进行SRTT的探测和选择即可,这样多个域名的平均递归延迟都可以有效降低,如在每次权威IP状态重置后快速重新探测到RTT最低的权威DNS IP,且对所有域名生效。

但现实是相反的,多数递归DNS对这种场景的权威DNS IP探测是域名相关的,不同域名的权威DNS IP即使相同,也是分别独立探测和存储状态的(如Unbound的)。

对于某些特殊场景,这种做法可能有一定用处,如同一个IP对域名A支持EDNS,对域名B不支持等,但这极大增加了递归DNS解析时延(也浪费了一些内存,虽然现代服务器不怎么在意了)。

目前腾讯云/DNSPod的递归服务器的做法是所有域名只要权威DNS IP相同即共享一份SRTT的探测和选择结果,可以大幅提高选择RTT低的权威DNS概率(尤其是请求量不大的域名),降低解析延迟,不考虑一些特别极端的场景(也可以解析,但可能不是最优的结果,且此类极端场景在国内的权威DNS服务商中基本不存在)。

2.2 权威DNS IP的GEO优化

大家都知道普通域名的DNS解析可以做智能解析(分线路、GEO、基于延迟等),来给不同地域不同运营商的客户端分配不同的解析结果,达到降低延迟或节省成本的目的。那么对于给递归DNS使用的NameServer(NS)域名是否可以使用智能解析,来达到同样的目的呢?毕竟因为各种原因,国内大部分权威DNS服务商很难完全使用完全相同的Anycast IP在全球同时提供很高的高可用和很低的延迟,如果该方案可行,则权威DNS服务商有了一个变通的方式来同时满足高可用和低延迟的需求,如除了Anycast IP外还可以在不通地区提供一些本地的Unicast IP来降低本地递归DNS的访问延迟,比如目前腾讯解析/DNSPod和阿里云解析都做了该操作,那么效果如何呢。

答案是一半一半。

递归获取一个域名的权威DNS的NS IP有两个渠道,胶水记录和权威服务器的记录,其中NS域名在注册商/局设置胶水记录不支持智能解析,而在其本身权威DNS设置的解析记录则是可以支持设置智能解析线路的,下面分别进行介绍。

2.2.1 注册局的TLD服务器返回的glue record

  • 胶水记录(Glue Record)是一种特殊的DNS记录,用于解决域名解析过程中的循环依赖问题。当域名的权威DNS服务器(NS记录)是当前域名的子域名时,胶水记录是必须存在的(如后续示例所示),由域名所有者直接在注册商/局(父域DNS,如.com的TLD服务器)中提供子域名服务器的IP地址,确保递归DNS能够正确找到目标的权威DNS服务器。
dig @a.gtld-servers.net ns3.dnsv5.com

; <<>> DiG 9.11.26-RedHat-9.11.26-6.tl3 <<>> @a.gtld-servers.net ns3.dnsv5.com
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 45009
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 2, ADDITIONAL: 11
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;ns3.dnsv5.com.                 IN      A

;; AUTHORITY SECTION:
dnsv5.com.              172800  IN      NS      ns3.dnsv5.com.
dnsv5.com.              172800  IN      NS      ns4.dnsv5.net.

# 因为NS域名ns3.dnsv5.com是请求域名dnsv5.com的子域名,此时胶水几率是必须返回的,否则后续继续向TLD服务器请求ns3.dnsv5.com还是返回上面两条NS记录,造成死循环
# 只返回tld相同的ns3.dnsv5.com的胶水记录,而不返回ns4.dnsv5.net的胶水记录
;; ADDITIONAL SECTION:
ns3.dnsv5.com.          172800  IN      A       1.12.0.17
ns3.dnsv5.com.          172800  IN      A       1.12.0.18
ns3.dnsv5.com.          172800  IN      A       1.12.14.17
ns3.dnsv5.com.          172800  IN      A       1.12.14.18
ns3.dnsv5.com.          172800  IN      A       101.227.168.52
ns3.dnsv5.com.          172800  IN      A       108.136.87.44
ns3.dnsv5.com.          172800  IN      A       163.177.5.75
ns3.dnsv5.com.          172800  IN      A       220.196.136.52
ns3.dnsv5.com.          172800  IN      AAAA    2402:4e00:1470:2::f
ns3.dnsv5.com.          172800  IN      A       35.165.107.227

;; Query time: 217 msec
;; SERVER: 192.5.6.30#53(192.5.6.30)
;; WHEN: Fri Aug 15 15:59:20 CST 2025
;; MSG SIZE  rcvd: 255
  • TLD服务器一般只有请求的域名的TLD和本域名设置的NS的TLD一致,会返回胶水记录的(这里大部分其实是非必须的,但是同时返回了胶水记录可以减少一次额外的请求,降低延迟),否则一般不返回胶水记录。如example.com设置NS为ns1.example.com, ns2.example.net, 那么这时候向TLD服务器请求example.com的时候,TLD服务器就会返回example.com的NS记录ns1.example.com和ns2.example.net,且同时返回ns1.example.com的A和AAAA记录(TLD相同,都是.com),但并不会返回ns2.example.net的胶水记录(TLD不同, .com vs .net),如上述和下述请求所示
dig @a.gtld-servers.net dnsov.net

; <<>> DiG 9.11.26-RedHat-9.11.26-6.tl3 <<>> @a.gtld-servers.net dnsov.net
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 8363
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 2, ADDITIONAL: 11
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;dnsov.net.                     IN      A

;; AUTHORITY SECTION:
dnsov.net.              172800  IN      NS      ns3.dnsv5.com.
dnsov.net.              172800  IN      NS      ns4.dnsv5.net.

# 只返回tld相同的ns4.dnsv5.net的胶水记录,而不返回ns3.dnsv5.com的胶水记录,但其实这里的胶水记录并不是必须要返回的,因为dnsov.net和dnsv5.net是两个不同的域名,再次请求并不会造成死循环,但是同时返回了胶水记录可以减少一次额外的请求,降低延迟
;; ADDITIONAL SECTION:
ns4.dnsv5.net.          172800  IN      A       1.12.0.16
ns4.dnsv5.net.          172800  IN      A       1.12.0.19
ns4.dnsv5.net.          172800  IN      A       1.12.14.16
ns4.dnsv5.net.          172800  IN      A       1.12.14.19
ns4.dnsv5.net.          172800  IN      A       112.80.181.106
ns4.dnsv5.net.          172800  IN      A       117.135.128.152
ns4.dnsv5.net.          172800  IN      A       124.64.205.152
ns4.dnsv5.net.          172800  IN      A       13.37.58.163
ns4.dnsv5.net.          172800  IN      AAAA    2402:4e00:111:fff::8
ns4.dnsv5.net.          172800  IN      A       49.7.107.152

;; Query time: 192 msec
;; SERVER: 192.5.6.30#53(192.5.6.30)
;; WHEN: Fri Aug 15 16:00:30 CST 2025
;; MSG SIZE  rcvd: 261
  • 这里未介绍子域名授权场景的胶水记录,总体原则是一致的。

2.2.2 NS域名本身权威服务器返回的IP记录

NS域名(如ns3.dnsv5.com)在其权威DNS(如腾讯云解析/DNSPod)设置解析记录的时候,和普通域名可以一样设置智能解析(如境内设置一批IP,境外设置另一批IP等),和普通域名的使用没有区别。

2.2.3 递归DNS的行为及优化

递归DNS在进行迭代查询时,有可能从TLD服务器直接获取到NS域名的胶水记录,也可能从权威DNS获取,且两处获取的记录可能不同(部分检测系统会告警,但忽略即可),而权威DNS设置的IP支持智能解析,提供了更大的优化空间,一般相比胶水记录是更好的选择(且非专业技术人员常常忽视对胶水记录的维护),那递归DNS如何使用和优化呢。

常规的递归DNS(如BIND和UNBOUND)一般是这种行为(会略有不同,总体行为类似)

  • 如果向TLD服务器请求某个域名时,TLD服务器同时返回了NS的胶水记录,那么直接使用胶水记录作为NS的IP,不会再去向NS的权威请求记录
  • 如果向TLD服务器请求某个域名时,TLD服务器没有返回NS的胶水记录,则再去正常查询NS域名的记录时,可能回会权威请求NS记录,获取到权威可能设置的分线路智能解析结果,从而后续优先使用权威返回的解析记录
  • 有用户向递归DNS显示请求NS域名的A/AAAA记录,那么递归DNS必然会向NS域名的权威请求一次NS的A/AAAA记录(胶水记录不是其域名权威返回的,用户显式的请求去需要去获取权威设置的记录,而不能直接返回缓存的胶水记录),获取到权威可能设置的分线路智能解析结果,从而后续优先使用权威返回的解析记录
  • 如果权威返回的解析记录全部不可用,则重新回退到使用胶水记录提升可用性

可以看出来,常规的递归DNS不会主动去获取权威DNS的分线路智能解析结果,只会在被动获取到权威返回结果的时候,代替胶水记录进行优先使用,从腾讯云解析/DNSPod使用的实际效果看,权威解析才会设置的解析IP的流量比例总体并不低,这样设置还是有比较好的效果的(具体数据就不提供了)。

那对递归DNS能不能更进一步的进行优化呢,当然可以

  • 即使递归DNS从TLD服务器获取到了NS域名的胶水记录,在正常向胶水记录IP发起查询请求的同时,也自动异步去解析NS域名在权威的A/AAAA记录, 使用NS权威解析记录的比例可以大幅提高。
    • 根域名、TLD域名以及海外大厂的NS等域名不必做这个操作,因为记录一般都是一致的Anycast IP
    • 腾讯云/DNSPod所有对外提供服务的公共DNS、HTTPDNS、DoH、DoT、云内网DNS等的后端递归DNS都支持该项优化

F-Stack如何修改TCP连接的MSS


本文仅介绍F-Stack服务器作为被动接受连接的一方如何在不修改程序的前提下如何相对简单的修改TCP建连时的MSS选项

TCP数据包在经过某些网络设备(如负载均衡等)时可能会被增加某些头(如vlan、gre、ipip等),但是又没有正确的减小syn包中MSS选项的值,导致后续的数据包加上额外的头后可能超过1500的MTU而无法正常收发包,此时我们可能无法控制中间链路的网络设备,就需要修改应用服务器的MSS配置,但最好上还是应该由中间网络设备进行兼容。

F-Stack-1.21.5(FreeBSD-11.0)

FreeBSD-11.0的ipfw工具不支持直接修改tcp的mss选项的值,必须要同时借助netgraph(工具:ngctl)来实现,参考命令如下

# 创建 tcpmss 节点并将其连接到 ng_ipfw 节点
ff_ngctl mkpeer ipfw: tcpmss 100 msshook

# 设置mss和hook函数
# 该命令会报错ff_ngctl: recv incoming message: Operation not permitted,实际为回显结果时f-stack的ff_ngctl对freesbd的ngctl的一个兼容性问题,不影响设置效果,仅影响设置成功后的回显
ff_ngctl msg ipfw:100 config '{ inHook="msshook" outHook="msshook" maxMSS=1440 }'

# 将流量导入 tcpmss 节点
ff_ipfw add 300 netgraph 100 tcp from any to any tcpflags syn out via f-stack-0

# 让数据包在被修改后继续由 ipfw 处理, 可选,也可以配置在config.ini
ff_sysctl net.inet.ip.fw.one_pass=0


参考:ng_tcpmss man page

F-Stack-1.22+(FreeBSD-13.0)

FreeBSD-13.0的ipfw工具通过pmod模块引入的方式支持了直接修改tcp的mss选项值的功能,命令参考如下:

ff_ipfw add 1000 tcp-setmss 1440 tcp from any to any tcpflags syn out

【注意:】目前除了dev分支最新代码默认支持了ip_fw_pmod模块,其他分支和release版本需要先打上下面这个patch,在F-Stack的ipfw开启ip_fw_pmod模块功能:ff_ipfw support ip_fw_pmod and tcpmod for tcp-setmss.

F-Stack通用方式

因为F-Stack并未实际实现修改网卡MTU的功能,所以可以通过修改用户态FreeBSD协议栈中网卡设备MTU的操作,来达到减小mss选项的值(MTU – 40),但网卡实际的MTU还是1500,并没有减小,从而达到也可以正常收发超过设置的MTU(如1480)大小的包(如1500),但这里借助了F-Stack的feature(不是bug)来实现的,并不建议作为标准方案使用,参考命令如下

ff_ifconfig f-stack-0 mtu 1480

Linux协议栈

linux协议栈对标ipfw,使用iptables修改mss选项值的命令参考

iptables -t mangle -A OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1440

过去的2024

2025.5.24 更新

本来是5.21周三去进行哥哥的海葬,结果周三全天大雾取消了,改成了今天。早上六点集合,6点25左右发船,经过一个多小时后到达海葬海域,船不大,海浪有点大,大部分人吐的稀里哗啦。不过总算结束了一件事情,哥哥一路走好,活着的时候半生自由活动受限,现在至少可以算是自由的,可以顺着大海游遍全世界。

======================================================

频繁的头疼欲裂,需要靠大量的布洛芬和替扎尼定续命,如果有选择,早就不再继续卷了。

原有家庭本身最后一个人离去,多次住院,加部分截肢多坚持了一年半吧,临走前可能也是有点感应吧,做梦失眠头疼加剧。

小孩小学还是卷的厉害呀,目前看文理科都没有很好的天赋,也是顺其自然吧。

最后是工作的事情,24年最大的体验其实是一些老生常谈的东西,当业务增长不动的时候,各种流程就来了。

不能说流程没用,在合理的流程下,可以提高项目质量的下限,虽然对部分项目的质量上限没什么太大作用,但是在一个比较大的部门中确实不能否认它的作用。

但是流程本身是问题了提升质量服务的,而当前完全本末倒置了,已经达到了丧心病狂的程度,各种不合理和冗余的流程,完全是为了流程而流程,对质量提升的终极目标的效果非常有限。

而且严重拖后腿,花费特别大的精力,流程搞了一大堆,再一看实际产出,大部分直接呵呵了,虽然即使有产出也不一定对最后的产品收益就一定有多大帮助,但合理的产出总比不合理的流程合理多了。

看到的最大好处算是至少有了审计的基础。

其他吐槽的欲望都没有了,这就样吧,结束。

2024年某些”小众”递归DNS那些事儿

本文就不写运营商递归DNS和相对比较知名一些的公共DNS了,通过几个case聊聊2024年当前可以应答DNS请求、在国内用户量不小,但是又搜索不到什么公开信息的一些”小众“递归DNS的那些事儿,部分已经无法访问的递归DNS就不再包含进来了。这些递归DNS的用户体量并不比不少知名公共DNS小,只是基本不为人知,关注该领域的可以大概了解下。

使用这些DNS比较容易出现解析线路不准确(如国内用户解析到国外IP)、域名被劫持等场景,因为让所有用户修改DNS并不现实,一般还是更建议有端的客户端(如APP等)尽量使用HttpDNS或其他技术手段规避类似场景。

【注1】本文部分线索来自其他伙伴。

【注2】本文所有数据为简单测试,仅供参考。

case 1:183.2.x.x

该DNS疑似某供应商为园区、公司等场景提供的递归DNS,在本省的使用率不低。在24年5月的时候测试,使用该DNS会大概率将域名解析到国外的IP,会导致终端用户访问延迟升高,也造成该省份域名解析到国外IP的概率略微高于其他省份。但是在24年下半年测试没什么大的问题了,可以正常解析为本省份结果,只是解析结果没有区分运营商,基本不影响正常使用。

下图分别为24年5月和12月的测试递归DNS出口IP的结果对比

case 2:45.14.x.x、45.128.x.x

该组递归DNS分布于多个C段,多个海外其他国家,目前看国内各省份运营商皆有固网/wifi用户访问,设置比例大约在小万分之x,使用移动网络则不会访问该组递归DNS,疑似从2024.12.04 16:00+开始对外提供DNS解析服务。

该组递归DNS后端在进行递归请求时固定携带127.0.0.1作为ECS IP,大部分域名可以解析到默认线路,无省份运营商区分,解析到国外IP的概率倒是不高。

该组DNS貌似会随机劫持某些域名到其他IP,并302跳转到其他违规网站。历史测试截图如下。

注3】截止本文时间(2024.12.18),之前历史被劫持域名已经没有被劫持,且之前被劫持到的相关IP也已经无法访问,疑似已经被处理。

case 3:154.86.x.x

该组递归DNS分布于多个C段,部署于中国香港,目前看国内各省份运营商皆有固网/wifi用户访问,设置比例大约在小千分之x,使用移动网络则不会访问该组递归DNS,具体对外提供服务时间则不确定。

该组递归DNS后端在进行递归请求时会随机转发给国内外多个其他公共DNS,且并未携带ecs,大部分域名的解析结果IP为国外IP,会导致终端用户访问延迟升高,递归DNS后端出口IP测试结果如下所示,其中包括Google、UltraDNS等其他的公共DNS的递归出口IP

该组DNS因为是纯转发,所以部分特征会测试到转发目标公共DNS的特征,如NeuStar家的UltraDNS,但实际并非该集群本身的版本特征,也并非转发目标公共DNS部署的DNS缓存转发服务,部分特征测试结果如下所示:

dig version.bind chaos txt @154.86.x.x +short
“1733297354.ultradns”
dig hostname.bind chaos txt @154.86.x.x +short
“rcrsv2.jptyo1.ultradns”

dig version.bind chaos txt @154.86.x.x
version.bind. 86400 CH TXT “PowerDNS Recursor 5.1.1 (built Jul 22 2024 13:49:14 by root@localhost)”

dig version.bind chaos txt @154.86.x.x +short
version.bind. 75428 CH TXT “Q9-P-7.6”

该组DNS貌似会随机劫持某些过期域名到其他IP,并302跳转到其他网站(有点类似纠错页),当前(2024.12.18)跳转的目标网站实测无法访问,也未找到历史网页内容,且很多安全情报都会报告风险并进行访问拦截,没有什么实质性的影响。

F-Stack 对 HTTP/3 的支持使用说明

Nginx 主线在1.25.x 版本中已经加入了对 HTTP/3 的支持,F-Stack 在等待了两个小版本之后,也移植了 Nginx-1.25.2 版本到 F-Stack 上,目前可以支持HTTP/3的测试使用,本文介绍移植过程中的一些兼容性改造及使用注意事项。

主要兼容项

主要是 F-Stack 的一些接口兼容和 FreeBSD 不支持一些 Linux 的部分选项,而 Nginx 的自动配置检测的是 Linux 是否支持,需要进行一些修改。

  1. 对 F-Stack 的 ff_recvmsg 和 ff_sendmsg 接口进行修改,兼容 Linux 接口,主要是部分结构体字段类型不一致的兼容,虽然结构体编译对齐后总长度是一致的。
  2. 关闭了 BPF sockhash 功能(bpf,SO_COOKIE)的功能检测和开始,该功能主要用于通过从 bpf 的 socket_cookie 直接获取数据,提升性能。
  3. 关闭了 UDP_SEGMENT 功能,主要功能设置 UDP 分段大小。
  4. IP_PKTINFO 选项不探测是否支持,强制使用 FreeBSD 的 IP_RECVDSTADDR 和 IP_SENDSRCADDR 选项。
  5. IP_MTU_DISCOVER 选项不探测是否支持, 强制使用 FreeBSD 的 IP_DONTFRAG 选项。
  6. IPV6_MTU_DISCOVER 选项不探测是否支持, 强制使用 IPV6_DONTFRAG 选项,该选项目前 FreeBSD 和 Linux 都支持。

编译过程

SSL 库

此处以 OpenSSL quic 为例,可以参考以下方式编译

cd /data/
wget https://github.com/quictls/openssl/archive/refs/tags/OpenSSL_1_1_1v-quic1.tar.gz
tar xzvf OpenSSL_1_1_1v-quic1.tar.gz
cd /data/openssl-OpenSSL_1_1_1v-quic1/
./config enable-tls1_3 no-shared --prefix=/usr/local/OpenSSL_1_1_1v-quic1
make
make install_sw

DPDK 和 F-Stack lib

总体编译方式不变,额外需要注意的是如果系统的 OpenSSL 库版本与上面使用的 OpenSSL quic 版本不兼容时,编译 DPDK lib 库时需要也使用上面的OpenSSL quic 库(通过配置 PKG_CONFIG_PATH 使用),参考以下方式编译

export FF_PATH=/data/f-stack
export PKG_CONFIG_PATH=/usr/local/OpenSSL_1_1_1v-quic1/lib/pkgconfig:/usr/lib64/pkgconfig:/usr/local/lib64/pkgconfig:/usr/lib/pkgconfig

mkdir -p /data/f-stack
git clone https://github.com/F-Stack/f-stack.git /data/f-stack

# DPDK lib
cd /data/f-stack/dpdk/
meson -Denable_kmods=true build
ninja -C build
ninja -C build install

# F-Stack lib
cd /data/f-stack/lib/
make
make install

# ff tools
cd /data/f-stack/tools
make
make install

F-Stack Nginx-1.25.2

Nginx 可以参考以下参数进行编译,如果有更多额外需求,自行调整相关配置

export FF_PATH=/data/f-stack
export PKG_CONFIG_PATH=/usr/local/OpenSSL_1_1_1v-quic1/lib/pkgconfig:/usr/lib64/pkgconfig:/usr/local/lib64/pkgconfig:/usr/lib/pkgconfig

cd /data/f-stack/app/nginx-1.25.2/
./configure --prefix=/usr/local/nginx_fstack --with-ff_module --with-http_ssl_module --with-http_v2_module --with-http_v3_module --with-cc-opt=-I/usr/local/OpenSSL_1_1_1v-quic1/include --with-ld-opt='-L/usr/local/OpenSSL_1_1_1v-quic1/lib/'
make
make install

测试使用注意事项

  1. keepalive_timeout = 65 # 因为 Nginx 的 quic 中将 keepalive_timeout参数值作为了读超时时间,所以不能设置为 0
  2. listen 443 quic; # 监听HTTP/3的时候不能设置REUSEPORT,否则多进程会有异常
  3. ulimit -n 100000 # 调大该参数值
  4. 其他使用注意事项可以参考 F-Stack 和 HTTP/3 相关配置文档

性能测试对比

这里不考虑现网实际客户端访问网站的延迟对比,仅考虑 F-Stack Nginx 和源生 Nginx 的性能对比测试。

但是在尝试了多种客户端后,仅 curl8 测试成功,但是只能测试单连接的延迟,这里不太关注。其他压测客户端工具 wrk-quic、h2load、Nighthawk 等在编译测试时都遇到了各种各样的问题,暂时未能成功测试,性能对比数据暂时缺失,如果有人有压测客户端,欢迎进行对比测试并提供测试数据。

乐观 DNS 缓存那些事

本文主要内容包括乐观 DNS 缓存的介绍,现网使用中的优缺点,国内部分递归 DNS(不包含境外 DNS) 的使用情况数据,及如何规避乐观 DNS 等内容。

  • 约60%的运营商递归 DNS 开启了乐观 DNS

乐观 DNS 缓存介绍

乐观 DNS 缓存(Optimistic DNS、RFC8767:Serving Stale Data to Improve DNS Resiliency等,本文后续以乐观 DNS 代替),简单的说就是在客户端向递归 DNS 发起一个域名的 DNS 查询请求时,如果递归 DNS 缓存中的记录已经过期(缓存时间超过了 TTL 时间)时,还是会应答该过期记录的行为。

【注意:】准确的说目前运营商的递归 DNS、南京信风公共 DNS(114.114.114.114)和阿里云公共 DNS(223.5.5.5)等使用过期缓存应答行为与 RFC8764 中乐观 DNS 的行为是冲突的,并不是真正的乐观 DNS 缓存,但此处也先以乐观 DNS 称呼。

  • RFC8767 中的对乐观 DNS 行为的是只有权威 DNS 解析失败时, 比如DDoS 攻击、网络等其他原因导致的超时、REFUSE等错误时才向客户端应答过期的缓存记录数据,而目前部分国内支持乐观 DNS 行为的递归 DNS 则不关注权威应答成功或失败,直接给客户端应答缓存中的过期记录,再去异步向权威查询。
    • 使用过期缓存应答的客户端查询超时时间在标准 RFC 中建议为 1.8 秒,但从个人的经验看,这个值在现网环境中明显有点高了,可以考虑酌情降低,个人建议低于 400 ms
  • 最大过期时间(A maximum stale timer)即 TTL 过期多长时间内可以继续应答过期的记录,RFC 的建议是 1 – 3 天,而目前部分国内递归 DNS 在实践中该时间是无限的。

乐观 DNS 的优点

乐观 DNS 对终端用户的使用体验以及递归 DNS 服务提供商有一些好处,主要包括以下方面:

  • 在缓存的记录 TTL 过期后降低客户端的 DNS 解析时延,尤其是请求量不是很大的冷域名没有了冷启动过程,因为省却了同步的向各级权威 DNS (包括根 DNS, TLD DNS, zone 域名 DNS等 ) 递归(迭代) DNS 查询的过程。
    • 此处主要是部分国内非标准实现的乐观 DNS 的优点
    • 运营商递归 DNS 或云厂商等公共 DNS 中普遍使用缓存和递归分离的多级架构中,此方式工程实现上更为简单,缓存层可以无需保存递归过程中的客户端连接状态信息。
  • 权威 DNS (包括根 DNS, TLD DNS, zone 域名 DNS等 ) 遭受 DDoS 攻击或其他故障无法响应递归 DNS 的查询时,不影响客户端获取请求域名的记录,虽然 TTL 已经过期,但是比解析失败更好一些(”stale bread is better than no bread.”)。
    • 此处可以减少递归 DNS 提供商的收到的无法解析等情况问题反馈、咨询工单等。
    • 降低递归 DNS 的成本,包括网络带宽和计算资源成本等,权威失败时仅间隔性的重试到权威的请求。

乐观 DNS 的缺点

如果按照标准的 RFC8767 去实现乐观 DNS,这里其实并没有什么显著的缺点,因为只有在权威 DNS 不可用或不稳定时才会触发该功能,虽然可能会有一些解析错误的情况出现,但至少不会使情况变得更糟。

乐观 DNS 更多的缺点来自目前部分递归 DNS 的非标实现,主要包括以下方面:

  • 过期的解析记录在递归 DNS 缓存中可能长期(数小时、数天、甚至数月)无法刷新,还可能解析到旧的记录,尤其是对平时解析量不大的冷域名,此时过期的记录很可能早已无法访问。
    • 尤其是最大过期时间(A maximum stale timer)设置为无限时,而不是标准建议的 1-3 天或自行稍微斟酌修改的时间。
    • 此处会增加递归 DNS 提供商的收到的解析错误等情况问题反馈、咨询工单等,具体最终工单量增加还是减少需视客户业务和网络等情况单独去看。
  • 对需要频繁切换记录的域名不友好,如全局负载均衡或 CDN 调度等。
    • 如果域名的 DNS 查询请求量足够大,则可以减轻该影响。

个人观点

  • 基本支持标准 RFC 的乐观 DNS 行为。
  • 强烈反对部分递归 DNS 的乐观 DNS 的非标实现,见之前乐观 DNS 介绍中的注意事项,降低解析时延不能以解析到错误甚至不可用的 IP 为代价
  • 使用开源软件等自建自用递归 DNS 的场景,根据个人需求自行设置缓存规则即可。

国内乐观 DNS 测试

测试范围及方法

  • 测试范围:境内 31 个省份三大运营商(电信、联通、移动)共 93 个测试目标,及部分公共 DNS (119.29.29.29,114.114.114.114,223.5.5.5)共 3 个测试目标。
  • 测试的大体步骤如下所示,该测试过程会重复测试多次:
    • 步骤1,首先设置测试域名opt.test.example.com的 A 记录为1.1.1.1,并设置 TTL 为 600 秒。
      • 此处的测试域名和测试 IP 都非真实的测试域名和测试 IP,仅为说明测试过程。
    • 步骤2, 通过遍布全国的测试客户端(IDC 及 LastMile)请求测试域名,其中包括直接指定目标递归 DNS 进行解析(IDC)和 http 请求中包含的 DNS 解析(LastMile)。
      • 本过程的目的使客户端使用的递归 DNS 都解析并缓存过opt.test.example.com的 A 记录为1.1.1.1
    • 步骤3,等待步骤2的全部测试全部结束后,修改测试域名opt.test.example.com的 A 记录为2.2.2.2,并设置 TTL 为 600 秒。
    • 步骤4,等待不小于 1 小时(数倍 600 秒的 TTL )后,确保递归 DNS 缓存记录的 TTL 已经过期。
    • 步骤5,重复步骤2,通过全国各地的测试客户端重新请求测试域名。
      • 此处绝大部分测试客户端已经排除了客户端本地 DNS 缓存的影响,尽量直接向本地设置的递归 DNS 进行请求。
    • 步骤6,过滤掉干扰 DNS(如 LastMile 的路由器的内网 IP段等),仅保留本地运营商实际的递归 DNS 和目标公共 DNS 的数据记录进行统计,如果仍然存在会解析到已经过期的旧 IP 1.1.1.1,则认为该递归 DNS 开启了乐观 DNS,如果所有记录都为新的 IP2.2.2.2,则认为该递归 DNS 未开启乐观 DNS。
      • 为减少偶然情况的影响,以上测试步骤会执行多次。

测试数据

从目前目前国内的主流的递归 DNS 测试数据看,过半数(约60%左右)已经开启了乐观 DNS,本文仅列出测试数据,不详细点评。

公共 DNS 乐观 DNS 开启情况

本次测试了三家公共 DNS,119.29.29.29,114.114.114.114和223.5.5.5,其中119.29.29.29未开启乐观 DNS,114.114.114.114和223.5.5.5则开启了乐观 DNS。

运营商递归 DNS(LocalDNS/LDNS)乐观 DNS 开启情况

运营商递归 DNS 开启了乐观 DNS 的总体比列大约在 60% 左右,以下为详细数据

  • 该数据仅代表本次测试的统计情况。
IDC + LastMile 测试数据
  • 在所有的 93 个测试目标中共有 57 个测试目标测试到开启了乐观 DNS,占比 57 / 93 = 61.29%
  • 电信 15 个测试目标开启了乐观 DNS,占比 15 / 31 = 48.39%
  • 联通 25 个测试目标开启了乐观 DNS,占比 25 / 31 = 80.65%
  • 移动 17 个测试目标开启了乐观 DNS,占比 17 / 31 = 54.84%
IDC测试数据

虽然在 LastMile 的测试中,已经尽量规避了客户端本地 DNS 缓存的影响,但是我们仍不能保证一定未使用客户端本地缓存,所以单独列出 IDC 的测试数据。

在所有的 93 个测试目标我们通过各种渠道搜集到 81 个对应的 IDC 客户端,可以完全排除本地 DNS 缓存的影响,数据如下:

  • 共 47 个测试目标开启了乐观 DNS,占比 47 / 81 = 58.02%
  • 电信共测试了 27 个测试目标,其中 11 个测试目标开启了乐观 DNS,占比 11 / 27 = 40.74%
  • 联通共测试了 28 个测试目标,其中 22 个测试目标开启了乐观 DNS,占比 22 / 28 = 78.57%
  • 移动共测试了 28 个测试目标,其中 14 个测试目标开启了乐观 DNS,占比 14 / 28 = 50.00%

如何规避乐观DNS

如果业务域名的请求量较大,可以较快的触发递归 DNS 快速刷新到新的记录,那么乐观 DNS 可以微弱的降低解析时延,目前没有看到太明显的缺点,正常使用各递归 DNS 即可。

  • 请求量大的热域名,则缓存命中率高,总体时延降低有限。

然而在很多实际的业务中,对切换实时性要求比较高,但是访问量又没有那么大的中小域名,目前如此高比例非标准实现的乐观 DNS肯定会对业务存在一定的影响,此时就需要一些其他技术手段来对乐观 DNS 进行规避,下面介绍一些方法,其中部分方法比较常见,外网已经有很多的公开信息可以查询参考,此处就不展开详细介绍。

提前修改解析记录/保留旧IP一段时间

  • 如果是有规划的切换 IP 时,也可以考虑预留几天(如3天)的缓冲期,这段时间内新旧 IP 都保持可用,来降低 LocalDNS 会返回过期 IP 造成的影响。
    • 类似于修改域名的 NS,需要留几天的缓冲期保持新旧NS的记录都可用并同步更新
    • 不适用于临时或频繁的切换 IP
    • 即使保留 3 天的缓存,也不能保证完全刷新掉旧的IP
    • 感谢 @changlinli 提供

可以使用未开启乐观 DNS 的公共 DNS 来规避乐观 DNS,目前国内腾讯云 DNSPod 的公共 DNS 等是未开启乐观 DNS 可供选用。不同的使用方式有不同的接入方法,更多信息可以参考这里

使用未开启乐观 DNS 的 公共DNS

通用的公共 DNS

  • 需要修改 WIFI/路由器 等使用的 DNS IP,有一定使用门槛,且普通 DNS 请求容易被劫持。

DoH/DoT(DoQ)

  • 需要较高的 PC 操作系统版本/浏览器/手机 OS(主流IOS/Android均支持)等支持,并进行配置,也有一定的使用门槛。

HttpDNS/HttpsDNS

近年来的常规方式,在各大 APP 中广泛使用,更多 HttpDNS 的介绍

  • 优点:可以指定 HttpDNS 服务器,绕开 LocalDNS,DNS 控制自由度较高,且有丰富的统计信息查看等,即使在 HttpDNS 故障时,也可以降级回本地 LocalDNS
  • 缺点:使用场景受限,主要是在移动端 APP、PC 客户端等有端场景,其他的浏览器等场景使用受限;有改造接入门槛;请求次数收费等使用成本较高。

刷新递归 DNS 缓存

联系运营商主动刷新缓存

部分运营商(如天翼云域名无忧)提供了递归 DNS 缓存刷新接口,但是也存在比较多的问题价格很贵,使用不便等问题。

  • 价格很贵:三大运营商的缓存刷新服务一般为数千元/次,可以刷新某域名在本运营商所有省份的递归 DNS 缓存,如重大业务重大故障的切换需要刷新可以考虑使用,但是如果普通的切换也使用的话则明显成本太高。
  • 使用不便:一般需要单独联系各个渠道去购买或使用该功能,缺少一键全量刷新的手段。
  • 覆盖不全:除了三大运营商外,其他中小运营商和公共 DNS 很少提供公开的缓存刷新服务。

通过拨测客户端被动刷新缓存

通过各种不同的客户端或拨测工具,大量请求目标域名,触发对应的递归 DNS 被动的刷新缓存。

  • 公开免费的拨测工具对各种递归 DNS 的覆盖度不够完整。
  • 使用商业拨测工具大量拨测的价格也不低。

F-Stack LD_PRELOAD 测试版介绍

跳票许久许久的LD_PRELOAD功能模块(后续以 libff_syscall.so 代替)在 F-Stack dev 分支的 adapter/sysctall 目录下已经提交,支持 hook 系统内核 socket 相关接口的代码,降低已有应用迁移到 F-Stack 的门槛。下面将分别进行具体介绍, 主要包括libff_syscall.so 相关的架构涉及其中的一些思考,支持的几种模式以及如何使用等内容。

总体结论:

  • 原有应用程序的接入门槛比原本的 F-Stack 有所降低,大部分情况下可以不修改原有的用户应用程序和 F-Stack lib 的代码,而是仅修改libff_syscall.so相关代码即可适配。
  • 可以支持多 F-Stack 实例(即原 F-Stack 应用程序进程),每个 F-Stack 实例可以对应 1 个或多个用户应用程序。
    • 为了达到最佳的性能,建议一个用户应用程序(进程或线程)对应一个 fstack 实例应用程序,即为一组应用实例。
  • 每组应用实例的性能会略高于系统内核的性能,与单个标准 F-Stack 应用进程互有高低;单机整体的性能相比系统内核仍有较大的优势,但与标准 F-Stack 仍有差距。
    • 新的每组应用实例需要运行在两个 CPU 核心上,而标准 F-Stack 应用进程只需要运行在一个 CPU 核心上,总体而言性价比不高,是否使用可以视各业务的具体情况而定。
    • Nginx 600 字节的 body 内存应答测试中,长连接中相同数量的新应用实例于标准 F-Stack 应用进程,短连接中相同数量的新应用实例则略于标准 F-Stack 应用进程,见 Nginx 接入介绍章节,但使用的 CPU 几乎翻倍。

【注意】目前 libff_syscall.so 功能尚不完善,仅供测试使用,欢迎所有的开发者一起进行完善,存在一些问题,如下所示:

  • 进程结束时尚存在内存泄漏、容易死锁等问题。
  • 有些接口(如sendmsg、readv、readmsg等)因为尚未使用到,没有优化测试,还需进一步性能优化和测试。
  • 缺乏更长时间的运行验证,可能存在一些未知的隐藏问题尚未发现。
  • 多个f-stack实例运行的时候,暂时无法作为客户端使用,如Nginx的代理。参考修改方案如下:
    • @铁皮大爷:我之前有实现过一套逻辑,和现在坐着实现的类似,但是在 hook中 加入了 rss,从延迟 socket 建立(仅在确定目标、源以后才真正的选择使用哪一个 fstack 作为 worker 进程,要求在网卡接收时,也设置 rss 对称 hash,保证输出和输入能在同一个 fstack-worke r中)。 app -> sock -> hold一个sock操作,创建 fd(fd1),返回给用户 app -> bind -> hold一个bind操作,将 bind 参数绑定在 fd1 上,返回给用户, app -> connect -> 加个connect参数绑定在 fd1 上面,根据 rss 对称 hash 计算,选择一个 fstack 进程(worker),并将 hold 的 sock、bind、connect一并交给 fstack 进程,并等待同步返回结果。

libff_syscall.so 的编译

先设置好FF_PATHPKG_CONFIG_PATH环境变量

export FF_PATH=/data/f-stack
export PKG_CONFIG_PATH=/usr/lib64/pkgconfig:/usr/local/lib64/pkgconfig:/usr/lib/pkgconfig

adapter/sysctall目录下直接编译即可得到ibff_syscall.so的相关功能组件

cd /data/f-stack/adapter/sysctall
make clean;make all

ls -lrt
fstack
libff_syscall.so
helloworld_stack
helloworld_stack_thread_socket
helloworld_stack_epoll
helloworld_stack_epoll_thread_socket
helloworld_stack_epoll_kernel

下面将分别进行介绍各个组件的主要作用

fstack 实例应用程序

fstack应用程序对标的是标准版 F-Stack 中的应用程序,其运行与普通的 F-Stack 应用程序完全相同,包括配置文件及其多进程(每进程即为一个实例)的运行方式等, 具体运行方式可以参考 F-Stack 主目录的 README, 在执行 LD_PRELOAD 的用户应用程序前必须先运行 fstack实例应用程序。

fstack 应用程序的作用主要是底层对接 F-Stack API,其主函数ff_handle_each_context即为普通 F-Stack 应用的用户层 loop 函数,非空闲时或每间隔 10ms (受 HZ参数影响) 时会调用该函数去循环处理与 APP 对接的上下文,如果 APP 有对应的 API 请求,则调用实际的 F-Stack API 进行处理。

libff_syscall.so用户应用进程间通信使用 DPDK 的 rte_malloc 分配的 Hugepage 共享内存进行。

该函数对 libff_syscall.so 的整体性能有至关重要的影响,目前是复用了 F-Stack 主配置文件(config.ini)中的 pkt_tx_dalay参数,死循环并延迟该参数指定的值后才会回到 F-Stack 的其他处理流程中。

如果想提高 libff_syscall.so的整体性能,那么fstack实例应用程序与 APP 应用程序的匹配十分重要,只有当一个ff_handle_each_context循环中尽量匹配一次循环的所有事件时才能达到最优的性能,这里需要调十分精细的调优,但是目前还是粗略的使用 pkt_tx_dalay参数值。

【提示】pkt_tx_dalay参数的默认值为 100us, 较适合长连接的场景。如果是 Nginx 短链接的场景,则应考虑设置为 50us,可以可获得更好的性能。当然不同的用用场景如果想达到最优的性能,可能需要业务自行调整及测试。复用该参数也只是临时方案,后续如果有更优的方案,则随时可能进行调整。

libff_syscall.so

该动态库主要作用是劫持系统的 socket 相关接口,根据 fd 参数判断是调用 F-Stack的相关接口(通过上下文 sc 与 fsack 实例应用程序交互)还是系统内核的相关接口。

fstack实例应用进程间通信使用 DPDK 的 rte_malloc 分配的 Hugepage 共享内存进行。

【注意】在第一次调用相关接口时分配相关内存,不再释放,进程退出时存在内存泄漏的问题,待修复。

F-Stack用户的应用程序 (如 helloworl 或 Nginx)设置 LD_PRELOAD劫持系统的 socket 相关 API 时使用,即可直接接入 F-Stack 开发框架,可以参考如下命令:

export LD_PRELOAD=/data/f-stack/adapter/syscall/libff_syscall.so

确保 fstack实例应用程序已经正确运行的前提下,然后启动用户应用程序。

当然如果是改造用户的 APP 使用 kqueue代替 Linux 的 epoll 相关事件接口时,也可以在用户 APP 中直接链接该运行库, 可以参考相关示例程序helloworld_stackhelloworld_stack_thread_socket对应的源文件main_stack.cmain_stack_thread_socket.c,因为不是使用的LD_PRELOAD, 所以本文档不再详细介绍。

【重要提示】一组对应的fstack应用程序和用户应用程序最好运行在同一个 CPU NUMA 节点不同物理核上,其他场景(运行在同一个CPU核心、两个 CPU 核心跨 NUMA 节点,物理核和超线程核混用)都无法达到一组实例的最佳性能。

  • 特别的,如果 CPU 物理核心比较缺乏,可以考虑一组实例分别运行在对应的一组 CPU 的物理核心和 HT 核心上,虽然单组实例性能会有所下降(约 20% 左右),但可以使用更多的 CPU 核心,单机总性能可能会有所提升。

DEMO 演示程序 helloworld_stack*

其他编译生成的hello_world开头的可执行文件为当前libff_syscall.so支持的几种不同运行模式的相关演示程序,下一节进行具体介绍。

F-Stack LD_PRELOAD 支持的几种模式

为了适应不同应用对 socket 接口的不同使用方式,降低已有应用迁移到 F-Stack 的门槛,并尽量提高较高的性能,目前 F-Stack 的 libff_syscall.so 主要支持以下几种模式,支持多线程的 PIPELINE 模式、线程(进程)内的 RTC(run to completion)模式、同时支持 F-Stack 和内核 socket 接口的 FF_KERNEL_EVENT 模式和类似内核 SO_REUSEPORT 的 FF_MULTI_SC 模式。

支持多线程的 PIPELINE 模式

该模式为默认模式,无需额外设置任何参数直接编译libff_syscall.so即可。

在此模式下,socket 相关接口返回的 fd 可以在不同线程交叉调用,即支持 PIPELINE 模式,对已有应用的移植接入更友好,但性能上相应也会有更多的损失。

该模式除了单进程运行方式外,同时可以支持用户应用程序多进程方式运行,每个用户进程对应一个fstack实例应用程序的实例,更多信息可以参考附录的运行参数介绍。

【注意】以此默认方式接入 F-Stack 的应用程序只能使用 F-Stack 的 socket 网络接口,而不能使用系统的 socket 接口。

hook 系统 epoll 接口

对于已有的 Linux 下的应用,事件接口都是一般使用的是epoll相关接口,对于没有更多特殊要求的应用程序,可以直接使用默认的编译参数编译libff_syscall.so后使用,参考 DEMO 程序helloworld_stack_epoll, 代码文件为main_stack_epoll.c

【注意】F-Stack 的epoll接口依然为kqueue接口的封装,使用上依然与系统标准的epoll事件接口有一定区别,主要是事件触发方式和multi accept的区别。

使用 kqueue

当然libff_syscall.so除了支持使用LD_PRELOAD方式 hook 系统的 socket 接口的方式使用,也支持普通的链接方式使用,此时除了可以使用系统的epoll事件接口之外,还可以使用 F-Stack(FreeBSD)具有的kqueue事件接口,参考 DEMO 程序helloworld_stack, 代码文件为main_stack.c

该使用方式的性能比LD_PRELOALD使用系统epoll接口的方式有略微的性能提升。

线程(进程)内的 RTC(run to completion)模式

该模式需要设置额外的编译参数后来编译libff_syscall.so才能开启,可以在adapter/sysctall/Makefile中使能FF_THREAD_SOCKET或执行以下 shell 命令来开启。

export FF_THREAD_SOCKET=1
make clean;make all

在此模式下,socket 相关接口返回的 fd 仅可以在本线程内调用,即仅支持线程内的 RTC 模式,对已有应用的移植接入门槛稍高,但性能上相应也会有一定的提升,适合原本就以 RTC 模式运行的应用移植。

同样的,该模式除了单进程运行方式外,同时可以支持用户应用程序多进程方式运行,每个用户进程对应一个fstack实例应用程序的实例,更多信息可以参考附录的运行参数介绍。

【注意】以此默认方式接入 F-Stack 的应用程序同样只能使用 F-Stack 的 socket 网络接口,而不能使用系统的 socket 接口。

hook 系统 epoll 接口

其他同默认的 PIPELINE 模式,可以参考 DEMO 程序helloworld_stack_epoll_thread_socket, 代码文件为main_stack_epoll_thread_socket.c

使用 kqueue

其他同默认的 PIPELINE 模式,可以参考 DEMO 程序helloworld_stack_thread_socket, 代码文件为main_stack_thread_socket.c

FF_KERNEL_EVENT 模式

该模式可以同时支持 F-Stack 和系统内核的 socket 接口,需要设置额外的编译参数后来编译libff_syscall.so才能开启,可以在adapter/sysctall/Makefile中使能FF_KERNEL_EVENT或执行以下 shell 命令来开启。

export FF_KERNEL_EVENT=1
make clean;make all

在此模式下,epoll相关接口在调用 F-Stack 接口的同时会调用系统内核的相关接口,并将 F-Stack 返回的 fd 与系统内核返回的 fd 建立映射关系,主要为了支持两个场景:

  • 用户应用程序中有控制 fd 与 数据 fd 使用相同的 epoll fd, 如 Nginx。
  • 希望本机也可以同时访问用户应用程序监听的网络接口。
    • 如果希望单独与本机系统内核进行普通网络通信,需要额外调用socket接口,并需要指定type | SOCK_KERNEL参数,并为返回的 fd 单独调用 bind()listen()epoll_ctl()等接口,参考 DEMO 程序helloworld_stack_epoll_kernel, 代码文件为main_stack_epoll_kernel.c

【注意1】F-Stack 中 FreeBSD 的内核参数 kern.maxfiles不应该大于 65536(原默认值为 33554432),以保证 F-Stack 的 epoll fd 到系统内核的 epoll fd 的正确映射。

【注意2】Nginx 的无缝接入需要开启此模式,因为在 Nginx 中有多个控制 fd 与 数据 fd 使用相同的 epoll fd。

FF_MULTI_SC 模式

该模式为 Nginx 等使用内核SO_REUSEPORTfork子进程 worker 运行等特殊的设置为设置,需要设置额外的编译参数后来编译libff_syscall.so才能开启,可以在adapter/sysctall/Makefile中使能FF_MULTI_SC或执行以下 shell 命令来开启。

export FF_MULTI_SC=1
make clean;make all

在此模式下,用户应用程序与fstack实例相关联的上下文sc除了保存在全局变量sc中之外,会额外保存在全局的scs数组中,在fork()子进程 worker 时会使用 current_worker_id设置sc变量为对应 worker 进程 fd 对应的 sc,供子进程复制及使用。

Nginx 的reuseport模式的主要流程为,主进程为每个 worker 分别调用 socket()bind()listen()等接口,并复制到 worker 进程,而后 woker 进程各自调用epoll相关接口处理各自的 fd, 需要各自 fd 对应的上下文 sc 才能正确运行。

【注意】Nginx 的无缝接入需要同时开启 FF_THREAD_SOCKETFF_MULTI_SC 模式。

Nginx 接入libff_syscall.so介绍

Nginx(以 F-Stack 默认携带的 Nginx-1.16.1 为例)目前可以不修改任何代码直接以LD_PRELOAD动态库libff_syscall.so的方式接入 F-Stack,以下为主要步骤及效果。

编译libff_syscall.so

需要同时开启 FF_THREAD_SOCKETFF_MULTI_SC 模式进行编译

export FF_PATH=/data/f-stack
export PKG_CONFIG_PATH=/usr/lib64/pkgconfig:/usr/local/lib64/pkgconfig:/usr/lib/pkgconfig

cd /data/f-stack/adapter/sysctall
export FF_KERNEL_EVENT=1
export FF_MULTI_SC=1
make clean;make all

配置nginx.conf

以下为主要需要注意及修改的相关配置参数示例(非全量参数):

user  root;
worker_processes 4; # worker 数量
worker_cpu_affinity 10000 100000 1000000 10000000; # 设置 CPU 亲和性

events {
  worker_connections 1024;
  multi_accept on; # epoll 是封装 kqueue 接口,必须要开启
  use epoll;
}

http {
access_log off; # 关闭访问日志,用于提高测试时的网络性能,否则每次请求都需要额外调用系统的 write() 接口记录访问日志

sendfile       off; # 使用 F-Stack 时需要关闭

keepalive_timeout 0; # 视长连接/短链接的业务需要调整
  #keepalive_timeout 65;
  #keepalive_requests 200; # 默认每个长连接最多 100 个请求,视业务需要调整,长连接时适当提高此值可以略微提高性能
   
  server {
      listen       80 reuseport; # 应该设置 reuseport,与使用系统的内核的 reuseport 行为不太一致,但都可以提高性能

      access_log off;
       
      location / {
          #root   html;
          #index index.html index.htm;
          return 200 "0123456789abcdefghijklmnopqrstuvwxyz"; # 直接返回数据用以测试单纯的网络性能
      }
  }
}

【注意】此处的 reuseport作用是使用多个不同的 socket fd, 而每个 fd 可以对接不同的fstack实例应用程序的上下文sc来分散请求,从而达到提高性能的目的。与系统内核的reuseport行为异曲同工。

运行

假设运行4组 Nginx – fstack 实例应用程序,可以简单按照以下步骤进行

  • 运行 fstack 实例
  • 设置config.ini中的lcore_mask=f00,即使用 CPU 核心 9-11, 其他配置按照标准 F-Stack 配置进行。
  • 参考以下命令启动 fstack 实例,并等待一段时间待 fstack 主进程和子进程都启动完成
  cd /data/f-stack
  bash ./start.sh -b adapter/syscall/fstack
  • 运行 Nginx
  • 参考以下命令配置libff_syscall.so所需的环境变量
  export LD_PRELOAD=/data/f-stack/adapter/syscall/libff_syscall.so # 设置 LD_PRELOAD libff_syscall.so
  export FF_NB_FSTACK_INSTANCE=4 # 设置有 4 个 fstack 实例应用程序,前面 nginx.conf 中也配置了 4 个worker
  • 启动 Nginx
  /usr/local/nginx/sbin/nginx # 启动 Nginx

性能对比

测试环境

CPU:Intel(R) Xeon(R) CPU E5-2670 v3 @ 2.30GHz * 2

网卡:Intel Corporation Ethernet Controller 10-Gigabit X540-AT2

OS :TencentOS Server 3.2 (Final)

内核:5.4.119-1-tlinux4-0009.1 #1 SMP Sun Jan 23 22:20:03 CST 2022 x86_64 x86_64 x86_64 GNU/Linux

Nginx长连接

  • body 大小为 602 字节(不包括 http 头等)。
  • LD_PRELOAD 实际使用的 CPU 为几乎横轴 CPU 核心数的双倍,系统内核均衡软中断实际使用的 CPU 也远高于 worker 数量对应的 CPU 核心数量。
  • 限于时间所限,其中 LD_PRELOAD 的测试数据为以上测试环境的数据,其他为历史 40G 测试环境的数据,后续会更新为相同测试环境的数据。
  • 受网卡硬件所限,8核 LD_PRELOAD 测试带宽已经接近 10G 网卡线速(服务端出带宽9.xG,148万 RPS), 导致的与标准 F-Stack 的数据差异,实际CPU尚有一些空闲,后续应使用 40G/100G 网卡进行对比测试
  • pkt_tx_delay 参数为 100us。

Nginx短链接

  • body 大小为 602 字节(不包括 http 头等)。
  • LD_PRELOAD 实际使用的 CPU 为几乎横轴 CPU 核心数的双倍,系统内核均衡软中断实际使用的 CPU 也远高于 worker 数量对应的 CPU 核心数量。
  • 受 CPU 硬件所限(12C24HT * 2),LD_PRELOAD 测试只能测试12组应用实例组,即使用了全部 CPU 的物理核心,无法进行更多实例组的测试。
  • 8核之后 LD_PRELOAD 的性能不如标准 F-Stack 的性能,最主要是受用户应用程序和fstack应用程序的匹配度不高(ff_handle_each_context的循环次数及时间等)影响很大,并未完全达到性能极致,如果持续的精细化调整可以进一步提高性能,但是通用性也不高。
  • pkt_tx_delay 参数由 100us 调整到 50us。

附录:详细参数介绍

编译参数

本段总体介绍各个编译选项,所有参数都可以在adapter/sysctall/Makefile中开启或通过 shell 命令设置环境变量来开启。

DEBUG

开启或关闭 DEBUG 模式,主要影响优化和日志输出等, 默认关闭。

export DEBUG=-O0 -gdwarf-2 -g3

默认的优化参数为

-g -O2 -DNDEBUG

FF_THREAD_SOCKET

是否开启线程级上下文sc变量,如果开启,则 socket 相关 fd 只能在本线程中调用,一般可以略微提高性能, 默认关闭。

export FF_THREAD_SOCKET=1

FF_KERNEL_EVENT

是否开启epoll相关接口在调用 F-Stack 接口的同时调用系统内核的相关接口,并将 F-Stack 返回的 fd 与系统内核返回的 fd 建立映射关系, 默认关闭,主要为了支持两个场景:

  • 用户应用程序中有控制 fd 与 数据 fd 使用相同的 epoll fd, 如 Nginx。
  • 希望本机也可以同时访问用户应用程序监听的网络接口。
export FF_KERNEL_EVENT=1

FF_MULTI_SC

在此模式下,用户应用程序与fstack实例相关联的上下文sc除了保存在全局变量sc中之外,会额外保存在全局的scs数组中,在fork()子进程 worker 时会使用 current_worker_id设置sc变量为对应 worker 进程 fd 对应的 sc,供子进程复制及使用。 默认关闭。

export FF_KERNEL_EVENT=1

运行参数

通过设置环境变量设置一些用户应用程序需要的参数值,如果后续通过配置文件配置的话可能需要修改原有应用,所以暂时使用设置环境变量的方式。

LD_PRELOAD

设置 LD_PRELOAD 的运行库,再运行实际的应用程序,可以参考以下命令

export LD_PRELOAD=/data/f-stack/adapter/syscall/libff_syscall.so

如果想通过gdb调试应用程序,则可以参考以下命令

export LD_PRELOAD=
gdb ./helloworld_stack_epoll
(gdb) set exec-wrapper env 'LD_PRELOAD=/data/f-stack/adapter/syscall/libff_syscall.so'

FF_NB_FSTACK_INSTANCE

设置fstack实例应用程序的实例数,用于和用户应用程序的进程/线程等 worker 数量相匹配,默认1。

export FF_NB_FSTACK_INSTANCE=4

建议用户应用程序 worker 数量与fstack实例应用程序尽量 1:1 配置,可以达到更好的性能。

FF_INITIAL_LCORE_ID

配置用户应用程序的 CPU 亲和性绑定的起始 CPU 逻辑 ID,16进制,默认0x4(0b0100),即 CPU 2。

export FF_INITIAL_LCORE_ID=0x4

如果用于应用程序可以配置 CPU 亲和性,则可以忽略该参数,如 Nginx 配置文件中的worker_cpu_affinity参数。

FF_PROC_ID

配置用户应用程序的进程 ID,可以配合FF_INITIAL_LCORE_ID参数设置 CPU 亲和性的绑定,10进制递增,默认0。

export FF_PROC_ID=1

如果用于应用程序可以配置 CPU 亲和性,则可以忽略该参数,如 Nginx 配置文件中的worker_cpu_affinity参数。

过去的2022

又是一年的流水账,虽然还是禁放的,不过外面还是时不时传来鞭炮声,当然我个人对过年也没什么特别的感觉,该干嘛还是干嘛,前些天每天晚上下班路边的彩灯一眼也就扫过去了,完全没有感觉 ,也就是年终奖没开奖时有点小期待罢了。

22年的主题还是YQ,不想做太多评价,还好年底算是过去了。只是21年只做了2次核酸的轻松被完全打破了,不知道做了多少次,总共出差深圳2次,每次都是去之前比较轻,一到深圳就开始变严重,结果两次回来都被隔离了,一次8+7,一次7天居家。

小孩2年级还是有点在落后,虽然没有什么太高和太严格的要求,不会强求什么,但还是会稍微给一定的压力去学习。

腰突比去年严重了很多,尤其是最后几个月,酸痛是常事,勉强还在能忍受的范围内,再严重一些的话估计就要影响日常活动了。一般认为游泳是可以缓解的,但是这两年YQ原因,基本都中断了,办了游泳卡的健身房上半年也卷款跑路了,说是有人接手,结果大半年后依然没什么确定的消息。下半年听说另一家有泳池的健身房也跑路了,后面想找个稳定的还要再观察下了。

22年工作上因为一些众所周知的“降本增效”,团队同事变化很大,虽然没有“降本增效”时变化也很大,但是其实前几年已经算是相对稳定的时期了。

权威DNS平台继续保持了稳定,后续主要是一些小的修补,暂时没有大的更新计划了。

HttpDNS/公共DNS投入了比较多的精力还发了一些功能,但是并没有作为owner,23年还是会有很大精力在公共DNS上,改善现有系统的一些长期遗留的问题。

F-Stack依然没有能够投入很多精力,目前还仅能保持最基本的维护,还好支持BBR的1.22版本终于是发布了,可以预见的23年大概率依然还是目前这种状态,不过如果nginx主线支持HTTP/3的话肯定会跟进支持。

22年工作相关收入因为众所周知的原因比21年有所降低,但是因为分出了一些精力在股市上,全年总收入算是有一些提高。虽然中间大半年股市惨不忍睹,账面的亏损也是不忍直视,还好最后两个月又把yh用起来了,才涨了回来,没把亏损带到23年。

最后一句,23年继续做好本职工作吧。