| 
                         first节点的entry方法,实际又是执行的super的fireEntry方法,那继续把目光转移到fireEntry方法,具体如下: 
- @Override 
 -  
 - public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args) 
 -  
 - throws Throwable { 
 -  
 - if (next != null) { 
 -  
 - next.transformEntry(context, resourceWrapper, obj, count, args); 
 -  
 - } 
 -  
 - } 
 
  
 
从这里可以看到,从fireEntry方法中就开始传递执行entry了,这里会执行当前节点的下一个节点transformEntry方法,上面已经分析过了,transformEntry方法会触发当前节点的entry,也就是说fireEntry方法实际是触发了下一个节点的entry方法。具体的流程如下图所示: 
 
从图中可以看出,从最初的调用Chain的entry()方法,转变成了调用SlotChain中Slot的entry()方法。从上面的分析可以知道,SlotChain中的第一个Slot节点是NodeSelectorSlot。 
执行Slot的entry方法 
现在可以把目光转移到SlotChain中的第一个节点NodeSelectorSlot的entry方法中去了,具体的代码如下: 
- @Override 
 -  
 - public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args) 
 -  
 - throws Throwable { 
 -  
 - DefaultNode node = map.get(context.getName()); 
 -  
 - if (node == null) { 
 -  
 - synchronized (this) { 
 -  
 - node = map.get(context.getName()); 
 -  
 - if (node == null) { 
 -  
 - node = Env.nodeBuilder.buildTreeNode(resourceWrapper, null); 
 -  
 - HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size()); 
 -  
 - cacheMap.putAll(map); 
 -  
 - cacheMap.put(context.getName(), node); 
 -  
 - map = cacheMap; 
 -  
 - } 
 -  
 - // Build invocation tree 
 -  
 - ((DefaultNode)context.getLastNode()).addChild(node); 
 -  
 - } 
 -  
 - } 
 -  
 - context.setCurNode(node); 
 -  
 - // 由此触发下一个节点的entry方法 
 -  
 - fireEntry(context, resourceWrapper, node, count, args); 
 -  
 - } 
 
  
 
从代码中可以看到,NodeSelectorSlot节点做了一些自己的业务逻辑处理,具体的大家可以深入源码继续追踪,这里大概的介绍下每种Slot的功能职责: 
    - NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
 
    - ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
 
    - StatistcSlot 则用于记录,统计不同纬度的 runtime 信息;
 
    - FlowSlot 则用于根据预设的限流规则,以及前面 slot 统计的状态,来进行限流;
 
    - AuthorizationSlot 则根据黑白名单,来做黑白名单控制;
 
    - DegradeSlot 则通过统计信息,以及预设的规则,来做熔断降级;
 
    - SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量;
 
 
执行完业务逻辑处理后,调用了fireEntry()方法,由此触发了下一个节点的entry方法。此时我们就知道了sentinel的责任链就是这样传递的:每个Slot节点执行完自己的业务后,会调用fireEntry来触发下一个节点的entry方法。 
所以可以将上面的图完整了,具体如下: 
 
至此就通过SlotChain完成了对每个节点的entry()方法的调用,每个节点会根据创建的规则,进行自己的逻辑处理,当统计的结果达到设置的阈值时,就会触发限流、降级等事件,具体是抛出BlockException异常。 
总结 
sentinel主要是基于7种不同的Slot形成了一个链表,每个Slot都各司其职,自己做完分内的事之后,会把请求传递给下一个Slot,直到在某一个Slot中命中规则后抛出BlockException而终止。 
前三个Slot负责做统计,后面的Slot负责根据统计的结果结合配置的规则进行具体的控制,是Block该请求还是放行。 
控制的类型也有很多可选项:根据qps、线程数、冷启动等等。 
然后基于这个核心的方法,衍生出了很多其他的功能: 
    - 1、dashboard控制台,可以可视化的对每个连接过来的sentinel客户端 (通过发送heartbeat消息)进行控制,dashboard和客户端之间通过http协议进行通讯。
 
    - 2、规则的持久化,通过实现DataSource接口,可以通过不同的方式对配置的规则进行持久化,默认规则是在内存中的
 
    - 3、对主流的框架进行适配,包括servlet,dubbo,rRpc等
 
 
Dashboard控制台 
sentinel-dashboard是一个单独的应用,通过spring-boot进行启动,主要提供一个轻量级的控制台,它提供机器发现、单机资源实时监控、集群资源汇总,以及规则管理的功能。 
我们只需要对应用进行简单的配置,就可以使用这些功能。 
1 启动控制台 
1.1 下载代码并编译控制台 
    - 下载 控制台 工程
 
    - 使用以下命令将代码打包成一个 fat jar: mvn cleanpackage
 
 
1.2 启动 
使用如下命令启动编译后的控制台: 
$ java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -jar target/sentinel-dashboard.jar 
上述命令中我们指定了一个JVM参数, -Dserver.port=8080 用于指定 Spring Boot 启动端口为 8080。 
2 客户端接入控制台 
控制台启动后,客户端需要按照以下步骤接入到控制台。 
2.1 引入客户端jar包 
通过 pom.xml 引入 jar 包: 
- <dependency> 
 -  
 - <groupId>com.alibaba.csp</groupId> 
 -  
 - <artifactId>sentinel-transport-simple-http</artifactId> 
 -  
 - <version>x.y.z</version> 
 -  
 - </dependency> 
 
  
 
2.2 配置启动参数 
启动时加入 JVM 参数 -Dcsp.sentinel.dashboard.server=consoleIp:port 指定控制台地址和端口。若启动多个应用,则需要通过 -Dcsp.sentinel.api.port=xxxx 指定客户端监控 API 的端口(默认是 8719)。 
除了修改 JVM 参数,也可以通过配置文件取得同样的效果。更详细的信息可以参考 启动配置项。 
2.3 触发客户端初始化 
确保客户端有访问量,Sentinel 会在客户端首次调用的时候进行初始化,开始向控制台发送心跳包。 
sentinel-dashboard是一个独立的web应用,可以接受客户端的连接,然后与客户端之间进行通讯,他们之间使用http协议进行通讯。他们之间的关系如下图所示: 
 
dashboard 
dashboard启动后会等待客户端的连接,具体的做法是在 MachineRegistryController 中有一个 receiveHeartBeat 的方法,客户端发送心跳消息,就是通过http请求这个方法。 
dashboard接收到客户端的心跳消息后,会把客户端的传递过来的ip、port等信息封装成一个 MachineInfo对象,然后将该对象通过 MachineDiscovery 接口的 addMachine 方法添加到一个ConcurrentHashMap中保存起来。 
这里会有问题,因为客户端的信息是保存在dashboard的内存中的,所以当dashboard应用重启后,之前已经发送过来的客户端信息都会丢失掉。 
client 
client在启动时,会通过CommandCenterInitFunc选择一个,并且只选择一个CommandCenter进行启动。 
启动之前会通过spi的方式扫描获取到所有的CommandHandler的实现类,然后将所有的CommandHandler注册到一个HashMap中去,待后期使用。 
PS:考虑一下,为什么CommandHandler不需要做持久化,而是直接保存在内存中。 
注册完CommandHandler之后,紧接着就启动CommandCenter了,目前CommandCenter有两个实现类: 
    - SimpleHttpCommandCenter 通过ServerSocket启动一个服务端,接受socket连接
 
    - NettyHttpCommandCenter 通过Netty启动一个服务端,接受channel连接
 
 
CommandCenter启动后,就等待dashboard发送消息过来了,当接收到消息后,会把消息通过具体的CommandHandler进行处理,然后将处理的结果返回给dashboard。 
这里需要注意的是,dashboard给client发送消息是通过异步的httpClient进行发送的,在HttpHelper类中。 
但是诡异的是,既然通过异步发送了,又通过一个CountDownLatch来等待消息的返回,然后获取结果,那这样不就失去了异步的意义的吗?具体的代码如下: 
- private String httpGetContent(String url) { final HttpGet httpGet = new HttpGet(url); final CountDownLatch latch = new CountDownLatch(1); 
 - final AtomicReference<String> reference = new AtomicReference<>(); 
 -  
 - httpclient.execute(httpGet, new FutureCallback<HttpResponse>() { 
 -  
 - @Override 
 -  
 - public void completed(final HttpResponse response) { 
 -  
 - try { 
 -  
 - reference.set(getBody(response)); 
 -  
 - } catch (Exception e) { 
 -  
 - logger.info("httpGetContent " + url + " error:", e); 
 -  
 - } finally { 
 -  
 - latch.countDown(); 
 -  
 - } 
 -  
 - } 
 -  
 - @Override 
 -  
 - public void failed(final Exception ex) { 
 -  
 - latch.countDown(); 
 -  
 - logger.info("httpGetContent " + url + " failed:", ex); 
 -  
 - } 
 -  
 - @Override 
 -  
 - public void cancelled() { 
 -  
 - latch.countDown(); 
 -  
 - } 
 -  
 - }); 
 -  
 - try { 
 -  
 - latch.await(5, TimeUnit.SECONDS); 
 -  
 - } catch (Exception e) { 
 -  
 - logger.info("wait http client error:", e); 
 -  
 - } 
 -  
 - return reference.get(); 
 -  
 - } 
 
  
主流框架的适配 
sentinel也对一些主流的框架进行了适配,使得在使用主流框架时,也可以享受到sentinel的保护。目前已经支持的适配器包括以下这些: 
    - Web Servlet
 
    - Dubbo
 
    - Spring Boot / Spring Cloud
 
    - gRPC
 
    - Apache RocketMQ
 
 
其实做适配就是通过那些主流框架的扩展点,然后在扩展点上加入sentinel限流降级的代码即可。拿Servlet的适配代码看一下,具体的代码是: 
- public class CommonFilter implements Filter { 
 -  
 - @Override 
 -  
 - public void init(FilterConfig filterConfig) { 
 -  
 - } 
 -  
 - @Override 
 -  
 - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
 -  
 - throws IOException, ServletException 
 -  
 - HttpServletRequest sRequest = (HttpServletRequest)request; 
 -  
 - Entry entry = null; 
 -  
 - try { 
 -  
 - // 根据请求生成的资源 
 -  
 - String target = FilterUtil.filterTarget(sRequest); 
 -  
 - target = WebCallbackManager.getUrlCleaner().clean(target); 
 -  
 - // “申请”该资源 
 -  
 - ContextUtil.enter(target); 
 -  
 - entry = SphU.entry(target, EntryType.IN); 
 -  
 - // 如果能成功“申请”到资源,则说明未被限流 
 -  
 - // 则将请求放行 
 -  
 - chain.doFilter(request, response); 
 -  
 - } catch (BlockException e) { 
 -  
 - // 否则如果捕获了BlockException异常,说明请求被限流了 
 -  
 - // 则将请求重定向到一个默认的页面 
 -  
 - HttpServletResponse sResponse = (HttpServletResponse)response; 
 -  
 - WebCallbackManager.getUrlBlockHandler().blocked(sRequest, sResponse); 
 -  
 - } catch (IOException e2) { 
 -  
 - // 省略部分代码 
 -  
 - } finally { 
 -  
 - if (entry != null) { 
 -  
 - entry.exit(); 
 -  
 - } 
 -  
 - ContextUtil.exit(); 
 -  
 - } 
 -  
 - } 
 -  
 - @Override 
 -  
 - public void destroy() { 
 -  
 - } 
 -  
 - } 
 
  
 
通过Servlet的Filter进行扩展,实现一个Filter,然后在doFilter方法中对请求进行限流控制,如果请求被限流则将请求重定向到一个默认页面,否则将请求放行给下一个Filter。 
规则持久化,动态化 
Sentinel 的理念是开发者只需要关注资源的定义,当资源定义成功,可以动态增加各种流控降级规则。 
Sentinel 提供两种方式修改规则: 
    - 通过 API 直接修改 ( loadRules)
 
    - 通过 DataSource适配不同数据源修改
 
 
通过 API 修改比较直观,可以通过以下三个 API 修改不同的规则: 
FlowRuleManager.loadRules(List<FlowRule> rules); // 修改流控规则 
DegradeRuleManager.loadRules(List<DegradeRule> rules); // 修改降级规则                         (编辑:91站长网) 
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! 
                      |