原创 吴就业 139 0 2020-09-22
本文为博主原创文章,未经博主允许不得转载。
本文链接:https://wujiuye.com/article/1175deea57d94348adcbd59c8f91d277
作者:吴就业
链接:https://wujiuye.com/article/1175deea57d94348adcbd59c8f91d277
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。
Sentinel中指标数据统计以资源为维度。资源使用ResourceWrapper对象表示,我们把ResourceWrapper对象称为资源ID。如果一个资源描述的是一个接口,那么资源名称通常就是接口的url,例如“GET:/v1/demo”。
public abstract class ResourceWrapper {
protected final String name;
protected final EntryType entryType;
protected final int resourceType;
public ResourceWrapper(String name, EntryType entryType, int resourceType) {
this.name = name;
this.entryType = entryType;
this.resourceType = resourceType;
}
}
ResourceWrapper有三个字段:
EntryType是一个枚举类型:
public enum EntryType {
IN("IN"),
OUT("OUT");
}
可以把IN和OUT简单理解为接收处理请求与发送请求。当接收到别的服务或者前端发来的请求,那么entryType为IN;当向其他服务发起请求时,那么entryType就为OUT。例如,在消费端向服务提供者发送请求,当请求失败率达到多少时触发熔断降级,那么服务消费端为实现熔断降级就需要统计资源的OUT类型流量。
Sentinel目前支持的资源类型有以下几种:
public final class ResourceTypeConstants {
public static final int COMMON = 0;
public static final int COMMON_WEB = 1;
public static final int COMMON_RPC = 2;
public static final int COMMON_API_GATEWAY = 3;
public static final int COMMON_DB_SQL = 4;
}
Node用于持有实时统计的指标数据,Node接口定义了一个Node类所需要提供的各项指标数据统计的相关功能,为外部屏蔽滑动窗口的存在。提供记录请求被拒绝、请求被放行、请求处理异常、请求处理成功的方法,以及获取当前时间窗口统计的请求总数、平均耗时等方法。
Node接口源码如下。
public interface Node extends OccupySupport, DebugSupport {
long totalRequest(); // 获取总的请求数
long totalPass(); // 获取通过的请求总数
long totalSuccess(); // 获取成功的请求总数
long blockRequest(); // 获取被Sentinel拒绝的请求总数
long totalException(); // 获取异常总数
double passQps(); // 通过qps
double blockQps(); // 拒绝QPS
double totalQps(); // 总qps
double successQps(); // 成功qps
double maxSuccessQps(); // 最大成功总数qps(例如秒级滑动窗口的数组大小默认配置为2,则取数组中最大)
double exceptionQps(); // 异常qps
double avgRt(); // 平均耗时
double minRt(); // 最小耗时
int curThreadNum(); // 当前并发占用的线程数
double previousBlockQps(); // 前一个时间窗口的被拒绝qps
double previousPassQps(); // 前一个时间窗口的通过qps
Map<Long, MetricNode> metrics();
List<MetricNode> rawMetricsInMin(Predicate<Long> timePredicate);
void addPassRequest(int count); // 添加通过请求数
void addRtAndSuccess(long rt, int success); // 添加成功请求数,并且添加处理成功的耗时
void increaseBlockQps(int count); // 添加被拒绝的请求数
void increaseExceptionQps(int count); // 添加异常请求数
void increaseThreadNum(); // 自增占用线程
void decreaseThreadNum(); // 自减占用线程
void reset(); // 重置滑动窗口
}
它的几个实现类:DefaultNode、ClusterNode、EntranceNode、StatisticNode的关系如下图所示。
Statistic即统计的意思,StatisticNode是Node接口的实现类,是实现实时指标数据统计Node。
public class StatisticNode implements Node {
// 秒级滑动窗口,2个时间窗口大小为500毫秒的Bucket
private transient volatile Metric rollingCounterInSecond = new ArrayMetric(2,1000);
// 分钟级滑动窗口,60个Bucket数组,每个Bucket统计的时间窗口大小为1秒
private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false);
// 统计并发使用的线程数
private LongAdder curThreadNum = new LongAdder();
}
如代码所示,一个StatisticNode包含一个秒级和一个分钟级的滑动窗口,以及并行线程数计数器。秒级滑动窗口用于统计实时的QPS,分钟级的滑动窗口用于保存最近一分钟内的历史指标数据,并行线程计数器用于统计当前并行占用的线程数。
StatisticNode的分钟级和秒级滑动窗口统计的指标数据分别有不同的用处。例如,StatisticNode记录请求成功和请求执行耗时的方法中调用了两个滑动窗口的对应指标项的记录方法,代码如下:
@Override
public void addRtAndSuccess(long rt, int successCount) {
// 秒级滑动窗口
rollingCounterInSecond.addSuccess(successCount);
rollingCounterInSecond.addRT(rt);
// 分钟级滑动窗口
rollingCounterInMinute.addSuccess(successCount);
rollingCounterInMinute.addRT(rt);
}
获取前一秒被Sentinel拒绝的请求总数从分钟级滑动窗口获取,代码如下:
@Override
public double previousBlockQps() {
return this.rollingCounterInMinute.previousWindowBlock();
}
而获取当前一秒内已经被Sentinel拒绝的请求总数则从秒级滑动窗口获取,代码如下:
@Override
public double blockQps() {
return rollingCounterInSecond.block() / rollingCounterInSecond.getWindowIntervalInSec();
}
获取最小耗时也是从秒级的滑动窗口取的,代码如下:
@Override
public double minRt() {
// 秒级滑动窗口
return rollingCounterInSecond.minRt();
}
由于方法比较多,这里就不详细介绍每个方法的实现了。
StatisticNode还负责统计并行占用的线程数,用于实现信号量隔离,按资源所能并发占用的最大线程数实现限流。当接收到一个请求时就将curThreadNum自增1,当处理完请求时就将curThreadNum自减一,如果同时处理10个请求,那么curThreadNum的值就为10。
假设我们配置tomcat处理请求的线程池大小为200,通过控制并发线程数实现信号量隔离的好处就是不让一个接口同时使用完这200个线程,避免因为一个接口响应慢将200个线程都阻塞导致应用无法处理其他请求的问题,这也是实现信号量隔离的目的。
DefaultNode是实现以资源为维度的指标数据统计的Node,是将资源ID和StatisticNode映射到一起的Node。
public class DefaultNode extends StatisticNode {
private ResourceWrapper id;
private volatile Set<Node> childList = new HashSet<>();
private ClusterNode clusterNode;
public DefaultNode(ResourceWrapper id, ClusterNode clusterNode) {
this.id = id;
this.clusterNode = clusterNode;
}
}
如代码所示,DefaultNode是StatisticNode的子类,构造方法要求传入资源ID,表示该Node用于统计哪个资源的实时指标数据,指标数据统计则由父类StatisticNode实现。
DefaultNode字段说明:
我们回顾下Sentinel的基本使用:
ContextUtil.enter("上下文名称,例如:sentinel_spring_web_context");
Entry entry = null;
try {
entry = SphU.entry("资源名称,例如:/rpc/openfein/demo", EntryType.IN (或者EntryType.OUT));
// 执行业务方法
return doBusiness();
} catch (Exception e) {
if (!(e instanceof BlockException)) {
Tracer.trace(e);
}
throw e;
} finally {
if (entry != null) {
entry.exit(1);
}
ContextUtil.exit();
}
如上代码所示,doBusiness业务方法被Sentinel保护,当doBusiness方法被多层保护时,就可能对同一个资源创建多个DefaultNode。一个资源可能有多个DefaultNode,是否有多个DefaultNode取决于是否存在多个名称不同的Context,即入口不同。这样做的目的可实现按不同调用链路(不同入口)对资源采取不同的流量控制策略。这部分内容在介绍NodeSelectorSlot时再作详细介绍。
Sentinel使用ClusterNode统计每个资源全局的指标数据,以及统计该资源按调用来源区分的指标数据。全局数据指的是不区分调用链路,一个资源ID只对应一个ClusterNode;区分来源是区分每个上游服务的调用情况。
public class ClusterNode extends StatisticNode {
// 资源名称
private final String name;
// 资源类型
private final int resourceType;
// 来源指标数据统计
private Map<String, StatisticNode> originCountMap = new HashMap<>();
// 控制并发修改originCountMap用的锁
private final ReentrantLock lock = new ReentrantLock();
public ClusterNode(String name, int resourceType) {
this.name = name;
this.resourceType = resourceType;
}
}
ClusterNode字段说明:
EntranceNode是一个特殊的Node,它继承DefaultNode,用于维护一颗树,从根节点到每个叶子节点都是不同请求的调用链路,所经过的每个节点都对应着调用链路上被Sentinel保护的资源,一个请求调用链路上的节点顺序正是资源被访问的顺序。
public class EntranceNode extends DefaultNode {
public EntranceNode(ResourceWrapper id, ClusterNode clusterNode) {
super(id, clusterNode);
}
}
在一个web mvc应用中,每个接口就是一个资源,Sentinel通过spring mvc拦截器拦截每个接口的入口,统一创建名为“sentinel_spring_web_context”的Context,名称相同的Context都使用同一个EntranceNode。一个web应用可能有多个接口,而childList就用于存储每个接口对应的DefaultNode。
如果想统计一个应用的所有接口(不一定是所有,没有被调用过的接口不会创建对应的DefaultNode)总的QPS,只需要调用EntranceNode#totalQps就能获取到。EntranceNode#totalQps方法代码如下。
@Override
public double totalQps() {
double r = 0;
// 遍历childList
for (Node node : getChildList()) {
r += node.totalQps();
}
return r;
}
EntranceNode、DefaultNode、ClusterNode与滑动窗口的关系如下图所示:
理解Context与Entry也是理解Sentinel整个工作流程的关键,其中Entry还会涉及到“调用树”这一概念。
Context 代表调用链路上下文,贯穿一次调用链路中的所有 Entry。Context 维持着入口节点(entranceNode)、本次调用链路的 curNode、调用来源(origin)等信息。Context 名称即为调用链路入口名称。
Context通过 ThreadLocal传递,只在调用链路的入口处创建。
假设服务B提供一个查询天气预报的接口给服务A调用,服务B实现查询天气预报的接口是调用第三方服务C实现的,服务B是一个mvc应用,同时服务B调用服务C接口使用OpenFeign实现RPC调用。那么,服务B即使用了Sentinel的mvc适配模块,也使用了Sentinel的OpenFeign适配模块。
当服务B接收到服务A请求时,会创建一个名为“sentinel_spring_web_context”的Context,服务B在向服务C发起接口调用时由于当前线程已经存在一个Context,所以还是用“sentinel_spring_web_context”这个Context,代表是同一个调用链路入口。
举个不恰当的例子:
那么,每次调用A.a()方法都会创建名为”a_context”的Context,每次调用B.b()方法都会创建名为“b_context”的Context。如果A.a()同时有20个请求,那么就会创建20个名为“a_context”的Context,Context代表了这20个请求每个请求的调用链路上下文,而“路径一”就是这20个请求相同的调用链路。
Context的字段定义如下。
public class Context {
private final String name;
private DefaultNode entranceNode;
private Entry curEntry;
private String origin = "";
// 我们不讨论异步的情况
// private final boolean async;
}
在调用Context#getCurNode方法获取调用链路上当前访问到的资源的DefaultNode时,实际是从Context#curEntry获取的,Entry维护了当前资源的DefaultNode,以及调用来源的StatisticNode。Entry抽象类字段的定义如下。
public abstract class Entry implements AutoCloseable {
private static final Object[] OBJECTS0 = new Object[0];
private long createTime;
// 当前节点(DefaultNode)
private Node curNode;
// 来源节点
private Node originNode;
private Throwable error;
// 资源
protected ResourceWrapper resourceWrapper;
}
CtEntry是Entry的直接子类,后面分析源码时,我们所说Entry皆指CtEntry。CtEntry中声明的字段信息如下代码所示。
class CtEntry extends Entry {
// 当前Entry指向的父Entry
protected Entry parent = null;
// 父Entry指向当前Entry
protected Entry child = null;
// 当前资源的ProcessorSlotChain
protected ProcessorSlot<Object> chain;
// 当前上下文
protected Context context;
}
CtEntry用于维护父子Entry,每一次调用SphU#entry方法都会创建一个CtEntry。如果服务B在处理一个请求的路径上会多次调用SphU#entry,那么这些CtEntry会构成一个双向链表。在每次创建CtEntry,都会将Context.curEntry设置为这个新的CtEntry,双向链表的作用就是在调用CtEntry#exit方法时,能够将Context.curEntry还原为上一个资源的CtEntry。
例如,在服务B接收到服务A的请求时,会调用SphU#entry方法创建一个CtEntry,我们取个代号ctEntry1,此时的ctEntry1的父节点(parent)为空。当服务B向服务C发起调用时,OpenFeign适配器调用SphU#entry的方法会创建一个CtEntry,我们取个代号ctEntry2,此时ctEntry2的父节点(parent)就是ctEntry1,ctEntry1的子节点(child)就是ctEntry2,如下图所示。
Constants常量类用于声明全局静态常量,Constants有一个ROOT静态字段,类型为EntranceNode。
在调用ContextUtil#enter方法时,如果还没有为当前入口创建EntranceNode,则会为当前入口创建EntranceNode,将其赋值给Context.entranceNode,同时也会将这个EntranceNode添加到Constants.ROOT的子节点(childList)。资源对应的DefaultNode则是在NodeSelectorSlot中创建,并赋值给Context.curEntry.curNode。
Constants.ROOT、Context.entranceNode与Entry.curNode三者关系如下图所示。
ProcessorSlot直译就是处理器插槽,是Sentinel实现限流降级、熔断降级、系统自适应降级等功能的切入点。Sentinel提供的ProcessorSlot可以分为两类,一类是辅助完成资源指标数据统计的切入点,一类是实现降级功能的切入点。
辅助资源指标数据统计的ProcessorSlot:
实现降级功能的ProcessorSlot:
关于每个ProcessorSlot实现的功能,将在后续文章详细分析。
参考文献:
声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。
本篇内容介绍如何使用r2dbc-mysql驱动程序包与mysql数据库建立连接、使用r2dbc-pool获取数据库连接、Spring-Data-R2DBC增删改查API、事务的使用,以及R2DBC Repository。
消息推送服务主要是处理同步给用户推送短信通知或是异步推送短信通知、微信模板消息通知等。本篇介绍如何使用Spring WebFlux + R2DBC搭建消息推送服务。
IDEA有着极强的扩展功能,它提供插件扩展支持,让开发者能够参与到IDEA生态建设中,为更多开发者提供便利、提高开发效率。我们常用的插件有Lombok、Mybatis插件,这些插件都大大提高了我们的开发效率。即便IDEA功能已经很强大,并且也已有很多的插件,但也不可能面面俱到,有时候我们需要自给自足。
Instrumentation之所以难驾驭,在于需要了解Java类加载机制以及字节码,一不小心就能遇到各种陌生的Exception。笔者在实现Java探针时就踩过不少坑,其中一类就是类加载相关的问题,也是本篇所要跟大家分享的。
订阅
订阅新文章发布通知吧,不错过精彩内容!
输入邮箱,提交后我们会给您发送一封邮件,您需点击邮件中的链接完成订阅设置。