Paper Time 回顾|MB2:为自治数据库建立行为模型


自治数据库旨在完全自动化数据库部署中的配置和运维任务,例如建立索引和进行参数调优。实现自治数据库的关键是建立能够预测各种优化任务的开销和收益的行为模型。但数据库的系统复杂度、并发操作、以及训练数据的获取代价使得建模具有挑战性。

基于此,今天为大家带来 OceanBase Paper Time 第二期文字版分享回顾,由卡耐基梅隆大学博士后马林分享《为自治数据库建立行为模型》,原论文名为“MB2: Decomposed Behavior Modeling for Self-Driving Database Management Systems”。为大家介绍一种分解式的建模框架解决上述难点,并利用所建模型准确预测不同优化任务的开销和收益,欢迎大家一起学习讨论分享。


以下为直播实录:

大家好,我是马林,现在 CMU 博士后在读,今天我跟大家分享的论文为“MB2: Decomposed Behavior Modeling for Self-Driving Database Management Systems”,是 2021 SIGMOD 上发表的论文。

MB2 是一个分解式的自治数据库建立行为模型的框架。在现在互联网时代,各种资源、数据非常多的情况下,人们通常会用数据管理系统去控制数据的存储和访问。但随着数据的爆发式增长,数据管理本身也变得越来越复杂,尤其伴随数据量级和数据多样性的增长,这些系统本身需要有很多的设置任务。

我们通常需要 DBA 去管理数据库系统部署中的各种任务,比如说数据管理员需要设置各种各样的索引(一种加速部分数据访问的一种数据结构);或者数据管理员要设置数据库各种各样的参数,比如说进行日志管理的间隔等;尤其是云时代,数据库管理员要去设置数据库资源的大小,有多少 CPU,多少 Memory,多少 IO 等等。这就导致企业需要花费很大成本去雇佣数据库管理员,对于很多大公司或者云数据库的服务商,可能要管理几千个、几十万个数据库,如果每一个数据库都要分配一个数据库管理员的话,就很不现实。

供<求,驱动数据管理思维变更

当今时代已经对数据驱动、数据密集型应用的部署造成了很多挑战,目前也有很多的解决办法。在数据库行业发展的很多年里,各个厂商、研究员开发了很多帮助数据管理的工具。但在工具使用的场景中,还需要借助很多人为的力量去使用这些工具。比如一个数据管理员想设置数据库的索引,或者设置一些参数。他先要准备一个具有代表性的数据库负载(workload),同时还要准备一些空余的硬件,然后再在硬件上去复制一些数据库的内容。要在空余的硬件上和准备好的 workload 上,去运行数据库调优、或者是推荐索引的工具。拿到这些推荐之后,在各种各样推荐的索引当中选择一个比较好的或者最优的索引,最后决定什么时间更改,这都需要人为的更改。通常这些更改都需要在数据库负载比较低的时候,比如凌晨三四点,对于数据库管理人员来说,这种流程很是痛苦。

 

之前‍这些大部分管理工具都是关注在数据库的某一个方面。比如说有的工具关注索引,有的工具关注怎么和用户协同,不同的数据放到不同的分区,然后很多工具进行参数的调整。很多工具要一遍一遍的重复这个过程,不同的工具都要用一遍是非常繁琐的过程。正因此,我今天为大家带来一种新思路。

我在另一篇论文中对数据库的自主帮助管理、自动部署的等级进行了区分(Make Your Database System Dream of Electric Sheep: Towards Self-Driving Operation, VLDB 2021)。

最低级,所有的数据都需要数据管理员自己用。有一些不同的厂商,会在中间层次帮助数据库进行自我管理的工具,比如说去控制某一个小的部分。但是我们在这里的项目就是希望达到最高等级——自动驾驶(self-driving)或者是自治的级别。意思是数据库可以完全自动管理不同的方面,包括索引、参数等等,通过一个统一的关键框架去管理,这个自治数据库不仅仅是给 DBA 去进行一些推荐,而且能预知之后的事情,就比如说数据库的负载是什么?怎么去进行数据库的自动调优、改变安排,并且完全可以自动的进行,不需要人为干预,这是我们要达到的目标。

简单定义下自治数据库:自治数据库是一个可以完全能够设置调优并且优化系统的各个方面,也不需要任何人为干预的数据库。

具体来说就是它可以自动优化任何跟数据库性能相关的方面。比如说 physical design(索引),或者冷热分离、调参数、资源配置。但是有一些设置还是需要人去进行价值判断,比如说数据库的哪一个表,或者哪一列,什么人会有权限去操作,数据库的本身仍然不能确定。

为什么在这个时代有可能做自治数据库?有几方面原因。

首先,在数据驱动的时代,不光是数据库本身可以存储很多客户的数据,同时数据库可以去对数据库本身的运行状态,去记录很多的指标、参数、运行状态的历史记录,还有数据库本身的运行状态。我们也可以尝试去发掘支持、帮助数据库进行自动优化。第二,我们这里有很多更好的硬件,可以存储更多这样的数据,也可以更快速地处理这些数据,并且完成数据库的自动优化。最后,现在这个时代有很多方便的人工智能、机器学习方面的库和工具,来帮助我们去部署这些自动调节的算法。

自治数据库的挑战

听起来有很多的机会、趋势去帮助我们完成这个任务,甚至感觉将一个现成的人工智能算法往数据库的系统上一套,就能实现完全的“自动驾驶”,但在现实当中还是有一些挑战。

第一,数据库本身是一个非常复杂的系统,如果你想要一个统一的框架去完全的去控制数据库各个方面复杂度很高。

第二,在数据库当中的很多操作,比如说想使用机器学习这些算法去训练模型、去控制智能化的数据库,需要数据去训练这些模型,但是数据库当中的很多操作,它其实是要花很多时间的,获取这些模型的训练数据也要花很多时间。比如说建一个索引,如果这个表上有几十亿个行,建一个可能就要好几个小时,好几天。

第三,你在实验室或者开发环境当中训练了一些模型,然后去部署这个智能数据库。但模型不是百分之百准确的,很难保证在这个开发环境当中训练的这些模型,在部署当中还能有好的效果。

还有一些其他的方面,比如说智能数据库或者自己数据库的可解释性,这个排除故障的难易程度等等,对于一个实际比较现实的自治数据库开发来说,这些方面都是需要解决的难点。

我们称之为自治数据库(self-driving database),其开发系统是受了自动驾驶的启发。但是我要说的是,现实当中自动驾驶的技术是非常复杂的,但是我觉得在这儿作为一个比喻对于理解还是很有帮助的。

一辆自动驾驶的汽车,它大概是这么几个部分。首先是感知部分,汽车需要感知其他的汽车、行人在路上的位置,并且也要预测一下车或者行人往哪移动,然后计算了一个汽车。第二,它对它自己控制的这些操作有一些模型可以预测,比如说方向盘打了多少度、汽车往哪拐,他需要有模型,这里数据库稍微有点不同,待会可以讲一下。

第三点,数据库对于道路其他东西的感知和一些操作模型。这个数据库最终要一个计划阶段,我们设计的自治数据库架构和自动驾驶汽车有很多相似。

首先,感知部分。我们觉得数据库首先要去接收和监测收到了哪些工作负载,并且他也要预测未来数据库一段时间内的工作负载是什么样。比如说负载高,负载低等等,因为未来这些负载,就是数据库它要自动优化的目标,我们叫这个为负载预测。

第二,数据库要进行不同的自动优化操作。比如说建索引,调参数等等。那你还是需要模型去估计你不同操作的开销和收益。因为这是你做决定的依据,这里和汽车不太一样,需要你自己建立一些模型。

第三,如果我们有了一些可能备选的优化操作,并且我们也知道未来的负载形态,我们最后就要选择在什么时间段去应用什么样的优化。比如说这个时间段的负载量比较低,我们可以在这个时间段内建一个索引。如果你要达到这个目标,你需要一个计划,根据预测和数据库操作开销和收益的模型等操作去自动优化数据库的性能。

关于自治数据库的框架。我们是集成在 CMU 数据库系统里面,叫做NoisePage,我们这个是从头开发的去支持自动化的操作。它既支持这个事务型,也支持分析型,并且它是MIT license,兼容 Postgres 接口和 Arrow 存储格式。

行为建模

接下来分享一下怎么给这个自治数据库去建立这些模型——即行为建模。具体来说,用行为模型的目标去估计不同数据库自动化的操作、开销和收益。这个模型输入包括两部分,一部分是预测的未来数据库负载。假设我们可以预测未来数据库里有什么负载,并且同时也要给这些模型一个可能的操作。比如说建立一个索引,或者是找一个参数, 输出这个可能的操作在预测未来负载上的开销和收益。

稍微更进一步的阐释为什么这些模型很重要,我们用一个简单的实验来说明。假设我们有一个数据集,但它一开始没有合适的二级索引,但我们要去假设有自治的操作,然后建一个二级索引,对这个负载进行加速。我们有两个选项,八个线程或四个线程,我们试图去加速查询。在这个负载一开始的时候,没有二级索引,它的 查询相对延迟比较高。然后我们大概在 50 秒钟的时候去建立索引。在不同的情形当中,虽然说最终查询都被加速了,但是它对数据库性能的影响非常不一样。如果你用八个线程,这个索引完成的特别快,但在这个完成的过程当中他有更大的开销,他对数据库过程中的影响非常大。而四个索引说就是反面,就是说在不同的应用的操作选择、时间点,实际上根据不同客户的要求、系统的要求、系统的实际状态,然后他最终的选择都是很不一样的,所以说能准确的预测自动化操作的影响,对自治数据库至关重要。但这里面也有几个挑战。

第一,现在的数据库非常复杂,可能有很多不同的负载。如果你想用一个单一的机器模型,去包括数据库的各个方面、各个操作等等。这个模型就很容易非常高维,作为结果,这个模型可能需要很多的训练数据去训练,并且可能也不太容易去排除故障,不太容易解释等等。

第二,现在的这些数据库在硬件上,大部分情况下都不是单线程的,很多时候都是多线程并发操作。这种情况下,不同线程上的并发操作互相之间可能会有影响,比如资源竞争等。那就是说这个模型要包括这些可能的操作情况,随着并发竞争维度的上升,它就呈指数级别、至少是组合数级别上升。

第三,很多数据库操作非常费时,怎么高效获取高质量的训练数据很有挑战。如果你获取一个新的数据就已经花了几个小时,这就很不现实。当然有一些其他的算法对数据库操作、运行状态、执行进行了一些建模。

它的建模方式大概可以分为两种。

第一种叫分析性模型,主要针对查询的执行,往往针对数据库的其他部分有专家去分析各个部分的操作特性,不需要任何的训练数据去写各种各样的公式,去刻画操作的行为。但这也有两方面的缺点,一方面需要专家非常了解数据库的每个部分,具体的行为是什么,可能有哪些的输入的变量,操作怎么用一个数学公式去表达,对于专家来说,要花很多时间才能把这个公式写出来。另一方面,它是比较难以迁移的。比如说数据库有一个模块进行了代码更新,进行了替换,那之前写的公式就不管用了,那就要重新再写一个公式。

第二种,机器学习类模型。用一些机器学习的方式去对这个数据库的操作,尤其是对查询的执行进行建模。这个好处是不需要太过于专家的分析,也不需要数学公式对数据库的操作有非常深的理解,也比较容易去进行迁移。但它的问题就是大部分技术模型它首先只关注单一查询的。但是数据库很多时候有不同的查询,甚至是不同的组成。有的时候执行需要建索引时,有的时候要进行这个日志的写入和收集。这些数据库的不同操作,不同部分之间怎么进行建模,它没有很好解决的。

根据之前的实验结果,这些机器学习类模型往往有另一个问题,在实验室或者开发环境当中,你建立一个模型并且测试好了,模型的准确度很高。但是当你把这个模型部署到不同的数据集上,或者是部署到实践当中,这个模型的准确度,因为训练数据不一样,模型训练数据都会大大下降。

MB2建模框架

我来介绍一下我们工作的主要解决方式,我们提供了 MB2 这样一个为自治数据库进行了建模的框架,它属于中间型(半分析半学习的建模方式),大概结合了我们刚才说的分析性模型和机器学习模型的优点。它是一个线下的框架,在自己的开发或者实验室环境当中就可以去做。当你建完这个模型之后,直接去进行部署,这里由几个部分组成。

首先,我们会有一些特定的 runner(运行器),去对数据库不同部分,就比如说建索引、查询执行等等,有特定的工作负载去测试数据库不同的部分在不同的情况下通过不同的行为去获得很多的训练数据。虽然它们会有不同的运行器去测试,但是我们有一个统一的数据收集系统,主要使用线程本地的存储去加速存储,使得数据库可以高效地去收集不同部分的训练数据,然后将收集的数据送到训练中心去测试不同的机器学习模型,最后去建立对数据库的两种模型,一种是我们叫 operating unit model——针对各个部分的操作单元模型,一种是 interference model ——针对不同的并发操作之间的互相影响。

很重要的一点,我要强调:MB2 是线下的建模框架,可能会稍微多一些开销,但是它是和数据库具体的负载和具体数据集什么样是独立的。虽然这个线下模型你需要花一些时间去训练数据,但是你把它部署在任何一个实际的或者数据集上面。数据集和工作负载本身没有太多的关联性。或者说这些模型尽量的去包括了各种各样的不同的数据集和负载,所以说线上就不需要再确定,但大部分情况下,线上不需要做非常繁琐的操作。

具体来说,这个自治数据库建模的核心思想是用一个分解式的建模方式。意思就是说我们会去把数据库的各种各样的操作去分解成比较小的操作单元。对于每一个操作单元,我们会去特定的收集训练数据,并且去建立模型。好处就是每一个模型它比较小,不需要很多很多的训练数据,比较容易去解释,如果出了问题也比较容易排除故障。如果这个数据库有一个软件的部分进行了更新,那也比较容易更新这些模型,因为每一个模型都是独立的。如果说数据库的某一个部分结构性的,你只需要把针对这一个部分,就比如说它的索引部分更新了,那你就根据这一个部分的模型,进行重新的训练就可以了,你不需要把所有的这些模型都重新的实验室数据收集和训练。

举一个稍微具体的例子,比如说我们的 NoisePage 系统,我们大概把它分成了 20 个基础的操作单元,比如说建哈希表、建索引、序列化日志等等,针对不同的操作单元,我们叫 input feature(输入特征),不同的操作单元有不同的输入特征,这些输入特征也会包括数据库的不同参数,如果这些参数影响到某一个操作单元,当中也会包括这些参数,但是所有的这些操作单元的模型,它的输出标签都是统一的。这些输出的标签会包括这些操作单元的完成时间、进行某个操作需要多少 CPU、多少 CPU、IO、多少 memory 等等。最后,如果说数据库要估计某一个自动化操作的开销和收益,它就先把这些自动化操作在具体的负载上,它有各种各样可能的影响都会分解成操作单元,进行分别预测并相加。

具体来说,在我们这个 NoisePage 的数据库当中,我们大概是有三种不同类型的操作单元。

第一种是单一的操作单元。比如说我们建一个哈希表,或者我们对某些数据进行一次排序这种简单的操作。

第二种是批处理的操作单元。就是说数据库当中有一些任务,它是分周期的被唤醒然后执行,比如说他们会去写日志,很难预测单一一次写了多少,写到哪儿。通过这种周期性进行唤醒的工作去预测一段时间之内这些操作带来的开销和影响。

第三种是并发的操作单元。数据库中有一些操作可能会进行多线程执行。比如说去建一个索引,建立索引本身就有多个线程同时完成。在这种情况下,多线程完成之间会有同步的机制,这些同步的机制会产生开销。所以说这些操作单元和相应的操作单元的输入特征当中,要包括这些同步机制的信息、线程、锁等等。

举例来说,我们建一个哈希表就是数据库中的一个操作单元。一个操作单元我们去用模型去刻画它有什么样的特征呢?这个哈希表有多少行,每一行总共的数据有多大大小,它的 cardinality estimation 是多少。建立哈希表有一些相关的参数可能会影响这个效果。比如说在我们的配置系统里面,它的解释模式、编译模式等等都会影响操作单元的运行状态。

在数据库当中,你需要收集、产生和训练数据去建立这些模型,去解决这个问题可以去应用一些定制的运行器去充分测试每个操作单元,然后去遍历它各种各样可能的输入特征的情况,并收集训练数据。比如说数据库有 N 个操作单元,相应的就有 N 个运行器,运行器它会执行一些工作负载,但这些都是我们自己写的合成的负载。它们去遍历各种可能的输入特征。比如说刚才这个建立哈希表的操作单元,它的运行去遍历了不同的行,不同的列、不同的列大小、不同的参数等等。这些组合对测试哈希表,在不同情况下测试它的运行效果,然后我们收集训练数据送到训练的中心,训练中心用一个 cross validation 的方式去遍历各种各样的比较流行的机器学习模型。比如说线性回归、随机森林、深度神经网络等等,去为每一个操作单元选择对他来说最好的模型。

在我们的观察当中,对于大部分操作人员,对我们数据的体量和特征情况来说,这个 gradient boosting 模型在大部分时候表现最好。

还有一个优化点,在数据库当中很多时候它收集训练数据非常昂贵,建一个索引可能就花了几个小时甚至更长的时间。这种开销是你不太能够承受的。我们观察到在数据库当中,其实很多我们规定出来的操作单元有确定的复杂度。比如说建一个哈希表,它的复杂度是 O(N) 的,排序的复杂度是 O(NlogN) 的。

因为它是渐进的复杂度,当这个 N 大到一定程度的时候,这个操作单元的开销复杂度和 N 的复杂度是成比例关系的。我们可以把每一个单元输出的标签去除以对应的复杂度,差不多可以得到每一个记录的操作单元的输出标签。

所以说如果我们用这种方式去进行预测的时候,我们其实并不需要收集到非常多的数据。因为我们只要收集到N大到一定量级,比如说在我们观察当中 100 万行的时候,它的输出已经非常稳定。你只需要再往上给这个标签的数量给它成比例的增加就可以了,这样我们需要训练的数据量就会大大减小。

给大家展示模型框架的预测结果:首先展示在单一线程里面的结果,对这个结果我们去测试了它既在分析场景下,又在事务场景下的预测效果。我们对于 MB2 的线下框架,他永远用的统一系列、一次建成的这些操作单元的行为目标模型,只是一套模型,但可以应用在不同的数据集上。

作为一个比较的 baseline,我们用的方法叫做 QPPNet,它是之前在数据库领域进行查询运行预测最好的 baseline,但是这个 QPPNet 就和很多其他的数据库模型一样,它是你在某一个数据集上训练,然后在其他的数据集上测试,问题就是它没有一个系统性的产生训练数据的方式,所以我们在不同的workload上面,选某一个负载去训练这个 QPPNet,并且在其他的负载进行测试。

这是 MB2 和 QPPNet 预测某个查询的执行时间在不同的数据集上的结果。这里的纵轴代表预测的错误,就是说这个纵轴越低,这个预测的错误率越低。如果你先去看这些数据集,既训练了 QPPNet,并且在同样的数据集上再进行测试,在这样的数据集上,QPPNet 的准确度和 MB2 相比是相似的,甚至更好。那是因为我们就是在某个数据集上训练的 QPPNet 的模型,同时我们又在这个数据集上进行测试,同一个数据集 QPPNet 有很多的模型结构,去捕捉训练数据集上具体特性,所以它的预测性比较好。

但是当你迁移到不同的数据集上,比如说你在某个数据集上训练 QPPNet,但是你在不同的数据集上测试,这个时候它的预测结果大大不如 MB2。MB2 是同一个模型定位在不同的数据库上,因为 MB2 有一个框架去把这个复杂的数据库里面的各种操作解构成单一独立的操作,并且每一个操作单元收集了足够的数据去建立准确的小模型,它更容易在不同的场景下进行迁移。这和机器学习,或者数据为中心的人工智能的一些观察是相似的。

对于应用机器学习的场景当中,很多时候除了机器学习优秀模型之外,怎么样去系统性的收集高质量的训练数据,这对模型的准确度有非常大的影响的。刚才我们展示的实验,只是在单一线程的情况下且没有任何并发情况下对某一个查询的执行时间预测。但是在现实当中,数据库通常是多线程的,在多线程的情况下,数据库的几个、10 个、20 个核都跑满了,此时查询执行时间可能会成倍延长,此时用之前的单一模型分解独立预测出来就不准了,所以需要有一个模型去捕捉有很多任务在并发执行的情况下,他们之间的竞争是什么样的,负载非常高的时候怎么对每一个操作台进行影响的。

我们建立了另外一个干扰/竞争模型(Interference model)去捕捉影响,干扰模型利用的信息恰好就是刚刚操作单元模型的输出。因为每个操作单元的输出,就是每一个操作单元的运行时间、CPU 消耗、memory 消耗等等,所以我们就利用这些作为输出。具体一点,根据我们之前对于负载的预测,在某一个时间段内并发的操作,我们先用操作单元模型预测资源消耗,然后利用一些统计数据,比如说操作单元总共的资源消耗的时间负载,或者 50 percentile,90 percentile 等等,作为这个时间段内这些操作单元它可能的影响的负载的刻画,这是它的输入特征。

影响干扰模型的输出特征,就是在这个时间段内,我有这些操作单元的负载及分布等等,它的输出在平均情况下,它对每一个查询会造成多大的影响,比如说每个查询慢了一倍,慢了两倍等等,这个类似的我们之前的对每个操作单元有一些运行去收集数据、训练这个模型。

MB2模型使用

我们回顾一下怎么使用整个模型。首先对于我们预测出来的某一个时间段内的数据库的负载,和某一个我们要想要进行的自动化的可能优化,我们先把它拆解成各个不同的小的操作单元。比如说建一个索引,比如说去扫描一些数据,建立哈希表等等,这是在同一个时间段内的。

对于这些操作,我们先用这个操作单元模型去预测。如果说是他自己的情况下,完全独立的情况下,他有多少的开销,有多少的时间等等。我们就把这些开销稍微汇总一下,作为输入数据放到干扰模型里边,然后去预测在这个复杂的情况下,我们该怎么样对每一个操作的运行时间进行调整,把它变成了两倍长等等,然后最后把所有的结果加起来,这就是数据库最终的行为。

我们用一个端到端的实验去展示一下。我们去测试一个数据库正常的运行情况下,去应用各种各样不同的自动化操作,这个模型能不能对这些操作的开销和好处进行准确的预测。这里我们首先是用了一个模拟的日夜周期,就是说白天是事务性,晚上是分析模拟的负载,但是这个时间就缩小了,只有两分钟。白天用 TPCC 模拟,晚上用 TPCH 模拟。很重要的一点就是说我们关注模型的预测,所以我们假设未来的负载预测和最后的计划都已经给定,然后看看我们这个模型预测的效果。

首先负载分为几个阶段。白天 TPCC 到晚上的 TPCH 再到白天的 TPCC,然后延迟也合标准,就是把它放到同一个规格上。在这个开始阶段没有最优的设置,没有最优的索引,所以它的延迟相对来说比较高。然后在 TPCH 阶段,数据库自动改变了一个查询执行模式的参数操作,MB2 这个模型就很准确地预测了这个操作对数据库可能有什么样的影响。

刚才是某一个参数,数据库对于分析型工作负载有一定的优化,但是对于事务型工作负载,数据库需要建一个索引。在这个时候,我们就应用了建立八个索引的操作。在这个操作上,不光是说我们这个模型准确预测操作之后,对这个数据库有什么样的好处,并且应用这个数据库操作的时候,我们也预测到了这个操作对这个数据库有什么样的影响,因为资源竞争的关系,他的延迟其实在增加。

不仅如此,这个分解式的建模方式还能够去预测具体的某一个数据库的查询变慢或者变快的原因。比如说资源怎么样增加?或者是某一个查询因为这个多维的索引怎么样继续加速?这些具体的对于某一个数据库的各个部分都能进行准确的预测,我觉得最终对一个自治数据库去高效进行自动化操作,有非常多的帮助。

结语

稍微总结一下,我们认为对于数据库进行了行为建模,构造模型去预测各种数据库的自动化操作的分销和收益,对于建立一个自治数据库,是一个基础性的步骤。我们今天介绍了一种解构式/分解式的对于数据库进行行为建模的方式,尤其是我们用各个运行器去对每一个数据库的操作单元去收集足够的训练数据,然后去建立准确的模型。

还有一点非常重要,就像在机器学习数据里观察到的,现在很多机器学习的应用都是以数据为中心。数据对机器学习的效果有大大的影响。我们也观察到很多时候如果是应用机器学习或者人工智能的技术在数据库里面,怎么样系统、高效的去搜集高质量的训练数据,对最终模型的准确度、智能度也是至关重要的。


以上为马林老师上期直播的全部分享实录,希望大家有所收获!

第三期 Paper Time 由武汉大学计算机学院副教授 王胜为大家带来“开放式时空大数据助力智能公交路线规划”的分享。

本周三晚 19:00

我们 Paper Time No.3 不见不散

欢迎大家微信扫描上方二维码预约报名~

  • 0
    点赞
  • 0
    收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值