即将发布的 Stardog 2.1的查询可扩展性提高了约3个数量级,可以在一个1万美元的服务器上处理500亿个triple。我们从来没有过于关注Stardog可扩展性本身:我们首先考虑它的易用性,然后考虑它的速度。我们也只是假定会使它极具可扩展性。Stardog 2.1使查询、数据加载和可扩展性取得了巨大飞跃。
在一个1万美元的服务器硬件(32个内核,256 GB的RAM)上运行Stardog 2.1可以处理200到500亿个triple。相比Stardog 2.0.x,Stardog 2.1加载1亿(左右)triple数据集快了约两倍;加载大约10亿triple数据集快了约3倍——同时占用更少的内存。2.1版本可以以每秒30万个triple的速度加载20B数据集。我们还大大地改进了查询评价的性能,使Stardog 2.1即便在更大的数据库中仍能高速运行。我们是如何做到的呢?
增强并发性
性能的提升大多源于考虑并发性和减少线程争用,尤其是在加载批量数据中(在初始数据库创建时伴随着大量的数据被添加)。我们避免更多的锁,使用更多的非阻塞算法和更多的数据结构。例如从BitSet到ConcurrentLinkedQueue的转变,尽管前者比后者有更多的空间效率,我们还是更愿意使用ThreadLocals减少线程争用和避免同步。当加载性能有所改善时,因为回收进程被附件填满,多个LRU缓存变得不确定了。在单个线程中批处理回收有效果,但也会增加内存压力和GC时间。
糟糕的哈希算法反而更好
Stardog进行哈希处理Uri、bnodes和文本值以存储在dictionary映射中;以前我们用64位 MurmurHash,因为它是非常快、冲突率低,允许我们将数值存储为长整形。当处理冲突和缓存缺失时,需要磁盘的访问权限;在一定规模上访问这些随机磁盘太昂贵了。转移到SHA1也许是并不直观,因为哈希值大小从64位增加到了160位。但因为可以很大程度上避免哈希冲突,我们就能够实现显著的提速——这也明显地简化了dictionary映射。
离堆内存管理
在2.1之前,为了使用操作系统的虚拟机、内存管理等等,我们在加载时积极地使用mmap。但在JVM中的内存映射文件的蹩脚(unmap!)是臭名远播的,而且当我们有很多周围的内存映射文件时,JVM崩溃时有发生。实在是不怎么样。
我们也认识到当有超过64GB的空闲RAM时,内存映射文件会导致性能下降。而且一旦我们失去控制权,内存映射文件被刷新到磁盘的问题就太常见了。但在 Java 堆上大规模保留这一信息显然不可行,因为GC处理机制。Stardog 2.1转向了离堆内存分配方案,这时我们能够非常精细地控制磁盘刷新、更高效地使用空闲的系统内存,几乎和内存映射一样快。
减少GC暂停
最后,要想显著提高加载性能,我们得解决GC开销问题了,这在大数据量加载过程中很重要,因为对象会被频繁地创建和销毁。使用不可变的对象使它能够提高并发性,但同时也伴随着产生附带垃圾的系统开销。使用GC机制并没有起到明显的效果。我们已经通过修改不必要创建对象的地方解决了GC处理成本。那种精细软件工程在相对较小Stardog代码库支持下进行着。类似于机械制造!
精细软件再造工程,例如深入研究使用不断重置单个 StringBuilder的RDF分析器本质,而不是为每个RDF值产生一个新的创建者。我们也减少了在堆上使用高速缓存,以减轻内存的压力。我们现在看到的GC暂停只用了1%甚至更少的整体批量加载时间。
查询评估
如果你连起码的查询评估性能也没有提升的话,那对提高大容量数据加载性也不会有太大作用。因此我们也是这样做的。提高2.1中的查询评价性能的关键是内存使用情况以及如何处理中间结果。思考一下从 SP2B中的SPARQL 查询:
SELECT DISTINCT ?name1 ?name2 WHERE { ?article1 rdf:type bench:Article . ?article2 rdf:type bench:Article . ?article1 dc:creator ?author1 . ?author1 foaf:name ?name1 . ?article2 dc:creator ?author2 . ?author2 foaf:name ?name2 . ?article1 swrc:journal ?journal . ?article2 swrc:journal ?journal FILTER (?name1 < ?name2)}
对于一个500万triple的数据库,这个查询将生成18,362,955(!)个结果,DISINCT将把它们放入内存中。在大规模使用情况下是不可行的。Stardog 2.1通过重用新离堆内存分配计划解决这个问题。一个小堆不能工作时,GC 进程结束它,代之以大堆,所以我们尽力避免堆。当然,这意味着我们必须手动管理内存,但这在JVM中的应用确实是成功的。我们使用了基于各种JVM内件的内存管理器,负责分配(和取消分配)查询评估期间的数据,包括保留中间查询结果。它管理JVM外部的堆,并在需要时流至磁盘。新的内存管理器还执行一些静态分析使用的数据库统计信息以指导其查询操作。
更多详情
新可扩展性的改进不会影响到公共API。要了解Stardog 2.1中其他一些改进,请参阅 此幻灯片。