一、Eureka Client
Eureka Client做的事情主要包括:
-
服务注册(Register)
Eureka Client会向Eureka Server进行服务注册,Client在我们的实际应用中可以是服务提供者或者是服务消费者
-
服务续约(Renew)
Eureka Client需要定期(默认30秒)向Eureka Server发送一次心跳来续订租约,以便让Eureka Server知道自已仍在运行,如果Eureka Server在90秒内未收到续订心跳,则会将Client实例从其注册表中删除
-
服务注册列表获取(Fetch)
Eureka Client需要从Eueka Server获取服务注册列表来查找其他服务信息,获取服务列表后会在本地进行缓存,可以定期(默认30秒)更新服务注册列表信息
-
服务下线(Cancel)
Eureka Client在shutdown时会向Eureka Server发送服务下线请求,以便Server将实例从注册表中删除
下面我们跟着源码梳理以上的相关功能代码流程,只做流程大概分析梳理,不具体到细节,说明部分在代码中通过注释的方式呈现。
在容器启动时,会加载com.netflix.discovery.DiscoveryClient并调用其构造方法,如下:
@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
Provider backupRegistryProvider) {
// 省略部分代码
// 如果配置不用注册到Eureka && 配置不用从注册中心获取配置,则不用初始化相关组件
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
}
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(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
.setDaemon(true)
.build()
); // use direct handoff
// 注册信息缓存刷新的线程池
cacheRefreshExecutor = new ThreadPoolExecutor(
1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
.setDaemon(true)
.build()
); // use direct handoff
} catch (Throwable e) {
throw new RuntimeException("Failed to initialize DiscoveryClient!", e);
}
// finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
// 初始化定时任务,服务心跳renew、服务注册、服务列表获取等功能在此处完成
initScheduledTasks();
}
接着看initScheduledTasks方法
private void initScheduledTasks() {
// 1.如果配置fetchRegistry=true,则定期执行获取服务列表的定时任务,默认30秒一次,通过registryFetchIntervalSeconds可以配置获取频率
if (clientConfig.shouldFetchRegistry()) {
// registry cache refresh timer
int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
scheduler.schedule(
new TimedSupervisorTask(
"cacheRefresh",
scheduler,
cacheRefreshExecutor,
registryFetchIntervalSeconds,
TimeUnit.SECONDS,
expBackOffBound,
// 具体执行逻辑在CacheRefreshThread.run()方法,后面再看
new CacheRefreshThread()
),
registryFetchIntervalSeconds, TimeUnit.SECONDS);
}
// 2.如果配置registerWithEureka=true,则定期向Eureka Server报送心跳,通过leaseRenewalIntervalInSeconds可以配置报送频率
if (clientConfig.shouldRegisterWithEureka()) {
int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);
// Heartbeat timer
scheduler.schedule(
new TimedSupervisorTask(
"heartbeat",
scheduler,
heartbeatExecutor,
renewalIntervalInSecs,
TimeUnit.SECONDS,
expBackOffBound,
// 具体执行逻辑在HeartbeatThread的run()方法,后面再看
new HeartbeatThread()
),
renewalIntervalInSecs, TimeUnit.SECONDS);
// 3.启动instanceInfoReplicator,服务注册就在这里完成,后面再看
instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
} else {
logger.info("Not registering with Eureka server per configuration");
}
}
再细看上述的3步流程
1)获取服务列表
跟踪CacheRefreshThread.run()方法
class CacheRefreshThread implements Runnable {
public void run() {
refreshRegistry();
}
}
@VisibleForTesting
void refreshRegistry() {
try {
// 获取服务列表信息
boolean success = fetchRegistry(remoteRegionsModified);
} catch (Throwable e) {
logger.error("Cannot fetch registry from server", e);
}
}
private boolean fetchRegistry(boolean forceFullRegistryFetch) {
try {
// If the delta is disabled or if it is the first time, get all
// applications
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
{
// 从Eureka Server全量获取服务列表缓存在本地
getAndStoreFullRegistry();
} else {
// 更新服务列表
getAndUpdateDelta(applications);
}
applications.setAppsHashCode(applications.getReconcileHashCode());
logTotalInstances();
} catch (Throwable e) {
logger.error(PREFIX + "{} - was unable to refresh its cache! status = {}", appPathIdentifier, e.getMessage(), e);
return false;
} finally {
if (tracer != null) {
tracer.stop();
}
}
return true;
}
2)服务心跳renew
跟踪HeartbeatThread.run()方法
private class HeartbeatThread implements Runnable {
public void run() {
if (renew()) {
lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
}
}
}
boolean renew() {
EurekaHttpResponse httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
logger.debug(PREFIX + "{} - Heartbeat status: {}", appPathIdentifier, httpResponse.getStatusCode());
if (httpResponse.getStatusCode() == 404) {
REREGISTER_COUNTER.increment();
logger.info(PREFIX + "{} - Re-registering apps/{}", appPathIdentifier, instanceInfo.getAppName());
long timestamp = instanceInfo.setIsDirtyWithTime();
boolean success = register();
if (success) {
instanceInfo.unsetIsDirty(timestamp);
}
return success;
}
return httpResponse.getStatusCode() == 200;
} catch (Throwable e) {
logger.error(PREFIX + "{} - was unable to send heartbeat!", appPathIdentifier, e);
return false;
}
}
3)服务注册
跟踪InstanceInfoReplicator.start()方法
public void start(int initialDelayMs) {
if (started.compareAndSet(false, true)) {
// 这里设置标识,便于启动时完成服务注册
instanceInfo.setIsDirty(); // for initial register
// 实际上执行的是run()方法,看下面
Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
public void run() {
try {
discoveryClient.refreshInstanceInfo();
Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
// 在start()方法中设置了标识位,所以此处能满足条件调用register()方法进行服务注册,看下面
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);
}
}
服务注册最终是调用register()方法
boolean register() throws Throwable {
logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
EurekaHttpResponse httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == 204;
}
4)服务下线
在应用shutdown时,会调用com.netflix.discovery.DiscoveryClient的shutdown()方法,其内部完成了服务下线的功能。
public synchronized void shutdown() {
if (isShutdown.compareAndSet(false, true)) {
logger.info("Shutting down DiscoveryClient ...");
if (statusChangeListener != null && applicationInfoManager != null) {
applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId());
}
cancelScheduledTasks();
// If APPINFO was registered
if (applicationInfoManager != null
&& clientConfig.shouldRegisterWithEureka()
&& clientConfig.shouldUnregisterOnShutdown()) {
// 设置应用状态为DOWN
applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
// 服务下线
unregister();
}
if (eurekaTransport != null) {
eurekaTransport.shutdown();
}
heartbeatStalenessMonitor.shutdown();
registryStalenessMonitor.shutdown();
logger.info("Completed shut down of DiscoveryClient");
}
}
void unregister() {
// It can be null if shouldRegisterWithEureka == false
if(eurekaTransport != null && eurekaTransport.registrationClient != null) {
try {
logger.info("Unregistering ...");
EurekaHttpResponse httpResponse = eurekaTransport.registrationClient.cancel(instanceInfo.getAppName(), instanceInfo.getId());
logger.info(PREFIX + "{} - deregister status: {}", appPathIdentifier, httpResponse.getStatusCode());
} catch (Exception e) {
logger.error(PREFIX + "{} - de-registration failed{}", appPathIdentifier, e.getMessage(), e);
}
}
}
在服务注册、心跳renew、服务下线的方法调用中,通过debug发现实际上是调用AbstractJerseyEurekaHttpClient的相关方法实现。
// 服务注册register
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
// 心跳renew
httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
// 服务下线cancel
EurekaHttpResponse httpResponse = eurekaTransport.registrationClient.cancel(instanceInfo.getAppName(), instanceInfo.getId());
以AbstractJerseyEurekaHttpClient类中服务注册的register()方法为例:
public EurekaHttpResponse register(InstanceInfo info) {
String urlPath = "apps/" + info.getAppName();
ClientResponse response = null;
try {
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);
return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
response == null ? "N/A" : response.getStatus());
}
if (response != null) {
response.close();
}
}
}
serviceUrl就是配置文件eureka.client.serviceUrl.defaultZone = http://127.0.0.1:8761/eureka/
,向Eureka Server请求实际上是通过Jersey框架完成。(Jersey是一个REST框架)
AbstractJerseyEurekaHttpClient类实现了EurekaHttpClient接口,接口中定义了服务注册与发现的相关方法。
public interface EurekaHttpClient {
EurekaHttpResponse register(InstanceInfo info);
EurekaHttpResponse cancel(String appName, String id);
EurekaHttpResponse sendHeartBeat(String appName, String id, InstanceInfo info, InstanceStatus overriddenStatus);
EurekaHttpResponse statusUpdate(String appName, String id, InstanceStatus newStatus, InstanceInfo info);
EurekaHttpResponse deleteStatusOverride(String appName, String id, InstanceInfo info);
EurekaHttpResponse getApplications(String... regions);
EurekaHttpResponse getDelta(String... regions);
EurekaHttpResponse getVip(String vipAddress, String... regions);
EurekaHttpResponse getSecureVip(String secureVipAddress, String... regions);
EurekaHttpResponse getApplication(String appName);
EurekaHttpResponse getInstance(String appName, String id);
EurekaHttpResponse getInstance(String id);
void shutdown();
}
二、Eureka Server
Eureka Server做的事情主要包括:
- 维护服务注册信息列表
- 接收来自Eureka Client的register、renew、cancel请求
- Eureka Server多节点之间的数据复制同步
查看spring-cloud-netflix-eureka-server-x.x.x.RELEASE,在META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration
在项目启动时EurekaServerAutoConfiguration会加载至spring容器
public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter {
// Eureka Server的相关配置类
@Configuration
protected static class EurekaServerConfigBeanConfiguration {
@Bean
@ConditionalOnMissingBean
public EurekaServerConfig eurekaServerConfig(EurekaClientConfig clientConfig) {
EurekaServerConfigBean server = new EurekaServerConfigBean();
if (clientConfig.shouldRegisterWithEureka()) {
// Set a sensible default if we are supposed to replicate
server.setRegistrySyncRetries(5);
}
return server;
}
}
// dashboard页面控制器
@Bean
@ConditionalOnProperty(prefix = "eureka.dashboard", name = "enabled", matchIfMissing = true)
public EurekaController eurekaController() {
return new EurekaController(this.applicationInfoManager);
}
// 处理Eureka Client的register、renew、cancel等请求
@Bean
public PeerAwareInstanceRegistry peerAwareInstanceRegistry(
ServerCodecs serverCodecs) {
this.eurekaClient.getApplications(); // force initialization
return new InstanceRegistry(this.eurekaServerConfig, this.eurekaClientConfig,
serverCodecs, this.eurekaClient,
this.instanceRegistryProperties.getExpectedNumberOfRenewsPerMin(),
this.instanceRegistryProperties.getDefaultOpenForTrafficCount());
}
// 处理Eureka Server多节点同步
@Bean
@ConditionalOnMissingBean
public PeerEurekaNodes peerEurekaNodes(PeerAwareInstanceRegistry registry,
ServerCodecs serverCodecs) {
return new RefreshablePeerEurekaNodes(registry, this.eurekaServerConfig,
this.eurekaClientConfig, serverCodecs, this.applicationInfoManager);
}
}
ApplicationResource和InstanceResource处理来自Eureka Client的register、renew、cancel等请求,以ApplicationResource中的服务注册接口为例。
@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info,
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
// 省略相关代码
registry.register(info, "true".equals(isReplication));
return Response.status(204).build(); // 204 to be backwards compatible
}
实际上调用的是PeerAwareInstanceRegistryImpl中的register方法
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();
}
// 调用AbstractInstanceRegistry父类的方法
super.register(info, leaseDuration, isReplication);
// 向其他Eureka Server节点同步注册信息
replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}
服务注册的信息保存在一个map的数据结构中
private final ConcurrentHashMap>> registry
= new ConcurrentHashMap>>();
其他renew、cancel逻辑也在PeerAwareInstanceRegistryImpl,这里不再赘述。
三、推荐阅读
- https://github.com/Netflix/eureka
- dive-into-eureka
如果文章对你有帮助的话,给文章点个赞吧。
如果有写得不正确的地方,欢迎指出。
文章首发公众号:会跳舞的机器人,欢迎扫码关注。