重学SpringCloud系列三之服务注册与发现---下
- 白话服务注册与发现
-
- 公益图书馆例子
- 服务注册与发现
-
- 服务注册
- 客户端负载均衡
- Spring Cloud常用的服务注册中心
- DiscoveryClient服务发现
-
- DiscoveryClient测试用例
- 两种服务发现注解的区别
- Eureka集群环境构建(linux)
-
- CentOS7(linux)环境准备
- profile配置文件修改
- 为eureka server加入actuator
- CentOS7环境下部署eureka集群
- 访问测试
- 其他需要注意的点:
- Eureka集群多网卡环境ip设置
-
- 配置实现
- 多网卡ip选择配置方法总结归纳
-
- 方法一:直接配置eureka.instance.ip-address
- 方法二:增加inetutils相关配置
- 通过启动命令行传递配置
- 源码解析
- Eureka集群服务注册与安全认证
-
- 服务注册配置
- Eureka安全认证(服务端)
- Eureka安全认证(客户端)
- Eureka自我保护与健康检查
-
- Eureka的健康检查
- Eureka自我保护模式
- 关闭 Eureka 的自我保护模式
- 主流服务注册中心对比(含nacos)
-
- 垂直扩展与水平扩展
- CAP理论
-
- 强数据一致性(High Consistency )
- 高可用性(High Availability)
- 分区容忍性(Partition Tolerance)
- 服务注册中心的CAP
- 主流注册中心产品
- zookeeper概念及功能简介
-
- zookeeper简介
- 集群节点必须是奇数个
-
- 容错率
- 防脑裂
- 临时节点与持久节点
-
- 持久znode
- 临时znode
- zookeeper-linux集群安装
-
- 环境准备
- 安装 zookeeper
-
- 解压
- 修改配置文件 zoo.cfg
- 创建 myid 文件
- 配置环境变量
- 开启防火墙端口
- 启动zookeeper服务
- zookeeper服务注册与发现
-
- 微服务向zookeeper注册
-
- 配置zookeeper
- 打开discovery开关
- 验证服务注册结果
- FeignClient远程服务名称调整
- 访问测试
- consul概念及功能介绍
-
- Consul是什么?
- Spring Cloud服务与consul
- consul集群
- consul-linux集群安装
-
- 环境准备
- consul 端口说明
- agent启动参数
- 安装过程
-
- consul下载、解压和目录创建
- consul配置文件
- linux consul systemd服务配置
- consul集群安装结果验证
- web UI
- consul命令详解(用到查一下、不用记)
- consul服务注册与发现
-
- 微服务向consul注册
-
- 引入依赖包
- 配置consul
- 打开discovery开关
- 启动日志
- 验证服务注册结果
- FeignClient远程服务名称调整
- 接口访问测试
- 通用-auatator导致401问题
-
- 问题描述
- 原因
- 解决方案一
- 解决方案二
- 解决方案三
- 解决方案四
- 解决方案五
白话服务注册与发现
公益图书馆例子
笔者不想直接用专业的术语来说明“微服务注册与发现”,所以我们来看生活中的一个案例:“公益图书馆”。
随着人们生活水平的不断提高,追求精神食粮的朋友也越来越多。笔者曾经在一些城市看见过公益图书馆,其运行逻辑是:一些公益组织和个人提供一块场所,然后由组织内的人向图书馆内捐书。捐出的书越多,一段时间内能够借阅的书也就越多。这种做法有助于大家分享图书、节约资金、交流读书心得。那我们来看一下几个关键环节:
- 捐书:组织内的人向公益图书馆捐书,是不是直接将书放到书架上就完事了呢?当然不是,是先向图书管理系统记录一下捐书的人、书名、捐书的时间等信息,再将书放到书架上。
- 借书:借书的人通常是通过图书管理系统的一个小程序查询图书,然后取书,全靠自觉。图书可能存在多个副本(多人捐的同一种书),借书的人会根据书籍状态择优选择。
- 这其中非常重要的一个角色就是图书管理系统,为大家捐书、借书提供了数据支持和集中管理功能。
- 兼职图书管理员定期维护图书,将破损图书从图书管理系统中下架维护。
其实上面的这个“公益图书馆的例子”就是典型的服务注册与发现:
- 每一本图书就是一个服务,捐书的过程就是“服务注册”的过程。
- 借书的查询图书的过程就是“服务发现”的过程。
- 其中最重要的角色:图书管理系统及管理员,就是服务注册中心或者服务注册平台。
- 捐书者可能同时是借书者。进行服务注册的微服务节点,同时可能也使用服务发现机制发现其他微服务。
- 捐书是主动行为,不是被动行为。这和微服务的注册是一样的,微服务必须在启动的时候向服务注册组件进行主动注册。这样做的目的就是降低数据维护成本,不需要专人维护注册数据。
- 图书下架是被动的,不是主动的,不是捐书的人将其下架。微服务也是一样,当服务出现故障发生问题,服务发现注册组件应具备将服务下线的能力。
- 图书管理员可以检查图书并下架,这过程在服务注册与发现中被称为:健康检查
- 对于同一种图书可能存在多个同样的副本,由使用者择优选择借哪一本书。对于服务发现获得的结果:同一种服务的多个副本的情况,由服务调用者择优决定使用哪一个服务副本。这种服务方式比较专业的说法是:客户端负载均衡。
与客户端负载均衡相对的方法就是服务端负载均衡,如果上面的例子中借书过程一本书有多个副本,由图书管理员或系统决定借书者借其中的哪一个副本,这个就是服务端负载均衡。如:nginx、haproxy等就是服务端负载均衡。
服务注册与发现
- 服务注册 -服务在中央注册表中注册其服务位置的过程。通常注册其主机和端口,有时还注册认证凭证,协议,版本号和或环境信息。
- 服务发现 -客户端应用程序查询中央注册表以了解服务位置的过程。
- 维护中央注册表的角色被称为服务注册平台或者服务注册中心
服务注册
当一个微服务启动的时候,必须主动向服务注册中心注册其服务地址,以供其他微服务查询调用。图中橘黄色为服务注册中心,绿色为微服务节点。
客户端负载均衡
- 当一个微服务有多个实例的时候,由调用者从服务注册中心获取注册服务列表
- 调用者拿到"注册服务列表"之后,决定访问哪一个服务实例。
Spring Cloud常用的服务注册中心
- Eureka:Spring Cloud的大儿子,出生的时候条件一般,长大后素质有限
- Nacos:后起之秀,曾经Spring Cloud眼中“别人家的孩子”,已经纳入收养范围(孵化项目)。
- Apache Zookeeper:关系户,与hadoop关系比较好
- Consul:关系户,曾经与docker关系比较好
- etcd:关系户,与kubernetes关系比较好
如果你的应用已经使用到了hadoop、kubernetes、docker,在Spring Cloud实施过程中可以考虑使用其关系户组件,避免搭建两套注册中心,节省资源。但是二者兼容使用说说容易,真正用起来还需要功夫。目前看,笔者觉得最佳选择应该是Nacos。
这里可以先简单的了解一下常见的这些服务注册中心,后面的章节我们会逐步的详细介绍。
DiscoveryClient服务发现
在前面的章节,我们已经为大家介绍了
- 服务注册的方式,在服务启动的时候主动向服务注册中心注册服务信息
- 服务消费者可以以负载均衡的方式,远程调用服务提供者提供的服务。
服务消费者调用微服务之前,需要向服务注册中心,获取注册服务列表及服务信息。这个过程就是“服务发现”,那么服务发现是通过什么类实现的?服务列表及服务信息又包含哪些内容?本节就带着大家来解开这样的疑惑!
DiscoveryClient测试用例
DiscoveryClient 代表的就是:服务发现操作对象。
public interface DiscoveryClient extends Ordered {
int DEFAULT_ORDER = 0;
String description();
List<ServiceInstance> getInstances(String serviceId);
List<String> getServices();
default int getOrder() {
return 0;
}
}
它有两个核心方法:
- getServices获取在服务注册中心,注册的所有服务的id。比如:ASERVICE-RBAC、ASERVICE-SMS。
- getInstances根据服务id,获取该服务的所有启动实例的注册信息。即:一个微服务的多个副本的注册信息。
下面是一个基于Spring、Junit的测试用例,使用上面两个方法来实现服务发现。
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class DiscoveryClientTest {
@Resource
private DiscoveryClient discoveryClient; // 进行eureka的发现服务
@Test
void discoveryClientTest() {
//获取服务Id
List<String> services = discoveryClient.getServices();
services.forEach(System.out::println);
//获取每个服务的多个启动实例的注册信息。
for (String service:services){
discoveryClient.getInstances(service)
.forEach(s -> {
System.out.println("InstanceId=" + s.getHost() + ":" + s.getPort());
System.out.println("Host:Port="+ s.getHost() + ":" + s.getPort());
System.out.println("Uri=" + s.getUri());
System.out.println("InstanceId=" + s.getInstanceId());
System.out.println("Schema=" + s.getScheme());
System.out.println("ServiceId=" + s.getServiceId());
System.out.println("Metadata="+ s.getMetadata());
});
}
}
}
结合测试结果的打印,可以更清楚的知道服务注册及发现相关的信息。理解DiscoveryClient 及其方法的作用。控制台打印结果如下:
两种服务发现注解的区别
在Spring Cloud中实现服务发现可以使用两种注解:@EnableDiscoveryClient和@EnableEurekaClient
,两者的用法基本上是一样的。但存在区别,简单地说:
- 如果服务注册中心是eureka,就需要在服务启动类加上
@EnableEurekaClient
注解,实现服务发现。 - 如果是其他的注册中心,那么更推荐使用
@EnableDiscoveryClient
,该注解更加的通用。
在Hello-microservice章节,实现微服务向服务注册中心注册的时候,我们使用了@EnableEurekaClient
,是因为我们当时搭建的服务注册中心是基于eureka搭建的。Spring Cloud中还有很多的其他服务注册中心的选项,比如:consul、zookeeper、nacos,这时就不能使用@EnableEurekaClient
注解了,需要使用@EnableDiscoveryClient
注解。
Eureka集群环境构建(linux)
服务注册中心在整个微服务体系中,至关重要!如果服务注册中心挂了,整个系统都将崩溃。所以服务注册中心通常不会被部署为单点应用,而是采用集群的部署方式,其中个别节点挂掉不影响整个系统的运行。
下面,我们就来为大家介绍,如何基于CentOS7服务器构建eureka服务注册中心。
CentOS7(linux)环境准备
主机名称 | 主机ip |
---|---|
peer1 | 192.168.161.3 |
peer2 | 192.168.161.4 |
peer3 | 192.168.161.5 |
并且在安装eureka服务注册中心之间,需要将服务器时间同步,不能相差太多,否则eureka服务有可能启动失败。可以使用ntp
进行时间同步。如:
ntpdate ntp.api.bz
profile配置文件修改
application-peer1.yml
- hostname为peer1
- defaultZone为peer2和peer3
- 下面配置我们之所以重写了health-check-url(健康检查路径),是因为设置了context-path。默认的健康检查路径是以“/”为项目的context-path。所以我们需要修改为server.servlet.context-path配置的值。
- 因为我们设置了context-path,所以defaultZone访问端点有两个eureka,第一个eureka是我们配置的context-path。如果不设置context-path,defaultZone:http://peer2:8761/eureka/,http://peer3:8761/eureka/
- 在eureka集群搭建过程中,fetch-registry和register-with-eureka一定设置为true。
这两个值之所以设置为true,目的是让eureka集群之间实现互相注册,互相心跳健康状态,从而达到集群的高可用。
#是否从其他实例获取服务注册信息,因为这是一个单节点的EurekaServer,不需要同步其他的EurekaServer节点的数据,所以设置为false;
fetch-registry: false
#表示是否向eureka注册服务,即在自己的eureka中注册自己,默认为true,此处应该设置为false;
register-with-eureka: false
把它们设置为false,是能解决你可能遇到的一些集群环境问题。这就好比你腿疼,你把腿砍了是不疼了,但你还能走路么。我们要的是让腿不疼,而不是把腿砍掉。“把腿砍了”这就不是“高可用”集群了,相当于你搭建了多个eureka server单点,这是“掩耳盗铃”的做法。
server:
port: 8761
servlet:
context-path: /eureka
spring:
application:
name: eureka-server
eureka:
instance:
hostname: peer1
health-check-url: http://${eureka.instance.hostname}:${server.port}/${server.servlet.context-path}/actuator/health
client:
#从其他两个实例同步服务注册信息
fetch-registry: true
#向其他的两个eureka注册当前eureka实例
register-with-eureka: true
service-url:
defaultZone: http://peer2:8761/eureka/eureka/,http://peer3:8761/eureka/eureka/
application-peer2.yml
- hostname为peer2
- defaultZone为peer1和peer3
server:
port: 8761
servlet:
context-path: /eureka
spring:
application:
name: eureka-server
eureka:
instance:
hostname: peer2
health-check-url: http://${eureka.instance.hostname}:${server.port}/${server.servlet.context-path}/actuator/health
client:
#从其他两个实例同步服务注册信息
fetch-registry: true
#向其他的两个eureka注册当前eureka实例
register-with-eureka: true
service-url:
defaultZone: http://peer1:8761/eureka/eureka/,http://peer3:8761/eureka/eureka/
application-peer3.yml
- hostname为peer3
- defaultZone为peer1和peer2
server:
port: 8761
servlet:
context-path: /eureka
spring:
application:
name: eureka-server
eureka:
instance:
hostname: peer3
health-check-url: http://${eureka.instance.hostname}:${server.port}/${server.servlet.context-path}/actuator/health
client:
#从其他两个实例同步服务注册信息
fetch-registry: true
#向其他的两个eureka注册当前eureka实例
register-with-eureka: true
service-url:
defaultZone: http://peer1:8761/eureka/eureka/,http://peer2:8761/eureka/eureka/
为eureka server加入actuator
spring-boot-starter-actuator是为Spring Boot服务提供相关监控信息的包。因为我们的eureka server要互相注册,并检查彼此的健康状态,所以这个包必须带上。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
CentOS7环境下部署eureka集群
如果你的网络环境内没有DNS,需要配置/etc/hosts文件,将主机名称与ip地址关联。这一步必须要做,否则linux主机之间通过hostname访问eureka服务将无效,每台eureka server主机上都要执行。
192.168.161.3 peer1
192.168.161.4 peer2
192.168.161.5 peer3
开放防火墙端口(CentOS7),每台eureka server主机上都要执行。
firewall-cmd --zone=public --add-port=8761/tcp --permanent
firewall-cmd --reload
- 第一条命令式在防火墙开放8761端口
- 第二条命令是是开放端口重新加载,使生效
将dhy-server-eureka通过maven打包,然后上传CentOS主机,启动Eureka服务注册中心集群
# 在peer1主机执行
nohup java -jar -Dspring.profiles.active=peer1 dhy-server-eureka-1.0.jar &
# 在peer2主机执行
nohup java -jar -Dspring.profiles.active=peer2 dhy-server-eureka-1.0.jar &
# 在peer3主机执行
nohup java -jar -Dspring.profiles.active=peer3 dhy-server-eureka-1.0.jar &
访问测试
访问http://192.168.161.3:8761/eureka/,即:访问peer1的eureka服务。可以见到DS Replicas中已经注册了peer3、peer2。
同理:
- 访问http://192.168.161.4:8761/eureka/,即:访问peer2的eureka服务。可以见到DS Replicas中已经注册了peer1、peer3。
- 访问http://192.168.161.5:8761/eureka/,即:访问peer3的eureka服务。可以见到DS Replicas中已经注册了peer1、peer2。
出现上面的这种eureka server之间互相注册的效果,表示我们的eureka服务注册中心集群模式搭建成功了!那么恭喜你,你是一个幸运儿。在实际的生产环境中,网络及主机环境往往更复杂,搭建过程的参数调整也更加复杂。
其他需要注意的点:
出现 unavailable-replicas 问题,首先要去检查一下你的health-check-url
是否能正常响应。如果没有设置context-path
,默认是:http://ip:端口/actuator/health
。UP状态表示处于可用状态。
如果健康检查没有问题:
1.是否开启了register-with-eureka=true
和fetch-registry=true
2.eureka.client.serviceUrl.defaultZone
配置项的地址,不能使用localhost
,要使用ip
或域名。或者可以通过hosts
或者DNS
解析的主机名称hostname
。
3.spring.application.name
要一致,不配置也可以,配置了要一致
4.默认情况下,Eureka
使用 hostname(如:peer1、peer2、peer3)
进行服务注册,以及服务信息的显示(eureka web
页面),那如果我们希望使用 IP
地址的方式,该如何配置呢?答案就是eureka.instance.prefer-ip-address=true
。当设置prefer-ip-address: true
时 ,修改配置defaultZone:http://你的IP:9001/eureka/
。如果此时你仍然使用http://peer1:8761/eureka
会导致健康检查失败。
Eureka集群多网卡环境ip设置
在上一小节,我们为大家讲解了如何在linux环境下搭建集群式Eureka服务注册中心。有的朋友可能会遇到下面的问题(导致服务注册失败、健康检查失败):
上图中蓝色部分:大家可以明确的看到eureka server的服务绑定的ip是10.0.2.15?这是为什么?我们上一节中,也没有使用过这个ip啊,我们使用的是192.168.161.3。这是因为我的CentOS服务器上有多个网卡,还有一些docker相关的虚拟网卡。“多网卡”在生产环境上是非常常见的情况。怎么让eureka server服务绑定实例我们期望它绑定的网卡
?
配置实现
首先来看一下,我的服务器(虚拟机)上面的网卡设备,一共五个(虚拟的)。
- docker0虚拟网卡是因为我在这台机器上安装了docker
- enp0s3网卡是一个NAT网络的网卡,虚拟机上常用。大家注意它的ip是10.0.2.15。
- enp0s8才是我们真正需要使用的本地网络。
那我们现在要做的就是通知spring cloud
,我们部署的微服务希望ip
是192.168
的本地网段。不要使用docker0和enp0s3
的网段。
-
spring.cloud.inetutils.preferredNetworks
表示我们期望使用的网段,可以使用正则表达式 -
spring.cloud.inetutils.ignoredInterfaces
表示我们希望忽略掉的网卡设备。 - 另外我们重新配置了
eureka.instance.instance-id
。这个问题比较特殊,spring cloud
在组成instance-id
规则的时候,并没有遵守我们的preferredNetworks和ignoredInterfaces
约定(有可能是版本问题,没准下一个版本就好了)。所以我们不要在instance-id
使用ip
(因为enp0s3
虚拟机桥接网卡的ip
在所有的虚拟机上都是10.0.2.15
),这导致所有eureka server的instance-id
全一样,所以只能注册成功其中一个。
server:
port: 8761
servlet:
context-path: /eureka
spring:
application:
name: eureka-server
cloud:
inetutils:
preferredNetworks:
- 192.168
ignoredInterfaces:
- enp0s3
- docker0
eureka:
instance:
hostname: peer1
instance-id: ${spring.application.name}-${eureka.instance.hostname}:${server.port}
health-check-url: http://${eureka.instance.hostname}:${server.port}/${server.servlet.context-path}/actuator/health
client:
#从其他两个实例同步服务注册信息
fetch-registry: true
#向其他的两个eureka注册当前eureka实例
register-with-eureka: true
service-url:
defaultZone: http://peer2:8761/eureka/eureka/,http://peer3:8761/eureka/eureka/
多网卡ip选择配置方法总结归纳
除去上面的配置方法,还有其他能实现多网卡ip
选择的方式,可以根据自己的网络环境情况选择使用。归纳如下:
方法一:直接配置eureka.instance.ip-address
eureka.instance.ip-address=192.168.1.7
直接配置一个完整的ip,一般适用于环境单一场景,对于复杂场景缺少有利支持。比如:你的eureka环境是结合docker容器部署的,就会有问题。因为docker容器的ip是动态的不固定的,所以你很难为docker容器中的服务指定ip。所以这种方式通常不建议使用。
方法二:增加inetutils相关配置
配置对应org.springframework.cloud.commons.util.InetUtilsProperties
,其中包含:
配置 | 说明 |
---|---|
spring.cloud.inetutils.default-hostname | 默认主机名,只有解析出错才会用到 |
spring.cloud.inetutils.default-ip-address | 默认ip地址,只有解析出错才会用到 |
spring.cloud.inetutils.ignored-interfaces | 配置忽略的网卡地址 |
spring.cloud.inetutils.preferred-networks | 期望优先匹配的网卡,正则匹配的ip地址或者ip前缀 |
spring.cloud.inetutils.timeout-seconds | 计算主机ip信息的超时时间,默认1秒钟 |
spring.cloud.inetutils.use-only-site-local-interfaces | 只使用内网ip |
上面已经为大家介绍了ignored-interfaces和preferred-networks用法,其他的配置举例说明如下:
使用/etc/hosts中主机名称映射的ip,这一种在docker swarm环境中比较好用。
# 随便配置一个不可能存在的ip,会走到InetAddress.getLocalHost()逻辑。
spring.cloud.inetutils.preferred-networks=none
当所有的网卡遍历逻辑都没有找到合适的网卡ip,会走JDK的InetAddress.getLocalHost()。该方法会返回当前主机的hostname, 然后会根据hostname解析出对应的ip。
# 只使用内网地址,遵循 RFC 1918
# 10/8 前缀
# 172.16/12 前缀
# 192.168/16 前缀
spring.cloud.inetutils.use-only-site-local-interfaces=true
通过启动命令行传递配置
java -jar xxx.jar --spring.cloud.inetutils.preferred-networks= #需要设置的IP地址
或者
java -jar xxx.jar --spring.cloud.inetutils.ignored-interfaces= #需要过滤掉的网卡
源码解析
为了说明这个问题的解决方案,我们需要翻看一下Eureka Client
的源码。com.netflix.appinfo
包下的InstanceInfo
类封装了本机信息,其中就包括了IP
地址。在 Spring Cloud
环境下,Eureka Client
并没有自己实现探测本机IP
的逻辑,而是交给Spring
的InetUtils
工具类的findFirstNonLoopbackAddress()
方法完成的:
public InetAddress findFirstNonLoopbackAddress() {
InetAddress result = null;
try {
// 记录网卡最小索引
int lowest = Integer.MAX_VALUE;
// 获取主机上的所有网卡
for (Enumeration<NetworkInterface> nics = NetworkInterface
.getNetworkInterfaces(); nics.hasMoreElements();) {
NetworkInterface ifc = nics.nextElement();
if (ifc.isUp()) {
log.trace("Testing interface: " + ifc.getDisplayName());
if (ifc.getIndex() < lowest || result == null) {
lowest = ifc.getIndex(); // 记录索引
}
else if (result != null) {
continue;
}
// 判断是否是被忽略的网卡
if (!ignoreInterface(ifc.getDisplayName())) {
for (Enumeration<InetAddress> addrs = ifc
.getInetAddresses(); addrs.hasMoreElements();) {
InetAddress address = addrs.nextElement();
if (address instanceof Inet4Address
&& !address.isLoopbackAddress()
&& !ignoreAddress(address)) {
log.trace("Found non-loopback interface: "
+ ifc.getDisplayName());
result = address;
}
}
}
// @formatter:on
}
}
}
catch (IOException ex) {
log.error("Cannot get first non-loopback address", ex);
}
if (result != null) {
return result;
}
try {
// 如果以上逻辑都没有找到合适的网卡,则使用JDK的InetAddress.getLocalhost()
return InetAddress.getLocalHost();
}
catch (UnknownHostException e) {
log.warn("Unable to retrieve localhost");
}
return null;
}
Eureka集群服务注册与安全认证
服务注册配置
目前我们的项目中有两个微服务,我们需要将它们注册到服务注册中心。
由于我们将eureka server从单节点升级为集群,所以响应的注册配置也要修改,但总体上大同小异。主要修改的内容如下:
- defaultZone从单节点配置修改为集群配置,各个
eureka server
配置之间使用逗号分隔。 - 微服务的ip选择,如果你的服务启动主机是多网卡的,这个要配置一下
spring.cloud.inetutils
。下面的配置我把VirtualBox
虚拟机网卡忽略掉,使用本地局域网络192.168
。
spring:
application:
name: aservice-sms
cloud:
inetutils:
preferredNetworks:
- 192.168
ignored-interfaces:
- .*VirtualBox.*
eureka:
client:
service-url:
defaultZone: http://dhy:centerpwd@peer1:8761/eureka/eureka/,http://dhy:centerpwd@peer2:8761/eureka/eureka/,http://dhy:centerpwd@peer3:8761/eureka/eureka/
FTP地址格式如下:“ftp://用户名:密码@FTP服务器IP”
对应下面一会讲到的spring security加密设置
因为defaultZone中使用主机名称访问注册服务,所以需要配置hosts文件。在windows中该文件C:\windows\System32\drivers\etc\hosts,在linux中该文件是/etc/hosts。
192.168.161.3 peer1
192.168.161.4 peer2
192.168.161.5 peer3
Eureka安全认证(服务端)
在此之前,我们的微服务向服务注册中心注册都是使用的公开访问权限,在实际的生产应用中,这很危险。因为随便的一个什么人,知道这个服务注册地址,都可以写一个服务注册到上面。通常我们需要使用eureka server对注册服务进行一个Spring Security的HttpBasic安全认证,虽然这个认证很简陋,但是能起到“防君子不防小人的”作用。
在dhy-server-eureka项目的pom.xml 中添加 Spring-Security 的依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
然后在dhy-server-eureka项目的 application.yml中加上认证的配置信息(用户名和密码):
spring:
security:
user:
name: dhy
password: centerpwd
然后在dhy-server-eureka项目增加Spring Security 配置类:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭csrf
http.csrf().disable();
// 支持httpBasic认证方式
http.authorizeRequests().anyRequest().authenticated().and().httpBasic();
}
}
- 重新启动注册中心,浏览器访问服务注册中心,此时浏览器会提示你输入用户名和密码,输入正确后才能继续访问 Eureka 提供的管理页面
Eureka安全认证(客户端)
在 Eureka Server开启认证后,所有的服务(包括eureka-server本身)向服务注册中心注册的时候也要加上认证的用户名和密码信息:
defaultZone: http://dhy:centerpwd@peer1:8761/eureka/eureka/,http://dhy:centerpwd@peer2:8761/eureka/eureka/,http://dhy:centerpwd@peer3:8761/eureka/eureka/
不只是aservice-rbac和aservice-sms的defaultZone需要加上用户名密码,所有需要向服务注册中心注册的微服务都要加上,否则无法正确的注册,会被安全策略拦截。即:eureka-server的defaultZone配置都要修改。
Eureka自我保护与健康检查
Eureka的健康检查
- 在
Status
栏显示着UP
,表示该服务及其多实例处于状态正常。其它取值DOWN、OUT_OF_SERVICE、UNKNOWN
等均表示该服务处于不可被请求的状态,只有UP
状态的微服务会被请求。 - 由于Eureka Server与Eureka Client之间使用心跳机制来确定Eureka Client(微服务实例)的状态。也就是说,当服务器端与客户端的心跳保持正常,服务的状态就会始终保持“UP”状态。所以说该UP状态不能完全说明该服务可以正常响应HTTP请求,只能说明Eureka Server与Eureka Client之间存在正常心跳。
- 基于上一点原因,Spring Boot Actuator提供了/health端点,该端点可展示应用程序的健康信息,只有将该端点中的健康状态传播到Eureka Server就可以了,实现这点很简单,只需为微服务配置如下内容:
#开启健康检查(需要spring-boot-starter-actuator依赖)(默认就是开启的)
eureka.client.healthcheck.enabled = true
如果需要更细粒度健康检查,可实现com.netflix.appinfo.HealthCheckHandler
接口 。 EurekaHealthCheckHandler
已实现了该接口。
正常情况下,当微服务的心跳消失,健康检查失败后,eureka server会将该服务从服务列表中剔除
。表示该服务下线了。 但是一些异常情况下,eureka不会剔除服务,比如:eureka自我保护模式被开启的情况下。
Eureka自我保护模式
访问Eureka主页时,如果看到这样一段大红色的句子,那么表明Eureka的自我保护模式被启动了。
- 默认情况下,如果在15分钟内超过85%的客户端节点都没有正常的心跳,那么Eureka就认为这些客户端与注册中心出现了网络故障(比如网络故障或频繁的启动关闭客户端),Eureka Server自动进入自我保护模式。
- 进入保护状态后,eureka server不再剔除任何服务,当网络故障恢复后,该节点自动退出自我保护模式。避免将仍在正常运行的服务,因为eureka server服务的网络问题导致其他业务服务无法正常发现及调用。
当个别服务健康检查失败,eureka server认为是该服务正常下线了,将其服务从列表中剔除。但是当85%以上服务都收不到心跳了,eureka server会认为是自己出了问题,就开启保护模式,不再将服务从列表中剔除。从而保障微服务系统的可用性。
eureka:
server:
#自我保护模式,当出现网络分区故障、频繁的开启关闭客户端、eureka在短时间内丢失过多客户端时,会进入自我保护模式,即一个服务长时间没有发送心跳,eureka也不会将其删除,默认为true
enable-self-preservation: true
#eureka server清理无效节点的时间间隔,默认60000毫秒,即60秒
eviction-interval-timer-in-ms: 60000
#阈值更新的时间间隔,单位为毫秒,默认为15 * 60 * 1000
renewal-threshold-update-interval-ms: 15 * 60 * 1000
#阈值因子,默认是0.85,如果阈值比最小值大,则自我保护模式开启
renewal-percent-threshold: 0.85
#清理任务程序被唤醒的时间间隔,清理过期的增量信息,单位为毫秒,默认为30 * 1000
delta-retention-timer-interval-in-ms: 30000
- 通常而言,生产环境建议开启自我保护模式,从而保障生产环境的健壮性。
- 测试环境可以将自我保护模式关闭,因为我们可能经常的启停微服务,导致心跳消失。经常性的触发自我保护模式,导致该下线的服务没下线。
关闭 Eureka 的自我保护模式
可以使用eureka.server.enable-self-preservation=false
来禁用自我保护模式,
- 通常而言,生产环境建议开启自我保护模式,从而保障生产环境的健壮性。
- 开发测试环境可以将自我保护模式关闭,因为我们可能经常的启停微服务,导致心跳消失。经常性的触发自我保护模式,导致该下线的服务没下线,导致调试异常。
关闭自我保护模式,需要在服务端和客户端配置。
服务端配置:
eureka:
server:
enable-self-preservation: false
#eureka server清理无效节点的时间间隔,默认60000毫秒,即60秒
eviction-interval-timer-in-ms: 60000 # 单位毫秒
客户端配置:
# 心跳检测检测与续约时间
# 测试时将值设置设置小些,保证服务关闭后注册中心能及时踢出服务
eureka:
instance:
lease-renewal-interval-in-seconds: 5 #默认30秒
lease-expiration-duration-in-seconds: 10 #默认90秒
配置说明
lease-renewal-interval-in-seconds 每间隔5s,向服务端发送一次心跳,证明自己依然”存活“。
lease-expiration-duration-in-seconds 告诉服务端,如果我10s之内没有给你发心跳,就代表我“死”了,请将我踢掉。
主流服务注册中心对比(含nacos)
在开始为大家介绍主流的服务注册中心之前,先给大家介绍一些分布式系统的常用理论知识,这样我们在后文为大家介绍主流的服务注册中心的时候,才能更好的理解它们之间的差异
垂直扩展与水平扩展
先为大家说明一下,应用的垂直扩展与水平扩展之间的区别。
垂直扩展
:将更多资源(CPU,内存)添加到现有的应用程序所在的服务器上。好处在于:我们不需要进行额外的开发,即可完成应用的处理能力的升级。但是,每次扩展服务器/主机时,垂直扩展的成本几乎都呈指数增长。并且垂直扩展很容易触及硬件资源的上限。
水平扩展
:是一种添加更多具有标准容量的服务器并同时运行多个应用程序副本的方法。与垂直扩展相比,此方法的最大优点是由于同一应用程序部署多个副本,提升了应用本身的容错能力(其中一个副本出现问题,其他的副本还能工作)。但是水平扩展也带来了一些挑战,比如应用程序之间的网络更多,架构设计更复杂。
CAP理论
CAP理论是埃里克·布鲁尔(Eric Brewer) 提出的理论。在分布式系统领域,特别是大数据应用、NOSQL数据库等分布式复杂系统的设计方面给出了很好的指导方针。CAP理论的核心思想就是:强数据一致性、高可用、分区容忍性之间,只能三选二。也就是说:当你选择其中的两项作为来满足你的系统需求,就必须舍弃第三项,三项无法同时满足。
下面为大家介绍一下强数据一致性、高可用、分区容忍性的具体含义。
强数据一致性(High Consistency )
国内很多文章,在提到CAP的C时都是说“一致性”,这是不对的,这个C必须是强数据一致性。如果应用仅仅满足了弱一致性或者最终一致性,它并不满足CAP的C特性。
- 强一致性: 强一致性是一种一致性模型,在该模型中,对分布式系统的所有后续访问将在更新后始终返回更新后的值。不能出现脏读或者幻读的现象,这种强一致性通常是通过数据锁(悲观锁)来实现的。
-
弱一致性
: 这是一种用于分布式计算的一致性模型,其中后续访问可能并不总是返回更新后的值。可能在某一时间段会有数据不一致的响应。这是一种尽力而为的一致性模型,分布式应用之间彼此通过网络交换数据,但是因为没有数据锁定机制,可能导致不同节点同一时间对外提供的服务数据不一致。 -
最终一致性
: 最终一致性是弱一致性中的一种特殊类型。这种模型的数据一致性,通常是通过消息队列来实现的,数据提供方将数据放入消息队列,他就不再去关注该数据是否被处理。消息队列保证数据被数据消费方成功处理一次(并且值处理一次)。至于该数据什么时候被成功处理,那就不一定了。
记住:CAP中的C是High C,也就是强数据一致性
。 我们常用的Eureka是一种弱一致性分布式系统,所以它是满足了CAP中的AP特性,舍弃了强数据一致性C。
高可用性(High Availability)
高可用通俗的说,就是任何时间都可用,任何请求都可以得到响应。怎么保障高可用?
应用水平扩展n个节点,其中1个或者几个节点挂了,其他的节点仍然可以支撑整体服务的运行。
什么样的系统是CA系统?
比如:我们的关系型数据库Oracle、MySQL,都是可以搭建高可用环境的,也就是满足A特性。
那么关系型数据库是如何保证强数据一致性的?
假如:我们现在有一个订单库DB、一个产品库DB,如何保障一个事务中操作这两个库,同时保证强数据一致性?
答案通常是:分布式事务,如:两阶段提交等。
分区容忍性(Partition Tolerance)
笔者之前曾经给一些朋友讲分区容忍性,发现他们理解这个概念有点困难。那我们就先去讲什么是AP,什么是CP,再讲什么是P。
- AP系统:
在分布式系统中某些节点网络不可达或故障的时候,仍然可以对外正常提供服务的系统
。比如:我们的Eureka服务注册中心,将部分节点的网络切断,仍然可以提供服务注册与发现的服务。需要注意的一点是:AP系统不是完全抛弃数据一致性,而是无法保证数据的强一致性。 - CP系统:在分布式系统中某些节点
网络不可达或故障
的时候,仍然以保证系统节点之间的数据一致性优先
,可能导致整个服务短暂锁定或者宕机。我们后面章节为大家介绍的zookeeper和consul都是这种系统。以consul为例,服务配置注册到consul的A节点,A节点必然要将该注册信息同步到B节点,如果B节点因为网络问题不可达,并且该节点是leader(领导者)节点。这就导致整个consul集群要重新选举leader节点,这将导致在选举期间无法对外提供服务。
那么,什么是分区容忍性?
通常是指分布式系统部分节点由于网络故障导致不可达,仍然可以对外提供满足一致性或者高可用的服务。
服务注册中心的CAP
由上面的讲解,大家可以知道eureka是AP系统,consul和zookeeper是CP系统,我们后面章节要重点为大家介绍的nacos支持CP或AP。笔者认为,就服务注册中心而言,AP要好于CP。
主流注册中心产品
核心对比 | Nacos | Eureka | Consul | Zookeeper |
---|---|---|---|---|
一致性协议 | CP或AP | AP | CP | CP |
版本迭代 | 迭代升级中 | 不再升级 | 迭代升级中 | 迭代升级中 |
SpringCloud集成 | 支持 | 支持 | 支持 | 支持 |
Dubbo集成 | 支持 | 不支持 | 不支持 | 支持 |
K8S集成 | 支持 | 不支持 | 支持 | 不支持 |
zookeeper概念及功能简介
zookeeper简介
zookeeper是一个分布式应用的协调服务。用于对分布式系统进行配置管理、节点管理、leader选举、分布式锁等。ZooKeeper最初是由“ Yahoo!”开发的。后来,Apache ZooKeeper成为Hadoop,HBase和其他分布式框架常用的服务配置管理的标准。
zookeeper 将集群内的节点进行了这样三种角色划分(上图中的Client不属于zookeeper集群
):
- leader:负责进行投票选举的发起和决议,更新系统状态。
- follower:用于接收客户端请求并向客户端返回结果以及在选举过程中参与投票。
- observer:也可以接收客户端连接,将写请求转发给leader节点,但是不参与投票过程,只同步leader的状态。通常对查询操作做负载。
zookeeper集群是一个强调保障数据一致性的分布式系统(CP),客户端发起的每次查询操作,集群节点都能返回同样的结果。
那么问题来了:对于客户端发起的修改、删除等能改变数据的操作呢?
集群中那么多台机器,如果是你修改你的,我修改我的,没有统一管理,最后查询返回集群中哪台机器的数据呢?
如果随意操作,就不能保证数据一致性了。于是在zookeeper集群中,leader的作用就体现出来了,
- 只有leader节点才有权利发起修改数据的操作,follower节点即使接收到了客户端发起的修改操作,也要将其转交给leader来处理
- leader接到数据修改的请求后,会向所有follower广播一条消息,让他们执行修改操作
- follower 执行完后,便会向 leader 回复执行完毕。当 leader 收到半数以上的 follower的确认消息,便会判定该操作执行完毕。
- 完成同步数据的follower,可以对外提供下一次查询或修改数据的服务。
这样就保障了zookeeper内各个节点的数据一致性。所以zookeeper集群中leader是不可缺少的,但是 leader 节点是怎么产生的呢?
其实就是由所有follower 节点选举产生的,且leader节点只能有一个。当前leader如果因为某些原因挂掉了,集群内剩余的节点会重新选举leader。
集群节点必须是奇数个
我们要搭建服务注册中心zookeeper集群,必须搭建奇数个节点,这是为什么呢?
容错率
首先从容错率来说明:(需要保证集群能够有半数进行投票)
- 2台服务器,至少2台正常运行才行(2的半数为1,半数以上最少为2),正常运行1台服务器都不允许挂掉,但是相对于
单节点服务器,2台服务器还有两个单点故障,所以直接排除了。 - 3台服务器,至少2台正常运行才行(3的半数为1.5,半数以上最少为2),正常运行可以允许1台服务器挂掉
- 4台服务器,至少3台正常运行才行(4的半数为2,半数以上最少为3),正常运行可以允许1台服务器挂掉
- 5台服务器,至少3台正常运行才行(5的半数为2.5,半数以上最少为3),正常运行可以允许2台服务器挂掉
防脑裂
脑裂集群的脑裂通常是发生在节点之间通信不可达的情况下,集群会分裂成不同的小集群,小集群各自选出自己的leader节点,导致原有的集群出现多个leader节点的情况,这就是脑裂。
- 3台服务器,投票选举半数为1.5,一台服务裂开,和另外两台服务器无法通行,这时候2台服务器的集群(2票大于半数1.5票),所以可以选举出leader,而1 台服务器的集群无法选举。
- 4台服务器,投票选举半数为2,可以分成 1,3两个集群或者2,2两个集群,对于1,3集群,3集群可以选举;对于2,2集群,则不能选择,造成没有leader节点。
- 5台服务器,投票选举半数为2.5,可以分成1,4两个集群,或者2,3两集群,这两个集群分别都只能选举一个集群,满足zookeeper集群搭建数目。
以上分析,我们从容错率以及防止脑裂两方面说明了3台服务器是搭建集群的最少数目,4台服务器发生脑裂时会造成没有leader节点的错误。
临时节点与持久节点
zookeeper中的数据是以树形结构组织的,类似于树形文件系统。树中的节点称为znode,znode按持久化类型分类可以分为:
持久znode
即使在创建该特定znode的客户端断开连接后,持久znode数据仍然存在。默认情况下,所有znode都是持久的。
临时znode
当客户端处于活跃状态时,该客户端创建的临时znode就是有效的。当客户端与ZooKeeper集合断开连接时,临时znode会自动删除。
Spring Cloud 微服务向zookeeper注册的过程就是在zookeeper增加临时znode节点,znode节点中保存了服务的注册信息。也正是利用了临时客户端断开连接后删除znode的的特性,实现了服务的自动下线。
zookeeper除了上面的这些内容,还能做很多事情,如:分布式锁。还有很多应用、运维的知识可以学习。本文只为大家介绍与“服务注册中心”相关的理论基础知识。下一节我们就手动搭建一个zookeeper集群。
zookeeper-linux集群安装
环境准备
zookeeper官网下载地址:http://mirror.bit.edu.cn/apache/zookeeper/stable/。一定要注意从3.5.5开始,带有bin名称的包才是我们想要的下载可以直接使用的里面有编译后的二进制的包,而之前版本的普通的tar.gz的包里面是只是源码的包无法直接使用。
安装JDK:由于zookeeper集群的运行需要Java运行环境,所以需要首先安装 JDK
主机名称 | 主机ip |
---|---|
peer1 | 192.168.161.3 |
peer2 | 192.168.161.4 |
peer3 | 192.168.161.5 |
安装 zookeeper
解压
在 /usr/local 目录下新建 software 目录,然后将 zookeeper 压缩文件上传到该目录中,然后通过如下命令解压。
mkdir -p /opt/zookeeper; #创建zookeeper目录
mkdir -p /opt/zookeeper/data; #创建zookeeper数据存放目录
mkdir -p /opt/zookeeper/log; #创建zookeeper日志存放目录
tar -zxvf apache-zookeeper-3.5.7-bin.tar.gz -C /opt/zookeeper #将zookeeper解压到-C指定的目录
这里我用的是zookeeper最新的stable版本3.5.7,根据你自己的下载版本及安装包调整上面的命令。
修改配置文件 zoo.cfg
将zookeeper压缩文件解压后,我们进入到 apache-zookeeper-3.5.7/conf
目录:
将 zoo_sample.cfg 文件复制并重命名为 zoo.cfg 文件。
cp /opt/zookeeper/apache-zookeeper-3.5.7-bin/conf/zoo_sample.cfg /opt/zookeeper/apache-zookeeper-3.5.7-bin/conf/zoo.cfg
然后通过 vim zoo.cfg 命令对该文件进行修改:
vim /opt/zookeeper/apache-zookeeper-3.5.7-bin/conf/zoo.cfg
上面红色框住的内容即是我们修改的内容,蓝色框是我们新增的内容:
- tickTime:基本事件单元,这个时间是作为Zookeeper服务器之间或客户端与服务器之间维持心跳的时间间隔,每隔tickTime时间就会发送一个心跳;最小的session过期时间为2倍tickTime
- dataDir:存储内存中数据快照的位置,除非另有说明,否则指向数据库更新的事务日志。注意:应该谨慎的选择日志存放的位置,使用专用的日志存储设备能够大大提高系统的性能,如果将日志存储在比较繁忙的存储设备上,那么将会很大程度上影像系统性能。
- clientPort:监听客户端连接的端口。
- initLimit:允许follower连接并同步到Leader的初始化连接时间,以tickTime为单位。当初始化连接时间超过该值,则表示连接失败。
- syncLimit:表示Leader与Follower之间发送消息时,请求和应答时间长度。如果follower在设置时间内不能与leader通信,那么此follower将会被丢弃。
- server.A=B:C:D
A:其中 A 是一个数字,表示这个是服务器的编号id;
B:是这个服务器的 ip 地址;
C: Leader选举的端口;
D: Zookeeper服务器之间的通信端口。
我们需要修改的第一个是 dataDir ,在指定的位置处创建好目录。
第二个需要新增的是 server.A=B:C:D 配置,其中 A 对应下面我们即将介绍的myid 文件。B是集群的各个IP地址,C:D 是端口配置。
创建 myid 文件
在 上一步 dataDir 指定的目录下,创建 myid 文件。然后在该文件添加上一步 server 配置的对应 A 数字,服务器的编号id。
touch /opt/zookeeper/data/myid; #创建文件
echo "0" > /opt/zookeeper/data/myid; #写入id
上面的命令是在192.168.161.3上面执行的,所以写入的id为0。后面的机器依次在相应目录创建myid文件,写上相应配置数字即可。
配置环境变量
为了能够在任意目录启动zookeeper集群,我们需要配置环境变量。
你也可以不配,这不是搭建集群的必要操作,只不过如果你不配置环境变量,那么每次启动zookeeper需要到安装文件的 bin 目录下去启动。
首先进入到 /etc/profile 目录,添加相应的配置信息:
#set zookeeper environment
export ZK_HOME=/opt/zookeeper/apache-zookeeper-3.5.7-bin
export PATH=$PATH:$ZK_HOME/bin
然后通过如下命令使得环境变量生效:
source /etc/profile
开启防火墙端口
firewall-cmd --zone=public --add-port=2181/tcp --permanent;
firewall-cmd --zone=public --add-port=2888/tcp --permanent;
firewall-cmd --zone=public --add-port=3888/tcp --permanent;
firewall-cmd --reload
启动zookeeper服务
启动命令:
zkServer.sh start
停止命令:
zkServer.sh stop
重启命令:
zkServer.sh restart
查看集群节点状态:
zkServer.sh status
我们分别对集群三台主机执行状态查看命令,其中一台是leader,其他两台是follwer。
出现上面的状态,基本可以认定,我们的集群安装是成功的。启动日志可以通过如下的文件查看:
启动日志中会有一些异常,如果是防火墙端口已经正确打开,通常是因为启动顺序的原因导致的,无关紧要。A连接B,B还没启动,所以抛出“拒绝连接”异常。等B启动了就好了。
zookeeper服务注册与发现
微服务向zookeeper注册
引入依赖包
在之前的章节已经为大家介绍过微服务向eureka注册的实现过程,大同小异。首先我们需要通过maven坐标引入zookeeper的包。
如果项目pom之前存在spring-cloud-starter-netflix-eureka-client,与eureka相关的要删掉。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
配置zookeeper
有过eureka和我们安装zookeeper集群的经验,下面的配置都很好理解。
spring:
cloud:
zookeeper:
connectString: 192.168.161.3:2181,192.168.161.4:2181,192.168.161.5:2181
discovery:
register: true
prefer-ip-address: true
enabled: true
特别说一下 prefer-ip-address这个配置,如果不设置为true。服务注册的时候注册的访问地址是当前服务运行所在的主机的hostname(所以通常需要配置/etc/hosts才能正确的实现ip与主机hostname映射,才能正确的进行远程服务调用)
设置 prefer-ip-address: true之后,服务注册的时候注册的访问地址是当前服务运行所在的主机的ip.(不用配置/etc/hosts,即可正确实现远程服务调用)
打开discovery开关
以上的步骤,在所有需要向zookeeper注册的服务上都要进行。如:aservice-rbac、aservice-sms
验证服务注册结果
zookeeper官方并不提供web管理界面(有一些第三方开发的)。所以我们通过命令行来查看服务注册的结果。
zkCli.sh -server 192.168.161.3:2181
我们之前已经给大家介绍过,zookeeper的数据存储结果是按目录的树形结构。所以,看下面的命令:
- 通过ls查看目录,“/”根目录,“/services”服务注册根目录,“/services/aservice-sms”的服务注册信息。
- 通过get命令查看具体的服务注册实例信息
通过上面的命令进一步认证我们的zookeeper集群安装的正确性。
FeignClient远程服务名称调整
需要注意的是,zookeeper注册的服务名称是spring.application.name,但是并不会将服务名称转成大写,这点与eureka有所区别。所以在使用FeignClient的时候,不能写“ASERVICE-SMS”,而是写“aservice-sms”。
访问测试
访问aservice-rbac的“/pwd/reset”接口(该接口中远程调用了aservice-sms的“/sms/send”短信发送方法),得到正确结果。说明我们的集群安装及服务注册发现全部可以正确的使用。
consul概念及功能介绍
Consul是什么?
Consul 官方站点:https://www.consul.io/
Consul 是一个支持多数据中心分布式高可用的服务发现和配置共享的服务软件,由 HashiCorp 公司用 Go 语言开发, 基于 Mozilla Public License 2.0 的协议进行开源. Consul 支持健康检查,并允许 HTTP 和 DNS 协议调用 API 存储键值对. 一致性协议采用 Raft 算法,用来保证服务的高可用. 使用 GOSSIP 协议管理成员和广播消息, 并且支持 ACL 访问控制.。它具备以下特性:
- 服务发现:Consul提供了通过DNS或者HTTP接口的方式来注册服务和发现服务。一些外部的服务通过Consul很容易的找到它所依赖的服务。
- 健康检测: Consul的Client提供了健康检查的机制,可以通过用来避免流量被转发到有故障的服务上。
- Key/Value存储: 应用程序可以根据自己的需要使用Consul提供的Key/Value存储。
Consul提供了简单易用的HTTP接口,结合其他工具可以实现动态配置、功能标记、领袖选举等等功能。 - 多数据中心: Consul支持开箱即用的多数据中心. 这意味着用户不需要担心需要建立额外的抽象层让业务扩展到多个区域。
- 提供自带的web管理界面,zookeeper没有自带的管理界面。
- consul也可以实现分布式的配置管理,但是我们在spring cloud范围内很少使用到。
Spring Cloud服务与consul
consul作为服务注册中心,并没有与eureka、zookeeper在核心的流程上有区别。仍然遵循于服务注册与发现的基本实现逻辑。
- 当Spring Cloud服务客户端注册Consul时,它提供有关自身的元数据,如主机和端口,ID,名称和标签等信息。
- Consul通过HTTP API和DNS提供服务发现功能。Spring Cloud Consul客户端利用HTTP API进行服务注册和发现。
- Consul实例的运行状况检查默认为“/actuator/health”,它是Spring Boot Actuator应用程序中有用端点的默认位置。consul默认情况下,将创建一个HTTP 检查,每隔10秒Consul访问注册服务的/actuator/health端点。如果健康检查失败,则服务实例被标记为不可用。
consul集群
很多人看到上面的这张图会晕掉,它并不像我们之前讲过的Eureka和zookeeper的集群部署拓扑那样好理解。下面就带大家一步一步剖析这张图
- 首先,我们要看明白图片的上半部分是数据中心一、图片的下半部分是数据中心二。consul天然支持多数据中心,多数据中心之间支持数据通信。脱离数据中心的概念去理解consul集群,我们只需要关注上半部分即可。
- 与eureka和zookeeper一样,为了增强可用性,都是多节点部署的,每一个节点被称为一个Agent。所有的agent都能运行DNS或者HTTP接口,并负责运行时检查和保持服务同步。
- 但是consul Agent的角色划分更为的复杂一点,consul Agent节点可以分成server和client两种模式运行,server又可以分为leader和follower。
上面这张图没有包含Spring Boot(Cloud)的服务。对于Spring Boot服务而言,consul的server和client都是服务端。所以这里的client和server都是对于consul集群内部而言。
Consul Agent节点运行模式(大使馆) | 特性 |
---|---|
Client(工作人员) | consul client不存储任何信息,当接收到服务注册请求的时候,会将服务注册及查询请求转发给Server进行处理。不参与集群Leader的选举,无状态节点不做数据存储 |
Server Follower(参赞) | 参与Leader选举,维护集群状态,存储服务注册数据,响应本地数据查询。与其他数据中心交互WAN gossip和转发查询给leader或者远程数据中心。 |
Server Leader(大使) | 管理整个集群的数据同步,与consul集群内的其他节点维持心跳 |
consul-linux集群安装
环境准备
IP | 节点名称 | Consul角色 |
---|---|---|
192.168.161.3 | s1 | bootstrap Server |
192.168.161.4 | s2 | Server |
192.168.161.5 | s3 | Server |
192.168.161.6 | c1 | Client(UI) |
bootstrap Server就是人为在consul server启动的时候指定Server Leader,不需要选举。参考下文中agent启动参数 -bootstrap参数说明。
consul 端口说明
端口 | 说明 |
---|---|
TCP/8300 | 8300 端口用于服务器节点。客户端通过该端口 RPC 协议调用服务端节点。服务器节点之间相互调用 |
TCP/UDP/8301 | 8301 端口用于单个数据中心所有节点之间的互相通信,即对 LAN 池信息的同步。它使得整个数据中心能够自动发现服务器地址,分布式检测节点故障,事件广播(如领导选举事件)。 |
TCP/UDP/8302 | 8302 端口用于单个或多个数据中心之间的服务器节点的信息同步,即对 WAN 池信息的同步。它针对互联网的高延迟进行了优化,能够实现跨数据中心请求。 |
8500 | 8500 端口基于 HTTP 协议,用于 API 接口或 WEB UI 访问。 |
8600 | 8600 端口作为 DNS 服务器,它使得我们可以通过节点名查询节点信息。 |
所以主机节点的防火墙开放如下端口:
firewall-cmd --zone=public --add-port=8300/tcp --permanent;
firewall-cmd --zone=public --add-port=8301/tcp --permanent;
firewall-cmd --zone=public --add-port=8302/tcp --permanent;
firewall-cmd --zone=public --add-port=8500/tcp --permanent;
firewall-cmd --zone=public --add-port=8600/tcp --permanent;
firewall-cmd --reload
agent启动参数
consul agent 启动命令参数,consul agent --help
-advertise:通知展现地址用来改变我们给集群中的其他节点展现的地址,一般情况下-bind地址就是展现地址
-bootstrap:用来控制一个server是否在bootstrap模式,在一个datacenter中只能有一个server处于bootstrap模式,当一个server处于bootstrap模式时,可以自己选举为raft leader。
-bootstrap-expect:在一个datacenter中期望提供的server节点数目,当该值提供的时候,consul一直等到达到指定sever数目的时候才会引导整个集群,该标记不能和bootstrap公用。
-bind:该地址用来在集群内部的通讯,集群内的所有节点到地址都必须是可达的,默认是0.0.0.0。
-client:consul绑定在哪个client地址上,这个地址提供HTTP、DNS、RPC等服务,默认是127.0.0.1。
-config-file:明确的指定要加载哪个配置文件
-config-dir:配置文件目录,里面所有以.json结尾的文件都会被加载
-data-dir:提供一个目录用来存放agent的状态,所有的agent都需要该目录,该目录必须是稳定的,系统重启后都继续存在。
-dc:该标记控制agent的datacenter的名称,默认是dc1。
-encrypt:指定secret key,使consul在通讯时进行加密,key可以通过consul keygen生成,同一个集群中的节点必须使用相同的key。
-join:加入一个已经启动的agent的ip地址,可以多次指定多个agent的地址。如果consul不能加入任何指定的地址中,则agent会启动失败。默认agent启动时不会加入任何节点。
-retry-join:和join类似,但是允许你在第一次失败后进行尝试。
-retry-interval:两次join之间的时间间隔,默认是30s。
-retry-max:尝试重复join的次数,默认是0,也就是无限次尝试。
-log-level:consul agent启动后显示的日志信息级别。默认是info,可选:trace、debug、info、warn、err。
-node:节点在集群中的名称,在一个集群中必须是唯一的,默认是该节点的主机名。
-protocol:consul使用的协议版本。
-rejoin:使consul忽略先前的离开,在再次启动后仍旧尝试加入集群中。
-server:定义agent运行在server模式,每个集群至少有一个server,建议每个集群的server不要超过5个。
-syslog:开启系统日志功能,只在linux/osx上生效。
-ui-dir:提供存放web ui资源的路径,该目录必须是可读的。
-pid-file:提供一个路径来存放pid文件,可以使用该文件进行SIGINT/SIGHUP(关闭/更新)agent。
举例:启动Bootstrap Server,具体启动参数实例说明看下文CMD_OPTS
consul agent -server -bootstrap 其他启动参数
举例:启动Server,不加-server启动的agent就是cllient运行模式
consul agent -server 其他启动参数
安装过程
因为consul每次启动都需要配置很多的参数,为了将启动流程及参数固化下来,我们通常会将它写成一个linux 的systemd服务。
不了解systemd服务?白话说就是linux服务启动、停止、重启脚本的一个模板。详细学习的话可以参考https://www.cnblogs.com/jhxxb/p/10654554.html
Systemd 简介
consul下载、解压和目录创建
wget https://releases.hashicorp.com/consul/1.7.2/consul_1.7.2_linux_amd64.zip
unzip consul_1.7.2_linux_amd64.zip
mv consul /usr/local/bin/
mkdir -p /opt/consul/{data,config} #创建数据目录和配置目录
consul配置文件
将以下的内容写入对应主机的配置文件
192.168.161.3:/etc/sysconfig/consul,-bootstrap指定了该agent为Server Leader,无需选举。
CMD_OPTS="agent -server -data-dir=/opt/consul/data -node=s1 -config-dir=/opt/consul/config -bind=192.168.161.3 -rejoin -client=0.0.0.0 -bootstrap"
192.168.161.4:/etc/sysconfig/consul,-join要求该agent启动之后加入192.168.161.3为leader的集群
CMD_OPTS="agent -server -data-dir=/opt/consul/data -node=s2 -config-dir=/opt/consul/config -bind=192.168.161.4 -rejoin -client=0.0.0.0 -join 192.168.161.3"
192.168.161.5:/etc/sysconfig/consul,-join要求该agent启动之后加入192.168.161.3为leader的集群
CMD_OPTS="agent -server -data-dir=/opt/consul/data -node=s3 -config-dir=/opt/consul/config -bind=192.168.161.5 -rejoin -client=0.0.0.0 -join 192.168.161.3"
192.168.161.6:/etc/sysconfig/consul,该节点没有指定-server,说明它是client运行模式。-ui说明该client提供web UI的管理界面。
CMD_OPTS="agent -ui -data-dir=/opt/consul/data -node=c1 -config-dir=/opt/consul/config -bind=192.168.161.6 -rejoin -client=0.0.0.0 -join 192.168.161.3"
linux consul systemd服务配置
这一步可以不做,只是用来简化每次启动consul都输入一串consul命令的痛苦。如果这一小节内容无法理解,直接用consul <上文定义的CMD_OPTS> 启动consul实例即可
创建service配置文件
touch /usr/lib/systemd/system/consul.service
通过cat命令将如下内容写入文件
cat > /usr/lib/systemd/system/consul.service<<EOF
[Unit]
Description=consul
After=network.target
[Service]
EnvironmentFile=-/etc/sysconfig/consul
ExecStart=/usr/local/bin/consul \$CMD_OPTS
ExecReload=/bin/kill -HUP \$MAINPID
KillSignal=SIGTERM
[Install]
WantedBy=multi-user.target
EOF
服务配置完成之后需要重新加载systemd配置文件
systemctl daemon-reload
如果希望consul服务开机启动,执行下面命令
systemctl enable consul #启动命令
journalctl -f #查看启动日志,都可以
journalctl -xe #查看启动日志,都可以
手动启动执行下面的命令
systemctl start consul
因为上面的CMD_OPTS中制定了-join参数,所以我们就不用手动的把启动后的agent加入集群。如果没有加-join参数,我们可以使用如下命令手动将新启动的agent加入集群。
consul join <Leader Server IP> # 非Leader节点执行 join
consul集群安装结果验证
通过consul members命令查看该集群agent的组成(分不出谁是leader)。使用consul info命令可以查看更详细的集群信息,可以分出leader。
consul members
Node Address Status Type Build Protocol DC Segment
s1 192.168.161.3:8301 alive server 1.7.2 2 dc1 <all>
s2 192.168.161.4:8301 alive server 1.7.2 2 dc1 <all>
s3 192.168.161.5:8301 alive server 1.7.2 2 dc1 <all>
c1 192.168.161.6:8301 alive client 1.7.2 2 dc1 <default>
web UI
访问192.168.161.6:8500/ui可以查看集群的agent节点组成,也可以表示我们安装成功。
consul命令详解(用到查一下、不用记)
- agent指令是consul的核心,它运行agent来维护成员的重要信息、运行检查、服务宣布、查询处理等等。
- join指令告诉consul agent加入一个已经存在的集群中,一个新的consul agent必须加入一个已经有至少一个成员的集群中,这样它才能加入已经存在的集群中,如果你不加入一个已经存在的集群,则agent是它自身集群的一部分,其他agent则可以加入进来。agent可以加入其他agent多次。如果你想加入多个集群,则可以写多个地址,consul会加入所有的地址。
- leave指令触发一个优雅的离开动作并关闭agent,节点离开后不会尝试重新加入集群中。运行在server状态的节点,节点会被优雅的删除,这是很严重的,在某些情况下一个不优雅的离开会影响到集群的可用性。
- members指令输出consul agent目前所知道的所有的成员以及它们的状态,节点的状态只有alive、left、failed三种状态。
- info指令提供了各种操作时可以用到的debug信息,对于client和server,info有返回不同的子系统信息,目前有以下几个KV信息:agent(提供agent信息),consul(提供consul库的信息),raft(提供raft库的信息),serf_lan(提供LAN gossip pool),serf_wan(提供WAN gossip pool)。
- event命令提供了一种机制,用来fire自定义的用户事件,这些事件对consul来说是不透明的,但它们可以用来构建自动部署、重启服务或者其他行动的脚本。
- exec指令提供了一种远程执行机制,比如你要在所有的机器上执行uptime命令,远程执行的工作通过job来指定,存储在KV中。agent使用event系统可以快速的知道有新的job产生,消息是通过gossip协议来传递的,因此消息传递是最佳的,但是并不保证命令的执行。事件通过gossip来驱动,远程执行依赖KV存储系统(就像消息代理一样)。
- force-leave可以强制consul集群中的成员进入left状态(空闲状态),记住,即使一个成员处于活跃状态,它仍旧可以再次加入集群中,这个方法的真实目的是强制移除failed的节点。如果failed的节点还是网络的一部分,则consul会周期性的重新链接failed的节点,如果经过一段时间后(默认是72小时),consul则会宣布停止尝试链接failed的节点。force-leave指令可以快速的把failed节点转换到left状态。
- keygen指令生成加密的密钥,可以用在consul agent通讯加密。
- monitor指令用来链接运行的agent,并显示日志。monitor会显示最近的日志,并持续的显示日志流,不会自动退出,除非你手动或者远程agent自己退出。
- reload指令可以重新加载agent的配置文件。SIGHUP指令在重新加载配置文件时使用,任何重新加载的错误都会写在agent的log文件中,并不会打印到屏幕。
- version指令用作打印consul的版本
- watch指令提供了一个机制,用来监视实际数据视图的改变(节点列表、成员服务、KV),如果没有指定进程,当前值会被dump出来。
consul服务注册与发现
微服务向consul注册
引入依赖包
在之前的章节已经为大家介绍过微服务向eureka、zookeeper注册的实现过程,大同小异。首先我们需要通过maven坐标引入consul的包。
如果项目pom之前存在spring-cloud-starter-netflix-eureka-client,与eureka相关的要删掉,spring-cloud-starter-zookeeper-discovery要删掉。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
因为consul的健康检查端点为’/actuator/health’,依赖于spring-boot-starter-actuator,所以对应的maven坐标要一并引入。
配置consul
将host指向consul集群内的任意一个server即可。
spring:
cloud:
consul: #Consul服务注册发现配置
host: 192.168.161.3
port: 8500
discovery:
prefer-ip-address: true
service-name: ${spring.application.name}
打开discovery开关
启动日志
出现如下的一些日志,表示服务注册成功
验证服务注册结果
可以通过consul的WEB UI界面查看。也可以通过命令行,获取aservice-sms的注册信息,可以使用如下命令。
curl -s 127.0.0.1:8500/v1/catalog/service/aservice-sms
FeignClient远程服务名称调整
需要注意的是,和zookeeper一样,consul注册的服务名称’spring.application.name’,但是并不会将服务名称转成大写,这点与eureka有所区别。所以在使用FeignClient的时候,不能写“ASERVICE-SMS”,而是写“aservice-sms”。
接口访问测试
访问aservice-rbac的“/pwd/reset”接口(该接口中远程调用了aservice-sms的“/sms/send”短信发送方法),得到正确结果。说明我们的集群安装及服务注册发现全部可以正确的使用。
如果出现401的错误,请看下一节内容。
通用-auatator导致401问题
问题描述
有的同学可能会遇到:当项目引入如下的spring-boot-starter-actuator坐标之后,项目的服务如“/user/”和“/role/”无法被访问到,返回401无权访问的问题。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
原因
这是因为你的项目同时引入了spring-boot-starter-security,二者同时引入,就会默认开启basicAuth的权限验证,导致本来公开访问的接口需要携带用户名密码才能访问。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
笔者根据经验,为您准备了如下的一系列方案,说实话基于不同的场景,需要使用不同的方法。根据版本差异也有可能部分解决方案失效,可以根据自己项目情况选择适用。
解决方案一
spring:
security:
basic:
enabled: false #关闭Basic认证
解决方案二
- 如果你的项目没有用到Spring Security,就将spring-boot-starter-security删除
- 或者我的项目暂时没有用到Spring Security管理接口权限(当前章节没用到,后面会结合网关使用Spring Security)。只是用到了Spring Security进行密码加密。我们就引入对应的子包即可,不要引入spring-boot-starter-security。因为spring-boot-starter-security有自动装配机制,自动就把默认的安全权限给你配置上了。
比如引入子包spring-security-core
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
解决方案三
- 如果你的项目确实使用到了Spring Security,请通过Spring Security的配置,正确的将项目接口访问权限开放。
- 需要你掌握Spring Security的配置方法,下面只是一个例子。自己还需要根据自己项目情况配置权限。去学Spring Security。既然用到,就要学到
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/actuator/**").permitAll()
.anyRequest().authenticated();
}
}
解决方案四
这种方法很简单,但经过笔者测试在当前版本下不生效。网上有人使用是可以生效的,应该是版本差异,所以记在这里备选。
management:
security:
enabled: false
解决方案五
这是一种比较通用的,防止Spring Security自动装配的方法。但是笔者测试,加入spring-boot-starter-actuator后,该配置失效。可能也是版本差异的问题,作为一种解决方案提供大家实验。