微服務(wù)03 分布式搜索引擎 elasticsearch ELK kibana RestAPI 索引庫 DSL查詢 RestClient 黑馬旅游
分布式搜索引擎03
1.數(shù)據(jù)聚合
聚合(aggregations)可以讓我們極其方便的實(shí)現(xiàn)對數(shù)據(jù)的統(tǒng)計(jì)、分析、運(yùn)算。例如:
-
什么品牌的手機(jī)最受歡迎?
-
這些手機(jī)的平均價(jià)格、最高價(jià)格、最低價(jià)格?
-
這些手機(jī)每月的銷售情況如何?
實(shí)現(xiàn)這些統(tǒng)計(jì)功能的比數(shù)據(jù)庫的sql要方便的多,而且查詢速度非???/strong>,可以實(shí)現(xiàn)近實(shí)時(shí)搜索效果。
1.1.聚合的種類
聚合常見的有三類:
-
桶(Bucket)聚合:用來對文檔做分組
-
TermAggregation:按照文檔字段值分組,例如按照品牌值分組、按照國家分組
-
Date Histogram:按照日期階梯分組,例如一周為一組,或者一月為一組
-
度量(Metric)聚合:用以計(jì)算一些值,比如:最大值、最小值、平均值等
-
Avg:求平均值
-
Max:求最大值
-
Min:求最小值
-
Stats:同時(shí)求max、min、avg、sum等
-
管道(pipeline)聚合:其它聚合的結(jié)果為基礎(chǔ)做聚合
注意: 參加聚合的字段必須是keyword、日期、數(shù)值、布爾類型( 不分詞 )
1.2.DSL實(shí)現(xiàn)聚合
現(xiàn)在,我們要統(tǒng)計(jì)所有數(shù)據(jù)中的酒店品牌有幾種,其實(shí)就是按照品牌對數(shù)據(jù)分組。此時(shí)可以根據(jù)酒店品牌的名稱做聚合,也就是Bucket聚合。
1.2.1.Bucket聚合語法
語法如下:
GET /hotel/_search
{
"size": 0, // 設(shè)置size為0,結(jié)果中不包含文檔,只包含聚合結(jié)果
"aggs": { // 定義聚合
"brandAgg": { //給聚合起個(gè)名字
"terms": { // 聚合的類型,按照品牌值聚合,所以選擇term
"field": "brand", // 參與聚合的字段
"size": 20 // 希望獲取的聚合結(jié)果數(shù)量
}
}
}
}
結(jié)果如圖:

1.2.2.聚合結(jié)果排序
默認(rèn)情況下,Bucket聚合會(huì)統(tǒng)計(jì)Bucket內(nèi)的文檔數(shù)量,記為_count,并且按照_count降序排序。
我們可以指定order屬性,自定義聚合的排序方式:
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"order": {
"_count": "asc" // 按照_count升序排列
},
"size": 20
}
}
}
}
1.2.3.限定聚合范圍
默認(rèn)情況下,Bucket聚合是對索引庫的所有文檔做聚合,但真實(shí)場景下,用戶會(huì)輸入搜索條件,因此聚合必須是對搜索結(jié)果聚合。那么聚合必須添加限定條件。
我們可以限定要聚合的文檔范圍,只要添加query條件即可:
GET /hotel/_search
{
"query": {
"range": {
"price": {
"lte": 200 // 只對200元以下的文檔聚合
}
}
},
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 20
}
}
}
}
這次,聚合得到的品牌明顯變少了:

1.2.4.Metric聚合語法
上節(jié)課,我們對酒店按照品牌分組,形成了一個(gè)個(gè)桶?,F(xiàn)在我們需要對桶內(nèi)的酒店做運(yùn)算,獲取每個(gè)品牌的用戶評分的min、max、avg等值。
這就要用到Metric聚合了,例如stat聚合:就可以獲取min、max、avg等結(jié)果。
語法如下:
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 20
},
"aggs": { // 是brands聚合的子聚合,也就是分組后對每組分別計(jì)算
"score_stats": { // 聚合名稱
"stats": { // 聚合類型,這里stats可以計(jì)算min、max、avg等
"field": "score" // 聚合字段,這里是score
}
}
}
}
}
}
這次的score_stats聚合是在brandAgg的聚合內(nèi)部嵌套的子聚合。因?yàn)槲覀冃枰?span style="color:#c21c13;">在每個(gè)桶分別計(jì)算。
另外,我們還可以給聚合結(jié)果做個(gè)排序,例如按照每個(gè)桶的酒店平均分做排序:

這里面order可以寫多個(gè)條件 寫在后面的先執(zhí)行 不信可以試一下
1.2.5.小結(jié)
aggs代表聚合,與query同級,此時(shí)query的作用是?
-
限定聚合的的文檔范圍
聚合必須的三要素:
-
聚合名稱
-
聚合類型
-
聚合字段
聚合可配置屬性有:
-
size:指定聚合結(jié)果數(shù)量
-
order:指定聚合結(jié)果排序方式
-
field:指定聚合字段
1.3.RestAPI實(shí)現(xiàn)聚合
1.3.1.API語法
聚合條件與query條件同級別,因此需要使用request.source()來指定聚合條件。
聚合條件的語法:

聚合的結(jié)果也與查詢結(jié)果不同,API也比較特殊。不過同樣是JSON逐層解析:

1.3.2.業(yè)務(wù)需求
需求:搜索頁面的品牌、城市等信息不應(yīng)該是在頁面寫死,而是通過聚合索引庫中的酒店數(shù)據(jù)得來的:

分析:
目前,頁面的城市列表、星級列表、品牌列表都是寫死的,并不會(huì)隨著搜索結(jié)果的變化而變化。但是用戶搜索條件改變時(shí),搜索結(jié)果會(huì)跟著變化。
例如:用戶搜索“東方明珠”,那搜索的酒店肯定是在上海東方明珠附近,因此,城市只能是上海,此時(shí)城市列表中就不應(yīng)該顯示北京、深圳、杭州這些信息了。
也就是說,搜索結(jié)果中包含哪些城市,頁面就應(yīng)該列出哪些城市;搜索結(jié)果中包含哪些品牌,頁面就應(yīng)該列出哪些品牌。
如何得知搜索結(jié)果中包含哪些品牌?如何得知搜索結(jié)果中包含哪些城市?
使用聚合功能,利用Bucket聚合,對搜索結(jié)果中的文檔基于品牌分組、基于城市分組,就能得知包含哪些品牌、哪些城市了。
因?yàn)槭菍λ阉鹘Y(jié)果聚合,因此聚合是限定范圍的聚合,也就是說聚合的限定條件跟搜索文檔的條件一致。
查看瀏覽器可以發(fā)現(xiàn),前端其實(shí)已經(jīng)發(fā)出了這樣的一個(gè)請求:

請求參數(shù)與搜索文檔的參數(shù)完全一致。
返回值類型就是頁面要展示的最終結(jié)果:

結(jié)果是一個(gè)Map結(jié)構(gòu):
-
key是字符串,城市、星級、品牌、價(jià)格
-
value是集合,例如多個(gè)城市的名稱

1.3.3.業(yè)務(wù)實(shí)現(xiàn)
在cn.itcast.hotel.web包的HotelController中添加一個(gè)方法,遵循下面的要求:
-
請求方式:POST
-
請求路徑:/hotel/filters
-
請求參數(shù):RequestParams,與搜索文檔的參數(shù)一致
-
返回值類型:Map<String, List<String>>
代碼:
@PostMapping("filters")
public Map<String, List<String>> getFilters(@RequestBody RequestParams params){
return hotelService.getFilters(params);
}
這里調(diào)用了IHotelService中的getFilters方法,尚未實(shí)現(xiàn)。
在cn.itcast.hotel.service.IHotelService中定義新方法:
124集有坑 , map的key要put定義成為英文brand...的 和定義的字段對應(yīng)
Map<String, List<String>> filters(RequestParams params);
在cn.itcast.hotel.service.impl.HotelService中實(shí)現(xiàn)該方法:
@Override
public Map<String, List<String>> filters(RequestParams params) {
try {
// 1.準(zhǔn)備Request
SearchRequest request = new SearchRequest("hotel");
// 2.準(zhǔn)備DSL
// 2.1.query
buildBasicQuery(params, request);
// 2.2.設(shè)置size
request.source().size(0);
// 2.3.聚合
buildAggregation(request);
// 3.發(fā)出請求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析結(jié)果
Map<String, List<String>> result = new HashMap<>();
Aggregations aggregations = response.getAggregations();
// 4.1.根據(jù)品牌名稱,獲取品牌結(jié)果
List<String> brandList = getAggByName(aggregations, "brandAgg");
result.put("品牌", brandList);
// 4.2.根據(jù)品牌名稱,獲取品牌結(jié)果
List<String> cityList = getAggByName(aggregations, "cityAgg");
result.put("城市", cityList);
// 4.3.根據(jù)品牌名稱,獲取品牌結(jié)果
List<String> starList = getAggByName(aggregations, "starAgg");
result.put("星級", starList);
return result;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void buildAggregation(SearchRequest request) {
request.source().aggregation(AggregationBuilders
.terms("brandAgg")
.field("brand")
.size(100)
);
request.source().aggregation(AggregationBuilders
.terms("cityAgg")
.field("city")
.size(100)
);
request.source().aggregation(AggregationBuilders
.terms("starAgg")
.field("starName")
.size(100)
);
}
private List<String> getAggByName(Aggregations aggregations, String aggName) {
// 4.1.根據(jù)聚合名稱獲取聚合結(jié)果
Terms brandTerms = aggregations.get(aggName);
// 4.2.獲取buckets
List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
// 4.3.遍歷
List<String> brandList = new ArrayList<>();
for (Terms.Bucket bucket : buckets) {
// 4.4.獲取key
String key = bucket.getKeyAsString();
brandList.add(key);
}
return brandList;
}
2.自動(dòng)補(bǔ)全
當(dāng)用戶在搜索框輸入字符時(shí),我們應(yīng)該提示出與該字符有關(guān)的搜索項(xiàng),如圖:

這種根據(jù)用戶輸入的字母,提示完整詞條的功能,就是自動(dòng)補(bǔ)全了。
因?yàn)樾枰鶕?jù)拼音字母來推斷,因此要用到拼音分詞功能。
2.1.拼音分詞器
要實(shí)現(xiàn)根據(jù)字母做補(bǔ)全,就必須對文檔按照拼音分詞。在GitHub上恰好有elasticsearch的拼音分詞插件。地址:GitHub - medcl/elasticsearch-analysis-pinyin: This Pinyin Analysis plugin is used to do conversion between Chinese characters and Pinyin.

課前資料中也提供了拼音分詞器的安裝包:

安裝方式與IK分詞器一樣,分三步:
①解壓
②上傳到虛擬機(jī)中,elasticsearch的plugin目錄
③重啟elasticsearch
④測試
詳細(xì)安裝步驟可以參考IK分詞器的安裝過程。
測試用法如下:
POST /_analyze
{
"text": "如家酒店還不錯(cuò)",
"analyzer": "pinyin"
}
結(jié)果:

2.2.自定義分詞器
默認(rèn)的拼音分詞器會(huì)將每個(gè)漢字單獨(dú)分為拼音,而我們希望的是每個(gè)詞條形成一組拼音,需要對拼音分詞器做個(gè)性化定制,形成自定義分詞器。
elasticsearch中分詞器(analyzer)的組成包含三部分:
-
character filters:在tokenizer之前對文本進(jìn)行處理。例如刪除字符、替換字符
-
tokenizer:
-
將文本按照一定的規(guī)則切割成詞條(term)。例如keyword,就是不分詞;還有ik_smart
-
tokenizer filter:
-
將tokenizer輸出的詞條做進(jìn)一步處理。例如大小寫轉(zhuǎn)換、同義詞處理、拼音處理等
文檔分詞時(shí)會(huì)依次由這三部分來處理文檔:

聲明自定義分詞器的語法如下:
我們可以在創(chuàng)建索引庫時(shí),通過settings來配置自定義的analyzer (分詞器)
PUT /test
{
"settings": {
"analysis": {
"analyzer": { // 自定義分詞器
"my_analyzer": { // 分詞器名稱
"tokenizer": "ik_max_word",
"filter": "py"
}
},
"filter": { // 自定義tokenizer filter
"py": { // 過濾器名稱
"type": "pinyin", // 過濾器類型,這里是pinyin
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "my_analyzer",
"search_analyzer": "ik_smart"
}
}
}
}
測試:


總結(jié):
如何使用拼音分詞器?
-
①下載pinyin分詞器
-
②解壓并放到elasticsearch的plugin目錄
-
③重啟即可
如何自定義分詞器?
-
①創(chuàng)建索引庫時(shí),在settings中配置,可以包含三部分
-
②character filter
-
③tokenizer
-
④filter
拼音分詞器注意事項(xiàng)?
-
為了避免搜索到同音字,搜索時(shí)不要使用拼音分詞器

DELETE /test
PUT /test
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
}
},
"filter": {
"py": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
},
"mappings": {
"properties": {
"name":{
"type":"text",
"analyzer":"my_analyzer",
"search_analyzer":"ik_smart"
}
}
}
}
POST /test/_doc/1
{
"id": 1,
"name": "獅子"
}
POST /test/_doc/2
{
"id": 2,
"name": "虱子"
}
GET /test/_search
{
"query": {
"match": {
"name": "掉入獅子籠咋辦"
}
}
}
"search_analyzer":"ik_smart" :這樣搜“掉入獅子籠咋辦”的時(shí)候就不會(huì)出來虱子
2.3.自動(dòng)補(bǔ)全查詢
elasticsearch提供了Completion Suggester查詢來實(shí)現(xiàn)自動(dòng)補(bǔ)全功能。這個(gè)查詢會(huì)匹配以用戶輸入內(nèi)容開頭的詞條并返回。為了提高補(bǔ)全查詢的效率,對于文檔中字段的類型有一些約束:
-
參與補(bǔ)全查詢的字段必須是completion類型。
-
字段的內(nèi)容一般是用來補(bǔ)全的多個(gè)詞條形成的數(shù)組。
比如,一個(gè)這樣的索引庫:
// 創(chuàng)建索引庫
PUT test
{
"mappings": {
"properties": {
"title":{
"type": "completion"
}
}
}
}
然后插入下面的數(shù)據(jù):
// 示例數(shù)據(jù)
POST test/_doc
{
"title": ["Sony", "WH-1000XM3"]
}
POST test/_doc
{
"title": ["SK-II", "PITERA"]
}
POST test/_doc
{
"title": ["Nintendo", "switch"]
}
查詢的DSL語句如下:
// 自動(dòng)補(bǔ)全查詢
GET /test/_search
{
"suggest": {
"title_suggest": {
"text": "s", // 關(guān)鍵字
"completion": {
"field": "title", // 補(bǔ)全查詢的字段
"skip_duplicates": true, // 跳過重復(fù)的
"size": 10 // 獲取前10條結(jié)果
}
}
}
}
2.4.實(shí)現(xiàn)酒店搜索框自動(dòng)補(bǔ)全
現(xiàn)在,我們的hotel索引庫還沒有設(shè)置拼音分詞器,需要修改索引庫中的配置。但是我們知道索引庫是無法修改的,只能刪除然后重新創(chuàng)建。
另外,我們需要添加一個(gè)字段,用來做自動(dòng)補(bǔ)全,將brand、suggestion、city等都放進(jìn)去,作為自動(dòng)補(bǔ)全的提示。
因此,總結(jié)一下,我們需要做的事情包括:
-
修改hotel索引庫結(jié)構(gòu),設(shè)置自定義拼音分詞器
-
修改索引庫的name、all字段,使用自定義分詞器
-
索引庫添加一個(gè)新字段suggestion,類型為completion類型,使用自定義的分詞器
-
給HotelDoc類添加suggestion字段,內(nèi)容包含brand、business
-
重新導(dǎo)入數(shù)據(jù)到hotel庫
2.4.1.修改酒店映射結(jié)構(gòu)
代碼如下:
// 酒店數(shù)據(jù)索引庫
PUT /hotel
{
"settings": {
"analysis": {
"analyzer": {
"text_anlyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
},
"completion_analyzer": {
"tokenizer": "keyword", //詞條 不分詞
"filter": "py"
}
},
"filter": {
"py": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
},
"mappings": {
"properties": {
"id":{
"type": "keyword"
},
"name":{
"type": "text",
"analyzer": "text_anlyzer",
"search_analyzer": "ik_smart",
"copy_to": "all"
},
"address":{
"type": "keyword",
"index": false
},
"price":{
"type": "integer"
},
"score":{
"type": "integer"
},
"brand":{
"type": "keyword",
"copy_to": "all"
},
"city":{
"type": "keyword"
},
"starName":{
"type": "keyword"
},
"business":{
"type": "keyword",
"copy_to": "all"
},
"location":{
"type": "geo_point"
},
"pic":{
"type": "keyword",
"index": false
},
"all":{
"type": "text",
"analyzer": "text_anlyzer",
"search_analyzer": "ik_smart"
},
"suggestion":{
"type": "completion",
"analyzer": "completion_analyzer"
}
}
}
}
2.4.2.修改HotelDoc實(shí)體
HotelDoc中要添加一個(gè)字段,用來做自動(dòng)補(bǔ)全,內(nèi)容可以是酒店品牌、城市、商圈等信息。按照自動(dòng)補(bǔ)全字段的要求,最好是這些字段的數(shù)組。
因此我們在HotelDoc中添加一個(gè)suggestion字段,類型為List<String>,然后將brand、city、business等信息放到里面。
代碼如下:
package cn.itcast.hotel.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@Data
@NoArgsConstructor
public class HotelDoc {
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String brand;
private String city;
private String starName;
private String business;
private String location;
private String pic;
private Object distance;
private Boolean isAD;
private List<String> suggestion;
public HotelDoc(Hotel hotel) {
this.id = hotel.getId();
this.name = hotel.getName();
this.address = hotel.getAddress();
this.price = hotel.getPrice();
this.score = hotel.getScore();
this.brand = hotel.getBrand();
this.city = hotel.getCity();
this.starName = hotel.getStarName();
this.business = hotel.getBusiness();
this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
this.pic = hotel.getPic();
// 組裝suggestion
if(this.business.contains("/")){
// business有多個(gè)值,需要切割
String[] arr = this.business.split("/");
// 添加元素
this.suggestion = new ArrayList<>();
this.suggestion.add(this.brand);
Collections.addAll(this.suggestion, arr);
}else {
this.suggestion = Arrays.asList(this.brand, this.business);
}
}
}
2.4.3.重新導(dǎo)入
重新執(zhí)行之前編寫的導(dǎo)入數(shù)據(jù)功能,可以看到新的酒店數(shù)據(jù)中包含了suggestion:

2.4.4.自動(dòng)補(bǔ)全查詢的JavaAPI
之前我們學(xué)習(xí)了自動(dòng)補(bǔ)全查詢的DSL,而沒有學(xué)習(xí)對應(yīng)的JavaAPI,這里給出一個(gè)示例:

而自動(dòng)補(bǔ)全的結(jié)果也比較特殊,解析的代碼如下:

2.4.5.實(shí)現(xiàn)搜索框自動(dòng)補(bǔ)全
查看前端頁面,可以發(fā)現(xiàn)當(dāng)我們在輸入框鍵入時(shí),前端會(huì)發(fā)起ajax請求:

返回值是補(bǔ)全詞條的集合,類型為List<String>
1)在cn.itcast.hotel.web包下的HotelController中添加新接口,接收新的請求:
@GetMapping("suggestion")
public List<String> getSuggestions(@RequestParam("key") String prefix) {
return hotelService.getSuggestions(prefix);
}
2)在cn.itcast.hotel.service包下的IhotelService中添加方法:
List<String> getSuggestions(String prefix);
3)在cn.itcast.hotel.service.impl.HotelService中實(shí)現(xiàn)該方法:
@Override
public List<String> getSuggestions(String prefix) {
try {
// 1.準(zhǔn)備Request
SearchRequest request = new SearchRequest("hotel");
// 2.準(zhǔn)備DSL
request.source().suggest(new SuggestBuilder().addSuggestion(
"suggestions",
? ? ? ? ? ? ? ?//要補(bǔ)全的字段"suggestion"
SuggestBuilders.completionSuggestion("suggestion")
.prefix(prefix)//關(guān)鍵字
.skipDuplicates(true)
.size(10)//獲取前10條
));
// 3.發(fā)起請求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析結(jié)果
Suggest suggest = response.getSuggest();
// 4.1.根據(jù)補(bǔ)全查詢名稱,獲取補(bǔ)全結(jié)果
CompletionSuggestion suggestions = suggest.getSuggestion("suggestions");
// 4.2.獲取options
List<CompletionSuggestion.Entry.Option> options = suggestions.getOptions();
// 4.3.遍歷
List<String> list = new ArrayList<>(options.size());
for (CompletionSuggestion.Entry.Option option : options) {
String text = option.getText().toString();
list.add(text);
}
return list;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
3.數(shù)據(jù)同步
elasticsearch中的酒店數(shù)據(jù)來自于mysql數(shù)據(jù)庫,因此mysql數(shù)據(jù)發(fā)生改變時(shí),elasticsearch也必須跟著改變,這個(gè)就是elasticsearch與mysql之間的數(shù)據(jù)同步。

3.1.思路分析
常見的數(shù)據(jù)同步方案有三種:
-
同步調(diào)用
-
異步通知
-
監(jiān)聽binlog
3.1.1.同步調(diào)用
方案一:同步調(diào)用

基本步驟如下:
-
hotel-demo對外提供接口,用來修改elasticsearch中的數(shù)據(jù)
-
酒店管理服務(wù)在完成數(shù)據(jù)庫操作后,直接調(diào)用hotel-demo提供的接口,
3.1.2.異步通知
方案二:異步通知

流程如下:
-
hotel-admin對mysql數(shù)據(jù)庫數(shù)據(jù)完成增、刪、改后,發(fā)送MQ消息
-
hotel-demo監(jiān)聽MQ,接收到消息后完成elasticsearch數(shù)據(jù)修改
3.1.3.監(jiān)聽binlog
方案三:監(jiān)聽binlog

流程如下:
-
給mysql開啟binlog功能
-
mysql完成增、刪、改操作都會(huì)記錄在binlog中
-
hotel-demo基于canal監(jiān)聽binlog變化,實(shí)時(shí)更新elasticsearch中的內(nèi)容
3.1.4.選擇
方式一:同步調(diào)用
-
優(yōu)點(diǎn):實(shí)現(xiàn)簡單,粗暴
-
缺點(diǎn):業(yè)務(wù)耦合度高
方式二:異步通知
-
優(yōu)點(diǎn):低耦合,實(shí)現(xiàn)難度一般
-
缺點(diǎn):依賴mq的可靠性
方式三:監(jiān)聽binlog
-
優(yōu)點(diǎn):完全解除服務(wù)間耦合
-
缺點(diǎn):開啟binlog增加數(shù)據(jù)庫負(fù)擔(dān)、實(shí)現(xiàn)復(fù)雜度高
3.2.實(shí)現(xiàn)數(shù)據(jù)同步
3.2.1.思路
利用課前資料提供的hotel-admin項(xiàng)目作為酒店管理的微服務(wù)。
當(dāng)酒店數(shù)據(jù)發(fā)生增、刪、改時(shí),要求對elasticsearch中數(shù)據(jù)也要完成相同操作。
步驟:
-
導(dǎo)入課前資料提供的hotel-admin項(xiàng)目,啟動(dòng)并測試酒店數(shù)據(jù)的CRUD
-
聲明exchange、queue、RoutingKey
-
在hotel-admin中的增、刪、改業(yè)務(wù)中完成消息發(fā)送
-
在hotel-demo中完成消息監(jiān)聽,并更新elasticsearch中數(shù)據(jù)
-
啟動(dòng)并測試數(shù)據(jù)同步功能
3.2.2.導(dǎo)入demo
導(dǎo)入課前資料提供的hotel-admin項(xiàng)目:

運(yùn)行后,訪問 http://localhost:8099

其中包含了酒店的CRUD功能:

3.2.3.聲明交換機(jī)、隊(duì)列
MQ結(jié)構(gòu)如圖:

1)引入依賴
在hotel-admin、hotel-demo中引入rabbitmq的依賴:
<!--amqp-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2)聲明隊(duì)列交換機(jī)名稱
在hotel-admin和hotel-demo中的cn.itcast.hotel.constatnts包下新建一個(gè)類MqConstants:
package cn.itcast.hotel.constatnts;
public class MqConstants {
/**
* 交換機(jī)
*/
public final static String HOTEL_EXCHANGE = "hotel.topic";
/**
* 監(jiān)聽新增和修改的隊(duì)列
*/
public final static String HOTEL_INSERT_QUEUE = "hotel.insert.queue";
/**
* 監(jiān)聽刪除的隊(duì)列
*/
public final static String HOTEL_DELETE_QUEUE = "hotel.delete.queue";
/**
* 新增或修改的RoutingKey
*/
public final static String HOTEL_INSERT_KEY = "hotel.insert";
/**
* 刪除的RoutingKey
*/
public final static String HOTEL_DELETE_KEY = "hotel.delete";
}
3)聲明隊(duì)列交換機(jī)
在hotel-demo中,定義配置類,聲明隊(duì)列、交換機(jī):
package cn.itcast.hotel.config;
import cn.itcast.hotel.constants.MqConstants;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MqConfig {
@Bean
public TopicExchange topicExchange(){
return new TopicExchange(MqConstants.HOTEL_EXCHANGE, true, false);
}
@Bean
public Queue insertQueue(){
return new Queue(MqConstants.HOTEL_INSERT_QUEUE, true);
}
@Bean
public Queue deleteQueue(){
return new Queue(MqConstants.HOTEL_DELETE_QUEUE, true);
}
@Bean
public Binding insertQueueBinding(){
return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(MqConstants.HOTEL_INSERT_KEY);
}
@Bean
public Binding deleteQueueBinding(){
return BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(MqConstants.HOTEL_DELETE_KEY);
}
}
3.2.4.發(fā)送MQ消息
在hotel-admin中的增、刪、改業(yè)務(wù)中分別發(fā)送MQ消息:

3.2.5.接收MQ消息
hotel-demo接收到MQ消息要做的事情包括:
-
新增消息:根據(jù)傳遞的hotel的id查詢hotel信息,然后新增一條數(shù)據(jù)到索引庫
-
刪除消息:根據(jù)傳遞的hotel的id刪除索引庫中的一條數(shù)據(jù)
1)首先在hotel-demo的cn.itcast.hotel.service包下的IHotelService中新增新增、刪除業(yè)務(wù)
void deleteById(Long id);
void insertById(Long id);
2)給hotel-demo中的cn.itcast.hotel.service.impl包下的HotelService中實(shí)現(xiàn)業(yè)務(wù):
@Override
public void deleteById(Long id) {
try {
// 1.準(zhǔn)備Request
DeleteRequest request = new DeleteRequest("hotel", id.toString());
// 2.發(fā)送請求
client.delete(request, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void insertById(Long id) {
try {
// 0.根據(jù)id查詢酒店數(shù)據(jù)
Hotel hotel = getById(id);
// 轉(zhuǎn)換為文檔類型
HotelDoc hotelDoc = new HotelDoc(hotel);
// 1.準(zhǔn)備Request對象
IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
// 2.準(zhǔn)備Json文檔
request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
// 3.發(fā)送請求
client.index(request, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
3)編寫監(jiān)聽器
在hotel-demo中的cn.itcast.hotel.mq包新增一個(gè)類:
package cn.itcast.hotel.mq;
import cn.itcast.hotel.constants.MqConstants;
import cn.itcast.hotel.service.IHotelService;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class HotelListener {
@Autowired
private IHotelService hotelService;
/**
* 監(jiān)聽酒店新增或修改的業(yè)務(wù)
* @param id 酒店id
*/
@RabbitListener(queues = MqConstants.HOTEL_INSERT_QUEUE)
public void listenHotelInsertOrUpdate(Long id){
hotelService.insertById(id);
}
/**
* 監(jiān)聽酒店刪除的業(yè)務(wù)
* @param id 酒店id
*/
@RabbitListener(queues = MqConstants.HOTEL_DELETE_QUEUE)
public void listenHotelDelete(Long id){
hotelService.deleteById(id);
}
}
4.集群
單機(jī)的elasticsearch做數(shù)據(jù)存儲(chǔ),必然面臨兩個(gè)問題:海量數(shù)據(jù)存儲(chǔ)問題、單點(diǎn)故障問題。
-
海量數(shù)據(jù)存儲(chǔ)問題:將索引庫從邏輯上拆分為N個(gè)分片(shard),存儲(chǔ)到多個(gè)節(jié)點(diǎn)
-
單點(diǎn)故障問題:將分片數(shù)據(jù)在不同節(jié)點(diǎn)備份(replica )
ES集群相關(guān)概念:
-
集群(cluster):一組擁有共同的 cluster name 的 節(jié)點(diǎn)。
-
節(jié)點(diǎn)(node) :集群中的一個(gè) Elasticearch 實(shí)例
-
分片(shard) :索引可以被拆分為不同的部分進(jìn)行存儲(chǔ),稱為分片。
-
在集群環(huán)境下,一個(gè)索引的不同分片可以拆分到不同的節(jié)點(diǎn)中
解決問題:數(shù)據(jù)量太大,單點(diǎn)存儲(chǔ)量有限的問題。

此處,我們把數(shù)據(jù)分成3片:shard0、shard1、shard2
-
主分片(Primary shard):相對于副本分片的定義。
-
副本分片(Replica shard)每個(gè)主分片可以有一個(gè)或者多個(gè)副本,數(shù)據(jù)和主分片一樣。
數(shù)據(jù)備份可以保證高可用,但是每個(gè)分片備份一份,所需要的節(jié)點(diǎn)數(shù)量就會(huì)翻一倍,成本實(shí)在是太高了!
為了在高可用和成本間尋求平衡,我們可以這樣做:
-
首先對數(shù)據(jù)分片,存儲(chǔ)到不同節(jié)點(diǎn)
-
然后對每個(gè)分片進(jìn)行備份,放到對方節(jié)點(diǎn),完成互相備份
這樣可以大大減少所需要的服務(wù)節(jié)點(diǎn)數(shù)量,如圖,我們以3分片,每個(gè)分片備份一份為例:

現(xiàn)在,每個(gè)分片都有1個(gè)備份,存儲(chǔ)在3個(gè)節(jié)點(diǎn):
-
node0:保存了分片0和1
-
node1:保存了分片0和2
-
node2:保存了分片1和2
4.1.搭建ES集群
參考課前資料的文檔:
需要虛擬機(jī)內(nèi)存>4G;我的虛擬機(jī)2G 就沒做

其中的第四章節(jié):

4.2.集群腦裂問題
4.2.1.集群職責(zé)劃分
elasticsearch中集群節(jié)點(diǎn)有不同的職責(zé)劃分:

默認(rèn)情況下,集群中的任何一個(gè)節(jié)點(diǎn)都同時(shí)具備上述四種角色。
但是真實(shí)的集群一定要將集群職責(zé)分離:
-
master節(jié)點(diǎn):對CPU要求高,但是內(nèi)存要求不特別高
-
data節(jié)點(diǎn):對CPU和內(nèi)存要求都高
-
coordinating節(jié)點(diǎn):對網(wǎng)絡(luò)帶寬、CPU要求高
職責(zé)分離可以讓我們根據(jù)不同節(jié)點(diǎn)的需求分配不同的硬件去部署。而且避免業(yè)務(wù)之間的互相干擾。
一個(gè)典型的es集群職責(zé)劃分如圖:

4.2.2.腦裂問題
腦裂是因?yàn)榧褐械墓?jié)點(diǎn)失聯(lián)導(dǎo)致的。
例如一個(gè)集群中,主節(jié)點(diǎn)與其它節(jié)點(diǎn)失聯(lián):

此時(shí),node2和node3認(rèn)為node1宕機(jī),就會(huì)重新選主:

當(dāng)node3當(dāng)選后,集群繼續(xù)對外提供服務(wù),node2和node3自成集群,node1自成集群,兩個(gè)集群數(shù)據(jù)不同步,出現(xiàn)數(shù)據(jù)差異。
當(dāng)網(wǎng)絡(luò)恢復(fù)后,因?yàn)榧褐?span style="background-color:#e6d6f0;">有兩個(gè)master節(jié)點(diǎn),集群狀態(tài)的不一致,出現(xiàn)腦裂的情況:

解決腦裂的方案是,要求選票超過 ( eligible節(jié)點(diǎn)數(shù)量 + 1 )/ 2 才能當(dāng)選為主,因此eligible節(jié)點(diǎn)數(shù)量最好是奇數(shù)。對應(yīng)配置項(xiàng)是discovery.zen.minimum_master_nodes,在es7.0以后,已經(jīng)成為默認(rèn)配置,因此一般不會(huì)發(fā)生腦裂問題
例如:3個(gè)節(jié)點(diǎn)形成的集群,選票必須超過 (3 + 1) / 2 ,也就是2票。node3得到node2和node3的選票,當(dāng)選為主。node1只有自己1票,沒有當(dāng)選。集群中依然只有1個(gè)主節(jié)點(diǎn),沒有出現(xiàn)腦裂。
4.2.3.小結(jié)
master eligible節(jié)點(diǎn)的作用是什么?
-
參與集群選主
-
主節(jié)點(diǎn)可以管理集群狀態(tài)、管理分片信息、處理創(chuàng)建和刪除索引庫的請求
data節(jié)點(diǎn)的作用是什么?
-
數(shù)據(jù)的CRUD
coordinator節(jié)點(diǎn)的作用是什么?
-
路由請求到其它節(jié)點(diǎn)
-
合并查詢到的結(jié)果,返回給用戶
4.3.集群分布式存儲(chǔ)
當(dāng)新增文檔時(shí),應(yīng)該保存到不同分片,保證數(shù)據(jù)均衡,那么coordinating node如何確定數(shù)據(jù)該存儲(chǔ)到哪個(gè)分片呢?
4.3.1.分片存儲(chǔ)測試
插入三條數(shù)據(jù):



測試可以看到,三條數(shù)據(jù)分別在不同分片:

結(jié)果:

4.3.2.分片存儲(chǔ)原理
elasticsearch會(huì)通過hash算法來計(jì)算文檔應(yīng)該存儲(chǔ)到哪個(gè)分片:

說明:
-
_routing默認(rèn)是文檔的id
-
算法與分片數(shù)量(number_of_shards)有關(guān),因此索引庫一旦創(chuàng)建,分片數(shù)量不能修改!
新增文檔的流程如下:

解讀:
-
1)新增一個(gè)id=1的文檔
-
2)對id做hash運(yùn)算,假如得到的是2,則應(yīng)該存儲(chǔ)到shard-2
-
3)shard-2的主分片在node3節(jié)點(diǎn),將數(shù)據(jù)路由到node3
-
4)保存文檔
-
5)同步給shard-2的副本replica-2,在node2節(jié)點(diǎn)
-
6)返回結(jié)果給coordinating-node節(jié)點(diǎn)
4.4.集群分布式查詢
elasticsearch的查詢分成兩個(gè)階段:
-
scatter phase:分散階段,coordinating node會(huì)把請求分發(fā)到每一個(gè)分片
-
gather phase:聚集階段,coordinating node匯總data node的搜索結(jié)果,并處理為最終結(jié)果集返回給用戶

4.5.集群故障轉(zhuǎn)移
集群的master節(jié)點(diǎn)會(huì)監(jiān)控集群中的節(jié)點(diǎn)狀態(tài),如果發(fā)現(xiàn)有節(jié)點(diǎn)宕機(jī),會(huì)立即將宕機(jī)節(jié)點(diǎn)的分片數(shù)據(jù)遷移到其它節(jié)點(diǎn),確保數(shù)據(jù)安全,這個(gè)叫做故障轉(zhuǎn)移。
1)例如一個(gè)集群結(jié)構(gòu)如圖:

現(xiàn)在,node1是主節(jié)點(diǎn),其它兩個(gè)節(jié)點(diǎn)是從節(jié)點(diǎn)。
2)突然,node1發(fā)生了故障:

宕機(jī)后的第一件事,需要重新選主,例如選中了node2:

node2成為主節(jié)點(diǎn)后,會(huì)檢測集群監(jiān)控狀態(tài),發(fā)現(xiàn):shard-1、shard-0沒有副本節(jié)點(diǎn)。因此需要將node1上的數(shù)據(jù)遷移到node2、node3:

微服務(wù)保護(hù)
1.初識Sentinel
1.1.雪崩問題及解決方案
1.1.1.雪崩問題
微服務(wù)中,服務(wù)間調(diào)用關(guān)系錯(cuò)綜復(fù)雜,一個(gè)微服務(wù)往往依賴于多個(gè)其它微服務(wù)。

如圖,如果服務(wù)提供者I發(fā)生了故障,當(dāng)前的應(yīng)用的部分業(yè)務(wù)因?yàn)橐蕾囉诜?wù)I,因此也會(huì)被阻塞。此時(shí),其它不依賴于服務(wù)I的業(yè)務(wù)似乎不受影響。

但是,依賴服務(wù)I的業(yè)務(wù)請求被阻塞,用戶不會(huì)得到響應(yīng),則tomcat的這個(gè)線程不會(huì)釋放,于是越來越多的用戶請求到來,越來越多的線程會(huì)阻塞:

服務(wù)器支持的線程和并發(fā)數(shù)有限,請求一直阻塞,會(huì)導(dǎo)致服務(wù)器資源耗盡,從而導(dǎo)致所有其它服務(wù)都不可用,那么當(dāng)前服務(wù)也就不可用了。
那么,依賴于當(dāng)前服務(wù)的其它服務(wù)隨著時(shí)間的推移,最終也都會(huì)變的不可用,形成級聯(lián)失敗,雪崩就發(fā)生了:

1.1.2.超時(shí)處理
解決雪崩問題的常見方式有四種:
?超時(shí)處理:設(shè)定超時(shí)時(shí)間,請求超過一定時(shí)間沒有響應(yīng)就返回錯(cuò)誤信息,不會(huì)無休止等待

1.1.3.倉壁模式
方案2:倉壁模式
倉壁模式來源于船艙的設(shè)計(jì),
船艙都會(huì)被隔板分離為多個(gè)獨(dú)立空間,當(dāng)船體破損時(shí),只會(huì)導(dǎo)致部分空間進(jìn)入,將故障控制在一定范圍內(nèi),避免整個(gè)船體都被淹沒。
于此類似,我們可以限定每個(gè)業(yè)務(wù)能使用的線程數(shù),避免耗盡整個(gè)tomcat的資源,因此也叫線程隔離。

缺點(diǎn):資源浪費(fèi),明明知道服務(wù)C掛了,請求來了還要嘗試訪問服務(wù)C占用線程;
1.1.4.斷路器
斷路器模式:
由斷路器統(tǒng)計(jì)業(yè)務(wù)執(zhí)行的異常比例,如果超出閾值則會(huì)熔斷該業(yè)務(wù),攔截訪問該業(yè)務(wù)的一切請求。
斷路器會(huì)統(tǒng)計(jì)訪問某個(gè)服務(wù)的請求數(shù)量,異常比例:

當(dāng)發(fā)現(xiàn)訪問服務(wù)D的請求異常比例過高時(shí),認(rèn)為服務(wù)D有導(dǎo)致雪崩的風(fēng)險(xiǎn),會(huì)攔截訪問服務(wù)D的一切請求,形成熔斷:

1.1.5.限流
流量控制:限制業(yè)務(wù)訪問的QPS(每秒處理請求的數(shù)量),避免服務(wù)因流量的突增而故障。

1.1.6.總結(jié)
什么是雪崩問題?
-
微服務(wù)之間相互調(diào)用,因?yàn)?span style="color:#116af0;">調(diào)用鏈中的一個(gè)服務(wù)故障,引起整個(gè)鏈路都無法訪問的情況。
可以認(rèn)為:
限流是對服務(wù)的保護(hù),避免因瞬間高并發(fā)流量而導(dǎo)致服務(wù)故障,進(jìn)而避免雪崩。是一種預(yù)防措施。
超時(shí)處理、線程隔離、降級熔斷是在部分服務(wù)故障時(shí),將故障控制在一定范圍,避免雪崩。是一種補(bǔ)救措施。
1.2.服務(wù)保護(hù)技術(shù)對比
在SpringCloud當(dāng)中支持多種服務(wù)保護(hù)技術(shù):
-
Netfix Hystrix
-
Sentinel
-
Resilience4J
早期比較流行的是Hystrix框架(已經(jīng)停止升級與維護(hù)),但目前國內(nèi)實(shí)用最廣泛的還是阿里巴巴的Sentinel框架,這里我們做下對比:
Sentinel |
Hystrix |
|
隔離策略 |
信號量隔離 |
線程池隔離/信號量隔離 |
熔斷降級策略 |
基于慢調(diào)用比例或異常比例 |
基于失敗比率 |
實(shí)時(shí)指標(biāo)實(shí)現(xiàn) |
滑動(dòng)窗口 |
滑動(dòng)窗口(基于 RxJava) |
規(guī)則配置 |
支持多種數(shù)據(jù)源 |
支持多種數(shù)據(jù)源 |
擴(kuò)展性 |
多個(gè)擴(kuò)展點(diǎn) |
插件的形式 |
基于注解的支持 |
支持 |
支持 |
限流 |
基于 QPS,支持基于調(diào)用關(guān)系的限流 |
有限的支持 |
流量整形 |
支持慢啟動(dòng)、勻速排隊(duì)模式 |
不支持 |
系統(tǒng)自適應(yīng)保護(hù) |
支持 |
不支持 |
控制臺(tái) |
開箱即用,可配置規(guī)則、查看秒級監(jiān)控、機(jī)器發(fā)現(xiàn)等 |
不完善 |
常見框架的適配 |
Servlet、Spring Cloud、Dubbo、gRPC 等 |
Servlet、Spring Cloud Netflix |
1.3.Sentinel介紹和安裝
1.3.1.初識Sentinel
Sentinel是阿里巴巴開源的一款微服務(wù)流量控制組件。
官網(wǎng)地址:home | Sentinel
Sentinel 具有以下特征:
?豐富的應(yīng)用場景:Sentinel 承接了阿里巴巴近 10 年的雙十一大促流量的核心場景,例如秒殺(即突發(fā)流量控制在系統(tǒng)容量可以承受的范圍)、消息削峰填谷、集群流量控制、實(shí)時(shí)熔斷下游不可用應(yīng)用等。
?完備的實(shí)時(shí)監(jiān)控:Sentinel 同時(shí)提供實(shí)時(shí)的監(jiān)控功能。您可以在控制臺(tái)中看到接入應(yīng)用的單臺(tái)機(jī)器秒級數(shù)據(jù),甚至 500 臺(tái)以下規(guī)模的集群的匯總運(yùn)行情況。
?廣泛的開源生態(tài):Sentinel 提供開箱即用的與其它開源框架/庫的整合模塊,例如與 SpringCloud、Dubbo、gRPC 的整合。您只需要引入相應(yīng)的依賴并進(jìn)行簡單的配置即可快速地接入 Sentinel。
?完善的 SPI 擴(kuò)展點(diǎn):Sentinel 提供簡單易用、完善的 SPI 擴(kuò)展接口。您可以通過實(shí)現(xiàn)擴(kuò)展接口來快速地定制邏輯。例如定制規(guī)則管理、適配動(dòng)態(tài)數(shù)據(jù)源等。
1.3.2.安裝Sentinel
1)下載
sentinel官方提供了UI控制臺(tái),方便我們對系統(tǒng)做限流設(shè)置。大家可以在GitHub下載。
課前資料也提供了下載好的jar包:

2)運(yùn)行
將jar包放到任意非中文目錄,執(zhí)行命令:
java -jar sentinel-dashboard-1.8.1.jar
如果要修改Sentinel的默認(rèn)端口、賬戶、密碼,可以通過下列配置:
配置項(xiàng) |
默認(rèn)值 |
說明 |
server.port |
8080 |
服務(wù)端口 |
sentinel.dashboard.auth.username |
sentinel |
默認(rèn)用戶名 |
sentinel.dashboard.auth.password |
sentinel |
默認(rèn)密碼 |
例如,修改端口:
報(bào)錯(cuò)的要,配置jdk8的環(huán)境變量;set Path=D:\ItheimaJava\jdk1.8.0_361\bin
報(bào)錯(cuò)的在控制臺(tái)先指定jdk版本。set Path=你的jdk里jre位置\bin

java -Dserver.port=8090 -jar sentinel-dashboard-1.8.1.jar
3)訪問
訪問http://localhost:8080頁面,就可以看到sentinel的控制臺(tái)了:

需要輸入賬號和密碼,默認(rèn)都是:sentinel
登錄后,發(fā)現(xiàn)一片空白,什么都沒有:

這是因?yàn)槲覀冞€沒有與微服務(wù)整合。

修改的話 就-Dserver.port=8090;或者
在jar包目錄寫一個(gè)配置文件就可以了;在包下寫個(gè)yml 配個(gè)server port 就會(huì)覆蓋了
jar包同級的同名配置文件,優(yōu)先級比 classpath要高的呀,可以在jar包同級目錄里創(chuàng)建配置文件的
更詳細(xì)的修改配置:

1.4.微服務(wù)整合Sentinel


先停一下,前面的項(xiàng)目沒跑,得跑一下,要不直接做黑馬點(diǎn)評吧,感這種“假”微服務(wù)沒意義
我們在order-service中整合sentinel,并連接sentinel的控制臺(tái),步驟如下:
1)引入sentinel依賴
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
2)配置控制臺(tái)
修改application.yaml文件,添加下面內(nèi)容:
server:
port: 8088
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080
3)訪問order-service的任意端點(diǎn)
打開瀏覽器,訪問http://localhost:8088/order/101,這樣才能觸發(fā)sentinel的監(jiān)控。
然后再訪問sentinel的控制臺(tái),查看效果:

2.流量控制
雪崩問題雖然有四種方案,但是限流是避免服務(wù)因突發(fā)的流量而發(fā)生故障,是對微服務(wù)雪崩問題的預(yù)防。我們先學(xué)習(xí)這種模式。
2.1.簇點(diǎn)鏈路
當(dāng)請求進(jìn)入微服務(wù)時(shí),首先會(huì)訪問DispatcherServlet,然后進(jìn)入Controller、Service、Mapper,這樣的一個(gè)調(diào)用鏈就叫做簇點(diǎn)鏈路。簇點(diǎn)鏈路中被監(jiān)控的每一個(gè)接口就是一個(gè)資源。
默認(rèn)情況下sentinel會(huì)監(jiān)控SpringMVC的每一個(gè)端點(diǎn)(Endpoint,也就是controller中的方法),因此SpringMVC的每一個(gè)端點(diǎn)(Endpoint)就是調(diào)用鏈路中的一個(gè)資源。
例如,我們剛才訪問的order-service中的OrderController中的端點(diǎn):/order/{orderId}

流控、熔斷等都是針對簇點(diǎn)鏈路中的資源來設(shè)置的,因此我們可以點(diǎn)擊對應(yīng)資源后面的按鈕來設(shè)置規(guī)則:
-
流控:流量控制
-
降級:降級熔斷
-
熱點(diǎn):熱點(diǎn)參數(shù)限流,是限流的一種
-
授權(quán):請求的權(quán)限控制
2.1.快速入門
2.1.1.示例
點(diǎn)擊資源/order/{orderId}后面的流控按鈕,就可以彈出表單。
表單中可以填寫限流規(guī)則,如下:
其含義是限制 /order/{orderId}這個(gè)資源的單機(jī)QPS為1,即每秒只允許1次請求,超出的請求會(huì)被攔截并報(bào)錯(cuò)。
2.1.2.練習(xí):
需求:給 /order/{orderId}這個(gè)資源設(shè)置流控規(guī)則,QPS不能超過 5,然后測試。
1)首先在sentinel控制臺(tái)添加限流規(guī)則
?
2)利用jmeter測試
如果沒有用過jmeter,可以參考課前資料提供的文檔《Jmeter快速入門.md》
課前資料提供了編寫好的Jmeter測試樣例:
?
打開jmeter,導(dǎo)入課前資料提供的測試樣例:
?
選擇:
?
20個(gè)用戶,2秒內(nèi)運(yùn)行完,QPS是10,超過了5.
選中流控入門,QPS<5右鍵運(yùn)行:
?
注意,不要點(diǎn)擊菜單中的執(zhí)行按鈕來運(yùn)行。
結(jié)果:
?
可以看到,成功的請求每次只有5個(gè)
2.2.流控模式
在添加限流規(guī)則時(shí),點(diǎn)擊高級選項(xiàng),可以選擇三種流控模式:
-
直接:統(tǒng)計(jì)當(dāng)前資源的請求,觸發(fā)閾值時(shí)對當(dāng)前資源直接限流,也是默認(rèn)的模式
-
關(guān)聯(lián):統(tǒng)計(jì)與當(dāng)前資源相關(guān)的另一個(gè)資源,觸發(fā)閾值時(shí),對當(dāng)前資源限流
-
鏈路:統(tǒng)計(jì)從指定鏈路訪問到本資源的請求,觸發(fā)閾值時(shí),對指定鏈路限流
快速入門測試的就是直接模式。
?
2.2.1.關(guān)聯(lián)模式
關(guān)聯(lián)模式:統(tǒng)計(jì)與當(dāng)前資源相關(guān)的另一個(gè)資源,觸發(fā)閾值時(shí),對當(dāng)前資源限流
配置規(guī)則:
?
語法說明:當(dāng)/write資源訪問量觸發(fā)閾值時(shí),就會(huì)對/read資源限流,避免影響/write資源。
使用場景:比如用戶支付時(shí)需要修改訂單狀態(tài),同時(shí)用戶要查詢訂單。查詢和修改操作會(huì)爭搶數(shù)據(jù)庫鎖,產(chǎn)生競爭。業(yè)務(wù)需求是優(yōu)先支付和更新訂單的業(yè)務(wù),因此當(dāng)修改訂單業(yè)務(wù)觸發(fā)閾值時(shí),需要對查詢訂單業(yè)務(wù)限流。
需求說明:
-
在OrderController新建兩個(gè)端點(diǎn):/order/query和/order/update,無需實(shí)現(xiàn)業(yè)務(wù)
-
配置流控規(guī)則,當(dāng)/order/ update資源被訪問的QPS超過5時(shí),對/order/query請求限流
1)定義/order/query端點(diǎn),模擬訂單查詢
@GetMapping("/query")
public String queryOrder() {
return "查詢訂單成功";
}
2)定義/order/update端點(diǎn),模擬訂單更新
@GetMapping("/update")
public String updateOrder() {
return "更新訂單成功";
}
重啟服務(wù),查看sentinel控制臺(tái)的簇點(diǎn)鏈路:
?
3)配置流控規(guī)則
對哪個(gè)端點(diǎn)限流,就點(diǎn)擊哪個(gè)端點(diǎn)后面的按鈕。我們是對訂單查詢/order/query限流,因此點(diǎn)擊它后面的按鈕:
?
在表單中填寫流控規(guī)則:
?
4)在Jmeter測試
選擇《流控模式-關(guān)聯(lián)》:
?
可以看到1000個(gè)用戶,100秒,因此QPS為10,超過了我們設(shè)定的閾值:5
查看http請求:
?
請求的目標(biāo)是/order/update,這樣這個(gè)斷點(diǎn)就會(huì)觸發(fā)閾值。
但限流的目標(biāo)是/order/query,我們在瀏覽器訪問,可以發(fā)現(xiàn):
?
確實(shí)被限流了。
5)總結(jié)
?
2.2.2.鏈路模式
鏈路模式:只針對從指定鏈路訪問到本資源的請求做統(tǒng)計(jì),判斷是否超過閾值。
配置示例:
例如有兩條請求鏈路:
-
/test1 --> /common
-
/test2 --> /common
如果只希望統(tǒng)計(jì)從/test2進(jìn)入到/common的請求,則可以這樣配置:
?
實(shí)戰(zhàn)案例
需求:有查詢訂單和創(chuàng)建訂單業(yè)務(wù),兩者都需要查詢商品。針對從查詢訂單進(jìn)入到查詢商品的請求統(tǒng)計(jì),并設(shè)置限流。
步驟:
-
在OrderService中添加一個(gè)queryGoods方法,不用實(shí)現(xiàn)業(yè)務(wù)
-
在OrderController中,改造/order/query端點(diǎn),調(diào)用OrderService中的queryGoods方法
-
在OrderController中添加一個(gè)/order/save的端點(diǎn),調(diào)用OrderService的queryGoods方法
-
給queryGoods設(shè)置限流規(guī)則,從/order/query進(jìn)入queryGoods的方法限制QPS必須小于2
實(shí)現(xiàn):
1)添加查詢商品方法
在order-service服務(wù)中,給OrderService類添加一個(gè)queryGoods方法:
public void queryGoods(){
System.err.println("查詢商品");
}
2)查詢訂單時(shí),查詢商品
在order-service的OrderController中,修改/order/query端點(diǎn)的業(yè)務(wù)邏輯:
@GetMapping("/query")
public String queryOrder() {
// 查詢商品
orderService.queryGoods();
// 查詢訂單
System.out.println("查詢訂單");
return "查詢訂單成功";
}
3)新增訂單,查詢商品
在order-service的OrderController中,修改/order/save端點(diǎn),模擬新增訂單:
@GetMapping("/save")
public String saveOrder() {
// 查詢商品
orderService.queryGoods();
// 查詢訂單
System.err.println("新增訂單");
return "新增訂單成功";
}
4)給查詢商品添加資源標(biāo)記
默認(rèn)情況下,OrderService中的方法是不被Sentinel監(jiān)控的,需要我們自己通過注解來標(biāo)記要監(jiān)控的方法。
給OrderService的queryGoods方法添加@SentinelResource注解:
@SentinelResource("goods")
public void queryGoods(){
System.err.println("查詢商品");
}
鏈路模式中,是對不同來源的兩個(gè)鏈路做監(jiān)控。但是sentinel默認(rèn)會(huì)給進(jìn)入SpringMVC的所有請求設(shè)置同一個(gè)root資源,會(huì)導(dǎo)致鏈路模式失效。
我們需要關(guān)閉這種對SpringMVC的資源聚合,修改order-service服務(wù)的application.yml文件:
spring:
cloud:
sentinel:
web-context-unify: false # 關(guān)閉context整合
重啟服務(wù),訪問/order/query和/order/save,可以查看到sentinel的簇點(diǎn)鏈路規(guī)則中,出現(xiàn)了新的資源:
?
5)添加流控規(guī)則
點(diǎn)擊goods資源后面的流控按鈕,在彈出的表單中填寫下面信息:
只統(tǒng)計(jì)從/order/query進(jìn)入/goods的資源,QPS閾值為2,超出則被限流。
6)Jmeter測試
選擇《流控模式-鏈路》:
?
可以看到這里200個(gè)用戶,50秒內(nèi)發(fā)完,QPS為4,超過了我們設(shè)定的閾值2
一個(gè)http請求是訪問/order/save:
?
運(yùn)行的結(jié)果:
?
完全不受影響。
另一個(gè)是訪問/order/query:
?
運(yùn)行結(jié)果:
?
?
每次只有2個(gè)通過。
2.2.3.總結(jié)
流控模式有哪些?
?直接:對當(dāng)前資源限流
?關(guān)聯(lián):高優(yōu)先級資源觸發(fā)閾值,對低優(yōu)先級資源限流。
?鏈路:閾值統(tǒng)計(jì)時(shí),只統(tǒng)計(jì)從指定資源進(jìn)入當(dāng)前資源的請求,是對請求來源的限流
2.3.流控效果
在流控的高級選項(xiàng)中,還有一個(gè)流控效果選項(xiàng):
流控效果是指請求達(dá)到流控閾值時(shí)應(yīng)該采取的措施,包括三種:
-
快速失?。哼_(dá)到閾值后,新的請求會(huì)被立即拒絕并拋出FlowException異常。是默認(rèn)的處理方式。
-
warm up:預(yù)熱模式,對超出閾值的請求同樣是拒絕并拋出異常。但這種模式閾值會(huì)動(dòng)態(tài)變化,從一個(gè)較小值逐漸增加到最大閾值。
-
排隊(duì)等待:讓所有的請求按照先后次序排隊(duì)執(zhí)行,兩個(gè)請求的間隔不能小于指定時(shí)長
2.3.1.warm up
閾值一般是一個(gè)微服務(wù)能承擔(dān)的最大QPS,但是一個(gè)服務(wù)剛剛啟動(dòng)時(shí),一切資源尚未初始化(冷啟動(dòng)),如果直接將QPS跑到最大值,可能導(dǎo)致服務(wù)瞬間宕機(jī)。
warm up也叫預(yù)熱模式,是應(yīng)對服務(wù)冷啟動(dòng)的一種方案。請求閾值初始值是 maxThreshold / coldFactor,持續(xù)指定時(shí)長后,逐漸提高到maxThreshold值。而coldFactor的默認(rèn)值是3.
例如,我設(shè)置QPS的maxThreshold為10,預(yù)熱時(shí)間為5秒,那么初始閾值就是 10 / 3 ,也就是3,然后在5秒后逐漸增長到10.
?
案例
需求:給/order/{orderId}這個(gè)資源設(shè)置限流,最大QPS為10,利用warm up效果,預(yù)熱時(shí)長為5秒
1)配置流控規(guī)則:
?
2)Jmeter測試
選擇《流控效果,warm up》:
?
QPS為10.
剛剛啟動(dòng)時(shí),大部分請求失敗,成功的只有3個(gè),說明QPS被限定在3:
?
隨著時(shí)間推移,成功比例越來越高:
?
到Sentinel控制臺(tái)查看實(shí)時(shí)監(jiān)控:
?
?
一段時(shí)間后:
2.3.2.排隊(duì)等待
當(dāng)請求超過QPS閾值時(shí),快速失敗和warm up 會(huì)拒絕新的請求并拋出異常。
而排隊(duì)等待則是讓所有請求進(jìn)入一個(gè)隊(duì)列中,然后按照閾值允許的時(shí)間間隔依次執(zhí)行。后來的請求必須等待前面執(zhí)行完成,如果請求預(yù)期的等待時(shí)間超出最大時(shí)長,則會(huì)被拒絕。
工作原理
例如:QPS = 5,意味著每200ms處理一個(gè)隊(duì)列中的請求;timeout = 2000,意味著預(yù)期等待時(shí)長超過2000ms的請求會(huì)被拒絕并拋出異常。
那什么叫做預(yù)期等待時(shí)長呢?
比如現(xiàn)在一下子來了12 個(gè)請求,因?yàn)槊?00ms執(zhí)行一個(gè)請求,那么:
-
第6個(gè)請求的預(yù)期等待時(shí)長 = 200 * (6 - 1) = 1000ms
-
第12個(gè)請求的預(yù)期等待時(shí)長 = 200 * (12-1) = 2200ms
現(xiàn)在,第1秒同時(shí)接收到10個(gè)請求,但第2秒只有1個(gè)請求,此時(shí)QPS的曲線這樣的:
?
如果使用隊(duì)列模式做流控,所有進(jìn)入的請求都要排隊(duì),以固定的200ms的間隔執(zhí)行,QPS會(huì)變的很平滑:
?
平滑的QPS曲線,對于服務(wù)器來說是更友好的。
案例
需求:給/order/{orderId}這個(gè)資源設(shè)置限流,最大QPS為10,利用排隊(duì)的流控效果,超時(shí)時(shí)長設(shè)置為5s
1)添加流控規(guī)則
?
2)Jmeter測試
選擇《流控效果,隊(duì)列》:
?
QPS為15,已經(jīng)超過了我們設(shè)定的10。
如果是之前的 快速失敗、warmup模式,超出的請求應(yīng)該會(huì)直接報(bào)錯(cuò)。
但是我們看看隊(duì)列模式的運(yùn)行結(jié)果:
?
全部都通過了。
再去sentinel查看實(shí)時(shí)監(jiān)控的QPS曲線:
?
QPS非常平滑,一致保持在10,但是超出的請求沒有被拒絕,而是放入隊(duì)列。因此響應(yīng)時(shí)間(等待時(shí)間)會(huì)越來越長。
當(dāng)隊(duì)列滿了以后,才會(huì)有部分請求失?。?img src="https://imgs.yssmx.com/Uploads/2023/06/511487-110.png" alt="微服務(wù)04 分布式搜索引擎 elasticsearch DSL數(shù)據(jù)聚合 自動(dòng)補(bǔ)全 數(shù)據(jù)同步 集群 Sentinel" referrerpolicy="no-referrer" />
?
2.3.3.總結(jié)
流控效果有哪些?
-
快速失?。篞PS超過閾值時(shí),拒絕新的請求
-
warm up: QPS超過閾值時(shí),拒絕新的請求;QPS閾值是逐漸提升的,可以避免冷啟動(dòng)時(shí)高并發(fā)導(dǎo)致服務(wù)宕機(jī)。
-
排隊(duì)等待:請求會(huì)進(jìn)入隊(duì)列,按照閾值允許的時(shí)間間隔依次執(zhí)行請求;如果請求預(yù)期等待時(shí)長大于超時(shí)時(shí)間,直接拒絕
2.4.熱點(diǎn)參數(shù)限流
之前的限流是統(tǒng)計(jì)訪問某個(gè)資源的所有請求,判斷是否超過QPS閾值。而熱點(diǎn)參數(shù)限流是分別統(tǒng)計(jì)參數(shù)值相同的請求,判斷是否超過QPS閾值。
2.4.1.全局參數(shù)限流
例如,一個(gè)根據(jù)id查詢商品的接口:
?
訪問/goods/{id}的請求中,id參數(shù)值會(huì)有變化,熱點(diǎn)參數(shù)限流會(huì)根據(jù)參數(shù)值分別統(tǒng)計(jì)QPS,統(tǒng)計(jì)結(jié)果:
?
當(dāng)id=1的請求觸發(fā)閾值被限流時(shí),id值不為1的請求不受影響。
配置示例:
?
代表的含義是:對hot這個(gè)資源的0號參數(shù)(第一個(gè)參數(shù))做統(tǒng)計(jì),每1秒相同參數(shù)值的請求數(shù)不能超過5
2.4.2.熱點(diǎn)參數(shù)限流
剛才的配置中,對查詢商品這個(gè)接口的所有商品一視同仁,QPS都限定為5.
而在實(shí)際開發(fā)中,可能部分商品是熱點(diǎn)商品,例如秒殺商品,我們希望這部分商品的QPS限制與其它商品不一樣,高一些。那就需要配置熱點(diǎn)參數(shù)限流的高級選項(xiàng)了:
?
結(jié)合上一個(gè)配置,這里的含義是對0號的long類型參數(shù)限流,每1秒相同參數(shù)的QPS不能超過5,有兩個(gè)例外:
?如果參數(shù)值是100,則每1秒允許的QPS為10
?如果參數(shù)值是101,則每1秒允許的QPS為15
2.4.4.案例
案例需求:給/order/{orderId}這個(gè)資源添加熱點(diǎn)參數(shù)限流,規(guī)則如下:
?默認(rèn)的熱點(diǎn)參數(shù)規(guī)則是每1秒請求量不超過2
?給102這個(gè)參數(shù)設(shè)置例外:每1秒請求量不超過4
?給103這個(gè)參數(shù)設(shè)置例外:每1秒請求量不超過10
注意事項(xiàng):熱點(diǎn)參數(shù)限流對默認(rèn)的SpringMVC資源無效,需要利用@SentinelResource注解標(biāo)記資源
1)標(biāo)記資源
給order-service中的OrderController中的/order/{orderId}資源添加注解:
?
2)熱點(diǎn)參數(shù)限流規(guī)則
訪問該接口,可以看到我們標(biāo)記的hot資源出現(xiàn)了:
?
這里不要點(diǎn)擊hot后面的按鈕,頁面有BUG
點(diǎn)擊左側(cè)菜單中熱點(diǎn)規(guī)則菜單:
?
點(diǎn)擊新增,填寫表單:
?
3)Jmeter測試
選擇《熱點(diǎn)參數(shù)限流 QPS1》:
?
這里發(fā)起請求的QPS為5.
包含3個(gè)http請求:
普通參數(shù),QPS閾值為2
?
運(yùn)行結(jié)果:
?
例外項(xiàng),QPS閾值為4
?
運(yùn)行結(jié)果:
?
例外項(xiàng),QPS閾值為10
?
運(yùn)行結(jié)果:
?
3.隔離和降級
限流是一種預(yù)防措施,雖然限流可以盡量避免因高并發(fā)而引起的服務(wù)故障,但服務(wù)還會(huì)因?yàn)槠渌蚨收稀?/p>
而要將這些故障控制在一定范圍,避免雪崩,就要靠線程隔離(艙壁模式)和熔斷降級手段了。
線程隔離之前講到過:調(diào)用者在調(diào)用服務(wù)提供者時(shí),給每個(gè)調(diào)用的請求分配獨(dú)立線程池,出現(xiàn)故障時(shí),最多消耗這個(gè)線程池內(nèi)資源,避免把調(diào)用者的所有資源耗盡。
?
熔斷降級:是在調(diào)用方這邊加入斷路器,統(tǒng)計(jì)對服務(wù)提供者的調(diào)用,如果調(diào)用的失敗比例過高,則熔斷該業(yè)務(wù),不允許訪問該服務(wù)的提供者了。
?
可以看到,不管是線程隔離還是熔斷降級,都是對客戶端(調(diào)用方)的保護(hù)。需要在調(diào)用方 發(fā)起遠(yuǎn)程調(diào)用時(shí)做線程隔離、或者服務(wù)熔斷。
而我們的微服務(wù)遠(yuǎn)程調(diào)用都是基于Feign來完成的,因此我們需要將Feign與Sentinel整合,在Feign里面實(shí)現(xiàn)線程隔離和服務(wù)熔斷。
3.1.FeignClient整合Sentinel
SpringCloud中,微服務(wù)調(diào)用都是通過Feign來實(shí)現(xiàn)的,因此做客戶端保護(hù)必須整合Feign和Sentinel。
3.1.1.修改配置,開啟sentinel功能
修改OrderService的application.yml文件,開啟Feign的Sentinel功能:
feign:
sentinel:
enabled: true # 開啟feign對sentinel的支持
3.1.2.編寫失敗降級邏輯
業(yè)務(wù)失敗后,不能直接報(bào)錯(cuò),而應(yīng)該返回用戶一個(gè)友好提示或者默認(rèn)結(jié)果,這個(gè)就是失敗降級邏輯。
給FeignClient編寫失敗后的降級邏輯
①方式一:FallbackClass,無法對遠(yuǎn)程調(diào)用的異常做處理
②方式二:FallbackFactory,可以對遠(yuǎn)程調(diào)用的異常做處理,我們選擇這種
這里我們演示方式二的失敗降級處理。
步驟一:在feing-api項(xiàng)目中定義類,實(shí)現(xiàn)FallbackFactory:
?
代碼:
package cn.itcast.feign.clients.fallback;
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable throwable) {
return new UserClient() {
@Override
public User findById(Long id) {
log.error("查詢用戶異常", throwable);
return new User();
}
};
}
}
步驟二:在feing-api項(xiàng)目中的DefaultFeignConfiguration類中將UserClientFallbackFactory注冊為一個(gè)Bean:
@Bean
public UserClientFallbackFactory userClientFallbackFactory(){
return new UserClientFallbackFactory();
}
步驟三:在feing-api項(xiàng)目中的UserClient接口中使用UserClientFallbackFactory:
import cn.itcast.feign.clients.fallback.UserClientFallbackFactory;
import cn.itcast.feign.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(value = "userservice", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
重啟后,訪問一次訂單查詢業(yè)務(wù),然后查看sentinel控制臺(tái),可以看到新的簇點(diǎn)鏈路:
3.1.3.總結(jié)
Sentinel支持的雪崩解決方案:
-
線程隔離(倉壁模式)
-
降級熔斷
Feign整合Sentinel的步驟:
-
在application.yml中配置:feign.sentienl.enable=true
-
給FeignClient編寫FallbackFactory并注冊為Bean
-
將FallbackFactory配置到FeignClient
3.2.線程隔離(艙壁模式)
3.2.1.線程隔離的實(shí)現(xiàn)方式
線程隔離有兩種方式實(shí)現(xiàn):
-
線程池隔離
-
信號量隔離(Sentinel默認(rèn)采用)
如圖:
?
線程池隔離:給每個(gè)服務(wù)調(diào)用業(yè)務(wù)分配一個(gè)線程池,利用線程池本身實(shí)現(xiàn)隔離效果
信號量隔離:不創(chuàng)建線程池,而是計(jì)數(shù)器模式,記錄業(yè)務(wù)使用的線程數(shù)量,達(dá)到信號量上限時(shí),禁止新的請求。
兩者的優(yōu)缺點(diǎn):
?
3.2.2.sentinel的線程隔離
用法說明:
在添加限流規(guī)則時(shí),可以選擇兩種閾值類型:
?
-
QPS:就是每秒的請求數(shù),在快速入門中已經(jīng)演示過
-
線程數(shù):是該資源能使用用的tomcat線程數(shù)的最大值。也就是通過限制線程數(shù)量,實(shí)現(xiàn)線程隔離(艙壁模式)。
案例需求:給 order-service服務(wù)中的UserClient的查詢用戶接口設(shè)置流控規(guī)則,線程數(shù)不能超過 2。然后利用jemeter測試。
1)配置隔離規(guī)則
選擇feign接口后面的流控按鈕:
?
填寫表單:
?
2)Jmeter測試
選擇《閾值類型-線程數(shù)<2》:
?
一次發(fā)生10個(gè)請求,有較大概率并發(fā)線程數(shù)超過2,而超出的請求會(huì)走之前定義的失敗降級邏輯。
查看運(yùn)行結(jié)果:
?
發(fā)現(xiàn)雖然結(jié)果都是通過了,不過部分請求得到的響應(yīng)是降級返回的null信息。
3.2.3.總結(jié)
線程隔離的兩種手段是?
-
信號量隔離
-
線程池隔離
信號量隔離的特點(diǎn)是?
-
基于計(jì)數(shù)器模式,簡單,開銷小
線程池隔離的特點(diǎn)是?
-
基于線程池模式,有額外開銷,但隔離控制更強(qiáng)
3.3.熔斷降級
熔斷降級是解決雪崩問題的重要手段。其思路是由斷路器統(tǒng)計(jì)服務(wù)調(diào)用的異常比例、慢請求比例,如果超出閾值則會(huì)熔斷該服務(wù)。即攔截訪問該服務(wù)的一切請求;而當(dāng)服務(wù)恢復(fù)時(shí),斷路器會(huì)放行訪問該服務(wù)的請求。
斷路器控制熔斷和放行是通過狀態(tài)機(jī)來完成的:
?
狀態(tài)機(jī)包括三個(gè)狀態(tài):
-
closed:關(guān)閉狀態(tài),斷路器放行所有請求,并開始統(tǒng)計(jì)異常比例、慢請求比例。超過閾值則切換到open狀態(tài)
-
open:打開狀態(tài),服務(wù)調(diào)用被熔斷,訪問被熔斷服務(wù)的請求會(huì)被拒絕,快速失敗,直接走降級邏輯。Open狀態(tài)5秒后會(huì)進(jìn)入half-open狀態(tài)
-
half-open:半開狀態(tài),放行一次請求,根據(jù)執(zhí)行結(jié)果來判斷接下來的操作。
-
請求成功:則切換到closed狀態(tài)
-
請求失敗:則切換到open狀態(tài)
斷路器熔斷策略有三種:慢調(diào)用、異常比例、異常數(shù)
3.3.1.慢調(diào)用
慢調(diào)用:業(yè)務(wù)的響應(yīng)時(shí)長(RT)大于指定時(shí)長的請求認(rèn)定為慢調(diào)用請求。在指定時(shí)間內(nèi),如果請求數(shù)量超過設(shè)定的最小數(shù)量,慢調(diào)用比例大于設(shè)定的閾值,則觸發(fā)熔斷。
例如:
?
解讀:RT超過500ms的調(diào)用是慢調(diào)用,統(tǒng)計(jì)最近10000ms內(nèi)的請求,如果請求量超過10次,并且慢調(diào)用比例不低于0.5,則觸發(fā)熔斷,熔斷時(shí)長為5秒。然后進(jìn)入half-open狀態(tài),放行一次請求做測試。
案例
需求:給 UserClient的查詢用戶接口設(shè)置降級規(guī)則,慢調(diào)用的RT閾值為50ms,統(tǒng)計(jì)時(shí)間為1秒,最小請求數(shù)量為5,失敗閾值比例為0.4,熔斷時(shí)長為5
1)設(shè)置慢調(diào)用
修改user-service中的/user/{id}這個(gè)接口的業(yè)務(wù)。通過休眠模擬一個(gè)延遲時(shí)間:
?
此時(shí),orderId=101的訂單,關(guān)聯(lián)的是id為1的用戶,調(diào)用時(shí)長為60ms:
?
orderId=102的訂單,關(guān)聯(lián)的是id為2的用戶,調(diào)用時(shí)長為非常短;
?
2)設(shè)置熔斷規(guī)則
下面,給feign接口設(shè)置降級規(guī)則:
?
規(guī)則:
?
超過50ms的請求都會(huì)被認(rèn)為是慢請求
3)測試
在瀏覽器訪問:http://localhost:8088/order/101,快速刷新5次,可以發(fā)現(xiàn):
?
觸發(fā)了熔斷,請求時(shí)長縮短至5ms,快速失敗了,并且走降級邏輯,返回的null
在瀏覽器訪問:http://localhost:8088/order/102,竟然也被熔斷了:
?
3.3.2.異常比例、異常數(shù)
異常比例或異常數(shù):統(tǒng)計(jì)指定時(shí)間內(nèi)的調(diào)用,如果調(diào)用次數(shù)超過指定請求數(shù),并且出現(xiàn)異常的比例達(dá)到設(shè)定的比例閾值(或超過指定異常數(shù)),則觸發(fā)熔斷。
例如,一個(gè)異常比例設(shè)置:
?
解讀:統(tǒng)計(jì)最近1000ms內(nèi)的請求,如果請求量超過10次,并且異常比例不低于0.4,則觸發(fā)熔斷。
一個(gè)異常數(shù)設(shè)置:
解讀:統(tǒng)計(jì)最近1000ms內(nèi)的請求,如果請求量超過10次,并且異常比例不低于2次,則觸發(fā)熔斷。
案例
需求:給 UserClient的查詢用戶接口設(shè)置降級規(guī)則,統(tǒng)計(jì)時(shí)間為1秒,最小請求數(shù)量為5,失敗閾值比例為0.4,熔斷時(shí)長為5s
1)設(shè)置異常請求
首先,修改user-service中的/user/{id}這個(gè)接口的業(yè)務(wù)。手動(dòng)拋出異常,以觸發(fā)異常比例的熔斷:
?
也就是說,id 為 2時(shí),就會(huì)觸發(fā)異常
2)設(shè)置熔斷規(guī)則
下面,給feign接口設(shè)置降級規(guī)則:
?
規(guī)則:
?
在5次請求中,只要異常比例超過0.4,也就是有2次以上的異常,就會(huì)觸發(fā)熔斷。
3)測試
在瀏覽器快速訪問:http://localhost:8088/order/102,快速刷新5次,觸發(fā)熔斷:
?
此時(shí),我們?nèi)ピL問本來應(yīng)該正常的103:
?
4.授權(quán)規(guī)則
授權(quán)規(guī)則可以對請求方來源做判斷和控制。
4.1.授權(quán)規(guī)則
4.1.1.基本規(guī)則
授權(quán)規(guī)則可以對調(diào)用方的來源做控制,有白名單和黑名單兩種方式。
-
白名單:來源(origin)在白名單內(nèi)的調(diào)用者允許訪問
-
黑名單:來源(origin)在黑名單內(nèi)的調(diào)用者不允許訪問
點(diǎn)擊左側(cè)菜單的授權(quán),可以看到授權(quán)規(guī)則:
?
-
資源名:就是受保護(hù)的資源,例如/order/{orderId}
-
流控應(yīng)用:是來源者的名單,
-
如果是勾選白名單,則名單中的來源被許可訪問。
-
如果是勾選黑名單,則名單中的來源被禁止訪問。
比如:
?
我們允許請求從gateway到order-service,不允許瀏覽器訪問order-service,那么白名單中就要填寫網(wǎng)關(guān)的來源名稱(origin)。
4.1.2.如何獲取origin
Sentinel是通過RequestOriginParser這個(gè)接口的parseOrigin來獲取請求的來源的。
public interface RequestOriginParser {
/**
* 從請求request對象中獲取origin,獲取方式自定義
*/
String parseOrigin(HttpServletRequest request);
}
這個(gè)方法的作用就是從request對象中,獲取請求者的origin值并返回。
默認(rèn)情況下,sentinel不管請求者從哪里來,返回值永遠(yuǎn)是default,也就是說一切請求的來源都被認(rèn)為是一樣的值default。
因此,我們需要自定義這個(gè)接口的實(shí)現(xiàn),讓不同的請求,返回不同的origin。
例如order-service服務(wù)中,我們定義一個(gè)RequestOriginParser的實(shí)現(xiàn)類:
package cn.itcast.order.sentinel;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
@Component
public class HeaderOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
// 1.獲取請求頭
String origin = request.getHeader("origin");
// 2.非空判斷
if (StringUtils.isEmpty(origin)) {
origin = "blank";
}
return origin;
}
}
我們會(huì)嘗試從request-header中獲取origin值。
4.1.3.給網(wǎng)關(guān)添加請求頭
既然獲取請求origin的方式是從reques-header中獲取origin值,我們必須讓所有從gateway路由到微服務(wù)的請求都帶上origin頭。
這個(gè)需要利用之前學(xué)習(xí)的一個(gè)GatewayFilter來實(shí)現(xiàn),AddRequestHeaderGatewayFilter。
修改gateway服務(wù)中的application.yml,添加一個(gè)defaultFilter:
spring:
cloud:
gateway:
default-filters:
- AddRequestHeader=origin,gateway
routes:
# ...略
這樣,從gateway路由的所有請求都會(huì)帶上origin頭,值為gateway。而從其它地方到達(dá)微服務(wù)的請求則沒有這個(gè)頭。
4.1.4.配置授權(quán)規(guī)則
接下來,我們添加一個(gè)授權(quán)規(guī)則,放行origin值為gateway的請求。
?
配置如下:
現(xiàn)在,我們直接跳過網(wǎng)關(guān),訪問order-service服務(wù):
?
通過網(wǎng)關(guān)訪問:
?
4.2.自定義異常結(jié)果
默認(rèn)情況下,發(fā)生限流、降級、授權(quán)攔截時(shí),都會(huì)拋出異常到調(diào)用方。異常結(jié)果都是flow limmiting(限流)。這樣不夠友好,無法得知是限流還是降級還是授權(quán)攔截。
4.2.1.異常類型
而如果要自定義異常時(shí)的返回結(jié)果,需要實(shí)現(xiàn)BlockExceptionHandler接口:
public interface BlockExceptionHandler {
/**
* 處理請求被限流、降級、授權(quán)攔截時(shí)拋出的異常:BlockException
*/
void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception;
}
這個(gè)方法有三個(gè)參數(shù):
-
HttpServletRequest request:request對象
-
HttpServletResponse response:response對象
-
BlockException e:被sentinel攔截時(shí)拋出的異常
這里的BlockException包含多個(gè)不同的子類:
異常 |
說明 |
FlowException |
限流異常 |
ParamFlowException |
熱點(diǎn)參數(shù)限流的異常 |
DegradeException |
降級異常 |
AuthorityException |
授權(quán)規(guī)則異常 |
SystemBlockException |
系統(tǒng)規(guī)則異常 |
4.2.2.自定義異常處理
下面,我們就在order-service定義一個(gè)自定義異常處理類:
package cn.itcast.order.sentinel;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
String msg = "未知異常";
int status = 429;
if (e instanceof FlowException) {
msg = "請求被限流了";
} else if (e instanceof ParamFlowException) {
msg = "請求被熱點(diǎn)參數(shù)限流";
} else if (e instanceof DegradeException) {
msg = "請求被降級了";
} else if (e instanceof AuthorityException) {
msg = "沒有權(quán)限訪問";
status = 401;
}
response.setContentType("application/json;charset=utf-8");
response.setStatus(status);
response.getWriter().println("{\"msg\": " + msg + ", \"status\": " + status + "}");
}
}
重啟測試,在不同場景下,會(huì)返回不同的異常消息.
5.規(guī)則持久化
現(xiàn)在,sentinel的所有規(guī)則都是內(nèi)存存儲(chǔ),重啟后所有規(guī)則都會(huì)丟失。在生產(chǎn)環(huán)境下,我們必須確保這些規(guī)則的持久化,避免丟失。
5.1.規(guī)則管理模式
規(guī)則是否能持久化,取決于規(guī)則管理模式,sentinel支持三種規(guī)則管理模式:
-
原始模式:Sentinel的默認(rèn)模式,將規(guī)則保存在內(nèi)存,重啟服務(wù)會(huì)丟失。
-
pull模式
-
push模式
5.1.1.pull模式
pull模式:控制臺(tái)將配置的規(guī)則推送到Sentinel客戶端,而客戶端會(huì)將配置規(guī)則保存在本地文件或數(shù)據(jù)庫中。以后會(huì)定時(shí)去本地文件或數(shù)據(jù)庫中查詢,更新本地規(guī)則。
5.1.2.push模式
push模式:控制臺(tái)將配置規(guī)則推送到遠(yuǎn)程配置中心,例如Nacos。Sentinel客戶端監(jiān)聽Nacos,獲取配置變更的推送消息,完成本地配置更新。文章來源:http://www.zghlxwxcb.cn/news/detail-511487.html
5.2.實(shí)現(xiàn)push模式
詳細(xì)步驟可以參考課前資料的《sentinel規(guī)則持久化》:文章來源地址http://www.zghlxwxcb.cn/news/detail-511487.html

到了這里,關(guān)于微服務(wù)04 分布式搜索引擎 elasticsearch DSL數(shù)據(jù)聚合 自動(dòng)補(bǔ)全 數(shù)據(jù)同步 集群 Sentinel的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!