4、Envoy 中 Wasm 源码解析

Higress 的核心思路是基于 Wasm 插件机制进行扩展,下面我们基于 Envoy 1.31.0 版本从源码角度解析 Envoy 中 WASM 沙箱是如何实现的

1)、Envoy 过滤器框架

Envoy 支持使用 WASM 来扩展七层 HTTP Filter 或者四层 Network Filter。但是无论是七层 HTTP Filter 还是四层 Network Filter,其本质都是类似的。Envoy 内置了一个原生 C++ HTTP Filter 和一个原生 C++ Network Filter 用于封装 WASM API,管理 WASM Sandbox,载入 WASM 字节码并且对 Envoy 暴露标准的 HTTP Filter 和 Network Filter 接口

对于一个普通的 C++ HTTP Filter,在配置的初始化阶段,Envoy 会完成 Proto 配置(通过静态文件或者 xDS 协议获取)的加载和初始化。而在请求阶段,对于每个 HTTP 请求,Envoy 会根据 HCM(HTTP Connection Manager,用于处理 HTTP 协议的四层 Network Filter)的配置创建一个 HTTP Filter 链,并且将每个 HTTP Filter 的配置注入到对应的 Filter 实例中

// source/extensions/filters/http/wasm/config.h
  template <class FactoryContext>
  Http::FilterFactoryCb createFilterFactoryFromProtoTyped(
      const envoy::extensions::filters::http::wasm::v3::Wasm& proto_config, const std::string&,
      FactoryContext& context) {
    context.serverFactoryContext().api().customStatNamespaces().registerStatNamespace(
        Extensions::Common::Wasm::CustomStatNamespace);
    // 根据 Proto 配置初始化 filter_config
    auto filter_config = std::make_shared<FilterConfig>(proto_config, context);
    return [filter_config](Http::FilterChainFactoryCallbacks& callbacks) -> void {
      // 创建 WASM HTTP Filter 实例
      auto filter = filter_config->createFilter();
      if (!filter) { // Fail open
        return;
      }
      // 将外层 WASM 过滤器添加到 L7 过滤器列表
      callbacks.addStreamFilter(filter);
      callbacks.addAccessLogHandler(filter);
    };
  }
};

createFilterFactoryFromProtoTyped 只会在配置初始化阶段执行一次。之后,每当请求到来,其返回的闭包函数就会被执行,用于创建 WASM HTTP Filter 实例来请求处理

FilterConfig 是一个关键类型,最终的 Filter 实例是调用 FilterConfigcreateFilter 接口创建出来的

小结:Envoy 通过一个内置的七层 HTTP Filter 和一个内置的四层 Network Filter 充当包装器,管理 WASM runtime 并与 WASM Sandbox 交互以实现 HTTP Filter 或者 Network Filter 的相关功能

2)、创建一个 WASM 沙箱

FilterConfig 只提供了一个 createFilter 接口,其核心的内容都在构造函数当中。其构造函数源码如下:

// source/extensions/filters/http/wasm/wasm_filter.cc
FilterConfig::FilterConfig(const envoy::extensions::filters::http::wasm::v3::Wasm& config,
                           Server::Configuration::FactoryContext& context) {
  auto& server = context.serverFactoryContext();
  const auto plugin = std::make_shared<Common::Wasm::Plugin>(
      config.config(), context.listenerInfo().direction(), server.localInfo(),
      &context.listenerInfo().metadata());
  // 调用 createWasm 方法
  createWasm(plugin, server, context.scope().createScope(""), context.initManager());
}

void FilterConfig::createWasm(PluginSharedPtr plugin,
                              Envoy::Server::Configuration::ServerFactoryContext& server,
                              const Stats::ScopeSharedPtr& scope,
                              Envoy::Init::Manager& init_manager) {
  tls_slot_ = ThreadLocal::TypedSlot<Common::Wasm::PluginHandleSharedPtrThreadLocal>::makeUnique(
      server.threadLocal());
  // 回调函数,当成功创建 WASM 虚拟机之后执行,用于在每个 Worker 线程之中都拷贝创建一个 WASM 虚拟机
  auto callback = [plugin, this](const Common::Wasm::WasmHandleSharedPtr& base_wasm) {
    // NB: the Slot set() call doesn't complete inline, so all arguments must outlive this call.
    tls_slot_->set([base_wasm, plugin](Event::Dispatcher& dispatcher) {
      return std::make_shared<PluginHandleSharedPtrThreadLocal>(
          Common::Wasm::getOrCreateThreadLocalPlugin(base_wasm, plugin, dispatcher));
    });
  };
  // 主线程根据配置创建 WASM 虚拟机
  if (!Common::Wasm::createWasm(
          plugin, scope, server.clusterManager(), init_manager, server.mainThreadDispatcher(),
          server.api(), server.lifecycleNotifier(), remote_data_provider_, std::move(callback))) {
    throw Common::Wasm::WasmException(
        fmt::format("Unable to create Wasm HTTP filter {}", plugin->name_));
  }
}

Common::Wasm::Plugin 是对 WASM 字节码、字节码插件本身配置以及一些 Filter 状态的封装和集合

FilterConfig::createWasm 方法中调用 Common::Wasm::createWasm 函数。Common::Wasm::createWasm 函数用于创建一个 WASM 虚拟机,其主要工作如下:

  • 根据配置中 WASM 字节码位置(本地或者远程),读取字节码
  • 创建 WASM 虚拟机
// source/extensions/common/wasm/wasm.cc
bool createWasm(const PluginSharedPtr& plugin, const Stats::ScopeSharedPtr& scope,
                Upstream::ClusterManager& cluster_manager, Init::Manager& init_manager,
                Event::Dispatcher& dispatcher, Api::Api& api,
                Server::ServerLifecycleNotifier& lifecycle_notifier,
                RemoteAsyncDataProviderPtr& remote_data_provider, CreateWasmCallback&& cb,
                CreateContextFn create_root_context_for_testing) {
  ...
  // proxy_wasm 是一个关键外部依赖,是对 WASM 虚拟机环境的封装
  auto vm_key = proxy_wasm::makeVmKey(vm_config.vm_id(),
                                      MessageUtil::anyToBytes(vm_config.configuration()), code);
  // 该闭包函数接收字节码 code 为参数,并捕获了相关的配置以及关键上下文
  auto complete_cb = [cb, vm_key, plugin, scope, &api, &cluster_manager, &dispatcher,
                      &lifecycle_notifier, create_root_context_for_testing,
                      &stats_handler](std::string code) -> bool {
    if (code.empty()) {
      cb(nullptr);
      return false;
    }

    auto config = plugin->wasmConfig();
    // 调用 proxy_wasm 中 createWasm 函数创建 WASM 沙箱
    auto wasm = proxy_wasm::createWasm(
        vm_key, code, plugin,
        // 核心参数:创建 WASM 虚拟机环境的工厂函数
        getWasmHandleFactory(config, scope, api, cluster_manager, dispatcher, lifecycle_notifier),
        getWasmHandleCloneFactory(dispatcher, create_root_context_for_testing),
        config.config().vm_config().allow_precompiled());
    Stats::ScopeSharedPtr create_wasm_stats_scope = stats_handler.lockAndCreateStats(scope);
    stats_handler.onEvent(toWasmEvent(wasm));
    if (!wasm || wasm->wasm()->isFailed()) {
      ENVOY_LOG_TO_LOGGER(Envoy::Logger::Registry::getLog(Envoy::Logger::Id::wasm), trace,
                          "Unable to create Wasm");
      cb(nullptr);
      return false;
    }
    // 执行 FilterConfig 中的回调函数,在各个 Worker 中创建 WASM 沙箱
    cb(std::static_pointer_cast<WasmHandle>(wasm));
    return true;
  };
	...
}

prxoy_wasm 由一个关键的外部依赖提供,包含了对 WASM 虚拟机的封装,仓库地址为 proxy-wasm-cpp-host

proxy_wasm::makeVmKey 函数主要作用是根据 WASM 字节码、WASM 沙箱配置(是沙箱本身配置而非运行在沙箱中的字节码插件配置)计算出一个 WASM 虚拟机 ID。该 ID 将唯一标识同一线程中的一个沙箱

proxy_wasm::createWasm 函数有四个重要的参数:

  • vm_key:沙箱唯一标识
  • code:字节码
  • plugin:FilterConfig 构造函数中创建的 Common::Wasm::Plugin 对象实例
  • proxy_wasm_factory:创建 WASM 虚拟机环境的工厂函数

proxy_wasm::createWasm 函数的源码如下:

// proxy_wasm src/wasm.cc
std::shared_ptr<WasmHandleBase> createWasm(const std::string &vm_key, const std::string &code,
                                           const std::shared_ptr<PluginBase> &plugin,
                                           const WasmHandleFactory &factory,
                                           const WasmHandleCloneFactory &clone_factory,
                                           bool allow_precompiled) {
  std::shared_ptr<WasmHandleBase> wasm_handle;
  {
    std::lock_guard<std::mutex> guard(base_wasms_mutex);
    if (base_wasms == nullptr) {
      base_wasms = new std::remove_reference<decltype(*base_wasms)>::type;
    }
    // base_wasms 管理 vm_key 到已有 WASM 虚拟机的映射
    // 如果 vm_key 已经存在一个对应的 WASM 虚拟机则可以直接复用。否则就调用传入的工厂函数创建一个新的虚拟机
    auto it = base_wasms->find(vm_key);
    if (it != base_wasms->end()) {
      wasm_handle = it->second.lock();
      if (!wasm_handle) {
        base_wasms->erase(it);
      }
    }
    if (!wasm_handle) {
      // If no cached base_wasm, creates a new base_wasm, loads the code and initializes it.
      // 调用传入的工厂函数,用于创建一个 WASM 虚拟机环境
      wasm_handle = factory(vm_key);
      if (!wasm_handle) {
        return nullptr;
      }
      if (!wasm_handle->wasm()->load(code, allow_precompiled)) {
        wasm_handle->wasm()->fail(FailState::UnableToInitializeCode, "Failed to load Wasm code");
        return nullptr;
      }
      // 调用 initialize 方法
      if (!wasm_handle->wasm()->initialize()) {
        wasm_handle->wasm()->fail(FailState::UnableToInitializeCode,
                                  "Failed to initialize Wasm code");
        return nullptr;
      }
      // 创建并初始化完成的 WASM 沙箱以参数中的 vm_key 为 key 存储在 base_wasms 中
      (*base_wasms)[vm_key] = wasm_handle;
    }
  }

  // Either creating new one or reusing the existing one, apply canary for each plugin.
  if (!wasm_handle->canary(plugin, clone_factory)) {
    return nullptr;
  }
  return wasm_handle;
};

在 Envoy 主干函数代码中传入的工厂闭包函数在此会被调用,用于创建一个 WASM 虚拟机环境

所以,此处需要回到 Envoy 中查看其该闭包工厂函数是如何实现的。该工厂函数会创建一个 Wasm::Wasm 实例,并封装在一个 WasmHandle 实例中(继承自 proxy_wasm::WasmHandleBase):

// source/extensions/common/wasm/wasm.cc
static proxy_wasm::WasmHandleFactory
getWasmHandleFactory(WasmConfig& wasm_config, const Stats::ScopeSharedPtr& scope, Api::Api& api,
                     Upstream::ClusterManager& cluster_manager, Event::Dispatcher& dispatcher,
                     Server::ServerLifecycleNotifier& lifecycle_notifier) {
  return [&wasm_config, &scope, &api, &cluster_manager, &dispatcher,
          &lifecycle_notifier](std::string_view vm_key) -> WasmHandleBaseSharedPtr {
    auto wasm = std::make_shared<Wasm>(wasm_config, toAbslStringView(vm_key), scope, api,
                                       cluster_manager, dispatcher);
    wasm->initializeLifecycle(lifecycle_notifier);
    return std::static_pointer_cast<WasmHandleBase>(std::make_shared<WasmHandle>(std::move(wasm)));
  };
}

Wasm::Wasm 则继承了 proxy_wasm::WasmBaseproxy_wasm::WasmBase 是对 proxy_wasm 中管理 WASM 虚拟机以及 Envoy 和 Sandbox 交互 API 的一个基础类型:

// source/extensions/common/wasm/wasm.cc
Wasm::Wasm(WasmConfig& config, absl::string_view vm_key, const Stats::ScopeSharedPtr& scope,
           Api::Api& api, Upstream::ClusterManager& cluster_manager, Event::Dispatcher& dispatcher)
    : WasmBase(
          createWasmVm(config.config().vm_config().runtime()), config.config().vm_config().vm_id(),
          MessageUtil::anyToBytes(config.config().vm_config().configuration()),
          toStdStringView(vm_key), config.environmentVariables(), config.allowedCapabilities()),
      scope_(scope), api_(api), stat_name_pool_(scope_->symbolTable()),
      custom_stat_namespace_(stat_name_pool_.add(CustomStatNamespace)),
      cluster_manager_(cluster_manager), dispatcher_(dispatcher),
      time_source_(dispatcher.timeSource()), lifecycle_stats_handler_(LifecycleStatsHandler(
                                                 scope, config.config().vm_config().runtime())) {
  lifecycle_stats_handler_.onEvent(WasmEvent::VmCreated);
  ENVOY_LOG(debug, "Base Wasm created {} now active", lifecycle_stats_handler_.getActiveVmCount());
}

此处的关键是 createWasmVm 函数。该函数会根据 runtime 类型创建某一种的 WASM 运行时环境。目前支持:null、v8、wamr、wasmtime 四种不同的 runtime。可以在 source/extensions/wasm_runtime 目录下找到它们的相关工厂类

Envoy 中封装的工厂类本质上又仅仅是对 proxy_wasm 中对应 runtime 的函数的封装,以 V8 为例:

// source/extensions/wasm_runtime/v8/config.cc
class V8RuntimeFactory : public WasmRuntimeFactory {
public:
  WasmVmPtr createWasmVm() override { return proxy_wasm::createV8Vm(); }

  std::string name() const override { return "envoy.wasm.runtime.v8"; }
};
// proxy_wasm src/v8/v8.cc
std::unique_ptr<WasmVm> createV8Vm() { return std::make_unique<v8::V8>(); }

其中 proxy_wasm::WasmVm 类型是对各种不同 WASM runtime 的一个统一封装

小结:FilterConfig 是 Envoy HTTP WASM Filter 机制的核心。它会在构造过程中根据配置以及字节码创建 WASM Sandbox。而 WASM Sandbox 创建需要外部依赖 proxy_wasm 的介入

在这里插入图片描述

在配置初始化阶段,Envoy 会创建 WASM 虚拟机运行环境,最终调用 proxy_wasm::createWasm 函数。 proxy_wasm::createWasm 最终会反向调用 Envoy 中一个工厂函数用于创建 Wasm::Wasm 实例以及包装该实例的 WasmHandle 实例(proxy::WasmHandleBase

该过程中涉及到的相关类型以及关系则是:Wasm::Wasm 是 Envoy 中对 WASM runtime 的抽象和封装,它直接继承自 proxy_wasm::WasmBaseproxy_wasm::WasmBase 会组合(包含)一个 WASM 虚拟机实例 proxy_wasm::WasmVm,同时负责沙箱的 API 暴露。而 proxy_wasm::WasmVm 则封装了 v8/wamr 等 WASM runtime 的一些通用功能

3)、WASM 沙箱初始化

现在回到 proxy_wasm::createWasm 函数。在创建 Wasm::Wasm 实例之后,会调用其 initialize 方法。该方法会向其管理的内部 WASM 沙箱注册相关的接口函数,同时也会把 WASM 沙箱暴露的函数绑定到 proxy_wasm::WasmBase 实例中

initialize 方法是整个过程中最为核心的内容,也是 Envoy 与 WASM 沙箱实现交互的关键。proxy_wasm::WasmBase 实例包含的沙箱 runtime 根据配置的不同也可能会有不同,所以此处仍旧以 V8 runtime 为例

// proxy_wasm src/wasm.cc
bool WasmBase::initialize() {
  if (!wasm_vm_) {
    return false;
  }

  if (started_from_ == Cloneable::NotCloneable) {
    // 载入字节码至虚拟机之中
    auto ok = wasm_vm_->load(base_wasm_handle_->wasm()->moduleBytecode(),
                             base_wasm_handle_->wasm()->modulePrecompiled(),
                             base_wasm_handle_->wasm()->functionNames());
    if (!ok) {
      fail(FailState::UnableToInitializeCode, "Failed to load Wasm module from base Wasm");
      return false;
    }
  }

  if (started_from_.has_value()) {
    abi_version_ = base_wasm_handle_->wasm()->abiVersion();
  }

  if (started_from_ != Cloneable::InstantiatedModule) {
    // 1)向 WASM runtime 注册 API
    registerCallbacks();
    // 2)
    if (!wasm_vm_->link(vm_id_)) {
      return false;
    }
  }

  vm_context_.reset(createVmContext());
  // 3)获取 WASM runtime 对外暴露 API
  getFunctions();

  if (started_from_ != Cloneable::InstantiatedModule) {
    // Base VM was already started, so don't try to start cloned VMs again.
    startVm(vm_context_.get());
  }

  return !isFailed();
}

其中需要关注的有三个函数:

  • registerCallbacks
  • link(proxy_wasm::WasmVm
  • getFunctions

registerCallbacks 会向 WASM runtime 注册 Envoy 暴漏的 API:

// proxy_wasm src/wasm.cc
void WasmBase::registerCallbacks() {
#define _REGISTER(_fn)                                                                             \
  wasm_vm_->registerCallback(                                                                      \
      "env", #_fn, &exports::_fn,                                                                  \
      &ConvertFunctionWordToUint32<decltype(exports::_fn),                                         \
                                   exports::_fn>::convertFunctionWordToUint32)
  _REGISTER(pthread_equal);
  _REGISTER(emscripten_notify_memory_growth);
#undef _REGISTER

  // Register the capability with the VM if it has been allowed, otherwise register a stub.
#define _REGISTER(module_name, name_prefix, export_prefix, _fn)                                    \
  if (capabilityAllowed(name_prefix #_fn)) {                                                       \
    wasm_vm_->registerCallback(                                                                    \
        module_name, name_prefix #_fn, &exports::export_prefix##_fn,                               \
        &ConvertFunctionWordToUint32<decltype(exports::export_prefix##_fn),                        \
                                     exports::export_prefix##_fn>::convertFunctionWordToUint32);   \
  } else {                                                                                         \
    typedef decltype(exports::export_prefix##_fn) export_type;                                     \
    constexpr export_type *stub = &exports::_fn##Stub<export_type>::stub;                          \
    wasm_vm_->registerCallback(                                                                    \
        module_name, name_prefix #_fn, stub,                                                       \
        &ConvertFunctionWordToUint32<export_type, stub>::convertFunctionWordToUint32);             \
  }

#define _REGISTER_WASI_UNSTABLE(_fn) _REGISTER("wasi_unstable", , wasi_unstable_, _fn)
#define _REGISTER_WASI_SNAPSHOT(_fn) _REGISTER("wasi_snapshot_preview1", , wasi_unstable_, _fn)
  FOR_ALL_WASI_FUNCTIONS(_REGISTER_WASI_UNSTABLE);
  FOR_ALL_WASI_FUNCTIONS(_REGISTER_WASI_SNAPSHOT);
#undef _REGISTER_WASI_UNSTABLE
#undef _REGISTER_WASI_SNAPSHOT

#define _REGISTER_PROXY(_fn) _REGISTER("env", "proxy_", , _fn)
  // 注册 proxy_ 方法
  FOR_ALL_HOST_FUNCTIONS(_REGISTER_PROXY);
  // 根据运行时支持的 ABI 版本注册对应的 Envoy export 方法
  if (abiVersion() == AbiVersion::ProxyWasm_0_1_0) {
    _REGISTER_PROXY(get_configuration);
    _REGISTER_PROXY(continue_request);
    _REGISTER_PROXY(continue_response);
    _REGISTER_PROXY(clear_route_cache);
  } else if (abiVersion() == AbiVersion::ProxyWasm_0_2_0) {
    _REGISTER_PROXY(continue_stream);
    _REGISTER_PROXY(close_stream);
  } else if (abiVersion() == AbiVersion::ProxyWasm_0_2_1) {
    _REGISTER_PROXY(continue_stream);
    _REGISTER_PROXY(close_stream);
    _REGISTER_PROXY(get_log_level);
  }
#undef _REGISTER_PROXY

#undef _REGISTER
}

registerCallbacks 会将 proxy_wasm::exports 中相关函数通过 WASM runtime 提供的 registerCallback 注册到 WASM runtime 中去,WASM 沙箱中的相关代码就可以执行对应的函数了(这里保证了 WASM 沙箱中可以调用 Envoy 暴露的 API

initialize 中,完成 registerCallbacks 之后,就可以开始进一步的操作 link。link 主要完成两个操作:将 registerCallbacks 所封装好的 API 注册绑定到 WASM 沙箱中去,并将 WASM 沙箱中暴露的 API 导出到 proxy_wasm 中来。这里以 V8 runtime 为例,其 link 方法源码如下:

// proxy_wasm src/v8/v8.cc
bool V8::link(std::string_view /*debug_name*/) {
  assert(module_ != nullptr);

  const auto import_types = module_.get()->imports();
  std::vector<const wasm::Extern *> imports;

  for (size_t i = 0; i < import_types.size(); i++) {
    std::string_view module(import_types[i]->module().get(), import_types[i]->module().size());
    std::string_view name(import_types[i]->name().get(), import_types[i]->name().size());
    const auto *import_type = import_types[i]->type();

    switch (import_type->kind()) {

    case wasm::EXTERN_FUNC: {
      // 根据 imports 中名称从 host_functions_ 搜索函数并注入到 WASM Module 中供 Sandbox 调用
      auto it = host_functions_.find(std::string(module) + "." + std::string(name));
      if (it == host_functions_.end()) {
        fail(FailState::UnableToInitializeCode,
             std::string("Failed to load Wasm module due to a missing import: ") +
                 std::string(module) + "." + std::string(name));
        return false;
      }
      auto *func = it->second->callback_.get();
      if (!equalValTypes(import_type->func()->params(), func->type()->params()) ||
          !equalValTypes(import_type->func()->results(), func->type()->results())) {
        fail(FailState::UnableToInitializeCode,
             std::string("Failed to load Wasm module due to an import type mismatch: ") +
                 std::string(module) + "." + std::string(name) +
                 ", want: " + printValTypes(import_type->func()->params()) + " -> " +
                 printValTypes(import_type->func()->results()) +
                 ", but host exports: " + printValTypes(func->type()->params()) + " -> " +
                 printValTypes(func->type()->results()));
        return false;
      }
      imports.push_back(func);
    } break;
		// ...
    }
  }

  // ...
  for (size_t i = 0; i < export_types.size(); i++) {
    std::string_view name(export_types[i]->name().get(), export_types[i]->name().size());
    const auto *export_type = export_types[i]->type();
    auto *export_item = exports[i].get();
    assert(export_type->kind() == export_item->kind());

    switch (export_type->kind()) {

    case wasm::EXTERN_FUNC: {
      assert(export_item->func() != nullptr);
      // 将 WASM 沙箱本身对外暴露的 API 添加到 module_functions_ 中
      module_functions_.insert_or_assign(std::string(name), export_item->func()->copy());
    } break;

    case wasm::EXTERN_GLOBAL: {
      // TODO(PiotrSikora): add support when/if needed.
    } break;

    case wasm::EXTERN_MEMORY: {
      assert(export_item->memory() != nullptr);
      assert(memory_ == nullptr);
      memory_ = exports[i]->memory()->copy();
      if (memory_ == nullptr) {
        return false;
      }
    } break;

    case wasm::EXTERN_TABLE: {
      // TODO(PiotrSikora): add support when/if needed.
    } break;
    }
  }

  return true;
}

载入字节码之后,可以获取导入符号表和导出符号表。imports 是在沙箱中需要使用但未在沙箱中实现或者创建的函数 API、内存块等(需要从外部引入)。此时可以看到 link 根据 imports 中名称从 host_functions_ 搜索函数并注入到 WASM Module 中供 Sandbox 调用。而反向的,link 也会将 WASM 沙箱本身对外暴露的 API 添加到 module_functions_

link 方法执行完成之后,就会执行 getFunctions 方法。该方法会将从 WASM 沙箱中导出的函数(存储在 module_functions_ 中)绑定到 proxy_wasm::WasmBase 的成员当中,以供后续调用(这里保证了 Envoy 中可以调用 WASM 沙箱暴露的 API

// proxy_wasm src/wasm.cc
void WasmBase::getFunctions() {
#define _GET(_fn) wasm_vm_->getFunction(#_fn, &_fn##_);
#define _GET_ALIAS(_fn, _alias) wasm_vm_->getFunction(#_alias, &_fn##_);
  _GET(_initialize);
  if (_initialize_) {
    _GET(main);
  } else {
    _GET(_start);
  }

  _GET(malloc);
  if (!malloc_) {
    _GET_ALIAS(malloc, proxy_on_memory_allocate);
  }
  if (!malloc_) {
    fail(FailState::MissingFunction, "Wasm module is missing malloc function.");
  }
#undef _GET_ALIAS
#undef _GET

  // Try to point the capability to one of the module exports, if the capability has been allowed.
#define _GET_PROXY(_fn)                                                                            \
  if (capabilityAllowed("proxy_" #_fn)) {                                                          \
    wasm_vm_->getFunction("proxy_" #_fn, &_fn##_);                                                 \
  } else {                                                                                         \
    _fn##_ = nullptr;                                                                              \
  }
#define _GET_PROXY_ABI(_fn, _abi)                                                                  \
  if (capabilityAllowed("proxy_" #_fn)) {                                                          \
    wasm_vm_->getFunction("proxy_" #_fn, &_fn##_abi##_);                                           \
  } else {                                                                                         \
    _fn##_abi##_ = nullptr;                                                                        \
  }

  FOR_ALL_MODULE_FUNCTIONS(_GET_PROXY);

  if (abiVersion() == AbiVersion::ProxyWasm_0_1_0) {
    _GET_PROXY_ABI(on_request_headers, _abi_01);
    _GET_PROXY_ABI(on_response_headers, _abi_01);
  } else if (abiVersion() == AbiVersion::ProxyWasm_0_2_0 ||
             abiVersion() == AbiVersion::ProxyWasm_0_2_1) {
    _GET_PROXY_ABI(on_request_headers, _abi_02);
    _GET_PROXY_ABI(on_response_headers, _abi_02);
    _GET_PROXY(on_foreign_function);
  }
#undef _GET_PROXY_ABI
#undef _GET_PROXY
}
// proxy_wasm src/v8/v8.cc
#define _GET_MODULE_FUNCTION(T)                                                                    \
  void getFunction(std::string_view function_name, T *f) override {                                \
    getModuleFunctionImpl(function_name, f);                                                       \
  };
  FOR_ALL_WASM_VM_EXPORTS(_GET_MODULE_FUNCTION)
#undef _GET_MODULE_FUNCTION

template <typename... Args>
void V8::getModuleFunctionImpl(std::string_view function_name,
                               std::function<void(ContextBase *, Args...)> *function) {
  auto it = module_functions_.find(std::string(function_name));
  if (it == module_functions_.end()) {
    *function = nullptr;
    return;
  }
  const wasm::Func *func = it->second.get();
  auto arg_valtypes = convertArgsTupleToValTypes<std::tuple<Args...>>();
  auto result_valtypes = convertArgsTupleToValTypes<std::tuple<>>();
  if (!equalValTypes(func->type()->params(), arg_valtypes) ||
      !equalValTypes(func->type()->results(), result_valtypes)) {
    fail(FailState::UnableToInitializeCode,
         "Bad function signature for: " + std::string(function_name) +
             ", want: " + printValTypes(arg_valtypes) + " -> " + printValTypes(result_valtypes) +
             ", but the module exports: " + printValTypes(func->type()->params()) + " -> " +
             printValTypes(func->type()->results()));
    *function = nullptr;
    return;
  }
  *function = [func, function_name, this](ContextBase *context, Args... args) -> void {
    const bool log = cmpLogLevel(LogLevel::trace);
    // 将 API 分装为一个闭包函数。且在执行 API 之前,会通过 SaveRestoreContext 的构造和析构来设置 current_context_
    // current_context_ 是一个 thread_local 的全局变量。每当 Envoy 从外部调用 WASM 沙箱内 API 时,将会设置该 context
    // 当 WASM 沙箱中代码调用 Envoy API 时,就可以获得正确的 context
    SaveRestoreContext saved_context(context);
    wasm::own<wasm::Trap> trap = nullptr;

    // Workaround for MSVC++ not supporting zero-sized arrays.
    if constexpr (sizeof...(args) > 0) {
      wasm::Val params[] = {makeVal(args)...};
      if (log) {
        integration()->trace("[host->vm] " + std::string(function_name) + "(" +
                             printValues(params, sizeof...(Args)) + ")");
      }
      trap = func->call(params, nullptr);
    } else {
      if (log) {
        integration()->trace("[host->vm] " + std::string(function_name) + "()");
      }
      trap = func->call(nullptr, nullptr);
    }

    if (trap) {
      fail(FailState::RuntimeError, getFailMessage(std::string(function_name), std::move(trap)));
      return;
    }
    if (log) {
      integration()->trace("[host<-vm] " + std::string(function_name) + " return: void");
    }
  };
}

getFunctions 中需要注意的一个细节是,将 WASM 沙箱暴露的 API 绑定到 proxy_wasm::WasmBase 成员时,会将 API 再次包装,并在 API 调用前后,通过 SaveRestoreContext 的构造和析构来完成 current_context_ 的设置和重置

再次回到 proxy_wasm::createWasm 函数,它还没有结束。可以从代码看到,在完成 WASM 沙箱的 initialize 之后,函数立刻克隆了一个新的沙箱并且调用其 start 以及 configure 等方法,校验字节码以及字节码配置。最后,一开始创建的 WASM 沙箱被包装在 WasmHandleBase 中被返回

另外,要注意到,在 proxy_wasm::createWasm 创建并初始化完成的 WASM 沙箱或者说虚拟机会以参数中的 vm_key 为 key 存储在 base_wasms

小结:initialize 是在创建 WASM 虚拟机过程中最为关键的一个函数。它完成的 Envoy 主进程和 Sandbox 的对接和交互

在这里插入图片描述

  1. 通过 registerCallbacks 将 Envoy 需要暴露的 API 封装为沙箱可以执行的函数类型(WASM runtime 提供 C++ API 实现)
  2. link 将封装好的 Envoy API 根据名称注册到沙箱中,并从 WASM 沙箱中导出 Envoy 主进程所需要的 API
  3. getFunctions 将沙箱导出的 API 绑定到 proxy_wasm::WasmBase 上,供后续 Envoy 调用
4)、HTTP 请求处理流程

当请求到来时,FilterConfig 的 createFilter 方法会被调用,创建一个 HTTP Filter。它首先会从通过 Envoy 的 TLS 机制(Thread Local Storage)获取配置初始化阶段最后创建的 PluginHandle 并获取对应的 WASM 沙箱,然后基于该沙箱创建一个 Wasm::Context 实例:

// source/extensions/filters/http/wasm/wasm_filter.h
  std::shared_ptr<Context> createFilter() {
    Wasm* wasm = nullptr;
    if (!tls_slot_->currentThreadRegistered()) {
      return nullptr;
    }
    PluginHandleSharedPtr handle = tls_slot_->get()->handle();
    if (!handle) {
      return nullptr;
    }
    if (handle->wasmHandle()) {
      wasm = handle->wasmHandle()->wasm().get();
    }
    if (!wasm || wasm->isFailed()) {
      if (handle->plugin()->fail_open_) {
        return nullptr; // Fail open skips adding this filter to callbacks.
      } else {
        return std::make_shared<Context>(nullptr, 0,
                                         handle); // Fail closed is handled by an empty Context.
      }
    }
    return std::make_shared<Context>(wasm, handle->rootContextId(), handle);
  }

Wasm::Context 是 Envoy WASM 中第二个核心类型。FilterConfig 负责了 WASM 沙箱的创建和管理,而 Wasm::Context 则负责了 Envoy API 的实现以及 Filter 的包装

Wasm::Context 实现了 HTTP Filter/Network Filter 的的接口,同时继承了 proxy_wasm::ContextBase 类型。几乎 Envoy 所有对 WASM 沙箱暴露的 API 都由 Wasm::Context 实际实现

当 Envoy 中 HCM 调用 Wasm::ContextdecodeTrailers 方法时,将会调用到基类 proxy_wasm::ContextBaseonRequestTrailers 方法,并最终调用沙箱对外暴露的相关 API(绑定在 proxy::WasmBase 中)

// source/extensions/common/wasm/context.cc
Http::FilterTrailersStatus Context::decodeTrailers(Http::RequestTrailerMap& trailers) {
  if (!in_vm_context_created_) {
    return Http::FilterTrailersStatus::Continue;
  }
  request_trailers_ = &trailers;
  auto result = convertFilterTrailersStatus(onRequestTrailers(headerSize(&trailers)));
  if (result == Http::FilterTrailersStatus::Continue) {
    request_trailers_ = nullptr;
  }
  return result;
}
// proxy_wasm src/context.cc
FilterTrailersStatus ContextBase::onRequestTrailers(uint32_t trailers) {
  CHECK_FAIL_HTTP(FilterTrailersStatus::Continue, FilterTrailersStatus::StopIteration);
  if (!wasm_->on_request_trailers_) {
    return FilterTrailersStatus::Continue;
  }
  DeferAfterCallActions actions(this);
  // 最终调用沙箱对外暴露的相关 API
  const auto result = wasm_->on_request_trailers_(this, id_, trailers);
  CHECK_FAIL_HTTP(FilterTrailersStatus::Continue, FilterTrailersStatus::StopIteration);
  return convertVmCallResultToFilterTrailersStatus(result);
}

而在 WASM 沙箱内部,当对应的 API 被调用,在代码的执行过程当中同样可能会调用外部主进程向沙箱注册的一些 API(前文中提到的 proxy_wasm::exports 名称空间下的相关函数)。举例来说,当在沙箱中调用 API 来获取 HTTP request 中 Headers 时,将会调用 proxy_wasm::exportsget_header_map_value 函数

// proxy_wasm src/exports.cc
Word get_header_map_value(Word type, Word key_ptr, Word key_size, Word value_ptr_ptr,
                          Word value_size_ptr) {
  if (type > static_cast<uint64_t>(WasmHeaderMapType::MAX)) {
    return WasmResult::BadArgument;
  }
  // 获取传递请求上下文 ContextBase
  auto *context = contextOrEffectiveContext();
  auto key = context->wasmVm()->getMemory(key_ptr, key_size);
  if (!key) {
    return WasmResult::InvalidMemoryAccess;
  }
  std::string_view value;
  // 调用 context 的 getHeaderMapValue 方法
  auto result =
      context->getHeaderMapValue(static_cast<WasmHeaderMapType>(type.u64_), key.value(), &value);
  if (result != WasmResult::Ok) {
    return result;
  }
  if (!context->wasm()->copyToPointerSize(value, value_ptr_ptr, value_size_ptr)) {
    return WasmResult::InvalidMemoryAccess;
  }
  return WasmResult::Ok;
}

这里获取传递请求上下文 ContextBase 并调用其 getHeaderMapValue 方法,如此就完成了 Envoy 调用沙箱 API,沙箱内调用 Envoy API 的完整交互过程了

小结:在 Envoy 请求处理过程中,当插件链执行到 WASM Filter 时,作为 HTTP Filter 和 Network Filter 包装器的 Wasm::Context 的相应接口会被调用,并且最终会调用沙箱 API。而沙箱在执行过程中,也会通过调用 Envoy API(proxy_wasm::exports 中的各个函数)获取或者修改请求状态。Envoy API 的最终也会调用 Wasm::Context 中的某个成员方法如 getHeaderMapValue 来实现对特定请求的处理

5)、proxy-wasm-cpp-sdk
  • 如何保证沙箱 API 与 proxy::WasmBase 中待绑定的成员一一对应?沙箱 API 最终都会在包装之后绑定到 proxy::WasmBase 的成员中再调用
  • 如何保证 Envoy API 与沙箱需要引用的外部 API 一一对应?沙箱可以调用的外部方法都在 proxy_wasm::exports 中,必须保证沙箱只引入这一部分 API

proxy-wasm-cpp-sdk 主要的责任在于两点:

  • 对 Envoy 主进程:暴露特定的沙箱 API 同时声明对 Envoy API 的引用
  • 对 WASM Filter:通过良好的封装隐藏 Envoy API 的细节;通过继承关系和接口的约束,保证暴露的沙箱 API 具备相应的实现

下面是 SDK 中沙箱 API 和 Envoy API 的相关声明。可以注意到,沙箱 API 声明和 proxy_wasm::WasmBase 中待绑定 API 的数据成员具备对应关系。而 Envoy API 声明则和 proxy_wasm::exports 中函数具备对应关系

// proxy-wasm-cpp-sdk proxy_wasm_externs.h
// Sandbox API 声明
extern "C" FilterTrailersStatus proxy_on_request_trailers(uint32_t context_id, uint32_t trailers);
extern "C" FilterMetadataStatus proxy_on_request_metadata(uint32_t context_id, uint32_t nelements);

// Envoy API 声明
extern "C" WasmResult proxy_add_header_map_value(WasmHeaderMapType type, const char *key_ptr,
                                                 size_t key_size, const char *value_ptr,
                                                 size_t value_size);
extern "C" WasmResult proxy_get_header_map_value(WasmHeaderMapType type, const char *key_ptr,
                                                 size_t key_size, const char **value_ptr,
                                                 size_t *value_size);

下面一段源码是沙箱 API 的实现(Envoy API 在 SDK 内没有实现只能加载后绑定 Envoy 主进程提供的实现)。可以看到,沙箱 API 最终会通过 context_id 找到和 Envoy 主进程中 Wasm::Contextproxy_wasm::ContextBase)实例对应的一个 WASM 沙箱内 SDK Context 实例,并调用其对应的接口实现

// proxy-wasm-cpp-sdk proxy_wasm_intrinsics.cc
// Sandbox API 实现
extern "C" PROXY_WASM_KEEPALIVE FilterMetadataStatus proxy_on_request_metadata(uint32_t context_id,
                                                                               uint32_t elements) {
  return getContext(context_id)->onRequestMetadata(elements);
}

extern "C" PROXY_WASM_KEEPALIVE FilterDataStatus proxy_on_request_body(uint32_t context_id,
                                                                       uint32_t body_buffer_length,
                                                                       uint32_t end_of_stream) {
  return getContext(context_id)
      ->onRequestBody(static_cast<size_t>(body_buffer_length), end_of_stream != 0);
}

SDK Context 的部分定义如下:

// proxy-wasm-cpp-sdk proxy_wasm_api.h
class Context : public ContextBase {
public:
  // ...
  virtual FilterHeadersStatus onRequestHeaders(uint32_t /* headers */, bool /* end_of_stream */) {
    return FilterHeadersStatus::Continue;
  }
  
  virtual FilterMetadataStatus onRequestMetadata(uint32_t /* elements */) {
    return FilterMetadataStatus::Continue;
  }
  
  virtual FilterDataStatus onRequestBody(size_t /* body_buffer_length */,
                                         bool /* end_of_stream */) {
    return FilterDataStatus::Continue;
  }
  
  virtual FilterTrailersStatus onRequestTrailers(uint32_t /* trailers */) {
    return FilterTrailersStatus::Continue;
  }

  virtual FilterHeadersStatus onResponseHeaders(uint32_t /* headers */, bool /* end_of_stream */) {
    return FilterHeadersStatus::Continue;
  }
  // ...
};
6)、小结

在这里插入图片描述

  • Wasm::Context:封装 HTTP/Network Filter 接口使得 Envoy 上层可以将其嵌入到插件链中。同时提供了对 Envoy API 的具体实现。封装了请求上下文。继承自 proxy_wasm::ContextBase
  • proxy_wasm::ContextBase:封装 proxy_wasm::WasmBase 中绑定的 WASM 沙箱 API
  • Wasm::Wasm:对 WASM 沙箱的上层封装,继承自 proxy_wasm::WasmBase。相比于其基类,增加了 Envoy 相关的一些状态,如 stats 指标监控,日志以及一些全局的 API 如 Cluster Manager 等
  • proxy_wasm::WasmBase:proxy_wasm 中对 WASM 沙箱的封装。通过组合管理 proxy_wasm::WasmVM。绑定 WASM 沙箱 API 以提供给 proxy_wasm::ContextBase 调用
  • proxy_wasm::WasmVM:对不同类型 WSAM runtime 的封装,暴露处统一的对外接口,如注册 API、获取 API 等
  • proxy_wasm::exports:名称空间。其中包含所有 Envoy 提供给 WASM 沙箱的 Envoy API 函数

在这里插入图片描述

参考:

Envoy WASM 源码抽丝剥茧

envoy源码分析:线程模型与沙箱机制

《Istio权威指南(下) 云原生服务网格Istio架构与源码》

推荐文章:

WebAssembly 在 MOSN 中的实践 - 基础框架篇

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐