接着上一篇的Hystrix進行進一步了解。
當系統用戶不斷增長時,每個微服務需要承受的並發壓力也越來越大,在分布式環境中,通常壓力來自對依賴服務的調用,因為親戚依賴服務的資源需要通過通信來實現,這樣的依賴方式比起進程內的調用方式會引起一部分的性能損失,
在高並發的場景下,Hystrix 提供了請求緩存的功能,我們可以方便的開啟和使用請求緩存來優化系統,達到減輕高並發時的請求線程消耗、降低請求響應時間的效果
Hystrix的緩存,這個功能是有點雞肋的,因為這個緩存是基於request的,為什么這么說呢?因為每次請求來之前都必須HystrixRequestContext.initializeContext();進行初始化,每請求一次controller就會走一次filter,上下文又會初始化一次,前面緩存的就失效了,又得重新來。
所以你要是想測試緩存,你得在一次controller請求中多次調用那個加了緩存的service或HystrixCommand命令。Hystrix的書上寫的是:在同一用戶請求的上下文中,相同依賴服務的返回數據始終保持一致。在當次請求內對同一個依賴進行重復調用,只會真實調用一次。在當次請求內數據可以保證一致性。
因此。希望大家在這里不要理解錯了。
請求緩存圖,如下:
假設兩個線程發起相同的HTTP請求,Hystrix會把請求參數初始化到ThreadLocal中,兩個Command異步執行,每個Command會把請求參數從ThreadLocal中拷貝到Command所在自身的線程中,Command在執行的時候會通過CacheKey優先從緩存中嘗試獲取是否已有緩存結果,
如果命中,直接從HystrixRequestCache返回,如果沒有命中,那么需要進行一次真實調用,然后把結果回寫到緩存中,在請求范圍內共享響應結果。
RequestCache主要有三個優點:
-
在當次請求內對同一個依賴進行重復調用,只會真實調用一次。
-
在當次請求內數據可以保證一致性。
-
可以減少不必要的線程開銷。
例子還是接着上篇的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