集算服务器.docx
《集算服务器.docx》由会员分享,可在线阅读,更多相关《集算服务器.docx(20页珍藏版)》请在冰豆网上搜索。
集算服务器
集算服务器:
助力大数据高性能计算
集算服务器
定位与理念
集算服务器是在集算器基础上发展出来的大数据计算引擎。
大数据在技术上的本质是高性能问题,即如何能计算得更快。
常用的方案有内存化、多线程并行、分布式集群等,以及索引、有序利用等技术手段。
而集算服务器即面向结构化数据提供了这些常见的算法类库。
集算服务器是众多大数据计算方案中的一种,它有适合的场景,不可能也不打算解决大数据的所有问题。
集算服务器重点面向如下两类的应用场景:
在线报表查询
这类场景的数据量并不算很大,运算的复杂度也不算高,涉及步骤较少,但常常需要秒级的响应,而且伴有多并发的情况。
随着数据量的增大,简单基于传统数据库的运算机制难以获得即时的响应速度了。
在线报表查询一般需要采用内存化技术来实现。
离线数据准备
这类场景的数据量大得多,涉及大量外存计算,运算的业务逻辑也很复杂,步骤多,但一般没有并发需求。
复杂外存运算很难用SQL和存储过程写出高效代码,开发和运行效率都低,大数据量处理耗时过长,会导致窗口时间不够用而影响下一轮的业务应用。
离线数据准备常常需要引入可横向扩展的并行计算方案。
集算服务器是个技术型产品,它以程序设计语言的形式提供了一些大数据运算需要的类库和方法,其中并没有行业分析模型,需要程序员进一步开发才能形成应用级解决方案。
集算器也没有机器学习和数据挖掘算法,用它写出的某些算法可以理解为数据挖掘的某种应用,但本身算不上数据挖掘产品。
集算服务器将设计计算方案的自由留给程序员,它提供各种计算类库(某些方法甚至相互矛盾而不可同时使用的),程序员可以根据计算任务和数据的特征设计计算方案,这样可能获得最优性能,但会导致透明化程度较低,程序员需要对数据物理存储和运算过程中数据变换都有深入的了解。
集算服务器不是(集群)计算框架,其重点在于提供类库,应用框架和计算流程由程序员决定,无论采用何种框架,这些底层的基础计算方法总是需要的。
特别地,集算服务器几乎没有集群框架,程序员可自由决定每个集群节点的计算任务和数据分布,这样能带来更高性能,但也会导致更多更细致的工作量,因此适合于中小规模的集群甚至单机,而不适合大规模集群(此时需要采用统一的管理方式),从这个意义上讲,集算器是个轻量级大数据解决方案。
Hadoop和SQL
说到大数据技术,就不能不提Hadoop。
集算服务器没有基于流行的Hadoop体系,完全是自己的并行和集群机制。
为什么不用Hadoop呢?
Hadoop的优点很多,但这里主要谈谈其缺点,也就是为什么没采用Hadoop的原因。
Hadoop是个庞大的重型解决方案,虽然软件本身开源免费,但要配置好并把它的众多功能都用好,产生的维护支持成本并不低。
Hadoop产品线丰富,这是好事情,但相互依赖性又进一步提高了维护成本。
Hadoop的设计目标是几百几千的大规模集群,这个规模下随时可能有设备故障,Hadoop投入了相当多资源用于解决容错,这非常必要。
但从这些意义上讲,Hadoop是个高端产品,并不很适合普通用户。
我们希望搞个轻量级的东西,从单机到集群都可以适用,面向几到十几最多几十的中小规模集群,对其它产品和技术的依赖性很小。
这个规模的集群也不需要太强的容错能力,但要强调自由度而换来更高的性能。
Hadoop有明确的框架体系,程序员只能去适应,这样会限制程序员的灵活性,难以写出适应业务和数据特征的代码。
比如想控制HDFS的文件冗余方案,我们后面会谈到特定的冗余能有效地减少网络传输量,这也许能够通过修改源码实现,但并不轻松,而且随意修改源码会影响升级。
再比如MapReduce为了容错把任务拆得太碎,且无法直接控制执行次序,这样导致许多序运算描述困难。
集算服务器的思路则是提供类库,无论程序员要怎么做,总需要一些底层的基础方法,那么我们就写出来让程序员去调用。
集算器几乎没有框架,流程由程序员用代码控制,不象MapReduce那样只要填空,这样可以充分利用业务和数据特征去编码。
Hadoop是个相对封闭完整的体系,要应用它的计算方案需要先数据置于Hadoop之内,Hadoop不能计算关系数据库或其它网络文件系统中的数据;而集算服务器则是个单纯的计算方案,相对来讲更为开放,可以计算包括HDFS之内的各种数据,作为纯Java产品,也可以被Hadoop的技术方案集成。
MPP技术让SQL在大数据时代再次找到了机会。
SQL语法的透明程度更好,程序员无须关心数据物理存储方案,但过于自动化的方案也会导致难以根据数据和任务的特征优化控制计算过程的细节。
集算服务器没有提供SQL风格的语法,需要程序员了解数据存储方案才能写出正确高效代码,好处是可以根据任务特征局部计算的每个步骤,充分利用硬件资源。
做SQL解决方案的厂商很多,有些已经做得很好,包括Hadoop阵营内也有几家,能在SQL机制下顺利解决的问题(难度和性能)已经有足够多成熟方案供选择。
但是,大家都知道,有许多运算用SQL并不好写,还要自己写很多代码,而这些事也涉及大数据和高性能,也需要并行和集群技术的支持,集算器的定位主要是来协助这类任务。
当然,集算器连接SQL数据库取出数据做运算是没有问题的,这样可以与MPPSQL配合工作,共同解决大数据性能问题。
内存化
简单来讲,内存化就是数据事先读入内存,利用内存的高速获得更高的计算性能。
内存计算是近年来的热门技术,现代计算机的内存已经可以做到几十几百G甚至上T的容量,这使得某些应用的全部数据被加载都成为可能。
内存计算特别适合在线报表查询业务。
事实上,除了用索引检索特定数据外,所有遍历式的计算任务,如果涉及数据量超过现代计算机的内存容量的话,基于外存也就不可能做到即时响应了。
外键指针化
内存不仅有更高的访问性能,更关键的是能很好地支持随机并发读取,即可以在常数时间内随意地访问任何地址的小段数据。
利用这个特征,可以实现快速外键连接计算。
在数据初始化加载进内存时,我们可以把数据之间的外键式关联事先转换成内存指针。
比如,这里有简化过的超市商品列表和购买记录表。
商品列表:
编号、名称、厂商、类别、单价
销售记录:
序号、时刻、商品编号、数量
销售记录中的商品编号是指向商品列表的外键,这是常见的数据结构。
现在我们需要计算销售金额,针对每一条销售记录,用其商品编号到商品列表中查出单价,乘以销售记录中的数量,然后合计这些乘积。
这样的算法需要执行大量(销售记录数量次)在商品列表中查寻单价的动作,如果是遍历查找,整个复杂度就相当高,可以在商品列表上建立索引提高查寻性能,但还是比较慢。
现代数据库一般采用HASH算法将两表键值对齐后再计算,算法要快许多,但实现很麻烦,而且仍然需要许多次的HASH计算和比较。
如果我们在加载数据之后把销售记录中的商品编号转换成指向商品记录的指针,那么计算时就不需要这些比较动作而可以直接访问到商品单价,查寻时间被消除了。
而转换成指针的过程中虽然仍需要这些查寻动作,但这是一次性的,一旦指针建立好了,以后的计算就可以高速进行了。
集算器提供了指针化建立和访问的机制,以文件数据源为例,上述指针化和计算代码是这样:
A
1
=file(“Products.txt”).import()
读入商品列表
2
=file(“Sales.txt”).import()
读入销售记录
3
>A2.switch(productid,A1:
id)
建立指针式连接,把商品编号转换成指针
4
=A2.sum(quantity*productid.price)
计算销售金额,用指针方式引用商品单价
实际代码中还有多任务共享这些加载数据的需求,写法会略有不同。
实际测试也表明这个结论。
我们设计了两个测试,先做一个没有连接运算的分组汇总,然后再做一个五表多层外键连接的分组汇总,数据规模相当,且都能装入内存。
用集算器和Oracle各做一遍,结果是这样的:
集算器
Oracle
单表无连接
0.57s
0.623s
五表外键连接
2.3s
5.1s
可以看出,在没有外键连接时,集算器与Oracle性能相差不大,但涉及了连接运算后,集算器比Oracle快了一倍多。
如果仅仅简单地把数据读入内存而不做指针化处理,那就无法享受这个性能优势了。
顺便说一句,有些仍然使用SQL语法的内存数据库产品也无法利用这一特点,SQL模型是面向外存运算的,不支持指针化记录。
外键序号化
有外键连接的计算,当数据量大到无法全部装入内存时(已不适合在线报表查询了),可以选择只将部分数据加载进内存。
还是上面的例子,相对于不断增长的销售记录,商品列表一般会小而且增长慢。
也就是说,在一套数据中,被外键指向的维表相对较小,而事实数据则要大得多。
这时我们有可能可以把维表全部加载进内存,事实数据放在外存遍历访问。
维表经常在多个任务都会使用,读入内存后可建立可复用的索引,人为干预内存化的数据能减少外存访问量。
上述运算在只将维表内存化时的写法将改成:
A
1
=file(“Products.txt”).import().primary@i(id)
读入商品列表并建立索引
2
=file(“Sales.txt”).cursor()
建立销售记录游标,准备遍历
3
=A2.switch(productid,A1:
id)
在游标上建立连接指针,准备遍历
4
=A3.groups(;sum(quantity*productid.price))
遍历计算销售金额,仍可用指针引用
但是,这时候不能事先建好的指针式连接,需要在遍历过程中临时建立。
集算器在这里也是采用HASH算法找寻匹配记录,但显然仍然会比前述全内存方案的性能要差。
为了提高生成连接指针的性能,我们还可以尝试使用序号外键,即把外键的键值事先改造成整数序号。
这个工作本身较为烦琐也很耗时,但它是一次性的,一旦准备完成后,后续计算中外键指向可以直接使用序号定位,不需要计算和比较HASH值。
这种方法可以使只有维表内存化时仍然能够获得指针式外键的高性能。
A
1
=file(“Products.txt”).import()
读入商品列表
2
=file(“Sales.txt”).cursor()
根据已序号化的销售记录建立游标
3
=A2.switch(productid,A1:
#)
用序号定位建立连接指针,准备遍历
4
=A3.groups(;sum(quantity*productid.price))
计算结果
需要注意的是,序号化外键后维表不能再随意插入删除数据,否则外键指向将发生混乱,这种方式适合计算不再改变的历史数据,而针对正在改变的当期数据操作时会有较大的风险或面临复杂的管理。
传统数据库机制下所有表都是逻辑等同的,不区分维表和事实表,在JOIN运算时将自动决定哪个表加载进内存,一般也是较小的表,在单次运算时和集算器的效果区别不大,但多次任务时这些数据就可能被反复加载并建立HASH索引而影响性能。
另外,数据库的记录没有次序,即使人为把外键序号化,数据库仍然会使用HASH算法去定位,事先准备数据并不能起到提高性能的作用。
内存利用率
Java对象会存储很多非数据的管理信息,外存数据读入内存对象化后大约会多占3-5倍的空间(以字节计),而Java程序只能处理对象化数据,这导致了内存不能有效利用。
集算服务器提供了设计一种用时间换空间的手段。
将外存数据以字节数组的方式紧致地读入内存(这时占用的空间和外存相差不大),在引用到某个数据之前再临时将其对象化。
这种内存化机制将大幅提高内存的使用率,其代价是引用到尚未对象化的数据时需要增加对象化时间,但能省去读外存的时间,毕竟硬盘访问速度还是慢得多;而且还能继续支持随机访问。
显然这没有全内存对象的性能好,但与外存运算相比仍有较大优势。
集算服务器提供的字节表方案可以实现这种内存化机制,上面包括序号化在内的指针式高效引用也能被保留。
A
1
=file(“Products.dat”).create().primary@i(id)
根据文件创建字节表并建索引
2
=file(“Sales.dat”).cursor@b()
建立销售记录游标,准备遍历
3
=A2.switch(productid,A1)
和普通内存表一样使用
4
=A3.groups(;sum(quantity*productid.price))
计算方案也一样
对字节表也可以采用序号化外键的方案。
与上面采用文本文件不同,字节表必须基于下面讲到的二进制文件创建。
多线程并行
采用多线程并行计算能有效发挥现代计算机的多个及多核CPU的效能,集算器提供了非常简便的多线程语法,配合内存游标手段,可以很轻松地编写并行计算。
A
B
1
=file(“Sales.dat”).create()
源数据读入成字节表
2
fork4
=A1.cursor(A2:
4)
分作4段并行,分别建立内存游标
3
=B2.groups(;sum(amount):
a)
遍历游标计算amount之和
4
=A2.conj().sum(a)
汇总每个线程的结果
其中fork语句即用来启动多线程执行后续的代码并收集每个线程的结果。
使用游标会逐步取用需要的数据,避免一次性将所有记录取出占用过多内存。
游标不仅适用于字节表,也适合用已对象化的普通内存表。
与Java和C++等更基础的程序设计语言不同,集算器并未提供多线程之间共享资源抢占与同步的机制。
集算器认为各线程在执行过程中无关,只是同时启动并最后汇总结果。
显然,这种机制不可能写出任意的并行算法,特别是实时逻辑,但对于数据分析和准备工作已基本够用。
集算器牺牲了一些不太必要的能力换来易用性的大幅提高。
并行并非越多越好,原则上不能超过CPU数量(核数),否则再多的线程都只能是逻辑上的,不会真正并行执行。
集算器还提供了某些函数的内置并行选项,可以更简单地实施并行计算。
A
1
=file(“Sales.txt”).import()
取出数据
2
=A1.select@m(amount>1000)
并行过滤
3
=A2.sort@m(amount:
-1)
并行排序
@m选项将自动根据当前配置决定并行的线程数量。
需要注意的是,使用并行选项时无法保证记录的取用次序,在涉及有序计算时不可使用。
外存计算
大数据计算离不开外存计算。
事实上计算并不能直接在外存进行,所谓外存计算是指每次只读入少量数据,分多次计算完成,过程中产生的中间结果过大还要缓存到外存。
外存设备一般就是硬盘,对硬盘上计算的优化方向主要为:
尽量减少对硬盘的访问,权衡用CPU换取硬盘时间;尽量顺序读取,硬盘只能批量访问数据(扇区或更大单位);对于机械硬盘,还要权衡并行访问量,因为寻道时间较长。
分段并行
文本是常见的外存数据源,文本读入内存时需要解析成相应的数据类型后才能运算,这个过程很慢,有时CPU时间甚至会超过硬盘访问时间,采用多线程并行就能有效地提高性能。
并行处理需要将源文件分段,每个线程处理其中一段。
文本文件一般是每一行对应一条记录,简单按字节分段时有可能造成一行被拆进两段,这会导致计算出错。
而按行分段需要从头遍历,完全起不到提高性能的目标。
集算器提供了自动去头补尾的字节分段机制,即段开头的行舍弃,段结束的行补齐,这样确保每一段都由完整行构成,不会导致数据错误。
配合前述集算器的并行机制,可以很方便地写出并行计算程序。
A
B
1
=file(“data.txt”)
源文件
2
fork4
=A1.cursor@t(amount;A2:
4)
分作4段并行,分别建立游标
3
=B2.groups(;sum(amount):
a)
遍历游标计算amount之和
4
=A2.conj().sum(a)
汇总每个线程的结果
上述代码把data.txt分作4段,产生4个线程分别用游标遍历每一分段并计算其中amount列的和,最后再汇总每段的返回值得到整个文件的amount列总和。
有时候我们会发现,文本解析的时间比计算要长得多,只要解析能够并行实现,计算本身是否并行并不重要。
集算器对于读取数据也提供了内置并行的选项,如果对数据读取次序不关心(比如上述的求和就不在乎次序),可以更简单地完成运算。
A
1
=file(“data.txt”).cursor@tm(amount)
定义并行取数的游标
2
=A1.groups(;sum(amount))._1
遍历游标并汇总amount列
上面代码中,游标取数时会自动启动多线程并行,但计算amount合计时是串行的。
并行数量不仅受CPU数量的限制,对于外存运算还将受硬盘的限制。
如果是机械硬盘,并行过多会导致硬盘寻道时间过长,这时要为每个线程设置更大的数据缓冲区来缓冲,又会造成内存的占用。
集算服务器提供了系统设置,方便在实际应用时权衡这些因素以获得最佳性能。
集算器提供了多线程之间动态平衡负担的功能,某线程结束时发现还有未处理的任务时会继续获取一个任务去处理。
由于每一任务的实际处理时间不可预计,这种动态平衡的机制会更有效地利用机器的计算能力。
数据库表的分段远没有文件自由,不是很适合进行分段计算,对于数据密集型的任务最好还是由数据库自行完成。
有些计算密集型的任务,复杂的计算在数据库内实现难度太大,需要读出到外部实现,这时候原则上也可以采用上面的分段并行机制。
一种办法是直接建立多个分表,每个线程分别处理若干个分表的数据,同一个分表不能再拆分给多个线程处理,这种方案下要拆分出较多分表才能让各线程负担相对均匀,而在数据库建立的表太多并不是个好设计,所以一般会让线程数和分表数基本相同,这会导致并行数被事先确定,较为死板。
使用WHERE条件分段则需要先建立索引,否则每个WHERE都会导致全表遍历,起不到性能优化的作用,有时即使建立索引也不一定能提高性能,还取决于记录的物理存储是否和索引一样连续。
Oracle等数据库的JDBC很慢,读出计算时JDBC会成为一个瓶颈。
如果数据库本身负担不重时,这时也可以采用分段并行的方法取数,从而缓解JDBC的性能损失。
A
B
1
fork4
=connect(db)
分4线程,要分别建立连接
2
=B1.query@x(“select*fromTwherepart=?
”,A2)
分别取每一段
3
=A1.conj()
合并结果
实测表明,并行取数在数据库负担不重时能达到数倍的性能提升。
数据存储
除了文本文件外,集算器还支持自有格式的二进制文件。
集算器的二进制文件中已经记录了数据类型,在读出时不需要再解析,这样将获得比文本好得多的性能。
而且,集算器为二进制文件做了压缩,同样数据占用的硬盘空间一般能比文本要小三分之一到一半,读取性能会更好。
当然,压缩比并非越高越好,解压缩会占用CPU时间,压缩比越高的算法占用CPU时间越长,集算器的压缩算法非常简单,在获得这个压缩比时几乎不多占用CPU时间。
如果文本数据将被反复使用,将其换成二进制格式将有更大优势。
转换程序非常简单:
A
1
=file(“data.txt”).cursor@t()
定义文本文件游标
2
=file(“data.bin”).export@z(A1)
写成可分段的二进制文件
集算器的二进制文件也支持分段,代码与文本文件几乎相同:
A
B
1
=file(“data.bin”)
源文件
2
fork4
=A1.cursor@b(amount;A2:
4)
@b表示二进制文件,其它参数相同
3
=B2.groups(;sum(amount):
a)
后续计算语法完全相同
4
=A2.conj().sum(a)
基于二进制文件,集算器可以提供列式存储方案,即将每一列存储成一个文件。
许多数运算只涉及较少的列,这样只需要读取较少量数据就可以完成运算,硬盘访问时间将大大缩短,这是列存的优势。
A
1
=file(“data.txt”).cursor@t()
原文本文件
2
=10.(file(“col”/~/”.bin”))
产生10个列的对应存储文件
3
>A2.export@z(A1)
将原数据写成可分段的多个列存文件
A
1
=[1,3].(file(“col”/~/”.bin”))
使用第1,3列
2
=A1.cursor()
定义列存游标
3
=A2.groups(;sum(col1+col3):
all)._1
使用游标计算合计
集算器的列存文件还支持分段并行,和行存文件的相关代码写法一样,集算器的文件格式在获得压缩比的同时还能确保多个列数据文件同步分段。
1
=[1,3].(file(“col”/~/”.bin”))
定义列存文件组
2
fork4
=A1.cursor(amount;A2:
4)
并行和游标代码与行存相同
3
=B2.groups(;sum(amount):
a)
后续计算语法完全相同
4
=A2.conj().sum(a)
不过,列存并不是总有效,如果读取的列较多时,对于机械硬盘又会产生寻道时间与缓冲区容量之间的严重矛盾。
而且,多个列文件再加上多线程并行会进一步恶化硬盘并发问题,这种方案已不适合普通机械硬盘,应当在有固态硬盘或可高并发磁盘阵列的场景下使用。
这里可以再次感受集算器的思路:
提供可能的基础支持,但不会主动替程序员决定。
特别地,集算器提供对列存的支持,但不会自动把数据处理成列存,也允许列存和行存共存,由程序员根据实际情况决定是否要采用列存以及列存的使用范围。
利用有序
行业业务中的数据对象经常有非常多的字段(几百甚至上千),在数据库中表现为宽表,由于各字段的丰满度和使用频度不同,大多数情况下会把字段分类存放在多个主键相同的物理表中,引用字段时再取出其中若干表做JOIN。
数据库厂商一般会自动在主键上建索引,相当于把这些表按主键排序,然后再JOIN时就变成了有序归并算法,复杂度要比普通JOIN低得多得多。
集算器针对文件也提供了类似的方法,几个已经有序的游标可以用低复杂度的归并算法被横向JOIN起来,类似地方法还可以用于执行有序集合的交并差等运算。
事先将要反复使用的原始数据排序后存储,再计算时就能获得更高的性能。
可以在准备数据时充分利用这一点。
事实上,准备数据本身的过程中也可以使用有序归并,已有数据是有序的,只要把新加入的数据排序后和已有数据一起做个归并就可以,而不必把所有历史数据重新排序。
数据量特别大也不必每次和历史数据合并,只将一段时间内的新数据排序另存,在使用时再和有序的历史数据一起归并。
归并算法的成本很低,只要归并段数不是非常多,使用时归并对总体性能并没有太大的影响。
除了事先准备的数据,我们有时也会明确知道某些数据已经有序,比如集算器返回的分组汇总结果缺省是按键值有序的,大分组结果本来也是针对有序小结果集归并出来的,这个结果仍然有序,可以继续使用归并算法。
和列式存储类似,有序文件也只能顺序归并,不能同步分段。
这样,希望并行时也需要象列存那样事先人为做好分段,当然也可以用同样的办法平衡各线程的负担。
用户行为分析(或帐户统计)是常见的数据处理任务。
单个用户(帐户)内的运算非常复杂,但跨用户的运算几乎没有。
过于复杂的运算使用SQL直接在数