关于istio:上帝和-Istio-打架时程序员如何自我救赎-记一次-Envoy-Filter-修正任性HTTP-Header

53次阅读

共计 6851 个字符,预计需要花费 18 分钟才能阅读完成。

故事产生在公元 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=0http-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.hhttp_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)的时候,更不是他懂得多少新概念,新技术。而是在现有架构呈现问题时,在没有前人教训的状况下,如何在各种技术、非技术条件受限的状况下,去摸索一条解决之道,并且为解决问题而引起的新问题作好筹备。

正文完
 0