在前两章介绍了微服务简介,spring-cloud简介这一章学习spring cloud中的第一个组件,Eureka。这是一个用于服务注册和发现的组件。分为Eureka Server和Eureka Client,Eureka Server为Eureka服务注册中心,Eureka Client为Eureka客户端。
基本架构
Eureka主要包括三种角色:
Register Service:服务注册中心,他是一个Eureka Server,提供服务注册和发现的功能。
Provider Service:服务提供者,它是一个Eureka Client,提供服务。
Consumer Service:服务消费者,它是一个Eureka Client,消费服务。
服务消费的基本流程
首先需要启动一个Eureka Server,做为服务注册中心,服务提供者Eureka Client向服务注册中心Eureka Server注册,将自己的服务名和IP地址等信息通过REST API的形式提交给服务注册中心Eureka Server。同样,服务消费者Eureka Client也向服务注册中心Eureka Server注册,同时服务消费者获取一份服务注册列表的信息,该列表包含了所有向服务注册中心Eureka Server注册的服务信息。获取服务注册列表信息之后,服务消费者就知道服务提供者的IP地址,可以通过Http远程调度来消费服务提供者的服务。
编写Eureka Server
第一步:引入Eureka Server的起步依赖spring-cloud-starter-eureka-server,以及spring boot测试的起步依赖spring-boot-starter-test。最后引入spring boot的maven插件spring-boot-maven-plugin,该插件的作用是可以试用maven插件的方式来启动spring boot工程。
org.springframework.cloud
spring-cloud-starter-eureka-server
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
第二步:在application.yml中做程序的相关配置。Eureka Server会向自己注册,这时需要配置eureka.client.registerWithEureka和eureka.client.fetchRegistry为false,防止自己注册自己。配置如下:
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
最后在工程启用类加上注解@EnableEurekaServer,开启Eureka Server的功能。代码如下:
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
启动项目,访问http://localhost:8761,可以在浏览器查看Eureka Server的主界面。在界面Instance currently registered with Eureka这一项上没有任何注册的实列,接下来编写Eureka Client,并注册到Eureka Server。
编写Euraka Client
引入依赖:
org.springframework.cloud
spring-cloud-starter-eureka
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
在工程配置文件application.yml中做相关配置。其中defaultZone为默认的Zone,来源于AWS的概念。区域(Region)和可用区(Availability Zone,AZ)是AWS的另外两个概念。区域是指服务器所在的区域,比如北美洲、欧洲、亚洲等,每个区域一般由多个可用区组成。在配置中defaultZone是指Eureka Server的注册地址。
server:
port: 8763
spring:
application:
name: eureka-client
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
在项目启动类上加上注解@EnableEurekaClient开启Eureka Client功能。代码如下:
@SpringBootApplication
@EnableEurekaClient
public class EurekaClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaClientApplication.class, args);
}
}
启动Client,启动成功后,控制台会打印出如下信息:
DiscoveryClient_EUREKA-CLIENT/DESKTOP-NB1C2U4:eureka-client:8763 - registration status: 204
说明已经向Eureka Server注册了。在浏览器访问http://localhost:8761,可在主界面看到有一个实列注册,Application为EUREKA-CLIENT,Status为UP,端口为8763,说明注册成功。
构建高可用的Eureka Server集群
在实际的项目中,可能由几十个或者几百个的微服务实例,这时Eureka Server承担了非常高的负载。由于Eureka Server在微服务架构中有着举足轻重的作用,所以需要对Eureka Server进行高可用集群。
第一步更改eureka-server的配置文件application.yml,在配置文件中采用多profile的格式,具体代码如下:
spring:
profiles: peer1
server:
port: 8761
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://peer2:8762/eureka/
---
spring:
profiles: peer2
server:
port: 8762
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://peer1:8761/eureka/
由于是本地代建Eureka Server集群,需要在本机hosts文件做域名映射。
127.0.0.1 peer1
127.0.0.1 peer2
通过mvn clean package编译项目,成功后在目录target文件夹下生成jar包。通过java -jar命令并指定spring-profiles-active参数。命令如下:
java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer1
java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer2
修改eureka-client配置文件,注意其中只是向8761端口的服务器注册,配置如下:
server:
port: 8763
spring:
application:
name: eureka-client
eureka:
client:
service-url:
defaultZone: http://peer1:8761/eureka/
启动eureka-client,访问 http://localhost:8762/,在DS Replicas选项中显示了其它多实例节点。此时发现eureka-client在配置文件中只是向8761端口的server注册,但在8762端口的服务器同样可以看到eureka-client的注册信息,可见peer1的注册列表信息已经同步到了peer2节点。
Eureka的一些概念
- Register-服务注册
当Eureka Client向Eureka Server注册时,client提供自身的元数据,如IP地址、端口、运行状态指标的Url、主页地址等信息。 - Renew-服务续约
Eureka Client默认情况下每隔30秒发送一次心跳来进行服务续约。如果Eureka Server在90秒内没有收到Eureka Client的心跳,Eureka Server会将Eureka Client实例从注册列表中剔除。 - Fetch Registries-获取服务注册列表信息
Eureka Client从Eureka Server获取服务注册表信息,并缓存在本地,可根据注册表信息查找服务,从而进行远程调用。该注册表信息每30秒更新一次。如果由于某种原因导致服务注册列表信息不能及时匹配,Eureka Client会重新获取整个注册表信息。 - Cancel-服务下线
Eureka Client在程序关闭时可以向Eureka Server发送下线信息。发送请求后,该客户端的实例信息将从Eureka Server的服务注册列表中删除。该下线请求不会自动完成,需要在程序关闭时调用以下代码:
DiscoveryManager.getInstance().shutdownComponent();
- Eviction-服务剔除
默认情况下,当Eureka Client连续90秒没有向Eureka Server发送服务续约(即心跳)时,Eureka Server会将该服务实例从服务注册列表删除。 - Eureka的自我保护模式
当一个新的Eureka Server出现时,会尝试从相邻的Peer节点获取所有服务实例的注册信息。如果从相邻的Peer节点获取信息时出现了故障,Eureka Server会尝试其它的Peer节点。如果Eureka Server能够成功获取所有的服务实例信息,则根据配置信息设置服务续约的阈值。在任何时间,如果Eureka Server接收到的服务续约低于该配置的百分比(默认为15分钟内低于85%),则服务器开启自我保护模式,即不在剔除注册列表的信息。这样做的好处是如果是Eureka Server自身的网络问题而导致Eureka Client无法续约,这样在注册列表中不会剔除注册信息,这样Eureka Client还可以被其他服务消费。同时这个功能也是有坑的,如果在服务较少时,服务由于意外情况挂掉后很容易阈值就低于85%,由于自我保护导致挂掉的服务在注册列表中还是存在,但其它服务无法消费,这会迷惑开发人员排错的方向。如果需要关闭该功能,在配置文件中添加如下代码:
eureka:
server:
enable-self-preservation: false
源码解析
接下来我们通过debug的形式,学习Eureka Client是如何进行注册的。在工程的Maven的依赖包下,找到eureka-client-1.6.2.jar包,在com.netflix.discovery包下有一个DiscoverClient类,这个类是Eureka Client和Eureka Server交互的核心类。
启动项目后首先会进入DiscoverClient的initScheduledTasks方法。在该方法中初始化并封装了刷新服务注册列表信息和发送心跳的定时任务。
在初始化定时任务后进入DiscoverClient的register()方法
具体进入EurekaHttpClient的register方法,可以看出通过http请求的Eureka Server路径,以及携带的请求信息。
同时来跟踪Eureka Server端的代码,Eureka server服务端请求入口为
ApplicationResource类,如下所示,可以看出Eureka Client是通过http post的方式去服务注册。
通过一系列的参数校验。最后以以下方法进行注册。
registry.register(info, "true".equals(isReplication));
进入register方法,实际执行的是PeerAwareInstanceRegistryImpl的register方法,该方法如下:
@Override
public void register(final InstanceInfo info, final boolean isReplication) {
int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
leaseDuration = info.getLeaseInfo().getDurationInSecs();
}
//调用父类的注册方法
super.register(info, leaseDuration, isReplication);
// 注册成功后同步Eureka中的服务注册信息
replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}
进入父类的注册方法,在AbstractInstanceRegistry类中可以看到Eureka真正的服务注册实现的代码。
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
read.lock();
Map> gMap = registry.get(registrant.getAppName());
REGISTER.increment(isReplication);
if (gMap == null) {
final ConcurrentHashMap> gNewMap = new ConcurrentHashMap>();
gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
if (gMap == null) {
gMap = gNewMap;
}
}
Lease existingLease = gMap.get(registrant.getId());
// Retain the last dirty timestamp without overwriting it, if there is already a lease
if (existingLease != null && (existingLease.getHolder() != null)) {
Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater" +
" than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");
registrant = existingLease.getHolder();
}
} else {
// The lease does not exist and hence it is a new registration
synchronized (lock) {
if (this.expectedNumberOfRenewsPerMin > 0) {
// Since the client wants to cancel it, reduce the threshold
// (1
// for 30 seconds, 2 for a minute)
this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2;
this.numberOfRenewsPerMinThreshold =
(int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
}
}
logger.debug("No previous lease information found; it is new registration");
}
Lease lease = new Lease(registrant, leaseDuration);
if (existingLease != null) {
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
gMap.put(registrant.getId(), lease);
synchronized (recentRegisteredQueue) {
recentRegisteredQueue.add(new Pair(
System.currentTimeMillis(),
registrant.getAppName() + "(" + registrant.getId() + ")"));
}
// This is where the initial state transfer of overridden status happens
if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the "
+ "overrides", registrant.getOverriddenStatus(), registrant.getId());
if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
logger.info("Not found overridden id {} and hence adding it", registrant.getId());
overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
}
}
InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
if (overriddenStatusFromMap != null) {
logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
registrant.setOverriddenStatus(overriddenStatusFromMap);
}
// Set the status based on the overridden status rules
InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
registrant.setStatusWithoutDirty(overriddenInstanceStatus);
// If the lease is registered with UP status, set lease service up timestamp
if (InstanceStatus.UP.equals(registrant.getStatus())) {
lease.serviceUp();
}
registrant.setActionType(ActionType.ADDED);
recentlyChangedQueue.add(new RecentlyChangedItem(lease));
registrant.setLastUpdatedTimestamp();
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
logger.info("Registered instance {}/{} with status {} (replication={})",
registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
} finally {
read.unlock();
}
}
在该类中服务注册信息其实就是存储在一个 ConcurrentHashMap结构中。
private final ConcurrentHashMap>> registry
= new ConcurrentHashMap>>();
流程总结:
Eureka Client:在DiscoveryClient先通过initScheduledTasks()方法封装定时任务,然后调用register()方法通过http访问服务器接口进行注册。
Eureka Server:ApplicationResource类接收Http服务请求,调用PeerAwareInstanceRegistryImpl的register方法,PeerAwareInstanceRegistryImpl完成服务注册后,调用replicateToPeers向其它Eureka Server节点(Peer)做状态同步。
在以上只是对服务注册进行了源码跟踪,感兴趣的可以以同样的方式对服务续约、服务剔除等进行源码学习。
总结
在这一章节中,我们学习了Eureka的概念、架构。如何编写Eureka Server和Eureka Client,并进行了Eureka Server高可用的演示。最后以服务注册为列进行了源码学习。在下一章学习如何使用RestTemplate和Ribbon结合作为服务消费者去消费服务。
PS:项目github地址:https://github.com/dzydzydzy/spring-cloud-example.git