Go HTTP 过滤器 (envoy.filters.http.golang) 允许使用 Go 编写 Envoy HTTP 过滤器。Go 代码被编译为共享库 (.so),在运行时通过 dlopen 加载,并借由 CGo bridge 集成到 Envoy 的 C++ 过滤器链中。

架构#

Go HTTP Filter 主要分为两端:

  • C++ 端:负责 DSO 加载、过滤器链集成以及线程安全。
  • Go 端:通过 Go SDK 实现过滤器逻辑,通过 CGo 与 C++ 通信。

ABI 边界由 contrib/golang/common/go/api/api.h 中的共享 C 结构体定义。

1d4a997271052a5a6344e5235da18b13_MD5

DSO (动态共享对象) 加载机制#

Go HTTP 过滤器通过 contrib/golang/common/dso/ 中定义的专用 DSO 机制加载 Go 编译的共享库。这与 Envoy 核心扩展中的 dynamic_modules 是两套独立的实现。

核心组件#

1. DSO 管理器 (DsoManager)#

DsoManager 类 (contrib/golang/common/dso/dso.h) 负责管理共享库的生命周期:

  • 动态加载:使用 dlopen() 在运行时加载 .so 文件。
  • 符号解析:在加载后通过 dlsym() 显式解析 10 个关键的 Go 导出函数(定义在 dso.cc:47-70,如 envoyGoFilterOnHttpHeader 等)。
  • RTLD 模式
    • RTLD_LAZY:延迟符号解析以提高性能。
    • RTLD_GLOBAL:使符号对其他加载的模块可见。
    • RTLD_NODELETE关键设置。由于 Go 运行时不支持 dlclose() 后的完全卸载,必须防止库被从进程中移除,否则会引发段错误。

支持范围与局限性#

与旨在覆盖 Envoy 所有扩展点的通用 Dynamic Modules 机制不同,当前的 Go 扩展机制专门针对 HTTP 过滤场景设计:

  • 专注点:目前仅支持 HTTP 过滤器扩展,代码位于 contrib/golang/filters/http/source/
  • 深层绑定:由于 Go 语言需要处理 Goroutine 调度、CGo 栈切换以及复杂的请求上下文映射,其 ABI 绑定(api.h)比通用的动态模块更加复杂和深入。
  • 不支持的扩展:目前无法使用此 Go 机制扩展网络过滤器、UDP 过滤器、负载均衡策略或集群类型等。

配置#

定义在 api/contrib/envoy/extensions/filters/http/golang/v3alpha/golang.proto 中:

  • library_id — 共享库的唯一 ID。
  • library_path.so 文件的路径。
  • plugin_name — 必须与 Go 中通过 RegisterHttpFilterFactoryAndConfigParser 注册的名称匹配。
  • plugin_config — 任意插件特定的配置 (google.protobuf.Any)。

支持通过 ConfigsPerRoute 进行按路由配置,并带有合并策略。

请求生命周期#

209152189f3ef904c0ee10c31c759ecb_MD5

启动 (Startup)#

Go 运行时在 dlopen() 内部初始化,甚至在返回 C++ 之前。以下是完整的时间线:

阶段 1: dlopen() 触发 Go 运行时引导

当 Envoy 在启动时解析过滤器配置时,它调用 DsoManager::load() (contrib/golang/common/dso/dso.h:306),后者构造一个 Dso 并调用:

handler_ = dlopen(dso_name.c_str(), RTLD_LAZY);  // dso.cc:26

.so 是使用 go build -buildmode=c-shared 构建的,它会注入一个 __attribute__((constructor)) 函数(来自 Go 内部的 runtime/cgo/gcc_libinit.c)。动态链接器在 dlopen() 返回之前运行此构造函数,该函数会:

  1. 分配 G0 goroutine 和 M0 OS 线程。
  2. 初始化 GC、调度器和信号处理。
  3. 将 GOMAXPROCS 设置为 CPU 核心数(Envoy 代码中没有覆盖此设置)。
  4. 调用 main() — 在 c-shared 模式下永远不会调用 main()

阶段 2: Go init() 函数运行 (仍在 dlopen() 内部)

在运行时引导之后,所有 init() 函数按依赖顺序执行:

contrib/golang/common/go/api_impl/capi_impl.go:61-86 中的关键 init()

func init() {
    api.SetCommonCAPI(&commonCApiImpl{})                   // 注册 C API 实现
    currLogLevel.Store(int32(C.envoyGoFilterLogLevel()))   // 第一次通过 CGo 回调 C++
    go func() { /* 后台 goroutine:每 1s 同步一次日志级别 */ }()
}

这是 Go 第一次回调 Envoy C++ 代码 — C.envoyGoFilterLogLevel()。它还派生了唯一的永久后台 goroutine。

用户插件的 init() 函数也在这里运行,通常调用 RegisterHttpFilterFactoryAndConfigParser() (contrib/golang/filters/http/source/go/pkg/http/filtermanager.go:53) 来注册过滤器工厂。

包级变量也在此初始化:

  • Requests = &requestMap{} (shim.go:46)
  • httpFilterFactoryAndParser = sync.Map{} (filtermanager.go:28)
  • configCache = &sync.Map{} (config.go:49)

在所有 init() 函数完成后,dlopen() 终于返回到 C++。

阶段 3: 符号解析 (dlopen() 返回后)

回到 C++,HttpFilterDsoImpl 调用 dlsym() 10 次 (dso.cc:47-70) 来解析 Go 导出的符号,如 envoyGoFilterOnHttpHeaderenvoyGoFilterOnHttpData 等。

阶段 4: 逐个工作线程的请求映射初始化

FilterConfig::newGoPluginConfig() (golang_filter.cc:1787) 调用 Go 的 envoyGoFilterNewHttpPluginConfig() (config.go:70),该函数运行一次:

Requests.initialize(uint32(c.concurrency))  // config.go:76

这会分配逐个工作线程 (per-worker-thread) 的请求映射 (shim.go:70-79),大小取决于 Envoy 的并发设置。这由 sync.Once 保护。

时间线总结

步骤 操作 位置
1 Envoy 在解析配置时调用 dlopen() dso.cc:26
2 Go 运行时引导 (GC, 调度器, G0/M0) Go 内部 __attribute__((constructor))
3 api_impl init() 运行:注册 CAPI, 首次 CGo 回调, 派生日志同步协程 api_impl/capi_impl.go:61
4 插件 init() 运行:注册过滤器工厂 用户插件代码
5 dlopen() 返回 dso.cc:26
6 dlsym() 解析 10 个 Go 导出的符号 dso.cc:47-70
7 newGoPluginConfig() 调用 Go; Requests.initialize(concurrency) 运行一次 config.go:70, shim.go:70
8 第一个请求触发 createRequest() 和用户的 StreamFilterFactory shim.go:128

线程注意事项

  • GOMAXPROCS 默认为 CPU 核心数 — Envoy 没有进行显式覆盖。
  • 代码中没有使用 runtime.LockOSThread()
  • 在 CGo 调用期间,Envoy 的工作线程变为 Go 的 “M” 线程。
  • Go 运行时无法被卸载 — 对于 Go .so 文件,dlclose() 实际上是一个空操作 (参见 https://github.com/golang/go/issues/11100, 在 dso.h:278 中被引用)。

单个请求 (Per-request)#

  1. 每个请求创建一个新的 Filter 实例 (contrib/golang/filters/http/source/config.cc:45)。
  2. Envoy 调用 Filter::decodeHeaders() -> doHeadersGo() (golang_filter.cc:236) -> 通过 CGo 调用 envoyGoFilterOnHttpHeader
  3. 在 Go 端 (contrib/golang/filters/http/source/go/pkg/http/shim.go:168),通过注册的工厂创建插件的 StreamFilter,然后调用 DecodeHeaders()
  4. Go 过滤器返回一个 状态 (status)
    • Continue — 继续处理下一个过滤器。
    • StopAndBuffer — 暂停并等待更多数据。
    • Running — 异步处理;Go 稍后将从 goroutine 调用 Continue()
    • LocalReply — 直接发送响应。
  5. 同样的模式在响应路径上的 decodeDatadecodeTrailers 以及 encodeHeadersencodeDataencodeTrailers 中重复。

异步 / 线程安全 (Async / Thread Safety)#

当 Go 代码异步运行(返回 Running)时,它最终会调用 cAPI.HttpContinue(),这会触发 C++ 的 envoyGoFilterHttpContinue() (contrib/golang/filters/http/source/cgo.cc:94)。这会 向 Envoy 调度器 (dispatcher) 发送一个任务,以便在正确的工作线程上恢复处理。

对于阻塞式的 Go->C++ 调用(例如从 goroutine 获取元数据),Go 端会在 sync.Cond 上阻塞,C++ 在调度器回调完成后通过 envoyGoRequestSemaDec() 通知它。

C++ 到 Go 调用流程细节#

以下是 decodeHeaders() 作为典型的 C++ → Go 调用示例的完整流程。decodeDataencodeHeaders 等的模式与之完全一致。

阶段 1: C++ 过滤器调用

入口点:golang_filter.cc:236 中的 Filter::decodeHeaders()

Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, bool end_stream) {
    return doHeadersGo(&headers, end_stream, /*is_trailers=*/false);
}

doHeadersGo() (golang_filter.cc:254-301) 准备 C 结构体并调用 Go:

Http::FilterHeadersStatus Filter::doHeadersGo(Http::HeaderMap* headers, bool end_stream, bool is_trailers) {
    // 1. 将 C++ 请求头序列化为 C 数组
    std::vector<absl::string_view> keys, values;
    headers->iterate([&](const Http::HeaderEntry& h) {
        keys.push_back(h.key().getStringView());
        values.push_back(h.value().getStringView());
        return Http::HeaderMap::Iterate::Continue;
    });

    // 2. 打包到 C 结构体 (定义在 api.h:30-45)
    auto* header_array = new CAPIHeaderMap[keys.size()];
    for (size_t i = 0; i < keys.size(); i++) {
        header_array[i].key = keys[i].data();
        header_array[i].key_len = keys[i].size();
        header_array[i].value = values[i].data();
        header_array[i].value_len = values[i].size();
    }
    CAPIHeaderMapValue header_map{header_array, keys.size()};

    // 3. 通过函数指针调用 Go (在 dso.cc:47-70 中由 dlsym 解析)
    auto status = dynamic_lib_->envoyGoFilterOnHttpHeader(
        config_id_, reinterpret_cast<uint64_t>(this), header_map, end_stream);

    delete[] header_array;
    return static_cast<Http::FilterHeadersStatus>(status);
}

阶段 2: DSO 函数分发

dynamic_lib_->envoyGoFilterOnHttpHeader 是在 dso.h:108 中定义的函数指针:

using envoyGoFilterOnHttpHeader_type = int (*)(uint64_t config_id, uint64_t http_request,
                                                CAPIHeaderMapValue headers, bool end_stream);
// 在加载时通过 dlsym() 解析 (dso.cc:47-70)

阶段 3: CGo 运行时转换

当调用函数指针时,控制权进入 Go 的 CGo 运行时 (runtime/cgo/gcc_amd64.S 或平台对应文件):

  1. 线程绑定 — 确保 OS 线程有一个关联的 Go “M” (machine)。如果没有,则从调度器中分配或窃取一个。
  2. 栈切换 — 从 C 栈切换到 Go goroutine 栈。线程上的第一次调用会分配一个 goroutine 栈。
  3. 寄存器保存 — 根据 AMD64 ABI 保存 C 寄存器,因为 Go 可能会覆盖它们。
  4. GC 协调 — 通知 GC 外部代码正在进入 Go(并发 GC 的内存屏障)。

阶段 4: Go 函数执行

Go 入口点在 cgo_exports.go:30-45(从 //export 注释生成):

//export envoyGoFilterOnHttpHeader
func envoyGoFilterOnHttpHeader(configId uint64, httpRequest uint64,
                                headers C.CAPIHeaderMapValue, endStream bool) int {
    // 将 C 请求头转换为 Go map
    goHeaders := make(map[string][]string)
    hdrSlice := (*[1 << 30]C.CAPIHeaderMap)(unsafe.Pointer(headers.headers))[:headers.len:headers.len]

    for i := 0; i < int(headers.len); i++ {
        key := C.GoStringN(hdrSlice[i].key, hdrSlice[i].key_len)
        value := C.GoStringN(hdrSlice[i].value, hdrSlice[i].value_len)
        goHeaders[key] = append(goHeaders[key], value)
    }

    // 路由到请求处理器
    return Requests.onHttpHeader(httpRequest, goHeaders, endStream)
}

shim.go:168-200 中的请求路由:

func (r *requestMap) onHttpHeader(httpRequest uint64, headers map[string][]string, endStream bool) int {
    req := r.get(httpRequest)  // 查找请求状态 (shim.go:128)
    if req == nil {
        return int(api.Continue)
    }

    // 在第一次调用时延迟创建过滤器
    if req.filter == nil {
        factory := getFilterFactory(req.configId)  // filtermanager.go:75
        req.filter = factory(req.config)
    }

    // 调用用户定义的过滤器
    headerMap := &requestHeaderMap{headers: headers}
    status := req.filter.DecodeHeaders(headerMap, endStream)  // 用户的 filter.go 实现
    return int(status)
}

阶段 5: 返回路径

  1. 用户的 DecodeHeaders() 返回 StatusType (filter.go:35-45):ContinueStopAndBufferRunningLocalReply
  2. 状态码通过 Requests.onHttpHeaderenvoyGoFilterOnHttpHeader 向上回传。
  3. CGo 运行时:恢复栈、GC 通知、返回 C。
  4. C++ 将返回的 int 转换为 FilterHeadersStatus 并继续过滤器链。

流程示意图:

C++: Filter::decodeHeaders()
    ↓
C++: doHeadersGo() — 将请求头序列化为 C 结构体 (CAPIHeaderMap)
    ↓
C++: dynamic_lib_->envoyGoFilterOnHttpHeader() — dlsym 解析的函数指针
    ↓
C: cgo 入口存根 (runtime/cgo) — 线程绑定, 栈切换, 耗时约 100-200ns
    ↓
Go: envoyGoFilterOnHttpHeader() — cgo_export, C 结构体 → Go map 转换
    ↓
Go: Requests.onHttpHeader() — 路由到具体请求, 延迟创建过滤器
    ↓
Go: req.filter.DecodeHeaders() — 用户过滤器逻辑
    ↓
Go: 返回 StatusType
    ↓
C: cgo 返回存根 — 栈恢复, GC 协调
    ↓
C++: 返回 FilterHeadersStatus
    ↓
C++: Envoy 过滤器链继续执行...

关键数据结构:

C 请求头结构体 (contrib/golang/common/go/api/api.h:30-45):

typedef struct {
    const char* key;
    size_t key_len;
    const char* value;
    size_t value_len;
} CAPIHeaderMap;

typedef struct {
    CAPIHeaderMap* headers;
    size_t len;
} CAPIHeaderMapValue;

Go 请求状态 (shim.go:40-60):

type httpRequest struct {
    configId   uint64
    config     interface{}      // 解析后的插件配置
    filter     StreamFilter     // 用户过滤器实例
    // ... 其他每个请求的状态
}

Go 到 C++ 回调流程细节#

Go → C++ 回调流程是 Go 过滤器代码调用回 Envoy 的方式(例如 Continue()SendLocalReply()GetDynamicMetadata())。以下是以 HttpContinue() 为例的完整流程。

阶段 1: Go 过滤器调用 CAPI

用户的协程调用 callbacks.Continue()

func (f *MyFilter) DecodeHeaders(headers api.RequestHeaderMap, endStream bool) api.StatusType {
    go func() {
        result := doExternalAuth()
        f.callbacks.Continue(api.Continue)  // 恢复过滤器链
    }()
    return api.Running
}

Go CAPI 接口 (capi.go:45):

type HttpCAPI interface {
    HttpContinue(r unsafe.Pointer, status uint64)
}

实现 (capi_impl.go:120-135):

func (c *commonCApiImpl) HttpContinue(r unsafe.Pointer, status uint64) {
    C.envoyGoFilterHttpContinue(r, C.uint64_t(status))  // CGo 调用
}

unsafe.Pointer r 是在 createRequest() (shim.go:128) 期间存储在 Go 请求状态中的 C++ Filter 实例指针。

阶段 2: CGo 进入 C++

C++ 入口点 (cgo.cc:94-130):

extern "C" void envoyGoFilterHttpContinue(void* r, uint64_t status) {
    auto* filter = static_cast<Filter*>(r);

    // 关键点:协程所在的 OS 线程可能与 Envoy 工作线程不同
    // 必须通过调度器 (dispatcher) 分发到正确的工作线程
    filter->dispatcher_.post([filter, status]() {
        filter->continueFilter(static_cast<FilterStatusType>(status));
    });
}

核心要点:Go 协程会在 OS 线程之间迁移。CGo 调用会在协程当时所在的任何线程上进入,这可能不是拥有该连接的 Envoy 工作线程。

阶段 3: 调度器任务执行

投递的 lambda 表达式在正确的工作线程上运行 (golang_filter.cc:150-180):

void Filter::continueFilter(FilterStatusType status) {
    current_status_ = status;

    if (decode_headers_done_ && !encode_headers_called_) {
        decoder_callbacks_->continueDecoding();  // 恢复解码路径
    } else if (encode_headers_called_) {
        encoder_callbacks_->continueEncoding();  // 恢复 encode 路径
    }
}

continueDecoding() 会唤醒暂停的过滤器链并继续处理下一个过滤器。

流程示意图:

Go: filter.callbacks.Continue(Continue)
    ↓
Go: C.envoyGoFilterHttpContinue(r, status)  // CGo 调用
    ↓
C: cgo.cc:94 中的 envoyGoFilterHttpContinue()
    ↓
C++: filter->dispatcher_.post(lambda)  // 在工作线程上调度
    ↓
[Go 协程返回, CGo 调用完成, 耗时约 100-200ns]
    ↓
[Envoy 工作线程 - 稍后]
    ↓
C++: lambda 执行 -> filter->continueFilter(status)
    ↓
C++: decoder_callbacks_->continueDecoding()
    ↓
C++: 过滤器链以新状态恢复执行

阻塞式回调模式:GetDynamicMetadata()

某些回调需要阻塞 Go 协程直到 C++ 提供结果,这使用了 sync.Cond

func (f *MyFilter) DecodeHeaders(headers api.RequestHeaderMap, endStream bool) api.StatusType {
    metadata := f.callbacks.GetDynamicMetadata("my_namespace")  // 在此处阻塞
    // ... 使用元数据
    return api.Continue
}

Go 端实现 (capi_impl.go:200-220):

func (c *commonCApiImpl) GetDynamicMetadata(r unsafe.Pointer, filterName string) map[string]interface{} {
    sema := acquireSema()  // 获取信号量 ID

    // 异步调用 C++
    C.envoyGoFilterGetDynamicMetadata(r, C.int64_t(sema.semaId), ...)

    sema.wait()  // 在 sync.Cond 上阻塞
    return sema.result  // 返回由 C++ 填充的结果
}

C++ 端实现 (cgo.cc:250-280):

extern "C" void envoyGoFilterGetDynamicMetadata(void* r, int64_t semaId, ...) {
    auto* filter = static_cast<Filter*>(r);
    const auto& metadata = filter->getDynamicMetadata();
    // ... 序列化为 C 字符串
    filter->semaDec(semaId, result);  // 通知完成
}

信号通知 (golang_filter.cc:400-420):

void Filter::semaDec(int64_t semaId, const std::string& result) {
    sema_results_[semaId] = result;
    envoyGoRequestSemaDec(semaId);  // 通过 CGo 回调 Go
}

Go 端解锁 (导出的函数):

//export envoyGoRequestSemaDec
func envoyGoRequestSemaDec(semaId C.int64_t) {
    sema := getSema(int64(semaId))
    sema.signal()  // 解锁等待中的协程
}

阻塞流程:

Go: GetDynamicMetadata()
    ↓
Go: 创建 sema, C.envoyGoFilterGetDynamicMetadata(r, semaId, ...)
    ↓
C: envoyGoFilterGetDynamicMetadata() — 在工作线程上读取元数据
    ↓
C++: filter->semaDec(semaId, result)
    ↓
C++: envoyGoRequestSemaDec(semaId)  // CGo 调用
    ↓
Go: sema.signal() — 解锁等待中的协程
    ↓
Go: [协程唤醒, 返回结果]

回调模式对比:

模式 示例 线程安全 使用场景
异步投递 (Async post) Continue() Go:任意线程;C++:调度器投递到工作线程 恢复过滤器链
阻塞 (Blocking) GetDynamicMetadata() Go:在 sync.Cond 上阻塞;C++:在工作线程运行并回传信号 读取同步状态
立即响应 (Immediate) SendLocalReply() Go:任意线程;C++:投递到调度器 发送响应

清理 (Cleanup)#

  1. 调用 onStreamComplete()log() 进行访问日志记录。
  2. onDestroy() 会唤醒任何等待中的 Go goroutine,并调用插件的 OnDestroy()
  3. Go 的 GC 终结器 (finalizer) 会回调 C++ 以释放 HttpRequestInternal 对象。

编写插件 (Writing a Plugin)#

插件需要实现 StreamFilter 接口 (contrib/golang/common/go/api/filter.go:80):

type StreamFilter interface {
    DecodeHeaders(RequestHeaderMap, bool) StatusType
    DecodeData(BufferInstance, bool) StatusType
    DecodeTrailers(RequestTrailerMap) StatusType
    EncodeHeaders(ResponseHeaderMap, bool) StatusType
    EncodeData(BufferInstance, bool) StatusType
    EncodeTrailers(ResponseTrailerMap) StatusType
    OnLog(RequestHeaderMap, ResponseHeaderMap, ResponseTrailerMap)
    OnDestroy(DestroyReason)
}

init() 中使用 RegisterHttpFilterFactoryAndConfigParser() (contrib/golang/filters/http/source/go/pkg/http/filtermanager.go:53) 进行注册。你可以嵌入 PassThroughStreamFilter 以仅覆盖你关心的函数。具体示例可参考 contrib/golang/filters/http/test/test_data/basic/

关键文件 (Key Files)#

区域 文件
C ABI 头文件 contrib/golang/common/go/api/api.h
DSO 加载 contrib/golang/common/dso/dso.{h,cc}
过滤器工厂 contrib/golang/filters/http/source/config.cc
核心过滤器逻辑 contrib/golang/filters/http/source/golang_filter.{h,cc}
Go->C++ 回调 contrib/golang/filters/http/source/cgo.cc
Go CGo 适配层 contrib/golang/filters/http/source/go/pkg/http/shim.go
Go SDK 接口 contrib/golang/common/go/api/filter.go
Proto 配置 api/contrib/envoy/extensions/filters/http/golang/v3alpha/golang.proto

常见问题 (Q&A)#

问:如果 Go 插件代码中的 main() 永远不被调用,为什么还需要它?

答:在 Go 的 -buildmode=c-shared 模式下,Go 编译器要求必须有 main() 函数,但它在加载 .so永远不会被执行

  1. Go 要求有 main — 即使是 c-shared 构建,Go 仍然要求代码是一个有效的、完整的 Go 程序,包含一个带有 func main()main 包。编译器会强制执行这一点;如果缺失,构建会报错 function main is undeclared in the main package
  2. C-shared 模式更改了入口点 — 当使用 -buildmode=c-shared 构建时:
    • main() 函数变成了一个永远不会被调用的存根 (stub)。
    • 实际的初始化是由生成的 __attribute__((constructor)) 函数完成的(来自 Go 的 runtime/cgo/gcc_libinit.c)。
    • 这个构造函数在 dlopen() 返回 之前 运行 init() 函数并引导运行时。
  3. main() 永远不会被调用main 符号确实会导出到 .so 中,但 dlopen() 不会调用它,也没有任何代码路径会调用它。它只是作为二进制文件中的一个未使用入口点。
  4. 设计产物 — Go 构建系统将 c-shared 视为普通程序的一种变体,而不是单纯的库。main() 函数充当了使其成为完整 Go 程序的“锚点”,尽管实际有用的导出是那些标记了 //export 的 C 可调用函数。

在 Envoy 的 Go 过滤器中,所有实际工作都发生在 init() 函数中(注册过滤器工厂、设置 CGo 桥接器),因此一个空的 main() 仅仅是满足编译器要求的样板代码。

问:调用导出的 Go 函数(CGo 调用)的性能损耗是多少?

答:CGo 调用具有 显著的开销 — 在现代 x86_64 硬件上,每次调用大约耗时 100-200 纳秒,比常规的 C++ 虚函数调用(~1-2 纳秒)慢约 100 倍

开销来源:

  1. 运行时上下文切换 — C 线程使用原生的 OS 栈;Go 使用分段/增长式栈。CGo 必须将线程绑定到 Go 的 “M” (machine) 和 “G” (goroutine),这可能涉及分配或窃取 M。
  2. 调度器协调 — CGo 调用被视为隐式的调度点。运行时会检查栈增长、GC 安全点,并可能触发带有内存屏障的协程重新调度,以进行 GC 同步。
  3. ABI 转换 — 保存/恢复寄存器、转换调用约定(从 System V AMD64 ABI 转换为 Go 的调用约定),以及设置 CGo 的 “G” 结构体。
  4. 栈管理 — 动态栈增长检查以及调用期间可能的栈分配。

对 Envoy 请求路径的影响:

仅仅为了完成基础的过滤器生命周期,每个请求至少涉及 6-7 次 CGo 跨越:

阶段 方向 CGo 调用
请求 C++ → Go decodeHeaders
请求 C++ → Go decodeData
请求 C++ → Go decodeTrailers
响应 C++ → Go encodeHeaders
响应 C++ → Go encodeData
响应 C++ → Go encodeTrailers
清理 C++ → Go onDestroy

回调到 C++ 的操作(例如 GetDynamicMetadataContinue)在反方向也会产生类似的开销。

实际影响:

  • 吞吐量:相比纯 C++ 过滤器,Go 过滤器可能会看到 10-20% 的性能下降。
  • 延迟:在高并发下,由于转换成本,P99 延迟会增加。
  • CPU 消耗:更多的周期消耗在运行时协调上,而不是业务逻辑。

建议:

  • 使用 Go 过滤器:当开发速度很重要、过滤器逻辑复杂(如鉴权、复杂业务逻辑)或者你需要利用 Go 生态系统时。
  • 避免 Go 过滤器:对于极低延迟的需求、简单的透传逻辑,或者每一纳秒都很关键的高吞吐量场景。
  • 性能关键点首选 C++

问:不同的 StatusType 值有什么区别,应该在什么时候使用?

答:四个 StatusType 值 (filter.go:35-45) 控制了 Envoy 处理过滤器链的方式。它们由 DecodeHeaders()DecodeData()EncodeHeaders() 等返回。

状态 用途 何时使用
Continue 立即恢复 同步工作已完成;无需阻塞
StopAndBuffer 暂停并缓冲主体 在做决定前需要完整的请求主体(如鉴权、校验)
Running 异步处理 从协程发起外部 I/O(HTTP 调用、数据库查询)
LocalReply 发送直接响应 鉴权失败、限流、校验不通过

Continue

立即恢复到下一个过滤器。用于简单的请求头检查/修改、日志记录或指标统计等无需外部 I/O 的场景。

func (f *MyFilter) DecodeHeaders(headers api.RequestHeaderMap, endStream bool) api.StatusType {
    headers.Set("x-custom-header", "value")
    return api.Continue  // 完成,恢复链条
}

StopAndBuffer

暂停链条并缓冲后续的主体分片。准备好后稍后调用 Continue()。用于基于主体的鉴权或内容校验。

func (f *MyFilter) DecodeHeaders(headers api.RequestHeaderMap, endStream bool) api.StatusType {
    return api.StopAndBuffer  // 等待主体
}

func (f *MyFilter) DecodeData(buffer api.BufferInstance, endStream bool) api.StatusType {
    f.bodyBuffer.Write(buffer.Bytes())
    if endStream {
        if validate(f.bodyBuffer.Bytes()) {
            return api.Continue
        }
        return api.LocalReply
    }
    return api.StopAndBuffer  // 需要更多数据
}

Running

表示异步工作正在进行中。派生一个 goroutine,稍后调用 callbacks.Continue(status)关键点:永远不要阻塞工作线程。

func (f *MyFilter) DecodeHeaders(headers api.RequestHeaderMap, endStream bool) api.StatusType {
    go func() {
        resp, _ := http.Get("http://auth-service/verify")
        if resp.StatusCode != 200 {
            f.callbacks.SendLocalReply(403, "Forbidden", nil, 0, "")
        } else {
            f.callbacks.Continue(api.Continue)  // 在工作线程上恢复
        }
    }()
    return api.Running  // 异步处理中
}

LocalReply

立即发送响应,不联系上游。直接短路到响应路径。

func (f *MyFilter) DecodeHeaders(headers api.RequestHeaderMap, endStream bool) api.StatusType {
    if !validateToken(headers) {
        f.callbacks.SendLocalReply(401, "Unauthorized", nil, 0, "auth_failure")
        return api.LocalReply
    }
    return api.Continue
}

核心区别:

  • StopAndBuffer vs Running:两者都会暂停链条,但 StopAndBuffer 用于同步的主体缓冲,而 Running 用于协程发起的异步 I/O。
  • Running 状态下,你 必须 调用 Continue(),否则请求会永久挂起。
  • LocalReply 可以从任何阶段(请求头、数据、尾部)返回,以立即中止。