Envoy是一种高性能C++分布式代理,专为单个服务和应用程序设计。作为Service Mesh中的重要组件,充分理解其配置就显得尤为重要。本文列出了使用Envoy而不用其他代理的原因。并给出了Envoy及其服务的配置,然后对其进行详细解读,帮助读者理解其配置,从而掌握Envoy。
服务网格是微服务设置中的通信层,也就是说往返于每个服务的所有请求都通过网格。服务网格在微服务设置中也成为基础架构层,它能够让服务之间的通信变得安全可靠。关于Service Mesh的基础内容,我们已经在这篇文章中详细介绍过。
每一个服务都有自己的代理服务(sidecars),然后所有代理服务一起形成服务网格。Sidecars处理服务之间的通信,也就是说所有的流量都会通过网格并且该透明层可以控制服务之间如何交互。
服务网格通过由API控制的组件提供可观察性、服务发现以及负载均衡等。
实际上,如果一个服务要调用另一个服务,它不会直接调用目标服务。而是先将请求路由到本地代理,然后代理再将该请求路由到目标服务。这一过程意味着服务实例不会和其他服务直接接触,仅与本地代理进行通信。
根据ThoughtWorks Technology Radar(这是一份半年度的文档,用于评估现有技术和新生技术的风险和收益)指出,“服务网格提供一致的发现、安全性、跟踪(tracing)、监控以及故障处理,而无需共享资源(如API网关或ESB)。一个十分典型的用例是轻量的反向代理进程会与每个服务进程或单独的容器一起部署。”
当谈到服务网格时,不可避免谈到的是“sidecar”——可用于每个服务实例的代理。每个sidecar负责管理一个服务的一个实例。我们将在本文中进一步详细讨论sidecar。
服务网格可以交付什么?
当前,越来越多的企业和组织开始转向微服务架构。这样的企业需要服务网格所提供的上述功能。解耦库的使用或自定义代码的方法无疑是赢家。
为什么使用Envoy?
Envoy不是构建一个服务网格的唯一选择,市面上还有其他的代理如Nginx、Traefik等。我之所以选择Envoy,这个用C++编写的高性能代理,是因为我更喜欢Envoy的轻量、强大的路由,及其提供的可观察性和可扩展性。
让我们首先构建1个包含3个服务的服务网格设置,这是我们正在尝试构建的架构:
Front Envoy
在我们的设置中Front Envoy是一个边缘代理,我们通常在其中执行TLS终止、身份验证、生成请求头等操作。
我们来看看Front Envoy配置:
---
admin:
access_log_path: "/tmp/admin_access.log"
address:
socket_address:
address: "127.0.0.1"
port_value: 9901
static_resources:
listeners:
-
name: "http_listener"
address:
socket_address:
address: "0.0.0.0"
port_value: 80
filter_chains:
filters:
-
name: "envoy.http_connection_manager"
config:
stat_prefix: "ingress"
route_config:
name: "local_route"
virtual_hosts:
-
name: "http-route"
domains:
- "*"
routes:
-
match:
prefix: "/"
route:
cluster: "service_a"
http_filters:
-
name: "envoy.router"
clusters:
-
name: "service_a"
connect_timeout: "0.25s"
type: "strict_dns"
lb_policy: "ROUND_ROBIN"
hosts:
-
socket_address:
address: "service_a_envoy"
port_value: 8786
Envoy配置主要由以下部分组成:
1、 监听器(Listener)
2、 路由
3、 集群
4、 端点
监听器
一个或多个监听器可以在单个Envoy实例中运行。在以上9到36行的代码提到了当前监听器的地址和端口。每个监听器也可以有一个或多个网络过滤器。这些过滤器可以启用路由、tls终止、流量转移等活动。除了envoy.http_connection_manager
使用的是内置过滤器之外,Envoy还有其他几个过滤器。
路 由
22行到34行代码为过滤器配置了路由规范,同时它也指定了我们所接受请求的域以及路由匹配器。路由匹配器可以根据配置的规则匹配每个请求,并将请求转发到适当的集群。
集 群
集群是Envoy将流量路由到的上游服务规范。41行到48行代码定义了“Service A”,这是Front Envoy要通信的唯一上游。“connect_timeout”是在返回503之前建立与上游服务的连接的时间限制。
通常情况下,有多个“Serivce A”实例,并且Envoy支持多种负载均衡算法来路由流量。在本例中,我们使用了一个简单的循环算法。
端 点
“host”指定我们要将流量路由到的Service A的实例。在本例中,我们只有1个实例。
第47行代码没有直接与Service A进行通信,而是与Service A的Envoy代理实例进行通信,该代理将路由到本地Service A实例。
我们还可以利用返回Service A的所有实例的服务名称(如Kubernetes中的headless服务),来执行客户端负载均衡。Envoy缓存Service A的所有host,并每5秒刷新一次host列表。
此外,Envoy还支持主动和被动的健康检查。因此,如果我们要进行主动健康检查,我们需要在集群配置部分对其进行配置。
其 他
第2行到第7行配置了管理服务器,它能够帮助查看配置、更改日志级别、查看统计信息等。
第8行的“static_resources”可以手动加载所有配置。我们将在下文讨论如何动态地执行此操作。
虽然这里描述了许多其他配置,但是我们的目标不是全面介绍所有配置,而是以最少的配置开始。
Service A
这是Service A的Envoy配置:
admin:
access_log_path: "/tmp/admin_access.log"
address:
socket_address:
address: "127.0.0.1"
port_value: 9901
static_resources:
listeners:
-
name: "service-a-svc-http-listener"
address:
socket_address:
address: "0.0.0.0"
port_value: 8786
filter_chains:
-
filters:
-
name: "envoy.http_connection_manager"
config:
stat_prefix: "ingress"
codec_type: "AUTO"
route_config:
name: "service-a-svc-http-route"
virtual_hosts:
-
name: "service-a-svc-http-route"
domains:
- "*"
routes:
-
match:
prefix: "/"
route:
cluster: "service_a"
http_filters:
-
name: "envoy.router"
-
name: "service-b-svc-http-listener"
address:
socket_address:
address: "0.0.0.0"
port_value: 8788
filter_chains:
-
filters:
-
name: "envoy.http_connection_manager"
config:
stat_prefix: "egress"
codec_type: "AUTO"
route_config:
name: "service-b-svc-http-route"
virtual_hosts:
-
name: "service-b-svc-http-route"
domains:
- "*"
routes:
-
match:
prefix: "/"
route:
cluster: "service_b"
http_filters:
-
name: "envoy.router"
-
name: "service-c-svc-http-listener"
address:
socket_address:
address: "0.0.0.0"
port_value: 8791
filter_chains:
-
filters:
-
name: "envoy.http_connection_manager"
config:
stat_prefix: "egress"
codec_type: "AUTO"
route_config:
name: "service-b-svc-http-route"
virtual_hosts:
-
name: "service-b-svc-http-route"
domains:
- "*"
routes:
-
match:
prefix: "/"
route:
cluster: "service_c"
http_filters:
-
name: "envoy.router"
clusters:
-
name: "service_a"
connect_timeout: "0.25s"
type: "strict_dns"
lb_policy: "ROUND_ROBIN"
hosts:
-
socket_address:
address: "service_a"
port_value: 8081
-
name: "service_b"
connect_timeout: "0.25s"
type: "strict_dns"
lb_policy: "ROUND_ROBIN"
hosts:
-
socket_address:
address: "service_b_envoy"
port_value: 8789
-
name: "service_c"
connect_timeout: "0.25s"
type: "strict_dns"
lb_policy: "ROUND_ROBIN"
hosts:
-
socket_address:
address: "service_c_envoy"
port_value: 8790
11行到39行定义了一个监听器来路由流量到实际的Service A实例。在103行到111行中找到service_a
实例的相应集群定义。
Service A与Service B和Service C进行通信,它指向了两个以上的监听器以及集群。在本例中,我们为每个上游(localhost、Service B和Service C)分离了监听器。另一种方法是使用单个监听器,并根据URL或请求头路由到任意上游。
Service B 和 Service C
Service B和Service C处于叶级,除了本地主机服务实例外,不与任何其他上游通信。因此其配置十分简单。
而让事情变得复杂的是上述配置中的单个监听器和单个集群。
配置完成后,我们将此设置部署到Kubernetes或使用docker-compose对其进行测试。你可以运行docker-compose build
和docker-compose up
并点击localhost:8080,以查看请求是否成功通过所有服务和Envoy代理。我们可以使用日志对其进行验证。
Envoy xDS
我们为每个sidecar提供了配置,并且根据不同的服务,服务之间的配置也有所不同。虽然我们可以手动制作和管理sidecar配置,但最初至少要提供2或3个服务,并且随着服务数量的增加,配置会变得十分复杂。此外,每次sidecar配置更改时,你都必须重新启动Envoy实例,以使更改生效。
正如上文所讨论的,我们可以通过使用API server来避免手动配置并加载所有组件:集群(CDS)、端点(EDS)、监听器(LDS)以及路由(RDS)。所以每个sidecar将与API server通信并接收配置。当新配置更新到API server时,它将自动反映在Envoy实例中,从而避免了重新启动。
你可以在以下链接中了解关于动态配置的信息:
https://www.envoyproxy.io/docs/envoy/latest/configuration/overview/v2_overview#dynamic
这有一个简单的xDS server:
https://github.com/tak2siva/Envoy-Pilot
如何在Kubernetes中实现
本部分将讨论如果我们要在Kubernetes中实现所讨论的设置该怎么办。以下是架构图:
因此,将需要更改:
Pod
服务
部署Pod
尽管Pod规范中仅定义了一个容器——按照定义,一个Pod可以容纳一个或多个容器。为了对每个服务实例运行sidecar代理,我们将Envoy容器添加到每个Pod中。为了与外界通信,服务容器将通过localhost与Envoy容器进行对话。
部署文件如下所示:
admin:
access_log_path: "/tmp/admin_access.log"
address:
socket_address:
address: "127.0.0.1"
port_value: 9901
static_resources:
listeners:
-
name: "service-b-svc-http-listener"
address:
socket_address:
address: "0.0.0.0"
port_value: 8789
filter_chains:
-
filters:
-
name: "envoy.http_connection_manager"
config:
stat_prefix: "ingress"
codec_type: "AUTO"
route_config:
name: "service-b-svc-http-route"
virtual_hosts:
-
name: "service-b-svc-http-route"
domains:
- "*"
routes:
-
match:
prefix: "/"
route:
cluster: "service_b"
http_filters:
-
name: "envoy.router"
clusters:
-
name: "service_b"
connect_timeout: "0.25s"
type: "strict_dns"
lb_policy: "ROUND_ROBIN"
hosts:
-
socket_address:
address: "service_b"
port_value: 8082
在容器部分添加了Envoy sidecar,并且在33到39行的configmap中我们挂载了我们的Envoy配置文件。
更改服务
Kubernetes服务负责维护Pod端点列表,该列表可以路由流量。尽管kube-proxy通常处理Pod端点之间的负载均衡,但在本例中,我们将执行客户端负载均衡,并且我们不希望kube-proxy进行负载均衡。此外,我们想要提取Pod端点列表并对其进行负载均衡。为此,我们需要使用“headless服务“来返回端点列表。
如下所示:
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: servicea
spec:
replicas: 2
template:
metadata:
labels:
app: servicea
spec:
containers:
- name: servicea
image: dnivra26/servicea:0.6
ports:
- containerPort: 8081
name: svc-port
protocol: TCP
- name: envoy
image: envoyproxy/envoy:latest
ports:
- containerPort: 9901
protocol: TCP
name: envoy-admin
- containerPort: 8786
protocol: TCP
name: envoy-web
volumeMounts:
- name: envoy-config-volume
mountPath: /etc/envoy-config/
command: ["/usr/local/bin/envoy"]
args: ["-c", "/etc/envoy-config/config.yaml", "--v2-config-only", "-l", "info","--service-cluster","servicea","--service-node","servicea", "--log-format", "[METADATA][%Y-%m-%d %T.%e][%t][%l][%n] %v"]
volumes:
- name: envoy-config-volume
configMap:
name: sidecar-config
items:
- key: envoy-config
path: config.yaml
有两件事需要注意。一是第6行使服务变成headless,二是我们不是将Kubernetes服务端口映射到应用程序的服务端口,而是映射到Envoy监听器端口。这意味着,流量首先通向Envoy。即便如此,Kubernetes也可以完美运行。
在本文中,我们看到了如何使用Envoy代理构建服务网格。其中,我们设置了所有通信都将通过网格。因此,现在网格不仅有大量有关流量的数据,而且还具有控制权。
以下链接中你可以获取我们所讨论的配置和代码:
https://github.com/dnivra26/envoy_servicemesh
原文链接:
https://www.thoughtworks.com/insights/blog/building-service-mesh-envoy-0