31

Istio1.5 & Envoy 数据面 WASM 实践

 4 years ago
source link: http://www.servicemesher.com/blog/202004-istio-envoy-wasm/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

简介

Istio 1.5 回归单体架构,并抛却原有的 out-of-process 的数据面(Envoy)扩展方式,转而拥抱基于 WASM 的 in-proxy 扩展,以期获得更好的性能。本文基于网易杭州研究院轻舟云原生团队的调研与探索,介绍 WASM 的社区发展与实践。

超简单版解释: > –> Envoy 内置 Google V8 引擎,支持WASM字节码运行,并开放相关接口用于和 WASM 虚拟机交互数据; > –> 使用各种语言开发相关扩展并编译为 .WASM 文件; > –> 将扩展文件挂载或者打包进入 Envoy 容器镜像,通过xDS动态下发文件路径及相关配置由虚拟机执行。

WebAssembly 简述

Istio 最新发布的 1.5 版本,架构发生了巨大调整,从原有的分布式结构回归为单体,同时抛却了原有的 out-of-process 的 Envoy 扩展方式,转而拥抱基于 WASM 的 in-proxy 扩展,以期获得更好的性能,同时减小部署和使用的复杂性。所有的 WASM 插件都在 Envoy 的沙箱中运行,相比于原生 C++ Envoy 插件,WASM 插件具有以下的优点:

  • 接近原生插件性能(存疑,待验证,社区未给出可信测试结果,但是 WASM 字节码和机器码比较接近,它的性能极限确实值得期待);
  • 沙箱运行,更安全,单个 filter 故障不会影响到 Envoy 主体执行,且 filter 通过特定接口和 Envoy 交互数据,Envoy 可以对暴露的数据进行限制(沙箱安全性对于 Envoy 整体稳定性保障具有很重要的意义);
  • 可动态分发和载入运行(单个插件可以编译为 .WASM 文件进行分发共享,动态挂载,动态载入,且没有平台限制);
  • 无开发语言限制,开发效率更高(WASM 本身支持语言众多,但是限定到 Envoy 插件开发,必然依赖一些封装好的 SDK 用于和 Envoy 进行交互,目前只有 C++ 语言本身、Rust 以及 AssemblysScript 有一定的支持)。

WASM 的诞生源自前端,是一种为了解决日益复杂的前端 web 应用以及有限的 JavaScript 性能而诞生的技术。它本身并不是一种语言,而是一种字节码标准,一个“编译目标”。WASM 字节码和机器码非常接近,因此可以非常快速的装载运行。任何一种语言,都可以被编译成 WASM 字节码,然后在 WASM 虚拟机中执行(本身是为 web 设计,必然天然跨平台,同时为了沙箱运行保障安全,所以直接编译成机器码并不是最佳选择)。理论上,所有语言,包括 JavaScript、C、C++、Rust、Go、Java 等都可以编译成 WASM 字节码并在 WASM 虚拟机中执行。

UrI3Erm.png!web

社区发展及现状

Envoy & WASM

Envoy 提供了一个特殊的 Http 七层 filter,名为 wasm,用于载入和执行 WASM 字节码。该七层 filter 同样也负责 WASM 虚拟机的创建和管理,使用的是 Google 内部的 v8 引擎(支持 JS 和 WASM)。当前 filter 未进入 Envoy 主干,而是在单独的一个 工程 中。该工程会周期性从主干合并代码。从机制看,WASM 扩展和 Lua 扩展机制非常相似,只是 Lua 载入的是原始脚本,而 WASM 载入的是编译后的 WASM 字节码。Envoy 暴露相关的接口如获取请求头、请求体,修改请求头,请求体,改变插件链执行流程等等,用于 WASM 插件和 Envoy 主体进行数据交互。

对于每一个 WASM 扩展插件都可以被编译为一个 *.WASM 文件,而 Envoy 七层提供的 wasm Filter 可以通过动态下发相关配置(指定文件路径)使其载入对应的文件并执行:前提是对应的文件已经在镜像中或者挂载进入了对应的路径。当然,WASM Filter 也支持从远程获取对应的 *.WASM 文件(和目前网易轻舟 API 网关对 Lua 脚本扩展的支持非常相似)。

Istio & WASM

现有的 Istio 提供了名为 Mixer 插件模型用于扩展 Envoy 数据面功能,具体来说,在 Envoy 内部,Istio 开发了一个原生 C++ 插件用于收集和获取运行时请求信息并通过 gRPC 将信息上报给 Mixer,外部 Mixer 则调用各个 Mixer Adapter 用于监控、授权控制、限流等等操作,相关处理结果如有必要再返回给 Envoy 中 C++ 插件用于做相关控制。 Mixer 模型虽然提高了极高的灵活性,且对 Envoy 侵入性极低,但是引入了大量的额外的外部调用和数据交互,带来了巨大的性能开销(相关的测试结果很多,按照 istio 社区的数据:移除 Mixer 可以使整体 CPU 消耗减少 50%)。而且 Istio 插件扩展模型和 Envoy 插件模型整体是割裂的,Istio 插件在 out-of-process 中执行,通过 gRPC 进行插件与 Envoy 主体的数据交互,而 Envoy 原生插件则是 in-proxy 模式,在同一个进程中通过虚函数接口进行调用和执行。

因此在 Istio 1.5 中,Istio 提供了全新的插件扩展模型:WASM in proxy。使用 Envoy 支持的WASM机制来扩展插件:兼顾性能、多语言支持、动态下发动态载入、以及安全性。唯一的缺点就是现有的支持还不够完善。

为了提升性能,Istio 社区在 1.5 发布中,已经将几个扩展使用 in-proxy 模型(基于 WASM API 而非原生 Envoy C++ HTTP 插件 API)进行实现。但是目前考虑到 WASM 还不够稳定,所以相关扩展默认不会执行在 WSAM 沙箱之中(在所谓 NullVM 中执行)。虽然 istio 也支持将相关扩展编译为 WASM 模块,并在沙箱中执行,但是不是默认选项。

所谓 Mixer V2 其最终目标就是将现有的 out-of-process 的插件模型最终用基于 WASM 的 in-proxy 扩展模型来替代。但是目前举例目标仍旧有较长一段路要走,毕竟即使 Istio 社区本身的插件,也未能完全在 WASM 沙箱中落地。但从 Istio 1.5 开始,Istio 社区应该会快速推动 WASM 的发展。

solo.io & WASM

solo.io 推出了 WebAssembly Hub,用于构建、发布以及共享 Envoy WASM 扩展。WebAssembly Hub 包括一套用于简化扩展开发的 SDK(目前 solo.io 提供了AssemblysScript SDK,而 Istio/Envoy 社区提供了 Rust/C++ SDK),相关的构建、发布命令,一个用于共享和复用的扩展仓库。具体的内容可以参考 solo.io 提供的教程

WASM 实践

下面简单实现一个 WASM 扩展作为演示 DEMO,可以帮助大家对 WASM 有进一步了解。此处直接使用了 solo.io 提供的构建工具,避免环境搭建等各个方面的一些冗余工作。 该扩展名为 path_rewrite,可以根据路由原始的 path 值匹配,来将请求 path 重写为不同值

执行以下命令安装 wasme:

curl -sL https://run.solo.io/wasme/install | sh
export PATH=$HOME/.wasme/bin:$PATH

wasme 是 solo.io 提供的一个命令行工具,一个简单的类比就是:docker cli 之于容器镜像,wasme 之于 WASM 扩展。

ping@ping-OptiPlex-3040:~/Desktop/wasm_example$ wasme init ./path_rewrite
Use the arrow keys to navigate: ↓ ↑ → ←
? What language do you wish to use for the filter:
  ▸ cpp
    assemblyscript

执行 wasme 初始化命令,会让用户选择使用何种语言开发 WASM 扩展,目前 wasme 工具仅支持 C++ 和 AssemblyScript,当前仍旧选择 cpp 进行开发(AssemblyScript 没有开发经验,后续有机会可以学习一下)。执行命令之后,会自动创建一个 bazel 工程,目录结构如下:其中关键的几个文件已经添加了注释。从目录结构看,solo.io 没有在 wasme 中添加任何黑科技,生成的模板非常的干净,完整而简洁。

.
├── bazel
│   └── external
│       ├── BUILD
│       ├── emscripten-toolchain.BUILD
│       └── envoy-wasm-api.BUILD      # 说明如何编译envoy api依赖
├── BUILD                             # 说明如何编译插件本身代码
├── filter.cc                         # 插件具体代码
├── filter.proto                      # 扩展数据面接口
├── README.md
├── runtime-config.json
├── toolchain
│   ├── BUILD
│   ├── cc_toolchain_config.bzl
│   ├── common.sh
│   ├── emar.sh
│   └── emcc.sh
└── WORKSPACE                         # 工程描述文件包含对envoy api依赖

filter.cc 中已经填充了样板代码,包括所有的插件需要实现的接口。开发者只需要按需修改某个接口的具体实现即可(此处列出了整个插件的全部代码,以供参考。虽然该代码没有实现什么特许功能,但是已经包含了一个 WASM 扩展(C++ 语言版)应当具备的所有结构,无论多么复杂的插件,都只是在该结构的基础上填充相关的逻辑代码而已:

// NOLINT(namespace-envoy)
#include <string>
#include <unordered_map>

#include "google/protobuf/util/json_util.h"
#include "proxy_wasm_intrinsics.h"
#include "filter.pb.h"

class AddHeaderRootContext : public RootContext {
public:
  explicit AddHeaderRootContext(uint32_t id, StringView root_id) : RootContext(id, root_id) {}
  bool onConfigure(size_t /* configuration_size */) override;

  bool onStart(size_t) override;

  std::string header_name_;
  std::string header_value_;
};

class AddHeaderContext : public Context {
public:
  explicit AddHeaderContext(uint32_t id, RootContext* root) : Context(id, root), root_(static_cast<AddHeaderRootContext*>(static_cast<void*>(root))) {}

  void onCreate() override;
  FilterHeadersStatus onRequestHeaders(uint32_t headers) override;
  FilterDataStatus onRequestBody(size_t body_buffer_length, bool end_of_stream) override;
  FilterHeadersStatus onResponseHeaders(uint32_t headers) override;
  void onDone() override;
  void onLog() override;
  void onDelete() override;
private:

  AddHeaderRootContext* root_;
};
static RegisterContextFactory register_AddHeaderContext(CONTEXT_FACTORY(AddHeaderContext),
                                                      ROOT_FACTORY(AddHeaderRootContext),
                                                      "add_header_root_id");

bool AddHeaderRootContext::onConfigure(size_t) { 
  auto conf = getConfiguration();
  Config config;
  
  google::protobuf::util::JsonParseOptions options;
  options.case_insensitive_enum_parsing = true;
  options.ignore_unknown_fields = false;

  google::protobuf::util::JsonStringToMessage(conf->toString(), &config, options);
  LOG_DEBUG("onConfigure name " + config.name());
  LOG_DEBUG("onConfigure " + config.value());
  header_name_ = config.name();
  header_value_ = config.value();
  return true; 
}

bool AddHeaderRootContext::onStart(size_t) { LOG_DEBUG("onStart"); return true;}

void AddHeaderContext::onCreate() { LOG_DEBUG(std::string("onCreate " + std::to_string(id()))); }

FilterHeadersStatus AddHeaderContext::onRequestHeaders(uint32_t) {
  LOG_DEBUG(std::string("onRequestHeaders ") + std::to_string(id()));
  return FilterHeadersStatus::Continue;
}

FilterHeadersStatus AddHeaderContext::onResponseHeaders(uint32_t) {
  LOG_DEBUG(std::string("onResponseHeaders ") + std::to_string(id()));
  addResponseHeader(root_->header_name_, root_->header_value_);
  replaceResponseHeader("location", "envoy-wasm");
  return FilterHeadersStatus::Continue;
}

FilterDataStatus AddHeaderContext::onRequestBody(size_t body_buffer_length, bool end_of_stream) {
  return FilterDataStatus::Continue;
}

void AddHeaderContext::onDone() { LOG_DEBUG(std::string("onDone " + std::to_string(id()))); }

void AddHeaderContext::onLog() { LOG_DEBUG(std::string("onLog " + std::to_string(id()))); }

void AddHeaderContext::onDelete() { LOG_DEBUG(std::string("onDelete " + std::to_string(id()))); }

注意到生成的样板代码类型名称仍旧以 AddHeader 为前缀,而没有根据提供的路径名称生成,此处是 wasme 可以优化的一个地方。此外, 自动生成的样板代码中已经包含了 AddHeader 的一些代码,逻辑简单,但是配置解析、API 访问,请求头修改等过程都具备,麻雀虽小,五脏俱全,正好可以帮助初次的开发者可以依葫芦画瓢熟悉 WASM 插件的开发过程 。对于入门是非常友好的。

针对 path_rewrite 具体的开发步骤如下:

STEP ONE首先修改模板代码中 filter.proto 文件,因为 path rewrite 肯定不能简单的只能替换固定值,修改后 proto 文件如下所示:

syntax = "proto3";

message PathRewriteConfig {
  message Rewrite {
    string regex_match = 1;      # path正则匹配时替换
    string custom_path = 2;      # 待替换值
  }
  repeated Rewrite rewrites = 1;
}

STEP TWO修改配置解析接口,具体方法名为 onConfigure。修改后解析接口如下:

bool AddHeaderRootContext::onConfigure(size_t) {
  auto conf = getConfiguration();
  PathRewriteConfig config; // message type in filter.proto
  if (!conf.get()) {
    return true;
  }
  google::protobuf::util::JsonParseOptions options;
  options.case_insensitive_enum_parsing = true;
  options.ignore_unknown_fields = false;
  // 解析字符串配置并转换为PathRewriteConfig类型:配置反序列化
  google::protobuf::util::JsonStringToMessage(conf->toString(), &config,
                                              options);

  // 配置阶段编译regex避免请求时重复编译,提高性能
  for (auto &rewrite : config.rewrites()) {
    rewrites_.push_back(
        {std::regex(rewrite.regex_match()), rewrite.custom_path()});
  }

  return true;
}

STEP THREE修改请求头接口,具体方法名为 onRequestHeaders,修改后接口代码如下:

FilterHeadersStatus AddHeaderContext::onRequestHeaders(uint32_t) {
  LOG_DEBUG(std::string("onRequestHeaders ") + std::to_string(id()));
  // Envoy中path同样存储在header中,key为:path
  auto path = getRequestHeader(":path");
  if (!path.get()) {
    return FilterHeadersStatus::Continue;
  }
  std::string path_string = path->toString();
  for (auto &rewrite : root_->rewrites_) {
    if (std::regex_match(path_string, rewrite.first) &&
        !rewrite.second.empty()) {
      replaceRequestHeader(":path", rewrite.second);
      replaceRequestHeader("location", "envoy-wasm");
      return FilterHeadersStatus::Continue;
    }
  }
  return FilterHeadersStatus::Continue;
}

从上述过程不难看出,整个扩展的开发体验相当简单,按需实现对应接口即可,扩展本身内容非常轻,内部具体的功能逻辑才是决定扩展开发复杂性的关键。而且借助 wasme 工具,自动生成代码后,效率可以更高(和目前在内部使用的 filter_creator.py 有部分相似,样板代码自动生成)。

至此,插件已经开发完成,可以打包编译了。wasm 同样提供了打包编译的功能,甚至可以类似于容器镜像将编译后结构推送到远端仓库之中,用于分享或者存储。不过有一个提示,在开发之前,先直接执行 bazel 命令编译,编译过程中,一些基础依赖会被自动拉取并缓存到本地,借助 IDE 可以获得更好的代码提示和开发体验。

bazel build :filter.wasm

接下来是 wasme 命令编译:

wasme build cpp -t webassemblyhub.io/wbpcode/path_rewrite:v0.1 .

该命令会使用固定镜像作为编译环境,但是本质和直接使用 bazel 编译并无不同。具体的编译日志可以看出,实际上,该命令也是使用的 bazel build :filter.wasm

Status: Downloaded newer image for quay.io/solo-io/ee-builder:0.0.19
Building with bazel...running bazel build :filter.wasm
Extracting Bazel installation...
Starting local Bazel server and connecting to it...

注意,上述命令中 wbpcode 为用户名,具体实践时提议替换为自身用户名,如果注册了 webassemblyhub.io 账号,甚至可以进行 push 和 pull 操作。此次就不做相关操作了,直接本地启动带 WASM 的 envoy。命令如下:

# --config参数用于指定wasm扩展配置
wasme deploy envoy webassemblyhub.io/wbpcode/path_rewrite:v0.1 --config "{\"rewrites\": [ {\"regex_match\":\"...\", \"custom_path\": \"/anything\"} ]}" --envoy-run-args "-l trace"

从 envoy 执行日志可以看到:最终 envoy 会执行七层 Filter: envoy.filters.http.wasm ,相关配置为:wasm 文件位置(docker 执行时挂载进入容器内部)、 wasm 文件对应插件配置、runtime 等等。通过在 http_filters 中重复添加多个 envoy.filters.http.wasm ,即可实现多个 WASM 扩展的执行。从下面的日志也可以看出,即使不使用 solo.io 的工具,只需要为 Envoy 指定编译好的 wasm 文件,其执行结果是完全相同的。

[2020-03-31 08:41:24.831][1][debug][config] [external/envoy/source/extensions/filters/network/http_connection_manager/config.cc:388]       name: envoy.filters.http.wasm
[2020-03-31 08:41:24.831][1][debug][config] [external/envoy/source/extensions/filters/network/http_connection_manager/config.cc:390]     config: {
 "config": {
  "rootId": "add_header_root_id",
  "vmConfig": {
   "code": {
    "local": {
     "filename": "/home/ping/.wasme/store/e58ddd90347b671ad314f1c969771cea/filter.wasm"
    }
   },
   "runtime": "envoy.wasm.runtime.v8"
  },
  "configuration": "{\"rewrites\": [ {\"regex_match\":\"...\", \"custom_path\": \"/anything\"} ]}",
  "name": "add_header_root_id"
 }
}

之后使用对应 path 调用接口:可发现 WASM 插件已经生效:

':authority', 'localhost:8080'
':path', '/ab' # 原始请求path匹配"..."
':method', 'GET'
'user-agent', 'curl/7.58.0'
'accept', '*/*'
':authority', 'localhost:8080'
':path', '/anything'
':method', 'GET'
':scheme', 'https'
'user-agent', 'curl/7.58.0'
'accept', '*/*'
'x-forwarded-proto', 'http'
'x-request-id', '1009236e-ab57-4ded-a8ff-3d1b17c6787b'
'location', 'envoy-wasm'
'x-envoy-expected-rq-timeout-ms', '15000'

WASM 总结

WASM 扩展仍在快速发展当中,但是 Isito 使用 WASM API 实现了相关的插件,说明已经做好了迁移的准备。前景美好,值得期待,但有待进一步确定 WASM 沙箱本身稳定性和性能。

从开发体验来说:

  • 借助 solo.io 工具,简单插件的开发几乎没有任何的难度,只是目前支持的语言只有 C++/AssemblyScript(Envoy 社区开发了 Rust 语言 SDK,但是正在开发当中而且使用 Rust 开发 WASM 扩展的价值存疑:Rust 相比于 C++ 最大的优势是通过严格的编译检查来保证内存安全,但是也使得上手难度又提升了一个台阶,在有 WASM 沙箱为内存安全兜底的情况下,使用 Rust 而不使用 JS、Go 等上手更简易的语言来开发扩展,实无必要)。
  • 对于相对复杂的插件,如果使用 WASM 的话,测试相比于原生插件会更困难一些,WASM 扩展配置的输入只能依赖手写 JSON 字符串,希望未来能够改善。
  • 缺少路由粒度的配置,所有配置都是全局生效,依赖插件内部判断,但是这一部分如果确实有需要,支持起来应该很快,不存在技术上的阻碍,倒是不用担心。

作者简介

王佰平,网易杭州研究院轻舟云原生团队工程师,负责轻舟 Envoy 网关与轻舟 Service Mesh 数据面开发、功能增强、性能优化等工作,对 Envoy 数据面开发、增强、落地具有较为丰富的经验。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK