Tcpdump 从 TCP_option_address 中根据真实 IP 过滤

这几天做了一个集群的迁移,我们搭建了一个新的集群,然后更新了 DNS 让域名访问新的集群,准备给老集群下线。下线的时候发现仍然有一部分请求到了老集群。看来是它们用了长链接,并没有根据新的 DNS 记录将请求发给新的地址。找到这些发送请求的客户端是谁,费了一番功夫,这里记录一下。

我们的架构如下:

 

LVS 是一个四层代理,将请求转发到 Nginx,一个七层代理,然后转发给后端应用。

现在的情况是:

  1. Nginx 转发给后端的 real server,real server 是同一组,即,从后端 real server 这一层,不知道是谁还通过旧的集群进行访问;用户来源的名字写在了一个特殊的 header 中;
  2. 我们可以从 Nginx 的 access.log 找到这些请求的真实 IP,但是这是不够的,因为用户请求访问走了 NAT 进行了地址转换,我们仍然不知道这个 IP 是哪一个团队的;

但是我们有了 IP,有了 Header,只要通过 tcpdump -A host <ip> 就可以将用户请求的 HTTP 内容全部打印出来,也就包括 header,就能知道是谁了。

这里有一个问题,就是 Nginx 在 LVS 后面,我们在 Nginx 上面进行 TCP dump,看到的是 LVS 的地址,而不是用户的真实地址。

LVS 里面使用了一个模块,叫做 TOA, TCP_option_address. 即 LVS 进行转发的时候,会将用户的真实 IP 写在 TCP 的 option 字段中。如果要根据真实地址进行 tcpdump,我们要过滤的是这个字段而不是原生的 host 地址。

注意这个 TOA 只会在 SYN 握手阶段设置,然后双方会把这个信息记录在内存里面,后面的 TCP 通讯就不会一直带上这个信息了。所以,要找到用户的真实 IP,必须过滤 TCP 的 SYN 包。

现在需求就变成了:用 tcpdump 在 Nginx 上,filter 出来 IP 为 A 的请求的 HTTP header,以便根据 header 中的信息找到调用来源的团队,和他们沟通重建连接的问题。

通过阅读 TOA 的源代码可以发现,代码中还原用户真实的 IP 地址的方式:遍历 TCP Option 字段,直到找到 option code 为 254(TCP option 254 是一个实验字段,RFC3692-style Experiment 2 (also improperly used for shipping products), opsize 固定为 8 的 option,就读 16 bit load成端口,读 32 bit load 成 IP.

所以只要针对这个 tcp option filter 即可。

需要注意的是,tcpoption 对于 tcpdump 来说不被理解,所以需要转成 hex 来匹配。我在 xbin 上面放了几个工具:

匹配 IP 是 120.120.111.111 的话可以用下面的语句:

其中:

  • tcp[tcpflags] & tcp-syn != 0是取 SYN 的 flag 为 1,即只抓取 SYN 包;
  • tcp[24:4]=0x78786F6F 是匹配 tcp option 字段中的 IP

如果要匹配 port 的话,就偏移两位即可:

 

这样做其实还有一些问题:

  • 上面说过 tcp option 里面有 IP 只发生在握手阶段,所以这样是抓不到后续的数据的。没找到 tcpdump follow tcp stream 的方法,可能还是全 dump 下来,然后去 wireshark 里面过滤比较好。不过我们这里 SYNACK 的时候已经有 HTTP 请求内容了,所以只过滤握手第三个包也足够;
  • 还有一个问题是,看 TOA 的代码,它是遍历所有的 TCP option,根据 option Kind + option size 去查是不是自己的 port + IP 字段。也就是说,port + IP 未必一定是在 TCP option 的开头,偏移未必一定是 tcp[24:4]。一个解决方法是把可能的都 dump 下来,然后用 wireshark 过滤,wireshark 支持搜索全部 TCP 包内容包含某个 hex value。比如:tcp.options contains fe:08:78:78:6f:6f.
  • 因为我们要找到的是长连接的用户,所以这些连接我们是没有 SYN 包的。所以本文描述的方法其实对本文提出的问题……无效。现实中我们通过其他的方法找到用户了。这篇文章权当是看个热闹吧。
 

用 BPF 动态追踪 Python 程序

最近在学习 BPF,这是一种目前比较流行的动态追踪技术,简单来说,它允许我们在不中断目前正在运行的程序的情况下,插入一段代码随着程序一起执行。比如你想知道某一个函数每次 return 的值是什么,就可以写一段 BPF 程序,把每次 return 的值给打印出来;然后把这个函数 hook 到函数调用的地方,这样,程序在每次调用到这个函数的时候都会执行到我们的 BPF 程序。

这种不终止程序,能去观测程序的运行时的技术是很有用的。

credit: https://www.brendangregg.com/ebpf.html

它的原理大致上是:

  1. 找到要观测函数调用的地址(所以用 BPF 的时候我们通常需要带 debug 符号的 binary),将这个地址的指令换成 int3,即一个 break,把原来应该执行的指令保存起来;
  2. 当程序执行到这里的时候,不是直接去调用函数了,而是发现这里是一个 uprobe handler,然后去执行我们定义的 handler;
  3. 原来的程序继续执行;
  4. 当我们停止 probe 的时候,把原来保存的指令复原。

例子:这行程序可以把系统中所有执行的命令打印出来 bpftrace -e 'tracepoint:syscalls:sys_enter_execve { join(args->argv); }'. 原理就是,每次执行 execve 的时候都会执行到我们定义的 join (print).

bpftrace exmaple

bpftrace 是一个命令行程序,它的语法类似 awk,做的事情就是:将我们写的程序(这里的程序指的是 bpftrace 定义的语法,而不是 BPF 程序)编译成 BPF 程序,然后 attach 到相应的事件上。

通过上面的描述可以看出,使用这个方法来追踪程序所需要的必备条件是:

  1. Kernel 要支持 BPF;(4.19+);
  2. 有 binary 可以追踪;

像 Python 这种解释型语言,binary 其实是一个解释器,解释器里面才是运行的 Python 代码。如果按照上述方法,追踪的其实是解释器的代码而不是我们的 Python 应用的代码。

Python Interpreter

使用 USDT 追踪 Python 程序

一个解决方法是使用 USDT 追踪。

USDT 全称是 User-level statically defined tracing. 是在程序的用户态定义的追踪点。Python 解释器为我们提前定义好了一些追踪点(可以叫做 probe,或者 marker),这些追踪点默认是不激活的,所以对性能丝毫没有影响。要追踪的时候可以激活其中的几个追踪点,然后将其提供的 arguments 打印出来。

能够使用 USDT 有几个前提:

  1. Kernel 要支持 BPF;(4.19+); (其实使用 Dtrace 或者 systemtap 也可以)
  2. Python 编译的时候要打开 trace 的支持 (编译的时候有 –with-dtrace 选项),我的几个服务器中,Ubuntu 上面的 Python3.10 是开了的,Fedora 上面的 Python 3.10 貌似没有开。

使用 USDT 追踪 Python 程序

首先需要安装 bpftrace. 安装说明可以参考这里

通过这个命令可以列出 binary 中的 USDT markers: bpftrace -l 'usdt:/usr/bin/python3:*'

可以看到目前支持的 markers 不多,一共也就 8 个。

先拿一个来试一下:执行这个命令可以打印出来 Python 每次函数调用的时间、源代码的地址,函数名字,和行号:

bpftrace -e 'usdt:/usr/bin/python3.10:function__entry {time("%H:%M:%S"); printf(" line filename=%s, funcname=%s, lineno=%d\n", str(arg0), str(arg1), arg2);}' -p 15552

其中,-p 15552 是正在运行的 Python 程序的 pid,-e 后面跟的就是 bpftrace 的代码,很像 awk,第一个是 probe,{} 里面是代码,主要要两行,第一行打印出来时间,第二行打印出来 function__entry 这个 probe 提供的三个参数。(需要稍微注意的是,字符串参数需要用 bpftrace 内置的 str() 函数打印出来字符串,否则的话打印出来的是 char * 的地址;另外要注意虽然 Python 文档中说的这三个参数是 $arg1 $arg2 $arg3,但是实际打印的时候,应该使用的是 arg0, arg1, arg2.)

效果如下:

其他的几个 marker 作用是:

  • function__entry: 函数入口
  • function__return: 函数退出的时候
  • line: 每执行一行 Python 代码都会触发
  • gc__start和gc__done: gc 开始和结束的时候触发
  • import__find__load__start 和 import__find__load__done:import 开始和结束的时候触发
  • audit:sys.audit 调用的时候触发

可以看到,当前支持的并不多,而且从这几个 marker 可以看出,大部分的作用是让你知道 Python 解释器正在干啥,而不是 debug。如果遇到性能问题的话可以通过这个解决,但是如果遇到逻辑错误,帮不上太大的忙。比如你无法通过 usdt 看到某一个变量在某个时刻的值。

USDT 的原理

USDT 工作的原理和上文提到的 uprobe 差不多,激活的时候会改变原来执行的指令,插入 int3 去执行我们的 handler.

Seeing is believing.

我们可以通过一些工具来观察它执行的原理。

function__entry 为例,我们先找到它在 Binary 中的 marker, 在 .note.stapsdt 里面:

可以看到我们刚刚使用的 function__entry 的位置是 0x000000000005d839.

然后我们找到这个位置的指令。

`gdb -p 17577` 去 attach 到正在运行的这个 pid 上。然后运行 info proc mappings dump 出来地址,可以看到 Offset 0x0 的位置是 0x55bfcc88d000.

那么 function__entry 的位置应该是 0x000000000005d839 + 0x55bfcc88d000.

下面用 disas 命令可以 dump 出来这段地址的指令:

查找 0x55bfcc8ea839 这个地址的指令:

发现是一个 nop,即什么都不做。所以这就是为什么上文说,在 usdt 不开启的时候,对性能是完全没有损失的。只是多了个一个什么都不做的占位指令而已。

下面使用之前的命令去 attach 这个 usdt probe:

然后重新看一下 0x55bfcc8ea839 这个位置的指令:

可以看到这个位置的指令已经变成了 int3. 当程序(解释器)执行到这里的时候,kernel 就会执行用 usdt 提供的变量去执行我们的 BPF 程序。当 BPF probe 退出的时候,int3 会被恢复成 nop.

更多的可能?

在网上查询和 Python 有关的 BPF 内容大部分都是“如何通过 Python(BCC)来使用 BPF”,而不是“如何用 BPF 去 profile Python 代码”,可能在解释型语言方面用的不多吧。

USDT 有很大的局限,就是只能使用 Python 解释器定义的几种 probes, 目前能做到的基本上是看看解释器正在干嘛,而不能看到一些具体的变量的参数,解释器的状态等等。如果要做到更加精细的动态追踪,目前有两个想法:

  1. 用 uprobe 去追踪 Python 解释器。不过大部分都是 PyObject 指针,需要了解解释器的工作原理,比较复杂;
  2. 自定义更多的 USDT。通过 python-stapsdt 这个库可以在 Python 程序中插入更多的 USDT marker。但是我觉得意义也不大:我们要解决的问题,往往是不知道问题出在哪里了,你要解决问题的时候,又不能停止程序,再想起来要插入 marker,就晚了。换句话将,既然知道一个地方可能会出问题,可观测行非常重要,那么为什么不直接打印日志呢?
 

2022 年的总结

2022 年终于过去了,对于大多数人来说是糟心的一年,但我觉得也有一些指的记录的地方。

今年完成的最大的事情是10月份举办了婚礼。前前后后几乎策划了一年多,所幸举办的还算圆满。办完之后又办了很多手续,飞回新加坡回到工作中,本来是年底旅游的好日子,却几乎没出去旅游,躺平休息了 2 个多月。(办完婚礼之后觉得工作简直太轻松了)今后有了另一个身份,做一个好丈夫!

今年做的一些 side projects: 首先是 xbin.io, 时间太长,我都以为是去年做的东西了。这个项目的初衷是满足我自己的一些需求,现在看目的也达到了,个人对现在的形态比较满意,使用率也还可以。在新加坡使用 xbin 的 latency 是 5ms (ping RTT),如果不是在国外公司,自己是不会有这个想法做这么个东西的。

iredis 在今年几乎没有什么大的改变,只是在处理一些用户的 issue 和 bug. iredis 一直有一个问题,就是所有命令的补全都是我自己编入的,每一次 redis-server 有什么 command 的变更,我都要跟进,否则的话自动补全和校验系统就识别不出来新的命令,或者同一个命令的新的语法。我一直想把它替换成根据 redis-doc 中的文档自动补全的。现在这个文档已经越来越规范了,也许现在是时候迁移到自动生成的补全系统了。下一年花一些时间钻研一下语法树和补全系统,争取能完成这个。

监控系统。做 SRE 的这段时间或多或少都在和监控系统打交道,今年为了解决我们团队遇到的问题,我从头搭建了一套监控系统,基于 VictoriaMetrics 上的,花了很多时间阅读他们的文档,也提交了一些 PR。今年还在 PromCon 上面分享了我们的一些经验。大部分的使用问题已经解决了,还有一个长久以来一直没有解决的问题:大规模的 recording rules, 比如,计算所有容器的 CPU 使用率。本质上,监控系统的数据是一个  OLTP 的问题,大部分场景只需要看到实时的数据就足够了,而且,也很少需要看到所有的 labels 加在一起的维度(只是看某一个 application)。但是我们有一些场景是需要对整个 AZ 做聚合,这有点像 OLAP 了。目前的解决方案是简单地作为一个客户端,去已有的数据里面查出来,然后做整体的计算,存储新的值。这样有很多问题:速度慢;占用太多资源。这个链路是有一些浪费的,raw metrics 首先被收集起来,然后存储到磁盘中,聚合进程再查出来,通过网络拿回来,再进行计算,一来一回消耗了很多磁盘、网络和 CPU 的资源。我在想能否直接让采集端发送 metrics 到聚合段,跳过读写磁盘的逻辑(原来 raw metrics 保存的链路是不变的)。这样或许可以提高一些性能。今年或许可以尝试一下。

做监控的时候也开源了一些项目:

  • promqlpy: 一个 Python 库,可以解析 MetricsQL/PromQL 的语法;
  • mepe: 一个命令行工具,可以 summary 应用暴露出来的 metrics,方便配置监控;
  • metrics-render: 一个 Python 服务,可以根据 url 渲染出来 metrics 的图标,GET 请求,返回 png,这个库还存在一些问题;
  • prometheus-http-sd: 一个给 Prometheus 的监控服务发现系统,支持 yaml/json,可用 Python 脚本方便地对接其他的系统;

这些完成的项目,没有完成的项目也有很多。反思这一年,我发现自己很大一个问题,就是学到什么东西之后急于投入使用,会有很多不成熟的想法,想实验一下行不行,于是会花很多时间做可行性调研,最后可能确定自己的想法是可行的,或者不可行的。但是已经并不重要了,自己这时基本上已经没有激情去实现了…… 这样就花了很多时间,但实际并没有什么产出。好处是可以有一些更有意思的想法,坏处是浪费时间。

所以今年就克制一下自己,除非工作必要,就不开新坑了。要学习的东西虚心去学习,不要急于卖弄学到的东西做出什么来证明自己的能力。

写到这里要穿帮一下,最近几年年终总结没有在年底准时写出来,是因为懒。今年是因为得了新冠。也不知道这几年在新加坡是怎么躲过去的,最近才得。今天(1月12日)终于算是没有症状了,于是开始继续写这篇文章。

对于去年的总结就到这里吧,新的一年,计划如下:

  1. 锻炼身体。年底回到新加坡之后买了两辆自行车跟太太一起到处骑,因为是折叠车,可以用公共交通蛙跳到各个地方去骑车,非常方便。新年就铁人三项:骑车、游泳、跑步,锻炼一个健康的身体吧。
  2. 打字训练:目前打字的速度是50 words per minute. 希望纠正自己的指法,速度提升到至少 70 words per minute.
  3. 学习:年底开始读一些 eBPF 的书,今年学习一下网络、Linux、CS 基础的内容,多总结,希望多写几篇博客。

最近有一个想法:假如喜欢编程这件事情并且想长久地坚持下去的话,比如 30 年,就会发现有些事情是不重要的,有些事情是重要的。比如一年工作的绩效考评,某一年的晋升,等等,放到30年里面,就不那么重要了。有一些事情是重要的,比如花1年时间熟练使用了一个高效的代码编辑器,比如提高代码的输入速度,比如掌握了画出精美的图片的技能,比如能写出通俗易懂的文档和博客的技巧,放到30年的编程生涯中,对于工作和个人的成长就很重要了。

这么一想,打算花 30 年去做一件事情,很多事情就会显的不那么急。我们就会有很多时间去寻找机会,也有很多时间去训练那种长远看来有益的事情。对于一些急功近利的事情也就看的不那么重要了。

说起来画图,我寻找合适的画图工具很多年了。尝试过 dot,(我还是 dot in Jupyter 的作者),OmniGraffle,D2,Mermaid, PlantUML 等等,还是没有一个满意的。就像一些数据库 ER 图 for dev, figma for design, 还缺少一个工具 for SRE. 对于我来说,这个工具应该是:

  1. 基于 text 的,text to diagram
  2. 命令式的,像编程语言一样描述动作。而不是声明式的
  3. 用户在使用的时候,应该快速的将所了解的事实通过这个语言表达出来,而不应该去考虑布局中每一个框的位置和排放,应该减少用户花在画图上的心智负担,将更多的精力放在所要表达的内容上面

过去一年也参考了很多其他的画图工具,也读了一些 DSL 设计有关的论文,今年看能不能把这个语言的设计实现出来。

就写这么多吧,杂七杂八写了很多不相关的东西。


2023年1月12日更新:上文中提到的这个 streaming aggregation metrics 的想法,生病期间朋友告诉我已经 VictoriaMetrics 官方已经实现了。

其他的年终总结列表:

  1. 2013年
  2. 2014年
  3. 2015年
  4. 2016年
  5. 2017年
  6. 2018年
  7. 2019年
  8. 2020年
  9. 2021年
  10. 2022年
  11. 2023年
  12. 2024年
 

记录一次问题排查的故事

今年工作中发生的一个问题,因为太简单了,觉得不值得记录。今天读 plantegg 的一篇文章,想起来这件事。技术上很简单,但是故事本身还是挺有意思的。这里尽量客观的记录一下事情经过,因为是当事人,就不做评论了。

故事的起因是,我们提供了一个 HTTP 服务,给不同于我们部门的团队使用,这个服务有些复杂,它本身提供的是 gRPC 服务,但是我们为了给外部不同技术栈的团队使用,做了一个 HTTP 转 gRPC,其他的团队通过公网调用这个 HTTP 服务。

HTTP 再前面就是公司的通用网关了,所以集团外其他用户访问我们的服务链路是 公网 -> 4层网关 -> 7层网关 -> HTTP 转 gRPC 服务 -> 服务本身。

然后有一个 BU,他们说调用我们的服务请求并发提不上去,原因是他们那边的 NAT 端口耗尽了。从他们那边访问这个服务的出口是 客户端 -> NAT 设备 -> 公网 -> … 因为我们只在公网上暴露了 2 个 IP,TCP 的五元组里面 4 个基本已经固定了,2IP+协议+目的端口,所以只有他们 NAT 的端口是一个变量,很快就到了瓶颈。

于是他们工单给我们,要求我们在公网上暴露第二个 IP,以便可以支持更多的 TCP 连接。我们内部讨论之后拒绝了,要求他们使用 HTTP 长链接来调用,而不是短连接。因为他们是作为客户端连续并发调用多次请求,完全是长链接的场景。

第一天,他们测试使用长链接,但是 QPS 高不上去,甚至比原来还低。然后他们让我查一下这个链路上支持不支持长链接,是不是我们的配置有问题。我明确回复支持。

然后他们要求我抓一下网关的包,确认可以支持长链接。我拒绝了。表示对方要先证明不支持长链接,我再去排查。

然后他们继续找经过的中间件团队,要求他们挨个检查是否中间有丢失信息。群里已经有20多个人了,包括对方自己的 NAT 团队,我们的网关团队,我们的 gRPC 团队和服务团队。

第二天,依然要求我们这边去抓包。我依然拒绝在对方没有证明我们这边存在问题的情况系去帮忙排查。然后提供了一个 curl,这个 curl 可以使用同一个 tcp 连接发送 3个请求,可以明确证明链路上都是支持长连接的。命令和输出大体如下:

但是并没有人去运行,这个群里多了很多级别更高的人物。

第三天一早,群里就要开会,拉了很多大佬,要求我加入帮忙排查,我依然拒绝了,我已经证明我们这边是没有问题的,如果要我排查我们的问题,需要对方先证明我们这边存在问题。然后让对方跑一下我昨天发的 curl ,看一下长连接到底可以不可以用。

有人去他们的程序运行的环境中跑了一下,从这个结果可以证明,所有的中间件都没有问题,大概率是他们的程序代码有问题。

下午,定位到 HTTP SDK 的客户端的参数用错了,程序会频繁关闭 TCP 连接。

 

pngpaste – | tesseract stdin stdout

总是有人喜欢贴截图而不是文字,我的工作又经常要求跟客户要他们的 trace id 来排查问题。为了可以少说几句话节省时间,可以用下面的 alias:

pngpaste 的作用是把剪切板的内容输出到 stdout 中。

tesseract 的作用是识别 stdin 中的图片并且输出到 stdout 中。

使用方法是,将图片右键复制到剪切板,然后到终端上执行命令 pocr

识别率非常高,并且 pocr | grep abc 可以接后续的命令来处理图片中的文字。

二者在 Mac 上都可以通过 brew 安装。