在之前的文章中,介绍了如何搭建neo4j集群,集群的故障切换和节点恢复,还介绍了如何配置驱动实现自动failover。本文聚焦在neo4j的数据上。聊聊如何从外部数据源将数据导入neo4j,如何对neo4j数据库进行备份和恢复。
数据导入
可以有多种方式将数据导入neo4j。大致分为cypher语句导入、apoc.load函数过程导入和离线import导入。
cypher语句导入
这个最简单,就是直接使用cypher语句的create或merge命令来创建neo4j的节点、边和属性。对比来说,create性能较好,但其不会进行存在与否的检查,可能会导致数据库中出现2个相同的节点等情况,造成数据不一致。merge虽性能较差,但功能强大很多。在导入场景,merge一般与on create和on match配合使用,导入语句形如:
MERGE (keanu:Person { name: 'Keanu Reeves' })
ON CREATE SET keanu.created = timestamp()
ON MATCH SET keanu.lastSeen = timestamp()
RETURN keanu.name, keanu.created, keanu.lastSeen
apoc.load函数过程
apoc是neo4j的一个插件(plugin),需要额外安装。可以从github neo4j-apoc-procedures上下载apoc的jar包,放入neo4j对应的plugins目录下,设置如下参数后重启neo4j进程后即可使用。
dbms.security.procedures.unrestricted=apoc.*
apoc.import.file.enabled=true
apoc的功能很强大,其使用文档可通过该链接查看。本文主要关心导入部分。 apoc支持导入json和csv两种格式的数据文件。数据文件内部格式可分为2种,对于csv格式,还分带schema行的(或者成为文件头)和不带schema的。apoc.load均可处理。可参考Neo4j APOC 使用做进一步了解。在此不详说。
离线import
使用neo4j-admin import,输入文件为csv格式。这是性能最好的导数据方式,但其是离线的,也就是neo4j进程关闭状态。不支持增量导数据,即图数据库中已存在的数据会被删除(或被重命名为其他数据库)。所以,import工具一般用于图数据库初始化后,全量导数据。 import工具有较大的灵活性,通过增加配置参数来实现多种不同的导入功能。详细介绍可参考import-tool,下面就其中几点展开说明下。
节点的格式和配置
首先来讨论下节点的设置。--nodes[:Label1:Label2]=<"file1,file2,...">,这是节点设置的选项。file可以是单独的schema头,也可以是schema头和数据在一起的文件,还可以将数据文件拆分为多个file。
movieId:ID,title,year:int,:LABEL
tt0133093,"The Matrix",1999,Movie
tt0234215,"The Matrix Reloaded",2003,Movie;Sequel
tt0242653,"The Matrix Revolutions",2003,Movie;Sequel
personId:ID,name,:LABEL
keanu,"Keanu Reeves",Actor
laurence,"Laurence Fishburne",Actor
carrieanne,"Carrie-Anne Moss",Actor
上面是2个节点文件,可以看到schema头和数据在同一个文件中,我们以第一个文件为例来说明。可以将“movieId:ID,title,year:int,:LABEL”这行抽出,单独放在一个新文件中作为头文件,可以将每行记录设置为不同的LABEL,同一行记录可设置多个LABEL。如果文件中的LABEL都是一样的,那么可将LABEL标签移到--nodes选项上来。在schema行可以设置各个属性的类型,比如设置year为int类型。如果没有显式设置,默认的属性为字符串。可以看到属性名为movieId这列被标记为ID列,ID用于在创建边是对节点进行索引。作为ID的字段需要具有唯一性,不过import工具允许文件中的ID列是重复的,可以通过设置--ignore-duplicate-nodes去除ID重复的冗余节点;默认的ID字段是字符串的,可以通过--id-type=<STRING|INTEGER|ACTUAL>来指定ID的类型。
需要注意的是,import工具会在导入数据时对ID列进行排序,这是必须的,因为后续在建立节点跟节点关系时,需要通过ID列进行索引,显然只有有序的ID才能够实现高效索引。这就要求我们在导入时一定要合理选择ID列,选择的标准应该跟数据库唯一索引的标准是类似的。不合适的ID列会极大印象import的效率,建议使用数值列作为ID列。
边的格式和配置
通过--relationships[:RELATIONSHIP_TYPE]=<"file1,file2,...">来设置边的类型和边的数据文件,文件格式如下:
:START_ID,role,:END_ID,:TYPE
keanu,"Neo",tt0133093,ACTED_IN
keanu,"Neo",tt0234215,ACTED_IN
keanu,"Neo",tt0242653,ACTED_IN
laurence,"Morpheus",tt0133093,ACTED_IN
laurence,"Morpheus",tt0234215,ACTED_IN
laurence,"Morpheus",tt0242653,ACTED_IN
carrieanne,"Trinity",tt0133093,ACTED_IN
carrieanne,"Trinity",tt0234215,ACTED_IN
carrieanne,"Trinity",tt0242653,ACTED_IN
边的数据文件中,每行都必须有:START_ID和:END_ID这2列,指定该边的起点和终点,在本例中,分别关联到节点文件的movieId和personId列。:TYPE列指定边的类型,如果一个QQ卖号平台文件中的所有边类型相同,那么可以将其提到外面,用RELATIONSHIP_TYPE来表示。
建边过程中,可能遇到起点或终点不存在的情况,那么可用--ignore-missing-nodes选项来忽略这样的边。
ID有重合的场景
如果movieId和personId有重复,比如都是采用自增主键作为ID字段:
movieId:ID,title,year:int,:LABEL
1,"The Matrix",1999,Movie
2,"The Matrix Reloaded",2003,Movie;Sequel
3,"The Matrix Revolutions",2003,Movie;Sequel
personId:ID,name,:LABEL
1,"Keanu Reeves",Actor
2,"Laurence Fishburne",Actor
3,"Carrie-Anne Moss",Actor
那就无法仅通过列的value值来索引:
:START_ID,role,:END_ID
1,"Neo",1
1,"Neo",2
1,"Neo",3
2,"Morpheus",1
2,"Morpheus",2
2,"Morpheus",3
3,"Trinity",1
3,"Trinity",2
3,"Trinity",3
因为无法区分:START_ID中的1代表movieId还是personId,:END_ID也是同理。这种情况下,需要修改节点和边的schema头。为movieId增加额外的标识如Movie-ID,为personId增加如Actor-ID。最终格式为:
movieId:ID(Movie-ID),title,year:int,:LABEL
1,"The Matrix",1999,Movie
2,"The Matrix Reloaded",2003,Movie;Sequel
3,"The Matrix Revolutions",2003,Movie;Sequel
personId:ID(Actor-ID),name,:LABEL
1,"Keanu Reeves",Actor
2,"Laurence Fishburne",Actor
3,"Carrie-Anne Moss",Actor
:START_ID(Actor-ID),role,:END_ID(Movie-ID)
1,"Neo",1
1,"Neo",2
1,"Neo",3
2,"Morpheus",1
2,"Morpheus",2
2,"Morpheus",3
3,"Trinity",1
3,"Trinity",2
3,"Trinity",3
这里可能会有一个疑问,为何不直接使用movieId和personId这两个属性名来标识边的schema头呢。这是因为作为ID的列,可以不指定属性的名称,表示在导入后,不保留这两列。形如:
:ID(Movie-ID),title,year:int,:LABEL
1,"The Matrix",1999,Movie
2,"The Matrix Reloaded",2003,Movie;Sequel
3,"The Matrix Revolutions",2003,Movie;Sequel
:ID(Actor-ID),name,:LABEL
1,"Keanu Reeves",Actor
2,"Laurence Fishburne",Actor
3,"Carrie-Anne Moss",Actor
经验教训
在import工具的使用过程中,我们也积累了一些经验,在此简单做一介绍。
确保schema和数据行格式相匹配。这又存在2种常见,1是schema头的列数比数据行多;2是数据行的列数更多。针对第2种情况,import工具提供了--ignore-extra-columns选项用于忽略数据行中多余的列。对于第一种情况,import工具无法感知,会触发bug导致import在对节点按照ID进行排序时hang住,网上也有相关的issueneo4j-admin import tool hanging;
合理选择属性列作为ID字段。还有一种情况也会导致在import工具一直不返回,通过i+ENTER或c+ENTER可以发现也是卡在SORT阶段。
DETAILS 2019-09-03 07:58:45.673+0000
Prepare node index
[*SORT----------------------------------------------------------------------------------------] 578M
Memory usage: 4.87 GB
Duration: 1m 9s 524ms
Done batches: 57811
.......... .......... .......... .......... .......... 5% ∆18s 408ms
.......... .......... .......... .......... .......... 10% ∆0ms
.......... .......... .......... .......... .......... 15% ∆1ms
.......... .......... .......... .......... .......... 20% ∆0ms
.......... .......... .......... .......... .......... 25% ∆0ms
.......... .......... .......... .......... .......... 30% ∆0ms
.......... .......... .......... .......... .......... 35% ∆0ms
.......... .......... .......... .......... .......... 40% ∆0ms
.......... .......... .......... .......... .......... 45% ∆0ms
.......... .......... .......... .......... .......... 50% ∆0ms
.......... .......... .......... .......... .......... 55% ∆0ms
.......... .......... .......... .......... .......... 60% ∆1ms
.........
这是因为import工具在导入节点时,需要对节点按照ID字段进行排序,如果是使用过长(比如几十个字母),那么排序的效率就会非常低。我们遇到的一个业务场景使用了长字符串作为ID,而且ID的前20多个字母是相同的。导致import在SORT阶段卡了数十个小时。那么为什么需要进行排序呢?有2个作用:1是基于ID列进行去重;2是方便后续创建节点关联关系时进行索引。
若不显式指定,ID字段默认是string类型,但很多情况下ID字段是数值的,该字段在外部数据源上也是整型的。如果在后续的MATCH...SET或MERGE阶段以整型形式处理,会导致出现重复节点(仅ID对应的属性列的数据类型不同)。这会导致图的结构被破坏,出现数据一致性问题。
数据备份
广义的数据备份可分为2种:一种是以标准格式导出,比如将其导出为csv或json等其他系统能识别的数据,进而导入其中。也成为数据导出(export),关系型数据库的逻辑备份其实也算是数据导出,因为其保存为SQL语句,也是一种标准的数据格式;另一种是在本小结要介绍的狭义的数据备份,其作用不是将其导入其他系统,而是防止本系统实例数据误删除,故障恢复或者创建新实例。数据备份一般形式为物理备份,即直接拷贝数据库物理文件。
neo4j提供了离线和在线2种数据备份方式。分别是通过dump/load进行离线备份,通过backup/restore进行在线备份。前一种方式可参考官方文档Dump and load databases。命令形式如下:
neo4j-admin dump --database= --to=
neo4j-admin load --from= --database= [--force]
dump命令将整个图数据库的数据导出成一个.dump文件。其相比在线备份的优势是可以进行增量导入,意思是目标数据库可不为空。
在线备份
相比离线备份,在线备份的优势包括:
备份源无需停服。对业务优化;
可以进行增量备份。而且全量还是增量可以根据备份目录中是否存在有效的全量备份而自动切换;
备份性能更好。一个亿级节点和边的数据库,备份时间仅需十几到数十分钟;
但在线备份仅在neo4j企业版中提供。开源的单机版无法进行在线备份。跟离线备份一样,在线备份也是通过neo4j-admin工具,命令为backup,参数较为丰富:
neo4j-admin backup --backup-dir= --name=<graph.db-backup>
[--from=<address>] [--protocol=<any|catchup|common>]
[--fallback-to-full[=<true|false>]]
[--pagecache=<pagecache>]
[--timeout=<timeout>]
[--check-consistency[=<true|false>]]
[--additional-config=<config-file-path>]
[--cc-graph[=<true|false>]]
[--cc-indexes[=<true|false>]]
[--cc-label-scan-store[=<true|false>]]
[--cc-property-owners[=<true|false>]]
[--cc-report-dir=<directory>]
为了能够进行正常备份,需要在neo4j实例的配置文件中增加如下配置项:
dbms.backup.enabled=true
dbms.backup.address=127.0.0.1:6362
--from参数指定备份源的地址和端口,对应到dbms.backup.address参数;--protocol指定了备份的方式,对于因果集群实例,需选择catchup。
--fallback-to-full用于设置在增量备份失败时的行为,可以选择备份操作失败退出,或者将增量回退到全量。增量备份失败的原因可以有多种,其中一种是增量备份所依赖的neo4j事务日志tx_log文件已被purge,比如上次备份的时间未T0,最新的事务ID为Trx0,如果在本次增量备份时,Trx1 (Trx1 > Trx0)事务日志所在的tx_log文件已被删,那么就无法进行增量备份。事务日志文件的清理参数为dbms.tx_log.rotation.retention_policy。为线上实例开启周期性增量备份操作时,一定要确保事务日志的保留时间长于增量备份周期。
--check-consistency和以--cc开头的一系列参数用于进行数据一致性检查。除了--cc-property-owners外,其他参数默认开启。
在线备份操作会对磁盘IO造成较大压力。对于neo4j集群,应该选择FOLLOWER或READ_REPLICA节点作为备份源,避免对线上业务造成过大影响,同时应该为集群留有一定的服务能力余量,确保进行备份时不会导致线上压力过载。
graph@music-neo4j1:~/neo4j1$ ./bin/neo4j-admin backup --from=127.0.0.1:6362 --backup-dir=/home/graph/neo4j1/backup --name=music.db-2019-9-3
2019-09-03 12:05:00.963+0000 INFO [o.n.b.i.BackupOutputMonitor] Start receiving store files
2019-09-03 12:05:00.966+0000 INFO [o.n.b.i.BackupOutputMonitor] Start receiving store file /mnt/ssd/0/neo4j1/backup/music.db-2019-9-3/neostore.nodestore.db.labels
2019-09-03 12:05:00.978+0000 INFO [o.n.b.i.BackupOutputMonitor] Finish receiving store file /mnt/ssd/0/neo4j1/backup/music.db-2019-9-3/neostore.nodestore.db.labels
......
2019-09-03 12:05:57.555+0000 INFO [o.n.b.i.BackupOutputMonitor] Start receiving store file /mnt/ssd/0/neo4j1/backup/music.db-2019-9-3/neostore
2019-09-03 12:05:57.560+0000 INFO [o.n.b.i.BackupOutputMonitor] Finish receiving store file /mnt/ssd/0/neo4j1/backup/music.db-2019-9-3/neostore
2019-09-03 12:05:57.560+0000 INFO [o.n.b.i.BackupOutputMonitor] Finish receiving store files
2019-09-03 12:05:57.561+0000 INFO [o.n.b.i.BackupOutputMonitor] Start receiving index snapshots
2019-09-03 12:05:57.561+0000 INFO [o.n.b.i.BackupOutputMonitor] Finished receiving index snapshots
2019-09-03 12:05:57.562+0000 INFO [o.n.b.i.BackupOutputMonitor] Start receiving transactions from 172
2019-09-03 12:06:00.802+0000 INFO [o.n.b.i.BackupOutputMonitor] Finish receiving transactions at 297
.................... 10%
.................... 20%
.................... 30%
.................... 40%
.................... 50%
.................... 60%
.................... 70%
.................... 80%
.................... 90%
...................
Checking node and relationship counts
.................... 10%
.................... 20%
.................... 30%
.................... 40%
.................... 50%
.................... 60%
.................... 70%
.................... 80%
.................... 90%
.................... 100%
Backup complete.
上述日志表示备份结束。本次备份实例的数据量为28G,备份所得数据为27G。备份拷贝文件所花时间仅需1分钟。
在备份期间,还会在neo4j实例的debug.log中输出如下操作,可以发现,在进行在线备份完成数据拷贝后,还会将该备份启动进行故障恢复,并检查数据的一致性。
2019-09-03 20:06:13.415+0800 INFO [o.n.k.NeoStoreDataSource] Commits found after last check point (which is at LogPosition{logVersion=3, byteOffset=16}). First txId after last checkpoint: 172
2019-09-03 20:06:13.415+0800 INFO [o.n.k.NeoStoreDataSource] Recovery required from position LogPosition{logVersion=3, byteOffset=16}
2019-09-03 20:06:14.940+0800 INFO [o.n.k.r.Recovery] 10% completed
2019-09-03 20:06:16.907+0800 INFO [o.n.k.r.Recovery] 20% completed
2019-09-03 20:06:18.029+0800 INFO [o.n.k.r.Recovery] 30% completed
2019-09-03 20:06:19.043+0800 INFO [o.n.k.r.Recovery] 40% completed
2019-09-03 20:06:19.900+0800 INFO [o.n.k.r.Recovery] 50% completed
2019-09-03 20:06:20.972+0800 INFO [o.n.k.r.Recovery] 60% completed
2019-09-03 20:06:22.318+0800 INFO [o.n.k.r.Recovery] 70% completed
2019-09-03 20:06:23.664+0800 INFO [o.n.k.r.Recovery] 80% completed
2019-09-03 20:06:24.764+0800 INFO [o.n.k.r.Recovery] 90% completed
2019-09-03 20:06:25.739+0800 INFO [o.n.k.r.Recovery] 100% completed
2019-09-03 20:06:25.740+0800 INFO [o.n.k.NeoStoreDataSource] Recovery completed. 126 transactions, first:172, last:297 recovered
....
2019-09-03 20:06:31.323+0800 INFO [o.n.k.i.s.c.CountsTracker] Rotated counts store at transaction 297 to [/mnt/ssd/0/neo4j1/backup/music.db-2019-9-3/neostore.counts.db.b], from [/mnt/ssd/0/neo4j1/backup/music.db-2019-9-3/neostore.counts.db.a].
数据恢复(restore)
基于在线备份,可以非常方便得进行数据恢复操作。由于在backup时已经进行了recovery及一致性检查,所以neo4j的restore操作相比MySQL的xtrabackup的apply环节更加快速。也就是说,neo4j在backup环节就将xtrabackup在restore环节需做的事情做掉了。
这看起来更为合理,因为从时间上考虑,备份往往不是急迫的,备份的有效性是第一位。大家更关心恢复阶段的效率,neo4j的restore在恢复阶段仅需将文件拷贝到指定的数据目录下即可。命令如下:
neo4j-admin restore --from= [--database=] [--force[=<true|false>]]
相比backup命令,restore命令的参数非常简单,--from指定备份所在目录,--database指定恢复的目标数据库。如果目标数据库已存在,那么--force用于指定是否覆盖数据库中的数据。
基于备份的集群数据初始化
如何基于在线备份恢复出一个实例的操作可以参考官方文档 Restore a backup。本小节主要说下在恢复成neo4j集群时需要注意的问题。
对于已有的neo4j因果集群,只要其已经在运行,那么无论其中是否有数据。若要使用备份进行初始化,需要先关闭集群各个节点的neo4j进程。然后执行:
./bin/neo4j-admin unbind
将其从集群中解绑。然后再通过形如下面的命令进行restore:
./bin/neo4j-admin restore --from=/home/graph/neo4j1/backup/music.db-backup --database=music.db --force
每个节点都进行restore后即可将各节点neo4j进程启动起来。集群的数据初始化就完成了。
常见问题
必须要执行unbind命令,否则会出现如下错误:
2019-09-02 18:19:48.346+0800 ERROR Failed to start Neo4j: Starting Neo4j failed: Component 'org.neo4j.server.database.LifecycleManagingDatabase@587a1cfb' w
as successfully initialized, but failed to start. Please see the attached cause exception "Unable to find transaction 1 in any of my logical logs: Couldn't
find any log containing 1". Starting Neo4j failed: Component 'org.neo4j.server.database.LifecycleManagingDatabase@587a1cfb' was successfully initialized,
but failed to start. Please see the attached cause exception "Unable to find transaction 1 in any of my logical logs: Couldn't find any log containing 1".
org.neo4j.server.ServerStartupException: Starting Neo4j failed: Component 'org.neo4j.server.database.LifecycleManagingDatabase@587a1cfb' was successfully i
nitialized, but failed to start. Please see the attached cause exception "Unable to find transaction 1 in any of my logical logs: Couldn't find any log con
taining 1".
小结
本文介绍了使用neo4j时必须涉及的数据导入,数据备份和恢复,已经如何使用备份初始化集群实例。分析了在各种工具使用过程中可能会遇到的问题,并提供了解决办法。希望对大家有所帮助。