今年下半年阿里開源了自研的限流系統 Sentinel,官方對 Sentinel 的介紹中用到了一系列高大山的名詞諸如 限流、熔斷降級、流量塑形、系統負載保護等,還有漂亮的形容詞諸如 輕巧、專業、實時等。作為技術消費者看到這樣的廣告詞之後禁不住要大聲感嘆 —— NiuB!更要不得的是 Sentinel 的釋出會由阿里的高階技術專家 子衿 主講,她是一位女性開發者,這在男性主導額 IT 產業也算得上難得一見的奇觀。
我花了一整天的時間仔細研究了 Sentinel 的功能和程式碼,大致摸清了整體的架構和區域性的一些技術細節,這裡給大家做一次全面的分享。
Sentinel 入門
首先,Sentinel 不算一個特別複雜的系統 ,普通技術開發者也可以輕鬆理解它的原理和結構。你別看架構圖上 Sentinel 的周邊是一系列的其它高大山的開源中介軟體,這不過是一種華麗的包裝,其核心 Sentinel Core 確實是非常輕巧的。
首先我們從它的 Hello World 開始,通過深入理解這段入門程式碼就可以洞悉其架構原理。
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.4.0</version>
</dependency>
複製程式碼
限流分為單機和分散式兩種,單機限流是指限定當前程式裡面的某個程式碼片段的 QPS 或者 併發執行緒數 或者 整個機器負載指數,一旦超出規則配置的數值就會丟擲異常或者返回 false。我把這裡的被限流的程式碼片段稱為「臨界區」。
而分散式則需要另啟一個集中的發票伺服器,這個伺服器針對每個指定的資源每秒只會生成一定量的票數,在執行臨界區的程式碼之前先去集中的發票服務領票,如果領成功了就可以執行,否則就會丟擲限流異常。所以分散式限流代價較高,需要多一次網路讀寫操作。如果讀者閱讀了我的小冊《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,就可以計算出相應的穩態併發執行緒數。當負載超標時,通過判定當前的執行緒數是否超出穩態執行緒數就可以明確是否需要拒絕當前的請求。
定義自適應限流規則需要提供多個引數
- 系統的負載水平線,超過這個值時觸發過載保護功能
- 當過載保護超標時,允許的最大執行緒數、最長響應時間和最大 QPS,可以不設定
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));
複製程式碼
從程式碼中也可以看出系統自適應限流規則不需要定義資源名稱,因為它是全域性的規則,會自動應用到所有的臨界區。如果當負載超標時,所有臨界區資源將一起勒緊褲腰帶渡過難關。