SpringCloud實戰4-Hystrix線程隔離&請求緩存&請求合併

接着上一篇的Hystrix進行進一步瞭解。git

當系統用戶不斷增加時,每一個微服務須要承受的併發壓力也愈來愈大,在分佈式環境中,一般壓力來自對依賴服務的調用,由於親戚依賴服務的資源須要經過通訊來實現,這樣的依賴方式比起進程內的調用方式會引發一部分的性能損失,github

在高併發的場景下,Hystrix 提供了請求緩存的功能,咱們能夠方便的開啓和使用請求緩存來優化系統,達到減輕高併發時的請求線程消耗、下降請求響應時間的效果spring

Hystrix的緩存,這個功能是有點雞肋的,由於這個緩存是基於request的,爲何這麼說呢?由於每次請求來以前都必須HystrixRequestContext.initializeContext();進行初始化,每請求一次controller就會走一次filter,上下文又會初始化一次,前面緩存的就失效了,又得從新來數據庫

因此你要是想測試緩存,你得在一次controller請求中屢次調用那個加了緩存的service或HystrixCommand命令。Hystrix的書上寫的是:在同一用戶請求的上下文中,相同依賴服務的返回數據始終保持一致。在當次請求內對同一個依賴進行重複調用,只會真實調用一次。在當次請求內數據能夠保證一致性。瀏覽器

所以。但願你們在這裏不要理解錯了。緩存

 

 請求緩存圖,以下:tomcat

假設兩個線程發起相同的HTTP請求,Hystrix會把請求參數初始化到ThreadLocal中,兩個Command異步執行,每一個Command會把請求參數從ThreadLocal中拷貝到Command所在自身的線程中,Command在執行的時候會經過CacheKey優先從緩存中嘗試獲取是否已有緩存結果,網絡

若是命中,直接從HystrixRequestCache返回,若是沒有命中,那麼須要進行一次真實調用,而後把結果回寫到緩存中,在請求範圍內共享響應結果。併發

RequestCache主要有三個優勢:app

  1. 在當次請求內對同一個依賴進行重複調用,只會真實調用一次。

  2. 在當次請求內數據能夠保證一致性。

  3. 能夠減小沒必要要的線程開銷。

 

例子仍是接着上篇的HelloServiceCommand來進行演示,咱們只須要實現HystrixCommand的一個緩存方法名爲getCacheKey()便可

代碼以下:

/**
 * Created by cong on 2018/5/9.
 */
public class HelloServiceCommand extends HystrixCommand<String> {

    private RestTemplate restTemplate;

    protected HelloServiceCommand(String commandGroupKey,RestTemplate restTemplate) {
    //根據commandGroupKey進行線程隔離的 super(HystrixCommandGroupKey.Factory.asKey(commandGroupKey));
this.restTemplate = restTemplate; } @Override protected String run() throws Exception { System.out.println(Thread.currentThread().getName()); return restTemplate.getForEntity("http://HELLO-SERVICE/hello",String.class).getBody(); } @Override protected String getFallback() { return "error"; } //Hystrix的緩存 @Override protected String getCacheKey() { //通常動態的取緩存Key,好比userId,這裏爲了作實驗寫死了,寫爲hello return "hello"; } }

 

Controller代碼以下:

 

/**
 * Created by cong on 2018/5/8.
 */
@RestController
public class ConsumerController {



    @Autowired
    private  RestTemplate restTemplate;

    @RequestMapping("/consumer")
    public String helloConsumer() throws ExecutionException, InterruptedException {

        //Hystrix的緩存實現,這功能有點雞肋。
        HystrixRequestContext.initializeContext();
        HelloServiceCommand command = new HelloServiceCommand("hello",restTemplate);
        String execute = command.execute();//清理緩存
//       HystrixRequestCache.getInstance("hello").clear();
        return null;

       
     

    }

}

在原來的兩個provider模塊都增長增長一條輸出語句,以下:

provider1模塊:

/**
 * Created by cong on 2018/5/8.
 */
@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello(){
        System.out.println("訪問來1了......");
        return "hello1";
    }

 
}

 

provider2模塊:

/**
 * Created by cong on 2018/5/8.
 */
@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello(){
        System.out.println("訪問來2了......");
        return "hello1";
    }

 
}

 

瀏覽器輸入localhost:8082/consumer 

運行結果以下:

能夠看到你刷新一次請求,上下文又會初始化一次,前面緩存的就失效了,又得從新來,這時候根本就沒有緩存了。所以,你不管刷新多少次請求都是出現「訪問來了」,緩存都是失效的。若是是從緩存來的話,根本就不會輸出「訪問來了」。

 

可是,你如你在一塊兒請求屢次調用同一個業務,這時就是從緩存裏面取的數據。不理解能夠看一下Hystrix的緩存解釋:在同一用戶請求的上下文中,相同依賴服務的返回數據始終保持一致。在當次請求內對同一個依賴進行重複調用,只會真實調用一次。在當次請求內數據能夠保證一致性。

Controller代碼修改以下:

/**
 * Created by cong on 2018/5/8.
 */
@RestController
public class ConsumerController {



    @Autowired
    private  RestTemplate restTemplate;

    @RequestMapping("/consumer")
    public String helloConsumer() throws ExecutionException, InterruptedException {

        //Hystrix的緩存實現,這功能有點雞肋。
        HystrixRequestContext.initializeContext();
        HelloServiceCommand command = new HelloServiceCommand("hello",restTemplate);
        String execute = command.execute();
     HelloServiceCommand command1 = new HelloServiceCommand("hello",restTemplate);
 String execute1 = command1.execute();
  //清理緩存 
  // HystrixRequestCache.getInstance("hello").clear();

  return null;
}

 

接着運行,運行結果以下:

能夠看到只有一個」訪問來了「,並無出現兩個」訪問來了「。

之因此沒出現第二個,是由於是從緩存中取了。

 

刪除緩存 例如刪除key名爲hello的緩存:

HystrixRequestCache.getInstance("hello").clear();

你要寫操做的時候,你把一條數據給給刪除了,這時候你就必須把緩存清空了。

 

 

下面進行請求的合併。

 爲何要進行請求合併?舉個例子,有個礦山,每過一段時間都會生產一批礦產出來(質量爲卡車載重量的1/100),卡車能夠一等到礦產生產出來就立刻運走礦產,也能夠等到卡車裝滿再運走礦產,

前者一次生產對應卡車一次往返,卡車須要往返100次,然後者只須要往返一次,能夠大大減小卡車往返次數。顯而易見,利用請求合併能夠減小線程和網絡鏈接,開發人員沒必要單獨提供一個批量請求接口就能夠完成批量請求。

 在Hystrix中進行請求合併也是要付出必定代價的,請求合併會致使依賴服務的請求延遲增高,延遲的最大值是合併時間窗口的大小,默認爲10ms,固然咱們也能夠經過hystrix.collapser.default.timerDelayInMilliseconds屬性進行修改,

若是請求一次依賴服務的平均響應時間是20ms,那麼最壞狀況下(合併窗口開始是請求加入等待隊列)此次請求響應時間就會變成30ms。在Hystrix中對請求進行合併是否值得主要取決於Command自己,高併發度的接口經過請求合併能夠極大提升系統吞吐量,

從而基本能夠忽略合併時間窗口的開銷,反之,併發量較低,對延遲敏感的接口不建議使用請求合併。

請求合併的流程圖以下:

 能夠看出Hystrix會把多個Command放入Request隊列中,一旦知足合併時間窗口週期大小,Hystrix會進行一次批量提交,進行一次依賴服務的調用,經過充寫HystrixCollapser父類的mapResponseToRequests方法,將批量返回的請求分發到具體的每次請求中。

 

例子以下:

首先咱們先自定義一個BatchCommand類來繼承Hystrix給咱們提供的HystrixCollapser類,代碼以下:

/**
 * Created by cong on 2018/5/13.
 */
public class HjcBatchCommand extends HystrixCollapser<List<String>,String,Long> {

    private Long id;

    private RestTemplate restTemplate;
  //在200毫秒內進行請求合併,不在的話,放到下一個200毫秒
    public HjcBatchCommand(RestTemplate restTemplate,Long id) {
        super(Setter.withCollapserKey(HystrixCollapserKey.Factory.asKey("hjcbatch"))
                .andCollapserPropertiesDefaults(HystrixCollapserProperties.Setter()
                        .withTimerDelayInMilliseconds(200)));
        this.id = id;
        this.restTemplate = restTemplate;
    }

    //獲取每個請求的請求參數
    @Override
    public Long getRequestArgument() {
        return id;
    }

    //建立命令請求合併
    @Override
    protected HystrixCommand<List<String>> createCommand(Collection<CollapsedRequest<String, Long>> collection) {
        List<Long> ids = new ArrayList<>(collection.size());
        ids.addAll(collection.stream().map(CollapsedRequest::getArgument).collect(Collectors.toList()));
        HjcCommand command = new HjcCommand("hjc",restTemplate,ids);
        return command;
    }

    //合併請求拿到告終果,將請求結果按請求順序分發給各個請求
    @Override
    protected void mapResponseToRequests(List<String> results, Collection<CollapsedRequest<String, Long>> collection) {
        System.out.println("分配批量請求結果。。。。");

        int count = 0;
        for (CollapsedRequest<String,Long> collapsedRequest : collection){
            String result = results.get(count++);
            collapsedRequest.setResponse(result);
        }
    }
}

 

接着用自定義個HjcCommand來繼承Hystrix提供的HystrixCommand來進行服務請求

/**
 * Created by cong on 2018/5/13.
 */
public class HjcCommand extends HystrixCommand<List<String>> {

    private RestTemplate restTemplate;
    private List<Long> ids;


    public HjcCommand(String commandGroupKey, RestTemplate restTemplate,List<Long> ids) {
      //根據commandGroupKey進行線程隔離 super(HystrixCommandGroupKey.Factory.asKey(commandGroupKey));
this.restTemplate = restTemplate; this.ids = ids; } @Override protected List<String> run() throws Exception { System.out.println("發送請求。。。參數爲:"+ids.toString()+Thread.currentThread().getName()); String[] result = restTemplate.getForEntity("http://HELLO-SERVICE/hjcs?ids={1}",String[].class, StringUtils.join(ids,",")).getBody(); return Arrays.asList(result); } }

可是注意一點:你請求合併必需要異步,由於你若是用同步,是一個請求完成後,另外的請求才能繼續執行,因此必需要異步才能請求合併。

因此Controller層代碼以下:

@RestController
public class ConsumerController {



    @Autowired
    private  RestTemplate restTemplate;

    @RequestMapping("/consumer")
    public String helloConsumer() throws ExecutionException, InterruptedException {


        //請求合併
        HystrixRequestContext context = HystrixRequestContext.initializeContext();
        HjcBatchCommand command = new HjcBatchCommand(restTemplate,1L);
        HjcBatchCommand command1 = new HjcBatchCommand(restTemplate,2L);
        HjcBatchCommand command2 = new HjcBatchCommand(restTemplate,3L);

        //這裏你必需要異步,由於同步是一個請求完成後,另外的請求才能繼續執行,因此必需要異步才能請求合併
        Future<String> future = command.queue();
        Future<String> future1 = command1.queue();

        String r = future.get();
        String r1 = future1.get();

        Thread.sleep(2000);
        //能夠看到前面兩條命令會合並,最後一條會單獨,由於睡了2000毫秒,而你請求設置要求在200毫秒內才合併的。
        Future<String> future2 = command2.queue();
        String r2 = future2.get();

        System.out.println(r);
        System.out.println(r1);
        System.out.println(r2);

        context.close();

        return null;

    }

}

兩個服務提供者provider1,provider2新增長一個方法來模擬數據庫數據,代碼以下:

/**
 * Created by cong on 2018/5/8.
 */
@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello(){
        System.out.println("訪問來2了......");
        return "hello2";
    }

    @RequestMapping("/hjcs")
    public List<String> laowangs(String ids){
        List<String> list = new ArrayList<>();
        list.add("laowang1");
        list.add("laowang2");
        list.add("laowang3");
        return list;
    }

}

啓動Ribbon模塊,運行結果以下:

 能夠看到上圖的兩個線程是隔離的。

當請求很是多的時候,你合併請求就變得很是重要了,若是你不合並,一個請求都1 到2秒,這明顯不能忍的,會形成效率緩慢,若是你合併後,這時就能夠並行處理,下降延遲,可是若是請求很少的時候,只有單個請求,這時候合併也會出現

效率緩慢的,由於若是請求一次依賴服務的平均響應時間是200ms,那麼最壞狀況下(合併窗口開始是請求加入等待隊列)此次請求響應時間就會變成300ms。因此說要看場合而定的。

 

下面用註解的代碼來實現請求合併。代碼以下:‘

/**
 * Created by cong on 2018/5/15.
 */
@Service
public class HjcService {

    @Autowired
    private RestTemplate restTemplate;

    @HystrixCollapser(batchMethod = "getLaoWang",collapserProperties = {@HystrixProperty(name = "timerDelayInMilliseconds",value = "200")})
    public Future<String> batchGetHjc(long id){
        return null;
    }

    @HystrixCommand
    public List<String> getLaoWang(List<Long> ids){
        System.out.println("發送請求。。。參數爲:"+ids.toString()+Thread.currentThread().getName());
        String[] result = restTemplate.getForEntity("http://HELLO-SERVICE/hjcs?ids={1}",String[].class, StringUtils.join(ids,",")).getBody();
        return Arrays.asList(result);
    }

}

 

若是咱們還要進行服務的監控的話,那麼咱們須要在Ribbon模塊,和兩個服務提供者模塊提供以下依賴:

Ribbon模塊依賴以下:

    <!--儀表盤-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
            <version>1.4.0.RELEASE</version>
        </dependency>

        <!--監控-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-actuator</artifactId>
        </dependency>

兩個provider模塊依賴以下:

    <!--監控-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-actuator</artifactId>
        </dependency>

接着在Ribbon啓動類打上@EnableHystrixDashboard註解,而後啓動,localhost:8082/hystrix,以下圖:

每次訪問都有記錄:以下:

 

 

接下來咱們看一下經常使用的Hystrix屬性:

hystrix.command.default和hystrix.threadpool.default中的default爲默認CommandKey

Command Properties:

1.Execution相關的屬性的配置:

  • hystrix.command.default.execution.isolation.strategy 隔離策略,默認是Thread, 可選Thread|Semaphore

  • hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds 命令執行超時時間,默認1000ms

  • hystrix.command.default.execution.timeout.enabled 執行是否啓用超時,默認啓用true
  • hystrix.command.default.execution.isolation.thread.interruptOnTimeout 發生超時是是否中斷,默認true
  • hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests 最大併發請求數,默認10,該參數當使用ExecutionIsolationStrategy.SEMAPHORE策略時纔有效。若是達到最大併發請求數,請求會被拒絕。理論上選擇semaphore size的原則和選擇thread size一致,但選用semaphore時每次執行的單元要比較小且執行速度快(ms級別),不然的話應該用thread。
    semaphore應該佔整個容器(tomcat)的線程池的一小部分。

2.Fallback相關的屬性

這些參數能夠應用於Hystrix的THREAD和SEMAPHORE策略

  • hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests 若是併發數達到該設置值,請求會被拒絕和拋出異常而且fallback不會被調用。默認10
  • hystrix.command.default.fallback.enabled 當執行失敗或者請求被拒絕,是否會嘗試調用hystrixCommand.getFallback() 。默認true

3.Circuit Breaker相關的屬性

  • hystrix.command.default.circuitBreaker.enabled 用來跟蹤circuit的健康性,若是未達標則讓request短路。默認true
  • hystrix.command.default.circuitBreaker.requestVolumeThreshold 一個rolling window內最小的請求數。若是設爲20,那麼當一個rolling window的時間內(好比說1個rolling window是10秒)收到19個請求,即便19個請求都失敗,也不會觸發circuit break。默認20
  • hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds 觸發短路的時間值,當該值設爲5000時,則當觸發circuit break後的5000毫秒內都會拒絕request,也就是5000毫秒後纔會關閉circuit。默認5000
  • hystrix.command.default.circuitBreaker.errorThresholdPercentage錯誤比率閥值,若是錯誤率>=該值,circuit會被打開,並短路全部請求觸發fallback。默認50
  • hystrix.command.default.circuitBreaker.forceOpen 強制打開熔斷器,若是打開這個開關,那麼拒絕全部request,默認false
  • hystrix.command.default.circuitBreaker.forceClosed 強制關閉熔斷器 若是這個開關打開,circuit將一直關閉且忽略circuitBreaker.errorThresholdPercentage

4.Metrics相關參數

  • hystrix.command.default.metrics.rollingStats.timeInMilliseconds 設置統計的時間窗口值的,毫秒值,circuit break 的打開會根據1個rolling window的統計來計算。若rolling window被設爲10000毫秒,則rolling window會被分紅n個buckets,每一個bucket包含success,failure,timeout,rejection的次數的統計信息。默認10000
  • hystrix.command.default.metrics.rollingStats.numBuckets 設置一個rolling window被劃分的數量,若numBuckets=10,rolling window=10000,那麼一個bucket的時間即1秒。必須符合rolling window % numberBuckets == 0。默認10
  • hystrix.command.default.metrics.rollingPercentile.enabled 執行時是否enable指標的計算和跟蹤,默認true
  • hystrix.command.default.metrics.rollingPercentile.timeInMilliseconds 設置rolling percentile window的時間,默認60000
  • hystrix.command.default.metrics.rollingPercentile.numBuckets 設置rolling percentile window的numberBuckets。邏輯同上。默認6
  • hystrix.command.default.metrics.rollingPercentile.bucketSize 若是bucket size=100,window=10s,若這10s裏有500次執行,只有最後100次執行會被統計到bucket裏去。增長該值會增長內存開銷以及排序的開銷。默認100
  • hystrix.command.default.metrics.healthSnapshot.intervalInMilliseconds 記錄health 快照(用來統計成功和錯誤綠)的間隔,默認500ms

5.Request Context 相關參數

hystrix.command.default.requestCache.enabled 默認true,須要重載getCacheKey(),返回null時不緩存
hystrix.command.default.requestLog.enabled 記錄日誌到HystrixRequestLog,默認true

6.Collapser Properties 相關參數

hystrix.collapser.default.maxRequestsInBatch 單次批處理的最大請求數,達到該數量觸發批處理,默認Integer.MAX_VALUE
hystrix.collapser.default.timerDelayInMilliseconds 觸發批處理的延遲,也能夠爲建立批處理的時間+該值,默認10
hystrix.collapser.default.requestCache.enabled 是否對HystrixCollapser.execute() and HystrixCollapser.queue()的cache,默認true

7.ThreadPool 相關參數

線程數默認值10適用於大部分狀況(有時能夠設置得更小),若是須要設置得更大,那有個基本得公式能夠follow:
requests per second at peak when healthy × 99th percentile latency in seconds + some breathing room
每秒最大支撐的請求數 (99%平均響應時間 + 緩存值)
好比:每秒能處理1000個請求,99%的請求響應時間是60ms,那麼公式是:
1000 (0.060+0.012)

基本得原則時保持線程池儘量小,他主要是爲了釋放壓力,防止資源被阻塞。
當一切都是正常的時候,線程池通常僅會有1到2個線程激活來提供服務

    • hystrix.threadpool.default.coreSize 併發執行的最大線程數,默認10
    • hystrix.threadpool.default.maxQueueSize BlockingQueue的最大隊列數,當設爲-1,會使用SynchronousQueue,值爲正時使用LinkedBlcokingQueue。該設置只會在初始化時有效,以後不能修改threadpool的queue size,除非reinitialising thread executor。默認-1。
    • hystrix.threadpool.default.queueSizeRejectionThreshold 即便maxQueueSize沒有達到,達到queueSizeRejectionThreshold該值後,請求也會被拒絕。由於maxQueueSize不能被動態修改,這個參數將容許咱們動態設置該值。if maxQueueSize == -1,該字段將不起做用
    • hystrix.threadpool.default.keepAliveTimeMinutes 若是corePoolSize和maxPoolSize設成同樣(默認實現)該設置無效。若是經過plugin(https://github.com/Netflix/Hystrix/wiki/Plugins)使用自定義實現,該設置纔有用,默認1.
    • hystrix.threadpool.default.metrics.rollingStats.timeInMilliseconds 線程池統計指標的時間,默認10000
    • hystrix.threadpool.default.metrics.rollingStats.numBuckets 將rolling window劃分爲n個buckets,默認10
相關文章
相關標籤/搜索