1. 程式人生 > 其它 >微服務:ES案例&sentinel初識

微服務: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的請求,則可以這樣配置:

實戰案例

需求:有查詢訂單和建立訂單業務,兩者都需要查詢商品。針對從查詢訂單進入到查詢商品的請求統計,並設定限流。

步驟:

  1. 在OrderService中新增一個queryGoods方法,不用實現業務

  2. 在OrderController中,改造/order/query端點,呼叫OrderService中的queryGoods方法

  3. 在OrderController中新增一個/order/save的端點,呼叫OrderService的queryGoods方法

  4. 給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閾值是逐漸提升的,可以避免冷啟動時高併發導致服務宕機。

  • 排隊等待:請求會進入佇列,按照閾值允許的時間間隔依次執行請求;如果請求預期等待時長大於超時時間,直接拒絕