微服務:ES案例&sentinel初識
ES實踐
實現旅遊網站的酒店搜尋功能,完成關鍵字搜尋和分頁
@Override public PageResult search(RequestParams params) { try { // 1.準備Request SearchRequest request = new SearchRequest("hotel"); // 2.準備DSL // 2.1.query String key = params.getKey(); if (key == null || "".equals(key)) { boolQuery.must(QueryBuilders.matchAllQuery()); } else { boolQuery.must(QueryBuilders.matchQuery("all", key)); } // 2.2.分頁 int page = params.getPage(); int size = params.getSize(); request.source().from((page - 1) * size).size(size); // 3.傳送請求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 4.解析響應 return handleResponse(response); } catch (IOException e) { throw new RuntimeException(e); } } // 結果解析 private PageResult handleResponse(SearchResponse response) { // 4.解析響應 SearchHits searchHits = response.getHits(); // 4.1.獲取總條數 long total = searchHits.getTotalHits().value; // 4.2.文件陣列 SearchHit[] hits = searchHits.getHits(); // 4.3.遍歷 List<HotelDoc> hotels = new ArrayList<>(); for (SearchHit hit : hits) { // 獲取文件source String json = hit.getSourceAsString(); // 反序列化 HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class); // 放入集合 hotels.add(hotelDoc); } // 4.4.封裝返回 return new PageResult(total, hotels); }
新增品牌、城市、星級、價格等過濾功能
private void buildBasicQuery(RequestParams params, SearchRequest request) { // 1.構建BooleanQuery BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); // 2.關鍵字搜尋 String key = params.getKey(); if (key == null || "".equals(key)) { boolQuery.must(QueryBuilders.matchAllQuery()); } else { boolQuery.must(QueryBuilders.matchQuery("all", key)); } // 3.城市條件 if (params.getCity() != null && !params.getCity().equals("")) { boolQuery.filter(QueryBuilders.termQuery("city", params.getCity())); } // 4.品牌條件 if (params.getBrand() != null && !params.getBrand().equals("")) { boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand())); } // 5.星級條件 if (params.getStarName() != null && !params.getStarName().equals("")) { boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName())); } // 6.價格 if (params.getMinPrice() != null && params.getMaxPrice() != null) { boolQuery.filter(QueryBuilders .rangeQuery("price") .gte(params.getMinPrice()) .lte(params.getMaxPrice()) ); } // 7.放入source request.source().query(boolQuery); }
我附近的酒店
// 2.3.排序 String location = params.getLocation(); if (location != null && !location.equals("")) { request.source().sort(SortBuilders .geoDistanceSort("location", new GeoPoint(location)) .order(SortOrder.ASC) .unit(DistanceUnit.KILOMETERS) ); }
讓指定的酒店在搜尋結果中排名置頂
// 2.算分控制
FunctionScoreQueryBuilder functionScoreQuery =
QueryBuilders.functionScoreQuery(
// 原始查詢,相關性算分的查詢
boolQuery,
// function score的陣列
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
// 其中的一個function score 元素
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
// 過濾條件
QueryBuilders.termQuery("isAD", true),
// 算分函式
ScoreFunctionBuilders.weightFactorFunction(10)
)
});
Sentinel
雪崩問題
微服務中,服務間呼叫關係錯綜複雜,一個微服務往往依賴於多個其它微服務。
如圖,如果服務提供者I發生了故障,當前的應用的部分業務因為依賴於服務I,因此也會被阻塞。此時,其它不依賴於服務I的業務似乎不受影響。
但是,依賴服務I的業務請求被阻塞,使用者不會得到響應,則tomcat的這個執行緒不會釋放,於是越來越多的使用者請求到來,越來越多的執行緒會阻塞:
伺服器支援的執行緒和併發數有限,請求一直阻塞,會導致伺服器資源耗盡,從而導致所有其它服務都不可用,那麼當前服務也就不可用了。
那麼,依賴於當前服務的其它服務隨著時間的推移,最終也都會變的不可用,形成級聯失敗,雪崩就發生了:
微服務呼叫鏈路中的某個服務故障,引起整個鏈路中的所有微服務都不可用,這就是雪崩。
解決雪崩問題的常見方式有四種:
- 超時處理:設定超時時間,請求超過一定時間沒有響應就返回錯誤資訊,不會無休止等待
- 倉壁模式
倉壁模式來源於船艙的設計:
船艙都會被隔板分離為多個獨立空間,當船體破損時,只會導致部分空間進入,將故障控制在一定範圍內,避免整個船體都被淹沒。
於此類似,我們可以限定每個業務能使用的執行緒數,避免耗盡整個tomcat的資源,因此也叫執行緒隔離。
- 斷路器模式:由斷路器統計業務執行的異常比例,如果超出閾值則會熔斷該業務,攔截訪問該業務的一切請求。
斷路器會統計訪問某個服務的請求數量,異常比例:
當發現訪問服務D的請求異常比例過高時,認為服務D有導致雪崩的風險,會攔截訪問服務D的一切請求,形成熔斷:
流量控制:限制業務訪問的QPS,避免服務因流量的突增而故障。
技術對比
期比較流行的是Hystrix框架,但目前國內實用最廣泛的還是阿里巴巴的Sentinel框架,這裡我們做下對比:
Sentinel | Hystrix | |
---|---|---|
隔離策略 | 訊號量隔離 | 執行緒池隔離/訊號量隔離 |
熔斷降級策略 | 基於慢呼叫比例或異常比例 | 基於失敗比率 |
實時指標實現 | 滑動視窗 | 滑動視窗(基於 RxJava) |
規則配置 | 支援多種資料來源 | 支援多種資料來源 |
擴充套件性 | 多個擴充套件點 | 外掛的形式 |
基於註解的支援 | 支援 | 支援 |
限流 | 基於 QPS,支援基於呼叫關係的限流 | 有限的支援 |
流量整形 | 支援慢啟動、勻速排隊模式 | 不支援 |
系統自適應保護 | 支援 | 不支援 |
控制檯 | 開箱即用,可配置規則、檢視秒級監控、機器發現等 | 不完善 |
常見框架的適配 | Servlet、Spring Cloud、Dubbo、gRPC 等 | Servlet、Spring Cloud Netflix |
Sentinel 具有以下特徵:
•豐富的應用場景:Sentinel 承接了阿里巴巴近 10 年的雙十一大促流量的核心場景,例如秒殺(即突發流量控制在系統容量可以承受的範圍)、訊息削峰填谷、叢集流量控制、實時熔斷下游不可用應用等。
•完備的實時監控:Sentinel 同時提供實時的監控功能。您可以在控制檯中看到接入應用的單臺機器秒級資料,甚至 500 臺以下規模的叢集的彙總執行情況。
•廣泛的開源生態:Sentinel 提供開箱即用的與其它開源框架/庫的整合模組,例如與 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相應的依賴並進行簡單的配置即可快速地接入 Sentinel。
•完善的 SPI 擴充套件點:Sentinel 提供簡單易用、完善的 SPI 擴充套件介面。您可以通過實現擴充套件介面來快速地定製邏輯。例如定製規則管理、適配動態資料來源等。
微服務整合Sentinel
-
引入依賴
<!--sentinel--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
-
配置
server: port: 8088 spring: cloud: sentinel: transport: dashboard: localhost:8080
-
訪問微服務端點,觸發sentinel監控
限流規則
當請求進入微服務時,首先會訪問DispatcherServlet,然後進入Controller、Service、Mapper,這樣的一個呼叫鏈就叫做簇點鏈路。簇點鏈路中被監控的每一個介面就是一個資源。
預設情況下sentinel會監控SpringMVC的每一個端點(Endpoint,也就是controller中的方法),因此SpringMVC的每一個端點(Endpoint)就是呼叫鏈路中的一個資源。
例如,我們剛才訪問的order-service中的OrderController中的端點:/order/{orderId}
流控模式
在新增限流規則時,點選高階選項,可以選擇三種流控模式:
- 直接:統計當前資源的請求,觸發閾值時對當前資源直接限流,也是預設的模式
- 關聯:統計與當前資源相關的另一個資源,觸發閾值時,對當前資源限流
- 鏈路:統計從指定鏈路訪問到本資源的請求,觸發閾值時,對指定鏈路限流
關聯模式
關聯模式:統計與當前資源相關的另一個資源,觸發閾值時,對當前資源限流
配置規則:
語法說明:當/write資源訪問量觸發閾值時,就會對/read資源限流,避免影響/write資源。
使用場景:比如使用者支付時需要修改訂單狀態,同時使用者要查詢訂單。查詢和修改操作會爭搶資料庫鎖,產生競爭。業務需求是優先支付和更新訂單的業務,因此當修改訂單業務觸發閾值時,需要對查詢訂單業務限流。
需求說明:
- 在OrderController新建兩個端點:/order/query和/order/update,無需實現業務
- 配置流控規則,當/order/ update資源被訪問的QPS超過5時,對/order/query請求限流
1)定義/order/query端點,模擬訂單查詢
@GetMapping("/query")
public String queryOrder() {
return "查詢訂單成功";
}
2)定義/order/update端點,模擬訂單更新
@GetMapping("/update")
public String updateOrder() {
return "更新訂單成功";
}
重啟服務,檢視sentinel控制檯的簇點鏈路:
3)配置流控規則
對哪個端點限流,就點選哪個端點後面的按鈕。我們是對訂單查詢/order/query限流,因此點選它後面的按鈕:
在表單中填寫流控規則:
4)在Jmeter測試
選擇《流控模式-關聯》:
可以看到1000個使用者,100秒,因此QPS為10,超過了我們設定的閾值:5
檢視http請求:
請求的目標是/order/update,這樣這個斷點就會觸發閾值。
但限流的目標是/order/query,我們在瀏覽器訪問,可以發現:
確實被限流了。
5)總結
鏈路模式
鏈路模式:只針對從指定鏈路訪問到本資源的請求做統計,判斷是否超過閾值。
配置示例:
例如有兩條請求鏈路:
-
/test1 --> /common
-
/test2 --> /common
如果只希望統計從/test2進入到/common的請求,則可以這樣配置:
實戰案例
需求:有查詢訂單和建立訂單業務,兩者都需要查詢商品。針對從查詢訂單進入到查詢商品的請求統計,並設定限流。
步驟:
-
在OrderService中新增一個queryGoods方法,不用實現業務
-
在OrderController中,改造/order/query端點,呼叫OrderService中的queryGoods方法
-
在OrderController中新增一個/order/save的端點,呼叫OrderService的queryGoods方法
-
給queryGoods設定限流規則,從/order/query進入queryGoods的方法限制QPS必須小於2
實現:
1)新增查詢商品方法
在order-service服務中,給OrderService類新增一個queryGoods方法:
public void queryGoods(){
System.err.println("查詢商品");
}
2)查詢訂單時,查詢商品
在order-service的OrderController中,修改/order/query端點的業務邏輯:
@GetMapping("/query")
public String queryOrder() {
// 查詢商品
orderService.queryGoods();
// 查詢訂單
System.out.println("查詢訂單");
return "查詢訂單成功";
}
3)新增訂單,查詢商品
在order-service的OrderController中,修改/order/save端點,模擬新增訂單:
@GetMapping("/save")
public String saveOrder() {
// 查詢商品
orderService.queryGoods();
// 查詢訂單
System.err.println("新增訂單");
return "新增訂單成功";
}
4)給查詢商品新增資源標記
預設情況下,OrderService中的方法是不被Sentinel監控的,需要我們自己通過註解來標記要監控的方法。
給OrderService的queryGoods方法新增@SentinelResource註解:
@SentinelResource("goods")
public void queryGoods(){
System.err.println("查詢商品");
}
鏈路模式中,是對不同來源的兩個鏈路做監控。但是sentinel預設會給進入SpringMVC的所有請求設定同一個root資源,會導致鏈路模式失效。
我們需要關閉這種對SpringMVC的資源聚合,修改order-service服務的application.yml檔案:
spring:
cloud:
sentinel:
web-context-unify: false # 關閉context整合
重啟服務,訪問/order/query和/order/save,可以檢視到sentinel的簇點鏈路規則中,出現了新的資源:
5)新增流控規則
點選goods資源後面的流控按鈕,在彈出的表單中填寫下面資訊:
只統計從/order/query進入/goods的資源,QPS閾值為2,超出則被限流。
6)Jmeter測試
選擇《流控模式-鏈路》:
可以看到這裡200個使用者,50秒內發完,QPS為4,超過了我們設定的閾值2
一個http請求是訪問/order/save:
執行的結果:
完全不受影響。
另一個是訪問/order/query:
執行結果:
每次只有2個通過。
流控效果
在流控的高階選項中,還有一個流控效果選項:
流控效果是指請求達到流控閾值時應該採取的措施,包括三種:
-
快速失敗:達到閾值後,新的請求會被立即拒絕並丟擲FlowException異常。是預設的處理方式。
-
warm up:預熱模式,對超出閾值的請求同樣是拒絕並丟擲異常。但這種模式閾值會動態變化,從一個較小值逐漸增加到最大閾值。
-
排隊等待:讓所有的請求按照先後次序排隊執行,兩個請求的間隔不能小於指定時長
warm up
閾值一般是一個微服務能承擔的最大QPS,但是一個服務剛剛啟動時,一切資源尚未初始化(冷啟動),如果直接將QPS跑到最大值,可能導致服務瞬間宕機。
warm up也叫預熱模式,是應對服務冷啟動的一種方案。請求閾值初始值是 maxThreshold / coldFactor,持續指定時長後,逐漸提高到maxThreshold值。而coldFactor的預設值是3.
例如,我設定QPS的maxThreshold為10,預熱時間為5秒,那麼初始閾值就是 10 / 3 ,也就是3,然後在5秒後逐漸增長到10.
案例
需求:給/order/{orderId}這個資源設定限流,最大QPS為10,利用warm up效果,預熱時長為5秒
1)配置流控規則:
2)Jmeter測試
選擇《流控效果,warm up》:
QPS為10.
剛剛啟動時,大部分請求失敗,成功的只有3個,說明QPS被限定在3:
隨著時間推移,成功比例越來越高:
到Sentinel控制檯檢視實時監控:
一段時間後:
排隊等待
當請求超過QPS閾值時,快速失敗和warm up 會拒絕新的請求並丟擲異常。
而排隊等待則是讓所有請求進入一個佇列中,然後按照閾值允許的時間間隔依次執行。後來的請求必須等待前面執行完成,如果請求預期的等待時間超出最大時長,則會被拒絕。
工作原理
例如:QPS = 5,意味著每200ms處理一個佇列中的請求;timeout = 2000,意味著預期等待時長超過2000ms的請求會被拒絕並丟擲異常。
那什麼叫做預期等待時長呢?
比如現在一下子來了12 個請求,因為每200ms執行一個請求,那麼:
- 第6個請求的預期等待時長 = 200 * (6 - 1) = 1000ms
- 第12個請求的預期等待時長 = 200 * (12-1) = 2200ms
現在,第1秒同時接收到10個請求,但第2秒只有1個請求,此時QPS的曲線這樣的:
如果使用佇列模式做流控,所有進入的請求都要排隊,以固定的200ms的間隔執行,QPS會變的很平滑:
平滑的QPS曲線,對於伺服器來說是更友好的。
案例
需求:給/order/{orderId}這個資源設定限流,最大QPS為10,利用排隊的流控效果,超時時長設定為5s
1)新增流控規則
2)Jmeter測試
選擇《流控效果,佇列》:
QPS為15,已經超過了我們設定的10。
如果是之前的 快速失敗、warmup模式,超出的請求應該會直接報錯。
但是我們看看佇列模式的執行結果:
全部都通過了。
再去sentinel檢視實時監控的QPS曲線:
QPS非常平滑,一致保持在10,但是超出的請求沒有被拒絕,而是放入佇列。因此響應時間(等待時間)會越來越長。
當佇列滿了以後,才會有部分請求失敗:
總結:
流控效果有哪些?
-
快速失敗:QPS超過閾值時,拒絕新的請求
-
warm up: QPS超過閾值時,拒絕新的請求;QPS閾值是逐漸提升的,可以避免冷啟動時高併發導致服務宕機。
-
排隊等待:請求會進入佇列,按照閾值允許的時間間隔依次執行請求;如果請求預期等待時長大於超時時間,直接拒絕