Envoy Proxy 与微服务实践

微服务介绍

  在传统模式下,如果微服务之间要进行通信,那么程序需要自己处理各种通信的细节,这就包括服务发现、熔断机制、超时重试和 tracing 等功能。这些功能通常实现为与某种编程语言相关的 library,这也导致了这样的 library 无法在不同的编程语言之间共享。


  更进一步,如果我们可以将这部分功能抽取出来,形成一个独立的进程,这样的进程称为 Sidecar。通常来说,我们会将应用程序和 Sidecar 部署在一起,那么程序的入口流量和出口流量都会由这个 Sidecar 去代理,这样就可以通过 Sidecar 去实现服务发现、熔断机制、超时重试等功能了。

Envoy Proxy 介绍

  Envoy Proxy 可以用来充当 Sidecar 进程。通常来说,我们会将应用程序和 Envoy 部署在一起,形成一个微服务。另一方面,为了实现高可用,通常一个微服务会部署多份副本,这些副本加在一起,就形成了 Service Cluster。下图显示的就是服务与服务之间的通信:


  除了可以充当 Sidecar 进程之外,Envoy Proxy 还可以充当反向代理,将流量转发给后端的 Service Cluster。充当反向代理的 Envoy,通常也可以称为 Edge Envoy。

构建程序镜像

  在部署应用程序之前,需要先构建 Docker 镜像,下面是一个简单的 Flask 程序app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import socket
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, World!'
@app.route('/healthy_check')
def healthy_check():
service = socket.gethostname()
ip = socket.gethostbyname(service)
return 'I am fine! <service_name: {}, ip: {}>'.format(service, ip)
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)

  具体的 Dockerfile 以及构建镜像需要用到的命令,可以见这里

在 K8S 部署微服务

  这个微服务由两个进程组成,一个是 Envoy 进程(充当 Sidecar),另一个是 Flask 程序。Envoy 的作用其实很简单,就是监听 80 端口,并将接收到的foo.com这个域名的全部流量都转发给 Flask 程序,下面是它的配置文件envoy.json(稍后再解释 Envoy 配置文件的含义):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
{
"listeners": [
{
"address": "tcp://0.0.0.0:80",
"filters": [
{
"type": "read",
"name": "http_connection_manager",
"config": {
"codec_type": "auto",
"stat_prefix": "ingress_http",
"route_config": {
"virtual_hosts": [
{
"name": "local_service",
"domains": ["foo.com"],
"routes": [
{
"timeout_ms": 0,
"prefix": "/",
"cluster": "local_service"
}
]
}
]
},
"filters": [
{
"type": "decoder",
"name": "router",
"config": {}
}
]
}
}
]
}
],
"admin": {
"access_log_path": "/dev/stdout",
"address": "tcp://0.0.0.0:8001"
},
"cluster_manager": {
"clusters": [
{
"name": "local_service",
"connect_timeout_ms": 250,
"type": "static",
"lb_type": "round_robin",
"hosts": [
{
"url": "tcp://127.0.0.1:5000"
}
]
}
]
}
}

  具体的 Kubernetes 配置文件放在了这里git clone这个项目,并执行下面的命令,部署微服务:

1
2
3
$ kubectl create configmap envoy-config --from-file flask-app-envoy/envoy.json
$ kubectl apply -f flask-app-envoy/deployment.yaml
$ kubectl apply -f flask-app-envoy/service.yaml

  通常,在部署微服务时,会部署微服务的多个副本,形成 Service Cluster。例如,在部署之后,可以看到,Service Cluster 的名称为flask-app-envoy-service

1
2
3
$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
flask-app-envoy-service ClusterIP 10.43.134.179 <none> 80/TCP 1m

  同时也可以看到,这个 Service Cluster 中包含了 4 个相同的 Service:

1
2
3
$ kubectl get deployments
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
flask-app-envoy-deployment 4 4 4 4 4h

动态服务发现

  Envoy 自身并不支持服务发现,不过可以借助开源社区提供的插件 kubernetes-envoy-sds,实现服务发现。使用下面的命令,在 Kubernetes 中,部署这个插件(需要 Kubernetes 1.6 以上版本):

1
2
3
4
$ git clone https://github.com/kelseyhightower/kubernetes-envoy-sds.git
$ cd kubernetes-envoy-sds
$ kubectl apply -f deployments/kubernetes-envoy-sds.yaml
$ kubectl apply -f services/kubernetes-envoy-sds.yaml

  这个插件的名称为kubernetes-envoy-sds,它部署在kube-system这个 namespace 下面:

1
2
3
4
5
6
$ kubectl get servicetemn kube-sys
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes-envoy-sds ClusterIP 10.43.93.232 <none> 80/TCP 1d
$ kubectl get deployments -n kube-system
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
kubernetes-envoy-sds 1 1 1 1 1d

Envoy 充当反向代理

  在部署好微服务之后,还需要部署一个 Envoy 充当反向代理(充当反向代理 Envoy,也称为 Edge Envoy),用来将foo.com这个域名的流量转发给后端的 Flask Service Cluster。下面是 Edge Envoy 的配置文件envoy.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
{
"listeners": [
{
"address": "tcp://0.0.0.0:80",
"filters": [
{
"type": "read",
"name": "http_connection_manager",
"config": {
"access_log": [
{
"path": "/dev/stdout"
}
],
"codec_type": "auto",
"stat_prefix": "ingress_http",
"route_config": {
"virtual_hosts": [
{
"name": "backend",
"domains": ["foo.com"],
"routes": [
{
"timeout_ms": 0,
"prefix": "/",
"cluster": "flask-app-envoy-service.default.svc.cluster.local"
}
]
}
]
},
"filters": [
{
"type": "decoder",
"name": "router",
"config": {}
}
]
}
}
]
}
],
"admin": {
"access_log_path": "/dev/stdout",
"address": "tcp://0.0.0.0:8001"
},
"cluster_manager": {
"clusters": [
{
"name": "flask-app-envoy-service.default.svc.cluster.local",
"connect_timeout_ms": 250,
"type": "sds",
"lb_type": "round_robin",
"service_name": "flask-app-envoy-service.default.svc.cluster.local"
}
],
"sds": {
"cluster": {
"name": "kubernetes-envoy-sds.kube-system",
"type": "logical_dns",
"connect_timeout_ms": 250,
"lb_type": "round_robin",
"hosts": [
{"url": "tcp://kubernetes-envoy-sds.kube-system:80"}
]
},
"refresh_delay_ms": 1000
}
}
}

  这里解释一下 Envoy 的配置文件,可以看到,配置文件中有一个listener,它负责监听 80 端口。注意到,listener中包含了一个名为http_connection_managerfilter ,这个filter主要用来处理 HTTP 请求(包括 HTTP/1.1 and HTTP/2 协议)。filter中最主要的配置就是virtual_hosts,它定义了一系列的路由规则,用来指定如何将流量转发给后端的 Service Cluster。
  例如,下面的配置会将foo.com的所有流量,都转发给后端的 Flask Service Cluster:

1
2
3
4
5
6
7
8
9
10
11
12
13
"virtual_hosts": [
{
"name": "backend",
"domains": ["foo.com"], # 域名
"routes": [
{
"timeout_ms": 0,
"prefix": "/", # URL 前缀
"cluster": "flask-app-envoy-service.default.svc.cluster.local"
}
]
}
]

  另一个比较重要的配置就是clusters,它定义了所有的后端 Service Cluster。一个 Service Cluster 中可以包含多个相同的 Service,Envoy 在转发流量给后端的 Service Cluster 时,允许指定负载均衡策略,例如下面的配置中,指定负载均衡策略为round_robin

1
2
3
4
5
6
7
8
9
"clusters": [
{
"name": "flask-app-envoy-service.default.svc.cluster.local",
"connect_timeout_ms": 250,
"type": "sds", # 使用动态的服务发现
"lb_type": "round_robin", # 负载均衡策略
"service_name": "flask-app-envoy-service.default.svc.cluster.local"
}
]

  既然在转发流量时,Envoy 可以指定负载均衡策略,那也就表示了 Envoy 必须知道 Service Cluster 中每个 Service 的 IP。即使当 Service Cluster 进行扩容或者缩容时,Envoy 也可以及时获取到每个 Service 的 IP。为了实现这一点,就需要动态的服务发现,所以type参数需要指定为sdssds其实就是我们前面说到的,用于实现动态服务发现的插件kubernetes-envoy-sds,它的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
"sds": {
"cluster": {
"name": "kubernetes-envoy-sds.kube-system",
"type": "logical_dns",
"connect_timeout_ms": 250,
"lb_type": "round_robin",
"hosts": [
{"url": "tcp://kubernetes-envoy-sds.kube-system:80"}
]
},
"refresh_delay_ms": 1000 # 刷新的时间间隔
}


  Kubernetes 配置文件放在了这里,git clone这个项目,并执行下面的命令,可以部署 Edge Envoy:

1
2
3
$ kubectl create configmap edge-envoy-config --from-file edge-envoy/envoy.json
$ kubectl apply -f edge-envoy/deployment.yaml
$ kubectl apply -f edge-envoy/service.yaml

  Edge Envoy 使用的是 NodePort 的方式暴露端口,可以看到它的端口为 32714:

1
2
3
$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
edge-envoy-service NodePort 10.43.219.6 <none> 80:32714/TCP 1d

  假设集群内,有一个节点的 IP 为172.16.1.127,那么可以使用下面的命令,测试 Edge Envoy 是否正常工作:

1
2
$ curl 172.16.1.127:32714 -H "Host: foo.com"
Hello, World!

参考资料