Sentinel高階
sentinel和springCloud整合
減少開發的複雜度,對大部分的主流框架,例如:Web Servlet、Dubbo、Spring Cloud、gRPC、Spring WebFlux、Reactor等做了適配。只需要引入對應應用的以來即可方便地整合Sentinel。
如果要實現SpringCloud和Sentinel的整合,可以通過引入Spring Cloud Alibaba Sentinel來更方便得整合Sentinel。
Spring Cloud Alibaba是阿里巴巴集團提供的,致力於提供微服務開發的一站式解決方案。Spring Cloud Alibaba預設為Sentinel整合Servlet、RestTemplate、FeignClient和Spring WebFlux、Sentinel在Spring Cloud生態中,不僅補全了hystrix在Servlet和RestTemplate這一塊的空白,而且完全相容hystrix在FeignClient種限流降級的用法,並且支援運用時靈活地配置和調整限流降級規則。
需求
使用SpringCloud + Sentinel實現訪問http://localhost:8080/ann路徑的流量控制。
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.3.7.RELEASE"
id("io.spring.dependency-management") version "1.0.10.RELEASE"
kotlin("jvm") version "1.3.72"
kotlin("plugin.spring") version "1.3.72"
java
}
group = "xyz.ytfs"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_1_8
repositories {
mavenCentral()
}
extra["springCloudAlibabaVersion"] = "2.2.2.RELEASE"
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
}
dependencyManagement {
imports {
mavenBom("com.alibaba.cloud:spring-cloud-alibaba-dependencies:${property("springCloudAlibabaVersion")}")
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "1.8"
}
}
@SentinelResource(value = "spring_cloud_sentinel_test", blockHandler = "exceptionHandler")
@GetMapping("ann")
fun springCloudSentinelTest(): String {
return "hello Spring-Cloud-Sentinel_test"
}
fun exceptionHandler(bx: BlockException): String {
return "系統繁忙,請稍後重試"
}
Sentinel對Feign的支援
Sentinel適配了Feign元件,如果想使用,除了引入spring-cloud-starter-alibaba-sentinel
的依賴外還需要2個步驟:
- 配置檔案開啟Sentinel對Feign的支援:
feign.sentinel.enabled=true
- 加入
spring-cloud-starter-openfeign
依賴Sentinel starter中的自動化配置類生效
需求
實現sentinel_feign_client微服務通過Feign訪問sentinel_feign_provider微服務的流量控制
建立spring-cloud-parent父工程
- 依賴檔案
extra["springCloudVersion"] = "Hoxton.SR9"
extra["springCloudAlibabaVersion"] = "2.2.2.RELEASE"
group = "xyz.ytfs"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_1_8
allprojects {
repositories {
maven(url = "http://maven.aliyun.com/nexus/content/groups/public/")
mavenCentral()
maven { url = uri("https://repo.spring.io/snapshot") }
maven { url = uri("https://repo.spring.io/milestone") }
}
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
}
建立eureka-server註冊中心子工程
- 依賴新增
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-server")
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
}
dependencyManagement {
imports {
mavenBom("com.alibaba.cloud:spring-cloud-alibaba-dependencies:${property("springCloudAlibabaVersion")}")
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
}
}
- 啟動類和配置檔案的修改
@EnableEurekaServer //在啟動類上新增此註解,表示開啟eureka註冊中心服務
@SpringBootApplication
class EurekaServerApplication
fun main(args: Array<String>) {
runApplication<EurekaServerApplication>(*args)
}
# 應用名稱
spring.application.name=eureka-server
server.port=8060
#eureka配置
eureka.client.service-url.defaultZone=http://127.0.0.1:8060/eureka
#不拉去服務
eureka.client.fetch-registry=false
#不註冊自己
eureka.client.register-with-eureka=false
建立sentinel-feign-client
-
新增依賴
implementation("com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel") implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client") implementation("org.springframework.cloud:spring-cloud-starter-openfeign") testImplementation("org.springframework.boot:spring-boot-starter-test") { exclude(group = "org.junit.vintage", module = "junit-vintage-engine") }
-
建立代理的個介面
@FeignClient(value="sentinel-feign-provider", fallback = FallBackService::class) interface ProviderClient { @GetMapping("hello") fun hello(): String }
-
建立controller
@RestController class TestController(val providerClient: ProviderClient) { @GetMapping("hello") fun hello(): String{ return this.providerClient.hello() } }
-
建立降級相應示例
@Service /** * 實現代理介面 **/ class FallBackService : ProviderClient { override fun hello(): String { return "系統繁忙,請稍後重試" } }
-
配置檔案
# 應用名稱 spring: application: name: sentinel-feign-client cloud: sentinel: transport: dashboard: localhost:8045 eureka: client: service-url: defaultZone: http://127.0.0.1:8060/eureka server: port: 8061 # 開啟Sentinel對feign的支援 feign: sentinel: enabled: true
-
啟動類新增註解
@SpringBootApplication @EnableFeignClients @EnableDiscoveryClient class SentinelFeignClientApplication fun main(args: Array<String>) { runApplication<SentinelFeignClientApplication>(*args) }
建立sentinel-feign-provider
-
新增依賴
dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client") implementation("org.springframework.cloud:spring-cloud-starter-openfeign") testImplementation("org.springframework.boot:spring-boot-starter-test") { exclude(group = "org.junit.vintage", module = "junit-vintage-engine") } } dependencyManagement { imports { mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}") mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}") } }
-
修改配置檔案
# 應用名稱 spring.application.name=sentinel-feign-provider # 應用服務 WEB 訪問埠 server.port=8062 eureka.client.service-url.defaultZone=http://127.0.0.1:8060/eureka
-
啟動類增加註解
@SpringBootApplication @EnableDiscoveryClient @EnableFeignClients class SentinelFeignProviderApplication fun main(args: Array<String>) { runApplication<SentinelFeignProviderApplication>(*args) }
-
提供介面
@RestController class ProviderController { @GetMapping("hello") fun hello(): String { return "Hello Feign Sentintl" } }
執行測試
啟動專案,在Sentinel控制檯中增加關於資源流控規則.Sentinel和Feign整合時,流控規則的編寫形式為:http請求方式:協議//服務名稱/請求路徑跟引數
例如GET:http://sentinel-feign-provider/hello
Sentinel對Spring Cloud Gateway的支援
從1.6.0版本開始,Sentinel提供了Spring Cloud Gateway的適配模組,可以提供兩種資源維度的限流:
- route維度:即在Spring的配置檔案種配置的路由條目,資源名對應相應的routeId
- 自定義API維度:使用者可以利用Sentinel提供的API來自定義一些API分組
微服務閘道器搭建
在上面基礎上建立
建立子工程sentinel-gateway
,在build.gradle.kts
中引入依賴
implementation("org.springframework.cloud:spring-cloud-starter-gateway")
整合Sentinel
-
匯入依賴
implementation("com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel") implementation("com.alibaba.cloud:spring-cloud-alibaba-sentinel-gateway")
-
建立一個配置類,配置流控降級回撥操作
@Configuration
class GatewayConfiguration {
@PostConstruct
fun doInit() {
GatewayCallbackManager.setBlockHandler(BlockRequestHandler {
serverWebExchange: ServerWebExchange?, throwable: Throwable? ->
return@BlockRequestHandler ServerResponse.status(200).bodyValue("系統繁忙,請稍後再試!")
})
}
}
-
路由的配置
# 配置路由 spring.cloud.gateway.routes[0].id=sentinel-feign-gateway # lb代表的是 Load Balance負載均衡,如果是一個服務(auth-service)多個例項,實現自主分發 spring.cloud.gateway.routes[0].uri=lb://sentinel-feign-client # 匹配路徑 spring.cloud.gateway.routes[0].predicates[0]=Path=/hello/** # 配置Stentinel的控制檯地址 spring.cloud.sentinel.transport.dashboard=http://localhost:8045
流量控制實現
Sentinel的所有規則都可以在記憶體太中動態的查詢及修改,修改之後立即生效。同時Sentinel也提供相關API,供您來定製自己的規則策略。
Sentinel主要支援一下幾種規則:
- 流量控制規則
- 熔斷降級規則
- 系統保護規則
- 來源訪問控制規則
- 動態規劃擴充套件
流量控制規則實現
流量控制(Flow Control) ,其原理是監控應用流量的QPS或併發執行緒數等指標,當達到指定的閥值時對流量進行控制,以免被瞬時的流量高峰沖垮,從而保障應用的高可用性。
流量控制主要兩種方式:
- 併發執行緒數:併發執行緒數限流用於保護業務執行緒數不被耗盡
- QPS:當QPS超過某個閥值的時候,則採取措施進行流量控制
一條限流規則主要由幾個因素組成,我們可以組合這些元素來實現不同的限流效果:
resource
:資源名,即限流規則的作用物件count
: 限流閾值grade
: 限流閾值型別(QPS 或併發執行緒數)limitApp
: 流控針對的呼叫來源,若為default
則不區分呼叫來源strategy
: 呼叫關係限流策略controlBehavior
: 流量控制效果(直接拒絕、Warm Up、勻速排隊)
直接拒絕
直接拒絕:(RuleConstant.CONTROL_BEHAVIOR_DEFAULT
)方式是預設的流量控制方式,當QPS超過任意規則的閾值後,新的請求就會被立即拒絕,拒絕方式為丟擲FlowException
。這種方式適用於對系統處理能力確切已知的情況下,比如通過壓測確定了系統的準確水位時。
Warm Up
Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP
)方式,即預熱/冷啟動方式。當系統長期處於低水位的情況下,當流量突然增加時,直接把系統拉昇到高水位可能瞬間把系統壓垮。通過"冷啟動",讓通過的流量緩慢增加,在一定時間內逐漸增加到閾值上限,給冷系統一個預熱的時間,避免冷系統被壓垮。
勻速排隊
勻速排隊(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER
)方式會嚴格控制請求通過的間隔時間,也即是讓請求以均勻的速度通過,對應的是漏桶演算法。該方式的作用如下圖所示:
這種方式主要用於處理間隔性突發的流量,例如訊息佇列。想象一下這樣的場景,在某一秒有大量的請求到來,而接下來的幾秒則處於空閒狀態,我們希望系統能夠在接下來的空閒期間逐漸處理這些請求,而不是在第一秒直接拒絕多餘的請求。
注意:勻速排隊模式暫時不支援 QPS > 1000 的場景。
熔斷降級
概述
除了流量控制以外,對呼叫鏈路中不穩定的資源進行熔斷降級也是保障高可用的重要措施之一。一個服務常常會呼叫別的模組,可能是另外的一個遠端服務、資料庫,或者第三方 API 等。例如,支付的時候,可能需要遠端呼叫銀聯提供的 API;查詢某個商品的價格,可能需要進行資料庫查詢。然而,這個被依賴服務的穩定性是不能保證的。如果依賴的服務出現了不穩定的情況,請求的響應時間變長,那麼呼叫服務的方法的響應時間也會變長,執行緒會產生堆積,最終可能耗盡業務自身的執行緒池,服務本身也變得不可用。
現代微服務架構都是分散式的,由非常多的服務組成。不同服務之間相互呼叫,組成複雜的呼叫鏈路。以上的問題在鏈路呼叫中會產生放大的效果。複雜鏈路上的某一環不穩定,就可能會層層級聯,最終導致整個鏈路都不可用。因此我們需要對不穩定的弱依賴服務呼叫進行熔斷降級,暫時切斷不穩定呼叫,避免區域性不穩定因素導致整體的雪崩。熔斷降級作為保護自身的手段,通常在客戶端(呼叫端)進行配置。
注意:本文件針對 Sentinel 1.8.0 及以上版本。1.8.0 版本對熔斷降級特性進行了全新的改進升級,請使用最新版本以更好地利用熔斷降級的能力。
重要的屬性
Field | 說明 | 預設值 |
---|---|---|
resource | 資源名,即規則的作用物件 | |
grade | 熔斷策略,支援慢呼叫比例/異常比例/異常數策略 | 慢呼叫比例 |
count | 慢呼叫比例模式下為慢呼叫臨界 RT(超出該值計為慢呼叫);異常比例/異常數模式下為對應的閾值 | |
timeWindow | 熔斷時長,單位為 s | |
minRequestAmount | 熔斷觸發的最小請求數,請求數小於該值時即使異常比率超出閾值也不會熔斷(1.7.0 引入) | 5 |
statIntervalMs | 統計時長(單位為 ms),如 60*1000 代表分鐘級(1.8.0 引入) | 1000 ms |
slowRatioThreshold | 慢呼叫比例閾值,僅慢呼叫比例模式有效(1.8.0 引入) |
熔斷降級策略詳解
Sentinel 提供以下幾種熔斷策略:
- 慢呼叫比例 (
SLOW_REQUEST_RATIO
):選擇以慢呼叫比例作為閾值,需要設定允許的慢呼叫 RT(即最大的響應時間),請求的響應時間大於該值則統計為慢呼叫。當單位統計時長(statIntervalMs
)內請求數目大於設定的最小請求數目,並且慢呼叫的比例大於閾值,則接下來的熔斷時長內請求會自動被熔斷。經過熔斷時長後熔斷器會進入探測恢復狀態(HALF-OPEN 狀態),若接下來的一個請求響應時間小於設定的慢呼叫 RT 則結束熔斷,若大於設定的慢呼叫 RT 則會再次被熔斷。 - 異常比例 (
ERROR_RATIO
):當單位統計時長(statIntervalMs
)內請求數目大於設定的最小請求數目,並且異常的比例大於閾值,則接下來的熔斷時長內請求會自動被熔斷。經過熔斷時長後熔斷器會進入探測恢復狀態(HALF-OPEN 狀態),若接下來的一個請求成功完成(沒有錯誤)則結束熔斷,否則會再次被熔斷。異常比率的閾值範圍是[0.0, 1.0]
,代表 0% - 100%。 - 異常數 (
ERROR_COUNT
):當單位統計時長內的異常數目超過閾值之後會自動進行熔斷。經過熔斷時長後熔斷器會進入探測恢復狀態(HALF-OPEN 狀態),若接下來的一個請求成功完成(沒有錯誤)則結束熔斷,否則會再次被熔斷。
注意異常降級僅針對業務異常,對 Sentinel 限流降級本身的異常(BlockException
)不生效。為了統計異常比例或異常數,需要通過 Tracer.trace(ex)
記錄業務異常。示例:
Entry entry = null;
try {
entry = SphU.entry(key, EntryType.IN, key);
// Write your biz code here.
// <<BIZ CODE>>
} catch (Throwable t) {
if (!BlockException.isBlockException(t)) {
Tracer.trace(t);
}
} finally {
if (entry != null) {
entry.exit();
}
}
開源整合模組,如 Sentinel Dubbo Adapter, Sentinel Web Servlet Filter 或 @SentinelResource
註解會自動統計業務異常,無需手動呼叫。
熔斷器事件監聽
Sentinel 支援註冊自定義的事件監聽器監聽熔斷器狀態變換事件(state change event)。示例:
EventObserverRegistry.getInstance().addStateChangeObserver("logging",
(prevState, newState, rule, snapshotValue) -> {
if (newState == State.OPEN) {
// 變換至 OPEN state 時會攜帶觸發時的值
System.err.println(String.format("%s -> OPEN at %d, snapshotValue=%.2f", prevState.name(),
TimeUtil.currentTimeMillis(), snapshotValue));
} else {
System.err.println(String.format("%s -> %s at %d", prevState.name(), newState.name(),
TimeUtil.currentTimeMillis()));
}
});
程式碼實現
//定義熔斷資源和回撥函式
@SentinelResource(value = "degrade_rule", blockHandler = "exceptionHandler")
@GetMapping("degrade")
fun ruleHello(): String {
return "hello rule sentinel"
}
//降級方法
fun exceptionHandler(e: BlockException): String {
e.printStackTrace()
return "系統繁忙,請稍後!,降級"
}
@PostConstruct
fun initDegradeRule() {
//1、建立存放熔斷規則的集合
val rules: ArrayList<DegradeRule> = ArrayList()
//2、建立熔斷規則
val rule: DegradeRule = DegradeRule()
//設定熔斷資源名稱
rule.resource = "degrade_rule"
//閥值
rule.count = 0.01
//降級的時間,單位S
rule.timeWindow = 10
//設定熔斷型別
/**
* 當資源的平均響應時間超過閥值(DegradeRule中的count以毫秒為單位)之後,資源進入準降級狀態。
* 然後持續進入5個請求,他們的RT都持續超過這個閥值,
* 那麼在接下來的時間視窗(DegradeRule中的timeWindow,以s秒為單位)之內
* 將丟擲DegradeException
*/
rule.grade = RuleConstant.DEGRADE_GRADE_RT
//3、將熔斷規則存入集合
rules.add(rule)
//4、載入熔斷規則集合
DegradeRuleManager.loadRules(rules)
}
黑白名單控制
很多時候,我們需要根據呼叫來源來判斷該次請求是否允許放行,這時候可以使用 Sentinel 的來源訪問控制(黑白名單控制)的功能。來源訪問控制根據資源的請求來源(origin
)限制資源是否通過,若配置白名單則只有請求來源位於白名單內時才可通過;若配置黑名單則請求來源位於黑名單時不通過,其餘的請求通過。
呼叫方資訊通過
ContextUtil.enter(resourceName, origin)
方法中的origin
引數傳入。
規則配置
來源訪問控制規則(AuthorityRule
)非常簡單,主要有以下配置項:
resource
:資源名,即限流規則的作用物件。limitApp
:對應的黑名單/白名單,不同 origin 用,
分隔,如appA,appB
。strategy
:限制模式,AUTHORITY_WHITE
為白名單模式,AUTHORITY_BLACK
為黑名單模式,預設為白名單模式。
示例
比如我們希望控制對資源 test
的訪問設定白名單,只有來源為 appA
和 appB
的請求才可通過,則可以配置如下白名單規則:
AuthorityRule rule = new AuthorityRule();
rule.setResource("test");
rule.setStrategy(RuleConstant.AUTHORITY_WHITE);
rule.setLimitApp("appA,appB");
AuthorityRuleManager.loadRules(Collections.singletonList(rule));
動態規則
規則
Sentinel 的理念是開發者只需要關注資源的定義,當資源定義成功後可以動態增加各種流控降級規則。Sentinel 提供兩種方式修改規則:
- 通過 API 直接修改 (
loadRules
) - 通過
DataSource
適配不同資料來源修改
手動通過 API 修改比較直觀,可以通過以下幾個 API 修改不同的規則:
FlowRuleManager.loadRules(List<FlowRule> rules); // 修改流控規則
DegradeRuleManager.loadRules(List<DegradeRule> rules); // 修改降級規則
手動修改規則(硬編碼方式)一般僅用於測試和演示,生產上一般通過動態規則源的方式來動態管理規則。
DataSource 擴充套件
上述 loadRules()
方法只接受記憶體態的規則物件,但更多時候規則儲存在檔案、資料庫或者配置中心當中。DataSource
介面給我們提供了對接任意配置源的能力。相比直接通過 API 修改規則,實現 DataSource
介面是更加可靠的做法。
我們推薦通過控制檯設定規則後將規則推送到統一的規則中心,客戶端實現 ReadableDataSource
介面端監聽規則中心實時獲取變更,流程如下:
DataSource
擴充套件常見的實現方式有:
- 拉模式:客戶端主動向某個規則管理中心定期輪詢拉取規則,這個規則中心可以是 RDBMS、檔案,甚至是 VCS 等。這樣做的方式是簡單,缺點是無法及時獲取變更;
- 推模式:規則中心統一推送,客戶端通過註冊監聽器的方式時刻監聽變化,比如使用 Nacos、Zookeeper 等配置中心。這種方式有更好的實時性和一致性保證。
Sentinel 目前支援以下資料來源擴充套件:
示例
1、啟動本地的nacos
啟動檔案在``nacos/bin`目錄下面
startup.cmd -m standalone
:代表單機啟動的意思
2、向nacos中新增限制規則
/**
* 向nacos中傳送配置
*/
fun send() {
val remoteAddress = "localhost"
val groupId = "Sentinel:Demo"
val dataId = "com.alibaba.csp.sentinel.demo.flow.rule"
val rule = """[
{
"resource": "TestResource",
"controlBehavior": 0,
"count": 5.0,
"grade": 1,
"limitApp": "default",
"strategy": 0
}
]"""
val configService = NacosFactory.createConfigService(remoteAddress)
println(configService.publishConfig(dataId, groupId, rule))
}
3、從nacos中讀取配置規則
// remoteAddress 代表 Nacos 服務端的地址
val remoteAddress = "127.0.0.1"
// groupId 和 dataId 對應 Nacos 中相應配置
val groupId = "Sentinel:Demo"
val dataId = "com.alibaba.csp.sentinel.demo.flow.rule"
/**
* 載入規則
*/
fun loadRules() {
val flowRuleDataSource: NacosDataSource<List<FlowRule?>> = NacosDataSource<List<FlowRule?>>(
remoteAddress, groupId, dataId
) { source: String? ->
JSON.parseObject<List<FlowRule?>>(
source,
object : TypeReference<List<FlowRule?>?>() {})
}
FlowRuleManager.register2Property(flowRuleDataSource.property)
}