上帝和 Istio 打架時,程序員如何自我救贖? —— 記一次開發(fā) Envoy WASM Filter 修正任性的 HTTP Header

image.png

故事發(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=0 solved 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)]:

image.png

解釋出錯發(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=0http-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。列一下手頭上的重要參考:

WASM Network Filter 設(shè)計

堅持一慣風(fēng)格,少說話,多上圖:

image.png

圖:WASM Network Filter 設(shè)計圖

沒太多可說的,下面介紹一下實現(xiàn)。

WASM Network Filter 實現(xiàn)

由于各種原因,不打算 copy 所有代碼上來,以下只是用為本文特別改寫的偽代碼來說明。

由于使用到 https://github.com/nodejs/http-parser 的源碼,其實就是兩個文件: http_parser.hhttp_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ù)延時有影響的??梢娢抑暗囊粋€分析:

記一次 Istio 沖刺調(diào)優(yōu):

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)備。

image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容