sentinal源码:
https://github.com/alibaba/Sentinel
Sentinel 是什么?
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 具有以下特征:
- 丰富的应用场景:
Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。 - 完备的实时监控:
Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。 - 广泛的开源生态:
Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。 - 完善的 SPI 扩展点:
Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
Sentinel 分为两个部分:
核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
如何接入sentinal?
STEP 1. 在应用中引入Sentinel Jar包
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.7.1</version>
</dependency>
STEP 2. 定义资源
接下来,我们把需要控制流量的代码用 Sentinel API SphU.entry("HelloWorld") 和 entry.exit() 包围起来即可。在下面的例子中,我们将 System.out.println("hello world"); 这端代码作为资源,用 API 包围起来(埋点)。参考代码如下:
public static void main(String[] args) {
initFlowRules();
while (true) {
Entry entry = null;
try {
entry = SphU.entry("HelloWorld");
/*您的业务逻辑 - 开始*/
System.out.println("hello world");
/*您的业务逻辑 - 结束*/
} catch (BlockException e1) {
/*流控逻辑处理 - 开始*/
System.out.println("block!");
/*流控逻辑处理 - 结束*/
} finally {
if (entry != null) {
entry.exit();
}
}
}
}
文末有实际的工程中的使用举例。
STEP 3. 定义规则
流量控制
- 并发线程数流量控制
并发线程数限流用于保护业务线程数不被耗尽。例如,当应用所依赖的下游应用由于某种原因导致服务不稳定、响应延迟增加,对于调用者来说,意味着吞吐量下降和更多的线程数占用,极端情况下甚至导致线程池耗尽。为应对太多线程占用的情况,业内有使用隔离的方案,比如通过不同业务逻辑使用不同线程池来隔离业务自身之间的资源争抢(线程池隔离)。这种隔离方案虽然隔离性比较好,但是代价就是线程数目太多,线程上下文切换的 overhead 比较大,特别是对低延时的调用有比较大的影响。Sentinel 并发线程数限流不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目,如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离。 - QPS流量控制
当 QPS 超过某个阈值的时候,则采取措施进行流量控制。流量控制的效果包括以下几种:直接拒绝、Warm Up、匀速排队。对应 FlowRule 中的 controlBehavior 字段。
2.1 直接拒绝
(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。
2.2 Warm Up
(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。
2.3 匀速排队
匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
接下来,通过规则来指定允许该资源通过的请求次数,例如下面的代码定义了资源 HelloWorld 每秒最多只能通过 20 个请求。
private static void initFlowRules(){
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource("HelloWorld");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// Set limit QPS to 20.
rule.setCount(20);
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
熔断降级
- 平均响应时间 (DEGRADE_GRADE_RT):当 1s 内持续进入 5 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count,以 ms 为单位),那么在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。
- 异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):当资源的每秒请求量 >= 5,并且每秒异常总数占通过量的比值超过阈值(DegradeRule 中的 count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
- 异常数 (DEGRADE_GRADE_EXCEPTION_COUNT):当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔断状态。
private static void initDegradeRule() {
List<DegradeRule> rules = new ArrayList<DegradeRule>();
DegradeRule rule = new DegradeRule();
rule.setResource(KEY);
// set threshold rt, 10 ms
rule.setCount(10);
rule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
rule.setTimeWindow(10);
rules.add(rule);
DegradeRuleManager.loadRules(rules);
}
sentinal源码中的规则定义:
/**
* @author youji.zj
* @author jialiang.linjl
*/
public final class RuleConstant {
public static final int FLOW_GRADE_THREAD = 0;
public static final int FLOW_GRADE_QPS = 1;
public static final int DEGRADE_GRADE_RT = 0;
/**
* Degrade by biz exception ratio in the current {@link IntervalProperty#INTERVAL} second(s).
*/
public static final int DEGRADE_GRADE_EXCEPTION_RATIO = 1;
/**
* Degrade by biz exception count in the last 60 seconds.
*/
public static final int DEGRADE_GRADE_EXCEPTION_COUNT = 2;
public static final int AUTHORITY_WHITE = 0;
public static final int AUTHORITY_BLACK = 1;
public static final int STRATEGY_DIRECT = 0;
public static final int STRATEGY_RELATE = 1;
public static final int STRATEGY_CHAIN = 2;
public static final int CONTROL_BEHAVIOR_DEFAULT = 0;
public static final int CONTROL_BEHAVIOR_WARM_UP = 1;
public static final int CONTROL_BEHAVIOR_RATE_LIMITER = 2;
public static final int CONTROL_BEHAVIOR_WARM_UP_RATE_LIMITER = 3;
public static final String LIMIT_APP_DEFAULT = "default";
public static final String LIMIT_APP_OTHER = "other";
public static final int DEFAULT_SAMPLE_COUNT = 2;
public static final int DEFAULT_WINDOW_INTERVAL_MS = 1000;
private RuleConstant() {}
}
有几点需要注意的是:
- 规则的持久化配置中心可以是redis、nacos、zk、file等等任何可以持久化的数据源,只要能保证更新规则时,客户端能得到通知即可
- 规则的更新可以通过 Sentinel Dashboard 也可以通过各个配置中心自己的更新接口来操作
- AbstractDataSource 中的 SentinelProperty 持有了一个 PropertyListener 接口,最终更新 RuleManager 中的规则是 PropertyListener 去做的
文末有实际工程中的使用举例。
实际工程中的应用:
sentinal定义redis访问的资源:
@Component
public class RedisCacheClient {
@Autowired
private Sedis sedis;
private RedisCacheClient() {
}
public <T> T get(final String key, final Class<T> tClass) {
Entry entry = null;
try {
entry = SphU.entry(RedisKeyConstant.SEDIS_RESKEY);
final byte[] bytes = this.sedis.get(key.getBytes(Charsets.UTF_8));
if (bytes == null || bytes.length == 0) {
return null;
}
return SerializeUtil.deserialize(bytes, tClass);
}catch (BlockException e1){
ExceptionLogger.error("RedisCacheClient getT BLOCK", e1);
}catch (final Exception e) {
Tracer.trace(e);
ExceptionLogger.error("RedisCacheClient get exception", e);
}finally {
if(entry != null){
entry.exit();
}
}
return null;
}
public String get(final String key) {
String result = "";
Entry entry = null;
try {
entry = SphU.entry(RedisKeyConstant.SEDIS_RESKEY);
result = this.sedis.get(key);
}catch (BlockException e1){
ExceptionLogger.error("RedisCacheClient get BLOCK", e1);
}catch (final Exception e) {
Tracer.trace(e);
ExceptionLogger.error("RedisCacheClient get exception", e);
}finally {
if(entry != null){
entry.exit();
}
}
return result;
}
public void set(final String key, final ExpireStrategy strategy, final Object object) {
final byte[] bytes = SerializeUtil.serialize(object);
Entry entry = null;
try {
entry = SphU.entry(RedisKeyConstant.SEDIS_RESKEY);
this.sedis.setex(key.getBytes(Charsets.UTF_8), strategy.activeDuration(), bytes);
} catch (BlockException e1){
ExceptionLogger.error("RedisCacheClient set BLOCK", e1);
} catch (final Exception e) {
Tracer.trace(e);
ExceptionLogger.error("RedisCacheClient set error", e);
} finally {
if(entry != null){
entry.exit();
}
}
}
}
http请求类的限流:方案是使用Filter,对每一个请求进行判断。
@Order(1)
@WebFilter(filterName = "Sentinel", urlPatterns = {"/needsentinaluri/*", "/xxx"})
@Slf4j
public class SentinelRateLimitFilter implements Filter {
@Override
public void init(final FilterConfig filterConfig) throws ServletException {
}
private static final String APP_SOURCE = "APP";
private static final String APP_URI_PREFIX = "/needsentinaluri";
@Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
throws IOException, ServletException {
final HttpServletRequest req = (HttpServletRequest) request;
String source = req.getParameter(Constants.REQ_SOURCE);
String uri = req.getRequestURI();
if (uri.startsWith(APP_URI_PREFIX)) {
source = APP_SOURCE;
}
req.setAttribute(Constants.ATTR_SOURCE, source);
req.setAttribute(Constants.ATTR_GRADE, grade);
req.setAttribute(Constants.ATTR_QRT, qrt);
//对请求参数进行处理,根据不同的参数,设置不同的资源名称以及规则。
//比如查询订单的请求,单独作为一种资源,设置的限流规则更严格,系统如果过载的话,优先对这些进行限流。
//比如下单的请求,单独作为一种资源,设置的限流规则则更宽松,系统优先保证这类功能。
//比如区分不同的渠道来源,定义为不同的资源,设置不同的限流策略,优先保证app侧的请求。
final String resource = grade + ":" + source + ":" + qrt;
Entry entry = null;
try {
entry = SphU.entry(resource);
chain.doFilter(request, response);
} catch (final BlockException e) {
Monitor.recordOne(resource + "_flow_limit");
} finally {
if (entry != null) {
entry.exit();
}
}
}
网友评论