过去的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年继续做好本职工作吧。

linux&F-Stack(FreeBSD)多vlan VIP策略路由设置

假设我们的服务器上有如下vlan和ip的配置

vlan 10:
IP:10.10.10.10 netmask:255.255.255.0 broadcast:10.10.10.255 gateway:10.10.10.1
外网VIP:110.110.110.0/24中的一个或多个IP,如110.110.110.110

vlan 20:
IP:10.10.20.20 netmask:255.255.255.0 broadcast:10.10.20.255 gateway:10.10.20.1
外网VIP:120.120.120.0/24中的一个或多个IP,如120.120.120.120

vlan 30:
IP:10.10.30.30 netmask:255.255.255.0 broadcast:10.10.30.255 gateway:10.10.30.1
外网VIP:130.130.130.0/24中的一个或多个IP,如30.130.130.130

外部主要访问各个vlan里的vip,需要系统应答包也正确的对应的vlan回复到该vlan的gateway地址上,下面为linux系统和F-Stack(FreeBSD)的策略设置方式

Linux 系统路由配置

创建 vlan 并设置 IP 地址

ip link add link eth0 name eth0.10 type vlan id 10
ifconfig eth0.10 10.10.10.10 netmask 255.255.255.0 broadcast 10.10.10.255 up
route add -net 10.10.10.0/24 gw 10.10.10.1 dev eth0.10

ip link add link eth0 name eth0.20 type vlan id 20
ifconfig eth0.20 10.10.20.20 netmask 255.255.255.0 broadcast 10.10.20.255 up
route add -net 10.10.20.0/24 gw 10.10.20.1 dev eth0.20

ip link add link eth0 name eth0.30 type vlan id 30
ifconfig eth0.30 10.10.30.30 netmask 255.255.255.0 broadcast 10.10.30.255 up
route add -net 10.10.30.0/24 gw 10.10.30.1 dev eth0.30

# 设置各 vlan 的外网 VIP
ifconfig lo.0 110.110.110.110 netmask 255.255.255.255
ifconfig lo.1 120.120.120.120 netmask 255.255.255.255
ifconfig lo.2 130.130.130.130 netmask 255.255.255.255

设置策略路由

echo "10 t0" >> /etc/iproute2/rt_tables
echo "20 t1" >> /etc/iproute2/rt_tables
echo "30 t2" >> /etc/iproute2/rt_tables

# 设置各 vlan 的路由表, 并设置对应的网关地址
ip route add default dev eth0.10 via 10.10.10.1 table 10
ip route add default dev eth0.20 via 10.10.20.1 table 20
ip route add default dev eth0.30 via 10.10.30.1 table 30

#systemctl restart network

# 从对应 VIP 出去的包使用对应 vlan 的路由表
ip rule add from 110.110.110.0/24 table 10
ip rule add from 120.120.120.0/24 table 20
ip rule add from 130.130.130.0/24 table 30

F-Stack(FreeBSD) 路由配置

前提条件:在 F-Stack 的 lib/Makefile中打开默认关闭的ipfw选项,并重新编译 F-Stack lib 库,各个工具及应用程序

# NETGRAPH drivers ipfw
FF_NETGRAPH=1
FF_IPFW=1

创建 vlan 并设置 IP 地址

ff_ifconfig -p 0 f-stack-0.10 create
ff_ifconfig -p 0 f-stack-0.10 inet 10.10.10.10 netmask 255.255.255.0 broadcast 10.10.10.255

ff_ifconfig -p 0 f-stack-0.20 create
ff_ifconfig -p 0 f-stack-0.20 inet 10.10.20.20 netmask 255.255.255.0 broadcast 10.10.20.255

ff_ifconfig -p 0 f-stack-0.30 create
ff_ifconfig -p 0 f-stack-0.30 inet 10.10.30.30 netmask 255.255.255.0 broadcast 10.10.30.255

# 设置各 vlan 的外网 VIP
ff_ifconfig -p0 f-stack-0.10 inet 110.110.110.110 netmask 255.255.255.255 alias
ff_ifconfig -p0 f-stack-0.20 inet 120.120.120.120 netmask 255.255.255.255 alias
ff_ifconfig -p0 f-stack-0.30 inet 130.130.130.130 netmask 255.255.255.255 alias

设置策略路由

# 设置各 vlan 的转发路由表(fib), 并设置对应的网关地址
ff_route -p 0 add -net 0.0.0.0 10.10.10.1 -fib 10
ff_route -p 0 add -net 0.0.0.0 10.10.20.1 -fib 20
ff_route -p 0 add -net 0.0.0.0 10.10.30.1 -fib 30

# 将对应 VIP 发出去的数据包设置对应 vlan 的 fib num,以便使用正确的转发路由表(fib)
ff_ipfw -P 0 add 100 setfib 10 ip from 110.110.110.0/24 to any out
ff_ipfw -P 0 add 200 setfib 20 ip from 120.120.120.0/24 to any out
ff_ipfw -P 0 add 300 setfib 30 ip from 130.130.130.0/24 to any out

参考资料

  1. F-Stack vlan 的支持与使用
  2. Nginx TCP 多证书透明代理及 Linux/F-Stack(FreeBSD) 路由相关设置
  3. linux 和 FreeBSD 相关工具的 man page

IOS 16 异常降级到 tls1.0 的问题

最近碰到个客户问题,微信公众号里有个服务链接,在 IOS 下偶发失败,报错 -1200,经过排查发现是 IOS 16 在特定情况下可能会出现发起的 https 请求降级为了 tls1.0 导致的。

排查过程

简单发几条排查的发现和截图,怀疑是 IOS 16 的网络请求有什么 bug 导致非正常的降级到了 tls1.0,而 tls1.0 宣称在 IOS 15 后已经不再支持,如果有 IOS 客户端同学可以看看具体是什么原因导致的

  • 只有 IOS 出现,目前发现的 case 都为 IOS 16,包括 16.0 和 16.1 ;9 月底之后开始出现;微信、chrome、safari、qq浏览器等都可以复现。
  • 服务端在香港,开启 UDP 的 443 端口,但是该业务域名并未配置开启 HTTP3(QUIC),请求如下所示
curl --http3 -vvv https://xxxx.cn/
*   Trying x.x.x.x:443...
* Connect socket 5 over QUIC to x.x.x.x:443
*  CAfile: /etc/pki/tls/certs/ca-bundle.crt
*  CApath: none
*  subjectAltName does not match xxxx.cn
* SSL: no alternative certificate subject name matches target host name 'xxxx.cn'
* connect to x.x.x.x port 443 failed: SSL peer certificate or SSH remote key was not OK
* Failed to connect to xxxx.cn port 443 after 102 ms: SSL peer certificate or SSH remote key was not OK
* Closing connection 0
curl: (60) SSL: no alternative certificate subject name matches target host name 'xxxx.cn'
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
  • 开启 H3 协商失败后降级到 http1.1,使用tcp请求,此时客户端发起的请求在服务端应答 syn+ack 后,经常性的会出现客户端直接rst连接的情况(110-160ms左右收到 syn+ack, 130ms的应答也可以正常连接),检查客户端发起的syn和服务端回复的 syn+ack,并没有发现什么异常。与未被客户端 rst 的请求/应答包也没什么异常区别。
    HTTP3协商失败和客户端TCP rst 貌似总是同时出现。
  • 如果连接未被 rst,则可能发起 tls1.3 或 tls1.0 的请求,如果发起 tls1.3 的请求,则可以正常请求应答
  • 如果客户端发起 tls1.0 的请求,如果服务端未开启 tls1.0 的支持,则无法正常握手;如果服务端开启tls1.0,因为客户端发起的 client hello中 携带了TLS_FALLBACK_SCSV标记,服务端则返回 Inappropriate Fallback错误,还是无法握手,如下所示。

临时规避方案

  1. 正确配置服务器的HTTP3(QUIC)
  2. 完全关闭服务器的HTTP3(QUIC)端口,即UDP 443

其他

腾讯云 DNSPod 已经支持 HTTPS(type65)记录类型设置,正确设置后可以对 IOS14 以上的访问有一定的优化作用。

该记录类型尚处于草案阶段, 腾讯云 DNSPod 后续会持续关注并跟进相关草案的更新。

F-Stack发送零拷贝介绍

数据包在服务器的处理分接收和发送两个方向,收包方向因为我们自己本身的业务场景涉及收包数据很少,后续另行介绍。

本文主要介绍F-Stack发包方向上当前的零拷贝处理方案、效果和应用场景的选择,发包方向上的数据拷贝目前主要为两个阶段,一是协议栈数据拷贝到DPDK的rte_mbuf中,二是应用层调用socket发送接口时会将数据从应用层拷贝到FreeBSD协议栈,下面将分别进行介绍。

协议栈到DPDK

该过程的零拷贝实现由 @jinhao2 提交的Pull Request #364 合并到F-Stack主线中,相关实现细节可以参考相关代码,这里仅对实现方案进行简要介绍。

方案介绍

  • 进程初始化时,通过mmap 为 BSD 堆栈分配指定大小的内存(目前默认256M),可以通过在config.ini中通过参数memsz_MB修改默认配置。
  • 通过 mlock() 固定物理内存,防止被换出到交换分区造成内存虚拟地址和物理地址对应关系的变化。
  • 计算每个页面的起始地址并保存,包括虚拟地址和物理地址,物理地址的计算可以通过DPDK的提供的相关接口进行。
  • 初始化一个堆栈结构来管理所有分配的页面。
  • 通过从已经初始化的堆栈结构中获取/释放一页来替换 ff_mmap()/ff_munmap()的实际mmap行为,而BSD协议栈调用kmem_malloc()/kmem_free()时调用ff_mmap()/ff_munmap()来获取内存页。
  • 在将BSD协议栈mbuf的数据地址赋值给DPDK的rte_mbuf时用于判断是否为初始化申请的内存池中的地址,并通过虚拟地址查找对应的物理地址,分别赋值给rte_buf结构的buf_addr/buf_physaddr,而不再实际进行内存拷贝。
  • 使用一个循环队列保存发送的mbuf的指针,队列的长度应该与NIC的tx_queue_length相同。在队列中的一项被推入新值之前,旧的 mbuf 必须由 NIC 处理并且可以安全地释放。
  • 如果mbufext_cluster类型,其中包括一个rte_mbuf,表示是收包时零拷贝附加的数据地址,则使用 rte_pktmbuf_clone()代替。

使用方式及注意事项

使用方式

该功能默认并未开启,需要通过在lib/Makefile中打开编译选项FF_USE_PAGE_ARRAY,并重新编译F-Stack lib 库和应用程序后才能生效。

其他应用编程及使用方式与常规拷贝模式没有区别,对应用层透明。

注意事项

  • 内存池初始化时在本进程通过mmapmlock申请,为进程私有地址空间,相关内存不能传递到其他进程使用。
    • 可以考虑在初始化时映射大页内存或者使用共享内存(同样需要SHM_LOCkmlock锁定内存,防止交换)来达到可以跨进程使用的目的,但是对应的地址保存和查找结构也需要进行变更,一般应用建议避免跨进程使用即可,不建议进行修改。
  • 协议栈到DPDK的零拷贝功能可以单独开启FF_USE_PAGE_ARRAY使用,也可以与零拷贝发送接口FF_ZC_SEND一起开启使用。
  • 此处减少的内存拷贝是否对应用性能有提升还需要结合具体的应用进行实际测试,数据包在一定大小且使用方式合适时则可以有一定的性能优化效果,但优化效果并不一定很明显,比如只有2-3%左右的提升。

应用层到协议栈

通过提供单独的零拷贝API,使应用层在通过socket接口发送数据时,避免应用层到BSD协议栈的数据拷贝,具体细节见提交e12886c,下面将进行较为具体的介绍。

方案介绍

  • 提供单独的零拷贝结构体ff_zc_mbuf,用于应用层缓存结构,后续应用层的数据操作和发送都应该使用该结构体,具体类型如下所示:struct ff_zc_mbuf {
      void *bsd_mbuf;         /* 指向BSD mbuf链的头节点 */
      void *bsd_mbuf_off;     /* 指向BSD mbuf链中偏移off后的当前节点 */
      int off;               /* mbuf链中的偏移量,应用层不应该直接修改 */
      int len;               /* 申请的mbuf链缓存的总长度,小于等于mbuf链实际能承载的数据长度 */
    };
  • 提供接口ff_zc_mbuf_get(),用于应用提前申请包含可以由内核直接使用的mbuf的结构体作为应用层数据缓存,接口声明如下。int ff_zc_mbuf_get(struct ff_zc_mbuf *m, int len);该接口输入struct ff_zc_mbuf *指针和需要申请的缓存总长度,内部将通过m_getm2()分配mbuf链,首地址保存在ff_zc_mbuf结构的bsd_mbuf变量中,后续可以传递给ff_write()接口。其中m_getm2()为标准socket接口拷贝应用层数据到协议栈时分配mbuf链的接口,所以使用该接口范围的mbuf链作为应用层缓存,可以在发送数据时完全兼容。
  • 提供了缓存数据写入函数ff_zc_mbuf_write(),函数声明如下,
  • int ff_zc_mbuf_write(struct ff_zc_mbuf *m, const char *data, int len); 应用层在保存待发送的数据时,应通过接口ff_zc_mbuf_wirte()直接将数据写到ff_zc_mbuf指向的mbuf链的缓存中,ff_zc_mbuf_wirte()接口可以多次调用写入缓存数据,接口内部自动处理缓存的偏移情况,但多次总的写入长度不能超过初始申请的缓存长度
  • 应用调用ff_write()接口时指定传递ff_zc_mubf.bsd_mbufbuf参数,示例如下所示,ff_write(clientfd, zc_buf.bsd_mbuf, buf_len);在m_uiotombuf()函数中,直接使用传递的mbuf链的首地址,不再额外进行mbuf链的分配和数据拷贝,如下所示,#ifdef FSTACK_ZC_SEND
    if (uio->uio_segflg == UIO_SYSSPACE && uio->uio_rw == UIO_WRITE) {
    m = (struct mbuf *)uio->uio_iov->iov_base; /* 直接使用应用层的mbuf链首地址 */
    uio->uio_iov->iov_base = (char *)(uio->uio_iov->iov_base) + total;
    uio->uio_iov->iov_len = 0;
    uio->uio_resid = 0;
    uio->uio_offset = total;
    progress = total;
    } else {
    #endif
    m = m_getm2(NULL, max(total + align, 1), how, MT_DATA, flags); /* 拷贝模式分配mbuf链*/
    if (m == NULL)
    return (NULL);
    m->m_data += align;

    /* Fill all mbufs with uio data and update header information. */
    for (mb = m; mb != NULL; mb = mb->m_next) {
    length = min(M_TRAILINGSPACE(mb), total – progress);

    error = uiomove(mtod(mb, void *), length, uio); /* 拷贝模式拷贝应用层数据到协议栈 */
    if (error) {
    m_freem(m);
    return (NULL);
    }

    mb->m_len = length;
    progress += length;
    if (flags & M_PKTHDR)
    m->m_pkthdr.len += length;
    }
    #ifdef FSTACK_ZC_SEND
    }
    #endif
  • ff_write()函数成功返回后,之前申请的ff_zc_mbuf结构内部mbuf链数据不需要释放,该结构可以在函数ff_zc_mbuf_get()中复用重新分配BSD的mbuf
    • 不能够再次直接在ff_zc_mbuf_wirte()使用,必须重新调用ff_zc_mbuf_get()分配新的mbuf链之后才可以继续使用

使用方式及注意事项

使用方式

该功能默认并未开启,需要通过在lib/Makefile中打开编译选项FF_ZC_SEND,并重新编译F-Stack lib 库和应用程序后才能生效。

零拷贝发送接口的使用方式与标准socket接口也有区别,具体可以参考前面的方案介绍及示例代码

注意事项

  • 使用零拷贝发送接口需要对原有应用进行修改才能接入,且并不一定有很明显的性能提升,所以默认不开启。
  • 零拷贝发送接口可以单独开启FF_ZC_SEND使用,也可以与FF_USE_PAGE_ARRAY一起开启使用。
  • 与协议栈到DPDK的零拷贝类似,此处减少的内存拷贝是否对应用性能有提升还需要结合具体的应用进行实际测试,在特定应用场景下才会有一定的性能提升,但效果并不一定很明显,比如只有2-3%左右的提升。
  • 目前struct ff_zc_mbuf *的结构是对外暴露给应用层的,可以更方便的进行测试使用,后续不排除隐藏该数据结构的可能。