Map Reduce两篇博文: Map Reduce – the Free Lunch is not over? Map Reduce: 一个重大的退步...

1.Map Reduce – the Free Lunch is not over?  

   原文地址:http://www.mengyan.org/blog/archives/2006/11/15/138.html

   微软著名的C++大师Herb Sutter在2005年初的时候曾经写过一篇重量级的文章:”The Free Lunch Is Over: A Fundamental Turn Toward Concurrency in Software“,预言OO之后软件开发将要面临的又一次重大变革-并行计算。

摩尔定律统制下的软件开发时代有一个非常有意思的现象:”Andy giveth, and Bill taketh away.”。不管CPU的主频有多快,我们始终有办法来利用它,而我们也陶醉在机器升级带来的程序性能提高中。

我记着我大二的时候曾经做过一个五子棋的程序,当时的算法就是预先设计一些棋型(有优先级),然后扫描棋盘,对形势进行分析,看看当前走哪部对自己最重要。当然下棋还要堵别人,这就需要互换双方的棋型再计算。如果只算一步,很可能被狡猾的对手欺骗,所以为了多想几步,还需要递归和回朔。在当时的机器上,算3步就基本上需要3秒左右的时间了。后来大学毕业收拾东西的时候找到这个程序,试了一下,发现算10步需要的时间也基本上感觉不出来了。

不知道你是否有同样的经历,我们不知不觉的一直在享受着这样的免费午餐。可是,随着摩尔定律的提前终结,免费的午餐终究要还回去。虽然硬件设计师还在努力:Hyper Threading CPU(多出一套寄存器,相当于一个逻辑CPU)使得Pipeline尽可能满负荷,使多个Thread的操作有可能并行,使得多线程程序的性能有5%-15%的提升;增加Cache容量也使得包括Single-Thread和Multi-Thread程序都能受益。也许这些还能帮助你一段时间,但问题是,我们必须做出改变,面对这个即将到来的变革,你准备好了么?

Concurrency Programming != Multi-Thread Programming。很多人都会说MultiThreading谁不会,问题是,你是为什么使用/如何使用多线程的?我从前做过一个类似AcdSee一样的图像查看/处理程序,我通常用它来处理我的数码照片。我在里面用了大量的多线程,不过主要目的是在图像处理的时候不要Block住UI,所以将CPU Intensive的计算部分用后台线程进行处理。而并没有把对图像矩阵的运算并行分开。

我觉得Concurrency Programming真正的挑战在于Programming Model的改变,在程序员的脑子里面要对自己的程序怎样并行化有很清楚的认识,更重要的是,如何去实现(包括架构、容错、实时监控等等)这种并行化,如何去调试,如何去测试

在Google,每天有海量的数据需要在有限的时间内进行处理(其实每个互联网公司都会碰到这样的问题),每个程序员都需要进行分布式的程序开发,这其中包括如何分布、调度、监控以及容错等等。Google的MapReduce正是把分布式的业务逻辑从这些复杂的细节中抽象出来,使得没有或者很少并行开发经验的程序员也能进行并行应用程序的开发。

MapReduce中最重要的两个词就是Map(映射)和Reduce(规约)。初看Map/Reduce这两个词,熟悉Function Language的人一定感觉很熟悉。FP把这样的函数称为”higher order function”(”High order function”被成为Function Programming的利器之一哦),也就是说,这些函数是编写来被与其它函数相结合(或者说被其它函数调用的)。如果说硬要比的化,可以把它想象成C里面的CallBack函数,或者STL里面的Functor。比如你要对一个STL的容器进行查找,需要制定每两个元素相比较的Functor(Comparator),这个Comparator在遍历容器的时候就会被调用。

拿前面说过图像处理程序来举例,其实大多数的图像处理操作都是对图像矩阵进行某种运算。这里的运算通常有两种,一种是映射,一种是规约。拿两种效果来说,”老照片”效果通常是强化照片的G/B值,然后对每个象素加一些随机的偏移,这些操作在二维矩阵上的每一个元素都是独立的,是Map操作。而”雕刻”效果需要提取图像边缘,就需要元素之间的运算了,是一种Reduce操作。再举个简单的例子,一个一维矩阵(数组)[0,1,2,3,4]可以映射为[0,2,3,6,8](乘2),也可以映射为[1,2,3,4,5](加1)。它可以规约为0(元素求积)也可以规约为10(元素求和)。

面对复杂问题,古人教导我们要“之”,英文中对应的词是”Divide and Conquer“。Map/Reduce其实就是Divide/Conquer的过程,通过把问题Divide,使这些Divide后的Map运算高度并行,再将Map后的结果Reduce(根据某一个Key),得到最终的结果。

Googler发现这是问题的核心,其它都是共性问题。因此,他们把MapReduce抽象分离出来。这样,Google的程序员可以只关心应用逻辑,关心根据哪些Key把问题进行分解,哪些操作是Map操作,哪些操作是Reduce操作。其它并行计算中的复杂问题诸如分布、工作调度、容错、机器间通信都交给Map/Reduce Framework去做,很大程度上简化了整个编程模型。

MapReduce的另一个特点是,Map和Reduce的输入和输出都是中间临时文件(MapReduce利用Google文件系统来管理和访问这些文件),而不是不同进程间或者不同机器间的其它通信方式。我觉得,这是Google一贯的风格,化繁为简,返璞归真。

接下来就放下其它,研究一下Map/Reduce操作。(其它比如容错、备份任务也有很经典的经验和实现,论文里面都有详述)

Map的定义:

Map, written by the user, takes an input pair and produces a set of intermediate key/value pairs. The MapReduce library groups together all intermediate values associated with the same intermediate key I and passes them to the Reduce function.

Reduce的定义:

The Reduce function, also written by the user, accepts an intermediate key I and a set of values for that key. It merges together these values to form a possibly smaller set of values. Typically just zero or one output value is produced per Reduce invocation. The intermediate values are supplied to the user’s reduce function via an iterator. This allows us to handle lists of values that are too large to fit in memory.

MapReduce论文中给出了这样一个例子:在一个文档集合中统计每个单词出现的次数。

Map操作的输入是每一篇文档,将输入文档中每一个单词的出现输出到中间文件中去。

map(String key, String value):
    // key: document name
    // value: document contents
    for each word w in value:
        EmitIntermediate(w, “1″);

比如我们有两篇文档,内容分别是

A - “I love programming”

B - “I am a blogger, you are also a blogger”。

B文档经过Map运算后输出的中间文件将会是:

	I,1
	am,1
	a,1
	blogger,1
	you,1
	are,1
	a,1
	blogger,1

Reduce操作的输入是单词和出现次数的序列。用上面的例子来说,就是 (“I”, [1, 1]), (“love”, [1]), (“programming”, [1]), (“am”, [1]), (“a”, [1,1]) 等。然后根据每个单词,算出总的出现次数。

reduce(String key, Iterator values):
    // key: a word
    // values: a list of counts
    int result = 0;
    for each v in values:
        result += ParseInt(v);
    Emit(AsString(result));

最后输出的最终结果就会是:(“I”, 2″), (“a”, 2″)……

实际的执行顺序是:

  1. MapReduce Library将Input分成M份。这里的Input Splitter也可以是多台机器并行Split
  2. Master将M份Job分给Idle状态的M个worker来处理;
  3. 对于输入中的每一个<key, value> pair 进行Map操作,将中间结果Buffer在Memory里;
  4. 定期的(或者根据内存状态),将Buffer中的中间信息Dump到本地磁盘上,并且把文件信息传回给Master(Master需要把这些信息发送给Reduce worker)。这里最重要的一点是,在写磁盘的时候,需要将中间文件做Partition(比如R个)。拿上面的例子来举例,如果把所有的信息存到一个文件,Reduce worker又会变成瓶颈。我们只需要保证相同Key能出现在同一个Partition里面就可以把这个问题分解。
  5. R个Reduce worker开始工作,从不同的Map worker的Partition那里拿到数据(read the buffered data from the local disks of the map workers),用key进行排序(如果内存中放不下需要用到外部排序 – external sort)。很显然,排序(或者说Group)是Reduce函数之前必须做的一步。 这里面很关键的是,每个Reduce worker会去从很多Map worker那里拿到X(0<X<R) Partition的中间结果,这样,所有属于这个Key的信息已经都在这个worker上了。
  6. Reduce worker遍历中间数据,对每一个唯一Key,执行Reduce函数(参数是这个key以及相对应的一系列Value)。
  7. 执行完毕后,唤醒用户程序,返回结果(最后应该有R份Output,每个Reduce Worker一个)。

可见,这里的分(Divide)体现在两步,分别是将输入分成M份,以及将Map的中间结果分成R份。将输入分开通常很简单,Map的中间结果通常用”hash(key) mod R”这个结果作为标准,保证相同的Key出现在同一个Partition里面。当然,使用者也可以指定自己的Partition Function,比如,对于Url Key,如果希望同一个Host的URL出现在同一个Partition,可以用”hash(Hostname(urlkey)) mod R”作为Partition Function。

对于上面的例子来说,每个文档中都可能会出现成千上万的 (“the”, 1)这样的中间结果,琐碎的中间文件必然导致传输上的损失。因此,MapReduce还支持用户提供Combiner Function。这个函数通常与Reduce Function有相同的实现,不同点在于Reduce函数的输出是最终结果,而Combiner函数的输出是Reduce函数的某一个输入的中间文件。

Tom White给出了Nutch[2]中另一个很直观的例子,分布式Grep。我一直觉得,Pipe中的很多操作,比如More、Grep、Cat都类似于一种Map操作,而Sort、Uniq、wc等都相当于某种Reduce操作。

加上前两天Google刚刚发布的BigTable论文,现在Google有了自己的集群 – Googel Cluster,分布式文件系统 – GFS,分布式计算环境 –MapReduce,分布式结构化存储 – BigTable,再加上Lock Service。我真的能感觉的到Google著名的免费晚餐之外的对于程序员的另一种免费的晚餐,那个由大量的commodity PC组成的large clusters。我觉得这些才真正是Google的核心价值所在。

呵呵,就像微软老兵Joel Spolsky(你应该看过他的”Joel on Software”吧?)曾经说过,对于微软来说最可怕的是[1],微软还在苦苦追赶Google来完善Search功能的时候,Google已经在部署下一代的超级计算机了。

The very fact that Google invented MapReduce, and Microsoft didn’t, says something about why Microsoft is still playing catch up trying to get basic search features to work, while Google has moved on to the next problem: building Skynet^H^H^H^H^H^H the world’s largest massively parallel supercomputer. I don’t think Microsoft completely understands just how far behind they are on that wave.

注1:其实,微软也有自己的方案 – DryAd。问题是,大公司里,要想重新部署这样一个底层的InfraStructure,无论是技术的原因,还是政治的原因,将是如何的难。

注2:Lucene之父Doug Cutting的又一力作,Project Hadoop - 由Hadoop分布式文件系统和一个Map/Reduce的实现组成,Lucene/Nutch的成产线也够齐全的了。

Popularity: 100% [?]

 

2.MapReduce:一个重大的倒退

 原文地址:http://www.databasecolumn.com/2008/01/mapreduce-a-major-step-back.html

   这篇文章是由databasecolumn的几个数据库大牛写的,简要的介绍了MapReduce以及将其与现代数据库管理系统进行了对比,并指出了一些不足之处。本文纯属学习性翻译,从多方面来了解MapReduce,不代表完全赞同原文的观点。请读者也辩证的看。
   一月八号,一个数据库专栏的读者询问我们关于对新的分布式数据库研究成果的意见。我们在这结合MapReduce谈谈我们的看法。现在是讨论这个问题的不错的时机,因为最近媒体上到处充斥着新的革命所谓“云计算”的信息。这种模式需要利用大量的(低端)处理器并行工作来解决计算问题。实际上,这建议利用大量的低端处理器来构建数据中心,而不是利用数目少的多的高端服务器来构建。
   举例来说,IBM和Google已经宣布计划用1000台处理器构建的集群提供给部分大学,传授学生们如何使用MapReduce工具在这些集群上编程。加利福尼亚大学伯克利分校甚至打算开设使用MapReduce框架编程的课程。我们对MapReduce支持者大肆炒作它如何如何能够开发更加具有扩展性,以及数据密集型程序感到震惊。MapReduce可能在某些特定类型的通用计算上是个不错的想法,但是对于数据库社区来说:
1. 从大规模数据应用程序模型来说是一个巨大的倒退。
2. 不是一个最优实现,因为它使用蛮力来代替索引。
3. 一点都不新奇,它只是实现了一个特定的25年前就有的众所周知的技术。
4. 失去了大部分目前数据库管理系统的特性。
5. 不能兼容所有目前数据库管理系统用户已经依赖的工具。
 首先我们将简要的讨论下MapReduce到底是什么,然后我们将就上面5点进行更深层次的讨论。
MapReduce是什么?
 MapReduce基础出发点是很易懂的。它由称为map和reduce的两部分用户程序组成,然后利用框架在计算机集群上面根据需求运行多个程序实例来处理各个子任务,然后再对结果进行归并。
 Map程序从输入流中读取一组“记录”,然后对记录进行需要的过滤或者转换,然后输出一组记录(key,data)。当map程序生成输出记录时,一个分割方法将记录划分为M个不相交的块并赋予一个键值。这个分割方法一般是一个hash函数,只要这个决定性的函数能够满足就行。当一个块被填充后,它将写入磁盘,map程序结束的时候每个块都将输出M个文件。
 通常情况下,将有多个map的程序实例运行在计算机集群的不同的节点上。每个map实例都将由MapReduce调度程序分配一个不重复的输入文件来独立执行。如果有N个节点参与map程序执行,那么N个节点中的每个节点都将有M个文件存储在各自的磁盘上,也就是说,总共将有N*M个文件。Fi,j, 1 ≤ i ≤ N, 1 ≤ j ≤ M.
 其中有个值得注意的关键点是每个map实例都必须使用一个相同的hash方法。这样,所有的拥有相同hash值的输出记录才会写入相应的输出文件。
 MapReduce的第二个阶段就是执行M个reduce的程序实例。Rj, 1 ≤ j ≤ M.每个reduce实例Rj的输入文件由文件 Fi,j组成,1 ≤ i ≤ N。还有一个值得注意的是:所有从map阶段输出的拥有相同hash值的记录,无论是哪个map实例生成的,都将由一个相同的reduce实例处理。在map-reduce框架收集整理之后,所有的输入记录都将根据它们的键值(key)编组然后提供给reduce程序。跟map程序一样,reduce程序也可以做任意的计算。所以,你可以对输入的记录做任何你想要的事情。举例来说,可能会对记录的别的字段进行一些附加的计算。每个reduce实例都可以将记录写入输出文件,只要是MapReduce计算所需要的结果。
 用SQL来做类比,map象聚合(aggregate)查询中的group-by子句。Reduce则类似计算group-by起来的行的聚合函数(例如求平均等)。
 现在我们基于这个计算模型来讨论上面提到的五点:
1. MapReduce是一个数据库存取的退步
 做为一个数据处理模型,MapReduce呈现出了一个巨大的退步。数据库社区从IBM在1968年第一次发布IMS以来的四十年中学到了以下三个经验:
* 结构描述是好的。
* 将结构描述从程序中分离是好的
* 高阶的访问语言是好的

 MapReduce没有吸引上面三个经验中的任何一个,而且还退步到了现在数据库管理系统发明前的60年代。
 数据库管理系统社区学习到的关于最重要的结构描述就是:记录的字段和它的数据类型都记录在存储系统中。更重要的是,数据库管理系统的运行时可以保证所有的记录都遵守结构描述。这是避免将垃圾数据添加到数据集中的最好的方法。MapReduce没有这样的方法,也没有避免将垃圾数据添加到数据集中的控制。一个毁坏的数据集可以悄无声息的破坏整个使用这个数据集的MapReduce程序。
 将数据描述与程序分离也很关键。如果开发者想在一个数据集上开发一个新的程序,他必须先去了解记录结构。在现代数据库管理系统中,结构描述存储在系统目录中,而且可以被用户用SQL查询来了解它的结构。与此相反的是,如果数据描述不存在,或者隐藏在程序之中,开发者要了解这个数据结构必须通过检查原有的代码。这个工作不仅仅是非常沉闷的,而且开发者必须先找到这个程序的源代码。如果没有相应的结构描述存在,后面的这个沉闷的问题将在所有的MapReduce程序中存在。
 在1970年数据库管理系统社区,关系型数据库支持者和数据系统语言协会(Codasyl)支持者进行了一场“剧烈的辩论”。其中一个最大的争议是数据库管理系统的访问程序以何种方式访问:
* 用统计来获取你想要的数据(关系型的观点)
* 提供一个算法来进行数据访问(Codasyl的观点)

 争论的结果已经是古代史了,但是整个世界都看到了高阶语言的价值以及关系型系统的胜利。以高阶语言的形式编程更加容易编写,易于修改,而且方便一个新来者的理解。Codasyl被批判为“以汇编语言的形式来对数据库管理系统进行访问”。MapReduce程序员有点类似Codasyl程序员。他们用低阶的语言来处理低阶记录。没有人提倡回归汇编语言,类似的,不应该强制任何人用MapReduce来编程。
 MapReduce提倡者可能会反对说他们的数据集没有数据描述的假设。那么我们解除这个断言。在从输入数据记录中提取一个key时,map方法在每个输入记录中至少依赖一个存在的数据字段。相同Reduce方法的持有者从接受到的处理数据中计算一些值。
 基于Google的BigTable或者Hadoop的Hbase来编写MapReduce程序并不会真正重大的改变这种状况。在相同的表中使用一种自描述元组格式(行号,列名,值)确实可以拥有不同的架构。但是,BigTable和Hbase并没有提供逻辑上的独立。以视图机制举例来说,视图有一个明显的简化作用,当逻辑数据描述(logic schema)改变后,仍保持程序运行。

2. MapReduce是一个粗燥的实现
 所有现在数据库管理系统使用hash或者B-tree来索引加快对数据的访问。如果一个用户在查找一个记录集的子记录集(比如雇员中谁的薪水在10000或者谁在鞋生产部门),那么他可以使用索引来有效的缩减查找范围。另外,还提供了一个查询优化器来决定到底是使用索引还是进行一个残忍野蛮的顺序查询。
 MapReduce没有索引,理所当然的只能使用蛮力来作为处理选项。而不管索引在当前情况下是否是一个最好的访问机制。
 一个值得争论的是,MapReduce提出的自动的在计算机集群中提供并行计算的价值。其实这个特性在1980年时代就被数据库管理系统研究社区研究过了,多个原型被提出来,比如Gamma,Bubba和Grace。商业化的利用这些思想在系统则在80年代末期,比如Teradata。
 概括起来说,在前20年已经出现了高性能,商业化的,面向网格计算机群的SQL引擎(带结构描述和索引)。MapReduce跟这些系统相比并没有那么好。
 MapReduce同时存在很多底层的实现问题,特别是数据交换和数据斜交的情况。
 一个因素是MapReduce支持者好像没有注意到关于数据斜交的问题。就像在“平行数据库系统:未来的高性能数据库系统”中提到的,数据斜交是构建成功高扩展性并行查询系统的巨大障碍。这个问题重现在map阶段,当拥有相同键的数据拥有大幅度差异的时候。这个差异,反过来导致某些reduce实例花费比其它实例更长甚至常很多的时间来运行。结果就是计算的运行时间由速度最慢的那个reduce实例决定。平行数据库社区已经广泛的研究了这个问题并且拥有了成熟的,MapReduce社区可能愿意采纳的解决方案。
 还有第二个严重的性能问题被MapReduce支持者掩盖了。回忆N个map实例中的每个实例都将生成M个输出文件。每个都分发给不同的reduce实例。这些文件都被写入本地硬盘以备map实例使用。如果N是1000,M是500,那么在map阶段将生成500000个本地文件。当reduce阶段开始,500个reduce实例必须读取1000个输入文件,必须使用类似FTP的协议将每个输入文件从各个map实例运行的节点中获取(pull)过来。在100秒内所有reduce实例将同时的运行起来,不可避免的会发生两个或者更多个reduce实例企图并行的从同一个map节点中获取输入文件,包括大量的磁盘搜索,当超过因子20时,将极大的降低磁盘的有效传输率。这就是为什么并行数据库系统不实现分割文件,而使用推(push to sockets)来代替拉(pull)。因为MapReduce通过实现分割文件来获得优秀的容错性,不好说如果MapReduce框架修改成使用推(push)模型是否会成功。
 鉴于实验评估,我们严重的怀疑MapReduce在大规模应用中会表现的很好。MapReduce的实现者还需要好好的研究过去25年来并行数据库管理系统的研究文献。

3. MapReduce并不新奇
 MapReduce社区看起来感觉他们发现了一个全新的处理大数据集的模型。实际上,MapReduce所使用的技术至少是20年前的。将大数据集划分为小数据集的思想是在Kitsuregawa首次提出的“Application of Hash to Data Base Machine and Its Architecture”的基础上发展出来的一个新的连接算法。在“Multiprocessor Hash-Based Join Algorithms”中,Gerber演示了如何将Kitsuregawa的技术扩展到使用联合分区表,分区执行以及基于hash的分割来连接并行的无共享集群。DeWitt演示了如何采用这些技术来执行有group by子句以及没有group by子句的并行聚合。DeWitt和Gray描述了并行数据库系统以及他们如何处理查询。Shatdal和Naughton探索了并行聚合的替代策略。
 Teradata已经出售利用这些技术构建的数据库管理系统20多年了,而这些技术正是MapReduce一伙声称的发明的技术。
 当然MapReduce提倡者将毫无疑问的声称他们编写的MapReduce函数实现他们的软件与使用并行SQL实现有多么大的不同,我们必须提醒他们,POSTGRES已经在80年代中期就支持了用户自定义函数以及用户自定义聚合。本质上来说,从1995年Illustra引擎开始算,所有现代数据库系统都提供了类似的功能很长一段时间了。
4. MapReduce失去了很多特性
 所有下面的特性都被现在的数据库管理系统提供了,而MapReduce没有:
* 批量导入 —— 将输入数据转化成想要的格式并加载到数据库中
* 索引 —— 如上文所述
* 更新 —— 改变数据集中的数据
* 事务 —— 支持并行更新以及从失败的更新中恢复
* 完善的约束 —— 防止垃圾数据添加到数据集
* 完善的引用 —— 类似FK,防止垃圾数据的存在
* 视图 —— 底层逻辑数据描述可以改变但不需要重写程序
 简单的说来,MapReduce只提供了现在数据库管理系统的函数性功能。

5. MapReduce与现有的数据库管理系统工具不兼容
 一个现代的SQL数据库管理系统都拥有如下可用的工具:
* 报表 —— (比如水晶报表) 将数据友好的展示给人
* 商业智能工具 —— (比如Business Objects or Cognos)允许在数据仓库中进行特定查询
* 数据挖掘工具 —— (比如Oracle Data Mining)允许用户在大数据集中发现数据规律
* 复制工具 —— 允许用户在不同的数据库中进行复制传输
* 数据库设计工具 —— 帮助用户构建数据库
 MapReduce不能使用这些工具,同时它也没有自己的工具。直到它能与SQL兼容或者有人编写了这些工具,MapReduce仍然在端到端的任务中显得十分困难。

总结
  看到一个庞大的社区参与到设计和实现可扩展的查询处理技术中是值得振奋的。但是,我们认为他们不应该忽视超过40年的数据库技术的经验。——特别是拥有巨大优势的数据描述模型以及物理数据与逻辑数据互相独立;描述性的查询语言(比如SQL);提供设计,实现,以及维护程序。此外,计算机科学社区不应该孤立起来而应该多读读别的社区的技术文献。我们鼓励好好的研究下近25年来的并行数据库管理系统的技术文献。最后,在MapReduce达到现代数据库管理系统之前,还有很多的没实现的特性以及必须的工具需要添加。
 我们完全的理解利用数据库来解决他们的问题是可以的。数据库社区意识到数据库系统解决他们的问题使用起来操作过于“复杂”。数据库社区也从MapReduce提供的优秀的容错机制中学到了不少的有价值的东西。最后我们注意到一些数据库的研究者开始探索以MapReduce框架为基础来构建可扩展的数据库系统,yahoo!研究中心的Pig项目就是其中一个努力的结果。

转载于:https://www.cnblogs.com/raindreams/archive/2012/06/05/2536351.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值