• eureka 源码


      本文为eureka学习笔记,错误之处请指正。

    -----------------------------------------------------------

    1、服务生产者是怎么注册到配置中心的
      a、第一步,构造实例信息,用于接下来的注册;
      这个过程是通过EurekaClientConfiguration类中的方法eurekaApplicationInfoManager来实现的,其中的InstanceInfoFactory.create(config)返回了一个实例,这个实例就是创建后的实例信息对象。通过查看create方法的源码可以看到该方法就是将读取的eureka的配置信息放入一个EurekaInstanceConfig,然后用这些配置创建了一个InstanceInfo,注意,这时候是没有注册到服务端的。
      EurekaClientConfiguration的部分代码:

    @Configuration
    @EurekaClientAutoConfiguration.ConditionalOnMissingRefreshScope
    protected static class EurekaClientConfiguration {
    //省略部分代码
    @Bean(destroyMethod = "shutdown")
    @ConditionalOnMissingBean(value = {EurekaClient.class},search = SearchStrategy.CURRENT)
    public EurekaClient eurekaClient(ApplicationInfoManager manager, EurekaClientConfig config) {//用于跟服务器交互的客户端实例,实际是DiscoveryClient的一个子类(spring cloud对eureka自己进行了一些封装)
        return new CloudEurekaClient(manager, config, this.optionalArgs, this.context);
    }
    @Bean
    @ConditionalOnMissingBean( value = {ApplicationInfoManager.class}, search = SearchStrategy.CURRENT ) //在没有ApplicationInfoManager的实例的时候创建
    public ApplicationInfoManager eurekaApplicationInfoManager(EurekaInstanceConfig config) {
        InstanceInfo instanceInfo = (new InstanceInfoFactory()).create(config); //在这里创建了instanceInfo
        return new ApplicationInfoManager(config, instanceInfo);
     }
    }
    
      创建过程:
    public InstanceInfo create(EurekaInstanceConfig config) {
        Builder leaseInfoBuilder = Builder.newBuilder().setRenewalIntervalInSecs(config.getLeaseRenewalIntervalInSeconds()).setDurationInSecs(config.getLeaseExpirationDurationInSeconds());
        com.netflix.appinfo.InstanceInfo.Builder builder = com.netflix.appinfo.InstanceInfo.Builder.newBuilder();
    builder.setNamespace(namespace).setAppName(config.getAppname()) // 这里用config的信息进行了大量的设置
          .setInstanceId(config.getInstanceId())
          .setAppGroupName(config.getAppGroupName())
          .setDataCenterInfo(config.getDataCenterInfo())
          .setIPAddr(config.getIpAddress()).setHostName(config.getHostName(false))
          .setPort(config.getNonSecurePort())
          .enablePort(InstanceInfo.PortType.UNSECURE,
                config.isNonSecurePortEnabled())
          .setSecurePort(config.getSecurePort())
          .enablePort(InstanceInfo.PortType.SECURE, config.getSecurePortEnabled())
          .setVIPAddress(config.getVirtualHostName())
          .setSecureVIPAddress(config.getSecureVirtualHostName())
          .setHomePageUrl(config.getHomePageUrlPath(), config.getHomePageUrl())
          .setStatusPageUrl(config.getStatusPageUrlPath(),
                config.getStatusPageUrl())
          .setHealthCheckUrls(config.getHealthCheckUrlPath(),
                config.getHealthCheckUrl(), config.getSecureHealthCheckUrl())
          .setASGName(config.getASGName());
    // 省略代码若干...
        return instanceInfo;
    }
    

      其中,方法参数EurekaInstanceConfig的实际实现是EurekaInstanceConfigBean,类注解如下:

    @ConfigurationProperties("eureka.instance")//读取配置文件中前缀为eureka.instance的内容
    public class EurekaInstanceConfigBean implements CloudEurekaInstanceConfig, EnvironmentAware {
         private static final String UNKNOWN = "unknown";
         private HostInfo hostInfo;
         private InetUtils inetUtils;
    ......
    }
    
      类似的配置信息类还有:EurekaClientConfigBean(客户端配置信息),EurekaServerConfigBean(服务端配置信息)等;
      b、利用初始化的discoveryClient,将实例信息注册到server端(如果配置文件中声明要注册的话)
      DiscoveryClient类在创建的时候会执行initScheduledTasks()方法进行初始化,显然这个方法是构建了一系列定时任务,其中一段如下:
    if (clientConfig.shouldRegisterWithEureka()) {
        int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
        int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
        logger.info("Starting heartbeat executor: " + "renew interval is: " + renewalIntervalInSecs);
        // Heartbeat timer
        scheduler.schedule(  //心跳检测的代码
                new TimedSupervisorTask(
                        "heartbeat",
                        scheduler,
                        heartbeatExecutor,
                        renewalIntervalInSecs,
                        TimeUnit.SECONDS,
                        expBackOffBound,
                        new HeartbeatThread()
                ),
                renewalIntervalInSecs, TimeUnit.SECONDS);
       //省略代码若干......
        if (clientConfig.shouldOnDemandUpdateStatusChange()) {
            applicationInfoManager.registerStatusChangeListener(statusChangeListener);
        }
        instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
    }
    

      我们看到该方法启动了一个线程,new HeartbeatThread的run方法代码如下:

    private class HeartbeatThread implements Runnable {
        public void run() {
            if (renew()) { //这个renew实际就是心跳检测的过程
                lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
            }
        }
    }
    

      renew方法代码:

    boolean renew() {
        EurekaHttpResponse<InstanceInfo> httpResponse;
        try {
            httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);//发送心跳请求
            logger.debug("{} - Heartbeat status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
            if (httpResponse.getStatusCode() == 404) {//返回状态码400,不存在,说明服务端没有信息,要进行注册操作
                REREGISTER_COUNTER.increment();
                logger.info("{} - Re-registering apps/{}", PREFIX + appPathIdentifier, instanceInfo.getAppName());
                return register();//进行注册
            }
            return httpResponse.getStatusCode() == 200;//服务端正常返回,心跳检测成功
        } catch (Throwable e) {
            logger.error("{} - was unable to send heartbeat!", PREFIX + appPathIdentifier, e);
            return false;
        }
    }
    
    2、服务端注册过程

      spring cloud中eureka的client跟server端都使用jersey进行交互,在server端,EurekaServerAutoConfiguration中的jerseyFilterRegistration方法动态注册了一个Jersey filter,用来拦截所有请求。具体对请求的处理是由ApplicationResource类的addInstance方法来完成的。

    @POST
    @Consumes({"application/json", "application/xml"})
    public Response addInstance(InstanceInfo info,  @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
        logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
        // validate that the instanceinfo contains all the necessary required fields
        //参数校验代码
    
        // handle cases where clients may be registering with bad DataCenterInfo with missing data
       //高可用部分代码
    
        registry.register(info, "true".equals(isReplication));
        return Response.status(204).build();  // 204 to be backwards compatible
    }
    

      父类InstanceRegistry的register是具体的执行代码,这个register方法干了两件事儿:1、发布了一个事件,2、进行了一个注册操作。再父类PeerAwareInstanceRegistryImpl的主要功能是将注册操作分发到eureka server集群的其它节点,以保持数据的一致性。继续一步步点击进入,最终的注册由AbstractInstanceRegistry类的register完成,部分代码如下:

    private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry  = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>(); //这个是map里边套了个map,键为服务名称
    public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
        try {
            read.lock();
            Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName()); 
            REGISTER.increment(isReplication);
            if (gMap == null) { 
                final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();//这个是内部的map,键为示例名称,值为实例的具体信息
                gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
                if (gMap == null) {
                    gMap = gNewMap;
                }
            }
           //其它代码
    }
    

      可以看到,最终服务端维护了一个ConcurrentHashMap,key为服务名值为该服务的实例组成的一个map,子map中key为实例名。

    3、集群节点间信息同步

      eureka中没有主从的概念,所有节点都是平等的。其数据传播依赖于“对等机制”(peer2peer)。
      EurekaServerAutoConfiguration类中有一个上下文初始化的bean--eurekaServerContext,该bean的初始化方法如下:
    @PostConstruct
    @Override
    public void initialize() throws Exception {
        logger.info("Initializing ...");
        peerEurekaNodes.start();  //服务启动,刷新peer集群列表并启动更新任务
        registry.init(peerEurekaNodes);  
        logger.info("Initialized");
    }
    

      其中start方法主要是启动了一个线程,每隔一段时间执行一次updatePeerEurekaNodes(List<String> newPeerUrls)方法,该方法主要作用是更新集群节点信息,添加新节点,删除关闭的节点。

      在服务端完成注册之后,PeerAwareInstanceRegistryImpl的register方法中:
    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);
        replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication); //复制注册信息到其它节点,isReplication用于防止复制自身,造成集群间节点相互复制
    }
    

      当然,信息的同步过程,不止在注册发生,在服务的续约(renew),下架(cancle)均会进行同步。  

      续约:从服务提供者发起的定时任务,类似于心跳机制,隔一段时间调用一次服务端接口,告诉服务端它还活着,不要把它T了。
      下架:服务提供者自己shut down的时候,通知服务端把自己剔除,避免客户端拿到不可用的服务。
      剔除:服务端判断服务不可用,将服务从服务端删除。
    4、服务的剔除及下架
      服务剔除是指eureka server认为服务不可用,进行剔除;下架指客户端关闭服务时,向服务端发送关闭请求。下架主要是client端调用DiscoveryClient的shutdown方法以及unregister方法。以下是服务剔除内容:
      EurekaServerInitializerConfiguration的start方法中有一行eurekaServerBootstrap.contextInitialized,其中方法内的initEurekaServerContext有registry.openForTraffic,具体代码实现者peerAwareInstanceRegistryImpl的openForTraffic最后一行super.postInit(),实现中new EvictionTask中的run中的evict(),,,,,,,终于到了剔除服务的代码了!
    public void evict(long additionalLeaseMs) {
    //其它代码
        // We collect first all expired items, to evict them in random order. For large eviction sets,
        // if we do not that, we might wipe out whole apps before self preservation kicks in. By randomizing it,
        // the impact should be evenly distributed across all applications.
        List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
        for (Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry : registry.entrySet()) {
            Map<String, Lease<InstanceInfo>> leaseMap = groupEntry.getValue();
            if (leaseMap != null) {
                for (Entry<String, Lease<InstanceInfo>> leaseEntry : leaseMap.entrySet()) {
                    Lease<InstanceInfo> lease = leaseEntry.getValue();
                    if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) { //判断服务失效
                        expiredLeases.add(lease);
                    }
                }
            }
        }
    //其它代码
    }
    
      isExpired代码:
    public boolean isExpired(long additionalLeaseMs) {
        return (evictionTimestamp > 0 || System.currentTimeMillis() > (lastUpdateTimestamp + duration + additionalLeaseMs)); //最后更新时间+lease的存活时间+补偿时间< 当前时间
    }
    
    5、自我保护机制
      自我保护机制不会剔除服务。
      三个数值:
      期望值:正常每分钟心跳数量,所有服务的实例数*2(因为默认30秒心跳一次)
      保护值:进入自我保护机制的数值,心跳数少于该数值则进入保护机制。期望值*阈值百分比
      阈值:进入保护机制的一个设置值
      源码在PeerAwareInstanceRegistryImpl#isLeaseExpirationEnabled
    6、消费者获取信息
      实际就是客户端有个定时任务去服务端获取信息并进行缓存的过程;
      DiscoveryClient类的initScheduledTasks的new CacheRefreshThread---->refreshRegistry---->EurekaHttpClient类的getApplications:
      其中,refreshRegistry中有拉取服务信息的操作fetchRegistry,该操作会根据客户端的配置决定是拉取全量信息还是增量信息
    private boolean fetchRegistry(boolean forceFullRegistryFetch) {
        Stopwatch tracer = FETCH_REGISTRY_TIMER.start();
        try {
            // If the delta is disabled or if it is the first time, get all  applications
            Applications applications = getApplications();
            if (clientConfig.shouldDisableDelta()  || (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress())) || forceFullRegistryFetch || (applications == null) || (applications.getRegisteredApplications().size() == 0)   || (applications.getVersion() == -1))  {//Client application does not have latest library supporting delta
                logger.info("Disable delta property : {}", clientConfig.shouldDisableDelta());
                logger.info("Single vip registry refresh property : {}", clientConfig.getRegistryRefreshSingleVipAddress());
                logger.info("Force full registry fetch : {}", forceFullRegistryFetch);
                logger.info("Application is null : {}", (applications == null));
                logger.info("Registered Applications size is zero : {}", (applications.getRegisteredApplications().size() == 0));
                logger.info("Application version is -1: {}", (applications.getVersion() == -1));
                getAndStoreFullRegistry();//全量数据
            } else {
                getAndUpdateDelta(applications);//增量数据
            }
            applications.setAppsHashCode(applications.getReconcileHashCode());
            logTotalInstances();
        } catch (Throwable e) {
            logger.error(PREFIX + appPathIdentifier + " - was unable to refresh its cache! status = " + e.getMessage(), e);
            return false;
        } finally {
            if (tracer != null) {
                tracer.stop();
            }
        }
        // Notify about cache refresh before updating the instance remote status
        onCacheRefreshed();
        // Update remote status based on refreshed data held in the cache
        updateInstanceRemoteStatus();
        // registry was fetched successfully, so return true
        return true;
    }
    
    代码看出,根据disable-delta的值以及是否是初次获取数据来判断是否全量获取数据,如果disable-delta为true(禁用delta增量更新),则以后每次获取均为全量获取,默认为false,也就是除了第一次,以后都是增量获取数据。进入getAndStoreFullRegistry,再进入获取httpResponse的eurekaTransport.queryClient.getApplications(remoteRegionsRef.get(),跟踪可进入SessionedEurekaHttpClient的execute方法:
    protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
        long now = System.currentTimeMillis();
        long delay = now - lastReconnectTimeStamp;
        if (delay >= currentSessionDurationMs) { //这里判断超时就创建新的session,实际是定期创建session,目的是防止一个客户端一直连接某个server,在server增加时不利于负载均衡的处理
            logger.debug("Ending a session and starting anew");
            lastReconnectTimeStamp = now;
            currentSessionDurationMs = randomizeSessionDuration(sessionDurationMs);
            TransportUtils.shutdown(eurekaHttpClientRef.getAndSet(null));
        }
       //省略代码
        return requestExecutor.execute(eurekaHttpClient); //还得进入看实现
    }
            继续跟踪,进入RetryableEurekaHttpClient的execute方法,在这里可以看到读取了配置文件内容,拿到了eureka-server的url,经过一系列跳转最终进入了AbstractJerseyEurekaHttpClient的getApplicationsInternal方法,在这里发送了请求并获取了返回结果。
            返回DiscoveryClient的getAndStoreFullRegistry方法,在拿到httpResponse并获取返回结果后执行了filterAndShuffle:
    private Applications filterAndShuffle(Applications apps) {
        if (apps != null) {
            if (isFetchingRemoteRegionRegistries()) {
                Map<String, Applications> remoteRegionVsApps = new ConcurrentHashMap<String, Applications>();
                apps.shuffleAndIndexInstances(remoteRegionVsApps, clientConfig, instanceRegionChecker);
                for (Applications applications : remoteRegionVsApps.values()) {
                    applications.shuffleInstances(clientConfig.shouldFilterOnlyUpInstances());
                }
                this.remoteRegionVsApps = remoteRegionVsApps;
            } else {
                apps.shuffleInstances(clientConfig.shouldFilterOnlyUpInstances());
            }
        }
        return apps;
    } 
      这里看名字就知道,过滤并洗牌操作,就是把获取的实例顺序打乱,目的是防止同一个实例总是出现在某个位置造成负载的时候偏重不均匀;过滤这个操作在嵌套的方法中,最终代码在Applications的shuffleAndFilterInstances方法,可以看到只保留了状态为UP的实例。
      洗牌之后,通过addInstancesToVIPMaps调用addInstanceToMap将实例添加到本地缓存map: appNameApplicationMap中。
    7、心跳机制
       DiscoveryClient 内部类HeartBeatThread:
    private class HeartbeatThread implements Runnable {
        public void run() {
            if (renew()) {//可以看到,就是调用了renew方法
                lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
            }
        }
    }
    

    ----------------------------------------------

     
     

  • 相关阅读:
    ATI Radeon HD 5450 with full QE/CI Support ( 转载 )
    从零开始学iPhone开发(5)——使用MapKit
    从零开始学iPhone开发(4)——使用WebView
    从零开始学iPhone开发(3)——视图及控制器的使用
    从零开始学iPhone开发(2)——控件的使用
    从零开始学iPhone开发(1)——工具的使用
    实战做项目如何选择开源许可协议(一)-了解协议
    git使用3
    git团队开发操作
    git分支管理
  • 原文地址:https://www.cnblogs.com/nevermorewang/p/9219851.html
Copyright © 2020-2023  润新知