加载过程
以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的存储结构,他通过固定的算法,可以算出任意时间在定长容器中的索引位置,获取当前时间对应索引处数据,为空则直接构建新的时间窗口放入,存在则比对窗口起始时间,相等,则继续累加计数,不相等(肯定是比之前大),则新建时间窗口覆盖原始数据重新计数。