图数据库Neo4j性能优化配置及对比

2021-12-28 00:00:00 索引 集群 事务 线程 服务器

Neo4j是什么

Neo4j是一个高性能的,NOSQL图形数据库,它将结构化数据存储在网络上而不是表中。它是一个嵌入式的、基于磁盘的、具备完全的事务特性的Java持久化引擎,但是它将结构化数据存储在网络(从数学角度叫做图)上而不是表中。Neo4j也可以被看作是一个高性能的图引擎,该引擎具有成熟数据库的所有特性。程序员工作在一个面向对象的、灵活的网络结构下而不是严格、静态的表中——但是他们可以享受到具备完全的事务特性、企业级的数据库的所有好处。

Neo4j的特点

  • SQL就像简单的查询语言Neo4j CQL
  • 遵循属性图数据模型
  • 通过使用Apache Lucence支持索引
  • 支持UNIQUE约束
  • 包含一个用于执行CQL命令的UI:Neo4j数据浏览器
  • 支持完整的ACID(原子性,一致性,隔离性和持久性)规则
  • 采用原生图形库与本地GPE(图形处理引擎)
  • 支持查询的数据导出到JSON和XLS格式
  • 提供了REST API,可以被任何编程语言(如Java,Spring,Scala等)访问
  • 提供了可以通过任何UI MVC框架(如Node JS)访问的Java脚本
  • 支持两种Java API:Cypher API和Native Java API来开发Java应用程序

集群模式

NEO4J提供了两种集群模式,一种是HA(主备模式),在neo4j 4.0之后就废弃了;另外一种是因果集群模式(causal cluster),它们的架构和特点如下。

因果集群

从上图可见,Neo4j集群由两个不同的角色Core Servers和Read Replicas组成,这两个角色是任何生产部署中的基础,但彼此之间的管理规模不同,并且在管理整个集群的容错性和可伸缩性方面承担着不同的角色。

Core Servers

核心服务器的主要责任是保护数据。 核心服务器通过使用Raft协议复制所有事务来做到这一点。 在确认向终用户应用程序提交事务之前,Raft确保数据安全持久。 在实际环境中,这意味着一旦集群(N / 2 + 1)中的大多数核心服务器都接受了事务,安全性要求会影响写入延迟。 隐式写入将以快的多数Core Servers被确认,但是随着群集中核心服务器数量的增加,确认一次写入所需的Core Servers的数量也会增加。实际上,这意味着典型的Core Server集群中需要一定数量的服务器,足以为特定部署提供足够的容错能力。 这是使用公式M = 2F +1计算的,其中M是容忍F故障所需的核心服务器数量。 例如:

  • 为了容忍两个发生故障的核心服务器,我们需要部署五个核心的集群。
  • 小的容错群集(一个可以容忍一个故障的群集)必须具有三个内核。
  • 也可以创建仅包含两个核心的因果集群。 但是,该群集不是容错的。 如果两个服务器之一发生故障,其余服务器将变为只读。

请注意,如果Core Server集群遭受足够的故障而无法处理写入,则它将变为只读状态以保持安全。

Read Replicas

只读副本的主要职责是扩展图数据负载能力(密码查询,过程等)。 只读副本的作用类似于Core Server保护的数据的缓存,但它们不是简单的键值缓存。 实际上,只读副本是功能齐全的Neo4j数据库,能够完成任意(只读)图数据查询和过程。

只读副本是通过事务日志传送从Core Servers异步复制的。 只读副本将定期(通常在ms范围内)轮询核心服务器以查找自上次轮询以来已处理的任何新事务,并且核心服务器会将这些事务发送到只读副本。 可以从相对较少的Core Server中馈送许多只读副本数据,从而使查询工作量大为增加,从而扩大规模。

但是,与核心服务器不同,只读副本不参与有关群集拓扑的决策。 只读副本通常应以相对较大的数量运行,并视为一次性使用。 丢失只读副本不会影响群集的可用性,除了丢失其图表查询吞吐量的一部分。 它不会影响群集的容错能力。

因果一致性

从应用程序的角度来看,集群的运行机制很有趣,但是考虑应用程序将如何使用数据库完成工作也很有帮助。 在应用程序中,我们通常希望从图中读取并写入图中。 根据工作负载的性质,我们通常希望从图中进行读取以考虑先前的写入,以确保因果一致性。

因果一致性使得可以写入Core Server(数据是安全的)并从Read Replica(其中图操作被扩展)中读取这些写入。 例如,因果一致性可确保当该用户随后尝试登录时,会出现创建该用户帐户的写操作。

在执行事务时,客户可以要求书签,然后将其作为参数提供给后续事务。 使用该书签,集群可以确保只有处理了该客户已添加书签的事务的服务器才能运行其下一个事务。 这提供了因果链,从客户的角度确保了正确的写后读语义。

除了书签之外,其他所有事情都由集群处理。 数据库驱动程序与群集拓扑管理器一起使用,以选择合适的核心服务器和只读副本,以提供高质量的服务。

高可用集群

Neo4j HA群集由一个主实例和多个从属实例组成。集群中的所有实例在其本地数据库文件中均具有数据的完整副本。基本集群配置包含三个实例:

每个实例都包含集群管理功能、数据复制功能和选举管理功能,如上图的绿色箭头所示。每个非仲裁实例(从属实例)均与主实例进行通信,以使数据库保持新状态,如上图中的蓝色箭头所示。

仲裁者实例

从属实例的一个特例是仲裁者实例。仲裁程序实例包含在仲裁程序模式下运行的完整Neo4j软件,因此它参与集群通信,但不复制数据存储的副本。

事务传播

直接在主服务器上执行的写事务和在非集群模式下执行模式一致。成功后,该事务将被推送到其它从属实例。如果其它实例推送失败,此事务仍然保持成功,这种模式和乐观锁类似。

在从实例上执行写事务时,每个写操作将与主机同步。主锁和从锁都将获得锁。当事务提交时,它将首先在主服务器上提交,如果成功,则在从服务器上提交。为了确保一致性,在执行写操作之前,主从必须保持一致并且新。从实例的自动更新内置在主从之间的通信协议中。

故障转移

每当Neo4j数据库不可用(例如,由硬件故障或网络中断引起)时,群集中的其他实例将检测到该情况并将其标记为暂时失败。数据库实例故障恢复后将自动追赶集群。

如果主服务器出现故障,则在集群中达到法定人数后,将选举另一个成员,并将其角色从从服务器切换为主服务器。新的主服务器将向群集的所有其他成员广播其可用性。通常,几秒钟之内就会选出一个新的主机并启动。在这段时间内无法进行写操作。

法定人数

群集必须具有法定人数才能选举新的主服务器。法定人数定义为:活动集群成员的50%以上。设计集群时的经验法则是:必须能够容忍n个主实例故障的集群需要2n + 1个实例来满足仲裁并允许进行选举。因此,简单的有效群集大小是三个实例,这允许单个主服务器故障。

选举规则

  • 如果主服务器发生故障,或者在集群的冷启动时,具有高已提交事务ID的从服务器将被选作新的主服务器。该规则确保具有新数据存储的从站成为新的主服务器。
  • 如果一个主服务器发生故障,并且两个或多个从服务器绑定在一起,即具有相同的高提交事务ID,则ha.server_id值低的从属服务器将被选举为新的主服务器。这是一个简易并快速的做法,因为ha.server_id在集群中是的,并且可配置。

数据分支

数据分支的产生可能有如下两种不同的方式:

  • 一个从服务器落在主服务器之后,然后离开或重新加入集群。这种分支是无害的。
  • 发生主服务器重新选举,并且旧的主服务器具有一个或多个已提交的事务,而从服务器在死前没有收到。这种分支是有害的,需要采取措施。

数据库通过创建一个目录来充分利用这种情况,该目录包含分支发生之前的数据库文件的内容,以便可以对其进行检查并解决该情况。在正常操作下不会发生数据分支。

性能优化配置

内存配置

NEO4J的内存管理模型如下:

操作系统内存

必须保留一些内存以运行操作系统本身的进程。1GB是专用于运行Neo4j的服务器的基本保障。当服务器内存较大时可保留更多。

Lucene索引缓存

Neo4j使用Apache Lucene来实现索引功能。通过确保将尽可能多的索引缓存到内存中来优化索引查找性能。与OS内存类似,无法显式配置Lucene索引缓存。

页面缓存

页面缓存用于缓存Neo4j数据和本机索引。将图形数据和索引缓存到内存中将有助于避免磁盘访问以获得佳性能。可调整参数实现页面缓存配置:dbms.memory.pagecache.size。

堆大小

堆空间用于查询执行,事务状态,图形管理等。堆所需的大小非常取决于Neo4j用法的性质。例如,长时间运行的查询或非常复杂的查询可能需要比简单查询更大的堆。一般来说,为了提高性能,我们要配置足够大的堆以支持并发操作。

如果出现性能问题,我们可能必须调整查询并监视其内存使用情况,以确定是否需要增加堆。堆内存大小由参数dbms.memory.heap.initial_size和dbms.memory.heap.max_size决定。建议将这两个参数设置为相同的值,因为这将有助于垃圾回收。

事务状态

事务状态是在更新数据库中记录的事务中保存数据和中间结果所需的内存。仅读取数据的查询不需要事务状态内存分配。默认情况下,事务状态是从堆内或者堆外分配的。请注意,在堆内部配置事务状态时,不能指定其大大小。

事务状态通过以下两种方式分配内存存储大小:

1、使用参数dbms.tx_state.memory_allocation在堆内分配。

2、使用参数dbms.tx_state.max_off_heap_memory在堆外分配。

注意,事务状态内存未预先分配;它会根据数据库活动的需要而变化。保持事务状态处于堆外对于大型,写密集型事务为特征的应用程序特别有利。

索引配置

字段索引

NEO4J采用了apache lucene进行本地化索引,采取的索引存储结构是B+树,可通过dbms.index.default_schema_provider来修改索引器版本,NEO4J提供如下几种索引类型:

native-btree-1.0:支持空间,时间,数值,字符串,数组,布尔类型索引

lucene+native-2.0:支持空间,时间,数字,字符串

lucene+native-1.0:支持空间,时间,数值

lucene-1.0:只支持空间索引

全文索引

全文索引的几个配置说明如下:

dbms.index.fulltext.default_analyzer:全文索引默认的分析器,默认是standard和lucene保持一致。

dbms.index.fulltext.eventually_consistent:全文索引是否保持终一致,只有创建全文索引的时候才有效,默认是false的,实现的是全文索引保持强一致。

dbms.index.fulltext.eventually_consistent_index_update_queue_max_length:全文索引终一致性数据队列的大长度,如果超过此长度,请求的更新数据会被阻塞并等待,直到队列有空间空出,此值在1~5000W之间,默认是10

如果在因果集群部署模式下,建议所有节点都需要显示的配置一致,这样可以保障leader节点切换后数据查询的一致性。

Bolt连接池管理

Bolt是NEO4J自定义的一种连接协议,连接器由服务器端的线程池支持。

线程池的工作机制

Bolt线程池可以设置小和大容量。它从可用线程的小数量开始,然后根据工作负载增长到大数量。空闲时间超过指定阈值的线程将被停止并从池中删除,以释放资源。

建立的每个连接都分配给连接器的线程池。空闲连接不会占用服务器端的任何资源,并且会针对来自客户端的消息进行监视。每个到达连接的消息都会在线程池中的可用线程上触发连接调度。如果所有可用线程都忙,并且仍有增长空间,则会创建一个新线程,并将连接移交给该线程进行处理。如果池容量已满,并且没有线程可用于处理,则作业提交将被拒绝,并生成一条失败消息以将问题通知客户端。

分配给Bolt线程池的默认值将适合大多数工作负载,因此通常不必显式配置连接池。如果大池大小设置得太低,会出现没有可用的线程可以使用的异常。

配置选项

dbms.connector.bolt.thread_pool_min_size:运行状态的小线程数,默认是5

dbms.connector.bolt.thread_pool_max_size:大线程数,默认是400

dbms.connector.bolt.thread_pool_keep_alive:线程空闲等待时间,超过此时间线程会被回收,默认5分钟

如何调整Bolt连接池大小

一个较为合适且典型的配置如下:

dbms.connector.bolt.thread_pool_min_size=10

dbms.connector.bolt.thread_pool_max_size=100

dbms.connector.bolt.thread_pool_keep_alive=10m

数据采集和执行计划

Neo4j会不断的采集节点和关系的信息来调整每次查询的执行计划。

信息采集

Neo4j采集的信息主要包含如下几点:

1、特定标签的节点数

2、特定类型的关系数

3、以某类标签开始或结束的节点和关系数

4、每种索引是否有效?

Neo4j有两种方式进行数据统计和采集:

1、标签和关系的数量,当给某一个节点删除或者设置标签的时候自动更新

2、索引就需要进行全表扫描,由于这是一个非常耗时的操作,所以是当积累到一定的数据更改,在后台执行收集。下面的几个配置是控制是否采集以及以什么速率进行采集:

dbms.index_sampling.background_enabled:数据到一定量之后,后台索引采集是否自动运行,默认是开启的。

dbms.index_sampling.update_percentage:控制在触发新的采样运行之前必须已更新的索引的百分比。默认是5%。

这些采样也可手动触发,如下:

db.resampleIndex():触发索引的重采样

db.resampleOutdatedIndexes():触发所有过时索引的重新采样

执行计划

执行计划被执行后会进行缓存,直到用于生成计划的统计信息发生更改,才会对其进行重新修改计划。使用以下设置,可以通过如下参数来控制重新修改计划对数据库更新的敏感程度:

cypher.statistics_divergence_threshold:

执行计划被视为过时的阈值,默认是0.75。如果用于创建执行计划的任何基础统计数据的更改超过了此值,则该执行计划将被视为过期,并将重新修改执行计划。阈值的计算方式为:abs(new-old)/max(new,old)。这意味着,默认阈值要求数据库的大小变化约为之前的四倍。值0表示尽快重新计划,但是仍然由参数定义cypher.min_replan_interval,默认为10秒。在此间隔之后,发散阈值将开始缓慢下降,大约7小时后达到10%。这保障了即使是很小的更改,长时间运行的数据库仍将获得查询重新修改执行计划,除非更改很大,否则不会频繁重新修改执行计划。

性能对比

写入性能

测试环境

机器: Surface Pro 4

系统: Microsoft Windows 10 专业版 64 位 版本 10.0.15063

处理器:Intel® Core™ i7-6650U CPU @ 2.20GHz 2208Mhz, 2 个内核, 4 个逻辑处理器

硬盘: 1 TB SSD

内存: 16 GB

版本: neo4j-community-3.4.1-windows.zip vs neo4j-enterprise-3.4.1-windows.zip

配置:

dbms.memory.heap.initial_size=5g

dbms.memory.heap.max_size=5g

dbms.memory.pagecache.size=7g

数据源:https://snap.stanford.edu/data/twitter-2010.txt.gz

twitter-2010.txt.gz (5,501,785,223 字节,展开后 26,141,060,589 字节,1,468,365,182 行)

测试步骤

1、次导入:

 USING PERIODIC COMMIT 1000
  LOAD CSV FROM file:/twitter-2010.txt.gz AS line FIELDTERMINATOR  
  WITH toInt(line[]) as id
  MERGE (n:Person {id:id})
  ON CREATE
  SET
   n.name = toString(id),
   n.sex = [, ][(id % 2)],
   n.age = (id % 50) + 15,
   n.country = [中国, 美国, 法国, 英国, 俄罗斯, 加拿大, 德国, 日本, 意大利][(id % 9)]

相关文章