Envoy 本地代理、过滤链与路由工具手册
从一次 /api/orders/42 请求开始
本地前端向 http://127.0.0.1:10000/api/orders/42 发出请求时,Envoy 不是简单地把端口 10000 转到 8080。连接先进入 Listener,经过 FilterChain 选择和 HTTP Connection Manager,HTTP 请求再经过过滤器、VirtualHost 与 Route 匹配,最后由 Router 把它交给 Cluster 中选出的 Endpoint。只要其中一个对象没有命中,请求就会在不同位置结束,并留下不同证据。
这正是 Envoy 比轻量端口转发器复杂、也更有价值的地方:路由、TLS、重试、健康状态和动态配置都能被解释。但如果团队没有人维护配置发布、指标和故障证据,这套复杂度也会反过来拖慢开发。
实验需要一个能直接访问的后端。它至少应提供:
GET /health 返回 200;GET /v1/orders/42 回显请求路径、Host、X-Forwarded-For 和 X-Request-Id;可以通过停止 app 容器制造明确的连接失败。
下面的 Compose 会创建该回显后端,并把它单独发布到宿主机 18080。先验证直连,再请求 Envoy;如果直连已经超时或拒绝连接,不要在路由中继续猜。
锁定镜像后启动本地数据面
Envoy 的配置 API 会随版本演进,当前项目基线锁定为 v1.38.0。完整镜像标签写入项目 .env,不使用 latest;下面的 Compose 会在变量缺失时直接失败,避免悄悄漂移版本。升级前重新检查 Envoy Releases 与对应版本文档,确认弃用字段、扩展类型和 bootstrap/xDS API 兼容性,再完成配置验证、故障回归和镜像摘要锁定。
ENVOY_IMAGE=envoyproxy/envoy:v1.38.0目录保持简单,证书和运行配置各有明确位置:
gateway/envoy/
.env
compose.yaml
envoy.yaml
backend.conf
certs/
README.md
evidence/
rollback.mdbackend.conf 提供健康检查和请求回显:
server {
listen 80;
server_name _;
location = /health {
default_type application/json;
return 200 '{"status":"ok"}';
}
location / {
default_type application/json;
return 200 '{"path":"$request_uri","host":"$host","xff":"$http_x_forwarded_for","requestId":"$http_x_request_id"}';
}
}compose.yaml 只把回显后端、业务入口和 Admin 发布到宿主机 loopback。Envoy 与 app 通过独立 Compose 网络通信:
services:
app:
image: nginx:1.30.3-alpine
ports:
- "127.0.0.1:18080:80"
volumes:
- ./backend.conf:/etc/nginx/conf.d/default.conf:ro
networks: [gateway]
envoy:
image: ${ENVOY_IMAGE:?set ENVOY_IMAGE to an approved immutable tag}
command:
- -c
- /etc/envoy/envoy.yaml
- --service-cluster
- local-gateway
- --log-level
- info
ports:
- "127.0.0.1:10000:10000"
- "127.0.0.1:9901:9901"
volumes:
- ./envoy.yaml:/etc/envoy/envoy.yaml:ro
- ./certs:/etc/envoy/certs:ro
restart: "no"
networks: [gateway]
networks:
gateway: {}镜像标签还可以进一步锁到 digest。执行前先确认镜像存在并记录摘要:
docker compose config --images
docker manifest inspect envoyproxy/envoy:v1.38.0
docker compose pull
docker compose images预期看到唯一 Envoy 镜像及明确 tag/digest。共享环境应把拉取结果中的 RepoDigest 写回部署变量,以 envoyproxy/envoy@sha256:... 形式固定实际镜像内容,并把 tag 到 digest 的变更纳入评审。manifest unknown 表示标签写错或镜像尚未同步;代理环境下的超时则应先检查 Docker daemon 的代理和镜像仓库访问,而不是更换成浮动标签。
一份能留下证据的静态配置
下面的 envoy.yaml 同时建立业务 Listener、受控 Admin、路径改写、访问日志和一个静态 Cluster。Admin 在容器内监听 0.0.0.0,但 Compose 只把它映射到宿主机 127.0.0.1;若容器加入共享网络,还要用网络策略限制其他容器访问 9901。
admin:
address:
socket_address:
address: 0.0.0.0
port_value: 9901
allow_paths:
- exact: /ready
- exact: /server_info
- exact: /clusters
- exact: /certs
- prefix: /stats
- prefix: /config_dump
static_resources:
listeners:
- name: local_http
address:
socket_address:
address: 0.0.0.0
port_value: 10000
filter_chains:
- name: plaintext_http
filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: local_http
generate_request_id: true
use_remote_address: true
route_config:
name: local_routes
virtual_hosts:
- name: local_backend
domains: ["localhost", "127.0.0.1", "127.0.0.1:10000", "localhost:10000"]
routes:
- name: api_get
match:
prefix: "/api/"
headers:
- name: ":method"
string_match: { exact: "GET" }
route:
cluster: backend
prefix_rewrite: "/v1/"
timeout: 2s
retry_policy:
retry_on: "connect-failure,reset,refused-stream,5xx"
num_retries: 2
per_try_timeout: 500ms
- name: api_write
match: { prefix: "/api/" }
route:
cluster: backend
prefix_rewrite: "/v1/"
timeout: 5s
- name: health
match: { path: "/health" }
route: { cluster: backend, timeout: 1s }
access_log:
- name: envoy.access_loggers.file
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
path: /dev/stdout
log_format:
typed_json_format:
started: "%START_TIME%"
request_id: "%REQ(X-REQUEST-ID)%"
method: "%REQ(:METHOD)%"
path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
route: "%ROUTE_NAME%"
cluster: "%UPSTREAM_CLUSTER%"
upstream_host: "%UPSTREAM_HOST%"
response_code: "%RESPONSE_CODE%"
response_flags: "%RESPONSE_FLAGS%"
response_details: "%RESPONSE_CODE_DETAILS%"
duration_ms: "%DURATION%"
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: backend
type: STATIC
connect_timeout: 500ms
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: backend
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: app
port_value: 80路径改写不是装饰。/api/orders/42 命中 api_get 后被改成 /v1/orders/42,Router 会向上游写入 x-envoy-original-path。该行为及相关请求头可在 Router filter 文档 中核对。
先让同版本 Envoy 解析配置,再启动:
docker compose run --rm envoy --mode validate -c /etc/envoy/envoy.yaml
docker compose up -d
docker compose ps
docker compose logs --tail 50 envoy
curl -i http://127.0.0.1:18080/health
curl -i http://127.0.0.1:18080/v1/orders/42validate 成功应以退出码 0 结束;启动后 docker compose ps 应显示 app 与 Envoy 运行。两个直连请求都应返回 200,第二个响应应包含 /v1/orders/42。YAML 缩进、未知字段或类型 URL 错误会在 validate 阶段直接报出字段路径。进程运行只证明 bootstrap 已加载,尚不能证明路由和 TLS 正常。
两条对象链不能混成一条箭头
TCP 连接到达 10000 后,先走下游连接链。Listener 接收 socket,listener filters 可以提取原始目的地址或 TLS ClientHello 信息,FilterChain 再依据 destination port、SNI、transport protocol、ALPN 等条件选择处理链。若 FilterChain 配有 DownstreamTlsContext,TLS 握手在进入 HCM 前完成。
HTTP 解码完成后,请求进入 HTTP filter chain。过滤器可以读取命中的路由元数据,但真正建立上游连接的是链尾 Router。Router 根据 Route 选择 Cluster,Cluster Manager 再按负载均衡策略从健康 Host/Endpoint 中选择目标,并通过连接池发出请求。
这一拆分能解释两个常见误判:TLS 失败时请求尚未进入 Route;no healthy upstream 则说明 Route 已经选出 Cluster,但负载均衡集合里没有可用 Host。
发出真实请求并同时观察业务响应、结构化日志和 Admin:
curl -i http://127.0.0.1:10000/api/orders/42
curl -s http://127.0.0.1:9901/ready
curl -s "http://127.0.0.1:9901/clusters?format=json"
docker compose logs --tail 20 envoy请求应返回后端响应;后端看到 /v1/orders/42,Envoy 日志中的 route 为 api_get、cluster 为 backend、response_code 为 200。/ready 返回 LIVE 只表示 Envoy 可以接流量,不等价于每个上游健康。若返回 404,先看 route 是否为 - 和 response_details;若返回 503,再看 response_flags 与 /clusters。
Admin 没有内建认证,而且包含关停、日志级别修改等写操作,也会暴露 cluster、证书和配置。官方 Administration interface 要求把它放在安全网络中,并特别提醒浏览器访问带来的 CSRF 风险。loopback 映射、allow_paths 和主机防火墙缺一不可;不要通过普通反向代理把 Admin 变成团队控制台。
TLS 终止、上游 TLS 与 mTLS 是三次不同决策
浏览器到 Envoy 需要 HTTPS 时,用独立开发 CA 为实际访问名签发证书。以下命令使用 mkcert 创建本机开发证书,私钥不能进入仓库:
mkcert -install
mkcert -cert-file certs/gateway.pem -key-file certs/gateway-key.pem localhost 127.0.0.1 ::1把 Listener 端口改为 10443,并在 FilterChain 中加入下游 TLS:
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
common_tls_context:
tls_certificates:
- certificate_chain: { filename: /etc/envoy/certs/gateway.pem }
private_key: { filename: /etc/envoy/certs/gateway-key.pem }Compose 还要增加 127.0.0.1:10443:10443 映射。重启后验证证书链、SAN 和真实请求:
openssl s_client -connect 127.0.0.1:10443 -servername localhost \
-CAfile "$(mkcert -CAROOT)/rootCA.pem" -verify_return_error </dev/null
curl -i https://localhost:10443/api/orders/42
curl -s http://127.0.0.1:9901/certs握手输出应包含成功的验证结果,curl 不需要 -k。SAN 不匹配会在客户端报主机名错误;证书文件不可读会让 Listener 无法启动。/certs 只能证明 Envoy 当前加载了什么,客户端无错误完成握手才证明信任链可用。
如果客户端也必须出示身份,先签发仅用于实验的客户端证书,并把公开的根证书复制到 Envoy 可读目录。根证书可以分发,mkcert 根私钥不能离开其受控存储:
mkcert -client -cert-file certs/client.pem \
-key-file certs/client-key.pem envoy-lab-client
cp "$(mkcert -CAROOT)/rootCA.pem" certs/client-ca.pem下游 TLS 配置需要同时包含服务端身份、客户端 CA 与强制客户端证书开关,层级如下:
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
require_client_certificate: true
common_tls_context:
tls_certificates:
- certificate_chain:
filename: /etc/envoy/certs/gateway.pem
private_key:
filename: /etc/envoy/certs/gateway-key.pem
validation_context:
trusted_ca:
filename: /etc/envoy/certs/client-ca.pem无客户端证书的握手必须失败,带批准客户端证书的握手才成功:
curl --cert certs/client.pem --key certs/client-key.pem \
--cacert "$(mkcert -CAROOT)/rootCA.pem" \
https://localhost:10443/api/orders/42Envoy 访问 HTTPS 上游是另一段链路,需要在 Cluster 配 UpstreamTlsContext。只打开 transport socket 并不会自动校验身份;官方 TLS 指南 明确要求通过 trusted CA 与 SAN/SNI 完成验证。
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
sni: backend.internal.example
common_tls_context:
validation_context:
trusted_ca: { filename: /etc/envoy/certs/backend-ca.pem }
match_typed_subject_alt_names:
- san_type: DNS
matcher: { exact: backend.internal.example }把 CA 文件换错、把 SNI 改成证书未覆盖的名称,再请求一次。预期请求失败,访问日志出现上游连接失败标记,Envoy 日志给出 TLS 校验原因。恢复正确 CA/SNI 后复验 200。生产证书轮换更适合通过 SDS 受控下发,但 SDS 的可用性、密钥读取权限和旧 secret 回退必须有独立监控。
健康、驱逐和熔断解决三类故障
主动健康检查定时探测确定入口,outlier detection 根据真实请求被动驱逐异常 Host,circuit breaker 则限制连接、排队请求和重试并发。它们不能互相替代。把下面片段加入 Cluster:
health_checks:
- timeout: 500ms
interval: 2s
unhealthy_threshold: 2
healthy_threshold: 2
http_health_check:
path: /health
outlier_detection:
consecutive_5xx: 5
interval: 5s
base_ejection_time: 30s
max_ejection_percent: 50
circuit_breakers:
thresholds:
- priority: DEFAULT
max_connections: 100
max_pending_requests: 50
max_requests: 200
retry_budget:
budget_percent: { value: 20.0 }
min_retry_concurrency: 3这些数字是实验起点,不是生产模板。容量值应从连接时长、并发、上游线程池和错误预算推导。主动与被动检查的区别可对照 Health checking、Outlier detection 和 Circuit breaking。
停止后端并持续观察:
curl -i http://127.0.0.1:10000/health
curl -s "http://127.0.0.1:9901/clusters?format=json"
curl -s "http://127.0.0.1:9901/stats?filter=health_check|outlier|circuit_breakers"连续失败达到阈值后,Host 应离开健康负载均衡集合,请求出现 503;日志常见 UH 或连接类 response flag。恢复后端并等待两次成功探测,Host 才应重新健康。若探针始终 200、业务接口持续 500,主动检查可能看不出问题,此时 outlier 统计和业务指标才有判断价值。
重试必须受方法、时间和预算约束
前面的配置只给 GET 路由重试,写请求进入 api_write,没有 retry policy。这比在一个宽泛 Route 上重试所有方法安全。num_retries: 2 表示一次原始请求最多再尝试两次;per_try_timeout 必须小于总 timeout,retry budget 则防止故障时重试并发无限放大。
让后端第一次返回 503、第二次返回 200,然后检查:
curl -i http://127.0.0.1:10000/api/orders/42
curl -s "http://127.0.0.1:9901/stats?filter=upstream_rq_retry"
docker compose logs --tail 20 envoy响应最终应为 200,重试计数增加。对 POST 执行相同故障注入时只应到达上游一次。支付、消息投递等操作即使使用 POST 也可能由调用方携带幂等键;是否允许重试必须由业务协议决定,不能仅按状态码决定。WebSocket、SSE 和 gRPC streaming 还要分别设计 idle timeout、max stream duration、心跳和断线恢复,不能照搬普通短请求重试。
用两个 Cluster 做可回滚灰度
稳定版与候选版必须是两个可独立观察的 Cluster。将 GET 路由的单一 cluster 替换为 weighted clusters:
route:
weighted_clusters:
clusters:
- name: backend_stable
weight: 90
- name: backend_candidate
weight: 10
total_weight: 100
timeout: 2s两个 Cluster 都要有自己的 endpoint、健康检查和容量限制。访问日志已经记录最终 cluster,连续请求后可以按 cluster 聚合成功率和延迟:
for i in $(seq 1 100); do
curl -fsS http://127.0.0.1:10000/api/orders/42 >/dev/null || true
done
docker compose logs envoy | grep 'backend_candidate'大样本下候选流量应接近配置权重,但短样本不会严格等于 10%。长连接、会话粘性、请求价值差异和客户端重试也会让“请求占比”不等于“业务风险占比”。回滚时先把候选权重归零,再确认新请求不再进入候选 Cluster;已有长连接是否需要排空要按协议另行处理。
xDS 把静态对象变成有依赖的资源图
静态 YAML 适合一个开发入口。大量 Envoy、动态 endpoint 或频繁灰度需要 xDS,但数据面仍从一个静态 bootstrap 开始:它必须知道自己的 node 身份以及如何连接管理服务器。
node:
id: local-gateway-01
cluster: local-dev
dynamic_resources:
ads_config:
api_type: GRPC
transport_api_version: V3
grpc_services:
- envoy_grpc:
cluster_name: xds_control_plane
lds_config: { ads: {} }
cds_config: { ads: {} }
static_resources:
clusters:
- name: xds_control_plane
type: STRICT_DNS
connect_timeout: 1s
typed_extension_protocol_options:
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
explicit_http_config:
http2_protocol_options: {}
load_assignment:
cluster_name: xds_control_plane
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: xds-control-plane
port_value: 18000典型依赖是 Listener → RouteConfiguration → Cluster → ClusterLoadAssignment。LDS 资源可以引用 RDS,CDS 资源可以引用 EDS;SDS 提供证书和验证上下文。ADS 让不同资源类型共享一条 gRPC 连接并由控制面安排发布顺序,独立 xDS 流则更容易出现最终一致性窗口。SotW 每次发送某类资源的期望全量状态,Delta xDS 只发送增加、修改和删除的资源。
发布新路由前,先让目标 Cluster 与 Endpoint 进入 active,再让 RDS 指向它,最后删除旧 Cluster。反向顺序会短暂产生 cluster_not_found 或无健康上游。Envoy 官方 xDS protocol 还指出:
ACK 表示资源经独立校验后有采用意图,不保证它已经成功进入最终运行状态;NACK 通过 error_detail 表示至少一个资源无效,并携带数据面仍在使用的版本;Listener 和 Cluster 更新可能进入 warming,等待所依赖的 RDS 或 EDS;
管理服务器失联时,最后已知配置通常继续工作,除非资源配置了 TTL。
控制面“发送成功”不能作为发布完成。连接真实 xDS 控制面后,数据面至少要同时留下这些证据:
curl -s http://127.0.0.1:9901/server_info
curl -s "http://127.0.0.1:9901/config_dump?resource=dynamic_active_listeners"
curl -s "http://127.0.0.1:9901/config_dump?resource=dynamic_active_clusters"
curl -s "http://127.0.0.1:9901/stats?filter=config_reload|update_success|update_rejected|warming"
docker compose logs envoy | grep -E 'gRPC config|NACK|warming|rejected'缺失 Cluster 是否触发 NACK 取决于 RouteConfiguration 的校验策略,不能用一个结论覆盖两种行为。默认未启用 cluster 校验时,RDS 可能 ACK 新路由,真实请求随后以 cluster_not_found 失败;这正是控制面必须按 CDS/EDS → RDS 顺序执行 make-before-break 的原因。
需要把错误引用变成发布期 NACK 时,在动态 RouteConfiguration 中显式启用:
name: local_routes
validate_clusters: true
virtual_hosts:
# 其余路由资源此时再把目标 Cluster 改成不存在的名称下发,预期是 rejected 计数增长、error_detail 记录缺失依赖、旧 active RouteConfiguration 继续服务。关闭该字段重复实验,则应记录“RDS 可能 ACK,但真实请求本地失败”的不同证据。修复顺序始终是先恢复 Cluster 与 Endpoint,再发布引用它们的 Route,最后确认 active version 和真实请求同时更新;仅看到控制面发送成功或 Envoy /ready 为 LIVE 都不够。
故障注入要回答“失败停在哪一层”
按照对象链逐层制造一次可恢复故障,比积累一张错误码表更有效:
把 YAML 字段写错,validate 应失败,进程不应带错配置启动。把 VirtualHost domain 改成不匹配值,请求应 404,日志 route 为 -。把 endpoint 端口改错,请求应 503,日志和 /clusters 指向上游连接失败。
停止后端,主动健康检查应把 Host 标记为 unhealthy;恢复后达到成功阈值再入池。换错上游 CA 或 SNI,TLS 握手应失败,恢复证书配置后请求重新成功。分两次下发引用缺失 Cluster 的 RDS:validate_clusters: true 时观察 NACK 与旧 route 继续服务;未启用时观察 ACK 后真实请求出现 cluster_not_found。RouteConfiguration 不进入 warming。
给候选 Cluster 注入 503,确认灰度指标告警后把权重归零。
每次实验都保存请求、访问日志、相关 Admin JSON、配置版本和恢复后的复验结果。状态码只说明表象;ROUTE_NAME、UPSTREAM_CLUSTER、RESPONSE_FLAGS 与 RESPONSE_CODE_DETAILS 才能把失败定位到路由、上游、超时或本地回复。
回滚不是“把文件改回去”
静态配置回滚先恢复上一份已知可用配置,在同版本镜像中 validate,再重建数据面并跑真实请求:
docker compose run --rm envoy --mode validate -c /etc/envoy/envoy.yaml
docker compose up -d --force-recreate envoy
curl -i http://127.0.0.1:10000/api/orders/42
curl -s http://127.0.0.1:9901/readyxDS 回滚是发布一个旧策略的“新资源版本”,不能假设版本号倒退。仍按依赖顺序先恢复 Cluster/Endpoint,再恢复 Route/Listener,并逐个数据面确认 active config。候选权重归零后还要观察错误率、延迟、连接排空和重试量,直到业务指标回到基线。
团队长期维护的判断线
单个 Envoy 加静态配置适合本地协议复现、复杂路由实验和应用前置代理。多个数据面、动态服务发现、统一灰度和证书分发会把问题提升为控制面产品:需要资源模型、发布顺序、版本兼容、ACK/NACK 聚合、回滚、审计与控制面可用性 owner。Envoy Gateway、服务网格和直接运行 Envoy 处在不同产品层,不能因为底层数据面相同就共享同一套操作手册。
以下条件同时满足,Envoy 才真正进入可维护状态:
镜像 tag/digest、bootstrap 与配置 API 版本被锁定,升级经过弃用字段扫描和故障回归;Listener、FilterChain、HCM、Route、HTTP filters、Cluster、LB 与 Endpoint 的两条对象链有图、有日志字段、有负责人;Admin 只在安全网络可达,允许路径最小化,不被浏览器或普通入口转发;
downstream TLS、upstream TLS、mTLS 与 SDS 各自有信任根、轮换、吊销和失败验证;health check、outlier detection、circuit breaker、timeout 和 retry 都有容量依据,非幂等请求不会被机械重放;灰度以业务指标和最终 Cluster 为证据,回滚覆盖新请求、旧连接和重试放大;
xDS 发布同时核对 ACK/NACK、warming、active config 与真实请求,控制面失联经过演练。
当入口长期只有少量稳定路由、团队没有动态控制面需求,也没有人承担 Envoy API 升级和排障责任时,Nginx 或 Caddy 往往更经济。工具能力越丰富,越需要用明确责任、可观察证据和可演练回滚抵消复杂度。
