掌门1对1微服务体系Solar|阿里巴巴Sentinel落地实践
前言
掌門1對1精耕在線教育領域,近幾年業務得到了快速發展,但同時也遭遇了“成長的煩惱”。隨著微服務數量不斷增加,流量進一步暴增,硬件資源有點不堪重負,那么,如何實現更好的限流熔斷降級等流量防護措施,這個課題就擺在了掌門人的面前。由于?Spring Cloud?體系已經演進到第二代,第一代的?Hystrix?限流熔斷降級組件已經不大適合現在的業務邏輯和規模,同時它目前被?Spring Cloud?官方置于維護模式,將不再向前發展。
如何選擇一個更好的限流熔斷降級組件?經過對?Alibaba Sentinel、Resilience4j、Hystrix?等開源組件做了深入的調研和比較,最終選定?Alibaba Sentinel?做微服務體系?Solar?中的限流熔斷降級必選組件。
Sentinel 簡介
阿里巴巴中間件部門開發的新一代以流量為切入點,從流量控制、熔斷降級、系統負載保護等多個維度保護服務的穩定性的分布式系統的流量防衛兵。它承接了阿里巴巴近10年的雙十一大促流量的核心場景,例如秒殺(即突發流量控制在系統容量可以承受的范圍)、消息削峰填谷、集群流量控制、實時熔斷下游不可用應用等。
它具有非常豐富的開源生態:
它和?Hystrix?相比,有如下差異:
摘自官網?Sentinel Roadmap
關于?Sentinel?如何使用,它的技術實現原理怎樣等,官方文檔或者民間博客、公眾號文章等可以提供非常詳盡且有價值的材料,這些不在本文的討論范圍內,就不一一贅述。筆者嘗試結合掌門1對1現有的技術棧以及中間件一體化的戰略,并著眼于強大的?Spring Cloud Alibaba?技術生態圈展開闡釋。
Sentinel 深度集成 Apollo
Sentinel?官方在?sentinel-datasource-apollo?模塊中已經對?Apollo?做了一些擴展,主要實現了?Sentinel?規則的讀取和訂閱邏輯。這些并不夠,我們需要對?Apollo?進行更深層次的集成。
摘自官網?在生產環境中使用 Sentinel
Solar SDK 環境初始化
定制?EnvironmentPostProcessor?類,實現如下:
- Sentinel Dashboard?的項目名稱從?Apollo?AppId?的維度進行展示
- 根據環境?env?值讀取相應的配置文件,并訪問對應環境的?Sentinel Dashboard?域名Sentinel Dashboard?在生產環境部署若干臺?ECS?實例,阿里云?SLB?做負載均衡,實現對集群的水平擴展
把?SentinelClientEnvironmentPostProcessor?類放置?\resources\META-INF\spring.factories?文件中,內容為
org.springframework.boot.env.EnvironmentPostProcessor=\ com.zhangmen.solar.component.sentinel.common.context.SentinelClientEnvironmentPostProcessor在?\resources\META-INF?目錄下,定制環境配置文件,文件名格式為?sentinel-{環境號}.properties?。下文以?dev?環境和?flow?流控配置(其它規則配置,請自行參考?Spring Cloud Alibaba Sentinel?的相關資料)為樣例。
sentinel-dev.properties
spring.cloud.sentinel.transport.dashboard=127.0.0.1:8080 spring.cloud.sentinel.datasource.ds.apollo.namespaceName=application spring.cloud.sentinel.datasource.ds.apollo.flowRulesKey=sentinel.flowRules spring.cloud.sentinel.datasource.ds.apollo.ruleType=flow ...Sentinel Dashboard 持久化改造
原生的?Sentinel Dashboard?在創建完規則后,規則內容保存在服務的內存中,當服務重啟后所有的規則內容都會消失。因此,在生產部署時需要考慮配置持久化,并且使用?Apollo?動態規則的感知能力。
① 向外暴露 Sentinel 規則的 Restful 接口
@RestController @RequestMapping(value = "/v2/flow") public class FlowControllerV2 {@Autowired@Qualifier("apolloFlowRuleProvider")private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;@Autowired@Qualifier("apolloFlowRulePublisher")private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;.... }② 實現 Sentinel Apollo 規則提供
@Component("apolloFlowRuleProvider") public class ApolloFlowRuleProvider extends BaseApolloRuleProvider<FlowRuleEntity> {@Overridepublic List<FlowRuleEntity> getRules(String appName) throws Exception {List<FlowRuleEntity> flowRuleEntityList = super.getRules(appName);if (!CollectionUtils.isEmpty(flowRuleEntityList)) {List<FlowRuleEntity> flowRuleEntities = JSONArray.parseArray(flowRuleEntityList.toString(), FlowRuleEntity.class);long id = 1;for (FlowRuleEntity entity : flowRuleEntities) {entity.setId(id++);entity.getClusterConfig().setFlowId(entity.getId());}return flowRuleEntities;} else {return null;}}@Overrideprotected String getDataId() {return ApolloConfigUtil.getFlowDataId();} }③ 實現 Sentinel Apollo 規則訂閱
@Component("apolloFlowRulePublisher") public class ApolloFlowRulePublisher extends BaseApolloRulePublisher<List<FlowRuleEntity>> {@Overridepublic void publish(String app, String operator, List<FlowRuleEntity> rules) throws Exception {if (!CollectionUtils.isEmpty(rules)) {for (int i = 0; i < rules.size(); i++) {rules.get(i).setId((long) (i + 1));rules.get(i).setApp(null);rules.get(i).setGmtModified(null);rules.get(i).setGmtCreate(null);rules.get(i).setIp(null);rules.get(i).setPort(null);rules.get(i).getClusterConfig().setFlowId((long) (i + 1));}} else {rules = null;}super.publish(app, operator, rules);}@Overrideprotected String getDataId() {return ApolloConfigUtil.getFlowDataId();} }上述代碼實現了對?Apollo?配置讀寫操作。熟悉?Apollo?的同學應該知道,這些操作需要基于?Apollo OpenApi?來操作;動態感知能力的邏輯已經由?sentinel-datasource-apollo?模塊實現。
Sentinel 集成 Skywalking
由于掌門1對1微服務技術棧落地的比較早,鑒于歷史的局限性(當時沒有更先進的技術可供選擇),除了 Hystrix 比較古老以外,另一個技術棧的痛點是全鏈路監控中間件的改造也提上議事日程,CAT 作為開源界老牌作品,為公司底層全鏈路監控提供強有力的保障,但隨著技術的演進,它逐漸已經不適合公司的未來發展方向,經過對比,最終選擇 Skywalking 將作為它的替代者(關于 Skywalking 的技術選型,將在后面掌門1對1微服務體系 Solar 的公眾號系列文章中會一一闡述)。
業務系統要求對限流熔斷降級實現全鏈路實時埋點,并希望在?Skywalking?界面上提供限流熔斷降級埋點的多維度統計。由于?Skywalking?實現了?OpenTracing?標準化協議,那么以?OpenTracing?為橋梁,通過?Solar?SDK 輸出?Sentinel?埋點到?Skywalking?Server 不失為一個好的技術選擇。下面簡單扼要介紹一下基于?Sentinel InitFunc?的?SPI?機制實現埋點輸出:
Sentinel?將?ProcessorSlot?作為?SPI?接口進行擴展(1.7.2 版本以前?SlotChainBuilder?作為?SPI?),使得?Slot Chain?具備了擴展的能力。您可以自行加入自定義的 slot 并編排 slot 間的順序,從而可以給?Sentinel?添加自定義的功能。
摘自官網?Sentinel 工作主流程
抽象 Sentinel ProcessorSlot 埋點輸出
Sentinel?的?ProcessorSlotEntryCallback?提供?onPass?和?onBlocked?兩個方法,畢竟限流熔斷降級并不是常規的功能,不會發生在大流量上面,所以?onPass?上我們不做任何處理,否則正常的調用去實現攔截,將為產生大量的埋點數據,會讓?Skywalking?Server 承受很大的性能壓力,所以?onBlocked?將是我們關注的重點,它除了輸出?Sentinel?本身的上下文參數之外,也會輸出微服務?Solar??指標參數,主要包括:
- 埋點?Span名稱,這里為?SENTINEL?,在?Skywalking?全鏈路監控界面中,用戶可以非常容易的找到這個埋點
- 服務所在的組名,指服務的邏輯分組
- 服務類型,包括服務和網關(網關也是一種特殊的服務),?Sentinel?埋點可以支持在服務和網關上的輸出
- 服務的?APPID,它為?Apollo?組件的范疇概念
- 服務名,它對應為?spring.application.name?的配置值
- 服務實例所在的?IP?地址和?Port?端口
- 服務版本號
- 服務所在的區域
- 服務所在的子環境
接下去是?Sentinel?層面的參數,請自行參考?Sentinel?官方文檔和源碼,了解其含義,這里不做具體講解。
public abstract class SentinelTracerProcessorSlotEntryCallback<S> implements ProcessorSlotEntryCallback<DefaultNode> {@Overridepublic void onPass(Context context, ResourceWrapper resourceWrapper, DefaultNode param, int count, Object... args) throws Exception {}@Overridepublic void onBlocked(BlockException e, Context context, ResourceWrapper resourceWrapper, DefaultNode param, int count, Object... args) {S span = buildSpan();PluginAdapter pluginAdapter = PluginContextAware.getStaticApplicationContext().getBean(PluginAdapter.class);outputSpan(span, DiscoveryConstant.SPAN_TAG_PLUGIN_NAME, context.getName());outputSpan(span, DiscoveryConstant.N_D_SERVICE_GROUP, pluginAdapter.getGroup());outputSpan(span, DiscoveryConstant.N_D_SERVICE_TYPE, pluginAdapter.getServiceType());String serviceAppId = pluginAdapter.getServiceAppId();if (StringUtils.isNotEmpty(serviceAppId)) {outputSpan(span, DiscoveryConstant.N_D_SERVICE_APP_ID, serviceAppId);}outputSpan(span, DiscoveryConstant.N_D_SERVICE_ID, pluginAdapter.getServiceId());outputSpan(span, DiscoveryConstant.N_D_SERVICE_ADDRESS, pluginAdapter.getHost() + ":" + pluginAdapter.getPort());outputSpan(span, DiscoveryConstant.N_D_SERVICE_VERSION, pluginAdapter.getVersion());outputSpan(span, DiscoveryConstant.N_D_SERVICE_REGION, pluginAdapter.getRegion());outputSpan(span, DiscoveryConstant.N_D_SERVICE_ENVIRONMENT, pluginAdapter.getEnvironment());outputSpan(span, SentinelStrategyConstant.ORIGIN, context.getOrigin());outputSpan(span, SentinelStrategyConstant.ASYNC, String.valueOf(context.isAsync()));outputSpan(span, SentinelStrategyConstant.RESOURCE_NAME, resourceWrapper.getName());outputSpan(span, SentinelStrategyConstant.RESOURCE_SHOW_NAME, resourceWrapper.getShowName());outputSpan(span, SentinelStrategyConstant.RESOURCE_TYPE, String.valueOf(resourceWrapper.getResourceType()));outputSpan(span, SentinelStrategyConstant.ENTRY_TYPE, resourceWrapper.getEntryType().toString());outputSpan(span, SentinelStrategyConstant.RULE_LIMIT_APP, e.getRuleLimitApp());if (tracerSentinelRuleOutputEnabled) {outputSpan(span, SentinelStrategyConstant.RULE, e.getRule().toString());}outputSpan(span, SentinelStrategyConstant.CAUSE, e.getClass().getName());outputSpan(span, SentinelStrategyConstant.BLOCK_EXCEPTION, e.getMessage());outputSpan(span, SentinelStrategyConstant.COUNT, String.valueOf(count));if (tracerSentinelArgsOutputEnabled) {outputSpan(span, SentinelStrategyConstant.ARGS, JSON.toJSONString(args));}finishSpan(span);}protected abstract S buildSpan();protected abstract void outputSpan(S span, String key, String value);protected abstract void finishSpan(S span); }整合 OpenTracing & Skywalking
實現?SentinelTracerProcessorSlotEntryCallback?的三個核心方法:
- buildSpan?- 創建?Skywalking?的埋點?Span?對象
- outputSpan?- 輸出相關埋點數據的鍵值對到?Skywalking?的埋點?Span?對象中
- finishSpan?- 提交?Skywalking?的埋點?Span?對象到?Skywalking?Server
實現 Sentinel InitFunc SPI 擴展
實現?SPI?的擴展切入類
public class SentinelSkywalkingTracerInitFunc implements InitFunc {@Overridepublic void init() throws Exception {StatisticSlotCallbackRegistry.addEntryCallback(SentinelSkywalkingTracerProcessorSlotEntryCallback.class.getName(), new SentinelSkywalkingTracerProcessorSlotEntryCallback());} }把?SPI?的擴展切入類放置?\resources\META-INF\services\com.alibaba.csp.sentinel.init.InitFunc?文件中,內容為
com.nepxion.discovery.plugin.strategy.sentinel.skywalking.monitor.SentinelSkywalkingTracerInitFunc摘自?Nepxion Discovery 開源社區
對于 Sentinel 跟 Opentracing, Skywalking, Jaeger 的集成可參考?https://github.com/Nepxion/Discovery?中的 discovery-plugin-strategy-sentinel-starter-opentracing, discovery-plugin-strategy-sentinel-starter-skywalking 等模塊。
最終在?Skywalking?全鏈路界面上輸出如下:
全鏈路調用鏈中,我們可以看到?solar-service-a?服務的鏈路上輸出了?SENTINEL?埋點,表示?solar-service-a?上發生了?Sentinel?限流熔斷降級事件之一。
點擊?SENTINEL?埋點,在呼出的內容看板上,我們可以看到?solar-service-a?服務發生了限流事件,上面顯示限流的規則和異常信息以及微服務?Solar??指標等一系列參數。
我們可以點擊界面上邊的【熔斷查詢】進行?Sentinel?相關數據的分析和統計
Sentinel 集成 InfluxDB & Grafana
監控數據持久化到 InfluxDB
① Sentinel MetricFetcher 拉取數據
實現?Dashboard?服務端拉取?Sentinel?客戶端(即?Solar?微服務)的監控數據
@Component public class MetricFetcher {@Autowired@Qualifier("influxDBMetricRepository")private MetricsRepository<MetricEntity> metricStore;... }② InfluxDB 實例初始化
@Configuration public class InfluxDBAutoConfiguration {@Value("${spring.influx.url}")private String influxDBUrl;@Value("${spring.influx.user}")private String userName;@Value("${spring.influx.password}")private String password;@Value("${spring.influx.database}")private String database;@Beanpublic InfluxDB influxDB() {InfluxDB influxDB = null;try {influxDB = InfluxDBFactory.connect(influxDBUrl, userName, password);influxDB.setDatabase(database).enableBatch(100, 1000, TimeUnit.MILLISECONDS);influxDB.setLogLevel(InfluxDB.LogLevel.NONE);} catch (Exception e) {LOG.error(e.getMessage());}return influxDB;} }③ Sentinel 數據寫入到 InfluxDB
@Component("influxDBMetricRepository") public class InfluxDBMetricRepository implements MetricsRepository<MetricEntity> {@Autowiredprivate InfluxDB influxDB;@Overridepublic void save(MetricEntity metric) {try {Point point = createPoint(metric);influxDB.write(point);} catch (Exception e) {LOG.error(e.getMessage());}}@Overridepublic void saveAll(Iterable<MetricEntity> metrics) {if (metrics == null) {return;}try {BatchPoints batchPoints = BatchPoints.builder().build();metrics.forEach(metric -> {Point point = createPoint(metric);batchPoints.point(point);});influxDB.write(batchPoints);} catch (Exception e) {LOG.error(e.getMessage());}} }Grafana 界面展現監控數據
Sentinel Limit-App 熔斷擴展
掌門1對1已經實現通過灰度藍綠發布方式,實現對流量的精確制導和調撥,但為了進一步實施更安全的流量保障,引入了基礎指標和灰度藍綠發布指標的熔斷,同時也支持業務自定義指標和組合指標的熔斷。
通過對?Sentinel?Limit-App機制的擴展并定制授權規則,實現微服務?Solar?的熔斷擴展。對于授權規則中涉及到的參數,簡要做如下說明:
- resource?為?@SentinelResource?注解的?value?,也可以是調用的?URL?路徑值
- limitApp?如果有多個,可以通過?,?分隔。特別注意,下文為了描述簡單,只以單個為例
- strategy?為?0?表示白名單,符合條件就放行流量;?strategy?為?1?表示黑名單,符合條件就限制流量。特別注意,下文為了描述簡單,只以白名單為例
基礎指標的熔斷
通過?Http Header?自動攜帶下游服務的基礎指標進行全鏈路傳遞的方式,對下游調用實施基礎指標的熔斷。支持如下指標:
① 服務名
當 A 服務發送請求到 B 服務,所攜帶的 A 服務名不滿足條件,該請求就會被 B 服務熔斷。
- B 服務增加配置項
- B 服務增加授權規則,?limitApp?為 A 服務名
② 服務的?APPID
當 A 服務發送請求到 B 服務,所攜帶的 A 服務的?APPID?不滿足條件,該請求就會被 B 服務熔斷。
- B 服務增加配置項
- B 服務增加授權規則,?limitApp?為 A 服務的?APPID
③ 服務實例所在的?IP?地址和?Port?端口
當 A 服務發送請求到 B 服務,所攜帶的 A 服務的?IP?地址和?Port?端口不滿足條件,該請求就會被 B 服務熔斷。
- B 服務增加配置項
- B 服務增加授權規則,?limitApp?為 A 服務實例所在的?IP?地址和?Port?端口
灰度藍綠發布指標的熔斷
通過?Http Header?自動攜帶下游服務的灰度藍綠發布指標進行全鏈路傳遞的方式,對下游調用實施灰度藍綠發布指標的熔斷。支持如下指標:
① 服務所在的組名
當 A 服務發送請求到 B 服務,所攜帶的 A 服務的組名和 B 服務的組名不一致,該請求就會被 B 服務熔斷。
- B 服務增加配置項
- B 服務增加授權規則,?limitApp?為 B 服務的組名
② 服務版本號
當 A 服務發送請求到 B 服務,所攜帶的 A 服務的版本號和 B 服務的版本號不一致,該請求就會被 B 服務熔斷。
- B 服務增加配置項
- B 服務增加授權規則,?limitApp?為 B 服務的版本號
③ 服務所在的區域
當 A 服務發送請求到 B 服務,所攜帶的 A 服務的區域值和 B 服務的區域值不一致,該請求就會被 B 服務熔斷。
- B 服務增加配置項
- B 服務增加授權規則,?limitApp?為 B 服務的區域值
④ 服務所在的子環境
當 A 服務發送請求到 B 服務,所攜帶的 A 服務的子環境值和 B 服務的子環境值不一致,該請求就會被 B 服務熔斷。
- B 服務增加配置項
- B 服務增加授權規則,?limitApp?為 B 服務的子環境值
業務自定義指標的熔斷
通過?Http Header?攜帶下游服務的業務自定義指標進行全鏈路傳遞的方式,對下游調用實施自定義指標的熔斷。
當 A 服務發送請求到 B 服務,所攜帶的 A 的自定義指標不滿足條件,該請求就會被 B 服務熔斷。例如: A 服務把?userName?通過?Http Header?傳遞給 B 服務,而 B 服務只接受?userName?為?zhangsan?的請求,那么我們可以通過如下方式來解決:
- B 服務通過適配類實現?Sentinel?Origin?值的解析
- B 服務的配置類里通過?@Bean?方式進行適配類創建
- B 服務增加授權規則,?limitApp?為?zhangsan
假如該方式仍未能滿足業務場景,業務系統希望根據?userName?獲取?userType,根據用戶類型做統一熔斷,例如,用戶類型為?AUTH_USER?的請求才能放行,其它都熔斷,那么我們可以把上面的例子修改如下:
- B 服務的適配類更改如下:
- B 服務的授權規則更改如下:
組合指標的熔斷
通過?Http Header?攜帶下游服務的業務自定義指標、基礎指標或者灰度藍綠發布指標進行全鏈路傳遞的方式,對下游調用實施組合指標的熔斷,例如,根據傳入的微服務版本號 + 用戶名,組合在一起進行熔斷。下面示例表示為下游服務版本為?1.0?且?userName?為?zhangsan,同時滿足這兩個條件下,所有服務的請求允許被放行,否則被熔斷。
- B 服務的適配類更改如下:
- B 服務的授權規則更改如下:
Sentinel 網關流控實踐
闡述網關流控實踐的時候,我們使用精確匹配的方式對某個服務的請求做限流控制為例;對網關代理的?solar-service-a?服務的接口?/inspector/inspect?做限流控制為例。
API 分組管理
API?管理頁面里添加?solar-service-a, 并精確匹配串?/inspector/inspect
網關流控規則
在流控規則界面里配置相關的規則
最終在?Skywalking?全鏈路界面上輸出如下(跟?Solar?服務側?Sentinel?埋點相似,不一一闡述了):
Sentinel 集群限流實踐
我們采用?Sentinel?官方提供的嵌入式?Token Server?解決方案,即服務集群中選擇一個節點做為?Token Server?,同時該節點也作為?Token Client?響應外部的請求的服務器。具體實現方式通過?Sentinel?實現預留的?SPI?InitFunc?接口,可以參考官方?sentinel-demo?模塊下面的?sentinel-demo-cluster-embedded?。
public class SentinelApolloTokenClusterInitFunc implements InitFunc {@Overridepublic void init() throws Exception {// Register client dynamic rule data source.initDynamicFlowRuleProperty();initDynamicParamRuleProperty();// Register token client related data source.// Token client common config:ClusterClientConfigInitializer.doInit();// Token client assign config (e.g. target token server) retrieved from assign map:ClusterClientAssignConfigInitializer.doInit();// Register token server related data source.// Register dynamic rule data source supplier for token server:ClusterRuleSupplierInitializer.doInit();// Token server transport config extracted from assign map:ServerTransportConfigInitializer.doInit();// Init cluster state property for extracting mode from cluster map data source.ClusterStateInitializer.doInit();// ServerFlowConfig 配置ServerFlowConfigInitializer.doInit();} }把?SPI?的擴展切入類放置?\resources\META-INF\services\com.alibaba.csp.sentinel.init.InitFunc?文件中,內容為
com.zhangmen.solar.sentinel.SentinelApolloTokenClusterInitFunc作者介紹
任浩軍,掌門基礎架構部研發經理。曾就職于平安銀行、萬達、惠普,曾負責平安銀行平臺架構部?PaaS?平臺基礎服務框架研發。10 多年開源經歷,Github?ID:@HaojunRen,Nepxion?開源社區創始人,Nacos?Group Member,Spring Cloud Alibaba?&?Nacos?&?Sentinel?&?OpenTracing?Committer。
張彬彬,掌門基礎架構部架構師。主要負責公司微服務架構以及開源項目的開發和實踐,開源項目愛好者,多年互聯網開發經驗。
非常感謝阿里巴巴?Sentinel?項目負責人宿何在落地過程中的支持和幫助。
原文鏈接
本文為云棲社區原創內容,未經允許不得轉載。
總結
以上是生活随笔為你收集整理的掌门1对1微服务体系Solar|阿里巴巴Sentinel落地实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 性能提升约7倍!Apache Flink
- 下一篇: SLS多云日志采集、处理及分析