赞
踩
(1)Envoy支持同时配置任意数量的上游集群,这些上游都由Cluster Manager进行统一管理;
- Cluster Manager主要负责管理上游主机的健康状态、负载均衡机制、连接类型及适用协议等;
- 生成集群配置的方式有静态定义或动态(CDS)定义两种;这里注意:尤其是使用CDS时,首先要把集群所依赖的配置信息、端点等获取到,然后这个集群才能够处理客户端连接,所以有了下面的集群预热;
(2)集群预热
- 集群在服务器启动或者通过CDS进行初始化时,需要一个预热的过程,这意味着集群存在下列状况:
a. 初始服务发现在配置加载(例如DNS解析、EDS更新等)完成之前集群不可用。
b. 配置了主动健康状态检查机制时,Envoy会主动发送健康状态检测请求报文至发现的每个上游主机,所以,初始的主动健康检查成功完成之前,集群不可用(就算相关配置加载完毕,也得等健康检查通过)。
(3)所以,新增集群初始化完成之前对Envoy的其它组件来说不可见,而对于需要更新的集群,在其预热完成后通过与旧集群的原子交换来确保不会发生流量中断类的错误;
# 原子交换:就是新集群在预热完之前,旧集群是不会被下线的,只有当新集群准备就绪,并且能够正常的接收流量了,旧集群才会开始终止接收新请求,并且现有的已经建立连接的请求,会进行正常终止。
集群管理器配置上游集群时,需要知道如何解析集群成员,相应的解析机制即为服务发现;
集群中的每个成员由endpoint进行标识,它可由用户静态配置,也可通过EDS或DNS服务动态发现;
- Static: 静态配置,即显式指定每个上游主机的已解析名称 (IP地址/端口或unix套接字文件);
- StrictDNS: 严格DNS,Envov将持续和异步地解析指定的DNS目标,并将DNS结果中的返回的每个IP地址视为上游集群中可用成员;
- LogicalDNS: 逻辑DNS,集群仅使用在需要启动新连接时,返回的第一个IP地址,而非严格获取DNS查询的结果,并假设它们构成整个上游集群;适用于必须通过DNS访问的大规模Web服务集群。
- Original destination: 当传入连接通过iptables的REDIRECT或TPROXY target或使用代理协议重定向到Envoy时,可以使用原始目标集群;
- Endpoint discovery service(EDS): EDS是一种基于GRPC协议或REST协议来向管理服务器获取集群成员的一种服务发现方式;
- Custom cluster: Envoy还支持在集群配置上的cluster_type字段中,指定使用自定义集群发现机制;
Envoy的服务发现并未采用完全一致的机制,而是假设主机以最终一致的方式加入或离开网格,它结合主动健康状态检查机制来判定集群的健康状态;
为集群启用主机健康状态检查机制后,Envoy基于如下方式判定是否路由请求到一个主机,如下表格:
Discovery Status(发现状态) | Health Check OK(健康检查成功) | Health Check Failed(健康检查失败) |
---|---|---|
Discovery(发现) | Route(表示可以路由) | Don’t Route |
Absent(不存在) | Route(表示可以路由) | Don’t Route / Delete |
服务发现会存在以下几种情况:
(1)当Discovery Status=Discovery,Health Check OK = Route,这段表示发现了一个端点,并且健康检查也通过了,后续请求可以被路由出去。
(2)Discovery Status=Discovery,Health Check Failed=Don't Route,这段表示发现了一个端点,但是健康检查失败,那么最终结果就是该端点存在,但是流量不能被路由。
(3)Discovery Status=Absent,Health Check OK = Route,此处假设端点之前存在,但是现在不存在了(如果直接从没发现过端点,后面的健康检查没法验证),健康检查依然成功,流量依旧可以被路由。
(4)Discovery Status=Absent,Health Check OK = Don't Route / Delete,这段表示此前存在的服务,现在不存在了,并且健康检查也失败了,所以流量不会再被路由,而且还会从上游可用端点列表中删除该端点。
Envoy提供了一系列开箱即用(就是原生自带)的故障处理机制,如下:
(1)超时(A连B,但是B没有办法响应,为了避免A一直等着B响应,所以配置超时时间,超过指定时间就不等了)
(2)有限次数的重试,并支持可变的重试延迟。
(3)主动健康检查与异常探测。
(4)连接池。# 就是当后端最大连接数满了的时候,新进来的请求先进入连接池,以此来缓冲连接。
(5)断路器。 # 最大连接满了,连接池也满了,这个时候可以配置断路器,直接返回异常,让下游稍后在请求。
上述所有特性,都可以在运行时动态配置,并且结合流量管理机制,用户还可为每个服务/版本定制所需的故障恢复机制;
健康状态检测主要作用:确保代理服务器不会将下游的请求代理至上游有异常的主机; Envoy支持两种类型的健康状态检测,二者均基于集群进行定义; (1)主动检测(Active Health Checking) Envoy周期性地发送探测报文至上游主机,并根据其响应判断其健康状态; Envoy目前支持三种类型的主动检测: - HTTP(L7):向上游主机发送HTTP请求报文; - L3/L4: 向上游主机发送L3/L4请求报文,基于响应的结果判定其健康状态,或仅通过连接状态进行判定; - Redis(L7):向上游的redis服务器发送Redis PING; (2)被动检测(Passive Health Checking) Envov通过异常检测(Outlier Detection) 机制进行被动模式的健康状态检测(就是基于响应状态码检测); 目前,仅http router、tcp proxy和redis proxy三个过滤器支持异常值检测,主要支持以下类型的异常检测; - 连续5XX:指所有类型的错误,非http router过滤器生成的错误也会在内部映射为5xx错误代码; - 连续网关故障:连续5XX的子集,单纯用于http的502、503或504错误,即网关故障; - 连续的本地原因故障:Envoy无法连接到上游主机或与上游主机的通信被反复中断; - 成功率:主机的聚合成功率数据阙值;
[root@k8s-harbor01 ~]# cd servicemesh_in_practise-MageEdu_N66/Cluster-Manager/ [root@k8s-harbor01 Cluster-Manager]# ll 总用量 0 drwxr-xr-x 2 root root 130 8月 5 2022 circuit-breaker drwxr-xr-x 2 root root 145 8月 5 2022 health-check drwxr-xr-x 2 root root 89 8月 5 2022 lb-subsets drwxr-xr-x 2 root root 129 8月 5 2022 least-requests drwxr-xr-x 2 root root 129 8月 5 2022 locality-weighted drwxr-xr-x 2 root root 106 8月 5 2022 outlier-detection drwxr-xr-x 2 root root 133 8月 5 2022 priority-levels drwxr-xr-x 2 root root 129 8月 5 2022 ring-hash drwxr-xr-x 2 root root 129 8月 5 2022 weighted-rr [root@k8s-harbor01 Cluster-Manager]# cd health-check/ [root@k8s-harbor01 health-check]# ls docker-compose.yaml envoy-sidecar-proxy.yaml front-envoy-with-tcp-check.yaml front-envoy.yaml README.md [root@k8s-harbor01 health-check]# [root@k8s-harbor01 health-check]# 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: ROUND_ROBIN load_assignment: cluster_name: web_cluster_01 endpoints: - lb_endpoints: - endpoint: address: socket_address: { address: myservice, port_value: 80 } health_checks: # 健康检查配置 - timeout: 5s # 超时时长。上游端点超过5s不响应就判定为超时。 interval: 10s # 健康检查时间间隔 unhealthy_threshold: 2 # 连续2次健康检查失败,就会把后端服务标记为不健康 healthy_threshold: 2 # 连续2次健康检查成功,就会把后端服务标记为健康 http_health_check: # http类型的健康检查。还有tcp_health_check、grpc_health_check、custom_health_check(自定义健康检查) path: /livez # http健康检查路径 expected_statuses: # 指定的健康状态返回码范围,只要返回状态码在200-399之间,就认为健康 start: 200 end: 399
[root@k8s-harbor01 health-check]# cat front-envoy-with-tcp-check.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: ROUND_ROBIN 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 # 连续检查失败2次,标记后端为不健康 healthy_threshold: 2 # 连续检查成功2次,标记后端为健康 tcp_health_check: {} # tcp健康检查,{}表示使用默认的TCP健康检查配置,同时{}也表示空负载,空负载意味着仅通过连接状态判定其检测结果 # 扩展: 还可以使用非空负载的tcp连接,添加send和receive来分别指定请求负荷及响应报文中期望模糊匹配的结果,如下: tcp_health_check: { send: {...} receive: [...] }
[root@k8s-harbor01 health-check]# cat docker-compose.yaml # Author: MageEdu <mage@magedu.com> # Version: v1.0.1 # Site: www.magedu.com # version: '3.3' services: envoy: #image: envoyproxy/envoy-alpine:v1.21-latest image: envoyproxy/envoy:v1.23-latest environment: - ENVOY_UID=0 - ENVOY_GID=0 volumes: - ./front-envoy.yaml:/etc/envoy/envoy.yaml # http健康检查 # - ./front-envoy-with-tcp-check.yaml:/etc/envoy/envoy.yaml # tcp健康检查 networks: envoymesh: ipv4_address: 172.31.18.2 aliases: - front-proxy depends_on: - webserver01-sidecar - webserver02-sidecar webserver01-sidecar: image: envoyproxy/envoy:v1.23-latest environment: - ENVOY_UID=0 - ENVOY_GID=0 volumes: - ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml hostname: red networks: envoymesh: ipv4_address: 172.31.18.11 aliases: - myservice webserver01: image: ikubernetes/demoapp:v1.0 environment: - PORT=8080 - HOST=127.0.0.1 network_mode: "service:webserver01-sidecar" depends_on: - webserver01-sidecar webserver02-sidecar: image: envoyproxy/envoy:v1.23-latest environment: - ENVOY_UID=0 - ENVOY_GID=0 volumes: - ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml hostname: blue networks: envoymesh: ipv4_address: 172.31.18.12 aliases: - myservice webserver02: image: ikubernetes/demoapp:v1.0 environment: - PORT=8080 - HOST=127.0.0.1 network_mode: "service:webserver02-sidecar" depends_on: - webserver02-sidecar networks: envoymesh: driver: bridge ipam: config: - subnet: 172.31.18.0/24
[root@k8s-harbor01 health-check]# 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 }
[root@k8s-harbor01 health-check]# docker-compose up -d
[root@k8s-harbor01 health-check]# docker-compose ps
Name Command State Ports
--------------------------------------------------------------------------------------
healthcheck_envoy_1 /docker-entrypoint.sh envo ... Up 10000/tcp
healthcheck_webserver01-sidecar_1 /docker-entrypoint.sh envo ... Up 10000/tcp
healthcheck_webserver01_1 /bin/sh -c python3 /usr/lo ... Up
healthcheck_webserver02-sidecar_1 /docker-entrypoint.sh envo ... Up 10000/tcp
healthcheck_webserver02_1 /bin/sh -c python3 /usr/lo ... Up
[root@k8s-harbor01 health-check]# curl 172.31.18.2:9901/clusters
[root@k8s-harbor01 health-check]# while true; do curl 172.31.18.2/hostname ;curl 172.31.18.2/livez;echo ;sleep 0.5$RANDOM; done
ServerName: blue # 后端服务返回
OK # 健康检查结果
ServerName: red # 后端服务返回
[root@k8s-harbor01 health-check]# curl -XPOST -d 'livez=FAIL' 172.31.18.11/livez # 动态修改livez=FAIL
[root@k8s-harbor01 health-check]# while true; do curl 172.31.18.2/hostname ;curl 172.31.18.2/livez;echo ;sleep 0.5$RANDOM; done
ServerName: blue
OK
[root@k8s-harbor01 health-check]# docker-compose down
官方文档:https://www.envoyproxy.io/docs/envoy/v1.24.10/intro/arch_overview/upstream/outlier#arch-overview-outlier-detection
确定主机异常->若尚未驱逐主机,且已驱逐的数量低于允许的值,则已经驱逐主机-> 主机处于驱逐状态一定时长 ->超出时长后自动恢复服务
异常探测通过outlier_dection字段定义在集群上下文中
(1)同主动健康检查一样,异常检测也要配置在集群级别;下面的示例用于配置在返回3个连续5xx错误时将主机弹出30秒; consecutive_5xx: "3" # 连续发生服务器端错误响应(对于HTTP流量,即5xx响应;对于TCP流量,即连接失败;对于Redis,即未能响应PONG等)的次数,在连续发生5xx剔除之前。默认为5次。 base_ejection_time: "30s" # 主机被剔除的基本时间,此处为30s,首次剔除30s后,会重新加入到集群。实际的剔除时间等于基本时间乘以主机被剔除的次数(1次30,2次60),并受到最大剔除时间的限制。默认为30000毫秒或30秒。 (2)在新服务上启用异常检测时,应该从不太严格的规则集开始,以便仅弹出具有网关连接错误的,并且仅在10%的时间内弹出它们主机 (HTTP 503) consecutive_gateway_failure: "3" # 连续发生网关失败(502、503、504状态码)的次数,在连续网关失败剔除之前。默认为5次。 base_ejection_time: "30s" # 主机被剔除的基本时间。实际的剔除时间等于基本时间乘以主机被剔除的次数(1次30,2次60),并受到最大剔除时间的限制。默认为30000毫秒或30秒。 enforcing_consecutive_gateway_failure: "10" # 当当通过连续网关失败检测到异常状态时,主机实际被剔除的概率百分比。该设置可用于禁用剔除或逐渐增加剔除的概率。默认为0%。 总的来说,就是上面连续网关异常3次了,剔除异常端点的概率也只有10%。 默认为0%,表示无限制,只要有异常,就剔除异常端点。 (3)同时,高流量、稳定的服务可以使用统计信息来弹出频繁异常的主机; 弹出错误率低于群集平均值1个标准差的任何端点,统计信息每10秒进行一次评估,并且算法不会针对任何在10秒内少于500个请求的主机运行 上面的描述有点绕,大概意思就是说:对于非常繁忙状态下的主机,可以根据这个主机对应统计出来的错误的比例,和平均的正常比例的偏差,来判定健康与否。 interval: "10s" # 在每10秒(默认也是10s)的时间间隔内,Envoy 将执行一次成功率检查。 base_ejection_time: "30s" # (持续时间)主机被剔除的基本时间。实际的剔除时间等于基本时间乘以主机被剔除的次数(1次30,两次60),并受到最大剔除时间的限制。默认为30000毫秒或30秒。 success_rate_minimum_hosts: "10" # 在一个集群中,必须具有足够请求量以检测成功率异常的主机数量。如果主机数量少于此设置,将不对集群中的任何主机执行基于成功率统计的异常检测。默认为5。 success_rate_request_volume: "500" # 在一个时间间隔(由上面的间隔持续时间定义:interval)内收集的最小总请求数,来进行评估 success_rate_stdev_factor: "1000" # 1000标准差为1
[root@k8s-harbor01 ~]# cd servicemesh_in_practise-MageEdu_N66/Cluster-Manager/outlier-detection/
[root@k8s-harbor01 outlier-detection]# ll
总用量 16
-rw-r--r-- 1 root root 1906 8月 5 2022 docker-compose.yaml
-rw-r--r-- 1 root root 1301 8月 5 2022 envoy-sidecar-proxy.yaml
-rw-r--r-- 1 root root 1322 8月 5 2022 front-envoy.yaml
-rw-r--r-- 1 root root 1386 8月 5 2022 README.md
[root@k8s-harbor01 outlier-detection]# 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 clusters: - name: web_cluster_01 connect_timeout: 0.25s type: STRICT_DNS lb_policy: ROUND_ROBIN load_assignment: cluster_name: web_cluster_01 endpoints: - lb_endpoints: - endpoint: address: socket_address: { address: myservice, port_value: 80 } # 注意这里调用的地址是myservice,一个fqdn名称,引用到docker-compose中的配置,在docker-compose中,所有容器都加上了myservice这个别名,调用这个域名就相当于轮询调用所有上游节点 outlier_detection: # 被动健康状态检查 consecutive_5xx: 3 # 上游连续返回3次5xx,就剔除集群中的异常端点 base_ejection_time: 10s # 首次剔除基本时间为10s,后续时间为基本时间 * 剔除次数 max_ejection_percent: 10 # 最大弹出比例10%。如果3个节点,那么至少弹出1个,如果2个节点,也只会弹出1个,因为比例限制死了
[root@k8s-harbor01 outlier-detection]# cat docker-compose.yaml version: '3.3' services: envoy: image: envoyproxy/envoy-alpine:v1.21-latest environment: - ENVOY_UID=0 - ENVOY_GID=0 volumes: - ./front-envoy.yaml:/etc/envoy/envoy.yaml networks: envoymesh: ipv4_address: 172.31.20.2 aliases: - front-proxy depends_on: - webserver01-sidecar - webserver02-sidecar - webserver03-sidecar webserver01-sidecar: image: envoyproxy/envoy-alpine:v1.21-latest environment: - ENVOY_UID=0 - ENVOY_GID=0 volumes: - ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml hostname: red networks: envoymesh: ipv4_address: 172.31.20.11 aliases: - myservice # 别名配置,这个别名就是边缘代理集群配置中引用的 webserver01: image: ikubernetes/demoapp:v1.0 environment: - PORT=8080 - HOST=127.0.0.1 network_mode: "service:webserver01-sidecar" depends_on: - webserver01-sidecar webserver02-sidecar: image: envoyproxy/envoy-alpine:v1.21-latest environment: - ENVOY_UID=0 - ENVOY_GID=0 volumes: - ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml hostname: blue networks: envoymesh: ipv4_address: 172.31.20.12 aliases: - myservice # 别名配置,这个别名就是边缘代理集群配置中引用的 webserver02: image: ikubernetes/demoapp:v1.0 environment: - PORT=8080 - HOST=127.0.0.1 network_mode: "service:webserver02-sidecar" depends_on: - webserver02-sidecar webserver03-sidecar: image: envoyproxy/envoy-alpine:v1.21-latest environment: - ENVOY_UID=0 - ENVOY_GID=0 volumes: - ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml hostname: green networks: envoymesh: ipv4_address: 172.31.20.13 aliases: - myservice # 别名配置,这个别名就是边缘代理集群配置中引用的 webserver03: image: ikubernetes/demoapp:v1.0 environment: - 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.20.0/24
[root@k8s-harbor01 outlier-detection]# 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 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 }
[root@k8s-harbor01 outlier-detection]# docker-compose up -d
[root@k8s-harbor01 outlier-detection]# docker-compose ps
Name Command State Ports
-------------------------------------------------------------------------------------------
outlierdetection_envoy_1 /docker-entrypoint.sh envo ... Up 10000/tcp
outlierdetection_webserver01-sidecar_1 /docker-entrypoint.sh envo ... Up 10000/tcp
outlierdetection_webserver01_1 /bin/sh -c python3 /usr/lo ... Up
outlierdetection_webserver02-sidecar_1 /docker-entrypoint.sh envo ... Up 10000/tcp
outlierdetection_webserver02_1 /bin/sh -c python3 /usr/lo ... Up
outlierdetection_webserver03-sidecar_1 /docker-entrypoint.sh envo ... Up 10000/tcp
outlierdetection_webserver03_1 /bin/sh -c python3 /usr/lo ... Up
[root@k8s-harbor01 outlier-detection]# curl 172.31.20.2/livez && echo
OK
[root@k8s-harbor01 outlier-detection]# curl -I 172.31.20.2/livez
HTTP/1.1 200 OK # 返回状态码200
content-type: text/html; charset=utf-8
content-length: 2
server: envoy # 也可以看到该请求是由sidecar返回的
date: Tue, 12 Sep 2023 05:26:36 GMT
x-envoy-upstream-service-time: 5
[root@k8s-harbor01 outlier-detection]# docker-compose down
[root@k8s-harbor01 outlier-detection]# docker-compose up
[root@k8s-harbor01 ~]# while true;do curl 172.31.20.2/livez; sleep 0.$RANDOM; echo; done
[root@k8s-harbor01 ~]# curl -XPOST -d 'livez=FAIL' 172.31.20.12/livez
[root@k8s-harbor01 ~]# curl -XPOST -d 'livez=FAIL' 172.31.20.13/livez
[root@k8s-harbor01 outlier-detection]# docker-compose down
官方文档:https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/load_balancers
Envov提供了几种不同的负载均衡策略,并可大体分为全局负载均衡和分布式负载均衡两类; (1)分布式负载均衡(调度算法负载均衡) Envoy自身基于上游主机 (区域感知)的位置及健康状态等来确定如何分配请求至相关端点。这里又分为3种负载配置方式 a. 主动健康检查 b. 区域感知路由 c. 负载均衡算法 (2)全局负载均衡 这是一种通过单个具有全局权限(指边缘网关)的组件来统一决策负载的机制,Envov的控制平面即是该类组件之一,它能够通过指定各种参数来调整应用于各端点的负载。这里有4种配置方式: a. 优先级 b. 位置权重 c. 端点权重 d. 端点健康状态 复杂的部署场景中,可以混合使用两类负载均衡策略,全局负载均衡通过定义高级路由优先级和权重,来控制同级别的流量。 而分布式负载均衡用于对系统中的微观变动作出反应 (例如主动健康检); # 在istio中,这些都是自动完成识别的。
Cluster Manager使用负载均衡策略将下游请求调度至选中的上游主机,它支持如下几个算法:
(1)加权轮询(weighted round robin,wrr):算法名称为 ROUND_ROBIN,特点是仅根据算法本身进行调度。
(2)加权最少请求(weighted least request,wlr):算法名称为 LEAST_REQUEST,特点是除了根据算法本身进行调度外,还要考虑后端每个主机上当前活跃连接数。
(3)环哈希 (ringhash):算法名称为RING_HASH,其工作方式类似于一致性哈希算法;
(4)磁悬浮(maglev):类似于环哈希,但其大小固定为65537,并需要各主机映射的节点填满整个环;
无论配置的主机和位置权重如何,算法都会尝试确保将每个主机至少映射一次,算法名称为MAGLEV。
(5)随机 (random):未配置健康检查策略,则随机负载均算法通常比轮询更好;
另外,还有原始目标集群负载均衡机制,其算法为ORIGINAL_DST_LB,但仅适用于原始目标集群的调度;
加权最少请求算法,根据主机的权重相同或不同而使用不同的算法
(1)所有主机的权重均相同
这是一种复杂度为O(1)调度算法,它随机选择N个 (默认为2,可配置) 可用主机并从中挑选具有最少活动请求的主机;
研究表明,这种称之为P2C的算法效果不亚于O(N)复杂度的全扫描算法,它确保了集群中具有最大连接数的端点决不会收到新的请求,直到其连接数小于等于其它主机;
(2)所有主机的权重并不完全相同,即集群中的两个或更多的主机具有不同的权重
调度算法将使用加权轮询调度的模式,权重将根据主机在请求时的请求负载进行动态调整。
方法是权重除以当前的活动请求计数。
例如,权重为2且活动请求计数为4的主机的综合权重为2/4=0.5;
该算法在稳定状态下可提供良好的平衡效果,但可能无法尽快适应不太均衡的负载场景;
与P2C不同,主机将永远不会真正排空,即便随着时间的推移它将收到更少的请求;
加权最少请求配置参数:
least_request_lb_config:
choice_count: "{...}" # 从健康的主机中随机挑选出默认2个节点,作为样本进行最少连接数比较
官方文档:https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/load_balancing_policies/ring_hash/v3/ring_hash.proto
如上图,有一个圆环,圆环上面还4个小圆环,分别是P1、P2、P3、P4,这里我们把小圆环称之为哈希值(通过对服务器的IP地址或主机名或权重进行哈希计算得出)。
这4个哈希值分别对应4台服务器,用户可以访问这4台中的任何一台。
那么我们客户端如何请求到哈希环上的哈希值(服务器)呢?
假设现在有userID=1的请求,经过哈希运算,请求落到了P4和P1之间,如下图:
那这个请求到底该去访问哪个服务器呢?
它会按照顺时针的顺序,去请求离它最近的一个哈希值(服务器),也就是P1。
假设该请求刚好是出于流量高峰期,那么会有非常多的流量都会请求到P1这个节点上面(同理其他几个哈希值也一样),
P1就要承受非常巨大的压力,非常容易把服务打死,而且其他如P2、P3、P4节点,都是处于闲置状态。
那么这种情况下应该怎么做呢?
为了应对上述情况,我们可以创造一些虚拟节点,如下图:
如上图:
除了P1 P2 P3 P4,还有很多椭圆形的圆环,这些都是根据原有的4个节点的地址(IP或主机名)或权重映射出来的虚拟的哈希值。
那么此时的请求在P4和P1的范围内的话,就多了2个虚拟节点(P3-1、P2-1,由P3和P2映射)来帮忙处理请求(分摊),
这样P1就不用单独承受所有的请求了,而且每个服务器处理的请求都会变得很均匀(散列的哈希环),这样就不会像之前一样,
p1在处理所有请求,P2 P3 P4闲置。
上述这个散列的哈希环就是最终的一个变种。
环哈希如何实现数字的有序排序?
答案是TreeMap。
TreeMap是属于红黑树的一个数据结构(源码中注释)。
注意:环哈希在工作工作过程中会有两个状态:
(1)Failover # 故障转移(节点异常后,会把新的请求调度到顺时针的下一个节点)
(2)Failback # 故障移回(节点正常后,请求会转移回到原来的节点,因为请求和后端端点是绑定的)
上述两种状态会导致状态丢失。
配置参数
ring_hash_lb_config:
"minimum_ring_size": "{...}", # 哈希环的最小值,环越大调度结果越接近权重酷比,默认为1024,最大为8M(8*1024*1024)。
"hash_function": "...", # 哈希算法,支持XX_HASH和MURMUR_HASH_2两种,默认为第一种。
"maximum_ring_size": "{...}" # 哈希环的最大值,默认为8M,不过值越大越消耗计算资源。
官方文档:https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/load_balancers.html#maglev
当我们使用哈希算法的时候,目的是为了将来自于某一个固定客户端的请求,始终和上游某一个被筛选出来的端点建立绑定关系,
那这个时候我们就还需要对客户端进行哈希计算,那到底对客户端的什么东西做哈希计算呢?
如客户端的IP地址,cookie,如果是HTTP请求,还可以对某一个具体的标头做哈希计算,比如把来自web端的请求调度给一个cluster,把来自移动端的请求调度给另一个clsuter。
因此我们就需要在envoy的路由端,我们称之为 “Hash Policy”,去指定计算的内容,然后再和后端的端点建立连接关系。
用于一致性哈希算法的散列策略列表,即指定将请求报文的哪部分属性,进行哈希运算并映射至主机的哈希环上以完成路由。
列表中每个哈希策略都将单独评估,合并后的结果用于路由请求,组合的方法是确定性的,以便相同的哈希策略列表将产生相同的哈希。
哈希策略检查的请求的特定部分不存时将会导致无法生成哈希结果,如果(且仅当)所有已配置的哈希策略均无法生成哈希,则不会为该路由生成哈希,
在这种情况下,其行为与未指定任何哈希策略的行为相同(即,环形哈希负载均衡器将选择一个随机后端) ;
若哈希策略将“terminal”属性设置为true,并且已经生成了哈希,则哈希算法将立即返回,而忽略哈希策略列表的其余部分;
定义在配置文件中
(1)负载均衡算法
- ROUND_ROBIN(轮询): 默认负载均衡算法。
- LEAST_REQUEST(最小连接数): 随机选取两个健康的主机,再从所选取的两个主机中选择一个链接数较少的主机。
- RANDOM(随机):从所有健康的主机中,随机选取一个主机,在负载平衡池的端点上均匀分配负载。在没有健康检查策略的情况下,随机通常会比轮询调度策略更加高效,但不会有任何顺序。
上述的3种策略,比较适合无状态应用。
那么如果是有状态服务的话,就要使用RING_HASH(环哈希)和MAGLEV(磁悬浮)来实现会话保持了。
(2)会话保持
- 根据HTTP header中的内容获取哈希: 流量治理根据HTTP header中的内容获取哈希。
- 根据cookle 中的内容获取哈希:支持用户输入Cookie键的名称,转发方式则由设定的Cookie键对应的值来计算哈希,哈希相同的请求则会转发至同一个容器组中。例如我们设定Cookie 中的User为键,则通过计算user对应的值的哈希来确认转发规则。
- 根据源IP获取哈希: 根据源P中的内容获得哈希。
会话保持需要结合路由配置值,并指定需要hash的内容。
[root@k8s-harbor01 ~]# cd servicemesh_in_practise-MageEdu_N66/Cluster-Manager/least-requests/
[root@k8s-harbor01 least-requests]# ll
总用量 20
-rw-r--r-- 1 root root 1951 8月 5 2022 docker-compose.yaml
-rw-r--r-- 1 root root 1301 8月 5 2022 envoy-sidecar-proxy.yaml
-rw-r--r-- 1 root root 1607 8月 5 2022 front-envoy.yaml
-rw-r--r-- 1 root root 930 8月 5 2022 README.md
-rwxr-xr-x 1 root root 609 8月 5 2022 send-request.sh
[root@k8s-harbor01 least-requests]# 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 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 # 负载均衡权重为1 - endpoint: address: socket_address: address: blue port_value: 80 load_balancing_weight: 3 # 负载均衡权重为3 - endpoint: address: socket_address: address: green port_value: 80 load_balancing_weight: 5 # 负载均衡权重为5
[root@k8s-harbor01 least-requests]# cat docker-compose.yaml version: '3.3' services: envoy: image: envoyproxy/envoy-alpine:v1.21-latest environment: - ENVOY_UID=0 - ENVOY_GID=0 volumes: - ./front-envoy.yaml:/etc/envoy/envoy.yaml networks: envoymesh: ipv4_address: 172.31.22.2 aliases: - front-proxy depends_on: - webserver01-sidecar - webserver02-sidecar - webserver03-sidecar webserver01-sidecar: image: envoyproxy/envoy-alpine:v1.21-latest environment: - ENVOY_UID=0 - ENVOY_GID=0 volumes: - ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml hostname: red networks: envoymesh: ipv4_address: 172.31.22.11 aliases: - myservice - red webserver01: image: ikubernetes/demoapp:v1.0 environment: - PORT=8080 - HOST=127.0.0.1 network_mode: "service:webserver01-sidecar" depends_on: - webserver01-sidecar webserver02-sidecar: environment: - ENVOY_UID=0 - ENVOY_GID=0 image: envoyproxy/envoy-alpine:v1.21-latest volumes: - ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml hostname: blue networks: envoymesh: ipv4_address: 172.31.22.12 aliases: - myservice - blue webserver02: image: ikubernetes/demoapp:v1.0 environment: - PORT=8080 - HOST=127.0.0.1 network_mode: "service:webserver02-sidecar" depends_on: - webserver02-sidecar webserver03-sidecar: environment: - ENVOY_UID=0 - ENVOY_GID=0 image: envoyproxy/envoy-alpine:v1.21-latest volumes: - ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml hostname: green networks: envoymesh: ipv4_address: 172.31.22.13 aliases: - myservice - green webserver03: image: ikubernetes/demoapp:v1.0 environment: - 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.22.0/24
[root@k8s-harbor01 least-requests]# 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"
[root@k8s-harbor01 least-requests]# 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 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 }
[root@k8s-harbor01 least-requests]# docker-compose up -d
[root@k8s-harbor01 least-requests]# docker-compose ps
[root@k8s-harbor01 least-requests]# sh send-request.sh 172.31.22.2
# 修改端点权重 [root@k8s-harbor01 least-requests]# docker-compose down [root@k8s-harbor01 least-requests]# cat front-envoy.yaml ……省略部分内容 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: 1 - endpoint: address: socket_address: address: green port_value: 80 load_balancing_weight: 1 # 启动容器 [root@k8s-harbor01 least-requests]# docker-compose up -d # 执行脚本测试 [root@k8s-harbor01 least-requests]# sh send-request.sh 172.31.22.2
[root@k8s-harbor01 least-requests]# cd .. [root@k8s-harbor01 Cluster-Manager]# cd weighted-rr/ [root@k8s-harbor01 weighted-rr]# ll -rt 总用量 20 -rwxr-xr-x 1 root root 609 8月 5 2022 send-request.sh -rw-r--r-- 1 root root 930 8月 5 2022 README.md -rw-r--r-- 1 root root 1605 8月 5 2022 front-envoy.yaml -rw-r--r-- 1 root root 1301 8月 5 2022 envoy-sidecar-proxy.yaml -rw-r--r-- 1 root root 1951 8月 5 2022 docker-compose.yaml [root@k8s-harbor01 weighted-rr]# 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 clusters: - name: web_cluster_01 connect_timeout: 0.25s type: STRICT_DNS lb_policy: ROUND_ROBIN # 轮询 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 # 端点权重
[root@k8s-harbor01 weighted-rr]# cat docker-compose.yaml version: '3.3' services: envoy: environment: - ENVOY_UID=0 - ENVOY_GID=0 image: envoyproxy/envoy-alpine:v1.21-latest volumes: - ./front-envoy.yaml:/etc/envoy/envoy.yaml networks: envoymesh: ipv4_address: 172.31.27.2 aliases: - front-proxy depends_on: - webserver01-sidecar - webserver02-sidecar - webserver03-sidecar webserver01-sidecar: environment: - ENVOY_UID=0 - ENVOY_GID=0 image: envoyproxy/envoy-alpine:v1.21-latest volumes: - ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml hostname: red networks: envoymesh: ipv4_address: 172.31.27.11 aliases: - myservice - red webserver01: image: ikubernetes/demoapp:v1.0 environment: - PORT=8080 - HOST=127.0.0.1 network_mode: "service:webserver01-sidecar" depends_on: - webserver01-sidecar webserver02-sidecar: environment: - ENVOY_UID=0 - ENVOY_GID=0 image: envoyproxy/envoy-alpine:v1.21-latest volumes: - ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml hostname: blue networks: envoymesh: ipv4_address: 172.31.27.12 aliases: - myservice - blue webserver02: image: ikubernetes/demoapp:v1.0 environment: - PORT=8080 - HOST=127.0.0.1 network_mode: "service:webserver02-sidecar" depends_on: - webserver02-sidecar webserver03-sidecar: environment: - ENVOY_UID=0 - ENVOY_GID=0 image: envoyproxy/envoy-alpine:v1.21-latest volumes: - ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml hostname: green networks: envoymesh: ipv4_address: 172.31.27.13 aliases: - myservice - green webserver03: image: ikubernetes/demoapp:v1.0 environment: - 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.27.0/24
[root@k8s-harbor01 weighted-rr]# 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"
[root@k8s-harbor01 weighted-rr]# 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 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 }
[root@k8s-harbor01 weighted-rr]# docker-compose up -d
[root@k8s-harbor01 weighted-rr]# docker-compose ps
Name Command State Ports
-------------------------------------------------------------------------------------
weightedrr_envoy_1 /docker-entrypoint.sh envo ... Up 10000/tcp
weightedrr_webserver01-sidecar_1 /docker-entrypoint.sh envo ... Up 10000/tcp
weightedrr_webserver01_1 /bin/sh -c python3 /usr/lo ... Up
weightedrr_webserver02-sidecar_1 /docker-entrypoint.sh envo ... Up 10000/tcp
weightedrr_webserver02_1 /bin/sh -c python3 /usr/lo ... Up
weightedrr_webserver03-sidecar_1 /docker-entrypoint.sh envo ... Up 10000/tcp
weightedrr_webserver03_1 /bin/sh -c python3 /usr/lo ... Up
./send-request.sh 172.31.27.2
# 调整配置 [root@k8s-harbor01 weighted-rr]# docker-compose down [root@k8s-harbor01 weighted-rr]# cat front-envoy.yaml ……省略部分内容 clusters: - name: web_cluster_01 connect_timeout: 0.25s type: STRICT_DNS lb_policy: ROUND_ROBIN 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: 1 - endpoint: address: socket_address: address: green port_value: 80 load_balancing_weight: 1 # 启动容器 [root@k8s-harbor01 weighted-rr]# docker-compose up -d # 测试 ./send-request.sh 172.31.27.2
[root@k8s-harbor01 weighted-rr]# cd ../ring-hash/ [root@k8s-harbor01 ring-hash]# ll 总用量 20 -rw-r--r-- 1 root root 1951 8月 5 2022 docker-compose.yaml -rw-r--r-- 1 root root 1301 8月 5 2022 envoy-sidecar-proxy.yaml -rw-r--r-- 1 root root 1755 8月 5 2022 front-envoy.yaml -rw-r--r-- 1 root root 1885 8月 5 2022 README.md -rwxr-xr-x 1 root root 537 8月 5 2022 send-request.sh [root@k8s-harbor01 ring-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 clusters: - name: web_cluster_01 connect_timeout: 0.5s type: STRICT_DNS lb_policy: RING_HASH # 负载均衡策略 环哈希 ring_hash_lb_config: # 环哈希配置(一般情况下可以不用定义,使用默认值就够了) maximum_ring_size: 1048576 # 环的最大值,表示最多能容纳1048576个端点(默认8M,也就是8*1024*1024,最大也是8M) minimum_ring_size: 512 # 环的最小值,表示最少能容纳512个端点(默认1024,最大8M,也就是8*1024*1024) 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
[root@k8s-harbor01 ring-hash]# cat docker-compose.yaml version: '3.3' services: envoy: environment: - ENVOY_UID=0 - ENVOY_GID=0 image: envoyproxy/envoy-alpine:v1.21-latest volumes: - ./front-envoy.yaml:/etc/envoy/envoy.yaml networks: envoymesh: ipv4_address: 172.31.25.2 aliases: - front-proxy depends_on: - webserver01-sidecar - webserver02-sidecar - webserver03-sidecar webserver01-sidecar: environment: - ENVOY_UID=0 - ENVOY_GID=0 image: envoyproxy/envoy-alpine:v1.21-latest volumes: - ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml hostname: red networks: envoymesh: ipv4_address: 172.31.25.11 aliases: - myservice - red webserver01: image: ikubernetes/demoapp:v1.0 environment: - PORT=8080 - HOST=127.0.0.1 network_mode: "service:webserver01-sidecar" depends_on: - webserver01-sidecar webserver02-sidecar: environment: - ENVOY_UID=0 - ENVOY_GID=0 image: envoyproxy/envoy-alpine:v1.21-latest volumes: - ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml hostname: blue networks: envoymesh: ipv4_address: 172.31.25.12 aliases: - myservice - blue webserver02: image: ikubernetes/demoapp:v1.0 environment: - PORT=8080 - HOST=127.0.0.1 network_mode: "service:webserver02-sidecar" depends_on: - webserver02-sidecar webserver03-sidecar: environment: - ENVOY_UID=0 - ENVOY_GID=0 image: envoyproxy/envoy-alpine:v1.21-latest volumes: - ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml hostname: green networks: envoymesh: ipv4_address: 172.31.25.13 aliases: - myservice - green webserver03: image: ikubernetes/demoapp:v1.0 environment: - 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.25.0/24
[root@k8s-harbor01 ring-hash]# 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"
[root@k8s-harbor01 ring-hash]# 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 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 }
[root@k8s-harbor01 ring-hash]# docker-compose up -d
[root@k8s-harbor01 ring-hash]# docker-compose ps
Name Command State Ports
-----------------------------------------------------------------------------------
ringhash_envoy_1 /docker-entrypoint.sh envo ... Up 10000/tcp
ringhash_webserver01-sidecar_1 /docker-entrypoint.sh envo ... Up 10000/tcp
ringhash_webserver01_1 /bin/sh -c python3 /usr/lo ... Up
ringhash_webserver02-sidecar_1 /docker-entrypoint.sh envo ... Up 10000/tcp
ringhash_webserver02_1 /bin/sh -c python3 /usr/lo ... Up
ringhash_webserver03-sidecar_1 /docker-entrypoint.sh envo ... Up 10000/tcp
ringhash_webserver03_1 /bin/sh -c python3 /usr/lo ... Up
我们在路由hash策略中,hash计算的是用户的浏览器类型,因而,使用如下命令持续发起请求可以看出,用户请求将始终被定向到同一个后端端点;因为其浏览器类型一直未变。
[root@k8s-harbor01 ring-hash]# while true; do curl 172.31.25.2; sleep .3; done
模拟使用另一个浏览器再次发请求;其请求可能会被调度至其它节点,也可能仍然调度至前一次的相同节点之上;这取决于hash算法的计算结果;
[root@k8s-harbor01 ring-hash]# while true; do curl -H "User-Agent: Hello" 172.31.25.2; sleep .3; done # "User-Agent: Hello" 表示修改浏览器类型为hello
先开一个窗口一直请求着
[root@k8s-harbor01 ring-hash]# while true; do curl -H "User-Agent: CCC" 172.31.25.2; sleep .3; done # 这里要注意,不同的User-Agent会被哈希运算后调度到不同的节点,换一个新环境执行相同的命令,不一定会调度到11节点
新开一个窗口,调整11节点的健康状态为不健康
[root@k8s-harbor01 ring-hash]# curl -X POST -d 'livez=FAIL' http://172.31.25.11/livez
[root@k8s-harbor01 ring-hash]# curl -I 172.31.25.11/livez
HTTP/1.1 506 Variant Also Negotiates # 响应状态码已经变成了506
content-type: text/html; charset=utf-8
content-length: 4
server: envoy
date: Thu, 14 Sep 2023 07:58:26 GMT
x-envoy-upstream-service-time: 1
返回第一个窗口查看请求详情
修改健康状态为正常
[root@k8s-harbor01 ~]# curl -X POST -d 'livez=OK' http://172.31.25.11/livez
查看请求
EDS配置中,属于某个特定位置的一组端点称为Locality Lb Endpoints,它们的特点如下:
(1)相同的位置 (locality):从大到小可由region (地域)、zone (区域)和sub_zone (子区域) 进行逐级标识;
(2)相同的权重(load balancing_weight):
可选参数,用于为每个priorit/region/zone/sub_zone配置权重,取值范围(1,n);
通常,一个locality权重除以具有相同优先级的所有localitv的权重之和即为当前locality的流量比例;
此配置仅启用了位置加权负载均衡机制时才会生效;
(3)相同的优先级(priority)
此Locality Lb Endpoints组的优先级,默认为最高优先级0。
通常,Envoy调度时仅挑选最高优先级的一组端点,且仅此优先级的所有端点均不可用时才进行故障转移至下一个优先级的相关端点;
注意,也可在同一位置配置多个LbEndpoints,但这通常仅在不同组需要具有不同的负载均衡权重或不同的优先级时才需要;
调度时,Envov仅将流量调度至最高优先级的一组端点(Locality Lb Enpoints)。 当最高优先级的端点变得不健康时,流量才会按比例转移至次一个优先级的端点, 例如一个优先级中20%的端点不健康时,也将有20%的流量转移至次一个优先级端点。 但是如果希望更多流量留在本组端点进行处理时,可以通过配置“超配因子(可溢出流量)”来实现。 - 超配因子:通过给一组端点设定超配因子,实现部分端点故障时仍将更大比例的流量导向至本组端点。 - 计算公式:转移出去的流量=100% - 健康端点的比例 * 超配因子。 - 假设:超配因子为1.4,当故障比例为20%时,所有流量任将保留在当前组;(那就是100% - 80%(正常的)* 1.4(超配因子)=-0.12,表示没有流量会溢出) - 假设:健康的端点比例为70%,这个时候就会有部分流量转移到次一级优先级端点;(100% - 70% * 1.4 = 0.02,表示有2%的流量会溢出,98%的流量会留下) 若各个优先级的健康评分总和(也称为标准化的总健康状态)小于100,则Envoy会认为没有足够的健康端点来分配所有待处理的流量,此时,各级别会根据其健康分值的比例重新分配100%的流量。 例如:有3组节点,优先级为0,1,2. - 高级优先级端点(优先级为0): 超配因子为1.2,健康节点为50%,溢出流量就是100% - 50% * 1.2 = 0.4(40%),最终会有40%的流量溢出到优先级为1的这组端点上。 - 中级优先级端点(优先级为1):超配因子为1.2,健康节点为50%,溢出流量就是100% - 50% * 1.2 = 0.4(40%),最终会有40%的流量溢出到优先级为2的这组端点上。。 - 低级优先级端点(优先级为2):超配因子为1.2,健康节点为50%,溢出流量就是100% - 50% * 1.2 = 0.4(40%),这个时候已经没有端点可以接受溢出的这40%的流量了。 换句话说就是3组端点都不能够完全承受溢出的这部分流量,那怎么办呢?这就是上面说的,envoy会重新分配100%的流量。 另外,优先级调度还支持同一优先级内部的端点降级(DEGRADE)机制,其工作方式类同于在两个不同优先级之间的端点分配流量的机制。 - 例如:一个优先级内部,共有10个端点,有8个端点是OK的,另外2个为降级状态,那么正常情况下,所有流量都由OK的8个端点进行处理,只有当这8个端点中,有一部分端点不可用时,才会有流量溢出到状态为降级的2个端点。 计算方式: - 非降级端点健康比例 * 超配因子大于等于100%时,降级端点不承接流量; - 非降级端点的健康比例 * 超配因子小于100%时,降级端点承接与100%差额部分的流量;
官方文档:https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/panic_threshold.html
调度期间,Envoy仅考虑上游主机列表中的可用 (健康或降级)端点,但可用端点的百分比过低时,Envoy将忽略所有端点的健康状态,
并将流量调度给所有端点,以此来避免流量将正常的节点打死,导致业务完全不可用。
此百分比即为Panic阈值,也称为恐慌阈值。
- 默认的Panic恐慌阈值为50%(一个优先级中的节点,有或超过50%不可用时,将激活恐慌阈值)。
- Panic阈值用于避免在流量增长时导致主机故障进入级联状态(仅剩的健康节点也被打死);
恐慌阈值可与优先级一同使用,给定优先级中的可用端点数量下降时,Envoy会将一些流量转移至较低优先级的端点;
- 若在低优先级中找到能承载所有流量的端点,则忽略恐慌阁值;
- 否则,Envov会在所有优先级之间分配流量,并在给定的优先级的可用性低于恐慌阙值时将该优先的流量分配至该优先级的所有主机;
位置不仅可以有优先级,还能有权重
(1)位置加权负载均衡(Locality weighted load balancing)即为特定的Locality及相关的LbEndpoints组显式赋予权重, 并根据此权重比在各Locality之间分配流量; - 所有Locality的所有Endpoint均可用时,则根据位置权重在各Locality之间进行加权轮询; - 例如,cn-north-1和cn-north-2两个region的权重分别为1和2时,且各region内的端点均处理于健康状态,则流量分配比例为“1:2”,即一个33%,一个是67%; 启用位置加权负载均衡及位置权重定义的方法: cluster: - name: ... ... common_lb_config: locality_weighted_lb_config: {} # 启用位置加权负载均衡机制,它没有可用的子参数; ... load_assignment: endpoints: locality: "{...}" lb_endpoints": [] load_balancing_weight: "{}" # 整数值,定义当前位置或优先级的权重,最小值为1; priority: "..." 注意: 位置加权负载均衡同区域感知负载均衡互斥,因此,用户仅可在Cluster级别设置locality_weighted_lb_config或zone_aware_lb_config其中之一,以明确指定启用的负载均衡策略。 为什么会互斥呢? 位置加权和区域感知,都是基于位置来做负载均衡的。 区别在于位置加权是基于权重调度,而区域感知是看客户端和服务端之间是否在同一区域进行调度,会尽可能的把客户端请求调度给同一区域内的端点,所以和权重这种逻辑不太兼容。 加载顺序:优先级--》权重 (2)当某Locality的某些Endpoint不可用时,Envoy则按比例动态调整该Locality的权重; - 位置加权负载均衡方式也支持为LbEndpoint配置超配因子,默认为1.4; - 于是,一个Locality(假设为X)的有效权重计算方式如下: a. health(L_X 健康状态比例) = 1.4(超配因子) * healthy_X_backends(健康的端点比例) / total_X_backends(组内总的后端端点比例) b. effective_weight(L_X 有效权重) = locality_weight_X(权重) * min(100%, health(L_X)) c. load to L_X(负载比例) = effective_weight(L_X 有效权重) * health(L_X 健康状态比例) 假设有三个服务器:A、B 和 C。 本地权重: - A:50 - B:30 - C:20 上游节点总数: - A:5 - B:5 - C:5 上游可用节点数: - A:2 - B:3 - C:4 可用性计算: 对于服务器 A:availability(L_A) = 140 * available_A_upstreams / total_A_upstreams = 140 * 2 / 5 = 56 对于服务器 B:availability(L_B) = 140 * available_B_upstreams / total_B_upstreams = 140 * 3 / 5 = 84 对于服务器 C:availability(L_C) = 140 * available_C_upstreams / total_C_upstreams = 140 * 4 / 5 = 112 接下来,我们计算每个服务器的有效权重。 有效权重计算: 对于服务器 A:effective_weight(L_A) = locality_weight_A * min(100, availability(L_A)) = 50 * min(100, 56) = 50 * 56 = 2800 对于服务器 B:effective_weight(L_B) = locality_weight_B * min(100, availability(L_B)) = 30 * min(100, 84) = 30 * 84 = 2520 对于服务器 C:effective_weight(L_C) = locality_weight_C * min(100, availability(L_C)) = 20 * min(100, 112) = 20 * 100 = 2000 最后,我们计算每个服务器的负载。 负载计算: 对于服务器 A:load to L_A = effective_weight(L_A) / (effective_weight(L_A) + effective_weight(L_B) + effective_weight(L_C)) = 2800 / (2800 + 2520 + 2000) ≈ 0.3825(约为 38.25%) 对于服务器 B:load to L_B = effective_weight(L_B) / (effective_weight(L_A) + effective_weight(L_B) + effective_weight(L_C)) = 2520 / (2800 + 2520 + 2000) ≈ 0.3442(约为 34.42%) 对于服务器 C:load to L_C = effective_weight(L_C) / (effective_weight(L_A) + effective_weight(L_B) + effective_weight(L_C)) = 2000 / (2800 + 2520 + 2000) ≈ 0.2732(约为 27.32%) 最终: A服务器的请求负载大约为:38.25% B服务器的请求负载大约为:34.42% B服务器的请求负载大约为:27.32% 若同时配置了优先级和权重,负载均衡器将会以如下步骤进行调度: - 选择priority(优先级); - 从选出的priority中选择locality(位置); - 从选出的locality中选择Endpoint;
Envoy支持在一个集群中基于子集实现更细粒度的流量分发。 大致的工作逻辑如下: (1)首先,在集群的上游主机上添加元数据(键值标签),并使用子集选择器(分类元数据)将上游主机划分 为子集; (2)而后,在路由配置中指定负载均衡器可以选择的且必须具有匹配的元数据的上游主机,从而实现向 特定子集的路由; (3)各子集内的主机间的负载均衡采用集群定义的策略(lb_policy); 怎么理解呢? 假设有若干个端点组成了一个上游集群,正常情况下全流量肯定是根据负载策略负载到上游聚集群的。 但是可以在route_config中,通过配置子集的方式,获取上游集群中每一个端点的元数据,并且允许我们在路由中,通过定义子集选择器(标签选择器),来对上游集群中的端点进行过滤,并生成一到多个子集。 从而实现把流量分发到一到多个子集中。 (4)配置了子集,但路由并未指定元数据或不存在与指定元数据匹配的子集时,则子集均衡器为其应用“回退策略”。 - NO_FALLBACK:请求失败(直接返回失败给客户端),类似集群中不存在任何主机;此为默认策略; - ANY_ENDPOINT:在所有主机间进行调度,不再考虑主机元数据; - DEFAULT_SUBSET:调度至默认的子集,该子集需要事先定义;
子集必须先预定义,方可由子集负载均衡器在调度时使用。 (1)定义主机子集元数据 配置示例: load_assignment: cluster_name: webcluster1 endpoints: - lb_endpoints: - endpoint: address: socket_address: protocol: TCP address: ep1 port_value: 80 metadata: filter_metadata: envoy.lb: # 主机子集元数据必须在envoy.lb过滤器下 version: '1.0' # 主机子集元数据。使用ClusterLoadAssignments定义主机时才支持主机元数据 stage: 'prod' # 主机子集元数据 (2)定义子集:基于子集选择器,目标仅支持键值列表 配置示例: clusters: - name ... ... lb_subset_config: # 负载均衡子集配置 fallback_policy: "..." # 回退策略,默认为NO_FALLBACK default_subset: "{...}" # 默认的子集; subset_selectors: [] # 子集选择器 - keys: [] # 定义一个选择器,指定用于归类主机元数据的键列表; fallback_policy: ... # 当前选择器专用的回退策略; locality_weight_aware: "..." # 是否在将请求路由到子集时考虑端点的位置和位置权重;存在一些潜在的缺陷; scale_locality_weight: "..." # 是否将子集与主机中的主机比率来缩放每个位置的权重; panic_mode_any: "..." # 是否在配置回退策略且其相应的子集无法找到主机时尝试从整个集群中选择主机; - 对于每个选择器,Envoy会遍历主机并检查其“envoy.lb”过滤器元数据,并为每个惟一的键值组 合创建一个子集; - 若某主机元数据可以匹配该选择器中指定每个键,则会将该主机添加至此选择器中;这同时意味 着,一个主机可能同时满足多个子集选择器的适配条件,此时,该主机将同时隶属于多个子集; - 若所有主机均未定义元数据,则不会生成任何子集; (3) 路由元数据匹配(metadata_match) 使用负载均衡器子集还要求在路由条目上配置元数据匹配(metadata_match)条件,它用于在路由期 间查找特定子集; - 仅在上游集群中与metadata_match中设置的元数据匹配的子集时才能完成流量路由; - 使用了weighted_clusters定义路由目标时,其内部的各目标集群也可定义专用的metadata_match; 不存在与路由元数据匹配的子集时,将启用后退策略; 配置示例: routes: - name: ... match: {...} route: {...} # 路由目标,cluster和weighted_clusters只能使用其一; cluster: # 此处路由给集群 metadata_match: {...} # 子集负载均衡器使用的端点元数据匹配条件; # 若使用了weighted_clusters且内部定义了metadat_match, # 则元数据将被合并,且weighted_cluster中定义的值优先; # 过滤器名称应指定为envoy.lb; filter_metadata: {...} # 元数据过滤器 envoy.lb: {...} key1: value1 # 过滤的条件 key2: value2 # 过滤的条件 ... weighted_clusters: {...} clusters: [] - name: ... weight: ... metadata_match: {...}
节点信息表格
下图定义了10个端点(endpoint)、4个标签(stage 目标环境、version 版本、type 类型、xlarge)
配置示例
clusters: - name: webclusters lb_policy: ROUND_ROBIN lb_subset_config: # 子集负载均衡配置 fallback_policy: DEFAULT_SUBSET # 回退策略:如果路由的子集不存在,就使用这个默认子集 default_subset: # 默认子集配置 stage: prod # 默认子集拥有的标签:key是stage,value是prod的 version: '1.0' # 默认子集拥有的标签:key是version,value是1.0的 type: std # 默认子集拥有的标签:key是type,value是std的 # 上述3个标签都必须同时存在,才能满足要求,对应之前的表格就是:e1、e2,那么默认子集就是e1、e2组成 subset_selectors: # 负载均衡子集选择器(这里筛选出来的子集,可以作为路由目标直接使用) - keys: [stage, type] # 根据这俩标签选择子集,对应子集就有3组,分别是:[prod、std] e1-e4、[prod、bigmem] e5 e6、[dev、std] e7 - keys: [stage, version] # 对应子集:[prod、1.0] e1 e2 e5、[prod、1.1] e3 e4 e6、[dev、1.2-pre] e7 - keys: [version] # 对应子集:1.0 e1 e2 e5、1.1 e3 e4 e6、1.2-pre e7 - keys: [xlarge, version] # 对应子集:e1 这些配置好了过后,就可以尝试着通过路由来调用这些子集了,看下面
路由时,路由上配置的元数据用于匹配和过滤出目标子集; 路由匹配条件也需要使用“envoy.lb”过滤器。 配置示例: routes: - match: prefix: "/" headers: - name: x-custom-version exact_match: pre-release route: cluster: webcluster1 # 如果请求头为x-custom-version,值是pre-release,就路由到webcluster1 metadata_match: filter_metadata: envoy.lb: # 优先将流量调度到具有下面2个标签的子集上,这里其实就变相实现了基于请求头的灰度 version: 1.2-pre stage: dev - match: prefix: "/" route: weighted_clusters: # 权重 clusters: - name: webcluster1 weight: 90 metadata_match: filter_metadata: envoy.lb: version: '1.0' # 满足该条件的子集会被调度90%的流量 - name: webcluster1 weight: 10 metadata_match: filter_metadata: envoy.lb: version: '1.1' # 满足该条件的子集会被调度10%的流量 # 上述的操作就相当于:在一个svc中有2个不同版本的endpoint,根据子集的不同来承接不同的流量比例,相当于实现了基于流量的灰度 metadata_match: filter_metadata: envoy.lb: stage: prod
# front-envoy.yaml配置 [root@k8s-harbor01 lb-subsets]# 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: "/" headers: - name: x-custom-version exact_match: pre-release route: cluster: webcluster1 metadata_match: filter_metadata: envoy.lb: version: "1.2-pre" stage: "dev" - match: prefix: "/" headers: - name: x-hardware-test exact_match: memory route: cluster: webcluster1 metadata_match: filter_metadata: envoy.lb: type: "bigmem" stage: "prod" - match: prefix: "/" route: weighted_clusters: clusters: - name: webcluster1 weight: 90 metadata_match: filter_metadata: envoy.lb: version: "1.0" - name: webcluster1 weight: 10 metadata_match: filter_metadata: envoy.lb: version: "1.1" metadata_match: filter_metadata: envoy.lb: stage: "prod" http_filters: - name: envoy.filters.http.router clusters: - name: webcluster1 connect_timeout: 0.5s type: STRICT_DNS lb_policy: ROUND_ROBIN load_assignment: cluster_name: webcluster1 endpoints: - lb_endpoints: - endpoint: address: socket_address: address: e1 port_value: 80 metadata: filter_metadata: envoy.lb: stage: "prod" version: "1.0" type: "std" xlarge: true - endpoint: address: socket_address: address: e2 port_value: 80 metadata: filter_metadata: envoy.lb: stage: "prod" version: "1.0" type: "std" - endpoint: address: socket_address: address: e3 port_value: 80 metadata: filter_metadata: envoy.lb: stage: "prod" version: "1.1" type: "std" - endpoint: address: socket_address: address: e4 port_value: 80 metadata: filter_metadata: envoy.lb: stage: "prod" version: "1.1" type: "std" - endpoint: address: socket_address: address: e5 port_value: 80 metadata: filter_metadata: envoy.lb: stage: "prod" version: "1.0" type: "bigmem" - endpoint: address: socket_address: address: e6 port_value: 80 metadata: filter_metadata: envoy.lb: stage: "prod" version: "1.1" type: "bigmem" - endpoint: address: socket_address: address: e7 port_value: 80 metadata: filter_metadata: envoy.lb: stage: "dev" version: "1.2-pre" type: "std" lb_subset_config: fallback_policy: DEFAULT_SUBSET default_subset: stage: "prod" version: "1.0" type: "std" subset_selectors: - keys: ["stage", "type"] - keys: ["stage", "version"] - keys: ["version"] - keys: ["xlarge", "version"] 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.yaml配置 [root@k8s-harbor01 lb-subsets]# cat docker-compose.yaml version: '3' services: front-envoy: image: envoyproxy/envoy-alpine:v1.21-latest volumes: - ./front-envoy.yaml:/etc/envoy/envoy.yaml networks: envoymesh: ipv4_address: 172.31.33.2 expose: # Expose ports 80 (for general traffic) and 9901 (for the admin server) - "80" - "9901" e1: image: ikubernetes/demoapp:v1.0 hostname: e1 networks: envoymesh: ipv4_address: 172.31.33.11 aliases: - e1 expose: - "80" e2: image: ikubernetes/demoapp:v1.0 hostname: e2 networks: envoymesh: ipv4_address: 172.31.33.12 aliases: - e2 expose: - "80" e3: image: ikubernetes/demoapp:v1.0 hostname: e3 networks: envoymesh: ipv4_address: 172.31.33.13 aliases: - e3 expose: - "80" e4: image: ikubernetes/demoapp:v1.0 hostname: e4 networks: envoymesh: ipv4_address: 172.31.33.14 aliases: - e4 expose: - "80" e5: image: ikubernetes/demoapp:v1.0 hostname: e5 networks: envoymesh: ipv4_address: 172.31.33.15 aliases: - e5 expose: - "80" e6: image: ikubernetes/demoapp:v1.0 hostname: e6 networks: envoymesh: ipv4_address: 172.31.33.16 aliases: - e6 expose: - "80" e7: image: ikubernetes/demoapp:v1.0 hostname: e7 networks: envoymesh: ipv4_address: 172.31.33.17 aliases: - e7 expose: - "80" networks: envoymesh: driver: bridge ipam: config: - subnet: 172.31.33.0/24 # 测试脚本 [root@k8s-harbor01 lb-subsets]# cat test.sh #!/bin/bash declare -i v10=0 declare -i v11=0 for ((counts=0; counts<200; counts++)); do if curl -s http://$1/hostname | grep -E "e[125]" &> /dev/null; then # $1 is the host address of the front-envoy. v10=$[$v10+1] else v11=$[$v11+1] fi done echo "Requests: v1.0:v1.1 = $v10:$v11" # 启动容器 [root@k8s-harbor01 lb-subsets]# docker-compose up # 然后新开一个窗口 # 新开一个窗口 [root@k8s-harbor01 lb-subsets]# ./test.sh 172.31.33.2 Requests: v1.0:v1.1 = 183:17 # 请求流量比例,这里请求由于没有传递请求头,所以走的是权重配置weighted_clusters # 指定请求头测试 [root@k8s-harbor01 lb-subsets]# curl -H "x-hardware-test: memory" 172.31.33.2/hostname ServerName: e6 [root@k8s-harbor01 lb-subsets]# curl -H "x-hardware-test: memory" 172.31.33.2/hostname ServerName: e5 [root@k8s-harbor01 lb-subsets]# curl -H "x-hardware-test: memory" 172.31.33.2/hostname ServerName: e5 [root@k8s-harbor01 lb-subsets]# curl -H "x-hardware-test: memory" 172.31.33.2/hostname ServerName: e6
假设我们的网格,按地域划分为华北、华南、华东三个地区。
那假设客户端是从华东地区发起的请求,那么通过配置区域管制路由,就可以让请求优先调度到处于华东地区的上游节点。
当然,为了避免同一区域客户端请求量过大,把后端服务给打死,区域感知路由会自动分配一部分流量到其他区域。
Envoy将流量路由到本地区域,还是进行跨区域路由,取决于始发集群和上游集群中健康主机的百分比。有两种情况:
(1)始发集群的本地区域百分比大于上游集群中本地区域的百分比(客户端数量大于服务端数量):Envoy 计算可以直接路由到上游集群的本地区域的请求的百分比,其余的请求被路由到其它区域;
(2)始发集群本地区域百分比小于上游集群中的本地区域百分比(客户端数量小于服务端数量):可实现所有请求的本地区域路由,并可承载一部分其它区域的跨区域路由(帮其他区域承接一点请求);
注意:区域感知路由仅支持0优先级;
common_lb_config:
zone_aware_lb_config: # 区域感知配置
"routing_enabled": "{...}", # 值类型为百分比,用于配置在多大比例的请求流量上启用区域感知路由机制,默认为100%,;
"min_cluster_size": "{...}" # 配置区域感知路由所需的最小上游集群大小,上游集群大小小于指定的值时即使配置了区域感知路由也不会执行 区域感知路由;默认值为6,可用值为64位整数;
(1)始发集群(客户端)和上游集群(服务端)都未处于恐慌模式;
(2)启用了区域感知路由;
(3)始发集群与上游集群具有相同数量的区域; # 意思是服务端在华东、客户端也必须在华东
(4)上游集群具有能承载所有请求流量的主机; # 如果没有,流量就可能会跨区域分配
(1)调度时,可用的目标上游主机范围将根据下游发出的请求连接上的元数据进行选定,并将请求调度至此范围内的某主机;
◼ 连接请求会被指向到将其重定向至Envoy之前的原始目标地址;换句话说,是直接转发
连接到客户端连接的目标地址,即没有做负载均衡;
◆ 原始连接请求发往Envoy之前会被iptables的REDIRECT或TPROXY重定向,其目标地址也就会发生
变动;
◼ 这是专用于原始目标集群(Original destination,即type参数为ORIGINAL_DST的集群)
的负载均衡器;
◼ 请求中的新目标由负载均衡器添按需添加到集群中,而集群也会周期性地清理不再被使
用的目标主机;
(2)Envoy也能够基于HTTP标头x-envoy-original-dst-host获取原始目标;
(3)需要注意的是,原始目标集群不与其它负载均衡策略相兼容;
上游服务(被调用者,即服务提供者)因压力过大而变得响应过慢甚至失败时,下游服务(服务消费者)通过暂时切断对上游的请求调用达到牺牲局部,保全上游甚至是整体之目的;
总结起来,熔断是分布式应用常用的一种流量管理模式,它能够让应用程序免受上游服务失败、延迟峰值或其它网络异常的侵害;
Envoy在网络层强制进行断路限制,于是不必单独在应用层进行配置;
(1)熔断打开(Open)
在固定时间窗口内,检测到的失败指标达到指定的阈值时启动熔断;
这个时候所有请求会直接失败而不再发往后端端点;
(2)熔断半打开(Half Open)
断路器在工作一段时间后自动切换至半打开状态,并根据下一次请求的返回结果判定状态切换。
- 请求成功:转为熔断关闭状态;
- 请求失败:切回熔断打开状态;
(3)熔断关闭(Closed)
一定时长后上游服务可能会变得再次可用,此时下游即可关闭熔断,并再次请求其服务;
在envoy中,上述三种状态都是靠断路器实现。
(1)Envoy支持多种类型的完全分布式断路机制,达到由其定义的阈值时,相应的断路器即会溢出:
- 集群最大连接数:Envoy同上游集群建立的最大连接数,仅适用于HTTP/1.1,因为HTTP/2可以链路复用;
- 集群最大请求数:在给定的时间,集群中的所有主机未完成的最大请求数,仅适用于HTTP/2;
- 集群可挂起的最大请求数:连接池满载时所允许的等待队列的最大长度;
- 集群最大活动并发重试次数:给定时间内集群中所有主机可以执行的最大并发重试次数;
- 集群最大并发连接池:可以同时实例化出的最大连接池数量;
(2)每个断路器都可在每个集群及每个优先级的基础上进行配置和跟踪,它们可分别拥有各自不同的设定;
(3)注意:在Istio中,熔断的功能通过连接池(连接池管理)和故障实例隔离(异常点检测)进行定义,而Envoy的断路器通常仅对应于Istio中的连接池功能;
- 通过限制某个客户端对目标服务的连接数、访问请求、队列长度和重试次数等,避免对一个服务的过量访问。
- 某个服务实例频繁超时或者出错时交其昨时逐出,以避免影响整个服务。
(1)连接池的常用指标
- 最大连接数:表示在任何给定时间内, Envoy 与上游集群建立的最大连接数,适用于 HTTP/1.1;
- 每个连接最大请求数:表示在任何给定时间内,上游集群中所有主机可以处理的最大请求数;若设为 1 则会禁止 keep alive 特性;
- 最大请求重试次数:在指定时间内对目标主机最大重试次数;
- 连接超时时间:TCP 连接超时时间,最小值必须大于 1ms;最大连接数和连接超时时间是对 TCP 和 HTTP 都有效的通用连接设置;
- 最大等待请求数:待处理请求队列的长度,若该断路器溢出,集群的 upstream_rq_pending_overflow计数器就会递增;
(2)断路器的常用指标(Istio上下文使用)
- 连续错误响应个数:在一个检查周期内,连续出现5xx错误的个数,例502、503状态码;
- 检查周期:将会对检查周期内的响应码进行筛选;
- 隔离实例比例:上游实例中,允许被隔离的最大比例;采用向上取整机制,假设有10个实例,13%则最多会隔离2个实例;
- 最短隔离时间:实例第一次被隔离的时间,之后每次隔离时间为隔离次数与最短隔离时间的乘积;
(1)断路器的相关设置由circuit_breakers定义。 (2)与连接池相关的参数有两个定义在cluster的上下文 --- clusters: - name: ... ... connect_timeout: ... # TCP连接的超时时长,即主机网络连接超时,合理的设置可以能够改善因调用服务变慢而导致整个链接变慢的情形; max_requests_per_connection: ... # 每个连接可以承载的最大请求数,HTTP/1.1和HTTP/2的连接池均受限于此设置,无设置则无限制,1表示禁用keep-alive ... circuit_breakers: {...} # 熔断相关的配置,可选; threasholds: [] # 适用于特定路由优先级的相关指标及阈值的列表; - priority: ... # 当前断路器适用的路由优先级; max_connections: ... # 可发往上游集群的最大并发连接数,仅适用于HTTP/1,默认为1024;超过指定数量的连接则将其短路; max_pending_requests: ... # 允许请求服务时的可挂起的最大请求数,默认为1024;;超过指定数量的连接则将其短路; max_requests: ... # Envoy可调度给上游集群的最大并发请求数,默认为1024;仅适用于HTTP/2 max_retries: ... # 允许发往上游集群的最大并发重试数量(假设配置了retry_policy),默认为3; track_remaining: ... # 其值为true时表示将公布统计数据以显示断路器打开前所剩余的资源数量;默认为false; max_connection_pools: ... # 每个集群可同时打开的最大连接池数量,默认为无限制;
# envoy-sidecar-proxy.yaml [root@k8s-harbor01 circuit-breaker]# 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 } circuit_breakers: # 断路器配置 thresholds: max_connections: 1 max_pending_requests: 1 max_retries: 2 # envoy-sidecar-proxy.yaml [root@k8s-harbor01 circuit-breaker]# 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 } circuit_breakers: # 断路器配置 thresholds: # 阈值配置 max_connections: 1 # 最大连接数1 max_pending_requests: 1 # 最大挂起连接数1 max_retries: 2 # 最大重试次数2 # 上述配置只是为了测试才配置这么小 # 测试可以用下面这个工具测试 # 项目地址:https://github.com/fortio/fortio fortio load -c 2 -qps 0 -n 20 -loglevel Warning URL # docker-compose.yaml [root@k8s-harbor01 circuit-breaker]# cat docker-compose.yaml version: '3' services: front-envoy: environment: - ENVOY_UID=0 - ENVOY_GID=0 #image: envoyproxy/envoy-alpine:v1.21-latest image: envoyproxy/envoy:v1.23-latest volumes: - ./front-envoy.yaml:/etc/envoy/envoy.yaml networks: - envoymesh expose: # Expose ports 80 (for general traffic) and 9901 (for the admin server) - "80" - "9901" webserver01-sidecar: environment: - ENVOY_UID=0 - ENVOY_GID=0 image: envoyproxy/envoy:v1.23-latest volumes: - ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml hostname: red networks: envoymesh: ipv4_address: 172.31.35.11 aliases: - webservice1 - red webserver01: image: ikubernetes/demoapp:v1.0 environment: - PORT=8080 - HOST=127.0.0.1 network_mode: "service:webserver01-sidecar" depends_on: - webserver01-sidecar webserver02-sidecar: environment: - ENVOY_UID=0 - ENVOY_GID=0 image: envoyproxy/envoy:v1.23-latest volumes: - ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml hostname: blue networks: envoymesh: ipv4_address: 172.31.35.12 aliases: - webservice1 - blue webserver02: image: ikubernetes/demoapp:v1.0 environment: - PORT=8080 - HOST=127.0.0.1 network_mode: "service:webserver02-sidecar" depends_on: - webserver02-sidecar webserver03-sidecar: environment: - ENVOY_UID=0 - ENVOY_GID=0 image: envoyproxy/envoy:v1.23-latest volumes: - ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml hostname: green networks: envoymesh: ipv4_address: 172.31.35.13 aliases: - webservice1 - green webserver03: image: ikubernetes/demoapp:v1.0 environment: - PORT=8080 - HOST=127.0.0.1 network_mode: "service:webserver03-sidecar" depends_on: - webserver03-sidecar webserver04-sidecar: environment: - ENVOY_UID=0 - ENVOY_GID=0 image: envoyproxy/envoy:v1.23-latest volumes: - ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml hostname: gray networks: envoymesh: ipv4_address: 172.31.35.14 aliases: - webservice2 - gray webserver04: image: ikubernetes/demoapp:v1.0 environment: - PORT=8080 - HOST=127.0.0.1 network_mode: "service:webserver04-sidecar" depends_on: - webserver04-sidecar webserver05-sidecar: environment: - ENVOY_UID=0 - ENVOY_GID=0 image: envoyproxy/envoy:v1.23-latest volumes: - ./envoy-sidecar-proxy.yaml:/etc/envoy/envoy.yaml hostname: black networks: envoymesh: ipv4_address: 172.31.35.15 aliases: - webservice2 - black webserver05: image: ikubernetes/demoapp:v1.0 environment: - 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.35.0/24 # send-requests.sh [root@k8s-harbor01 circuit-breaker]# cat send-requests.sh #!/bin/bash # if [ $# -ne 2 ] then echo "USAGE: $0 <URL> <COUNT>" exit 1; fi URL=$1 COUNT=$2 c=1 #interval="0.2" while [[ ${c} -le ${COUNT} ]]; do #echo "Sending GET request: ${URL}" curl -o /dev/null -w '%{http_code}\n' -s ${URL} & (( c++ )) # sleep $interval done wait # 启动容器 [root@k8s-harbor01 circuit-breaker]# docker-compose up # 请求测试 [root@k8s-harbor01 circuit-breaker]# ./send-requests.sh 172.31.35.2 300 # 发起300个请求,通过返回可以看到后续的请求已经被熔断了 200 200 200 200 200 503 503 503 503 503 503 503 503 503 503
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。