
引
故事發(fā)生在公元 2022 年的夏天。上帝(化名)在上線流量測試中,發(fā)現(xiàn)在未引入 Istio 前正常 HTTP 200 的請求,引入 Istio Gateway 后變?yōu)?HTTP 400 了。而出現(xiàn)問題的流量均帶有不合 HTTP 規(guī)范的 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
在向上帝發(fā)出修正問題的請求后,“無辜”的程序員作好了應(yīng)對最壞情況的打算,準(zhǔn)備嘗試打造一條把控自己命運的諾亞方舟(希伯來語:??? ??;英語:Noah's Ark)。
計劃 - 兩艘諾亞方舟
人們談?wù)?Istio 時,人們大多數(shù)情況其實是在談?wù)?Envoy。而 Envoy 用的 HTTP 1.1 解釋器是已經(jīng) 2 年沒更新的 c 語言寫的庫 nodejs/http-parser 。最直接的思路是,讓解釋器去兼容問題 HTTP Header。好,程序員打開了搜索引擎。
1號方舟 - 讓解釋器兼容
如果說選擇搜索引擎是個條件問題,那么搜索關(guān)鍵字的選用才是個技術(shù)+經(jīng)驗的活兒。這里不細說程序員如何搜索了??傊?,結(jié)果是被引擎帶到:White spaces in header fields will cause parser failed #297
然后當(dāng)然是喜憂參半地讀到:
Set the
HTTP_PARSER_STRICT=0solved my issue, thanks.
即需要在 istio-proxy / Envoy / http-parser 編譯期加入上面參數(shù),就可以兼容后帶空格的 Header 名。
由于所在的廠還算大廠,有自己的基礎(chǔ)架構(gòu)部,一般大廠都會定制編譯開源項目,而不是直接使用二進制 Release。所以程序員折騰數(shù)天,才定制編譯了公司基礎(chǔ)架構(gòu)部的這個 istio-proxy,加入了 HTTP_PARSER_STRICT=0。測試結(jié)果也的確解決了兼容性的問題。
但這個解決方法有幾個問題:
- 重編譯是個讓基礎(chǔ)架構(gòu)部不支持后面其它問題解決的理由。容易背鍋和引入比較多未知風(fēng)險
- 問題解決有個原本原則,就是控制問題本身的影響和解決方案本身的風(fēng)險。避免為解決一個 bug 引入 n 個 bug 的情況。
- 如果 Istio Gateway 讓問題 Header 透傳了,那么后面的各層 sidecar proxy 和應(yīng)用服務(wù),也要兼容和透傳這個問題 Header。風(fēng)險未知。
2號方舟 - 修正問題 Header
Envoy 自稱是個可編程的 Proxy。很多人知道,可以通過為它增加定制開發(fā)的 HTTP Filter 來實現(xiàn)各種功能,其中當(dāng)然包括 HTTP Header 的定制和改寫。
But,請細心想想。如果你細心讀過我之前寫的《逆向工程與云原生現(xiàn)場分析 Part2 —— eBPF 跟蹤 Istio/Envoy 之啟動、監(jiān)聽與線程負載均衡》 或者是 Envoy 原作者 Matt Klein, Lyft 的 [Envoy Internals Deep Dive - Matt Klein, Lyft (Advanced Skill Level)]:

解釋出錯發(fā)生在 HTTP Codec,在 HTTP Filter 之前!所以不能用 HTTP Filter。
為求證這個問題,我 gdb 和斷點了 http-parser 的 http_parser_execute 函數(shù),看 stack。gdb 的方法見 《gdb 調(diào)試 istio proxy (envoy)》
HTTP Filter 不行,那么 TCP Filter 呢?理論上當(dāng)然可以,可以在 Byte Buffer 傳到 HTTP Codec 前,用 TCP Filter 去修正問題 Header。當(dāng)然,不是簡單的覆蓋字節(jié),可能要刪減字節(jié)……
于是又一個選擇來了,實現(xiàn) TCP Filter(下文叫Network Filter) 有兩種方式:
- Native C++ Filter
- 相對性能好,不需要 copy buffer。但要重新編譯 Envoy。
- WASM Filter
- 因沙箱VM,需要 在 VM 和 Native 程序間 copy buffer,引入 cpu/內(nèi)存使用和延遲
上面也說了,不能重新編譯 Envoy,可憐的程序員只能選擇 WASM Filter。
如果“無辜”的程序員是個純架構(gòu)師,只要想通了路子,寫個 PPT架構(gòu)圖就可以收工了,那么是個 Happy Ending??上В盁o辜”的程序員注定需要為“2號方舟”的建成付出數(shù)天的無眠。木板和針子都得親手來……
WASM Network Filter 學(xué)步
WASM 語言的選擇
編寫 WASM Filter 有幾種可選語言。時髦的 Rust,不愁找工的 Go,昨日黃花的 C++。無論是出于內(nèi)存自動和安全考慮,還是刷簡歷考慮,最不應(yīng)該選擇的都是 C++。但,“無辜”的程序員選擇了 C++。除了不值一文的情懷,還有一個深度考慮后的原因:
—— 重用 Envoy 相同的、打開兼容模式編譯期配置
HTTP_PARSER_STRICT=0的http-parser。
要修正有問題的 HTTP Header,首先要在 Byte Buffer 中定位(或者說是解析到)Header。當(dāng)然可以用更時髦的解釋器。以上幾種語言都有自己的 HTTP 解釋器。但,誰保證這些解釋器的結(jié)果和 Envoy 兼容?會不會引入新問題?那么,直接使用 Envoy 同樣的解釋器,是個不錯的選擇。如果解釋器有問題,就算不加這個 Fitler ,Envoy 本身也會有問題。即基本保證不在解釋器上引入新問題。
小眾的 WASM Network Filter
最幸運的程序總可以在搜索引擎/Stackoverflow/Github上找到一個 copy/paste 的模板代碼或神 Issue workaround 而輕松完成績效。而“倒霉”的程序員往往是去解決那些沒有標(biāo)準(zhǔn)答案的難題(雖然筆者喜歡后者),最后折騰自己且不一定有績效。
顯然,網(wǎng)上可以找到一堆 WASM HTTP Filter 的資料和參考實現(xiàn),但 WASM Network Filter 極少,有也是讀一下 Buffer Bytes,做做簡單統(tǒng)計的功能。沒有一個是在 L3/4 層上修改字節(jié)流的,更別提要解釋字節(jié)流上的 HTTP 了。
Proxy WASM C++ SDK
開源打開的不單單是代碼,更應(yīng)該是人們求真相的機會?!暗姑埂钡某绦騿T記得 2002 年學(xué)習(xí) Visual C++ MFC 時,只能看到 MSDN 上的文檔,而不明其所以的痛苦。
小眾的 WASM Network Filter 再小眾,也是 Open Source 的。不單單 SDK Open Source,接口的定義 ABI Spec 也是 Open Source。列一下手頭上的重要參考:
-
Proxy WASM 接口規(guī)范 API 說明
-
Envoy 實現(xiàn) WASM 的說明
-
https://github.com/proxy-wasm/spec/blob/master/docs/WebAssembly-in-Envoy.md
Proxy WASM 是個 Proxy 下使用 WASM擴展的規(guī)范。即除了 Envoy ,還有其它幾個 Proxy 也支持的。
-
-
C++ SDK 實現(xiàn)和簡單的使用文檔
-
https://github.com/proxy-wasm/proxy-wasm-cpp-sdk
包括如何編譯自己的 C++ WASM Filter 實現(xiàn)
-
-
網(wǎng)上僅有的 WASM Network Fitler 例子(Rust)
WASM Network Filter 設(shè)計
堅持一慣風(fēng)格,少說話,多上圖:

圖:WASM Network Filter 設(shè)計圖
沒太多可說的,下面介紹一下實現(xiàn)。
WASM Network Filter 實現(xiàn)
由于各種原因,不打算 copy 所有代碼上來,以下只是用為本文特別改寫的偽代碼來說明。
由于使用到 https://github.com/nodejs/http-parser 的源碼,其實就是兩個文件: http_parser.h 與 http_parser.c 。先下載并保存到新項目目錄。假設(shè)叫 $REPAIRER_FILTER_HOME 。這個 http-parser 解釋器最大的好處是無依賴和實現(xiàn)簡單。
現(xiàn)在開始編寫核心代碼,我假設(shè)叫:$repairer_fitler.cc
#include ...
#include "proxy_wasm_intrinsics.h"
#include "http_parser.h" //from https://github.com/nodejs/http-parser
/**
在每個 Filter 配置對應(yīng)一個對象實例
**/
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 連接對應(yīng)一個對象實例
**/
class MainContext : public Context
{
public:
http_parser_settings settings_;
http_parser parser_;
...
//構(gòu)造函數(shù),在每個新 downstream 連接可用時調(diào)用。如 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 的回調(diào)事件
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 請求由于網(wǎng)絡(luò)原因,可以打散為多個 Buffer,回調(diào)多次。
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());
}
//這里會執(zhí)行各種 HTTP 解釋,調(diào)用相關(guān)的 HTTP 解釋回調(diào)函數(shù)。我們實現(xiàn)了這些函數(shù),記錄下問題 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++ 項目已經(jīng)是 17 年前的事了,這個程序員花了一周(加班)的時間才實現(xiàn)了一個可以工作的原型。并且,未優(yōu)化和對性能影響的測試。Sandbox VM 的實現(xiàn)方式注定對服務(wù)延時有影響的??梢娢抑暗囊粋€分析:
image.png
圖:Flame Graph(火焰圖)中的 WASM
悟
這是一個最好的年代,架構(gòu)師們有各種開源組件,只需要簡單粘合,就可以實現(xiàn)需求。
這是一個最壞的年代,開箱即用寵壞了架構(gòu)師們,利用別人的東西我們飛得很高也很自信,認為自己掌握了魔法。但一個不幸踩到坑掉下時,也因為對現(xiàn)實的無知而重重的受傷。
我的 yysd —— Brendan Gregg 曾經(jīng)說過:
You never know a company (or person) until you see them on their worst day
你永遠不會認清一家公司(或個人),直到你在他們最糟糕的一天看到他們。
真正考驗一個程序員或架構(gòu)師的時候,不是去為一個新項目繪畫宏偉藍圖(PPT)的時候,更不是他懂得多少新概念,新技術(shù)。而是在現(xiàn)有架構(gòu)出現(xiàn)問題時,在沒有前人經(jīng)驗的情況下,如何在各種技術(shù)、非技術(shù)條件受限的情況下,去探索一條解決之道,并且為解決問題而引起的新問題作好準(zhǔn)備。

