實(shí)踐:從配置到滑動(dòng)窗口與規(guī)則持久化)
提到Sentinel很多同學(xué)第一反應(yīng)是限流熔斷組件但真正到要接入項(xiàng)目的時(shí)候又容易被一堆概念繞暈資源、規(guī)則、Slot鏈、滑動(dòng)窗口、控制臺(tái)推送……這套東西要是沒(méi)搞懂底層機(jī)制配置半天不生效生產(chǎn)環(huán)境一壓測(cè)就原形畢露。這篇文章把我自己從“會(huì)用注解”到“能看懂源碼鏈路”這段時(shí)間踩過(guò)的坑、梳理清楚的關(guān)鍵點(diǎn)完整整理出來(lái)。適用的讀者比較明確已經(jīng)在用或者準(zhǔn)備用Sentinel做流量治理的Java后端開(kāi)發(fā)尤其是微服務(wù)架構(gòu)剛起步、接口偶發(fā)性抖動(dòng)、想引入限流但不知道從哪下手的團(tuán)隊(duì)。我會(huì)先從配置流程講起再深入到底層實(shí)現(xiàn)原理這一次盡量把“為什么這樣配置”和“運(yùn)行時(shí)到底發(fā)生了什么”串起來(lái)講而不是貼一堆官方文檔。1. 先搞清楚Sentinel解決什么問(wèn)題1.1 三個(gè)核心場(chǎng)景直擊痛點(diǎn)Sentinel的核心能力概括起來(lái)就三塊流量控制、熔斷降級(jí)、系統(tǒng)負(fù)載保護(hù)。聽(tīng)起來(lái)很抽象實(shí)際遇到的問(wèn)題是具體的。流量控制對(duì)應(yīng)的是突發(fā)流量把服務(wù)打掛的問(wèn)題。比如某個(gè)活動(dòng)頁(yè)面零點(diǎn)上線瞬間涌入的請(qǐng)求是平時(shí)的幾十倍數(shù)據(jù)庫(kù)連接池和下游服務(wù)根本沒(méi)有喘息機(jī)會(huì)。Sentinel做的不是單純限制QPS數(shù)字而是根據(jù)你定義好的閾值和策略把超過(guò)閾值的流量擋在業(yè)務(wù)邏輯之前保護(hù)入口鏈路不被打穿。熔斷降級(jí)對(duì)應(yīng)的是下游依賴出問(wèn)題時(shí)的“雪崩”傳導(dǎo)。比如訂單服務(wù)調(diào)用了庫(kù)存服務(wù)庫(kù)存服務(wù)GC停頓三秒訂單服務(wù)的線程就大批堆積堆積的線程又占滿Tomcat線程池緊接著整個(gè)訂單服務(wù)也假死。Sentinel通過(guò)慢調(diào)用比例、異常比例這些指標(biāo)判斷下游是否處于不健康狀態(tài)一旦觸發(fā)就快速返回兜底結(jié)果不再去請(qǐng)求異常的依賴。系統(tǒng)負(fù)載保護(hù)更偏向整機(jī)維度不是只看單個(gè)接口的QPS而是結(jié)合CPU使用率、系統(tǒng)Load、平均RT這些機(jī)器級(jí)指標(biāo)做兜底。這一點(diǎn)在很多網(wǎng)關(guān)場(chǎng)景特別有用后端機(jī)器已經(jīng)高負(fù)載了即使每個(gè)接口都沒(méi)達(dá)到流控閾值也應(yīng)該主動(dòng)削峰。三條能力各有適用場(chǎng)景但它們的底層都是“統(tǒng)計(jì) 規(guī)則判斷”理解這一點(diǎn)后面的機(jī)制就好懂很多。1.2 資源和規(guī)則理解配置的最小單元Sentinel整個(gè)模型可以壓縮成兩個(gè)詞資源和規(guī)則。資源是你要保護(hù)的目標(biāo)規(guī)則是定義怎么保護(hù)。資源在代碼里通常是一個(gè)方法、一個(gè)接口、或者一段業(yè)務(wù)邏輯。手動(dòng)埋點(diǎn)時(shí)用SphU.entry(someResource)創(chuàng)建入口用注解SentinelResource(someResource)也可以起到類似效果。規(guī)則則通過(guò)Rule對(duì)象描述比如FlowRule描述流控規(guī)則、DegradeRule描述熔斷規(guī)則、AuthorityRule描述黑白名單規(guī)則。配置規(guī)則的本質(zhì)就是把“資源”和“限制條件”關(guān)聯(lián)起來(lái)。我剛開(kāi)始接觸時(shí)有個(gè)誤區(qū)以為只要在控制臺(tái)配了規(guī)則代碼里不用做任何改動(dòng)就自動(dòng)生效。實(shí)際上資源埋點(diǎn)才是前提控制臺(tái)規(guī)則只是告訴Sentinel“對(duì)這個(gè)已埋點(diǎn)的資源做什么限制”。如果代碼里根本沒(méi)對(duì)某接口埋點(diǎn)控制臺(tái)配再多規(guī)則也是空轉(zhuǎn)。1.3 Sentinel和其他限流組件的本質(zhì)差異和Hystrix這類線程隔離方案比Sentinel的差異很直觀。Hystrix用線程池隔離來(lái)保護(hù)依賴每個(gè)依賴分配固定線程數(shù)超了直接拒絕代價(jià)是每一個(gè)下游調(diào)用都要跨線程切換線程開(kāi)銷很大。Sentinel走的是統(tǒng)計(jì)流量再加規(guī)則判斷的路線不額外創(chuàng)建線程池請(qǐng)求仍然走原始調(diào)用線程只是在入口處做了計(jì)數(shù)器判斷性能開(kāi)銷要小得多。Resilience4j是另一種輕量級(jí)方案它的滑動(dòng)窗口和熔斷器也做得不錯(cuò)但規(guī)則管理和控制臺(tái)體驗(yàn)比Sentinel弱一些。實(shí)際業(yè)務(wù)中Sentinel的優(yōu)勢(shì)主要體現(xiàn)在幾方面細(xì)粒度的實(shí)時(shí)監(jiān)控?cái)?shù)據(jù)、豐富的流控效果快速失敗、Warm Up、排隊(duì)等待、與微服務(wù)體系的控制臺(tái)聯(lián)動(dòng)。這些不是單純一個(gè)算法庫(kù)能提供的屬于完整的治理方案。2. 最小可用配置十分鐘接入哨兵2.1 引入依賴和初始化接入Sentinel的第一步是引入依賴。以最常見(jiàn)的Spring Boot項(xiàng)目為例Maven里加上對(duì)應(yīng)依賴即可。如果你是Spring Cloud Alibaba體系可以直接引spring-cloud-starter-alibaba-sentinel會(huì)連自動(dòng)配置一起帶進(jìn)來(lái)如果是純Spring Boot項(xiàng)目引sentinel-core和sentinel-transport-simple-http也夠用。dependency groupIdcom.alibaba.csp/groupId artifactIdsentinel-core/artifactId version1.8.6/version /dependency !-- 控制臺(tái)通信依賴接入dashboard時(shí)需要 -- dependency groupIdcom.alibaba.csp/groupId artifactIdsentinel-transport-simple-http/artifactId version1.8.6/version /dependency初始化代碼很少Sentinel提供了SPI機(jī)制大多數(shù)組件會(huì)自動(dòng)加載。如果是在非Spring環(huán)境里使用手動(dòng)調(diào)用一下InitExecutor.doInit()也可以強(qiáng)制觸發(fā)初始化。這個(gè)階段最容易忽略的是配置project.name所有接入控制臺(tái)的客戶端都會(huì)用這個(gè)名字上報(bào)不設(shè)置的話控制臺(tái)的“機(jī)器列表”里會(huì)看到一個(gè)默認(rèn)名多個(gè)應(yīng)用混在一起很難排查。2.2 用注解和API手動(dòng)埋點(diǎn)接入后第一件事不是配規(guī)則而是決定在哪里做資源埋點(diǎn)。注解方式最推薦在關(guān)鍵業(yè)務(wù)方法上使用直觀同時(shí)便于維護(hù)SentinelResource(value createOrder, blockHandler handleBlock) public Order createOrder(OrderRequest request) { // 核心業(yè)務(wù)邏輯 } public Order handleBlock(OrderRequest request, BlockException ex) { // 被限流或熔斷后的兜底邏輯 return Order.fallback(); }注意blockHandler方法的簽名必須和原方法一致且多一個(gè)BlockException參數(shù)。這里有個(gè)很容易踩的坑如果不寫(xiě)blockHandler接口被限流后直接把異常拋給上層用戶看到的就是500錯(cuò)誤而不是一個(gè)優(yōu)雅的降級(jí)提示。平時(shí)我看到不少項(xiàng)目過(guò)度依賴SentinelResource所有方法都加注解。其實(shí)有些場(chǎng)景適合手動(dòng)埋點(diǎn)比如被調(diào)用的第三方SDK方法不好加注解時(shí)可以用SphU直接包一層try (Entry entry SphU.entry(cacheGet)) { return cache.get(key); } catch (BlockException ex) { return getFromBackup(key); }埋點(diǎn)要在真實(shí)的熱點(diǎn)路徑上比如查詢接口入口、秒殺下單入口、數(shù)據(jù)庫(kù)寫(xiě)入前而不是在鏈路末端打個(gè)沒(méi)意義的標(biāo)記。2.3 三種規(guī)則配置方式規(guī)則配置有硬編碼、通過(guò)控制臺(tái)推送、通過(guò)外部數(shù)據(jù)源拉取三種常見(jiàn)方式。硬編碼方式適合本地驗(yàn)證和測(cè)試直接代碼里創(chuàng)建FlowRuleFlowRule rule new FlowRule(); rule.setResource(createOrder); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); rule.setCount(100); FlowRuleManager.loadRules(Collections.singletonList(rule));loadRules一調(diào)用規(guī)則立刻內(nèi)存生效不需要重啟。但生產(chǎn)環(huán)境千萬(wàn)別這么做每次發(fā)版改規(guī)則都要改代碼而且規(guī)則只存在本地內(nèi)存應(yīng)用重啟規(guī)則就沒(méi)了這是硬編碼最大的局限??刂婆_(tái)推送方式指在Dashboard頁(yè)面新增流控規(guī)則通過(guò)簡(jiǎn)單HTTP接口下發(fā)到客戶端內(nèi)存里。好處是可視化壞處是同樣不持久化應(yīng)用重啟后規(guī)則丟失需要重新推一遍。更致命的是多臺(tái)實(shí)例時(shí)控制臺(tái)會(huì)向每臺(tái)客戶端分別推送如果有一臺(tái)實(shí)例網(wǎng)絡(luò)抖動(dòng)沒(méi)收到該實(shí)例規(guī)則就跟其他實(shí)例不一致。所以生產(chǎn)環(huán)境只用控制臺(tái)推規(guī)則等于給自己埋雷。外部數(shù)據(jù)源方式是把規(guī)則放到注冊(cè)中心或配置中心客戶端通過(guò)DataSource自動(dòng)監(jiān)聽(tīng)變化。這是生產(chǎn)環(huán)境推薦方案具體做法下一章展開(kāi)。2.4 控制臺(tái)的安裝與客戶端接入Sentinel控制臺(tái)是個(gè)獨(dú)立的Web應(yīng)用官方給出的啟動(dòng)方式很直接java -Dserver.port8080 \ -Dcsp.sentinel.dashboard.serverlocalhost:8080 \ -Dproject.namesentinel-dashboard \ -jar sentinel-dashboard.jarcsp.sentinel.dashboard.server對(duì)控制臺(tái)本身來(lái)說(shuō)是注冊(cè)到哪個(gè)服務(wù)端默認(rèn)指向自己即可。啟動(dòng)成功后訪問(wèn)localhost:8080默認(rèn)賬號(hào)密碼都是sentinel??蛻舳私尤肟刂婆_(tái)時(shí)必須指定控制臺(tái)地址和應(yīng)用名java -Dcsp.sentinel.dashboard.serverlocalhost:8080 \ -Dproject.nameorder-service \ -jar order-service.jar如果是在Spring Boot的application.properties里配置對(duì)應(yīng)是這樣spring.cloud.sentinel.transport.dashboardlocalhost:8080 spring.application.nameorder-service客戶端接入后不會(huì)立即出現(xiàn)在控制臺(tái)必須等某個(gè)Sentinel資源第一次被訪問(wèn)產(chǎn)生了統(tǒng)計(jì)數(shù)據(jù)客戶端才會(huì)上報(bào)心跳和指標(biāo)。我第一次部署時(shí)在控制臺(tái)等機(jī)器列表一直不出現(xiàn)后來(lái)臨時(shí)調(diào)了一下接口才發(fā)現(xiàn)是“懶加載”機(jī)制。3. 規(guī)則持久化與動(dòng)態(tài)推送3.1 為啥規(guī)則老丟很多團(tuán)隊(duì)用控制臺(tái)配置完規(guī)則后遇到過(guò)一個(gè)詭異現(xiàn)象規(guī)則當(dāng)時(shí)是生效的但應(yīng)用一重啟規(guī)則全部消失還得重新配置。原因就是上一章說(shuō)的控制臺(tái)默認(rèn)模式是把規(guī)則推給客戶端內(nèi)存并沒(méi)有落到任何持久化存儲(chǔ)。Sentinel的FlowRuleManager.loadRules只負(fù)責(zé)更新內(nèi)存中的規(guī)則集合它本身沒(méi)有“保存到數(shù)據(jù)庫(kù)”的邏輯。這種設(shè)計(jì)在線上的問(wèn)題不止重啟丟規(guī)則。假設(shè)訂單服務(wù)部署了10臺(tái)機(jī)器你在控制臺(tái)把下單接口的閾值從100改到200控制臺(tái)會(huì)向每臺(tái)機(jī)器的內(nèi)存分別推送新規(guī)則。只要其中一臺(tái)推送失敗它留在舊值整個(gè)集群的流控閾值就是不均勻的流量調(diào)度隨機(jī)到這臺(tái)機(jī)器時(shí)表現(xiàn)會(huì)和別的不一樣。3.2 DataSource機(jī)制解析Sentinel為了解決規(guī)則來(lái)源問(wèn)題抽象出了DataSource接口它負(fù)責(zé)從外部存儲(chǔ)讀取規(guī)則并把變更推送給對(duì)應(yīng)RuleManager。核心流程是一個(gè)外部源配置中心或數(shù)據(jù)庫(kù)保存規(guī)則內(nèi)容格式通常是JSON字符串。DataSource定時(shí)拉取或者訂閱變更事件。拿到內(nèi)容后通過(guò)Converter轉(zhuǎn)換成規(guī)則列表。調(diào)用FlowRuleManager.register2Property(property)或DegradeRuleManager.register2Property(property)把規(guī)則送入對(duì)應(yīng)的規(guī)則管理器。讀到這里你會(huì)發(fā)現(xiàn)DataSource只是解決了“規(guī)則從哪里加載”的問(wèn)題至于你怎么往配置中心寫(xiě)規(guī)則可以手動(dòng)寫(xiě)、可以用控制臺(tái)改造后寫(xiě)、也可以寫(xiě)個(gè)腳本定時(shí)同步。常見(jiàn)可用的數(shù)據(jù)源有Nacos、Zookeeper、Apollo、Etcd核心思想都一樣配置中心是規(guī)則唯一權(quán)威來(lái)源。3.3 基于Nacos的規(guī)則持久化實(shí)例以Nacos作為配置中心做一個(gè)最簡(jiǎn)單的流控規(guī)則持久化。先引入擴(kuò)展依賴dependency groupIdcom.alibaba.csp/groupId artifactIdsentinel-datasource-nacos/artifactId version1.8.6/version /dependencySpring Boot項(xiàng)目里配置數(shù)據(jù)源BeanConfiguration public class SentinelNacosConfig { Bean public FlowRuleDataSource flowRuleDataSource() { String rule [\n {\n \resource\: \createOrder\,\n \grade\: 1,\n \count\: 100,\n \limitApp\: \default\\n }\n ]; return new FlowRuleDataSource(localhost:8848, sentinel-group, order-create-flow-rules, new NacosConverter()); } private static class NacosConverter implements ConverterString, ListFlowRule { Override public ListFlowRule convert(String source) { return JSON.parseArray(source, FlowRule.class); } } }配置完之后應(yīng)用啟動(dòng)時(shí)會(huì)自動(dòng)從Nacos拉取規(guī)則后續(xù)Nacos配置變更也會(huì)被監(jiān)聽(tīng)并熱更新。注意幾個(gè)參數(shù)dataId對(duì)應(yīng)Nacos配置IDgroup對(duì)應(yīng)Nacos分組grade為1表示QPS維度0表示并發(fā)線程數(shù)維度容易配反。實(shí)際生產(chǎn)環(huán)境里我建議把規(guī)則配置放在獨(dú)立的配置文件中不同環(huán)境用不同dataId分段例如order-service-flow-dev、order-service-flow-prod避免測(cè)試環(huán)境改閾值影響到生產(chǎn)。3.4 生產(chǎn)環(huán)境規(guī)則治理建議規(guī)則持久化只是第一步規(guī)則治理牽扯到更細(xì)的問(wèn)題。規(guī)則變更要遵循可追溯原則。每一次修改閾值都算一次變更最好有記錄誰(shuí)改的、什么時(shí)候改的、從多少改到多少??刂婆_(tái)本身日志比較弱可以通過(guò)改造數(shù)據(jù)源的寫(xiě)入端統(tǒng)一走公司的配置中心變更審計(jì)這樣出問(wèn)題時(shí)能查得到。規(guī)則發(fā)布要關(guān)注生效范圍。Nacos配置更新后所有客戶端實(shí)例會(huì)同時(shí)收到新規(guī)則。如果新閾值設(shè)置得太激進(jìn)比如從1000改成100全量實(shí)例瞬間收緊流量接口可用性可能大幅下降。穩(wěn)妥的做法是先改一臺(tái)實(shí)例的規(guī)則做灰度驗(yàn)證穩(wěn)定后再推全局。還要區(qū)分“業(yè)務(wù)規(guī)則”和“治理規(guī)則”。限流閾值往往和業(yè)務(wù)容量評(píng)估、壓測(cè)結(jié)果強(qiáng)相關(guān)不是運(yùn)維憑空填一個(gè)數(shù)。每隔一段時(shí)間需要根據(jù)峰值流量回放壓測(cè)看當(dāng)前閾值是否合理避免閾值定得太高起不到保護(hù)作用或者定得太低誤傷正常流量。4. 底層原理資源與Slot鏈的協(xié)作4.1 默認(rèn)鏈路從入口到最終攔截Sentinel的底層核心是一個(gè)責(zé)任鏈模式叫ProcessorSlotChain。每個(gè)資源在第一次訪問(wèn)時(shí)會(huì)構(gòu)建一條獨(dú)立的Slot鏈后續(xù)所有經(jīng)過(guò)該資源的流量都會(huì)在這條鏈上依次過(guò)一遍。一條默認(rèn)鏈路包含的主要Slot按順序是NodeSelectorSlot負(fù)責(zé)為當(dāng)前資源創(chuàng)建DefaultNode并把調(diào)用樹(shù)上下文綁到當(dāng)前入口。ClusterBuilderSlot負(fù)責(zé)創(chuàng)建ClusterNode用于統(tǒng)計(jì)整個(gè)資源粒度的全局?jǐn)?shù)據(jù)。StatisticSlot最核心的統(tǒng)計(jì)實(shí)例負(fù)責(zé)把請(qǐng)求的通過(guò)數(shù)、拒絕數(shù)、RT數(shù)據(jù)寫(xiě)入滑動(dòng)窗口。AuthoritySlot做黑白名單校驗(yàn)。SystemSlot做系統(tǒng)負(fù)載保護(hù)。FlowSlot根據(jù)限流規(guī)則判斷是否需要拒絕。DegradeSlot根據(jù)熔斷規(guī)則判斷當(dāng)前資源是否處于熔斷狀態(tài)。我最早看這個(gè)鏈時(shí)很疑惑為什么統(tǒng)計(jì)放在最前面規(guī)則判斷放在后面。后來(lái)一想就通了規(guī)則判斷需要依賴統(tǒng)計(jì)數(shù)據(jù)所以先讓StatisticSlot把本次請(qǐng)求寫(xiě)入統(tǒng)計(jì)窗口隨后FlowSlot讀取窗口數(shù)據(jù)時(shí)就能計(jì)算當(dāng)前QPS是否超限。如果超限拋出BlockException請(qǐng)求在鏈上被攔截如果正常請(qǐng)求繼續(xù)向下走最終進(jìn)入真實(shí)業(yè)務(wù)邏輯。這個(gè)設(shè)計(jì)還有一個(gè)精妙之處StatisticSlot在任何規(guī)則判斷之前都會(huì)把原始數(shù)據(jù)采集下來(lái)所以即使某個(gè)規(guī)則沒(méi)觸發(fā)控制臺(tái)依然能看到實(shí)時(shí)指標(biāo)。整個(gè)鏈路像流水線上的傳感器加閘機(jī)傳感器先記錄流量閘機(jī)再?zèng)Q定放不放行。4.2 統(tǒng)計(jì)數(shù)據(jù)的滑動(dòng)窗口實(shí)現(xiàn)Sentinel統(tǒng)計(jì)的核心不是簡(jiǎn)單計(jì)數(shù)器而是一個(gè)滑動(dòng)窗口。具體實(shí)現(xiàn)類是LeapArray它內(nèi)部維護(hù)一個(gè)環(huán)形數(shù)組數(shù)組每個(gè)元素就是一個(gè)窗口桶WindowWrap。每個(gè)窗口桶里保存了該時(shí)間窗口內(nèi)統(tǒng)計(jì)到的總請(qǐng)求數(shù)、總RT、成功數(shù)、異常數(shù)等指標(biāo)。在默認(rèn)秒級(jí)統(tǒng)計(jì)場(chǎng)景下Sentinel把1秒劃分成2個(gè)500毫秒的桶數(shù)組長(zhǎng)度也按這個(gè)粒度初始化。當(dāng)時(shí)間推進(jìn)到新的桶周期時(shí)會(huì)復(fù)用或覆蓋舊桶數(shù)據(jù)。這樣做的直接好處是QPS計(jì)算不需要額外加鎖讀當(dāng)前時(shí)間所在桶的計(jì)數(shù)除以窗口跨度即可。相比傳統(tǒng)的整秒固定窗口滑動(dòng)窗口能明顯緩解臨界問(wèn)題——固定窗口在秒邊界切換瞬間可能出現(xiàn)兩倍流量穿透滑動(dòng)窗口把粒度細(xì)化統(tǒng)計(jì)更連續(xù)。值得注意的是規(guī)則里的閾值比較是通過(guò)ClusterNode的指標(biāo)計(jì)算的不是單機(jī)DefaultNode。這意味著即使在多線程并發(fā)下統(tǒng)計(jì)也能拿到整個(gè)應(yīng)用內(nèi)該資源的總流量。int windowLength 500; // 500ms一個(gè)桶 int sampleCount 2; // 1s內(nèi)2個(gè)桶 long currentQps node.passQps(); // 統(tǒng)計(jì)通過(guò)QPS4.3 節(jié)點(diǎn)模型DefaultNode與ClusterNode節(jié)點(diǎn)模型是理解Sentinel統(tǒng)計(jì)隔離性的關(guān)鍵。每個(gè)資源在每次請(qǐng)求入口處會(huì)創(chuàng)建DefaultNode它歸屬于當(dāng)前調(diào)用鏈上下文。更準(zhǔn)確地說(shuō)同一個(gè)資源的DefaultNode在相同Context下是同一個(gè)實(shí)例在不同Context下是不同的實(shí)例。ClusterNode則是資源維度的全局節(jié)點(diǎn)和應(yīng)用入口無(wú)關(guān)。FlowSlot做限流判斷時(shí)用的是ClusterNode的統(tǒng)計(jì)值因?yàn)橄蘖麝P(guān)心的是這個(gè)資源總共過(guò)來(lái)了多少流量而不是某個(gè)上游或某個(gè)調(diào)用關(guān)系單獨(dú)給了多少流量。我自己寫(xiě)代碼時(shí)有一個(gè)習(xí)慣如果發(fā)現(xiàn)需要對(duì)某個(gè)上游來(lái)源單獨(dú)限流比如只限制某個(gè)內(nèi)部系統(tǒng)請(qǐng)求這個(gè)接口的頻率那就需要配合AuthoritySlot或者自定義origin白名單邏輯而不是單純依賴默認(rèn)的ClusterNode統(tǒng)計(jì)。5. 限流算法的內(nèi)核與配置取舍5.1 滑動(dòng)窗口計(jì)數(shù)模型Sentinel的默認(rèn)限流基于滑動(dòng)窗口計(jì)數(shù)它的判斷邏輯很簡(jiǎn)單讀取當(dāng)前滑動(dòng)窗口內(nèi)已經(jīng)通過(guò)的請(qǐng)求數(shù)如果加上當(dāng)前這個(gè)請(qǐng)求會(huì)超過(guò)閾值就拒絕。舉個(gè)例子閾值QPS100當(dāng)前500ms桶里已經(jīng)通過(guò)80個(gè)請(qǐng)求這時(shí)新請(qǐng)求進(jìn)來(lái)判斷通過(guò)總數(shù)如果即將超過(guò)100就攔截。但注意讀到的不是嚴(yán)格的1秒整窗口而是最近若干個(gè)桶的區(qū)間。窗口劃分得越細(xì)統(tǒng)計(jì)越平滑但計(jì)算也越重。Sentinel秒級(jí)統(tǒng)計(jì)默認(rèn)2個(gè)桶分鐘級(jí)統(tǒng)計(jì)默認(rèn)60個(gè)桶各有各的用途?;瑒?dòng)窗口的問(wèn)題在于它不是一個(gè)基于排隊(duì)論的平滑算法流量短時(shí)脈沖仍可能瞬時(shí)超過(guò)閾值只是不會(huì)像固定窗口那樣在窗口翻新瞬間出現(xiàn)雙倍量級(jí)的問(wèn)題。對(duì)于絕大多數(shù)接口限流場(chǎng)景這個(gè)精度已經(jīng)足夠。5.2 三種流控效果怎么選配置FlowRule時(shí)controlBehavior字段決定了流量超過(guò)閾值后的行為??焖偈∈悄J(rèn)模式超過(guò)閾值直接拋出BlockException在注解場(chǎng)景下進(jìn)入blockHandler。適合大多數(shù)業(yè)務(wù)接口對(duì)調(diào)用方來(lái)說(shuō)語(yǔ)義清晰服務(wù)忙請(qǐng)重試。Warm Up模式適合處理冷系統(tǒng)啟動(dòng)場(chǎng)景。比如配置了1000 QPS系統(tǒng)剛剛啟動(dòng)時(shí)不會(huì)讓流量一下子全上來(lái)而是從一個(gè)較小的值默認(rèn)是閾值的1/3逐漸增長(zhǎng)到1000 QPS。我經(jīng)常用在需要預(yù)熱的場(chǎng)景比如頻繁使用Java JIT、連接池剛建立等這個(gè)模式能有效避免冷啟動(dòng)瞬間被打滿。排隊(duì)等待模式本質(zhì)上是漏桶算法的實(shí)現(xiàn)。請(qǐng)求超閾值后不直接拒絕而是讓它在隊(duì)列里等待以固定的速率放行。適合應(yīng)對(duì)突發(fā)性質(zhì)的流量比如消息任務(wù)偶發(fā)大批量執(zhí)行、定時(shí)任務(wù)集中跑批不適合實(shí)時(shí)在線接口請(qǐng)求延遲會(huì)明顯變大。FlowRule rule new FlowRule(); rule.setResource(batchJob); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); rule.setCount(50); rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER); rule.setMaxQueueingTimeMs(500);這個(gè)例子里超出的請(qǐng)求最多等500毫秒如果等待超時(shí)還是沒(méi)獲得放行直接拒絕。5.3 熔斷降級(jí)的狀態(tài)機(jī)與恢復(fù)策略熔斷降級(jí)和限流最大區(qū)別在于它關(guān)注的是“被調(diào)用方是否健康”而不是“調(diào)用方有多少流量”。Sentinel的熔斷狀態(tài)有三個(gè)CLOSED關(guān)閉、OPEN打開(kāi)、HALF_OPEN半開(kāi)。初始狀態(tài)是CLOSED一切正常。當(dāng)統(tǒng)計(jì)周期內(nèi)慢調(diào)用比例或異常比例達(dá)到閾值熔斷器切換到OPEN接下來(lái)所有請(qǐng)求直接走降級(jí)邏輯不再真實(shí)調(diào)用下游。OPEN狀態(tài)持續(xù)timeWindow毫秒后自動(dòng)進(jìn)入HALF_OPEN半開(kāi)狀態(tài)此時(shí)放行少量探測(cè)請(qǐng)求如果探測(cè)請(qǐng)求成功熔斷器回到CLOSED如果還失敗重新回到OPEN。我這里有一個(gè)配置經(jīng)驗(yàn)慢調(diào)用比例閾值不能只看平均值要結(jié)合TP99來(lái)定。假如TP99本身是200ms你把最大RT定成100ms那幾乎所有請(qǐng)求都會(huì)被判成慢調(diào)用接口稍微波動(dòng)一下直接熔斷恢復(fù)后馬上又熔斷像個(gè)開(kāi)關(guān)一樣反復(fù)橫跳。初始配置寧愿寬松一點(diǎn)先觸發(fā)了再逐步收緊。DegradeRule degradeRule new DegradeRule(); degradeRule.setResource(getUserInfo); degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_RT); degradeRule.setCount(200); // 最大RT 200ms degradeRule.setTimeWindow(10); // 熔斷10秒 degradeRule.setMinRequestAmount(5); // 至少5個(gè)請(qǐng)求才觸發(fā)統(tǒng)計(jì) degradeRule.setStatIntervalMs(1000);minRequestAmount這個(gè)參數(shù)容易忽略但非常重要。請(qǐng)求量太少的時(shí)候統(tǒng)計(jì)樣本沒(méi)有統(tǒng)計(jì)意義可能一個(gè)慢請(qǐng)求就讓整個(gè)接口熔斷。加這個(gè)參數(shù)等于要求“至少積累這么多樣本才能下結(jié)論”。5.4 熱點(diǎn)參數(shù)與系統(tǒng)自適應(yīng)限流常規(guī)流控只針對(duì)資源整體熱點(diǎn)參數(shù)限流則細(xì)化到參數(shù)值維度。比如同一個(gè)商品查詢接口全網(wǎng)熱賣商品和普通商品的流量天差地別你希望針對(duì)商品ID做精細(xì)化限流某個(gè)熱門商品ID最多每秒查20次其他商品最多每秒查200次。這時(shí)需要用ParamFlowRule指定參數(shù)索引位置。系統(tǒng)自適應(yīng)規(guī)則相對(duì)更“大而全”它不會(huì)去區(qū)分具體資源而是全局生效。典型配置包括CPU使用率閾值、系統(tǒng)Load閾值、平均RT閾值、入口總QPS閾值、線程數(shù)閾值。其中系統(tǒng)Load的保護(hù)在Linux下才真正可靠因?yàn)橐x系統(tǒng)負(fù)載文件Windows下會(huì)退化為不生效。這套規(guī)則特別適合做入口網(wǎng)關(guān)的兜底保護(hù)比如整個(gè)網(wǎng)關(guān)的入口QPS先設(shè)置一個(gè)總量閾值其次再通過(guò)每條業(yè)務(wù)線自己的流控規(guī)則細(xì)化形成兩層保護(hù)。優(yōu)先級(jí)上系統(tǒng)規(guī)則在最前面防止整機(jī)資源被打滿業(yè)務(wù)規(guī)則在后面做更精準(zhǔn)的接口級(jí)控制。6. 常見(jiàn)問(wèn)題速查與踩坑排雷6.1 規(guī)則不生效怎么查這是出現(xiàn)頻率最高的問(wèn)題。第一步先確認(rèn)資源名是否一致??刂婆_(tái)配置里寫(xiě)的資源名必須和代碼埋點(diǎn)里的value一模一樣一個(gè)字符差都不行。很多人控制臺(tái)里寫(xiě)的是接口路徑/order/create代碼里注解寫(xiě)的是createOrder看起來(lái)像是同一個(gè)接口但Sentinel只認(rèn)字符串字面量對(duì)不上就是不生效。第二步確認(rèn)是否真的訪問(wèn)了資源。Sentinel是懶加載的沒(méi)有流量經(jīng)過(guò)埋點(diǎn)控制臺(tái)就不會(huì)有該資源的統(tǒng)計(jì)信息規(guī)則自然不會(huì)被觸發(fā)。檢查機(jī)器列表里是否能看到應(yīng)用如果看不到說(shuō)明客戶端跟控制臺(tái)心跳都不通更談不上規(guī)則下發(fā)了。第三步看是否有異常被吞掉。比如SentinelResource搭配了兜底方法但兜底方法內(nèi)部被try-catch吃掉了你看到的可能就是接口正常返回掩蓋了限流發(fā)生的事實(shí)。最好在兜底方法里加日志把BlockException的類型打出來(lái)快速定位是流控、熔斷、還是系統(tǒng)保護(hù)觸發(fā)的。6.2 監(jiān)控?cái)?shù)據(jù)與預(yù)期不符常見(jiàn)表現(xiàn)是控制臺(tái)看到的QPS和壓測(cè)工具統(tǒng)計(jì)的TPS對(duì)不上。一方面Sentinel控制臺(tái)顯示的QPS是經(jīng)過(guò)埋點(diǎn)資源統(tǒng)計(jì)的如果埋點(diǎn)位置在網(wǎng)關(guān)之后那就統(tǒng)計(jì)不到網(wǎng)關(guān)前的流量另一方面控制臺(tái)指標(biāo)是客戶端定時(shí)上報(bào)的本身有秒級(jí)延遲不是實(shí)時(shí)精確值。更隱蔽的一個(gè)坑是多個(gè)實(shí)例的統(tǒng)計(jì)隔離。如果應(yīng)用部署了多臺(tái)機(jī)器控制臺(tái)上顯示的是單臺(tái)機(jī)器的指標(biāo)不是說(shuō)所有實(shí)例聚合后的總量。你給單機(jī)配額100 QPS兩臺(tái)機(jī)器就是200 QPS總?cè)肟诹髁窟@在容量規(guī)劃時(shí)要算清楚。如果發(fā)現(xiàn)壓測(cè)流量被限流了但指標(biāo)沒(méi)明顯異??梢钥紤]是不是有Warm Up規(guī)則存在一開(kāi)始流量被壓低一段時(shí)間后才恢復(fù)也可能是maxQueueingTimeMs設(shè)置過(guò)小大量請(qǐng)求排無(wú)可排直接拒絕但統(tǒng)計(jì)數(shù)據(jù)里排隊(duì)時(shí)間看著正常。6.3 性能開(kāi)銷和依賴沖突Sentinel本身的性能開(kāi)銷主要集中在統(tǒng)計(jì)寫(xiě)入上由于滑動(dòng)窗口數(shù)據(jù)結(jié)構(gòu)是預(yù)分配數(shù)組加原子更新正常情況下單次統(tǒng)計(jì)耗時(shí)很短暫不會(huì)成為瓶頸。但如果你的業(yè)務(wù)本身QPS已經(jīng)特別高每個(gè)請(qǐng)求又埋了非常多資源點(diǎn)統(tǒng)計(jì)和Slot鏈遍歷的開(kāi)銷會(huì)成倍增加。這種時(shí)候要合理控制埋點(diǎn)粒度盡量在入口處和關(guān)鍵依賴處埋點(diǎn)不要層層埋點(diǎn)。依賴沖突常見(jiàn)于同時(shí)引入老版本Sentinel和應(yīng)用自帶的fastjson或jackson版本不一致導(dǎo)致反序列化規(guī)則JSON報(bào)錯(cuò)。排查方法很簡(jiǎn)單看到Converter轉(zhuǎn)ListFlowRule異常時(shí)多數(shù)是JSON庫(kù)沖突調(diào)整版本或者用自定義轉(zhuǎn)換器處理。還有一類問(wèn)題是控制臺(tái)版本和客戶端版本不一致。Dashboard版本過(guò)舊客戶端用新版本注冊(cè)時(shí)可能出現(xiàn)協(xié)議不兼容反饋到現(xiàn)象上是機(jī)器列表不顯示、規(guī)則推不進(jìn)去。我建議客戶端和Dashboard盡量保持同一個(gè)主版本升級(jí)時(shí)先升控制臺(tái)再升客戶端。6.4 一套推薦的配置落地順序根據(jù)我接入多個(gè)項(xiàng)目的經(jīng)驗(yàn)生產(chǎn)落地不要一上來(lái)就堆規(guī)則。建議按這個(gè)順序推進(jìn)先做好資源埋點(diǎn)梳理核心接口和熱點(diǎn)場(chǎng)景保證所有需要治理的入口都有明確的資源名。然后部署控制臺(tái)拿到基礎(chǔ)監(jiān)控?cái)?shù)據(jù)搞清每個(gè)接口的真實(shí)QPS、RT、異常率基線。有了基線之后再配限流規(guī)則閾值設(shè)為基線峰值的1.2到1.5倍先寬松后收緊。最后再逐步補(bǔ)充熔斷規(guī)則、熱點(diǎn)參數(shù)規(guī)則和系統(tǒng)規(guī)則每加一條規(guī)則都要觀察一段時(shí)間看觸發(fā)頻率和業(yè)務(wù)影響。規(guī)則之間也會(huì)互相作用比如入口QPS規(guī)則和慢調(diào)用熔斷規(guī)則同時(shí)存在時(shí)出現(xiàn)流量突增入口限流先攔掉一批熔斷統(tǒng)計(jì)到的請(qǐng)求數(shù)可能不足以觸發(fā)熔斷。這種時(shí)候要結(jié)合監(jiān)控日志看整個(gè)鏈路而不是盯著某一條規(guī)則調(diào)參。接入Sentinel這件事難點(diǎn)從來(lái)不在寫(xiě)幾行配置而在于你能否回答出“每個(gè)閾值為什么是這個(gè)數(shù)”“每一類規(guī)則到底在保護(hù)哪個(gè)薄弱點(diǎn)”。先把底層原理摸清楚配置只是順?biāo)浦鄣氖?。最后提個(gè)建議所有限流規(guī)則定義完畢之后花半天時(shí)間做一輪全鏈路壓測(cè)讓規(guī)則在真實(shí)流量下多跑一會(huì)兒這會(huì)暴露很多控制臺(tái)圖表看不出來(lái)的問(wèn)題。