本文已参与周末学习计划,点击链接查看详情 链接
一、前言
看源码:抓大放小,先主流程,再细枝末节。
用技巧连蒙带猜:
- 看方法名(英文名)
- 看注释
- 从单元测试入手
搭建环境:
// 定位:eureka-examples 模块下
// 修改 ExampleEurekaClient.java
// 1. 增加一个方法,用于初始化环境变量,方便调试
private static void injectEurekaConfiguration() throws UnknownHostException {
String myHostName = InetAddress.getLocalHost().getHostName();
String myServiceUrl = "http://" + myHostName + ":8080/v2/";
System.setProperty("eureka.region", "default");
System.setProperty("eureka.name", "eureka");
System.setProperty("eureka.vipAddress", "eureka.mydomain.net");
System.setProperty("eureka.port", "8080");
System.setProperty("eureka.preferSameZone", "false");
System.setProperty("eureka.shouldUseDns", "false");
System.setProperty("eureka.shouldFetchRegistry", "false");
System.setProperty("eureka.serviceUrl.defaultZone", myServiceUrl);
System.setProperty("eureka.serviceUrl.default.defaultZone", myServiceUrl);
System.setProperty("eureka.awsAccessId", "fake_aws_access_id");
System.setProperty("eureka.awsSecretKey", "fake_aws_secret_key");
System.setProperty("eureka.numberRegistrySyncRetries", "0");
}
// 2. 在 main 方法添加
public static void main(String[] args) throws UnknownHostException {
// 添加如下这行
injectEurekaConfiguration();
... ...
}
二、从源码中学到了什么
凡凡觉得 “学到了” 的东西。
Tips
:方法名、变量名日常值得学习。
(1)锁的使用
锁的使用有:
- 读写锁
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");
}
(2)队列使用
队列有:
-
最近修改队列:
ConcurrentLinkedQueue
private ConcurrentLinkedQueue<RecentlyChangedItem> recentlyChangedQueue = new ConcurrentLinkedQueue<RecentlyChangedItem>();
-
最近取消队列:
CircularQueue
private final CircularQueue<Pair<Long, String>> recentCanceledQueue = new CircularQueue<Pair<Long, String>>(1000); // 要并发使用,要加锁,如下: synchronized (recentCanceledQueue) { // 在最近取消队列中,把这个事件加入 recentCanceledQueue.add( new Pair<Long, String>(System.currentTimeMillis(), appName + "(" + id + ")")); }
三、直接怼源码
(1)eureka-client
如何启动(初始化)?
代码如下:
// 定位:com.netflix.eureka.ExampleEurekaClient.java
public static void main(String[] args) throws UnknownHostException {
// 1. 初始化 eureka 环境变量
injectEurekaConfiguration();
// 2. 创建 eureka 服务
ExampleEurekaClient sampleClient = new ExampleEurekaClient();
// 3. 创建服务实例管理器
ApplicationInfoManager applicationInfoManager =
initializeApplicationInfoManager(new MyDataCenterInstanceConfig());
// 4. 创建 eureka-client
EurekaClient client =
initializeEurekaClient(applicationInfoManager, new DefaultEurekaClientConfig());
... ...
}
过程如下:
-
初始化
eureka
环境变量 -
创建
eureka
服务,会有一个eureka-client
-
创建服务实例管理器
- 构建服务实例(
InstanceInfo
) - 构建服务实例管理器(
ApplicationInfoManager
)
ApplicationInfoManager applicationInfoManager = initializeApplicationInfoManager(new MyDataCenterInstanceConfig());
- 构建服务实例(
-
创建
eureka-client
(通过构造DiscoveryClient
)- 处理配置
- 服务的注册和注册表的抓取(初始化网络通信组件)
- 创建几个线程池,启动调度任务
- 注册监控项
(2)eureka-client
如何服务注册的?
针对注册,提出问题:
- 什么时候进行服务注册? 初始化
eureka-client
时候
eureka-client
的服务注册,是在InstanceInfoReplicator
中完成的。
- 服务注册做哪些操作? 主要发送
HTTP
请求
针对这个两个问题,来看下源码。虽然这部分的源码写的不好,但也可以学习了解下他人的思路。
这部分源码比较难找,实际是在创建 DiscoveryClient
的 initScheduledTasks()
(初始化调度任务)
// 定位:com.netflix.discovery.DiscoveryClient.java
private void initScheduledTasks() {
// InstanceRegisterManager:实例注册管理器,专门来管理实例注册
// 传参:默认 40秒
instanceInfoReplicator.start(...);
}
- 启动实例注册管理器(
InstanceRegisterManager
)
// 定位:com.netflix.discovery.InstanceInfoReplicator.java
public void start(int initialDelayMs) {
// 原子操作
if (started.compareAndSet(false, true)) {
// 1. 先设置 isDirty = true
instanceInfo.setIsDirty();
// 2. 调度器执行,将自身传入,并且调度时间默认为 40 秒
// 所以会执行 InstanceInfoReplicator.run() 方法
Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
- 执行
InstanceInfoReplicator.run()
// 定位:com.netflix.discovery.InstanceInfoReplicator.java
// 会发现:class InstanceInfoReplicator implements Runnable,是可创建线程的。
public void run() {
try {
// 1. 刷新了服务实例的信息,拿到服务的状态
discoveryClient.refreshInstanceInfo();
Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
if (dirtyTimestamp != null) {
// 2. 注册:因为之前已经设置 isDirty = true,所以下面直接注册
discoveryClient.register();
// 3. 设置 isDirty = false
instanceInfo.unsetIsDirty(dirtyTimestamp);
}
} catch (Throwable t) {
logger.warn("There was a problem with the instance info replicator", t);
} finally {
// 4. 再次把自己丢进调度线程中
Future next =
scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
- 重要:注册
discoveryClient.register();
// 定位:com.netflix.discovery.DiscoveryClient.java
boolean register() throws Throwable {
EurekaHttpResponse<Void> httpResponse;
try {
// 发送 HTTP 请求
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
... ...
}
... ...
return httpResponse.getStatusCode() == 204;
}
(3)eureka-client
第一次启动全量抓取注册表
// 定位:com.netflix.discovery.DiscoveryClient.java
if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
fetchRegistryFromBackup();
}
private boolean fetchRegistry(boolean forceFullRegistryFetch) {
... ...
try {
// 第一次获取,为 null
Applications applications = getApplications();
if (...) {
// applications == null 时,
// 抓取全量注册表,并存储
getAndStoreFullRegistry();
} else {
// 抓取增量注册表,并更新
getAndUpdateDelta(applications);
}
... ...
} catch (Throwable e) {
... ...
}
... ...
}
在启动的时候抓取全量的注册表信息,步骤如下:
eureka-client
初始化的时候,会自动全量抓取注册表- 先获取本地的
Applications
缓存(即,所有服务信息,Application
对应一个InstanceInfo
) - 发送
HTTP
请求(GET http://localhost:8080/v2/apps
),获取全量注册表信息并缓存到本地
(4)eureka-client
增量抓取注册表
在初始化
eureka-client
时候,会全量抓取注册表; 同时会起一个定时任务,增量抓取注册表。
增量抓取注册表,定时任务如下:
// 定位:com.netflix.discovery.DiscoveryClient.java
// 1. 调度器
private void initScheduledTasks() {
... ...
// 调度间隔默认 30s,执行 CacheRefreshThread()
scheduler.schedule(
new TimedSupervisorTask(
"cacheRefresh",
scheduler,
cacheRefreshExecutor,
30,
TimeUnit.SECONDS,
expBackOffBound,
new CacheRefreshThread() // 重要:执行方法
),
30, TimeUnit.SECONDS);
}
// 2. 发现 CacheRefreshThread() 直接调用 refreshRegistry()
class CacheRefreshThread implements Runnable {
public void run() {
refreshRegistry();
}
}
// 3. 进入 refreshRegistry()
void refreshRegistry() {
... ...
// 直接调用 fetchRegistry()
boolean success = fetchRegistry(remoteRegionsModified);
... ...
}
// 4. 进入 fetchRegistry()
private boolean fetchRegistry(boolean forceFullRegistryFetch) {
... ...
try {
// 第一次获取,为 null
Applications applications = getApplications();
if (...) {
// applications == null 时,
// 抓取全量注册表,并存储
getAndStoreFullRegistry();
} else {
// 重要:抓取增量注册表,并更新
getAndUpdateDelta(applications);
}
... ...
} catch (Throwable e) {
... ...
}
... ...
}
重点:抓取增量注册表 getAndUpdateDelta(applications);
// 定位:
private void getAndUpdateDelta(Applications applications) throws Throwable {
... ...
Applications delta = null;
// HTTP请求,拿到增量的注册表
EurekaHttpResponse<Applications> httpResponse =
eurekaTransport.queryClient.getDelta(remoteRegionsRef.get());
// 如果 HTTP 状态OK,说明请求成功
if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
// 赋值
delta = httpResponse.getEntity();
}
... ...
if (delta == null) {
// 增量为 null,则去拿全量
getAndStoreFullRegistry();
} else if (比较版本并设置)) {
String reconcileHashCode = "";
if (fetchRegistryUpdateLock.tryLock()) {
try {
// 将抓取到的注册表和本地缓存的注册表进行合并,更新操作
updateDelta(delta);
// 并计算 Hash 值
reconcileHashCode = getReconcileHashCode(applications);
} finally {
fetchRegistryUpdateLock.unlock();
}
} else {
logger.warn("Cannot acquire update lock, aborting getAndUpdateDelta");
}
// 本地注册表的 Hash值 与 远端 Hash值比较,若不一致:
if (!reconcileHashCode.equals(delta.getAppsHashCode())
|| clientConfig.shouldLogDeltaDiff()) {
// 从 eureka-server 抓取全量的注册表到本地
reconcileAndLogDifference(delta, reconcileHashCode);
}
} else {
// 打异常日志
... ...
}
}
总结,如图:
(5)服务实例与注册中心的心跳机制(服务续约)
eureka-client
每隔一定的时间,会给 eureka-server
发送心跳, 告诉服务端 “我健康着呢!!”
心跳(heartbeat
),在代码中等同于 续约(renew lease
)
创建 eureka-client
初始化时,初始化调度器,代码分析如下:
// 定位:com.netflix.discovery.DiscoveryClient.java
// 1. 调度器
private void initScheduledTasks() {
... ...
// 默认 30s:发送一次心跳
scheduler.schedule(
new TimedSupervisorTask(
"heartbeat",
scheduler,
heartbeatExecutor,
renewalIntervalInSecs,
TimeUnit.SECONDS,
expBackOffBound,
new HeartbeatThread() // 重要
),
30, TimeUnit.SECONDS);
... ...
}
// 2. 执行 HeartbeatThread()
private class HeartbeatThread implements Runnable {
public void run() {
// 续约
if (renew()) {
lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
}
}
}
// 3. renew()
boolean renew() {
try {
// 发送 HTTP 请求:PUT apps/{appName}/{id}
EurekaHttpResponse<InstanceInfo> httpResponse = ... ...
// 404 则表示没注册上,需要重新去注册
if (httpResponse.getStatusCode() == 404) {
... ...
// 去调用注册
boolean success = register();
... ...
return success;
}
return httpResponse.getStatusCode() == 200;
} catch (Throwable e) {
... ...
}
}
eureka-server
接收到 eureka-client
的heartbeat
,且找到这个实例,则会进行:
Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
- 执行续约
leaseToRenew.renew();
- 更性上次更新的时间戳(
lastUpdateTimestamp
)
(6)停止服务实例
服务下线,就会调用 shutdown()
方法。
eureka-client
停止服务,代码如下:
// 定位:com.netflix.discovery.DiscoveryClient.java
public synchronized void shutdown() {
//
if (isShutdown.compareAndSet(false, true)) {
... ...
// 1. 取消各种调度任务
cancelScheduledTasks();
// If APPINFO was registered
if (applicationInfoManager != null && clientConfig.shouldRegisterWithEureka()) {
// 设置状态
applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
// 2. HTTP 请求,DELETE apps/{appName}/{id}
unregister();
}
// 3. 网络组件关闭
if (eurekaTransport != null) {
eurekaTransport.shutdown();
}
// 4. 监听器等关闭
heartbeatStalenessMonitor.shutdown();
registryStalenessMonitor.shutdown();
}
}
eureka-server
端处理,步骤如下:
- 接收到
HTTP
下线请求,执行InstanceResource.cancelLease()
方法 - 调用
AbstractInstanceRegistry.internalCancel()
方法
protected boolean internalCancel(String appName, String id, boolean isReplication) {
try {
read.lock();
CANCEL.increment(isReplication);
// 1. 拿到注册表中对应的 map
Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
Lease<InstanceInfo> leaseToCancel = null;
if (gMap != null) {
// 2. 从注册表中移除
leaseToCancel = gMap.remove(id);
}
// 3. 同步,最近取消队列
synchronized (recentCanceledQueue) {
// 3.1. 在最近取消队列中,把这个事件加入
recentCanceledQueue.add(
new Pair<Long, String>(System.currentTimeMillis(), appName + "(" + id + ")"));
}
// 其他 map 中移除
... ...
if (leaseToCancel == null) {
... ...
} else {
// 4. 续约取消
leaseToCancel.cancel();
InstanceInfo instanceInfo = leaseToCancel.getHolder();
String vip = null;
String svip = null;
if (instanceInfo != null) {
... ...
// 5. 最近修改队列:添加服务实例变更记录,保留3分钟
recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel));
instanceInfo.setLastUpdatedTimestamp();
vip = instanceInfo.getVIPAddress();
svip = instanceInfo.getSecureVipAddress();
}
// 6. 让缓存失效,即 readWriteCacheMap 里全部清理掉
// 有定时任务每隔30秒让 readOnlyCacheMap 和 readWriteCacheMap 进行同步操作
invalidateCache(appName, vip, svip);
return true;
}
} finally {
read.unlock();
}
}
之后 eureka-client
拉取增量注册表的时候:
eureka-server
就会返回recentlyChangedQueue
里的数据eureka-client
接收数据,并进行本地注册表合并,再计算Hash
值eureka-client
对比server
端的Hash
值,再决定是否重新拉取全量注册表
服务下线,总结如图: