从sentinel源码浅析工作原理和统计实现

加载过程

以SpringCloud使用sentinel为例,cloud项目需要引入spring-cloud-starter-alibaba-sentinel,解析spring-cloud-starter-alibaba-sentinel可以找到spring.factories(SpringBoot扩展机制,项目启动时会扫描它)

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.sentinel.SentinelWebAutoConfiguration,\
com.alibaba.cloud.sentinel.SentinelWebFluxAutoConfiguration,\
com.alibaba.cloud.sentinel.endpoint.SentinelEndpointAutoConfiguration,\
com.alibaba.cloud.sentinel.custom.SentinelAutoConfiguration,\
com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration

org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker=\
com.alibaba.cloud.sentinel.custom.SentinelCircuitBreakerConfiguration

可以看到,其中EnableAutoConfiguration装配了5个配置类,而从字面意思可以发现SentinelWebAutoConfiguration是对web环境自动配置的支持,找到SentinelWebAutoConfiguration阅读代码,主要看注释部分:

@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(CommonFilter.class)
@ConditionalOnProperty(name = "spring.cloud.sentinel.enabled", matchIfMissing = true)
@EnableConfigurationProperties(SentinelProperties.class)
public class SentinelWebAutoConfiguration {
	private static final Logger log = LoggerFactory
			.getLogger(SentinelWebAutoConfiguration.class);
	@Autowired
	private SentinelProperties properties;
	@Autowired
	private Optional<UrlCleaner> urlCleanerOptional;
	@Autowired
	private Optional<UrlBlockHandler> urlBlockHandlerOptional;
	@Autowired
	private Optional<RequestOriginParser> requestOriginParserOptional;
	@PostConstruct
	public void init() {
		urlBlockHandlerOptional.ifPresent(WebCallbackManager::setUrlBlockHandler);
        //如果UrlCleaner存在,将其set到WebCallbackManager中
		urlCleanerOptional.ifPresent(WebCallbackManager::setUrlCleaner);
		requestOriginParserOptional.ifPresent(WebCallbackManager::setRequestOriginParser);
	}
    //装配了一个FilterRegistrationBean
	@Bean
	@ConditionalOnProperty(name = "spring.cloud.sentinel.filter.enabled", matchIfMissing = true)
	public FilterRegistrationBean sentinelFilter() {
		FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
		SentinelProperties.Filter filterConfig = properties.getFilter();
		if (filterConfig.getUrlPatterns() == null
				|| filterConfig.getUrlPatterns().isEmpty()) {
			List<String> defaultPatterns = new ArrayList<>();
            //拦截所有请求
			defaultPatterns.add("/*");
			filterConfig.setUrlPatterns(defaultPatterns);
		}
		registration.addUrlPatterns(filterConfig.getUrlPatterns().toArray(new String[0]));
        //注册一个CommonFilter
		Filter filter = new CommonFilter();
		registration.setFilter(filter);
		registration.setOrder(filterConfig.getOrder());
		registration.addInitParameter("HTTP_METHOD_SPECIFY",
				String.valueOf(properties.getHttpMethodSpecify()));
		return registration;

	}

主要有两关注点,一个是初始化时,判断了是否存在UrlCleaner,如果存在,将他放入了WebCallbackManager,这个类之后会用到(url清洗是sentinel的基础功能之一,简单说就是请求的路径中携带了一些参数,比如{id}等,这样每次请求路径都会有所不同,但从统计限流降级的需要上来讲,他们应该是一个请求,所有就需要对请求进行清洗,将资源进行统一);

另一个是注册了一个过滤器,适用范围是所有请求,这里就是sentinel逻辑开始的地方。

工作原理

根据上边的代码可知,所有对项目的请求都会进入CommonFilter的doFilter方法:

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest sRequest = (HttpServletRequest) request;
        Entry urlEntry = null;

        try {
            String target = FilterUtil.filterTarget(sRequest);
            //从WebCallbackManager获取UrlCleaner
            UrlCleaner urlCleaner = WebCallbackManager.getUrlCleaner();
            if (urlCleaner != null) {
                //url清洗
                target = urlCleaner.clean(target);
            }
            if (!StringUtil.isEmpty(target)) {
                String origin = parseOrigin(sRequest);
                String contextName = webContextUnify ? WebServletConfig.WEB_SERVLET_CONTEXT_NAME : target;
                ContextUtil.enter(contextName, origin);
                //使用SphU.entry对当前的url限流埋点
                //SphU.entry往下执行,最终会进入Sph实现类CtSph的entryWithPriority方法
                if (httpMethodSpecify) {
                    String pathWithHttpMethod = sRequest.getMethod().toUpperCase() + COLON + target;
                    urlEntry = SphU.entry(pathWithHttpMethod, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
                } else {
                    urlEntry = SphU.entry(target, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
                }
            }
            chain.doFilter(request, response);
        } catch (BlockException e) {
            HttpServletResponse sResponse = (HttpServletResponse) response;
            WebCallbackManager.getUrlBlockHandler().blocked(sRequest, sResponse, e);
        } catch (IOException | ServletException | RuntimeException e2) {
            Tracer.traceEntry(e2, urlEntry);
            throw e2;
        } finally {
            if (urlEntry != null) {
                urlEntry.exit();
            }
            ContextUtil.exit();
        }
    }

这里从WebCallbackManager获取UrlCleaner,如果存在,先对请求资源进行清洗整合,然后在SphU.entry执行逻辑,向代码内追踪,最终在CtSph类中找到如下代码:

    private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
        throws BlockException {
        Context context = ContextUtil.getContext();
        if (context instanceof NullContext) {
            return new CtEntry(resourceWrapper, null, context);
        }
        if (context == null) {
            context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
        }
        if (!Constants.ON) {
            return new CtEntry(resourceWrapper, null, context);
        }
        //赋值
        ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
        if (chain == null) {
            return new CtEntry(resourceWrapper, null, context);
        }
        Entry e = new CtEntry(resourceWrapper, chain, context);
        try {
        //执行
            chain.entry(context, resourceWrapper, null, count, prioritized, args);
        } catch (BlockException e1) {
            e.exit(count, args);
            throw e1;
        } catch (Throwable e1) {
            RecordLog.info("Sentinel unexpected exception", e1);
        }
        return e;
    }

先调用lookProcessChain对chain赋值,然后调用chain.entry,先看lookProcessChain内创建chain的方法,最后调用了DefaultSlotChainBuilder的build方法:

public class DefaultSlotChainBuilder implements SlotChainBuilder {
    @Override
    public 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 AuthoritySlot());
        //系统总流量控制
        chain.addLast(new SystemSlot());
        //限流控制
        chain.addLast(new FlowSlot());
        //熔断降级
        chain.addLast(new DegradeSlot());
        return chain;
    }
}

可以看到,方法中将各个Slot插槽按顺序添加到了chain中,这就是sentinel的责任链模式,每个Slot插槽聚焦于各自的功能,然后将不同的功能组合在了一起:

NodeSelectorSlot:以调用路径构建统计对象。

ClusterBuilderSlot:以资源名构建统计对象。

LogSlot:顾名思义,记录日志的。

StatisticSlot:统计不同维度的各种信息,请求数、通过数、限流数、线程数等等。

AuthoritySlot:权限控制,支持黑名单和白名单。

SystemSlot:系统总的流量控制。

FlowSlot:根据各个请求节点的限流规则和统计数据进行限流。

DegradeSlot:根据各个请求节点的降级规则和统计数据进行降级。 

在CtSph的entryWithPriority方法中看到过,先对chain进行了赋值,然后调用了chain.entry方法,这个方法的调用会经过各个Slot的entry方法。

统计原理

无论是限流还是降级,都离不开各个请求的数据统计,这里主要看一下sentinel的统计原理。

看StatisticSlot的entry方法中有:

node.addPassRequest(count);

这里是对节点通过的请求进行了计数:

    @Override
    public void addPassRequest(int count) {
        //秒计数默认1秒
        rollingCounterInSecond.addPass(count);
        //分钟计数默认1分钟
        rollingCounterInMinute.addPass(count);
    }

这两个对象其实都是同一个类ArrayMetric的实例对象,其中addPass代码:

    @Override
    public void addPass(int count) {
        WindowWrap<MetricBucket> wrap = data.currentWindow();
        wrap.value().addPass(count);
    }

data是ArrayMetric类的内部属性,是一个LeapArray<MetricBucket>对象,这里先简单说一下他的数据结构,可以理解为一个存放时间段的容器,但他不是随时间不断增长的(内存也承受不住),而是一个定长的容器,具体的实现,看代码中调用的currentWindow方法就可以略知一二,currentWindow顾名思义,获取当前的时间窗口,代码和注释:

    public WindowWrap<T> currentWindow(long timeMillis) {
        if (timeMillis < 0) {
            return null;
        }
        //通过算法获取当前时间对应的窗口索引
        int idx = calculateTimeIdx(timeMillis);
        //每个窗口都有记录窗口的开始时间,当前时间-(通过当前时间%窗口时间跨度)求得
        long windowStart = calculateWindowStart(timeMillis);
        while (true) {
            //根据索引获取窗口
            WindowWrap<T> old = array.get(idx);
            //如果窗口不存在创建窗口
            if (old == null) {
                WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
                if (array.compareAndSet(idx, null, window)) {
                    return window;
                } else {
                    //释放cpu避免长时间占用
                    Thread.yield();
                }
            //如果窗口存在且窗口的开始时间相等,则直接返回
            } else if (windowStart == old.windowStart()) {
                return old;
            //如果窗口存在,但开始时间小于当前开始时间,则覆盖并返回
            } else if (windowStart > old.windowStart()) {
                if (updateLock.tryLock()) {
                    try {
                        return resetWindowTo(old, windowStart);
                    } finally {
                        updateLock.unlock();
                    }
                } else {
                    Thread.yield();
                }
            //新的时间小于旧时间,理论上不可能
            } else if (windowStart < old.windowStart()) {
                return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
            }
        }
    }

从上述代码和注释也能看出,LeapArray的存储结构,他通过固定的算法,可以算出任意时间在定长容器中的索引位置,获取当前时间对应索引处数据,为空则直接构建新的时间窗口放入,存在则比对窗口起始时间,相等,则继续累加计数,不相等(肯定是比之前大),则新建时间窗口覆盖原始数据重新计数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值