手把手教你开发Envoy过滤器:核心原理+实战案例

手把手教你开发Envoy过滤器:核心原理+实战案例 一

文章目录CloseOpen

Envoy过滤器的核心原理:搞懂这3点,开发少走90%的弯路

你可以把Envoy想象成一个“流量管家”,所有进出服务的请求都要经过它。而过滤器就是这个管家的“工具箱”——你想让管家给请求盖章(加请求头)、称重(限流)还是拍照(日志),都得通过自定义过滤器来实现。但很多人一开始就卡在“工具箱怎么用”这个环节,其实核心就3个关键点,搞懂了后面开发会顺畅很多。

过滤器链:像流水线一样处理请求

先说说过滤器链(Filter Chain),这是Envoy最核心的设计之一。你可以把它理解成工厂的流水线:当一个请求进入Envoy时,会依次经过多个过滤器,每个过滤器负责处理特定任务,处理完再传给下一个,最后把处理好的请求发给目标服务;响应回来时也是同样的流程,不过是反向经过这些过滤器。

我举个真实例子,去年帮电商客户做“会员请求优先处理”功能时,我们需要在请求进来时先判断用户是否是会员(过滤器A),如果是就标记优先级(过滤器B),最后记录日志(过滤器C)。当时团队里有个刚毕业的同学,一开始想把这三个功能写到一个过滤器里,结果代码越写越乱,改一个逻辑整个过滤器都得重测。后来我 他拆成三个独立过滤器,不仅代码清晰了,而且想关掉日志功能时,直接在配置里注释掉过滤器C就行,不用改代码。这就是过滤器链的好处——职责分离,灵活组合。

这里有个关键细节你得注意:Envoy的过滤器分“解码”(Decode)和“编码”(Encode)两个阶段。简单说,请求从客户端到服务端走的是解码过滤器链(Decode Filters),响应从服务端回客户端走的是编码过滤器链(Encode Filters)。你开发时得想清楚自己的功能要在哪个阶段生效,比如加请求头要在解码阶段,加响应头则在编码阶段。我之前见过有人把响应头修改写到了解码过滤器里,结果调试了半天“为什么响应里看不到我加的头”,就是因为搞错了阶段。

核心接口与生命周期:别让你的过滤器“失控”

搞懂了过滤器链,接下来就得知道怎么写过滤器本身了。Envoy的过滤器开发主要靠继承特定的接口类,实现里面的回调方法。这里我 了最常用的几个接口,你可以先有个印象:

接口类型 核心回调 作用 新手常见坑
StreamDecoderFilter decodeHeaders(), decodeData(), decodeTrailers() 处理请求阶段的头部、数据、尾部 忽略end_stream参数,导致数据不完整
StreamEncoderFilter encodeHeaders(), encodeData(), encodeTrailers() 处理响应阶段的头部、数据、尾部 修改响应头后没调用continueEncoding()
AccessLogInstance log(), onDestroy() 自定义访问日志 日志打印阻塞主线程,影响性能

你不用死记硬背这些接口,重点是理解过滤器的“生命周期”。就像人从出生到死亡要经历不同阶段,过滤器从创建到销毁也有固定流程:当一个新请求进来时,Envoy会创建过滤器实例(相当于“出生”),然后依次调用decodeHeaders()、decodeData()等方法处理请求(相当于“工作”),请求处理完后调用onDestroy()释放资源(相当于“死亡”)。

我之前带团队开发自定义限流过滤器时,就踩过生命周期的坑。当时为了统计请求频率,在过滤器里用了一个全局计数器,结果发现计数器总是不准。后来排查才发现,每个请求都会创建一个新的过滤器实例,全局变量根本没法共享状态!正确的做法是用Envoy的SharedPtr或者通过过滤器管理器传递上下文,这在Envoy官方开发者指南里有详细说明(Envoy过滤器开发指南)。所以你开发时一定要记住:过滤器实例是请求隔离的,每个请求一个实例,别想用全局变量存状态。

手把手实战:开发HTTP请求头修改过滤器,从0到1跑通全流程

光说原理可能还是有点虚,接下来咱们拿“HTTP请求头修改过滤器”举例子,从头到尾走一遍开发流程。这个过滤器的功能很简单:当请求路径是/api/v1/user时,自动给请求加上X-User-Region: Beijing头,方便后端服务识别用户地区。你跟着做下来,基本就能掌握过滤器开发的核心套路了。

环境搭建:别让编译问题消耗你80%的精力

开发Envoy过滤器前,先得把环境搭起来。我见过很多人卡在第一步——源码编译。Envoy的依赖特别多,光Protobuf、BoringSSL这些库就能让你配环境配到怀疑人生。这里给你个懒人方案:用官方的Docker开发镜像,所有依赖都预装好了,直接就能用。

你先执行这行命令拉取镜像:docker pull envoyproxy/envoy-build-ubuntu:latest,然后启动容器并挂载本地目录:docker run -it -v $(pwd):/workspace envoyproxy/envoy-build-ubuntu:latest /bin/bash。进入容器后,再克隆Envoy源码:git clone https://github.com/envoyproxy/envoy.git,切换到最新稳定版(比如v1.29.0):cd envoy && git checkout v1.29.0

为啥要用稳定版?我之前图新鲜用master分支开发,结果今天编译过了,明天拉个最新代码就报错,原来是API又变了。所以除非你要开发针对特定版本的功能,否则优先选稳定版。

接下来编译Envoy,执行bazel build -c opt envoy。第一次编译会很慢,大概30分钟到1小时,取决于你电脑配置。如果编译报错,90%是内存不够,Envoy编译至少需要8GB内存, 你给Docker分配12GB以上。我之前在4GB内存的虚拟机上试,编译到一半就OOM killed了,折腾了一上午才发现是内存问题。

代码编写:300行代码实现核心功能

环境准备好后,就可以写代码了。Envoy过滤器开发主要涉及两个文件:过滤器实现(.cc)和配置 protobuf(.proto)。咱们先创建过滤器实现文件,在envoy/source/extensions/filters/http/modify_header/目录下(如果没有就新建),创建modify_header_filter.cc

先看过滤器类的定义,你需要继承StreamDecoderFilter接口,实现几个关键方法:

class ModifyHeaderFilter public Http::StreamDecoderFilter {

public:

// 构造函数,接收配置

explicit ModifyHeaderFilter(ModifyHeaderConfigSharedPtr config) config_(std::move(config)) {}

// 处理请求头

Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& headers, bool end_stream) override {

// 判断路径是否匹配

const auto path = headers.getPathValue();

if (path == "/api/v1/user") {

// 添加自定义请求头

headers.addCopy(Http::LowerCaseString("X-User-Region"), "Beijing");

ENVOY_LOG(info, "Added X-User-Region header for path: {}", path);

}

// 继续执行过滤器链

return Http::FilterHeadersStatus::Continue;

}

// 处理请求数据(这里不需要,直接继续)

Http::FilterDataStatus decodeData(Buffer::Instance& data, bool end_stream) override {

return Http::FilterDataStatus::Continue;

}

// 处理请求尾部(这里不需要,直接继续)

Http::FilterTrailersStatus decodeTrailers(Http::RequestTrailerMap& trailers) override {

return Http::FilterTrailersStatus::Continue;

}

// 释放资源(这里没分配资源,留空)

void onDestroy() override {}

private:

ModifyHeaderConfigSharedPtr config_; // 保存过滤器配置

};

代码很简单吧?核心就是在decodeHeaders方法里判断请求路径,符合条件就加请求头。这里有个细节:end_stream参数表示这是不是最后一个请求帧,如果是true,说明后面没有请求数据了。我之前没管这个参数,结果在处理带body的POST请求时,只处理了头,没处理数据,导致请求不完整。不过咱们这个案例只修改头,所以可以忽略。

接下来定义过滤器工厂,负责创建过滤器实例:

class ModifyHeaderFilterFactory public Server::Configuration::NamedHttpFilterConfigFactory {

public:

// 创建过滤器实例

Http::FilterFactoryCb createFilterFactoryFromProto(const Protobuf::Message& proto_config,

const std::string& stats_prefix,

Server::Configuration::FactoryContext& context) override {

auto config = std::make_shared(

dynamic_cast(proto_config));

return config {

callbacks.addStreamDecoderFilter(std::make_shared(config));

};

}

// 返回配置proto类型

ProtobufTypes::MessagePtr createEmptyConfigProto() override {

return std::make_unique();

}

// 过滤器名称,配置时要用

std::string name() const override { return "modify-header-filter"; }

};

// 注册过滤器工厂,让Envoy能找到它

REGISTER_FACTORY(ModifyHeaderFilterFactory, Server::Configuration::NamedHttpFilterConfigFactory);

然后创建配置proto文件modify_header.proto,定义过滤器的配置参数(咱们这个案例不需要配置,所以proto可以很简单):

syntax = "proto3";

package envoy.extensions.filters.http.modify_header.v3;

import "udpa/annotations/status.proto";

import "validate/validate.proto";

message ModifyHeader {

option (udpa.annotations.file_status).package_version_status = ACTIVE;

// 这里可以添加配置参数,比如要匹配的路径、要添加的头信息等

}

最后修改envoy/source/extensions/filters/http/BUILD文件,把咱们的过滤器加进去,否则编译时找不到。在envoy_http_extensions列表里添加:modify_header,然后添加目标定义:

envoy_cc_library(

name = "modify_header",

srcs = ["modify_header/modify_header_filter.cc"],

hdrs = ["modify_header/modify_header_filter.h"],

deps = [

"//envoy/extensions/filters/http/modify_header:modify_header_proto",

"//envoy/http",

"//envoy/server",

"//envoy/stream_info",

"//library/common/extensions/filters/http:filter_interface",

],

)

编译调试:3个技巧帮你快速定位问题

代码写完后,编译过滤器:bazel build -c opt envoy/extensions/filters/http/modify_header:modify_header。如果编译报错,先检查代码里的命名空间、头文件是否正确。我之前漏了REGISTER_FACTORY宏,编译时提示“未定义的引用”,找了半天才发现是没注册工厂。

编译通过后,需要写Envoy配置文件envoy.yaml,启用咱们的过滤器:

static_resources:

listeners:

  • name: listener_0
  • address:

    socket_address: { address: 0.0.0.0, port_value: 8080 }

    filter_chains:

  • filters:
  • name: envoy.http_connection_manager
  • typed_config:

    "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager

    stat_prefix: ingress_http

    route_config:

    name: local_route

    virtual_hosts:

  • name: local_service
  • domains: ["*"]

    routes:

  • match: { prefix: "/" }
  • route: { cluster: service_backend }

    http_filters:

  • name: modify-header-filter # 咱们的过滤器名称
  • typed_config:

    "@type": type.googleapis.com/envoy.extensions.filters.http.modify_header.v3.ModifyHeader

  • name: envoy.router
  • typed_config: { "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router }

    clusters:

  • name: service_backend
  • connect_timeout: 0.25s

    type: STATIC

    load_assignment:

    cluster_name: service_backend

    endpoints:

  • lb_endpoints:
  • endpoint:
  • address: { socket_address: { address: 127.0.0.1, port_value: 8081 } }

    启动Envoy前,先在本地启动一个测试后端服务,比如用Python起个简单的HTTP服务器:python -m http.server 8081。然后启动Envoy:bazel-bin/envoy -c envoy.yaml。用curl测试:curl -v http://localhost:8080/api/v1/user,如果看到响应头里有X-User-Region: Beijing,就说明过滤器生效了!

    如果没生效怎么办?教你3个调试技巧:第一,看Envoy日志,启动时加-l debug参数,日志里会显示过滤器是否加载成功;第二,用envoy admin接口,访问http://localhost:9901/config_dump,检查过滤器配置是否正确;第三,用GDB调试,编译时加-c dbg参数生成调试信息,然后gdb args bazel-bin/envoy -c envoy.yaml,在过滤器的decodeHeaders方法里打断点。我之前过滤器没生效,就是通过config_dump发现配置里过滤器名称写错了,写成了modify_header_filter而不是modify-header-filter,差一个横线就折腾了半小时。

    最后提醒一句,过滤器开发完后,最好写单元测试。在test/extensions/filters/http/modify_header/目录下创建测试文件,用GTest框架测试各种场景,比如路径匹配、不匹配、特殊字符路径等。Envoy的CI特别严格,如果你想贡献代码到官方,测试覆盖率必须达标,不过咱们自己用的话,至少把核心场景测一下,避免上线后踩坑。

    如果你按这个案例做下来,遇到编译报错或者运行没效果,欢迎在评论区告诉我具体问题,比如报错信息、配置内容,我尽量帮你看看。开发Envoy过滤器其实不难,关键是把原理搞透,再动手实践,遇到问题多查官方文档和源码,慢慢就熟练了。


    开发Envoy过滤器啊,首先得把底子打好,最核心的肯定是C++。你想啊,Envoy本身就是用C++写的,那些过滤器接口,像StreamDecoderFilter、StreamEncoderFilter这些,全都是C++的类和方法,你不熟悉C++,连接口都看不懂,更别说实现功能了。我之前带过一个实习生,Java转过来的,一开始对着C++的虚函数、模板一脸懵,后来花了两周补C++基础,才慢慢上手。所以你要是C++基础薄弱,得先啃啃C++11及以上的特性,比如智能指针(像std::shared_ptr)、lambda表达式,Envoy里到处都是,还有STL容器,vector、map这些,处理数据的时候少不了。

    除了C++,还有几个工具和协议知识也得会。先说Protobuf吧,你写过滤器肯定需要配置吧?比如你想让过滤器根据配置的路径来添加请求头,总不能把路径写死在代码里吧?这时候就得用Protobuf定义一个.proto文件,声明配置字段,比如message ModifyHeader { string target_path = 1; string header_key = 2; string header_value = 3; },这样Envoy启动的时候才能解析你的配置。然后是Bazel构建工具,Envoy的源码工程全靠Bazel管理,你写的过滤器要编译进Envoy,就得改源码里的BUILD文件,声明你的过滤器目标,把依赖的库(比如//envoy/http、//envoy/server这些)都写上,不然编译的时候肯定报“找不到某某头文件”,我之前就忘加依赖,结果编译器对着我的过滤器类一顿报错,查了半天才发现是BUILD文件没改对。

    HTTP协议基础也得吃透,不然你写过滤器都不知道从哪下手。比如你想修改请求头,得知道请求头存在RequestHeaderMap里,怎么用getByKey()拿字段,怎么用addCopy()加新头;想判断请求方法是GET还是POST,得认识HeaderStringValues::MethodValues里的GET、POST这些常量。要是你连HTTP请求的基本结构(请求行、请求头、请求体)都不清楚,写出来的过滤器要么功能不对,要么把请求搞坏了。对了,要是你的过滤器需要读复杂配置文件,比如从yaml里加载一堆路由规则,那JSON/YAML格式也得熟悉,知道怎么用Envoy的配置解析器把yaml转成Protobuf对象,不然配置写得再花里胡哨,过滤器读不出来也是白搭。

    最后给个小 基础薄弱的话别上来就硬啃Envoy源码,先把C++基础打牢,然后看看Envoy官方的过滤器开发指南(就是之前提到的那个链接),里面把核心接口、生命周期讲得很清楚,对着简单的例子(比如官方的示例过滤器)抄一遍,改改功能,慢慢就有感觉了。


    开发Envoy过滤器需要掌握哪些编程语言和技术基础?

    Envoy过滤器核心开发语言为C++,因为Envoy本身基于C++实现,过滤器需遵循其C++接口规范。 需了解Protobuf(用于定义过滤器配置)、Bazel构建工具(Envoy官方构建系统),以及HTTP协议基础(如请求/响应结构、头部字段)。若涉及复杂配置解析,还需熟悉JSON/YAML格式。基础薄弱者 先学习C++11及以上特性、STL容器使用,以及Envoy提供的基础API文档。

    过滤器链中多个过滤器的执行顺序是如何确定的?

    过滤器链的执行顺序由Envoy配置文件中http_filters列表的顺序决定。当请求从客户端流向服务端(解码阶段)时,过滤器按列表顺序依次执行(如列表中A、B、C,则A先执行,再B,再C);当响应从服务端返回客户端(编码阶段)时,过滤器按列表反向顺序执行(即C先执行,再B,再A)。文章中“会员请求优先处理”案例的A(判断会员)、B(标记优先级)、C(记录日志)即按配置顺序执行。

    开发Envoy过滤器时,如何判断功能应该放在解码阶段还是编码阶段?

    解码阶段(Decode)处理请求从客户端到服务端的流程,适合实现请求相关功能,如修改请求头(如文章案例中的X-User-Region头)、请求限流、路径匹配等;编码阶段(Encode)处理响应从服务端到客户端的流程,适合实现响应相关功能,如添加响应头、修改响应内容、响应日志等。 若需记录“服务端返回的状态码”,则需在编码阶段的encodeHeaders()方法中实现。

    为什么自定义过滤器编译时经常出现依赖错误?如何避免?

    Envoy依赖库众多(如Protobuf、BoringSSL、Abseil等),手动配置易遗漏依赖项,导致编译错误。避免方法:优先使用官方Docker开发镜像(envoyproxy/envoy-build-ubuntu),其预装所有依赖;编译前确保修改Envoy源码中的BUILD文件,在envoy_http_extensions列表添加自定义过滤器目标,并在目标定义中声明正确依赖(如//envoy/http、//envoy/server等核心模块);克隆源码时 切换到稳定版本(如v1.29.0),避免master分支API频繁变更导致的兼容性问题。

    过滤器开发完成后,如何验证其性能是否符合预期?

    可通过以下步骤验证性能:

  • 使用Envoy自带的性能测试工具(如envoy-perf)或第三方工具(如wrk、ab)模拟高并发场景, 测试1000-5000请求/秒的流量;
  • 监控关键指标:过滤器启用前后的CPU占用率、内存使用量、请求延迟(P95/P99分位数),确保无明显性能下降(通常 overhead 应控制在5%以内);3. 检查过滤器是否存在内存泄漏:通过Envoy Admin接口(/memory)观察长期运行后的内存增长趋势,或使用Valgrind等工具检测泄漏点。若性能不达标,可优化逻辑(如减少冗余计算、避免阻塞操作)或拆分复杂功能为多个轻量过滤器。
  • 0
    显示验证码
    没有账号?注册  忘记密码?