微服务学习,源码探究[1] 服务的注册与发现

        许久没有做过学习笔记了,之前挖的坑也要因为手头的事情暂告一段落。

        由于技术上的需求,最近一段时间对微服务的架构学习了许多。粗略的学习过后决定一探其中源码,究其本质在于探索一些spirng springboot springcloud中的注解以及这些注解在配置项中起到的作用。偶尔也会涉及比如hystrix的设计模式的探讨与启发。

        该博客不是科普微服务的博客,所以认为读者已经对微服务的基本架构和用法有所了解,这里只是对源码有一些自己的拙见。

        众所周知,spring cloud以eureka组件作为其服务的注册中心。服务注册中心起到了注册服务,分发服务的作用。提供方和消费方都不知道其中的细节。这个组件的工作方式是怎么样的呢,让我们来窥视一下源代码:

        我们从服务的客户端开始探究这个问题,道理很简单,从提供方注册服务、消费方获取服务的这个过程对我们来说比较直观。

/**
 * Convenience annotation for clients to enable Eureka discovery configuration
 * (specifically). Use this (optionally) in case you want discovery and know for sure that
 * it is Eureka you want. All it does is turn on discovery and let the autoconfiguration
 * find the eureka classes if they are available (i.e. you need Eureka on the classpath as
 * well).
 *
 * @author Dave Syer
 * @author Spencer Gibb
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@EnableDiscoveryClient
public @interface EnableEurekaClient {

}

这是@EnableEurekaClient注解的源代码 。

[充电站]@Inherited注解:使子类可以继承父类的注解。

在这个注释中没有实际的代码,但是通过阅读作者留下的说明,我们大概了解到了有了这个注解,就会去加载eureka客户端的配置并且尝试发现eureka服务。

[充电站]@EnalbeDiscoveryClient注解:允许应用发现服务,这个注解和@EnableEurekaClient的区别就在于,除了Eureka还有很多的注册中心可以使用,例如大名鼎鼎的zk(zookeeper)。

我们通过DiscoveryClient类去深入了解其中的机制:

/**
 * The class that is instrumental for interactions with <tt>Eureka Server</tt>.
 *
 * <p>
 * <tt>Eureka Client</tt> is responsible for a) <em>Registering</em> the
 * instance with <tt>Eureka Server</tt> b) <em>Renewal</em>of the lease with
 * <tt>Eureka Server</tt> c) <em>Cancellation</em> of the lease from
 * <tt>Eureka Server</tt> during shutdown
 * <p>
 * d) <em>Querying</em> the list of services/instances registered with
 * <tt>Eureka Server</tt>
 * <p>
 *
 * <p>
 * <tt>Eureka Client</tt> needs a configured list of <tt>Eureka Server</tt>
 * {@link java.net.URL}s to talk to.These {@link java.net.URL}s are typically amazon elastic eips
 * which do not change. All of the functions defined above fail-over to other
 * {@link java.net.URL}s specified in the list in the case of failure.
 * </p>
 *
 * @author Karthik Ranganathan, Greg Kim
 * @author Spencer Gibb
 *
 */

秉承一贯的特点,我们先看作者留下的说明文档:这个类是为了与eureka服务端做交互的。这个类起如下的作用:向服务中心注册实例、与服务中心续约、服务崩溃时解除与服务中心的注册关系、查询在服务中心已注册的可用服务清单。客户端会从配置文件中找到服务中心的地址,当且仅当所有的服务请求都失败了,才会认为这一次的请求是失败的。

从说明文档里已经很明白的了解到了类起到的作用。

接下来我们要去了解更为细节的东西,首先是获取配置的方式,至于配置的设计,在这里先不多说。(有部分方法已经被标注为过时方法,可以通过@link寻找到EndpointUtils类的相同方法)

public static List<String> getServiceUrlsFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
        List<String> orderedUrls = new ArrayList<String>();
        
        String region = getRegion(clientConfig);

        String[] availZones = clientConfig.
                getAvailabilityZones(clientConfig.getRegion());
        
        if (availZones == null || availZones.length == 0) {
            availZones = new String[1];
            availZones[0] = DEFAULT_ZONE;
        }

        int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);

        List<String> serviceUrls = clientConfig.
                getEurekaServerServiceUrls(availZones[myZoneOffset]);
        if (serviceUrls != null) {
            orderedUrls.addAll(serviceUrls);
        }

        int currentOffset = myZoneOffset == 
                (availZones.length - 1) ? 0 : (myZoneOffset + 1);

        while (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!");
        }
        return orderedUrls;
    }

一上来就有一个region的概念,从config对象中获取了region的信息,如果没有设置region,将会采用默认的region,也就是defaultRegion,从方法的返回值可以知道,region应该只能配置一个。

接下来就是一个zone的概念,从方法的返回值也可以知道zone是可以有多个的,并且是在这个region下的若干zone,如果没有配置,将会采用默认值也就是defaultZone。注意这里,如果设置了prefersamezone,将会有限寻找到相同的zone的偏移量,并且返回的有序列表中,该zone下的url将会在列表前段。

之后的事情就比较简单,遍历所有的zone,并且获得url,将其放到有序列表中。

看这个方法:

 public List<String> getEurekaServerServiceUrls(String myZone) {
        String serviceUrls = configInstance.getStringProperty(
                namespace + CONFIG_EUREKA_SERVER_SERVICE_URL_PREFIX + "." + myZone,             
                null).get();
        if (serviceUrls == null || serviceUrls.isEmpty()) {
            serviceUrls = configInstance.getStringProperty(
                    namespace + CONFIG_EUREKA_SERVER_SERVICE_URL_PREFIX + ".default", 
                    null).get();

        }
        if (serviceUrls != null) {
            return Arrays.asList(serviceUrls.split(","));
        }

        return new ArrayList<String>();
 }

配置文件中的配置,本质上由来于它解析的方式,eureka.client.serviceUrls.defaultZone就是这样的。

因此我们也可以配置自己的region、zone以及相应的url:

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    prefer-same-zone-eureka: true
    region: beijing
    availability-zones:
      beijing: zone-1,zone-2
    service-url:
      zone-1: http://localhost:30000/eureka/
      zone-2: http://localhost:30001/eureka/

在我们使用Ribbon实现服务调用时,对于zone的设置可以在负载均衡时实现区域亲和:Ribbon的默认策略会优先访问和客户端在同一个zone里的服务端实例,只有当同一个zone里没有可用服务实例才会去访问其他zone。在这点上,我们可以设计出对区域性故障的容灾集群。

再来看看服务的注册:

private void initScheduledTasks() {
        if (clientConfig.shouldFetchRegistry()) {
            // registry cache refresh timer
            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("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);

            // InstanceInfo replicator
            instanceInfoReplicator = new InstanceInfoReplicator(
                    this,
                    instanceInfo,
                    clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                    2); // burstSize

            statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
                @Override
                public String getId() {
                    return "statusChangeListener";
                }

                @Override
                public void notify(StatusChangeEvent statusChangeEvent) {
                    if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
                            InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
                        // log at warn level if DOWN was involved
                        logger.warn("Saw local status change event {}", statusChangeEvent);
                    } else {
                        logger.info("Saw local status change event {}", statusChangeEvent);
                    }
                    instanceInfoReplicator.onDemandUpdate();
                }
            };

            if (clientConfig.shouldOnDemandUpdateStatusChange()) {
                applicationInfoManager.registerStatusChangeListener(statusChangeListener);
            }

            instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
        } else {
            logger.info("Not registering with Eureka server per configuration");
        }
    }

分析第一段:clientConfig.shouldFetchRegistry()条件告诉我们,只有我们设置了registry-with-eureka=true才会触发这一段内容,其中涉及了两个参数值:registryFetchIntervalSeconds是从eureka服务端获取注册信息的时间间隔,默认30s。

                         cacheRefreshExecutorExponentialBackOffBound是缓存刷新重试延迟时间的最大乘数,不是特别重要。

那么这个计划将会在每30秒去做一次服务的注册,有注册就会有续约和获取,我们先看续约:

 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) {
                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;
        }
    }

可以发现,本质上是一个http请求,像中心发送请求,其实服务的获取、注册等操作也是以rest请求做成的。

本章分析完后,将会自己制作一个简单的类eureka服务注册机制,以进一步体会其中的设计和参数必要性。

猜你喜欢

转载自blog.csdn.net/zkANewer/article/details/84873803
今日推荐