侧边栏壁纸
  • 累计撰写 28 篇文章
  • 累计创建 23 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Envoy 集群管理 Upstream Cluster负载均衡器策略及案例

zhanjie.me
2025-09-16 / 0 评论 / 0 点赞 / 10 阅读 / 0 字

一、 负载均衡器策略

在Envoy中,负载均衡(Load Balancing)策略用于决定如何在多个上游(upstream)服务实例之间分配传入的请求。Envoy支持多种负载均衡策略,每种策略都有其独特的行为和应用场景。
以下是Envoy支持的一些主要负载均衡策略:

1. ROUND_ROBIN(轮询)

Round Robin 策略按顺序循环地将请求分配给每个上游实例。它是一种简单且常见的负载均衡策略,适用于请求量均匀且服务实例性能相似的场景。
配置示例:
lb_policy: ROUND_ROBIN

2. LEAST_REQUEST(最少请求)

Least Request 策略将请求分配给当前处理请求数量最少的上游实例。这种策略适用于负载分布不均匀的场景,有助于平衡负载并提高整体性能。
配置示例:
lb_policy: LEAST_REQUEST

3. RANDOM(随机)

Random 策略随机选择一个上游实例来处理请求。这种策略适用于简单的负载均衡需求,不需要任何复杂计算。
配置示例:
lb_policy: RANDOM

4. RING_HASH(环哈希)

Ring Hash策略使用一致性哈希将请求分配给上游实例,适用于需要会话粘性或状态持久性的场景。该策略常用于缓存服务,以确保相同的请求始终路由到相同的上游实例。
配置示例:
lb_policy: RING_HASH

5. MAGLEV

Maglev策略也是一种基于哈希的负载均衡算法,设计用于在更改上游集群时提供较低的请求重新分配率。这有助于实现更平滑的负载转移。
配置示例:
lb_policy: MAGLEV

Maglev负载均衡策略的特点
  1. 低重新分配率:在上游节点发生变化(增加或减少)时,Maglev算法能最大限度地减少请求的重新分配。
  2. 高效率:Maglev能够快速计算目标节点,从而实现高性能的请求分配。
  3. 一致性哈希:Maglev采用一致性哈希技术,确保相同的请求尽量路由到相同的上游节点。

以下是Envoy中使用Maglev负载均衡策略的配置示例:

在这个示例中,我们配置了一个名为 maglev_service_cluster的集群,并指定其负载均衡策略为 MAGLEV。Envoy会根据Maglev算法在 service1.example.com和 service2.example.com之间分配流量。

static_resources:
  clusters:
  - name: maglev_service_cluster
    connect_timeout: 0.25s
    type: STRICT_DNS
    lb_policy: MAGLEV
    load_assignment:
      cluster_name: maglev_service_cluster
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: service1.example.com
                port_value: 80
        - endpoint:
            address:
              socket_address:
                address: service2.example.com
                port_value: 80
Maglev算法的工作原理

Maglev算法的核心在于创建一个哈希环并将请求分配到环上的节点。它通过以下步骤工作:

  1. 初始化哈希环:在启动时,根据上游节点的数量和配置的哈希函数初始化一个哈希环。
  2. 哈希映射:每个上游节点在环上占据多个位置,通过哈希函数将这些位置映射到节点。
  3. 请求分配:对于每个传入的请求,计算其哈希值,并在环上找到最近的节点位置,将请求分配给该节点。

Maglev的关键在于减少节点变更时的请求重新分配。当有节点加入或离开时,只会重新分配受影响的部分请求,而不是全部请求。

Maglev负载均衡策略的使用场景

Maglev负载均衡策略适用于以下场景:

  1. 大规模分布式系统:需要在多个数据中心或区域之间均匀分配负载,且节点数量较多。
  2. 高稳定性要求:需要在节点变更时保持较低的请求重新分配率,以减少服务中断。
  3. 一致性需求:需要确保相同的请求尽量路由到相同的上游节点,适用于缓存服务、会话保持等场景。

Maglev负载均衡策略通过优化一致性哈希算法,提供了高效、低延迟、低重新分配率的负载均衡方案。它在大规模分布式系统和高稳定性、高一致性要求的场景中表现尤为出色。在Envoy中,通过简单的配置即可使用Maglev策略,实现稳定高效的请求分配。

6. WEIGHTED_LEAST_REQUEST(加权最少请求)

Weighted Least Request 策略是在最少请求策略的基础上增加了权重因素。上游实例可以根据其权重和当前负载来决定请求的分配。适用于实例性能不同的场景,通过权重调整分配比例。
配置示例:

lb_policy: LEAST_REQUEST
least_request_lb_config:
  choice_count: 2
  active_request_bias:
    default_value: 1.0
    runtime_key: "new_active_request_bias"
7. ORIGINAL_DST(原始目标)

Original Destination 策略将请求直接路由到原始目标地址,而不进行任何负载均衡。这种策略适用于需要保留原始目标地址的场景。
配置示例:

lb_policy: ORIGINAL_DST_LB
original_dst_lb_config:
  use_http_header: true
8. CLUSTER_PROVIDED(集群提供)

Cluster Provided 策略允许集群自己决定负载均衡策略,通常用于集成自定义负载均衡逻辑的场景。
配置示例:
lb_policy: CLUSTER_PROVIDED

9. LOAD_BALANCING_POLICY(动态负载均衡策略)

Envoy还支持通过 LoadBalancingPolicy 配置动态选择负载均衡策略,允许在运行时调整策略。
配置示例:

load_balancing_policy:
  policies:
    - policy:
        typed_extension_config:
          name: envoy.load_balancing_policies.round_robin
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.load_balancing_policies.round_robin.v3.RoundRobin

完整示例配置:

static_resources:
  clusters:
  - name: example_service
    connect_timeout: 0.25s
    type: STRICT_DNS
    lb_policy: LEAST_REQUEST
    load_assignment:
      cluster_name: example_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: service.example.com
                port_value: 80
    least_request_lb_config:
      choice_count: 2
      active_request_bias:
        default_value: 1.0
        runtime_key: "new_active_request_bias"

Envoy提供了多种负载均衡策略,以满足不同的应用场景和需求。从简单的轮询和随机策略,到复杂的哈希和最少请求策略,每种策略都有其特定的优势和适用场景。通过灵活选择和配置负载均衡策略,Envoy能够优化请求分配,提高服务的可靠性和性能。


二、 全局负载均衡及分布式负载均衡

在Envoy中,全局负载均衡和分布式负载均衡是两种不同的负载均衡策略,它们在工作机制和适用场景上有所不同。

2.1 全局负载均衡(Global Load Balancing)

全局负载均衡是指在整个服务网格中实现跨多个区域或数据中心的负载均衡。它关注的是如何在多个地理位置上的实例之间分配流量,以实现高可用性和灾备能力。
特点:

  • 跨区域:流量可以在不同的区域或数据中心之间分配。
  • 高可用性:在某个区域或数据中心出现故障时,可以将流量分配到其他区域。
  • 灾备能力:确保即使在大规模故障发生时,服务仍然可用。

2.2 分布式负载均衡(Local Load Balancing)

分布式负载均衡是指在本地服务实例之间进行流量分配。它主要关注的是如何在单个区域或数据中心内部的多个实例之间均匀分配流量,以优化资源利用和性能。
特点:

  • 本地性:流量在本地实例之间分配。
  • 低延迟:由于负载均衡发生在同一区域或数据中心,网络延迟较低。
  • 资源优化:在本地实例之间均匀分配负载,优化资源利用。

2.3 Envoy的负载均衡策略分类

Envoy提供的负载均衡策略可以分为全局负载均衡和分布式负载均衡。以下是对上述负载均衡算法的分类:

全局负载均衡策略
  • RING_HASH:环哈希策略,适用于需要会话粘性或跨数据中心的一致性哈希。
  • MAGLEV:Maglev负载均衡策略,设计用于低重新分配率的跨区域流量分配。
分布式负载均衡策略
  • ROUND_ROBIN:轮询策略,适用于简单的本地负载均衡。
  • LEAST_REQUEST:最少请求策略,适用于需要优化本地负载的场景。
  • RANDOM:随机策略,适用于简单且均匀分布的本地负载均衡。
  • WEIGHTED_LEAST_REQUEST:加权最少请求策略,结合权重和最少请求进行本地负载均衡。
  • ORIGINAL_DST:原始目标策略,保持请求的原始目标地址,用于本地的特殊场景。
  • CLUSTER_PROVIDED:集群提供策略,允许集群自行决定本地负载均衡逻辑。
具体示例与理解
全局负载均衡示例

假设有一个服务需要在多个区域进行负载均衡,可以使用RING_HASH策略:

static_resources:
  clusters:
  - name: global_service
    connect_timeout: 0.25s
    type: STRICT_DNS
    lb_policy: RING_HASH
    load_assignment:
      cluster_name: global_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: global-service-region1.example.com
                port_value: 80
        - endpoint:
            address:
              socket_address:
                address: global-service-region2.example.com
                port_value: 80
分布式负载均衡示例

对于在单个数据中心内进行负载均衡,可以使用LEAST_REQUEST策略:

static_resources:
  clusters:
  - name: local_service
    connect_timeout: 0.25s
    type: STRICT_DNS
    lb_policy: LEAST_REQUEST
    load_assignment:
      cluster_name: local_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: local-service.example.com
                port_value: 80
  • 全局负载均衡关注跨区域或数据中心的流量分配,常用的策略有 RING_HASHMAGLEV
  • 分布式负载均衡关注本地实例之间的流量分配,常用的策略有 ROUND_ROBINLEAST_REQUESTRANDOM 等。

通过选择合适的负载均衡策略,Envoy能够实现高效的流量分配,满足不同的业务需求和性能要求。在实际应用中,可以根据具体的场景和需求,灵活配置和组合这些负载均衡策略。


三、Envoy Upstream Cluster负载均衡案例

3.1 加权最小请求案例

环境说明:
七个Service:

  • envoy:Front Proxy,地址为172.31.2.2
  • webserver01:第一个后端服务
  • webserver01-sidecar:第一个后端服务的Sidecar Proxy,地址为172.31.2.3
  • webserver02:第二个后端服务
  • webserver02-sidecar:第二个后端服务的Sidecar Proxy,地址为172.31.2.4
  • webserver03:第三个后端服务
  • webserver03-sidecar:第三个后端服务的Sidecar Proxy,地址为172.31.2.5

测试脚本:send-request.sh 172.31.2.2 # 发起服务请求,并根据结果中统计的各后端端点的响应大体比例,判定其是否能够大体符合加权最少连接的调度机制;

测试脚本及启动配置
.
├── docker-compose.yaml           # 容器编排启动配置文件,使用 docker-compose up -d ; docker-compose down 命令启动/关闭
├── envoy-sidecar-proxy.yaml      # 每个后端服务的Sidecar Proxy的envoy配置
├── front-envoy.yaml              # 前置代理 envoy 的配置,其中定义了加权最小请求策略
└── send-request.sh               # 测试脚本
# cat docker-compose.yaml
services:
  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.2.2
        aliases:
        - front-proxy
    depends_on:
    - webserver01-sidecar
    - webserver02-sidecar
    - webserver03-sidecar

  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.2.3
        aliases:
        - myservice
        - 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.2.4
        aliases:
        - myservice
        - 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.2.5
        aliases:
        - myservice
        - 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

networks:
  envoymesh:
    driver: bridge
    ipam:
      config:
        - subnet: 172.31.2.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:
  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: webservice
              domains: ["*"]
              routes:
              - match: { prefix: "/" }
                route: { cluster: web_cluster_01 }
          http_filters:
          - name: envoy.filters.http.router
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
  clusters:
  - name: web_cluster_01
    connect_timeout: 0.25s
    type: STRICT_DNS
    lb_policy: LEAST_REQUEST
    load_assignment:
      cluster_name: web_cluster_01
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: red
                port_value: 80
          load_balancing_weight: 1
        - endpoint:
            address:
              socket_address:
                address: blue
                port_value: 80
          load_balancing_weight: 3
        - endpoint:
            address:
              socket_address:
                address: green
                port_value: 80
          load_balancing_weight: 5

这个Shell脚本用于向一个指定的主机发送300个HTTP请求,并根据响应的内容统计不同服务端点(red、blue、green)的响应次数。

# cat send-request.sh
#!/bin/bash
declare -i red=0
declare -i blue=0
declare -i green=0

#interval="0.1"
counts=300

echo "Send 300 requests, and print the result. This will take a while."
echo ""
echo "Weight of all endpoints:"
echo "Red:Blue:Green = 1:3:5"

for ((i=1; i<=${counts}; i++)); do
        if curl -s http://$1/hostname | grep "red" &> /dev/null; then
                # $1 is the host address of the front-envoy.
                red=$[$red+1]
        elif curl -s http://$1/hostname | grep "blue" &> /dev/null; then
                blue=$[$blue+1]
        else
                green=$[$green+1]
        fi
#       sleep $interval
done

echo ""
echo "Response from:"
echo "Red:Blue:Green = $red:$blue:$green"
启动并测试
# docker-compose up -d
[+] Running 8/8
 ✔ Network envoy_cluster_lb_least_request_envoymesh                Created                  0.0s
 ✔ Container envoy_cluster_lb_least_request-webserver02-sidecar-1  Created                  0.0s
 ✔ Container envoy_cluster_lb_least_request-webserver03-sidecar-1  Created                  0.0s
 ✔ Container envoy_cluster_lb_least_request-webserver01-sidecar-1  Created                  0.0s
 ✔ Container envoy_cluster_lb_least_request-webserver02-1          Created                  0.0s
 ✔ Container envoy_cluster_lb_least_request-webserver03-1          Created                  0.0s
 ✔ Container envoy_cluster_lb_least_request-webserver01-1          Created                  0.0s
 ✔ Container envoy_cluster_lb_least_request-envoy-1                Created                  0.0s
Attaching to envoy-1, webserver01-1, webserver01-sidecar-1, webserver02-1, webserver02-sidecar-1, webserver03-1, webserver03-sidecar-1
# chmod +x send-request.sh
# ./send-request.sh 172.31.2.2
Send 300 requests, and print the result. This will take a while.

Weight of all endpoints:
Red:Blue:Green = 1:3:5

Response from:
Red:Blue:Green = 32:85:183

3.2 Ring Hash算法案例

参照上述配置,仅对 front-envoy.yamlsend-request.sh 文件做出修改,其中根据 User Agent 做hash计算分配负载

测试脚本及启动配置
# cat front-envoy.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: webservice
              domains: ["*"]
              routes:
              - match: { prefix: "/" }
                route:
                  cluster: web_cluster_01
                  hash_policy:
                  # - connection_properties:
                  #     source_ip: true
                  - header:
                      header_name: User-Agent
          http_filters:
          - name: envoy.filters.http.router
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

  clusters:
  - name: web_cluster_01
    connect_timeout: 0.5s
    type: STRICT_DNS
    lb_policy: RING_HASH
    ring_hash_lb_config:
      maximum_ring_size: 1048576
      minimum_ring_size: 512
    load_assignment:
      cluster_name: web_cluster_01
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: myservice
                port_value: 80
    health_checks:
    - timeout: 5s
      interval: 10s
      unhealthy_threshold: 2
      healthy_threshold: 2
      http_health_check:
        path: /livez
        expected_statuses:
          start: 200
          end: 399
# cat send-request.sh
#!/bin/bash
declare -i red=0
declare -i blue=0
declare -i green=0

interval="0.1"
counts=200

echo "Send 300 requests, and print the result. This will take a while."

for ((i=1; i<=${counts}; i++)); do
        if curl -s http://$1/hostname | grep "red" &> /dev/null; then
                # $1 is the host address of the front-envoy.
                red=$[$red+1]
        elif curl -s http://$1/hostname | grep "blue" &> /dev/null; then
                blue=$[$blue+1]
        else
                green=$[$green+1]
        fi
        sleep $interval
done

echo ""
echo "Response from:"
echo "Red:Blue:Green = $red:$blue:$green"
启动测试
# docker-compose up -d
[+] Running 8/8
 ✔ Network envoy_cluster_lb_rang_hash_envoymesh                Created                                                                                 0.1s
 ✔ Container envoy_cluster_lb_rang_hash-webserver03-sidecar-1  Started                                                                                 0.4s
 ✔ Container envoy_cluster_lb_rang_hash-webserver01-sidecar-1  Started                                                                                 0.3s
 ✔ Container envoy_cluster_lb_rang_hash-webserver02-sidecar-1  Started                                                                                 0.3s
 ✔ Container envoy_cluster_lb_rang_hash-webserver01-1          Started                                                                                 0.5s
 ✔ Container envoy_cluster_lb_rang_hash-webserver03-1          Started                                                                                 0.5s
 ✔ Container envoy_cluster_lb_rang_hash-envoy-1                Started                                                                                 0.5s
 ✔ Container envoy_cluster_lb_rang_hash-webserver02-1          Started                                                                                 0.4s
# 我们在路由hash策略中,hash计算的是用户的浏览器类型,因而,使用如下命令持续发起请求可以看出,用户请求将始终被定向到同一个后端端点;因为其浏览器类型一直未变。
while true; do curl 172.31.2.2; sleep .3; done

# 我们可以模拟使用另一个浏览器再次发请求;其请求可能会被调度至其它节点,也可能仍然调度至前一次的相同节点之上;这取决于hash算法的计算结果;
while true; do curl -H "User-Agent: Hello" 172.31.2.2; sleep .3; done

# 也可使用如下脚本,验证同一个浏览器的请求是否都发往了同一个后端端点,而不同浏览器则可能会被重新调度;
while true; do index=$[$RANDOM%10]; curl -H "User-Agent: Browser_${index}" 172.31.2.2/user-agent && curl -H "User-Agent: Browser_${index}" 172.31.2.2/hostname && echo ; sleep .1; done

# 也可以使用如下命令,将一个后端端点的健康检查结果置为失败,动态改变端点,并再次判定其调度结果,验证此前调度至该节点的请求是否被重新分配到了其它节点;
curl -X POST -d 'livez=FAIL' http://172.31.2.3/livez
0

评论区