从定时任务分析 Eureka 架构设计
Contents
写在前面
Eureka 功能是服务注册发现,核心实现在 package com.netflix.eureka.registry,其他几万行代码都绕着这 2000 来行代码展开。
接口 LookupService 声明了服务发现功能,接口 LeaseManager 声明租期管理功能,接口 InstanceRegistry 继承前两者并额外声明了实例信息覆盖、实例列表增量获取、响应缓存、自我保护等功能。最高抽象类 AbstractInstanceRegistry 实现了 InstanceRegistry,实际上就是在一个 ConcurrentHashMap 中维护注册信息,然后围绕该 Map 处理服务发现、服务注册、服务续租、服务下线、服务淘汰。
接口 PeerAwareInstanceRegistry 在 InstanceRegistry 之上提供 Eureka 集群实例之间的信息同步功能,而 PeerAwareInstanceRegistryImpl 继承抽象 AbstractInstanceRegistry 并实现了 PeerAwareInstanceRegistry 接口,所以 Eureka 中使用的实现类为 PeerAwareInstanceRegistryImpl。
在 registry 包基础上,Eureka 为实现高并发低延迟、配置动态更新、故障自动检测和监控等目标,大量使用了缓存、异步、批处理等技术,而这些在实现时多依赖定时任务。所以顺着定时任务扒,很容易弄懂 Eureka 的设计架构和高性能原理。
Eureka 在架构上主要围绕以下几点方面做文章:
- 围绕 Get 类请求,如获取应用列表和详情,Eureka 支持增量(delta) 获取,同时设置 2 级服务端缓存和客户端缓存,大幅度减少了核心 Registry 的并发度,带来了更少的带宽消耗和更低的延迟。
- 针对集群 Peer replication,每个 Peer Node 启用独立异步线程组,其一是异步处理,快速响应客户端请求,其二是实例信息批量同步,可以有效提高吞吐;最后则是使用舱壁模式隔离 Peer Node,这样即使某个 Peer Node 出现响应过慢或者无响应,并不会因线程耗尽而影响其他 Peer Node 接收信息同步。
- 围绕定时淘汰未续租过期实例功能,设计实现了灵活可配置的自我保护模式,可以解决网络分区问题,提高集群可用性
let’s go
启动 Eureka server,将其注册到某个集群,启动后,可通过 jstack
|
|
Server
以下定时任务处理 Get 请求 2 级缓存和增量 delta 队列
- Eureka-CacheFillTimer
- Eureka-DeltaRetentionTimer
以下定时任务处理 Peer Replication
- TaskAcceptor-target_peer*
- TaskBatchingWorker-target_peer*-0 至 TaskBatchingWorker-target_peer*-19
- TaskAcceptor-peer*,TaskNonBatchingWorker-peer*-0
以下定时任务处理过期实例淘汰及自我保护
- Eureka-EvictionTimer
- ReplicaAwareInstanceRegistry - RenewalThresholdUpdater
- Eureka-MeasureRateTime
注:以下标题中的 task 名均省去了 Eureka-
前缀 (如果有) 。
CacheFillTimer
CacheFillTimer 定时任务的作用就是每 30s 从 readWriteCache 同步一次实例信息至 readOnlyCache, 实现细节见 com.netflix.eureka.registry.ResponseCacheImpl。
请求——2 级响应缓存——Registry 流转逻辑
默认情况下 Eureka, 开启使用 2 级缓存提高实例获取性能,只有当 2 层缓存均不存在实例信息时,才会从 InstanceRegistry 获取注册信息,路径 /{version}/apps/ 下的 GET 请求均会被缓存。
|
|
根据 2 级响应缓存时效,结合后文会提到的客户端缓存时长 30s,可以量化 Eureka 的弱一致性。
假设某个实例在退出前取消注册,server 会 invalidate readWriteCache,因此最长存在 60s(readOnlyCache 30s + clientCache 30s) 延迟;假设某个实例在发送完 heart-beat 后立刻退出且没取消注册,那么可能存在 120s 延迟(readOnlyCache 30s + evict every 60s + clientCache 30s)。即使禁用 readOnlyCache,最大也会存在 90s 延迟。
该定时任务对应配置如下,readOnlyCache 可以通过 eureka.server.use-read-only-response-cache
禁用(CacheFillTimer定时任务也随之被禁用),readwriteCache 无法被禁用。
|
|
DeltaRetentionTimer
Eureka client 向 server 获取实例列表时,一般会使用增量获取而非全量,这样做可以减少传输数据量、并降低响应时间。
com.netflix.eureka.registry.AbstractInstanceRegistry 使用 recentlyChangedQueue(ConcurrentLinkedQueue
注:delta 层注册、续租、下线等状态与核心 Map 存储同步更新,不存在滞后性。
默认情况下,定时任务每 30s 执行一次,超过 180s 未更新的实例将被清理,可通过 eureka.server.retention-time-in-m-s-in-delta-queue
控制保存时长。
如果 eureka.server.disable-delta=true
,定时任务仍旧照常执行,只不过,Eureka server 在接到 get delta 请求时,会把它重定向到 get all。
|
|
相关配置如下
|
|
TaskAcceptor & TaskBatchingWorker
Eureka server 针对每个集群节点(Peer Node),都会启动 1 组线程批量处理集群实例信息同步,同步范围包括实例 register、cancel、heartbeat、statusUpdate 和 deleteStatusOverride,这部分逻辑集中在 com.netflix.eureka.cluster.PeerEurekaNode。
线程组包含 1 个 Acceptor 线程加上若干 Worker 线程(默认 20,通过 eureka.server.max-threads-for-peer-replication
设置)
|
|
Acceptor 在设计上使用了 3 级队列,第 1 级包括了最新的实例变更信息,没有显式设置容量限制。
- Eureka server PeerAwareInstanceRegistry 实现在接到客户端请求后,先写本地 registry,随之将变更提交到 Acceptor 对象的 accept queue (通过 PeerEurekaNode 实现),请求即立刻返回客户端,实际同步采用批处理方式完成。
- Acceptor daemon 线程不断尝试从 accept queue 获取任务并转至第 2 级的 pending task queue(实现上是 LinkedList + HashMap),pending task queue 容量默认 10,000,由
eureka.server.max-elements-in-peer-replication-pool
设置,超过容量后,使用 FIFO 方式丢弃任务。 - 同时,每当 pending queue 任务数量到达容量阈值 (10,000),或者每隔 500ms,Acceptor daemon 线程会将任务成批打包成 List(容量上限 250,写死无法配置),放置到 3 级队列 batch work queue 中。所有 Worker 线程共享该 3 级队列, JDK BlockQueue 为线程安全实现。Worker 线程不断尝试从 batch work queue 获取到 batch List 后,使用封装好的 HTTP Transport 实现将状态传递给集群其他节点。
每个 Peer Node 使用 20 线程处理信息同步,对于小集群来说可能比较浪费,可根据实际情况调整线程数量。
TaskAcceptor & TaskNonBatchingWorker
TaskNonBatchingWorker 其实就是 TaskBatchingWorker batch size 为 1 的特殊情况,netflix 专门用于处理 AWS Autoscaling group(ASG)变更同步,不使用 ASG 就用不到。
其对应配置如下
|
|
Evict 任务
- 主任务 EvictionTimer
对于未能在租期(默认 90s)通过心跳请求续租的实例,Eureka 使用定时任务,执行清理工作,实现在 com.netflix.eureka.registry。AbstractInstanceRegistry$EvictionTask。
租期淘汰任务执行频率由 eureka.server.eviction-interval-timer-in-ms
控制,默认 60s 一次。
Eureka 实现了名为「自我保护」的功能,用来检测网络分裂(network partition)这类问题。如果关闭了自我保护,直接执行清理;如果开启了自我保护,会使用结合辅助任务 RenewalThresholdUpdater 和 MeasureRateTimer → renewsLastMin 判断是否停止清理注册实例。
|
|
- 辅助任务 RenewalThresholdUpdater
用来更新自我保护开启阈值,实现见 com.netflix.eureka.registry.ReplicaAwareInstanceRegistry。
自我保护开启时(enable-self-preservation=true
),默认每 15 分钟更新一次续约刷新阈值,如果 eureka 每分钟收到的心跳数量小于阈值 注册instances数量x2x0.85,便会开启自我保护,不再淘汰实例。
相关配置如下,值得注意的是,即使关闭自我保护,该任务仍旧会定时执行。
|
|
- 辅助任务 MeasureRateTimer → renewsLastMin
默认记录并定期更新当前 60s 和上一 60s 注册实例的续租次数,见 com.netflix.eureka.registry.AbstractInstanceRegistry 属性 renewsLastMin,使用实现类 MeasuredRate 存储注册实例租约刷新次数,每 60s 更新上一分钟的租约刷新次数,供 Registry 实现判断是否需开启自我保护之用。
PeerNodesUpdater
实现见 com.netflix.eureka.cluster.PeerEurekaNodes#start。
作用是动态更新 Peer 节点列表,当配置 eureka.client.serviceUrl
和 eureka.client.availability-zones
发生变化时,动态更新通信集群节点,对应配置如下
|
|
MeasureRateTimer → numberOfReplicationsLastMin
com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl 定时任务,属性 numberOfReplicationsLastMin。 使用实现类 MeasuredRate 存储 peer 节点同步实例数量,每 60s 更新上一分钟的同步次数,供监控使用。
Client
以下线程任务处理客户端缓存更新
- DiscoveryClient-0
- DiscoveryClient-CacheRefreshExecutor-0
以下线程任务处理实例续租
- DiscoveryClient-0
- DiscoveryClient-HeartbeatExecutor-0
DiscoveryClient-InstanceInfoReplicator-0 处理服务状态更新,AsyncResolver-bootstrap-0 处理 Eureka Server 节点更新。
Eureka Client 在处理缓存定时更新和定时心跳续租时,采用双级线程池模式,里层线程池定时执行业务逻辑,外层线程池处理超时(超时时间等于执行间隔,实现见 com.netflix.discovery.TimedSupervisorTask),所以各需要 2 个线程。如果频繁超时导致任务积压,可能会启动临时业务线程 DiscoveryClient-CacheRefreshExecutor-1 和 DiscoveryClient-HeartbeatExecutor-1。
Eureka client 核心类为 com.netflix.discovery.DiscoveryClient,它在构造时会根据配置启动一系列定时任务。
|
|
CacheRefresh
集群应用列表获取任务,,一般由 scheduler DiscoveryClient-0 线程管理,由 DiscoveryClient-CacheRefreshExecutor-0 或 DiscoveryClient-CacheRefreshExecutor-1 执行,默认 30s 一次。
当 eureka.client.registerWithEureka=false
时,不会启动该任务。
可以通过 eureka.client.registry-fetch-interval-seconds
设置执行频率,默认 30s 一次。
heartbeat
心跳续租任务,一般由 scheduler DiscoveryClient-1 线程管理,由 DiscoveryClient-HeartbeatExecutor-0 或 DiscoveryClient-HeartbeatExecutor-1 执行,默认 30s 一次。
当 eureka.client.registerWithEureka=false
时,不会启动该任务。
可以通过 eureka.instance.lease-expiration-duration-in-seconds
调整租期,默认 90s。
可以通过 eureka.instance.lease-renewal-interval-in-seconds
调整心跳频率,默认 30s/次。
InstanceInfoReplicator
处理实例状态变更(通过 ApplicationInfoManager#setInstanceStatus),如从 UP 变为其状态 {DOWN,STARTING,OUT_OF_SERVICE,UNKNOWN},ApplicationInfoManager 的StatusChangeListener 会向 InstanceInfoReplicator 提交变更任务。
也就是说,实例状态变更和 heartbeat 续租分属不同逻辑分支。
InstanceInfoReplicator 经过一些 RateLimiter 策略判断后,会调用 DiscoveryClient#register 向 Eureka 重新注册应用状态。
主要用于实例上下线
|
|
AsyncResolver-bootstrap-0
逻辑入口 scheduleServerEndpointTask#scheduleServerEndpointTask
任务位置 AsyncResolver#updateTask
Client 端 Eureka Service 列表定时更新任务,不断读取内存配置并尝试更新 Eureka Service 列表,默认 5 分钟执行一次,无法禁止,可以使用 eureka-service-url-poll-interval-seconds
(默认 300s) 控制执行间隔。
Transport
Eureka Server 或 Eureka Client 部分均使用 JerseyClient HTTP Transport 抽象(实现为 Apache HttpClient)处理实例信息复制或实例信息注册,每个 Eureka 相关实例(包括自己)均对应一个独立的 Transport Client。
对每个 Transport Client,Eureka 均会启动 Eureka-JerseyClient-Conn-Cleaner 线程清理空闲 HTTP 连接,任务位置 com.netflix.discovery.shared.transport.jersey.ApacheHttpClientConnectionCleaner#cleanIdle(long delayMs)。
定时任务每 30s 执行一次,无法修改频率,无法被关闭。默认情况下,超过 30s 未活动连接将被关闭。可以通过 eureka.client.eureka-connection-idle-timeout-seconds
调整闲置时间阈值。
Author Zeng Xu
LastMod 2020-05-08 00:22
License 本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,转载时请注明原文链接。