高併發服務降級特技
背景
在今天,基於SOA的架構已經大行其道。伴隨着架構的SOA化,相關聯的服務熔斷、降級、限流等思想,也在各種技術講座中頻繁出現。本文將結合Netflix開源的Hystrix框架,對這些思想做一個梳理。
伴隨着業務複雜性的提高,系統的不斷拆分,一個面向用戶端的API,其內部的RPC調用層層嵌套,調用鏈條可能會非常長。這會造成以下幾個問題:
API接口可用性降低
引用Hystrix官方的一個例子,假設tomcat對外提供的一個application,其內部依賴了30個服務,每個服務的可用性都很高,爲99.99%。那整個applicatiion的可用性就是:99.99%的30次方 = 99.7%,即0.3%的失敗率。
這也就意味着,每1億個請求,有30萬個失敗;按時間來算,就是每個月的故障時間超過2小時。
服務熔斷
爲了解決上述問題,服務熔斷的思想被提出來。類似現實世界中的“保險絲“,當某個異常條件被觸發,直接熔斷整個服務,而不是一直等到此服務超時。
熔斷的觸發條件可以依據不同的場景有所不同,比如統計一個時間窗口內失敗的調用次數。
服務降級
有了熔斷,就得有降級。所謂降級,就是當某個服務熔斷之後,服務器將不再被調用,此時客戶端可以自己準備一個本地的fallback回調,返回一個缺省值。
這樣做,雖然服務水平下降,但好歹可用,比直接掛掉要強,當然這也要看適合的業務場景。
關於Hystrix中fallback的使用,此處不詳述,參見官網。
項目搭建
需求:搭建一套分佈式rpc遠程通訊案例:比如訂單服務調用會員服務實現服務隔離,防止雪崩效應案例
訂單工程
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency> <dependency> <groupId>com.netflix.hystrix</groupId> <artifactId>hystrix-metrics-event-stream</artifactId> <version>1.5.12</version> </dependency> <dependency> <groupId>com.netflix.hystrix</groupId> <artifactId>hystrix-javanica</artifactId> <version>1.5.12</version> </dependency> </dependencies> |
@RestController @RequestMapping("/order") public class OrderController { @Autowired private MemberService memberService;
@RequestMapping("/orderIndex") public Object orderIndex() throws InterruptedException { JSONObject member = memberService.getMember(); System.out.println("當前線程名稱:" + Thread.currentThread().getName() + ",訂單服務調用會員服務:member:" + member); return member; }
@RequestMapping("/orderIndexHystrix") public Object orderIndexHystrix() throws InterruptedException { return new OrderHystrixCommand(memberService).execute(); }
@RequestMapping("/orderIndexHystrix2") public Object orderIndexHystrix2() throws InterruptedException { return new OrderHystrixCommand2(memberService).execute(); }
@RequestMapping("/findOrderIndex") public Object findIndex() { System.out.println("當前線程:" + Thread.currentThread().getName() + ",findOrderIndex"); return "findOrderIndex"; } } |
public class HttpClientUtils { private static Logger logger = LoggerFactory.getLogger(HttpClientUtils.class); // 日誌記錄
private static RequestConfig requestConfig = null;
static { // 設置請求和傳輸超時時間 requestConfig = RequestConfig.custom().setSocketTimeout(2000).setConnectTimeout(2000).build(); }
/** * post請求傳輸json參數 * * @param url * url地址 * @param json * 參數 * @return */ public static JSONObject httpPost(String url, JSONObject jsonParam) { // post請求返回結果 CloseableHttpClient httpClient = HttpClients.createDefault(); JSONObject jsonResult = null; HttpPost httpPost = new HttpPost(url); // 設置請求和傳輸超時時間 httpPost.setConfig(requestConfig); try { if (null != jsonParam) { // 解決中文亂碼問題 StringEntity entity = new StringEntity(jsonParam.toString(), "utf-8"); entity.setContentEncoding("UTF-8"); entity.setContentType("application/json"); httpPost.setEntity(entity); } CloseableHttpResponse result = httpClient.execute(httpPost); // 請求發送成功,並得到響應 if (result.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { String str = ""; try { // 讀取服務器返回過來的json字符串數據 str = EntityUtils.toString(result.getEntity(), "utf-8"); // 把json字符串轉換成json對象 jsonResult = JSONObject.parseObject(str); } catch (Exception e) { logger.error("post請求提交失敗:" + url, e); } } } catch (IOException e) { logger.error("post請求提交失敗:" + url, e); } finally { httpPost.releaseConnection(); } return jsonResult; }
/** * post請求傳輸String參數 例如:name=Jack&sex=1&type=2 * Content-type:application/x-www-form-urlencoded * * @param url * url地址 * @param strParam * 參數 * @return */ public static JSONObject httpPost(String url, String strParam) { // post請求返回結果 CloseableHttpClient httpClient = HttpClients.createDefault(); JSONObject jsonResult = null; HttpPost httpPost = new HttpPost(url); httpPost.setConfig(requestConfig); try { if (null != strParam) { // 解決中文亂碼問題 StringEntity entity = new StringEntity(strParam, "utf-8"); entity.setContentEncoding("UTF-8"); entity.setContentType("application/x-www-form-urlencoded"); httpPost.setEntity(entity); } CloseableHttpResponse result = httpClient.execute(httpPost); // 請求發送成功,並得到響應 if (result.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { String str = ""; try { // 讀取服務器返回過來的json字符串數據 str = EntityUtils.toString(result.getEntity(), "utf-8"); // 把json字符串轉換成json對象 jsonResult = JSONObject.parseObject(str); } catch (Exception e) { logger.error("post請求提交失敗:" + url, e); } } } catch (IOException e) { logger.error("post請求提交失敗:" + url, e); } finally { httpPost.releaseConnection(); } return jsonResult; }
/** * 發送get請求 * * @param url * 路徑 * @return */ public static JSONObject httpGet(String url) { // get請求返回結果 JSONObject jsonResult = null; CloseableHttpClient client = HttpClients.createDefault(); // 發送get請求 HttpGet request = new HttpGet(url); request.setConfig(requestConfig); try { CloseableHttpResponse response = client.execute(request);
// 請求發送成功,並得到響應 if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { // 讀取服務器返回過來的json字符串數據 HttpEntity entity = response.getEntity(); String strResult = EntityUtils.toString(entity, "utf-8"); // 把json字符串轉換成json對象 jsonResult = JSONObject.parseObject(strResult); } else { logger.error("get請求提交失敗:" + url); } } catch (IOException e) { logger.error("get請求提交失敗:" + url, e); } finally { request.releaseConnection(); } return jsonResult; }
} |
會員工程
@RestController @RequestMapping("/member") public class MemberController {
@RequestMapping("/memberIndex") public Object memberIndex() throws InterruptedException { Map<String, Object> hashMap = new HashMap<String, Object>(); hashMap.put("code", 200); hashMap.put("msg", "memberIndex"); Thread.sleep(1500); return hashMap; }
} |
Hystrix簡介
使用Hystrix實現服務隔離
Hystrix 是一個微服務關於服務保護的框架,是Netflix開源的一款針對分佈式系統的延遲和容錯解決框架,目的是用來隔離分佈式服務故障。它提供線程和信號量隔離,以減少不同服務之間資源競爭帶來的相互影響;提供優雅降級機制;提供熔斷機制使得服務可以快速失敗,而不是一直阻塞等待服務響應,並能從中快速恢復。Hystrix通過這些機制來阻止級聯失敗並保證系統彈性、可用。
什麼是服務隔離
當大多數人在使用Tomcat時,多個HTTP服務會共享一個線程池,假設其中一個HTTP服務訪問的數據庫響應非常慢,這將造成服務響應時間延遲增加,大多數線程阻塞等待數據響應返回,導致整個Tomcat線程池都被該服務佔用,甚至拖垮整個Tomcat。因此,如果我們能把不同HTTP服務隔離到不同的線程池,則某個HTTP服務的線程池滿了也不會對其他服務造成災難性故障。這就需要線程隔離或者信號量隔離來實現了。
使用線程隔離或信號隔離的目的是爲不同的服務分配一定的資源,當自己的資源用完,直接返回失敗而不是佔用別人的資源。
Hystrix實現服務隔離兩種方案
Hystrix的資源隔離策略有兩種,分別爲:線程池和信號量。
線程池方式
1、 使用線程池隔離可以完全隔離第三方應用,請求線程可以快速放回。 2、 請求線程可以繼續接受新的請求,如果出現問題線程池隔離是獨立的不會影響其他應用。
3、 當失敗的應用再次變得可用時,線程池將清理並可立即恢復,而不需要一個長時間的恢復。
4、 獨立的線程池提高了併發性
缺點:
線程池隔離的主要缺點是它們增加計算開銷(CPU)。每個命令的執行涉及到排隊、調度和上 下文切換都是在一個單獨的線程上運行的。
public class OrderHystrixCommand extends HystrixCommand<JSONObject> { @Autowired private MemberService memberService;
/** * @param group */ public OrderHystrixCommand(MemberService memberService) { super(setter()); this.memberService = memberService; }
protected JSONObject run() throws Exception { JSONObject member = memberService.getMember(); System.out.println("當前線程名稱:" + Thread.currentThread().getName() + ",訂單服務調用會員服務:member:" + member); return member; }
private static Setter setter() {
// 服務分組 HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory.asKey("members"); // 服務標識 HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey("member"); // 線程池名稱 HystrixThreadPoolKey threadPoolKey = HystrixThreadPoolKey.Factory.asKey("member-pool"); // ##################################################### // 線程池配置 線程池大小爲10,線程存活時間15秒 隊列等待的閾值爲100,超過100執行拒絕策略 HystrixThreadPoolProperties.Setter threadPoolProperties = HystrixThreadPoolProperties.Setter().withCoreSize(10) .withKeepAliveTimeMinutes(15).withQueueSizeRejectionThreshold(100); // ######################################################## // 命令屬性配置Hystrix 開啓超時 HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter() // 採用線程池方式實現服務隔離 .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD) // 禁止 .withExecutionTimeoutEnabled(false); return HystrixCommand.Setter.withGroupKey(groupKey).andCommandKey(commandKey).andThreadPoolKey(threadPoolKey) .andThreadPoolPropertiesDefaults(threadPoolProperties).andCommandPropertiesDefaults(commandProperties);
}
@Override protected JSONObject getFallback() { // 如果Hystrix發生熔斷,當前服務不可用,直接執行Fallback方法 System.out.println("系統錯誤!"); JSONObject jsonObject = new JSONObject(); jsonObject.put("code", 500); jsonObject.put("msg", "系統錯誤!"); return jsonObject; } } |
信號量
使用一個原子計數器(或信號量)來記錄當前有多少個線程在運行,當請求進來時先判斷計數 器的數值,若超過設置的最大線程個數則拒絕該請求,若不超過則通行,這時候計數器+1,請求返 回成功後計數器-1。
與線程池隔離最大不同在於執行依賴代碼的線程依然是請求線程
tips:信號量的大小可以動態調整, 線程池大小不可以
public class OrderHystrixCommand2 extends HystrixCommand<JSONObject> { @Autowired private MemberService memberService;
/** * @param group */ public OrderHystrixCommand2(MemberService memberService) { super(setter()); this.memberService = memberService; }
protected JSONObject run() throws Exception {
// Thread.sleep(500); // System.out.println("orderIndex線程名稱" + // Thread.currentThread().getName()); // System.out.println("success"); JSONObject member = memberService.getMember(); System.out.println("當前線程名稱:" + Thread.currentThread().getName() + ",訂單服務調用會員服務:member:" + member); return member; }
private static Setter setter() { // 服務分組 HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory.asKey("members"); // 命令屬性配置 採用信號量模式 HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter() .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE) // 使用一個原子計數器(或信號量)來記錄當前有多少個線程在運行,當請求進來時先判斷計數 // 器的數值,若超過設置的最大線程個數則拒絕該請求,若不超過則通行,這時候計數器+1,請求返 回成功後計數器-1。 .withExecutionIsolationSemaphoreMaxConcurrentRequests(50); return HystrixCommand.Setter.withGroupKey(groupKey).andCommandPropertiesDefaults(commandProperties); }
@Override protected JSONObject getFallback() { // 如果Hystrix發生熔斷,當前服務不可用,直接執行Fallback方法 System.out.println("系統錯誤!"); JSONObject jsonObject = new JSONObject(); jsonObject.put("code", 500); jsonObject.put("msg", "系統錯誤!"); return jsonObject; } } |
應用場景
線程池隔離:
1、 第三方應用或者接口
2、 併發量大
信號量隔離:
1、 內部應用或者中間件(redis)
2、 併發需求不大