前言

前情回顾

上一讲看了Eureka 注册中心的自我保护机制,以及里面提到的bug问题。

哈哈 转眼间都2020年了,这个系列的文章从12.17 一直写到现在,也是不容易哈,每天持续不断学习,输出博客,这一段时间确实收获很多。

今天在公司给组内成员分享了Eureka源码剖析,反响效果还可以,也算是感觉收获了点东西。后面还会继续feign、ribbon、hystrix的源码学习,依然文章连载的形式输出。

本讲目录

本讲主要是EurekaServer集群模式的数据同步讲解,主要目录如下。

目录如下:

  1. eureka server集群机制
  2. 注册、下线、续约的注册表同步机制
  3. 注册表同步三层队列机制详解

技术亮点:

  1. 3层队列机制实现注册表的批量同步需求

说明

原创不易,如若转载 请标明来源!

博客地址:一枝花算不算浪漫

微信公众号:壹枝花算不算浪漫

源码分析

eureka server集群机制

Eureka Server会在注册、下线、续约的时候进行数据同步,将信息同步到其他Eureka Server节点。

可以想象到的是,这里肯定不会是实时同步的,往后继续看注册表的同步机制吧。

注册、下线、续约的注册表同步机制

我们以Eureka Client注册为例,看看Eureka Server是如何同步给其他节点的。

PeerAwareInstanceRegistryImpl.java :

  1. public void register(final InstanceInfo info, final boolean isReplication) {
  2. int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
  3. if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
  4. leaseDuration = info.getLeaseInfo().getDurationInSecs();
  5. }
  6. super.register(info, leaseDuration, isReplication);
  7. replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
  8. }
  9. private void replicateToPeers(Action action, String appName, String id,
  10. InstanceInfo info /* optional */,
  11. InstanceStatus newStatus /* optional */, boolean isReplication) {
  12. Stopwatch tracer = action.getTimer().start();
  13. try {
  14. if (isReplication) {
  15. numberOfReplicationsLastMin.increment();
  16. }
  17. // If it is a replication already, do not replicate again as this will create a poison replication
  18. if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
  19. return;
  20. }
  21. for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
  22. // If the url represents this host, do not replicate to yourself.
  23. if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
  24. continue;
  25. }
  26. replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
  27. }
  28. } finally {
  29. tracer.stop();
  30. }
  31. }
  32. private void replicateInstanceActionsToPeers(Action action, String appName,
  33. String id, InstanceInfo info, InstanceStatus newStatus,
  34. PeerEurekaNode node) {
  35. try {
  36. InstanceInfo infoFromRegistry = null;
  37. CurrentRequestVersion.set(Version.V2);
  38. switch (action) {
  39. case Cancel:
  40. node.cancel(appName, id);
  41. break;
  42. case Heartbeat:
  43. InstanceStatus overriddenStatus = overriddenInstanceStatusMap.get(id);
  44. infoFromRegistry = getInstanceByAppAndId(appName, id, false);
  45. node.heartbeat(appName, id, infoFromRegistry, overriddenStatus, false);
  46. break;
  47. case Register:
  48. node.register(info);
  49. break;
  50. case StatusUpdate:
  51. infoFromRegistry = getInstanceByAppAndId(appName, id, false);
  52. node.statusUpdate(appName, id, newStatus, infoFromRegistry);
  53. break;
  54. case DeleteStatusOverride:
  55. infoFromRegistry = getInstanceByAppAndId(appName, id, false);
  56. node.deleteStatusOverride(appName, id, infoFromRegistry);
  57. break;
  58. }
  59. } catch (Throwable t) {
  60. logger.error("Cannot replicate information to {} for action {}", node.getServiceUrl(), action.name(), t);
  61. }
  62. }
  1. 注册完成后,调用replicateToPeers(),注意这里面有一个参数isReplication,如果是true,代表是其他Eureka Server节点同步的,false则是EurekaClient注册来的。
  2. replicateToPeers()中一段逻辑,如果isReplication为true则直接跳出,这里意思是client注册来的服务实例需要向其他节点扩散,如果不是则不需要去同步
  3. peerEurekaNodes.getPeerEurekaNodes()拿到所有的Eureka Server节点,循环遍历去同步数据,调用replicateInstanceActionsToPeers()
  4. replicateInstanceActionsToPeers()方法中根据注册、下线、续约等去处理不同逻辑

接下来就是真正执行同步逻辑的地方,这里主要用了三层队列对同步请求进行了batch操作,将请求打成一批批 然后向各个EurekaServer进行http请求。

注册表同步三层队列机制详解

到了这里就是真正进入了同步的逻辑,这里还是以上面注册逻辑为主线,接着上述代码继续往下跟:

PeerEurekaNode.java :

  1. public void register(final InstanceInfo info) throws Exception {
  2. long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
  3. batchingDispatcher.process(
  4. taskId("register", info),
  5. new InstanceReplicationTask(targetHost, Action.Register, info, null, true) {
  6. public EurekaHttpResponse<Void> execute() {
  7. return replicationClient.register(info);
  8. }
  9. },
  10. expiryTime
  11. );
  12. }

这里会执行batchingDispatcher.process() 方法,我们继续点进去,然后会进入 TaskDispatchers.createBatchingTaskDispatcher() 方法,查看其中的匿名内部类中的process()方法:

  1. void process(ID id, T task, long expiryTime) {
  2. // 将请求都放入到acceptorQueue中
  3. acceptorQueue.add(new TaskHolder<ID, T>(id, task, expiryTime));
  4. acceptedTasks++;
  5. }

将需要同步的Task数据放入到acceptorQueue队列中。

接着回到createBatchingTaskDispatcher()方法中,看下AcceptorExecutor,它的构造函数中会启动一个后台线程:

  1. ThreadGroup threadGroup = new ThreadGroup("eurekaTaskExecutors");
  2. this.acceptorThread = new Thread(threadGroup, new AcceptorRunner(), "TaskAcceptor-" + id);

我们继续跟AcceptorRunner.java:

  1. class AcceptorRunner implements Runnable {
  2. @Override
  3. public void run() {
  4. long scheduleTime = 0;
  5. while (!isShutdown.get()) {
  6. try {
  7. // 处理acceptorQueue队列中的数据
  8. drainInputQueues();
  9. int totalItems = processingOrder.size();
  10. long now = System.currentTimeMillis();
  11. if (scheduleTime < now) {
  12. scheduleTime = now + trafficShaper.transmissionDelay();
  13. }
  14. if (scheduleTime <= now) {
  15. // 将processingOrder拆分成一个个batch,然后进行操作
  16. assignBatchWork();
  17. assignSingleItemWork();
  18. }
  19. // If no worker is requesting data or there is a delay injected by the traffic shaper,
  20. // sleep for some time to avoid tight loop.
  21. if (totalItems == processingOrder.size()) {
  22. Thread.sleep(10);
  23. }
  24. } catch (InterruptedException ex) {
  25. // Ignore
  26. } catch (Throwable e) {
  27. // Safe-guard, so we never exit this loop in an uncontrolled way.
  28. logger.warn("Discovery AcceptorThread error", e);
  29. }
  30. }
  31. }
  32. private void drainInputQueues() throws InterruptedException {
  33. do {
  34. drainAcceptorQueue();
  35. if (!isShutdown.get()) {
  36. // If all queues are empty, block for a while on the acceptor queue
  37. if (reprocessQueue.isEmpty() && acceptorQueue.isEmpty() && pendingTasks.isEmpty()) {
  38. TaskHolder<ID, T> taskHolder = acceptorQueue.poll(10, TimeUnit.MILLISECONDS);
  39. if (taskHolder != null) {
  40. appendTaskHolder(taskHolder);
  41. }
  42. }
  43. }
  44. } while (!reprocessQueue.isEmpty() || !acceptorQueue.isEmpty() || pendingTasks.isEmpty());
  45. }
  46. private void drainAcceptorQueue() {
  47. while (!acceptorQueue.isEmpty()) {
  48. // 将acceptor队列中的数据放入到processingOrder队列中去,方便后续拆分成batch
  49. appendTaskHolder(acceptorQueue.poll());
  50. }
  51. }
  52. private void appendTaskHolder(TaskHolder<ID, T> taskHolder) {
  53. if (isFull()) {
  54. pendingTasks.remove(processingOrder.poll());
  55. queueOverflows++;
  56. }
  57. TaskHolder<ID, T> previousTask = pendingTasks.put(taskHolder.getId(), taskHolder);
  58. if (previousTask == null) {
  59. processingOrder.add(taskHolder.getId());
  60. } else {
  61. overriddenTasks++;
  62. }
  63. }
  64. }

认真跟这里面的代码,可以看到这里是将上面的acceptorQueue放入到processingOrder, 其中processingOrder也是一个队列。

AcceptorRunner.javarun()方法中,还会调用assignBatchWork()方法,这里面就是将processingOrder打成一个个batch,接着看代码:

  1. void assignBatchWork() {
  2. if (hasEnoughTasksForNextBatch()) {
  3. if (batchWorkRequests.tryAcquire(1)) {
  4. long now = System.currentTimeMillis();
  5. int len = Math.min(maxBatchingSize, processingOrder.size());
  6. List<TaskHolder<ID, T>> holders = new ArrayList<>(len);
  7. while (holders.size() < len && !processingOrder.isEmpty()) {
  8. ID id = processingOrder.poll();
  9. TaskHolder<ID, T> holder = pendingTasks.remove(id);
  10. if (holder.getExpiryTime() > now) {
  11. holders.add(holder);
  12. } else {
  13. expiredTasks++;
  14. }
  15. }
  16. if (holders.isEmpty()) {
  17. batchWorkRequests.release();
  18. } else {
  19. batchSizeMetric.record(holders.size(), TimeUnit.MILLISECONDS);
  20. // 将批量数据放入到batchWorkQueue中
  21. batchWorkQueue.add(holders);
  22. }
  23. }
  24. }
  25. }
  26. private boolean hasEnoughTasksForNextBatch() {
  27. if (processingOrder.isEmpty()) {
  28. return false;
  29. }
  30. // 默认maxBufferSize为250
  31. if (pendingTasks.size() >= maxBufferSize) {
  32. return true;
  33. }
  34. TaskHolder<ID, T> nextHolder = pendingTasks.get(processingOrder.peek());
  35. // 默认maxBatchingDelay为500ms
  36. long delay = System.currentTimeMillis() - nextHolder.getSubmitTimestamp();
  37. return delay >= maxBatchingDelay;
  38. }

这里加入batch的规则是:maxBufferSize 默认为250

maxBatchingDelay 默认为500ms,打成一个个batch后就开始发送给server端。至于怎么发送 我们接着看 PeerEurekaNode.java, 我们在最开始调用register() 方法就是调用PeerEurekaNode.register(), 我们来看看它的构造方法:

  1. PeerEurekaNode(PeerAwareInstanceRegistry registry, String targetHost, String serviceUrl,
  2. HttpReplicationClient replicationClient, EurekaServerConfig config,
  3. int batchSize, long maxBatchingDelayMs,
  4. long retrySleepTimeMs, long serverUnavailableSleepTimeMs) {
  5. this.registry = registry;
  6. this.targetHost = targetHost;
  7. this.replicationClient = replicationClient;
  8. this.serviceUrl = serviceUrl;
  9. this.config = config;
  10. this.maxProcessingDelayMs = config.getMaxTimeForReplication();
  11. String batcherName = getBatcherName();
  12. ReplicationTaskProcessor taskProcessor = new ReplicationTaskProcessor(targetHost, replicationClient);
  13. this.batchingDispatcher = TaskDispatchers.createBatchingTaskDispatcher(
  14. batcherName,
  15. config.getMaxElementsInPeerReplicationPool(),
  16. batchSize,
  17. config.getMaxThreadsForPeerReplication(),
  18. maxBatchingDelayMs,
  19. serverUnavailableSleepTimeMs,
  20. retrySleepTimeMs,
  21. taskProcessor
  22. );
  23. }

这里会实例化一个ReplicationTaskProcessor.java, 我们跟进去,发下它是实现TaskProcessor的,所以一定会执行此类中的process()方法,执行方法如下:

  1. public ProcessingResult process(List<ReplicationTask> tasks) {
  2. ReplicationList list = createReplicationListOf(tasks);
  3. try {
  4. EurekaHttpResponse<ReplicationListResponse> response = replicationClient.submitBatchUpdates(list);
  5. int statusCode = response.getStatusCode();
  6. if (!isSuccess(statusCode)) {
  7. if (statusCode == 503) {
  8. logger.warn("Server busy (503) HTTP status code received from the peer {}; rescheduling tasks after delay", peerId);
  9. return ProcessingResult.Congestion;
  10. } else {
  11. // Unexpected error returned from the server. This should ideally never happen.
  12. logger.error("Batch update failure with HTTP status code {}; discarding {} replication tasks", statusCode, tasks.size());
  13. return ProcessingResult.PermanentError;
  14. }
  15. } else {
  16. handleBatchResponse(tasks, response.getEntity().getResponseList());
  17. }
  18. } catch (Throwable e) {
  19. if (isNetworkConnectException(e)) {
  20. logNetworkErrorSample(null, e);
  21. return ProcessingResult.TransientError;
  22. } else {
  23. logger.error("Not re-trying this exception because it does not seem to be a network exception", e);
  24. return ProcessingResult.PermanentError;
  25. }
  26. }
  27. return ProcessingResult.Success;
  28. }

这里面是将List<ReplicationTask> tasks 通过submitBatchUpdate() 发送给server端。

server端在PeerReplicationResource.batchReplication()去处理,实际上就是循环调用ApplicationResource.addInstance() 方法,又回到了最开始注册的方法。

到此 EurekaServer同步的逻辑就结束了,这里主要是三层队列的数据结构很绕,通过一个batchList去批量同步数据的。

注意这里还有一个很重要的点,就是Client注册时调用addInstance()方法,这里到了server端PeerAwareInstanceRegistryImpl会执行同步其他EurekaServer逻辑。

而EurekaServer同步注册接口仍然会调用addInstance()方法,这里难不成就死循环调用了?当然不是,addInstance()中也有个参数:isReplication, 在最后调用server端方法的时候如下:registry.register(info, "true".equals(isReplication));

我们知道,EurekaClient在注册的时候isReplication传递为空,所以这里为false,而Server端同步的时候调用:

PeerReplicationResource:

  1. private static Builder handleRegister(ReplicationInstance instanceInfo, ApplicationResource applicationResource) {
  2. applicationResource.addInstance(instanceInfo.getInstanceInfo(), REPLICATION);
  3. return new Builder().setStatusCode(Status.OK.getStatusCode());
  4. }

这里的REPLICATION 为true

另外在AbstractJersey2EurekaHttpClient中发送register请求的时候,有个addExtraHeaders()方法,如下图:

如果是使用的Jersey2ReplicationClient发送的,那么header中的x-netflix-discovery-replication配置则为true,在后面执行注册的addInstance()方法中会接收这个参数的:

总结

仍然一图流,文中解析的内容都包含在这张图中了:

申明

本文章首发自本人博客:https://www.cnblogs.com/wang-meng 和公众号:壹枝花算不算浪漫,如若转载请标明来源!

感兴趣的小伙伴可关注个人公众号:壹枝花算不算浪漫

【一起学源码-微服务】Nexflix Eureka 源码十二:EurekaServer集群模式源码分析的更多相关文章

  1. 微服务管理平台nacos虚拟ip负载均衡集群模式搭建

    一.Nacos简介 Nacos是用于微服务管理的平台,其核心功能是服务注册与发现.服务配置管理. Nacos作为服务注册发现组件,可以替换Spring Cloud应用中传统的服务注册于发现组件,如:E ...

  2. 【一起学源码-微服务】Eureka+Ribbon+Feign阶段性总结

    前言 想说的话 这里已经梳理完Eureka.Ribbon.Feign三大组件的基本原理了,今天做一个总结,里面会有一个比较详细的调用关系流程图. 说明 原创不易,如若转载 请标明来源! 博客地址:一枝 ...

  3. Spring Boot + Spring Cloud 构建微服务系统(六):熔断监控集群(Turbine)

    Spring Cloud Turbine 上一章我们集成了Hystrix Dashboard,使用Hystrix Dashboard可以看到单个应用内的服务信息,显然这是不够的,我们还需要一个工具能让 ...

  4. spring cloud微服务快速教程之(十二) 分布式ID解决方案(mybatis-plus篇)

    0-前言 分布式系统中,分布式ID是个必须解决的问题点: 雪花算法是个好方式,不过不能直接使用,因为如果直接使用的话,需要配置每个实例workerId和datacenterId,在微服务中,实例一般动 ...

  5. .Net Core微服务——Consul(4):主从、集群

    延续上一篇的话题继续,顺便放上一篇的传送门:点这里. 集群的必要性 consul本身就是管理集群的,现在还需要给consul搞个集群,这是为啥?因为consul单点也容易挂啊!万一管理集群的consu ...

  6. 【Azure微服务 Service Fabric 】如何转移Service Fabric集群中的种子节点(Seed Node)

    注意:在对Service Fabric的节点做操作之前,请务必确认是否是种子节点(Seed Node)且当前节点的数量是否与SF的持久层要求的数量一致. 可靠性级别是 Service Fabric 群 ...

  7. 【一起学源码-微服务】Nexflix Eureka 源码十三:Eureka源码解读完结撒花篇~!

    前言 想说的话 [一起学源码-微服务-Netflix Eureka]专栏到这里就已经全部结束了. 实话实说,从最开始Eureka Server和Eureka Client初始化的流程还是一脸闷逼,到现 ...

  8. 【一起学源码-微服务】Nexflix Eureka 源码十:服务下线及实例摘除,一个client下线到底多久才会被其他实例感知?

    前言 前情回顾 上一讲我们讲了 client端向server端发送心跳检查,也是默认每30钟发送一次,server端接收后会更新注册表的一个时间戳属性,然后一次心跳(续约)也就完成了. 本讲目录 这一 ...

  9. 【一起学源码-微服务】Nexflix Eureka 源码五:EurekaClient启动要经历哪些艰难险阻?

    前言 在源码分析三.四都有提及到EurekaClient启动的一些过程.因为EurekaServer在集群模式下 自己本身就是一个client,所以之前初始化eurekaServerContext就有 ...

随机推荐

  1. SDUT-3331_数据结构实验之串三:KMP应用

    数据结构实验之串三:KMP应用 Time Limit: 1000 ms Memory Limit: 65536 KiB Problem Description 有n个小朋友,每个小朋友手里有一些糖块, ...

  2. C# 局部函数与事件

    本文告诉大家使用局部函数可能遇到的坑. 在以前,如果有一个事件public event EventHandler Foo和一个函数private void Program_Foo(object sen ...

  3. CNN输出维度的计算

    在 CNN 的一层中的 patch 中共享权重 w ,无论猫在图片的哪个位置都可以找到.   当我们试图识别一个猫的图片的时候,我们并不在意猫出现在哪个位置.无论是左上角,右下角,它在你眼里都是一只猫 ...

  4. Android教程-03 常见布局的总结

    常见的布局 视频建议采用超清模式观看, 欢迎点击订阅我的优酷 Android的图形用户界面是由多个View和ViewGroup构建出来的.View是通用的UI窗体小组件,比如按钮(Button)或者文 ...

  5. Codeforces Round #577 (Div 2)

    A. Important Exam 水题 #include<iostream> #include<string.h> #include<algorithm> #in ...

  6. xUtils框架的介绍(二)

    xUtils中有四大组件可以供我们使用,分别是ViewUtils.HttpUtils.BitmapUtils以及DbUtils.如果你没能先读一下我的上一篇文章,那么请你移步过去先整体了解一下,再回过 ...

  7. Java语法格式

    任何一种语言都有自己的语法规则,Java也一样,既然是规则,那么知道其如何使用就可以了. 代码都定义在类中,类由class来定义,区分 public class  和  class; 代码严格区分大小 ...

  8. xml 校验

    package sax.parsing; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoun ...

  9. H3C ACL包过滤显示与调试

  10. 怎么让FOXMAIL关了以后在右下角自动收取邮件

    1.缩小到任务栏:打开foxmail,在工具-系统设置-常规,选项中有一项最小化时在任务栏显示,勾选上即可.2.要自动收取邮件,选中邮件账户,右键打开菜单,属性-接收邮件,右边勾选上“每隔*分钟自动收 ...