apollo客戶端的長輪詢機制的原理


工作原理

總體架構:

 

 

 

  1. 用戶在Portal操作配置發布
  2. Portal調用Admin Service的接口操作發布
  3. Admin Service發布配置后,發送ReleaseMessage給各個Config Service
  4. Config Service收到ReleaseMessage后,通知對應的客戶端

 

客戶端:

 

 

上圖簡要描述了Apollo客戶端的實現原理:

  1. 客戶端和服務端保持了一個長連接,從而能第一時間獲得配置更新的推送。(通過Http Long Polling實現)
  2. 客戶端還會定時從Apollo配置中心服務端拉取應用的最新配置。
  • 這是一個fallback機制,為了防止推送機制失效導致配置不更新
  • 客戶端定時拉取會上報本地版本,所以一般情況下,對於定時拉取的操作,服務端都會返回304 - Not Modified
  • 定時頻率默認為每5分鍾拉取一次,客戶端也可以通過在運行時指定System Property: apollo.refreshInterval來覆蓋,單位為分鍾。
  1. 客戶端從Apollo配置中心服務端獲取到應用的最新配置后,會保存在內存中
  2. 客戶端會把從服務端獲取到的配置在本地文件系統緩存一份
  • 在遇到服務不可用,或網絡不通的時候,依然能從本地恢復配置
  1. 應用程序可以從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

apollo關於配置變更通知這塊,主要分為了2個部分:
1.  如何發現配置變更?  apollo解耦了「配置變更」和「配置變更發布」, 配置變更就是一個數據庫操作,除了保存本身的配置信息,還會新增保存一個「ReleaseMessage」到數據庫, 表示有變更發生, config server中的一個線程,每隔1S掃描「ReleaseMessage」表, 以此發現變更的配置。
2. 如何通知客戶端? 使用的是http的長輪詢, 客戶端在啟動后,會連接cofig server, 連接90秒超時, config server收到請求會hang到服務端不返回, 如果90秒內沒有配置變更,就自動返回, 如果在90秒內有配置變更,請求會立刻返回,並且攜帶namespace。 客戶端收到請求后,會立刻重新請求服務端。

長連接 / 長輪詢

長輪詢實際上就是在一個類似死循環里,不停請求 ConfigServer 的配置變化通知接口 notifications/v2,如果配置有變更,就會返回變更信息,然后向定時任務線程池提交一個任務,任務內容是執行 sync 方法。

在請求 ConfigServer 的時候,ConfigServer 使用了 Servlet 3 的異步特性,將 hold 住連接 30 秒,等到有通知就立刻返回,這樣能夠實現一個基於 HTTP 的長連接。

關於為什么使用 HTTP 長連接,初次接觸 Apollo 的人都會疑惑,為什么使用這種方式,而不是"那種"方式?

下面是作者宋順的回復:
1240

總結一下:

  1. 為什么不使用消息系統?太復雜,殺雞用牛刀。
  2. 為什么不用 TCP 長連接?對網絡環境要求高,容易推送失敗。且有雙寫問題。
  3. 為什么使用 HTTP 長輪詢?性能足夠,結合 Servlet3 的異步特性,能夠維持萬級連接(一個客戶端只有一個長連接)。直接使用 Servlet 的 HTTP 協議,比單獨用 TCP 連接方便。HTTP 請求/響應模式,保證了不會出現雙寫的情況。最主要還是簡單,性能暫時不是瓶頸。

長連接 / 長輪詢

長輪詢實際上就是在一個類似死循環里,不停請求 ConfigServer 的配置變化通知接口 notifications/v2,如果配置有變更,就會返回變更信息,然后向定時任務線程池提交一個任務,任務內容是執行 sync 方法。

在請求 ConfigServer 的時候,ConfigServer 使用了 Servlet 3 的異步特性,將 hold 住連接 30 秒,等到有通知就立刻返回,這樣能夠實現一個基於 HTTP 的長連接。

關於為什么使用 HTTP 長連接,初次接觸 Apollo 的人都會疑惑,為什么使用這種方式,而不是"那種"方式?

下面是作者宋順的回復:
1240

總結一下:

  1. 為什么不使用消息系統?太復雜,殺雞用牛刀。
  2. 為什么不用 TCP 長連接?對網絡環境要求高,容易推送失敗。且有雙寫問題。
  3. 為什么使用 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關於配置變更通知這塊,主要分為了2個部分:
1.  如何發現配置變更?  apollo解耦了「配置變更」和「配置變更發布」, 配置變更就是一個數據庫操作,除了保存本身的配置信息,還會新增保存一個「ReleaseMessage」到數據庫, 表示有變更發生, config server中的一個線程,每隔1S掃描「ReleaseMessage」表, 以此發現變更的配置。
2. 如何通知客戶端? 使用的是http的長輪詢, 客戶端在啟動后,會連接cofig server, 連接90秒超時, config server收到請求會hang到服務端不返回, 如果90秒內沒有配置變更,就自動返回, 如果在90秒內有配置變更,請求會立刻返回,並且攜帶namespace。 客戶端收到請求后,會立刻重新請求服務端。
http://www.kailing.pub/article/index/arcid/163.html
https://www.jianshu.com/p/da11fcc4e8e6

Apollo 3 — 定時/長輪詢拉取配置的設計

2018.06.30 12:21:28字數 1,435閱讀 989
 
Apollo 基礎模型

前言

如上圖所示,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 服務里獲取到配置。大概步驟如下:

  1. 首先限流。獲取服務列表。然后根據是否有更新通知,決定此次重試幾次,如果有更新,重試2次,反之一次。
  2. 優先請求通知自己的 configService,如果失敗了,就要進行休息,休息策略要看是否得到更新通知了,如果是,就休息一秒,否則按照 SchedulePolicy 策略來。
  3. 拿到數據后,重置強制刷新狀態和失敗休息狀態,返回配置。

觸發監聽器步驟:

  1. 循環遠程倉庫的監聽器,調用他們的 onRepositoryChange 方法。其實就是 Config。
  2. 然后,更新 Config 內部的引用,循環向線程池提交任務—— 執行 Config 監聽器的 onChange 方法。

好,到這里,定時任務就算處理完了,總之就是調用 sync 方法,請求遠程 configServer 服務,得到結果后,更新 Config 對象里的配置,並通知監聽器。

再來說說長輪詢。

長連接 / 長輪詢

長輪詢實際上就是在一個類似死循環里,不停請求 ConfigServer 的配置變化通知接口 notifications/v2,如果配置有變更,就會返回變更信息,然后向定時任務線程池提交一個任務,任務內容是執行 sync 方法。

在請求 ConfigServer 的時候,ConfigServer 使用了 Servlet 3 的異步特性,將 hold 住連接 30 秒,等到有通知就立刻返回,這樣能夠實現一個基於 HTTP 的長連接。

關於為什么使用 HTTP 長連接,初次接觸 Apollo 的人都會疑惑,為什么使用這種方式,而不是"那種"方式?

下面是作者宋順的回復:


 

總結一下:

  1. 為什么不使用消息系統?太復雜,殺雞用牛刀。
  2. 為什么不用 TCP 長連接?對網絡環境要求高,容易推送失敗。且有雙寫問題。
  3. 為什么使用 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個連接是沒有問題的。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM