手把手实例教学,原来数据平滑迁移这么简单

开发工作中,可能会遇到如\”大表拆分\”、\”跨库数据迁移\”等场景,本文介绍互联网常见架构下的数据迁移方案及实现。

一、数据迁移的业务场景

以下是需要数据迁移的场景业务场景:

1、大表拆分

由于历史原因,为了业务快速迭代,设计之初未考虑用户的规模,对于如用户权益、用户订单的数据,仅做了单表存储。

随着业务规模快速增长,单表数据膨胀,读写压力变大,可能出现单条慢SQL拖垮整个表的风险,以至于使整个业务不可用的情况,因此,需要将存储大量数据的单表拆分为多表(分库分表)。

2、数据迁移

业务发展之初,业务模块小,多个业务都放在同一个数据库中;随着业务规模的增长,当初聚合的多个业务模块要做业务交割,交由不同的团队维护和迭代,各个业务都有自己的库表。

为了防止不同业务之间,可能由于其他业务异常拖累整个数据库,要做业务存储的物理隔离,因此需要将旧库表的数据迁移到新的库表。

3、数据同步

举个例子,用户的支付订单的数据都保存在支付中台,由于历史原因,当前业务方的部分业务订单未做存储;

现在要做一个支付管控版本(如未成年人防沉迷专项),需要依赖订单中台来协助实现功能,而跨部门的需求对接周期长成本高;因此需要 [拉取并实时同步支付中台的订单数据] ,当前业务得到这部分数据后,就可以自行实现上述的跟订单相关的功能,从而将交易管控能力(实名制/防沉迷、商品限购)收敛在当前业务团队,提升需求版本迭代效率,减少跨部门的开发、测试之间的沟通、合作、资源等待问题,达到更快速响应业务需求目的。

二、方案选择

1、有损迁移

有损方案指的是停机变更方案,因为需要一段时间停止服务,因此对于业务是有损的;可以按照如下步骤执行:

1)研发同事通过观察历史的用户流量监控,选择流量较小的时间段执行变更来尽可能减小业务影响,如凌晨4点开始运维;运营同事前一天在网站或者 APP挂个公告,说次日4点到早上6点进行系统升级,无法访问;

2)接着到凌晨4点停机,系统停掉,关闭用户流量入口,此时老数据库表不再有变更产生,属于临时静态数据;然后通过提前得写好的一个数据导出导入工具,将老数据库表的数据按序读出来,写到分库分表里面去;当然,为了提升数据导出速度,可以使用多线程分段读写数据,甚至可以联系DBA直接物理复制数据库表,然后再执行rename;

3)数据导出之后,通过修改系统的数据库连接配置,重启服务,连到新的分库分表上去;模拟用户请求验证一下对数据的读写,这时发现如预期一样,这时打开用户流量,再观察一段时间,确认无误后,伸个懒腰回家调休。

说到停机迁移这种方案,可能有些同学觉得很low,但是实际上目前存在相当一部分迁移方案都是使用凌晨停机迁移的方式(例如有时候会看到停机维护公告),因为这种方式成本低简单粗暴、无需考虑新老库数据临界情况不一致的问题、整个迁移周期非常短不存在灰度放量与多次修改代码发布的情况,缺点就是短暂业务有损,需要DBA、研发值班。

2、平滑迁移

平滑迁移也叫无损迁移,服务在迁移过程中不需要停机,最多只需要短暂的重启时间,对于业务几乎没有任何影响的;双写方案,就是平滑迁移方案,简单来说,就是修改线上代码,在之前所有写老库(增删改操作)的地方,都加上对新库的增删改,这就是所谓的双写。

然后系统部署之后,由于我们只处理了某个时间节点之后的增量数据,新库数据整体数据差太远,用之前说的数据导出导入工具,跑起来读老库数据写新库,写的时候要根据类似gmt_modified字段判断这条数据最后修改的时间,除非是读出来的数据在新库里没有,或者是比新库的数据新才会写,简单来说,就是不允许用老数据覆盖新数据,新库的数据总是最新的。

导完一轮之后,有可能数据还是存在不一致,保险起见,通过定时任务做多轮新老库的数据校验,比对新老库每个表的每条数据,接着如果有不一样的,按照以最新数据为准的原则,重新从老库读数据再次写新库,直到两个库的数据追平,完全一致为止。

接着当数据完全一致了,切换为仅读写新库的代码,重新部署服务,重新部署后,数据的读写全部都落在新库中;求稳起见,这一阶段往往建议通过灰度策略,把用户的流量(仅读写新库)慢慢的切换到新库里,注意灰度的用户放量只能逐渐加大,不能回滚,因为灰度命中的用户数据都落在了新库,而老库没有;最终放量100%,迁移完成。

相对有损迁移方案,这种平滑迁移方案无需长达几个小时的停机时间,对业务的影响几乎没有,所以现在基本做数据迁移都会偏向这种方案。

3、增量迁移

这种迁移方案一般应用于特殊的业务数据,这类业务数据的历史数据价值不大具备有效期属性,如用户的优惠券数据;例如有这样的一个业务场景:用户优惠券表由于历史原因未做分库分表,目前数据增长过快,查询、写入性能变差;该业务属于部门核心业务,每天都有大量的用户流量和数据写入;为了保证业务的可用性需要对优惠券大表做拆分。

针对以上业务场景,考虑用户优惠券本身的特性(存在有效期),采取以下方案:灰度期间,用户优惠券新增数据全部保存至新表中,旧表的历史数据不迁移仅更新,共存阶段同时查询新库和旧库数据做数据聚合,运行N个月后(旧库数据已全部业务失效)用户请求的读写全部切到新库中,旧库直接作为归档。

该方案的特点就是历史数据天然的无需迁移,方案逐渐放量、无需考虑回滚,适用于解决大表拆分问题。

三、平滑迁移方案的实现细节

双写方案,总的来说就5个步骤:

  • 双写,增量数据同步;
  • 历史数据同步;
  • 定时任务校验数据一致性并修复;
  • 灰度放量,异常时可回滚;
  • 功能全量,完成平滑迁移。

1、双写的实现方式

首先,配置新库和老库两个数据源Datasource,来分别执行对老库和新库的读写;对于写操作的实现,有两种方式:

1)修改业务代码,在写老库的地方,加上一句对新库的写操作;这种方法将写新库与原业务操作耦合到了一起,修改代码的位置可能非常多,业务代码会改的很混乱,并且当老库写成功,新库写失败时,需要考虑如何补偿,至少不能影响老库已经成功的业务逻辑,这种方法不推荐。

2)因为业务请求只在数据迁移完成后才切换,所以并不要求老库与新库的写操作同步执行;因此可以考虑使用消息中间件解耦,如记录“对旧库上的数据修改”的日志持久化并丢入消息队列,然后按序消费队列里面的所有消息。

3)借助公司大数据团队提供的binlog解析采集工具,如基于开源的canal binlog parser模块二次开发的binlog接入工具,通过MySQL主从复制协议去业务db捕获增量变更数据(模拟MySQL slave,通过socket连接去拉取和解析数据,对于MySQL的性能损耗很小,按照MySQL官方的说法,损耗为1%),解析成json格式,写入kafka,最终由业务消费数据,按照自己需求写入hive,hbase,es等等异构数据源中;),作为业务方只需要消费数据库增量变更的Kafka消息即可。

2、边双写边迁移历史数据

对旧库上的数据修改的同时,在新库上进行相同的修改操作,这就是所谓的“双写”,主要修改操作包括:

  • 旧库与新库的同时insert
  • 旧库与新库的同时delete
  • 旧库与新库的同时update

由于新库中此时是没有历史数据的,所以双写旧库与新库中的affect rows可能不一样,不过这完全不影响业务功能,因为此时还未切库,依然是旧库提供业务服务;

新库写操作的数据变更原则为:

  • 对于单条数据的重复变更,以最新的数据为准,不能用旧数据替换了比它更新的数据;
  • 双写时,只有当老库执行写操作成功,才会对新库执行操作;
  • 新库执行写操作失败不能影响旧库的写操作成功的结果。

接着,执行历史数据迁移定时任务,由于迁移数据的过程中,旧库新库双写操作在同时进行,怎么保证数据迁移完成之后数据就基本一致了呢?可以参考下面的图来做一个说明。

上方是旧库中的数据,数据量还在随时间线持续写入;下方是新库中的数据,边通过迁移工具同步历史数据,边处理旧库中新发生的增量数据(这里的增量数据指的是双写时,新库的写操作导致的数据变更,不仅限于insert操作);

下面针对数据迁移过程中,由于双写同时产生的修改操作,讨论下该如何处理:

1)旧库执行insert操作后

新库执行insert操作,操作一定能成功(因为新库的数据是旧库的子集,旧库既然可以插入成功,那么新库也可以插入成功),此时旧库新库都插入了数据,数据一致性没有被破坏;

2)旧库执行delete操作后

根据删除的数据所处的区间,分为两种情况:

情况1:假设这delete的数据属于[start, current]范围,即已经写入了新库,则旧库新库都删除了该条数据,数据一致性没有被破坏。

情况2:假设这delete的数据属于[current, latest]范围,即还未写入新库,则旧库中删除操作的affect rows(影响的行数)为1,新库中删除操作的affect rows为0。

但是数据迁移工具在后续数据迁移中,因为这条\”未来的\”数据已经从旧库删除,因此并不会将这条旧库中被删除的数据迁移到新库中,所以数据一致性仍没有被破坏。

3)旧库执行update操作后

根据更新的数据所处的区间,分为两种情况:

情况1:假设这update的数据属于[start, current]范围,即已经写入了新库,则旧库新库都更新了该条数据,数据一致性没有被破坏。

情况2:假设这update的数据属于[current, latest]范围,即还未写入新库,此时新库将这条update操作改为insert操作即可;数据迁移工具在后续数据迁移这条数据时,由于这条数据的更新时间与新库中已插入的这条记录的更新时间相同(同一条数据),因此不会执行迁移替换,所以数据一致性仍没有被破坏。

也可以这么理解,可以认为update操作是一个delete加一个insert操作的复合操作,结合上面对于inser和delete的分析,所以数据仍然是一致的;

特殊情况:

  • 数据迁移工具刚好从旧库中将某一条数据X取出,准备执行迁移插入新库;
  • 在X插入到新库中之前,旧库发生了写操作(delete),旧库删除了这条数据,affect rows为1,新库由于此时还没有这条数据,affect rows为0;
  • 双写完成后,数据迁移工具再将X插入到新库中;导致新库比旧库多出一条\”本应被删除的\”数据X,导致数据不一致。

因此,为了保证数据的一致性,切库之前,新库和老库的数据校验是必要的!

在数据迁移完成之后,需要写一个数据校验和修复的数据校验的定时任务,将旧库和新库中的数据进行比对,完全一致则符合预期;如果出现某种极端情况下导致的老库新库数据不一致情况,则以旧库中的(最新的)数据为准。

这个定时任务跟数据迁移写法类似,只不过前者是遍历查旧库写新库,后者是遍历查旧库,然后用旧库结果去查新库,做数据对比与修正;整个过程依然是旧库对线上提供服务,因此没有任何操作风险。

数据校验和修复的定时任务可以执行的久一点,一段时间后都没有出现旧库与新库数据存在不一致的告警时,可以开始逐渐灰度了,即将命中灰度策略的用户请求全部走读写新库,放量可以慢点,直到所有用户请求都路由到新库为止,则数据迁移完成。

四、实际工作中使用的方案

我在工作期间,设计过大表拆分数据同步这两种业务场景的技术方案;这里介绍下在做数据同步时我用到的方案,仅供参考。

1)背景

组内做了一个针对部门业务的订单小中台,负责对接公司的支付系统(下单、回调、重试),目的是减少部门各个业务模块对接公司支付中台的成本,屏蔽如订单重试补偿的逻辑;此外,将部门内业务订单数据收拢,方便做整个部门业务的支付管控。

2)任务

对于已经自主对接公司支付中台的部门内业务,需要把这些业务存储的业务订单数据迁移到订单小中台;数据跨库,且新库的表结构与旧库不同。

3)方案

① binlog采集工具

公司大数据团队做了一款基于canal binlog parser模块二次开发的binlog接入工具,它通过MySQL主从复制协议去业务db捕获增量变更数据(模拟MySQL slave,通过socket连接去拉取和解析数据,对于MySQL的性能损耗很小,按照MySQL官方的说法,损耗为1%),解析成json格式,写入kafka,最终由业务消费数据,按照自己需求写入hive,hbase,es等等异构数据源中。

② 工具接入步骤

  • 业务方需要为被采集binlog日志的数据库,申请一个离线从库,并开启这个从库的binlog开关、开启row full 模式、开启gtid,获取当前的GTID信息或者点位文件信息;也就是说只采集位点之后的binlog,对于位点之前的biglog,可以新建一张表并用同样方式监听其binlog,然后将位点前的旧库数据按序写入;
  • 配置采集任务,分别采集旧库位点前后的binlog,binlog event会被处理成方便解析的数据丢入kafka broker集群;并且,会对每条binlog event分配一个全集唯一的版本号,并且保证后面产生的binlog的版本号比前面的大,从而为业务方提供了一种数据顺序性;
  • 业务方只需要写一个消费kafka消息的listener即可;需要注意的是消息可能是乱序的(位点前后的binlog一起消费)。

数据同步的消息处理逻辑如下:

历史数据迁移完成后,还是需要一个定时任务来处理新库和老库不一样的数据。

>>>>

参考资料

  • 数据迁移方案 – CSDN
  • 100亿数据,非“双倍”扩容,如何不影响服务,数据平滑迁移?- 掘金

作者丨七海健人

来源丨网址:https://blog.csdn.net/minghao0508/article/details/124336706

dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn

关于我们

dbaplus社群是围绕Database、BigData、AIOps的企业级专业社群。资深大咖、技术干货,每天精品原创文章推送,每周线上技术分享,每月线下技术沙龙,每季度Gdevops&DAMS行业大会。

关注公众号【dbaplus社群】,获取更多原创技术文章和精选工具下载

从 HBase 到 TiDB:我们如何实现零停机在线数据迁移

本文最初发布于Pinterest Engineering技术博客,InfoQ 经翻译授权发布。

在 Pinterest,Hbase 一直是我们最关键的存储后端之一,持续为众多线上存储服务提供支持,涵盖 Zen(图数据库)、UMS(宽列数据存储)和 Ixia(近实时二级索引服务)。HBase 生态系统具备一系列突出优势,例如在大容量请求中保障行级强一致性、灵活的模式选项、低延迟数据访问、Hadoop集成等,但也由于运营成本高、过于复杂和缺少二级索引/事务支持等问题,而明显无法满足未来三到五年内的客户实际需求。

在评估了十余种不同的存储后端选项,向三种入围方案导入影子流量(将生产流量异步复制至非生产环境)并开展深入性能评估之后,我们最终决定将 TiDB 选为这场统一存储服务角逐的胜出者。

如何将统一存储服务的支持职责顺利移交给TiDB,无疑是一项需要数个季度才能完成的艰难挑战。我们需要将数据从 HBase 迁移至 TiDB,重新设计并实现统一存储服务,将 Ixia/Zen/UMS 的 API 迁移至统一存储服务,再把各类离线作业由 HBase/Hadoop 生态系统迁移至 TiSpark 生态系统——而且整个过程中,现有业务的可用性和延迟 SLA 均不能受到影响。

在本篇博文中,我们将首先探讨不同数据迁移方法及其各自利弊。之后,我们再深入探究如何将数据从 HBase 迁移至 TiDB,这也是 Pinterest 第一次以零停机方式迁移一个每秒 14000 次读取查询、400 次写入查询的 4 TB 大表。最后,我们将共同验证这套新方案能否实现 99.999%的数据一致性,并了解如何衡量两个表之间的数据一致性。

一般来讲,零停机时间数据迁移的实施策略可以概括为以下几点:

  1. 假定已有数据库 A,需要将数据迁移至数据库 B,则首先开始对数据库 A 和 B 进行双重写入。
  2. 将数据库 A 的转储数据导入至数据库 B,并解决与实时写入间的冲突。
  3. 对两套数据集进行验证。
  4. 停止向数据库 A 写入。

当然,具体用例肯定各有不同,所以实际项目中往往会包含一些独特的挑战。

我们综合考量了各种数据迁移方法,并通过以下权衡筛选最适合我们需求的选项:

  1. 从服务向两个表(HBase 和 TiDB)执行双重写入(以同步/异步方式写入 2 个数据源),并在 Lightning 中使用 TiDB 后端模式进行数据摄取。

这种方式最简单也最易行。但 TiDB 后端模式提供的传输速率仅为每小时 50 GB,因此只适合对较小的表进行数据迁移。

  1. 获取 HBase 表的快照转储,并将来自 HBase cdc(变更数据捕捉)的数据流实时写入至 kafka 主题,而后使用 lightning 工具中的本地模式对该转储执行数据摄取。接下来,即可从服务层执行双重写入,并应用来自 kafka 主题的全部更新。

应用 cdc 更新时往往会引发复杂冲突,因此这种方法的实施难度较高。另外,我们此前负责捕捉 HBase cdc 的自制工具只能存储键,所以还需要额外的开发工作才能满足需求。

  1. 另一种替代方案,就是直接从 cdc 中读取键,并将存储在另一数据存储内。接下来,在面向两个表的双重写入启动后,我们从数据源(HBase)读取各键的最新值并写入 TiDB。这种方法实施难度很低,不过一旦通过 cdc 存储各键的异步路径发生可用性故障,则可能引发更新丢失风险。

在评估了各项策略的利弊优劣之后,我们决定采取下面这种更加稳妥可靠的方法。

客户端:与 thrift 服务对话的下游服务/库

服务:用于支持在线流量的 thrift 服务;在本次迁移用例中,服务指的是 Ixia

MR Job:在 map reduce 框架上运行的应用程序

异步写入:服务向客户端返回 OK 响应,无需等待数据库响应

同步写入:仅在收到数据库响应后,服务才向客户端返回响应

双重写入:服务以同步或异步方式同时写入两个基础表

由于 HBase 为无模式(schemaless),而 TiDB 使用严格模式,因此在着手迁移之前,需要先设计一个包含正确数据类型和索引的 schema。对于我们这个 4 TB 大小的表,HBase 和 TiDB schema 之间为 1:1 映射,也就是说 TiDB 架构会通过 map reduce 作业来分析 HBase 行中的所有列和最大列大小,之后再分析查询以创建正确的索引。下面来看具体步骤:

  1. 我们使用 hbasesnapshotmanager 获取 HBase 快照,并将其以 csv 格式存储在 S3 内。各 CSV 行使用 Base64 编码,以解决特殊字符受限的问题。接下来,我们在本地模式下使用 TiDB lightning 对这一 csv 转储执行摄取,而后进行 base64 解码,并将行存储至 TiDB 内。摄取完成且 TiDB 表上线后,即可启动对 TiDB 的异步双写。异步双写能够既保障 TiDB 的 SLA,又不影响服务 SLA。虽然我们也为 TiDB 设置了监控和分页,但此时 TiDB 仍然以影子模式运行。
  2. 使用 map reduce 作业对 HBase 和 TiDB 表执行快照保存。各行会首先被转换为一个通用对象,再以 SequenceFiles 的形式存储在 S3 内。我们使用 MR Connector 开发了一款自定义 TiDB 快照管理器,并在 HBase 中使用 hbasesnapshotmanager。
  3. 使用 map reduce 作业读取各 sequencefiles,再将不匹配的行写回至 S3。
  4. 从 S3 中读取这些不匹配的行,从服务中读取其最新值(来自 HBase),再将这些值写入至辅数据库(TiDB)。
  5. 启用双重同步写入,同时向 HBase 和 TiDB 执行写入。运行步骤 3、4、5 中的协调作业,每天比较 TiDB 与 HBase 内的数据奇偶性,借此获取 TiDB 与 HBase 间数据不匹配的统计信息并进行协调。双重同步写入机制不具备回滚功能,无法解决对某一数据库的写入失败。因此必须定期运行协调作业,确保两个表间数据一致。
  6. 继续保持对 TiDB 同步写入,同时对 HBase 启用异步写入。启用对 TiDB 的读取,此阶段中的服务 SLA 将完全取决于 TiDB 的可用性。我们将继续保持对 HBase 的异步写入,尽可能继续保持双方的数据一致性,以备发生回滚需求。
  7. 彻底停止写入 HBase,弃用 HBase 表。
  1. 由后端不可用导致的不一致

在 Ixia 服务层构建的双写框架无法回滚写入操作,这是为了防止因任一数据库不可用而导致局部故障。在这种情况下,就需要定期运行协调作业以保持 HBase 与 TiDB 双表同步。在修复此类不一致时,主数据库 HBase 为数据源,因此一旦出现 HBase 表写入失败、但 TiDB 表写入成功的情况,则协调过程会将这部分数据从 TiDB 中删除。

  1. 双重写入和协调过程中,因竞态条件引发的不一致

如果事件按以下顺序发生,则可能导致将旧数据写入 TiDB:(1)协调作业从 HBase 表读取;(2)实时写入将数据同步写入至 HBase,异步写入至 TiDB;(3)协调作业将之前读取的值写入 TiDB。

此类问题可以通过多次运行协调作业来解决,每次协调都能显著减少此类不一致数量。在实践中,对于支持每秒 400 次写入查询的 4 TB 表,只需要运行一次协调作业即可在 HBase 与 TiDB 之间达成 99.999%的一致性。这项一致性指标的验证源自对 HBase 和 TiDB 表转储值的二次比较。在逐行比较之后,我们发现两表的一致性为 99.999%。

  1. 我们看到,第 99 百分位处的读取延迟降低至三分之一到五分之一。在本用例中,第 99 百分位的查询延迟从 500 毫秒下降至 60 毫秒。
  2. 实现了写后读取一致性,这也是我们希望通过替换 Ixia 达成的重要目标之一。
  3. 迁移完成后,整个架构更简单、涉及的组件数量更少。这将极大改善对生产问题的调试流程。

由于我们没有使用 TiUP(TiDB 的一站式部署工具),所以 Pinterest 基础设施中的整个 TiDB 部署流程成了我们一次宝贵的学习经历。之所以没有选择 TiUP,是因为它有很多功能都跟 Pinterest 内部系统相互重叠(例如部署系统、运营工具自动化服务、量化管道、TLS 证书管理等),而综合二者间差异的成本会超出使用收益。

因此,我们决定继续维护自己的 TiDB 版本代码仓库和构建、发布与部署管道。集群的安全管理绝非易事、涉及大量细节,如果我们自己不努力探索,就只能把这些工作一股脑交给 TiUP。

现在我们拥有了自己的 TiDB 平台,构建在 Pinterest 的 AWS 基础设施之上。我们可以在其中实现版本升级、实例类型升级和集群扩展等操作,且不会引发任何停机。

在数据摄取和协调过程中,我们也遇到了不少现实问题。感谢 Pingcap 在各个环节中提供的全力支持。我们也为 TiDB 代码库贡献了一些补丁,这些成果已经由上游社区完成了合并。

  1. TiDB lightning 5.3.0 版本不支持自动刷新 TLS 证书,而且由于缺少相关日志而难以调试。Pinterest 的内部证书管理服务则每 12 小时刷新一次证书,所以期间总会发生一些失败的摄取操作,只能依靠 pingcap 来解决。好在证书自动刷新功能现已在 TiDB 5.4.0 版本中正式发布。
  2. Lightning 的本地模式会在数据摄取阶段消耗大量资源,并影响到同一集群上运行的其他表的在线流量。为此,Pingcap 与我们开展合作,对 Placement Rules 做出了短期和长期修复,因此支持在线流量的副本已经不会受到本地模式的影响。
  3. TiDB MR Connector 需要进行可扩展性修复,才能把 4 TB 表的快照保存时间控制在合理范围。此外,MR Connector 的 TLS 也有改进空间,目前这些改进贡献已经完成了提交及合并。

在调优和修复之后,我们已经能够在约八小时之内摄取 4 TB 数据,且每轮协调和验证运行只需要七小时左右。

在本轮迁移中,我们的表由 ixia 负责支持。期间,我们在异步/同步双重写入和查询模式变更中遇到了几个可靠性问题。由于 ixia 本身的分布式系统架构非常复杂,导致 thrift 服务(ixia)极难进行调试。感兴趣的朋友请参阅我们的其他博文以了解更多细节。

这里,我们要感谢 Pinterest 存储和缓存团队的各位前成员和现同事,谢谢大家在这场将最新 NewSQL 技术引入 Pinterest 存储堆栈的攻坚战中做出的卓越贡献。

我们还要感谢Pingcap团队为种种复杂问题提供的持续支持、联合调查和根本原因分析(RCA)。

最后,我们要感谢各位客户在此次大规模表迁移过程中表现出的耐心和支持。谢谢您的理解与配合!

原文链接:

本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com

点赞 0
收藏 0

文章为作者独立观点不代本网立场,未经允许不得转载。