微服務系列(二)(1) Eureka源碼分析

微服務系列(二)(1) Eureka源碼分析

關於eureka的使用,就不做介紹了,不熟悉的可以參考官方文檔

引入依賴,修改好配置文件,在主類上加上註解@EnableEurekaServer,啓動服務,一個簡單的eureka搭建好了。

先看看@EnableEurekaServer

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EurekaServerMarkerConfiguration.class)
public @interface EnableEurekaServer {

}

繼續EurekaServerMarkerConfiguration.class

@Configuration
public class EurekaServerMarkerConfiguration {

   @Bean
   public Marker eurekaServerMarkerBean() {
      return new Marker();
   }

   class Marker {
   }
}

這裏的Marker內部類並沒有具體的實現邏輯,因爲它僅僅是一個“標記”,用於判斷“真正幹活的類”是否要加載。

以後在Spring Cloud的源碼探索過程會常常看到,可以加入到自己的“技能庫”。

別走丟,真正幹活的類在這裏。

org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration

關注其上的幾個眼熟的類:

@Configuration
@Import(EurekaServerInitializerConfiguration.class)
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
@EnableConfigurationProperties({ EurekaDashboardProperties.class,
		InstanceRegistryProperties.class })
@PropertySource("classpath:/eureka/server.properties")
  • EurekaServerInitializerConfiguration.class:用於eureka初始化,別急,等會進入看看詳請,看看它到底初始化了什麼,如何初始化,以哪種順序
  • EurekaServerMarkerConfiguration.Marker.class:熟悉嗎,剛剛說到的"標記"
  • EurekaDashboardProperties.class:控制檯配置信息,常見的*Properties類,不做介紹
  • InstanceRegistryProperties.class:註冊中心相關配置信息,可以無需關心,和配置文件中的配置內容不同
  • @PropertySource("classpath:/eureka/server.properties"):原來eureka提供了properties的配置方式

這裏順帶提一下EurekaServerConfigBean這個類纔是跟我們經常打交道的配置bean,不要和InstanceRegistryProperties混淆。

找到了eureka入口,那麼用問題來驅動看源碼的方向。

問題1:如何解決單點故障問題

關注RefreshablePeerEurekaNodes這個類

它有以下方法:

shouldUpdate:判斷是否刷新PeerEurekaNodes

onApplicationEvent:Spring事件監聽器,當觸發EnvironmentChangeEvent時,判斷shouldUpdate,並選擇是否update(com.netflix.eureka.cluster.PeerEurekaNodes#updatePeerEurekaNodes

更新PeerEurekaNodes的邏輯

protected void updatePeerEurekaNodes(List<String> newPeerUrls) {
    if (newPeerUrls.isEmpty()) {
        logger.warn("The replica size seems to be empty. Check the route 53 DNS Registry");
        return;
    }

    Set<String> toShutdown = new HashSet<>(peerEurekaNodeUrls);
    toShutdown.removeAll(newPeerUrls);
    Set<String> toAdd = new HashSet<>(newPeerUrls);
    toAdd.removeAll(peerEurekaNodeUrls);

    if (toShutdown.isEmpty() && toAdd.isEmpty()) { // No change
        return;
    }

    // Remove peers no long available
    List<PeerEurekaNode> newNodeList = new ArrayList<>(peerEurekaNodes);

    if (!toShutdown.isEmpty()) {
        logger.info("Removing no longer available peer nodes {}", toShutdown);
        int i = 0;
        while (i < newNodeList.size()) {
            PeerEurekaNode eurekaNode = newNodeList.get(i);
            if (toShutdown.contains(eurekaNode.getServiceUrl())) {
                newNodeList.remove(i);
                eurekaNode.shutDown();
            } else {
                i++;
            }
        }
    }

    // Add new peers
    if (!toAdd.isEmpty()) {
        logger.info("Adding new peer nodes {}", toAdd);
        for (String peerUrl : toAdd) {
            newNodeList.add(createPeerEurekaNode(peerUrl));
        }
    }

    this.peerEurekaNodes = newNodeList;
    this.peerEurekaNodeUrls = new HashSet<>(newPeerUrls);
}

邏輯很簡單,更新peerEurekaNodeUrlspeerEurekaNodes,先創建副本,在副本中移除舊節點再新增節點,最後用副本替換原集合,很常見的線程安全的編程方式,優點在於邏輯簡單、線程安全、效率高,缺點在於浪費內存。

peerEurekaNodeUrls:同伴節點(暫時這樣翻譯吧,挺好理解的)的url地址

peerEurekaNodes:同伴節點的詳細信息,包括serviceUrl、config等信息,其中還有PeerAwareInstanceRegistryHttpReplicationClientTaskDispatcher這三個重要的工作類,暫且不看

先看看com.netflix.eureka.cluster.PeerEurekaNodes#start中有一段很重要的代碼

Runnable peersUpdateTask = new Runnable() {
                @Override
                public void run() {
                    try {
                        updatePeerEurekaNodes(resolvePeerUrls());
                    } catch (Throwable e) {
                        logger.error("Cannot update the replica Nodes", e);
                    }

                }
            };
taskExecutor.scheduleWithFixedDelay(
        peersUpdateTask,
        serverConfig.getPeerEurekaNodesUpdateIntervalMs(),
        serverConfig.getPeerEurekaNodesUpdateIntervalMs(),
        TimeUnit.MILLISECONDS
);

可以發現,在PeerEurekaNodes調用start()方法後,會創建一個守護進程來定期更新同伴節點的信息,至於頻率怎樣,當然是提供了配置入口,在EurekaServerConfigBean可以找到默認是10分鐘。

繼續進入peerEurekaNode

去看看PeerAwareInstanceRegistryHttpReplicationClientTaskDispatcher做了什麼重要的工作

這裏的PeerAwareInstanceRegistry是通過@inject註解注入,想了解@inject可以參考JSR330規範,注入過程就不說了,並不是本文重點。

PeerAwareInstanceRegistry作爲一個接口,需要找到其實現類,在EurekaServerAutoConfiguration中:

@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());
}

PeerAwareInstanceRegistry提供了以下方法:

init:做一些初始化的工作,用於從遠程區域獲取註冊表信息及增量更新的RemoteRegionRegistry,以及用Monitors爲該類提供JMS遠程監控服務等

syncUp:從同伴節點拉取註冊表信息,這樣如果出現單節點故障問題時,會訪問到其他存活的同伴節點上,以此來保證eureka的高可用性

shouldAllowAccess:檢查此時是否允許註冊表的訪問

register:主要做了兩件事,1. 註冊節點信息 2.如果不是副本節點,則同步到同伴節點上

statusUpdate:更新實例的狀態,並同步到同伴節點上

初始化過程中有一個這樣的方法需要關注

private void scheduleRenewalThresholdUpdateTask() {
    timer.schedule(new TimerTask() {
                       @Override
                       public void run() {
                           updateRenewalThreshold();
                       }
                   }, serverConfig.getRenewalThresholdUpdateIntervalMs(),
            serverConfig.getRenewalThresholdUpdateIntervalMs());
}

可以看到它是一個定時任務線程,它是爲了防止在網絡分區故障下,失效大量實例而導致服務大面積癱瘓,它通過調整更新閾值來控制短時間內大量實例的狀態變更。

現在重新進入PeerEurekaNode,關注這樣一個方法com.netflix.eureka.cluster.PeerEurekaNode#syncInstancesIfTimestampDiffers

private void syncInstancesIfTimestampDiffers(String appName, String id, InstanceInfo info, InstanceInfo infoFromPeer) {
    try {
        if (infoFromPeer != null) {
            logger.warn("Peer wants us to take the instance information from it, since the timestamp differs,"
                    + "Id : {} My Timestamp : {}, Peer's timestamp: {}", id, info.getLastDirtyTimestamp(), infoFromPeer.getLastDirtyTimestamp());

            if (infoFromPeer.getOverriddenStatus() != null && !InstanceStatus.UNKNOWN.equals(infoFromPeer.getOverriddenStatus())) {
                logger.warn("Overridden Status info -id {}, mine {}, peer's {}", id, info.getOverriddenStatus(), infoFromPeer.getOverriddenStatus());
                registry.storeOverriddenStatusIfRequired(appName, id, infoFromPeer.getOverriddenStatus());
            }
            registry.register(infoFromPeer, true);
        }
    } catch (Throwable e) {
        logger.warn("Exception when trying to set information from peer :", e);
    }
}

當發現同伴eureka實例的時間戳與本地不同時,從同伴節點同步信息並設置最新的重寫狀態(Overridden Status: 通常由外部進程設置以禁用實例獲取流量)。

可以發現,PeerAwareInstanceRegistry主要工作內容是與同伴節點進行信息的交互,包括從同伴節點同步信息、推送新節點信息到同伴節點等。

下面來了解一下HttpReplicationClient,先看看它的API,大致做哪些事

register/cancel/statusUpdate/sendHeartBeat…等

感覺和PeerAwareInstanceRegistry比較相似…

打開它的實現類,只有一個com.netflix.eureka.transport.JerseyReplicationClient

原來它是爲了支持Jersey框架的通訊類,至於什麼是Jersey

來看看PeerEurekaNode中的最後一個工作的類TaskDispatcher

先看看PeerEurekaNode是如何使用它的

public void register(final InstanceInfo info) throws Exception {
    long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
    batchingDispatcher.process(
            taskId("register", info),
            new InstanceReplicationTask(targetHost, Action.Register, info, null, true) {
                public EurekaHttpResponse<Void> execute() {
                    return replicationClient.register(info);
                }
            },
            expiryTime
    );
}
public void cancel(final String appName, final String id) throws Exception {
    long expiryTime = System.currentTimeMillis() + maxProcessingDelayMs;
    batchingDispatcher.process(
            taskId("cancel", appName, id),
            new InstanceReplicationTask(targetHost, Action.Cancel, appName, id) {
                @Override
                public EurekaHttpResponse<Void> execute() {
                    return replicationClient.cancel(appName, id);
                }

                @Override
                public void handleFailure(int statusCode, Object responseEntity) throws Throwable {
                    super.handleFailure(statusCode, responseEntity);
                    if (statusCode == 404) {
                        logger.warn("{}: missing entry.", getTaskName());
                    }
                }
            },
            expiryTime
    );
}

看起來像是一個分發器,並且在PeerEurekaNode內部有batchDispachernobatchDispacher兩種,初步推測是一種通用api,將register/cancel/heartbeat等操作抽象,通過調用相同的API來執行操作。

慢慢看

this.batchingDispatcher = TaskDispatchers.createBatchingTaskDispatcher(
        batcherName,
        config.getMaxElementsInPeerReplicationPool(),
        batchSize,
        config.getMaxThreadsForPeerReplication(),
        maxBatchingDelayMs,
        serverUnavailableSleepTimeMs,
        retrySleepTimeMs,
        taskProcessor
);
this.nonBatchingDispatcher = TaskDispatchers.createNonBatchingTaskDispatcher(
        targetHost,
        config.getMaxElementsInStatusReplicationPool(),
        config.getMaxThreadsForStatusReplication(),
        maxBatchingDelayMs,
        serverUnavailableSleepTimeMs,
        retrySleepTimeMs,
        taskProcessor
);
public static <ID, T> TaskDispatcher<ID, T> createBatchingTaskDispatcher(String id,
                                                                         int maxBufferSize,
                                                                         int workloadSize,
                                                                         int workerCount,
                                                                         long maxBatchingDelay,
                                                                         long congestionRetryDelayMs,
                                                                         long networkFailureRetryMs,
                                                                         TaskProcessor<T> taskProcessor) {
    final AcceptorExecutor<ID, T> acceptorExecutor = new AcceptorExecutor<>(
            id, maxBufferSize, workloadSize, maxBatchingDelay, congestionRetryDelayMs, networkFailureRetryMs
    );
    final TaskExecutors<ID, T> taskExecutor = TaskExecutors.batchExecutors(id, workerCount, taskProcessor, acceptorExecutor);
    return new TaskDispatcher<ID, T>() {
        @Override
        public void process(ID id, T task, long expiryTime) {
            acceptorExecutor.process(id, task, expiryTime);
        }

        @Override
        public void shutdown() {
            acceptorExecutor.shutdown();
            taskExecutor.shutdown();
        }
    };
}

看來它實際上就是一個調用框架,作爲分發器,實際上最終邏輯就是調用:

acceptorExecutor.process(id, task, expiryTime);

進入com.netflix.eureka.util.batcher.AcceptorExecutor

這裏代碼比較多,就不貼了,感興趣的可以打開idea去了解下

它是一個任務分配器,由一個工作線程分配,可以有批量分配和單獨分配兩種方式,並且它能在舊版本未分派出去的情況下有新版本的任務進入時替換舊版本的任務,且當任務執行失敗時會將任務放回。

這樣的設計方式是值得借鑑的,把不同的任務抽象成task,交給單獨的線程處理,在調用時無需關心其實現且使用統一的API,達到了解耦的效果。

到這裏,可以回答這幾個問題了:

  1. 如何解決單點故障問題
  2. 如何保證信息不丟失
  3. 通訊方式是怎樣的,效率如何

問題1:

eureka使用了同伴節點的方式,當一個eureka初始化時,會主動拉取已在線的eureka的信息,包括eureka節點信息、配置信息、註冊表信息等,並定期更新是否有新的eureka節點加入,並進行增量的信息同步。當一個節點發生故障時,會將其移除本地維護的在線列表,並繼續與其他節點交互,這樣在eureka集羣下,即使eureka節點大量癱瘓,只要有一個eureka存活,就可以提供服務,但也會存在數據丟失、不同步的問題。

問題2:

eureka保證信息不丟失的方式是”弱一致性“的,eureka通過在同伴節點上冗餘自己的節點信息來”儘量保證“數據不丟失,在同步過程中依然可以對外提供服務,因爲理論上是會存在節點數據未同步成功就出現服務器宕機的情況,可以看到,eureka在CAP的抉擇中選擇了AP,對數據一致性的要求降低了許多。

問題3:

eureka節點間是採用http協議通信,從JerseyReplicationClient類可以看到,它還支持了rest風格Jersey框架,http協議的特點在於無狀態性,這也是eureka無法保證數據強一致性的原因之一。

關於問題3.信息是如何存儲的,關注這個類AbstractInstanceRegistry,不要覺得陌生,它其實是org.springframework.cloud.netflix.eureka.server.InstanceRegistry的父抽象類,而InstanceRegistry則是com.netflix.eureka.registry.PeerAwareInstanceRegistry的注入實現類

private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
        = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
protected Map<String, RemoteRegionRegistry> regionNameVSRemoteRegistry = new HashMap<String, RemoteRegionRegistry>();
protected final ConcurrentMap<String, InstanceStatus> overriddenInstanceStatusMap = CacheBuilder
        .newBuilder().initialCapacity(500)
        .expireAfterAccess(1, TimeUnit.HOURS)
        .<String, InstanceStatus>build().asMap();

通過前文的深入,這幾個類應該比較熟悉了,InstanceInfo實例信息/RemoteRegionRegistry遠程區域註冊中心/InstanceStatus實例狀態

可以知道的是,eureka完全採用內存存儲信息,且都是JVM內存,所以如果用eureka支持大量的服務時,一定要調整好JVM堆參數,防止內存溢出。

對於問題5.功能是否豐富,從編碼上可以看到一些靈活的配置項,本質上其功能僅有服務發現、服務註冊、限流,如:

renewalThresholdUpdateIntervalMs:刷新閾值更新間隔時間

peerEurekaNodesUpdateIntervalMs:同伴節點更新間隔時間

peerEurekaStatusRefreshTimeIntervalMs:同伴節點狀態刷新超時間隔

waitTimeInMsWhenSyncEmpty:同步到空信息時的等待時間

…由於eureka的運行機制,大量運用”輪詢",有很多時間上的配置

另外:

rateLimiterEnabled用於流控

org.springframework.cloud.netflix.eureka.server.EurekaDashboardProperties配置控制檯

以上均爲eureka-server端配置信息org.springframework.cloud.netflix.eureka.server.EurekaServerConfigBean,client配置信息請參考org.springframework.cloud.netflix.eureka.EurekaClientConfigBean

到這裏,這些問題都有了答案,但看源碼的路並沒有到盡頭,最後去了解一下eureka的初始化過程吧。

不要忘了這個類org.springframework.cloud.netflix.eureka.server.EurekaServerInitializerConfiguration

它實現了這三個接口ServletContextAware, SmartLifecycle, Ordered,這意味着它用到了ServletContext並把自己這個bean交給了Spring來管理它的生命週期,由Spring來初始化和銷燬它。

public void start() {
   new Thread(new Runnable() {
      @Override
      public void run() {
         try {
            //TODO: is this class even needed now?
            eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext);
            log.info("Started Eureka Server");

            publish(new EurekaRegistryAvailableEvent(getEurekaServerConfig()));
            EurekaServerInitializerConfiguration.this.running = true;
            publish(new EurekaServerStartedEvent(getEurekaServerConfig()));
         }
         catch (Exception ex) {
            // Help!
            log.error("Could not initialize Eureka servlet context", ex);
         }
      }
   }).start();
}

代碼很簡單,只做了三件事,一、使用eurekaServerBootstrap引導上下文的初始化,二、發佈eureka spring事件,三、修改初始化Bean的狀態

public void contextInitialized(ServletContext context) {
   try {
      initEurekaEnvironment();
      initEurekaServerContext();

      context.setAttribute(EurekaServerContext.class.getName(), this.serverContext);
   }
   catch (Throwable e) {
      log.error("Cannot bootstrap eureka server :", e);
      throw new RuntimeException("Cannot bootstrap eureka server :", e);
   }
}

eureka上下文初始化過程:1.環境初始化 2.eureka服務上下文初始化 3.把eureka服務上下文交給servletContext

環境初始化:設置dataCenter和environment值

服務上下文初始化:JSON解析器、XML解析器初始化、構建EurekaServerContextHolder、從同伴節點同步信息、註冊JMX等遠程服務

那麼這次的源碼追蹤就結束了,關於一些細節上的實現,如線程模型、調度規則、流控規則、AWS支持等,如果不是工作內容是做類似中間件的話,很難遇到相關問題,那就等真正遇到問題的時候再去驅動源碼追蹤吧。

無論如何,這次源碼追蹤也是有不少的收穫,也相信讀者也清楚的認識到了eureka的工作原理及存儲方式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章