Sentinel規則持久化改造(pull+push+Ahas)
每天多學一點點~
話不多說,這就開始吧…
文章目錄
- Sentinel規則持久化改造(pull+push+Ahas)
- 1.前言
- 2. 官網參考
- 3. 原生模式
- 4. PULL 拉模式
- 5. push 推模式
- 6. 阿裡雲Ahas
- 7.結語
1.前言
之前學習過阿裡的sentinel元件,但是重新開機之後,配置的規則都消失了。于是乎上網找了點資料,自己也來對sentinel持久化改造一下,這裡記錄一下過程。
以sentinel 1.7.0 版本 為例
sentinel git位址
2. 官網參考
在生産環境中使用 Sentinel
官網上提供了三種方式。根據官網提供資料,一個個來
3. 原生模式
Dashboard的推送規則方式是通過 API 将規則推送至用戶端并直接更新到記憶體
優缺點:這種做法的好處是簡單,無依賴;壞處是應用重新開機規則就會消失,僅用
于簡單測試,不能用于生産環境
4. PULL 拉模式
首先 Sentinel 控制台通過 API 将規則推送至用戶端并更新到記憶體中,接着注冊
的寫資料源會将新的規則儲存到本地的檔案中。使用 pull 模式的資料源時一般
不需要對 Sentinel 控制台進行改造。
這種實作方法好處是簡單,不引入新的依賴,壞處是無法保證監控資料的一緻性。
(定時功能周期預設3s)
代碼改造
-
通過SPI擴充機制進行擴充,寫一個拉模式的實作類
在微服務共工程 resources 目錄下 建立
在 工程下建立META-INF 下建立services目錄
檔案名稱是 com.alibaba.csp.sentinel.init.InitFunc
内容 com.zjq.persistence.PullModeByFileDataSource # 指向自己的類
-
PullModeByFileDataSource 類,實作InitFunc 接口
這裡就是和官網上的例子差不多,拿過來改一改
這裡以 流控規 為例
PullModeByFileDataSource 類
@Slf4j
public class PullModeByFileDataSource implements InitFunc {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
@Override
public void init() throws Exception {
log.info("time:{}讀取配置",sdf.format(new Date()));
try {
//建立檔案存儲目錄(若路徑不存在就建立路徑)
RuleFileUtils.mkdirIfNotExits(PersistenceRuleConstant.storePath);
//建立規則檔案()
RuleFileUtils.createFileIfNotExits(PersistenceRuleConstant.rulesMap);
// 處理流控規則邏輯
dealFlowRules();
// 處理降級規則
dealDegradeRules();
// 處理系統規則
dealSystemRules();
// 熱點參數規則
dealParamFlowRules();
dealAuthRules();
}catch (Exception e) {
log.error("錯誤原因:{}",e);
}
}
/**
* 方法實作說明:處理流控規則邏輯
* @author:zjq
* @return: void
* @exception: FileNotFoundException
* @date:2020/05/3013:26
*/
private void dealFlowRules() throws FileNotFoundException {
String ruleFilePath = PersistenceRuleConstant.rulesMap.get(PersistenceRuleConstant.FLOW_RULE_PATH).toString();
//建立流控規則的可讀資料源
ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new FileRefreshableDataSource(
ruleFilePath,RuleListParserUtils.flowRuleListParser
);
// 将可讀資料源注冊至FlowRuleManager 這樣當規則檔案發生變化時,就會更新規則到記憶體
FlowRuleManager.register2Property(flowRuleRDS.getProperty());
WritableDataSource<List<FlowRule>> flowRuleWDS = new FileWritableDataSource<List<FlowRule>>(
ruleFilePath, RuleListParserUtils.flowFuleEnCoding
);
// 将可寫資料源注冊至 transport 子產品的 WritableDataSourceRegistry 中.
// 這樣收到控制台推送的規則時,Sentinel 會先更新到記憶體,然後将規則寫入到檔案中.
WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);
}
。。。。。 其他 規則差不多
}
PersistenceRuleConstant Sentinel 規則持久化 常量配置類
public class PersistenceRuleConstant {
/**
* 存儲檔案路徑
*/
public static final String storePath = System.getProperty("user.home")+"\\sentinel\\rules\\";
/**
* 各種存儲sentinel規則映射map
*/
public static final Map rulesMap = new HashMap<String,String>();
//流控規則檔案
public static final String FLOW_RULE_PATH = "flowRulePath";
//降級規則檔案
public static final String DEGRAGE_RULE_PATH = "degradeRulePath";
//授權規則檔案
public static final String AUTH_RULE_PATH = "authRulePath";
//系統規則檔案
public static final String SYSTEM_RULE_PATH = "systemRulePath";
//熱點參數檔案
public static final String HOT_PARAM_RULE = "hotParamRulePath";
static {
rulesMap.put(FLOW_RULE_PATH,storePath+"flowRule.json");
rulesMap.put(DEGRAGE_RULE_PATH,storePath+"degradeRule.json");
rulesMap.put(SYSTEM_RULE_PATH,storePath+"systemRule.json");
rulesMap.put(AUTH_RULE_PATH,storePath+"authRule.json");
rulesMap.put(HOT_PARAM_RULE,storePath+"hotParamRule.json");
}
}
RuleFileUtils sentinel 規則檔案存儲工具類
@Slf4j
public class RuleFileUtils {
/**
* 方法實作說明:若路徑不存在就建立路徑
* @author:zjq
* @param filePath:檔案存儲路徑
* @return: void
* @exception: IOException
* @date:2020/05/3016:32
*/
public static void mkdirIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if(!file.exists()) {
log.info("建立Sentinel規則目錄:{}",filePath);
file.mkdirs();
}
}
/**
* 方法實作說明:若檔案不存在就建立路徑
* @author:zjq
* @param ruleFileMap 規則存儲檔案
* @return: void
* @exception: IOException
* @date:2020/05/3016:32
*/
public static void createFileIfNotExits(Map<String,String> ruleFileMap) throws IOException {
Set<String> ruleFilePathSet = ruleFileMap.keySet();
Iterator<String> ruleFilePathIter = ruleFilePathSet.iterator();
while (ruleFilePathIter.hasNext()) {
String ruleFilePathKey = ruleFilePathIter.next();
String ruleFilePath = PersistenceRuleConstant.rulesMap.get(ruleFilePathKey).toString();
File ruleFile = new File(ruleFilePath);
if(ruleFile.exists()) {
log.info("建立Sentinel 規則檔案:{}",ruleFile);
ruleFile.createNewFile();
}
}
}
}
RuleListParserUtils 規則清單解析工具類
public class RuleListParserUtils {
/**
* 流控清單解析器
*/
public static final Converter<String, List<FlowRule>> flowRuleListParser = new Converter<String, List<FlowRule>>() {
@Nullable
@Override
public List<FlowRule> convert(String source) {
return JSON.parseObject(source, new TypeReference<List<FlowRule>>() {});
}
};
/**
* 流控清單 編碼器
*/
public static final Converter<List<FlowRule>,String> flowFuleEnCoding= new Converter<List<FlowRule>,String>() {
@Nullable
@Override
public String convert(List<FlowRule> source) {
return JSON.toJSONString(source);
}
};
...... 其他同理
}
這樣,會在我們的 配置的路徑地下生成json檔案
json檔案
[{
"clusterConfig": {
"fallbackToLocalWhenFail": true,
"sampleCount": 10,
"strategy": 0,
"thresholdType": 0,
"windowIntervalMs": 1000
},
"clusterMode": false,
"controlBehavior": 0,
"count": 1.0,
"grade": 1,
"limitApp": "default",
"maxQueueingTimeMs": 500,
"resource": "/selectOrderInfoById/1",
"strategy": 0,
"warmUpPeriodSec": 10
}]
這樣,pull模式的持久化改造就完成了。
但是也如上文說的,pull 模式的自身缺點 :因為有定時任務 ,若 時間太短,伺服器受不了;時間太長,有延遲
5. push 推模式
這邊已nacos為例,也是官網推薦的 生産環境 使用
大體原理如下:
-
控制台推送規則:
将規則推送到Nacos或其他遠端配置中心。Sentinel用戶端連結Nacos,擷取規則配置;并監聽Nacos配置變化,如發生變化,就更新本地緩存(進而讓本地緩存總是和Nacos一緻)
- 控制台監聽Nacos配置變化,如發生變化就更新本地緩存(進而讓控制台本地緩存總是和Nacos一緻)
這裡用sentinel1.7.0 版本為例
代碼改造:
- 去掉 sentinel-dashboard 工程的pom中 test
-
控制台源碼改造
DynamicRuleProvider:從Nacos上讀取配置
DynamicRulePublisher:将規則推送到Nacos上
在sentinel-dashboard工程目錄com.alibaba.csp.sentinel.dashboard.rule 下建立一個Nacos的包,
然後把我們的各個場景的配置規則類寫道該包下,如下圖
2.1 将test下的NacosConfig 複制到 nacos包下 ,寫入我們自己的nacos位址
其實源碼中的 com.alibaba.csp.sentinel.dashboard.rule.nacos 目錄下 已經幫我們寫了示例,copy一下改一改就行。
NacosConfig 類
/**
* @author Eric Zhao
* @since 1.4.0
*/
@Configuration
public class NacosConfig {
/**
* 流控規則
* @return
*/
@Bean
public Converter<List<FlowRuleEntity>, String> flowRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter<String, List<FlowRuleEntity>> flowRuleEntityDecoder() {
return s -> JSON.parseArray(s, FlowRuleEntity.class);
}
/**
* 授權規則
* @return
*/
@Bean
public Converter<List<AuthorityRuleEntity>, String> authorRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter<String, List<AuthorityRuleEntity>> authorRuleEntityDecoder() {
return s -> JSON.parseArray(s, AuthorityRuleEntity.class);
}
/**
* 降級規則
* @return
*/
@Bean
public Converter<List<DegradeRuleEntity>, String> degradeRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter<String, List<DegradeRuleEntity>> degradeRuleEntityDecoder() {
return s -> JSON.parseArray(s, DegradeRuleEntity.class);
}
/**
* 熱點參數 規則
* @return
*/
@Bean
public Converter<List<ParamFlowRuleEntity>, String> paramRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter<String, List<ParamFlowRuleEntity>> paramRuleEntityDecoder() {
return s -> JSON.parseArray(s, ParamFlowRuleEntity.class);
}
/**
* 系統規則
* @return
*/
@Bean
public Converter<List<SystemRuleEntity>, String> systemRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter<String, List<SystemRuleEntity>> systemRuleEntityDecoder() {
return s -> JSON.parseArray(s, SystemRuleEntity.class);
}
/**
* 網關API
* @return
* @throws Exception
*/
@Bean
public Converter<List<ApiDefinitionEntity>,String> apiDefinitionEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter<String , List<ApiDefinitionEntity>> apiDefinitionEntityDecoder(){
return s -> JSON.parseArray(s,ApiDefinitionEntity.class);
}
/**
* 網關flowRule
* @return
* @throws Exception
*/
@Bean
public Converter<List<GatewayFlowRuleEntity>,String> gatewayFlowRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter<String , List<GatewayFlowRuleEntity>> gatewayFlowRuleEntityDecoder(){
return s -> JSON.parseArray(s,GatewayFlowRuleEntity.class);
}
@Bean
public ConfigService nacosConfigService() throws Exception {
return ConfigFactory.createConfigService("47.111.191.111");
}
}
2.2 以FlowRule為例,配置Publisher和Provider
将test下的FlowRuleNacosPublisher 和 FlowRuleNacosProvider 複制到 nacos 包下
2.3 修改流控規則 FlowControllerV1
2.3.1 添加我們剛才複制來的 Publisher 和 Provider
//添加我們自己寫的ruleProvider
@Autowired
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
//添加我們自己寫的 publisher
@Autowired
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
2.3.2 新增推送到nacos的方法
/**
* 原方法
* @param app
* @param ip
* @param port
* @return
*/
private boolean publishRules(String app, String ip, Integer port) {
List<FlowRuleEntity> rules = repository.findAllByMachine(MachineInfo.of(app, ip, port));
return sentinelApiClient.setFlowRuleOfMachine(app, ip, port, rules);
}
/**
* 我們自己的publishRules ,将規則推送到 nacos
* @param app
* @throws Exception
*/
private void publishRules(String app) throws Exception {
List<FlowRuleEntity> rules = repository.findAllByApp(app);
rulePublisher.publish(app,rules);
}
2.3.4 修改FlowControllerV1
1 将原來的 List rules = sentinelApiClient.fetchFlowRuleOfMachine(app, ip, port); 換成從nacos中擷取
2 将 儲存後的會澤推送到nacos 即 publishRules(entity.getApp());
@RestController
@RequestMapping(value = "/v1/flow")
public class FlowControllerV1 {
private final Logger logger = LoggerFactory.getLogger(FlowControllerV1.class);
@Autowired
private InMemoryRuleRepositoryAdapter<FlowRuleEntity> repository;
@Autowired
private AuthService<HttpServletRequest> authService;
//添加我們自己寫的ruleProvider
@Autowired
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
//添加我們自己寫的 publisher
@Autowired
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
@Autowired
private SentinelApiClient sentinelApiClient;
@GetMapping("/rules")
public Result<List<FlowRuleEntity>> apiQueryMachineRules(HttpServletRequest request,
@RequestParam String app,
@RequestParam String ip,
@RequestParam Integer port) {
AuthUser authUser = authService.getAuthUser(request);
authUser.authTarget(app, PrivilegeType.READ_RULE);
if (StringUtil.isEmpty(app)) {
return Result.ofFail(-1, "app can't be null or empty");
}
if (StringUtil.isEmpty(ip)) {
return Result.ofFail(-1, "ip can't be null or empty");
}
if (port == null) {
return Result.ofFail(-1, "port can't be null");
}
try {
//List<FlowRuleEntity> rules = sentinelApiClient.fetchFlowRuleOfMachine(app, ip, port);
// 去nacos上擷取配置規則
List<FlowRuleEntity> rules = ruleProvider.getRules(app);
rules = repository.saveAll(rules);
return Result.ofSuccess(rules);
} catch (Throwable throwable) {
logger.error("Error when querying flow rules", throwable);
return Result.ofThrowable(-1, throwable);
}
}
private <R> Result<R> checkEntityInternal(FlowRuleEntity entity) {
if (StringUtil.isBlank(entity.getApp())) {
return Result.ofFail(-1, "app can't be null or empty");
}
if (StringUtil.isBlank(entity.getIp())) {
return Result.ofFail(-1, "ip can't be null or empty");
}
if (entity.getPort() == null) {
return Result.ofFail(-1, "port can't be null");
}
if (StringUtil.isBlank(entity.getLimitApp())) {
return Result.ofFail(-1, "limitApp can't be null or empty");
}
if (StringUtil.isBlank(entity.getResource())) {
return Result.ofFail(-1, "resource can't be null or empty");
}
if (entity.getGrade() == null) {
return Result.ofFail(-1, "grade can't be null");
}
if (entity.getGrade() != 0 && entity.getGrade() != 1) {
return Result.ofFail(-1, "grade must be 0 or 1, but " + entity.getGrade() + " got");
}
if (entity.getCount() == null || entity.getCount() < 0) {
return Result.ofFail(-1, "count should be at lease zero");
}
if (entity.getStrategy() == null) {
return Result.ofFail(-1, "strategy can't be null");
}
if (entity.getStrategy() != 0 && StringUtil.isBlank(entity.getRefResource())) {
return Result.ofFail(-1, "refResource can't be null or empty when strategy!=0");
}
if (entity.getControlBehavior() == null) {
return Result.ofFail(-1, "controlBehavior can't be null");
}
int controlBehavior = entity.getControlBehavior();
if (controlBehavior == 1 && entity.getWarmUpPeriodSec() == null) {
return Result.ofFail(-1, "warmUpPeriodSec can't be null when controlBehavior==1");
}
if (controlBehavior == 2 && entity.getMaxQueueingTimeMs() == null) {
return Result.ofFail(-1, "maxQueueingTimeMs can't be null when controlBehavior==2");
}
if (entity.isClusterMode() && entity.getClusterConfig() == null) {
return Result.ofFail(-1, "cluster config should be valid");
}
return null;
}
@PostMapping("/rule")
public Result<FlowRuleEntity> apiAddFlowRule(HttpServletRequest request, @RequestBody FlowRuleEntity entity) {
AuthUser authUser = authService.getAuthUser(request);
authUser.authTarget(entity.getApp(), PrivilegeType.WRITE_RULE);
Result<FlowRuleEntity> checkResult = checkEntityInternal(entity);
if (checkResult != null) {
return checkResult;
}
entity.setId(null);
Date date = new Date();
entity.setGmtCreate(date);
entity.setGmtModified(date);
entity.setLimitApp(entity.getLimitApp().trim());
entity.setResource(entity.getResource().trim());
try {
entity = repository.save(entity);
//推送規則并儲存
publishRules(entity.getApp());
} catch (Throwable throwable) {
logger.error("Failed to add flow rule", throwable);
return Result.ofThrowable(-1, throwable);
}
if (!publishRules(entity.getApp(), entity.getIp(), entity.getPort())) {
logger.error("Publish flow rules failed after rule add");
}
return Result.ofSuccess(entity);
}
@PutMapping("/save.json")
public Result<FlowRuleEntity> updateIfNotNull(HttpServletRequest request, Long id, String app,
String limitApp, String resource, Integer grade,
Double count, Integer strategy, String refResource,
Integer controlBehavior, Integer warmUpPeriodSec,
Integer maxQueueingTimeMs) {
AuthUser authUser = authService.getAuthUser(request);
authUser.authTarget(app, PrivilegeType.WRITE_RULE);
if (id == null) {
return Result.ofFail(-1, "id can't be null");
}
FlowRuleEntity entity = repository.findById(id);
if (entity == null) {
return Result.ofFail(-1, "id " + id + " dose not exist");
}
if (StringUtil.isNotBlank(app)) {
entity.setApp(app.trim());
}
if (StringUtil.isNotBlank(limitApp)) {
entity.setLimitApp(limitApp.trim());
}
if (StringUtil.isNotBlank(resource)) {
entity.setResource(resource.trim());
}
if (grade != null) {
if (grade != 0 && grade != 1) {
return Result.ofFail(-1, "grade must be 0 or 1, but " + grade + " got");
}
entity.setGrade(grade);
}
if (count != null) {
entity.setCount(count);
}
if (strategy != null) {
if (strategy != 0 && strategy != 1 && strategy != 2) {
return Result.ofFail(-1, "strategy must be in [0, 1, 2], but " + strategy + " got");
}
entity.setStrategy(strategy);
if (strategy != 0) {
if (StringUtil.isBlank(refResource)) {
return Result.ofFail(-1, "refResource can't be null or empty when strategy!=0");
}
entity.setRefResource(refResource.trim());
}
}
if (controlBehavior != null) {
if (controlBehavior != 0 && controlBehavior != 1 && controlBehavior != 2) {
return Result.ofFail(-1, "controlBehavior must be in [0, 1, 2], but " + controlBehavior + " got");
}
if (controlBehavior == 1 && warmUpPeriodSec == null) {
return Result.ofFail(-1, "warmUpPeriodSec can't be null when controlBehavior==1");
}
if (controlBehavior == 2 && maxQueueingTimeMs == null) {
return Result.ofFail(-1, "maxQueueingTimeMs can't be null when controlBehavior==2");
}
entity.setControlBehavior(controlBehavior);
if (warmUpPeriodSec != null) {
entity.setWarmUpPeriodSec(warmUpPeriodSec);
}
if (maxQueueingTimeMs != null) {
entity.setMaxQueueingTimeMs(maxQueueingTimeMs);
}
}
Date date = new Date();
entity.setGmtModified(date);
try {
entity = repository.save(entity);
/**
* 自定義推送
*/
publishRules(entity.getApp());
if (entity == null) {
return Result.ofFail(-1, "save entity fail");
}
} catch (Throwable throwable) {
logger.error("save error:", throwable);
return Result.ofThrowable(-1, throwable);
}
if (!publishRules(entity.getApp(), entity.getIp(), entity.getPort())) {
logger.info("publish flow rules fail after rule update");
}
return Result.ofSuccess(entity);
}
@DeleteMapping("/delete.json")
public Result<Long> delete(HttpServletRequest request, Long id) {
AuthUser authUser = authService.getAuthUser(request);
if (id == null) {
return Result.ofFail(-1, "id can't be null");
}
FlowRuleEntity oldEntity = repository.findById(id);
if (oldEntity == null) {
return Result.ofSuccess(null);
}
authUser.authTarget(oldEntity.getApp(), PrivilegeType.DELETE_RULE);
try {
repository.delete(id);
//删除nacos上的配置規則
publishRules(oldEntity.getApp());
} catch (Exception e) {
return Result.ofFail(-1, e.getMessage());
}
if (!publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) {
logger.info("publish flow rules fail after rule delete");
}
return Result.ofSuccess(id);
}
/**
* 原方法
* @param app
* @param ip
* @param port
* @return
*/
private boolean publishRules(String app, String ip, Integer port) {
List<FlowRuleEntity> rules = repository.findAllByMachine(MachineInfo.of(app, ip, port));
return sentinelApiClient.setFlowRuleOfMachine(app, ip, port, rules);
}
/**
* 我們自己的publishRules ,将規則推送到 nacos
* @param app
* @throws Exception
*/
private void publishRules(String app) throws Exception {
List<FlowRuleEntity> rules = repository.findAllByApp(app);
rulePublisher.publish(app,rules);
}
}
其他規則 同理。
2.4 微服務工程改造
2.4.1 增加 sentinel與nacos整合後的依賴
<!-- sentilen用 nacos 做 持久化-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
2.4.2 yml配置
spring:
datasource:
druid:
...... # 省略db配置
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:9999
#namespace: bc7613d2-2e22-4292-a748-48b78170f14c #指定namespace的id
datasource:
# 名稱随意
flow:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-flow-rules # 都是在 sentinel源碼持久化改造中的規則名稱
groupId: SENTINEL_GROUP
rule-type: flow
degrade:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-degrade-rules # 都是在 sentinel源碼持久化改造中的規則名稱
groupId: SENTINEL_GROUP
rule-type: degrade
system:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-system-rules # 都是在 sentinel源碼持久化改造中的規則名稱
groupId: SENTINEL_GROUP
rule-type: system
authority:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-authority-rules # 都是在 sentinel源碼持久化改造中的規則名稱
groupId: SENTINEL_GROUP
rule-type: authority
param-flow:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-param-flow-rules # 都是在 sentinel源碼持久化改造中的規則名稱
groupId: SENTINEL_GROUP
rule-type: param-flow
application:
name: order-center
server:
port: 8080
#是否開啟@SentinelRestTemplate注解
resttemplate:
sentinel:
enabled: true
management:
endpoints:
web:
exposure:
include: '*'
這樣到此為止就改造好了
這樣無論是重新開機 nacos也好,sentinel也罷,業務工程也罷,都會執行個體化~
6. 阿裡雲Ahas
能用錢解決的事,都不是事情~
ahas開通位址
阿裡雲ahas文檔
選擇sdk接入—>springboot接入
随便起一個工程,接入ahas,通路接口
跟sentinel一樣的操作
7.結語
世上無難事,隻怕有心人,每天積累一點點,fighting!!!