第三章:Spring Cloud Eureka(Spring cloud微服务实战)下

本章主要内容:

1.源码分析

2.配置详解


源码分析

    我们从Eureka的客户端看它如何完成通信行为的。

    我们将一个普通的Spring Boot应用注册到Eureka Server 或者是 从Eureka Server 中获取服务列表时,主要做了两个事情:

  •     在应用主类中配置了@EnableDiscoveryClient 注解
  •     在application.properties中用eureka.client.serviceUrl.defaultZone参数指定了服务注册中心的位置。

我们来看一下@EnableDiscoveryClient 注解的源码

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(EnableDiscoveryClientImportSelector.class)
public @interface EnableDiscoveryClient {

}

该注解主要是用来开启DiscoveryClient的实例

org.springframework.cloud.client.discovery.DiscoveryClient是Spring Cloud 的接口,定义了用来发现服务的常用抽象方法,通过该接口可以有效地屏蔽服务治理的实现细节,所以使用Spring Cloud构建的微服务应用可以方便切换不同服务治理框架,不用改动程序代码,只需要添加一些针对服务治理框架的配置即可。

package org.springframework.cloud.client.discovery;

import java.util.List;
import org.springframework.cloud.client.ServiceInstance;

public interface DiscoveryClient
{

    public abstract String description();

    public abstract ServiceInstance getLocalServiceInstance();

    public abstract List getInstances(String s);

    public abstract List getServices();
}

org.springframework.cloud.netflix.eureka.EnableDiscoveryClient是对DiscoveryClient接口的实现,实现的是对Eureka发现服务的封装。真正实现发现服务的是com.netflix.discovery.DiscoveryClient类

DiscoveryClient类主要用于帮助与Eureka Server互相协作。

Eureka Client 负责下面的任务:

  1. 向Eureka Server注册服务实例
  2. 向Eureka Server 服务租约
  3. 当服务关闭期间,向Eureka Server 取消租约
  4. 查询Eureka Server中的服务实例列表

Eureka Cient 还需要配置一个Eureka Server的URL列表


先分析一下Eureka Server的URL列表:

com.netflix.discovery.endpoint.EndpointUtils

    public static List getServiceUrlsFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone)
    {
        List orderedUrls = new ArrayList();
        String region = getRegion(clientConfig);
        String availZones[] = clientConfig.getAvailabilityZones(clientConfig.getRegion());
        if(availZones == null || availZones.length == 0)
        {
            availZones = new String[1];
            availZones[0] = "default";
        }
        logger.debug("The availability zone for the given region {} are {}", region, Arrays.toString(availZones));
        int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);
        List serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[myZoneOffset]);
        if(serviceUrls != null)
            orderedUrls.addAll(serviceUrls);
        for(int currentOffset = myZoneOffset != availZones.length - 1 ? myZoneOffset + 1 : 0; currentOffset != myZoneOffset;)
        {
            serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[currentOffset]);
            if(serviceUrls != null)
                orderedUrls.addAll(serviceUrls);
            if(currentOffset == availZones.length - 1)
                currentOffset = 0;
            else
                currentOffset++;
        }

        if(orderedUrls.size() < 1)
            throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!");
        else
            return orderedUrls;
    }

从上面的函数中发现,客户端一共加载了两个内容,一个是Region,一个是Zone。

    public static String getRegion(EurekaClientConfig clientConfig)
    {
        String region = clientConfig.getRegion();
        if(region == null)
            region = "default";
        region = region.trim().toLowerCase();
        return region;
    }

getRegion函数从配置中读取了一个Region返回,所以一个微服务应用只可以属于一个Region。默认是default。通过eureka.client.region属性定义region。

    public String[] getAvailabilityZones(String region)
    {
        String value = (String)availabilityZones.get(region);
        if(value == null)
            value = "defaultZone";
        return value.split(",");
    }

getAvailabilityZones函数,默认是defaultZone,可以看到Region与ZOne是一对多的关系,Zone可以设置多个,用逗号分隔。

在获取了Region和Zone的信息后,才开始真正加载Eureka Server的具体地址。根据传入的参数按一定算法加载位于哪一个Zone配置的serviceUrls。

int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);
List serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[myZoneOffset]);

看一下clientConfig.getEurekaServerServiceUrls的实现:

    public List getEurekaServerServiceUrls(String myZone)
    {
        String serviceUrls = (String)serviceUrl.get(myZone);
        if(serviceUrls == null || serviceUrls.isEmpty())
            serviceUrls = (String)serviceUrl.get("defaultZone");
        if(!StringUtils.isEmpty(serviceUrls))
        {
            String serviceUrlsSplit[] = StringUtils.commaDelimitedListToStringArray(serviceUrls);
            List eurekaServiceUrls = new ArrayList(serviceUrlsSplit.length);
            String as[] = serviceUrlsSplit;
            int i = as.length;
            for(int j = 0; j < i; j++)
            {
                String eurekaServiceUrl = as[j];
                if(!endsWithSlash(eurekaServiceUrl))
                    eurekaServiceUrl = (new StringBuilder()).append(eurekaServiceUrl).append("/").toString();
                eurekaServiceUrls.add(eurekaServiceUrl);
            }

            return eurekaServiceUrls;
        } else
        {
            return new ArrayList();
        }
    }

当在微服务应用中使用Ribbon实现服务调用时,对于Zone的设置可以在负载均衡时实现区域亲和特性:Ribbon的默认策略会优先访问客户端处于同一个Zone的服务端实例,只有当同一个Zone中没有可用服务端实例的时候才会访问其他Zone中的实例。


服务注册

接着看DiscoveryClient如何实现服务注册的:

    private void initScheduledTasks()
    {
        if(clientConfig.shouldFetchRegistry())
        {
            int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
            int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
            scheduler.schedule(new TimedSupervisorTask("cacheRefresh", scheduler, cacheRefreshExecutor, registryFetchIntervalSeconds, TimeUnit.SECONDS, expBackOffBound, new CacheRefreshThread()), registryFetchIntervalSeconds, TimeUnit.SECONDS);
        }
        if(clientConfig.shouldRegisterWithEureka())
        {
            int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
            int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
            logger.info((new StringBuilder()).append("Starting heartbeat executor: renew interval is: ").append(renewalIntervalInSecs).toString());
            scheduler.schedule(new TimedSupervisorTask("heartbeat", scheduler, heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new HeartbeatThread()), renewalIntervalInSecs, TimeUnit.SECONDS);
            instanceInfoReplicator = new InstanceInfoReplicator(this, instanceInfo, clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2);
            statusChangeListener = new com.netflix.appinfo.ApplicationInfoManager.StatusChangeListener() {

                public String getId()
                {
                    return "statusChangeListener";
                }

                public void notify(StatusChangeEvent statusChangeEvent)
                {
                    if(com.netflix.appinfo.InstanceInfo.InstanceStatus.DOWN == statusChangeEvent.getStatus() || com.netflix.appinfo.InstanceInfo.InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus())
                        DiscoveryClient.logger.warn("Saw local status change event {}", statusChangeEvent);
                    else
                        DiscoveryClient.logger.info("Saw local status change event {}", statusChangeEvent);
                    instanceInfoReplicator.onDemandUpdate();
                }

                final DiscoveryClient this$0;

            
            {
                this.this$0 = DiscoveryClient.this;
                super();
            }
            };
            if(clientConfig.shouldOnDemandUpdateStatusChange())
                applicationInfoManager.registerStatusChangeListener(statusChangeListener);
            instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
        } else
        {
            logger.info("Not registering with Eureka server per configuration");
        }
    }

可以看到在if(clientConfig.shouldRegisterWithEureka())里有一个InstanceInfoReplicator的实例,它会执行一个定时任务,该类的run()函数如下:

    public void run()
    {
        discoveryClient.refreshInstanceInfo();
        Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
        if(dirtyTimestamp != null)
        {
            discoveryClient.register();
            instanceInfo.unsetIsDirty(dirtyTimestamp.longValue());
        }
        Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
        scheduledPeriodicRef.set(next);
        break MISSING_BLOCK_LABEL_140;
        Throwable t;
        t;
        logger.warn("There was a problem with the instance info replicator", t);
        Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
        scheduledPeriodicRef.set(next);
        break MISSING_BLOCK_LABEL_140;
        Exception exception;
        exception;
        Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
        scheduledPeriodicRef.set(next);
        throw exception;
    }

真正触发调用注册的地方就在discoveryClient.register();内容如下:

    boolean register()
        throws Throwable
    {
        logger.info((new StringBuilder()).append("DiscoveryClient_").append(appPathIdentifier).append(": registering service...").toString());
        EurekaHttpResponse httpResponse;
        try
        {
            httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
        }
        catch(Exception e)
        {
            logger.warn("{} - registration failed {}", new Object[] {
                (new StringBuilder()).append("DiscoveryClient_").append(appPathIdentifier).toString(), e.getMessage(), e
            });
            throw e;
        }
        if(logger.isInfoEnabled())
            logger.info("{} - registration status: {}", (new StringBuilder()).append("DiscoveryClient_").append(appPathIdentifier).toString(), Integer.valueOf(httpResponse.getStatusCode()));
        return httpResponse.getStatusCode() == 204;
    }

注册操作是通过REST请求的方式进行的。同时可以看到发起注册请求的时候,传入了一个com.netflix.appinfo.InstanceInfo对象,该对象就是注册时客户端给服务端的元数据。


服务获取与服务续约

DiscoveryClient的initScheduledTasks函数中,还有两个定时任务,分别是服务获取和服务续约:

cacheRefresh和heartbeat


服务注册中心处理

Eureka Server对于各类REST请求的定义都位于com.netflix.eureka.resources包下



配置详解

Eureka客户端的配置主要分为两个方面:

1.服务注册相关的配置信息,包括服务注册中心的地址、服务获取的间隔时间、可用区域等。

2.服务实例相关的配置信息,包括服务实例的名称、IP地址、端口号、健康检查路径等。


服务注册类配置

指定注册中心

    通过eureka.client.serviceUrl参数实现。它的配置值存储在HashMap中,并且设置有一组默认值,默认值的key为defaultZone、Value为http://localhost:8761/eureka/

    通常配置为:eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/

    为了服务注册中心的安全考虑,可以加入安全校验,配置如下:

    http://<username>:<password>@localhost:1111/eureka/

    username为安全校验信息的用户名,password为该用户的密码。

其他配置

EurekaClientConfigBean定义了常用的配置参数,这些参数都以eureka.client为前缀

参数名 说明 默认值
enabled 启用Enable客户端 true
registryFetchIntervalSeconds 从Eureka服务器获取注册信息的间隔时间,单位是秒 30
instanceInfoReplicationIntervalSeconds 更新实例信息的变化到Eureka服务器的间隔时间,单位是秒 30
initialInstanceInfoReplicationIntervalSeconds 初始化实例信息到Eureka服务端的间隔时间,单位是秒 40
eurekaServiceUrlPollIntervalSeconds 轮询Eureka服务端地址更改的间隔时间,单位是秒 300
eurekaServerReadTimeoutSeconds 读取Eureka Server信息的超时时间,单位是秒    8
eurekaServerConnectTimeoutSeconds 连接Eureka Server的超时时间,单位是秒 5
eurekaServerTotalConnections 从Eureka客户端到所有Eureka服务端主机的连接总数 200
eurekaServerTotalConnectionsPerHost 从Eureka 客户端到每个Eureka服务端主机的连接总数 50
eurekaConnectionIdleTimeoutSeconds Eureka服务端链接的空闲关闭时间,单位是秒 30
heartbeatExecutorThreadPoolSize 心跳连接池的初始化线程数 2
heartbeatExecutorExponentialBackOffBound 心跳超时重试延迟时间的最大乘数值 10
cacheRefreshExecutorThreadPoolSize 刷新缓存线程池的初始化线程数 2
cacheRefreshExecutorExponentialBackOffBound 缓存刷新重试延迟时间的最大乘数值 10
useDnsForFetchingServiceUrls 使用DNS来获取Eureka服务端的serviceURL false
registerWithEureka 是否要将自身的实例信息注册到Eureka服务端 true
preferSameZoneEureka 是否偏好使用处于相同Zone的Eureka服务端 true
filterOnlyUpInstances 获取实例时是否过滤, 仅保留UP状态的实例 true
fetchRegistry 是否从Eureka服务端获取注册信息 true

服务实例类配置

实例名配置:

默认使用的主机名,可以通过spring.application.name 或者spring.application.instance_id设置



猜你喜欢

转载自blog.csdn.net/dxh0823/article/details/79981505