深入解析 Envoy 外部处理过滤器(ext_proc)

本文详细介绍了 Envoy 的 ext_proc 外部处理过滤器的功能、配置与性能优化策略。通过示例演示,展示了如何配置 Envoy 与实现外部处理服务器,帮助开发人员灵活处理请求与响应。

版权声明
本文为 Jimmy Song 原创。转载请注明来源: https://jimmysong.io/blog/envoy-ext-proc-guide/
查看本文大纲

在微服务架构中,API 网关通常需要对请求和响应进行高级别的处理,如身份验证、数据转换和安全检查。Envoy 提供的 ext_proc 外部处理过滤器,是一个强大的工具,通过与 gRPC 服务交互,实现灵活的请求与响应处理。本文将深入解析该过滤器的功能、配置与性能优化策略,帮助开发人员和 DevOps 工程师高效应用该特性。

ext_proc 与其他过滤器的关系

ext_proc 和 Envoy 中的其他 gRPC 接口过滤器(如 ext_authz)在功能上有相似之处,但 ext_proc 提供了更强大的功能,支持完整的请求和响应处理。这使其特别适用于需要深度内容检查和修改的应用场景。

你可以通过下面的 Envoy 外部处理过滤器思维导图快速了解 ext_proc

image
Envoy ext_proc 思维导图

这张思维导图展示了 Envoy ext_proc 外部处理过滤器的核心结构和功能模块。ext_proc 通过 gRPC 双向流协议与外部服务交互,可灵活处理 HTTP 请求和响应的各个阶段,并支持同步与异步处理。

ext_proc 工作原理与配置

定义与功能

ext_proc 是 Envoy 提供的 HTTP 过滤器,支持将请求和响应外包给 gRPC 服务进行处理,允许在外部服务中实现复杂的逻辑,灵活应对业务需求。例如,在安全场景中,ext_proc 可用于执行身份验证和授权检查;在数据转换场景中,可以实现数据格式转换与内容过滤。此外,还可用于记录审计日志、动态请求重写以及内容增强等功能,适用于各种企业应用环境中的深度流量管理。

工作原理

ext_proc 使用双向 gRPC 流与外部服务通信,实现请求和响应处理的实时交互。这使得 Envoy 可以将复杂任务(如身份验证、数据转换和自定义 Header 操作)卸载到外部服务,从而提高灵活性和可扩展性。

Envoy 发送 ProcessingRequest 消息,外部服务返回 ProcessingResponse 消息。需要注意的是,每个 HTTP 请求流都会创建一个独立的 gRPC 流,而不会在多个请求之间共享。每个由 Envoy 处理的 HTTP 请求都会创建其专属的 gRPC 流,从而确保请求与响应的隔离和精确管理。

这种设计允许外部服务在请求与响应生命周期的不同阶段进行干预,甚至能够生成全新的响应内容。``

下图概述 Envoy 外部处理过滤器的处理过程:

image
Envoy ext_proc 流程

关键功能

  • 请求和响应处理:读取和修改 HTTP 请求和响应的头部、主体和尾部。
  • 灵活性:根据业务需求定义自定义逻辑,弥补内置功能的不足。
  • 异步处理:支持异步处理模式,防止请求阻塞。

Envoy 配置示例

以下是一个基本的 Envoy 配置示例:

static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address:
        protocol: TCP
        address: 0.0.0.0
        port_value: 8080
    filter_chains:
    - 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: ingress_http
          access_log:
          - name: envoy.access_loggers.stdout
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
              log_format:
                text_format: "[%START_TIME%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %RESPONSE_FLAGS% \"%RESP(X-EXTPROC-HELLO)%\" \"%RESP(CONTENT-TYPE)%\" \"%RESP(CONTENT-LENGTH)%\" %DURATION% ms\n"
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: [ "*" ]
              routes:
              - match:
                  prefix: "/"
                route:
                  host_rewrite_literal: www.envoyproxy.io
                  cluster: service_envoyproxy_io
          http_filters:
          - name: envoy.filters.http.ext_proc
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor
              grpc_service:
                envoy_grpc:
                  cluster_name: ext_proc_cluster
              failure_mode_allow: true
              processing_mode:
                request_header_mode: SKIP
                response_header_mode: SEND
          - name: envoy.filters.http.router
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
  clusters:
  - name: ext_proc_cluster
    connect_timeout: 0.25s
    type: LOGICAL_DNS
    lb_policy: ROUND_ROBIN
    http2_protocol_options: {}
    load_assignment:
      cluster_name: ext_proc_cluster
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 9000
  - name: service_envoyproxy_io
    type: LOGICAL_DNS
    dns_lookup_family: V4_ONLY
    load_assignment:
      cluster_name: service_envoyproxy_io
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: www.envoyproxy.io
                port_value: 443
    transport_socket:
      name: envoy.transport_sockets.tls
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
        sni: www.envoyproxy.io

为了理解 Envoy 配置与 gRPC 服务之间的关联,我们需要了解以下配置项如何影响流量处理:

  • grpc_service: 定义与 gRPC 服务通信的目标地址和集群名,对应 Envoy 配置中的 ext_proc_cluster
  • processing_mode: 控制请求头、请求体和响应头等处理阶段的触发行为,决定了何时调用 gRPC 服务。
  • failure_mode_allow: 指定当 gRPC 服务失败时是否继续请求处理,确保服务在部分失败场景下的高可用性。
  • listeners: 定义了 Envoy 接收请求的网络地址和端口。
  • filter_chains: 配置请求的处理链,包括 HTTP 连接管理器和外部处理过滤器。
  • http_filters: 列出启用的过滤器,如 ext_procrouter
  • clusters: 定义上游服务和外部处理 gRPC 服务的位置。

详细的配置说明请参考 Envoy 文档

Envoy 使用这些配置选项将请求和响应外包给 gRPC 服务,处理结果通过双向流协议返回,影响请求的转发行为。

gRPC 服务示例

以下是一个简单的 gRPC 外部处理服务器实现,演示如何通过 ext_proc 添加自定义响应头。该实现展示了核心方法的选择和设计决策,例如使用 Process 方法持续接收请求和发送响应,确保处理过程的连续性。此外,采用 HeaderMutation 配置修改 HTTP 响应头,展现了 gRPC 消息结构与 Envoy 配置的紧密集成,便于动态扩展和灵活管理。

package main

import (
	"log"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	configPb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
	extProcPb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
)

type extProcServer struct {
	extProcPb.UnimplementedExternalProcessorServer
}

// Process handles external processing requests from Envoy.
// It listens for incoming requests, modifies response headers,
// and sends the updated response back to Envoy.
//
// When a request with response headers is received, it adds a custom header
// "x-extproc-hello" with the value "Hello from ext_proc" and returns the modified headers.
//
// Note: The `RawValue` field is used instead of `Value` because it supports
// setting the header value as a byte slice, allowing precise handling of binary data.
//
// This function is called once per HTTP request to process gRPC messages from Envoy.
// It exits when an error occurs while receiving or sending messages.

func (s *extProcServer) Process(
	srv extProcPb.ExternalProcessor_ProcessServer,
) error {
	for {
		req, err := srv.Recv()
		if err != nil {
			return status.Errorf(codes.Unknown, "error receiving request: %v", err)
		}

		log.Printf("Received request: %+v\n", req)

		// Prepare the response to be returned to Envoy.
		resp := &extProcPb.ProcessingResponse{}

		// Only process response headers, not request headers.
		if respHeaders := req.GetResponseHeaders(); respHeaders != nil {
			log.Println("Processing Response Headers...")

			resp = &extProcPb.ProcessingResponse{
				Response: &extProcPb.ProcessingResponse_ResponseHeaders{
					ResponseHeaders: &extProcPb.HeadersResponse{
						Response: &extProcPb.CommonResponse{
							HeaderMutation: &extProcPb.HeaderMutation{
								SetHeaders: []*configPb.HeaderValueOption{
									{
										Header: &configPb.HeaderValue{
											Key:      "x-extproc-hello",
											RawValue: []byte("Hello from ext_proc"),
										},
									},
								},
							},
						},
					},
				},
			}
			log.Printf("Sending response: %+v\n", resp)
			// Send the response back to Envoy.
			if err := srv.Send(resp); err != nil {
				return status.Errorf(codes.Unknown, "error sending response: %v", err)
			}
		} else {
			// If it is not a callback in the response header stage, do not make any modifications and continue processing the next event.
			// For request_headers or other events, do not modify & ensure that Envoy will not be stuck.
			// An empty processing can be returned for request_headers, or it can be skipped in envoy.yaml.
			// Here, simply continue to wait for the next event.
			continue
		}
	}
}

func main() {
	lis, err := net.Listen("tcp", ":9000")
	if err != nil {
		log.Fatalf("Failed to listen: %v", err)
	}

	grpcServer := grpc.NewServer()
	// Register the ExternalProcessorServer with the gRPC server.
	extProcPb.RegisterExternalProcessorServer(grpcServer, &extProcServer{})

	log.Println("Starting gRPC server on :9000...")
	if err := grpcServer.Serve(lis); err != nil {
		log.Fatalf("Failed to serve: %v", err)
	}
}

预期结果:请求返回状态码 200,响应头中包含自定义头 x-extproc-hello: Hello from ext_proc。如果缺少该头,检查以下内容:

  • gRPC 服务是否正常运行:确认 gRPC 服务器是否已启动并监听端口 :9000
  • Envoy 配置是否正确:检查 Envoy 配置文件,确保 ext_proc 过滤器已启用,并且 ext_proc_cluster 配置无误。
  • 日志和错误排查:查看 Envoy 和 gRPC 服务器的日志,排查潜在错误。

在本地运行 Envoy 和 gRPC 服务后,使用 curl 进行测试:

envoy -c envoy.yaml
go run main.go
curl -v http://localhost:8080

你将看到包含自定义头 x-extproc-hello: Hello from ext_proc 的响应。

你将看到如下图所示的结果。

image
示例结果

在 curl 请求的响应中包含了我们自定义的 header x-extproc-hello: Hello from ext_proc

统计与监控

ext_proc 输出的统计信息位于 http.<stat_prefix>.ext_proc. 命名空间,其中 stat_prefix 是 HTTP 连接管理器的前缀。常用统计信息包括:

指标名称 类型 描述
streams_started Counter 启动的 gRPC 流数量
streams_msgs_sent Counter 发送的消息数量
streams_msgs_received Counter 接收的消息数量
spurious_msgs_received Counter 接收的违反协议的意外消息数量
streams_closed Counter 成功关闭的流数量
streams_failed Counter 产生 gRPC 错误的流数量
failure_mode_allowed Counter 错误被忽略的次数(根据配置)
message_timeouts Counter 配置超时内未收到响应的消息数量
rejected_header_mutations Counter 被拒绝的头部更改数量
clear_route_cache_ignored Counter 忽略的清理路由缓存请求数量
clear_route_cache_disabled Counter 被禁用的清理路由缓存请求数量

详见 Envoy 文档

应用场景与优化策略

常见应用场景

  • 身份验证:外部身份验证服务检查用户凭证。
  • 数据审计:记录请求和响应的数据以满足合规要求。
  • 流量管理:根据动态分析调整流量路由策略。

配置说明与优化策略

  • 故障恢复与负载均衡:部署多实例 gRPC 服务,使用负载均衡自动转移请求。

  • 消息超时与重试配置:

    http_filters:
      - name: envoy.filters.http.ext_proc
        config:
          grpc_service:
            envoy_grpc:
              cluster_name: ext_proc_server
          processing_mode:
            request_header_mode: SEND
            response_header_mode: SEND
          message_timeout: 500ms
          max_message_timeout: 1000ms
          failure_mode_allow: false
    
  • 元数据选项与安全策略:

    metadata_options:
      forwarding_namespaces:
        untyped: ["custom_namespace"]
    

总结

Envoy 的 ext_proc 过滤器通过灵活的请求和响应处理能力,为微服务架构中的服务治理、数据转换和请求检查提供了强大的支持。正确配置和优化 ext_proc 可以显著提高系统的灵活性和可扩展性,满足多样化的业务需求。

参考

最后更新于 2024/12/20