一、 节点优先级
在 Envoy 的负载均衡中,节点优先级是一个核心概念,它允许你根据不同的标准对上游集群中的节点进行分组和排序。 DEFAULT 和 HIGH 是 Envoy 内置的优先级,而数字(通常是 0、1、2 等)则是用来表示这些优先级的。它们之间的区别可以这样理解:
1.1 Envoy 负载均衡中的节点优先级
Envoy 采用分层结构来实现负载均衡,其中优先级就是一层。它允许你在将流量分发到上游节点之前,先按照优先级将这些节点分组。
- 流量分发逻辑:当 Envoy 代理接收到请求时,它会首先尝试将请求路由到优先级最高的节点组。只有当最高优先级的节点组健康状况不佳或容量不足(例如,因为连接断开或健康检查失败),Envoy 才会溢出(failover)到下一个较低优先级的节点组。
这个机制非常有用,比如,你可以将主要流量发送到离你最近、延迟最低的数据中心,而将另一个较远的数据中心设置为较低的优先级,作为灾备或溢出用途。
1.2 DEFAULT 和 HIGH 的区别
DEFAULT 和 HIGH 实际上是 Envoy 内部为了简化配置而定义的命名常量,它们背后对应着数字。
- HIGH:这个优先级在 Envoy 内部被映射为数字 0。数字越小,优先级越高。通常,你将 HIGH 优先级用于承载绝大部分正常流量的主要节点。比如,一个集群内所有正常工作的、健康的节点都可以被分配这个优先级。
- DEFAULT:这个优先级在 Envoy 内部被映射为数字 1。它的优先级低于 HIGH。通常用于溢出节点或备用节点。比如,一个位于不同区域或云服务提供商的灾备数据中心就可以被设置为这个优先级。只有当所有 HIGH 优先级的节点都不可用时,流量才会切换到这里的节点。
1.3 为什么 Envoy 使用数字?
Envoy 使用数字来表示优先级,这是一个更通用的、可扩展的设计。
- 层级扩展性:虽然 Envoy 默认只提供了 HIGH 和 DEFAULT 两个命名优先级,但在实际应用中,你可能需要更多层次的优先级。比如,你可以有三个数据中心,一个主中心,一个本地备用中心,以及一个远程灾备中心。你可以分别用 0、1、2 来表示这三个优先级,数字越小优先级越高。这种数字化的表示方式提供了更大的灵活性。
- 配置的通用性:在 Envoy 的配置(如
Cluster配置中的lb_endpoints)中,你可以直接使用数字来指定优先级,这使得配置更加直观和可编程。priority: 0明确表示最高优先级,priority: 1表示次高优先级,以此类推。
总结一下:
| 特性 | HIGH / DEFAULT | 数字 (0, 1, 2...) |
|---|---|---|
| 本质 | 命名常量 | 实际的优先级值 |
| 对应关系 | HIGH 对应数字 0<br>DEFAULT 对应数字 1 | 直接表示优先级,数字越小,优先级越高 |
| 使用场景 | 简化配置,通常用于表示最常用的两个优先级层级 | 提供更细粒度的控制,用于多层次的优先级设置 |
| 优先级 | HIGH > DEFAULT | 0 > 1 > 2 ... |
在实际配置中,可以根据需求选择使用 HIGH、DEFAULT 或直接使用数字。如果只需要简单的双层优先级,使用命名常量会更清晰。如果需要更复杂的、多层级的优先级策略,直接使用数字则更加灵活。
二、 优先级调度
2.1 优先级调度(Priority Scheduling)
优先级调度是 Envoy 负载均衡的核心机制,它利用之前提到的节点优先级概念来智能地分发流量。简单来说,它的工作方式是:
- 首选最高优先级:Envoy 总是将新请求优先发送到优先级最高(数字最小,例如 0)的节点组。
- 实时健康检查:Envoy 会持续监控这些最高优先级节点组中所有节点的健康状况(通过健康检查或连接状态)。
- 动态流量分配:只要最高优先级的节点组中有足够多的健康节点来处理请求,所有流量都会被限制在这个优先级内。
- 溢出(Failover):只有当最高优先级的节点组因为健康检查失败、连接断开或容量不足而无法处理更多请求时,Envoy 才会将部分或全部流量“溢出”或“故障转移”到下一个较低优先级的节点组。
这个过程是动态且无缝的。如果之前故障的最高优先级节点组恢复了健康,Envoy 会重新将流量切回这个优先级,确保流量总是流向“最优”的节点。
配置示例
static_resources:
clusters:
- name: example_service
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: example_service
endpoints:
- priority: 0
lb_endpoints:
- endpoint:
address:
socket_address:
address: primary.example.com
port_value: 80
- priority: 1
lb_endpoints:
- endpoint:
address:
socket_address:
address: secondary.example.com
port_value: 80
circuit_breakers:
thresholds:
- priority: DEFAULT
max_connections: 100
max_pending_requests: 1000
max_requests: 5000
- priority: HIGH
max_connections: 200
max_pending_requests: 2000
max_requests: 10000
三、 优先级调度的工作原理
3.1 基本工作原理
- 默认优先级调度:Envoy首先会尝试将请求路由到默认优先级(priority: 0)组中的节点。
- 次高优先级调度:如果默认优先级组中的所有节点都不可用或过载,Envoy会自动将请求转移到次高优先级(priority: 1)组中的节点。
- 健康检查:通过健康检查,Envoy持续监控各个优先级组中的节点状态,确保在默认优先级组恢复健康后,流量可以回流到默认优先级组。
3.2 深层次工作原理
Envoy是一种智能的网络代理,常用于管理和分配应用程序之间的网络流量,就像一个交通指挥员,确保数据流在不同服务器之间顺畅地流动。
1. LocalityLbEndpoints(位置端点)
LocalityLbEndpoints可以理解为一组在相同地理位置(比如同一个城市或数据中心)中的服务器。每组服务器都有相同的优先级和负载均衡权重。
2. Locality(位置)
- 地域(region):一个大区域,比如中国。
- 区域(zone):一个较小的区域,比如北京。
- 子区域(sub_zone):更小的区域,比如北京的某个具体机房。
3. Load Balancing Weight(负载均衡权重)
负载均衡权重就像每组服务器的工作能力分数。权重越高,这组服务器分到的工作(流量)就越多。比如,北京有两组服务器A和B,如果A的权重是5,B的权重是3,那么A会分到更多的工作。
4. Priority(优先级)
优先级类似于紧急处理顺序。优先级为0的是最优先处理的服务器,只有当这些服务器全部出问题时,才会去用优先级为1的服务器。
举个例子:
假设你有一个网站,有两个数据中心,一个在北京,一个在上海,每个数据中心有几个服务器。北京的数据中心优先级是0,上海的数据中心优先级是1。这样,当所有服务器都正常工作时,流量会优先发送到北京的数据中心。当北京的数据中心不可用时,流量会自动转移到上海的数据中心。
5. 超配因子(Overprovisioning Factor)
超配因子是为了在部分服务器故障时,仍将更多的流量分配给剩余健康的服务器。例如,如果某组服务器的超配因子为默认值1.4,当其中20%的服务器故障时,仍会保留所有流量在当前组,只有当健康服务器少于72%时,才会转移部分流量到次优先级的服务器。或者看成 健康服务器占比80% ✖️ 1.4 = 112% ,健康评分大于 100%,流量不会溢出当前组。
要计算当第一个region中某个节点不可用时,超配因子为1.4的情况下,应该如何迁移流量,我们需要首先计算健康节点的比例以及根据公式计算转移的流量。
配置示例:
overprovisioning_factor: 140
情况描述:
第一个region有5个节点,当前2个节点不可用,即3个节点健康。
第二个region有3个节点,全部健康。
超配因子为1.4。
计算步骤:
- 计算健康节点比例:
第一个region的健康节点比例 = 健康节点数 / 总节点数 = 3 / 5 ≈ 0.6(即60%)。 小于 72%,流量会溢出到第二个region。 - 计算健康评分:
健康评分 = 健康节点比例 ✖️ 超配因子 = 0.6 * 1.4 = 84% 。健康评分小于 100%,流量会溢出到第二个region。 - 确定是否需要转移流量:
结合健康节点比例和健康评分的两种对比结果来判断,流量都需要转移。 - 计算转移的流量:
转移的流量 = 100% - 健康节点比例 ✖️ 超配因子 = 1 - 0.6 * 1.4 = 16% .
结论: 当第一个region中,5节点有2个不可用时,有 16%的流量需要转移。
关于72%值的计算
72%的计算是基于健康评分和超配因子的关系。我们可以通过以下步骤来详细解释这个计算过程:
计算逻辑
健康评分 = 健康节点比例 × 超配因子
当健康节点比例达到某个临界值时,如果超配因子乘以这个比例的结果刚好等于100%,那么这个临界值就是我们需要找的72%。换句话说,我们要找到健康节点比例的一个值,使得健康评分等于100%。
计算步骤
- 设定健康评分的临界值为100%
我们需要找到一个健康节点比例,使得健康评分等于100%。 - 使用健康评分公式
健康评分 = 健康节点比例 × 超配因子 - 代入健康评分为100%
100% = 健康节点比例 × 1.4 - 解方程求健康节点比例
健康节点比例 = 100% / 1.4 = 1 / 1.4 ≈ 0.7143(即71.43%)
为了简化计算,通常将其四舍五入为72%。
6. Panic阈值(Panic Threshold)
在 Envoy 负载均衡中,恐慌阈值(Panic Threshold) 是一个至关重要的参数,它决定了当上游集群中的健康节点数量低于某个百分比时,负载均衡的行为将如何改变。
正常情况下,Envoy 的负载均衡器只会将流量分发给健康的上游节点。如果一个节点健康检查失败,它会被移出负载均衡池,不再接收流量。这种机制非常有效,可以避免请求发送到故障节点。
但是,如果健康节点数量持续减少,这个机制本身可能反而会成为一个问题。想象一下,如果一个集群中只剩下极少数节点是健康的,而 Envoy 仍然只将所有流量都集中到它们身上,那么这些仅存的健康节点很可能会被流量压垮,导致整个集群彻底瘫痪。
为了避免这种情况,Envoy 引入了恐慌模式(Panic Mode)和恐慌阈值。
恐慌模式是如何工作的
- 设置阈值:你为集群设置一个恐慌阈值,通常是一个百分比(例如 50%)。
- 监控健康节点:Envoy 会持续计算集群中健康节点占总节点数的比例。
- 进入恐慌模式:当健康节点的比例低于这个恐慌阈值时,Envoy 会进入“恐慌模式”。
- 负载均衡行为改变:在恐慌模式下,负载均衡器会忽略所有节点的健康状态,将流量均匀地分发给集群中的所有节点(包括那些被标记为不健康的节点)。
为什么需要恐慌模式?
进入恐慌模式是一种“孤注一掷”的策略,其核心思想是:
- 避免集群完全崩溃:与其让少数健康的节点被压垮,不如将请求分散到所有节点上。尽管部分不健康的节点可能会返回错误(例如 503 错误),但这样做至少给它们一个机会来处理请求,或者让请求均匀地失败,而不是让所有请求都集中在一个点上。
- 提高整体可用性:通过在所有节点之间“赌一把”,即使有部分请求失败,整个集群作为整体依然可能提供部分服务,而不是完全不可用。
举个例子:
假设你有一个包含 10 个节点的集群,恐慌阈值设置为 50%。
- 正常情况:如果 8 个节点健康(80%),2 个节点不健康,Envoy 只会将流量发送给这 8 个健康节点。
- 进入恐慌模式:如果又有 4 个节点变得不健康,现在只有 4 个健康节点了(40%),这低于 50% 的阈值。
- 结果:Envoy 进入恐慌模式。现在,所有 10 个节点都会接收流量,包括那 6 个不健康的节点。
重要提示:恐慌模式下的负载均衡是基于总节点数的,这意味着不健康节点和健康节点被一视同仁,流量是均匀分配的。
总结:恐慌阈值是 Envoy 在面临大规模上游节点故障时的一种自救机制,它通过放宽对节点健康的要求,将请求分散,从而防止仅存的健康节点被流量压垮,并尽可能地保持服务的可用性。
四、 监控和调试
Envoy 提供了丰富的监控指标,让我们能实时洞察调度策略的实际效果。通过访问 Envoy 的 /admin 接口(通常是 http://localhost:9901/stats),你可以获得大量宝贵的数据:
cluster.<cluster_name>.upstream_rq_total: 这是一个总览指标,它告诉你整个集群收到了多少请求,是衡量整体流量负载的基础。cluster.<cluster_name>.priority_<priority>.upstream_rq_total: 这个指标更为精细,它会告诉你特定优先级(如priority_0或priority_1)的节点组分别处理了多少请求。通过对比这些数据,你可以验证流量是否如你所想,优先流向了最高优先级的节点。cluster.<cluster_name>.priority_<priority>.upstream_rq_pending_active: 这个指标非常关键,它揭示了当前正在等待处理的请求数。如果某个优先级下的pending_active数量持续增高,这可能意味着该优先级节点已经达到性能瓶颈,是时候考虑扩容或调整负载均衡策略了。
这些详尽的统计数据,配合优先级调度策略,让我们可以更灵活、更可靠地管理上游集群。无论是为了提升系统容错能力,还是为了优化性能,Envoy 都提供了强大的工具集,帮助我们从容应对各种复杂的流量挑战。
五、节点优先级及优先级调度案例

环境说明
十一个Service:
- envoy:Front Proxy,地址为172.31.4.2
- webserver01:第一个后端服务
- webserver01-sidecar:第一个后端服务的Sidecar Proxy,地址为172.31.4.3, 别名为red和webservice1
- webserver02:第二个后端服务
- webserver02-sidecar:第一个后端服务的Sidecar Proxy,地址为172.31.4.4, 别名为blue和webservice1
- webserver03:第三个后端服务
- webserver03-sidecar:第一个后端服务的Sidecar Proxy,地址为172.31.4.5, 别名为green和webservice1
- webserver04:第四个后端服务
- webserver04-sidecar:第四个后端服务的Sidecar Proxy,地址为172.31.4.6, 别名为gray和webservice2
- webserver05:第五个后端服务
- webserver05-sidecar:第五个后端服务的Sidecar Proxy,地址为172.31.4.7, 别名为black和webservice2
启动配置文件
.
├── docker-compose.yaml
├── envoy-sidecar-proxy.yaml
└── front-envoy.yaml
# cat docker-compose.yaml
services:
front-envoy:
image: envoyproxy/envoy:v1.30.1
environment:
- ENVOY_UID=0
- ENVOY_GID=0
volumes:
- ./front-envoy.yaml:/etc/envoy/envoy.yaml
networks:
envoymesh:
ipv4_address: 172.31.4.2
aliases:
- front-proxy
expose:
# Expose ports 80 (for general traffic) and 9901 (for the admin server)
- "80"
- "9901"
webserver01-sidecar:
image: envoyproxy/envoy:v1.30.1
environment:
- ENVOY_UID=0
- ENVOY_GID=0
volumes:
- ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml
hostname: red
networks:
envoymesh:
ipv4_address: 172.31.4.3
aliases:
- webservice1
- red
webserver01:
image: demoapp:v1.0
environment:
- ENVOY_UID=0
- ENVOY_GID=0
- PORT=8080
- HOST=127.0.0.1
network_mode: "service:webserver01-sidecar"
depends_on:
- webserver01-sidecar
webserver02-sidecar:
image: envoyproxy/envoy:v1.30.1
environment:
- ENVOY_UID=0
- ENVOY_GID=0
volumes:
- ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml
hostname: blue
networks:
envoymesh:
ipv4_address: 172.31.4.4
aliases:
- webservice1
- blue
webserver02:
image: demoapp:v1.0
environment:
- ENVOY_UID=0
- ENVOY_GID=0
- PORT=8080
- HOST=127.0.0.1
network_mode: "service:webserver02-sidecar"
depends_on:
- webserver02-sidecar
webserver03-sidecar:
image: envoyproxy/envoy:v1.30.1
environment:
- ENVOY_UID=0
- ENVOY_GID=0
volumes:
- ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml
hostname: green
networks:
envoymesh:
ipv4_address: 172.31.4.5
aliases:
- webservice1
- green
webserver03:
image: demoapp:v1.0
environment:
- ENVOY_UID=0
- ENVOY_GID=0
- PORT=8080
- HOST=127.0.0.1
network_mode: "service:webserver03-sidecar"
depends_on:
- webserver03-sidecar
webserver04-sidecar:
image: envoyproxy/envoy:v1.30.1
environment:
- ENVOY_UID=0
- ENVOY_GID=0
volumes:
- ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml
hostname: gray
networks:
envoymesh:
ipv4_address: 172.31.4.6
aliases:
- webservice2
- gray
webserver04:
image: demoapp:v1.0
environment:
- ENVOY_UID=0
- ENVOY_GID=0
- PORT=8080
- HOST=127.0.0.1
network_mode: "service:webserver04-sidecar"
depends_on:
- webserver04-sidecar
webserver05-sidecar:
image: envoyproxy/envoy:v1.30.1
environment:
- ENVOY_UID=0
- ENVOY_GID=0
volumes:
- ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml
hostname: black
networks:
envoymesh:
ipv4_address: 172.31.4.7
aliases:
- webservice2
- black
webserver05:
image: demoapp:v1.0
environment:
- ENVOY_UID=0
- ENVOY_GID=0
- PORT=8080
- HOST=127.0.0.1
network_mode: "service:webserver05-sidecar"
depends_on:
- webserver05-sidecar
networks:
envoymesh:
driver: bridge
ipam:
config:
- subnet: 172.31.4.0/24
# cat envoy-sidecar-proxy.yaml
admin:
profile_path: /tmp/envoy.prof
access_log_path: /tmp/admin_access.log
address:
socket_address:
address: 0.0.0.0
port_value: 9901
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 80 }
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
codec_type: AUTO
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route: { cluster: local_cluster }
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: local_cluster
connect_timeout: 0.25s
type: STATIC
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: local_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: 127.0.0.1, port_value: 8080 }
# cat front-envoy.yaml
admin:
access_log_path: "/dev/null"
address:
socket_address:
address: 0.0.0.0
port_value: 9901
static_resources:
listeners:
- address:
socket_address:
address: 0.0.0.0
port_value: 80
name: listener_http
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
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: backend
domains:
- "*"
routes:
- match:
prefix: "/"
route:
cluster: webcluster1
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: webcluster1
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
http2_protocol_options: {}
load_assignment:
cluster_name: webcluster1
policy:
overprovisioning_factor: 140 # 超配因子
endpoints:
- locality:
region: cn-north-1
priority: 0
lb_endpoints:
- endpoint:
address:
socket_address:
address: webservice1
port_value: 80
- locality:
region: cn-north-2
priority: 1
lb_endpoints:
- endpoint:
address:
socket_address:
address: webservice2
port_value: 80
health_checks:
- timeout: 5s
interval: 10s
unhealthy_threshold: 2
healthy_threshold: 1
http_health_check:
path: /livez
expected_statuses:
start: 200
end: 399
启动并测试
# docker-compose up -d
[+] Running 12/12
✔ Network envoy_cluster_priority_envoymesh Created 0.0s
✔ Container envoy_cluster_priority-webserver02-sidecar-1 Started 0.3s
✔ Container envoy_cluster_priority-webserver05-sidecar-1 Started 0.3s
✔ Container envoy_cluster_priority-webserver04-sidecar-1 Started 0.4s
✔ Container envoy_cluster_priority-front-envoy-1 Started 0.3s
✔ Container envoy_cluster_priority-webserver01-sidecar-1 Started 0.4s
✔ Container envoy_cluster_priority-webserver03-sidecar-1 Started 0.4s
✔ Container envoy_cluster_priority-webserver05-1 Started 0.5s
✔ Container envoy_cluster_priority-webserver03-1 Started 0.6s
✔ Container envoy_cluster_priority-webserver04-1 Started 0.6s
✔ Container envoy_cluster_priority-webserver02-1 Started 0.5s
✔ Container envoy_cluster_priority-webserver01-1 Started 0.6s
持续请求服务,可发现,请求均被调度至优先级为0的webservice1相关的后端端点之上;
while true; do curl 172.31.4.2; sleep .5; done
# 等确定服务的调度结果后,另启一个终端,修改webservice1中任何一个后端端点的/livez响应为非"OK"值,例如,修改第一个后端端点;
curl -X POST -d 'livez=FAIL' http://172.31.4.3/livez
# 而后通过请求的响应结果可发现,因过载因子为1.4,客户端的请求仍然始终只发往webservice1的后端端点blue和green之上;
# 等确定服务的调度结果后,再修改其中任何一个服务的/livez响应为非"OK"值,例如,修改第一个后端端点;
curl -X POST -d 'livez=FAIL' http://172.31.4.4/livez
# 请求中,可以看出第一个端点因响应5xx的响应码,每次被加回之后,会再次弹出,除非使用类似如下命令修改为正常响应结果;
curl -X POST -d 'livez=OK' http://172.31.4.3/livez
# 而后通过请求的响应结果可发现,因过载因子为1.4,优先级为0的webserver1已然无法锁住所有的客户端请求,于是,客户端的请求的部分流量将被转发至webservice2的端点之上;
评论区