笔者不想直接用专业的术语来说明“微服务注册与发现”,所以我们来看生活中的一个案例:“公益图书馆”。
随着人们生活水平的不断提高,追求精神食粮的朋友也越来越多。笔者曾经在一些城市看见过公益图书馆,其运行逻辑是:一些公益组织和个人提供一块场所,然后由组织内的人向图书馆内捐书。捐出的书越多,一段时间内能够借阅的书也就越多。这种做法有助于大家分享图书、节约资金、交流读书心得。那我们来看一下几个关键环节:
其实上面的这个“公益图书馆的例子”就是典型的服务注册与发现:
与客户端负载均衡相对的方法就是服务端负载均衡,如果上面的例子中借书过程一本书有多个副本,由图书管理员或系统决定借书者借其中的哪一个副本,这个就是服务端负载均衡。如:nginx、haproxy等就是服务端负载均衡。
当一个微服务启动的时候,必须主动向服务注册中心注册其服务地址,以供其他微服务查询调用。图中橘黄色为服务注册中心,绿色为微服务节点。
如果你的应用已经使用到了hadoop、kubernetes、docker,在Spring Cloud实施过程中可以考虑使用其关系户组件,避免搭建两套注册中心,节省资源。但是二者兼容使用说说容易,真正用起来还需要功夫。目前看,笔者觉得最佳选择应该是Nacos。
这里可以先简单的了解一下常见的这些服务注册中心,后面的章节我们会逐步的详细介绍。
在前面的章节,我们已经为大家介绍了
服务消费者调用微服务之前,需要向服务注册中心,获取注册服务列表及服务信息。这个过程就是“服务发现”,那么服务发现是通过什么类实现的?服务列表及服务信息又包含哪些内容?本节就带着大家来解开这样的疑惑!
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;
}
}
它有两个核心方法:
下面是一个基于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
,两者的用法基本上是一样的。但存在区别,简单地说:
@EnableEurekaClient
注解,实现服务发现。@EnableDiscoveryClient
,该注解更加的通用。在Hello-microservice章节,实现微服务向服务注册中心注册的时候,我们使用了@EnableEurekaClient
,是因为我们当时搭建的服务注册中心是基于eureka搭建的。Spring Cloud中还有很多的其他服务注册中心的选项,比如:consul、zookeeper、nacos,这时就不能使用@EnableEurekaClient
注解了,需要使用@EnableDiscoveryClient
注解。
服务注册中心在整个微服务体系中,至关重要!如果服务注册中心挂了,整个系统都将崩溃。所以服务注册中心通常不会被部署为单点应用,而是采用集群的部署方式,其中个别节点挂掉不影响整个系统的运行。
下面,我们就来为大家介绍,如何基于CentOS7服务器构建eureka服务注册中心。
主机名称 | 主机ip |
---|---|
peer1 | 192.168.161.3 |
peer2 | 192.168.161.4 |
peer3 | 192.168.161.5 |
并且在安装eureka服务注册中心之间,需要将服务器时间同步,不能相差太多,否则eureka服务有可能启动失败。可以使用ntp
进行时间同步。如:
ntpdate ntp.api.bz
application-peer1.yml
这两个值之所以设置为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/
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
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/
spring-boot-starter-actuator是为Spring Boot服务提供相关监控信息的包。因为我们的eureka server要互相注册,并检查彼此的健康状态,所以这个包必须带上。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
如果你的网络环境内没有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
将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。
同理:
出现上面的这种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
会导致健康检查失败。
在上一小节,我们为大家讲解了如何在linux环境下搭建集群式Eureka服务注册中心。有的朋友可能会遇到下面的问题(导致服务注册失败、健康检查失败):
上图中蓝色部分:大家可以明确的看到eureka server的服务绑定的ip是10.0.2.15?这是为什么?我们上一节中,也没有使用过这个ip啊,我们使用的是192.168.161.3。这是因为我的CentOS服务器上有多个网卡,还有一些docker相关的虚拟网卡。“多网卡”在生产环境上是非常常见的情况。怎么让eureka server服务绑定实例我们期望它绑定的网卡
?
首先来看一下,我的服务器(虚拟机)上面的网卡设备,一共五个(虚拟的)。
那我们现在要做的就是通知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
选择的方式,可以根据自己的网络环境情况选择使用。归纳如下:
eureka.instance.ip-address=192.168.1.7
直接配置一个完整的ip,一般适用于环境单一场景,对于复杂场景缺少有利支持。比如:你的eureka环境是结合docker容器部署的,就会有问题。因为docker容器的ip是动态的不固定的,所以你很难为docker容器中的服务指定ip。所以这种方式通常不建议使用。
配置对应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 server从单节点升级为集群,所以响应的注册配置也要修改,但总体上大同小异。主要修改的内容如下:
eureka server
配置之间使用逗号分隔。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 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 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配置都要修改。
Status
栏显示着UP
,表示该服务及其多实例处于状态正常。其它取值DOWN、OUT_OF_SERVICE、UNKNOWN
等均表示该服务处于不可被请求的状态,只有UP
状态的微服务会被请求。#开启健康检查(需要spring-boot-starter-actuator依赖)(默认就是开启的)
eureka.client.healthcheck.enabled = true
如果需要更细粒度健康检查,可实现com.netflix.appinfo.HealthCheckHandler
接口 。 EurekaHealthCheckHandler
已实现了该接口。
正常情况下,当微服务的心跳消失,健康检查失败后,eureka server会将该服务从服务列表中剔除
。表示该服务下线了。 但是一些异常情况下,eureka不会剔除服务,比如:eureka自我保护模式被开启的情况下。
访问Eureka主页时,如果看到这样一段大红色的句子,那么表明Eureka的自我保护模式被启动了。
当个别服务健康检查失败,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.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之内没有给你发心跳,就代表我“死”了,请将我踢掉。
在开始为大家介绍主流的服务注册中心之前,先给大家介绍一些分布式系统的常用理论知识,这样我们在后文为大家介绍主流的服务注册中心的时候,才能更好的理解它们之间的差异
先为大家说明一下,应用的垂直扩展与水平扩展之间的区别。
垂直扩展
:将更多资源(CPU,内存)添加到现有的应用程序所在的服务器上。好处在于:我们不需要进行额外的开发,即可完成应用的处理能力的升级。但是,每次扩展服务器/主机时,垂直扩展的成本几乎都呈指数增长。并且垂直扩展很容易触及硬件资源的上限。
水平扩展
:是一种添加更多具有标准容量的服务器并同时运行多个应用程序副本的方法。与垂直扩展相比,此方法的最大优点是由于同一应用程序部署多个副本,提升了应用本身的容错能力(其中一个副本出现问题,其他的副本还能工作)。但是水平扩展也带来了一些挑战,比如应用程序之间的网络更多,架构设计更复杂。
CAP理论是埃里克·布鲁尔(Eric Brewer) 提出的理论。在分布式系统领域,特别是大数据应用、NOSQL数据库等分布式复杂系统的设计方面给出了很好的指导方针。CAP理论的核心思想就是:强数据一致性、高可用、分区容忍性之间,只能三选二。也就是说:当你选择其中的两项作为来满足你的系统需求,就必须舍弃第三项,三项无法同时满足。
下面为大家介绍一下强数据一致性、高可用、分区容忍性的具体含义。
国内很多文章,在提到CAP的C时都是说“一致性”,这是不对的,这个C必须是强数据一致性。如果应用仅仅满足了弱一致性或者最终一致性,它并不满足CAP的C特性。
弱一致性
: 这是一种用于分布式计算的一致性模型,其中后续访问可能并不总是返回更新后的值。可能在某一时间段会有数据不一致的响应。这是一种尽力而为的一致性模型,分布式应用之间彼此通过网络交换数据,但是因为没有数据锁定机制,可能导致不同节点同一时间对外提供的服务数据不一致。最终一致性
: 最终一致性是弱一致性中的一种特殊类型。这种模型的数据一致性,通常是通过消息队列来实现的,数据提供方将数据放入消息队列,他就不再去关注该数据是否被处理。消息队列保证数据被数据消费方成功处理一次(并且值处理一次)。至于该数据什么时候被成功处理,那就不一定了。记住:CAP中的C是High C,也就是强数据一致性
。 我们常用的Eureka是一种弱一致性分布式系统,所以它是满足了CAP中的AP特性,舍弃了强数据一致性C。
高可用通俗的说,就是任何时间都可用,任何请求都可以得到响应。怎么保障高可用?
应用水平扩展n个节点,其中1个或者几个节点挂了,其他的节点仍然可以支撑整体服务的运行。
什么样的系统是CA系统?
比如:我们的关系型数据库Oracle、MySQL,都是可以搭建高可用环境的,也就是满足A特性。
那么关系型数据库是如何保证强数据一致性的?
假如:我们现在有一个订单库DB、一个产品库DB,如何保障一个事务中操作这两个库,同时保证强数据一致性?
答案通常是:分布式事务,如:两阶段提交等。
笔者之前曾经给一些朋友讲分区容忍性,发现他们理解这个概念有点困难。那我们就先去讲什么是AP,什么是CP,再讲什么是P。
在分布式系统中某些节点网络不可达或故障的时候,仍然可以对外正常提供服务的系统
。比如:我们的Eureka服务注册中心,将部分节点的网络切断,仍然可以提供服务注册与发现的服务。需要注意的一点是:AP系统不是完全抛弃数据一致性,而是无法保证数据的强一致性。网络不可达或故障
的时候,仍然以保证系统节点之间的数据一致性优先
,可能导致整个服务短暂锁定或者宕机。我们后面章节为大家介绍的zookeeper和consul都是这种系统。以consul为例,服务配置注册到consul的A节点,A节点必然要将该注册信息同步到B节点,如果B节点因为网络问题不可达,并且该节点是leader(领导者)节点。这就导致整个consul集群要重新选举leader节点,这将导致在选举期间无法对外提供服务。那么,什么是分区容忍性?
通常是指分布式系统部分节点由于网络故障导致不可达,仍然可以对外提供满足一致性或者高可用的服务。
由上面的讲解,大家可以知道eureka是AP系统,consul和zookeeper是CP系统,我们后面章节要重点为大家介绍的nacos支持CP或AP。笔者认为,就服务注册中心而言,AP要好于CP。
核心对比 | Nacos | Eureka | Consul | Zookeeper |
---|---|---|---|---|
一致性协议 | CP或AP | AP | CP | CP |
版本迭代 | 迭代升级中 | 不再升级 | 迭代升级中 | 迭代升级中 |
SpringCloud集成 | 支持 | 支持 | 支持 | 支持 |
Dubbo集成 | 支持 | 不支持 | 不支持 | 支持 |
K8S集成 | 支持 | 不支持 | 支持 | 不支持 |
zookeeper是一个分布式应用的协调服务。用于对分布式系统进行配置管理、节点管理、leader选举、分布式锁等。ZooKeeper最初是由“ Yahoo!”开发的。后来,Apache ZooKeeper成为Hadoop,HBase和其他分布式框架常用的服务配置管理的标准。
zookeeper 将集群内的节点进行了这样三种角色划分(上图中的Client不属于zookeeper集群
):
zookeeper集群是一个强调保障数据一致性的分布式系统(CP),客户端发起的每次查询操作,集群节点都能返回同样的结果。
那么问题来了:对于客户端发起的修改、删除等能改变数据的操作呢?
集群中那么多台机器,如果是你修改你的,我修改我的,没有统一管理,最后查询返回集群中哪台机器的数据呢?
如果随意操作,就不能保证数据一致性了。于是在zookeeper集群中,leader的作用就体现出来了,
这样就保障了zookeeper内各个节点的数据一致性。所以zookeeper集群中leader是不可缺少的,但是 leader 节点是怎么产生的呢?
其实就是由所有follower 节点选举产生的,且leader节点只能有一个。当前leader如果因为某些原因挂掉了,集群内剩余的节点会重新选举leader。
我们要搭建服务注册中心zookeeper集群,必须搭建奇数个节点,这是为什么呢?
首先从容错率来说明:(需要保证集群能够有半数进行投票)
脑裂集群的脑裂通常是发生在节点之间通信不可达的情况下,集群会分裂成不同的小集群,小集群各自选出自己的leader节点,导致原有的集群出现多个leader节点的情况,这就是脑裂。
以上分析,我们从容错率以及防止脑裂两方面说明了3台服务器是搭建集群的最少数目,4台服务器发生脑裂时会造成没有leader节点的错误。
zookeeper中的数据是以树形结构组织的,类似于树形文件系统。树中的节点称为znode,znode按持久化类型分类可以分为:
即使在创建该特定znode的客户端断开连接后,持久znode数据仍然存在。默认情况下,所有znode都是持久的。
当客户端处于活跃状态时,该客户端创建的临时znode就是有效的。当客户端与ZooKeeper集合断开连接时,临时znode会自动删除。
Spring Cloud 微服务向zookeeper注册的过程就是在zookeeper增加临时znode节点,znode节点中保存了服务的注册信息。也正是利用了临时客户端断开连接后删除znode的的特性,实现了服务的自动下线。
zookeeper除了上面的这些内容,还能做很多事情,如:分布式锁。还有很多应用、运维的知识可以学习。本文只为大家介绍与“服务注册中心”相关的理论基础知识。下一节我们就手动搭建一个zookeeper集群。
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 |
在 /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,根据你自己的下载版本及安装包调整上面的命令。
将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
上面红色框住的内容即是我们修改的内容,蓝色框是我们新增的内容:
A:其中 A 是一个数字,表示这个是服务器的编号id; B:是这个服务器的 ip 地址; C: Leader选举的端口; D: Zookeeper服务器之间的通信端口。
我们需要修改的第一个是 dataDir ,在指定的位置处创建好目录。
第二个需要新增的是 server.A=B:C:D 配置,其中 A 对应下面我们即将介绍的myid 文件。B是集群的各个IP地址,C:D 是端口配置。
在 上一步 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
启动命令:
zkServer.sh start
停止命令:
zkServer.sh stop
重启命令:
zkServer.sh restart
查看集群节点状态:
zkServer.sh status
我们分别对集群三台主机执行状态查看命令,其中一台是leader,其他两台是follwer。
出现上面的状态,基本可以认定,我们的集群安装是成功的。启动日志可以通过如下的文件查看:
启动日志中会有一些异常,如果是防火墙端口已经正确打开,通常是因为启动顺序的原因导致的,无关紧要。A连接B,B还没启动,所以抛出“拒绝连接”异常。等B启动了就好了。
引入依赖包
在之前的章节已经为大家介绍过微服务向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>
有过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,即可正确实现远程服务调用)
以上的步骤,在所有需要向zookeeper注册的服务上都要进行。如:aservice-rbac、aservice-sms
zookeeper官方并不提供web管理界面(有一些第三方开发的)。所以我们通过命令行来查看服务注册的结果。
zkCli.sh -server 192.168.161.3:2181
我们之前已经给大家介绍过,zookeeper的数据存储结果是按目录的树形结构。所以,看下面的命令:
通过上面的命令进一步认证我们的zookeeper集群安装的正确性。
需要注意的是,zookeeper注册的服务名称是spring.application.name,但是并不会将服务名称转成大写,这点与eureka有所区别。所以在使用FeignClient的时候,不能写“ASERVICE-SMS”,而是写“aservice-sms”。
访问aservice-rbac的“/pwd/reset”接口(该接口中远程调用了aservice-sms的“/sms/send”短信发送方法),得到正确结果。说明我们的集群安装及服务注册发现全部可以正确的使用。
Consul 官方站点:https://www.consul.io/
Consul 是一个支持多数据中心分布式高可用的服务发现和配置共享的服务软件,由 HashiCorp 公司用 Go 语言开发, 基于 Mozilla Public License 2.0 的协议进行开源. Consul 支持健康检查,并允许 HTTP 和 DNS 协议调用 API 存储键值对. 一致性协议采用 Raft 算法,用来保证服务的高可用. 使用 GOSSIP 协议管理成员和广播消息, 并且支持 ACL 访问控制.。它具备以下特性:
consul作为服务注册中心,并没有与eureka、zookeeper在核心的流程上有区别。仍然遵循于服务注册与发现的基本实现逻辑。
很多人看到上面的这张图会晕掉,它并不像我们之前讲过的Eureka和zookeeper的集群部署拓扑那样好理解。下面就带大家一步一步剖析这张图
上面这张图没有包含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集群内的其他节点维持心跳 |
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参数说明。
端口 | 说明 |
---|---|
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
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 简介
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} #创建数据目录和配置目录
将以下的内容写入对应主机的配置文件 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"
这一步可以不做,只是用来简化每次启动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 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>
访问192.168.161.6:8500/ui可以查看集群的agent节点组成,也可以表示我们安装成功。
在之前的章节已经为大家介绍过微服务向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坐标要一并引入。
将host指向consul集群内的任意一个server即可。
spring:
cloud:
consul: #Consul服务注册发现配置
host: 192.168.161.3
port: 8500
discovery:
prefer-ip-address: true
service-name: ${spring.application.name}
出现如下的一些日志,表示服务注册成功
可以通过consul的WEB UI界面查看。也可以通过命令行,获取aservice-sms的注册信息,可以使用如下命令。
curl -s 127.0.0.1:8500/v1/catalog/service/aservice-sms
需要注意的是,和zookeeper一样,consul注册的服务名称’spring.application.name’,但是并不会将服务名称转成大写,这点与eureka有所区别。所以在使用FeignClient的时候,不能写“ASERVICE-SMS”,而是写“aservice-sms”。
访问aservice-rbac的“/pwd/reset”接口(该接口中远程调用了aservice-sms的“/sms/send”短信发送方法),得到正确结果。说明我们的集群安装及服务注册发现全部可以正确的使用。
如果出现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-core
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
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后,该配置失效。可能也是版本差异的问题,作为一种解决方案提供大家实验。