最近啃了两周 Envoy 网关性能的硬骨头。线上 4 台 8C16G 物理机(Intel Xeon Gold 6248,万兆网卡),跑 Envoy 1.29.3,业务高峰时单实例 QPS 卡在 1.2 万上不去,P99 延迟飙到 180ms,软中断占 CPU 直奔 38%。从内核协议栈、Envoy 线程与连接模型到过滤器链路逐层扒下来,最终单实例稳定跑 2.8 万 QPS,P99 压到 42ms,整体吞吐提升 133%。
很多团队用 Envoy 只堆路由和过滤器配置,默认参数直接上线,最后瓶颈卡在哪都找不到。这篇把完整调优路径沉淀下来,三大核心维度,每一条都是压测 5 轮以上、线上跑过一周验证过的干货。
一、内核层调优:先拆掉操作系统的性能天花板
Envoy 本质是用户态转发代理,所有网络 IO 最终都落在内核协议栈上。80% 的网关性能瓶颈,根源不在 Envoy 本身,而在内核参数把上限锁死了。这一层调优性价比最高,改完重启服务立竿见影。
1. 基础资源限制:先把文件描述符拉满
高并发下每条 TCP 连接占一个文件描述符,系统默认 1024 的软限制在网关场景等于裸奔。
# /etc/security/limits.conf
envoy soft nofile 1048576
envoy hard nofile 1048576
envoy soft nproc 65535
envoy hard nproc 65535
踩坑提醒:systemd 启动的服务会忽略 limits.conf,必须在 service 单元里显式加 LimitNOFILE=1048576;容器环境还要检查宿主机的全局 limit,容器内的配置不会突破宿主机上限。
2. TCP 协议栈核心参数:万级并发必调项
这组参数是反复压测后敲定的最优值,适配 API 网关短请求、高并发的场景,CentOS 7.9 / 内核 5.4 验证通过:
# /etc/sysctl.conf
# 1. 端口池:保证源端口足够,避免端口耗尽
net.ipv4.ip_local_port_range = 1024 65535
# 2. TIME_WAIT 复用:网关场景必开,端口快速回收
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
# 3. 连接队列:应对突发流量,避免 SYN 丢包
net.ipv4.tcp_max_syn_backlog = 16384
net.core.somaxconn = 16384
# 4. 连接跟踪表:NAT/容器环境必调,否则连接数上去直接丢包
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
# 5. 收发缓冲区:适配万兆网卡,平衡内存与吞吐
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
# 6. 关闭时间戳:减少内核计算开销,NAT 场景无副作用
net.ipv4.tcp_timestamps = 0
# 7. TCP Keepalive:配合 Envoy 连接池,提前清理死连接
net.ipv4.tcp_keepalive_time = 300
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 3
避坑:别开 tcp_tw_recycle,NAT 网络下会导致大量客户端连接失败,这个参数在内核 4.10 之后已经被废弃,老教程别再信。
3. 网卡中断亲和性:解决单核软中断瓶颈
默认下单队列网卡的中断全打在 0 号 CPU 核上,流量一高软中断直接把核打满,成为整条链路的瓶颈。
# 查看网卡队列数,万兆网卡一般支持多队列
ethtool -l eth0
# 开启 8 队列,对应 8 核 CPU
ethtool -L eth0 combined 8
# 中断亲和性绑定:每个队列对应一个 CPU 核,避免中断争抢
for i in $(seq 0 7); do
irq_num=$(cat /proc/interrupts | grep "eth0-$i" | awk '{print $1}' | tr -d ':')
[ -n "$irq_num" ] && echo $((1 << i)) > /proc/irq/$irq_num/smp_affinity
done
优化效果:做完内核层这三件事,我们当场复测,软中断 CPU 占比从 38% 降到 11%,单实例 QPS 直接从 1.2 万冲到 1.8 万,P99 延迟下降 40%。
二、Envoy 核心层:线程模型与连接池深度调优
内核打通了底层瓶颈,接下来要把 Envoy 本身的转发效率拉满。Envoy 单进程多线程模型和 Nginx 类似,但配置体系更复杂,默认值偏保守,不适合高并发网关场景。
1. Worker 线程与 CPU 绑定:别让线程来回切换
Envoy 通过 --concurrency 控制 worker 线程数,每个线程跑一个独立事件循环。官方建议等于 CPU 核数,但实测网关场景设为物理核数 - 1 最优——留 1 个核给内核软中断和管理线程,避免 worker 和中断抢 CPU。
# 启动参数显式指定,8 核机器设 7
--concurrency 7
致命坑点:K8s 容器里绝对不能让 Envoy 自动探测核数。如果 Pod limit 设 4 核,但节点是 32 核,Envoy 会启动 32 个 worker 线程,大量上下文切换直接把性能拖垮,必须显式指定 concurrency。
进阶可以配合 cpuset 把 Envoy worker 线程和网卡中断绑定在不同核上,进一步降低争抢。
2. 上游连接池:网关性能的命门
连接池配置直接决定了建连开销和上游稳定性。配小了频繁建连握手,配大了浪费内存还会把上游服务打挂。
clusters:
- name: upstream_service
connect_timeout: 1s
type: EDS
lb_policy: LEAST_REQUEST
# 核心连接池参数
max_connections: 2048 # TCP 最大连接数
max_pending_requests: 4096 # 等待连接的排队请求数
max_requests: 10240 # 总并发请求数(HTTP/2 场景核心参数)
max_requests_per_connection: 0 # 单连接最大请求数,0=不限制,长连接复用
# 空闲连接回收
idle_timeout: 300s
# TCP Keepalive,和内核参数呼应
tcp_keepalive:
keepalive_time: 300
keepalive_interval: 30
keepalive_probes: 3
# 开启 HTTP/2 多路复用
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:
max_concurrent_streams: 100
initial_stream_window_size: 65536
调优经验公式:
- HTTP/1.1 场景:
max_connections= 峰值并发连接数 × 1.5,max_pending_requests=max_connections× 2 - HTTP/2 场景:单连接可承载多路并发,
max_connections可以降到原来的 1/10,靠max_requests控制总并发
我们把上游从 HTTP/1.1 切到 HTTP/2 后,上游活跃连接数从 1800+ 降到 87,建连带来的 CPU 开销直接下降 65%。
3. 连接缓冲区:按需分配,别浪费内存
per_connection_buffer_limit_bytes 控制每条连接的读写缓冲区,默认 1MB。API 网关场景请求体大多在几十 KB,1MB 缓冲纯属浪费内存。
listeners:
- name: listener_80
per_connection_buffer_limit_bytes: 32768 # 32KB,API 场景推荐值
实测数据:1 万活跃连接下,缓冲区从 1MB 调到 32KB,Envoy 内存占用从 12GB 降到 1.8GB;仅超过 32KB 的请求会触发流式转发,CPU 上升约 3%,API 场景完全值得。如果是文件上传/下载网关,建议调到 256KB 以上。
三、过滤器链层:裁剪执行路径,砍掉无效计算
Envoy 的 HTTP 过滤器是线性执行的,每多一个过滤器,请求就多一层处理开销。很多团队配置时一股脑堆 JWT、限流、Lua、WASM、全量日志,请求在过滤器链里绕一圈,延迟自然就上去了。
1. 过滤器排序:越早拒绝,浪费越少
过滤器顺序不是随便排的,核心原则:能拒绝请求的过滤器越靠前越好,非法请求在入口就被打掉,不会浪费后续计算资源。
通用排序规则(请求从外到内):
- 协议适配类:
proxy_protocol、original_src - 安全鉴权类:
jwt_authn、rbac(第一道关卡) - 流量治理类:
ratelimit、fault - 业务扩展类:
wasm、lua - 统计日志类:
stats、access_log - 最后必须是
router
http_filters:
- name: envoy.filters.http.jwt_authn
- name: envoy.filters.http.ratelimit
- name: envoy.filters.http.wasm
- name: envoy.filters.http.router
优化效果:我们把 JWT 鉴权从 Lua 后面移到链首,在遭遇恶意刷接口时,CPU 占用直接下降 40%——大部分非法请求在鉴权层就被拒绝了,没走到后面的业务逻辑。
2. 扩展选型:Lua 慎用,优先原生/WASM
自定义逻辑的性能差距非常大,我们压测过同一段 Header 处理逻辑,三种实现方式的延迟开销对比:
- 原生 C++ 过滤器:基准延迟 0.3ms
- WASM(C++/Rust 编译):额外开销 ~0.1ms,比基准慢 33%
- Lua:额外开销 ~0.8ms,比基准慢 167%
选型建议:
- 简单逻辑优先用原生过滤器能力解决,别上来就写扩展
- 性能敏感、逻辑固定的场景,用 WASM 实现
- 迭代频繁、逻辑复杂的场景,再用 Lua,且严格控制脚本复杂度
Lua 优化铁则:别在脚本里读 Body、别做阻塞 IO、别用全局变量(每个 worker 有独立 Lua VM),复杂计算全部下沉到业务服务。
3. 可观测性裁剪:全量日志是性能杀手
1 万 QPS 下全开访问日志,CPU 占用能涨 20% 以上,磁盘 IO 也会成为瓶颈。生产环境必须做采样,异常请求全量记录,正常请求按比例采样。
access_log:
- name: envoy.access_loggers.file
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
path: /var/log/envoy/access.log
filter:
or_filter:
filters:
# 异常请求全量记录
- response_flag_filter:
flags: [UH, UF, UO, DC]
# 正常请求 10% 采样
- runtime_filter:
runtime_key: access_log_sampling
percent:
numerator: 10
denominator: HUNDRED
Tracing 同理,生产环境设 1%~5% 采样率足够定位问题;Stats 指标用 stats_matcher 做白名单,关掉不需要的细分维度,减少内存和计算开销。
附:Envoy 性能诊断工具(优化版 Python 脚本)
调优过程中需要频繁对比指标变化,重写了一版诊断脚本,直接对接 Envoy Admin API,支持单次快照、连续监控、异常告警,不用依赖 Prometheus 就能快速定位瓶颈。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Envoy 网关性能诊断工具
功能:
1. 单次性能快照:连接数、QPS、延迟分布、连接池水位、异常告警
2. 连续监控模式:间隔采样,输出实时吞吐与延迟趋势
3. 自动识别性能瓶颈:连接池满、排队溢出、错误率超标等
用法:
单次诊断: python envoy_perf_diag.py --admin 127.0.0.1:9901
连续监控: python envoy_perf_diag.py --admin 127.0.0.1:9901 --interval 2
指定监听器: python envoy_perf_diag.py --admin 127.0.0.1:9901 --listener ingress_http
"""
import argparse
import requests
import time
import sys
from typing import Dict, List
class EnvoyPerfDiag:
def __init__(self, admin_addr: str, listener: str = "ingress_http"):
self.base_url = f"http://{admin_addr}"
self.listener = listener
self._prev_stats: Dict[str, int] = {
}
self._prev_ts: float = 0
self._session = requests.Session()
self._session.timeout = 5
def _request(self, path: str) -> dict:
"""统一请求封装,带异常处理"""
try:
resp = self._session.get(f"{self.base_url}{path}")
resp.raise_for_status()
return resp.json()
except Exception as e:
print(f"[ERROR] 请求 Envoy Admin 失败: {str(e)}")
sys.exit(1)
def fetch_stats(self) -> Dict[str, int]:
"""拉取扁平化统计指标"""
data = self._request("/stats?format=json")
stats = {
}
for item in data.get("stats", []):
if "value" in item:
stats[item["name"]] = item["value"]
return stats
def calc_delta_rate(self, curr_stats: Dict[str, int], keys: List[str]) -> Dict[str, float]:
"""计算两次采样间的速率(QPS)"""
now = time.time()
delta = now - self._prev_ts
result = {
}
if delta <= 0 or not self._prev_stats:
return result
for key in keys:
curr = curr_stats.get(key, 0)
prev = self._prev_stats.get(key, 0)
result[key] = round((curr - prev) / delta, 2)
return result
def get_latency_percentiles(self, stats: Dict[str, int]) -> Dict[str, int]:
"""提取延迟分位数据,单位毫秒"""
prefix = f"http.{self.listener}.downstream_rq_time"
percentiles = ["0.5", "0.9", "0.99", "0.999"]
result = {
}
for p in percentiles:
result[f"P{p}"] = stats.get(f"{prefix}.p{p}", 0)
return result
def get_server_info(self) -> dict:
"""获取服务基础信息"""
return self._request("/server_info")
def get_cluster_health(self) -> List[dict]:
"""获取上游集群健康状态与连接池水位"""
data = self._request("/clusters?format=json")
return data.get("cluster_statuses", [])
def check_bottlenecks(self, stats: Dict[str, int], qps: Dict[str, float]) -> List[str]:
"""自动检测性能瓶颈点,返回告警列表"""
warnings = []
prefix = f"http.{self.listener}"
# 5xx 错误率超过 1% 告警
total_qps = qps.get(f"{prefix}.downstream_rq_total", 0)
err_qps = qps.get(f"{prefix}.downstream_rq_5xx", 0)
if total_qps > 100 and err_qps / total_qps > 0.01:
err_rate = round(err_qps / total_qps * 100, 2)
warnings.append(f"5xx 错误率超标: {err_rate}%")
# 连接池溢出检测
cx_overflow = stats.get(f"{prefix}.downstream_cx_overflow", 0)
if cx_overflow > 0:
warnings.append(f"连接队列溢出累计: {cx_overflow} 次,建议调大 max_connections")
# 排队请求堆积
rq_pending = stats.get(f"{prefix}.downstream_rq_pending_overflow", 0)
if rq_pending > 0:
warnings.append(f"请求排队溢出累计: {rq_pending} 次,建议调大 max_pending_requests")
return warnings
def print_snapshot(self):
"""输出一次完整性能快照"""
curr_stats = self.fetch_stats()
prefix = f"http.{self.listener}"
print("=" * 65)
print(f"Envoy 网关性能快照 {time.strftime('%Y-%m-%d %H:%M:%S')}")
print("=" * 65)
# 基础信息
info = self.get_server_info()
print(f"版本: {info['version']} | 状态: {info['state']}")
print()
# 连接指标
print("[连接指标]")
print(f" 总连接数累计: {curr_stats.get('server.total_connections', 0)}")
print(f" 当前活跃连接: {curr_stats.get('server.active_connections', 0)}")
print(f" 连接失败累计: {curr_stats.get(f'{prefix}.downstream_cx_destroy_remote_with_active_rq', 0)}")
print()
# QPS 指标
qps_keys = [
f"{prefix}.downstream_rq_total",
f"{prefix}.downstream_rq_2xx",
f"{prefix}.downstream_rq_5xx",
]
qps_data = self.calc_delta_rate(curr_stats, qps_keys)
if qps_data:
print("[实时 QPS]")
print(f" 总请求: {qps_data.get(f'{prefix}.downstream_rq_total', 0)} req/s")
print(f" 2xx : {qps_data.get(f'{prefix}.downstream_rq_2xx', 0)} req/s")
print(f" 5xx : {qps_data.get(f'{prefix}.downstream_rq_5xx', 0)} req/s")
print()
# 延迟分布
latency = self.get_latency_percentiles(curr_stats)
print("[请求延迟 (ms)]")
for label, val in latency.items():
print(f" {label.ljust(5)}: {val}")
print()
# 上游集群状态
clusters = self.get_cluster_health()
print("[上游集群状态]")
for cluster in clusters:
name = cluster["name"]
hosts = cluster.get("host_statuses", [])
healthy = sum(1 for h in hosts if h.get("healthy", {
}).get("healthy", False))
print(f" {name}: {healthy}/{len(hosts)} 健康")
print()
# 瓶颈告警
warnings = self.check_bottlenecks(curr_stats, qps_data)
if warnings:
print("[⚠️ 性能告警]")
for w in warnings:
print(f" - {w}")
else:
print("[✅ 无明显性能瓶颈]")
# 更新历史状态
self._prev_stats = curr_stats
self._prev_ts = time.time()
print("=" * 65)
print()
def run_continuous(self, interval: int):
"""连续监控模式"""
print(f"启动连续监控,采样间隔 {interval} 秒,Ctrl+C 停止\n")
# 先采一次作为基线
self._prev_stats = self.fetch_stats()
self._prev_ts = time.time()
time.sleep(interval)
try:
while True:
self.print_snapshot()
time.sleep(interval)
except KeyboardInterrupt:
print("\n已停止监控")
def main():
parser = argparse.ArgumentParser(description="Envoy 网关性能诊断工具")
parser.add_argument("--admin", default="127.0.0.1:9901", help="Envoy Admin 地址")
parser.add_argument("--listener", default="ingress_http", help="HTTP 监听器名称")
parser.add_argument("--interval", type=int, default=0, help="连续监控间隔(秒),0表示单次诊断")
args = parser.parse_args()
diag = EnvoyPerfDiag(args.admin, args.listener)
if args.interval <= 0:
diag.print_snapshot()
else:
diag.run_continuous(args.interval)
if __name__ == "__main__":
main()
脚本使用很简单,默认对接本地 9901 管理端口,单次执行直接看全量快照;加 --interval 参数就能持续刷新,调优的时候改完参数马上能看到数值变化。脚本内置了瓶颈检测,连接池溢出、错误率超标都会自动提示,不用自己对着指标逐个排查。
最后:调优不是一劳永逸
整个调优做下来最深的体会是:没有万能的最优参数,只有适配业务场景的配置。小请求高并发场景要收缓冲、开长连接、压短过滤器链;大文件传输场景要扩缓冲、调窗口、保连接稳定。
三层优化的优先级非常明确:内核调优 > 连接管理 > 过滤器链。底层参数的天花板决定了上层能跑到的上限,地基没打牢,上层怎么调都是杯水车薪。
建议大家调优前先跑一轮基准测试拿到基线,每改一组参数就复测一次,用数据说话,别凭感觉调。