Skip to content

Commit c5f7278

Browse files
authored
lua: add a shared vm key to tell the lua to share scripts across configurations (#47562)
Commit Message: lua: add a shared vm key to tell the lua to share scripts across configurations Additional Description: Add a `shared_vm_id` to the Lua configuration. If the `shared_vm_id` is set, then the parsed Lua scripts will be shared globally and the `shared_vm_id` + script + package paths will be used to the unique cache key. Risk Level: low. Testing: unit. Docs Changes: n/a. Release Notes: added. Platform Specific Features: n/a. --------- Signed-off-by: wbpcode <wbphub@gmail.com>
1 parent 726d7ac commit c5f7278

10 files changed

Lines changed: 469 additions & 27 deletions

File tree

‎api/envoy/extensions/filters/http/lua/v3/lua.proto‎

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
2222
// Lua :ref:`configuration overview <config_http_filters_lua>`.
2323
// [#extension: envoy.filters.http.lua]
2424

25-
// [#next-free-field: 9]
25+
// [#next-free-field: 10]
2626
message Lua {
2727
option (udpa.annotations.versioning).previous_message_type =
2828
"envoy.config.filter.http.lua.v2.Lua";
@@ -127,9 +127,39 @@ message Lua {
127127
// ``package.cpath``, i.e. modules which are loadable C libraries rather than Lua source, for
128128
// example ``/etc/envoy/lua/?.so``.
129129
repeated string package_cpaths = 8 [(validate.rules).repeated = {items {string {min_len: 1}}}];
130+
131+
// If set, the Lua VMs this filter builds are shared with every other Lua filter configuration
132+
// that sets the same ``shared_vm_id`` and configures the same script, rather than each
133+
// configuration building its own. This applies to every script this message configures:
134+
// :ref:`default_source_code
135+
// <envoy_v3_api_field_extensions.filters.http.lua.v3.Lua.default_source_code>`,
136+
// :ref:`inline_code <envoy_v3_api_field_extensions.filters.http.lua.v3.Lua.inline_code>` and
137+
// every entry of :ref:`source_codes
138+
// <envoy_v3_api_field_extensions.filters.http.lua.v3.Lua.source_codes>`.
139+
//
140+
// Sharing is decided per script, not per configuration: two configurations that agree on this
141+
// id share a VM only for the scripts whose contents match, and whose :ref:`package_paths
142+
// <envoy_v3_api_field_extensions.filters.http.lua.v3.Lua.package_paths>` and
143+
// :ref:`package_cpaths
144+
// <envoy_v3_api_field_extensions.filters.http.lua.v3.Lua.package_cpaths>` match, since a
145+
// script that resolves its ``require`` calls differently does not produce an equivalent VM.
146+
// A :ref:`LuaPerRoute.shared_vm_id
147+
// <envoy_v3_api_field_extensions.filters.http.lua.v3.LuaPerRoute.shared_vm_id>` participates in
148+
// the same sharing, so a route's inline script can reuse a VM built here and the other way
149+
// around.
150+
//
151+
// A VM is not only an amount of memory, it is also a set of Lua globals that outlive a request.
152+
// Scripts sharing a VM therefore see each other's globals, exactly as separate requests through
153+
// one configuration already do. Leave this field unset, which is the default, to keep every
154+
// configuration's scripts in VMs of their own.
155+
//
156+
// A shared VM lives for as long as at least one configuration using it is alive. Once the last
157+
// one is drained the VM is torn down, and the next configuration asking for that id and script
158+
// builds a fresh one.
159+
string shared_vm_id = 9;
130160
}
131161

132-
// [#next-free-field: 7]
162+
// [#next-free-field: 8]
133163
message LuaPerRoute {
134164
oneof override {
135165
// Disable the Lua filter for this particular vhost or route. If disabled is specified in
@@ -178,4 +208,16 @@ message LuaPerRoute {
178208
// <envoy_v3_api_field_extensions.filters.http.lua.v3.LuaPerRoute.package_paths>`, but for
179209
// ``package.cpath``.
180210
repeated string package_cpaths = 6 [(validate.rules).repeated = {items {string {min_len: 1}}}];
211+
212+
// As :ref:`Lua.shared_vm_id
213+
// <envoy_v3_api_field_extensions.filters.http.lua.v3.Lua.shared_vm_id>`, but for the VM built
214+
// from this route's :ref:`source_code
215+
// <envoy_v3_api_field_extensions.filters.http.lua.v3.LuaPerRoute.source_code>`. Routes and
216+
// filter configurations share one pool of VMs, so a route setting the same id as a filter
217+
// configuration reuses that configuration's VM when the script matches.
218+
//
219+
// Setting this has no effect when this route selects a script by :ref:`name
220+
// <envoy_v3_api_field_extensions.filters.http.lua.v3.LuaPerRoute.name>`, or configures no
221+
// script at all, since that VM belongs to the filter and follows the filter's setting.
222+
string shared_vm_id = 7;
181223
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Added :ref:`shared_vm_id <envoy_v3_api_field_extensions.filters.http.lua.v3.Lua.shared_vm_id>` and
2+
:ref:`LuaPerRoute.shared_vm_id
3+
<envoy_v3_api_field_extensions.filters.http.lua.v3.LuaPerRoute.shared_vm_id>` to the Lua filter.
4+
Configurations that set the same id share one set of Lua VMs for every script whose contents and
5+
package search paths match, instead of each building its own. This is off by default: with the
6+
field unset the filter keeps building one set of VMs per configured script.

‎docs/root/configuration/http/http_filters/lua_filter.rst‎

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,50 @@ well. A route which selects a script by :ref:`name
170170
script at all, runs in a VM belonging to the filter and already has the filter-level patterns; its
171171
own patterns are unused.
172172

173+
.. _config_http_filters_lua_shared_vm_id:
174+
175+
Sharing Lua VMs between configurations
176+
--------------------------------------
177+
178+
Every configured script gets its own set of Lua VMs: one per worker thread plus one on the main
179+
thread. A deployment that repeats the same script across many listeners, filter chains or routes
180+
therefore pays for the same code many times over. Setting :ref:`shared_vm_id
181+
<envoy_v3_api_field_extensions.filters.http.lua.v3.Lua.shared_vm_id>` opts a configuration into
182+
sharing those VMs with every other Lua configuration that sets the same id:
183+
184+
.. code-block:: yaml
185+
186+
http_filters:
187+
- name: envoy.filters.http.lua
188+
typed_config:
189+
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
190+
shared_vm_id: my_scripts
191+
default_source_code:
192+
filename: /etc/envoy/lua/common.lua
193+
194+
Sharing is decided per script rather than per configuration. Two configurations that agree on the
195+
id share a VM only for the scripts whose contents match and whose :ref:`package_paths
196+
<envoy_v3_api_field_extensions.filters.http.lua.v3.Lua.package_paths>` and :ref:`package_cpaths
197+
<envoy_v3_api_field_extensions.filters.http.lua.v3.Lua.package_cpaths>` match, since a script that
198+
resolves its ``require`` calls elsewhere does not produce an equivalent VM. The id applies to every
199+
script the configuration defines: the default script, the deprecated ``inline_code``, and each
200+
entry of :ref:`source_codes <envoy_v3_api_field_extensions.filters.http.lua.v3.Lua.source_codes>`.
201+
Routes draw from the same pool, so a route setting :ref:`LuaPerRoute.shared_vm_id
202+
<envoy_v3_api_field_extensions.filters.http.lua.v3.LuaPerRoute.shared_vm_id>` can reuse a VM a
203+
filter configuration already built, and the other way around.
204+
205+
.. attention::
206+
207+
A Lua VM is not only an amount of memory, it is also a set of Lua globals that outlive a
208+
request. Scripts sharing a VM see each other's globals, exactly as separate requests through one
209+
configuration already do. Only share VMs between configurations whose scripts are prepared for
210+
that.
211+
212+
A shared VM lives for as long as at least one configuration using it is alive. Once the last one is
213+
drained the VM is torn down, and the next configuration asking for that id and script builds a
214+
fresh one. Leaving the field unset, which is the default, keeps every configuration's scripts in
215+
VMs of their own.
216+
173217
Upstream Filter
174218
---------------
175219

@@ -196,7 +240,8 @@ individual filter instance/script can be tracked by providing a per-filter
196240
In addition, a single process-wide ``lua.lua_vm_count`` gauge (not affected by ``stat_prefix``) tracks
197241
the total number of active Lua VMs across every filter-config-level and route-level Lua script
198242
configured in the process. Each configured script accounts for ``concurrency + 1`` VMs (one per
199-
worker thread, plus the main thread).
243+
worker thread, plus the main thread), except that scripts sharing VMs through :ref:`shared_vm_id
244+
<config_http_filters_lua_shared_vm_id>` are counted once between them.
200245

201246
Script examples
202247
---------------

‎source/extensions/filters/http/lua/BUILD‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ envoy_cc_library(
2020
":wrappers_lib",
2121
"//envoy/http:codes_interface",
2222
"//envoy/http:filter_interface",
23+
"//envoy/singleton:manager_interface",
2324
"//envoy/upstream:cluster_manager_interface",
2425
"//source/common/buffer:buffer_lib",
2526
"//source/common/common:enum_to_int",
27+
"//source/common/common:hex_lib",
28+
"//source/common/common:thread_lib",
2629
"//source/common/config:datasource_lib",
2730
"//source/common/crypto:utility_lib",
2831
"//source/common/http:message_lib",
@@ -31,6 +34,7 @@ envoy_cc_library(
3134
"//source/extensions/filters/common/lua:protobuf_converter_lib",
3235
"//source/extensions/filters/common/lua:wrappers_lib",
3336
"//source/extensions/filters/http/common:factory_base_lib",
37+
"@abseil-cpp//absl/container:flat_hash_map",
3438
"@abseil-cpp//absl/strings",
3539
"@envoy_api//envoy/extensions/filters/http/lua/v3:pkg_cc_proto",
3640
],

‎source/extensions/filters/http/lua/config.cc‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ absl::StatusOr<Envoy::Http::FilterFactoryCb> LuaFilterConfig::createHttpFilterFa
1616
Server::Configuration::ServerFactoryContext& context,
1717
Server::Configuration::ExtraFactoryContext& extra_context) {
1818
absl::Status creation_status = absl::OkStatus();
19-
FilterConfigConstSharedPtr filter_config(
20-
new FilterConfig{proto_config, context.threadLocal(), context.clusterManager(), context.api(),
21-
extra_context.scopeOr(context), extra_context.stats_prefix,
22-
context.options().concurrency(), creation_status});
19+
FilterConfigConstSharedPtr filter_config(new FilterConfig{
20+
proto_config, context.threadLocal(), context.clusterManager(), context.api(),
21+
extra_context.scopeOr(context), extra_context.stats_prefix, context.options().concurrency(),
22+
context.singletonManager(), creation_status});
2323
RETURN_IF_NOT_OK_REF(creation_status);
2424
auto& time_source = context.mainThreadDispatcher().timeSource();
2525
return [filter_config, &time_source](Http::FilterChainFactoryCallbacks& callbacks) -> void {

‎source/extensions/filters/http/lua/lua_filter.cc‎

Lines changed: 106 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@
1313
#include "source/common/buffer/buffer_impl.h"
1414
#include "source/common/common/assert.h"
1515
#include "source/common/common/enum_to_int.h"
16+
#include "source/common/common/hex.h"
17+
#include "source/common/common/thread.h"
1618
#include "source/common/config/datasource.h"
1719
#include "source/common/crypto/crypto_impl.h"
1820
#include "source/common/crypto/utility.h"
1921
#include "source/common/http/message_impl.h"
2022

23+
#include "absl/container/flat_hash_map.h"
2124
#include "absl/strings/escaping.h"
25+
#include "absl/strings/str_cat.h"
2226
#include "absl/strings/str_join.h"
2327

2428
namespace Envoy {
@@ -212,6 +216,85 @@ Stats::Gauge& lookupLuaVmCountGauge(Stats::Scope& server_scope) {
212216

213217
} // namespace
214218

219+
SINGLETON_MANAGER_REGISTRATION(lua_shared_http_filter_code_setups);
220+
221+
namespace {
222+
223+
// Identifies a shareable VM setup. The digest covers the package search paths as well as the
224+
// code, because two configurations that agree on an id and on a script but disagree on where
225+
// `require` looks for modules do not describe equivalent VMs and must not share one. Each part
226+
// is length-prefixed so that no two distinct inputs produce the same string to digest.
227+
std::string sharedVmKey(absl::string_view shared_vm_id, absl::string_view lua_code,
228+
const Filters::Common::Lua::PackagePaths& package_paths) {
229+
const std::string digest_input =
230+
absl::StrCat(lua_code.size(), ":", lua_code, package_paths.path.size(), ":",
231+
package_paths.path, package_paths.cpath.size(), ":", package_paths.cpath);
232+
// The digest is a fixed-length hex string at the end of the key, so an id that happens to
233+
// contain the separator cannot be mistaken for a different id with a different digest.
234+
return absl::StrCat(
235+
shared_vm_id, "/",
236+
Hex::encode(Envoy::Common::Crypto::UtilitySingleton::get().getSha256Digest(digest_input)));
237+
}
238+
239+
SharedLuaCodeSetupRegistrySharedPtr sharedCodeSetupRegistry(Singleton::Manager& singleton_manager) {
240+
return singleton_manager.getTyped<SharedLuaCodeSetupRegistry>(
241+
SINGLETON_MANAGER_REGISTERED_NAME(lua_shared_http_filter_code_setups),
242+
[] { return std::make_shared<SharedLuaCodeSetupRegistry>(); });
243+
}
244+
245+
// Builds the VM setup for one configured script, reusing an already built one when the
246+
// configuration opted into sharing. `registry` is null when it did not, which keeps the
247+
// original behavior of one dedicated set of VMs per configured script.
248+
PerLuaCodeSetupSharedPtr
249+
createPerLuaCodeSetup(const SharedLuaCodeSetupRegistrySharedPtr& registry,
250+
absl::string_view shared_vm_id, const std::string& lua_code,
251+
const Filters::Common::Lua::PackagePaths& package_paths,
252+
ThreadLocal::SlotAllocator& tls, Stats::Gauge& vm_count_gauge,
253+
uint32_t concurrency, absl::Status& creation_status) {
254+
if (registry == nullptr) {
255+
return std::make_shared<PerLuaCodeSetup>(lua_code, package_paths, tls, vm_count_gauge,
256+
concurrency, creation_status);
257+
}
258+
return registry->getOrCreate(shared_vm_id, lua_code, package_paths, tls, vm_count_gauge,
259+
concurrency, creation_status);
260+
}
261+
262+
} // namespace
263+
264+
PerLuaCodeSetupSharedPtr SharedLuaCodeSetupRegistry::getOrCreate(
265+
absl::string_view shared_vm_id, const std::string& lua_code,
266+
const Filters::Common::Lua::PackagePaths& package_paths, ThreadLocal::SlotAllocator& tls,
267+
Stats::Gauge& vm_count_gauge, uint32_t concurrency, absl::Status& creation_status) {
268+
// Filter and route configurations are only ever built on the main thread, so the map needs no
269+
// lock. Nothing touches it when a setup is released, which is what makes that safe: a setup
270+
// whose last owner goes away on a worker thread just leaves an expired entry behind.
271+
ASSERT_IS_MAIN_OR_TEST_THREAD();
272+
273+
const std::string key = sharedVmKey(shared_vm_id, lua_code, package_paths);
274+
const auto it = setups_.find(key);
275+
if (it != setups_.end()) {
276+
if (PerLuaCodeSetupSharedPtr setup = it->second.lock()) {
277+
ENVOY_LOG(debug, "reusing shared Lua VM for shared_vm_id '{}'", shared_vm_id);
278+
return setup;
279+
}
280+
}
281+
282+
PerLuaCodeSetupSharedPtr setup = std::make_shared<PerLuaCodeSetup>(
283+
lua_code, package_paths, tls, vm_count_gauge, concurrency, creation_status);
284+
if (!creation_status.ok()) {
285+
// A script that does not parse is rejected for this configuration; it must not be handed to
286+
// the next one that asks for the same id.
287+
return nullptr;
288+
}
289+
290+
// Drop the entries whose last user has gone away, so that a server churning through scripts
291+
// over its lifetime does not accumulate dead keys.
292+
absl::erase_if(setups_, [](const auto& entry) { return entry.second.expired(); });
293+
setups_[key] = setup;
294+
ENVOY_LOG(debug, "created shared Lua VM for shared_vm_id '{}'", shared_vm_id);
295+
return setup;
296+
}
297+
215298
PerLuaCodeSetup::PerLuaCodeSetup(const std::string& lua_code,
216299
const Filters::Common::Lua::PackagePaths& package_paths,
217300
ThreadLocal::SlotAllocator& tls, Stats::Gauge& vm_count_gauge,
@@ -925,7 +1008,8 @@ FilterConfig::FilterConfig(const envoy::extensions::filters::http::lua::v3::Lua&
9251008
ThreadLocal::SlotAllocator& tls,
9261009
Upstream::ClusterManager& cluster_manager, Api::Api& api,
9271010
Stats::Scope& scope, const std::string& stats_prefix,
928-
uint32_t concurrency, absl::Status& creation_status)
1011+
uint32_t concurrency, Singleton::Manager& singleton_manager,
1012+
absl::Status& creation_status)
9291013
: cluster_manager_(cluster_manager),
9301014
clear_route_cache_(
9311015
proto_config.has_clear_route_cache() ? proto_config.clear_route_cache().value() : true),
@@ -938,6 +1022,11 @@ FilterConfig::FilterConfig(const envoy::extensions::filters::http::lua::v3::Lua&
9381022
Stats::Gauge& vm_count_gauge = lookupLuaVmCountGauge(api.rootScope());
9391023
const Filters::Common::Lua::PackagePaths package_paths =
9401024
packagePaths(proto_config.package_paths(), proto_config.package_cpaths());
1025+
// Left null when no id is configured, which is what tells the helper below to build VMs that
1026+
// belong to this configuration alone.
1027+
if (!proto_config.shared_vm_id().empty()) {
1028+
shared_code_setup_registry_ = sharedCodeSetupRegistry(singleton_manager);
1029+
}
9411030

9421031
if (proto_config.has_default_source_code()) {
9431032
if (!proto_config.inline_code().empty()) {
@@ -949,21 +1038,23 @@ FilterConfig::FilterConfig(const envoy::extensions::filters::http::lua::v3::Lua&
9491038

9501039
auto code_or = Config::DataSource::read(proto_config.default_source_code(), true, api);
9511040
SET_AND_RETURN_IF_NOT_OK(code_or.status(), creation_status);
952-
default_lua_code_setup_ = std::make_unique<PerLuaCodeSetup>(
953-
code_or.value(), package_paths, tls, vm_count_gauge, concurrency, creation_status);
1041+
default_lua_code_setup_ = createPerLuaCodeSetup(
1042+
shared_code_setup_registry_, proto_config.shared_vm_id(), code_or.value(), package_paths,
1043+
tls, vm_count_gauge, concurrency, creation_status);
9541044
RETURN_ONLY_IF_NOT_OK_REF(creation_status);
9551045
} else if (!proto_config.inline_code().empty()) {
956-
default_lua_code_setup_ =
957-
std::make_unique<PerLuaCodeSetup>(proto_config.inline_code(), package_paths, tls,
958-
vm_count_gauge, concurrency, creation_status);
1046+
default_lua_code_setup_ = createPerLuaCodeSetup(
1047+
shared_code_setup_registry_, proto_config.shared_vm_id(), proto_config.inline_code(),
1048+
package_paths, tls, vm_count_gauge, concurrency, creation_status);
9591049
RETURN_ONLY_IF_NOT_OK_REF(creation_status);
9601050
}
9611051

9621052
for (const auto& source : proto_config.source_codes()) {
9631053
auto code_or = Config::DataSource::read(source.second, true, api);
9641054
SET_AND_RETURN_IF_NOT_OK(code_or.status(), creation_status);
965-
auto per_lua_code_setup_ptr = std::make_unique<PerLuaCodeSetup>(
966-
code_or.value(), package_paths, tls, vm_count_gauge, concurrency, creation_status);
1055+
auto per_lua_code_setup_ptr = createPerLuaCodeSetup(
1056+
shared_code_setup_registry_, proto_config.shared_vm_id(), code_or.value(), package_paths,
1057+
tls, vm_count_gauge, concurrency, creation_status);
9671058
RETURN_ONLY_IF_NOT_OK_REF(creation_status);
9681059
per_lua_code_setups_map_[source.first] = std::move(per_lua_code_setup_ptr);
9691060
}
@@ -982,9 +1073,13 @@ FilterConfigPerRoute::FilterConfigPerRoute(
9821073
auto code_or = Config::DataSource::read(config.source_code(), true, context.api());
9831074
SET_AND_RETURN_IF_NOT_OK(code_or.status(), creation_status);
9841075
Stats::Gauge& vm_count_gauge = lookupLuaVmCountGauge(context.api().rootScope());
985-
per_lua_code_setup_ptr_ = std::make_unique<PerLuaCodeSetup>(
986-
code_or.value(), packagePaths(config.package_paths(), config.package_cpaths()),
987-
context.threadLocal(), vm_count_gauge, context.options().concurrency(), creation_status);
1076+
if (!config.shared_vm_id().empty()) {
1077+
shared_code_setup_registry_ = sharedCodeSetupRegistry(context.singletonManager());
1078+
}
1079+
per_lua_code_setup_ptr_ = createPerLuaCodeSetup(
1080+
shared_code_setup_registry_, config.shared_vm_id(), code_or.value(),
1081+
packagePaths(config.package_paths(), config.package_cpaths()), context.threadLocal(),
1082+
vm_count_gauge, context.options().concurrency(), creation_status);
9881083
}
9891084
}
9901085

0 commit comments

Comments
 (0)