介绍
几个星期前我第一次遇到Envoy代理,当时我的一位博客读者建议我写一篇关于它的文章。我以前从未听说过,我的第一个想法是,这不是我的经验领域。事实上,这个工具并不像nginx或haproxy那样受欢迎,但它提供了一些有趣的功能,其中我们可以区分对MongoDB,Amazon RDS的开箱即用支持,发现和负载平衡的灵活性或生成很多有用的流量统计。好吧,我们对它的优势了解一点但是Envoy代理究竟是什么?'Envoy是一个开源边缘和服务代理,专为云原生应用而设计'。它最初由Lift开发,是一种高性能C ++分布式代理,专为独立服务和应用程序以及大型微服务服务网格而设计。现在听起来真的很棒。这就是为什么我决定仔细研究它并准备一个使用Envoy和基于Spring Boot的微服务实现的服务发现和分布式跟踪的示例。
特使配置
在以前基于Spring Cloud的大多数示例中,我们使用Zuul作为边缘和代理。Zuul是流行的Netflix OSS工具,在您的微服务架构中充当API网关。事实证明,它可以被Envoy代理成功取代。在Envoy中我真正喜欢的一件事就是创建配置的方法。默认格式为JSON,并根据JSON模式进行验证。这个JSON属性和模式记录良好,易于理解。正是您对现代解决方案的期望,推荐的开始使用方法是使用预先构建的Docker镜像。因此,在开始时我们必须创建Dockerfile以使用Envoy构建Docker镜像,并提供JSON格式的配置文件。这是我的Dockerfile。参数service-cluster
和service-node
是可选的,与提供的服务发现配置有关,我将在一分钟内详细说明。
1 2 3 4 |
FROM lyft/envoy:latest RUN apt-get update COPY envoy.json /etc/envoy.json CMD /usr/local/bin/envoy -c /etc/envoy.json --service-cluster samplecluster --service-node sample1 |
我假设你有关于Docker及其命令的基本知识,此时这是强制性的。提供envoy.json
配置文件后,我们可以继续构建Docker镜像。
1 |
docker build -t envoy:v1 . |
然后使用docker run
命令运行它。有用的端口应暴露在外面。
1 |
docker run -d --name envoy -p 9901:9901 -p 10000:10000 envoy:v1 |
第一个非常有用的功能是本地HTTP管理员服务器。它可以在admin
属性内的JSON文件中配置。出于示例目的,我选择了端口9901,您可能已经注意到我也在Envoy Docker容器外部暴露了该端口。现在,管理控制台可以在http://192.168.99.100:9901 /下找到。如果调用该地址,则会打印所有可用命令。对我来说最有用的是统计数据,它会打印与代理和日志记录相关的所有重要统计信息,我可以在这里动态更改某些已定义类别的日志记录级别。所以,首先,如果你有任何问题,Envoy尝试通过调用/logging?name=level
并在运行docker logs envoy
命令后在Docker容器上观察它们来更改日志记录级别。
1 2 3 4 |
"admin" : { "access_log_path" : "/tmp/admin_access.log" , "address" : "tcp://0.0.0.0:9901" } |
下一个必需的配置属性是listeners
。在那里,我们定义路由设置和Envoy将侦听传入TCP连接的地址。符号tcp://0.0.0.0:10000是具有端口10000的任何IPv4地址的通配符匹配。此端口也在Envoy Docker容器外部公开。在这种情况下,它将是我们的API网关,可在http://192.168.99.100:10000/地址下获得。我们将在ltare阶段回到代理配置细节,现在让我们仔细研究一下这个示例的架构。
建筑
所示解决方案的架构在下图中可见。我们将Envoy代理作为API网关,这是我们系统的入口点。Envoy与Zipkin集成并向其发送跟踪消息,其中包含有关传入的HTTP请求和发回的响应的信息。两个示例微服务Person和Product在启动时在服务发现中注册,在注销时注销。它们隐藏在API网关后面的外部客户端。特使必须使用已注册服务的地址获取实际配置,并正确路由传入的HTTP请求。如果每个服务有多个实例可用,则应执行负载平衡。

事实证明,Envoy不支持像Consul或Zookeeper这样众所周知的发现服务器,但是定义了自己的基于REST的通用API,需要实现它才能启用集群成员获取。此API的主要方法GET /v1/registration/:service
用于获取当前已注册的服务实例列表。Lyft提供了Python的默认实现,但出于示例目的,我们使用Java和Spring Boot开发了自己的解决方案。GitHub上提供了示例应用程序源代码。除了服务发现实现,您还可以找到两个示例微服务。
服务发现
我们的自定义发现实现只是将基于REST的API暴露给注册,取消注册和获取服务实例的方法。GET方法需要返回与以下模式匹配的特定JSON结构。
1 2 3 4 五 6 7 |
{ "hosts" : [{ "ip_address" : "..." , "port" : "..." , ... }] } |
这是带有发现API实现的REST控制器类。
1 2 3 4 五 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 三十 31 32 33 34 35 36 37 38 39 |
@RestController public class EnvoyDiscoveryController { private static final Logger LOGGER = LoggerFactory.getLogger(EnvoyDiscoveryController. class ); private Map<String, List<DiscoveryHost>> hosts = new HashMap<>(); @GetMapping (value = "/v1/registration/{serviceName}" ) public DiscoveryHosts getHostsByServiceName( @PathVariable ( "serviceName" ) String serviceName) { LOGGER.info( "getHostsByServiceName: service={}" , serviceName); DiscoveryHosts hostsList = new DiscoveryHosts(); hostsList.setHosts(hosts.get(serviceName)); LOGGER.info( "getHostsByServiceName: hosts={}" , hostsList); return hostsList; } @PostMapping ( "/v1/registration/{serviceName}" ) public void addHost( @PathVariable ( "serviceName" ) String serviceName, @RequestBody DiscoveryHost host) { LOGGER.info( "addHost: service={}, body={}" , serviceName, host); List<DiscoveryHost> tmp = hosts.get(serviceName); if (tmp == null ) tmp = new ArrayList<>(); tmp.add(host); hosts.put(serviceName, tmp); } @DeleteMapping ( "/v1/registration/{serviceName}/{ipAddress}" ) public void deleteHost( @PathVariable ( "serviceName" ) String serviceName, @PathVariable ( "ipAddress" ) String ipAddress) { LOGGER.info( "deleteHost: service={}, ip={}" , serviceName, ipAddress); List<DiscoveryHost> tmp = hosts.get(serviceName); if (tmp != null ) { Optional<DiscoveryHost> optHost = tmp.stream().filter(it -> it.getIpAddress().equals(ipAddress)).findFirst(); if (optHost.isPresent()) tmp.remove(optHost.get()); hosts.put(serviceName, tmp); } } } |
让我们回到Envoy配置设置。假设我们从Dockerfile构建了一个可见的图像,然后在默认端口上运行容器,我们可以在地址http://192.168.99.100:9200下调用它。该地址应放在envoy.json
配置文件中。应在“Cluster Manager”部分中提供服务发现连接设置。
1 2 3 4 五 |
FROM openjdk:alpine MAINTAINER Piotr Minkowski <[email protected]> ADD target/envoy-discovery.jar envoy-discovery.jar ENTRYPOINT ["java", "-jar", "/envoy-discovery.jar"] EXPOSE 9200 |
这是envoy.json
文件中的片段。应将服务发现的集群定义为全局SDS配置,必须在sds
property(1)中指定。最重要的是提供正确的URL(2),并在此基础上,Envoy自动尝试调用端点GET / v1 / registration / {service_name}。该部分的最后一个有趣的配置字段refresh_delay_ms
是负责设置提取在发现服务器中注册的服务列表之间的延迟。那不是全部。我们还必须定义集群成员。它们由名称(4)标识。它们的类型是sds(5),这意味着该集群使用服务发现服务器来定位调用微服务的网络地址,其名称在service-name
属性中定义。
1 2 3 4 五 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
"cluster_manager" : { "clusters" : [{ "name" : "service1" , ( 4 ) "type" : "sds" , // (5) "connect_timeout_ms" : 5000 , "lb_type" : "round_robin" , "service_name" : "person-service" // (6) }, { "name" : "service2" , "type" : "sds" , "connect_timeout_ms" : 5000 , "lb_type" : "round_robin" , "service_name" : "product-service" }], "sds" : { // (1) "cluster" : { "name" : "service_discovery" , "type" : "strict_dns" , "connect_timeout_ms" : 5000 , "lb_type" : "round_robin" , "hosts" : [{ "url" : "tcp://192.168.99.100:9200" // ( 2 ) }] }, "refresh_delay_ms" : 3000 // (3) } } |
为route_config
属性内的每个侦听器定义路由配置(1)。第一条路由配置为人员服务,由集群服务1(2)处理,第二条路由服务 2集群进行产品服务处理。因此,我们的服务可在http://192.168.99.100:10000/person和http://192.168.99.100:10000/product地址下找到。
1 2 3 4 五 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
{ "name" : "http_connection_manager" , "config" : { "codec_type" : "auto" , "stat_prefix" : "ingress_http" , "route_config" : { // (1) "virtual_hosts" : [{ "name" : "service" , "domains" : [ "*" ], "routes" : [{ "prefix" : "/person" , // (2) "cluster" : "service1" }, { "prefix" : "/product" , // (3) "cluster" : "service2" }] }] }, "filters" : [{ "name" : "router" , "config" : {} }] } } |
构建微服务
Envoy代理上的路由已经配置。我们仍然没有运行微服务。它们的实现基于Spring Boot框架,除了公开REST API之外,它还提供对象列表上的简单操作以及在发现服务器上注册/取消注册服务。这是负责该注册的@Service bean。在正常关闭之前onApplicationEvent
,应用程序启动和destroy
方法之后会触发该方法。
1 2 3 4 五 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 三十 31 32 33 34 35 36 37 38 39 40 41 |
@Service public class PersonRegister implements ApplicationListener<ApplicationReadyEvent> { private static final Logger LOGGER = LoggerFactory.getLogger(PersonRegister. class ); private String ip; @Value ( "${server.port}" ) private int port; @Value ( "${spring.application.name}" ) private String appName; @Value ( "${envoy.discovery.url}" ) private String discoveryUrl; @Autowired RestTemplate template; @Override public void onApplicationEvent(ApplicationReadyEvent event) { LOGGER.info( "PersonRegistration.register" ); try { ip = InetAddress.getLocalHost().getHostAddress(); DiscoveryHost host = new DiscoveryHost(); host.setPort(port); host.setIpAddress(ip); template.postForObject(discoveryUrl + "/v1/registration/{service}" , host, DiscoveryHosts. class , appName); } catch (Exception e) { LOGGER.error( "Error during registration" , e); } } @PreDestroy public void destroy() { try { template.delete(discoveryUrl + "/v1/registration/{service}/{ip}/" , appName, ip); LOGGER.info( "PersonRegister.unregistered: service={}, ip={}" , appName, ip); } catch (Exception e) { LOGGER.error( "Error during unregistration" , e); } } } |
正确关闭Spring Boot应用程序的最佳方法是通过其Actuator端点。为服务启用此类端点包括spring-boot-starter-actuator
项目依赖项。默认情况下禁用关闭,因此我们应添加以下属性application.yml
以启用它,并另外禁用默认安全性(endpoints.shutdown.sensitive=false
)。现在,只需调用POST / shutdown,我们就可以停止Spring Boot应用程序并测试取消注册方法。
1 2 3 4 |
endpoints: shutdown: enabled: true sensitive: false |
对于微服务我们也一样,我们也构建了docker镜像。这是人员服务Dockerfile,它允许覆盖默认服务和SDS端口。
1 2 3 4 五 6 |
FROM openjdk:alpine MAINTAINER Piotr Minkowski <[email protected]> ADD target/person-service.jar person-service.jar ENV DISCOVERY_URL http://192.168.99.100:9200 ENTRYPOINT ["java", "-jar", "/person-service.jar"] EXPOSE 9300 |
要使用自定义侦听端口构建映像并运行服务容器,请键入以下docker命令。
1 2 |
docker build -t piomin /person-service . docker run -d --name person-service -p 9301:9300 piomin /person-service |
分布式跟踪
现在是最后一块拼图的时候了 - Zipkin追踪。应在那里发送与所有传入请求相关的统计信息。Envoy代理中配置的第一部分是inside tracing
属性,它指定HTTP跟踪器的全局设置。
1 2 3 4 五 6 7 8 9 10 11 |
"tracing" : { "http" : { "driver" : { "type" : "zipkin" , "config" : { "collector_cluster" : "zipkin" , "collector_endpoint" : "/api/v1/spans" } } } } |
Zipkin连接的网络位置和设置应定义为集群成员。
1 2 3 4 五 6 7 8 9 10 11 |
"clusters" : [{ "name" : "zipkin" , "connect_timeout_ms" : 5000 , "type" : "strict_dns" , "lb_type" : "round_robin" , "hosts" : [ { "url" : "tcp://192.168.99.100:9411" } ] }] |
我们还应该tracing
在HTTP连接管理器配置中添加新的部分(1)。字段operation_name
是必需的并设置范围名称。仅支持“入口”和“出口”值。
1 2 3 4 五 6 7 8 9 10 11 |
"listeners" : [{ "filters" : [{ "name" : "http_connection_manager" , "config" : { "tracing" : { // (1) "operation_name" : "ingress" // (2) } // ... } }] }] |
Zipkin服务器可以使用其Docker镜像启动。
1 |
docker run -d --name zipkin -p 9411:9411 openzipkin /zipkin |
概要
这是用于测试目的的运行Docker容器的列表。您可能还记得我们有Zipkin,Envoy,自定义发现,两个人员服务实例和一个产品服务实例。您可以通过调用POST / person添加一些人物对象,并通过调用GET / person显示所有人的列表。应根据服务发现中的条目在两个实例之间对请求进行负载平衡。

有关每个请求的信息将发送到Zipkin,服务名称为-service-cluster Envoy代理运行参数。
