1. 功能概述
Envoy 的 Lua 过滤器是一个非常强大的功能,它允许开发人员使用 Lua 脚本编写高度定制化的 HTTP 请求和响应处理逻辑。它提供了与 Envoy 核心功能的深度集成,包括请求/响应修改、路由决策、异步请求处理、加密操作等。
该功能的主要特点包括:
-
高灵活性:允许使用 Lua 脚本编写自定义逻辑,满足复杂的业务需求
-
快速开发:使用熟悉的 Lua 语言快速编写和部署自定义逻辑
-
性能优化:与 Envoy 架构深度集成,提供高效的执行环境
-
简化架构:减少了需要编写和维护的自定义代码
-
与其他 Envoy 功能的深度集成:包括 JWT 认证、速率限制、路由决策等
通过合理配置和使用 Lua 过滤器,开发人员可以在不修改 Envoy 核心代码的情况下,快速实现复杂的业务需求,提高系统的可维护性和可扩展性。
2. 解决的问题
2.1 高灵活性
-
允许开发人员使用 Lua 脚本编写自定义逻辑,满足复杂的业务需求
-
提供了与 Envoy 内部系统的深度集成,包括请求/响应修改、路由决策、异步请求处理等
-
支持在请求和响应阶段都执行自定义逻辑
2.2 快速开发
-
允许使用熟悉的 Lua 语言快速编写和部署自定义逻辑
-
提供了强大的调试和日志记录功能,帮助开发人员快速定位和修复问题
-
支持热更新功能,可以在不重启服务的情况下修改和部署新的 Lua 脚本
2.3 性能优化
-
与 Envoy 架构深度集成,确保高效的执行
-
支持异步操作,避免阻塞主事件循环
-
提供了轻量级的执行环境,适合高并发场景
2.4 简化架构
-
减少了需要编写和维护的自定义代码
-
直接在 Envoy 层面处理复杂的业务逻辑
-
提供了与其他 Envoy 功能的集成支持
3. 架构设计
3.1 核心组件架构

3.2 类图

3.3 基础配置
css
http_filters:- name: envoy.filters.http.lua typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua inline_code: | function envoy_on_request(request_handle) request_handle:logInfo("Received request from " .. request_handle:headers():get(":authority")) end
function envoy_on_response(response_handle) response_handle:logInfo("Received response with status " .. response_handle:headers():get(":status")) end
route_config: name: local_route virtual_hosts: - name: backend domains: ["*"] routes: - match: { prefix: "/" } route: { cluster: backend_cluster }
static_resources: clusters: - name: backend_cluster connect_timeout: 0.25s type: STRICT_DNS lb_policy: ROUND_ROBIN load_assignment: cluster_name: backend_cluster endpoints: - lb_endpoints: - endpoint: address: socket_address: address: backend.example.com port_value: 80
3.4 高级配置
swift
http_filters:- name: envoy.filters.http.lua typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua default_source_code: filename: /etc/envoy/lua_scripts/default.lua source_codes: api_scripts: filename: /etc/envoy/lua_scripts/api.lua static_scripts: filename: /etc/envoy/lua_scripts/static.lua
route_config: name: local_route virtual_hosts: - name: backend domains: ["*"] routes: - match: prefix: "/api" route: { cluster: api_cluster } per_filter_config: envoy.filters.http.lua: "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute name: "api_scripts" - match: prefix: "/static" route: { cluster: static_cluster } per_filter_config: envoy.filters.http.lua: "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute name: "static_scripts" - match: prefix: "/" route: { cluster: default_cluster }
static_resources: clusters: - name: api_cluster connect_timeout: 0.25s type: STRICT_DNS lb_policy: ROUND_ROBIN load_assignment: cluster_name: api_cluster endpoints: - lb_endpoints: - endpoint: address: socket_address: address: api.example.com port_value: 80 - name: static_cluster connect_timeout: 0.25s type: STRICT_DNS lb_policy: ROUND_ROBIN load_assignment: cluster_name: static_cluster endpoints: - lb_endpoints: - endpoint: address: socket_address: address: static.example.com port_value: 80 - name: default_cluster connect_timeout: 0.25s type: STRICT_DNS lb_policy: ROUND_ROBIN load_assignment: cluster_name: default_cluster endpoints: - lb_endpoints: - endpoint: address: socket_address: address: default.example.com port_value: 80
3.5 禁用 Lua 过滤器配置
python
route_config: name: local_route virtual_hosts: - name: backend domains: ["*"] routes: - match: prefix: "/healthz" route: { cluster: health_cluster } per_filter_config: envoy.filters.http.lua: "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute disabled: true - match: prefix: "/" route: { cluster: default_cluster }
static_resources: clusters: - name: health_cluster connect_timeout: 0.25s type: STRICT_DNS lb_policy: ROUND_ROBIN load_assignment: cluster_name: health_cluster endpoints: - lb_endpoints: - endpoint: address: socket_address: address: health.example.com port_value: 80 - name: default_cluster connect_timeout: 0.25s type: STRICT_DNS lb_policy: ROUND_ROBIN load_assignment: cluster_name: default_cluster endpoints: - lb_endpoints: - endpoint: address: socket_address: address: default.example.com port_value: 80
4. 工作流程分析
4.1 过滤器执行流程

4.2 异步 HTTP 调用流程

5. 代码实现ER图

6. 最佳实践
6.1 与其他过滤器配合使用
sql
http_filters:- name: envoy.filters.http.jwt_authn typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication # JWT认证配置- name: envoy.filters.http.lua typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua inline_code: | function envoy_on_request(request_handle) -- 自定义逻辑 end
function envoy_on_response(response_handle) -- 自定义逻辑 end- name: envoy.filters.http.router
6.2 性能优化
css
http_filters:- name: envoy.filters.http.lua typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua default_source_code: filename: /etc/envoy/lua_scripts/optimized.lua
route_config: name: local_route virtual_hosts: - name: backend domains: ["*"] routes: - match: prefix: "/api" route: { cluster: api_cluster } per_filter_config: envoy.filters.http.lua: "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute name: "api_scripts" - match: prefix: "/" route: { cluster: default_cluster }
6.3 复杂路由配置
css
route_config: name: local_route virtual_hosts: - name: backend domains: ["*"] routes: - match: prefix: "/api/v1" route: { cluster: api_v1_cluster } per_filter_config: envoy.filters.http.lua: "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute name: "api_v1_scripts" - match: prefix: "/api/v2" route: { cluster: api_v2_cluster } per_filter_config: envoy.filters.http.lua: "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute name: "api_v2_scripts" - match: prefix: "/static" route: { cluster: static_cluster } per_filter_config: envoy.filters.http.lua: "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute name: "static_scripts" - match: prefix: "/" route: { cluster: default_cluster }
6.4 禁用特定路径的 Lua 过滤器
css
route_config: name: local_route virtual_hosts: - name: backend domains: ["*"] routes: - match: prefix: "/healthz" route: { cluster: health_cluster } per_filter_config: envoy.filters.http.lua: "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute disabled: true - match: prefix: "/metrics" route: { cluster: metrics_cluster } per_filter_config: envoy.filters.http.lua: "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute disabled: true - match: prefix: "/" route: { cluster: default_cluster }
7. 代码实现细节
7.1 过滤器初始化
css
Filter::Filter(FilterConfigConstSharedPtr config, TimeSource& time_source) : config_(config), time_source_(time_source) {}
7.2 请求处理
php
Http::FilterHeadersStatusFilter::doHeaders(StreamHandleRef& handle, Filters::Common::Lua::CoroutinePtr& coroutine, FilterCallbacks& callbacks, int function_ref, PerLuaCodeSetup* setup, Http::RequestOrResponseHeaderMap& headers, bool end_stream) { if (function_ref == LUA_REFNIL) { return Http::FilterHeadersStatus::Continue; } ASSERT(setup); coroutine = setup->createCoroutine();
handle.reset(StreamHandleWrapper::create(coroutine->luaState(), *coroutine, headers, end_stream, *this, callbacks, time_source_), true);
Http::FilterHeadersStatus status = Http::FilterHeadersStatus::Continue; try { status = handle.get()->start(function_ref); handle.markDead(); } catch (const Filters::Common::Lua::LuaException& e) { scriptError(e); }
return status;}
7.3 响应处理
php
Http::FilterHeadersStatus Filter::encodeHeaders(Http::ResponseHeaderMap& headers, bool end_stream) { PerLuaCodeSetup* setup = getPerLuaCodeSetup(config_.get(), decoder_callbacks_.callbacks_); const int function_ref = setup ? setup->responseFunctionRef() : LUA_REFNIL; return doHeaders(response_stream_wrapper_, response_coroutine_, encoder_callbacks_, function_ref, setup, headers, end_stream);}
7.4 异步 HTTP 调用
php
int StreamHandleWrapper::doSynchronousHttpCall(lua_State* state, Tracing::Span& span) { http_request_ = makeHttpCall(state, filter_, span, *this); if (http_request_ != nullptr) { state_ = State::HttpCall; return lua_yield(state, 0); } else { // Immediate failure case. The return arguments are already on the stack. ASSERT(lua_gettop(state) >= 2); return 2; }}
void StreamHandleWrapper::onSuccess(const Http::AsyncClient::Request&, Http::ResponseMessagePtr&& response) { ASSERT(state_ == State::HttpCall || state_ == State::Running); ENVOY_LOG(debug, "async HTTP response complete"); http_request_ = nullptr;
// We need to build a table with the headers as return param 1. The body will be return param 2. lua_newtable(coroutine_.luaState()); response->headers().iterate([lua_State = coroutine_.luaState()]( const Http::HeaderEntry& header) -> Http::HeaderMap::Iterate { lua_pushlstring(lua_State, header.key().getStringView().data(), header.key().getStringView().size()); lua_pushlstring(lua_State, header.value().getStringView().data(), header.value().getStringView().size()); lua_settable(lua_State, -3); return Http::HeaderMap::Iterate::Continue; });
// TODO(mattklein123): Avoid double copy here. if (response->body().length() > 0) { lua_pushlstring(coroutine_.luaState(), response->bodyAsString().data(), response->body().length()); } else { lua_pushnil(coroutine_.luaState()); }
// In the immediate failure case, we are just going to immediately return to the script. We // have already pushed the return arguments onto the stack. if (state_ == State::HttpCall) { state_ = State::Running; markLive();
try { coroutine_.resume(2, yield_callback_); markDead(); } catch (const Filters::Common::Lua::LuaException& e) { filter_.scriptError(e); }
if (state_ == State::Running) { headers_continued_ = true; callbacks_.continueIteration(); } }}
7.5 响应生成
ruby
void Filter::DecoderCallbacks::respond(Http::ResponseHeaderMapPtr&& headers, Buffer::Instance* body, lua_State*) { if (Runtime::runtimeFeatureEnabled( "envoy.reloadable_features.lua_respond_with_send_local_reply")) { uint64_t status = Http::Utility::getResponseStatus(*headers); auto modify_headers = [&headers](Http::ResponseHeaderMap& response_headers) { headers->iterate( [&response_headers](const Http::HeaderEntry& header) -> Http::HeaderMap::Iterate { response_headers.addCopy(Http::LowerCaseString(header.key().getStringView()), header.value().getStringView()); return Http::HeaderMap::Iterate::Continue; }); }; callbacks_->sendLocalReply(static_cast<Envoy::Http::Code>(status), body ? body->toString() : "", modify_headers, absl::nullopt, HttpResponseCodeDetails::get().LuaResponse); } else { callbacks_->encodeHeaders(std::move(headers), body == nullptr, HttpResponseCodeDetails::get().LuaResponse); if (body && !parent_.destroyed_) { callbacks_->encodeData(*body, true); } }}