注册中心-Eureka 源码分析

注册中心-Eureka 源码分析

前言

上篇我们了解了注册中心的一些基本概念以及 Eureka 的使用,本章我们主要是分析 Eureka 如何实现服务注册和服务发现,以及一些 Eureka 中的设计细节。

项目环境

1.Eureka 基本概念

1.1 Register - 服务注册

当 Eureka Client 向 Eureka Server 注册时,Eureka Client 提供自身的元数据,比如 IP 地址、端口、运行状况指标的 Url、主页地址等信息。

1.2 Renew - 服务续约

Eureka Client 在默认情况下会每隔 30 秒来发送一次心跳来进行服务续约。通过服务续约来告知 Eureka Server 该 Eureka Client 仍然可用,没有出现故障。

如果 Eureka Server 90 秒内没有收到 Eureka Client 的心跳,Eureka Server 会将该 Eureka Client 实例从注册列表中剔除。

注意:官网不建议修改服务续约的间隔时间。

1.3 Fetch Registries - 获取服务注册列表信息

Eureka Client 从 Eureka Server 获取服务注册表信息,并将其缓存在本地(内存中)。Eureka Client 再从本地的注册列表中获取需要的服务信息,从而进行远程调用。

这个注册列表信息定时(每 30 秒)更新一次,每次返回的列表信息可能与当前缓存的数据不一致,Eureka Client 会自己处理这些信息。

Eureka Client 和 Eureka Server 可以使用 JSON 和 XML 数据格式进行通信,默认情况下,Eureka Client 使用 JSON 的方式来获取服务注册列表信息。

Eureka Client 缓存了所有的服务注册列表信息,其实这是一个 Eureka 的缺点,比较浪费内存,当服务器实例达到一定数量之后,比如 40K 以上的集群规模,每次更新内存都会消耗很大的资源,而且每个客户端实例都会缓存这么一份数据,实际上很多都是无效数据。

1.4 Cancel - 服务下线

Eureka Client 在程序关闭时可以向 Eureka Server 发送下线请求。发送请求后,该客户端的实例信息将从 Eureka Server 的服务注册列表中删除。

该下线请求不会自动完成,需要在程序关闭时调用以下代码:

DiscoveryManager.getInstance().shutdownComponent();

注意这是一个过期方法,一般不会使用。

1.5 Eviction - 服务剔除

默认情况下,如果 Eureka Client 连续 90 秒没有向 Eureka Server 发送服务续约(心跳),Eureka Server 会将该客户端的实例信息将从服务注册列表中删除,即服务剔除。

2.Eureka 高可用架构分析

如下图所示,Eureka 的架构主要分为 Eureka Server 和 Eureka Client 两部分,Eureka Client 又分为 Applicaton Service 和 Application Client,Applicaton Service 就是服务提供者,Application Client 就是服务消费者。

每个区域有一个 Eureka 集群,并且每个区域至少有一个 Eureka Server 可以处理区域故障,以防服务器瘫痪。
在这里插入图片描述
我们首先会在应用程序中依赖 Eureka Client,项目启动后 Eureka Client 会向 Eureka Server 发送请求,进行注册,并将自己的一些信息发送给 Eureka Server。

注册成功后,每隔一定的时间,Eureka Client 会向 Eureka Server 发送心跳来续约服务,也就是汇报健康状态。 如果客户端长时间没有续约,那么 Eureka Server 大约将在 90 秒内(三次心跳的时间)从服务器注册表中删除客户端的信息。

Eureka Client 还会定期从 Eureka Server 拉取注册表信息,然后根据负载均衡算法得到一个目标,并发起远程调用,关于负载均衡在后面的章节会详细介绍,也就是 Ribbon 组件。

应用停止时也会通知 Eureka Server 移除相关信息,信息成功移除后,对应的客户端会更新服务的信息,这样就不会调用已经下线的服务了,当然这个会有延迟,有可能会调用到已经失效的服务,所以在客户端会开启失败重试功能来避免这个问题。

Eureka Server 会有多个节点组成一个集群,保证高可用。Eureka Server 没有集成其他第三方存储,而是存储在内存中。所以 Eureka Server 之间会将注册信息复制到集群中的 Eureka Server 的所有节点。 这样数据才是共享状态,任何的 Eureka Client 都可以在任何一个 Eureka Server 节点查找注册表信息。

3.Eureka 注册表

注册表存储各个服务注册的相关信息,定义在 AbstractInstanceRegistry 类中,具体的代码如下:

    private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
            = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();

可以看到是一个 ConcurrentHashMap 的存储结构。

以本示例来说,Map 的 key 是服务名称,也就是 EUREKA-CLIENT-PROVIDER

value 也是一个 Map。value 的 Map 的 key 是服务实例的 ID, 比如这里的 192.168.0.106:eureka-client-provider:8081

value 的 Map 里的 value 是 Lease 类,Lease 中存储了实例的注册时间、上线时间等信息,还有具体的实例信息,比如 IP、端口、健康检查的地址等信息,对应的是 InstanceInfo。

Lease 源码:

public class Lease<T> {

    enum Action {
        Register, Cancel, Renew
    };

    public static final int DEFAULT_DURATION_IN_SECS = 90;

    private T holder;//实例信息 InstanceInfo
    private long evictionTimestamp;//取消注册时间
    private long registrationTimestamp;//服务注册的时间
    private long serviceUpTimestamp;//服务上线的时间
    // Make it volatile so that the expiration task would see this quicker
    private volatile long lastUpdateTimestamp;//最后更新的时间
    private long duration;//租约的时长
    ...

4.Eureka 核心操作

核心接口:com.netflix.eureka.lease.LeaseManager

4.1 注册 register

服务注册,即 Eureka Client 向 Eureka Server 提交自己的服务信息,包括 IP 地址,端口,ServiceId 等信息。ServiceId 默认为配置文件中的服务名,即 ${spring.application.name}。

客户端注册核心方法:com.netflix.discovery.DiscoveryClient#register

通过 Http 的方式向 Eureka Server 注册。

    boolean register() throws Throwable {
        logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
        EurekaHttpResponse<Void> 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() == 204;
    }

继续追踪,可以看到 register() 方法被 com.netflix.discovery.InstanceInfoReplicator#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);
        }
    }

整个的调用链路如下:

DiscoveryClient 初始化 -> initScheduledTasks -> InstanceInfoReplicator#run -> DiscoveryClient#register

客户端的注册流程我们大概了解了,我们再看看服务端的注册,这里我们可以先从 EurekaBootStrap 引导类开始,源码位置:eureka-core-1.9.2.jar 中 com.netflix.eureka.EurekaBootStrap。

在 initEurekaServerContext 方法中初始化了 PeerAwareInstanceRegistry,从名称上看和注册相关,我们看看实现类 PeerAwareInstanceRegistryImpl#register 的代码:

    @Override
    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);
    }

replicateToPeers 方法后续讨论,主要是用于将注册表信息同步到其他 Eureka Server 的其他 Peers 节点。

继续追踪 super.register,可以发现 PeerAwareInstanceRegistryImpl 的 register 方法实现了服务注册,并且向其他 Eureka Server 的 Peer 节点同步了该注册信息,那么 register 方法在哪里被调用呢?我们知道 Eureka Client 通过 Http 的方式来进行注册,那么 Eureka Server 一定会提供相关的 API 接口给 Eureka Client 进行调用。

接口如下:com.netflix.eureka.resources.ApplicationResource#addInstance

    @POST
    @Consumes({"application/json", "application/xml"})
    public Response addInstance(InstanceInfo info,
                       @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
        ...
        registry.register(info, "true".equals(isReplication));
        return Response.status(204).build();  // 204 to be backwards compatible
    }

整个服务注册的调用过程如下:

Eureka Client 启动 -> DiscoveryClient#register 服务注册 -> Eureka Server-> addInstance -> PeerAwareInstanceRegistryImpl#register

4.2 续约 renew

客户端 renew 调用同样是在 com.netflix.discovery.DiscoveryClient#initScheduledTasks 方法中,在 Eureka Client 程序启动之后,除了服务注册,还同时开启了服务续约的定时线程,代码如下:

    private void initScheduledTasks() {    
        ...
            // Heartbeat timer
            scheduler.schedule(
                    new TimedSupervisorTask(
                            "heartbeat",
                            scheduler,
                            heartbeatExecutor,
                            renewalIntervalInSecs,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                            new HeartbeatThread()
                    ),
                    renewalIntervalInSecs, TimeUnit.SECONDS);
        ...

HeartbeatThread 心跳线程会调用 renew 续约服务

    private class HeartbeatThread implements Runnable {

        public void run() {
            if (renew()) {
                lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
            }
        }
    }

同样 Eureka Server 一定会提供相关的 API 接口给 Eureka Client 进行调用,接口如下:

    @PUT
    public Response renewLease(
            @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,
            @QueryParam("overriddenstatus") String overriddenStatus,
            @QueryParam("status") String status,
            @QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {
        boolean isFromReplicaNode = "true".equals(isReplication);
        boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);
        ...

4.3 下线 cancel

cancel 的流程和之前的分析比较类似,Eureka Server 提供 cancel 相关的 API 接口如下:

    @DELETE
    public Response cancelLease(
            @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
        try {
            boolean isSuccess = registry.cancel(app.getName(), id,
                "true".equals(isReplication));
            ...
    }

4.4 剔除 evict

源码调用链路如下:

initEurekaServerContext -> PeerAwareInstanceRegistryImpl#openForTraffic -> AbstractInstanceRegistry#postInit -> AbstractInstanceRegistry.EvictionTask#run -> AbstractInstanceRegistry#evict(long)

Eureka Server 服务端程序启动的时候,会开启一个 EvictionTask 定时线程,用来监控服务的健康状况。

    protected void postInit() {
        renewsLastMin.start();
        if (evictionTaskRef.get() != null) {
            evictionTaskRef.get().cancel();
        }
        evictionTaskRef.set(new EvictionTask());
        evictionTimer.schedule(evictionTaskRef.get(),
                serverConfig.getEvictionIntervalTimerInMs(),
                serverConfig.getEvictionIntervalTimerInMs());
    }

提供 evictionIntervalTimerInMs 配置参数,默认值 60 * 1000。

5.Eureka 集群各节点的数据同步

如图所示,Eureka 集群采用相互注册的方式实现高可用集群,任何一台注册中心故障都不会影响服务的注册与发现。

在这里插入图片描述
那么我们看看 Eureka Server 如何实现,服务复制功能。在 4.1 注册 register 我们分析的时候,其实已经找到了复制的方法如下:

    private void replicateToPeers(Action action, String appName, String id,
                                  InstanceInfo info /* optional */,
                                  InstanceStatus newStatus /* optional */, boolean isReplication) {
        Stopwatch tracer = action.getTimer().start();
        try {
            ...
            for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
                // If the url represents this host, do not replicate to yourself.
                if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
                    continue;
                }
                replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
            }
        } finally {
            tracer.stop();
        }
    }
  • 通过 peerEurekaNodes.getPeerEurekaNodes() 得到 Eureka Server 的所有节点信息;
  • 在当前节点中循环进行复制操作,需要排除自己,不需要将信息同步给自己;
  • 复制操作会根据 Action 来进行对应的操作,通过 node 对象的方法构建复制的任务,任务本质还是通过调用 Eureka 的 Rest API 来进行操作的。

6.Eureka 自我保护机制

自我保护机制是为了避免因网络分区故障而导致服务不可用的问题。具体现象为当网络故障后,所有的服务与 Eureka Server 之间无法进行正常通信,一定时间后,Eureka Server 没有收到续约的信息,将会移除没有续约的实例,这个时候正常的服务也会被移除掉,所以需要引入自我保护机制来解决这种问题。

在这里插入图片描述
如上图所示,当服务生产者出现网络故障,无法与 Eureka Server 进行续约,Eureka Server 会将该实例移除,此时服务消费者从 Eureka Server 拉取不到对应的信息,实际上服务生产者处于可用的状态,问题就是这样产生的。

在这里插入图片描述
如上图所示,再来看已开启自我保护,当服务生产者出现网络故障,无法与 Eureka Server 进行续约时,虽然 Eureka Server 开启了自我保护模式,但没有将该实例移除,服务消费者还是可以正常拉取服务生产者的信息,正常发起调用,但是如果服务生产者确实是下线状态,调用就会出错。

在任何时间,如果 Eureka Server 接收到的服务续约低于该值配置的百分比(默认为 15 分钟内低于 85%),则服务器开启自动保护模式,不再剔除注册列表的信息。

7.Eureka 健康检查

4.2 续约 renew 中我们讲过了 Eureka 的心跳机制,Eureka Client 会定时发送心跳给 Eureka Server 来证明自己处于健康的状态,如下图所示:

在这里插入图片描述
假设在某种情况下,服务仍处理存活状态,但是已经不能正常提供服务了,比如数据库挂了,但是 Eureka Client 还是会定时发送心跳,由于心跳正常,客户端在请求时还是会请求到这个出了问题的服务实例。

我们是否有一种方式来自己控制 Eureka Client 的状态?

答案是肯定的,首先我们需要引入 actuator 依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

自定义心跳指标类

@Component
public class CustomHealthIndicator extends AbstractHealthIndicator {

    private boolean status;

    public void setStatus(boolean status) {
        this.status = status;
    }

    @Override
    public void doHealthCheck(Health.Builder builder) throws Exception {
        if(status){
            builder.up();
        }else{
            builder.down();
        }
    }
}

提供一个可以访问的接口

@RestController
public class StatusUpdateController {

    @Autowired
    private CustomHealthIndicator customHealthIndicator;

    @RequestMapping("/statusUpdate")
    public String statusUpdate(boolean status) {
        customHealthIndicator.setStatus(status);
        return "修改状态:" + status;
    }
}

配置文件增加

eureka:
  client:
    healthcheck:
      enabled: true

测试

浏览器输入 http://127.0.0.1:8081/statusUpdate?status=false 修改服务器状态为 DOWN

在这里插入图片描述
浏览器输入 http://127.0.0.1:8081/actuator/health 查看服务实例状态,已经修改为 DOWN

在这里插入图片描述
查看注册中心,可以看到目前的实例状态已经为 DOWN

在这里插入图片描述

8.为什么注册服务这么慢?

官网原文地址:https://docs.spring.io/spring-cloud-netflix/docs/2.2.4.RELEASE/reference/html/#why-is-it-so-slow-to-register-a-service

成为实例还涉及到注册表的周期性心跳(通过客户端serviceUrl),默认持续时间为 30 秒。直到实例服务器和客户端在其本地缓存中都具有相同的元数据后,客户端才能发现该服务(因此可能需要3个心跳)。您可以通过设置更改周期eureka.instance.leaseRenewalIntervalInSeconds。将其设置为小于 30 的值可以加快使客户端连接到其他服务的过程。在生产中,最好使用默认值,因为服务器中的内部计算对租约续订期进行了假设。

9.参考

  • 《深入理解 Spring Cloud 与微服务架构》- 方志朋

  • 《300分钟搞懂 Spring Cloud》- 尹吉欢

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值