亿级流量网站架构核心技术

月伴飞鱼 2024-08-13 22:02:01
学习书籍 > 编程书籍
支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者!

书籍介绍:https://book.douban.com/subject/26999243/

第1章:交易型系统设计的一些原则

一个好的设计要做到,解决现有需求和问题,把控实现和进度风险,预测和规划未来

  • 不要过度设计,从迭代中演进和完善

墨菲定律:

  • 任何事都没有表面看起来那么简单
  • 所有的事都会比你预计的时间长
  • 可能出错的事总和出错
  • 如果你担心某种情况发生,那么它就更有可能发生

康威定律:

  • 系统架构是公司组织架构的反映
  • 应该按照业务闭环进行系统拆分/组织架构划分
    • 实现闭环/高内聚/低耦合,减少沟通成本
  • 如果沟通出现问题,那么久应该考虑进行系统好组织架构的调整
  • 在合适时机进行系统拆分,不要一开始就把系统/服务拆分得非常细
    • 虽然闭环,但是每个人维护的系统多,维护成本高

第2章:负载均衡与反向代理

第3章:隔离术

目的:将系统或资源分隔开,在系统发生故障时

  • 限定传播范围和影响范围,不会出现雪球效应

隔离方式:

  • 包含了线程隔离、进程隔离、集群隔离、机房隔离、读写隔离、动静隔离、爬虫隔离、热点隔离

Hystrix隔离

基于Servlet3实现请求隔离

  • 将请求解析和业务处理线程池分离
  • 线程池隔离可以方便对整个系统的把控
    • 如根据业务重要性对线程池分解、隔离、监控、运维、降级
  • 异步化可以带来更高的吞吐量,但响应时间也会变长
    • 异步化并不会提升响应时间,但会增加吞吐量和我们需要的灵活性

第4章:限流详解

目的:通过对并发访问/请求进行限速或者一个时间窗口内的请求进行限速来保护系统

  • 一旦达到限制速率则可用拒绝服务、排队或等待、降级

限流算法:

  • 令牌桶算法、漏桶算法

不同层次限流:

  • 应用级限流、分布式限流、接入层限流

节流:

  • 在特定时间窗口内对重复的相同事件最多只处理一次,或者想限制多个连续相同事件最小执行时间间隔
  • 那么可使用节流实现,其能防止多个相同事件连续重复执行

节流主要有几种用法:

  • throttleFirst、throttleLast、throttleWithTimeout

第5章:降级特技

目的:降级的最终目的是保证核心服务可用,即使是有损的

降级预案:

  • 页面降级、页面片段降级、页面异步请求降级
  • 服务功能降级、读降级、写降级、爬虫降级、风控降级

关键字:

  • 自动开关降级、人工开关降级、读服务奖、写服务降级
  • 多级降级、统一配置中心(参考有赞降级热熔断系统Tesla)

Hystrix原理实现

第6章:超时与重试机制

使用超时与重试机制的原因:

  • 实际开发过程中,有许多是因为没有设置超时或者设置得不对而造成的

如果应用不设置超时,则可能会导致请求响应慢,慢请求累积导致连锁反应,甚至造成应用雪崩

  • 读服务天然适合重试,写服务需要根据实际情况(如是否做幂等)

重试次数太多会导致多倍请求流量,即模拟了DDos攻击

  • 参考有赞的Token鉴权超时重试导致流量翻倍,击垮五倍容灾的机器,进而影响线上

超时与重试机制的分类

  • 代理层超时与重试
  • Web容器超时
  • 中间件客户端超时与重试
  • 数据库客户端超时
  • NoSql客户端超时
  • 业务超时(如订单超时未支付取消任务、活动超时自动关闭等)
  • 前端Ajax超时

超时策略处理

  • 重试(等一会再试、尝试其他分组服务、尝试其他机房服务,重试算法可考虑使用如指数退避算法)
  • 摘掉不存活节点(负载均衡/分布式缓存场景下)
  • 托底数据(返回历史数据/静态数据/缓存数据/)
  • 等待页或者错误页

第7章:回滚机制

回滚概念

  • 当程序或数据出错时,将程序或数据恢复到最近的一个正确版本的行为

最常见的如事务回滚、代码库回滚、部署版本回滚、数据版本回滚、静态资源版本回滚等

  • 通过回滚机制可以保证系统在某些场景下的高可用

事务回滚

  • 单库事务回滚这里就不多说了

分布式事务:

  • 最常见的如两阶段提交、三阶段提交协议,这种方式实现事务回滚难度较低,但是对性能影响较大
    • 因为我们大多数场景中需要的是最终一致性,而不是强一致性

因此,可以考虑如事务表、消息队列、补偿机制(执行/回滚)、TCC模式(预占/确认/取消)Sagas模式(拆分事务+补偿机制)等实现最终一致性

部署版本回滚:

  • 部署版本化、小版本增量发布、大版本灰度发布、架构升级并发发布

第8章:压测与预案

在大促来临前,一般会对现有系统进行梳理,发现系统瓶颈和问题

  • 然后进行系统调优来提升系统的健壮性和处理能力

一般通过系统压测来发现系统瓶颈和问题,然后进行系统优化和容灾(如系统参数调优、单机房容灾、多机房容灾等)

即使系统优化和容灾做的非常好,也存在一些不稳定因素,如网络、依赖服务的SLA不稳定等

  • 这就需要我们制定应急预案,在出现这些因素后进行路由切换或降级处理
  • 在大促前进行预案演戏,确定预案的有效性

系统压测流程

  • 压测前要有压测方案:如压测接口、并发量、压测策略(突发、逐步加压、并发量)、压测指标(机器负载、QPS/TPS、响应时间)
  • 之后要产出压测报告【压测方案、机器负载、QPS/TPS、响应时间(平均、最小、最大)、成功率、相关参数(JVM参数、压缩参数)等
  • 最后根据压测报告分析的结果进行系统优化和容灾

线下压测

  • 通过如JMeter、Apache ab压测系统的某个接口或者某个组件
    • 然后进行调优,实现单个接口或组件的性能最优

线上压测:

  • 按读写分为读压测、写压测和混合压测,按数据仿真度分为仿真压测和引流压测
    • 按是否给用户提供服务分为隔离集群压测和线上集群压测

单机压测可以评估出单机极限处理能力,但单机压测结果不能反映集群整体处理能力

  • 在压测时,需要选择离散压测,即选择的数据应该是分散的或者长尾的
  • 为了保证压测的真实性,应该进行全链路压测

系统优化:

  • 拿到压测报告后,接下来会分析报告,然后进行一些有针对性的优化
    • 如硬件升级、系统扩容、参数调优、代码优化(如代码同步概异步)、配置调优
      • 慢查询优化、架构优化(如加缓存、读写分离、历史数据归档)等

应用扩容:

  • 可根据去年流量、与运营业务方沟通促销力度、最近一段时间的流量、预计GMV增长等来评估出是否需要进行扩容,需要扩容多少倍
  • 扩容之后还要预留一些机器应对突发情况,在扩容上尽量支持快速扩容,从而在出现突发情况时可以几分钟内完成扩容

系统容灾:

  • 在扩容时要考虑系统容灾,比如分组部署、跨机房部署

    • 容灾是通过部署多组(单机房/多机房)
  • 相同应用系统,当其中一组出现问题时,可以切换到另一个分组,保证系统可用

需要应急预案的原因:

  • 在进行系统容灾后,仍然会存在一些风险,如网络抖动、某台机器负载过高、某个服务变慢、数据库oad值过高等
    • 为了防止因为这些问题而出现系统雪崩,需要针对这些情况制定应急预案,从而出现突发情况时
      • 有相应的措施来解决掉这些掉这些问题

应急预发步骤:

  • 首先进行系统分级,然后进行全链路分析、配置监控报警,最后制定应急预案,预案演练等

系统分级:

  • 针对交易型系统可以按照交易核心系统和交易支撑系统进行划分
    • 实际系统分级要根据公司特色进行,目的是对不同级别的系统实施不同的质量保障,核心系统要投入更多资源保障系统高可用
      • 外围系统要投入较少资源允许系统暂时不可用

全链路分析,制定应急预案:

  • 对核心场景进行全链路分析,从用户入口到后端存储,梳理出各个关键路径,对相关路径进行评估并制定预案
    • 即当出现问题时,该路径可以执行什么操作来保证用户可下单、可购物,并且也要防止问题的级联效应和雪崩效应
      • 梳理系统关键路径,包括网络接入层、应用接入层、Web应用层、服务层、数据层等,并制定应急预案

第9章:应用级缓存

缓存回收策略:

  • 基于空间、基于容量、基于时间(TTL、TTI)
  • 基于Java对象引用(软引用、弱引用)
  • 基于会是算法(FIFO、LRU(Leas Recently Used)、LFU(Lease Frequently Used))

Java缓存类型:

  • 堆缓存(最快,没有序列化/反序列化,但GC暂停时间会变长)(Guava Cache、Ehcache 3.x、MapDB
  • 堆外缓存(比堆缓存慢,需要序列化/反序列化,可以减少GC暂停时间)(Ehcache 3.x、MapDB)
  • 磁盘缓存(Ehcache 3.x、MapDB)
  • 分布式缓存(Redis、Memcached)

缓存使用模式

  • Cache-Aside,即业务代码围绕着Cache写,是由业务代码直接维护缓存

    • Cache-Aside适合使用AOP模式去实现
  • Cache-AS-SoR(system of record,或者可以叫数据源)

    • 所有的操作都是对Cache进行,然后再委托给SoR进行真实的读/写
    • 即业务代码中只看到Cache的操作,看不到关于SoR相关的代码

Cache-AS-SoR共有三种实现方式

  • Read-Through,业务代码首先调用Cache
    • 如果Cache不命中,由Cache回源到SoR,而不是业务代码(即由Cache读SoR)
    • Guava Cache和Ehcache 3.x都支持该模式
  • Write-Through,被称为穿透写模式/直写模式
    • 业务代码首先调用Cache写(新增/修改)数据,然后由Cache负责写缓存和写SoR,而不是业务代码
    • Ehcache 3.x支持
  • Write-Behind,也叫Write-Back,我们称之为回写模式
    • 不同于Write-Through是同步写SoR和Cache,Write-Behind是异步写
    • 异步之后可以实现批量写、合并写、延时和限流

第10章:HTTP缓存

HTTP缓存

  • 服务端响应的Last-Modified会在下次请求时,将If-Modified-Since请求头带到服务器端进行文档是否修改的验证

    • 如果没有修改则返回304,浏览器可以直接使用缓存内容
  • Cache-Control:

    • max-age和Expires用于决定浏览器端内容缓存多久,即多久过期
      • 过期后则删除缓存重新从服务器端获取最新的
        • 另外,可以用于from cache场景
  • HTTP/1.1规范定义ETag为被请求变量的实体值,可简单理解为文档内容摘要

    • ETag可用来判断页面内容是否已经被修改了

HttpClient客户端缓存、Nginx配置、Nginx代理层缓存

第11章:多级缓存

多级缓存,是指在整个系统架构的不同系统层级进行数据缓存

  • 以提升访问效率,这是应用最广的方案之一

维度华缓存与增量缓存、大Value缓存处理、热点缓存处理

缓存分布式及应用负责均衡算法选择

  • 负载较低时,使用一致性哈希

  • 热点请求降级一致性哈希为轮询,或者如果请求数据有规律

    • 则可考虑带权重的一致性哈希
  • 将热点数据推送到接入层Nginx,直接响应给用户

热点数据

  • 热点数据会造成服务器压力过大,导致服务器性能、吞吐量、带宽达到极限
    • 出现响应慢或者拒绝服务的情况,这肯定是不允许的

单机全量缓存+主从(热点数据解决方案1)

  • 一般不采用,针对缓存量比较大的应用不适用

分布式缓存+应用本地热点(热点数据解决方案2)

  • 接入Nginx将请求转发给应用Nginx
    • 这种更适合应用层面的,对于服务内部的缓存,还可以采用Guava cache等本地的堆内缓存、堆外缓存等

应用Nginx首先读取本地缓存

  • 如果命中,则直接返回,不命中会读取分布式缓存、回源到Tomcat进行处理

应用Nginx会将请求上报给实时热点发现系统

  • 上报给实时热点发现系统后,它将进行热点统计

根据设置的阈值将热点数据推送到应用Nginx本地缓存

缓存数据一致性

  • 订阅数据变更消息

  • 如果无法订阅消息或者订阅消息成本比较高,并且对短暂的数据一致性要求不严格

    • 比如,在商品详情页看到的库存,可以短暂的不一致,只要保证下单时一致即可
      • 那么可以设置合理的过期时间,过期后再查询新的数据
  • 如果是秒杀之类的(热点数据),可以订阅活动开启消息

    • 将相关数据提前推送到前端应用,并将负载均衡机制降级为轮询
  • 建立实时热点发现系统来对热点进行统一推送和更新

缓存崩溃恢复

  • 主从机制,做好冗余,即其中一部分不可用,将对等的部分补上去
  • 如果因为缓存导致应用可用性已经下降,可以考虑部分用户降级
    • 然后慢慢减少降级量,后台通过Worker预热缓存数据

第12章:连接池线程池详解

池化技术:

在应用系统开发中,我们经常会用到池化技术,如对象池、连接池、线程池等,通过池化来减少一些消耗,以提升性能

对象池通过复用对象从而减少创建对象、垃圾回收的开销,但是池不能太大,太大会影响GC时的扫描时间

连接池如数据库连接池、Redis连接池、HTTP连接池,通过复用TCP连接来减少创建和释放连接的时间来提升性能

  • 线程池也是类似的,通过复用线程提升性能
    • 也就是说池化的目的是通过复用技术提升性能

连接池使用的建议

  • 注意网络阻塞/不稳定时的级联效应,连接池内部应该根据当前网络的状态(比如超时次数太多)

    • 对于一定时间内的(如100ms)全部timeout,根本不进行await(maxWait)
      • 即有熔断和快速失败机制
  • 当前等待连接池的数目超过一定量时,接下来的等待是没有意义的,还会造成滚雪球效应

  • 等待超时应尽可能小点(除非很必要)

    • 即使返回错误页,也比等待并阻塞强

线程池

更多的Java TreadExecutors相关的内容

相关配置项如:

  • 核心线程池大小、线程池最大大小、线程的最大空闲时间(超过则回收,直到线程数减为核心线程大小)
  • 线程池的任务缓冲队列、创建线程的工厂、缓冲队列满后的拒绝策略等
    • 另外还有支持延时任务的线程池

线程池使用的建议

  • 根据任务类型是IO密集型还是CPU密集型、CPU核数,来设置合理的线程池大小、队列大小、拒绝策略
    • 并进行压测和调优来决定适合场景的参数
  • 使用线程池时务必设置池大小、队列大小并设置相应的拒绝策略
    • 不如可能导致瞬间线程数过高、GC慢等问题,造成系统响应慢甚至OOM

第13章:异步并发实战

异步与并发

当一个线程在处理任务时,通过Fork多个线程来处理任务并等待这些线程的处理结果,这种并不是真正的异步,这只是并发

  • 异步是针对CPU和IO的,当IO没有就绪时,要让出CPU来处理其他任务,这才是异步

异步Future

  • 线程池配合Future实现,但是阻塞主请求流程,高并发时依然会造成线程数过多、CPU上下文切换

异步Callback

  • 通过回调机制实现,即首先发出网络请求,当网络返回时回调相关方法,采用线程池分发事件通知,从而有效支撑大量并发连接
    • 这种机制并不能提升性能,而是为了支撑大量并发连接或者提升吞吐量

异步编排CompletableFuture

  • JDK 8 CompletableFuture提供了新的异步编程思路,可以对多个异步处理进行编排,实现更复杂的异步处理
    • 其内部使用了ForkJoinPool实现异步处理
      • 比如三个服务异步并发调用、两个服务并发调用、服务1执行完后再并发执行服务2服务3等场景

异步Web服务实现

请求缓存

请求合并

第14章:如何扩容

跨库事务

  • 当然分布式事务是一种方式,另一种方式可以考虑sharding-jdbc提供的柔性事务实现
    • 目前支持最大努力送达,就是当事务失败后通过最大努力反复尝试,是在假定数据库操作一定可以成功的前提下进行的,保证数据最终的一致性
      • 其使用场景是幂等性操作,如根据主键删除数据、带主键插入数据

柔性事务

sharding-jdbc的最大努力送达型柔性事务分为同步送达和异步送达两种,同步送达不需要Zookeeper和elastic-job,内置在柔性事务模块中

异步送达比较复杂,是对柔性事务的最终补充,不能和应用程序部署在一起,需要额外地通过elastic-job实现

最大努力送达型事务也可能出现错误,即无论如何补充都不能正确提交

  • 为了避免反复尝试带来的系统开销,同步送达和异步送达均可配置最大重试次数
    • 超过最大重试次数的事务将进入失败列表

数据异构

查询维度异构、聚合据异构

分布式任务

Quartz支持任务的集群调度,如果一个实例失效,则可以漂移到其他实例进行处理,但是其不支持任务分片

tbschedule和elastic-job除了支持集群调度特性

  • 还不支持任务分片,从而可以进行动态扩容/缩容

Elastic-Job

Elastic-Job是当当开源的一款分布式任务调度框架,目前提供了两个独立子项目:

  • Elastic-Job-Lite和Elastic-Job-Cloud

Elastic-Job-Lite定位为轻量级无中心解决方案,可以动态暂停/恢复任务实例,目前不支持动态扩容任务实例

  • Elastic-Job-Cloud使用Mesos+Docker解决方案
    • 可以根据任务负载来动态实现启动/停止任务实例,以及任务治理

Elastic-Job-Lite功能与架构

  • Elastic-Job-Lite实现了分布式任务调度、动态扩容缩容、任务分片、失效转移、运维平台等功能

Elastic-Job-Lite采用去中心化的调度方案,由Elastic-Job-Lite的客户端定时自动触发任务调度

通过任务分片的概念实现服务器负载的动态扩容/缩容,并且使用Zookeeper作为分布式任务调度的注册和协调中心

  • 当某任务实例崩溃后,自动失效转移,实现高可用,并提供了运维控制台,实现任务参数的动态修改

第15章:队列术

使用队列注意点

  • 我们要考虑是否需要保证消息处理的有序性及如何保证,是否能重复消费及如何保证重复消费的幂等性

队列应用场景

  • 异步处理、系统解耦、数据同步、流量削锋、扩展性、缓冲等

缓冲队列

比如Log4j日志缓冲区就是使用的缓冲队列。使用缓冲队列应对突发流量时,并不能使处理速度变快,而是使处理速度平滑

  • 从而不因瞬间压力太大而压垮应用。通过缓冲区队列可以实现批量处理、异步处理和平滑流量

任务队列

  • 使用任务队列可以将一些不需要与主线程同步执行的任务扔到任务队列进行异步处理
    • 可以实现异步处理、任务分解/聚合处理

消息队列

  • 通过消息队列可以实现异步处理、系统解耦和数据异构

请求队列

  • 类似在Web环境下对用户请求排队,从而进行一些特殊控制:流量控制、请求分级、请求隔离
    • 例如将请求按照功能划分不同的队列,从而使得不同的队列出问题后相互不影响
      • 还可以对请求分级,一些重要的请求可以优先处理(发展到一定程度应将功能物理分离)

数据总线队列

混合队列

  • 如MQ与Redis的协同组合使用
    • Disruptor+Redis队列(P303)

下单系统水平可扩展架构

缓冲表+缓存+同步worker+降级

基于Canal实现数据异构

背景:在大型网站架构中,一般会采用分库分表来解决容量和性能问题

  • 但分库分表后,不同维度的查询或者聚合查询就会非常棘手,而且这种方式在大流量系统架构中肯定是不行的
  • 一种解决方式就是数据异构,可以包含具体场景接口的异构、商家维度的异构、ES搜索异构、订单缓存异构等

第16章:构建需求响应式亿级商品详情页

单品页流量特点

单品页流量的特点是离散数据、热点少,可以使用各种爬虫、比价软件抓取

商品详情页架构设计原则

  • 数据闭环
  • 数据维度化
  • 拆分系统
  • Worker无状态化+任务化
  • 异步化+并发化
  • 多级缓存化
  • 动态化(数据动态化、模板渲染实时化、重启应用秒级化、需求上线快速化)
  • 弹性化(软件打包成基础镜像,自动扩容)
  • 降级开关
  • 多机房多活

第17章:京东商品详情页服务闭环实践

设计一个高度灵活的系统时,要想着当出现问题时怎么办

  • 是否可降级?不可降级怎么处理?是否会发送滚雪球问题?如何快速响应异常?
  • 服务如何更好更有效或者在异常下工作?

京东商品详情页整体流程

请求首先进入Nginx,Nginx调用Lua进行一些前置逻辑处理

  • 如果前置逻辑不合法,那么直接返回,然后查询本地缓存,如果命中,则直接返回数据

如果本地缓存未命中数据,则查询分布式Redis集群:

  • 如果命中数据,则直接返回。

如果分布式Redis集群未命中数据,则调用Tomcat进行回源处理

  • 然后把结果异步写入Redis集群,并返回

第18章:使用OpenRestry开发高性能Web应用

OpenResty简介

OpenResty由Nginx、Lua、ngx_lua模块构成

  • ngx_lua是章亦春编写的Nginx的一个模块,将Lua嵌入到Nginx中,从而可以使用Lua来编写脚本,部署到Nginx中运行,即Nginx变成了一个Web容器
    • 这样就可以使用Lua来开发高性能的Web应用了

OpenResty生态

  • OpenResty提供了一些常用的ngx_lua开发模块,如lua-resty-memcached、lua-resty-mysql、lua-resty-redis、lua-resty-dns、lua-resty-dns、lua-resty-limit-traffic、lua-resty-template
  • 这些模块涉及如Mysql数据库、Redis、限流、模块渲染等常用功能组件
    • 另外,也有很多第三方的ngx_lua组件供我们使用

京东使用OpenResty场景

  • Web应用:

    • 会进行一些业务逻辑处理,设置进行CPU的模板渲染,一般流程包括mysql/Redis/HTTP获取数据、业务处理、产生JSON/XML/模板渲染内容
      • 比如,京东的列表页/商品详情页
  • 接入网关:

    • 实现如数据校验前置、缓存前置、数据过滤、API请求聚合、A/B测试、灰度发布、降级、监控等功能
      • 比如,京东的交易大Nginx节点、无线部门的无线网关、单品页统一服务、实时价格、动态服务
  • Web防火墙:

    • 可以进行IP/URL/UserAgent/Referer黑名单、限流等功能
  • 缓存服务器:

    • 可以对响应内容进行缓存,减少到底后端的请求,从而提升性能
  • 其他:

    • 如静态资源服务器、消息推送服务、缩略图裁剪等

基于OpenResty的常用架构模式

  • 负载均衡
  • 单机闭环
  • 分布式闭环
  • 接入网关

常见的一些问题

  • 在开放Nginx应用时,使用UTF-8编码可以减少很多麻烦
  • GBK转码解码时,应使用GB18030,否则一些特殊字符会出现乱码
  • cjson库对于像\uab1这种错误的Unicode转码会失败,可以使用纯Lua编写的dkjson
  • 社区版Nginx不支持upstream的域名动态解析,可以考虑proxy_pass,然后配合resolver来实现
    • 或者在Lua中进行HTTP调用。如果DNS遇到性能瓶颈,则可以考虑再本机部署如dnsmasq来缓存
      • 或者考虑使用balancer_by_lua功能实现动态upstream
  • 为响应添加处理服务器IP的响应头,方便定位问题
  • 根据业务设置合理的超时时间
  • 运行CDN的业务,当发生错误时,不要给返回的500/503/302/301等非正常响应设置缓存

第19章:应用数据静态化架构高性能单页Web应用

传统静态化及页面构建需要考虑的问题

  • 页面模板部分变更类需要重新全部生成
  • 动态化模板渲染支持
  • 数据和模板的多版本化:生成版本、灰度版和预发布版本
  • 版本回滚问题,假设当前发布的生成版本出问题了,如何快速回滚到上一个版本
  • 异常问题,假设渲染模板时,遇到了异常情况(比如Redis出问题了),该如何处理
  • 灰度发布问题,比如切20%量给灰度版本
  • 预发布问题,目的是在正式环境测试数据和模板的正确

常见异常问题

  • 本机从发布数据存储Redis和主发布数据存储Redis都不能用了
    • 那么可以直接调用CMS系统暴露的HTTP服务,直接从元数据存储Mysql获取数据
  • 数据和模板获取到了,但是渲染模板出错了,比如遇到500、503
    • 解决方案是使用上一个版本的数据进行渲染
  • 数据和模板都没问题,但是因为一些疏忽,渲染出来的页面错乱了,或者有些区域出现了空白
    • 对于这种问题没有很好的解决方案。可以根据自己的场景定义异常扫描库
      • 扫到当前版本有异常就发警告给相关人员,并自动降级到上一个版本
支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者!