目錄
1. 前言
Envoy入門並不簡單,可以說有些陡峭,本文盡可能幫助降低入門門檻。本文內容主要基於Envoy-1.12.2版本,官方鏈接:
https://www.envoyproxy.io/docs/envoy/v1.12.2/configuration/overview/v2_overview |
1.1. Envoy是什么
Envoy是Lyft開源的一個C++實現的代理(Proxy),和Nginx及HAProxy類似,可代理L3/L4層和L7層。代理是它最核心和基礎的功能,它也是服務網格框架Istio的Sidecar。
1.2. 如何入門Envoy
從研究Envoy的配置文件開始,Envoy支持多種格式的配置文件:YAML、JSON和PB等,其中YAML使用最多,官方示例基本都是YAML格式的。
配置文件中涉及多個概念,所以最好先將概念了解清楚,然后使用最簡單的配置走一遍流程,如果會用Docker則這一步會比較簡單。在了解概念之前,最好先了解Envoy的基本架構,以弄明白各概念間的協作關系。
1.3. Envoy的源碼在哪
Envoy的源碼托管在Github上:https://github.com/envoyproxy/envoy。
2. 縮略語
縮寫 |
全寫 |
說明 |
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 |
3. Envoy架構
3.1. 外部架構
下圖展示了Envoy的外部架構,從圖很容易看到服務間、應用和服務間都是通過Envoy串聯起來的,Envoy是它們間的高速公路,Envoy的作用就是在各部分間轉發讀寫請求(也可叫讀寫操作),所以Envoy是名副其實的代理(Proxy)。
3.2. 內部架構
外部架構展示了Envoy的作用,但無法窺見它是如何實現的,Envoy的內部結構展示出了它的實現原理。
其中過濾器(Filter)是Envoy的核心中的核心,多Filter形成了過濾器鏈(Chain),和iptables的Chain類似,請求經過過濾器鏈后到達目的服務(Service)。
如果將Envoy看成黑盒,則它所處位置可定義成如下圖所示:
4. Envoy配置文件
對Envoy架構有初步了解后,再通過對Enovy配置文件的了解,將對掌握Enovy十分有幫助。Envoy的配置文件定義了代理轉發規則,規則也可通過gRPC或REST動態拉取。
Envoy配置文件支持四種書寫格式:json、yaml、pb和pb_text,官方文檔和示例基本使用yaml格式。可將Envoy配置文件分解成三大部分:
admin |
定義Envoy進程的管理端口 |
static_resources |
靜態配置,定義靜態資源 |
dynamic_resources |
態配置,定義動態資源,static_resources中一些配置可通過服務調用(接口調用)動態拉取。 |
4.1. admin
管理配置,比較簡單,內容一般如下:
admin: access_log_path: /tmp/admin_access.log address: socket_address: { address: 192.168.0.1, port_value: 9901 } |
通過admin可以查詢到大量的配置、監控和運維等數據:
4.2. static_resources
static_resources又可分解為兩大部分:
listeners |
定義監聽器,服務下游(downstream) |
clusters |
定義上游(upstream)的微服務集群 |
4.2.1. listeners
在listeners中,可以定義多個listener,每個listener由三部分組成:
name |
定義listener名稱 |
address |
定義listener的監聽地址和端口 |
filter_chains |
定義過濾器(Filter)鏈,這是最核心和最復雜的部分 |
4.2.2. clusters
定義上游集群,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 |
4.3. dynamic_resources
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.”。
4.3.1. 怎么理解
怎么理解dynamic_resources?在static_resouces基礎上,動態拉取動態資源,即有動態資源配置不是直接寫在配置中,而是需要通過服務調用動態取得,Envoy支持gRPC/HTTP2和REST兩種方式動態拉取。
4.3.2. 全動態配置
可以部分動態配置,也可全動態配置。下列為一個官方的全動態配置示例:
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”。
4.3.3. gRPC接口
1) CDS
POST /envoy.api.v2.ClusterDiscoveryService/StreamClusters |
gRPC服務定義在文件cds.proto,鏈接地址:
https://github.com/envoyproxy/envoy/blob/v1.12.2/api/envoy/api/v2/cds.proto |
2) 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 |
3) 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 |
4) 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 |
4.3.4. REST接口
1) CDS
POST /v2/discovery:clusters |
2) EDS
POST /v2/discovery:endpoints |
3) LDS
POST /v2/discovery:listeners |
4) RDS
POST /v2/discovery:routes |
5. 試跑體驗
試跑Enovy要求有Docker基礎(如無基礎可參考《Docker入門之安裝Docker》和《Docker入門之創建鏡像初步》),從源代碼構建會有些復雜,所以本文直接使用官方提供的Docker景象作為試跑對象。試跑前提要求Docker環境已准備好,並且試跑機要能訪問外網。
5.1. 體驗目標
代理https://www.baidu.com,當訪問本機的8080端口時,實際為訪問被代理的https://www.baidu.com。
5.2. 下載Envoy鏡像
執行下列命令拉取Envoy的Docker鏡像:
docker pull envoyproxy/envoy |
5.3. 准備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不是必須的。
5.4. 啟動Envoy容器
在宿主機上,執行下列命令啟動Envoy容器:
docker run -it --rm -v=/tmp:/tmp -p 8080:8080 -p 8081:8081 envoyproxy/envoy bash |
可在宿主機上執行命令“docker ps|grep envoy”,檢查Envoy容器是否起來了。
5.5. 啟動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 |
5.6. 體驗效果
假設Envoy容器所在機器IP為192.168.1.21,則訪問http://192.168.1.21:8080等同於訪問https://www.baidu.com。
6. 動態配置
6.1. 動態配置EDS
Envoy定時訪問EDS服務取EDS配置。
6.1.1. Envoy配置文件
$ 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的服務端口 |
6.1.2. 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) } |
6.1.3. 啟動EDS進程
先將程序xds復制到容器中(以容器ID為0779d56f4f65為例):
docker cp eds 0779d56f4f65:/tmp |
進入容器:
docker container exec -it 0779d56f4f65 /bin/bash |
然后,在Envoy容器中啟動EDS進程:
/tmp/eds |
6.1.4. 啟動Envoy進程
在Envoy容器中啟動Envoy進程:
envoy -c /tmp/bd.yaml --service-cluster xds_cluster --service-node 1 |