阅读前的思考
使用netflix eureka做服务管理时,若你只停留在对eureka的概念理解和使用层面,那么你面试时会得到面试官的灵魂拷问,例如:
1)eureka将服务注册信息存放在哪里?服务注册信息都有哪些内容?
2)eureka如何做到高可用?底层的通信机制是什么?
3)心跳机制到底发送些什么内容,有了解吗?
4)服务注册列表是存在客户端还是服务端?如果多复本数据不一致怎么处理?
5)若网络故障服务注册失败了,eureka是如何保证注册成功的?
6)注册,同步,下线,剔除分别是怎么实现的?
7)为什么刚启动的服务没有即时被eureka发现?对此你还遇到过哪些坑?
带着这些问题或疑惑,作者决定推出eureka源码解读系列,从众所周知的Eureka功能着手,对register,renew,heartbeat,fetch,剔除/关闭,数据复制等进行源码解读,意在深入理解eureka功能。
Register Client 端实现原理
服务注册先由eureka client端发起请求,具体代码定位于eureka-client-1.9.25.jar com.netflix.discovery包下。
DiscoveryCilent类的register():boolean方法是服务注册的实现。代码如下:
/**
* Register with the eureka service by making the appropriate REST call.
*/
boolean register() throws Throwable {
logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
EurekaHttpResponse httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}
看到httpResponse了说明eureka采用http方式进行服务通信,将服务注册的信息封装到InstanceInfo类中。首先来看一下InstanceInfo类包含哪些内容?
在设计上com.netfilx.appinfo.InstanceInfo定义了很多属性,服务实例相关的有instanceId,appGroupName,ipAddr,port,securePort,homePageUrl,statusPageUrl等。封装后的实例通过eurekaTransport.registrationClient具体实现。
EurekaTransport为DiscoveryClient的静态内部类,源码中集成了EurekaHttpClient,EurekaHttpClientFactory,TransportClientFactory,从设计上可以看出该类只是个工具类,具体实现由EurekaHttpClient接口来实现。
展开EurekaHttpClient接口的register(),实现类分别为EurekaHttpClientDecorator,AbstractJerseyEurekaHttpClient.其中EurekaHttpClientDecorator只是进行了封装和定义,具体实现在AbstractJerseyEurekaHttpClient中
public EurekaHttpResponse register(InstanceInfo info) {
String urlPath = "apps/" + info.getAppName();
ClientResponse response = null;
try {
Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
addExtraHeaders(resourceBuilder);
response = resourceBuilder
.header("Accept-Encoding", "gzip")
.type(MediaType.APPLICATION_JSON_TYPE)
.accept(MediaType.APPLICATION_JSON)
.post(ClientResponse.class, info);
return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
response == null ? "N/A" : response.getStatus());
}
if (response != null) {
response.close();
}
}
}
Http请求由Jersey(RESTFUL请求服务JAVA框架)来完成。如果是自己实现http通信,完全可以选择apache httpClient,OKHttp或者自定义封装http服务,如何使用http通信不是本文讨论的重点。需要关注的是http请求发送方式是post,采用json的格式进行发送和接收,使用常用的gzip进行编码压缩传输。
client端是如何创建实例并向服务端发起请求的?
理解了eureka client的register实现后,接下来的问题是如何调用DiscoveryClient的register方法。怎么用?何时发送给服务器端?
- DiscoveryManager进行客户端初始化
eureka-client-1.9.25.jar com.netflix.discovery下有一个DiscoveryManager类,该类被定义为@Deprecated说明过时官方不推荐使用。但仍然可以看到它聚合了DiscoveryClinet,EurekaInstanceConfig,EurekaClientConfig配置项。在initComponent方法内进行了初始化。
- EurekaBootStrap进行客户端初始化
EurekaBootStrap位于eureka-core-1.9.25.jar com.netflix.eureka包下,它是server和client端的启动项。实现了ServletContextListener接口,说明在web服务启动时会去做初始化。
contextInitialized(ServletContextEventevent)方法中调用了initEurekaServerContext(),里面有new DiscoveryClient(applicationInfoManager,eurekaClientConfig)
查看DiscoveryClient的构造函数,scheduler 构建为定时任务执行者,heartbeatExecutor 实例为心跳检测的Executor,cacheRefreshExecutor实例为刷新服务注册表的Executor 这三个线程池都是守护式线程。
initScheduledTasks()会对如上Executor进行任务设置,方法的最后调用了
instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
public void start(int initialDelayMs) {
if (started.compareAndSet(false, true)) {
instanceInfo.setIsDirty(); // for initial register
//这里注册调用有40秒的延时
Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
注:默认新服务注册到eureka服务器要40秒的延时.
InstanceInfoReplicator实现了runnable接口,查看run方法代码如下:
public void run() {
try {
discoveryClient.refreshInstanceInfo();
Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
if (dirtyTimestamp != null) {
discoveryClient.register();
instanceInfo.unsetIsDirty(dirtyTimestamp);
}
} catch (Throwable t) {
logger.warn("There was a problem with the instance info replicator", t);
} finally {
Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
总结一下调用流程:
EurekaBootStrap.initEurekaServerContext方法实例化DiscoveryClient -> DiscoveryClient构造方法 -> initScheduledTasks()-> 创建InstanceInfoReplicator实例(Runnable) -> 启动run方法 -> DiscoveryClient.register()
Server 端实现原理
Eureka服务端需先从EurekaBootStrap类切入。代码定位eureka-core-1.9.25.jar com.netflix.eureka.EurekaBootStrap。
通常以BootStrap命名的类一般为服务启动类,Eureka也遵循这个设计原则。它实现了ServletContextListener接口,用于监听ServletContext对象的生命周期即监听整个web应用的生命周期。contextInitialized(ServletContextEvent event)具体实现如下:
/**
* Initializes Eureka, including syncing up with other Eureka peers and publishing the registry.
*
* @see
* javax.servlet.ServletContextListener#contextInitialized(javax.servlet.ServletContextEvent)
*/
@Override
public void contextInitialized(ServletContextEvent event) {
try {
initEurekaEnvironment();
initEurekaServerContext();
ServletContext sc = event.getServletContext();
sc.setAttribute(EurekaServerContext.class.getName(), serverContext);
} catch (Throwable e) {
logger.error("Cannot bootstrap eureka server :", e);
throw new RuntimeException("Cannot bootstrap eureka server :", e);
}
}
有两个主要init方法,跟踪代码发现处理内容繁多,
initEurekaServerContext()方法里有一段代码:
PeerAwareInstanceRegistry registry;
if (isAws(applicationInfoManager.getInfo())) {
……
} else {
registry = new PeerAwareInstanceRegistryImpl(
eurekaServerConfig,
eurekaClient.getEurekaClientConfig(),
serverCodecs,
eurekaClient
);
}
PeerAwareInstanceRegistryImpl中有一个register方法,实现自AbstractInstanceRegistry类。其中InstanceRegistry为它的接口,InstanceRegistry接口本身实现LeaseManager
AbstractInstanceRegistry.register内容如下:
/**
*Registers a new instance with a given duration.
*
*@see com.netflix.eureka.lease.LeaseManager#register(java.lang.Object,int,boolean)
*/
public void register(InstanceInforegistrant,int leaseDuration,boolean isReplication){
try{
//获取读锁,即读取操作不受阻塞,写操作会阻塞。
read.lock();
//gMap是一个CurrentHashMap
Map>gMap=registry.get(registrant.getAppName());
//EurekaMontior计数器
REGISTER.increment(isReplication);
//InstanceInfo封装成一个Lease对象,存储到registry中。registry结构为ConcurrentHashMap>> registry
//注意此处gNewMap并没有添加元素
if(gMap==null){
final ConcurrentHashMap>gNewMap=new ConcurrentHashMap>();
gMap=registry.putIfAbsent(registrant.getAppName(),gNewMap);
if(gMap==null){
gMap=gNewMap;
}
}
//判断gMap中是否存在instanceId,如果不存在就设置
LeaseexistingLease=gMap.get(registrant.getId());
if(existingLease!=null&&(existingLease.getHolder()!=null)){
LongexistingLastDirtyTimestamp=existingLease.getHolder().getLastDirtyTimestamp();
LongregistrationLastDirtyTimestamp=registrant.getLastDirtyTimestamp();
logger.debug("Existingleasefound(existing={},provided={}",existingLastDirtyTimestamp,registrationLastDirtyTimestamp);
if(existingLastDirtyTimestamp>registrationLastDirtyTimestamp){
logger.warn("Thereisanexistingleaseandtheexistinglease'sdirtytimestamp{}isgreater"+
"thantheonethatisbeingregistered{}",existingLastDirtyTimestamp,registrationLastDirtyTimestamp);
logger.warn("UsingtheexistinginstanceInfoinsteadofthenewinstanceInfoastheregistrant");
registrant=existingLease.getHolder();
}
}else{
//The lease does not exist and hence it is a new registration
synchronized(lock){
if(this.expectedNumberOfClientsSendingRenews>0){
this.expectedNumberOfClientsSendingRenews=this.expectedNumberOfClientsSendingRenews+1;
updateRenewsPerMinThreshold();
}
}
logger.debug("Nopreviousleaseinformationfound;itisnewregistration");
}
Lease lease = new Lease(registrant,leaseDuration);
if(existingLease != null){
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
//此处进行了添加 instanceId作为key,lease对象作为value写入gMap。
gMap.put(registrant.getId(),lease);
………………
}finally{
read.unlock();
}
}
总结:Eureka服务端将InstanceInfo封装到一个CurrentHashMap
>>中存储。
接下来的问题:Eureka server是如何接收并处理client端发送过来的请求?
Eureka server是一个web服务,只要是web服务都需要web容器如tomcat,jetty,jboss等。在Eureka源生项目中如何使用web容器呢?
查阅Netfilx的Eureka项目源码发现一个web.xml配置文件 https://github.com/Netflix/eureka/blob/master/eureka-server/src/main/webapp/WEB-INF/web.xml
在没有使用springboot自动装配的情况下,我们看到了最原始的web开发配置。其中有一个ServletContainer的Filter,引入com.sun.jersey依赖包查看
com.sun.jersey
jersey-servlet
1.19
public class ServletContainer extends HttpServlet implements Filter{
……
我们发现ServletContainer既是一个Servlet也是一个Filter。所以容易理解原生Eureka Web服务依然是一个熟悉的传统web开发项目。使用Servlet进行服务对外交互。
ServletContainer作为Servlet容器提供服务交互,但具体处理逻辑肯定不在此类中。我们看到web配置中init-param
com.sun.jersey.config.property.packages
com.sun.jersey;com.netflix
Servlet容器初始化时会扫描com.sum.jersey和com.netflix两个包,主要是为解析对应包中的注解。我们回到eureka项目源码com.netfilx.eureka.resources.ApplicationsResource下发现了
@Path("/{version}/apps")
@Produces({"application/xml","application/json"})
public class ApplicationsResource{
……
进一步跟踪方法getApplicationResource在ApplicationResource.class中有一个addInstance(InstanceInfo info,String isReplication)方法,代码registry.register(info,"true".equals(isReplication));调用自PeerAwareInstanceRegistryImpl.register方法。
至此eureka服务注册端流程梳理完毕。
总结一下:
Eureka server端接收clinet端请求处理逻辑:
web服务启动 -> 扫描resources/ApplicationsResource -> 加载ApplicationResource对象 -> 调用addInstance -> 调用PeerAwareInstanceRegistry.register方法