目录
Envoy入门并不简单,可以说有些陡峭,本文尽可能帮助降低入门门槛。本文内容主要基于Envoy-1.12.2版本,官方链接:
https://www.envoyproxy.io/docs/envoy/v1.12.2/configuration/overview/v2_overview |
Envoy是Lyft开源的一个C++实现的代理(Proxy),和Nginx及HAProxy类似,可代理L3/L4层和L7层。代理是它最核心和基础的功能,它也是服务网格框架Istio的Sidecar。
从研究Envoy的配置文件开始,Envoy支持多种格式的配置文件:YAML、JSON和PB等,其中YAML使用最多,官方示例基本都是YAML格式的。
配置文件中涉及多个概念,所以最好先将概念了解清楚,然后使用最简单的配置走一遍流程,如果会用Docker则这一步会比较简单。在了解概念之前,最好先了解Envoy的基本架构,以弄明白各概念间的协作关系。
Envoy的源码托管在Github上:https://github.com/envoyproxy/envoy。
缩写 | 全写 | 说明 |
lb | load balance | 负载均衡 |
lb_policy | load balance policy | 负载均衡策略 |
SNI | Server Name Indication | TLS的扩展,用来解决一个服务器拥有多个域名或证书。 工作原理:在连接到服务器建立SSL链接前,先发送要访问的域名,服务器根据这个域名返回一个合适的证书。 |
TLS | Transport Layer Security | 传输层安全性协议 |
L3 | Layer 3 | 网络层(IP) |
L4 | Layer 4 | 传输层(PORT) |
L7 | Layer 7 | 应用层(HTTP) |
L2 | Layer 2 | 数据链路层(MAC) |
YAML | YAML Ain't a Markup Language | 以数据做为中心的标记语(Yet Another Markup Language) |
JSON | JavaScript Object Notation | JS 对象简谱,一种轻量级的数据交换格式 |
REST | Representational State Transfer | 表述性状态传递,一种软件架构风格 |
gRPC | Google RPC | 谷歌开源的RPC框架 |
pb | Protocol buffers | 谷歌开发的一种数据描述语言,常被简称为protobuf |
Endpoint |
| 可理解为IP端口对,每个服务需要配置一个Endpoint |
下图展示了Envoy的外部架构,从图很容易看到服务间、应用和服务间都是通过Envoy串联起来的,Envoy是它们间的高速公路,Envoy的作用就是在各部分间转发读写请求(也可叫读写操作),所以Envoy是名副其实的代理(Proxy)。
外部架构展示了Envoy的作用,但无法窥见它是如何实现的,Envoy的内部结构展示出了它的实现原理。
其中过滤器(Filter)是Envoy的核心中的核心,多Filter形成了过滤器链(Chain),和iptables的Chain类似,请求经过过滤器链后到达目的服务(Service)。
如果将Envoy看成黑盒,则它所处位置可定义成如下图所示:
对Envoy架构有初步了解后,再通过对Enovy配置文件的了解,将对掌握Enovy十分有帮助。Envoy的配置文件定义了代理转发规则,规则也可通过gRPC或REST动态拉取。
Envoy配置文件支持四种书写格式:json、yaml、pb和pb_text,官方文档和示例基本使用yaml格式。可将Envoy配置文件分解成三大部分:
admin | 定义Envoy进程的管理端口 |
static_resources | 静态配置,定义静态资源 |
dynamic_resources | 态配置,定义动态资源,static_resources中一些配置可通过服务调用(接口调用)动态拉取。 |
管理配置,比较简单,内容一般如下:
admin: access_log_path: /tmp/admin_access.log address: socket_address: { address: 192.168.0.1, port_value: 9901 } |
通过admin可以查询到大量的配置、监控和运维等数据:
static_resources又可分解为两大部分:
listeners | 定义监听器,服务下游(downstream) |
clusters | 定义上游(upstream)的微服务集群 |
在listeners中,可以定义多个listener,每个listener由三部分组成:
name | 定义listener名称 |
address | 定义listener的监听地址和端口 |
filter_chains | 定义过滤器(Filter)链,这是最核心和最复杂的部分 |
定义上游集群,Envoy最基础的功能即是将来自下游的请求转发给上游。clusters的内容包括五大部分,其中load_assignment部分是核心:
name | 下游集群名,可定义一或多个 |
connect_timeout | 连接上游的超时时长,可带单位,如“0.25s”表示250毫秒 |
type | 集群类型,如STATIC、STRICT_DNS、LOGICAL_DNS和EDS等 |
lb_policy | 负载均衡策略,如ROUND_ROBIN表示轮询 |
load_assignment | type为STATIC、STRICT_DNS和LOGICAL_DNS时,如果type为EDS则使用eds_cluster_config |
lb_policy可取值:
ROUND_ROBIN | 轮询 |
LEAST_REQUEST | 请求最少 |
RING_HASH | 环形哈希 |
RANDOM | 随机 |
MAGLEV | 一致性哈希算法 |
CLUSTER_PROVIDED | 定制 |
load_assignment示例:
load_assignment: cluster_name: some_service endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 port_value: 1234 |
clusters示例:
clusters: - name: some_service connect_timeout: 0.25s type: STATIC lb_policy: ROUND_ROBIN load_assignment: cluster_name: some_service endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 port_value: 1234 |
static_resources中的四种动态资源可通过动态服务发现(xDS)来动态配置,共有六种动态服务发现:
缩写 | 全写 | 说明 |
LDS | Listener Discovery Service | 监听器(资源)发现服务,解决有哪些监听器问题 |
CDS | Cluster Discovery Service | 集群(资源)发现服务,解决有哪些集群问题 |
RDS | Route Discovery Service | 路由(资源)发现服务,解决有哪些路由规则问题 |
EDS | Endpoint Discovery Service | 端点(资源)发现服务,解决集群内有哪些端点问题 |
ADS | Aggregated Discovery Service | 这并不是一个独立的发现服务,而是对其它发现服务的聚合 |
动态配置在启动Envoy进程时,需要指定id和cluster,否则报错“node 'id' and 'cluster' are required.”。
怎么理解dynamic_resources?在static_resouces基础上,动态拉取动态资源,即有动态资源配置不是直接写在配置中,而是需要通过服务调用动态取得,Envoy支持gRPC/HTTP2和REST两种方式动态拉取。
可以部分动态配置,也可全动态配置。下列为一个官方的全动态配置示例:
admin: access_log_path: /tmp/admin_access.log address: socket_address: { address: 127.0.0.1, port_value: 9901 }
dynamic_resources: lds_config: api_config_source: api_type: GRPC grpc_services: envoy_grpc: cluster_name: xds_cluster cds_config: api_config_source: api_type: GRPC grpc_services: envoy_grpc: cluster_name: xds_cluster
static_resources: clusters: - name: xds_cluster connect_timeout: 0.25s type: STATIC lb_policy: ROUND_ROBIN http2_protocol_options: {} upstream_connection_options: # configure a TCP keep-alive to detect and reconnect to the admin # server in the event of a TCP socket half open connection tcp_keepalive: {} load_assignment: cluster_name: xds_cluster endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 port_value: 5678 |
上例为gRPC/HTTP2方式动态拉取配置,提供配置的服务名为xds_cluster,服务端口为“127.0.0.1:5678”。
POST /envoy.api.v2.ClusterDiscoveryService/StreamClusters |
gRPC服务定义在文件cds.proto,链接地址:
https://github.com/envoyproxy/envoy/blob/v1.12.2/api/envoy/api/v2/cds.proto |
- EDS
POST /envoy.api.v2.EndpointDiscoveryService/StreamEndpoints |
gRPC服务定义在文件eds.proto,链接地址:
https://github.com/envoyproxy/envoy/blob/v1.12.2/api/envoy/api/v2/eds.proto |
- LDS
POST /envoy.api.v2.ListenerDiscoveryService/StreamListeners |
gRPC服务定义在文件lds.proto,链接地址:
https://github.com/envoyproxy/envoy/blob/v1.12.2/api/envoy/api/v2/lds.proto |
- RDS
POST /envoy.api.v2.RouteDiscoveryService/StreamRoutes |
gRPC服务定义在文件rds.proto,链接地址:
https://github.com/envoyproxy/envoy/blob/v1.12.2/api/envoy/api/v2/rds.proto |
POST /v2/discovery:clusters |
- EDS
POST /v2/discovery:endpoints |
- LDS
POST /v2/discovery:listeners |
- RDS
POST /v2/discovery:routes |
试跑Enovy要求有Docker基础(如无基础可参考《Docker入门之安装Docker》和《Docker入门之创建镜像初步》),从源代码构建会有些复杂,所以本文直接使用官方提供的Docker景象作为试跑对象。试跑前提要求Docker环境已准备好,并且试跑机要能访问外网。
代理https://www.baidu.com,当访问本机的8080端口时,实际为访问被代理的https://www.baidu.com。
执行下列命令拉取Envoy的Docker镜像:
docker pull envoyproxy/envoy |
在本地主备配置文件“/tmp/bd.yaml”,文件内容如下:
$ cat /tmp/bd.yaml admin: access_log_path: /tmp/admin_access.log address: socket_address: protocol: TCP address: 0.0.0.0 # 管理地址 port_value: 8081 # 管理端口
static_resources: listeners: # 监听器数组 - name: listener_0 # 监听器 address: socket_address: protocol: TCP address: 0.0.0.0 # 监听地址 port_value: 8080 # 监听端口 filter_chains: # 过滤器链 - filters: # 过滤器数组 - name: envoy.http_connection_manager # 过滤器名 typed_config: "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager stat_prefix: ingress_http route_config: # 路由配置 name: local_route # 路由配置名 virtual_hosts: # 虚拟主机数组 - name: local_service domains: ["*"] # 需代理的域名数组 routes: # 定义路由 - match: prefix: "/" # 匹配规则 route: host_rewrite: www.baidu.com # 将HOST重写为 cluster: bd_service # 下游集群名,通过它找到下游集群的配置 http_filters: - name: envoy.router clusters: # 下游集群数组 - name: bd_service # 下游集群名 connect_timeout: 0.25s # 连接下游的超时时长 type: LOGICAL_DNS # Comment out the following line to test on v6 networks dns_lookup_family: V4_ONLY # 域名查找范围,这里表示只查找IPV4地址 lb_policy: ROUND_ROBIN # 负载均衡策略 load_assignment: cluster_name: bd_service endpoints: - lb_endpoints: - endpoint: address: socket_address: address: www.baidu.com port_value: 443 transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/envoy.api.v2.auth.UpstreamTlsContext sni: www.baidu.com
|
由于代理的是域名www.baidu.com,所以clusters的type值需为LOGICAL_DNS或strict_dns,type还有如下几个取值(不区分大小写):
STATIC | 缺省值,在集群中列出所有可代理的主机(Endpoints) |
LOGICAL_DNS | Envoy使用DNS添加主机,但如果DNS不再返回时,也不会丢弃 |
STRICT_DNS | Envoy将监控DNS,而每个匹配的A记录都将被认为是有效的 |
OriginalDst |
|
EDS | Envoy调用一个外部的gRPC或REST服务查找被代理的主机(Endpoints) |
自定义值 |
|
访问https://www.baidu.com,一定要配置transport_socket,否则将报错“upstream connect error or disconnect/reset before headers. reset reason: connection failure”。如果是访问http://www.baidu.com,则不用配置transport_socket,sni不是必须的。
在宿主机上,执行下列命令启动Envoy容器:
docker run -it --rm -v=/tmp:/tmp -p 8080:8080 -p 8081:8081 envoyproxy/envoy bash |
可在宿主机上执行命令“docker ps|grep envoy”,检查Envoy容器是否起来了。
在Envoy容器中,执行下列命令拉起Envoy进程:
envoy -c /tmp/bd.yaml |
启动成功可看到如下日志:
[info][config] [source/server/configuration_impl.cc:62] loading 0 static secret(s) [info][config] [source/server/configuration_impl.cc:68] loading 1 cluster(s) [info][config] [source/server/configuration_impl.cc:72] loading 1 listener(s) [info][config] [source/server/configuration_impl.cc:97] loading tracing configuration [info][config] [source/server/configuration_impl.cc:117] loading stats sink configuration [info][main] [source/server/server.cc:549] starting main dispatch loop [info][upstream] [source/common/upstream/cluster_manager_impl.cc:161] cm init: all clusters initialized [info][main] [source/server/server.cc:528] all clusters initialized. initializing init manager [info][config] [source/server/listener_manager_impl.cc:578] all dependencies initialized. starting workers |
假设Envoy容器所在机器IP为192.168.1.21,则访问http://192.168.1.21:8080等同于访问https://www.baidu.com。
Envoy定时访问EDS服务取EDS配置。
$ cat /tmp/bd.yaml admin: access_log_path: /tmp/admin_access.log address: socket_address: protocol: TCP address: 0.0.0.0 # 管理地址 port_value: 8081 # 管理端口
static_resources: listeners: # 监听器数组 - name: listener_0 # 监听器 address: socket_address: protocol: TCP address: 0.0.0.0 # 监听地址 port_value: 8080 # 监听端口 filter_chains: # 过滤器链 - filters: # 过滤器数组 - name: envoy.http_connection_manager # 过滤器名 typed_config: "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager stat_prefix: ingress_http route_config: # 路由配置 name: local_route # 路由配置名 virtual_hosts: # 虚拟主机数组 - name: local_service domains: ["*"] # 需代理的域名数组 routes: # 定义路由 - match: prefix: "/" # 匹配规则 route: host_rewrite: www.baidu.com # 将HOST重写为 cluster: bd_service # 下游集群名,通过它找到下游集群的配置 http_filters: - name: envoy.router clusters: # 下游集群数组 - name: bd_service # 下游集群名 connect_timeout: 0.25s # 连接下游的超时时长 type: eds lb_policy: ROUND_ROBIN # 负载均衡策略 eds_cluster_config: eds_config: api_config_source: api_type: rest refresh_delay: "10s" # 动态一定要有这个配置 cluster_names: [xds_cluster] # 这里并不提供静态的endpoints,需访问EDS服务得到 transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/envoy.api.v2.auth.UpstreamTlsContext sni: www.baidu.com - name: xds_cluster connect_timeout: 0.25s type: static lb_policy: ROUND_ROBIN load_assignment: cluster_name: xds_cluster endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 # EDS的服务地址 port_value: 2020 # EDS的服务端口 |
// 演示Envoy的动态EDS(Endpoint Discovery Service) // 执行命令“go build eds.go ”,即生成可执行程序eds package main
import ( "encoding/json" "fmt" "net/http" "time" )
// { // "version_info": "0", // "resources": [ // { // "@type": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment", // "cluster_name": "some_service", // "endpoints": [ // { // "lb_endpoints": [ // { // "endpoint": { // "address": { // "socket_address": { // "address": "127.0.0.2", // "port_value": 1234 // } // } // } // } // ] // } // ] // } // ] // }
type SocketAddress struct { Address string `json:"address"` PortValue int `json:"port_value"` }
type Address struct { SocketAddress SocketAddress `json:"socket_address"` }
type Endpoint struct { Address Address `json:"address"` }
type LbEndpoint struct { Endpoint Endpoint `json:"endpoint"` }
type LbEndpoints struct { LbEndpoints []LbEndpoint `json:"lb_endpoints"` }
type Resource struct { Type string `json:"@type"` ClusterName string `json:"cluster_name"` Endpoints []LbEndpoints `json:"endpoints"` }
type EDS struct { VersionInfo string `json:"version_info"` Resources []Resource `json:"resources"` }
func DiscoveryEndpointsHandler(w http.ResponseWriter, r *http.Request) { // LbEndpoint var lb_endpoint1 LbEndpoint lb_endpoint1.Endpoint.Address.SocketAddress.Address = "180.101.49.12" lb_endpoint1.Endpoint.Address.SocketAddress.PortValue = 443
// LbEndpoint var lb_endpoint2 LbEndpoint lb_endpoint2.Endpoint.Address.SocketAddress.Address = "180.101.49.11" lb_endpoint2.Endpoint.Address.SocketAddress.PortValue = 443
// LbEndpoint var lb_endpoint3 LbEndpoint lb_endpoint3.Endpoint.Address.SocketAddress.Address = "14.215.177.38" lb_endpoint3.Endpoint.Address.SocketAddress.PortValue = 443
// LbEndpoints var lb_endpoints LbEndpoints lb_endpoints.LbEndpoints = append(lb_endpoints.LbEndpoints, lb_endpoint1) lb_endpoints.LbEndpoints = append(lb_endpoints.LbEndpoints, lb_endpoint2) lb_endpoints.LbEndpoints = append(lb_endpoints.LbEndpoints, lb_endpoint3)
// Resource var resource Resource resource.Type = "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment" resource.ClusterName = "bd_service" resource.Endpoints = append(resource.Endpoints, lb_endpoints)
// EDS var eds EDS eds.VersionInfo = "0" eds.Resources = append(eds.Resources, resource)
// struct to json jsonBytes, err := json.Marshal(eds) if err != nil { fmt.Println(err) }
// json to string str := string(jsonBytes)
// output json string now := time.Now() // 注意只能是“2006-01-02 15:04:05” fmt.Printf("[%s] %s\n", now.Format("2006-01-02 15:04:05"), string(jsonBytes)) fmt.Println(str) fmt.Fprintln(w, str) }
func main() { http.HandleFunc("/v2/discovery:endpoints", DiscoveryEndpointsHandler) http.ListenAndServe("0.0.0.0:2020", nil) } |
先将程序xds复制到容器中(以容器ID为0779d56f4f65为例):
docker cp eds 0779d56f4f65:/tmp |
进入容器:
docker container exec -it 0779d56f4f65 /bin/bash |
然后,在Envoy容器中启动EDS进程:
/tmp/eds |
在Envoy容器中启动Envoy进程:
envoy -c /tmp/bd.yaml --service-cluster xds_cluster --service-node 1 |