源码解析-Eureka Client(一)
一、Eureka Client源码的重要API
1、InstanceInfo
该类用于保存一个微服务主机的信息。一个该类实例就代表了一个微服务主机。该主机注册到Eureka Server就是将其InstanceInfo写入到了Eureka注册表,且被其它Server读取到的该Server的信息也是这个InstanceInfo。
// 记录当前InstanceInfo在Server端被修改的时间戳 private volatile Long lastUpdatedTimestamp; // 记录当前InstanceInfo在Client端被修改的时间戳 private volatile Long lastDirtyTimestamp; // 记录当前Client在Server端的状态 private volatile InstanceStatus status = InstanceStatus.UP; // 该状态用于计算Client在Server端的状态status(在Client提交注册请求与Renew续约请求 时) private volatile InstanceStatus overriddenStatus = InstanceStatus.UNKNOWN;
// 重写了euqals()方法:只要两个InstanceInfo的instanceId相同,那么这两个 InstanceInfo就相同 @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } InstanceInfo other = (InstanceInfo) obj; String id = getId(); if (id == null) { if (other.getId() != null) { return false; } } else if (!id.equals(other.getId())) { return false; } return true; }
2、Application
// 保存着当前name所指定的微服务名称的所有InstanceInfo private final Set<InstanceInfo> instances; // key为instanceId,value为instanceInfo private final Map<String, InstanceInfo> instancesMap;
3、Applications
// key为微服务名称,value为Application private final Map<String, Application> appNameApplicationMap;
4、Jersey框架
Spring Cloud中Eureka Client与Eureka Server的通信,及Eureka Server间的通信,均采用的是Jersey框架。
Jersey框架是一个开源的RESTful框架,实现了JAX-RS规范。该框架的作用与SpringMVC是相同的,其也是用户提交URI后,在处理器中进行路由匹配,路由到指定的后台业务。这个路由功能同样也是通过处理器完成的,只不过这里的处理器不叫Controller,而叫Resource。
二、Eureka Client解析入口中的重要类
1、EurekaClientAutoConfiguration类
2、封装配置文件中的两个属性的类
EurekaClientConfigBean
其读取的是eureka.client前辍的配置信息。这个类已经被@ConfigurationProperties注解了,所以这些配置信息可以被自动封装并注册到容器。
EurekaInstanceConfigBean
3、EurekaClient
这就是我们寻找的客户端实例。默认情况下创建的Eureka Client本身可以实现动态更新,即配置文件中相关的配置信息发生了变更,这个Client注册到Server的信息也会自动变更到Eureka Server的注册表中。
三、Eureka Client源码解析
1、解析总流程
注:强制注册失败时,会抛出异常, 无法执行后续流程。如定时更新等。
2、定时更新“客户端注册表”解析
// 定时更新“客户端注册表”解析 if (clientConfig.shouldFetchRegistry()) { // registry cache refresh timer,客户端从服务端下载更新注册表的时间间隔,默认为30s int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds(); // 指定client从server更新注册表的最大时间间隔指数(倍数),默认为10倍 int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound(); // 创建任务 cacheRefreshTask = new TimedSupervisorTask( "cacheRefresh", scheduler, cacheRefreshExecutor, registryFetchIntervalSeconds, TimeUnit.SECONDS, expBackOffBound, new CacheRefreshThread() ); // 开启一次性的定时任务 scheduler.schedule( cacheRefreshTask, registryFetchIntervalSeconds, TimeUnit.SECONDS); }
思考:为什么一次性的定时任务能循环多次执行?
TimedSupervisorTask的run()方法,在finally()中会循环开启任务。
@Override public void run() { Future<?> future = null; try { future = executor.submit(task); threadPoolLevelGauge.set((long) executor.getActiveCount()); future.get(timeoutMillis, TimeUnit.MILLISECONDS); // block until done or timeout delay.set(timeoutMillis); threadPoolLevelGauge.set((long) executor.getActiveCount()); successCounter.increment(); } catch (TimeoutException e) { logger.warn("task supervisor timed out", e); timeoutCounter.increment(); long currentDelay = delay.get(); long newDelay = Math.min(maxDelay, currentDelay * 2); delay.compareAndSet(currentDelay, newDelay); } catch (RejectedExecutionException e) { if (executor.isShutdown() || scheduler.isShutdown()) { logger.warn("task supervisor shutting down, reject the task", e); } else { logger.warn("task supervisor rejected the task", e); } rejectedCounter.increment(); } catch (Throwable e) { if (executor.isShutdown() || scheduler.isShutdown()) { logger.warn("task supervisor shutting down, can't accept the task"); } else { logger.warn("task supervisor threw an exception", e); } throwableCounter.increment(); } finally { if (future != null) { future.cancel(true); } // 如果定时器未关闭,则循环执行 if (!scheduler.isShutdown()) { scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS); } } }
3、定时续约解析
// 默认心跳间隔,30秒 int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs(); int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound(); logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs); // Heartbeat timer heartbeatTask = new TimedSupervisorTask( "heartbeat", scheduler, heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new HeartbeatThread() ); scheduler.schedule( heartbeatTask, renewalIntervalInSecs, TimeUnit.SECONDS);
4、定时更新Client信息给Server
- 通过监听器监听配置文件变更,如果发生变更,则Client提交register()。
- 默认每30秒定时更新一次。
- 通过限流器,限制配置文件频繁变更
5、总结
Client提交register()请求的情况有三种:
- 在应用启动时就可以直接进行register(),不过,需要提前在配置文件中配置
- 在renew时,如果server端返回的是NOT_FOUND,则提交register()
- 当Client的配置信息发生了变更,则Client提交register()
四、服务离线源码解析
服务离线,即某服务不能对外提供服务了。服务离线的原因有两种:服务下架与服务下线。
1、基于Actuator监控器实现
- 服务下架:http://localhost:8081/actuator/shutdown,无需请求体
- 服务下线:http://localhost:8081/actuator/serviceregistry,请求体为
// 下线 { "status":"OUT_OF_SERVICE" } // 上线 { "status":"UP" }
需要引入actuator依赖,并开启配置
management: endpoints: web: exposure: include: "*" # 开启shutdown监控终端 endpoint: shutdown: enabled: true
注意:从Spring Cloud 2020.0.0版本开始,服务平滑上下线的监控终端由sevice-registry变更为了serviceregistry
2、直接向EurekaServer提交请求
可以通过直接向Eureka Server提交不同的请求的方式来实现指定服务离线操作。
- 服务下架:通过向eureka server发送DELETE请求来删除指定client的服务
http://${server}:${port}/eureka/apps/${serviceName}/${instanceId} - 服务下线:通过向eureka server发送PUT请求来修改指定client的status,其中${value}的取值为:OUT_OF_SERVICE或UP
http://${server}:${port}/eureka/apps/${serviceName}/${instanceId}/status?value=${value}
3、特殊状态CANCEL_OVERRIDE
用户提交的状态修改请求中指定的状态,除了InstanceInfo的内置枚举类InstanceStatus中定义的状态外,还可以是CANCEL_OVERRIDE状态。
4、服务下架源码解析
@PreDestroy @Override 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()) { applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN); // 注销服务 unregister(); } if (eurekaTransport != null) { // 下架 eurekaTransport.shutdown(); } heartbeatStalenessMonitor.shutdown(); registryStalenessMonitor.shutdown(); Monitors.unregisterObject(this); logger.info("Completed shut down of DiscoveryClient"); } }
5、服务下线源码解析
应用不会挂掉,只是修改状态,不再被Euraka发现。
@Override public void setStatus(EurekaRegistration registration, String status) { InstanceInfo info = registration.getApplicationInfoManager().getInfo(); // TODO: howto deal with delete properly? if ("CANCEL_OVERRIDE".equalsIgnoreCase(status)) { registration.getEurekaClient().cancelOverrideStatus(info); return; } // TODO: howto deal with status types across discovery systems? InstanceInfo.InstanceStatus newStatus = InstanceInfo.InstanceStatus.toEnum(status); registration.getEurekaClient().setStatus(newStatus, info); }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
2019-04-01 打开一个vue项目
2019-04-01 VUE开发环境配置