引
故事产生在公元 2022 年的夏天。上帝(化名)在上线流量测试中,发现在未引入 Istio 前失常 HTTP 200 的申请,引入 Istio Gateway 后变为 HTTP 400 了。而呈现问题的流量均带有不合 HTTP 标准的 HTTP Header。如冒号前多了个 空格:
GET /headers HTTP/1.1\r\n
Host: httpbin.org\r\n
User-Agent: curl/7.68.0\r\n
Accept: */*\r\n
SpaceSuffixHeader : normalVal\r\n
在向上帝收回修改问题的申请后,“无辜”的程序员作好了应答最坏状况的打算,筹备尝试打造一条把控本人命运的诺亚方舟(希伯来语:יבת נח;英语:Noah’s Ark)。
打算 – 两艘诺亚方舟
人们议论 Istio 时,人们大多数状况其实是在议论 Envoy。而 Envoy 用的 HTTP 1.1 解释器是曾经 2 年没更新的 c 语言写的库 nodejs/http-parser。最间接的思路是,让解释器去兼容问题 HTTP Header。好,程序员关上了搜索引擎。
1 号方舟 – 让解释器兼容
如果说抉择搜索引擎是个条件问题,那么搜寻关键字的选用才是个技术 + 教训的活儿。这里不细说程序员如何搜寻了。总之,后果是被引擎带到:White spaces in header fields will cause parser failed #297
而后当然是喜忧参半地读到:
Set the
HTTP_PARSER_STRICT=0
solved my issue, thanks.
即须要在 istio-proxy / Envoy / http-parser 编译期退出下面参数,就能够兼容后带空格的 Header 名。
因为所在的厂还算大厂,有本人的基础架构部,个别大厂都会定制编译开源我的项目,而不是间接应用二进制 Release。所以程序员折腾数天,才定制编译了公司基础架构部的这个 istio-proxy,退出了 HTTP_PARSER_STRICT=0
。测试后果也确实解决了兼容性的问题。
但这个解决办法有几个问题:
- 重编译是个让基础架构部不反对前面其它问题解决的理由。容易背锅和引入比拟多未知危险
-
问题解决有个本来准则,就是管制问题自身的影响和解决方案自身的危险。防止为解决一个 bug 引入 n 个 bug 的状况。
- 如果 Istio Gateway 让问题 Header 透传了,那么前面的各层 sidecar proxy 和应用服务,也要兼容和透传这个问题 Header。危险未知。
2 号方舟 – 修改问题 Header
Envoy 自称是个可编程的 Proxy。很多人晓得,能够通过为它减少定制开发的 HTTP Filter 来实现各种性能,其中当然包含 HTTP Header 的定制和改写。
But,请仔细想想。如果你仔细读过我之前写的《逆向工程与云原生现场剖析 Part2 —— eBPF 跟踪 Istio/Envoy 之启动、监听与线程负载平衡》或者是 Envoy 原作者 Matt Klein, Lyft 的 [Envoy Internals Deep Dive – Matt Klein, Lyft (Advanced Skill Level)]:
解释出错产生在 HTTP Codec,在 HTTP Filter 之前!所以不能用 HTTP Filter。
为求证这个问题,我 gdb 和断点了 http-parser 的 http_parser_execute 函数,看 stack。gdb 的办法见《gdb 调试 istio proxy (envoy)》
HTTP Filter 不行,那么 TCP Filter 呢?实践上当然能够,能够在 Byte Buffer 传到 HTTP Codec 前,用 TCP Filter 去修改问题 Header。当然,不是简略的笼罩字节,可能要删减字节……
于是又一个抉择来了,实现 TCP Filter(下文叫 Network Filter) 有两种形式:
-
Native C++ Filter
- 绝对性能好,不须要 copy buffer。但要从新编译 Envoy。
-
WASM Filter
- 因沙箱 VM,须要 在 VM 和 Native 程序间 copy buffer,引入 cpu/ 内存应用和提早
下面也说了,不能从新编译 Envoy,可怜的程序员只能抉择 WASM Filter。
如果“无辜”的程序员是个纯架构师,只有想通了路子,写个 PPT 架构图就能够出工了,那么是个 Happy Ending。惋惜,“无辜”的程序员注定须要为“2 号方舟”的建成付出数天的无眠。木板和针子都得亲手来……
WASM Network Filter 学步
WASM 语言的抉择
编写 WASM Filter 有几种可选语言。时尚的 Rust,不愁找工的 Go,昨日黄花的 C++。无论是出于内存主动和平安思考,还是刷简历思考,最不应该抉择的都是 C++。但,“无辜”的程序员抉择了 C++。除了不值一文的情怀,还有一个深度思考后的起因:
—— 重用 Envoy 雷同的、关上兼容模式编译期配置
HTTP_PARSER_STRICT=0
的http-parser
。
要修改有问题的 HTTP Header,首先要在 Byte Buffer 中定位(或者说是解析到)Header。当然能够用更时尚的解释器。以上几种语言都有本人的 HTTP 解释器。但,谁保障这些解释器的后果和 Envoy 兼容?会不会引入新问题?那么,间接应用 Envoy 同样的解释器,是个不错的抉择。如果解释器有问题,就算不加这个 Fitler,Envoy 自身也会有问题。即根本保障不在解释器上引入新问题。
小众的 WASM Network Filter
最侥幸的程序总能够在搜索引擎 /Stackoverflow/Github 上找到一个 copy/paste 的模板代码或神 Issue workaround 而轻松实现绩效。而“晦气”的程序员往往是去解决那些没有标准答案的难题(尽管笔者喜爱后者),最初折腾本人且不肯定有绩效。
显然,网上能够找到一堆 WASM HTTP Filter 的材料和参考实现,但 WASM Network Filter 极少,有也是读一下 Buffer Bytes,做做简略统计的性能。没有一个是在 L3/4 层上批改字节流的,更别提要解释字节流上的 HTTP 了。
Proxy WASM C++ SDK
开源关上的不单单是代码,更应该是人们求假相的机会。“晦气”的程序员记得 2002 年学习 Visual C++ MFC 时,只能看到 MSDN 上的文档,而不明其所以的苦楚。
小众的 WASM Network Filter 再小众,也是 Open Source 的。不单单 SDK Open Source,接口的定义 ABI Spec 也是 Open Source。列一下手头上的重要参考:
-
Proxy WASM 接口标准 API 阐明
- https://github.com/proxy-wasm…
-
Envoy 实现 WASM 的阐明
-
https://github.com/proxy-wasm…
Proxy WASM 是个 Proxy 下应用 WASM 扩大的标准。即除了 Envoy,还有其它几个 Proxy 也反对的。
-
-
C++ SDK 实现和简略的应用文档
-
https://github.com/proxy-wasm…
包含如何编译本人的 C++ WASM Filter 实现
-
-
网上仅有的 WASM Network Fitler 例子(Rust)
- https://github.com/layer5io/w…
WASM Network Filter 设计
保持一惯格调,少谈话,多上图:
图:WASM Network Filter 设计图
没太多可说的,上面介绍一下实现。
WASM Network Filter 实现
因为各种起因,不打算 copy 所有代码上来,以下只是用为本文特地改写的伪代码来阐明。
因为应用到 https://github.com/nodejs/htt… 的源码,其实就是两个文件: http_parser.h
与 http_parser.c
。先下载并保留到新我的项目目录。假如叫 $REPAIRER_FILTER_HOME
。这个 http-parser 解释器最大的益处是无依赖和实现简略。
当初开始编写外围代码,我假如叫:$repairer_fitler.cc
#include ...
#include "proxy_wasm_intrinsics.h"
#include "http_parser.h" //from https://github.com/nodejs/http-parser
/**
在每个 Filter 配置对应一个对象实例
**/
class ExampleRootContext : public RootContext
{
public:
explicit ExampleRootContext(uint32_t id, std::string_view root_id) : RootContext(id, root_id) {}
//Fitler 启动事件
bool onStart(size_t) override
{LOG_DEBUG("ready to process streams");
return true;
}
};
而后是外围类:
/**
在每个 downstream 连贯对应一个对象实例
**/
class MainContext : public Context
{
public:
http_parser_settings settings_;
http_parser parser_;
...
// 构造函数,在每个新 downstream 连贯可用时调用。如 TLS 握手后,或 Plain text 时的 TCP 连贯后。留神,HTTP 1.1 是反对长连贯的,即这个 object 须要反对多个 Request。explicit MainContext(uint32_t id, RootContext *root) : Context(id, root)
{logInfo(std::string("new MainContext"));
// http_parser_settings_init(&settings_);
http_parser_init(&parser_, HTTP_REQUEST);
parser_.data = this;
// 注册 HTTP Parser 的回调事件
settings_ = {
//on_message_begin:
[](http_parser *parser) -> int
{MainContext *hpContext = static_cast<MainContext *>(parser->data);
return hpContext->on_message_begin();},
//on_header_field
[](http_parser *parser, const char *at, size_t length) -> int
{MainContext *hpContext = static_cast<MainContext *>(parser->data);
return hpContext->on_header_field(at, length);
},
//on_header_value
[](http_parser *parser, const char *at, size_t length) -> int
{MainContext *hpContext = static_cast<MainContext *>(parser->data);
return hpContext->on_header_value(at, length);
},
//on_headers_complete
[](http_parser *parser) -> int
{MainContext *hpContext = static_cast<MainContext *>(parser->data);
return hpContext->on_headers_complete();},
...
}
}
// 收到新 Buffer 事件,留神,一个 HTTP 申请因为网络起因,能够打散为多个 Buffer,回调屡次。FilterStatus onDownstreamData(size_t length, bool end_of_stream) override
{logInfo(std::string("onDownstreamData START"));
...
WasmDataPtr wasmDataPtr = getBufferBytes(WasmBufferType::NetworkDownstreamData, 0, length);
{
std::ostringstream out;
out << "onDownstreamData length:" << length << ",end_of_stream:" << end_of_stream;
logInfo(out.str());
logInfo(std::string("onDownstreamData Buf:\n") + wasmDataPtr->toString());
}
// 这里会执行各种 HTTP 解释,调用相干的 HTTP 解释回调函数。咱们实现了这些函数,记录下问题 Header 的地位。并修改。size_t parsedBytes = http_parser_execute(&parser_, &settings_, wasmDataPtr->data(), length); // callbacks
...
// because Envoy drain `length` size of buf require start=0 :
// see proxy-wasm-cpp-sdk proxy_wasm_api.h setBuffer()
// see proxy-wasm-cpp-host src/exports.cc set_buffer_bytes()
// see Envoy source/extensions/common/wasm/context.cc Buffer::copyFrom()
size_t start = 0;
// WasmResult setBuffer(WasmBufferType type, size_t start, size_t length, std::string_view data,
// size_t *new_size = nullptr)
// Ref. https://github.com/proxy-wasm/spec/tree/master/abi-versions/vNEXT#proxy_set_buffer
// Set content of the buffer buffer_type to the bytes (buffer_data, buffer_size), replacing size bytes, starting at offset in the existing buffer.
// setBuffer(WasmBufferType::NetworkDownstreamData, start, length, data);
setBuffer(WasmBufferType::NetworkDownstreamData, start, length, outputBuffer);
}
/**
* on HTTP Stream(Connection) closed
*/
void onDone() override { logInfo("onDone" + std::to_string(id())); }
最初注册:
static RegisterContextFactory register_ExampleContext(CONTEXT_FACTORY(MainContext),
ROOT_FACTORY(ExampleRootContext),
"my_root_id");
因为解释 Buffer,HTTP Request/Header 跨 Buffer 等状况均须要思考。还须要反对 HTTP 1.1 keepalive 长连贯。加上上次做 C++ 我的项目曾经是 17 年前的事了,这个程序员花了一周(加班)的工夫才实现了一个能够工作的原型。并且,未优化和对性能影响的测试。Sandbox VM 的实现形式注定对服务延时有影响的。可见我之前的一个剖析:
记一次 Istio 冲刺调优:
图:Flame Graph(火焰图)中的 WASM
悟
这是一个最好的年代,架构师们有各种开源组件,只须要简略粘合,就能够实现需求。
这是一个最坏的年代,开箱即用宠坏了架构师们,利用他人的货色咱们飞得很高也很自信,认为本人把握了魔法。但一个可怜踩到坑掉下时,也因为对事实的无知而重重的受伤。
我的 yysd —— Brendan Gregg 已经说过:
You never know a company (or person) until you see them on their worst day
你永远不会认清一家公司(或集体),直到你在他们最蹩脚的一天看到他们。
真正考验一个程序员或架构师的时候,不是去为一个新我的项目绘画宏伟蓝图(PPT)的时候,更不是他懂得多少新概念,新技术。而是在现有架构呈现问题时,在没有前人教训的状况下,如何在各种技术、非技术条件受限的状况下,去摸索一条解决之道,并且为解决问题而引起的新问题作好筹备。