今年下半年阿里開源了自研的限流系統 Sentinel,官方對 Sentinel 的介紹中用到了一系列高大山的名詞諸如 限流、熔斷降級、流量塑形、系統負載保護等,還有漂亮的形容詞諸如 輕巧、專業、實時等。做爲技術消費者看到這樣的廣告詞以後禁不住要大聲感嘆 —— NiuB!更要不得的是 Sentinel 的發佈會由阿里的高級技術專家 子衿 主講,她是一位女性開發者,這在男性主導額 IT 產業也算得上可貴一見的奇觀。java
我花了一成天的時間仔細研究了 Sentinel 的功能和代碼,大體摸清了總體的架構和局部的一些技術細節,這裏給你們作一次全面的分享。redis
首先,Sentinel 不算一個特別複雜的系統 ,普通技術開發者也能夠輕鬆理解它的原理和結構。你別看架構圖上 Sentinel 的周邊是一系列的其它高大山的開源中間件,這不過是一種華麗的包裝,其內核 Sentinel Core 確實是很是輕巧的。算法
首先咱們從它的 Hello World 開始,經過深刻理解這段入門代碼就能夠洞悉其架構原理。數據庫
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.4.0</version>
</dependency>
複製代碼
限流分爲單機和分佈式兩種,單機限流是指限定當前進程裏面的某個代碼片斷的 QPS 或者 併發線程數 或者 整個機器負載指數,一旦超出規則配置的數值就會拋出異常或者返回 false。我把這裏的被限流的代碼片斷稱爲「臨界區」。bash
而分佈式則須要另啓一個集中的發票服務器,這個服務器針對每一個指定的資源每秒只會生成必定量的票數,在執行臨界區的代碼以前先去集中的發票服務領票,若是領成功了就能夠執行,不然就會拋出限流異常。因此分佈式限流代價較高,須要多一次網絡讀寫操做。若是讀者閱讀了個人小冊《Redis 深度歷險》,裏面就提到了 Redis 的限流模塊,Sentinel 限流的原理和它是相似的,只不過 Sentinel 的發票服務器是自研的,使用了 Netty 框架。服務器
Sentinel 在使用上提供了兩種形式,一種是異常捕獲形式,一種是布爾形式。也就是當限流被觸發時,是拋出異常來仍是返回一個 false。下面咱們看看它的異常捕獲形式,這是單機版網絡
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
public class SentinelTest {
public static void main(String[] args) {
// 配置規則
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource("tutorial");
// QPS 不得超出 1
rule.setCount(1);
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setLimitApp("default");
rules.add(rule);
// 加載規則
FlowRuleManager.loadRules(rules);
// 下面開始運行被限流做用域保護的代碼
while (true) {
Entry entry = null;
try {
entry = SphU.entry("tutorial");
System.out.println("hello world");
} catch (BlockException e) {
System.out.println("blocked");
} finally {
if (entry != null) {
entry.exit();
}
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {}
}
}
}
複製代碼
使用 Sentinel 須要咱們提供限流規則,在規則的基礎上,將臨界區代碼使用限流做用域結構包裹起來。在上面的例子中限定了 tutorial 資源的單機 QPS 不得超出 1,可是實際上它的運行 QPS 是 2,這多出來的執行邏輯就會被限制,對應的 Sphu.entry() 方法就會拋出限流異常 BlockException。下面是它的運行結果數據結構
INFO: log base dir is: /Users/qianwp/logs/csp/
INFO: log name use pid is: false
hello world
blocked
hello world
blocked
hello world
blocked
hello world
blocked
...
複製代碼
從輸出中能夠看出 Sentinel 在本地文件中記錄了詳細的限流日誌,能夠將這部分日誌收集起來做爲報警的數據源。架構
咱們再看看它的 bool 形式,使用也是很簡單,大同小異。併發
import java.util.ArrayList;
import java.util.List;
import com.alibaba.csp.sentinel.SphO;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
public class SentinelTest {
public static void main(String[] args) {
// 配置規則
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource("tutorial");
// QPS 不得超出 1
rule.setCount(1);
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setLimitApp("default");
rules.add(rule);
FlowRuleManager.loadRules(rules);
// 運行被限流做用域保護的代碼
while (true) {
if (SphO.entry("tutorial")) {
try {
System.out.println("hello world");
} finally {
SphO.exit();
}
} else {
System.out.println("blocked");
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {}
}
}
}
複製代碼
上面的例子中規則都是經過代碼寫死的,在實際的項目中,規則應該須要支持動態配置。這就須要有一個規則配置源,它能夠是 Redis、Zookeeper 等數據庫,還須要有一個規則變動通知機制和規則配置後臺,容許管理人員能夠在後臺動態配置規則並實時下發到業務服務器進行控制。
有一些規則源存儲不支持事件通知機制,好比關係數據庫,Sentinel 也提供了定時刷新規則,好比每隔幾秒來刷新內存裏面的限流規則。下面是 redis 規則源定義// redis 地址
RedisConnectionConfig redisConf = new RedisConnectionConfig("localhost", 6379, 1000);
// 反序列化算法
Converter<String, List<FlowRule>> converter = r -> JSON.parseArray(r, FlowRule.class);
// 定義規則源,包含全量和增量部分
// 全量是一個字符串key,增量是 pubsub channel key
ReadableDataSource<String, List<FlowRule>> redisDataSource = new RedisDataSource<List<FlowRule>>(redisConf,"app_key", "app_pubsub_key", converter);
FlowRuleManager.register2Property(redisDataSource.getProperty());
複製代碼
接入 Sentinel 的應用服務器須要將本身的限流狀態上報到 Dashboard,這樣就能夠在後臺實時呈現全部服務的限流狀態。Sentinel 使用拉模型來上報狀態,它在當前進程註冊了一個 HTTP 服務,Dashboard 會定時來訪問這個 HTTP 服務來獲取每一個服務進程的健康情況和限流信息。
Sentinel 須要將服務的地址以心跳包的形式上報給 Dashboard,如此 Dashboard 才知道每一個服務進程的 HTTP 健康服務的具體地址。若是進程下線了,心跳包就中止了,那麼對應的地址信息也會過時,如此Dashboard 就能準實時知道當前的有效進程服務列表。當前版本開源的 Dashboard 不具有持久化能力,當管理員在後臺修改了規則時,它會直接經過 HTTP 健康服務地址來同步服務限流規則直接控制具體服務進程。若是應用重啓,規則將自動重置。若是你但願經過 Redis 來持久化規則源,那就須要本身定製 Dashboard。定製不難,實現它內置的持久化接口便可。
前面咱們說到分佈式限流須要另起一個 Ticket Server,由它來分發 Ticket,可以獲取到 Ticket 的請求才能夠容許執行臨界區代碼,Ticket 服務器也須要提供規則輸入源。
Ticket Server 是單點的,若是 Ticket Server 掛掉了,應用服務器限流將自動退化爲本地模式。Sentinel 保護的臨界區是代碼塊,經過拓展臨界區的邊界就能夠直接適配各類框架,好比 Dubbo、SpringBoot 、GRPC 和消息隊列等。每一種框架的適配器會在請求邊界處統必定義臨界區做用域,用戶就能夠徹底沒必要手工添加熔斷保護性代碼,在毫無感知的狀況下就自動植入了限流保護功能。
限流在於限制流量,也就是 QPS 或者線程的併發數,還有一種狀況是請求處理不穩定或者服務損壞,致使請求處理時間過長或者總是頻繁拋出異常,這時就須要對服務進行降級處理。所謂的降級處理和限流處理在形式上沒有明顯差別,也是以一樣的形式定義一個臨界區,區別是須要對拋出來的異常須要進行統計,這樣才能夠知道請求異常的頻率,有了這個指標纔會觸發降級。
// 定義降級規則
List<DegradeRule> rules = new ArrayList<>();
DegradeRule rule = new DegradeRule();
rule.setResource("tutorial");
// 5s內異常不得超出10
rule.setCount(10);
rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);
rule.setLimitApp("default");
rules.add(rule);
DegradeRuleManager.loadRules(rules);
Entry entry = null;
try {
entry = SphU.entry(key);
// 業務代碼在這裏
} catch (Throwable t) {
// 記錄異常
if (!BlockException.isBlockException(t)) {
Tracer.trace(t);
}
} finally {
if (entry != null) {
entry.exit();
}
}
複製代碼
觸發限流時會拋出 FlowException,觸發熔斷時會拋出 DegradeException,這兩個異常都繼承自 BlockException。
還有一種特殊的動態限流規則,用於限制動態的熱點資源。內部採用 LRU 算法計算出 topn 熱點資源,而後對 topn 的資源進行限流,同時還提供特殊資源特殊對待的參數設置。 好比在下面的例子中限定了同一個用戶的訪問頻次,同時也限定了同一本書的訪問頻次,可是對於某個特殊用戶和某個特殊的書進行了特殊的頻次設置。
ParamFlowRule ruleUser = new ParamFlowRule();
// 一樣的 userId QPS 不得超過 10
ruleUser.setParamIdx(0).setCount(10);
// qianwp用戶特殊對待,QPS 上限是 100
ParamFlowItem uitem = new ParamFlowItem("qianwp", 100, String.class);
ruleUser.setParamFlowItemList(Collections.singletonList(uitem));
ParamFlowRule ruleBook = new ParamFlowRule();
// 一樣的 bookId QPS 不得超過 20
ruleBook.setParamIdx(1).setCount(20);
// redis 的書特殊對待,QPS 上限是 100
ParamFlowItem bitem = new ParamFlowItem("redis", 100, String.class);
ruleBook.setParamFlowItemList(Collections.singletonList(item));
// 加載規則
List<ParamFlowRule> rules = new ArrayList<>();
rules.add(ruleUser);
rules.add(ruleBook);
ParamFlowRuleManager.loadRules(rules);
// userId的用戶訪問bookId的書
Entry entry = Sphu.entry(key, EntryType.IN, 1, userId, bookId);
複製代碼
熱點限流的難點在於如何統計定長滑動窗口時間內的熱點資源的訪問量,Sentinel 設計了一個特別的數據結構叫 LeapArray,內部有較爲複雜的算法設計後續須要單獨分析。
當系統的負載較高時,爲了不繫統被洪水般的請求沖垮,須要對當前的系統進行限流保護。保護的方式是逐步限制 QPS,觀察到系統負載恢復後,再逐漸放開 QPS,若是系統的負載又降低了,就再逐步下降 QPS。如此達到一種動態的平衡,這裏面涉及到一個特殊的保持平衡的算法。系統的負載指數存在一個問題,它取自操做系統負載的 load1 參數,load1 參數更新的實時性不足,從 load1 超標到恢復的過程存在一個較長的過渡時間,若是使用一刀切方案,在這段恢復時間內阻止任何請求,待 load1 恢復後又當即放開請求,勢必會致使負載的大起大落,服務處理的時斷時開。爲此做者將 TCP 擁塞控制算法的思想移植到這裏實現了系統平滑的過載保護功能。這個算法很精巧,代碼實現並不複雜,效果倒是很是顯著。
算法定義了一個穩態公式,穩態一旦打破,系統負載就會出現波動。算法的本質就是當穩態被打破時,經過持續調整相關參數來從新創建穩態。
穩態公式很簡單:ThreadNum * (1/ResponseTime) = QPS,這個公式很好理解,就是系統的 QPS 等於線程數乘以單個線程每秒能夠執行的請求數量。系統會實時採樣統計全部臨界區的 QPS 和 ResponseTime,就能夠計算出相應的穩態併發線程數。當負載超標時,經過斷定當前的線程數是否超出穩態線程數就能夠明確是否須要拒絕當前的請求。
定義自適應限流規則須要提供多個參數
List<SystemRule> rules = new ArrayList<SystemRule>();
SystemRule rule = new SystemRule();
rule.setHighestSystemLoad(3.0);
rule.setAvgRt(10);
rule.setQps(20);
rule.setMaxThread(10);
rules.add(rule);
SystemRuleManager.loadRules(Collections.singletonList(rule));
複製代碼
從代碼中也能夠看出系統自適應限流規則不須要定義資源名稱,由於它是全局的規則,會自動應用到全部的臨界區。若是當負載超標時,全部臨界區資源將一塊兒勒緊褲腰帶渡過難關。