Day 16 / 共 20 天 · 第 4 周 扩展/可观测/构建

扩展机制

第 4 周讲 Envoy 的生态。今天是地基:扩展机制。Envoy 有 275 个扩展(62 个 HTTP 过滤器…),靠 Registry::RegisterFactory 工厂模式 + 编译期静态注册 + 裁剪。理解它才能看懂 Wasm、过滤器等一切扩展。

📍 你在整门课的位置 · 第 4 周「扩展/可观测/构建」(W1 架构 → W2 请求 → W3 xDS/上游 → W4 扩展生态·收官
D16 扩展机制 D17 Wasm D18 可观测 D19 Buffer/Bazel D20 收官
L01

按类别组织

source/extensions/ 每个子目录 = 一个扩展点类别:filters/http(62 个:cors/ext_authz/jwt_authn/wasm/lua…)、filters/networkclusterstransport_socketsaccess_loggerstracersload_balancing_policieswasm_runtime

读法:每个类别对应一个"扩展点"——Envoy 在这些点上允许插入不同实现。你前面学的负载均衡(Day 14)、健康检查(Day 15)、L7 过滤器(Day 08)全是扩展。类别名如 envoy.filters.httpenvoy.tracers
🤔 痛点:配置里写个字符串,Envoy 凭什么找到那段代码? 你在 YAML 里写 name: envoy.filters.http.wasm。Envoy 里有 275 个扩展、62 个 HTTP 过滤器——它怎么把这个字符串对上正确的那段 C++ 实现?总不能写一堆 if name=="..." 硬编码吧?
💡 本质:Registry = 公司"通讯录(名字 → 分机)" 注册表就是一本电话簿:每个扩展在程序启动时把"我的名字 → 我的工厂"登记进一个全局哈希表。运行时按配置里的名字一查,就拿到工厂。更妙的是"构造即注册"——像新员工入职自动被录入通讯录:你只要在全局作用域声明一个 RegisterFactory 对象,静态初始化时就自动登记,不用手动调注册函数。REGISTER_FACTORY 宏就是这一行"入职登记表"。
📝 举个例子:三个阶段各发生一次启动REGISTER_FACTORY(WasmFilterConfig, …)"envoy.filters.http.wasm" → 工厂 写进电话簿。
配置解析:HCM 读到该名字 → getFactory("envoy.filters.http.wasm") 查电话簿拿工厂。
每请求建链:调工厂的 createFilterFactoryFromProto 返回闭包 → 把过滤器装进链子。
① 启动·注册REGISTER_FACTORY名字→工厂 入册 ② 配置解析·查找getFactory(name)查电话簿拿工厂 ③ 每请求·创建createFilterFactory装过滤器进链 📒 全局注册表 FactoryRegistry<Base>按扩展类型分表:HTTP过滤器一本、tracer 一本…
扩展的一生三阶段:启动时"入册"、配置解析时"按名查找"、每请求时"创建并装链"。注册表就是那本电话簿。
L02

Registry 注册表

// envoy/registry/registry.h(扩展体系地基,669 行)
template<class Base> class FactoryRegistry {          // :166 按 Base 类型模板化的全局注册表
  static ... factories();                             // :190 name → 工厂 的静态单例哈希表
  static void registerFactory(Base&, name);           // :230 按名注册(重复注册抛异常)
  static Base* getFactory(name);                      // :293 按名查找
};
注册表 = "按名字找实现的电话簿" 你在配置里写 envoy.filters.http.wasm,Envoy 怎么知道用哪段代码?靠注册表:每个扩展在程序启动时把"我的名字 → 我的工厂"登记进一个全局哈希表(电话簿)。运行时按配置里的名字一查,就拿到对应的工厂。FactoryRegistry<Base> 按扩展类型(Base)分表——HTTP 过滤器一张表、tracer 一张表…,避免混淆。这是"依赖注入/服务定位"的经典实现。
L03

RegisterFactory

// registry.h:507
template<class T, class Base> class RegisterFactory {
  RegisterFactory() {   // :512 构造函数静态实例化时自动注册
    FactoryRegistry<Base>::registerFactory(instance_, instance_.name());
    // :522 并把 category() 注册进 FactoryCategoryRegistry
  }
};
"构造即注册"的巧思 注意 RegisterFactory构造函数里做注册——只要在全局作用域创建一个它的实例,程序启动(静态初始化)时就自动执行注册,把工厂登记进电话簿。这是 C++ "静态初始化时自动执行代码"的常用技巧:你不用手动调"注册",只要声明一个全局对象,链接进二进制就自动生效。下一课的 REGISTER_FACTORY 宏就是包装这个。
L04

REGISTER_FACTORY 宏

// registry.h:641
#define REGISTER_FACTORY(FACTORY, BASE)                    \
  void forceRegister##FACTORY() {                          \
    static auto registered = new RegisterFactory<FACTORY, BASE>();  \
  }
// 用法(source/extensions/filters/http/wasm/config.cc:15):
REGISTER_FACTORY(WasmFilterConfig, NamedHttpFilterConfigFactory);
读法:一个宏,一行搞定注册。写完一个扩展,在它的 config.ccREGISTER_FACTORY(你的工厂, 类别基类),它就自动登记。Wasm 过滤器就是这样注册的。LEGACY_REGISTER_FACTORY 带旧名别名(兼容)。
L05

工厂接口

// envoy/server/filter_config.h:310 —— HTTP 过滤器工厂接口
class NamedHttpFilterConfigFactory {
  virtual absl::StatusOr<Http::FilterFactoryCb>
  createFilterFactoryFromProto(const Protobuf::Message& config, ...) PURE;  // :323
  std::string category() const override { return "envoy.filters.http"; }   // :265
};
读法:createFilterFactoryFromProto 是核心:拿到配置(proto),返回一个 FilterFactoryCb(一个"把过滤器塞进链子"的闭包)。category() 声明"我属于哪类扩展"。模板基类 CommonFactoryBase 去掉样板代码,子类只写关键逻辑。
L06

完整注册链路

配置里出现 envoy.filters.http.wasm
 → HCM 用该 name 调 getFactory("envoy.filters.http.wasm")   # 查电话簿
 → 拿到 WasmFilterConfig 工厂
 → createFilterFactoryFromProto()  # 返回一个把 filter 塞进链的闭包
 → 每个请求建链时执行闭包,安装 filter
读法:这就是"配置里的一个名字"到"运行时真正装上过滤器"的完整链路。注册(启动时静态)+ 查找(配置解析时)+ 创建(每请求建链时)三个阶段。Day 05/07 讲的"创建 L7 过滤器链"就是在这一步查工厂、执行闭包。
💬 小白 vs 老师:扩展是"运行时加载"还是"编译进去"? 👶 小白:注册表听着像插件系统,是不是运行时把 .so 动态加载进来?
👨‍🏫 老师:不是。Envoy 的扩展是编译进二进制的——性能最优、类型安全、链接期就发现问题。注册表只是"启动时自动登记 + 运行时按名查找",代码本身早就在二进制里了。
👶 小白:那加个扩展岂不是要重编译整个 Envoy?
👨‍🏫 老师:对,代价就是重编译。所以有两条出路:① Bazel 的 extensions_build_config.bzl 裁剪(换清单,L07/D19);② Wasm 运行时动态加载(D17)——这正是 Higress 插件生态的基石。
⚠️ 常见误解:以为 category() 只是个标签没用。其实它决定"登记进哪一本电话簿"——HTTP 过滤器、tracer、LB 各一本,避免同名扩展跨类别冲突,也让 Envoy 能按类别枚举可用扩展。
L07

编译期裁剪

哪些扩展被编进二进制?由 Bazel 的 extensions_build_config.bzl(275 条)决定。下游(Higress/Istio)可以整体替换这份配置来裁剪扩展集,无需改 Envoy 源码。

"静态注册 + 编译期裁剪"的取舍 Envoy 的扩展是编译进二进制的(不是运行时动态加载 .so)。好处:性能最优(无动态加载开销)、类型安全、链接期就能发现问题。代价:加/减扩展要重新编译。但 Bazel 的裁剪机制让这很灵活——extensions_build_config.bzl 列出要编入哪些扩展,Higress 可以提供自己的这份清单(去掉用不到的、加上自己的),生成一个定制的 Envoy 二进制。Wasm(Day 17)则提供了"运行时动态加载"的补充——用 WebAssembly 插件避免重编译。这就是为什么 Higress 生态重度依赖 Wasm。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 扩展按什么组织?举几个扩展类别。
  • Registry 注册表解决什么问题?
  • "构造即注册"是什么技巧?REGISTER_FACTORY 宏怎么用?
  • 工厂接口的 createFilterFactoryFromProto 返回什么?
  • "配置名 → 装上过滤器"的完整链路?编译期裁剪的取舍?

✋ 动手

cd /Users/bitmart/work/codes/github/higress-group/envoy
sed -n '166,240p' envoy/registry/registry.h | head -40
sed -n '641,655p' envoy/registry/registry.h    # REGISTER_FACTORY 宏
grep -n 'REGISTER_FACTORY' source/extensions/filters/http/wasm/config.cc
明天预告 · Day 17Wasm 扩展(Higress 的基础!)——WebAssembly 插件怎么工作、proxy-wasm ABI、Context 双向翻译层、VM 生命周期。这是理解 Higress 插件生态的关键一课。
← Day 10 Codec Day 17 · Wasm 扩展 →