工作原理
總體架構:
- 用戶在Portal操作配置發布
- Portal調用Admin Service的接口操作發布
- Admin Service發布配置后,發送ReleaseMessage給各個Config Service
- Config Service收到ReleaseMessage后,通知對應的客戶端
客戶端:
上圖簡要描述了Apollo客戶端的實現原理:
- 客戶端和服務端保持了一個長連接,從而能第一時間獲得配置更新的推送。(通過Http Long Polling實現)
- 客戶端還會定時從Apollo配置中心服務端拉取應用的最新配置。
- 這是一個fallback機制,為了防止推送機制失效導致配置不更新
- 客戶端定時拉取會上報本地版本,所以一般情況下,對於定時拉取的操作,服務端都會返回304 - Not Modified
- 定時頻率默認為每5分鍾拉取一次,客戶端也可以通過在運行時指定System Property: apollo.refreshInterval來覆蓋,單位為分鍾。
- 客戶端從Apollo配置中心服務端獲取到應用的最新配置后,會保存在內存中
- 客戶端會把從服務端獲取到的配置在本地文件系統緩存一份
- 在遇到服務不可用,或網絡不通的時候,依然能從本地恢復配置
- 應用程序可以從Apollo客戶端獲取最新的配置、訂閱配置更新通知
這里重點分析下apollo客戶端和后端服務的長輪詢機制的原理是啥
Apollo為什么用長輪詢而不是長連接?
1、長連采用HTTP而非TCP的主要考慮點是多語言的適配;
2、采用異步servlet,單機可以支撐1W個連接,也就是一萬個客戶端,一般10台服務器就能抗住一個中小型公司的連接數;
http://www.iocoder.cn/Apollo/config-service-notifications/?self
http://www.kailing.pub/article/index/arcid/163.html
長連接 / 長輪詢
長輪詢實際上就是在一個類似死循環里,不停請求 ConfigServer 的配置變化通知接口 notifications/v2,如果配置有變更,就會返回變更信息,然后向定時任務線程池提交一個任務,任務內容是執行 sync 方法。
在請求 ConfigServer 的時候,ConfigServer 使用了 Servlet 3 的異步特性,將 hold 住連接 30 秒,等到有通知就立刻返回,這樣能夠實現一個基於 HTTP 的長連接。
關於為什么使用 HTTP 長連接,初次接觸 Apollo 的人都會疑惑,為什么使用這種方式,而不是"那種"方式?
下面是作者宋順的回復:
總結一下:
- 為什么不使用消息系統?太復雜,殺雞用牛刀。
- 為什么不用 TCP 長連接?對網絡環境要求高,容易推送失敗。且有雙寫問題。
- 為什么使用 HTTP 長輪詢?性能足夠,結合 Servlet3 的異步特性,能夠維持萬級連接(一個客戶端只有一個長連接)。直接使用 Servlet 的 HTTP 協議,比單獨用 TCP 連接方便。HTTP 請求/響應模式,保證了不會出現雙寫的情況。最主要還是簡單,性能暫時不是瓶頸。
長連接 / 長輪詢
長輪詢實際上就是在一個類似死循環里,不停請求 ConfigServer 的配置變化通知接口 notifications/v2,如果配置有變更,就會返回變更信息,然后向定時任務線程池提交一個任務,任務內容是執行 sync 方法。
在請求 ConfigServer 的時候,ConfigServer 使用了 Servlet 3 的異步特性,將 hold 住連接 30 秒,等到有通知就立刻返回,這樣能夠實現一個基於 HTTP 的長連接。
關於為什么使用 HTTP 長連接,初次接觸 Apollo 的人都會疑惑,為什么使用這種方式,而不是"那種"方式?
下面是作者宋順的回復:
總結一下:
- 為什么不使用消息系統?太復雜,殺雞用牛刀。
- 為什么不用 TCP 長連接?對網絡環境要求高,容易推送失敗。且有雙寫問題。
- 為什么使用 HTTP 長輪詢?性能足夠,結合 Servlet3 的異步特性,能夠維持萬級連接(一個客戶端只有一個長連接)。直接使用 Servlet 的 HTTP 協議,比單獨用 TCP 連接方便。HTTP 請求/響應模式,保證了不會出現雙寫的情況。最主要還是簡單,性能暫時不是瓶頸。
總結
本文沒有貼很多的代碼。因為不是一篇源碼分析的文章。
總之,Apollo 的更新配置設計就是通過定時輪詢和長輪詢進行組合而來。
定時輪詢負責調用獲取配置接口,長輪詢負責調用配置更新通知接口,長輪詢得到結果后,將提交一個任務到定時輪詢線程池里,執行同步操作——也就是調用獲取配置接口。
為什么使用 HTTP 長輪詢? 簡單!簡單!簡單!
轉載於:https://www.cnblogs.com/stateis0/p/9255966.html
我們來看下這個對應的代碼http://www.iocoder.cn/Apollo/config-service-notifications/?self
https://juejin.cn/post/6844903733990703118
http://www.iocoder.cn/Apollo/config-service-notifications/?self
Apollo 3 — 定時/長輪詢拉取配置的設計

前言
如上圖所示,Apollo portal 更新配置后,進行輪詢的客戶端獲取更新通知,然后再調用接口獲取最新配置。不僅僅只有輪詢,還有定時更新(默認 5 分鍾一次)。目的就是讓客戶端能夠穩定的獲取到最新的配置。
一起來看看他的設計。
核心代碼
具體的類是 RemoteConfigRepository
,每一個 Config —— 也就是 namespace 都有一個 RemoteConfigRepository 對象,表示這個 Config 的遠程配置倉庫,可以利用這個倉庫請求遠程服務,得到配置。
RemoteConfigRepository 的構造方法需要一個 namespace
字符串,表明這個 Repository 所屬的 Config 名稱。
下面是該類的構造方法。
public RemoteConfigRepository(String namespace) { m_namespace = namespace;// Config 名稱 m_configCache = new AtomicReference<>(); // Config 引用 m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);// 單例的 config 配置,存放 application.properties m_httpUtil = ApolloInjector.getInstance(HttpUtil.class);// HTTP 工具 m_serviceLocator = ApolloInjector.getInstance(ConfigServiceLocator.class);// 遠程服務 URL 更新類 remoteConfigLongPollService = ApolloInjector.getInstance(RemoteConfigLongPollService.class);// 長輪詢服務 m_longPollServiceDto = new AtomicReference<>();// 長輪詢發現的當前配置發生變化的服務 m_remoteMessages = new AtomicReference<>(); m_loadConfigRateLimiter = RateLimiter.create(m_configUtil.getLoadConfigQPS());// 限流器 m_configNeedForceRefresh = new AtomicBoolean(true);// 是否強制刷新 m_loadConfigFailSchedulePolicy = new ExponentialSchedulePolicy(m_configUtil.getOnErrorRetryInterval(),//1 m_configUtil.getOnErrorRetryInterval() * 8);// 1 * 8;失敗定時重試策略: 最小一秒,最大 8 秒. gson = new Gson();// json 序列化 this.trySync(); // 第一次同步 this.schedulePeriodicRefresh();// 定時刷新 this.scheduleLongPollingRefresh();// 長輪詢刷新 }
可以看到,在構造方法中,就執行了 3 個本地方法,其中就包括定時刷新和長輪詢刷新。這兩個功能在 apollo 的 github 文檔中也有介紹:
1.客戶端和服務端保持了一個長連接,從而能第一時間獲得配置更新的推送。
2.客戶端還會定時從Apollo配置中心服務端拉取應用的最新配置。
3.這是一個fallback機制,為了防止推送機制失效導致配置不更新。
4.客戶端定時拉取會上報本地版本,所以一般情況下,對於定時拉取的操作,服務端都會返回304 - Not Modified。
5.定時頻率默認為每5分鍾拉取一次,客戶端也可以通過在運行時指定System Property: apollo.refreshInterval來覆蓋,單位為分鍾。
所以,長連接是更新配置的主要手段,然后用定時任務輔助長連接,防止長連接失敗。
那就看看長連接和定時任務的具體代碼。
定時任務
定時任務主要由一個單 core 的線程池維護這定時任務。
static { // 定時任務,單個 core. 后台線程 m_executorService = Executors.newScheduledThreadPool(1, ApolloThreadFactory.create("RemoteConfigRepository", true)); } private void schedulePeriodicRefresh() { // 默認 5 分鍾同步一次. m_executorService.scheduleAtFixedRate( new Runnable() { @Override public void run() { trySync(); } }, m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(),// 5 m_configUtil.getRefreshIntervalTimeUnit());//單位:分鍾 }
具體就是每 5 分鍾執行 sync 方法。我簡化了一下 sync 方法,一起看看:
protected synchronized void sync() { ApolloConfig previous = m_configCache.get(); // 加載遠程配置 ApolloConfig current = loadApolloConfig(); //reference equals means HTTP 304 if (previous != current) { m_configCache.set(current); // 觸發監聽器 this.fireRepositoryChange(m_namespace, this.getConfig()); } }
首先,拿到上一個 config 對象的引用,然后,加載遠程配置,判斷是否相等,如果不相等,更新引用緩存,觸發監聽器。
可以看出,關鍵是加載遠程配置和觸發監聽器,這兩個操作。
loadApolloConfig 方法主要邏輯就是通過 HTTP 請求從 configService 服務里獲取到配置。大概步驟如下:
- 首先限流。獲取服務列表。然后根據是否有更新通知,決定此次重試幾次,如果有更新,重試2次,反之一次。
- 優先請求通知自己的 configService,如果失敗了,就要進行休息,休息策略要看是否得到更新通知了,如果是,就休息一秒,否則按照 SchedulePolicy 策略來。
- 拿到數據后,重置強制刷新狀態和失敗休息狀態,返回配置。
觸發監聽器步驟:
- 循環遠程倉庫的監聽器,調用他們的 onRepositoryChange 方法。其實就是 Config。
- 然后,更新 Config 內部的引用,循環向線程池提交任務—— 執行 Config 監聽器的 onChange 方法。
好,到這里,定時任務就算處理完了,總之就是調用 sync 方法,請求遠程 configServer 服務,得到結果后,更新 Config 對象里的配置,並通知監聽器。
再來說說長輪詢。
長連接 / 長輪詢
長輪詢實際上就是在一個類似死循環里,不停請求 ConfigServer 的配置變化通知接口 notifications/v2,如果配置有變更,就會返回變更信息,然后向定時任務線程池提交一個任務,任務內容是執行 sync 方法。
在請求 ConfigServer 的時候,ConfigServer 使用了 Servlet 3 的異步特性,將 hold 住連接 30 秒,等到有通知就立刻返回,這樣能夠實現一個基於 HTTP 的長連接。
關於為什么使用 HTTP 長連接,初次接觸 Apollo 的人都會疑惑,為什么使用這種方式,而不是"那種"方式?
下面是作者宋順的回復:

總結一下:
- 為什么不使用消息系統?太復雜,殺雞用牛刀。
- 為什么不用 TCP 長連接?對網絡環境要求高,容易推送失敗。且有雙寫問題。
- 為什么使用 HTTP 長輪詢?性能足夠,結合 Servlet3 的異步特性,能夠維持萬級連接(一個客戶端只有一個長連接)。直接使用 Servlet 的 HTTP 協議,比單獨用 TCP 連接方便。HTTP 請求/響應模式,保證了不會出現雙寫的情況。最主要還是簡單,性能暫時不是瓶頸。
總結
本文沒有貼很多的代碼。因為不是一篇源碼分析的文章。
總之,Apollo 的更新配置設計就是通過定時輪詢和長輪詢進行組合而來。
定時輪詢負責調用獲取配置接口,長輪詢負責調用配置更新通知接口,長輪詢得到結果后,將提交一個任務到定時輪詢線程池里,執行同步操作——也就是調用獲取配置接口。
為什么使用 HTTP 長輪詢? 簡單!簡單!簡單!
https://blog.csdn.net/xiao_jun_0820/article/details/82956593
通過spring提供的DeferredResult實現長輪詢服務端推送消息
DeferredResult字面意思就是推遲結果,是在servlet3.0以后引入了異步請求之后,spring封裝了一下提供了相應的支持,也是一個很老的特性了。DeferredResult可以允許容器線程快速釋放以便可以接受更多的請求提升吞吐量,讓真正的業務邏輯在其他的工作線程中去完成。
最近再看apollo配置中心的實現原理,apollo的發布配置推送變更消息就是用DeferredResult實現的,apollo客戶端會像服務端發送長輪訓http請求,超時時間60秒,當超時后返回客戶端一個304 httpstatus,表明配置沒有變更,客戶端繼續這個步驟重復發起請求,當有發布配置的時候,服務端會調用DeferredResult.setResult返回200狀態碼,然后輪訓請求會立即返回(不會超時),客戶端收到響應結果后,會發起請求獲取變更后的配置信息。
下面我們自己寫一個簡單的demo來演示這個過程
springboot啟動類:
/** * 功能說明: * 功能作者: * 創建日期: * 版權歸屬:每特教育|螞蟻課堂所有 www.itmayiedu.com */ package com.itmayiedu.api.controller; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig; /** * 功能說明: <br> * 創建作者:每特教育-余勝軍<br> * 創建時間:2018年8月28日 下午9:09:14<br> * 教育機構:每特教育|螞蟻課堂<br> * 版權說明:上海每特教育科技有限公司版權所有<br> * 官方網站:www.itmayiedu.com|www.meitedu.com<br> * 聯系方式:qq644064779<br> * 注意:本內容有每特教育學員共同研發,請尊重原創版權 */ //@EnableApolloConfig({"application", "spring-rocketmq","spring-redis"}) @SpringBootApplication public class App implements WebMvcConfigurer { public static void main(String[] args) { SpringApplication.run(App.class, args); } @Bean public ThreadPoolTaskExecutor mvcTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setQueueCapacity(100); executor.setMaxPoolSize(25); return executor; } //配置異步支持,設置了一個用來異步執行業務邏輯的工作線程池,設置了默認的超時時間是60秒 @Override public void configureAsyncSupport(AsyncSupportConfigurer configurer) { configurer.setTaskExecutor(mvcTaskExecutor()); configurer.setDefaultTimeout(60000L); } }
首先我來看下啟動類實現implements WebMvcConfigurer,在里面模擬創建了一個線程池,這個線程池用來模擬apollo的configserver的進程,在線程池中設置超時時間為60秒,等價於apollo的客戶端長輪詢confisevice,configserverhold住線程60秒
我們來看下controller類
package com.itmayiedu.api.controller; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import java.util.Collection; @RestController public class ApolloController { private final Logger logger = LoggerFactory.getLogger(this.getClass()); //guava中的Multimap,多值map,對map的增強,一個key可以保持多個value private Multimap<String, DeferredResult<String>> watchRequests = Multimaps.synchronizedSetMultimap(HashMultimap.create()); //模擬長輪詢 @RequestMapping(value = "/watch/{namespace}", method = RequestMethod.GET, produces = "text/html") public DeferredResult<String> watch(@PathVariable("namespace") String namespace) { logger.info("Request received"); DeferredResult<String> deferredResult = new DeferredResult<>(); //當deferredResult完成時(不論是超時還是異常還是正常完成),移除watchRequests中相應的watch key deferredResult.onCompletion(new Runnable() { @Override public void run() { System.out.println("remove key:" + namespace); watchRequests.remove(namespace, deferredResult); } }); watchRequests.put(namespace, deferredResult); logger.info("Servlet thread released"); return deferredResult; } //模擬發布namespace配置 @RequestMapping(value = "/publish/{namespace}", method = RequestMethod.GET, produces = "text/html") public Object publishConfig(@PathVariable("namespace") String namespace) { if (watchRequests.containsKey(namespace)) { Collection<DeferredResult<String>> deferredResults = watchRequests.get(namespace); Long time = System.currentTimeMillis(); //通知所有watch這個namespace變更的長輪訓配置變更結果 for (DeferredResult<String> deferredResult : deferredResults) { deferredResult.setResult(namespace + " changed:" + time); } } return "success"; } //模擬發布namespace配置 @RequestMapping(value = "/hello", method = RequestMethod.GET, produces = "text/html") public Object publishConfig2() { return "success"; } }
在這個control控制的業務類中,有兩個接口,這里使用到了servlet 3.0 異步響應的DeferredResult技術
當訪問/watch/{namespace}接口的時候,DeferredResult技術中會把當前的客戶端訪問的線程hold住,如果DeferredResult中保存的值有變化會通過http立刻返回給調用方,如果DeferredResult中保存的值沒有發送變化,會一直hold住線程60秒,直到發生網絡超時
當外部訪問"/publish/{namespace}接口的時候,就是更新DeferredResult中保存的值,key為外部訪問傳入的namespace,value為當前對應的時間,如果外部更新了namespace對應的值,那么DeferredResult會立刻釋放hold住的線程給客戶端
apolloconfigserver設計的時候,當管理員更新了配置的時候,會更新releaseMessage這張表,configserver進程會每隔60秒掃描releaseMessage這張表是否有變化,一旦存在變化,就會觸發DeferredResult中保存的對應的namespace,會立刻通過apollo的客戶端,這也就是apollo的客戶端能夠1秒鍾能夠實時收到響應的原因
如果在hold住線程60秒內,配置的值沒有發送變化,就會拋出網絡異常,我們需要對整個網絡異常進行攔截處理,返回給請求的客戶端一個http狀態碼為304的狀態碼,表示整個配置項沒有發生變化
package com.itmayiedu.api.controller; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.context.request.async.AsyncRequestTimeoutException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @ControllerAdvice class GlobalControllerExceptionHandler { protected static final Logger logger = LoggerFactory.getLogger(GlobalControllerExceptionHandler.class); @ResponseStatus(HttpStatus.NOT_MODIFIED)//返回304狀態碼 @ResponseBody @ExceptionHandler(AsyncRequestTimeoutException.class) //捕獲特定異常 public void handleAsyncRequestTimeoutException(AsyncRequestTimeoutException e, HttpServletRequest request) { System.out.println("handleAsyncRequestTimeoutException"); } }
然后我們通過postman工具發送請求http://localhost:8080/watch/mynamespace,請求會掛起,60秒后,DeferredResult超時,客戶端正常收到了304狀態碼,表明在這個期間配置沒有變更過。
然后我們在模擬配置變更的情況,再次發起請求http://localhost:8080/watch/mynamespace,等待個10秒鍾(不要超過60秒),然后調用http://localhost:8080/publish/mynamespace,發布配置變更。這時postman會立刻收到response響應結果:
mynamespace changed:1538880050147
表明在輪訓期間有配置變更過。
這里我們用了一個MultiMap來存放所有輪訓的請求,Key對應的是namespace,value對應的是所有watch這個namespace變更的異步請求DeferredResult,需要注意的是:在DeferredResult完成的時候記得移除MultiMap中相應的key,避免內存溢出請求。
采用這種長輪詢的好處是,相比一直循環請求服務器,實例一多的話會對服務器產生很大的壓力,http長輪詢的方式會在服務器變更的時候主動推送給客戶端,其他時間客戶端是掛起請求的,這樣同時滿足了性能和實時性。
整個代碼相當的經典呀,整個代碼的下載地址為如下
https://blog.csdn.net/kouwoo/article/details/83898788
設置springboot自帶tomcat的最大連接數和最大並發數
從源代碼來看,最大連接數和最大並發數默認是10000和200
可以通過工程下的application.yml配置文件來改變這個值
server:
tomcat:
uri-encoding: UTF-8
max-threads: 1000
max-connections: 20000
默認情況下apollo的長輪詢是基於http的異步響應的,一個tomcat的默認最大連接數是10000,所以一個configservice進程支持最大10000個連接是沒有問題的。