先明了Eureka做了哪些事情:
- 服務(wù)注冊
- 自我保護
- 心跳檢測
- 狀態(tài)同步
- ...
從這幾件事出發(fā) 看看Eureka如何實現(xiàn)的。
服務(wù)注冊<客戶端注冊>
服務(wù)注冊是由Eureka Client向Eureka Server上報自己的服務(wù)信息,是在服務(wù)啟動的時候就做了。那就從客戶端啟動入手。
但重點是如何找 哪個方法是做服務(wù)注冊的呢?說實話 很迷茫 ,于是從網(wǎng)上找到Spring Cloud的一些規(guī)范,
ServiceRegistry
接口是Spring Cloud定義的服務(wù)注冊的約定。那就從這個接口的實現(xiàn)類著手。也就是EurekaServiceRegistry.register
,那這個方法是哪里調(diào)用的呢?最終找到了org.springframework.cloud.netflix.eureka.serviceregistry.EurekaAutoServiceRegistration#start
。這個方法其實是IOC容器中Bean的生命周期函數(shù)。所以,有兩個入手點,一是客戶端的注解
EnableEurekaClient
或者AutoConfiguration
;一是EurekaAutoServiceRegistration
。聲明一下:在客戶端使用時 不論
EnableEurekaClient
還是EnableDiscoveryClient
都是可選項。且EnableEurekaClient
的注釋明確說了 注解為可選項。因此,第一個入手點僅剩下AutoConfiguration
。
EurekaClientAutoConfiguration
EurekaClientAutoConfiguration
本身就是個配置類,里面全是Bean的定義,下面截取的代碼片段 都是根據(jù)上面的分析而來,將相關(guān)Bean的初始化入口列出來。
// Spring Cloud的規(guī)范實現(xiàn)類
// 約定使用Service Registry注冊和注銷實例。
@Bean
public EurekaServiceRegistry eurekaServiceRegistry() {
return new EurekaServiceRegistry();
}
// 服務(wù)自動注冊
@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled",
matchIfMissing = true)
public EurekaAutoServiceRegistration eurekaAutoServiceRegistration(
ApplicationContext context, EurekaServiceRegistry registry,
EurekaRegistration registration) {
return new EurekaAutoServiceRegistration(context, registry, registration);
}
@Bean(destroyMethod = "shutdown")
@ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT)
@org.springframework.cloud.context.config.annotation.RefreshScope
@Lazy
public EurekaClient eurekaClient(ApplicationInfoManager manager,
EurekaClientConfig config, EurekaInstanceConfig instance,
@Autowired(required = false) HealthCheckHandler healthCheckHandler) {
// If we use the proxy of the ApplicationInfoManager we could run into a
// problem
// when shutdown is called on the CloudEurekaClient where the
// ApplicationInfoManager bean is
// requested but wont be allowed because we are shutting down. To avoid this
// we use the
// object directly.
ApplicationInfoManager appManager;
if (AopUtils.isAopProxy(manager)) {
appManager = ProxyUtils.getTargetObject(manager);
} else {
appManager = manager;
}
CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager,
config, this.optionalArgs, this.context);
cloudEurekaClient.registerHealthCheck(healthCheckHandler);
return cloudEurekaClient;
}
@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled",
matchIfMissing = true)
public EurekaRegistration eurekaRegistration(EurekaClient eurekaClient,
CloudEurekaInstanceConfig instanceConfig,
ApplicationInfoManager applicationInfoManager,
@Autowired(required = false) ObjectProvider<HealthCheckHandler> healthCheckHandler) {
return EurekaRegistration.builder(instanceConfig).with(applicationInfoManager)
.with(eurekaClient).with(healthCheckHandler).build();
}
EurekaAutoServiceRegistration
在
AutoConfiguration
中找到了實例化此類的地方,說明這個類必然是IoC容器中的一個Bean。同時,關(guān)注一下此類的接口實現(xiàn),重點關(guān)注一下SmartLifecycle
。點一下AutoServiceRegistration
,這個其實也算是Spring Cloud提供的一個自動注冊的點。public class EurekaAutoServiceRegistration implements AutoServiceRegistration, SmartLifecycle, Ordered, SmartApplicationListener { .... }
這就表明,在IOC容器啟動時 會回調(diào)里面的生命周期方法,例如
start
、stop
等
// 回調(diào)時觸發(fā)
public void start() {
// only set the port if the nonSecurePort or securePort is 0 and this.port != 0
if (this.port.get() != 0) {
if (this.registration.getNonSecurePort() == 0) {
this.registration.setNonSecurePort(this.port.get());
}
if (this.registration.getSecurePort() == 0 && this.registration.isSecure()) {
this.registration.setSecurePort(this.port.get());
}
}
// only initialize if nonSecurePort is greater than 0 and it isn't already running
// because of containerPortInitializer below
if (!this.running.get() && this.registration.getNonSecurePort() > 0) {
// serviceRegistry = org.springframework.cloud.netflix.eureka.serviceregistry.EurekaServiceRegistry
this.serviceRegistry.register(this.registration);
this.context.publishEvent(new InstanceRegisteredEvent<>(this,
this.registration.getInstanceConfig()));
this.running.set(true);
}
}
EurekaServiceRegistry#register
這里雖然是真正的注冊方法,但是Spring還是進行了一些其他的設(shè)計,是通過發(fā)布事件這種方式來表示狀態(tài)變更的。
@Override
public void register(EurekaRegistration reg) {
maybeInitializeClient(reg);
if (log.isInfoEnabled()) {
log.info("Registering application "
+ reg.getApplicationInfoManager().getInfo().getAppName()
+ " with eureka with status "
+ reg.getInstanceConfig().getInitialStatus());
}
// 這里其實是發(fā)布了狀態(tài)變更的事件
reg.getApplicationInfoManager() // =com.netflix.appinfo.ApplicationInfoManager
.setInstanceStatus(reg.getInstanceConfig().getInitialStatus());
reg.getHealthCheckHandler().ifAvailable(healthCheckHandler -> reg
.getEurekaClient().registerHealthCheck(healthCheckHandler));
}
這里穿插一下com.netflix.appinfo.ApplicationInfoManager.setInstanceStatus
// 設(shè)置此實例的狀態(tài)。應(yīng)用程序可以使用它來指示是否準備接收通信流。在這里設(shè)置狀態(tài)還會將狀態(tài)更改事件通知所有注冊的偵聽器。
public synchronized void setInstanceStatus(InstanceStatus status) {
InstanceStatus next = instanceStatusMapper.map(status);
if (next == null) {return;}
InstanceStatus prev = instanceInfo.setStatus(next);
if (prev != null) {
// listeners是從哪里添加的內(nèi)容呢 看方法-registerStatusChangeListener
for (StatusChangeListener listener : listeners.values()) {
try {
// 狀態(tài)的變更 意味著 有服務(wù)進行注冊或者更新
listener.notify(new StatusChangeEvent(prev, next));
} catch (Exception e) {
logger.warn("failed to notify listener: {}", listener.getId(), e);
}
}
}
}
// 點擊alt+F7 找到調(diào)用這個方法的入口:com.netflix.discovery.DiscoveryClient#initScheduledTasks
public void registerStatusChangeListener(StatusChangeListener listener) {
listeners.put(listener.getId(), listener);
}
DiscoveryClient#initScheduledTasks
在
DiscoveryClient
的構(gòu)造函數(shù)里面調(diào)了initScheduledTasks
,這里就可以找一下哪里對DiscoveryClient
進行了初始化,Spring Cloud這里做了一個巧妙的設(shè)計,它定義了一個類CloudEurekaClient
繼承了DiscoveryClient
,而在EurekaClientAutoConfiguration
里面 初始化了CloudEurekaClient
。
public class CloudEurekaClient extends DiscoveryClient {
....
public CloudEurekaClient(ApplicationInfoManager applicationInfoManager,
EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs<?> args,
ApplicationEventPublisher publisher) {
// 這里調(diào)用了DiscoveryClient的構(gòu)造
super(applicationInfoManager, config, args);
this.applicationInfoManager = applicationInfoManager;
this.publisher = publisher;
this.eurekaTransportField = ReflectionUtils.findField(DiscoveryClient.class,
"eurekaTransport");
ReflectionUtils.makeAccessible(this.eurekaTransportField);
}
....
}
public class DiscoveryClient implements EurekaClient {
....
@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config,
AbstractDiscoveryClientOptionalArgs args,
Provider<BackupRegistry> backupRegistryProvider,
EndpointRandomizer endpointRandomizer) {
// finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch 就在這里了
initScheduledTasks();
}
....
}
梳理前面的類的關(guān)系
到這里就全通了。
首先在AutoConfiguration中實例化了CloudEurekaClient
,從而調(diào)用了DiscoveryClient#initScheduledTasks
,初始化了StatusChangeListener
,同時將其添加到了com.netflix.appinfo.ApplicationInfoManager#listeners
里面。
之后IoC容器初始化完成后回調(diào)所有SmartLifecycle
的實現(xiàn)類的start()
方法,進而到org.springframework.cloud.netflix.eureka.serviceregistry.EurekaServiceRegistry#register
,然后就是com.netflix.appinfo.ApplicationInfoManager#setInstanceStatus
,之后com.netflix.appinfo.ApplicationInfoManager.StatusChangeListener#notify
StatusChangeListener
事件的監(jiān)聽 一個接口 只有一個匿名內(nèi)部類
以下代碼片段來自DiscoveryClient#initScheduledTasks
,是事件監(jiān)聽的一個匿名內(nèi)部類。
statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
@Override
public String getId() {
return "statusChangeListener";
}
@Override
public void notify(StatusChangeEvent statusChangeEvent) {
if (statusChangeEvent.getStatus() == InstanceStatus.DOWN) {
logger.error("Saw local status change event {}", statusChangeEvent);
} else {
logger.info("Saw local status change event {}", statusChangeEvent);
}
// 真正將實例信息更新到注冊中心
instanceInfoReplicator.onDemandUpdate();
}
};
com.netflix.discovery.InstanceInfoReplicator#onDemandUpdate
這個方法其實就是調(diào)用了com.netflix.discovery.InstanceInfoReplicator#run
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);
}
}
com.netflix.discovery.shared.transport.jersey.AbstractJerseyEurekaHttpClient#register
從這里面 只能看出來是通過Post方式請求了
http://{eureka.ip:port}/eureka/apps/{appName}
地址。這樣只需要找一下服務(wù)端是如何暴漏相關(guān)的接口 并如何處理注冊信息的即可。
String urlPath = "apps/" + info.getAppName();
Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
addExtraHeaders(resourceBuilder);
response = resourceBuilder
.header("Accept-Encoding", "gzip")
.type(MediaType.APPLICATION_JSON_TYPE)
.accept(MediaType.APPLICATION_JSON)
.post(ClientResponse.class, info);
服務(wù)注冊<服務(wù)端接收>
Eureka使用的
jersey
框架,所以說 很難像對待Spring Web MVC
的方式找暴漏的接口服務(wù),所以從網(wǎng)上找了一下對應(yīng)的方法。最后即使找到了方式 也沒搞明白是咋樣映射到url的。
ApplicationResource#addInstance
從網(wǎng)上找到的,接收注冊請求的方式是
com.netflix.eureka.resources.ApplicationResource#addInstance
,方法前面都是校驗參數(shù),真正執(zhí)行注冊操作的是registry.register(info, "true".equals(isReplication));
,其中registry=org.springframework.cloud.netflix.eureka.server.InstanceRegistry
看一下關(guān)于InstanceRegistry
的類圖
org.springframework.cloud.netflix.eureka.server.InstanceRegistry#register
public void register(final InstanceInfo info, final boolean isReplication) {
// 等于 publishEvent(new EurekaInstanceRegisteredEvent(this, info, leaseDuration, isReplication));
handleRegistration(info, resolveInstanceLeaseDuration(info), isReplication);
// com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#register
super.register(info, isReplication);
}
com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#register
public void register(final InstanceInfo info, final boolean isReplication) {
// 心跳的超時時間 默認90s
int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
// 如果自定義配置了超時時間 這里重新賦值一下
if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
leaseDuration = info.getLeaseInfo().getDurationInSecs();
}
// com.netflix.eureka.registry.AbstractInstanceRegistry#register
super.register(info, leaseDuration, isReplication);
// 高可用情況下 節(jié)點間的同步復(fù)制
replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}
com.netflix.eureka.registry.AbstractInstanceRegistry#register
存儲節(jié)點信息的容器:ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
外層 KEY = spring.application.name
內(nèi)層 KEY = instanceID
內(nèi)層 VALUE = 實例信息
代碼很長 把logger相關(guān)的內(nèi)容刪掉了
入?yún)?registrant 其實是遠程調(diào)用接口時傳入的 而 registry.get(registrant.getAppName()).get(registrant.getId())
則是本地的副本
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
read.lock();
try {
// registry = ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>
// 根據(jù)spring.application.name獲取服務(wù)的列表<集群情況下才會是列表>
Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
REGISTER.increment(isReplication);
// 這里代表第一次來注冊 實例化一個
if (gMap == null) {
final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
// 說明上一步成功進行了初始化 這里大概是考慮其他節(jié)點插入了同樣的服務(wù)并同步了過來這種情況
if (gMap == null) {
gMap = gNewMap;
}
}
// 本地副本
Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
// Retain the last dirty timestamp without overwriting it, if there is already a lease
// 如果不為空 說明服務(wù)器本地存在副本 這里需要保留下本地臟時間戳 而且不覆蓋它
if (existingLease != null && (existingLease.getHolder() != null)) {
// registry存在的這個臟時間戳
Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
// 遠程傳輸過來 的時間戳
Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
// 只有大于的情況下 使用服務(wù)器本地副本
// 在小于等于時 使用遠程傳輸過來的
if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
registrant = existingLease.getHolder();
}
} else {
// The lease does not exist and hence it is a new registration
synchronized (lock) {
if (this.expectedNumberOfClientsSendingRenews > 0) {
// 這個好像是記錄的客戶端的數(shù)量 提供給心跳續(xù)約那里用的
this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
// 更新一下 每分鐘心跳續(xù)約次數(shù)閾值
// TODO 這個值啥用
updateRenewsPerMinThreshold();
}
}
}
// 重新構(gòu)造一個
Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
// 如果本地副本不為空 那就將重新構(gòu)造出來的更新 服務(wù)啟動的時間
if (existingLease != null) {
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
// 這一步就已經(jīng)將服務(wù)信息加入到了注冊中心了
gMap.put(registrant.getId(), lease);
// 這一步?jīng)]啥用 recentRegisteredQueue好像是提供給Eureka的控制臺用的
recentRegisteredQueue.add(new Pair<Long, String>(
System.currentTimeMillis(),
registrant.getAppName() + "(" + registrant.getId() + ")"));
// 下面開始處理狀態(tài)的問題 不知道是干啥的
if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
}
}
InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
if (overriddenStatusFromMap != null) {
registrant.setOverriddenStatus(overriddenStatusFromMap);
}
// Set the status based on the overridden status rules
InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
registrant.setStatusWithoutDirty(overriddenInstanceStatus);
// If the lease is registered with UP status, set lease service up timestamp
if (InstanceStatus.UP.equals(registrant.getStatus())) {
// 這一步等于 serviceUpTimestamp = System.currentTimeMillis();
lease.serviceUp();
}
registrant.setActionType(ActionType.ADDED);
// 這個應(yīng)該是被 readWriteCacheMap 用的
recentlyChangedQueue.add(new RecentlyChangedItem(lease));
registrant.setLastUpdatedTimestamp();
// 更新緩存 也是更新 readWriteCacheMap 里面的內(nèi)容
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
} finally {
read.unlock();
}
}
服務(wù)信息緩存
未能保證多級緩存的一致性
com.netflix.eureka.registry.ResponseCacheImpl
public class ResponseCacheImpl implements ResponseCache {
...
private final ConcurrentMap<Key, Value> readOnlyCacheMap = new ConcurrentHashMap<Key,Value>();
// guava的緩存
private final LoadingCache<Key, Value> readWriteCacheMap;
ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs,
AbstractInstanceRegistry registry) {
....
this.readWriteCacheMap= CacheBuilder.newBuilder()
.initialCapacity(serverConfig.getInitialCapacityOfResponseCache())
// 默認180s
.expireAfterWrite(serverConfig.getResponseCacheAutoExpirationInSeconds(),
TimeUnit.SECONDS)
.removalListener(new RemovalListener<Key, Value>() {
@Override
// readWriteCacheMap.invalidate的時候 回調(diào)這個方法
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
// readWriteCacheMap.get的時候 回調(diào)這個方法
public Value load(Key key) throws Exception {
if (key.hasRegions()) {
Key cloneWithNoRegions = key.cloneWithoutRegions();
regionSpecificKeys.put(cloneWithNoRegions, key);
}
Value value = generatePayload(key);
return value;
}
});
// 定時任務(wù) 更新只讀緩存 默認30s
// responseCacheUpdateIntervalMs = eureka.server.responseCacheUpdateIntervalMs
if (shouldUseReadOnlyResponseCache) {
timer.schedule(getCacheUpdateTask(),
new Date(
(
(System.currentTimeMillis() / responseCacheUpdateIntervalMs)
* responseCacheUpdateIntervalMs
) + responseCacheUpdateIntervalMs),
responseCacheUpdateIntervalMs);
}
try {
Monitors.registerObject(this);
} catch (Throwable e) {
logger.warn("Cannot register the JMX monitor for the InstanceRegistry", e);
}
}
....
}
心跳續(xù)約<客戶端發(fā)送>
入口
心跳只可能在注冊之后開始,所以在看注冊相關(guān)的代碼的時候可以留意一下有關(guān)
heartbeat
相關(guān)的內(nèi)容。這里就提兩個點
- DiscoveryClient#DiscoveryClient
- DiscoveryClient#initScheduledTasks
這兩個 都有和心跳相關(guān)的線程池 或者 定時任務(wù),那就從這里著手
DiscoveryClient#DiscoveryClient
構(gòu)造方法種創(chuàng)建了一個線程池
heartbeatExecutor = new ThreadPoolExecutor(
1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
.setDaemon(true)
.build()
);
DiscoveryClient#initScheduledTasks
這里構(gòu)建了一個Task 然后最終走到了com.netflix.discovery.TimedSupervisorTask#run
,這個方法其實就是將HeartbeatThread
作為一個task 交由 heartbeatExecutor里面執(zhí)行
heartbeatTask = new TimedSupervisorTask(
"heartbeat",
scheduler,
heartbeatExecutor,
renewalIntervalInSecs,
TimeUnit.SECONDS,
expBackOffBound,
new HeartbeatThread()
);
// 線程調(diào)度
scheduler.schedule(heartbeatTask, renewalIntervalInSecs, TimeUnit.SECONDS);
有個小疑問:
為啥不直接heartbeatExecutor.submit(new HeartbeatThread())
,為啥要費勁弄一個TimedSupervisorTask
類 然后使用 ScheduledExecutorService.schedule
呢?
其實看一眼邏輯 無非就是處理了一些異常情況
DiscoveryClient.HeartbeatThread
其實最終還是起一個HeartbeatThread
的線程
private class HeartbeatThread implements Runnable {
public void run() {
if (renew()) {
lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
}
}
}
DiscoveryClient#renew
這里無非還是一個遠程調(diào)用而已
EurekaHttpResponse<InstanceInfo> httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
com.netflix.discovery.shared.transport.jersey.AbstractJerseyEurekaHttpClient#sendHeartBeat
這里只傳入了狀態(tài)和臟時間戳
String urlPath = "apps/" + appName + '/' + id;
WebResource webResource = jerseyClient.resource(serviceUrl)
.path(urlPath)
.queryParam("status", info.getStatus().toString())
.queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString());
if (overriddenStatus != null) {
webResource = webResource.queryParam("overriddenstatus", overriddenStatus.name());
}
Builder requestBuilder = webResource.getRequestBuilder();
addExtraHeaders(requestBuilder);
ClientResponse response = requestBuilder.put(ClientResponse.class);
心跳續(xù)約<服務(wù)端接收>
InstanceResource#renewLease
服務(wù)端接收請求的地方 核心代碼就是 registry.renew(app.getName(), id, isFromReplicaNode);
org.springframework.cloud.netflix.eureka.server.InstanceRegistry#renew
public boolean renew(final String appName, final String serverId, boolean isReplication) {
List<Application> applications = getSortedApplications();
for (Application input : applications) {
if (input.getName().equals(appName)) {
InstanceInfo instance = null;
for (InstanceInfo info : input.getInstances()) {
if (info.getId().equals(serverId)) {
instance = info;
break;
}
}
// 發(fā)布事件
publishEvent(new EurekaInstanceRenewedEvent(this, appName, serverId,
instance, isReplication));
break;
}
}
// com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#renew
return super.renew(appName, serverId, isReplication);
}
com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#renew
public boolean renew(final String appName, final String id, final boolean isReplication) {
//
if (super.renew(appName, id, isReplication)) {
// 同步其他節(jié)點
replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication);
return true;
}
return false;
}
com.netflix.eureka.registry.AbstractInstanceRegistry#renew
public boolean renew(String appName, String id, boolean isReplication) {
RENEW.increment(isReplication);
Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
Lease<InstanceInfo> leaseToRenew = null;
if (gMap != null) {
leaseToRenew = gMap.get(id);
}
// lease doesn't exist
if (leaseToRenew == null) {
RENEW_NOT_FOUND.increment(isReplication);
return false;
} else {
InstanceInfo instanceInfo = leaseToRenew.getHolder();
if (instanceInfo != null) {
// touchASGCache(instanceInfo.getASGName());
// 感覺這個方法比較重要
InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(
instanceInfo, leaseToRenew, isReplication);
if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
RENEW_NOT_FOUND.increment(isReplication);
return false;
}
if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
}
}
// 計數(shù)器遞增
renewsLastMin.increment();
// 更新 最后更新時間戳
leaseToRenew.renew();
return true;
}
}
自我保護機制
自我保護機制的觸發(fā)條件:在15分鐘內(nèi)超過85%的客戶端都沒有正常的心跳,那么Eureka就認為客戶端與注冊中心之間出現(xiàn)了網(wǎng)絡(luò)故障,Eureka Server自動進入自我保護機制。
進入自我保護機制后,Eureka Server會做以下的事情:
- Eureka Server不再剔除注冊表中失去心跳連接的服務(wù)
- Eureka Server可以繼續(xù)接受新服務(wù)的注冊和查詢請求,但不會將信息同步到其他節(jié)點中,保證當前節(jié)點依然可用
- 當網(wǎng)絡(luò)穩(wěn)定后,當前Eureka Server的新注冊信息才會被同步到其他節(jié)點中
EurekaServerBootstrap
這個類以及其中方法的由來:
- 在
EurekaServerAutoConfiguration
中進行了實例化 注入了IoC容器EurekaServerBootstrap#contextInitialized
這個方法是用來做自我保護機制的contextInitialized
被EurekaServerInitializerConfiguration#start
調(diào)用
調(diào)用鏈:org.springframework.cloud.netflix.eureka.server.EurekaServerBootstrap#contextInitialized
org.springframework.cloud.netflix.eureka.server.EurekaServerBootstrap#initEurekaServerContext
com.netflix.eureka.registry.InstanceRegistry#openForTraffic
InstanceRegistry#openForTraffic
單純調(diào)用了父類的方法 com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#openForTraffic
@Override
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
// Renewals happen every 30 seconds and for a minute it should be a factor of 2.
this.expectedNumberOfClientsSendingRenews = count;
// 更新每分鐘的續(xù)約閾值
updateRenewsPerMinThreshold();
logger.info("Got {} instances from neighboring DS node", count);
logger.info("Renew threshold is: {}", numberOfRenewsPerMinThreshold);
this.startupTime = System.currentTimeMillis();
if (count > 0) {
this.peerInstancesTransferEmptyOnStartup = false;
}
DataCenterInfo.Name selfName = applicationInfoManager.getInfo().getDataCenterInfo().getName();
boolean isAws = Name.Amazon == selfName;
if (isAws && serverConfig.shouldPrimeAwsReplicaConnections()) {
logger.info("Priming AWS connections for all replicas..");
primeAwsReplicas(applicationInfoManager);
}
logger.info("Changing status to UP");
applicationInfoManager.setInstanceStatus(InstanceStatus.UP);
// 重點在這里
super.postInit();
}
com.netflix.eureka.registry.AbstractInstanceRegistry#updateRenewsPerMinThreshold
expectedNumberOfClientsSendingRenews 這個值是預(yù)期的 有幾個客戶端就是幾個
serverConfig.getExpectedClientRenewalIntervalSeconds() 默認等于 30
serverConfig.getRenewalPercentThreshold() 默認等于 0.85
protected void updateRenewsPerMinThreshold() {
this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfClientsSendingRenews
* (60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds())
* serverConfig.getRenewalPercentThreshold());
}
AbstractInstanceRegistry#postInit
protected void postInit() {
// 開啟一個定時任務(wù) 統(tǒng)計 每分鐘 心跳續(xù)約的次數(shù)
renewsLastMin.start();
if (evictionTaskRef.get() != null) {
evictionTaskRef.get().cancel();
}
// 定時器的定時任務(wù)提交 每60s執(zhí)行一次 serverConfig.getEvictionIntervalTimerInMs()=60*1000
evictionTaskRef.set(new EvictionTask());
evictionTimer.schedule(evictionTaskRef.get(), serverConfig.getEvictionIntervalTimerInMs(),
serverConfig.getEvictionIntervalTimerInMs());
}
EvictionTask里面的run方法 最終調(diào)用了com.netflix.eureka.registry.AbstractInstanceRegistry#evict(long)
AbstractInstanceRegistry#evict
public void evict(long additionalLeaseMs) {
// 判斷是否需要開啟自我保護
if (!isLeaseExpirationEnabled()) {
logger.debug("DS: lease expiration is currently disabled.");
return;
}
// 先收集所有的過期項,以隨機的順序驅(qū)逐他們。
// 對于大規(guī)模的驅(qū)逐項集合,如果我們不隨機驅(qū)逐,我們可能會在自我保護開始之前把整個應(yīng)用程序抹去。
// 通過隨機化,影響應(yīng)該均勻分布在所有應(yīng)用程序中。
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();
if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
expiredLeases.add(lease);
}
}
}
}
// 判斷是不是 超過85%的客戶端都沒有正常的心跳
int registrySize = (int) getLocalRegistrySize();
int registrySizeThreshold = registrySize * serverConfig.getRenewalPercentThreshold();
int evictionLimit = registrySize - registrySizeThreshold;
int toEvict = Math.min(expiredLeases.size(), evictionLimit);
if (toEvict > 0) {
Random random = new Random(System.currentTimeMillis());
for (int i = 0; i < toEvict; i++) {
// Pick a random item (Knuth shuffle algorithm)
int next = i + random.nextInt(expiredLeases.size() - i);
Collections.swap(expiredLeases, i, next);
Lease<InstanceInfo> lease = expiredLeases.get(i);
String appName = lease.getHolder().getAppName();
String id = lease.getHolder().getId();
EXPIRED.increment();
logger.warn("DS: Registry: expired lease for {}/{}", appName, id);
// 下線操作
internalCancel(appName, id, false);
}
}
}
客戶端拉取服務(wù)節(jié)點信息
客戶端的配置
eureka.client.fetch-registry
在默認的情況下是true,這個配置表示是否從注冊中心拉取服務(wù)信息。在構(gòu)造方法DiscoveryClient#DiscoveryClient里面有一段判斷這個配置的內(nèi)容:if (clientConfig.shouldFetchRegistry()) { try { // 這里就是拉取的入口 boolean primaryFetchRegistryResult = fetchRegistry(false); .... } catch (Throwable th) { throw new IllegalStateException(th); } }
private boolean fetchRegistry(boolean forceFullRegistryFetch) {
Stopwatch tracer = FETCH_REGISTRY_TIMER.start();
try {
// 如果增量被禁用 或者是 第一次來 那就執(zhí)行這個
Applications applications = getApplications();
if (clientConfig.shouldDisableDelta()
|| (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress()))
|| forceFullRegistryFetch
|| (applications == null)
|| (applications.getRegisteredApplications().size() == 0)
|| (applications.getVersion() == -1)) //Client application does not have latest library supporting delta
{
// 全量拉取 調(diào)用接口 apps/
getAndStoreFullRegistry();
} else {
// 增量拉取 調(diào)用接口 apps/delta
getAndUpdateDelta(applications);
}
applications.setAppsHashCode(applications.getReconcileHashCode());
logTotalInstances();
} catch (Throwable e) {
return false;
} finally {
if (tracer != null) {
tracer.stop();
}
}
// Notify about cache refresh before updating the instance remote status
onCacheRefreshed();
// Update remote status based on refreshed data held in the cache
updateInstanceRemoteStatus();
// registry was fetched successfully, so return true
return true;
}
客戶端保存服務(wù)節(jié)點信息
在上一步 已經(jīng)將服務(wù)信息通過遠程通信的方式請求而來,如何保存 其實就是如何處理響應(yīng)數(shù)據(jù)的問題。
這部分的代碼不想看了。
節(jié)點間數(shù)據(jù)同步
在上面的章節(jié) 注冊,有一點說了以下節(jié)點間的同步
PeerAwareInstanceRegistryImpl#replicateToPeers
與Ribbon的整合
Ribbon獲取服務(wù)列表的地方是
com.netflix.loadbalancer.DynamicServerListLoadBalancer#updateListOfServers
,在這個方法里面有一個ServerList.getUpdatedListOfServers()
,如果是從配置文件的話,那么子類是ConfigurationBasedServerList
,現(xiàn)在換成了Eureka,那子類是org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList
。
DomainExtractingServerList#getUpdatedListOfServers
public List<DiscoveryEnabledServer> getUpdatedListOfServers() {
// this.list = com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList
List<DiscoveryEnabledServer> servers = setZones(
this.list.getUpdatedListOfServers());
return servers;
}
DiscoveryEnabledNIWSServerList#getUpdatedListOfServers
public List<DiscoveryEnabledServer> getUpdatedListOfServers(){
return obtainServersViaDiscovery();
}
private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>();
if (eurekaClientProvider == null || eurekaClientProvider.get() == null) {
logger.warn("EurekaClient has not been initialized yet, returning an empty list");
return new ArrayList<DiscoveryEnabledServer>();
}
EurekaClient eurekaClient = eurekaClientProvider.get();
if (vipAddresses!=null){
for (String vipAddress : vipAddresses.split(",")) {
// if targetRegion is null, it will be interpreted as the same region of client
List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
for (InstanceInfo ii : listOfInstanceInfo) {
if (ii.getStatus().equals(InstanceStatus.UP)) {
if(shouldUseOverridePort){
if(logger.isDebugEnabled()){
logger.debug("Overriding port on client name: " + clientName + " to " + overridePort);
}
// copy is necessary since the InstanceInfo builder just uses the original reference,
// and we don't want to corrupt the global eureka copy of the object which may be
// used by other clients in our system
InstanceInfo copy = new InstanceInfo(ii);
if(isSecure){
ii = new InstanceInfo.Builder(copy).setSecurePort(overridePort).build();
}else{
ii = new InstanceInfo.Builder(copy).setPort(overridePort).build();
}
}
DiscoveryEnabledServer des = createServer(ii, isSecure, shouldUseIpAddr);
serverList.add(des);
}
}
if (serverList.size()>0 && prioritizeVipAddressBasedServers){
break; // if the current vipAddress has servers, we dont use subsequent vipAddress based servers
}
}
}
return serverList;
}
Eureka Server小記
服務(wù)端的搭建過程很簡單,除了引入依賴與yml文件的配置之外,就是一個Spring Boot的項目,在啟動類上標注了
@EnableEurekaServer
,就完成了。因此入手點從這個注解開始。
@EnableEurekaServer
這個注解設(shè)計的極為巧妙,通過注釋也可以看出來,他的作用就是激活這個Eureka Server的配置的;但是他是怎么激活的呢?重點就在
@Import(EurekaServerMarkerConfiguration.class)
導(dǎo)入的這個配置也極其簡單,聲明了一個Marker的Bean。這里就很奇怪了,憑什么一個簡單的Marker就能激活配置呢?在Spring自己實現(xiàn)的類SPI的機制下,一個類配置在了
spring.factories
中時,Spring 啟動時就可以掃描到這里面配置的類,也就是EurekaServerAutoConfiguration
。但是EurekaServerAutoConfiguration
上的條件就是當前環(huán)境中必須有Marker這個Bean。這樣@EnableEurekaServer
就可以實現(xiàn)他注釋所說的了。為什么會這樣設(shè)計呢?個人理解,可能就是為了避免錯誤的引入依賴而導(dǎo)致項目加載很多不必要的類吧。
/**
* Annotation to activate Eureka Server related configuration.
* {@link EurekaServerAutoConfiguration}
* 用以 <激活 Eureka Server 相關(guān)的配置> 的注解
*/
@Import(EurekaServerMarkerConfiguration.class)
public @interface EnableEurekaServer {
}
/**
* Responsible for adding in a marker bean to activate
* {@link EurekaServerAutoConfiguration}.
* 負責添加一個標記Bean 來激活 配置類EurekaServerAutoConfiguration
*/
@Configuration(proxyBeanMethods = false)
public class EurekaServerMarkerConfiguration {
@Bean
public Marker eurekaServerMarkerBean() {
return new Marker();
}
class Marker {}
}
// 重點就是這個條件
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
public class EurekaServerAutoConfiguration implements WebMvcConfigurer {
}
控制臺入口 EurekaController
這個是Spring提供的控制臺接口,按照Spring Web MVC的方式定義的。還是比較容易看懂。
http://localhost:8761
相關(guān)接口