一、什麼是Sentinel
Sentinel,中文翻譯為哨兵,是為微服務提供流量控制、熔斷降級的功能,它和Hystrix提供的功能一樣,可以有效的解決微服務調用產生的“雪崩效應”,為微服務系統提供了穩定性的解決方案。隨著Hystrix進入了維護期,不再提供新功能,Sentinel是一個不錯的替代方案。通常情況下,Hystrix採用線程池對服務的調用進行隔離,Sentinel採用了用戶線程對接口進行隔離,二者相比,Hystrix是服務級別的隔離,Sentinel提供了接口級別的隔離,Sentinel隔離級別更加精細,另外Sentinel直接使用用戶線程提供限制,相比Hystrix的線程池隔離,減少了線程切換的開銷。另外Sentinel的DashBoard提供了在線更改限流規則的配置,也更加的優化。
二、開源生態
Sentinel和Hystrix
三、Sentinel特性
1、豐富的應用場景
Sentinel承接了阿里巴巴近十年雙十一大促流量的核心場景,例如秒殺(突發流量控制在系統容量可以承受的範圍)、消息削峰填谷、實時熔斷下游不可用應用等。
2、完備的實時監控
Sentinel同時提供實時的監控功能。我們可以在控制檯中看到接入應用的單臺機器秒級數據,甚至500臺以下規模的集群的彙總進行情況。
3、廣泛的開源生態
Sentinel提供開箱即用的與其它開源框架的整合模塊,例如與spring cloud、dubbo、grpc的整合。我們只需要引入響應的依賴並進行簡單的配置即可快速的接入Sentinel。
4、完美的SPI擴展點
Sentinel提供簡單易用、完善的SPI擴展點。我們可以通過實現擴展點,快速的定製邏輯。例如定製規則管理、適配數據源等。
四、資源和規則
資源是Sentinel的關鍵概念。它可以是java應用程序中的任何內容,例如,由應用程序提供的服務,或由應用程序調用的其它應用提供的服務,甚至可以是一段代碼。只是通過Sentinel API定義的代碼,就是資源,能夠被Sentinel保護起來,大部分情況下,可以使用方法簽名,URL,甚至服務名稱作為資源名來表示資源。
圍繞資源的實時狀態設定的規則,可以包括流量控制規則、熔斷降級規則以及系統保護規則。所有規則可以動態實時調整。
Sentinel中調用SphU或者SphO的entry方法獲取限流資源,不同的是前者獲取限流資源失敗時會跑BlockException異常,後者返回false,二者的實現都是基於CtSph類完成的。
五、核心概念
1、Resource
resource是Sentinel中最重要的一個概念,Sentinel通過資源來保護具體的業務代碼或其它後方服務。Sentinel把複雜的邏輯給屏蔽了,用戶只需要為受保護的代碼或服務定義一個資源,然後定義規則就可以了,剩下的通通交給Sentinel來處理。並且資源和規則是解耦的,規則甚至可以在運行時動態修改。定義完資源後,就可以通過在程序中埋點來保護你自己的服務,埋點的方式有兩種:
(1)try-catch方式(通過SphU.entry(...)),當catch到BlockException時執行異常處理或fallback。
(2)if-else方式(通過SphO.entry(...)),當返回false時執行異常處理或fallback。
以上兩種方式都是通過硬編碼的形式定義資源然後進行資源埋點的,對業務代碼的侵入太大,從0.1.1版本開始,Sentinel加入了註解的支持,可以通過註解來定義資源,具體的註解為:SentinelResource。通過註解除了可以定義資源外,還可以指定blockHandler和fallback方法。
在Sentinel中具體表示資源的類:ResourceWrapper,它是一個抽象的包裝類,包裝了資源的Name和EntryType。他有兩個實現類,分別是:StringResourceWrapper和MethodResourceWrapper。顧名思義,StringResourceWrapper是通過對一串字符進行包裝,是一個通用的資源包裝類,MethodResourceWrapper是對方法調用的包裝。
2、Context
Context是對資源操作時的上下文環境,每個資源操作(針對resource的entry和exit)必須屬於一個Context,如果程序中未指定Context,會創建name為“Sentinel_default_context”的默認Context。一個Context生命週期內可能有多個資源操作,Context生命週期內的最後一個資源exit時會清理該Context,這也預示著整個Context生命週期的結束。Context主要屬性如下:
<code>public class Context { // context名字,默認名字 "sentinel_default_context" private final String name; // context入口節點,每個context必須有一個entranceNode private DefaultNode entranceNode; // context當前entry,Context生命週期中可能有多個Entry,所有curEntry會有變化 private Entry curEntry; // The origin of this context (usually indicate different invokers, e.g. service consumer name or origin IP). private String origin = ""; private final boolean async;}/<code>
一個Context生命週期內Context只能初始化一次,存到ThreadLocal中,並且只有在非NULL時才會進行初始化。如果想在調用SphU.entry()或SphO.entry()前,自定義一個context,則通過ContextUtil.enter()方法來創建。context保存在ThreadLocal中,每次執行的時候會優先到ThreadLocal中獲取,為null時會創建一個context。當Entry執行exit方法時,如果entry的parent節點為null,表示當前context中最外層的entry了,此時將threadLocals中的context清空。
3、Entry
每次執行SphU.entry()或SphO.entry()都會返回一個Entry,Entry表示一次資源操作,內部會保存單籤invocation信息。在一個context聲明週期中多次資源操作,也就是對應多個Entry,parent/child結構保存在Entry實例中,Entry類CtEntry結構如下:
<code>class CtEntry extends Entry { protected Entry parent = null; protected Entry child = null; protected ProcessorSlot<object> chain; protected Context context;}public abstract class Entry implements AutoCloseable { private long createTime; private Node curNode; /** * {@link Node} of the specific origin, Usually the origin is the Service Consumer. */ private Node originNode; private Throwable error; // 是否出現異常 protected ResourceWrapper resourceWrapper; // 資源信息}/<object>/<code>
4、DefaultNode
Node默認實現類DefaultNode,該類還有一個子類EntranceNode;context有一個entranceNode屬性,Entry中有一個curNode屬性。
- EntranceNode:該類的創建在初始化context時完成的,注意該類是針對context維度的,也就是一個context有且僅有一個EntranceNode。
- DefaultNode:該類的創建是在NodeSelectorSlot.entry完成的,當不存在context.name對應的DefaultNode時會創建並保存在本地緩存;獲取到context.name對應的DefaultNode後將該DefaultNode設置到當前context的curEntry.curNode屬性,也就是說,在DefaultSelectorSlot中是一個context有且僅有一個DefaultNode。
看到這裡,你是不是有疑問?為什麼一個context有且僅有一個DefaultNode,我們的resouece跑哪去了呢,其實,這裡的一個context有且僅有一個DefaultNode是在NodeSelectorSlot範圍內,NodeSelectorSlot是ProcessorSlotChain中的一環,獲取ProcessorSlotChain是根據Resource維度來的。總結為一句話就是:針對同一個Resource,多個context對應多個DefaultNode;針對不同Resource,(不管是否是同一個context)對應多個不同DefaultNode。這還沒看明白 : ),好吧,我不bb了,上圖吧:
DefaultNode結構如下:
<code>public class DefaultNode extends StatisticNode { private ResourceWrapper id; /** * The list of all child nodes. * 子節點集合 */ private volatile Set<node> childList = new HashSet<>(); /** * Associated cluster node. */ private ClusterNode clusterNode;}/<node>/<code>
一個Resouce只有一個clusterNode,多個defaultNode對應一個clusterNode,如果defaultNode.clusterNode為null,則在ClusterBuilderSlot.entry中會進行初始化。 同一個Resource,對應同一個ProcessorSlotChain,這塊處理邏輯在lookProcessChain方法中,如下:
<code>ProcessorSlot<object> lookProcessChain(ResourceWrapper resourceWrapper) { ProcessorSlotChain chain = chainMap.get(resourceWrapper); if (chain == null) { synchronized (LOCK) { chain = chainMap.get(resourceWrapper); if (chain == null) { // Entry size limit. if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) { return null; } chain = SlotChainProvider.newSlotChain(); Map<resourcewrapper> newMap = newHashMap<resourcewrapper>( chainMap.size() + 1); newMap.putAll(chainMap); newMap.put(resourceWrapper, chain); chainMap = newMap; } } } return chain;}/<resourcewrapper>/<resourcewrapper>/<object>/<code>
5、StatisticNode
StatisticNode中保存了資源的實時統計數據(基於滑動時間窗口機制),通過這些統計數據,sentinel才能進行限流、降級等一系列操作。StatisticNode屬性如下:
<code>public class StatisticNode implements Node { /** * 秒級的滑動時間窗口(時間窗口單位500ms) */ private transient volatile Metric rollingCounterInSecond = newArrayMetric(SampleCountProperty.SAMPLE_COUNT, IntervalProperty.INTERVAL); /** * 分鐘級的滑動時間窗口(時間窗口單位1s) */ private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000,false); /** * The counter for thread count. * 線程個數用戶觸發線程數流控 */ private LongAdder curThreadNum = new LongAdder();}public class ArrayMetric implements Metric { private final LeapArray<metricbucket> data;}public class MetricBucket {// 保存統計值 private final LongAdder[] counters;// 最小rt private volatile long minRt;}/<metricbucket>/<code>
其中MetricBucket.counters數組大小為MetricEvent枚舉值的個數,每個枚舉對應一個統計項,比如PASS表示通過個數,限流可根據通過的個數和設置的限流規則配置count大小比較,得出是否觸發限流操作,所有枚舉值如下:
<code>public enum MetricEvent { PASS, // Normal pass. BLOCK, // Normal block. EXCEPTION, SUCCESS, RT, OCCUPIED_PASS}/<code>
6、Slot
Slot是sentinel中非常重要的概念,sentinel的工作流程就是圍繞著一個個插槽所組成的插槽鏈來展開的。需要注意的是每個插槽都有自己的職責,他們各司其職完美的配合,通過一定的編排順序,來達到最終的限流降級。默認的各個插槽之間的順序是固定的,因為有的插槽需要依賴其他的插槽計算出來的結果才能進行工作。
sentinel通過SlotChainBuilder作為SPI接口,使得Slot Chain具備了擴展的能力。我們可以通過實現SlotChainBuilder接口加入自定義Slot並且定義編排各個slot之間的排序,從而可以給sentinel添加自定義的功能。
那SlotChain是在哪創建的呢?是在 CtSph.lookProcessChain() 方法中創建的,並且該方法會根據當前請求的資源先去一個靜態的HashMap中獲取,如果獲取不到才會創建,創建後會保存到HashMap中。這就意味著,同一個資源會全局共享一個SlotChain。默認生成ProcessorSlotChain為:
<code>// DefaultSlotChainBuilderpublic ProcessorSlotChain build() { ProcessorSlotChain chain = new DefaultProcessorSlotChain(); chain.addLast(new NodeSelectorSlot()); chain.addLast(new ClusterBuilderSlot()); chain.addLast(new LogSlot()); chain.addLast(new StatisticSlot()); chain.addLast(new SystemSlot()); chain.addLast(new AuthoritySlot()); chain.addLast(new FlowSlot()); chain.addLast(new DegradeSlot()); return chain;/<code>
六、springcloud如何使用sentinel
學習了sentinel核心概念之後,感覺整個人都不好了,真的是晦澀難懂,來個helloworld,輕鬆一下。
1、pom.xml
<code><dependency> <groupid>org.springframework.cloud/<groupid> <artifactid>spring-cloud-starter-alibaba-sentinel/<artifactid>/<dependency>/<code>
2、 controller
<code>@RestControllerpublic class TestController { @GetMapping(value = "/hello") @SentinelResource("hello") public String hello() { return "Hello Sentinel"; }}/<code>
3、引入dashboard
直接下載sentinel-dashboard的jar包。
默認是8080端口,在瀏覽器輸入:localhost:8080,默認賬號密碼:sentinel:sentinel,看到控制檯界面為部署成功。
4、application.properties
<code>server.port=8088spring.application.name=spring-cloud-alibaba-sentinel-demo# sentinel dashboardspring.cloud.sentinel.transport.dashboard=localhost:8080/<code>
5、 啟動spring boot 項目,繼續訪問localhost:8080,會看到如下界面
6、 使用Sentinel實現接口限流(在控制檯)
7、測試
通過上面的配置,實現的是/hello接口qps最大是2,如果qps大於2,則快速失敗,配置完成,點擊保存,我們快速刷新瀏覽器,會發現快速失敗
七、總結
本文主要介紹了Sentinel的概念、特性、與Hystrix的區別、一些核心概念和與SpringCloud的簡單整合。隨著微服務的流行,服務和服務之間的穩定性變得越來越重要。 Sentinel 以流量為切入點,從流量控制、熔斷降級、系統負載保護等多個維度保護服務的穩定性。
閱讀更多 阿里Java架構師 的文章