本文已参与「新人创作礼」活动,一起开启掘金创作之路。
Netflix Eureka 目录汇总
-
eureka server启动以及初始化
-
eureka client启动以及初始化
-
服务注册
3.1. 可重入读写锁-读锁
-
服务发现
4.1. 全量抓取注册表
4.2. 注册表多级缓存机制
4.3. 注册表多级缓存过期机制(主动、定时、被动)
4.4. 增量抓取注册表
4.4.1. 一致性Hash对比机制 4.4.2. 可重入读写锁-写锁 -
服务续约
Netflix Eureka 时间间隔简要
读写缓存 - 定时过期(180秒)
只读缓存 - 被动过期(30秒(整30秒))
定时抓取增量注册表(30秒)
定时删除超过3分钟的服务实例变更记录(30秒)
服务续约间隔(30秒)
服务续约
服务续约:eureka client每隔一定的时间,会给eureka server发送心跳,保持心跳,让eureka server知道自己还活着
eureka client 发送服务续约请求
DiscoveryClient构造函数
- 保存配置信息
- 初始化线程池(调度、心跳、缓存刷新)
- 初始化网络通信组件
- 全量拉取注册表
- 初始化调度任务
5.1. 服务发现
5.2. 服务注册 - 监视器注册
@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config,
AbstractDiscoveryClientOptionalArgs args,
Provider<BackupRegistry> backupRegistryProvider) {
if (args != null) {
this.healthCheckHandlerProvider = args.healthCheckHandlerProvider;
this.healthCheckCallbackProvider = args.healthCheckCallbackProvider;
this.eventListeners.addAll(args.getEventListeners());
this.preRegistrationHandler = args.preRegistrationHandler;
} else {
// args == null
this.healthCheckCallbackProvider = null;
this.healthCheckHandlerProvider = null;
this.preRegistrationHandler = null;
}
// 应用信息管理器
this.applicationInfoManager = applicationInfoManager;
// 实例信息
InstanceInfo myInfo = applicationInfoManager.getInfo();
// EurekaClientConfig
clientConfig = config;
// @Deprecated
staticClientConfig = clientConfig;
// EurekaTransportConfig
transportConfig = config.getTransportConfig();
instanceInfo = myInfo;
if (myInfo != null) {
// appName:服务名称。通过eureka-client.properties文件获取eureka.name=eureka
// id:instanceId 如果为空,则返回hostname。一个服务可以存在多个服务实例,构建成集群。id(instanceId)表示一个服务实例
appPathIdentifier = instanceInfo.getAppName() + "/" + instanceInfo.getId();
} else {
logger.warn("Setting instanceInfo to a passed in null value");
}
// TODO LEON
this.backupRegistryProvider = backupRegistryProvider;
this.urlRandomizer = new EndpointUtils.InstanceInfoBasedUrlRandomizer(instanceInfo);
// 本地服务实例缓存
localRegionApps.set(new Applications());
fetchRegistryGeneration = new AtomicLong(0);
// null
remoteRegionsToFetch = new AtomicReference<>(clientConfig.fetchRegistryForRemoteRegions());
remoteRegionsRef = new AtomicReference<>(
// true
remoteRegionsToFetch.get() == null ?
null :
remoteRegionsToFetch.get().split(","));
// 期望抓取注册表
if (config.shouldFetchRegistry()) {
// 允许抓取注册表
this.registryStalenessMonitor = new ThresholdLevelsMetric(this,
METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L,
240L, 480L});
} else {
this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
}
// 允许将自己注册到注册中心
if (config.shouldRegisterWithEureka()) {
this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this,
METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L,
120L, 240L, 480L});
} else {
this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
}
logger.info("Initializing Eureka in region {}", clientConfig.getRegion());
// 如果既不允许注册服务到注册中心,也不允许抓取注册表, 清理资源并结束方法
if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
logger.info("Client configured to neither register nor query for data.");
scheduler = null;
heartbeatExecutor = null;
cacheRefreshExecutor = null;
eurekaTransport = null;
instanceRegionChecker =
new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config),
clientConfig.getRegion());
// This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
// to work with DI'd DiscoveryClient
DiscoveryManager.getInstance().setDiscoveryClient(this);
DiscoveryManager.getInstance().setEurekaClientConfig(config);
initTimestampMs = System.currentTimeMillis();
logger.info("Discovery Client initialized at timestamp {} with initial instances " +
"count: {}", initTimestampMs, this.getApplications().size());
return; // no need to setup up an network tasks and we are done
}
// 初始化3个线程池(调度、心跳、缓存刷新)
try {
// 支持调度的线程池
// default size of 2 - 1 each for heartbeat and cacheRefresh
scheduler = Executors.newScheduledThreadPool(2,
new ThreadFactoryBuilder().setNameFormat("DiscoveryClient-%d").setDaemon(true).build());
// 支持心跳的线程池
heartbeatExecutor = new ThreadPoolExecutor(1,
clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), new ThreadFactoryBuilder().setNameFormat(
"DiscoveryClient-HeartbeatExecutor-%d").setDaemon(true).build()); //
// use direct handoff
// 支持缓存刷新的线程池
cacheRefreshExecutor = new ThreadPoolExecutor(1,
clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), new ThreadFactoryBuilder().setNameFormat(
"DiscoveryClient-CacheRefreshExecutor-%d").setDaemon(true).build()); // use
// direct handoff
// 网络通信组件
eurekaTransport = new EurekaTransport();
// 初始化网络通信组件
scheduleServerEndpointTask(eurekaTransport, args);
AzToRegionMapper azToRegionMapper;
if (clientConfig.shouldUseDnsForFetchingServiceUrls()) { // false
azToRegionMapper = new DNSBasedAzToRegionMapper(clientConfig);
} else {
azToRegionMapper = new PropertyBasedAzToRegionMapper(clientConfig);
}
if (null != remoteRegionsToFetch.get()) {
azToRegionMapper.setRegionsToFetch(remoteRegionsToFetch.get().split(","));
}
instanceRegionChecker = new InstanceRegionChecker(azToRegionMapper,
clientConfig.getRegion());
} catch (Throwable e) {
throw new RuntimeException("Failed to initialize DiscoveryClient!", e);
}
// 允许拉取注册表,并且拉取(全量、增量)注册表成功
if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
// 抓取失败,从备份里抓取
fetchRegistryFromBackup();
}
// call and execute the pre registration handler before all background tasks (inc
// registration) is started
if (this.preRegistrationHandler != null) {
this.preRegistrationHandler.beforeRegistration();
}
// 初始化调度任务(注册、发现)
initScheduledTasks();
try {
// 注册监视器
Monitors.registerObject(this);
} catch (Throwable e) {
logger.warn("Cannot register timers", e);
}
// This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
// to work with DI'd DiscoveryClient
DiscoveryManager.getInstance().setDiscoveryClient(this);
DiscoveryManager.getInstance().setEurekaClientConfig(config);
initTimestampMs = System.currentTimeMillis();
logger.info("Discovery Client initialized at timestamp {} with initial instances count: " + "{}", initTimestampMs, this.getApplications().size());
}
eureka server 处理服务续约请求
根据appName和instanceId,从注册表中获取Lease<InstanceInfo>,更新Lease对象的lastUpdateTimestamp时间戳
ApplicationsResource:
ApplicationResource:
InstanceResource:
ApplicationsResource
@Path("/{version}/apps")
@Produces({"application/xml", "application/json"})
public class ApplicationsResource {
/**
* Gets information about a particular {@link com.netflix.discovery.shared.Application}.
*
* @param version the version of the request.
* @param appId the unique application identifier (which is the name) of the
* application.
* @return information about a particular application.
*/
@Path("{appId}")
public ApplicationResource getApplicationResource(
@PathParam("version") String version,
@PathParam("appId") String appId) {
CurrentRequestVersion.set(Version.toEnum(version));
return new ApplicationResource(appId, serverConfig, registry);
}
}
ApplicationResource
@Produces({"application/xml", "application/json"})
public class ApplicationResource {
/**
* Gets information about a particular instance of an application.
*
* @param id the unique identifier of the instance.
* @return information about a particular instance.
*/
@Path("{id}")
public InstanceResource getInstanceInfo(@PathParam("id") String id) {
return new InstanceResource(this, id, serverConfig, registry);
}
}
InstanceResource
@Produces({"application/xml", "application/json"})
public class InstanceResource {
/**
* A put request for renewing lease from a client instance.
*
* @param isReplication
* a header parameter containing information whether this is
* replicated from other nodes.
* @param overriddenStatus
* overridden status if any.
* @param status
* the {@link InstanceStatus} of the instance.
* @param lastDirtyTimestamp
* last timestamp when this instance information was updated.
* @return response indicating whether the operation was a success or
* failure.
*/
/**
* 处理服务续约操作
*
* @param isReplication
* @param overriddenStatus
* @param status
* @param lastDirtyTimestamp
* @return
*/
@PUT
public Response renewLease(
// x-netflix-discovery-replication
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,
@QueryParam("overriddenstatus") String overriddenStatus,
@QueryParam("status") String status,
@QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {
boolean isFromReplicaNode = "true".equals(isReplication);
// 调用注册表(registry)的续约方法(renew),更新lastUpdateTimestamp(System.currentTimeMillis() + duration)
boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);
// Not found in the registry, immediately ask for a register
// 服务没有注册,返回404。eureka client 会立即发起服务注册请求。
if (!isSuccess) {
logger.warn("Not Found (Renew): {} - {}", app.getName(), id);
return Response.status(Status.NOT_FOUND).build();
}
// Check if we need to sync based on dirty time stamp, the client
// instance might have changed some value
Response response = null;
if (lastDirtyTimestamp != null && serverConfig.shouldSyncWhenTimestampDiffers()) {
response = this.validateDirtyTimestamp(Long.valueOf(lastDirtyTimestamp),
isFromReplicaNode);
// Store the overridden status since the validation found out the node that
// replicates wins
if (response.getStatus() == Response.Status.NOT_FOUND.getStatusCode() && (overriddenStatus != null) && !(InstanceStatus.UNKNOWN.name().equals(overriddenStatus)) && isFromReplicaNode) {
registry.storeOverriddenStatusIfRequired(app.getAppName(), id,
InstanceStatus.valueOf(overriddenStatus));
}
} else {
response = Response.ok().build();
}
logger.debug("Found (Renew): {} - {}; reply status={}" + app.getName(), id,
response.getStatus());
return response;
}
}
服务续约流程
(1)DiscoveryClient初始化的时候,初始化3个线程池,其中有一个就是心跳线程(HeartbeatThread)
(2)默认每隔30秒发送一次心跳,每隔30秒执行一次HeartbeatThread线程的逻辑,发送租约续约请求
(3)发送续约请求,调用eurekaTransport.registrationClient.sendHeartBeat()方法,发送PUT请求【http://localhost:8080/v2/apps/ServiceA/InstanceId001】
(4)eureka server 项目的ApplicationsResource类接收【/{version}/apps/{appId}】,进入ApplicationResource类接收【/{id}】,进入InstanceResource类接收【PUT请求】执行【renewLease()】方法逻辑
(5)通过注册表的renew()方法,完成服务续约,实际执行AbstractInstanceRegistry.renew()方法
(6)根据服务名和实例id,从注册表中获取Lease,更新【Lease】对象的【lastUpdateTimestamp】时间戳【lastUpdateTimestamp = System.currentTimeMillis() + duration;】【90秒】。
(7)如果续约失败,返回HTTP Status NOT_FOUND给 eureka client
(8)eureka client 发现如果是404,表示续约失败,会重新发起服务注册请求;如果返回续约成功,更新lastSuccessfulHeartbeatTimestamp为当前时间戳
(9)eureka client 再次发起服务注册请求时,会进入eureak server服务注册处理逻辑的服务实例已存在分支。从注册表中获取已存在的服务实例,比较【要新注册的服务实例】与【已存在的服务实例】的【lastDirtyTimestamp】值,留存【lastDirtyTimestamp】大的【服务实例】,放入注册表registry中