kindling
kindling copied to clipboard
[理论上的bug] Kindling 不能正确处理TCP 长连接的两个场景
Describe the bug
参见这里 对connection, stream, message 的定义。在一个TCP connection上,Kindling不能正确处理下面两种类型的通信场景
case1: 两个流的response message 逆序
case2: client发起的流包裹 server 发起的流
图示如下
case1: Client 连续向Server发送两个请求request1 和 request2,Server先响应了response2,后响应response1。 按照当前 Network.ConsumeEvent的处理逻辑:request2 报文会被merge到request1的尾部(即request2被隐藏),response1 会被丢弃,request1 和response2会被错误的配对在一起,进行 NetworkAnalyzer.distributeTraceMetric分析
case2: Client 首先向Server发送request1,Server收到request1 没有立即响应,而是向Client发送了request2, Client响应了response2 后,Server才响应response1。 按照当前 Network.ConsumeEvent 的处理逻辑:request1 和response2 会被配对,进行NetworkAnalyzer.distributeTraceMetric分析,response2 和response1 也会被配对,进行NetworkAnalyzer.distributeTraceMetric分析。错误的配对,必然导致解析失败
Additional context
这两个case,是我用kindling解析公司自研协议观察到的,我设计了对这个问题的解决方案,正在编码实现。本周我会把解决方案提交成proposal
请问贵司的自研协议是类似于HTTP/2的流式协议吗?如果是的话,对于这两个场景Kindling必须拿全每个系统调用的data
部分才能够解析识别出不同的stream
;而目前考虑到性能影响,默认只获取了200bytes。可以在这里先简单描述一下你的方案吗?
自研协议哪些特征:
- 自研协议没有frame的概念,即message 的header 和 body不被划分为多个frame传输
- 每个message 都有 stream ID
- 目前抓包来看,message 和 syscall的关系如下
- 几乎全部,一个message 对应一个syscall
- 存在一个message需要经过两次syscall 才接收完成的情况
- 不存在多个message 被合并到一个syscall的情况
- 自研协议和http/2 一样,是binary protocol,不是 ASCII protocol。参见
还需要我补充自研协议的哪些信息?
场景根因
对于这两个场景Kindling必须拿全每个系统调用的
data
部分才能够解析识别出不同的stream
我不认为,kindling不能正确处理这两个场景是因为暂定不拿全data。 我认为是何时进行 message pairing(报文配对)的问题,如何做报文配对的问题。 我认为 issue277 也是报文配对时机不对导致的。
描述方案
NetworkAnalyzer 为了把KindlingEvent 转化为DataGroup,我认为是做了三件事:报文配对,协议识别,协议解析。协议识别完全依赖协议解析,方案暂时不考虑协议识别。当前的做法是,先报文配对,后协议解析。我的方案的核心是先协议解析,后根据解析结果进行报文配对去生成一个request-response的相关指标。协议解析本质上也是对request 和response message 分别的解析,message解析并不依赖报文配对(不认同,我们可以讨论)。但先报文配对,在如上的场景下,就造成若干个message 非法,但其实它们都合法,是报文配对错了。
我设想的方案中是需要拿全data的。有些协议可能会出现多个message 合并在一个syscall 被捕获,拿全data对kindling正确处理这种协议至关重要。同时,kindling不应该对用户态程序如何做Network syscall 做假设。
社区可能会担心,采用我描述的方案后,如何做协议识别。我能给出一个不太成熟的方案。另外我认为,kindling 需要对协议识别做多大程度的保证,是一个需要思考和讨论的问题。
性能影响
我想测试一下:解除目前data只能200字节长的限制会对kindling 的性能造成多大影响。 我想问,我应该对 falco-lib, kindling-probe, kindling-collector 的源代码做哪些改动?我主观认为解除限制,性能影响不会很大。因为解除限制,增加的开销只在于增大了event data 在各个组件间传递时的内存拷贝,中间如果是指针传递,内存拷贝其实是免了的。同时解除限制完全不会影响NetworkAnalyzer之后的分析流程。出于性能考虑
,是因为已经观察到拿全数据带来很大性能开销吗?或有什么样经验知识?希望提供数据或资料参考。
根据你对特征的描述,我理解这个协议不是类似HTTP/2的通信模型,所以我上面的假设不成立,这两个场景不是因为拿不全data导致的。
但是根据你在另一个issue #436 中的描述:
通信时,每个stream 有一个stream ID(简记为sid),严格遵守一问一答的方式,即client必须收到前一个stream 的response,才会发出后一个stream的request。
在 case1 中,对于同一个TCP Stream,Client 是如何连续向Server发送两个请求request1 和 request2的?
#436 和 这个issue确实是说的同一个协议。#436 是一个测试场景,强制让协议工作在一问一答模式下的。这个协议是允许case1 场景存在的,case1 场景的存在是这个协议的一个优势。
了解了。
关于性能影响,目前从probe到collector存在事件的内存拷贝,这部分能否全部转换为指针传递还要进一步研究。另外,事件从底层ring buffer中拿出来时也存在拷贝,所以falco/libs设置了snaplen
这个参数。
性能影响有两方面,一个是CPU会升高,一个是内存占用会升高(因为要缓存系统调用中的data),我们之前测试过获取5000bytes相比于获取80bytes,CPU会有升高一些,这也是为什么falco/libs会设置这个参数的原因。
好奇问一下,社区认为流式协议的特征是什么?为什么我所说的自研协议不是流式协议?
首先明确一个概念,所谓“流式协议”并不是一个标准的术语(有人会说TCP是流式协议,但更准确的说法是面向流的协议),Kindling使用这个词主要用来描述基于HTTP/2实现的协议。HTTP/2支持多路复用,可以将请求/响应分解为frames在同一个连接上乱序发送,这种现象反映在系统调用上就是,在一个系统调用中存在多个请求或响应的字节。对于这种模式的通信协议,Kindling目前无法解析,如之前所说,要解析这种协议的前提是获取完整的报文字节。
回到问题本身:
社区认为流式协议的特征是什么?
我并不敢给“流式协议”这个词下定义,所以前面讨论Magic协议时都没有直接用这个词,而是带上了HTTP/2:
请问贵司的自研协议是类似于HTTP/2的流式协议吗? 我理解这个协议不是类似HTTP/2的通信模型。
这里的关键点是,Magic协议是否拥有类似于HTTP/2的分拆frame和乱序发送的特性,这个特性会导致在一个系统调用中存在多个请求或响应的字节,并且他们还可能是乱序的。
为什么我所说的自研协议不是流式协议?
如前面所述,准确的说,Magic协议不是类似于HTTP/2的通信模型,因为从你描述的特征来看,Magic协议不会出现在一个系统调用中存在多个请求或响应的字节的问题。
综上,关于“Magic协议是否是流式协议”这个问题本身其实并不重要,重要的是Magic协议中是否存在一个系统调用中存在多个请求/响应,这决定了目前Kindling的协议解析模型是否适用。
明白了,下周我找Magic协议的开发者仔细确认这个问题,“是否允许多个message出现在同一个系统调用中”。我现在能确定,Magic协议不存在单个message 被拆分为多个frame,也就不存在多个message的frame组合出现在一个系统调用中。
Magic协议允许每个message出现在同一个系统调用中,而且实际应用场景“这种组包”频繁发生
如果是这样的话,要想基于报文内容来解析协议,就不得不获取完整的报文,这可能对性能有较大影响。在获取完整报文的基础上还需要调整解析代码的模型。
另一个方案是基于uprobe来实现该协议的识别和关联。请问这个协议都有哪些编程语言在使用?
Rust,Python,C++
uprobe比较麻烦的地方在于需要针对不同的编程语言不同的API做适配,然后再对获取到的事件做关联;由于添加uprobe后会频繁触发上下文切换,可能会对用户程序造成影响,因此uprobe只在确认“解析报文无法识别协议“时才会被使用。
还有一个问题要确认,Magic协议是否会像HTTP/2一样在连接建立时建立Header编解码表?这个表的存在导致HTTP/2协议几乎不能用“解析报文”的方式来识别,因为在探针启动时如果连接已经建立,则探针无法获取到这个表,也就无法解析后续请求。
根据你之前的描述,应该不存在这个问题,所以我建议你可以尝试将获取报文大小的环境变量SNAPLEN
调整到最大65000,观察一下是否获取到了完整的协议内容和性能表现,如果可以接受的话,我们再一起修改解析模型来适配你的场景。
基于Case1 Kindling本身是支持的,DNS解析就是基于该场景,详见parseMultipleRequests():network_analyzer.go 基于Case2 Kindling也考虑过方案,基于FD与协议强绑定,不同协议采用不同的模型解析。
- Kindling冷启动接收消息试着基于不同协议识别该FD数据,匹配成功后进行协议绑定,后续数据基于该协议进行解析
- 考虑到数据拆包情况,第一个包携带可解析的重要信息,长度、标识、关键指标;后续包只是补充数据,由于获取包时限定长度截断导致第二个、第三个、第N个包无法进行合并然后统一解析。
- 解析出的request / response放置在相应队列中等待新response/request进行匹配
该方案的局限性
- FD必须只有1个协议,不能复用
- 由于Kindling探针包截断的局限性,第一个数据包需携带长度信息,用于拼接拆包数据
由于HTTP2协议编码问题导致冷启动的Kindling无法获取相应数据进行解析采用uprobe分析,网关复用FD,不是所有协议都有长度等因素,Case2的方案也就暂时没有启用
@hocktea214 case1: ConsumeEvent接收序列 request1, request2, response2, response1 之后,报文配对方法不是
request2 报文会被merge到request1的尾部(即request2被隐藏),response1 会被丢弃,request1 和response2会被错误的配对在一起
我搞错了,应该是,request2 报文会被merge到request1的尾部,response1 报文会被merge到response2的尾部。所以 Kindling 解析DNS确实正确的处理了该场景。
想到一种新场景(记为case3),和case1类似,但Kindling解析DNS时是无法正确处理的。可能DNS不会遇到该场景,我现在也无法确信Magic是否存在该场景。该场景ConsumeEvent接收的序列是,request1, request2, response1, request3, response2, response3。
Kindling如果允许前一次调用distributeTraceMetric 时将未完成配对的request保留下来,供下一次调用distributeTraceMetric时response与其配对,可以解决这个问题
您介绍的,针对case2的方案
解析出的request / response放置在相应队列中等待新response/request进行匹配
这需要改变目前的报文配对规则吧? 您能更详细描述针对case2的方案吗?
@dxsup 看起来如果换用uprobe,很大开发量,而且要求Kindling和Magic的实现版本绑定。
Magic协议没有Header编解码表。
我现在的考虑是,允许不拿全数据,丢弃因此受到影响的request/response message(不让他们生成DataGroup)。Kindling 最后输出的指标比真实值小一点,我想也是可以接收的。
@dxsup 看起来如果换用uprobe,很大开发量,而且要求Kindling和Magic的实现版本绑定。
Magic协议没有Header编解码表。
我现在的考虑是,允许不拿全数据,丢弃因此受到影响的request/response message(不让他们生成DataGroup)。Kindling 最后输出的指标比真实值小一点,我想也是可以接收的。
这个要测试一下看看究竟有多大的差距,如果能从中发现一些概率关系就可以这么做。但使用这个指标时必须要明确标明是近似值,否则如果基于错误假设实现后续功能会出现连环问题。
@LambertZhaglog 现已基于流式协议提供一版方案 协议开发流程.pdf
请求先基于连接缓存,当该连接为第一次建立,通过Check() API识别是否流式协议,并匹配该协议;否则采用原MessagePair模型。
当识别为流式协议后,以后所有请求/响应都实时解析分析
- ParseHead()获取长度、streamId、是否请求(双工场景)
- 通过streamId获取请求
- 存在oneway场景,此时如果接收到的是request,则将缓存中的request先发送,再解析接收的request
- 基于长度判断是否存在粘包场景(一个系统调用发送多个请求/响应报文)
- 存在粘包则基于长度拆包并解析报文
- 长度不足则等待后续报文
- 长度匹配则解析该完整报文
- 由于流式协议响应报文缺失请求报文的API Key等信息,此时在解析前会先MergeRequest()将请求解析的属性合并到响应中
- ParsePayload()解析报文
- 请求/响应都存在则返回请求响应
- 只有请求则缓存解析出的请求属性
- 只有响应则丢弃该请求(存在探针冷启动时只获取到响应数据)
此外,对于双工场景,同时存在双向通信数据,需实现IsRequest() 和 IsReverse()方法即可校准该连接数据的方向