在之前的文章:EurekaClient自动装配及启动流程解析中,我们提到了在构造DiscoveryClient时除了包含注册流程之外,还调度了一个心跳线程:

  1. scheduler.schedule(
  2. new TimedSupervisorTask(
  3. "heartbeat",
  4. scheduler,
  5. heartbeatExecutor,
  6. renewalIntervalInSecs,
  7. TimeUnit.SECONDS,
  8. expBackOffBound,
  9. new HeartbeatThread()
  10. ),
  11. renewalIntervalInSecs, TimeUnit.SECONDS);

其中HeartbeatThread线程如下:

  1. private class HeartbeatThread implements Runnable {
  2. public void run() {
  3. //续约
  4. if (renew()) {
  5. //续约成功时间戳更新
  6. lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
  7. }
  8. }
  9. }
  10. boolean renew() {
  11. EurekaHttpResponse<InstanceInfo> httpResponse;
  12. try {
  13. //发送续约请求
  14. httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
  15. logger.debug(PREFIX + "{} - Heartbeat status: {}", appPathIdentifier, httpResponse.getStatusCode());
  16. if (httpResponse.getStatusCode() == 404) {
  17. REREGISTER_COUNTER.increment();
  18. logger.info(PREFIX + "{} - Re-registering apps/{}", appPathIdentifier, instanceInfo.getAppName());
  19. long timestamp = instanceInfo.setIsDirtyWithTime();
  20. //重新注册
  21. boolean success = register();
  22. if (success) {
  23. instanceInfo.unsetIsDirty(timestamp);
  24. }
  25. return success;
  26. }
  27. return httpResponse.getStatusCode() == 200;
  28. } catch (Throwable e) {
  29. logger.error(PREFIX + "{} - was unable to send heartbeat!", appPathIdentifier, e);
  30. return false;
  31. }
  32. }

这里直接发出了续约请求,如果续约请求失败则会尝试再次去注册

服务端接受续约请求

服务端接受续约请求的Controller在InstanceResource类中

  1. @PUT
  2. public Response renewLease(
  3. @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,
  4. @QueryParam("overriddenstatus") String overriddenStatus,
  5. @QueryParam("status") String status,
  6. @QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {
  7. boolean isFromReplicaNode = "true".equals(isReplication);
  8. //续约
  9. boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);
  10. // 续约失败
  11. if (!isSuccess) {
  12. logger.warn("Not Found (Renew): {} - {}", app.getName(), id);
  13. return Response.status(Status.NOT_FOUND).build();
  14. }
  15. // 校验客户端与服务端的时间差异,如果存在问题则需要重新发起注册
  16. Response response = null;
  17. if (lastDirtyTimestamp != null && serverConfig.shouldSyncWhenTimestampDiffers()) {
  18. response = this.validateDirtyTimestamp(Long.valueOf(lastDirtyTimestamp), isFromReplicaNode);
  19. if (response.getStatus() == Response.Status.NOT_FOUND.getStatusCode()
  20. && (overriddenStatus != null)
  21. && !(InstanceStatus.UNKNOWN.name().equals(overriddenStatus))
  22. && isFromReplicaNode) {
  23. registry.storeOverriddenStatusIfRequired(app.getAppName(), id, InstanceStatus.valueOf(overriddenStatus));
  24. }
  25. } else {
  26. response = Response.ok().build();
  27. }
  28. logger.debug("Found (Renew): {} - {}; reply status={}", app.getName(), id, response.getStatus());
  29. return response;
  30. }

可以看到续约之后还有一个检查时间差的问题,这个不详细展开,继续往下看续约的相关信息

  1. public boolean renew(final String appName, final String id, final boolean isReplication) {
  2. if (super.renew(appName, id, isReplication)) {
  3. //集群同步
  4. replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication);
  5. return true;
  6. }
  7. return false;
  8. }

这里集群同步的相关内容在之前的文章已经说过了,不再展开,续约的核心处理在下面

  1. public boolean renew(String appName, String id, boolean isReplication) {
  2. RENEW.increment(isReplication);
  3. //获取已存在的租约
  4. Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
  5. Lease<InstanceInfo> leaseToRenew = null;
  6. if (gMap != null) {
  7. leaseToRenew = gMap.get(id);
  8. }
  9. //租约不存在
  10. if (leaseToRenew == null) {
  11. RENEW_NOT_FOUND.increment(isReplication);
  12. logger.warn("DS: Registry: lease doesn't exist, registering resource: {} - {}", appName, id);
  13. return false;
  14. } else {
  15. //获取客户端
  16. InstanceInfo instanceInfo = leaseToRenew.getHolder();
  17. //设置客户端的状态
  18. if (instanceInfo != null) {
  19. // touchASGCache(instanceInfo.getASGName());
  20. InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(
  21. instanceInfo, leaseToRenew, isReplication);
  22. if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
  23. logger.info("Instance status UNKNOWN possibly due to deleted override for instance {}"
  24. + "; re-register required", instanceInfo.getId());
  25. RENEW_NOT_FOUND.increment(isReplication);
  26. return false;
  27. }
  28. if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
  29. logger.info(
  30. "The instance status {} is different from overridden instance status {} for instance {}. "
  31. + "Hence setting the status to overridden status", instanceInfo.getStatus().name(),
  32. instanceInfo.getOverriddenStatus().name(),
  33. instanceInfo.getId());
  34. //覆盖当前状态
  35. instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
  36. }
  37. }
  38. renewsLastMin.increment();
  39. //设置租约最后更新时间
  40. leaseToRenew.renew();
  41. return true;
  42. }
  43. }

对于看过之前文章的同学来说整体流程比较简单

服务端过期租约清理

在文章Eureka应用注册与集群数据同步源码解析一文中大家应该对下面这行代码比较熟悉

  1. int registryCount = registry.syncUp();

上面这行代码发起了集群数据同步,而紧接着这行代码的就是服务端的过期租约清理逻辑

  1. registry.openForTraffic(applicationInfoManager, registryCount);

openForTraffic方法的最后调用了一个方法postInit,而在postInit方法中启动了一个线程EvictionTask,这个线程就负责清理已经过期的租约

  1. evictionTimer.schedule(evictionTaskRef.get(),       
  2. serverConfig.getEvictionIntervalTimerInMs(), 
  3. serverConfig.getEvictionIntervalTimerInMs());

看一下这个线程

  1. class EvictionTask extends TimerTask {
  2. @Override
  3. public void run() {
  4. try {
  5. //补偿时间毫秒数
  6. long compensationTimeMs = getCompensationTimeMs();
  7. logger.info("Running the evict task with compensationTime {}ms", compensationTimeMs);
  8. // 清理逻辑
  9. evict(compensationTimeMs);
  10. } catch (Throwable e) {
  11. logger.error("Could not run the evict task", e);
  12. }
  13. }
  14. }

其中补偿时间的获取是这样的:

  1. long getCompensationTimeMs() {
  2. long currNanos = getCurrentTimeNano();
  3. long lastNanos = lastExecutionNanosRef.getAndSet(currNanos);
  4. if (lastNanos == 0l) {
  5. return 0l;
  6. }
  7. long elapsedMs = TimeUnit.NANOSECONDS.toMillis(currNanos - lastNanos);
  8. //当前时间 - 最后任务执行时间 - 任务执行频率
  9. long compensationTime = elapsedMs - serverConfig.getEvictionIntervalTimerInMs();
  10. return compensationTime <= 0l ? 0l : compensationTime;
  11. }

接着看清理的核心逻辑

  1. public void evict(long additionalLeaseMs) {
  2. logger.debug("Running the evict task");
  3. if (!isLeaseExpirationEnabled()) {
  4. logger.debug("DS: lease expiration is currently disabled.");
  5. return;
  6. }
  7. // 1. 获得所有的过期租约
  8. List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
  9. for (Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry : registry.entrySet()) {
  10. Map<String, Lease<InstanceInfo>> leaseMap = groupEntry.getValue();
  11. if (leaseMap != null) {
  12. for (Entry<String, Lease<InstanceInfo>> leaseEntry : leaseMap.entrySet()) {
  13. Lease<InstanceInfo> lease = leaseEntry.getValue();
  14. if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
  15. expiredLeases.add(lease);
  16. }
  17. }
  18. }
  19. }
  20. // 2. 计算允许清理的数量
  21. int registrySize = (int) getLocalRegistrySize();
  22. int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());
  23. int evictionLimit = registrySize - registrySizeThreshold;
  24. int toEvict = Math.min(expiredLeases.size(), evictionLimit);
  25. // 3. 过期
  26. if (toEvict > 0) {
  27. logger.info("Evicting {} items (expired={}, evictionLimit={})", toEvict, expiredLeases.size(), evictionLimit);
  28. Random random = new Random(System.currentTimeMillis());
  29. for (int i = 0; i < toEvict; i++) {
  30. // Pick a random item (Knuth shuffle algorithm)
  31. int next = i + random.nextInt(expiredLeases.size() - i);
  32. Collections.swap(expiredLeases, i, next);
  33. Lease<InstanceInfo> lease = expiredLeases.get(i);
  34. String appName = lease.getHolder().getAppName();
  35. String id = lease.getHolder().getId();
  36. EXPIRED.increment();
  37. logger.warn("DS: Registry: expired lease for {}/{}", appName, id);
  38. internalCancel(appName, id, false);
  39. }
  40. }
  41. }

整个过期的执行过程主要分为以下3个步骤:

  1. 获得所有的过期租约

    过期租约的计算方法为isExpired
  1. public boolean isExpired(long additionalLeaseMs) {   
  2. return (evictionTimestamp > 0 || System.currentTimeMillis() >
  3. (lastUpdateTimestamp + duration + additionalLeaseMs));
  4. }

服务下线时间>0||当前时间>(最后更新时间+租约持续时间+补偿时间)

  1. 计算允许清理的数量

    getRenewalPercentThreshold()默认值为0.85,也是就说默认情况下每次清理最大允许过期数量和15%的所有注册数量两者之间的最小值
  2. 过期

    过期的清理是随机进行的,这样设计也是为了避免单个应用全部过期的。

    过期的处理则和注册的处理正好是相反的:
  1. protected boolean internalCancel(String appName, String id, boolean isReplication) {
  2. try {
  3. read.lock();
  4. CANCEL.increment(isReplication);
  5. Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
  6. Lease<InstanceInfo> leaseToCancel = null;
  7. if (gMap != null) {
  8. leaseToCancel = gMap.remove(id);
  9. }
  10. synchronized (recentCanceledQueue) {
  11. recentCanceledQueue.add(new Pair<Long, String>(System.currentTimeMillis(), appName + "(" + id + ")"));
  12. }
  13. InstanceStatus instanceStatus = overriddenInstanceStatusMap.remove(id);
  14. if (instanceStatus != null) {
  15. logger.debug("Removed instance id {} from the overridden map which has value {}", id, instanceStatus.name());
  16. }
  17. if (leaseToCancel == null) {
  18. CANCEL_NOT_FOUND.increment(isReplication);
  19. logger.warn("DS: Registry: cancel failed because Lease is not registered for: {}/{}", appName, id);
  20. return false;
  21. } else {
  22. leaseToCancel.cancel();
  23. InstanceInfo instanceInfo = leaseToCancel.getHolder();
  24. String vip = null;
  25. String svip = null;
  26. if (instanceInfo != null) {
  27. instanceInfo.setActionType(ActionType.DELETED);
  28. recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel));
  29. instanceInfo.setLastUpdatedTimestamp();
  30. vip = instanceInfo.getVIPAddress();
  31. svip = instanceInfo.getSecureVipAddress();
  32. }
  33. invalidateCache(appName, vip, svip);
  34. logger.info("Cancelled instance {}/{} (replication={})", appName, id, isReplication);
  35. return true;
  36. }
  37. } finally {
  38. read.unlock();
  39. }
  40. }

原文地址

Eureka客户端续约及服务端过期租约清理源码解析的更多相关文章

  1. 确保客户端可以接收到服务端的异常serviceDebug includeExceptionDetailInFaults="true"

    1.为了确保客户端可以接收到服务端反馈的异常 在服务端的配置文件中需要有 <system.serviceModel> <behaviors> <serviceBehavi ...

  2. 客户端技术:Cookie 服务端技术:HttpSession

    客户端技术:Cookie 服务端技术:HttpSession 07. 五 / android基础 / 没有评论   一.会话技术1.什么是会话:客户打开浏览器访问一个网站,访问完毕之后,关闭浏览器.这 ...

  3. 『集群』003 Slithice 最简分布式(多个客户端,一个独立服务端)

    Slithice 最简分布式(多个客户端,一个独立服务端) 案例Demo 展示: 我们搭建一个 可以 独立运行 的 服务端:然后 多个客户端 并发链接 这个 服务端 完成 分布式逻辑: 服务器 独立运 ...

  4. MVC文件上传09-使用客户端jQuery-File-Upload插件和服务端Backload组件让每个用户有专属文件夹,并在其中创建分类子文件夹

    为用户创建专属上传文件夹后,如果想在其中再创建分类子文件夹,该怎么做?可以在提交文件的视图中再添加一个隐藏域,并设置 name="uploadContext". 相关兄弟篇: MV ...

  5. MVC文件上传08-使用客户端jQuery-File-Upload插件和服务端Backload组件让每个用户有专属文件夹

    当需要为每个用户建立一个专属上传文件夹的时候,可以在提交文件的视图中添加一个隐藏域,并设置name="objectContext". 相关兄弟篇: MVC文件上传01-使用jque ...

  6. MVC文件上传07-使用客户端jQuery-File-Upload插件和服务端Backload组件裁剪上传图片

    本篇通过在配置文件中设置,对上传图片修剪后保存到指定文件夹. 相关兄弟篇: MVC文件上传01-使用jquery异步上传并客户端验证类型和大小  MVC文件上传02-使用HttpPostedFileB ...

  7. MVC文件上传06-使用客户端jQuery-File-Upload插件和服务端Backload组件自定义控制器上传多个文件

    当需要在控制器中处理除了文件的其他表单字段,执行控制器独有的业务逻辑......等等,这时候我们可以自定义控制器. MVC文件上传相关兄弟篇: MVC文件上传01-使用jquery异步上传并客户端验证 ...

  8. MVC文件上传05-使用客户端jQuery-File-Upload插件和服务端Backload组件自定义上传文件夹

    在零配置情况下,文件的上传文件夹是根目录下的Files文件夹,如何自定义文件的上传文件夹呢? MVC文件上传相关兄弟篇: MVC文件上传01-使用jquery异步上传并客户端验证类型和大小  MVC文 ...

  9. MVC文件上传04-使用客户端jQuery-File-Upload插件和服务端Backload组件实现多文件异步上传

    本篇使用客户端jQuery-File-Upload插件和服务端Badkload组件实现多文件异步上传.MVC文件上传相关兄弟篇: MVC文件上传01-使用jquery异步上传并客户端验证类型和大小  ...

随机推荐

  1. django项目中cxselect三级联动

    下载cxselect插件放在static文件夹下 前端引入 <script src="/static/js/jQuery-1.8.2.min.js"></scri ...

  2. Rust中的模块及私有性控制

    好像没有其它语言的private, protected关键字,应了一个public关键字. mod plant { pub struct Vegetable { pub name: String, _ ...

  3. 代码审计-strcmp比较字符串

    <?php $flag = "flag{xxxxx}"; if (isset($_GET['a'])) { if (strcmp($_GET['a'], $flag) == ...

  4. No archetypes currently available. The archetype list will refresh when the indexes finish updating

    配置方法: 1. 在卡住的而画面点击"config" 2. 点击"Add remote catalog", 然后设置华为云的maven仓库地址, 然后点击&qu ...

  5. django的几个常见命令、request请求取值形式、数据库连接、

    django基础知识薄弱点 几个常见的命令 #创建django项目 django-admin startproject mysite #启动django项目 python manage.py runs ...

  6. JDOJ 3055: Nearest Common Ancestors

    JDOJ 3055: Nearest Common Ancestors JDOJ传送门 Description 给定N个节点的一棵树,有K次查询,每次查询a和b的最近公共祖先. 样例中的16和7的公共 ...

  7. 学习-velocity

    Velocity是什么?  Velocity是一个基于java的模板引擎(template engine).它允许任何人仅仅简单的使用模板语言(template language)来引用由java代码 ...

  8. springcloud2.x之management.security.enabled=false报错处理

    1. springcloud1.5.x的消息总线配置是 # RabbitMq的地址.端口,用户名.密码 spring.rabbitmq.host=localhost spring.rabbitmq.p ...

  9. sonatype nexus安装教程

    1. 安装nexus前需要先安装maven.(详见jdk安装教程)2. 将nexus-2.0.2.rar放到d:\teamwork中,点击右键,解压到当前文件夹中.其中包含两个文件夹:nexus,so ...

  10. JavaScript 正则表达式匹配成功后的返回结果

    原文地址:https://blog.csdn.net/liupeifeng3514/article/details/79005604 使用正则表达式EDIT 正则表达式可以被用于RegExp的exec ...