IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> Sentinel -> 正文阅读

[系统运维]Sentinel

Sentinel控制台

Sentinel是一个限流保护组件,而这个组件和Hystrix相比还有一定的优势:有一个完整的控制台,而且整个的应用可以直接部。Hystrix控制台:在使用的过程里面仅仅是做为一个流量的监控,也不能进行各种神奇的配置,Hystrix还需要自己搭建一系列的服务,但是这些问题在Sentinel组件里面就全部解决。
如果要想使用Sentinel组件进行开发,那么一定需要进行服务的部署,本次依然要通过Linux系统进行服务部署。

1、【GITHUB】 Sentinel组件可以通过GITHUB直接下载打包完成,或者是依据开发者自己的需要进行应用的编译(通过下载的源代码进行组件的编译,从而得到一个部署的应用)。
git clone https://github.com/alibaba/Sentinel
在这里插入图片描述
2、【本地系统】既然要进行服务的部署就需要创有一个虚拟机,本次的虚拟机配置名称如下

192.168.190.160 sentinel-server

3、【sentinel-server主机】新的主机需要根据当前的应用环境进行服务的相关配置:

打开网卡配置文件: vi/etc/sysconfig/network-scripts/ifcfg-ens33
修改网卡的IP地址: IPADDR=192.168.190.160
修改本机的名称: vi/etc/hostname
修改主机映射列表: vi /etc/hosts
重新启动虚拟机: reboot

4、【sentinel-server主机】进入到“/usr/local/src”目录: cd /usr/local/src;

5、【sentinel-server主机】下载Sentinel打包后的应用:

wget https://github.com/alibaba/Sentinel/releases/download/1.8.1/sentinel-dashboard-1.8.1.jar

6、【sentinel-server主机】 Sentinel使用的是SpringBoot 技术开发的,所以这个开发包可以直接使用,这个时候就需要查看一下GITHUB上所给出的文档信息了,文档路径:

https://github.com/alibaba/Sentinel/wiki/控制台

7、【sentinel-server主机】启动当前的Sentinel应用:

前台启动
java -Dserver.port=8888 -Dcsp.sentinel.dashboard.server=localhost:8888 -Dproject.name=sentinel-dashboard -Dsentinel.dashboard.auth.username=muyan -Dsentinel.dashboard.auth.password=yootk -jar /usr/local/src/sentinel-dashboard-1.8.1.jar 
后台启动
java -Dserver.port=8888 -Dcsp.sentinel.dashboard.server=localhost:8888 -Dproject.name=sentinel-dashboard -Dsentinel.dashboard.auth.username=muyan -Dsentinel.dashboard.auth.password=yootk -jar /usr/local/src/sentinel-dashboard-1.8.1.jar > /usr/local/src/sentinel.log 2>&1 &

8、【sentinel-server主机】查看当前系统的端口占用:

netstat -nptl

在这里插入图片描述

8888是当前设置的Sentinel管理控制台的端口(WEB用户访问的),而现在所给出的8719端口就是未来微服务与Sentinel组件对应的操作端口。

9、【sentinel-server主机】修改防火墙规则,添加新的端口访问


添加访问规则
firewall-cmd --zone=public --add-port=8888/tcp --permanent
firewall-cmd --zone=public --add-port=8719/tcp --permanent

重新加载配置
firewall-cmd --reload

Sentinel资源监控

如果现在要想进行服务的整合处理,肯定要进行微服务的配置修改,同时引入所需要的相关的依赖库组件。

1、【microcloud项目】修改build.gradle配置文件,进行Sentinel依赖的配置,本次先在“provider-dept-8001”的子模块之中进行依赖库的配置

project(":provider-dept-8001") {    // 部门微服务
    dependencies {
        implementation(project(":common-api")) // 导入公共的子模块
        implementation(libraries.'mybatis-plus-boot-starter')
        implementation(libraries.'mysql-connector-java')
        implementation(libraries.'druid')
        implementation(libraries.'springfox-boot-starter')
        implementation('org.springframework.boot:spring-boot-starter-security')
        implementation('com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel')
        // 以下的依赖库为Nacos注册中心所需要的依赖配置
        implementation('com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery') {
            exclude group: 'com.alibaba.nacos', module: 'nacos-client' // 移除旧版本的Nacos依赖
        }
        implementation('com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-config') {
            exclude group: 'com.alibaba.nacos', module: 'nacos-client' // 移除旧版本的Nacos依赖
        }
        implementation(libraries.'nacos-client') // 引入与当前的Nacos匹配的依赖库
    }
}

2、【provider-dept-8001】application.yml

spring:
  application: # 配置应用信息
    name: dept.provider # 是微服务的名称
  cloud: # Cloud配置
    sentinel: # 监控配置
      transport: # 传输配置
        port: 8719 # Sentinel组件启用之后默认会启动一个8719端口
        dashboard: sentinel-server:8888 # 控制台地址

Sentinel限流保护

现在为止所能够见到的Sentinel使用全部都在流量统计上了(因为没有统计的信息就一定没有保护的配置),所以在使用Sentinel过程里面还可以实现限流保护(限流本质上就是针对于一个操作进行一些访问级别的设置,例如:这一个操作每秒只允许10个用户访问,这种操作机制就是限流处理),这种操作机制全部都是在Sentinel控制台里面完成的。
1、【Postman工具】为了便于观察限流的操作,建议先将一些方法多执行几次。
在这里插入图片描述
只有执行了某些访问的路径之后,才可以见到所谓的簇点链路,而有了这些簇点链路信息,才可以进行流控、熔断、热点、授权等限流控制。﹒

1、流控规则
流量控制(Flow Control)主要是监控保护资源流量的QPS(Query per Second、每秒查询率)或者并发线程数量,当指定的监控指标达到阈值时对流量进行控制,以避免被瞬时流量高峰冲垮,从而保障服务负源的高可用性。开发者可以通过流控的选项为指定的资源添加流控规则。
在开启流量控制时需要明确的设置流控的阈值,只要超过了该阈值就会触发具体的流控处理操作,而在Sentinel之中对于流控提供有三种处理模式(需要开启“高级选项”才可以配置),具体的使用特点如下:
·快速失败: 默认流控模式,当访问量超过了规定阈值后新的请求会被立即拒绝;
·Warm Up: 采用“预热/冷启动”方式,当访问量瞬间激增时让通过的流量缓慢增加,给冷系统一个预热缓冲;
·匀速排队: 当出现间隔性流量激增时,会根据请求通过的间隔时间让所有的请求匀速通过(漏桶算法)。
在这里插入图片描述
当流控规则增加完成之后,就可以直接在流控配置列表里面见到如下的信息了。
在这里插入图片描述
如果此时没有违反流控规则,这个时候可以正常获取服务端的数据响应,但是一旦你违反了这个规则,那么在执行的时候就会触发熔断操作,得到“Blocked by Sentinel (flow limiting)”信息。

2、降级规则(熔断规则):
熔断降级主要是对微服务调用链中某个资源出现不稳定状态时(例如:响应时间过长、产生异常),为避免其他资源调用而导致的级联错误,会在某个特定的时长内实现资源熔断处理
在这里插入图片描述

如果当前进行微服务调用的过程里面,响应的时间超过了1毫秒,那么最终就会触发熔断处理,返回“Blocked by Sentinel(flow limiting)”提示信息。

3、系统规则:
系统规则是对应用的入口流量进行控制,不是针对于某一个资源的保护,而是实现了一个应用整体纬度的保护规则,在系统规则中支持有如下的几种模式:
. Load 自适应模式: 交由操作系统(仅对Linux或类UNIX系统生效)进行保护控制—般的参考值为“系统硬件的CPU
内核数量*2.5”;
·平均响应时间: 当单台主机上的所有入口流量的平均RT达到阈值时触发系统保护;
·并发线程数: 当单台主机上的所有入口流量达到并发线程阈值时触发系统保护;
·入口QPS: 当单台主机上的所有入口流量达到阈值时触发系统保护;
·CPU使用率: 当系统CPU使用率超过阈值时触发系统保护。

系统规则属于一个全局规则,所有的接入到Sentinel 中的微服务都可以使用系统规则进行流量的保护,但是由于其所涵盖的范围太大了,是否使用要根据具体的开发要求来决定。

自定义限流错误页

Sentinel已经发现了其可以实现流量监控,以及限流保护,但是在默认进行熔断处理的时候会出现一个问题:所产生的错误信息不是我们系统中想的,现在能不能根据自己的需要创建一个属于自己的错误页。

1、 【provider-dept-8001子模块】具体的限流的错误页肯定是由各自的微服务来定义的,创建一个BlockAction。

package com.yootk.provider.action;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/errors/*") // 父路径
public class BlockAction {
    @RequestMapping("block_handler")
    public Object globalBlockHandler() {
        Map<String, Object> result = new HashMap<>(); // 保存错误信息
        result.put("status", HttpServletResponse.SC_BAD_REQUEST); // 设置状态码
        result.put("message", "Blocked by Sentinel (flow limiting)");
        return result;
    }
}

2、【provider-dept-8001子模块】修改 application.yml配置文件,追加错误的数据显示路径

spring:
  application: # 配置应用信息
    name: dept.provider # 是微服务的名称
  cloud: # Cloud配置
    sentinel: # 监控配置
      transport: # 传输配置
        port: 8719 # Sentinel组件启用之后默认会启动一个8719端口
        dashboard: sentinel-server:8888 # 控制台地址
      block-page: /errors/block_handler # 阻断页

重新启动部门微服务的应用,随后再次进行限流访问,观察一下最终可以得到的响应的结果信息

如果此时微服务重新启动,那么基于该微服务所配置的全部的流控规则将全部消失,需要开发者自己重新定义。

Fallback失败回退

在之前进行的错误提示信息是针对于整个的Sentinel组件完成的,“block-page:/errors/block_ handler”,这个配置指的是只要出现了错误,就执行指定的路径,现在希望可以定义完全属于自己业务逻辑的Fallback处理,这种机制是完全可以实现的。

1、【provider-dept-8001子模块】如果要想手工进行Fallback配置,则需要创建一个Setntinel切面管理类,而且这个切面在进行处理的时候需要考虑到可能是针对于Action类完成的(它不是接口,不能使用JDK动态代理机制完成,应该基于CGLIB处理)。

package com.yootk.provider.config;

import com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true) // CGLIB代理
public class SentinelAOPConfig { // Sentinel配置
    // 所有的Fallback的处理操作全部都是基于切面的形式负责完成的
    @Bean
    public SentinelResourceAspect getSentinelResourceAspect() {
        return new SentinelResourceAspect();
    }
}

2、【provider-dept-8001子模块】既然已经定义好了配置的切面,那么随后就需要进行Action的修改

package com.yootk.provider.action;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.yootk.common.dto.DeptDTO;
import com.yootk.service.IDeptService;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;

@RestController
@RequestMapping("/provider/dept/*") // 微服务提供者父路径
@Slf4j // 使用一个注解
public class DeptAction {
    @Autowired
    private IDeptService deptService;

    @SentinelResource(value = "/dept_get",fallback = "getFallback")
    @ApiOperation(value="部门查询", notes = "根据部门编号查询部门详细信息")
    @GetMapping("get/{id}")
    public Object get(@PathVariable("id") long id) {
        this.printRequestHeaders("get");
        return this.deptService.get(id);
    }
    public Object getFallback(@PathVariable("id") long id) {
        DeptDTO dto = new DeptDTO();
        dto.setDeptno(id);
        dto.setDname("【Fallback】部门名称");
        dto.setLoc("【Fallback】部门位置");
        return dto;
    }
    @ApiOperation(value="部门增加", notes = "增加新的部门信息")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "deptDTO", required = true,
                    dataType = "DeptDTO", value = "部门传输对象实例")
    })
    @PostMapping("add")
    @SentinelResource(value = "/dept_add", fallback = "addFallback")
    public Object add(@RequestBody  DeptDTO deptDTO) {    // 后面会修改参数模式为JSON
        this.printRequestHeaders("add");
        return this.deptService.add(deptDTO);
    }
    public Object addFallback(@RequestBody  DeptDTO deptDTO) {
        return false;
    }
    @ApiOperation(value="部门列表", notes = "查询部门的完整信息")
    @GetMapping("list")
    @SentinelResource(value = "/dept_list", fallback = "listFallback")
    public Object list() {
        this.printRequestHeaders("list");
        return this.deptService.list();
    }
    public Object listFallback() {
        return new ArrayList<>();
    }
    @ApiOperation(value="部门分页查询", notes = "根据指定的数据库参数实现部门数据的分页加载")
    @ApiImplicitParams({
            @ApiImplicitParam(name="cp", value = "当前所在页", required = true, dataType = "int"),
            @ApiImplicitParam(name="ls", value = "每页显示的数据行数", required = true, dataType = "int"),
            @ApiImplicitParam(name="col", value = "模糊查询列", required = true, dataType = "String"),
            @ApiImplicitParam(name="kw", value = "模糊查询关键字", required = true, dataType = "String")
    })
    @GetMapping("split")
    @SentinelResource(value = "/dept_split", fallback = "splitFallback")
    public Object split(int cp, int ls, String col, String kw) {
        this.printRequestHeaders("split");
        return this.deptService.split(cp, ls, col, kw);
    }
    public Object splitFallback() {
        return new HashMap<>();
    }
    private void printRequestHeaders(String restName) {    // 实现所有请求头信息的输出
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        Enumeration<String> headerEnums = request.getHeaderNames();
        while (headerEnums.hasMoreElements()) {
            String headerName = headerEnums.nextElement();
            log.info("【{}】头信息:{} = {}", restName, headerName, request.getHeader(headerName));
        }
    }
}

3、【Sentinel控制台】随后又需要重新启动微服务,重新在Sentinel控制台里面进行微服务的访问规则配置。
在这里插入图片描述
Block Page是针对于整个的熔断配罩的信息,而Fallback是针对于每一个具体的服务的处理而提供的,针对于各自微服务接口的Fallback一定拥有最高的优先调度。

BlockHandler

在之前使用Fallback配置的过程里面,每一个业务的处理方法内部都需要提供有一个Fallback方法的操作定义,但是这样会造成核心功能的语句和Fallback功能语句混淆的问题,那么就可以考虑将所有的Fallback处理方法定义在一个专属的类中。
在这里插入图片描述
通过当前的源代码分析可以发现BlockException是所有的可能出现的限流异常的最大的父类,而后每一个异常也同时会带有一个与之匹配的限流规则(Rule接口)。

1、【provider-dept-8001子模块】创建一个与DeptAction有关的拦截处理类

·注意事项一: 所有的方法必须加上static关键字,进行方法唯一性的标记;
·注意事项二: 所有编写的Block处理方法定义时要与Action中的方法保持一致,最后可以追加一个BlockException异常。

package com.yootk.provider.action.block;

import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.yootk.common.dto.DeptDTO;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class DeptBlockHandler { // 限流信息
    public static Object addBlockHandler(DeptDTO deptDTO, BlockException e) {
        Map<String, Object> map = new HashMap<>();
        map.put("rule", e.getRule()); // 获取失败的信息
        map.put("message", e.getMessage()); // 异常信息
        map.put("result", false); // 本次的执行结果
        return map;
    }

    public static Object getBlockHandler(long id, BlockException e) {
        Map<String, Object> map = new HashMap<>();
        map.put("rule", e.getRule()); // 获取失败的信息
        map.put("message", e.getMessage()); // 异常信息
        DeptDTO dept = new DeptDTO();
        dept.setDeptno(id);
        dept.setDname("【Block】部门名称");
        dept.setLoc("【Block】部门位置");
        map.put("result", dept); // 本次的执行结果
        return map;
    }

    public static Object listBlockHandler(BlockException e) {
        Map<String, Object> map = new HashMap<>();
        map.put("rule", e.getRule()); // 获取失败的信息
        map.put("message", e.getMessage()); // 异常信息
        map.put("result", new ArrayList<>()); // 本次的执行结果
        return map;
    }

    public static Object splitBlockHandler(int cp, int ls, String col, String kw, BlockException e) {
        Map<String, Object> map = new HashMap<>();
        map.put("rule", e.getRule()); // 获取失败的信息
        map.put("message", e.getMessage()); // 异常信息
        map.put("result", new HashMap<>()); // 本次的执行结果
        return map;
    }
}

2、【provider-dept-8001子模块】修改DeptAction程序类,进行block的相关配置

package com.yootk.provider.action;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.yootk.common.dto.DeptDTO;
import com.yootk.provider.action.block.DeptBlockHandler;
import com.yootk.service.IDeptService;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;

@RestController
@RequestMapping("/provider/dept/*") // 微服务提供者父路径
@Slf4j // 使用一个注解
public class DeptAction {
    @Autowired
    private IDeptService deptService;

    @SentinelResource(value = "/dept_get", blockHandlerClass = DeptBlockHandler.class, blockHandler = "getBlockHandler")
    @ApiOperation(value = "部门查询", notes = "根据部门编号查询部门详细信息")
    @GetMapping("get/{id}")
    public Object get(@PathVariable("id") long id) {
        this.printRequestHeaders("get");
        return this.deptService.get(id);
    }

    @ApiOperation(value = "部门增加", notes = "增加新的部门信息")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "deptDTO", required = true,
                    dataType = "DeptDTO", value = "部门传输对象实例")
    })
    @PostMapping("add")
    @SentinelResource(value = "/dept_add", blockHandlerClass = DeptBlockHandler.class, blockHandler = "addBlockHandler")
    public Object add(@RequestBody DeptDTO deptDTO) {    // 后面会修改参数模式为JSON
        this.printRequestHeaders("add");
        return this.deptService.add(deptDTO);
    }

    @ApiOperation(value = "部门列表", notes = "查询部门的完整信息")
    @GetMapping("list")
    @SentinelResource(value = "/dept_add", blockHandlerClass = DeptBlockHandler.class, blockHandler = "listBlockHandler")
    public Object list() {
        this.printRequestHeaders("list");
        return this.deptService.list();
    }

    @ApiOperation(value = "部门分页查询", notes = "根据指定的数据库参数实现部门数据的分页加载")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "cp", value = "当前所在页", required = true, dataType = "int"),
            @ApiImplicitParam(name = "ls", value = "每页显示的数据行数", required = true, dataType = "int"),
            @ApiImplicitParam(name = "col", value = "模糊查询列", required = true, dataType = "String"),
            @ApiImplicitParam(name = "kw", value = "模糊查询关键字", required = true, dataType = "String")
    })
    @GetMapping("split")
    @SentinelResource(value = "/dept_add", blockHandlerClass = DeptBlockHandler.class, blockHandler = "splitBlockHandler")
    public Object split(int cp, int ls, String col, String kw) {
        this.printRequestHeaders("split");
        return this.deptService.split(cp, ls, col, kw);
    }

    private void printRequestHeaders(String restName) {    // 实现所有请求头信息的输出
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        Enumeration<String> headerEnums = request.getHeaderNames();
        while (headerEnums.hasMoreElements()) {
            String headerName = headerEnums.nextElement();
            log.info("【{}】头信息:{} = {}", restName, headerName, request.getHeader(headerName));
        }
    }
}

3、【Sentinel控制台】为相应的服务接口添加流控规则
在这里插入图片描述
4、【provider-dept-8001子模块】为DeptAction.get()方法添加Fallback处理

package com.yootk.provider.action;

@RestController
@RequestMapping("/provider/dept/*") // 微服务提供者父路径
@Slf4j // 使用一个注解
public class DeptAction {
    @Autowired
    private IDeptService deptService;

    @SentinelResource(value = "/dept_get", fallback = "getFallback", blockHandlerClass = DeptBlockHandler.class, blockHandler = "getBlockHandler")
    @ApiOperation(value = "部门查询", notes = "根据部门编号查询部门详细信息")
    @GetMapping("get/{id}")
    public Object get(@PathVariable("id") long id) {
        this.printRequestHeaders("get");
        return this.deptService.get(id);
    }

    public Object getFallback(long id) {
        DeptDTO dept = new DeptDTO();
        dept.setDeptno(id);
        dept.setDname("【Fallback】部门名称");
        dept.setLoc("【Fallback】部门位置");
        return dept;
    }

}

BlockException拥有限流异常处理的最高的级别,级别要高于Fallback,不管是Fallback还是BlockHandler都需要自己编写一大堆的配置项。

热点规则

在进行RESTful设计的时候一般都需要在Action的处理方法中进行请求参数的接收,于是这个时候可以针对于参数进行限流,这种规则就称为热点规则。
热点参数限流会统计参数中的热点参数,并根据配置的限流阀值与模式,对包含热点参数的资源调用进行限流。热点参数限制可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。如果要想使用热点规则,则需要在控制层的相关方法中使用“@SentinelResource”注解进行声明。

1、【provider-dept-8001子模块】热点参数的限流操作直接在热点上进行一个注解的定义即可

package com.yootk.provider.action;

@RestController
@RequestMapping("/provider/dept/*") // 微服务提供者父路径
@Slf4j // 使用一个注解
public class DeptAction {
    @Autowired
    private IDeptService deptService;

    @SentinelResource(value = "/dept_get", fallback = "getFallback", blockHandlerClass = DeptBlockHandler.class, blockHandler = "getBlockHandler")
    @ApiOperation(value = "部门查询", notes = "根据部门编号查询部门详细信息")
    @GetMapping("get/{id}")
    public Object get(@PathVariable("id") long id) {
        this.printRequestHeaders("get");
        return this.deptService.get(id);
    }
}

2、【Sentinel控制台】热点限流的配置是依据参数来定义的,第一个参数的索引是0,而后继续叠加。
在这里插入图片描述
此时配置的操作形式与之前的流控很相似,但是唯一的不同在于,此时需要设置有一个参数的索引。

3、【Sentinel控制台】热点规则与之前的流控规则的区别主要在于可以添加参数的例外。
在这里插入图片描述
相比较之前的流控处理,此时可以针对于参数的某些选项来进行例外的配置,而这种配置可以使得流控更加方便,在Spring框架里面提供有一个SpringCache缓存工具,这个缓存可以将一些数据保存在Redis里面,避免通过数据库查询。

授权规则

所谓的授权指的就是在请求的时候需要一些特定的数据来进行请求操作,如果没有特定的数据则认为不符合授权检查的规则,所以不能够进行资源的访问。

1、【Sentinel控制台】为指定的请求资源添加授权规则,而这个授权规则添加的是一个白名单,只要拥有app 或者pc两个授权信息其中之一的客户即可进行访问。
在这里插入图片描述
2、【provider-dept-8001子模块】如果要想让现在的授权规则生效,则必须进行程序类的修改,进行规则参数的解析,现在假设每一次用户请求的时候都要附带有一个“serviceName”的参数,这个参数负责保存有授权信息。

package com.yootk.provider.config;

import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
@Component
public class SentinelRequestOriginParser implements RequestOriginParser { // 请求解析
    @Override
    public String parseOrigin(HttpServletRequest request) {
        String serviceName = request.getParameter("serviceName"); // 接收请求参数
        if (serviceName == null || "".equals(serviceName)) {    // 参数的内容是空
            serviceName = request.getHeader("serviceName"); // 通过头信息传递参数
        }
        if (StringUtils.isEmpty(serviceName)) {
            return request.getRemoteAddr(); // 根据IP地址处理
        }
        return serviceName;
    }
}

3、【Postman工具】对当前的操作进行测试,首先测试没有传递serviceName参数的操作:
在这里插入图片描述
4、【Postman工具】为项目添加一个头信息:
在这里插入图片描述
此时传递的pc服务名称正好符合当前的授权规则,所以可以直接进行服务的调用。

5、【consumer-springboot-80子模块】现在使用的是Postman进行接口测试,而在实际的开发之中肯定会由消费者进行接口调用,这个时候就需要考虑通过拦截器的方式进行请求数据的传递。

package com.yootk.service.config;

import feign.Logger;
import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;

public class FeignConfig { // 定义Feign配置类
    @Bean
    public Logger.Level level() {
        return Logger.Level.FULL; // 输出完全的日志信息
    }
    @Bean
    public RequestInterceptor getFeignRequestInterceptor() {    // 请求拦截器
        return (template -> template.header("serviceName", "pc"));
    }
}

在每次请求的时候都自动追加一个头信息,这样在最终调用的时候就可以通过头信息来进行授权数据内容的传递处理了。

BlockExceptionHandler

在进行微服务调用的过程之中,如果某一个微服务出现了错误,那么最终应该根据错误产生的个数来进行熔断的处理,整个的Sentinel组件都拥有熔断的功能。
每当触发了Sentinel防护规则时,实际上都会在系统内部产生BlockException异常实例,而后会基于默认的方式进行异常数据的响应,考虑到定制化异常信息显示的需要,Sentinel内部提供了BlockExceptionHandler接口,开发者可以直接依据此接口进行自定义异常数据的响应。

1、【provider-dept-8001子模块】创建拦截异常处理类

package com.yootk.provider.action.block;

import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import org.springframework.util.MimeTypeUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

@Component
public class SentinelBlockExceptionHandler implements BlockExceptionHandler {
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response, BlockException e) throws Exception {
        Map<String, Object> errors = new HashMap<>(); // 数据的保存
        errors.put("type", e.getClass().getName()); // 异常的类型
        // 之所以这样编写是为了帮助大家加深关于Sentinel组件内部提供的异常类型
        if (e instanceof FlowException) {
            errors.put("message", "服务限流");
        } else if (e instanceof DegradeException) {
            errors.put("message", "服务降级");
        } else if (e instanceof ParamFlowException) {
            errors.put("message", "热点参数限流");
        } else if (e instanceof SystemBlockException) {
            errors.put("message", "系统拦截");
        } else if (e instanceof AuthorityException) {
            errors.put("message", "授权拦截");
        } else {
            errors.put("message", "其他异常");
        }
        errors.put("path", request.getRequestURI()); // 产生异常的路径
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); // 状态码
        response.setCharacterEncoding("UTF-8"); // 设置响应编码
        response.setHeader("Content-Type", MimeTypeUtils.APPLICATION_JSON_VALUE); // JSON直接响应
        new ObjectMapper().writeValue(response.getWriter(), errors); // Jackson组件的输出
    }
}

2、【provider-dept-8001子模块】修改根据ID查询操作类,产生点异常

    // @SentinelResource(value = "/dept_get", fallback = "getFallback", blockHandlerClass = DeptBlockHandler.class, blockHandler = "getBlockHandler")
    @ApiOperation(value = "部门查询", notes = "根据部门编号查询部门详细信息")
    @GetMapping("get/{id}")
    public Object get(@PathVariable("id") long id) {
        if (id % 2 == 0) {   // 查询ID为偶数
            throw new RuntimeException("查询iD不能为偶数!");
        }
        this.printRequestHeaders("get");
        return this.deptService.get(id);
    }

3、【Postman工具]为了便于观察,直接让当前的部门微服务产生错误,随后可以发现在Postman内部会出现一个SpringCloud开发框架自己所使用的异常信息的打印;
在这里插入图片描述
4、【Sentinel控制台】既然此时有了异常,就应该增加异常的相关熔断处理操作。
在这里插入图片描述
如果此时的接口产生了1个异常之后,那么在2秒内不允许进行调用,此时如果已经启用拦截,随后继续进行访问,那么就会出现如下的提示信息,而这个提示信息就是之前自己开发的提示信息。

{
	"path": "/provider/dept/ get/2",
	"type" : "com.alibaba.csp.sentinel.slots.block.degrade.DegradeException" ,
	"message" :"服务降级"
}

Sentinel集群流控

你的项目之中一旦使用到了微服务,那么永远不可避免的就是集群环境,例如:为了提高部门微服务的处理性能,可以设置多个微服务的实例节点,共同进行业务的处理完成,在之前进行流控规则添加的时候会发现提供有一个是否集群的配置选项。
在这里插入图片描述
例如:

现在有一个“dept.provider”微服务集群,并为其设置限流规则(QPS设置为60),这样就意味着整个集群每秒可以处理的请求总量就是60(不管集群中有多少个微服务节点)。但是如果要想进行这种多实例节点的集群的限流控制,那么就一定要有一个共同的计数服务器。

如果要想在Sentinel 中实现集群限流,本质上都需要提供有一个相关访问数据的统计,在单一实例的情况下,这个统计操作是在每个实例中实现的。而如果在集群环境下,就需要提供有一个专门的实例(TokenServer)进行数据统计,并且该TokenServer要收集所有TokenClient 发送来的统计信息,而后根据集群流控规则来决定是否允许该请求进行资源访问。
在这里插入图片描述

Sentinel 从1.4.0版本开始引入了集群流控实现模块,基于Netty 实现了服务通信,当用户在SpringCloud项目模块中引入了“spring-cloud-starter-alibaba-sentinel”依赖库后就会自动引入这些相关模块,通过图可以发现,与集群流控有关的模块一共有三个,这三个模块的具体作用如下:
1、sentinel-cluster-common-default: 集群流控公共模块,包含了公共的接口以及实体类;.
2、sentinel-cluster-server-default: TokenServer实现模块,基于Sentinel核心逻辑进行规则扩展实现;
3、sentinel-cluster-client-default: TokenClient实现模块。
在这里插入图片描述

按照分布式项目的设计原则,此时应该提供有一个独立的TokenServer进行统计的计数,随后在Sentinel组件的内部要指定此计数服务器地址。

1、【microcloud项目】创建一个“sentinel-token-server”新的子模块;
2、【microcloud项目】修改build.gradle配置文件,为当前的项目配置所需要的依赖库;

project(":sentinel-token-server") {
    dependencies { // 配置模块所需要的依赖库
        implementation("org.springframework.boot:spring-boot-starter-web") // SpringBoot依赖
        implementation('com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel') {
            exclude group: 'com.alibaba.csp', module: 'sentinel-cluster-client-default'
        }
    }
}

3、【sentinel-token-server子模块】直接创建一个Token应用启动类:

package com.yootk.sentinel;

import com.alibaba.csp.sentinel.cluster.server.ClusterTokenServer;
import com.alibaba.csp.sentinel.cluster.server.SentinelDefaultTokenServer;
import com.alibaba.csp.sentinel.cluster.server.config.ClusterServerConfigManager;
import com.alibaba.csp.sentinel.cluster.server.config.ServerTransportConfig;

public class StartTokenServerApplication {
    // -Dcsp.sentinel.dashboard.server=sentinel-server:8888 -Dcsp.sentinel.api.port=8719
    // -Dproject.name=sentinel-token-server -Dcsp.sentinel.log.use.pid=true
    static {    // 使用系统属性代替启动参数
        System.setProperty("csp.sentinel.dashboard.server", "sentinel-server:8888");            // 控制台地址
        System.setProperty("csp.sentinel.api.port", "8719");    // sentinel端口
        System.setProperty("project.name", "token-server");        // 服务名称
        System.setProperty("csp.sentinel.log.use.pid", "true");    // 设置pid(可选)
    }

    public static void main(String[] args) throws Exception {
        ClusterTokenServer tokenServer = new SentinelDefaultTokenServer(); // 实例化Token集群管理
        ClusterServerConfigManager.loadGlobalTransportConfig(new ServerTransportConfig().setIdleSeconds(600).setPort(10217));
        tokenServer.start();// 启动服务器
    }
}

4、【Sentinel控制台】由于此时是静态设置了Sentinel连接地址,所以当应用启动的时候就可以直接向Sentinel控制台注册
在这里插入图片描述
5、【本地系统】为了便于TokenServer访问,修改 hosts主机名称进行地址配置:

127.0.0.1	sentinel-token-server

6、【Sentinel控制台】此时已经提供了完整的 TokenServer,那么随后就需要在Sentinel控制台里面添加一个流控规则
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
7、【Sentinel控制台】进行流控规则的指定
在这里插入图片描述
此时所配置的服务限流可以考虑到集群之中所有的主机节点,而这些的统计的信息都被TokenServe所记录,一旦触发了流控规则之后,就会产生如下的错误信息。
在这里插入图片描述

Sentinel流控规则持久化

Sentinel为了便于资源限流规则的持久化管理,专门提供了ReadableDataSource(配置读取)与WritableDataSource(配置写入接口)两个操作接口,利用这两个接口可以向指定的存储设备中实现规则的读写处理。

import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
import com.alibaba.csp.sentinel.datasource.writableDataSource;

如果要想实现配置规则的读写操作,那么必然要有一个存储的终端,而且这个终端需要保证性能高,同时又可以很稳定,这样一来几乎就不会去考虑到SQL数据库了(配置的限流规则一般很少去改变,更多的是进行配置规则的读取)。
在这里插入图片描述

如果说现在你要开发一个系统,这个系统可以由管理人员在服务应用部署上线之前,就可以进行所有限流规则的维护,这个时候一定需要有一个完善的管理界面(排除那些没有完善管理界面: Redis ZooKeeper),所以最佳的做法就是使用Nacos,因为Nacos里面支持有一个完善的处理界面,而且其本身是提供有JSON数据(各种的数据类型都是支持的)的直接存储,最重要的是Nacos还是SpringCloudAlibaba 套件之中最为重要的注册中心。
Sentinel 提供的DataSource是一个逻辑上的概念,具体的存储可以是关系型数据库、文件、ZooKeeper、Redis、Nacos等存储终端,在终端中可以保存所需要的限流规则,而对于DataSource的操作形式也提供有两种:.
1、拉模式(Pull-Based):客户端主动向某个 DataSource存储中心定期轮询开读取规则,这个配置中心可能是一个文件,或者是关系型数据库,虽然此种方式简单,但是却无法及时获取到配置更新;
2、推模式(Push-Based):所有的限流规则由配置中心(Nacos、Zookeeper、Redis等)统一推送,客户端通过注册监听器的方式监听规则的变化,这样可以更好的保持配置的实时性和一致性。

注意:

在早期讲解 Nacos的时候曾经说过,Nacos 不能够作为外网的服务使用
在这里插入图片描述
如果此时你的系统之中的Nacos使用了认证模式(配置用户名和密码),就会出现 BUG,因为如果要使用Nacos进行存储,Sentinel无法进行用户名和密码的配置。
为了解决Sentinel组件无法正常的与具有认证功能的Nacos整合的设计问题,最佳的做法是直接部署一个新的Nacos应用服务
( 也可以取消Nacos中的认证模式 )

(以下为 直接部署一个新的Nacos应用服务)

1、【本地系统】为了便于Nacos的服务配置,本地修改hosts配置文件,追加一个新的主机映射:

192.168.190.169  sentinel-nacos-server

2、 【sentinel-nacos-server主机】将Nacos服务组件解压缩到“/usr/local”目录之中

tar xzvf /var/ftp/nacos-server-2.0.2.tar.gz -C /usr/local/

3、【sentinel-nacos-server主机】启动Nacos应用服务进程

/usr/local/nacos/bin/startup.sh -m standalone

4、【sentinel-nacos-server主机】修改防火墙的使用规则,开放相关的访问端口:开放访问端口:

firewall-cmd --zone=public --add-port=8848/tcp --permanent
firewall-cmd --zone=public --add-port=9848/tcp --permanent
firewall-cmd --zone=public --add-port=7848/tcp --permanent
firewall-cmd --zone=public --add-port=9849/tcp --permanent

重新加载配置:

firewall-cmd --reload

5、【Nacos控制台】通过外部浏览器访问当前的Nacos 控制台,路径:

sentinel-nacos-server:8848/nacos

在这里插入图片描述
在Sentinel之中流控规则是最为常用的一项规则,也是系统保护之中最重要的一项技术了,如果要想进行流控配置的时候,是需要根据不同的应用名称来实现定义的。
在这里插入图片描述

1、【Sentiel-Nacos控制台】要想进行规则的存储,那么一定要存在有一个领域模型,首先创建一个新的命名空间。
在这里插入图片描述
当前我们所使用的UUID:51586a27-b10d-4165-9cd2-38f1464c780d

2、【Sentiel-Nacos控制台】在Sentinel命名空间下创建一个新的配置项,配置项的名称为"dept.provider-flow-rules",其中“dept.provider”是注册微服务的名称,这个时候将配置项保存在“SENTINEL_GROUP”组之中;

[
  {
    "resource": "/provider/dept/list",
    "limitApp": "default",
    "grade": 1,
    "count": 1,
    "strategy": 0,
    "controlBehavior": 0,
    "clusterMode": false
  }
]

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
3、【microcloud项目】既然此时已经通过了Nacos保存了Sentinel相关限流控制规则,那么就需要在部门微服务之中进行数据源数据的读取,所以修改配置文件,添加核心的依赖库。

dependencies.gradle

ext.versions = [               
    sentinel             : '1.8.2', 
]
ext.libraries = [          
    'sentinel-datasource-nacos'         : "com.alibaba.csp:sentinel-datasource-nacos:${versions.sentinel}"
]

build.gradle

project(":provider-dept-8001") {   
    dependencies {
        implementation(project(":common-api")) 
        implementation(libraries.'mybatis-plus-boot-starter')
        implementation(libraries.'mysql-connector-java')
        implementation(libraries.'druid')
        implementation(libraries.'springfox-boot-starter')
        implementation(libraries.'sentinel-datasource-nacos')    //刚加入的依赖
        implementation('org.springframework.boot:spring-boot-starter-security')
        implementation('com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel')
        implementation('com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery') {
            exclude group: 'com.alibaba.nacos', module: 'nacos-client'
        }
        implementation('com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-config') {
            exclude group: 'com.alibaba.nacos', module: 'nacos-client'
        }
        implementation(libraries.'nacos-client')
    }
}

4.【provider-dept-8001子模块】此时已经成功的引入了Nacos 数据源的存储配置了,那么随后就需要修改项目之中的application.yml配置文件,引入相应的Nacos 的配置(这个Nacos 的配置里面可不能够使用用户认证信息)。

spring:
  application: # 配置应用信息
    name: dept.provider # 是微服务的名称
  cloud: # Cloud配置
    sentinel: # 监控配置
      transport: # 传输配置
        port: 8719 # Sentinel组件启用之后默认会启动一个8719端口
        dashboard: sentinel-server:8888 # 控制台地址
      block-page: /errors/block_handler # 阻断页
      eager: true # 饥饿加载
      datasource:  # 数据源配置
        flow-datasource: # 流控的数据源
          nacos: # 当前的存储介质为Nacos
            server-addr: sentinel-nacos-server:8848 # SentinelNacos地址
            namespace: 51586a27-b10d-4165-9cd2-38f1464c780d # 命名空间ID
            group-id: SENTINEL_GROUP # 一般建议大写
            data-id: ${spring.application.name}-flow-rules # 配置项的名称
            data-type: json # 配置的文件结构
            rule_type: flow # 流控规则

5、【provder-dept-8001子模块】修改DeptAction的路径,配置与规则相同的资源名称

去掉@SentinelResource注解中的value属性
在这里插入图片描述
6、【provider-dept-8001】一切配置完成之后,重新启动当前的部门微服务;

加载日志信息
在这里插入图片描述

7、【Sentinel控制台】启动的时候应该已经进行了规则加载,那么随后打开Sentinel控制台,观察一下是否有规则
在这里插入图片描述
在以后的维护过程之中,只需要由管理员负责修改Nacos 中的配置项即可轻松的实现流控规则的定义,这种定义是非常方便使用的。

8、【Sentinel-Nacos控制台】所有在 Nacos 中所定义的配置项都可以及时修改的,修改完成之后微服务一定可以及时获取到对应的服务配置(SpringCloudConfig 的特点决定的)。
在这里插入图片描述
当Nacos 中的配置修改完成之后,由于配置项允许动态的刷新(微服务会启动一个Nacos配置项的监听),会自动加载新的配置,随后可以得到如下的日志信息:

[2a540420-51d6-4ea3-b6f7-0e1bcfb78190_config-0]receive server push request,request=ConfigChangeNotifyRequest,requestId=1
[2a540420-51d6-4ea3-b6f7-0e1bcfb78190_config-0] [server-push] config changed. dataId=dept.provider-flow-rules, group=SENTINEL_GROUP,tenant=51586a27-b10d-4165-9cd2-38f1464c780d
[2a540420-51d6-4ea3-b6f7-0e1bcfb78190_config-0]ack server push request,request=ConfigChangeNotifyRequest,requestId=1
[config_rpc_client] [notify-listener] time cost=0ms in ClientWorker, dataId=dept.provider-flow-rules, group=SENTINEL_GROUP, md5=a648b66d966d505aec4490ba3e11be12, listener=com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource$1@2f530d28 
[config_rpc_client] [notify-ok] dataId=dept.provider-flow-rules, group=SENTINEL_GROUP, md5=a648b66d966d505aec4490ba3e11be12, listener=com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource$1@2f530d28 ,cost=1 millis.

限流规则解析

Sentinel组件里面对于流控规则有很多种,例如:系统流控、授权流控、热点流控,对于这些流控该如何进行持久化的配置?
在这里插入图片描述
在整个实际的开发之中,如果要想更合理的去使用Sentinel组件,那么最重要的一点就是进行各类规则的配置,例如:现在需要定义一个系统规则,这个系统规则也应该在Nacos里面进行定义,但是具体的定义项,就需要根据SystemRule子类分析。

1、【Sentinel源代码】查看一下 SystemRule子类之中的属性。
在这里插入图片描述
2、【SentinelNacos控制台】追加一个“dept.provider-system-rules”配置项,进行QPS限流规则定义。

[
  {
    "qps": 1
  }
]

在这里插入图片描述
3、【provider-dept-8001子模块】修改application.yml 配置文件,追加系统规则数据源的读取。

spring:
  application: # 配置应用信息
    name: dept.provider # 是微服务的名称
  cloud: # Cloud配置
    sentinel: # 监控配置
      transport: # 传输配置
        port: 8719 # Sentinel组件启用之后默认会启动一个8719端口
        dashboard: sentinel-server:8888 # 控制台地址
      block-page: /errors/block_handler # 阻断页
      eager: true # 饥饿加载
      datasource:  # 数据源配置
        system-datasource: # 系统规则数据源
          nacos: # 当前的存储介质为Nacos
            server-addr: sentinel-nacos-server:8848 # SentinelNacos地址
            namespace: 51586a27-b10d-4165-9cd2-38f1464c780d # 命名空间ID
            group-id: SENTINEL_GROUP # 一般建议大写
            data-id: ${spring.application.name}-system-rules # 配置项的名称
            data-type: json # 配置的文件结构
            rule_type: system # 流控规则
        flow-datasource: # 流控的数据源
          nacos: # 当前的存储介质为Nacos
            server-addr: sentinel-nacos-server:8848 # SentinelNacos地址
            namespace: 51586a27-b10d-4165-9cd2-38f1464c780d # 命名空间ID
            group-id: SENTINEL_GROUP # 一般建议大写
            data-id: ${spring.application.name}-flow-rules # 配置项的名称
            data-type: json # 配置的文件结构
            rule_type: flow # 流控规则

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-04-07 23:07:00  更:2022-04-07 23:07:10 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/15 22:24:39-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码