服务器 频道

GitLab稳定性工程建设|得物技术

  一 前言

  GitLab作为支撑技术部门各种研发平台工具的核心系统之一,系统的稳定性保障尤其重要!内部的研发相关的工具平台大部分都底层依赖GitLab系统,可以试想一下:一旦GitLab系统挂了,哪些系统受影响?哪些研发活动不能正常进行?代码不能提交合并、发布系统不能使用、App不能打包、新的代码不能进行测试验证等……研发活动将完全处于停止状态。由此可见GitLab系统的稳定性的优先级非常高!GitLab的稳定性建设一方面要从架构上升级,另一方面也要持续治理使用场景,规避那些不合理的使用行为。今天在这里将GitLab系统的稳定性建设过程通过文字的形式分享给大家,欢迎大家一起交流探讨!

  二 架构演进过程

  在得物GitLab系统的架构模式分为三个阶段,具体如下:  

  本地单体模式

  在2022年2月份之前,GitLab是本地化部署单体模式,是指所有的组件都部署在一台服务器上,包括redis、postgresql数据库,存储盘使用的机械盘,通过翻阅过往的文档资料了解到该阶段实际上经历过两个小的阶段:虚拟机部署、物理机部署,该模式下可能会出现如下问题:

  虚拟机模式:分配的CPU、内存可能不足导致GitLab访问异常的问题

  物理机模式:受限于本地机房的保障措施,例如电源插头、网络保障、电力保障、硬件生命周期等

  本地机房部署有一定的优势和弊端都非常明显,优势如下:

  代码都存储在本地机房安全可靠

  本地网络环境内的访问速度较高

  在2022年人员增长的阶段,GitLab访问量越来越大,本地机房单体化模式的弊端也很明显,例如:

  本地机房稳定性保障不够,面临着断电、断网问题

  受限于硬件现有的配置,机器升级流程慢(从评估采购合理性、采购流程、新机器采购、部署到机房、迁移数据等,至少得一两个月的时间)

  故障恢复时间长,例如机房服务器宕机、硬件坏掉这些风险点一旦发生,将是灾难性的故障

  结合当前的现状来分析,GitLab已经不适合部署在本地机房了,因为技术部不仅有上海区域,也有杭州区域,发布平台也部署在云上环境,从稳定性的角度来看GitLab上云部署非常有必要,而且上云后的影响利大于弊,上云后有如下优势:

  机器硬件升配扩容方便,不再依赖漫长的采购流程

  数据库、存储的备份依赖云产品的快照备份能力

  云产品的SLA高于本地机房

  由此开启了GitLab上云利用云产品的高可用来做稳定性保障的新的阶段。

  云产品主备模式

  2022年年初开始启动GitLab上云的工作,前期也经过了大量的评估讨论,因为GitLab系统上云不是简单的在云上购买一台服务器将数据完全迁移进去,而是要一些中间件的拆分,例如将redis、postgresql拆分出来为独立的实例,与GitLab服务自身隔离,避免CPU、内存压力都在一台服务器上,同时增强了GitLab服务的QPS承载能力,在云产品选型上选取的多区可用的产品,nas盘作为存储设备,基于安全的角度考虑选的是加密产品型号,并且限制了挂载的服务器IP。在上云初期也考虑过容器化模式,为什么不选容器化模式,原因如下:

  GitLab自身是有状态的服务,未拆分组件的情况下直接启动多个Pod会导致并发写的问题

  在高峰期CPU和内存的消耗很高,容器模式下单个pod需要分配较高的CPU、内存,但性能却比不上物理机

  部署在k8s集群后,不仅要踩坑部署后的各种问题处理及优化,另一方面需要熟悉k8s集群的各种维护事项,日常维护成本大大增加

  有状态的服务并且只能启动一个Pod,依然避免不了单点的问题,性能还差,那为什么不直接物理机部署呢?而且额外增加非常大的维护成本,所以容器化方案被放弃,下面来回顾一下上云的过程!

  上云的架构图

  上云最主要是要解决本地资源瓶颈带来的稳定性问题,在架构设计上以最简单改造,架构图如下:  

  当前架构为主备模式,关键点如下:

  postgresql、redis拆分为单独的外部实例,实例自身高可用

  存储改为nas盘,每小时进行快照备份

  主备两台服务器安装GitLab服务,备节点默认情况下不启动

  每天晚上完整的备份一次数据到备节点

  该模式最主要的挑战是需要对GitLab系统的配置文件深度了解以及数据迁移问题,同时也要确保备节点启用后服务依然能正常访问,数据不出现落后的问题。GitLab的配置文件里提供了外部redis、postgresql的配置方法,一些参数调优可以通过阅读官方文档以及大量的测试验证来解决,主要需要需要的是:authorized_keys共享的问题,当时有两个方案:一是放nas盘、二是数据库存储,该文件对权限验证要求较高,nas盘的方案经过验证后行不通,所以采用了GitLab推荐的数据库存储模式,在官方文档里对读文件迁移到数据库也有步骤说明。

  上云的过程

  在云上搭建一套环境,修改调整配置文件并没有什么难度,最关键的是数据迁移问题,那如何解决呢?由于我们的数据体量较大,本地机房到杭州生产环境直接上传受限于专线带宽(当时专线带宽较小),想要把这么大的数据迁移到云上,通过OSS走公网中转速度比直接上传要快很多,按当时本地机房服务器到云存储的公网带宽,这么大的数据传输也需要消耗非常长的时间,而且还只能低峰期进行,长时间的同步又存在增量数据丢失问题,需要停机操作,经过多轮测试验证,初步评估完全的数据迁移需要20+个小时!

  20+个小时的停机迁移显然也不现实,即使是周末也仍然有较多的GitLab使用需求,还需要考虑紧急修复线上问题的场景支持的问题!所以迁移方案的制定是一个比较大的挑战!为了最短时间内完成上云数据迁移以及应对紧急需求场景支持的问题,迁移方案如下:

  第一步:提前一周部署好云上的GitLab环境,域名访问通过绑host,此时是一个空的环境

  第二步:将本地数据进行全量备份,还原到云上的GitLab环境,此步骤历经25个小时,这一步我们有了一个基于备份的全量数据的环境,数据版本停留在备份的时刻,后面需要解决增量的问题

  第三步:每天晚上本地代码文件增量同步到云上的nas盘内,光这一步还不够,因为数据库的数据没有同步

  第四步:简化备份脚本,只备份数据库、redis缓存,在1.5小时内即可得到数据的完整备份

  第五步:串联文件同步脚本+数据备份还原脚本,执行同步还原,同时执行数据的备份还原以及增量文件的同步,将最新的备份上传至云上环境,降低增量的数据量,经过多天的测试验证,一天的增量数据从本地机房迁移到云上4个小时左右即可完成,验证操作过程形成SOP

  第六步:执行正式的数据迁移,旧的环境开启只读模式,避免迁移过程的增量数据丢失问题;若有紧急需求临时开启单个仓库的代码提交通道,后续将该仓库重新推送一次代码即可;

  第七步:数据迁移完后切换域名解析即生效

  方案是可行了,为了控制影响范围以及应对一些突发情况,所以计划迁移过程定在周末的夜间12点后操作,对外提前3天通知说是将停机迁移(实际上仅仅开启只读模式,紧急需求依然可以快速支持),考虑到一些突发情况的影响以及功能验证的耗时,我们计划在8小时内完成整体迁移+验证工作。由于前期验证工作充足,迁移的过程比较顺利,经过测试验证,功能也都符合预期。

  未考虑到的一些问题

  尽管完成了GitLab服务上云的工作,但由于网络环境的复杂,也有一些问题未考虑到位,例如:

  本地机房ios打包机全量拉代码打包慢、部分webhook地址不通,ios打包慢的问题通过打包机缓存+带宽定向调优解决

  webhook不通是由于要访问的地址有公网解析、内网解析、办公网解析各种情况,这类问题通过修改网络策略以及解析方式解决

  需要改进的方向

  主备模式支撑2022~2023年的一段时间里,在此期间未出现过系统架构的故障问题,由于GitLab的使用场景不仅仅是日常开发代码提交代码,还有内部的其他平台对接GitLab API的调用,如:代码管理平台、持续集成流水线、发布平台、算法训练任务、安全白盒检测、覆盖率平台等等,但该架构模式下仍然存在一定的优化空间:

  主备模式严格意义上来说并非较好的高可用架构方案,需要人工介入切换

  nas盘并不支持多区可用的特性,nas盘也存在出问题的风险点

  由于rails组件和gitaly组件部署在一起,这么多的仓库的文件操作都在一台服务器上,导致主服务器的峰值压力过大,在一些特定场景下会导致CPU峰值过高,如下图所示:  

  所以整体上来看,GitLab系统架构仍然需要升级!

  多区多活集群模式备模式

  通过大量阅读消化GitLab官方文档,以及模式初步验证后得知目前我们的GitLab版本支持Cluster集群架构的改造!云产品多区集群模式是指基于云产品部署多区多ECS节点的Cluster集群架构,过去我们都没有GitLab Cluster集群的维护经验,起初对这方案也有较多的担忧,担忧的点并非在于架构改造的问题,部署一个空的Cluster集群环境很简单,难点在于这种架构模式的升级过程影响范围,会不会出现意料之外的问题以及过万级别仓库的数据迁移,还有回滚预案!下面一步步的来看看过程!

  Cluster集群架构

  通过阅读GitLab官方文档得知我们现有的版本也支持Cluster集群架构,在这版本之前也经历过较多版本的迭代更新,所以理论上来说应该也是一个比较成熟的版本了。基于官方文档的阅读学习以及Demo验证,整理出来的适合得物现状的架构如下:  

  该架构最主要的优势是多区多活,另外还可以按仓库来指定存储池,也就是说可以根据仓库的特征来把仓库分配到不同的节点机器里做到互相不影响,架构将GitLab服务拆分为三个组件,这三个组件的作用分别为:

  Rails:提供web服务,例如GitLab页面的的访问就上由该组件提供服务的,另外在每一次请求中还需要经过它来做身份认证,该组件服务消耗内存

  Praetect:负责将Rails节点接受到文件操作的请求的转到Gitaly节点,充当一个桥梁作用,Praetect负责多Gitaly节点的数据一致性保障,该组件服务只需要较低的资源

  Gitaly:负责与磁盘上的代码文件进行交互,例如Clone/pull代码、新增的代码更新、新增分支、新增tag都由Gitaly负责读取或写入存储盘,该服务大量操作磁盘,对CPU和磁盘的吞吐要求较高

  由单一服务拆分为多组件后,架构模式也变得复杂,该Cluster集群架构的核心点在于:

  GitLab服务拆分为Rails、Praetect、Gitaly三个组件,每个组件分别部署3台服务器(Praetect、Gitaly部署在相同节点上),分布在两个可用区

  Rails节点用于提供web服务,上层通过SLB为用户提供统一访问入口

  Rails节点与Gitaly的节点通讯采用SLB到Praetect,由Praetect进行访问Gitaly,Praetect先将数据写入Gitaly主节点,再将数据复制到Gitaly辅节点来保证三副本数据最终一致性

  代码文件存储从nas盘改为SSD存储,降低网络存储的延时

  一个新的架构部署一套新的环境并没有太大的难点,比较复杂的是多台服务器多个节点均有不同的配置参数,从主备模式服务到Cluster集群模式有几十处配置需要调整,一个配置错误就影响功能的正常运行。然而这并非最有挑战的地方,毕竟是一个全新的环境,在正式上线前即使配置错误也并不会带来任何影响,后期可以通过大量的功能验证来修复这些配置。最有挑战的地方在于架构的切换以及数据迁移,下面来基于Cluster集群配置、架构切换、数据迁移三个方向来分别讲下架构升级的过程。

  Cluster集群部署

  Cluster集群架构相比之前变得复杂,需要的资源也相应的增加,在资源选型上要特别注意可用区分布以及跨区耗时的问题,Cluster集群配置需要的资源有:

  SLB分别用于访问rails节点为外部提供服务以及rails访问Praetect节点

  Rails节点、Praetect和Gitaly节点,需要分布在不同的可用区,可用区之间的延时越低越好

  SSD存储盘,挂载到Gitaly节点服务器

  PG数据库实例、Redis在Rails节点、Praetect都会使用到

  nas盘挂载到Rails节点,nas盘在这里主要的作用是用于gitlab-ci任务的制品共享,该数据并非核心存储,若遇到极端的nas挂的情况下可以将Rails改成单节点本地存储提供服务

  准备好资源后,需要在准备好的6台服务器上都安装好GitLab服务,安装好即可无需启动,因为在启动之前需要先修改较多的配置,配置修改的关键点如下:

  基础的配置:

  域名、证书等基础配置的参数修改

  PG数据库、Redis的链接配置参数,Rails节点与Praetect是使用不同的库(为避免直接影响生产环境,这里的数据库和Redis均采用临时的实例,与生产环境数据隔离)

  SLB上的节点端口配置,Rails节点相关的端口,Praetect节点相关的端口,确保网络策略放行这些端口

  Rails节点关键配置:

  关闭默认开启的Gitaly、Praetect组件功能以及其他不需要的组件

  git_data_dirs指定存储池,需要指定两个存储池default和gitaly_cluster,官方文档里提到default是必需的,gitaly_address需要指定为为Praetect节点准备的SLB地址,另外基于安全考虑,在通信过程中需要增加Token认证,这个认证的Token多处使用到,需要保持一致  

  修改gitlab_workhorse监听地址为tcp模式,默认是unix,监听端口需要与后面的gitaly访问的端口保持一致

  修改指定的gitaly访问的监听配置端口为后面gitaly节点上监听的端口

  修改gitlab_rails['shared_path']参数为nas盘的挂载的路径

  Gitaly节点关键的配置:

  关闭默认开启的Rails组件功能以及其他不需要的组件

  gitlab-secrets.json配置需要与Rails节点上的该文件保持一致,reconfigure后手动复制覆盖即可

  修改Praetect、Gitaly组件的Token信息,注意与Rails节点里配置保持一致

  上述配置信息并不完整,仅列出几个关键的配置点,例如还有prometheus、grafana、Sidekiq参数均需要根据实际情况来调整,每一个节点都需要根据节点类型来进行修改。

  各个节点配置完成后,分别在各个节点上执行reconfigure并启动服务,检查基础功能是否正常!在得物除了修改基础的配置外还不够,还需要额外修改自动生成的nginx配置来保证我们一些额外的功能的正常运行!这里还涉及到一个小版本的升级,在后面的数据迁移部分的有原因说明!另外还需要经过压测,这里可以使用GitLab官方提供的压测工具:gitlab performance tool(https://gitlab.com/gitlab-org/quality/performance),具体方法这里就省略了。

  存储架构切换

  新的Cluster集群环境配置好后,下一步要做的事情就是将生产环境切到新架构,这里分为三个关键的步骤:存储架构上线Cluster存储模式、数据迁移、服务架构切换,先来说下存储架构切换,存储架构这里最主要的是要支持Cluster模式的storage存储池,为后续数据迁移做准备,这里比较好的方式是基于现有的主备模式架构挂载新的Cluster存储池,这样既不影响服务的正常运行,也完成了数据迁移的准备工作。操作方法为:

  修改上述搭建好的Cluster集群里的Gitaly节点上的数据库配置为主备模式服务生产环境所用的配置

  主备模式服务生产主节点里的git_data_dirs的default为默认存储不变,gitaly_cluster存储改为新的存储池SLB地址

  gitlab_workhorse、gitaly监听地址、认证Token修改方式与Rails节点一致

  拷贝主备模式下的gitlab-secrets.json文件到各个Gitaly节点机器

  其他配置保持不变,这样就有一个原有主备模式服务作为主节点提供Rails服务和Gitaly默认存储池服务,Gitaly三台服务器提供新的Cluster存储池服务,默认情况下依然使用原有的默认存储池。这样就完成了新的Cluster集群存储池的挂载。此时架构变为如下模式:  

  在GitLab管理后台可以将调整存储池的权重,将新的Cluster存储池的权重修改为100%,以便于新创建的仓库直接在存新存储池上,配置如下图:  

  存量数据迁移

  架构升级完后一周内新创建的仓库都运行良好!下一步要做的就是如何将存量的万级别的仓库迁移到新的Cluster存储池,若不迁移则无法发挥Cluster集群模式的优势,所以这一步必不可少!GitLab社区版本默认提供单个仓库的迁移能力,只有企业版本才提供更高级的迁移能力,在执行迁移之前还需要开启一个Flag特性才支持迁移。开启该Flag的方法如下:

  gitlab-rails console# 检查配置项,默认是falseFeature.enabled?(:gitaly_replicate_repository_direct_fetch)# 开启Feature.enable(:gitaly_replicate_repository_direct_fetch)

  我们仓库数量众多,手动一个一个的迁移非常费劲,而且还担心一旦迁移失败要该如何回滚的问题,为了方便数据迁移,我们基于GitLab的API编写了迁移脚本,迁移脚本具备如下功能:

  支持批量迁移用户的snippets代码片段

  支持批量迁移多个仓库代码

  支持按指定的仓库进行迁移

  支持按Group进行迁移Group下所有的仓库

  支持查询迁移的状态(部分仓库确实存在失败的情况,例如脏数据引起的readonly)

  准备好迁移脚本后,迁移的策略也很重要,比如什么时间段迁移?先迁移哪些数据?迁移后有问题怎么办?都是未知数,所以必须有比较稳妥的迁移策略。经过测试阶段的验证,我们使用的这个GitLab 13.12.2的版本,在一定概率下在迁移过程中会导致仓库数据丢失,官方论坛里也找到了相关的issue,详情见:https://gitlab.com/gitlab-org/gitaly/-/issues/3752,这是一个比较大的风险!但好在这个问题在13.12.12版本里修复了,为了解决该问题,我们只好转向13.12.12版本,但这将是一次版本升级!好在这是一个小版本号的升级,理论上来说升级无风险,但了避免其他风险问题,我们也做了非常多的功能验证,不仅仅如此!我们还通过官方历史版本升级说明文档从13.12.2~13.12.12之间的升级说明都阅读了一遍,还看了一些代码的变更过程,确保升级万无一失!由于是小补丁版本升级,升级的方法也比较简单直接替换新的安装文件,这一步在搭建新集群的时候就需要完成。在默认情况下迁移后原有的存储池内的数据就会被删除,我们还通过修改GitLab的源码来让迁移后不删除原有代码文件。

  准备工作都准备好后,后面的就是具体的执行过程了,最粗暴的方法就是一把梭全部迁移完!但这个不符合咱们干事的风格!为了稳妥还得分批灰度进行,迁移动作都放在晚上11点后或周末进行,我们采用的策略如下:

  第一批:迁移自己部门所在的仓库,大概数十个仓库左右,确保至少3天使用下来无问题,即使有问题也影响的是自己部门的

  第二批:3天后迁移内部支撑所有部门的仓库,大概20%的仓库,观察3天(这个时候若出问题仅影响内部系统,并不会影响业务的服务,影响范围相对可控)

  第三批:这一批选择的是仓库体积不太大的业务部门,因为仓库体积小迁移时间快,以部门的方式分多个小批次进行操作迁移,小批次迁移分了3天完成,到此完成了60%左右

  第四批:最后一批主要是体积较大的一些仓库,例如算法、App组件仓库,大于2G的仓库较多,这里采用了比较稳妥的策略,按仓库体积进行排序,将Top20的仓库单独放一批进行迁移,其余的让脚本依次执行

  整体上迁移过程比较顺利,迁移操作都在夜晚11点+周末进行,操作过程中一边刷监控,一边盯屏日志,整个迁移过程花了一周多的时间,踩过的坑就是少数几个仓库出现read-only状态无法迁移,也查到的相应的issueshttps://gitlab.com/gitlab-org/gitlab/-/issues/289816,通过修改数据库里对应的字段解决了!

  服务架构切换

  前面完成了Cluster存储架构的升级以及存量数据的迁移,所有的仓库都在Cluster存储池内,Gitaly组件已经是多区多活模式了,但Rails节点还是单点,现在要将Rails切为多区高可用架构,为了稳妥我们没有在旧的机器上直接改配置,而是在新的Cluster集群内将要修改的配置全部改好,将旧的机器作为回滚预案节点,验证通过后再进行流量切换,关键步骤为:

  修改Cluster集群Rails节点里的数据库、Redis链接配置为生产环境实例

  将原有生产环境机器上的文件/etc/ssh/ssh_host*覆盖至Rails节点的三台服务器

  拷贝主备模式下的gitlab-secrets.json文件到各个Redis节点机器

  执行reconfigure命令,确保修改的配置生效,另外咱们内部还有一些二开的功能需要额外修改nginx配置让其生效

  启动服务后,检查各项基础功能是否正常,如出现报错则需要查看日志来排查原因

  最后要确保authorized_keys认证走数据库逻辑,否则就会出现新添加的公钥仅在某个节点生效导致66.6%的概率SSH认证不通过,若原来走的文件认证,需要在管理后台修改,如下图:  

  所有的配置修改完后,可以通过绑定hosts来进行功能测试验证,确保新的环境里各项功能都正常!最后一步就是切SLB上的流量,在SLB上修改后端虚拟服务器为新的3个Rails节点即可!此时已经完成了整体的架构切换,所有的流量都在新的Cluster集群架构里运行了!

  三 治理经验

  架构的升级是提升系统稳定性很关键的一步,但仅仅是架构的升级还不够!还需要在日常运营过程中持续的治理。GitLab自身是一个文件存储系统,是有状态的服务,没有办法做到动态扩容,也存在一定的性能瓶颈,GitLab系统的设计之初是在GitLab系统内完成了DevOps的闭环,而且也没有这么多的外部接口调用。我们目前是把GitLab作为一个代码管理系统,其他的平台都依赖API接口、Hook事件来做上下游链路的打通,大部分的使用场景对于得物来说是合理的!GitLab自带的限流策略也不够完善,所以离不开日常治理!下面简单的从如下几个点总结一下治理经验:

  system/web hook治理:造成该现象的原因是因为大量的hook事件需要处理(包含非常多的失效接口,超时10s),默认的sidekiq线程数处理不过来,解决方法为一方面清理无效hook地址、系统钩子收敛hook转发到kafka后由各自消费kafka,另一个是增加sidekiq线程数数量,如下:

  # 该值不宜太大,太大会导致数据库连接数过大sidekiq['queue_groups'] = ['*'] * 8

  API调用治理:GitLab默认仅支持账号级别限流,不支持接口级别限流,部分接口的QPS上限并不高,另外也无法看到每个账号的实际调用情况,这里我们采取的做法是实现了一个GitLab API接口网关服务,对于新增的API调用与高频调用的系统要求必须接入我们的网关服务才能使用(接入成本低)

  元数据使用治理:较多的内部平台都需要获取仓库列表、分支、成员等信息,我们仓库数量多,如果每个系统都直接去调GitLab,这个请求量也非常大耗时久而且还需要分页,为了解决这个问题,我们基于原生API+system hook扩展出元数据代理服务,通过独立的redis缓存这些数据,为各个平台提供更简单的API,一方面降低了请求压力,另一方面让调用方更方便的获取到需要的元数据

  Clone代码治理:在高频Clone代码的使用场景,需要指定depth参数为1,仅Clone最新的一个版本,避免全量Clone带来的磁盘IO压力

  错误码治理:非200的很多请求都是无效请求,若请求过多也会对服务产生压力,这部分请求可以通过调用IP以及账号联系相关使用方进行改进优化

  变更规范治理:GitLab服务所有的变更都有可能影响服务的稳定性,无论是运维侧、安全侧还是我们自己的日常变更,都应遵守变更规范,只允许低峰进行,并且所有的变更都需要经过系统Owner的同意!

  使用行为监测:要做到提前发现风险,并且在风险发生时能快速定位到问题原因,我们结合治理过程的经验,做了账号调用行为监测机制,例如:API调用告警、账号Clone代码量等

  最近一年多的维护过程中,治理了大量的不规范Case,规避了较多的风险点。

  四 后续规划

  目前尽管已经上线了Cluster集群架构,也具备了横向扩容的能力。但目前的一些风险点还需要人工介入处理,而且GitLab版本升级也是个老大难的问题,新版本在限流的一些特性上确实有较多的改进,但经过阅读官方文档以及与官方的沟通,跨大版本升级必须停机操作,这对我们影响太大了!按照我们目前的数据量若升级到最新版本,需要经过数次停机升级,每次升级约20+小时左右,升级过程中还会产生较多的非预期的问题需要解决!但长远来看版本升级是迟早要面临的问题。

  结合目前的现状,后续的规划主要是如下几个方向:

  故障演练:持续的通过故障演练验证监控告警的有效性以及各项故障恢复SOP流程

  完善巡检工具:完善风险治理监测巡检工具,例如数据库风险、API调用风险、Clone代码风险,提前感知风险,提升问题定位效率

  版本升级探索:实现外部网关功能支持SSH/Https协议,通过网关让新老版本并存,分批迁移仓库数据达到版本升级的目的,这种方式对研发同学的使用无感

0
相关文章