在 Spring Cloud 体系中,几乎每个角色都会有两个以上的产品提供选择,比如在注册中心有:Eureka、Consul、zookeeper、etcd 等;网关的产品有 Zuul、Spring Cloud Gateway 等。在注册中心产品中,最常使用的是 Eureka 和 Consul,两者各有特点,企业可以根据自述项目情况来选择。
一、什么是Consul
Consul 是 HashiCorp 公司推出的开源产品,用于实现分布式系统的服务发现、服务隔离、服务配置,这些功能中的每一个都可以根据需要单独使用,也可以同时使用所有功能。Consul 官网目前主要推 Consul 在服务网格中的使用。
与其它分布式服务注册与发现的方案相比,Consul 的方案更“一站式”——内置了服务注册与发现框架、分布一致性协议实现、健康检查、Key/Value 存储、多数据中心方案,不再需要依赖其它工具。Consul 本身使用 go 语言开发,具有跨平台、运行高效等特点,也非常方便和 Docker 配合使用。
Consul 的主要特点
- Service Discovery : 服务注册与发现,Consul 的客户端可以做为一个服务注册到 Consul,也可以通过 Consul 来查找特定的服务提供者,并且根据提供的信息进行调用。
- Health Checking: Consul 客户端会定期发送一些健康检查数据和服务端进行通讯,判断客户端的状态、内存使用情况是否正常,用来监控整个集群的状态,防止服务转发到故障的服务上面。
- KV Store: Consul 还提供了一个容易使用的键值存储。这可以用来保持动态配置,协助服务协调、建立 Leader 选举,以及开发者想构造的其它一些事务。
- Secure Service Communication: Consul 可以为服务生成分布式的 TLS 证书,以建立相互的 TLS 连接。 可以使用 intentions 定义允许哪些服务进行通信。 可以使用 intentions 轻松管理服务隔离,而不是使用复杂的网络拓扑和静态防火墙规则。
- Multi Datacenter: Consul 支持开箱即用的多数据中心,这意味着用户不需要担心需要建立额外的抽象层让业务扩展到多个区域。
- Consul 角色
- Server:服务端, 保存配置信息, 高可用集群, 在局域网内与本地客户端通讯, 通过广域网与其它数据中心通讯。 每个数据中心的 Server 数量推荐为 3 个或是 5 个。
- Client:客户端, 无状态, 将 HTTP 和 DNS 接口请求转发给局域网内的服务端集群。
Consul 旨在对 DevOps 社区和应用程序开发人员友好,使其成为现代、弹性基础架构的理想选择。
使用Consul 的优势
- 使用 Raft 算法来保证一致性, 比复杂的 Paxos 算法更直接。相比较而言, zookeeper 采用的是 Paxos, 而 etcd 使用的则是 Raft。
- 支持多数据中心,内外网的服务采用不同的端口进行监听。多数据中心集群可以避免单数据中心的单点故障,而其部署则需要考虑网络延迟, 分片等情况等。 zookeeper 和 etcd 均不提供多数据中心功能的支持。
- 支持健康检查。 etcd 不提供此功能。
- 支持 http 和 dns 协议接口。 zookeeper 的集成较为复杂, etcd 只支持 http 协议。
- 官方提供 Web 管理界面, etcd 无此功能。
- Consul 保持了 CAP 中的 CP,保持了强一致性和分区容错性。
- Consul 支持 Http\gRPC\DNS 多种访问方式。
Consul 的调用过程
- 当 Producer 启动的时候,会向 Consul 发送一个 post 请求,告诉 Consul 自己的 IP 和 Port;
- Consul 接收到 Producer 的注册后,每隔 10s(默认)会向 Producer 发送一个健康检查的请求,检验 Producer 是否健康;
- 当 Consumer 发送 GET 方式请求 /api/address 到 Producer 时,会先从 Consul 中拿到一个存储服务 IP 和 Port 的临时表,从表中拿到 Producer 的 IP 和 Port 后再发送 GET 方式请求 /api/address;
- 该临时表每隔 10s 会更新,只包含有通过了健康检查的 Producer。
Spring Cloud Consul 项目是针对 Consul 的服务治理实现。Consul 是一个分布式高可用的系统,它包含多个组件,但是作为一个整体,在微服务架构中,为我们的基础设施提供服务发现和服务配置的工具。
Consul 和 eureka的对比
通过对比可以得知, Consul 功能更强大,Euerka 更容易使用。
Consul 强一致性(C)带来的是:
- 服务注册相比 Eureka 会稍慢一些。因为 Consul 的 raft 协议要求必须过半数的节点都写入成功才认为注册成功,。Leader 挂掉时,重新选举期间整个 Consul 不可用。保证了强一致性但牺牲了可用性。
- Consul 强烈的一致性意味着它可以作为领导选举和集群协调的锁定服务。
Eureka 保证高可用(A)和最终一致性:
服务注册相对要快,因为不需要等注册信息 replicate 到其它节点,也不保证注册信息是否 replicate 成功。当数据出现不一致时,虽然 A, B 上的注册信息不完全相同,但每个 Eureka 节点依然能够正常对外提供服务,这会出现查询服务信息时如果请求 A 查不到,但请求 B 就能查到。如此保证了可用性但牺牲了一致性。
二、如何使用Consul
安装Consul
Consul 不同于 Eureka 是由 go 语言开发而成,因此需要我们单独来安装。
打开 Consul官网 根据不同的操作系统选择最新的 Consul 版本,我们这里以 Windows 64 操作系统为例。
下载下来是一个consul_1.8.4_windows_amd64.zip的压缩包,解压出一个 consul.exe 的执行文件。
cd 到对应的目录下,使用 cmd 启动 Consul:
cd D:\Common Files\consul #cmd启动: consul agent -dev # -dev表示开发模式运行,另外还有-server表示服务模式运行
为了方便启动,可以在同级目录下创建一个 run.bat 脚本来启动,脚本内容如下:
consul agent -dev
下次启动的时候直接双击 run.bat 文件即可;当然也可以把 consul 的 exe 文件路径加入到本机的 path 路径下,这样后期只需要在 cmd 命令行下运行。
启动成功之后访问:localhost:8500,就可以看到Consul的管理界面:
Consul 的 Web 管理界面有一些菜单,我们这里做一下简单的介绍:
- Services:管理界面的默认页面,用来展示注册到 Consul 的服务,启动后默认会有一个 consul 服务,也就是它本身。
- Nodes:在 Services 界面双击服务名就会来到 Services 对于的 Nodes 界面,Services 是按照服务的抽象来展示的,Nodes 展示的是此服务的具体节点信息。比如启动了两个订单服务实例,Services 界面会出现一个订单服务,Nodes 界面会展示两个订单服务的节点。
- Key/Value :如果有用到 Key/Value 存储,可以在界面进行配置、查询。
- ACL:全称 Access Control List,为访问控制列表的展示信息。
- Intentions:可以在页面配置请求权限。
当我们看到这个页面后,也就意味着 Consul 已经安装成功了。
consul服务端(服务提供者)
创建一个服务提供者
(1)引入pom依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--监控,提供了健康检查--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency>
(2)启动类添加 @EnableDiscoveryClient 注解
(3)配置注册中心信息
spring.application.name=spring-cloud-consul-producer server.port=8501 # consul注册中心的信息 spring.cloud.consul.host=localhost spring.cloud.consul.port=8500 #注册到 consul 中的服务名称 spring.cloud.consul.discovery.serviceName=service-producer
这里,注册的服务名称属性为spring.cloud.consul.discovery.serviceName,而不再是像Eureka中使用 spring.application.name=xx
启动服务后,在控制台可以看到多了对应的服务
配置常见的问题
- 一般按照上边的配置是没有问题了,但是,服务注册时,默认使用的是hostname,在开发环境也就是localhost,如果注册中心Consul所在服务器,和注册的微服务不在同一台服务器,就会出现访问不到,健康检查失败,因此,可以增加配置:
spring.cloud.consul.discovery.prefer-ip-address=true spring.cloud.consul.discovery.ip-address= 服务ip
- 如果使用的是Spring Cloud版本为Dalston.SR1,更具体的应该是 Spring Cloud consul版本1.3之前的,需要增加如下配置,否则健康检查总是返回 503, {status: ‘DOWN’} 导致服务不可用,修改SpringCloud版本为Finchley.RELEASE(SpringBoot需要2.0以上版本)之后,没有改问题。
# 取消 consul 的安全检查 management.health.consul.enabled=false
健康检查失败问题说明:
- 添加 management.security.enabled=false 配置,该配置属性用于设置是否需要授权才能访问,默认是true,即暴露所有的断点。Spring Boot 2.0后没有了该配置,想要实现该功能需要使用如下配置
# 启用端点 env management.endpoint.env.enabled=true # 暴露端点 env 配置多个,隔开 management.endpoints.web.exposure.include=env management.endpoints.web.exposure.include=*
- 添加了以上配置之后,模拟健康检查,使用 /health 请求访问,返回结果如下,可以看到主要是consul 健康检查失败,导致了整个服务健康检查失败,上边取消了consul的健康检查之后,整个服务可以通过健康检查。
{ "status": "DOWN", "discoveryComposite": { "description": "Spring Cloud Consul Discovery Client", "status": "UP", "discoveryClient": { "description": "Spring Cloud Consul Discovery Client", "status": "UP", "services": [ "consul", "service-producer" ] } }, "diskSpace": { "status": "UP", "total": 159015497728, "free": 75346374656, "threshold": 10485760 }, "refreshScope": { "status": "UP" }, "consul": { "status": "DOWN", "services": { "consul": [ ], "service-producer": [ ] }, "error": "java.lang.IllegalArgumentException: Value must not be null" }, "hystrix": { "status": "UP" } }
Consul 消费者
- 创建一个消费者应用,pom文件和服务生产者一致。
配置文件:
spring.application.name=spring-cloud-consul-consumer server.port=8503 spring.cloud.consul.host=localhost spring.cloud.consul.port=8500 #设置不需要注册到 consul 中 spring.cloud.consul.discovery.register=false
消费者可以注册到注册中心,也可以不注册,因为这里选择不注册,因此启动类就不用添加服务发现的注解。
- 调用生产者
根据Consul的工作原理,消费者调用生产者时,会通过Consul来获取对应的地址,具体实现:
- 通过LoadBalancerClient 的 choose(service-id)方法随机选择一个对应的应用名称的服务实例
- 然后使用 RestTemplate()来模拟发送请求。
@RestController public class CallHelloController { @Autowired private LoadBalancerClient loadBalancer; @RequestMapping("/call") public String call() { ServiceInstance serviceInstance = loadBalancer.choose("service-producer"); System.out.println("服务地址:" + serviceInstance.getUri()); System.out.println("服务名称:" + serviceInstance.getServiceId()); String callServiceResult = new RestTemplate().getForObject(serviceInstance.getUri().toString() + "/hello", String.class); System.out.println(callServiceResult); return callServiceResult; } }
消费者调用主要有两个接口:LoadBalancerClient,DiscoveryClient具体使用如下:
@RestController public class ServiceController { @Autowired private LoadBalancerClient loadBalancer; @Autowired private DiscoveryClient discoveryClient; /** * 获取所有服务 */ @RequestMapping("/services") public Object services() { // 获取对应服务名称的所有实例信息 return discoveryClient.getInstances("service-producer"); } /** * 从所有服务中选择一个服务(轮询) */ @RequestMapping("/discover") public Object discover() { return loadBalancer.choose("service-producer").getUri().toString(); } }
loadBalancer.choose(“service-producer”):随机选择一个服务名称对应的实例返回;
discoveryClient.getInstances(“service-producer”):查询服务名称的所有实例信息。
使用ribbon、feign消费服务
使用ribbon
SpringCloudConsul中已经集成了ribbon,上边的消费者调用,我们可以看出,实际也是使用了ribbon的方式来调用,我们可以对其稍作修改:
// 添加RestTemplate bean @Bean @LoadBalanced RestTemplate restTemplate() { return new RestTemplate(); } @GetMapping("/ribbon") public String call2() { return restTemplate.getForObject("http://service-producer/hello", String.class); }
之前只不过是先使用LoadBalancerClient接口,获取对应了url,然后访问;使用ribbon之后,可以直接使用service-id进行访问。
使用feign
添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
启动类添加@EnableFeignClients 注解,启用Feign客户端注解。
创建一个调用服务的interface:
@FeignClient("service-producer") public interface HelloRemote { @RequestMapping(value = "/hello") public String hello(@RequestParam(value = "name") String name); }
controller中调用:
@RestController public class CallHelloController { @Autowired private HelloRemote helloRemote; @GetMapping("/feign") public String call3() { return helloRemote.hello(); } }