WEBKT

Envoy 正则表达式性能优化:配置与代码层面的深度剖析

91 0 0 0

Envoy 正则表达式性能优化:配置与代码层面的深度剖析

为什么需要关注 Envoy 中的正则表达式性能?

1. 配置层面的优化

1.1 避免使用过于复杂的正则表达式

1.2 谨慎使用回溯

1.3 选择合适的正则表达式引擎

1.4 启用正则表达式缓存

2. 代码层面的优化 (C++)

2.1 使用 RE2 类

2.2 预编译正则表达式

2.3 避免不必要的字符串拷贝

2.4 使用 RE2::Set 进行多模式匹配 (高级技巧)

3. 总结与案例分析

Envoy 正则表达式性能优化:配置与代码层面的深度剖析

作为一名经常和 Envoy 打交道的开发者,你肯定遇到过需要使用正则表达式进行路由、匹配请求头、重写 URL 等场景。正则表达式的强大毋庸置疑,但如果使用不当,它也可能成为性能瓶颈,甚至导致 Envoy 崩溃。今天咱们就来聊聊 Envoy 中正则表达式的优化,从配置层面和代码层面两个维度入手,帮你彻底掌握 Envoy 正则表达式的优化技巧。

为什么需要关注 Envoy 中的正则表达式性能?

Envoy 作为高性能代理,对性能的要求非常苛刻。正则表达式引擎的执行效率直接影响 Envoy 的吞吐量和延迟。不合理的正则表达式,特别是那些会导致“灾难性回溯”的表达式,会消耗大量的 CPU 资源,严重时可能导致 Envoy 进程阻塞,无法处理新的请求。因此,理解并优化 Envoy 中的正则表达式至关重要。

1. 配置层面的优化

在 Envoy 的配置文件中,我们经常使用 routeheader_matcherregex_rewrite 等配置项来定义路由规则和请求处理逻辑。这些配置项都支持正则表达式。在配置层面,我们可以通过以下几个方面来优化正则表达式的性能:

1.1 避免使用过于复杂的正则表达式

这是最基本也是最重要的一点。过于复杂的正则表达式,例如包含大量嵌套、回溯、捕获组的表达式,会显著增加引擎的计算量。尽量保持正则表达式的简洁,只匹配必要的内容。能用字符串匹配解决的问题,就不要用正则表达式。

反例:

route_config:
virtual_hosts:
- name: backend
domains: ["*"]
routes:
- match:
prefix: "/"
headers:
- name: "x-my-header"
safe_regex_match:
google_re2: {}
regex: "^(?:(?:https?|ftp):\/\/)?(?:www\.)?([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,6}(?:\/.*)?$"
route:
cluster: service_backend

上面的例子试图用一个正则表达式匹配所有可能的 URL。这种做法非常低效,而且容易出错。应该根据实际需要,拆分成更小的、更具体的正则表达式。

正例:

如果只需要匹配特定域名,可以这样写:

route_config:
virtual_hosts:
- name: backend
domains: ["*"]
routes:
- match:
prefix: "/"
headers:
- name: "x-my-header"
string_match: "www.example.com"
route:
cluster: service_backend

1.2 谨慎使用回溯

回溯是正则表达式引擎的特性,但也是性能杀手。当正则表达式无法匹配时,引擎会尝试不同的路径,这就是回溯。过多的回溯会导致“灾难性回溯”,CPU 使用率飙升。

要避免回溯,可以注意以下几点:

  • 避免使用嵌套量词:例如 (a*)*,这种表达式会导致大量的回溯。
  • 使用原子组:原子组 (?>...) 可以阻止引擎在组内回溯。
  • 使用独占量词:独占量词 ?+*+++{m,n}+ 也可以阻止回溯。

示例:

# 不推荐:可能导致大量回溯
regex: "a*b"
# 推荐:使用原子组阻止回溯
regex: "(?>a*)b"
# 推荐:使用独占量词阻止回溯
regex: "a*+b"

1.3 选择合适的正则表达式引擎

Envoy 支持两种正则表达式引擎:RE2BoringSSLregex(基于 PCRE)。

  • RE2:Google 开发的正则表达式引擎,安全性高,性能稳定,不会出现灾难性回溯。强烈推荐使用。
  • BoringSSL regex (PCRE):性能可能略高于 RE2,但存在灾难性回溯的风险。不推荐在生产环境中使用,除非你非常确定你的正则表达式不会导致回溯。

在 Envoy 配置中,可以通过 safe_regex_match 字段选择使用 RE2 引擎,regex_match 则使用 BoringSSL regex。 建议使用 safe_regex_match

# 推荐:使用 RE2 引擎
safe_regex_match:
google_re2: {}
regex: "..."
# 不推荐:使用 BoringSSL regex (PCRE)
regex_match: "..."

1.4 启用正则表达式缓存

Envoy 默认会缓存编译后的正则表达式。这可以避免重复编译相同的正则表达式,提高性能。通常情况下,不需要手动配置缓存。但你可以通过以下配置项调整缓存大小:

  • runtime.overload_manager.regex_cache_size:控制全局正则表达式缓存大小。默认值通常足够大,不需要修改。

一般情况下, 保持默认配置即可.

2. 代码层面的优化 (C++)

如果你正在开发 Envoy 的扩展,或者直接修改 Envoy 源码,那么你可以在代码层面进行更精细的正则表达式优化。

2.1 使用 RE2

Envoy 主要使用 RE2 引擎,因此在代码中应该使用 RE2 类进行正则表达式操作。RE2 类提供了丰富的 API,可以进行匹配、替换、查找等操作。

#include "re2/re2.h"
// ...
std::string text = "example text";
re2::RE2 re("example");
if (re.Match(text, 0, text.size(), re2::RE2::UNANCHORED, nullptr, 0)) {
// 匹配成功
}

2.2 预编译正则表达式

如果同一个正则表达式会被多次使用,应该预编译它,避免重复编译。可以将 RE2 对象作为成员变量保存,或者使用静态变量。

class MyFilter : public Envoy::Http::StreamDecoderFilter {
public:
MyFilter() : regex_("some_pattern") {} // 预编译
// ...
private:
re2::RE2 regex_;
};

2.3 避免不必要的字符串拷贝

RE2Match 方法支持 StringPiece 类型,可以直接在原始字符串上进行匹配,避免不必要的字符串拷贝。

// 假设 request_headers 是 Envoy::Http::RequestHeaderMap 对象
const Envoy::Http::HeaderString& value = request_headers.get(header_name);
re2::StringPiece input(value.data(), value.size());
if (regex_.Match(input, 0, input.size(), re2::RE2::UNANCHORED, nullptr, 0)) {
// ...
}

2.4 使用 RE2::Set 进行多模式匹配 (高级技巧)

如果需要同时匹配多个正则表达式,可以使用 RE2::SetRE2::Set 可以将多个正则表达式编译成一个状态机,一次匹配可以找出所有匹配的模式,效率更高。

#include "re2/set.h"
// ...
re2::RE2::Set set(re2::RE2::Options(), re2::RE2::UNANCHORED);
set.Add("pattern1", nullptr);
set.Add("pattern2", nullptr);
set.Add("pattern3", nullptr);
set.Compile();
std::string text = "...";
std::vector<int> match_ids;
if (set.Match(text, &match_ids)) {
for (int id : match_ids) {
// id 对应匹配的模式的索引
}
}

RE2::Set 特别适用于有大量匹配规则的场景, 例如 WAF (Web Application Firewall) 场景.

3. 总结与案例分析

通过以上配置层面和代码层面的优化,可以显著提高 Envoy 中正则表达式的性能。总结一下关键点:

  • 保持正则表达式简洁
  • 避免回溯
  • 使用 RE2 引擎
  • 启用正则表达式缓存
  • 预编译正则表达式
  • 避免不必要的字符串拷贝
  • 使用 RE2::Set 进行多模式匹配

案例分析:

假设我们需要根据请求路径中的参数值进行路由。例如,将 /user/123 路由到 user_service_1,将 /user/456 路由到 user_service_2

不推荐的做法:

route_config:
virtual_hosts:
- name: backend
domains: ["*"]
routes:
- match:
path_regex: "/user/123"
route:
cluster: user_service_1
- match:
path_regex: "/user/456"
route:
cluster: user_service_2

这种方法为每个用户 ID 都定义了一个正则表达式,如果有成千上万个用户,就会导致 Envoy 配置过于庞大,性能下降。

推荐的做法:

route_config:
virtual_hosts:
- name: backend
domains: ["*"]
routes:
- match:
safe_regex_match:
google_re2:{}
regex: "^/user/(?P<user_id>[0-9]+)$"
route:
cluster_header: user_service
metadata_match:
filter_metadata:
envoy.lb:
user_id: "{user_id}"

这种方法使用一个正则表达式匹配所有用户 ID,并使用命名捕获组提取用户 ID。然后,通过 cluster_headermetadata_match 将用户 ID 传递给负载均衡器,由负载均衡器根据用户 ID 选择对应的服务实例。这种方法更简洁、高效,而且易于扩展。

通过学习和应用这些优化技巧,相信你一定能写出高性能、高可靠的 Envoy 正则表达式配置和代码,让你的 Envoy 飞起来!

Envoy老司机 Envoy正则表达式性能优化

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/8202