对于 http filter,Envoy 提供了一大堆状态码,虽然每个都有不少的注释,但是依旧很头大,傻傻搞不清楚。
本文记录一下自己的理解,如有错误,欢迎指正~
http filter 是什么
Envoy 提供的 http 层的扩展机制,开发者可以通过实现 Envoy 约定的接口,在 Envoy 的处理流程中注入逻辑
比如,这两个请求阶段的接口:
1 | Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& headers, bool end_stream) |
如果想修改请求头,那就在 decodeHeaders
中修改 headers
,如果想修改请求的 body
,那就在 decodeData
中修改 data
。
本文所关注的状态码,就是这些函数的返回值,比如示例中的 FilterHeadersStatus
和 FilterDataStatus
。
Envoy 为 filter 提供了啥
流式
这个从上面的从 API 形式就可以看出来,核心是,http header 以及 http body 的每个 data 块,是一个个处理的。
异步
当一个 filter 在处理过程中,如果需要等待外部响应,也可以先反馈给 Envoy 某种状态,等 filter ready 之后再继续。
并发
这个其实很自然,因为有流式,所以,从逻辑上来说,不同的 filter 之间是存在并发的。
但是,又容易被忽视,因为 Envoy 的工作模式中,具体到某个 http 请求是工作在单线程上的,所以容易有误解。
所以,Envoy 内部有一个 filter manager 模块,目的是用于管理 filter 的运行,也可以简单理解为,一个复杂的状态机。
状态码
交代完背景,我们具体看看状态码,以及 filter 和 filter manager 的状态机运转关系
Header 状态码
以 header 的状态码为例,咱们先挨个解读一下,找找感觉
Continue
这个最简单,表示当前 filter 已经处理完毕,可以继续交给下一个 filter 处理了
StopIteration
表示 header 还不能继续交给下一个 filter 来处理
ContinueAndDontEndStream
表示 header 可以继续交给下一个 fitler 处理,但是下一个 filter 收到的 end_stream = false,也就是标记请求还未结束;以便当前 fitler 再增加 body。
StopAllIterationAndBuffer
表示 header 不能继续交给下一个 filter,并且当前 filter 也不能收到 body data。
意思是,请求中的 body data 先 filter manager 缓存起来,如果缓存大小超过了 buffer limit(一个配置值),那就直接返回 413 了。
StopAllIterationAndWatermark
同上,区别是,当缓存超过了 limit,filter manager 就会启动流控,也就是暂停从连接上读数据了。
过了一遍之后,有这么几个关键点
- StopIteration,只是先不交给下一个 filter 处理,但是并不停止从连接读数据,继续触发 body data 的处理
- header 阶段,也可以对 body data 进行写操作,可想而知,也就是 add 操作了。
- filter manager 有一个 data 的缓冲区,帮 filter 临时缓冲数据
data 状态码
再看 data 的状态码,
Continue
跟 header 类似,表示当前 filter 已经处理完毕,可以继续交给下一个 filter 处理了。
只是,如果 header 之前返回的是 StopIteration,且尚未交给下一个 fitler,那么,此时 header 也会被交给下一个 fitler 处理。
StopIterationAndBuffer
表示当前 data 不能继续交给下一个 filter,由 fitler manager 缓存起来。
并且,与 header 类似,如果达到 buffer limit,直接返回 413。
StopIterationAndWatermark
同上,只是达到 buffer limit,只是触发流控。
StopIterationNoBuffer
表示当前 data 不能继续交给下一个 filter,但是,fitler manager 也不需要缓存 data。
这里也有几个点:
如果 data 要被交给下一个 filter 处理了,header 是肯定也会被交给下一个 fitler 处理了。
我们可以把 header 理解为,一个带有特殊语义的首个 data 块,无论怎么流式处理,数据的顺序,是必须要保证的。
data 阶段多了一个 NoBuffer 的状态,这又是什么目的呢?
关键解读
有几个容易误解的地方,咱们展开聊聊
异步
为了支持 filter 的异步,fitler 可以返回 Stop 语义的状态码,这样 filter manager 不会继续后续的 filter,但是:
并不意味着整个 filter manager 都停止了,当前 filter 以及之前的 filter,还是会接受到当前请求上的数据
并发
每个 filter 接受到的数据,在处理期间,是独享的,没有并发风险,但是 filter manager 的缓冲区,只有一个,这个是有并发风险的。
因为 filter 是逻辑上并发的,但是 filter manager 只有一个,所以是存在逻辑并发风险的。
举个例子:
filter A => filter B => filter C
,这样一个处理链表
先来了第一个 data 块:filter A 和 B 反馈 continue,filter C 返回 StopIterationAndBuffer
,此时 buffer 中是 data 1,
再来第二个 data 块,filter A 返回了 StopIterationAndBuffer
,此时 buffer 中是 data 1 和 2 了,
然后 filter A 通知 filter manager 恢复处理,那么此时,filter B 看到的数据,就是 data 1 和 2 了。
显然,这并不符合预期。
也就是说,对于每个 filter 而言,如果在 data continue 之后,再返回 StopIterationAndBuffer
的话,就可能有缓冲区并发风险。
简单的理解,如果要复用 filter manager 的缓冲区,每个 filter 只有首次异步的机会。
如果需要随时异步,那怎么办呢?
解决方案,也就是 StopIterationNoBuffer
,filter 自己搞缓冲,也就不存在并发风险了。至于副作用嘛,主要就是自己多实现一些代码。
理论上来说,StopIterationNoBuffer
是最灵活的,不过也是更麻烦的,效率也会略差一点的。
个人体会
Envoy 的状态设计,为大部分常见的场景,提供了比较方便易用的设计,但是相对复杂的场景,就需要自己多实现一些逻辑了。
只是这么搞了之后,对于新人的上手门槛就高了,如果不搞懂这些状态,闭着眼睛用,也是容易踩坑的。
最后
一个小问题,MOE 作为把 MOSN(Go)塞进 Envoy 的方案,此时的开发语言已经是 Go 这种天然异步的,MOE 又提供的是什么样的开发体验呢?
MOE 近期会开源第一版,等开源之后,后面可以聊聊~