十一. SpringCloud Alibaba
- 2021 年 3 月 7 日
- 筆記
- SpringCloud, 微服務
1. SpringCloud Alibaba簡介
1.1 為什麼會出現SpringCloud Alibaba
Spring Cloud Netflix項目進入到維護模式
什麼是維護模式?=> 將模組置於維護模式,意味著Spring Cloud團隊將不會再向模組添加新功能(我們將修復block級別的bug以及安全問題,我們也會考慮並審查社區的小型pull request)。
進入維護模式意味著?=> SpringCloud Netflix將不再開發新的組件。新組件功能將以其他替代品代替的方法實現。
1.2 SpringCloud Alibaba有什麼
官網 誕生於2018.10.31,Spring Clooud Alibaba正式入駐Spring Cloud官方孵化器,並在Maven中央庫發布了第一個版本。
能幹什麼?
功能 | 具體說明 |
---|---|
服務限流與降級 | 默認支援Servlet,Feign,RestTemplate,Dubbo和RocketMQ限流降級功能的接入,可以在運行時通過控制台實時修改限流降級規則,還支援查看限流降級Metrics監控。 |
服務註冊與發現 | 適配Spring Cloud服務註冊與發現標準,默認集成了Ribbon的支援。 |
分散式配置管理 | 支援分散式系統中的外部化配置,配置更改時自動刷新。 |
消息驅動能力 | 基於SpringCloud Stream 為微服務應用構建消息驅動能力。 |
阿里雲對象儲存 | 阿里雲提供的海量,安全,底成本,高可靠的雲存儲服務。支援在任何時間,任何地點儲存和訪問任意類型的數據。 |
分散式任務的調度 | 提供秒級,精準,高可靠,高可用的定時(基於Cron表達式)任務調度服務。同時提供分散式的任務執行模型,如網格任務。網格任務支援海量子任務均勻分配到所有Worker(schedulerx-client)上執行。 |
使用SpringCloud Alibaba需要在POM文件中引入其依賴:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2. Nacos作為服務註冊中心
2.1 Nacos簡介
「Nacos」,前四個字母分別問Naming和Configuration的前兩個字母,最後的s為Service,它是一個更易於構建雲原生應用的動態服務發現、配置管理和服務管理平台,說白了就是 註冊中心 + 配置中心 的組合,等價於SpringCloud之前的 Eureka + Config + Bus,所以Nacos可以替代Eureka做服務中心,可以替代Config做配置中心。
2.2 Nacos安裝與運行
在Nacos官網下載地址中下載Nacos,在安裝Nacos前需要本地Java8和Maven的環境已經準備好,然後下載其壓縮包:
解壓該壓縮文件後直接運行在bin目錄下的startup.cmd啟動Nacos:
然後訪問 //localhost:8848/nacos/ 進入Nacos,其默認的帳號密碼都是nacos,登錄成功後即可看到如下介面:
在之前學習服務註冊中心的時候講過CAP理論,即
C | A | P |
---|---|---|
Consistency | Available | Partition tolerance |
強一致性 | 可用性 | 分區容錯性 |
CAP原則又稱CAP定理,指的是在一個分散式系統中,一致性、可用性、分區容錯性。
CAP 原則指的是,這三個要素最多只能同時實現兩點,不可能三者兼顧。在分散式架構中,P永遠要求被保證,所以當前的分散式架構只有AP和CP兩種。Nacos屬於AP模型,再次對比這些服務註冊中心:
服務註冊與發現框架 | CAP模型 | 控制台管理 | 社區活躍度 |
---|---|---|---|
Eureka | AP | 支援 | 低(2.x版本歷史問題) |
Zookeeper | CP | 不支援 | 中 |
Consul | CP | 支援 | 高 |
Nacos | AP(事實上也可以支援CP) | 支援 | 高 |
據說Nacos在阿里巴巴內部有超過10萬的實例運行,已經過了類似雙十一等各種大型流量的考驗。
2.3 Nacos作為服務註冊中心
基於Nacos的服務提供者
新建Module:cloudalibaba-provider-payment9002作為服務提供方微服務。當然為了實現Nacos的負載均衡,使nacos-payment-provider服務集群化,我們同事也仿照9002搭建一個9003微服務。
然後在工程的父POM中引入依賴(Spring Cloud Alibaba簡介中引入的依賴)的前提下,在該模組的POM引入依賴Nacos服務註冊發現的依賴:
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
然後添加其配置文件application.yml:
server:
port: 9002
spring:
application:
name: nacos-payment-provider # 微服務名稱
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*' # 監控端點全部打開
編寫其主啟動類並在主啟動類上添加 @EnableDiscoveryClient
註解,使9002微服務能夠被註冊中心發現:
@EnableDiscoveryClient
@SpringBootApplication
public class PaymentMain9002 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9002.class);
}
}
然後編寫一個簡單的業務類:
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping("/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id) {
return "Nacos服務註冊,埠:" + serverPort + ";id:" + id;
}
}
啟動9002、9003微服務模組,在Nacos的服務列表中我們可以看到這兩個微服務已經入駐服務註冊中心:
點開nacos-payment-provider服務的詳情頁面,可以看到服務的實例詳情:
基於Nacos的服務消費者
新建Module:cloudalibaba-consumer-nacos-order83作為服務消費方微服務,在該模組的POM中同樣引入令Nacos服務註冊中心發現自己的依賴,配置其配置文件:
server:
port: 83
spring:
application:
name: nacos-order-cosumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
# 消費者將要去訪問的微服務名稱(註冊成功進Nacos的微服務提供者)
service-url:
nacos-user-service: //nacos-payment-provider
然後編寫服務消費方的主啟動類,Nacos本身就具有負載均衡功能,因為引入Nacos依賴的同時,Nacos內部集成了Ribbon,如圖所示:
而我們在學習Ribbon時知道,用了Ribbon就需要使用 RestTemplate
,所有我們編寫配置類,向Spring容器中注入 RestTemplate
:
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced //負載均衡
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
注意 注入 RestTemplate
時一定要添加 @LoadBalanced
註解,否則不會開啟負載均衡,在服務提供方集群的情況下,由於沒有開啟負載均衡,消費方會無法選擇具體調用哪個微服務實例,也就是服務提供方實例有很多個,而消費方服務不知道要調用哪個具體的服務提供方實例,不加該註解會產生 UnknownHostException
的異常。
然後編寫其業務類:
@RestController
@Slf4j
public class OrderNacosController {
@Resource
private RestTemplate restTemplate;
//直接讀取配置文件中的值,減少程式碼冗餘
@Value("${service-url.nacos-user-service}")
private String serverURL;
@GetMapping("/consumer/payment/nacos/{id}")
public String paymentInfo(@PathVariable("id") Long id) {
return restTemplate.getForObject(serverURL
+ "/payment/nacos/" + id, String.class);
}
}
啟動83服務消費方,在Nacos服務註冊中心中我們可以看到入駐了兩個服務提供方實例和一個服務消費方實例。
測試
多次訪問 //localhost:83/consumer/payment/nacos/1 ,我們發現服務消費方微服務83可以完成對服務提供方微服務9002/9003的輪詢負載均衡調用,也就是 Nacos內部就整合了Ribbon,實現了負載均衡,默認負載均衡演算法採用輪詢。
2.4 各種服務註冊中心的對比
事實上,Nacos可以在AP模式和CP模式中進行切換,也就是說Nacos不僅僅支援AP(可用性和分區容錯性),它同樣支援CP(一致性和分區容錯性)。當 採取入駐到Nacos服務註冊中心的微服務對自己的健康狀態進行上報時,也就是對入駐到註冊中心的微服務進行非持久化的保存,一旦客戶端上報不健康資訊,就將不健康的實例摘除掉,這類似於Eureka(當然Eureka可以開啟自我保護模式);當 採取由Nacos服務註冊中心自己探測入駐到中心的微服務是否健康時,也就是對入駐到註冊中心的微服務進行持久化保存,即使服務註冊中心發現微服務已經不健康了,也不會刪除到微服務,這類似於Consul。如下圖(CoreDNS也是一種服務註冊中心):
Nacos與其他服務註冊中心特性的對比:
Nacos | Eureka | Consul | CoreDNS | Zookeeper | |
---|---|---|---|---|---|
一致性協議 | CP/AP | AP | CP | / | CP |
健康檢查 | TCP/HTTP/MySQL/Client Beat(客戶端心跳) | Clent Beat | TCP/HTTP/gRPC/cmd | / | Client Beat |
負載均衡 | 權重/DSL/元數據/CMDB | Ribbon | Fabio | RR | / |
雪崩保護 | 支援 | 支援 | 不支援 | 不支援 | 不支援 |
自動註銷實例 | 支援 | 支援 | 不支援 | 不支援 | 支援 |
訪問協議 | HTTP/DNS/UDP | HTTP | HTTP/DNS | DNS | TCP |
監聽支援 | 支援 | 支援 | 支援 | 不支援 | 不支援 |
多數據中心 | 支援 | 支援 | 支援 | 不支援 | 不支援 |
跨註冊中心 | 支援 | 不支援 | 支援 | 不支援 | 不支援 |
SpringCloud集成 | 支援 | 支援 | 支援 | 不支援 | 不支援 |
Dubbo集成 | 支援 | 不支援 | 不支援 | 不支援 | 支援 |
K8s集成 | 支援 | 不支援 | 支援 | 支援 | 不支援 |
從理論知識中我們知道Nacos服務註冊中心可以在AP和CP模式中進行切換,C是所有節點在同一時間看到的數據是一致的,而A是所有的請求都會受到響應(A可以近似理解為高可用)。
一般來說,如果不需要存儲服務級別的資訊且服務實例是通過Nacos-Client註冊的,並且服務能保持心跳上報,那麼就可以選擇AP模式,當前主流的微服務框架如SpringCloud和Dubbo,都適用於AP模式,AP模式為了服務的可能性而減弱了一致性,因此 AP模式下只支援註冊臨時實例。
而如果需要在服務級別編輯或存儲配置資訊,那麼CP是必須的,K8s服務和DNS服務使用於CP模式,CP模式下則支援註冊持久化實例,此時以Raft協議為集群運行模式,該模式下註冊實例之前必須先註冊服務,如果服務不存在,則會返回錯誤。
如果需要讓Nacos服務從AP模式切換到CP模式的話,只需要向服務註冊中心發送POST請求即可:
curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'
3. Nacos作為服務配置中心
在用SpringCloud Config結合SpringCloud Bus時,我們可以把配置資訊託管到遠端如GitHub上,而現在我們應用了Nacos後,可以直接在Nacos上託管配置資訊。
3.1 基礎配置
新建Module:cloudalibaba-config-nacos-client3377作為服務配置中心微服務,在其POM文件中引入必要的依賴(如Nachos服務註冊中心的依賴)為,引入配置中心的依賴:
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
Nacos同SpringCloud Config一樣,在項目初始化時,要保證先從配置中心(直接以Nacos作為配置中心)進行配置拉取,拉取配置後,才能保證項目的正常啟動,而我們知道SpringBoot配置文件的載入順序是存在優先順序的,bootstrap的優先順序要高於application,所以我們創建兩個配置文件 bootstrap.yml
(從配置中心拉取配置)和 application.yml
(寫自己的配置),我們配置 bootstrap.yml
以使3377服務從Nacos上拉取配置資訊:
# nacos配置
server:
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
#Nacos服務註冊中心地址
server-addr: localhost:8848
config:
#Nacos作為配置中心地址
server-addr: localhost:8848
# 指定yaml格式的配置,也就是說從Nacos上讀yaml格式的配置文件
file-extension: yaml
# 配置匹配規則
# ${spring.application.name}-${spring.profile.active}.${file-extension}
然後用 application.yml
訂製自己的配置資訊,將環境定義為開發環境:
spring:
profiles:
active: dev # 表示開發環境
編寫其主啟動類後,然後編寫其業務類,在業務來上添加SpringCloud的原生註解@RefreshScope
以使服務可以支援從Nacos配置中心動態刷新配置資訊:
@RestController
@RefreshScope //支援Nacos的動態刷新功能
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo() {
return configInfo;
}
}
在上面的業務層中我們從配置中心讀取了配置資訊
而我們在Nacos配置中心中添加配置資訊,在Nacos配置中心中添加配置文件要遵循一定的匹配規則。 ↓
Nacos中的 dataId
的組成格式及與SpringBoot配置文件中的匹配規則,更詳細的資訊可以參考Nacos官方文檔。
在Nacos SpringCloud中,dataId
的完整格式為:
${prefix}-${spring.profile.active}.${file-extension}
prefix
默認為spring.application.name
(服務名)的值,也可以通過配置項spring.cloud.nacos.config.prefix
來配置。spring.profile.active
即為當前環境對應的 profile(上面我們在application.yml
中配置的屬性),詳情可以參考 Spring Boot文檔。 注意:當spring.profile.active
為空時,對應的連接符-
也將不存在,dataId 的拼接格式變成${prefix}.${file-extension}
, 建議 不要使spring.profile.active
為空。file-exetension
為配置內容的數據格式,可以通過配置項spring.cloud.nacos.config.file-extension
來配置。目前只支援properties
和yaml
類型。
由於prefix
默認為 spring.application.name
(服務名)的值,所以 dataId
的完整格式可以替換為:
${spring.application.name}-${spring.profile.active}.${file-extension}
結合我們在 boostrap.yml
配置文件和 application.yml
配置文件的配置,我們可以得到在當前實例中的 dataId
應該為:
nacos-config-client-dev.yaml
根據上述公式得到 dataId
應為的值後,我們就可以在Nacos配置中心的配置列表中新建配置文件。在新建配置文件時,Data ID
中填入我們得到的 dataId
,組名先選擇默認的即可,配置格式選擇我們在 file-extension
中設置的YAML格式,然後編寫配置文件,編寫後點擊 發布即可:
注意,在Nacos中的
dataId
中的後綴名必須用yaml
而不能用yml
。
添加配置文件後再點開Nacos配置中心的配置列表,就能發現已經存在剛才創建的配置文件:
然後我們啟動3377微服務,訪問 //localhost:3377/config/info 查看能否訪問都Nacos配置中心的配置資訊:
Nacos配置中心直接就 支援動態刷新,再更改了Nacos配置中心的配置資訊後,再通過3377微服務訪問,就可以得到更新後的配置資訊。目前Nacos已經實現了Eureka + Config + Bus的功能,但是Nacos之所以如此優秀是因為這些框架有的功能它都有,而這些框架沒有的功能它還有!下面看Nacos的一些高級功能。
3.2 分類配置
問題:多環境多項目管理
在多環境多項目管理時,也就是實際開發中,通常一個系統會準備不同的開發環境,如dev開發環境、test測試環境、prod生產環境,如何保證指定環境啟動時服務能正確讀取到Nacos上相應環境的配置文件呢?一個大型分散式微服務系統會有很多微服務子項目,每個微服務項目又都會有相應的開發環境、測試環境、預發環境、正式環境等,那對這些微服務配置該如何管理呢?=> 在Nacos配置中心中就可以進行分類配置,一個配置文件的具體所屬由 namespace
+ Group
+ dataId
所構成:
為什麼如此設計?
這種設計就類似於Java中的包名和類名,最外層的 namespace
是可以用於區分部署環境的,主要用來實現隔離,比如有三個環境:開發、測試、生產,那就可以創建三個 namespace
。
Group
和 dataId
邏輯上區分兩個目標對象。Group可以把不同的微服務劃分到同一個分組裡面去。Service就是微服務,一個Service可以包含多個Cluster(集群),Nacos默認Cluster是DEFALUT,Cluster是對指定微服務的一個虛擬劃分。
舉例:比如說為了容災,將Service微服務分別部署在了杭州機房和廣州機房,這時就可以給杭州機房的Service微服務起一個集群名稱HZ,給廣州機房的Service微服務起一個集群名字GZ,還可以盡量讓同一個機房的微服務互相調用,以提升性能。
最後就是Instance
,就是微服務的實例。
默認情況下 namespace
的值為 public
, Group
的值為 DEFAULT_GROUP
,默認Cluster是DEFAULT。
下面對這三個屬性進行詳細展開。
dataId
方案
指定 spring.profile.active
配置屬性和配置文件的 dataId
來使不同環境下讀取不同的配置,也就是說我們現在用默認的命名空間 namespace
(即 public
),默認的分組 Group
(即 DEFAULT_GROUP
),然後在Nacos配置中心中建立兩個 dataId
分別為 dev
環境和test
環境。此時在同一命名空間同一組中就有了兩個應用於開發、測試不同環境的配置文件:
然後我們通過修改微服務自己訂製配置資訊 application.yml
配置文件中的spring.profile.active
屬性即可進行多環境下的配置文件的讀取,配置什麼就載入什麼,在之前我們的3377微服務獲取的是配置中心中 nacos-config-client-dev.yaml
配置文件的配置資訊,現在我們將 application.yml
修改為:
spring:
profiles:
# active: dev # 表示開發環境
active: test # 表示開發環境
然後重啟3377微服務,再次進行測試發現訪問到的配置資訊為測試環境的配置資訊:
Group
方案
不僅通過 dataId
可以實現環境分區,應用 Group
也可以實現環境分區,首先我們在Nacos配置中心新建兩個相同 dataId
,不同 Group
的配置文件,一個用於開發環境,一個用於測試環境:
由於這兩個配置文件的 dataId
均為 nacos-config-client-info.yaml
,根據之前解釋的 dataId
的命名規則,我們應該將3377微服務的自定義配置文件 application.yml
中的 spring.profile.active
屬性更改為 info
:
spring:
profiles:
active: info
在確認了讀取的配置文件的 dataId
後,我們在其 bootstrap.yml
配置文件中添加 spring.cloud.nacos.config.group
屬性,設置為相應的組名即可,比如我們現在設置為測試環境組:
spring:
cloud:
nacos:
config:
group: TEST_GROUP
重啟3377微服務,再次訪問可以發現獲取到的配置資訊即為TEST_GROUP
組的配置資訊。
namespace
方案
根據namespace同樣可以進行環境區分。
新建兩個命名空間
服務列表查看
在 bootstrap.yml
配置文件中添加 spring.cloud.nacos.config.namespace
屬性,設置為相應的命名空間流水號即可,比如我們現在設置為開發環境命名空間:
spring:
cloud:
nacos:
config:
namespace: 5e4fbf07-10f3-4216-86c5-e40391667310
在dev命名空間下新建不同group的配置文件
重啟3377微服務,再次訪問可以發現獲取到的配置資訊即為 dev
命名空間下TEST_GROUP
組的配置資訊。
4. Nacos集群和持久化配置
4.1 官網說明
官網集群部署架構圖
開源的時候推薦用戶把所有服務列表放到一個vip下面,然後掛到一個域名下面
//ip1:port/openAPI 直連ip模式,機器掛則需要修改ip才可以使用。
//SLB:port/openAPI 掛載SLB模式(內網SLB,不可暴露到公網,以免帶來安全風險),直連SLB即可,下面掛server真實ip,可讀性不好。
//nacos.com:port/openAPI 域名 + SLB模式(內網SLB,不可暴露到公網,以免帶來安全風險),可讀性好,而且換ip方便,推薦模式
VIP:即虛擬IP,即Nginx
上圖翻譯一下就是
4.2 Nacos持久化配置解釋
默認Nacos使用嵌入式資料庫實現數據的存儲,所以,如果啟動多個默認配置下的Nacos節點,數據儲存是存在一致性問題的。Nacos默認自帶的嵌入式資料庫為derby。
為了解決這個問題,Nacos採用了 集中式存儲的方式來支援集群化部署,目前只支援MySQL的存儲。
單機模式支援MySQL,在0.7版本之前,在單機模式時Nacos使用嵌入式資料庫實現數據的儲存,不方便觀察數據的基本情況。0.7版本增加了支援MySQL數據源能力,具體的操作步驟如下。
derby到mysql切換配置步驟:
- 安裝資料庫,版本要求:5.6.6+
- nacos-server-1.1.4\nacos\conf目錄下找到sql腳本(nacos-mysql.sql),初始化MySQL資料庫nacos_config,資料庫初始化文件:nacos-mysql.sql
- nacos-server-1.1.4\nacos\conf目錄下找到application.properties,修改conf/application.properties文件,增加支援MySQL數據源配置(目前只支援MySQL),添加MySQL數據源的url,用戶名和密碼。
# 如果是Mysql5.+
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://mpolaris.top:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&serverTimezone=UTC
db.user=root
db.password=123456
# 如果是Mysql8.+
spring.datasource.platform=mysql
jdbc.DriverClassName=com.mysql.cj.jdbc.Driver
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?serverTimezone=GMT%2B8&characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user= root
db.password= 123456
啟動Nacos,可以看到是個全新的空記錄介面,以前是記錄進derby。
4.4 Linux版Nacos+MySQL生產環境配置
需要一個Nginx,三個Nacos註冊中心,一個Mysql
Nacos下載Linux版,下載地址,nacos-server-1.1.4.tar.gz,解壓後安裝
集群配置步驟
Linux伺服器上MySQL資料庫配置(按照4.3配置)
application.properties配置(按照4.3配置)
Linux伺服器上Nacos的集群配置cluster.conf
- 梳理出三台Nacos機器的不同服務埠號(3333,4444,5555)
- 複製出cluster.conf
cd /opt/nacos/conf
cp cluster.conf.example cluster.conf
- 修改內容
注意:這個IP不能寫127.0.0.1,必須是Linux命令hostname -i 能夠識別的IP
# it is ip
# example
# 這裡我填的是自己的私網ip
172.20.xxx.xxx:3333
172.20.xxx.xxx:4444
172.20.xxx.xxx:5555
編輯Nacos的啟動腳本startup.sh(先備份),使它能夠接受不同的啟動埠
- /mynacos/nacos/bin 目錄下有startup.sh
- 怎樣改?
# 平時單機版的啟動,都是./startup.sh即可
# 但是集群啟動,我們希望可以類似其他軟體的shell命令,傳遞不同的埠號啟動不同的nacos實例。
# 例如:命令 ./startup.sh -p 3333 表示啟動埠號為3333的nacos伺服器實例
vim ./startup.sh
# 修改第57行開始增加p
export SERVER="nacos-server"
export MODE="cluster"
export FUNCTION_MODE="all"
while getopts ":m:f:s:p:" opt
do
case $opt in
m)
MODE=$OPTARG;;
f)
FUNCTION_MODE=$OPTARG;;
s)
SERVER=$OPTARG;;
p)
PORT=$OPTARG;;
?)
echo "Unknown parameter"
exit 1;;
esac
done
# 修改第134行,增加 - Dserver.port=${PORT}
nohup $JAVA -Dserver.port=${PORT} ${JAVA_OPT} nacos.nacos >> ${BASE_DIR}/logs/start.out 2>&1
- 執行方式
./startup.sh -p 3333
./startup.sh -p 4444
./startup.sh -p 5555
#注意:提示伺服器記憶體不夠,可以把nacos啟動腳本里-Xms和-Xmx jvm記憶體調整小一點
# 同時通過window瀏覽器訪問:
//192.168.42.82:3333/nacos/#/login
//192.168.42.82:4444/nacos/#/login
//192.168.42.82:5555/nacos/#/login
Nginx的配置,由它作為負載均衡器
- 修改Nginx的配置文件Nginx.conf
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
upstream cluster {
server 127.0.0.1:3333;
server 127.0.0.1:4444;
server 127.0.0.1:5555;
}
server {
listen 1111;
server_name localhost;
location / {
# root html;
# index index.html index.htm;
proxy_pass //cluster;
}
}
}
- 按照指定配置文件啟動Nginx
cd /usr/local/nginx/sbin
./nginx -c /opt/nginx/conf/nginx.conf
開始測試
- 測試通過Nginx訪問Nacos:http//192.168.42.82:1111/nacos/#/login,成功訪問!
- 新建一個配置測試資料庫,Linux伺服器的MySQL也插入一條記錄!
微服務測試
微服務springalibaba-provider-payment9002啟動註冊進nacos集群
- yml配置文件
server:
port: 9002
spring:
application:
name: nacos-payment-provider # 微服務名稱
cloud:
nacos:
discovery:
# server-addr: localhost:8848 # 配置Nacos地址
# 換成Nginx的1111埠,做集群
server-addr: 192.168.42.1:1111
management:
endpoints:
web:
exposure:
include: '*' # 監控端點全部打開
- 結果:成功啟動!
高可用總結
5. Sentinel熔斷與限流
5.1 Sentiel簡介
主要特性
與Hystrix對比
- Hystrix需要我們自己手工搭建監控平台,沒有一套Wbe介面可以給我們進行更加細粒度化的配置,流控,速率控制,服務熔斷,服務降級。
- Sentinel單獨一個組件,可以獨立出來。直接介面化的細粒度統一配置。
Sentiel可以解決的問題
- 服務雪崩
- 服務降級
- 服務熔斷
- 服務限流
- …
5.2 安裝Sentiel控制台
Sentinel分為兩部分:
- 核心庫(Java客戶端)不依賴任何框架/庫,能夠運行於所有Java運行時環境,同時對Dubbo / Spring Cloud等框架也有較好的支援。
- 控制台(Dashboard)基於Spring Boot開發,打包後可以直接運行,不需要額外的Tomcat等應用容器.
運行命令
# 前提:Java8環境,8080埠不被佔用
java -jar sentinel-dashboard-1.7.0.jar
訪問Sentinel管理介面
- //localhost:8080
- 登陸帳號密碼均為sentinel
5.3 初始化演示工程
啟動Nacos8848,//localhost:8848/nacos/#/login
新建Moudle:cloudalibaba-sentinel-service8401
pom.xml
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel-datasource-nacos 後續做持久化用到-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--其他基礎包-->
yml配置文件
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 # Nacos服務註冊中心地址
sentinel:
transport:
dashboard: localhost:8080 # 配置Sentinel dashboard地址
# 默認8719埠,假如被佔用會自動從8719開始依次+1掃描,直至找到未被佔用的埠
port: 8719
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: cloudalibaba-sentinel-service
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
management:
endpoints:
web:
exposure:
include: '*'
feign:
sentinel:
enabled: true # 激活Sentinel對Feign的支援
主啟動
@EnableDiscoveryClient
@SpringBootApplication
public class MainApp8401 {
public static void main(String[] args) {
SpringApplication.run(MainApp8401.class, args);
}
}
業務類FlowLimitController
@RestController
@Slf4j
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
return "==> testA";
}
@GetMapping("/testB")
public String testB() {
log.info(Thread.currentThread().getName() + "\t" + "==> testB");
return "==> testB";
}
}
啟動Sentinel8080,啟動微服務8401
啟動8401微服務後台查看sentinel控制,啥也沒有?=> Sentinel採用懶載入,執行兩次訪問http:localhost8401/testA,http:localhost8401/testB,效果如下:
5.4 流控規則
名詞說明:
- 資源名:唯一名字,默認請求路徑
- 針對來源:Sentinel可以針對調用者進行限流,填寫微服務名,默認default(不區分來源)
- 閾值類型/單機閾值:
- QPS(每秒鐘的請求數量):當調用該API的QPS達到閾值的時候,進行限流
- 執行緒數:當調用該API的執行緒數達到閾值的時候,進行限流
- 是否集群:我們這裡不需要集群
- 流控模式:
- 直接:API達到限流條件時,直接限流
- 關聯:當關聯的資源達到閾值時,就限流自己
- 鏈路:只記錄指定鏈路上的流量(指定資源從入口資源進來的流量,如果達到閾值,就進行限流)【API級別的針對來源】
- 流控效果:
- 快速失敗:直接失敗,拋異常
- Warm Up:根據codeFactor(冷載入因子,默認3)的值,從閾值/codeFactor,經過預熱時長,才達到設置的QPS閾值
- 排隊等待:勻速排隊,讓請求以勻速的速度通過,閾值類型必須設置為QPS,否則無效
流控模式
直接(默認)
- 配置:表示1秒鐘內查詢1次就是OK,若超過次數1,就直接快速失敗,報默認錯誤
-
測試:快速點擊訪問 //localhost:8401/testA => Blocked by Sentinel(flow limiting)
-
思考:直接調用了默認報錯資訊,技術方面是沒問題的,但是 是否因該有我們自己的後續處理呢?(類似有一個fallback的兜底方法),後面會講到如何配置
關聯
- 是什麼:當關聯的資源達到閾值時,就限流自己。當與A關聯的資源B達到閾值後,就限流自己。例如B惹事A掛了
- 配置A:當關聯資源/testB的QPS閾值超過1時,就限流/testA的Rest訪問地址,當關聯資源到閾值後限制配置好的資源名
- postman模擬並發密集訪問testB
- 訪問B成功
- postman里新建多執行緒集合組(Collection)
- 將訪問地址添加進新執行緒組(Collection)
- RUN => 大批量執行緒高並發訪問B,導致A失效了
- 運行後發現testA掛了(點擊訪問A =>Blocked by Sentinel(flow limiting) )
鏈路
- 多個請求調用了同一個微服務,相同道理,這裡就不演示了
流控效果
快速失敗
- 直接失敗,拋出異常(Blocked by Sentinel(flow limiting))
- 源碼:com.alibaba.csp.sentinel.slots.block.controller.DefaultController
Warm Up(預熱)
- 說明:公式 => 閾值除以coldFactor冷載入因子(默認值為3),經過預熱時長後才會達到閾值
- 源碼:com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController
- Warm Up
- Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即預熱/冷啟動方式。當系統長期處於低水位的情況下,當流量突然增加時,直接把系統提升到高水位可能瞬間把系統壓垮。通過「冷啟動」,讓通過的流量緩慢增加,在一定時間內逐漸增加到閾值上限,給冷系統一個預熱的時間,避免冷系統被壓垮。詳細文檔可以參考 流量控制 – Warm Up文檔,具體的例子可見 WarmUpFlowDemo。
- 通常冷處理的過程系統允許通過的QPS曲線如下圖所示:
- WarmUp配置
- 多次點擊//localhost:8401/testB,剛開始不行,後續慢慢OK
- 應用場景如:秒殺系統在開啟瞬間,會有很多流量上來,很可能把系統打死,預熱方式就是為了保護系統,可慢慢的把流量放進來,慢慢的把閾值增加到設置的閾值。
排隊等待
- 勻速排隊,閾值必須設置為QPS,否則無效
- 源碼:com.ailibaba.csp.sentinel.slots.block.controller.RateLimiterController
- 勻速排隊:
- 勻速排隊(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式會嚴格控制請求通過的間隔時間,也即是讓請求以均勻的速度通過,對應的是漏桶演算法。詳細文檔可以參考 流量控制 – 勻速排隊模式,具體的例子可以參見 PaceFlowDemo
- 該方式的作用如下圖所示:這種方式主要用於處理間隔性突發的流量,例如消息隊列。想像一下這樣的場景,在某一秒有大量的請求到來,而接下來的幾秒則處於空閑狀態,我們希望系統能夠在接下來的空閑期逐漸處理這些請求,而不是在第一秒直接拒絕多餘的請求。
- 配置:/test每秒1次請求,超過的話就排隊等待,等待的超時時間為20000毫秒
5.5 降級規則
名詞說明:
RT(平均響應時間,秒級)
- 平均響應時間,超出閾值 且 在時間窗口內通過的請求>=5,兩個條件同時滿足後觸發降級,窗口期過後關閉斷路器
- RT最大4900 ms(更大的需要通過
-Dcsp.sentinel.statistic.max.rt=XXXX
才能生效)
異常比例(秒級)
- QPS >= 5 且 比例(秒級統計)超過閾值時,觸發降級,窗口期過後關閉斷路器
注意:Sentinel的斷路器是沒有半開狀態的
半開狀態:半開的狀態,系統自動去檢測是否請求有異常,沒有異常就關閉斷路器恢復使用,有異常則繼續打開斷路器不可用,具體可以參考Hystrix。
Sentinel熔斷降級會在調用鏈路中某個資源出現不穩定狀態時(例如調用超時或異常比例升高),對這個資源的調用進行限制,讓請求快速失敗,避免影響到其他的資源而導致級聯錯誤。
當資源被降級後,在接下來的降級時間窗口之內,對該資源的調用都自動熔斷(默認行為是拋出DegradException )。
降級策略演示
RT(平均響應時間(DEGRADE_GRADE_RT
))
- 是什麼:當 1s 內持續進入 5 個請求,且對應時刻的平均響應時間(秒級)均超過閾值(
count
,以ms為單位),那麼在接下來的時間窗口(DegradeRule
中的timeWindow
,以 s 為單位)之內,對這個方法的調用都會自動的熔斷(拋出DegradeException
)。注意 Sentinel 莫仍統計的 RT 上限是 4900 ms,超出次閾值的都會算作 4900 ms,若需要變更此上限可以通過啟動配置項-Dcsp.sentinel.statistic.max.rt=xxx
來配置。
-
測試
- 程式碼
@GetMapping("/testD")
public String testD() {
//暫停幾秒執行緒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("testD 測試RT");
return "==> testD";
}
- 配置
- jmeter壓測:直接 Blocked by Sentinel (flow limiting)
- 結論:① 默認請求是 1 秒 5個,而我們能jmeter壓測設置的 1 秒 10個。② 程式碼中設置的平均響應時間為 1 秒,而我們設置的RT為 2 毫秒。兩個條件都不滿足則直接斷路器開啟,微服務不可用了,服務熔斷降級。
異常比例(DEGRADE_GRADE_EXCEPTION_RATIO)
- 是什麼:當資源的每秒請求量 >= 5,並且每秒異常總數占通過量的比值超過閾值(
DegradeRule
中的count
)之後,資源進入降級狀態,即在接下來的時間窗口(DegradeRule
中的timeWindow
,以 s 為單位)之內,對這個方法的調用都會自動的返回。異常比率的閾值範圍是[0.0,1.0]
,代表 0% – 100%。
-
測試
- 程式碼
@GetMapping("/testD")
public String testD() {
log.info("testD 測試RT");
int age = 10 / 0;
return "==> testD";
}
- 配置
- jmeter壓測:直接 Blocked by Sentinel (flow limiting)
異常數(DEGRADE_GRADE_EXCEPTION_COUNT):
- 是什麼:當資源近 1 分鐘的異常數目超過閾值之後會進行熔斷降級。注意由於統計 時間窗口時分鐘級別的,若
timeWindow
小於 60s,則結束熔斷狀態後仍可能再進入熔斷狀態。即 時間窗口一定要 >= 60 秒。
-
測試
- 程式碼:同異常比例
- 配置
- jemter壓測:直接 Blocked by Sentinel (flow limiting)
5.6 熱點key限流
源碼
com.alibaba.csp.sentinel.slots.block.BlockException
名詞說明
何為熱點?熱點即經常訪問的數據。很多時候我們希望統計某個熱點數據中訪問頻次最高的 Top K 數據,並對其訪問進行限制。比如
- 商品 ID 為參數,統計一段時間內最常購買的商品 ID 並進行限制
- 用戶 ID 為參數,針對一段時間內頻繁訪問的用戶 ID 進行限制
熱點參數限流會統計傳入的熱點參數,並根據配置的限流閾值與模式,對包含熱點參數的資源調用進行限流。熱點參數限流可以看作是一種特殊的流量控制,僅對包含熱點參數的資源調用生效。
Sentinel 利用 LRU 策略統計最近最常訪問的熱點參數,結合令牌桶演算法來進行參數級別的流控。熱點參數限流支援集群模式。
回顧 – 兜底方法
兜底方法分為 系統默認 和 客戶自定義 兩種,之前限流出現問題後,都是用sentinel 系統默認的提示:Blocked by Sentinel (flow limiting)。
我們能不能自定義?類似hystrix,某個方法出問題了,就找對應的兜底降級方法!
=> 在sentinel中通過 @SentinelResource
註解能夠實現 (類似於hystrix的HystrixCommand
)。
/**
* value: 唯一標識,對應熱點規則資源名
* blockHandler:違背了熱點規則後指向的兜底方法,如果不配置,異常就會直接打到前台去(不友好)
*/
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey",blockHandler="deal_testHotKey")
public String testHotKey(@RequestParam(value="p1",required=false) String p1,
@RequestParam(value="p2",required=false) String p2){
return "==> testHotKey";
}
public String deal_testHotKey(String p1, String p2, BlockException e) {
return "==> deal_testHotKey";
}
配置
方法testHotKey裡面第一個參數只要QPS超過每秒1次,馬上降級處理。且用了我們自定義的兜底方法。
測試
# QPS超過每秒1次會限流
//localhost:8401/testHotKey?p1=0
# QPS超過每秒1次會限流
//localhost:8401/testHotKey?p1=0&p2=1
# 無限制,不會限流
//localhost:8401/testHotKey?p2=1
參數例外項
上述案例演示了第一個參數p1,當QPS超過每秒1次立刻就會被限流。
特殊情況:我們期望 p1 參數當它是某個特殊值時,它的限流和平時不一樣,例如當 p1 的值等於 5 時,它的閾值可以達到 200。怎樣實現呢?=> 配置參數例外項。
前提條件:熱點參數必須是 基本類型 或者 String
其他注意點
@SentineResource 處理的是Sentinel控制台配置的違規情況,有blockHandler方法配置的兜底方法處理。假如程式程式碼出錯如 int age = 10/0,這個是Java運行時爆出的運行時異常RunTimeException,@SentineResource 不管。
總結:@SentineResource 主管配置出錯,運行出錯該走異常還是會走異常!
5.7 系統規則
各項配置說明
系統保護規則是從應用級別的入口流量進行控制,從單台機器的 load,CPU使用率,平均RT,入口QPS 和並發執行緒數等幾個維度監控應用指標,讓系統儘可能跑在最大吞吐量的同時保證系統整體的穩定性。
系統保護規則是 應用整體維度的,而不是資源維度的,並且 僅對入口流量生效。入口流量指的是進入應用的流量(EntryType.IN
),比如 Web 伺服器 或Dubbo 服務端接受的請求,都屬於入口流量。
系統規則支援以下的模式:
Load 自適應
(僅對 Linux/Unix-like 機器生效):系統的 load1作為啟髮指標,進行自適應系統保護。當系統 load1 超過設定的啟發值,且系統當前的並發執行緒數超過估算的系統容量時才會觸發系統保護(BBR階段)。系統容量由系統的maxQps * minRt
估算得出。設定參考值一般是CPU cores * 2.5
。CPU usage
(1.5.0+ 版本):當系統CPU 使用率超過閾值即觸發系統保護(取值範圍 0.0 ~ 1.0),比較靈敏。平均RT
:當單台機器上所有入口流量的平均RT達到閾值即觸發系統保護,單位是毫秒。並發執行緒數
:當單台機器上所有入口流量的並發執行緒數達到閾值即觸發系統保護。入口QPS
:當單台機器上所有入口流量的 QPS 達到閾值即觸發系統保護。
配置全局QPS為例:
系統規則一般少用,使用危險,一竹竿打死一船人!
5.8 @SentinelResource
按資源名稱限流 + 後續處理
啟動nacos,sentinel
修改8401pom.xml,將公共模組也引入
<!-- 公共模組 -->
<dependency>
<groupId>com.polaris</groupId>
<artifactId>cloud-api-common</artifactId>
<version>${project.version}</version>
</dependency>
新增業務類RateLimitController,便於演示
@RestController
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")
public CommonResult byResource() {
return new CommonResult(200, "按資源名稱限流測試OK",
new Payment(2020L, "serial001"));
}
public CommonResult handleException(BlockException exception) {
return new CommonResult(444,
exception.getClass().getCanonicalName() + "\t 服務不可用");
}
}
配置流控規則:表示1秒鐘內查詢次數大於1,就跑到我們自定義的限流處進行限流
測試:出現了我們自定義的限流兜底方案!
發現問題:此時如果關閉服務8401會怎樣?=> Sentinel控制台流控規則消失了?臨時存儲?
按照Url地址限流 + 後續處理
通過訪問URL來限流,會返回Sentinel自帶默認的限流處理資訊
業務類增加
@GetMapping("/rateLimit/byUrl")
@SentinelResource(value = "byUrl")
public CommonResult byUrl() {
return new CommonResult(200,
"按url限流測試OK", new Payment(2020L, "serial002"));
}
配置流控規則
測試:會返回Seninel自帶的限流處理結果:Blocked by Sentinel (flow limiting)
上述兜底方案面臨的問題
系統默認的方案,沒有體現我們自己的業務需求
依照現有條件,我們自定義的處理方法又和業務程式碼耦合在一起,不直觀
每個業務方法都添加一個兜底方法,會導致程式碼膨脹加劇
全局統一的處理方法沒有體現
客戶自定義限流處理邏輯
創建CustomerBlockHandler類用於自定義限流處理邏輯
public class CustomerBlockHandler {
public static CommonResult handlerException(BlockException exception) {
return new CommonResult(4444,
"按客戶自定義,global handlerException => 1");
}
public static CommonResult handlerException2(BlockException exception) {
return new CommonResult(4444,
"按客戶自定義,global handlerException => 2");
}
}
RateLimitController增加方法
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class,
blockHandler = "handlerException2")
public CommonResult customerBlockHandler() {
return new CommonResult(200, "按客戶自定義",
new Payment(2020L, "serial003"));
}
測試:出現了我們自定義的限流兜底方案!
@SentinelResource的其他屬性
屬性 | 作用 | 是否必須 |
---|---|---|
value | 資源名稱 | 是 |
entryType | entry類型,標記流量的方向,取值IN/OUT,默認是OUT | 否 |
blockHandler | 處理BlockException的函數名稱。函數要求: 1. 必須是 public 2.返回類型與原方法一致 3. 參數類型需要和原方法相匹配,並在最後加 BlockException 類型的參數。4. 默認需和原方法在同一個類中。若希望使用其他類的函數,可配置 blockHandlerClass ,並指定blockHandlerClass裡面的方法。 |
否 |
blockHanderClass | 存放blockHandler的類。對應的處理函數必須static修飾,否則無法解析,其他要求:同blockHandler。 | 否 |
fallback | 用於在拋出異常的時候提供fallback處理邏輯。fallback函數可以針對所有類型的異常(除了 exceptionsToIgnore 裡面排除掉的異常類型)進行處理。函數要求:1. 返回類型與原方法一致 2. 參數類型需要和原方法相匹配,Sentinel 1.6開始,也可在方法最後加 Throwable 類型的參數。3.默認需和原方法在同一個類中。若希望使用其他類的函數,可配置 fallbackClass ,並指定fallbackClass裡面的方法。 |
否 |
fallbackClass (1.6) | 存放fallback的類。對應的處理函數必須static修飾,否則無法解析,其他要求:同fallback。 | 否 |
defaultFallback | 用於通用的 fallback 邏輯。默認fallback函數可以針對所有類型的異常(除了 exceptionsToIgnore 裡面排除掉的異常類型)進行處理。若同時配置了 fallback 和 defaultFallback,以fallback為準。函數要求:\1. 返回類型與原方法一致 \2. 方法參數列表為空,或者有一個 Throwable 類型的參數。\3. 默認需要和原方法在同一個類中。若希望使用其他類的函數,可配置 fallbackClass ,並指定 fallbackClass 裡面的方法。 |
否 |
exceptionTolgnore (1.6) | 指定排除掉哪些異常。排除的異常不會計入異常統計,也不會進入fallback邏輯,而是原樣拋出。 | 否 |
exceptionsToTrace | 需要trace的異常 | Throwable |
注意:
1.6.0 之前的版本 fallback 函數只針對降級異常(
DegradeException
)進行處理,不能針對業務異常進行處理。若 blockHandler 和 fallback 都進行了配置,則被限流降級而拋出
BlockException
時只會進入blockHandler
處理邏輯。若未配置blockHandler
、fallback
和defaultFallback
,則被限流降級時會將BlockException
直接拋出。從 1.4.0 版本開始,註解方式定義資源支援自動統計業務異常,無需手動調用
Tracer.trace(ex)
來記錄業務異常。Sentinel 1.4.0 以前的版本需要自行調用Tracer.trace(ex)
來記錄業務異常。
5.9 服務熔斷功能
sentinel整合Ribbon + openFeign + fallback
啟動nacos和sentinel
新建服務提供者9004/9005
- pom重要依賴
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- yml
server:
port: 9004
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
業務類
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
public static HashMap<Long, Payment> hashMap = new HashMap<>();
static {
hashMap.put(1L, new Payment(1L, "28a8c1e3bc2742d8848569891fb42181"));
hashMap.put(2L, new Payment(2L, "bba8c1e3bc2742d8848569891ac32182"));
hashMap.put(3L, new Payment(3L, "6ua8c1e3bc2742d8848569891xt92183"));
}
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
Payment payment = hashMap.get(id);
CommonResult<Payment> result = new CommonResult(200,
"from mysql,serverPort: " + serverPort, payment);
return result;
}
}
新建服務消費者84
- pom重要依賴
<!--SpringCloud openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
- yml
server:
port: 84
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
#配置Sentinel dashboard地址
dashboard: localhost:8080
#默認8719埠,假如被佔用會自動從8719開始依次+1掃描,直至找到未被佔用的埠
port: 8719
#消費者將要去訪問的微服務名稱(註冊成功進nacos的微服務提供者)
service-url:
nacos-user-service: //nacos-payment-provider
# 激活Sentinel對Feign的支援
feign:
sentinel:
enabled: true
- 主啟動
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class OrderNacosMain84 {
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain84.class, args);
}
}
- 配置類
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
- service
@Component
public class PaymentFallbackService implements PaymentService {
@Override
public CommonResult<Payment> paymentSQL(Long id) {
return new CommonResult<>(44444,
"服務降級返回,=> PaymentFallbackService",
new Payment(id, "errorSerial"));
}
}
- controller
- fallback管運行異常
- blockHandler管配置違規
- 若 blockHandler 和 fallback 都進行了配置,則被限流降級而拋出 BlockException 只會進入 blockHandler處理邏輯
@RestController
@Slf4j
public class CircleBreakerController {
public static final String SERVICE_URL = "//nacos-payment-provider";
@Resource
private RestTemplate restTemplate;
//① 沒有配置
// @SentinelResource(value = "fallback")
//② fallback只負責業務異常
// @SentinelResource(value = "fallback",fallback = "handlerFallback")
//③ blockHandler只負責sentinel控制台配置違規
// @SentinelResource(value = "fallback",blockHandler = "blockHandler")
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback", fallback = "handlerFallback",
blockHandler = "blockHandler",
exceptionsToIgnore = {IllegalArgumentException.class})
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL
+ "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException(
"IllegalArgumentException,非法參數異常!");
} else if (result.getData() == null) {
throw new NullPointerException(
"NullPointerException,該ID沒有對應記錄,空指針異常");
}
return result;
}
//本例是fallback
public CommonResult handlerFallback(@PathVariable Long id, Throwable e) {
Payment payment = new Payment(id, "null");
return new CommonResult<>(444, "兜底異常handlerFallback,exception內容 "
+ e.getMessage(), payment);
}
//本例是blockHandler
public CommonResult blockHandler(@PathVariable Long id, BlockException blockException) {
Payment payment = new Payment(id, "null");
return new CommonResult<>(445,
"blockHandler-sentinel限流,無此流水: blockException "
+ blockException.getMessage(), payment);
}
//==> OpenFeign
@Resource
private PaymentService paymentService;
@GetMapping(value = "/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
return paymentService.paymentSQL(id);
}
}
測試
- 測試輪詢://localhost:84/consumer/fallback/1
- 測試@SentinelResource
- 沒有任何配置:給客戶error頁面,不友好
- 添加fallback,blockHandler,exceptionsToIgnore:① 忽略了非法參數異常,出現非法參數異常仍是系統錯誤頁面,② 其他情況都有了兜底方案
5.10 規則持久化
是什麼
一旦我們重啟應用,Sentinel規則消失,生產環境需要將配置規則進行持久化
怎麼做
將限流規則持久進Nacos保存,只要刷新8401某個rest地址,Sentinel控制台的流控規則就能看的到,只要Nacos裡面的配置不刪除,針對8401上的流控規則持續有效。
步驟演示
現象:我們先在Sentinel配置一條流控規則,當重啟8401後,該流控規則就會失效消失了!
修改8401
- pom
<!--SpringCloud ailibaba sentinel-datasource-nacos 做持久化用 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
- yml (添加Nacos數據源配置)
添加Nacos業務規則配置(規則即可以保存在Sentinel中,也可以保存在Nacos中等,只要是一個持久化媒介就行)
[
{
"resource":"/rateLimit/byUrl", //資源名稱
"limitApp":"default", //來源應用
"grade":1, //閾值類型,0表示執行緒數,1表示QPS
"count":1, //單機閾值
"strategy":0, //流控模式,0表示直接,1表示關聯,2表示鏈路
"controlBehavior":0, //流控效果,0表示快速失敗,1表示Warm Up,2表示排隊等候
"clusterMode":false //是否集群
}
]
啟動8401 刷新Sentinel發現業務規則有了
停止8401,再看sentinel,發現停機後流控規則消失了!
重啟8401,再看sentinel,多次調用介面,多次刷新sentinel後,發現流控規則重新出現了,持久化驗證成功!
6. Seata處理分散式事務
6.1 分散式事務問題
分散式之前
單機庫存沒有這個問題
從1:1 => 1:N => N:N,開始出現分散式事務問題
分散式之後
單機應用被拆分成微服務應用,原來的三個模組被拆分成 三個獨立的應用,分別使用 三個獨立的數據源 ,業務操作需要調用 三個服務來完成。
此時 每個服務內部的數據一致性由 本地 事務來保證,但是 全局 的數據一致性問題沒法保證。
一句話:一次業務操作需要跨多個數據源或需要跨多個系統進行遠程調用,就會產生分散式事務問題。
6.2 Seata簡介
是什麼
Seata是一款開源的分散式事務解決方案,致力於在微服務架構下提供高性能和簡單易用的分散式事務服務。
作用
一個典型的分散式事務過程:ID + 三組件模型
- Transaction ID(XID):全局唯一的事務ID
- 三組件概念
- Transaction Coordinator(TC):事務協調器,維護全局事務的運行狀態,負責協調並驅動全局事務的提交或回滾
- Transaction Manager(TM):控制全局事務的邊界,負責開啟一個全局事務,並最終發起全局提交或全局回滾的決議
- Resource Manage(RM):控制分支(本地)事務,負責分支註冊,狀態彙報,並接受事務協調的指令,驅動分支(本地)事務的提交和回滾
處理過程(我們可以用這種方式幫助理解:RM-學生,TM-班主任,TC-授課老師)
- TM向TC申請開啟一個全局事務,全局事務創建成功並生成一個全局唯一的XID
- XID在微服務調用鏈路的上下文中傳播
- RM向TC註冊分支事務,將其納入XID對應全局事務的管轄
- TM向TC發起針對XID的全局提交或回滾決議
- TC調度XID下管轄的全部分支事務完成提交或回滾請求
怎樣用
本地 @Transational
全局 @GlobalTransational
- seata的分散式交易解決方案
6.3 Seata-Server安裝
下載版本
修改conf目錄下的file.conf配置文件
-
先備份原始file.conf文件
-
主要修改:自定義事務組名稱 + 事務日誌存儲模式為db + 資料庫連接
- service模組 / store模組
mysql5.7資料庫新建庫seata,建表db_store.sql在seata-server-0.9.0\seata\conf目錄裡面
修改seata-server-0.9.0\seata\conf目錄下的registry.conf目錄下的registry.conf配置文件
目的是指明註冊中心為nacos,及修改nacos連接資訊。
先啟動Nacos,埠號為8848
再啟動seata-server:bin目錄下seata-server.bat
6.4 訂單/庫存/賬戶業務資料庫準備
分散式事務業務說明
這裡我們創建三個服務,一個訂單服務,一個庫存服務,一個賬戶服務。
當用戶下單時,會在訂單服務中創建一個訂單,然後通過遠程調用庫存服務來扣減下單商品的庫存,再通過遠程調用賬戶服務來扣減用戶賬戶裡面的餘額,最後在訂單服務中修改訂單狀態為已完成。
該操作跨越三個資料庫,有兩次遠程調用,很明顯會有分散式事務問題。
創建業務資料庫
- 建庫sql
create database seata_order;
create database seata_storage;
create database seata_account;
- 按照上訴3庫分別建立對應業務表
# seata_order庫下新建t_order表
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`int` bigint(11) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) DEFAULT NULL COMMENT '用戶id',
`product_id` bigint(11) DEFAULT NULL COMMENT '產品id',
`count` int(11) DEFAULT NULL COMMENT '數量',
`money` decimal(11, 0) DEFAULT NULL COMMENT '金額',
`status` int(1) DEFAULT NULL COMMENT '訂單狀態: 0:創建中 1:已完結',
PRIMARY KEY (`int`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '訂單表' ROW_FORMAT = Dynamic;
# seata_storage庫下新建t_storage表
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`product_id` bigint(11) DEFAULT NULL COMMENT '產品id',
`total` int(11) DEFAULT NULL COMMENT '總庫存',
`used` int(11) DEFAULT NULL COMMENT '已用庫存',
`residue` int(11) DEFAULT NULL COMMENT '剩餘庫存',
PRIMARY KEY (`int`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '庫存' ROW_FORMAT = Dynamic;
INSERT INTO `t_storage` VALUES (1, 1, 100, 0, 100);
# seata_account庫下新建t_account表
CREATE TABLE `t_account` (
`id` bigint(11) NOT NULL COMMENT 'id',
`user_id` bigint(11) DEFAULT NULL COMMENT '用戶id',
`total` decimal(10, 0) DEFAULT NULL COMMENT '總額度',
`used` decimal(10, 0) DEFAULT NULL COMMENT '已用餘額',
`residue` decimal(10, 0) DEFAULT NULL COMMENT '剩餘可用額度',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '賬戶表' ROW_FORMAT = Dynamic;
INSERT INTO `t_account` VALUES (1, 1, 1000, 0, 1000);
- 按照上述3庫分別建立對應的回滾日誌表(三個庫都需要建立各自獨立的回滾日誌表)
# 該sql在seata\conf目錄下的db_undo_log.sql中
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
6.5 訂單/庫存/賬戶業務微服務準備
業務需求
下訂單 => 減庫存 => 扣餘額 => 改(訂單)狀態
新建訂單Order-Module seata-order-service2001
pom
<dependencies>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- SpringBoot整合Web組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--mysql-connector-java-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--日常通用jar包配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
yml
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
#自定義事務組名稱需要與seata-server中的對應
tx-service-group: fsp_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://mpolaris.top:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 1234321
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
domain
//CommonResult
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>{
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message)
{
this(code,message,null);
}
}
//order
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order{
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
private Integer status; //訂單狀態:0:創建中;1:已完結
}
Dao
@Mapper
public interface OrderDao{
//1 新建訂單
void create(Order order);
//2 修改訂單狀態,從零改為1
void update(@Param("userId") Long userId,
@Param("status") Integer status);
}
service
public interface OrderService{
void create(Order order);
}
@FeignClient(value = "seata-storage-service")
public interface StorageService{
@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId,
@RequestParam("count") Integer count);
}
@FeignClient(value = "seata-account-service")
public interface AccountService{
@PostMapping(value = "/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId,
@RequestParam("money") BigDecimal money);
}
@Service
@Slf4j
public class OrderServiceImpl implements OrderService{
@Resource
private OrderDao orderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
/**
* 創建訂單->調用庫存服務扣減庫存->調用賬戶服務扣減賬戶餘額->修改訂單狀態
* 簡單說:下訂單->扣庫存->減餘額->改狀態
*/
@Override
@GlobalTransactional(name="fsp-create-order",rollbackFor=Exception.class)
public void create(Order order)
{
log.info("==> 開始新建訂單");
//1 新建訂單
orderDao.create(order);
//2 扣減庫存
log.info("==> 訂單微服務開始調用庫存,做扣減Count");
storageService.decrease(order.getProductId(),order.getCount());
log.info("==> 訂單微服務開始調用庫存,做扣減end");
//3 扣減賬戶
log.info("==> 訂單微服務開始調用賬戶,做扣減Money");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("==> 訂單微服務開始調用賬戶,做扣減end");
//4 修改訂單狀態,從零到1,1代表已經完成
log.info("==> 修改訂單狀態開始");
orderDao.update(order.getUserId(),0);
log.info("==> 修改訂單狀態結束");
log.info("==> 下訂單結束了,O(∩_∩)O哈哈~");
}
}
controller
@RestController
public class OrderController{
@Resource
private OrderService orderService;
@GetMapping("/order/create")
public CommonResult create(Order order)
{
orderService.create(order);
return new CommonResult(200,"訂單創建成功");
}
}
config
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource() {
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(
DataSourceProxy dataSourceProxy)throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean =
new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(
new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
@Configuration
@MapperScan({"com.polaris.springcloud.dao"})
public class MyBatisConfig {
}
主啟動
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消數據源的自動創建
public class SeataOrderMainApp2001 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMainApp2001.class, args);
}
}
mapper
<mapper namespace="com.polaris.springcloud.dao.OrderDao">
<resultMap id="BaseResultMap" type="com.polaris.springcloud.domain.Order">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="product_id" property="productId" jdbcType="BIGINT"/>
<result column="count" property="count" jdbcType="INTEGER"/>
<result column="money" property="money" jdbcType="DECIMAL"/>
<result column="status" property="status" jdbcType="INTEGER"/>
</resultMap>
<insert id="create">
insert into t_order (id,user_id,product_id,count,money,status)
values (null,#{userId},#{productId},#{count},#{money},0);
</insert>
<update id="update">
update t_order set status = 1
where user_id=#{userId} and status = #{status};
</update>
</mapper>
將seata-server/conf目錄下的file.conf和registry.conf拷貝到項目模組下的resource中(seata-server/conf下的是總控)
新建庫存Storage-Module seata-storage-service2002
新建賬戶Account-Module seata-account-service2003
參考訂單Module,源碼參考個人GitHub
6.6 測試
資料庫初始情況
正常下單
超時異常,沒加@GlobalTransactional
- AccountServiceImpl設置超時
@Override
public void decrease(Long userId, BigDecimal money) {
LOGGER.info("==> account-service中扣減賬戶餘額開始");
//模擬超時異常,全局事務回滾
//暫停幾秒鐘執行緒
try {
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
accountDao.decrease(userId, money);
LOGGER.info("==> account-service中扣減賬戶餘額結束");
}
- 資料庫情況:① 加進來了但是訂單狀態為0,表示未支付 ②庫存減少了 ③ 錢也被扣了
- 故障情況:
- 當庫存和賬戶金額扣減後,訂單狀態並沒有設置為已經完成,沒有從0改為1
- 而且由於feign的重試機制,賬戶餘額還有可能被多次扣減
超時異常,添加了@GlobalTransactional
- AccountServiceImpl設置超時
- 三個資料庫都未增加數據,即全局事務回滾了
6.7 Seata原理淺析
再次理解TC/TM/RM三個組件 – 分散式事務的執行流程:
- TM開啟分散式事務(TM向TC註冊全局事務記錄)
- 按業務場景,編排資料庫,服務等事務內資源(RM向TC彙報資源準備狀態)
- TM結束分散式事務,事務一階段結束(TM通知TC提交/回滾分散式事務)
- TC彙報事務資訊,決定分散式事務是否提交還是回滾
- TC通知所有RM提交/回滾資源,事務二階段結束
AT模式如何做到對業務的無侵入:
- AT模式
- 一階段載入
- 二階段提交
- 二階段回滾