spring cloud 流量控制_dubbo实战与源码分析

(35) 2024-06-19 09:01:01

实战示例

控制台初体验

Sentinel的控制台启动后,控制台页面的内容数据都是空的,接下来我们来逐步操作演示结合控制台的使用,在上一节也已说明整合SpringCloud Alibaba第一步先加入spring-cloud-starter-alibaba-sentinel启动器依赖

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第1张

配置文件添加参数,dashboard即为控制台的端口,我们是本地启动使用8858端口

spring: cloud: sentinel: enabled: true transport: dashboard: localhost:8858 port: 8719 

订单添加控制器中有一个订单添加的接口

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第2张

启动订单微服务程序,访问订单控制器的添加订单接口,http://localhost:4070/add-order

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第3张

然后再查看sentinel的控制台首页-簇点链路,这时已经有/add-order这个资源名的数据

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第4张

多次访问后在实时监控也可以看到相应资源的实时QPS统计数据

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第5张

  • Sentinel 的所有规则都可以在内存态中动态地查询及修改,修改之后立即生效。同时 Sentinel 也提供相关 API来定制自己的规则策略。
  • Sentinel 支持以下几种规则:流量控制规则熔断降级规则系统保护规则来源访问控制规则热点参数规则
  • 流控规则

定义

  • 流量控制:Sentinel的流控的原理主要是监控应用流量或者说是资源的QPS或者并发线程数,当达到指定的阈值后对流量进行控制,以避免被瞬时的流量洪峰冲垮,从而保障应用的高可用性

  • QPS:每秒请求数,即在不断向服务器发送请求的情况下,服务器每秒能够处理的请求数量。

  • 并发线程数:指的是施压机施加的同时请求的线程数量。

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第6张

流量规则的定义的重要属性:

Field 说明 默认值
resource 资源名,资源名是限流规则的作用对象
count 限流阈值
grade 限流阈值类型,QPS 或线程数模式 QPS 模式
limitApp 流控针对的调用来源 default,代表不区分调用来源
strategy 调用关系限流策略:直接、链路、关联 根据资源本身(直接)
controlBehavior 流控效果(直接拒绝 / 排队等待 / 慢启动模式),不支持按调用关系限流 直接拒绝

FlowSlot 会根据预设的规则,结合前面 NodeSelectorSlotClusterNodeBuilderSlotStatistcSlot 统计出来的实时信息进行流量控制。同一个资源可以对应多条限流规则。FlowSlot 会对该资源的所有限流规则依次遍历,直到有规则触发限流或者所有规则遍历完毕。一条限流规则主要由下面几个因素组成,可以组合这些元素来实现不同的限流效果:

  • resource:资源名,即限流规则的作用对象
  • count: 限流阈值
  • grade: 限流阈值类型,QPS 或线程数
  • strategy: 根据调用关系选择策略

流控类型

基于QPS流控

通过控制台首页-簇点链路,在相应资源的记录右边点击流控按钮并设置相应的流控规则。

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第7张

快速访问订单控制器的添加订单接口,当前设置每秒超过2次就会被流控

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第8张

同样我们也可以和前面核心库示例一样自定流控提示

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第9张

设置订单查询接口的流控规则

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第10张

通过控制台首页-流控规则查看当前的流控规则,每次重启微服务模块其设置规则在内存中丢失了,也即是前面设置添加订单的流控规则也没有了,如需要则需重新设置

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第11张

快速访问订单控制器的查询订单接口

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第12张

并发线程数

线程数限流用于保护业务线程数不被耗尽。例如,当应用所依赖的下游应用由于某种原因导致服务不稳定、响应延迟增加,对于调用者来说,意味着吞吐量下降和更多的线程数占用,极端情况下甚至导致线程池耗尽。为应对高线程占用的情况,业内有使用隔离的方案,比如通过不同业务逻辑使用不同线程池来隔离业务自身之间的资源争抢(线程池隔离),或者使用信号量来控制同时请求的个数(信号量隔离)。这种隔离方案虽然能够控制线程数量,但无法控制请求排队时间。当请求过多时排队也是无益的,直接拒绝能够迅速降低系统压力。Sentinel线程数限流不负责创建和管理线程池,而是简单统计当前请求上下文的线程个数,如果超出阈值,新的请求会被立即拒绝。我们在查询订单接口中增加休眠来演示效果

 @RequestMapping("/query-order") @SentinelResource(value = "query-order",blockHandler = "querydOrderBlockHandler") public String querydOrder() { 
    try { 
    TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { 
    e.printStackTrace(); } return "订单查询成功"; } 

设置并发线程数的阈值为1

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第13张

访问第一个页面的紧跟着再打开另外一个页面继续访问

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第14张

第二个页面就显示被流控了

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第15张

流控模式

模式分类
  • 直接拒绝:接口达到限流条件时,直接限流
  • 关联:当关联的资源达到阈值时,就限流自己
  • 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就可以限流)
直接拒绝

默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出。前面的例子都是使用直接拒绝,这里就不再说明。

关联流控

具有关系的资源流量控制,当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢,举例来说,read_dbwrite_db 这两个资源分别代表数据库读写,我们可以给 read_db 设置限流规则来达到写优先的目的:设置 FlowRule.strategyRuleConstant.RELATE 同时设置 FlowRule.ref_identitywrite_db。这样当写库操作过于频繁时,读数据的请求会被限流。我们先把前面的querydOrder休眠去掉,设置流控模式为关联,关联资源为添加订单接口

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第16张

先通过ApiFox设置循环访问,也可以通过其他工具如jmeter等,ApiFox也可以前面的文章也有讲解去使用

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第17张

启动持续的访问订单查询接口

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第18张

访问订单添加已经显示被流控了,暂停ApiFox后访问则正常。

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第19张

链路流控

NodeSelectorSlot 中记录了资源之间的调用链路,这些资源通过调用关系,相互之间构成一棵调用树。这棵树的根节点是一个名字为 machine-root 的虚拟节点,调用链的入口都是这个虚节点的子节点。在订单实现类中增加测试方法

 @Override @SentinelResource(value = "getOrder") public String getOrder() { 
    return "测试查询订单"; } 

订单控制器增加两个接口方法,都调用到了getOrder

 @RequestMapping("/test1") public String test1(){ 
    return orderService.getOrder(); } @RequestMapping("/test2") public String test2(){ 
    return orderService.getOrder(); } 

配置文件增加下面参数

spring: cloud: sentinel: web-context-unify: false 

启动程序为getOrder资源设置链路流控模式,入口资源为/test2

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第20张

快速访问http://localhost:4070/test2 ,出现被流控了,而快速访问http://localhost:4070/test1则都是正常请求

流控效果

快速失败

默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。前面大部分例子都是使用了快速失败演示。

Warm Up
  • Sentinel的Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,让服务器一点一点处理,再慢慢加量,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。
  • 预热底层是根据令牌桶算法实现的。
  • warm up冷启动主要用于启动需要额外开销的场景,例如建立数据库连接。设定QPS阈值为3,流控效果为warm up,预热时长为5秒,这样配置之后有什么效果呢:QPS起初会从(3/3/=1)每秒通过一次请求开始预热直到5秒之后达到每秒通过3次请求;前几秒是频繁流控的,直到5秒,QPS阈值达到了3。

设置相应流控规则,流控效果选择Warm Up

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第21张

有激增的流量持续请求订单查询接口http://localhost:4070/query-order,查看实时监控数据,有一个慢慢预热的过程

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第22张

匀速排队
  • 匀速排队方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。有个超时等待时间,一旦超过这个预定设置的时间将会被限流。
  • 适合用于请求以突刺状来到,这个时候我们不希望一下子把所有的请求都通过,这样可能会把系统压垮;同时我们也期待系统以稳定的速度,逐步处理这些请求,以起到“削峰填谷”的效果,而不是拒绝所有请求。

熔断降级

概述

除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第23张

现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。

熔断策略

Sentinel 提供以下几种熔断策略:

  • 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
  • 异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  • 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

慢调用比例

在添加订单接口休眠两秒来演示spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第24张

簇点链路点击熔断按钮进行设置

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第25张

先用用apifox多个线程调用http://localhost:4070/add-order

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第26张

再访问http://localhost:4070/add-order,显示当前请求被流量限制了

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第27张

异常比例

在添加订单接口中添加异常代码

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第28张

设置规则

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第29张

同样先用apifox多个线程调用http://localhost:4070/add-order,然后再访问http://localhost:4070/add-order,显示当前请求被流量限制了

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第30张

同样异常数的设置也是如此

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第31张

热点参数限流

  • 热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制。
  • 热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
  • Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。

热点参数规则(ParamFlowRule)类似于前面列出流量控制规则(FlowRule),详细可以查阅官网

创建测试方法,带路径变量参数

 @RequestMapping("/get/{id}") public String getByOrderId(@PathVariable("id") Integer id){ 
    log.info("getByOrderId id={}",id); return "查询订单正常"; } 

启动程序,访问http://localhost:4070/order/get/1

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第32张

设置热点规则

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第33张

点击编辑进入设置高级选项,增加参数例外项,点击添加

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第34张

访问http://localhost:4070/order/get/2 则可以正常,不受限流阈值2的限制,而连续访问http://localhost:4070/order/get/1 则出现限流

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第35张

统一异常处理

统一异常处理适合对BlockException返回的信息处理是一样的,如果不一样则还是需要使用@SentinelResource

创建统一返回实体类Result

package cn.itxs.ecom.commons.entity; public class Result<T> { 
    private Integer code; private String msg; private T data; public Result(Integer code, String msg, T data) { 
    this.code = code; this.msg = msg; this.data = data; } public Result(Integer code, String msg) { 
    this.code = code; this.msg = msg; } public Integer getCode() { 
    return code; } public void setCode(Integer code) { 
    this.code = code; } public String getMsg() { 
    return msg; } public void setMsg(String msg) { 
    this.msg = msg; } public T getData() { 
    return data; } public void setData(T data) { 
    this.data = data; } public static Result error(Integer code,String msg){ 
    return new Result(code,msg); } } 

添加ItxsBlockExceptionHandler.java

package cn.itxs.ecom.order.exception; import cn.itxs.ecom.commons.entity.Result; 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.fastjson.JSON; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; @Component @Slf4j public class ItxsBlockExceptionHandler implements BlockExceptionHandler { 
    @Override public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws IOException { 
    //getRule返回资源、规则的详细信息 log.info("BlockExceptionHandler BlockException================"+e.getRule()); Result r = null; if(e instanceof FlowException){ 
    r = Result.error(400,"哈哈哈,统一处理方法处接口被限流了"); }else if (e instanceof DegradeException){ 
    r = Result.error(401,"哈哈哈,统一处理方法处服务降级了"); }else if (e instanceof ParamFlowException){ 
    r = Result.error(402,"哈哈哈,统一处理方法处热点参数限流了"); }else if (e instanceof AuthorityException){ 
    r = Result.error(404,"哈哈哈,统一处理方法处理授权规则不通过"); }else if (e instanceof SystemBlockException){ 
    r = Result.error(405,"哈哈哈,统一处理方法处理系统规则不通过"); } //返回Json数据 response.setStatus(200); response.setCharacterEncoding("UTF-8"); response.setContentType(MediaType.APPLICATION_JSON_VALUE); PrintWriter writer=null; try { 
    writer=response.getWriter(); writer.write(JSON.toJSONString(r)); writer.flush(); } catch (IOException ioException) { 
    log.error("异常:{}",ioException); }finally { 
    if(writer!=null) { 
    writer.close(); } } } } 

测试的控制器则无需使用@SentinelResource

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第36张

启动访问http://localhost:4070/order/add ,然后设置流控规则,再次访问,这是则是统一异常返回结果,配置熔断降级规则命中也是如此。

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第37张

系统规则限流

系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 和线程数四个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。在系统规则中设置阈值类型CPU ,阈值为0.1

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第38张

由于我本机CPU一直高于10%的,访问http://localhost:4070/order/add 后会出现系统规则限流了,而调高CPU的阈值如0.8后访问则是正常的。

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第39张

整合OpenFeign使用

准备一个库存微服务,前面我们已经使用,在库存控制器增加一个测试方法和启动库存微服务

 @RequestMapping("/deduct-storage") public String deductStorage(){ 
    int i = 1/0; return "扣减库存"; } 

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第40张

订单微服务中增加一个StorageFeignService的Feign接口声明

package cn.itxs.ecom.order.service; import cn.itxs.ecom.commons.service.openfeign.StorageFeignServiceFackBack; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.RequestMapping; @FeignClient(value = "ecom-storage-service",fallback = StorageFeignServiceFackBack.class) public interface StorageFeignService { 
    @RequestMapping("/deduct-storage") String deductStorage(); } 

创建降级的实现类

package cn.itxs.ecom.order.service; import cn.itxs.ecom.commons.service.openfeign.StorageFeignService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @Component @Slf4j public class StorageFeignServiceFackBack implements StorageFeignService { 
    @Override public String deductStorage() { 
    log.info("进入补偿处理的流程----------"); return "进入补偿处理的流程----------"; } } 

订单控制器增加方法

 @Autowired OrderService orderService; @RequestMapping("/create-order") public String createOrder(){ 
    return orderService.createOrder(); } 

订单服务接口的实现类调用声明库存的Feign接口

package cn.itxs.ecom.order.service.impl; import cn.itxs.ecom.commons.service.OrderService; import cn.itxs.ecom.commons.service.openfeign.StorageFeignService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class OrderServiceImpl implements OrderService { 
    @Autowired private StorageFeignService storageFeignService; @Override public String createOrder() { 
    return storageFeignService.deductStorage(); } } 

启动配置文件增加启用feign整合sentinel

feign: sentinel: enabled: true 

在订单微服务的启动类增加@EnableFeignClients开启

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第41张

访问http://localhost:4070/order/create-order,触发库存服务异常后返回补偿流程提示。

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第42张

规则持久化

DataSource 扩展常见的实现方式有:

  • 拉模式:客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件,甚至是 VCS 等。这样做的方式是简单,缺点是无法及时获取变更;
  • 推模式:规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。

Sentinel 目前支持以下数据源扩展:

  • Pull-based: 动态文件数据源、Consul, Eureka
  • Push-based: ZooKeeper, Redis, Nacos, Apollo, etcd

Dashboard中添加的规则数据存储在内存,微服务停掉规则数据就消失,在⽣产环境下不合适。我们可以将Sentinel规则数据持久化到Nacos配置中⼼,让微服务从Nacos获取规则数据。

添加依赖

 <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency> 

Nacos增加配置

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第43张

启动配置文件中增加

spring: application: name: ecom-order-service cloud: sentinel: enabled: true transport: dashboard: localhost:8858 port: 8719 datasource: # 此处的flow为⾃定义数据源名 flow: # 流控规则 nacos: # server-addr: ${spring.cloud.nacos.discovery.server-addr} server-addr: ${ 
   spring.cloud.nacos.server-addr} namespace: a2b1a5b7-d0bc-48e8-ab65-04695e61db01 data-id: ${ 
   spring.application.name}-flow-rules groupId: order-group username: itsx password: itxs123 data-type: json rule-type: flow # 类型来⾃RuleType类 

快速访问http://localhost:4070/order/add,出现被流控的提示

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第44张

查看sentinel控制台流控规则也是我们在Nacos上的流控规则配置

spring cloud 流量控制_dubbo实战与源码分析 (https://mushiming.com/)  第45张

**本人博客网站 **IT小神 www.itxiaoshen.com

THE END

发表回复