目录 1
1. 前言 2
1.1. Envoy是什么 2
1.2. 如何入门Envoy 2
1.3. Envoy的源码在哪 2
2. 缩略语 3
3. Envoy架构 3
3.1. 外部架构 3
3.2. 内部架构 4
4. Envoy配置文件 5
4.1. admin 5
4.2. static_resources 6
4.2.1. listeners 6
4.2.2. clusters 6
4.3. dynamic_resources 8
4.3.1. 怎么理解 8
4.3.2. 全动态配置 8
4.3.3. gRPC接口 9
4.3.4. REST接口 10
5. 试跑体验 10
5.1. 体验目标 10
5.2. 下载Envoy镜像 10
5.3. 准备Envoy配置文件 10
5.4. 启动Envoy容器 12
5.5. 启动Envoy进程 12
5.6. 体验效果 13
6. 动态配置 13
6.1. 动态配置EDS 13
6.1.1. Envoy配置文件 13
6.1.2. EDS服务源码 15
6.1.3. 启动EDS进程 17
6.1.4. 启动Envoy进程 18
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 |
POST /envoy.api.v2.EndpointDiscoveryService/StreamEndpoints |
gRPC服务定义在文件eds.proto,链接地址:
https://github.com/envoyproxy/envoy/blob/v1.12.2/api/envoy/api/v2/eds.proto |
POST /envoy.api.v2.ListenerDiscoveryService/StreamListeners |
gRPC服务定义在文件lds.proto,链接地址:
https://github.com/envoyproxy/envoy/blob/v1.12.2/api/envoy/api/v2/lds.proto |
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 |
POST /v2/discovery:endpoints |
POST /v2/discovery:listeners |
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 |