Upload
others
View
38
Download
0
Embed Size (px)
Citation preview
By Fanghua Yu, Dec. 2017
APOC Java存储过程库实现复杂和高性能的图遍历
Fanghua(Joshua) YuField Engineering, APAC.
高级应用技术专题系列
俞方桦, 博士2730625048, QQ群: Neo4j中文社区Neo4j中文社区:neo4j.com.cn, 用户: GraphWay
https://www.linkedin.com/in/joshuayu/
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
3、实现高效的数据写入:iterate()
Neo4j之所以能够极为高效地遍历关系,是因为它在存储数据时同时也存储了数据间的关系。
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
测试准备本篇中所有实例均基于stackoverflow开放的在线技术文章导出数据。
✓ 数据类型:用户,帖子,分类标签;以及相互之间的关联关系
✓ 数据规模:~3100万节点,7800万关系,2.6亿属性
本篇将主要介绍如何使用apoc存储过程包中的iterate()来加速数据写入。
关于stackoverflow数据的下载、导入,以及APOC存储过程包的安装,请参见本系列的第2篇 –路径扩展过程expand()。
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
是不是总想抱怨,“写入Neo4j的速度怎么这么慢,这么慢,这么慢?!”
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
优化数据库写入的策略数据库的写入速度是有多种因素决定的。本篇将选择一个实例、采用下面的步骤,来说明怎样在有限的硬件之上最大化写入速度。
1) 硬件的选择
3) 理解更新的执行
5) 并行处理
6) 优化查询语句2) 监控系统状况
7) 其他因素
4) 估计写入量
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
测试用系统本篇中的所有测试结果均在本人使用的普通笔记本上得到。
系统配置:
▪ Lenovo ideapad 510
▪ Intel i-7 7500 CPU, 4 cores
▪ 12GB DDR4 RAM
▪ Seagate 2TB SATA 2 机械硬盘
▪ Windows 10 专业版
另外,还有一个Samsung 256GB SSD 外接硬盘,通过USB 3.0连接在笔记本上。
测试环境和配置:
▪ Neo4j 企业版3.3.1
▪ 数据库:16.5GB
▪ Java页缓存:4GB
▪ Java堆缓存:最大4GB
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
感觉用来测试的系统并不怎么高大上啊。。。?
本篇的目的并不是要获得最快的写入速度,而是仅使用基本的工具、在普通的硬件上,通过对测试结果的估算和分析来优化写入性能。
咋还搞出来个移动硬盘做啥哩?
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
硬件的考虑写入操作的主要决定因素是硬盘的读写速度。在CPU、内存相当的情况下,硬盘读写,尤其是随机读写速度将决定数据库写入的性能。所以,有必要先测试一下硬盘的性能。
本地机械硬盘的测试结果 USB3.0 外接SSD硬盘的测试结果
顺序读写的性能外接SSD
是本地机械硬盘的2倍
随机读写的性能外接SSD
是本地机械硬盘的15~150倍不等!
注:硬盘读写测试工具用的是CrystalDiskMark 64 v6
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
硬件的考虑(续)由于无需移动读写臂的机械操作,SSD硬盘在随机读写中的性能测试结果远远优于机械硬盘。这在后面的测试结果中会进一步得到证实。
其他因素:
▪ 本地硬盘只有一个物理硬盘,系统、Neo4j和数据只是分布在不同逻辑分区中
▪ USB3.0可以达到640MB/s的数据传输量,是USB2.0的10倍多
▪ SSD对并行写入的支持更好
▪ 对数据库的插入/更新多数时候是随机写入,而从目录到目录的复制可以做到更加序列化的写入
下面的测试结果,如果没有特别说明,都是将数据库存放在外接SSD硬盘上、在本地硬盘运行Neo4j得到的。
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
监控系统运行状况在测试过程中需要对CPU、内存和硬盘的使用情况进行监控,以做事后的分析。本篇中主要使用Windows任务管理器,以及JConsole、JDK自带的JMX客户端。
在Neo4j中配置JMX请参考以下文档:
- Neo4j Configuration
https://neo4j.com/docs/java-reference/current/jmx-metrics/
在Windows中设置jmx.passoword文件的独享访问权限:
https://docs.oracle.com/javase/8/docs/technotes/guides/management/security-
windows.html
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
监控系统运行状况(续)
任务管理器中,硬盘的实际读写速度是我们要关注的。
JConsole中堆内存使用情况是重要的参数
线程数在并行执行测试中会用到
CPU使用率通常不会很高,但是也要关注。
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
模型和测试用例Stackoverflow的数据模型如右图所示。
我们将要用来测试数据库写入的查询语句也很简单:
基本上对每篇文章(Post),通过POSTED
关系找到发贴用户名,然后将用户名写入文章的新属性postedBy。
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
模型和测试用例这么简单?那还等什么?执行就行了啊!
通过Neo4j的计数器(Count Store),我们知道大概有2千6百万个Post节点、4百50万用户节点。这意味着要遍历2千6百万个节点和同样数量的关系,然后更新每一个节点。在不清楚直接更新所有节点对数据库造成的影响前,最好先取个样本测试一下。更何况我们的测试系统并不怎么“高大上”。
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
理解数据更新的执行我们先试试每次更新1百万个节点:
在执行时记录系统资源的使用情况:
▪ CPU
▪ 内存
▪ 硬盘的实际读写速度
为减少浏览器的影响,查询使用cypher-shell命令行执行。
通过对Post节点的内部id进行限制,来确保每次只更新1百万个节点。
由于Neo4j对内部id会自动建立索引,这样的过滤条件对整个查询带来的额外开销微乎其微。
Neo4j的计数器(Count Store)使得下面的查询可以总是立即返回结果,无论数据库的规模有多大。
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
测试用例#2.1 用Cypher查询更新节点-1M
实际更新节点数 943K
耗时(秒) 46.5
写入性能(节点/秒) 20279
CPU使用 <25%
Java Heap使用(MB) <750
系统硬盘使用率* <30%
数据库硬盘读/写(MB/s) 25/10
* 系统硬盘是笔记本的机械硬盘。OS和Neo4j在上面运行。
理解数据更新的执行(续)
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
有趣的是,如果立即再次执行同样的Cypher查询,使用的时间大概只有9秒。然而,根据对数据库硬盘的监控,发现没有任何真正的写入操作。这应该是Neo4j在写入前先在内存中对属性现有的值和新值进行了比较,只有在结果不同时才写入。考虑到写入物理存储器的开销,这样的优化是有必要的。
为了保证测试结果的独立性,在接下来的测试中:
- 每次都会移动id的范围,使得每次更新的Post节点都是以前没有更新过的。这是因为过去查询过的节点会在Java页缓存中保留;
- 当所有2千6百万个Post节点都已经被更新过后,修改要被创建的属性名,例如postedBy1,
postedBy2等等;
- 当数据库大小的增长超过50%后,删除当前数据库,恢复一个刚导入数据时的备份并重新开始;
- Neo4j启动后Java堆内存大致会被立即占用400MB,在前一次测试结束后如果堆内存没有恢复到这个值或更小,重新启动Neo4j服务后再做新的测试。
理解数据更新的执行(续)
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
测试用例#2.2 用Cypher查询更新节点-1.5M
实际更新节点数 1.49M
耗时(秒) 58
写入性能(节点/秒) 25657
CPU使用 <25%
Java Heap使用(MB) 3500
系统硬盘使用率 <20%
数据库硬盘读/写(MB/s) 25/10
在更新1百50万个节点时,堆内存已经接近4GB的上限了。
堆内存保存用于回滚(roll-back)的交易记录。这里,似乎随着要更新的节点数增加,堆内存的消耗也迅速增加。
理解数据更新的执行(续)
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
测试用例#2.3 用Cypher查询更新节点-2M,失败
的确,在试图更新2百万个节点时,发生了OutOfMemory错误。也就是说,对于这个更新操作,Neo4j需要大约2.5GB堆内存来保留每1百万节点的交易日志。
CPU的使用率
是不是除了增加堆内存就没有其他办法了?是不是需要至少65GB堆内存才能一次性更新2千6百万的节点?否
则只能分批处理?
是不是?是不是??
是不是???
理解数据更新的执行(续)
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
还记得他吗?我们的“程序员”朋友,APOC?
在电影中,他没有Neo和Cypher那样重要的角色和上镜率,但是这里,他是真正的救星。
我们要使用的apoc过程是iterate()。它的作用是能够将一个大的数据库更新操作分解成等量的批次依次执行。下面是例子:
第一个参数是Cypher查询,返回要更新的节点集合。
第二个参数是另一个Cypher查询,从返回的节点中依次执行更新操作。
每次更新的节点数/批次大小。
是否并行执行。 是否将整个批次包括在一个交易中执行。
了解apoc.periodic.iterate()
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
估计写入量测试用例#3.1 用iterate更新节点,单进程
batchSize 2000
parallel false
iterateList false
更新节点总数 1M
耗时(秒) #1=1402, #2 = 1332
写入性能(节点/秒) #1 = 713, #2 = 750
CPU使用 <25%
Java Heap使用(MB) <750
系统硬盘使用率* <20%
数据库硬盘读/写(MB/s) -/-
iterateList为false
时,iterate()会将每一个节点的更新作为一个交易,其总体性能还不如Cypher语句。所以,始终将这个变量设成true。
batchSize大致在1k到10k之间。我们先以2k作为开始。
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
估计写入量(续)测试用例#3.2 ~ 3.6 决定最佳的批量大小(batchSize)
测试用例编号 3.2 3.3 3.4 3.5 3.6
batchSize 2000 200 10k 15k 20k
更新节点总数 1M 1M 1M 1M 1M
平均耗时(秒) 38 47 28 25 36
写入性能(节点/秒) 26315 21280 35714 40000 27778
CPU使用 <25% <30% <40% <50% <50%
Java Heap使用(MB) <900 <900 <900 <2400 <2400
系统硬盘使用率* <30% <30% <40% <40% <40%
数据库硬盘读/写(MB/s) -/10~18 -/10 -/30 -/32 -/32
以下测试都设置parallel = false, iterateList = true,唯一改变的是batchSize。
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
估计写入量(续)JVM资源使用情况
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
估计写入量(续)前面的硬盘读写测试结果显示随机读写的速度在26~30MB/秒的范围。batchSize
的大小决定了数据库一次性提交更新的规模。在更新总数是1百万节点的前提下,从测试结果可以看出:
▪ 批次的规模越大,总批次越少,提交的次数也越少;
▪ 批次规模从2000到15k,总体写入时间降低,但是并不明显,大约有17%的改进;
▪ 当批次规模过大(在20k时),性能反而下降19%,这是因为硬盘的写入速度有限,过多的写入请求造成响应延迟;
▪ 批次规模减少到200,总批次增加到5000,总体写入时间增加59%。
在batchSize为2000时,硬盘写入的峰值已经达到18MB(测试峰值的60%),为了给其他写入操作留有足够处理能力,我们在后面的测试中就继续使用这一设置。
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
并行处理测试用例#4.1 使用iterate()的并行处理
batchSize 500
parallel true, 4核
iterateList true
更新节点总数 1M
平均耗时(秒) 18.2
写入性能(节点/秒) 54945
CPU使用 <30%
Java Heap使用(MB) <800
系统硬盘使用率* <20%
数据库硬盘读/写(MB/s) -/41
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
并行处理(续)测试用例#5 并行处理,超过1百万节点的更新
batchSize 500
parallel true, 4核
iterateList true
更新节点总数 2M 4M 10M 15M
平均耗时(秒) 43 117 290 403
写入性能(节点/秒) 46511 34188 34482 37220
CPU使用 <55% <55% <55% <55%
Java Heap使用(MB) <900 <900 <900 <900
系统硬盘使用率* <20% <20% <20% <20%
数据库硬盘读/写(MB/s) -/45 max -/46 max -/60 max -/90 max
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
并行处理(续)使用apoc的iterate()过程进行数据库更新具有的优点:
▪ 并行处理(只要能够避免竞争性写入)
▪ 更少的堆内存需求
▪ 更高效率
0
10000
20000
30000
40000
50000
60000
Cypher iterate - 无并行 iterate - 并行 iterate - 2M更新 iterate - 4M更新 iterate - 10M更新 iterate - 15M更新
更新速度(节点/秒)
更新速度(节点/秒)
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
优化查询语句
即第一个查询返回节点Post和User对象,而不只是Post的id,结果会怎么样呢?
在此之前,我们一直用下面的Cypher语句实现数据库节点更新:
如果换成更常用的写法:
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
优化查询语句(续)测试用例#6 使用返回的节点进行更新,其他设置与#5中一致。
batchSize 500
parallel true, 4核
iterateList true
更新节点总数 1M 2M 4M 8M
平均耗时(秒) 34 67 121 219
写入性能(节点/秒) 29411 29850 33057 36529
CPU使用 <55% <60% <80% <89%
Java Heap使用(MB) <1400 <2200 <1800* <2400
系统硬盘使用率* <20% <20% <20% <20%
数据库硬盘读/写(MB/s) -/- -/- -/- -/-
* 这里重启了数据库服务。
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
优化查询语句(续)测试用例#6 使用返回的节点进行更新,性能略有降低,但是CPU和堆内存的使用要明显高于用例#5。
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
优化查询语句(续)测试用例#7.1 好了,现在可以进行全库更新了。继续使用#5中的参数,应该是小菜一碟了。。。只要去掉查询中对 id的筛选条件就行了。
去喝杯奶,休息休息。。。
回来一看屏幕。。。
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
Java OutOfMemory Error !!!!!!!
不是说apoc的iterate()没有这个问问问题吗?
前面的测试中1千5百万节点也没发觉内存使用增加啊???
是不是说前面的测试都白做了?
怎么办?怎么办??怎么办???
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
优化查询语句(续)
在Neo4j数据库中,内部产生的节点id都是正数,所以上面的筛选条件什么作用都没起。但是,全库更新可以继续了。
好吧,我也不知道。。。
既然有筛选条件时可以运行,那只好把它加上去了。不过,得加得聪明些:
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
优化查询语句(续)测试用例#7.2 使用修改过的全库更新查询,其他设置与#5中一致。
batchSize 500
parallel true, 4核
iterateList true
更新节点总数 26,545,725
耗时(秒) 1006
写入性能(节点/秒) 26387
CPU使用 <42%
Java Heap使用(MB) <1800
系统硬盘使用率* <20%
数据库硬盘读/写(MB/s) -/-
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
终于、终于完成了!
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
总结▪ 数据库写入性能的关键是硬盘的读写速度
- SSD远远胜过机械硬盘:* 在使用本地硬盘的测试中,使用单进程的写入性能在7700节点/秒左右
- 即使外接的SSD也胜过本地的机械硬盘* 在使用外接SSD硬盘的测试中,写入性能在 25000节点/秒以上,最快达到
55000节点/秒- SSD可以更好地支持并行的随机写入,而机械硬盘在并行的情况下写入性能反
而大大降低(只有2100节点/秒)
- 将系统盘和数据库盘在物理上分开- 如果服务器和客户端在同一台机器,那么使用cypher-shell,而不是Neo4j浏览
器可以进一步减少系统资源消耗。建议把Neo4j浏览器也退出。
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
总结(续)▪ 使用Cypher进行类似全库的更新需要预留足够大的Java堆内存(Heap
memory),大概是每更新1百万个节点需要2.5 ~ 3GB。▪ 使用apoc.periodic.iterate()将大的更新任务分成小的批量执行:
- iterateList永远设成true
- batchSize根据存储媒介的随机写入速度和单次更新的数据量决定,通常在1000到10k之间
- 如果是SSD存储器,使用并行处理:parallel = true,并根据CPU内核数调整batchSize(做除法,你懂的!)
▪ 调整查询逻辑- 在iterate()的第一个查询中返回尽可能简单的数据,只要不影响第二个更新操
作的性能▪ 最后的杀手锏来对付Java OutOfMemory错误:加一个不起作用的WHERE
By Fanghua Yu, Dec. 2017
高级应用技术专题系列
感谢阅读!欢迎提出问题、意见和建议。
Neo4j中文社区: http://neo4j.com.cn
QQ群:Neo4j中文社区 / 547190638
个人QQ号:Neo4j-APAC技术支持 / 2730625048
如果你实现了更快的节点更新性能,请一定告诉我!