Eureka服务集群以及原理
服务注册
提供服务ip地址,端点等各项服务发现基本信息
服务取消
客户端连续一段时间没有向Eureka服务端发送服务续约消息,就会将其从服务列表剔除
Eureka服务存储和缓存
在Eureka,InstanceRegistry接口及其实现类实现这部分职能
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
保存得数据结构是一个双层得HashMap,采用得是线程安全得ConcurrentHashMap.
第一层得key为spring.application.name(服务名),value为ConcurrentHashMap;第二层得ConcurrentHash得key为instanceId,服务实例id,value为Lease(租约)对象。
public interface LeaseManager<T> {
void register(T r, int leaseDuration, boolean isReplication);
boolean cancel(String appName, String id, boolean isReplication);
boolean renew(String appName, String id, boolean isReplication);
void evict();
}
里面就包括了注册,服务续约,服务取消和服务剔除操作。
LoookupService接口对应应用程序于服务实例得管理
public interface LookupService<T> {
Application getApplication(String appName);
Applications getApplications();
List<InstanceInfo> getInstancesById(String id);
InstanceInfo getNextServerFromEureka(String virtualHostname, boolean secure);
}
针对register方法来分析
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
//从已存储的 registry 获取一个服务定义
Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
REGISTER.increment(isReplication);
if (gMap == null) {
//初始化一个 Map<String, Lease<InstanceInfo>> ,并放入 registry 中
}
//根据当前注册的 ID 找到对应的 Lease
Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
if (existingLease != null && (existingLease.getHolder() != null)) {
//如果 Lease 能找到,根据当前节点的最新更新时间和注册节点的最新更新时间比较
//如果前者的时间晚于后者的时间,那么注册实例就以已存在的实例为准
} else {
//如果找不到,代表是一个新注册,则更新其每分钟期望的续约数量及其阈值
}
//创建一个新 Lease 并放入 Map 中
Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
gMap.put(registrant.getId(), lease);
//处理服务的 InstanceStatus
registrant.setActionType(ActionType.ADDED);
//更新服务最新更新时间
registrant.setLastUpdatedTimestamp();
//刷选缓存
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
}
}
cancle和renew方法也是同样得处理逻辑
Eureka服务缓存源码分析
Eureka服务器组件另外一个核心功能提供服务列表。为了提高性能,Eureka服务器会缓存一份所有已注册得服务列表,并通过一定得定时机制对缓存数据进行更新。
http://<eureka-server-ip>:8761/eureka/apps/<APPID>
Eureaka中所有对服务端得访问都是通过Restful风格进行获取。
getApllication方法分析
Key cacheKey = new Key(
Key.EntityType.Application,
appName,
keyType,
CurrentRequestVersion.get(),
EurekaAccept.fromString(eurekaAccept)
);
String payLoad = responseCache.get(cacheKey);
if (payLoad != null) {
logger.debug("Found: {}", appName);
return Response.ok(payLoad).build();
} else {
logger.debug("Not Found: {}", appName);
return Response.status(Status.NOT_FOUND).build();
}
构建了一个CacheKey,并直接调用了responseCache.get(cacheKey)来响应,同时采用了缓存机制,最核心是get方法
public interface ResponseCache {
void invalidate(String appName, @Nullable String vipAddress, @Nullable String secureVipAddress);
AtomicLong getVersionDelta();
AtomicLong getVersionDeltaWithRegions();
String get(Key key);
byte[] getGZIP(Key key);
}
从类层关系上看,ResponseCache 只有一个实现类 ResponseCacheImpl,我们来看它的 get 方法,发现该方法使用了如下处理策略:
Value getValue(final Key key, boolean useReadOnlyCache) {
Value payload = null;
try {
//如果存储存在
if (useReadOnlyCache) {
//根据key获取缓存信息
final Value currentPayload = readOnlyCacheMap.get(key);
//当前缓存不为空
if (currentPayload != null) {
//直接赋值
payload = currentPayload;
//缓存为空重新写入
} else {
payload = readWriteCacheMap.get(key);
readOnlyCacheMap.put(key, payload);
}
} else {
payload = readWriteCacheMap.get(key);
}
} catch (Throwable t) {
logger.error("Cannot get value for key : {}", key, t);
}
return payload;
}
这里有一个设计和实现上的技巧。把缓存设计为一个只读的 readOnlyCacheMap 以及一个可读写的 readWriteCacheMap,可以更好地分离职责。但因为两个缓存中保存的实际上是同一份数据,所以,我们在不断更新 readWriteCacheMap 时,也需要确保 readOnlyCacheMap 中的数据得到同步。为此 ResponseCacheImpl 提供了一个定时任务 CacheUpdateTask,如下所示:
private TimerTask getCacheUpdateTask() {
return new TimerTask() {
@Override
public void run() {
for (Key key : readOnlyCacheMap.keySet()) {
try {
CurrentRequestVersion.set(key.getVersion());
Value cacheValue = readWriteCacheMap.get(key);
Value currentCacheValue = readOnlyCacheMap.get(key);
if (cacheValue != currentCacheValue) {
readOnlyCacheMap.put(key, cacheValue);
}
} catch (Throwable th) {
}
}
}
};
}
定时任务从readWriteCacheMap更新数据到readOnlyCacheMap
Eureka高可用源码解析
@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 方法,该方法作就是用来实现服务器节点之间的状态同步。replicateToPeers 方法的核心代码如下所示:
for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
//如何该 URL 代表主机自身,则不用进行注册
if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
continue;
}
replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
}
为了理解这个操作,我们首先需要理解 Eureka 中的集群模式,这部分代码位于 com.netflix.eureka.cluster 包中,其中包含了代表节点的 PeerEurekaNode 和 PeerEurekaNodes 类,以及用于节点之间数据传递的 HttpReplicationClient 接口。
public interface HttpReplicationClient extends EurekaHttpClient {
EurekaHttpResponse<Void> statusUpdate(String asgName, ASGStatus newStatus);
EurekaHttpResponse<ReplicationListResponse> submitBatchUpdates(ReplicationList replicationList);
}
HttpReplicationClient 状态更新实例
@Override
public EurekaHttpResponse<Void> statusUpdate(String asgName, ASGStatus newStatus) {
ClientResponse response = null;
try {
String urlPath = "asg/" + asgName + "/status";
response = jerseyApacheClient.resource(serviceUrl)
.path(urlPath)
.queryParam("value", newStatus.name())
.header(PeerEurekaNode.HEADER_REPLICATION, "true")
.put(ClientResponse.class);
return EurekaHttpResponse.status(response.getStatus());
} finally {
if (response != null) {
response.close();
}
}
}