點贊再看,養成習慣,公衆號搜一搜【一角錢技術】關注更多原創技術文章。本文 GitHub org_hejianhui/JavaStudy 已收錄,有個人系列文章。html
業務場景,高併發調用java
簡單來講就是超時機制,配置如下超時時間,假如1秒——每次請求在1秒內必須返回,不然到點就把線程掐死,釋放資源!git
思路:一旦超時,就釋放資源。因爲釋放資源速度較快,應用就不會那麼容易被拖死。github
代碼演示:(針對調用方處理)web
// 第一步:設置RestTemplate的超時時間
@Configuration
public class WebConfig {
@Bean
public RestTemplate restTemplate() {
//設置restTemplate的超時時間
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setReadTimeout(1000);
requestFactory.setConnectTimeout(1000);
RestTemplate restTemplate = new RestTemplate(requestFactory);
return restTemplate;
}
}
// 第二步:進行超時異常處理
try{
ResponseEntity<ProductInfo> responseEntity= restTemplate.getForEntity(uri+orderInfo.getProductNo(), ProductInfo.class);
productInfo = responseEntity.getBody();
}catch (Exception e) {
log.info("調用超時");
throw new RuntimeException("調用超時");
}
// 設置全局異常處理
@ControllerAdvice
public class NiuhExceptionHandler {
@ExceptionHandler(value = {RuntimeException.class})
@ResponseBody
public Object dealBizException() {
OrderVo orderVo = new OrderVo();
orderVo.setOrderNo("-1");
orderVo.setUserName("容錯用戶");
return orderVo;
}
}
複製代碼
有興趣能夠先了解一下船艙構造——通常來講,現代的輪船都會分不少艙室,艙室直接用鋼板焊死,彼此隔離。這樣即便有某個/某些船艙進水,也不會營銷其它艙室,浮力夠,船不會沉。spring
代碼中的艙壁隔離(線程池隔離模式)json
M類使用線程池1,N類使用線程池2,彼此的線程池不一樣,而且爲每一個類分配的線程池大小,例如 coreSIze=10。api
舉例子:M類調用B服務,N類調用C服務,若是M類和N類使用相同的線程池,那麼若是B服務掛了,N類調用B服務的接口併發又很高,你又沒有任何保護措施,你的服務就極可能被M類拖死。而若是M類有本身的線程池,N類也有本身的線程池,若是B服務掛了,M類頂可能是將本身的線程池佔滿,不會影響N類的線程池——因而N類依然能正常工做。緩存
思路:不把雞蛋放在一個籃子裏,你有你的線程池,我有個人線程池,你的線程池滿類和我也不要緊,你掛了也和我也不要緊。服務器
現實世界的斷路器你們確定都很瞭解,每一個人家裏都會有斷路器。斷路器實時監控電路的狀況,若是發現電路電流異常,就會跳閘,從而防止電路被燒燬。
軟件世界的斷路器能夠這樣理解:實時監測應用,若是發如今必定時間內失敗次數/失敗率達到必定閥值,就「跳閘」,斷路器打開——次數,請求直接返回,而不去調用本來調用的邏輯。
跳閘一段時間後(例如15秒),斷路器會進入半開狀態,這是一個瞬間態,此時容許一個請求調用該調的邏輯,若是成功,則斷路器關閉,應用正常調用;若是調用依然不成功,斷路器繼續回到打開狀態,過段時間再進入半開狀態嘗試——經過「跳閘」,應用能夠保護本身,並且避免資源浪費;而經過半開的設計,能夠實現應用的「自我修復」
A lightweight powerful flow control component enabling reliability and monitoring for microservices.(輕量級的流量控制、熔斷降級 Java 庫) github官網地址:github.com/alibaba/Sen… wiki:github.com/alibaba/Sen…
Hystrix 在 Sentinel 面前就是弟弟
niuh04-ms-alibaba-sentinel-helloworld
V1版本:
<!--導入Sentinel的相關jar包-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.7.1</version>
</dependency>
複製代碼
@RestController
@Slf4j
public class HelloWorldSentinelController {
@Autowired
private BusiServiceImpl busiService;
/** * 初始化流控規則 */
@PostConstruct
public void init() {
List<FlowRule> flowRules = new ArrayList<>();
/** * 定義 helloSentinelV1 受保護的資源的規則 */
//建立流控規則對象
FlowRule flowRule = new FlowRule();
//設置流控規則 QPS
flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
//設置受保護的資源
flowRule.setResource("helloSentinelV1");
//設置受保護的資源的閾值
flowRule.setCount(1);
flowRules.add(flowRule);
//加載配置好的規則
FlowRuleManager.loadRules(flowRules);
}
/** * 頻繁請求接口 http://localhost:8080/helloSentinelV1 * 這種作法的缺點: * 1)業務侵入性很大,須要在你的controoler中寫入 非業務代碼.. * 2)配置不靈活 若須要添加新的受保護資源 須要手動添加 init方法來添加流控規則 * @return */
@RequestMapping("/helloSentinelV1")
public String testHelloSentinelV1() {
Entry entity =null;
//關聯受保護的資源
try {
entity = SphU.entry("helloSentinelV1");
//開始執行 本身的業務方法
busiService.doBusi();
//結束執行本身的業務方法
} catch (BlockException e) {
log.info("testHelloSentinelV1方法被流控了");
return "testHelloSentinelV1方法被流控了";
}finally {
if(entity!=null) {
entity.exit();
}
}
return "OK";
}
}
複製代碼
測試效果:http://localhost:8080/helloSentinelV1 V1版本的缺陷以下:
V2版本:基於V1版本,再添加一個依賴
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
<version>1.7.1</version>
</dependency>
複製代碼
// 配置一個切面
@Configuration
public class SentinelConfig {
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
}
/** * 初始化流控規則 */
@PostConstruct
public void init() {
List<FlowRule> flowRules = new ArrayList<>();
/** * 定義 helloSentinelV2 受保護的資源的規則 */
//建立流控規則對象
FlowRule flowRule2 = new FlowRule();
//設置流控規則 QPS
flowRule2.setGrade(RuleConstant.FLOW_GRADE_QPS);
//設置受保護的資源
flowRule2.setResource("helloSentinelV2");
//設置受保護的資源的閾值
flowRule2.setCount(1);
flowRules.add(flowRule2);
}
/** * 頻繁請求接口 http://localhost:8080/helloSentinelV2 * 優勢: 須要配置aspectj的切面SentinelResourceAspect ,添加註解@SentinelResource * 解決了v1版本中 sentinel的業務侵入代碼問題,經過blockHandler指定被流控後調用的方法. * 缺點: 若咱們的controller中的方法逐步變多,那麼受保護的方法也愈來愈多,會致使一個問題 * blockHandler的方法也會愈來愈多 引發方法急劇膨脹 怎麼解決 * * 注意點: * blockHandler 對應處理 BlockException 的函數名稱, * 可選項。blockHandler 函數訪問範圍須要是 public,返回類型須要與原方法相匹配, * 參數類型須要和原方法相匹配而且最後加一個額外的參數, * 類型爲 BlockException。blockHandler 函數默認須要和原方法在同一個類中 * @return */
@RequestMapping("/helloSentinelV2")
@SentinelResource(value = "helloSentinelV2",blockHandler ="testHelloSentinelV2BlockMethod")
public String testHelloSentinelV2() {
busiService.doBusi();
return "OK";
}
public String testHelloSentinelV2BlockMethod(BlockException e) {
log.info("testRt流控");
return "testRt降級 流控...."+e;
}
複製代碼
測試效果:http://localhost:8080/helloSentinelV2
V3版本 基於V2缺點改進
/** * 初始化流控規則 */
@PostConstruct
public void init() {
List<FlowRule> flowRules = new ArrayList<>();
/** * 定義 helloSentinelV3 受保護的資源的規則 */
//建立流控規則對象
FlowRule flowRule3 = new FlowRule();
//設置流控規則 QPS
flowRule3.setGrade(RuleConstant.FLOW_GRADE_QPS);
//設置受保護的資源
flowRule3.setResource("helloSentinelV3");
//設置受保護的資源的閾值
flowRule3.setCount(1);
flowRules.add(flowRule3);
}
/** * 咱們看到了v2中的缺點,咱們經過blockHandlerClass 來指定處理被流控的類 * 經過testHelloSentinelV3BlockMethod 來指定blockHandlerClass 中的方法名稱 * ***這種方式 處理異常流控的方法必需要是static的 * 頻繁請求接口 http://localhost:8080/helloSentinelV3 * @return */
@RequestMapping("/helloSentinelV3")
@SentinelResource(value = "helloSentinelV3",blockHandler = "testHelloSentinelV3BlockMethod",blockHandlerClass = BlockUtils.class)
public String testHelloSentinelV3() {
busiService.doBusi();
return "OK";
}
// 異常處理類
@Slf4j
public class BlockUtils {
public static String testHelloSentinelV3BlockMethod(BlockException e){
log.info("testHelloSentinelV3方法被流控了");
return "testHelloSentinelV3方法被流控了";
}
}
複製代碼
測試效果:http://localhost:8080/helloSentinelV3 缺點:不能動態的添加規則。如何解決問題?
<!--加入sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--加入actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
複製代碼
添加Sentinel後,會暴露/actuator/sentinel 端點http://localhost:8080/actuator/sentinel
而Springboot默認是沒有暴露該端點的,因此咱們須要本身配置
server:
port: 8080
management:
endpoints:
web:
exposure:
include: '*'
複製代碼
下載地址:github.com/alibaba/Sen… (我這裏版本是:1.6.3)
java -jar sentinel-dashboard-1.6.3.jar
啓動(就是一個SpringBoot工程)spring:
cloud:
sentinel:
transport:
dashboard: localhost:9999
複製代碼
在這個面板中咱們監控咱們接口的 經過的QPS 和 拒絕的QPS,在沒有設置流控規則,咱們是看不到拒絕的QPS。
用來線上微服務的所監控的API
簇點鏈路 選擇具體的訪問的API,而後點擊「流控按鈕」 含義:
瘋狂的請求這個路徑
業務場景:咱們如今有兩個API,第一個是保存訂單,一個是查詢訂單,假設咱們但願有限操做「保存訂單」 測試:寫兩個讀寫測試接口
/** * 方法實現說明:模仿 流控模式【關聯】 讀接口 * @author:hejianhui * @param orderNo * @return: * @exception: * @date:2019/11/24 22:06 */
@RequestMapping("/findById/{orderNo}")
public Object findById(@PathVariable("orderNo") String orderNo) {
log.info("orderNo:{}","執行查詢操做"+System.currentTimeMillis());
return orderInfoMapper.selectOrderInfoById(orderNo);
}
/** * 方法實現說明:模仿流控模式【關聯】 寫接口(優先) * @author:hejianhui * @return: * @exception: * @date:2019/11/24 22:07 */
@RequestMapping("/saveOrder")
public String saveOrder() throws InterruptedException {
//Thread.sleep(500);
log.info("執行保存操做,模仿返回訂單ID");
return UUID.randomUUID().toString();
}
複製代碼
測試代碼:寫一個for循環一直調用咱們的寫接口,讓寫接口QPS達到閥值
public class TestSentinelRule {
public static void main(String[] args) throws InterruptedException {
RestTemplate restTemplate = new RestTemplate();
for(int i=0;i<1000;i++) {
restTemplate.postForObject("http://localhost:8080/saveOrder",null,String.class);
Thread.sleep(10);
}
}
}
複製代碼
此時訪問咱們的讀接口:此時被限流了。
用法說明,本地實驗沒成功,用alibaba 未畢業版本0.9.0能夠測試出效果,API級別的限制流量
代碼:
@RequestMapping("/findAll")
public String findAll() throws InterruptedException {
orderServiceImpl.common();
return "findAll";
}
@RequestMapping("/findAllByCondtion")
public String findAllByCondtion() {
orderServiceImpl.common();
return "findAllByCondition";
}
@Service
public class OrderServiceImpl {
@SentinelResource("common")
public String common() {
return "common";
}
}
複製代碼
根據流控規則來講: 只會限制/findAll的請求,不會限制/findAllByCondtion規則
源碼:com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController
源碼:com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController>
當流量忽然增大的時候,咱們經常會但願系統從空閒狀態到繁忙狀態的切換的時間長一些。即若是系統在此以前長期處於空閒的狀態,咱們但願處理請求的數量是緩步增長,通過預期的時間後,到達系統處理請求個數的最大值。Warm Up (冷啓動,預熱)模式就是爲了實現這個目的。
冷加載因子:codeFacotr 默認是3
上圖設置:就是QPS從100/3=33開始算, 通過10秒鐘,達到一百的QPS 才進行限制流量。
源碼:com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController
這種方式適合用於請求以突刺狀來到,這個時候咱們不但願一會兒把全部的請求都經過,這樣可能會把系統壓垮;同時咱們也期待系統以穩定的速度,逐步處理這些請求,以起到「削峯填谷」的效果,而不是拒絕全部請求。
選擇排隊等待的閥值類型必須是****QPS 上圖設置:單機閥值爲10,表示每秒經過的請求個數是10,也就是每一個請求平均間隔恆定爲 1000 / 10 = 100 ms,每個請求的最長等待時間(maxQueueingTimeMs)爲 20 * 1000ms = 20s。,超過20s就丟棄請求。
平均響應時間(DEGRADE_GRADE_RT):當 1s 內持續進入5個請求,對應時刻的平均響應時間(秒級)均超過閥值(count,以 ms 爲單位),那麼在接下來的時間窗口(DegradeRule 中的 timeWindow,以 s 爲單位)以內,對這個方法的調用都會自動地熔斷(拋出 DegradeException)。
注意:Sentinel 默認同級的 RT 上限是4900ms,超出此閥值都會算作4900ms,若須要變動此上限能夠經過啓動配置項:-Dcsp.sentinel.statistic.max.rt=xxx 來配置
當資源的每秒請求量 >= 5,而且每秒異常總數佔經過量的比值超過閥值(DegradeRule 中的 count)以後,資源進入降級狀態,即在接下的時間窗口(DegradeRule 中的 timeWindow,以 s 爲單位)以內,對這個方法的調用都會自動地返回。異常比例的閥值範圍是 [0.0, 1.0],表明 0% ~ 100% 。
當資源近千分之的異常數目超過閥值以後會進行熔斷。注意因爲統計時間窗口是分鐘級別的,若 timeWindow 小於 60s,則結束熔斷狀態後仍可能再進入熔斷狀態。
業務場景:秒殺業務,好比商場作促銷秒殺,針對蘋果11(商品id=1)進行9.9秒殺活動,那麼這個時候,咱們去請求訂單接口(商品id=1)的請求流量十分大,咱們就能夠經過熱點參數規則來控制 商品id=1 的請求的併發量。而其餘正常商品的請求不會受到限制。那麼這種熱點參數規則使用。
咱們經過觀察到sentinel-dashboard的機器列表上觀察註冊服務微服務信息。咱們的 控制檯就能夠經過這些微服務的註冊信息跟咱們的具體的微服務進行通訊.
@RestController
public class AddFlowLimitController {
@RequestMapping("/addFlowLimit")
public String addFlowLimit() {
List<FlowRule> flowRuleList = new ArrayList<>();
FlowRule flowRule = new FlowRule("/testAddFlowLimitRule");
//設置QPS閾值
flowRule.setCount(1);
//設置流控模型爲QPS模型
flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
flowRuleList.add(flowRule);
FlowRuleManager.loadRules(flowRuleList);
return "success";
}
@RequestMapping("/testAddFlowLimitRule")
public String testAddFlowLimitRule() {
return "testAddFlowLimitRule";
}
}
複製代碼
添加效果截圖: 執行:http://localhost:8080/addFlowLimit Sentinel具體配置項:github.com/alibaba/Sen…
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:9999
filter:
enabled: true #關閉Spring mvc的端點保護
複製代碼
那麼咱們的這種類型的接口 不會被sentinel保護 只有加了 @SentinelResource
的註解的資源纔會被保護
<!--加入ribbon-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<!--加入sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--加入actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
複製代碼
在咱們的RestTemplate組件上添加@SentinelRestTemplate註解。而且咱們能夠經過在@SentinelRestTemplate 一樣的能夠指定咱們的 blockHandlerClass、fallbackClass、blockHandler、fallback 這四個屬性
@Configuration
public class WebConfig {
@Bean
@LoadBalanced
@SentinelRestTemplate( blockHandler = "handleException",blockHandlerClass = GlobalExceptionHandler.class, fallback = "fallback",fallbackClass = GlobalExceptionHandler.class )
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
*****************全局異常處理類*****************
@Slf4j
public class GlobalExceptionHandler {
/** * 限流後處理方法 * @param request * @param body * @param execution * @param ex * @return */
public static SentinelClientHttpResponse handleException(HttpRequest request, byte[] body, ClientHttpRequestExecution execution, BlockException ex) {
ProductInfo productInfo = new ProductInfo();
productInfo.setProductName("被限制流量拉");
productInfo.setProductNo("-1");
ObjectMapper objectMapper = new ObjectMapper();
try {
return new SentinelClientHttpResponse(objectMapper.writeValueAsString(productInfo));
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
/** * 熔斷後處理的方法 * @param request * @param body * @param execution * @param ex * @return */
public static SentinelClientHttpResponse fallback(HttpRequest request, byte[] body, ClientHttpRequestExecution execution, BlockException ex) {
ProductInfo productInfo = new ProductInfo();
productInfo.setProductName("被降級拉");
productInfo.setProductNo("-1");
ObjectMapper objectMapper = new ObjectMapper();
try {
return new SentinelClientHttpResponse(objectMapper.writeValueAsString(productInfo));
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
}
複製代碼
何時關閉:通常在咱們的本身測試業務功能是否正常的狀況,關閉該配置
#是否開啓@SentinelRestTemplate註解
resttemplate:
sentinel:
enabled: true
複製代碼
在niuh05-ms-alibaba-feignwithsentinel-order上 pom.xml中添加配置
<!--加入sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--加入actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.niuh</groupId>
<artifactId>niuh03-ms-alibaba-feign-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
複製代碼
@FeignClient(name = "product-center",fallback = ProductCenterFeignApiWithSentinelFallback.class)
public interface ProductCenterFeignApiWithSentinel {
/** * 聲明式接口,遠程調用http://product-center/selectProductInfoById/{productNo} * @param productNo * @return */
@RequestMapping("/selectProductInfoById/{productNo}")
ProductInfo selectProductInfoById(@PathVariable("productNo") String productNo) throws InterruptedException;
}
複製代碼
咱們feign的限流降級接口(經過fallback沒有辦法獲取到異常的)
@Component
public class ProductCenterFeignApiWithSentinelFallback implements ProductCenterFeignApiWithSentinel {
@Override
public ProductInfo selectProductInfoById(String productNo) {
ProductInfo productInfo = new ProductInfo();
productInfo.setProductName("默認商品");
return productInfo;
}
}
複製代碼
package com.niuh.feignapi.sentinel;
import com.niuh.entity.ProductInfo;
import com.niuh.handler.ProductCenterFeignApiWithSentielFallbackFactoryasdasf;
import com.niuh.handler.ProductCenterFeignApiWithSentinelFallback;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
/** * Created by hejianhui on 2019/11/22. */
@FeignClient(name = "product-center",fallbackFactory = ProductCenterFeignApiWithSentielFallbackFactoryasdasf.class)
public interface ProductCenterFeignApiWithSentinel {
/** * 聲明式接口,遠程調用http://product-center/selectProductInfoById/{productNo} * @param productNo * @return */
@RequestMapping("/selectProductInfoById/{productNo}")
ProductInfo selectProductInfoById(@PathVariable("productNo") String productNo) throws InterruptedException;
}
複製代碼
經過FallbackFactory屬性能夠處理咱們的異常
@Component
@Slf4j
public class ProductCenterFeignApiWithSentielFallbackFactoryasdasf implements FallbackFactory<ProductCenterFeignApiWithSentinel> {
@Override
public ProductCenterFeignApiWithSentinel create(Throwable throwable) {
return new ProductCenterFeignApiWithSentinel(){
@Override
public ProductInfo selectProductInfoById(String productNo) {
ProductInfo productInfo = new ProductInfo();
if (throwable instanceof FlowException) {
log.error("流控了....{}",throwable.getMessage());
productInfo.setProductName("我是被流控的默認商品");
}else {
log.error("降級了....{}",throwable.getMessage());
productInfo.setProductName("我是被降級的默認商品");
}
return productInfo;
}
};
}
}
複製代碼
Sentinel-dashboard 配置的規則,在咱們的微服務以及控制檯重啓的時候就清空了,由於它是基於內存的。
Dashboard 的推送規則方式是經過 API 將規則推送至客戶端並直接更新到內存。 優缺點:這種作法的好處是簡單,無依賴;壞處是應用重啓規則就會消失,僅用於簡單測試,不能用於生產環境。
首先 Sentinel 控制檯經過 API 將規則推送至客戶端並更新到內存中,接着註冊的寫數據源會將新的規則保存到本地的文件中。使用 pull 模式的數據源時通常不須要對 Sentinel 控制檯進行改造。
這種實現方法好處是簡單,不引入新的依賴,壞處是沒法保證監控數據的一致性
經過SPI擴展機制進行擴展,咱們寫一個拉模式的實現類 com.niuh.persistence.PullModeByFileDataSource ,而後在工廠目錄下建立 META-INF/services/com.alibaba.csp.sentinel.init.InitFun文件。 文件的內容就是寫咱們的拉模式的實現類:
代碼在niuh05-ms-alibaba-sentinelrulepersistencepull-order 工程的persistence包下。
微服務改造方案
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
複製代碼
spring:
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: 47.111.191.111:8848
dataId: ${spring.application.name}-flow-rules
groupId: SENTINEL_GROUP
rule-type: flow
degrade:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-degrade-rules
groupId: SENTINEL_GROUP
rule-type: degrade
system:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-system-rules
groupId: SENTINEL_GROUP
rule-type: system
authority:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-authority-rules
groupId: SENTINEL_GROUP
rule-type: authority
param-flow:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-param-flow-rules
groupId: SENTINEL_GROUP
rule-type: param-flow
複製代碼
Sentinel-dashboard改造方案
<!-- for Nacos rule publisher sample -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<!-- <scope>test</scope>--> // 須要把test註釋掉
</dependency>
複製代碼
控制檯改造主要是爲規則實現:
在sentinel-dashboard工程目錄com.alibaba.csp.sentinel.dashboard.rule 下建立一 個Nacos的包,而後把咱們的各個場景的配置規則類寫到該包下. 咱們以ParamFlowRuleController(熱點參數流控類做爲修改做爲演示)
/** * @author Eric Zhao * @since 0.2.1 */
@RestController
@RequestMapping(value = "/paramFlow")
public class ParamFlowRuleController {
private final Logger logger = LoggerFactory.getLogger(ParamFlowRuleController.class);
@Autowired
private SentinelApiClient sentinelApiClient;
@Autowired
private AppManagement appManagement;
@Autowired
private RuleRepository<ParamFlowRuleEntity, Long> repository;
@Autowired
@Qualifier("niuhHotParamFlowRuleNacosPublisher")
private DynamicRulePublisher<List<ParamFlowRuleEntity>> rulePublisher;
@Autowired
@Qualifier("niuhHotParamFlowRuleNacosProvider")
private DynamicRuleProvider<List<ParamFlowRuleEntity>> ruleProvider;
@Autowired
private AuthService<HttpServletRequest> authService;
private boolean checkIfSupported(String app, String ip, int port) {
try {
return Optional.ofNullable(appManagement.getDetailApp(app))
.flatMap(e -> e.getMachine(ip, port))
.flatMap(m -> VersionUtils.parseVersion(m.getVersion())
.map(v -> v.greaterOrEqual(version020)))
.orElse(true);
// If error occurred or cannot retrieve machine info, return true.
} catch (Exception ex) {
return true;
}
}
@GetMapping("/rules")
public Result<List<ParamFlowRuleEntity>> apiQueryAllRulesForMachine(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 cannot be null or empty");
}
if (StringUtil.isEmpty(ip)) {
return Result.ofFail(-1, "ip cannot be null or empty");
}
if (port == null || port <= 0) {
return Result.ofFail(-1, "Invalid parameter: port");
}
if (!checkIfSupported(app, ip, port)) {
return unsupportedVersion();
}
try {
/* return sentinelApiClient.fetchParamFlowRulesOfMachine(app, ip, port) .thenApply(repository::saveAll) .thenApply(Result::ofSuccess) .get();*/
List<ParamFlowRuleEntity> rules = ruleProvider.getRules(app);
rules = repository.saveAll(rules);
return Result.ofSuccess(rules);
} catch (ExecutionException ex) {
logger.error("Error when querying parameter flow rules", ex.getCause());
if (isNotSupported(ex.getCause())) {
return unsupportedVersion();
} else {
return Result.ofThrowable(-1, ex.getCause());
}
} catch (Throwable throwable) {
logger.error("Error when querying parameter flow rules", throwable);
return Result.ofFail(-1, throwable.getMessage());
}
}
private boolean isNotSupported(Throwable ex) {
return ex instanceof CommandNotFoundException;
}
@PostMapping("/rule")
public Result<ParamFlowRuleEntity> apiAddParamFlowRule(HttpServletRequest request, @RequestBody ParamFlowRuleEntity entity) {
AuthUser authUser = authService.getAuthUser(request);
authUser.authTarget(entity.getApp(), PrivilegeType.WRITE_RULE);
Result<ParamFlowRuleEntity> checkResult = checkEntityInternal(entity);
if (checkResult != null) {
return checkResult;
}
if (!checkIfSupported(entity.getApp(), entity.getIp(), entity.getPort())) {
return unsupportedVersion();
}
entity.setId(null);
entity.getRule().setResource(entity.getResource().trim());
Date date = new Date();
entity.setGmtCreate(date);
entity.setGmtModified(date);
try {
entity = repository.save(entity);
//publishRules(entity.getApp(), entity.getIp(), entity.getPort()).get();
publishRules(entity.getApp());
return Result.ofSuccess(entity);
} catch (ExecutionException ex) {
logger.error("Error when adding new parameter flow rules", ex.getCause());
if (isNotSupported(ex.getCause())) {
return unsupportedVersion();
} else {
return Result.ofThrowable(-1, ex.getCause());
}
} catch (Throwable throwable) {
logger.error("Error when adding new parameter flow rules", throwable);
return Result.ofFail(-1, throwable.getMessage());
}
}
private <R> Result<R> checkEntityInternal(ParamFlowRuleEntity entity) {
if (entity == null) {
return Result.ofFail(-1, "bad rule body");
}
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 || entity.getPort() <= 0) {
return Result.ofFail(-1, "port can't be null");
}
if (entity.getRule() == null) {
return Result.ofFail(-1, "rule can't be null");
}
if (StringUtil.isBlank(entity.getResource())) {
return Result.ofFail(-1, "resource name cannot be null or empty");
}
if (entity.getCount() < 0) {
return Result.ofFail(-1, "count should be valid");
}
if (entity.getGrade() != RuleConstant.FLOW_GRADE_QPS) {
return Result.ofFail(-1, "Unknown mode (blockGrade) for parameter flow control");
}
if (entity.getParamIdx() == null || entity.getParamIdx() < 0) {
return Result.ofFail(-1, "paramIdx should be valid");
}
if (entity.getDurationInSec() <= 0) {
return Result.ofFail(-1, "durationInSec should be valid");
}
if (entity.getControlBehavior() < 0) {
return Result.ofFail(-1, "controlBehavior should be valid");
}
return null;
}
@PutMapping("/rule/{id}")
public Result<ParamFlowRuleEntity> apiUpdateParamFlowRule(HttpServletRequest request, @PathVariable("id") Long id, @RequestBody ParamFlowRuleEntity entity) {
AuthUser authUser = authService.getAuthUser(request);
if (id == null || id <= 0) {
return Result.ofFail(-1, "Invalid id");
}
ParamFlowRuleEntity oldEntity = repository.findById(id);
if (oldEntity == null) {
return Result.ofFail(-1, "id " + id + " does not exist");
}
authUser.authTarget(oldEntity.getApp(), PrivilegeType.WRITE_RULE);
Result<ParamFlowRuleEntity> checkResult = checkEntityInternal(entity);
if (checkResult != null) {
return checkResult;
}
if (!checkIfSupported(entity.getApp(), entity.getIp(), entity.getPort())) {
return unsupportedVersion();
}
entity.setId(id);
Date date = new Date();
entity.setGmtCreate(oldEntity.getGmtCreate());
entity.setGmtModified(date);
try {
entity = repository.save(entity);
//publishRules(entity.getApp(), entity.getIp(), entity.getPort()).get();
publishRules(entity.getApp());
return Result.ofSuccess(entity);
} catch (ExecutionException ex) {
logger.error("Error when updating parameter flow rules, id=" + id, ex.getCause());
if (isNotSupported(ex.getCause())) {
return unsupportedVersion();
} else {
return Result.ofThrowable(-1, ex.getCause());
}
} catch (Throwable throwable) {
logger.error("Error when updating parameter flow rules, id=" + id, throwable);
return Result.ofFail(-1, throwable.getMessage());
}
}
@DeleteMapping("/rule/{id}")
public Result<Long> apiDeleteRule(HttpServletRequest request, @PathVariable("id") Long id) {
AuthUser authUser = authService.getAuthUser(request);
if (id == null) {
return Result.ofFail(-1, "id cannot be null");
}
ParamFlowRuleEntity oldEntity = repository.findById(id);
if (oldEntity == null) {
return Result.ofSuccess(null);
}
authUser.authTarget(oldEntity.getApp(), PrivilegeType.DELETE_RULE);
try {
repository.delete(id);
/*publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort()).get();*/
publishRules(oldEntity.getApp());
return Result.ofSuccess(id);
} catch (ExecutionException ex) {
logger.error("Error when deleting parameter flow rules", ex.getCause());
if (isNotSupported(ex.getCause())) {
return unsupportedVersion();
} else {
return Result.ofThrowable(-1, ex.getCause());
}
} catch (Throwable throwable) {
logger.error("Error when deleting parameter flow rules", throwable);
return Result.ofFail(-1, throwable.getMessage());
}
}
private CompletableFuture<Void> publishRules(String app, String ip, Integer port) {
List<ParamFlowRuleEntity> rules = repository.findAllByMachine(MachineInfo.of(app, ip, port));
return sentinelApiClient.setParamFlowRuleOfMachine(app, ip, port, rules);
}
private void publishRules(String app) throws Exception {
List<ParamFlowRuleEntity> rules = repository.findAllByApp(app);
rulePublisher.publish(app, rules);
}
private <R> Result<R> unsupportedVersion() {
return Result.ofFail(4041,
"Sentinel client not supported for parameter flow control (unsupported version or dependency absent)");
}
private final SentinelVersion version020 = new SentinelVersion().setMinorVersion(2);
}
複製代碼
第一步:訪問 help.aliyun.com/document_de… 第二步:免費開通 第三步:開通 第四步:接入應用 第五步:點擊接入SDK 第六步:加入咱們的應用
以niuh05-ms-alibaba-sentinelrulepersistence-ahas-order工程爲例
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>spring‐boot‐starter‐ahas‐sentinel‐client</artifactId> 4 <version>1.5.0</version>
</dependency>
複製代碼
ahas.namespace: default
project.name: order-center
ahas.license: b833de8ab5f34e4686457ecb2b60fa46
複製代碼
@SentinelResource("hot-param-flow-rule")
@RequestMapping("/testHotParamFlowRule")
public OrderInfo testHotParamFlowRule(@RequestParam("orderNo") String orderNo) {
return orderInfoMapper.selectOrderInfoById(orderNo);
}
複製代碼
第一次訪問接口: AHas控制檯出現咱們的微服務 添加咱們直接的流控規則 瘋狂刷新咱們的測試接口:
發現這兩種錯誤都是醫院,顯然這裏咱們須要優化 UrlBlockHandler
提供了一個接口,咱們須要實現這個接口
/** * @vlog: 高於生活,源於生活 * @desc: 類的描述:處理流控,降級規則 * @author: hejianhui * @createDate: 2019/12/3 16:40 * @version: 1.0 */
@Component
public class NiuhUrlBlockHandler implements UrlBlockHandler {
public static final Logger log = LoggerFactory.getLogger(NiuhUrlBlockHandler.class);
@Override
public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws IOException {
if(ex instanceof FlowException) {
log.warn("觸發了流控");
warrperResponse(response,ErrorEnum.FLOW_RULE_ERR);
}else if(ex instanceof ParamFlowException) {
log.warn("觸發了參數流控");
warrperResponse(response,ErrorEnum.HOT_PARAM_FLOW_RULE_ERR);
}else if(ex instanceof AuthorityException) {
log.warn("觸發了受權規則");
warrperResponse(response,ErrorEnum.AUTH_RULE_ERR);
}else if(ex instanceof SystemBlockException) {
log.warn("觸發了系統規則");
warrperResponse(response,ErrorEnum.SYS_RULE_ERR);
}else{
log.warn("觸發了降級規則");
warrperResponse(response,ErrorEnum.DEGRADE_RULE_ERR);
}
}
private void warrperResponse(HttpServletResponse httpServletResponse, ErrorEnum errorEnum) throws IOException {
httpServletResponse.setStatus(500);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setHeader("Content-Type","application/json;charset=utf-8");
httpServletResponse.setContentType("application/json;charset=utf-8");
ObjectMapper objectMapper = new ObjectMapper();
String errMsg =objectMapper.writeValueAsString(new ErrorResult(errorEnum));
httpServletResponse.getWriter().write(errMsg);
}
}
複製代碼
優化後:
Sentinel 提供了一個 RequestOriginParser
接口,咱們能夠在這裏實現編碼從請求頭中區分來源
/** * @vlog: 高於生活,源於生活 * @desc: 類的描述:區分來源接口 * @author: hejianhui * @createDate: 2019/12/4 13:13 * @version: 1.0 */
/*@Component*/
@Slf4j
public class NiuhRequestOriginParse implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
String origin = request.getHeader("origin");
if(StringUtils.isEmpty(origin)) {
log.warn("origin must not null");
throw new IllegalArgumentException("request origin must not null");
}
return origin;
}
}
複製代碼
配置設置區分來源爲:yijiaoqian
例如:/selectOrderInfoById/2 、 /selectOrderInfoById/1 須要轉爲/selectOrderInfoById/{number}
/** * @vlog: 高於生活,源於生活 * @desc: 類的描述:解決RestFule風格的請求 * eg: /selectOrderInfoById/2 /selectOrderInfoById/1 須要轉爲/selectOrderInfoById/{number} * @author: hejianhui * @createDate: 2019/12/4 13:28 * @version: 1.0 */
@Component
@Slf4j
public class NiuhUrlClean implements UrlCleaner {
@Override
public String clean(String originUrl) {
log.info("originUrl:{}",originUrl);
if(StringUtils.isEmpty(originUrl)) {
log.error("originUrl not be null");
throw new IllegalArgumentException("originUrl not be null");
}
return replaceRestfulUrl(originUrl);
}
/** * 方法實現說明:把/selectOrderInfoById/2 替換成/selectOrderInfoById/{number} * @author:hejianhui * @param sourceUrl 目標url * @return: 替換後的url * @exception: * @date:2019/12/4 13:46 */
private String replaceRestfulUrl(String sourceUrl) {
List<String> origins = Arrays.asList(sourceUrl.split("/"));
StringBuffer targetUrl = new StringBuffer("/");
for(String str:origins) {
if(NumberUtils.isNumber(str)) {
targetUrl.append("/{number}");
}else {
targetUrl.append(str);
}
}
return targetUrl.toString();
}
}
複製代碼
PS:以上代碼提交在 Github :github.com/Niuh-Study/…
文章持續更新,能夠公衆號搜一搜「 一角錢技術 」第一時間閱讀, 本文 GitHub org_hejianhui/JavaStudy 已經收錄,歡迎 Star。