本文已参与周末学习计划,点击链接查看详情 链接
一、前言
看源码:抓大放小,先主流程,再细枝末节。
用技巧连蒙带猜:
- 看方法名(英文名)
- 看注释
- 从单元测试入手
eureka
运行的核心的流程:
- 服务注册:
eureka client
往eureka server
注册的过程 - 服务发现:
eureka client
从eureka server
获取注册表的过程 - 服务心跳:
eureka client
定时往eureka server
发送续约通知(心跳) - 服务实例摘除:服务下线、故障等
- 通信:
HTTP
- 限流
- 自我保护:自动识别是否出现网络故障
server
集群:eureka-server
之间相互注册,多级队列的任务批处理机制
eureka server
:提供注册中心的功能。
前提需知:
eureka-server
是一个web
应用,可以打成war
包,在tomcat
里启动。
(1)源码着手点
源码着手点可分为两点:
eureka-server
的build.gradle
:各种依赖和构建所需的配置eureka-server
的web.xml
:web
应用最核心之处,定义各种listener
和filter
1)从 build.gradle
可看出
文件路径:
eureka-server/build.gradle
可以看到,依赖的模块有:
-
eureka-client
:在集群模式中,每个server
也是一个client
,可以互相注册。 -
eureka-core
:注册中心的核心角色,接收服务注册请求,提供服务发现的功能,保持心跳(续约请求),摘除故障服务实例。 -
jersey
:RESTful HTTP
框架,eureka client
和eureka server
之间进行通信,都是基于jersey
。例如:
eureka-client-jersey2
,eureka-core-jersey2
-
等等,其他可从
build.gradle
查阅
2)从 web.xml
可看出
文件路径:
eureka-server/src/main/webapp/WEB-INF/web.xml
从 web.xml
可看出:
-
listener
监听器: 负责web
应用初始化; 例如,启动后台线程去加载配置文件对应
eureka-server
里,就是com.netflix.eureka.EurekaBootStrap
-
filter
过滤器:对请求进行处理
核心的 filter
有四个:
当然,每个
filter
在web.xml
也定义了相对应的URL
匹配。
StatusFilter
:负责状态相关的处理逻辑ServerRequestAuthFilter
:授权认证相关的处理逻辑RateLimitingFilter
:负责限流相关的处理逻辑GzipEncodingEnforcingFilter
:gzip
,压缩相关的
最后一个是 jersey
的 ServletContainer
: 接收所有的请求,作为请求的入口
<filter>
<filter-name>jersey</filter-name>
<filter-class>com.sun.jersey.spi.container.servlet.ServletContainer</filter-class>
... ...
</filter>
(2)一些概念
读源码准备,Eureka
的一些概念:
Register
服务注册Renew
服务续约Fetch Registries
获取服务注册列表信息Cancel
服务下线Eviction
服务剔除
二、从源码中学到了什么
凡凡觉得 “学到了” 的东西。
Tips
:方法名、变量名日常值得学习。
(1)线程池的使用
学习他人怎么使用线程池,方便以后自己写架构时候,拿来就用。
源码中线程池有:
- 调度线程池
- 心跳线程池
- 缓存刷新线程池
- 节点同步注册表线程池
详细解析,如下:
-
调度线程池
// 定位:com.netflix.discovery.DiscoveryClient.java private final ScheduledExecutorService scheduler; // 支持调度的线程池,核心线程数 2个,使用 google 工具包中线程工厂,后台线程 scheduler = Executors.newScheduledThreadPool( 2, new ThreadFactoryBuilder() .setNameFormat("DiscoveryClient-%d") .setDaemon(true) .build());
-
心跳线程池
// 定位:com.netflix.discovery.DiscoveryClient.java private final ThreadPoolExecutor heartbeatExecutor; // 支持心跳的线程池: 核心线程数1个,默认最大线程数5个,且线程存活时间为0,后台线程 heartbeatExecutor = new ThreadPoolExecutor( 1, 5, 0, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new ThreadFactoryBuilder() .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d") .setDaemon(true) .build()
-
缓存刷新线程池
// 定位:com.netflix.discovery.DiscoveryClient.java private final ThreadPoolExecutor cacheRefreshExecutor; // 支持缓存刷新的线程池: 核心线程数1个,默认最大线程数5个,且线程存活时间为0,后台线程 cacheRefreshExecutor = new ThreadPoolExecutor( 1, 5, 0, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new ThreadFactoryBuilder() .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d") .setDaemon(true) .build() );
-
节点同步注册表线程池
// 定位:com.netflix.eureka.cluster.PeerEurekaNodes.java private ScheduledExecutorService taskExecutor; taskExecutor = Executors.newSingleThreadScheduledExecutor( new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r, "Eureka-PeerNodesUpdater"); thread.setDaemon(true); return thread; } } }
(2)ConcurrentHashMap
使用
// 定位:com.netflix.eureka.registry.ResponseCacheImpl.java
// 1. 只读缓存
// 过期策略:被动过期
private final ConcurrentMap<Key, Value> readOnlyCacheMap =
new ConcurrentHashMap<Key, Value>();
// 2. 读写缓存:使用 guava 的 cache
// 过期策略:主动过期
private final LoadingCache<Key, Value> readWriteCacheMap =
CacheBuilder.newBuilder()
.initialCapacity(1000)
.expireAfterWrite(180, TimeUnit.SECONDS) // 过期时间:默认 180 秒
.removalListener(new RemovalListener<Key, Value>() {
@Override
public void onRemoval(RemovalNotification<Key, Value> notification) {
Key removedKey = notification.getKey();
if (removedKey.hasRegions()) {
Key cloneWithNoRegions = removedKey.cloneWithoutRegions();
regionSpecificKeys.remove(cloneWithNoRegions, removedKey);
}
}
})
.build(new CacheLoader<Key, Value>() {
@Override
public Value load(Key key) throws Exception {
if (key.hasRegions()) {
Key cloneWithNoRegions = key.cloneWithoutRegions();
regionSpecificKeys.put(cloneWithNoRegions, key);
}
Value value = generatePayload(key);
return value;
}
});
(3)循环队列
在
com.netflix.eureka.registry.AbstractInstanceRegistry.java
里定义两个循环队列。
// 最近注册的队列 private final CircularQueue<Pair<Long, String>> recentRegisteredQueue; // 最近取消的队列 private final CircularQueue<Pair<Long, String>> recentCanceledQueue;
private class CircularQueue<E> extends ConcurrentLinkedQueue<E> {
private int size = 0;
public CircularQueue(int size) {
this.size = size;
}
@Override
public boolean add(E e) {
this.makeSpaceIfNotAvailable();
return super.add(e);
}
private void makeSpaceIfNotAvailable() {
if (this.size() == size) {
this.remove();
}
}
public boolean offer(E e) {
this.makeSpaceIfNotAvailable();
return super.offer(e);
}
}
(4)锁的使用
锁的使用有:
- 读写锁
ReentrantReadWriteLock
synchronized
使用ReentrantLock
使用
详细使用,如下:
- 读写锁
ReentrantReadWriteLock
// 定位:com.netflix.eureka.registry.AbstractInstanceRegistry.java
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock read = readWriteLock.readLock();
private final Lock write = readWriteLock.writeLock();
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
// 读锁
read.lock();
} finally {
// 释放
read.unlock();
}
}
synchronized
使用
// 定位:com.netflix.eureka.registry.AbstractInstanceRegistry.java
protected final Object lock = new Object();
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
synchronized (lock) {
... ...
}
}
ReentrantLock
使用
// 定位:com.netflix.discovery.DiscoveryClient.java
private final Lock fetchRegistryUpdateLock = new ReentrantLock();
// 尝试加锁
if (fetchRegistryUpdateLock.tryLock()) {
try {
... ...
} finally {
// 释放锁
fetchRegistryUpdateLock.unlock();
}
} else {
// 获取锁失败,打日志
logger.warn("Cannot acquire update lock, aborting getAndUpdateDelta");
}
(5)定时器使用
发现 eureka
中大量使用了 Timer
定时器:
-
Timer
属于JDK
比较早期版本的实现,它可以实现固定周期的任务,以及延迟任务。 -
Timer
会起动一个异步线程去执行到期的任务,任务可以只被调度执行一次,也可以周期性反复执行多次。
Timer
是如何使用的,示例代码如下:
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
// do something
}
}, 10000, 1000); // 10s 后调度一个周期为 1s 的定时任务
可以看出,任务是由 TimerTask
类实现,TimerTask
是实现了 Runnable
接口的抽象类,Timer
负责调度和执行 TimerTask
。
接下来看下 Timer
的内部构造:
public class Timer {
// 小根堆,run操作 O(1)、新增 O(logn)、cancel O(logn)
private final TaskQueue queue = new TaskQueue();
// 创建另外线程,任务处理,会轮询 queue
private final TimerThread thread = new TimerThread(queue);
public Timer(String name) {
thread.setName(name);
thread.start();
}
}
Timer
它是存在不少设计缺陷的,所以并不推荐用户使用:
-
Timer
是单线程模式。如果某个TimerTask
执行时间很久,会影响其他任务的调度。 -
Timer
的任务调度是基于系统绝对时间的,如果系统时间不正确,可能会出现问题。 -
TimerTask
如果执行出现异常,Timer
并不会捕获,会导致线程终止,其他任务永远不会执行。
三、直接怼源码
(1)初始化
在eureka-server
初始化时,就会触发这个监听器:
在
eureka-server/src/main/webapp/WEB-INF/web.xml
定义了
<listener>
<listener-class>com.netflix.eureka.EurekaBootStrap</listener-class>
</listener>
而这个监听器的执行初始化的方法,contextInitialized()
方法,源码如下:
// 定位:eureka-core/src/main/java/com/netflix/eureka/EurekaBootStrap.java
@Override
public void contextInitialized(ServletContextEvent event) {
try {
// 1. 初始化环境变量
initEurekaEnvironment();
// 2. 初始化上下文
initEurekaServerContext();
ServletContext sc = event.getServletContext();
sc.setAttribute(EurekaServerContext.class.getName(), serverContext);
} catch (Throwable e) {
logger.error("Cannot bootstrap eureka server :", e);
throw new RuntimeException("Cannot bootstrap eureka server :", e);
}
}
目前源码着手点,可分为两点:
-
初始化环境变量
initEurekaEnvironment()
-
初始化上下文
initEurekaServerContext()
1)初始化环境变量 initEurekaEnvironment()
主要过程步骤如下:
-
创建
ConcurrentCompositeConfiguration
:代表了所谓的配置,包括了eureka
需要的所有的配置1.1 在创建时候,先调用了
clear()
方法1.2 之后,
fireEvent()
发布了一个事件(EVENT_CLEAR
) -
往
ConcurrentCompositeConfiguration config
实例加入了一堆别配置2.1 初始化数据中心的配置,如果没有配置的话,就是
DEFAULT data center
2.2 初始化
eureka
运行的环境,如果没有配置,默认设置为test
环境
进入方法,查看对应源码:
// 定位:eureka-core/src/main/java/com/netflix/eureka/EurekaBootStrap.java
protected void initEurekaEnvironment() throws Exception {
logger.info("Setting the eureka configuration..");
// 1. 重要的是:
String dataCenter = ConfigurationManager.getConfigInstance().getString(EUREKA_DATACENTER);
// 2. 其他别的配置
... ...
}
进入 ConfigurationManager.getConfigInstance()
,可看到:
// 定位:com.netflix.config.ConfigurationManager.java
public static AbstractConfiguration getConfigInstance() {
if (instance == null) {
synchronized (ConfigurationManager.class) {
if (instance == null) {
instance = getConfigInstance(Boolean.getBoolean(DynamicPropertyFactory.DISABLE_DEFAULT_CONFIG));
}
}
}
return instance;
}
初始化配置管理器(ConfigurationManager
):采用 双检查锁(double check
)和volatile
单例模式 来实现线程安全。
进入 getConfigInstance()
方法:
// 定位:com.netflix.config.ConfigurationManager.java
private static AbstractConfiguration getConfigInstance(boolean defaultConfigDisabled) {
if (instance == null && !defaultConfigDisabled) {
// 1. 创建 ConcurrentCompositeConfiguration
instance = createDefaultConfigInstance();
registerConfigBean();
}
return instance;
}
private static AbstractConfiguration createDefaultConfigInstance() {
// 创建主要配置
ConcurrentCompositeConfiguration config = new ConcurrentCompositeConfiguration();
// 加载其他配置
... ...
return config;
}
// 定位:com.netflix.config.ConcurrentCompositeConfiguration.java
// 1.1. 创建主要配置时,会先清理
public ConcurrentCompositeConfiguration() {
clear();
}
public final void clear() {
// 1.2 发布事件
fireEvent(EVENT_CLEAR, null, null, true);
... ...
fireEvent(EVENT_CLEAR, null, null, false);
... ...
}
2)初始化上下文 initEurekaServerContext()
主要过程步骤如下:
- 第一步:加载
eureka-server
文件配置 - 第二步:初始化
eureka-server
内部的eureka-client
(用来跟其他的eureka-server
节点进行注册和通信) - 第三步:处理注册相关的事情
- 第四步:处理
peer
相关节点 - 第五步:完成
eureka-server
上下文(context
)的构建 - 第六步:处理一些善后的事情,从相邻的
eureka
节点拷贝注册信息 - 第七步:注册所有监控
protected void initEurekaServerContext() throws Exception {
// 第一步:加载 eureka-server 文件配置
EurekaServerConfig eurekaServerConfig = new DefaultEurekaServerConfig();
// 第二步:初始化 eureka-server 内部的 eureka-client
//(跟其他的 eureka-server 节点进行注册和通信)
ApplicationInfoManager applicationInfoManager = null;
// 第三步:处理注册相关的事情
PeerAwareInstanceRegistry registry;
// 第四步:处理 peer 相关节点
PeerEurekaNodes peerEurekaNodes = getPeerEurekaNodes();
// 第五步:完成 eureka-server 上下文(context)的构建
serverContext = new DefaultEurekaServerContext();
// 第六步:处理一些善后的事情,从相邻的 eureka 节点拷贝注册信息
int registryCount = registry.syncUp();
registry.openForTraffic(applicationInfoManager, registryCount);
// 第七步:注册所有监控
EurekaMonitors.registerAllStats();
}
下面进入主要流程:
第一步:加载 eureka-server
文件配置
加载 eureka-server.properties
的过程:
-
创建了一个
DefaultEurekaServerConfig
对象,创建时会调用init()
方法DefaultEurekaServerConfig
提供的获取配置项的各个方法,都是通过硬编码的配置项名称,从DynamicPropertyFactory
中获取配置项的值,DynamicPropertyFactory
又是从ConfigurationManager
那儿获取来的,所以也包含了所有配置项的值。
// 1. init()
private void init() {
// 拿到当前的环境:eureka.environment
String env = ConfigurationManager.getConfigInstance().getString(
EUREKA_ENVIRONMENT, TEST);
// 设置当前属性
ConfigurationManager.getConfigInstance().setProperty(
ARCHAIUS_DEPLOYMENT_ENVIRONMENT, env);
// 加载 eureka 配置文件的名字: eureka-server
String eurekaPropsFile = EUREKA_PROPS_FILE.get();
// 2. 加载配置:eureka-server.properties
ConfigurationManager.loadCascadedPropertiesFromResources(eurekaPropsFile);
}
-
将
eureka-server.properties
中的配置加载到了一个Properties
对象中,然后将Properties
对象中的配置放到ConfigurationManager
中去,此时ConfigurationManager
中去就有了所有的配置了。eureka-server.properties
默认没有配置,所以读取的都是默认值。
第二步:初始化 eureka-server
内部的 eureka-client
ApplicationInfoManager applicationInfoManager = null;
if (eurekaClient == null) {
// 1. 将 eureka-client.properties 配置加载到 ConfigurationManager
EurekaInstanceConfig instanceConfig = isCloud(ConfigurationManager.getDeploymentContext())
? new CloudInstanceConfig()
: new MyDataCenterInstanceConfig();
// 2. 这期间会构建 InstanceInfo(服务实例的实例本身信息)
// 基于 EurekaInstanceConfig 和 InstanceInfo 构建了 ApplicationInfoManager
// ApplicationInfoManager:是对服务实例进行管理
applicationInfoManager = new ApplicationInfoManager(
instanceConfig, new EurekaConfigBasedInstanceInfoProvider(instanceConfig).get());
// 3. 创建 eureka-client 配置项
EurekaClientConfig eurekaClientConfig = new DefaultEurekaClientConfig();
// 4. 创建 EurekaClient,DiscoveryClient 是其子类
eurekaClient = new DiscoveryClient(applicationInfoManager, eurekaClientConfig);
} else {
applicationInfoManager = eurekaClient.getApplicationInfoManager();
}
-
加载服务实例相关配置:将
eureka-client.properties
配置加载到ConfigurationManager
,并提供EurekaInstanceConfig
详细可看:
PropertiesInstanceConfig
问题:这里不是初始化
eureka-server
嘛?为什么要加载eureka-client
?eureka-server
同时也是一个eureka-client
,因为他可能要向其他的eureka-server
去进行注册,从而组成一个eureka-server
的集群。 -
基于
EurekaInstanceConfig
和InstanceInfo
构建了ApplicationInfoManager
-
创建
eureka-client
配置项 -
重点:创建
EurekaClient
,DiscoveryClient
是其子类// 定位:com.netflix.discovery.DiscoveryClient.java DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, Provider<BackupRegistry> backupRegistryProvider) { ... ... // 1. 读取 EurekaClientConfig,包括 TransportConfig clientConfig = config; transportConfig = config.getTransportConfig(); // 2. 保存 EurekaInstanceConfig 和 InstanceInfo instanceInfo = myInfo; ... ... // 3. 是否需要抓取注册表 if (config.shouldFetchRegistry()) {... ...} // 3. 是否需要注册到 eureka if (config.shouldRegisterWithEureka()) {... ...} try { // 4. 支持调度的线程池,核心线程数 2个,使用 google 工具包中线程工厂 scheduler = Executors.newScheduledThreadPool(... ...); // 5. 支持心跳的线程池: 核心线程数1个,默认最大线程数5个,且线程存活时间为0 heartbeatExecutor = new ThreadPoolExecutor(... ...); // 6. 支持缓存刷新的线程池: 核心线程数1个,默认最大线程数5个,且线程存活时间为0 cacheRefreshExecutor = new ThreadPoolExecutor(... ...); // 7. 创建 EurekaTransport: // 支持底层的 eureka-client 跟 eureka-server 进行网络通信的组件 eurekaTransport = new EurekaTransport(); ... ... } // 8. 抓取注册表 // 这里代码写的不好,难以懂得 if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) { fetchRegistryFromBackup(); } // 9. 初始化调度任务 initScheduledTasks(); ... ... }
步骤如下:
- 读取
EurekaClientConfig
,包括TransportConfig
- 保存
EurekaInstanceConfig
和InstanceInfo
- 是否要注册以及抓取注册表,如果不要的话,释放一些资源
- 支持调度的线程池
- 支持心跳的线程池
- 支持缓存刷新的线程池
- 创建
EurekaTransport
:支持底层的eureka-client
跟eureka-server
进行网络通信的组件 - 抓取注册表
- 初始化调度任务:
- 如果要抓取注册表的话,就会注册一个定时任务,按照设定的抓取的间隔(默认是30s),在调度线程池去执行
CacheRefreshThread
- 如果要向
eureka-server
进行注册的话,启动定时任务,每隔一定时间发送心跳,执行HeartbeatThread
- 创建了服务实例副本传播器,将自己作为一个定时任务进行调度
- 创建了服务实例的状态变更的监听器,如果配置了监听,那么就会注册监听器
- 如果要抓取注册表的话,就会注册一个定时任务,按照设定的抓取的间隔(默认是30s),在调度线程池去执行
- 读取
第三步:处理注册相关的事情
// 注册表: 感知 eureka-server 集群的服务实例注册表
PeerAwareInstanceRegistry registry;
if (isAws(applicationInfoManager.getInfo())) {
registry = new AwsInstanceRegistry(... ...);
... ...
} else {
// 实现类
registry = new PeerAwareInstanceRegistryImpl(
eurekaServerConfig,
eurekaClient.getEurekaClientConfig(),
serverCodecs,
eurekaClient
);
}
第四步:处理 peer
相关节点
// PeerEurekaNodes : 代表了eureka server集群
// peers 是多个相同的实例组成的一个集群,peer 是 peers 集群中的一个实例
PeerEurekaNodes peerEurekaNodes = getPeerEurekaNodes(... ...);
初始化方法上有一行注释,说明了 peer
的意思 :
/**
Initializes Eureka, including syncing up with other Eureka peers and publishing the registry.
*/
第五步:完成 eureka-server
上下文的构建
// 1. 创建上下文
serverContext = new DefaultEurekaServerContext(.. ...);
// 2. 将上下文放到 holder 中,以供其他使用
EurekaServerContextHolder.initialize(serverContext);
// 3. 初始化上下文
serverContext.initialize();
这里重要提一下 serverContext.initialize();
:
public void initialize() throws Exception {
logger.info("Initializing ...");
// 1. 用于更新 eureka-server 集群的信息
// 让当前的 eureka-server 能感知到其他 eureka-server
// 原理:后台搞了一个定时调度任务,定时更新 eureka-server 集群信息
peerEurekaNodes.start();
// 2. 初始化注册表
// 将 eureka-server 集群中所有的 eureka-server 的注册表的信息抓取过来,存放到本地的注册表里
registry.init(peerEurekaNodes);
logger.info("Initialized");
}
第六步:处理善后的事情,从相邻的 eureka
节点拷贝注册信息
// 从相邻的一个 eureka-server 节点拷贝注册表的信息
// 如果拷贝失败,就找下一个
int registryCount = registry.syncUp();
第七步:注册所有监控
EurekaMonitors.registerAllStats();
(2)eureka-server
如何完成服务注册?
从单元测试入手,一步步看 eurek-server
完成服务注册。
环境准备,如图:
在
eureka-core
和eureka-server
模块下: 把eureka-server
下的resources
的eureka-client.properties
拷贝到eureka-core
的resources
那么就可以愉快的 DEBUG
了,定位代码如下:
// 定位:eureka-core 模块,test包下:
// com.netflix.eureka.resources.ApplicationResourceTest.java
@Test
public void testGoodRegistration() throws Exception {
// 1. 生成服务实例
InstanceInfo noIdInfo = InstanceInfoGenerator.takeOne();
// 2. 发送 HTTP 请求 - 添加实例
Response response = applicationResource.addInstance(noIdInfo, false+"");
assertThat(response.getStatus(), is(204));
}
InstanceInfo
,服务实例,里面最主要数据:
hostname
(主机名)、ip
地址、端口号、url
地址lease
(租约)的信息:心跳的间隔时间、最近心跳时间、服务注册时间、服务启动时间
灵魂提问:说白了,注册啥玩意?
- 告诉注册中心,
client
的信息 - 把注册中心的信息同步缓存到本地
主要关心第2步骤:(ApplicationResource.addInstance()
)
// 定位:
public Response addInstance(InstanceInfo info,
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
// 校验一堆 InstanceInfo 参数
... ...
// 处理数据中心,地区等信息
... ...
// 真正注册,并判断是否是集群模式
registry.register(info, "true".equals(isReplication));
// 成功,就返回 HTTP 状态码 204
return Response.status(204).build();
}
注册,如下代码:
// 定位:com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.java
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();
}
// 1. 调用父级方法,进行注册
super.register(info, leaseDuration, isReplication);
// 2. 向同级注册
replicateToPeers(Action.Register, info.getAppName(),
info.getId(), info, null, isReplication);
}
- 注册
AbstractInstanceRegistry.register()
// 定位:
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
// 使用读锁,多个服务实例,可以同时来注册
read.lock();
// 通过 AppName 获取 Map
Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
... ...
// 通过 instanceId,从 gMap 中获取服务实例对应的租约
... ...
} finally {
// 释放读锁
read.unlock();
}
}
内存注册表,就是:Map<String, Lease<InstanceInfo>>
每个服务的实例信息保存起来
(3)eureka-server
注册表多级缓存机制
代码如下:
// 定位:com.netflix.eureka.resources.ApplicationResource.java
public Response getApplication(...) {
... ...
// 构建缓存 key
Key cacheKey = new Key(
Key.EntityType.Application,
appName,
keyType,
CurrentRequestVersion.get(),
EurekaAccept.fromString(eurekaAccept)
);
// 重要:缓存机制
String payLoad = responseCache.get(cacheKey);
// 返回
}
进入 responseCache.get(cacheKey)
,代码如下:
// 定位:com.netflix.eureka.registry.ResponseCacheImpl.java
String get(final Key key, boolean useReadOnlyCache) {
// 重要,获取值
Value payload = getValue(key, useReadOnlyCache);
if (payload == null || payload.getPayload().equals(EMPTY_PAYLOAD)) {
return null;
} else {
return payload.getPayload();
}
}
// 重要:多级缓存
Value getValue(final Key key, boolean useReadOnlyCache) {
Value payload = null;
try {
if (useReadOnlyCache) {
// 1. 先从只读缓存中获取
final Value currentPayload = readOnlyCacheMap.get(key);
if (currentPayload != null) {
payload = currentPayload;
} else {
// 1.2. 只读缓存中没有,从读写缓存中获取
payload = readWriteCacheMap.get(key);
// 并将数据放到只读缓存中
readOnlyCacheMap.put(key, payload);
}
} else {
// 2. 从读写缓存中获取
payload = readWriteCacheMap.get(key);
}
} catch (Throwable t) {
logger.error("Cannot get value for key :" + key, t);
}
return payload;
}
总结下,如图:
(4)eureka-server
注册表多级缓存过期机制
过期机制,分为两种:
- 主动过期:主动通知过期,服务实例发生注册、下线、故障的时候
- 定时过期:
- 定时器:定时调度
- 过期时效判断
只读缓存和读写缓存过期,详解:
-
只读缓存(
readOnlyCacheMap
)- 执行一个定时调度器(
TimeTask
),默认 30秒。 - 对
readOnlyCacheMap
和readWriteCacheMap
中的数据进行一个Hash
比对:- 不一致,将
readWriteCacheMap
数据同步到readOnlyCacheMap
中。
- 不一致,将
- 执行一个定时调度器(
// 定位:com.netflix.eureka.registry.ResponseCacheImpl.java
private final java.util.Timer timer = new java.util.Timer("Eureka-CacheFillTimer", true);
if (shouldUseReadOnlyResponseCache) {
timer.schedule(getCacheUpdateTask(),
new Date(((System.currentTimeMillis() / responseCacheUpdateIntervalMs) * responseCacheUpdateIntervalMs) + responseCacheUpdateIntervalMs),
responseCacheUpdateIntervalMs);
}
- 读写缓存(
readWriteCacheMap
)
过期分为,两种:
- 缓存组件过期,使用
guava
的cache
:
// 定位:com.netflix.eureka.registry.ResponseCacheImpl.java
// 1. 读写缓存:使用 guava 的 cache
// 过期策略:主动过期
private final LoadingCache<Key, Value> readWriteCacheMap =
CacheBuilder.newBuilder()
.initialCapacity(1000)
.expireAfterWrite(180, TimeUnit.SECONDS) // 过期时间:默认 180 秒
......
- 主动通知:服务实例发生变化,注册、下线、故障的时候
// 例如:注册的时候
// 定位:com.netflix.eureka.registry.AbstractInstanceRegistry.java
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
... ...
// 服务实例注册信息变化的时候,就要更新这个缓存
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
... ...
}
总结,如图:
(5)自动故障感知以及服务实例自动摘除
eureka-server
凭借心跳来感知 eureka-client
是否存活着。
如果在一定的时间内没有收到心跳,那么就认定 eureka-client
已经宕机了,此时会修改对应 client
状态,并摘除。
eureka
想要能定时检测心跳,那么肯定会有一个定时任务,进行判断。
eureka-server
启动就会开启自动感知,代码如下:
// 定位:com.netflix.eureka.EurekaBootStrap.java
// 1. 初始化 eureka时,打开注册表
protected void initEurekaServerContext() throws Exception {
... ...
// 打开注册表,并接收请求
registry.openForTraffic(applicationInfoManager, registryCount);
... ...
}
// 定位:com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.java
// 2.
@Override
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
... ...
// 重点:
super.postInit();
}
// 定位:com.netflix.eureka.registry.AbstractInstanceRegistry.java
// 3.
protected void postInit() {
// 1. 启动调度器:每 60s 跑一次,统计一分钟内心跳次数
renewsLastMin.start();
... ...
// 2. 默认每隔 60s,调度一次:判断服务实例的租约是否过期了
evictionTimer.schedule(new EvictionTask(), 60, 60);
}
接下来,分析 new EvictionTask()
调度任务:
// 定位:com.netflix.eureka.registry.AbstractInstanceRegistry.java
class EvictionTask extends TimerTask {
private final AtomicLong lastExecutionNanosRef = new AtomicLong(0l);
@Override
public void run() {
try {
// 获取补偿时间,compensation:补偿
long compensationTimeMs = getCompensationTimeMs();
evict(compensationTimeMs);
} catch (Throwable e) {
logger.error("Could not run the evict task", e);
}
}
// 1. 获取补偿时间
// 为什么要补偿时间:可能 JVM GC问题或者系统时钟问题,导致没有刚好 60s 执行任务
long getCompensationTimeMs() {
// 先获取当前时间
long currNanos = getCurrentTimeNano();
// getAndSet:设置新值,返回旧值
// 例如:
// 第一次:10:00:00 ,就会将 10:00:00 设置到 lastExecutionNanosRef 这个里面去
// 第二次:10:01:00 设置到 lastExecutionNanosRef 去
long lastNanos = lastExecutionNanosRef.getAndSet(currNanos);
if (lastNanos == 0l) {
return 0l;
}
// 过去秒数 = 10:01:00 - 10:00:00 = 60s
long elapsedMs = TimeUnit.NANOSECONDS.toMillis(currNanos - lastNanos);
// 用这个 60s - 配置的 60s = 0s
long compensationTime = elapsedMs - serverConfig.getEvictionIntervalTimerInMs();
return compensationTime <= 0l ? 0l : compensationTime;
}
long getCurrentTimeNano() {
return System.nanoTime();
}
}
// 2. 驱逐服务实例
public void evict(long additionalLeaseMs) {
// 是否主动删除掉故障的服务实例
if (!isLeaseExpirationEnabled()) {
logger.debug("DS: lease expiration is currently disabled.");
return;
}
// 2.1. 过期服务实例列表,判断是否过期
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();
// 租约是否过期
// 对每个服务实例的租约判断一下,如果一个服务实例上一次的心跳时间到现在为止
// 超过了 90 × 2 = 180s 的话,才会认为这个服务实例过期了,故障了
if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
expiredLeases.add(lease);
}
}
}
}
// 2.2. 计算摘除任务:不能一次性拆除过多的服务实例
// 假设现在一共是 20 个服务实例,现在有 6个服务实例不可用了,
... ...
// 计算出来:可以摘除 3个
if (toEvict > 0) {
// 要摘除的服务实例一共有 6 个,但是最多只能摘除 3个服务实例
// 在 6个 服务实例中,随机选择3个服务实例来摘除
Random random = new Random(System.currentTimeMillis());
for (int i = 0; i < toEvict; i++) {
// 随机选择
int next = i + random.nextInt(expiredLeases.size() - i);
// ... ...
// 摘除:调用服务下线方法
internalCancel(appName, id, false);
}
}
}
重点,Eureka
BUG
判断租约是否过期:isExpired()
// 定位:com.netflix.eureka.lease.Lease.java
/**
Note that due to renew() doing the 'wrong" thing and setting lastUpdateTimestamp to + duration more than what it should be, the expiry will actually be 2 * duration. This is a minor bug and should only affect instances that ungracefully shutdown. Due to possible wide ranging impact to existing usage, this will not be fixed.
*/
public boolean isExpired(long additionalLeaseMs) {
// 当前时间是否 > 上一次心跳的时间 + 90s(续约持续时间) + 92s(补偿时间)
return (evictionTimestamp > 0 || System.currentTimeMillis() > (lastUpdateTimestamp + duration + additionalLeaseMs));
}
// 举例:
// 当前时间:20:02:32
// 判断:20:02:32 > 19:56:20 + 90 + 92s = 19:59:32
// 先不看补偿时间,假如说,当前时间比上次心跳的时间差了超过 90s, 说明,90s之内,都没有更新过心跳
// 就说明那个服务实例在 90s 内没有更新过心跳
// 此时就认为那个服务实例可能已经宕机了
// 19:55:00 -> 如果到了 19:56:30, 没有心跳,就该认为服务实例故障了
// 19:56:30 -> 如果到了 19:56:30, 没有心跳,还不会认为服务实例宕机了
// 20:00:00 -> 19:56:30 + 90s = 19:58:00
// 所以需要等待 2个 90s之后,都没有心跳,才会认为这个服务实例挂掉了,才会下线, 90s,180s
// 3分钟没有心跳,才会认为服务实例宕机了,下线了
// 源码层面有 bug ,90 * 2 = 180s,才会服务实例下线
// 失效多级缓存, 30s 才能同步, 服务30s才会重新抓取增量注册表
// 一个服务实例挂掉之后,可能要过几分钟,4 5分钟,才能让其他的服务感知到
(6)eureka-server
网络故障时自我保护机制
在某个时间段,大量的服务实例过期(没有收到心跳),那么 euerka-server
是否要把这些实例都移除掉呢?
答案是:否 因为,可能是
eureka-server
所在的机器出现了网络故障(网络分区),导致接收不到client
的心跳。
总图,如下:
在剔除服务时候时候,会判断是否开启服务自我保护,代码如下:
// 定位: com.netflix.eureka.registry.AbstractInstanceRegistry.java
public void evict(long additionalLeaseMs) {
// 是否主动删除掉故障的服务实例
if (!isLeaseExpirationEnabled()) {
logger.debug("DS: lease expiration is currently disabled.");
return;
}
}
public boolean isLeaseExpirationEnabled() {
// 默认配置是 true
if (!isSelfPreservationModeEnabled()) {
// The self preservation mode is disabled, hence allowing the instances to expire.
return true;
}
// 动态判断是否开启自我保护机制:
// numberOfRenewsPerMinThreshold 期望心跳数:期望服务实例 1分钟 发送多少心跳
// getNumOfRenewsInLastMin() 实际心跳数:上1分钟所有服务实例一共发送多少次心跳
// 期望心跳数 < 实际心跳数:返回true,就可以清理服务实例
// 期望心跳数 > 实际心跳数:返回false,不清理服务实例
return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
}
感觉这块有些问题,为什么统计所有服务实例的心跳数?如果某个服务频繁发送心跳,那么这个机制不就被破解了?
根据上面代码,仍然有两问题:
- 如果计算期望的一分钟心跳次数(
numberOfRenewsPerMinThreshold
)? - 实际的上一分钟心跳次数是如何计算的?
1)如果计算期望的一分钟心跳次数?
计算期望的一分钟心跳次数,时机有:
-
初始化时,给个默认值
-
定时调度更新
-
服务实例状态发生改变:注册、下线、故障
-
初始化时,会给默认值。
EurekaBootStrap
是启动初始化的类,有一行registry.openForTraffic
(开启故障检查)的代码。
// 定位:com.netflix.eureka.EurekaBootStrap.java
protected void initEurekaServerContext() throws Exception {
... ...
// 启动一个定时检查服务实例有没有宕机的后台线程任务
registry.openForTraffic(applicationInfoManager, registryCount);
... ...
}
// 定位:com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.java
@Override
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
// 心跳 30s 一次, 期望每分钟心跳 = 注册数量 * 2
this.expectedNumberOfRenewsPerMin = count * 2;
// 期望每分钟最小心跳 = 期望每分钟心跳 × 2
this.numberOfRenewsPerMinThreshold =
(int) (this.expectedNumberOfRenewsPerMin * 0.85);
... ...
}
这里硬编码 count * 2
不好。如果心跳间隔修改了,那么这块就是个 BUG。
- 定时调度器
// 定位:com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.java
private Timer timer = new Timer(
"ReplicaAwareInstanceRegistry - RenewalThresholdUpdater", true);
private void scheduleRenewalThresholdUpdateTask() {
// 15分钟跑一次
timer.schedule(new TimerTask() {
@Override
public void run() {
updateRenewalThreshold();
}
}, 15 * 60 * 1000, 15 * 60 * 1000);
}
// 操作如下:
private void updateRenewalThreshold() {
try {
// 同步,从其他的 eureka-server 拉取注册表
... ...
synchronized (lock) {
// 根据拉取的注册表,再次计算一次
if ((count * 2) > (serverConfig.getRenewalPercentThreshold() * numberOfRenewsPerMinThreshold)
|| (!this.isSelfPreservationModeEnabled())) {
this.expectedNumberOfRenewsPerMin = count * 2;
this.numberOfRenewsPerMinThreshold = (int) ((count * 2) * serverConfig.getRenewalPercentThreshold());
}
}
... ...
} catch (Throwable e) {
... ...
}
}
- 服务实例状态发生改变:注册、下线、故障
// 1. 注册时
// 定位:com.netflix.eureka.registry.AbstractInstanceRegistry.java
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
... ...
synchronized (lock) {
if (this.expectedNumberOfRenewsPerMin > 0) {
// 一个服务实例注册一次,就 + 2
// 表示 30s 一次心跳,统计的是 1分钟
this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2;
// 期望每分钟心跳数 × 0.85
this.numberOfRenewsPerMinThreshold =
(int) (this.expectedNumberOfRenewsPerMin * 0.85);
}
}
... ...
}
// 2. 下线时
// 定位:com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.java
public boolean cancel(final String appName, final String id,
final boolean isReplication) {
if (super.cancel(appName, id, isReplication)) {
// 同步给其他 server
replicateToPeers(Action.Cancel, appName, id, null, null, isReplication);
synchronized (lock) {
if (this.expectedNumberOfRenewsPerMin > 0) {
// 一个服务下线,就 - 2
this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin - 2;
this.numberOfRenewsPerMinThreshold =
(int) (this.expectedNumberOfRenewsPerMin * 0.85);
}
}
return true;
}
return false;
}
// 3. 故障时
// 故障,没有找到,可能又是一个 BUG
2)实际的上一分钟心跳次数是如何计算的?
心跳方法: renew()
// 定位:com.netflix.eureka.registry.AbstractInstanceRegistry.java
private final MeasuredRate renewsLastMin;
public boolean renew(String appName, String id, boolean isReplication) {
... ...
// 心跳次数 + 1
renewsLastMin.increment();
}
// 定位:com.netflix.eureka.util.MeasuredRate.java
private final AtomicLong lastBucket = new AtomicLong(0); // 上一分钟统计
private final AtomicLong currentBucket = new AtomicLong(0); // 当前分钟统计
this.timer = new Timer("Eureka-MeasureRateTimer", true);
public synchronized void start() {
if (!isActive) {
timer.schedule(new TimerTask() {
@Override
public void run() {
try {
// 每分钟(60s)调度一次
// 保存上一分钟次数,同时更新本次为 0
lastBucket.set(currentBucket.getAndSet(0));
} catch (Throwable e) {
logger.error("Cannot reset the Measured Rate", e);
}
}
}, sampleInterval, sampleInterval);
isActive = true;
}
}
public void increment() {
currentBucket.incrementAndGet();
}
(7)集群机制 - 注册表同步以及高可用
eureka-server
可实现集群高可用:是需要相互注册的,然后相互同步服务实例列表。
不同于主从模式,是
peer to peer
模式。
总图,如下:
注册表同步,时机分为:
eureka-server
初始化时- 有服务注册、下线、心跳时,同步
eureka-server
初始化时
eureka-server
启动时候,会处理 peer
节点,代码如下:
// 定位:com.netflix.eureka.EurekaBootStrap.java
protected void initEurekaServerContext() throws Exception {
... ...
// 第四步: 处理 peer 相关节点
PeerEurekaNodes peerEurekaNodes = getPeerEurekaNodes(
registry,
eurekaServerConfig,
eurekaClient.getEurekaClientConfig(),
serverCodecs,
applicationInfoManager
);
... ...
// 第六步:从相邻的一个 eureka-server 节点拷贝注册表的信息
int registryCount = registry.syncUp();
}
// 定位:com.netflix.eureka.cluster.PeerEurekaNodes.java
// 定时更新 eureka-server 集群信息
public void start() {
... ...
try {
// 配置文件中的 url 来刷新 eureka-server 列表
updatePeerEurekaNodes(resolvePeerUrls());
Runnable peersUpdateTask = new Runnable() {
@Override
public void run() {
try {
updatePeerEurekaNodes(resolvePeerUrls());
} catch (Throwable e) {
logger.error("Cannot update the replica Nodes", e);
}
}
};
// 10 分钟调度一次
private ScheduledExecutorService taskExecutor;
taskExecutor.scheduleWithFixedDelay(
peersUpdateTask,
serverConfig.getPeerEurekaNodesUpdateIntervalMs(),
serverConfig.getPeerEurekaNodesUpdateIntervalMs(),
TimeUnit.MILLISECONDS
);
} catch (Exception e) {
throw new IllegalStateException(e);
}
for (PeerEurekaNode node : peerEurekaNodes) {
logger.info("Replica node URL: " + node.getServiceUrl());
}
}
@Override
public int syncUp() {
// Copy entire entry from neighboring DS node
int count = 0;
for (int i = 0; ((i < serverConfig.getRegistrySyncRetries()) && (count == 0));
i++) {
if (i > 0) {
try {
// 如果第一次没有在自己本地的 eureka-client 中获取注册表
// 说明自己本地的 eureka-client 还没有从任何其他的 eureka-server 上获取注册表
// 所以此时重试,等待 30 秒
Thread.sleep(serverConfig.getRegistrySyncRetryWaitMs());
} catch (InterruptedException e) {
logger.warn("Interrupted during registry transfer..");
break;
}
}
// 拿所有注册表,并处理(注册)
... ...
}
return count;
}
- 有服务注册、下线、心跳、故障时,同步
// 1. 注册
// 定位:com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.java
public void register(final InstanceInfo info, final boolean isReplication) {
... ...
// 注册
super.register(info, leaseDuration, isReplication);
// 向同级同步注册信息
replicateToPeers(Action.Register, info.getAppName(), info.getId(),
info, null, isReplication);
}
// 2. 下线
@Override
public boolean cancel(final String appName, final String id,
final boolean isReplication) {
if (super.cancel(appName, id, isReplication)) {
// 向同级同步注册信息
replicateToPeers(Action.Cancel, appName, id, null, null, isReplication);
... ...
return true;
}
return false;
}
// 3. 心跳
public boolean renew(final String appName, final String id, final boolean isReplication) {
if (super.renew(appName, id, isReplication)) {
// 向同级同步注册信息
replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication);
return true;
}
return false;
}
replicateToPeers()
方法中执行了 action
,如下:
// 定位:com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.java
private void replicateInstanceActionsToPeers(Action action, String appName,
String id, InstanceInfo info, InstanceStatus newStatus, PeerEurekaNode node) {
try {
InstanceInfo infoFromRegistry = null;
CurrentRequestVersion.set(Version.V2);
switch (action) {
case Cancel:
... ...
case Heartbeat:
... ...
case Register:
... ...
case StatusUpdate:
... ...
case DeleteStatusOverride:
... ...
}
} catch (Throwable t) {
... ...
}
}
(8)集群之间注册表同步- 3层队列任务处理机制
eureka-server
同步任务批处理机制。
重点:
- 集群同步的机制:
eureka-client
向eureka-server
发送请求,这个eureka-server
会将请求同步到其他所有的eureka-server
上去,其他的eureka-server
仅仅会在自己本地执行(isReplication
判别),不会再次同步了。 - 数据同步的异步批处理机制:三个队列,第一个队列(
acceptorQueue
):就是纯写入;第二个队列(processingOrder
):是用来根据时间和大小,来拆分队列;第三个队列(batchWorkQueue
):用来放批处理任务(异步批处理机制)
例如,注册,之后会调用 PeerEurekaNode.register(InstanceInfo info)
。
代码如下:
// 定位:com.netflix.eureka.cluster.PeerEurekaNode.java
private final TaskDispatcher<String, ReplicationTask> batchingDispatcher;
// 1. 接收请求,封装成任务
public void register(final InstanceInfo info) throws Exception {
long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
batchingDispatcher.process(
// 封装成任务
... ...
);
}
// 进入 batchingDispatcher.process()
// 定位:com.netflix.eureka.util.batcher.TaskDispatchers.java
// 2. 处理任务
public static <ID, T> TaskDispatcher<ID, T> createBatchingTaskDispatcher(...) {
... ...
return new TaskDispatcher<ID, T>() {
@Override
public void process(ID id, T task, long expiryTime) {
// 接收队列,纯写入
acceptorExecutor.process(id, task, expiryTime);
}
@Override
public void shutdown() {
acceptorExecutor.shutdown();
taskExecutor.shutdown();
}
};
}
// 定位:com.netflix.eureka.util.batcher.AcceptorExecutor.java
// 3. 接收任务,并处理
class AcceptorExecutor<ID, T> {
... ...
// 处理
void process(ID id, T task, long expiryTime) {
// 将任务放入队列
acceptorQueue.add(new TaskHolder<ID, T>(id, task, expiryTime));
acceptedTasks++;
}
... ...
}
// 4. 创建后台线程
class AcceptorExecutor<ID, T> {
private final Thread acceptorThread;
AcceptorExecutor(...) {
// 创建线程
ThreadGroup threadGroup = new ThreadGroup("eurekaTaskExecutors");
this.acceptorThread = new Thread(threadGroup,
new AcceptorRunner(), "TaskAcceptor-" + id);
this.acceptorThread.setDaemon(true);
this.acceptorThread.start();
}
}
// 5. 打成一批来处理,默认 3个为一批
class AcceptorExecutor<ID, T> {
void assignBatchWork() {
... ...
batchWorkQueue.add(holders);
... ...
}
}
// 定位:com.netflix.eureka.cluster.PeerEurekaNode.java
// 6. 处理批量请求
ReplicationTaskProcessor taskProcessor =
new ReplicationTaskProcessor(targetHost, replicationClient);
总结如图: