Eureka服务注册register源码分析

阅读前的思考

使用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定义了很多属性,服务实例相关的有instanceIdappGroupNameipAddrportsecurePorthomePageUrlstatusPageUrl等。封装后的实例通过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,LookupService的多继承。
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

image.png

在没有使用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方法

你可能感兴趣的:(Eureka服务注册register源码分析)