google三大论文

原创 2015年11月19日 23:27:15

 Google三大论文之一:BigTable

Bigtable是一个分布式的结构化数据存储系统,它被设计用来处理海量数据:通常是分布在数千台普通服务器上的PB级的数据。Google的很多项目使用Bigtable存储数据,包括Web索引、Google Earth、Google Finance。

什么是BigTable

Bigtable是一个分布式的结构化数据存储系统。设计目的是可靠的处理PB级别的数据,并且能够部署到上千台机器上。Bigtable已经实现了下面的几个目标:适用性广泛、可扩展、高性能和高可用性。

Bigtable看起来像一个数据库,采用了很多数据库的实现策略。Bigtable将存储的数据都视为字符串,但是Bigtable本身不去解析这些字符串,客户程序通常会在把各种结构化或者半结构化的数据串行化到这些字符串里。通过仔细选择数据的模式,客户可以控制数据的位置相关性。最后,可以通过BigTable的模式参数来控制数据是存放在内存中、还是硬盘上。

BigTable的数据模型

Bigtable是一个稀疏的、分布式的、持久化存储的多维度排序Map。稀疏的意思是,一个表里不同的行,列可能完完全全不一样。Bigtable建立在GFS之上本身就意味着分布式,当然分布式的意义还不仅限于此。持久化的意思很简单,Bigtable的数据最终会以文件的形式放到GFS去。

Map的索引是行关键字、列关键字以及时间戳;
       (row:string,column:string,time:int64)->string

      假设我们想要存储海量的网页及相关信息,这些数据可
以用于很多不同的项目,我们姑且称这个特殊的表为Webtable。在Webtable里,我们使用URL作为行关键字,使用网页的某些属性作为列名,网页的内容存在“contents:”列中,并用获取该网页的时间戳作为标识

 

行:

表中的行关键字可以是任意的字符串(目前支持最大64KB的字符串,但是对大多数用户,10-100个字节就足够了)。对同一个行关键字的读或者写操作都是原子的(不管读或者写这一行里多少个不同列). Bigtable通过行关键字的字典顺序来组织数据。表中的每个行都可以动态分区。每个分区叫做一个”Tablet”,Tablet是数据分布和负载均衡调整的最小单位。

行是表的一级索引,将列和时间戳合并查询。如:

        

 

Table{

‘aaaa’:{sth.}

‘bbbb’:{sth.}

‘cccc’:{sth.}     

}

举例来说,在Webtable里,通过反转URL中主机名的方式,可以把同一个域名下的网页聚集起来组织成连续的行。具体来说,我们可以把maps.google.com/index.html的数据存放在关键字com.google.maps/index.html下。把相同的域中的网页存储在连续的区域可以让基于主机和域名的分析更加有效。

列族:

列关键字组成的集合叫做“列族“,列族是访问控制的基本单位。列关键字的命名语法如下:列族:限定词。比如,Webtable有个列族language,language列族用来存放撰写网页的语言。我们在language列族中只使用一个列关键字,用来存放每个网页的语言标识ID。Webtable中另一个有用的列族是anchor;这个列族的每一个列关键字代表一个锚链接,如图一所示。Anchor列族的限定词是引用该网页的站点名;Anchor列族每列的数据项存放的是链接文本。

访问控制、磁盘和内存的使用统计都是在列族层面进行的。列族是访问表的二级索引,如:

Table{

         ‘aaaa’:{

                   ‘yyyy’:{sth.}

                   ‘zzzz’:{sth.}

         }

         ‘bbbb’:{

                   ‘www’:{sth.}

                   ‘xxxx’:{sth.}

         }

}

 

时间戳:

在Bigtable中,表的每一个数据项都可以包含同一份数据的不同版本;不同版本的数据通过时间戳来索引。Bigtable时间戳的类型是64位整型。

在Webtable的举例里,contents:列存储的时间戳信息是网络爬虫抓取一个页面的时间。上面提及的垃圾收集机制可以让我们只保留最近三个版本的网页数据。

时间戳是表的三级索引,数据项中,不同版本的数据按照时间戳倒序排序,即最新的数据排在最前面。如;

Table{

         ‘aaaa’:{

                   12:{sth.}

                   3:{sth.}

         }

         ‘bbbb’:{

                   6:{sth.}

                   2:{sth.}

         }

    ‘cccc’:{

                   7:{sth.}

         }

}

BigTableAPI

Bigtable提供了建立和删除表以及列族的API函数。Bigtable还提供了修改集群、表和列族的元数据的API,比如修改访问权限。

客户程序可以对Bigtable进行如下的操作:写入或者删除Bigtable中的值、从每个行中查找值、或者遍历表中的一个数据子集。

Bigtable还支持一些其它的特性,利用这些特性,用户可以对数据进行更复杂的处理。首先,Bigtable支
持单行上的事务处理,利用这个功能,用户可以对存储在一个行关键字下的数据进行原子性的读-更新-写操作。虽然Bigtable提供了一个允许用户跨行批量写入数据的接口,但是,Bigtable目前还不支持通用的跨行事务处理。

BigTable构件

Bigtable是建立在其它的几个Google基础构件上的。BigTable使用Google的分布式文件系统(GFS)存储日志文件和数据文件。BigTable集群通常运行在一个共享的机器池中,池中的机器还会运行其它的各种各样的分布式应用程序,BigTable的进程经常要和其它应用的进程共享机器。

BigTable内部存储数据的文件是GoogleSSTable格式的。从内部看,SSTable是一系列的数据块(通常每个块的大小是64KB,这个大小是可以配置的)。SSTable使用块索引(通常存储在SSTable的最后)来定位数据块;在打开SSTable的时候,索引被加载到内存。每次查找都可以通过一次磁盘搜索完成:首先使用二分查找法在内存中的索引里找到数据块的位置,然后再从硬盘读取相应的数据块。也可以选择把整个SSTable都放在内存中,这样就不必访问硬盘了。

BigTable还依赖一个高可用的、序列化的分布式锁服务组件,叫做Chubby。一个Chubby服务包括了5个活动的副本,其中的一个副本被选为Master,并且处理请求。只有在大多数副本都是正常运行的,并且彼此之间能够互相通信的情况下,Chubby服务才是可用的。当有副本失效的时候,Chubby使用Paxos算法来保证副本的一致性。

BigTable的结构介绍

Bigtable包括了三个主要的组件:链接到客户程序中的库、一个Master服务器和多个Tablet服务器。针对系统工作负载的变化情况,BigTable可以动态的向集群中添加(或者删除)Tablet服务器。

Master服务器主要负责以下工作:为Tablet服务器分配Tablets、检测新加入的或者过期失效的Table服务器、对Tablet服务器进行负载均衡、以及对保存在GFS上的文件进行垃圾收集。除此之外,它还处理对模式的相关修改操作,例如建立表和列族。

每个Tablet服务器都管理一个Tablet的集合(通常每个服务器有大约数十个至上千个Tablet)。每个Tablet服务器负责处理它所加载的Tablet的读写操作,以及在Tablets过大时,对其进行分割。

和很多Single-Master类型的分布式存储系统类似,客户端读取的数据都不经过Master服务器:客户程序直接和Tablet服务器通信进行读写操作。由于BigTable的客户程序不必通过Master服务器来获取Tablet的位置信息,因此,大多数客户程序甚至完全不需要和Master服务器通信。

Tablet分配

在任何一个时刻,一个Tablet只能分配给一个Tablet服务器。Master服务器记录了当前有哪些活跃的Tablet服务器、哪些Tablet分配给了哪些Tablet服务器、哪些Tablet还没有被分配。当一个Tablet还没有被分配、并且刚好有一个Tablet服务器有足够的空闲空间装载该Tablet时,Master服务器会给这个Tablet服务器发送一个装载请求,把Tablet分配给这个服务器。

BigTable使用Chubby跟踪记录Tablet服务器的状态。当一个Tablet服务器启动时,它在Chubby的一个指定目录下建立一个有唯一性名字的文件,并且获取该文件的独占锁。Master服务器实时监控着这个目录(服务器目录),因此Master服务器能够知道有新的Tablet服务器加入了。如果Tablet服务器丢失了Chubby上的独占锁 — 比如由于网络断开导致Tablet服务器和Chubby的会话丢失 — 它就停止对Tablet提供服务。

Tablet服务

如图5所示,Tablet的持久化状态信息保存在GFS上。更新操作提交到REDO日志中。在这些更新操作中,最近提交的那些存放在一个排序的缓存中,我们称这个缓存为memtable;较早的更新存放在一系列SSTable中。为了恢复一个Tablet,Tablet服务器首先从METADATA表中读取它的元数据。Tablet的元数据包含了组成这个Tablet的SSTable的列表,以及一系列的RedoPoint这些RedoPoint指向可能含有该Tablet数据的已提交的日志记录。Tablet服务器把SSTable的索引读进内存,之后通过重复RedoPoint之后提交的更新来重建memtable。

当对Tablet服务器进行写操作时,Tablet服务器首先要检查这个操作格式是否正确、操作发起者是否有执
行这个操作的权限。权限验证的方法是通过从一个Chubby文件里读取出来的具有写权限的操作者列表来
进行验证(这个文件几乎一定会存放在Chubby客户缓存里)。成功的修改操作会记录在提交日志里。

 

 

Google三大论文之二:The Google File System

Google GFS文件系统是一个面向大规模数据密集型应用的、可伸缩的分布式文件系统。GFS虽然运行在廉价的普遍硬件设备上,但是它依然了提供灾难冗余的能力,为大量客户机提供了高性能的服务。

设计思路:

首先,组件失效被认为是常态事件,而不是意外事件。GFS包括几百甚至几千台普通的廉价设备组装的存储机器,同时被相当数量的客户机访问。因此有一部分机器宕机也是很常见的事情。

其次,以通常的标准衡量,我们的文件非常巨大。数GB的文件非常普遍。

第三,绝大部分文件的修改是采用在文件尾部追加数据,而不是覆盖原有数据的方式。对文件的随机写入操作在实际中几乎不存在。一旦写完之后,对文件的操作就只有读,而且通常是按顺序读。

第四,应用程序和文件系统API的协同设计提高了整个系统的灵活性。

另外,GFS支持常用的操作,如创建新文件、删除文件、打开文件、关闭文件、读和写文件。且提供了快照和记录追加操作。

GFS的主要假设如下:

GFS的服务器都是普通的商用计算机,并不那么可靠,集群出现结点故障是常态。因此必须时刻监控系统的结点状态,当结点失效时,必须能检测到,并恢复之。

系统存储适当数量的大文件。理想的负载是几百万个文件,文件一般都超过100MB,GB级别以上的文件是很常见的,必须进行有效管理。支持小文件,但不对其进行优化。

负载通常包含两种读:大型的流式读(顺序读),和小型的随机读。前者通常一次读数百KB以上,后者通常在随机位置读几个KB。

负载还包括很多连续的写操作,往文件追加数据(append)。文件很少会被修改,支持随机写操作,但不必进行优化。

系统必须实现良好定义的语义,用于多客户端并发写同一个文件。同步的开销必须保证最小。

高带宽比低延迟更重要,GFS的应用大多需要快速处理大量的数据,很少会严格要求单一操作的响应时间。

 

GFS架构:

一个GFS集群包含一个单独的Master节点,多台Chunk服务器,并且同时被多个客户端访问,如图1所示。所有的这些机器通常都是普通的Linux机器,运行着用户级别(user-level)的服务进程。我们可以很容易的把Chunk服务器和客户端都放在同一台机器上,前提是机器资源允许,并且我们能够接受不可靠的应用程序代码带来的稳定性降低的风险。

 

GFS存储的文件都被分割成固定大小的Chunk。在Chunk创建的时候,Master服务器会给每个Chunk分配一个不变的、全球唯一的64位的Chunk标识。缺省情况下,我们使用3个存储复制节点,不过用户可以为不同的文件命名空间设定不同的复制级别。

Master节点管理所有的文件系统元数据。这些元数据包括名字空间、访问控制信息、文件和Chunk的映射信息、以及当前Chunk的位置信息。GFS客户端代码以库的形式被链接到客户程序里。客户端代码实现了GFS文件系统的API接口函数、应用程序与Master节点和Chunk服务器通讯、以及对数据进行读写操作。

单一Master节点

单一的Master节点的策略大大简化了我们的设计。单一的Master节点可以通过全局的信息精确定位Chunk的位置以及进行复制决策。另外,我们必须减少对Master节点的读写,避免Master节点成为系统的瓶颈。客户端并不通过Master节点读写文件数据。反之,客户端向Master节点询问它应该联系的Chunk服务器。

Chunk尺寸:

Chunk的大小是关键的设计参数之一。我们选择了64MB,这个尺寸远远大于一般文件系统的Blocksize。每个Chunk的副本都以普通Linux文件的形式保存在Chunk服务器上,只有在需要的时候才扩大。惰性空间分配策略避免了因内部碎片造成的空间浪费,内部碎片或许是对选择这么大的Chunk尺寸最具争议一点。

元数据

GFS是典型的集中式元数据服务,所有的元数据都存放在一个master结点内。元数据主要包括三种:文件和数据块的命名空间,文件-数据块映射表,数据块的副本位置。所有的元数据都是放在内存里的。

前两种元数据会被持久化到本地磁盘中,以操作日志的形式。操作日志会记录下这两种元数据的每一次关键变化,因此当master宕机,就可以根据日志恢复到某个时间点。日志的意义还在于,它提供了一个时间线,用于定义操作的先后顺序,文件、数据块的版本都依赖于这个时间顺序。

数据块的副本位置则没有持久化,因为动辄数以百计的chunkserver是很容易出错的,因此只有chunkserver对自己存储的数据块有绝对的话语权,而master上的位置信息很容易因为结点失效等原因而过时。取而代之的方法是,master启动时询问每个chunkserver的数据块情况,而且chunkserver在定期的心跳检查中也会汇报自己存储的部分数据块情况。

GFS物理上没有目录结构,也不支持链接操作,使用一张表来映射文件路径名和元数据。

操作日志

操作日志包含了关键的元数据变更历史记录。操作日志非常重要,我们必须确保日志文件的完整,确保只有在元数据的变化被持久化后,日志才对客户端是可见的。Master服务器在灾难恢复时,通过重演操作日志把文件系统恢复到最近的状态。为了缩短Master启动的时间,我们必须使日志足够小。

缓存和预取

GFS的客户端和chunkserver都不会缓存任何数据,这是因为GFS的典型应用是顺序访问大文件,不存在时间局部性。空间局部性虽然存在,但是数据集一般很大,以致没有足够的空间缓存。

我们知道集中式元数据模型的元数据服务器容易成为瓶颈,应该尽量减少客户端与元数据服务器的交互。因此GFS设计了元数据缓存。client需要访问数据时,先询问master数据在哪儿,然后将这个数据地址信息缓存起来,之后client对该数据块的操作都只需直接与chunkserver联系了,当然缓存的时间是有限的,过期作废。

master还会元数据预取。因为空间局部性是存在,master可以将逻辑上连续的几个数据块的地址信息一并发给客户端,客户端缓存这些元数据,以后需要时就可以不用找master的麻烦了。

 

 

Google三大论文之三:Google MapReduce

MapReduce是一个编程模型,也是一个处理和生成超大数据集的算法模型的相关实现。用户首先创建一个Map函数处理一个基于key/value pair的数据集合,输出中间的基于key/valuepair的数据集合;然后再创建一个Reduce函数用来合并所有的具有相同中间key值的中间value值。

编程模型

MapReduce编程模型的原理是:利用一个输入key/valuepair集合来产生一个输出的key/valuepair集合。MapReduce库的用户用两个函数表达这个计算:Map和Reduce。

用户自定义的Map函数接受一个输入的key/value pair值,然后产生一个中间key/valuepair值的集合。MapReduce库把所有具有相同中间key值I的中间value值集合在一起后传递给reduce函数。用户自定义的Reduce函数接受一个中间key的值I和相关的一个value值的集合。

Reduce函数合并这些value值,形成一个较小的value值的集合。一般的,每次Reduce函数调用只产生0或1个输出value值。通常我们通过一个迭代器把中间value值提供给Reduce函数,这样我们就可以处理无法全部放入内存中的大量的value值的集合。


例如,计算一个大的文档集合中每个单词出现的次数,下面是伪代码段:
map(String key, String value):
// key: document name
// value: document contents
for each word w in value:
EmitIntermediate(w, “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));
Map函数输出文档中的每个词、以及这个词的出现次数(在这个简单的例子里就是1)。Reduce函数把Map函数产生的每一个特定的词的计数累加起来。

实现步骤:

通过将Map调用的输入数据自动分割为M个数据片段的集合,Map调用被分布到多台机器上执行。输入的数据片段能够在不同的机器上并行处理。使用分区函数将Map调用产生的中间key值分成R个不同分区(例如,hash(key) mod R),Reduce调用也被分布到多台机器上执行。分区数量(R)和分区函数由用户来指定。

 

图1展示了我们的MapReduce实现中操作的全部流程。当用户调用MapReduce函数时,将发生下面的一系列动作(下面的序号和图1中的序号一一对应):
        1.用户程序首先调用的MapReduce库将输入文件分成M个数据片度,每个数据片段的大小一般从 16MB到64MB(可以通过可选的参数来控制每个数据片段的大小)。然后用户程序在机群中创建大量的程序副本。
        2.这些程序副本中的有一个特殊的程序–master。副本中其它的程序都是worker程序,由master分配任务。有M个Map任务和R个Reduce任务将被分配,master将一个Map任务或Reduce任务分配给一个空闲的worker。
        3.被分配了map任务的worker程序读取相关的输入数据片段,从输入的数据片段中解析出key/valuepair,然后把key/value pair传递给用户自定义的Map函数,由Map函数生成并输出的中间key/valuepair,并缓存在内存中。
        4.缓存中的key/valuepair通过分区函数分成R个区域,之后周期性的写入到本地磁盘上。缓存的key/valuepair在本地磁盘上的存储位置将被回传给master,由master负责把这些存储位置再传送给Reduceworker。
        5.当Reduceworker程序接收到master程序发来的数据存储位置信息后,使用RPC从Map worker所在主机的磁盘上读取这些缓存数据。当Reduceworker读取了所有的中间数据后,通过对key进行排序后使得具有相同key值的数据聚合在一起。由于许多不同的key值会映射到相同的Reduce任务上,因此必须进行排序。如果中间数据太大无法在内存中完成排序,那么就要在外部进行排序。
        6.Reduce worker程序遍历排序后的中间数据,对于每一个唯一的中间key值,Reduce worker程序将这个key值和它相关的中间value值的集合传递给用户自定义的Reduce函数。Reduce函数的输出被追加到所属分区的输出文件。
        7.当所有的Map和Reduce任务都完成之后,master唤醒用户程序。在这个时候,在用户程序里的对MapReduce调用才返回。在成功完成任务之后,MapReduce的输出存放在R个输出文件中(对应每个Reduce任务产生一个输出文件,文件名由用户指定)。

 

Combiner函数

combiner函数首先在本地将这些记录进行一次合并,然后将合并的结果再通过
网络发送出去。Combiner函数在每台执行Map任务的机器上都会被执行一次。一般情况下,Combiner和Reduce函数是一样的。Combiner函数和Reduce函数之间唯一的区别是MapReduce库怎样控制函数的输出。Reduce函数的输出被保存在最终的输出文件里,而Combiner函数的输出被写到中间文件里,然后被发送给Reduce任务。

 

容错

Worker失败  

因为MapReduce库的设计初衷是使用由成百上千的机器组成的集群来处理超大规模的数据,所以,这个库必须要能很好的处理机器故障。worker故障master周期性的ping每个worker。如果在一个约定的时间范围内没有收到worker返回的信息,master将把这个worker标记为失效。所有由这个失效的worker完成的Map任务被重设为初始的空闲状态,之后这些任务就可以被安排给其他的worker。同样的,worker失效时正在运行的Map或Reduce任务也将被重新置为空闲状态,等待重新调度。当worker故障时,由于已经完成的Map任务的输出存储在这台机器上,Map任务的输出已不可访问了,因此必须重新执行。而已经完成的Reduce任务的输出存储在全局文件系统上,因此不需要再次执行。当一个Map任务首先被worker A执行,之后由于worker A失效了又被调度到worker B执行,这个“重新执行”的动作会被通知给所有执行Reduce任务的worker。任何还没有从worker A读取数据的Reduce任务将从worker B读取数据。

master失败

一个简单的解决办法是让master周期性的将上面描述的数据结构的写入磁盘,即检查点(checkpoint)。如果这个master任务失效了,可以从最后一个检查点(checkpoint)开始启动另一个master进程。然而,由于只有一个master进程,master失效后再恢复是比较麻烦的,因此我们现在的实现是如果master失效,就中止MapReduce运算。客户可以检查到这个状态,并且可以根据需要重新执行MapReduce操作。在失效方面的处理机制当用户提供的Map和Reduce操作是输入确定性函数(即相同的输入产生相同的输出)时,我们的分布式实现在任何情况下的输出都和所有程序没有出现任何错误、顺序的执行产生的输出是一样的。我们依赖对Map和Reduce任务的输出是原子提交的来完成这个特性。每个工作中的任务把它的输出写到私有的临时文件中。每个Reduce任务生成一个这样的文件,而每个Map任务则生成R个这样的文件(一个Reduce任务对应一个文件)。当一个Map任务完成的时,worker发送一个包含R个临时文件名的完成消息给master。如果master从一个已经完成的Map任务再次接收到到一个完成消息,master将忽略这个消息;否则,master将这R个文件的名字记录在数据结构里。

 

 

 

MapReduce编程模型在Google内部成功应用于多个领域。我们把这种成功归结为几个方面:首先,由于MapReduce封装了并行处理、容错处理、数据本地化优化、负载均衡等等技术难点的细节,这使得MapReduce库易于使用。即便对于完全没有并行或者分布式系统开发经验的程序员而言;其次,大量不同类型的问题都可以通过MapReduce简单的解决。比如,MapReduce用于生成Google的网络搜索服务所需要的数据、用来排序、用来数据挖掘、用于机器学习,以及很多其它的系统;第三,我们实现了一个在数千台计算机组成的大型集群上灵活部署运行的MapReduce。这个实现使得有效利用这些丰富的计算资源变得非常简单,因此也适合用来解决Google遇到的其他很多需要大量计算的问题。

 

 

 

 

 

 

Hadoop实际上就是谷歌三宝的开源实现,Hadoop MapReduce对应Google MapReduce,HBase对应BigTable,HDFS对应GFS。HDFS(或GFS)为上层提供高效的非结构化存储服务,HBase(或BigTable)是提供结构化数据服务的分布式数据库,Hadoop MapReduce(或Google MapReduce)是一种并行计算的编程模型,用于作业调度。

目前MapReduce已经有多种实现,除了谷歌自己的实现外,还有著名的hadoop,区别是谷歌是c++,而hadoop是用java。另外斯坦福大学实现了一个在多核/多处理器、共享内存环境内运行的MapReduce,称为Phoenix(介绍),相关的论文发表在07年的HPCA,是当年的最佳论文哦!

版权声明:本文为博主原创文章,未经博主允许不得转载。

google大数据三大论文-中文版-英文版

Google-Bigtable中文版 下载地址: http://pan.baidu.com/s/1eQxmrVc Google-MapReduce中文版_1.0 下载地址: http://...
  • zhangt85
  • zhangt85
  • 2015年01月26日 15:19
  • 14272

Google三大理论(论文)

Google引爆大数据时代的三篇论文谈到Hadoop的起源,就不得不提Google的三驾马车:Google FS、MapReduce、BigTable。虽然Google没有公布这三个产品的源码,但是他...

Google大数据三篇著名论文中文版

Google大数据三篇著名论文中文版

Google MapReduce/GFS/BigTable三大技术的论文中译版

        今天查找分布式计算的有关资料,发现Google的三大核心技术MapReduce、GFS和BigTable的论文都已经被翻译成高质量的中文,更巧的是,这三篇中译版的原发地都是CSDN的B...
  • myan
  • myan
  • 2007年08月04日 22:15
  • 65221

Delphi7高级应用开发随书源码

  • 2003年04月30日 00:00
  • 676KB
  • 下载

Delphi7高级应用开发随书源码

  • 2003年04月30日 00:00
  • 676KB
  • 下载

Google大数据三篇著名论文中文版

http://blog.bizcloudsoft.com/?page_id=61 Google File System中文版 Google Bigtable中文版 Google Ma...

Delphi7高级应用开发随书源码

  • 2003年04月30日 00:00
  • 676KB
  • 下载

谷歌三大论文之the Google File System

The Google File System 中文版 摘要 我们设计并实现了Google文件系统,一个面向分布式数据密集型应用的、可伸缩的分布式文件系统。虽然运行在廉价的日用硬件设备上,但是它依...

Delphi7高级应用开发随书源码

  • 2003年04月30日 00:00
  • 676KB
  • 下载
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:google三大论文
举报原因:
原因补充:

(最多只允许输入30个字)