ArangoDB 把文档、图、键值三件事塞进同一个引擎里,宣传语听着很省心:一套数据库顶三套,运维只维护一个集群。但真把它搬到租来的服务器上跑,最先出问题的往往不是"存不下",而是"跑不动"。同一台机器存上亿条文档时 CPU 常年空闲、内存只用到一半,看着很富余;一旦业务侧开始跑三跳以上的图遍历,内存曲线会像被人猛拽一把一样往上窜,最后不是查询变慢,是进程被系统直接杀掉。本文不打算重复多模型的好处,只谈代价:AQL 遍历在内存里到底堆了什么东西,RocksDB 在磁盘上到底写了多少倍数据,以及这两种行为叠加之后,租服务器时内存、盘、CPU 该按什么比例配。
先把结论摆在前面,后面逐条展开:
文档型负载的访问模式基本是"按主键或按索引定位一条记录",命中索引之后 RocksDB 要做的只是一次或几次 key-value 查找,中间结果集大小恒定,一条查询的内存占用峰值几十 KB 到几 MB,跟集合总量没关系。你把集合从一千万条涨到一亿条,只要索引建对了,单条点查的内存曲线几乎是平的。这就是为什么很多人用文档库养成的直觉——"内存按热数据集的百分之十配就够"——在 ArangoDB 上会翻车。
图负载完全不是这个形状。一条 FOR v, e, p IN 1..3 OUTBOUND 这样的 AQL 遍历,引擎必须把起点顶点的邻边读出来,再把这些边指向的顶点读出来,再读这些顶点的邻边……每一步的中间结果都是"上一层结果 × 这一层的平均出度"。内存占用跟深度不是线性关系,是乘法关系。更要命的是,AQL 的遍历语法默认会把路径对象 p 一起构造出来,路径里包含沿途的顶点和边的完整文档,不是只存 id。你写 RETURN v 只返回顶点,很多人就以为路径对象不会被构造,实际上只要你在 FOR 里写了第三个变量 p,路径就在内存里实实在在占着地方。
同一台 64GB 内存的机器,跑文档负载时可能连 20GB 都用不到,业务方觉得这台机器"能力过剩",于是把图分析的任务也接到同一实例上。结果就是白天跑业务没问题,晚上跑一次全图的关系挖掘,内存直接打满,然后被系统的内存回收机制选中杀掉。这不是机器不行,是两类负载的内存曲线形状根本不同,却挤在同一个内存池里。
举个典型场景,方便理解量级:图 A 有一亿条边,平均出度 4;图 B 有三百万条边,平均出度 200。图 B 的边数只有图 A 的百分之三,但跑一次三跳遍历,图 A 的中间结果量级是 4 的三次方也就是几十到几百个顶点,图 B 是 200 的三次方也就是八百万量级。后者是前者的四个数量级以上。所以给你做容量规划的人如果只问"你们有多少条边",你可以直接判定他没做过图库——正确的问题是"平均出度是多少、出度分布是不是长尾、有没有超级节点"。
超级节点(degree 上万甚至几十万的顶点,比如"某热门商品""某头部账号""根分类节点")是图库内存事故的头号来源。遍历只要碰上它,边界集合瞬间膨胀到百万级。真实业务图几乎都是长尾分布,平均度数看着只有十几,但总有那么几十个节点度数是平均值的上千倍。评估内存的时候用平均度数算出来的数字,在这几十个节点面前没有任何意义。
AQL 的图遍历在逻辑上是一层一层往外推的。起点顶点进队列,读它的出边集合,得到第一层;对第一层的每个顶点再读出边,得到第二层;依此类推,直到达到 min..max 指定的深度上限。每一层的顶点数量约等于上一层的顶点数量乘以该层顶点的平均出度,这就是那个乘法。如果遍历路径上出现环(现实图里几乎必然出现),不做去重的话同一批顶点会被反复访问,中间结果集还会再翻几倍。
ArangoDB 提供了 uniqueVertices 和 uniqueEdges 两个选项来控制去重。默认设置下不会做全局去重,也就是同一顶点可以在不同路径里重复出现。一旦你把它们设成 "global",引擎就要额外维护一张全局访问过的顶点(或边)集合——通常是一个哈希表。这张表本身也占内存,规模等于遍历触达过的顶点总数,而且它在整个遍历过程中都不能释放。所以去重不是免费的:它用"一张额外的大哈希表"换"中间结果集不重复膨胀"。在密度高的图里这笔交易通常是划算的;在稀疏的树状图里,去重表的开销反而比不去重更大。
深度参数写成 1..5 和写成 1..10,表面上只是"多找几层",实际上中间结果集可能差三到五个数量级。很多团队做图分析时习惯性地写个大范围深度,理由是"我也不知道几跳能找到,先放宽一点"。这在图库上是最坏的习惯之一。正确的做法是先跑小深度摸清你这张图的度数分布,再定深度上限;如果业务确实需要不确定的深度,应该用 PRUNE 条件在遍历过程中提前剪枝,或者用最短路径(SHORTEST_PATH)这类有明确目标的算法替代无目标的深度遍历。
PRUNE 的价值常被低估。它是一个在遍历器展开某个顶点之前就执行的判断条件,命中就整条分支不再往下走。这跟"遍历完再用 FILTER 过滤"是两回事:FILTER 是结果已经产生了才筛掉,内存已经花出去了;PRUNE 是内存根本不分配。比如你要找"三跳内、且金额大于某个阈值的关系链",把金额条件写成 PRUNE 而不是事后 FILTER,中间结果集能砍掉绝大部分。
AQL 遍历默认走 bfs(options { bfs: true })。bfs 的特点是必须把当前层的全部顶点都展开完,才能进入下一层,因此它在任意时刻要留住的,是"当前层 + 下一层"两层的边界集合。而边界集合的规模在深度 d 处约等于平均出度的 d 次方。也就是说 bfs 的内存峰值由最深那两层的宽度决定,这个宽度是随深度指数增长的。
dfs 则是沿着一条路径一路扎到底,回溯再走下一条。它在任意时刻只需要留住"当前路径的栈",栈的深度等于遍历深度,通常是个位数到两位数,内存占用看起来极小。但 dfs 的代价在另一处:它必须记住已经走过哪些分支,否则在稠密图里会反复绕圈、时间复杂度爆炸;而一旦你为它加上全局去重,那张访问表又要吃掉跟 bfs 同量级的内存。换句话说,dfs 省掉的是"层宽度",省不掉"触达过的顶点总数"。
所以判断标准其实很简单:如果你的查询关心的是最短距离、层序关系,或者深度上限很小(两三跳),用默认的 bfs,内存可控;如果你要的是"存在性判断"——只要找到一条符合条件的路径就收工,深度又比较深——dfs 加上早期返回语义会明显更省内存。但无论哪种,真正的解药都不是换遍历策略,而是把深度和剪枝条件收紧。
这是图库调优里最容易被忽略的一条。AQL 是惰性求值的执行模型,理论上 FOR ... LIMIT 10 RETURN ... 这样的写法,引擎拉够 10 条就可以停止向下游要数据,遍历也就随之停下。但这个"提前终止"有个前提:LIMIT 之前不能有强制物化的算子。
具体来说,下面三种写法里只有第一种真正省内存:
还有一类更隐蔽的情况:把遍历放进子查询,然后在外层 LIMIT。很多人以为外层截断能传导到内层,实际上传导与否取决于优化器能不能把限制条件下推,不要赌它。稳妥的办法是把 LIMIT 写在最靠近遍历的那一层,并且用 EXPLAIN 看一眼执行计划里有没有出现物化节点。看到执行计划里遍历节点上方挂着 Sort 或 Aggregate 节点,就可以判定这条查询的内存峰值与深度和度数直接挂钩,LIMIT 救不了它。
ArangoDB 的存储引擎是 RocksDB,底层结构是 LSM 树(日志结构合并树)。它的基本套路是:写入先落 WAL(预写日志,保证崩溃可恢复)再进内存里的 memtable,memtable 攒够了就整体刷成磁盘上的一个不可变文件(SSTable),落到 L0 层。后面的层(L1、L2……)由后台线程做 compaction——把上一层的文件读出来、归并排序、去重、再写到下一层。
问题就在这个"读出来再写下去"上。一条键值对从写入到最终稳定在最后一层,中间会被反复读写多次,这就是写放大。典型配置下写放大系数在十倍到几十倍这个量级,也就是你业务侧写入 1GB 数据,磁盘上实际发生的写入量可能是 10GB 到 30GB。图库场景还要再糟一点:边的更新、顶点属性的修改,在 RocksDB 眼里都是新版本的键,旧版本要等 compaction 时才真正回收,所以空间放大(磁盘实际占用远大于逻辑数据量)同样明显。
compaction 由后台线程执行,不占用业务请求的线程,很多人因此以为它"不影响线上"。这是错觉。compaction 要做的就是大量顺序读 + 大量顺序写,它会把磁盘的 IO 带宽和队列深度吃掉一大块。业务侧此刻正在进行的是随机读——图遍历读邻边,本质是大量小块的随机 key 查找。这两类 IO 挤在同一块盘上,随机读的延迟会被拉长好几倍,表现出来就是"查询时快时慢,慢的时候毫无规律"。慢的那几次,八成是撞上 compaction 高峰。
更麻烦的是磁盘快满的时候。RocksDB 需要空闲空间来做 compaction,磁盘使用率越高,compaction 越难顺利完成,越难完成就越积压,越积压读取要翻的文件越多,读延迟越高。这是一条正反馈的下坡路。图库的经验做法是把磁盘使用率红线定在七成左右,不要等到九成才想起来扩容——那时候 compaction 可能已经跟不上写入速度了。
LSM 树读一个键,路径是:先查 memtable,再查 L0 的每个文件,再逐层往下查 L1、L2……直到找到为止。层数越多、每层文件越多,一次逻辑读对应的物理 IO 次数就越多,这就是读放大。RocksDB 用布隆过滤器缓解这个问题——每个 SSTable 带一个布隆过滤器,能快速判断"这个键肯定不在这个文件里",从而跳过大部分文件。布隆过滤器本身要占内存,而且它只能帮你跳过,不能帮你找到。
真正帮读取提速的是 block cache。RocksDB 把 SSTable 按块(默认几十 KB 量级)缓存到内存里,命中就直接从内存返回,不用碰磁盘。图遍历的读取特征是"同一批顶点和边会被不同查询反复访问"——尤其是靠近根部的节点和高热度节点,缓存命中率能到相当高的水平。所以图库的性能天花板,很大程度上取决于 block cache 能装下多少热数据。
单看顺序带宽,SATA SSD 和 NVMe 差个几倍,好像不是不能接受。但图库吃的不是顺序带宽,是两样东西:小块随机读的 IOPS,以及延迟的稳定性。NVMe 走 PCIe,队列深度远高于 SATA 的 AHCI,随机读 IOPS 通常是 SATA SSD 的一个数量级以上,延迟也低一个量级。图遍历一次三跳查询背后可能是几万次小块随机读,这些读全部落在延迟敏感路径上——单次读从 100 微秒涨到 1 毫秒,整条查询就从 200 毫秒变成 2 秒。
再叠加 compaction 的后台 IO,SATA 盘的处境就更难看:一边是 compaction 吃掉的几百 MB/s 顺序带宽,一边是业务侧几万 IOPS 的随机读,两者共享一条 6Gbps 的通道(实际可用带宽远低于标称值)。结果通常不是"慢一点",而是 IO 队列排队、超时、连接池打满,然后上游应用雪崩。所以如果预算只够加一样东西,我的判断是:图库先上 NVMe,再谈加内存。内存不够顶多查询失败一次,盘不行是整个实例不可用。
还有一种常见的折中想法:拿一块小容量 NVMe 当缓存盘、大容量 SATA 当数据盘,靠操作系统或者某些缓存软件做分层。对图库我不推荐这么干。图遍历的随机读模式命中率低、分布又散,分层缓存很容易退化成"每次都要从 SATA 现取,还多绕一层"。与其做分层,不如直接把热数据规模压到能放进 NVMe 的范围——该归档的归档,该拆分片的拆分片。
一台跑 ArangoDB 的机器上,内存大致被三件事占用:
最常见的配置错误是这样的:机器 128GB 内存,运维想着"给数据库多一点缓存总是好的",把 RocksDB 的 block cache 设成 96GB,觉得还剩 32GB 给查询用,很宽裕。这个算法漏掉了三件事。
第一,ArangoDB 进程本身除了 block cache 还有大量常驻开销:memtable、布隆过滤器和索引元数据、线程池栈、网络缓冲区、AQL 查询解析与执行器的固定结构。这部分在负载起来之后通常是几个 GB 到十几个 GB,不是零。
第二,AQL 中间结果不是"一个查询一份",而是"并发数 × 每份"。你按单查询估算出来 4GB 够用,线上同时跑 8 条复杂遍历就是 32GB。很多人只测单条查询的内存峰值,然后按这个数字配机器,上线就 OOM。
第三,也是最容易被忽略的:block cache 和操作系统页缓存不是同一块内存,但它们在物理内存里是此消彼长的关系。把 block cache 设太大,OS 页缓存就没了,文件系统元数据、WAL 刷盘、乃至日志写入都要直接落盘,IO 路径变长。有人以为"我不用页缓存,反正 block cache 已经把热数据都装了",可 block cache 只装 SSTable 的数据块,装不了 WAL 写入缓冲、装不了操作系统层面的文件元数据,这些还是要靠页缓存。
一个更稳妥的分配思路是:block cache 给到物理内存的三成到四成,剩下的留给 AQL 中间结果和 OS 页缓存,然后用真实并发压一遍,看内存峰值落在哪里再微调。这里提醒一句,block cache 的配置项名在不同版本里不一样(早期版本是 --cache.size 一类,较新版本改成了 --rocksdb.block-cache-size 体系),动手之前先对着你实际用的版本号查一遍文档,别照着三年前的教程改配置文件。
ArangoDB 集群(Active Failover 之上的完整集群模式)由三类进程组成,它们的资源消耗形状完全不同。很多团队上集群时按"三种角色各来三台同样配置的机器"去租,钱花掉了性能却没起来,根子就在这里——三类角色要的东西不一样,用同一份配置必然有一类是浪费、有一类是不够。
| 集群角色 | 主要消耗的资源 | 内存怎么留 | 磁盘要求 | 常见误判 |
|---|---|---|---|---|
| DBServer | 磁盘 IOPS 与容量、内存(block cache + 本地遍历中间结果);CPU 中等 | 物理内存三到四成给 block cache,其余留给分片内遍历与页缓存,务必留并发余量 | 必须 NVMe;使用率红线定在七成,给 compaction 留足空间 | 以为"加机器就等于加容量",忘了分片要重新均衡、单分片仍可能过热 |
| Coordinator | CPU(AQL 解析、执行计划、gather/scatter 归并)与网络吞吐 | 不用大内存,但要给并发查询的归并缓冲区留够;本层不做数据存储 | 本地盘要求低,普通 SSD 即可;重点是网卡带宽与低延迟内网 | 查询慢就加 Coordinator,实际瓶颈常在 DBServer 的随机读 |
| Agency | 几乎不消耗 CPU 与内存,消耗的是写入延迟(Raft 日志 fsync) | 小而稳即可,几 GB 足够;不要和其他角色抢内存 | 容量极小,但对 fsync 延迟极其敏感,必须是低延迟 SSD | 把 Agency 和重负载 DBServer 混布,Raft 选举超时引发集群抖动 |
| 单实例(非集群) | 三种负载合一:磁盘、内存、CPU 全部压在同一台机器上 | block cache 与查询中间结果共用一池,峰值叠加风险最高,余量要留更大 | 同样要求 NVMe;单机容量上限即集群分片前的天花板 | 以为单机可以无限加内存替代拆分,忽略了单机 CPU 与 IO 上限 |
上集群的真实理由只有两个:单机磁盘装不下,或者单机 CPU 与 IO 扛不住并发。如果只是"某几条查询内存峰值太高",先别急着上集群——集群把遍历拆到多个 DBServer 上执行,Coordinator 还要做跨节点归并,中间结果集并不会凭空消失,跨网络的 gather 阶段甚至会新增一层内存开销。这种情况下,给单实例加内存、同时收紧遍历深度和剪枝条件,通常比上集群便宜得多,见效也快得多。
真正该上集群的信号是这些:数据总量已经接近单机磁盘的六到七成(再往上 compaction 就要出问题);写入吞吐已经把单机的 compaction 线程压到持续积压;并发查询数让单机 CPU 长期在八成以上;或者业务要求故障自动切换、不能接受单机宕机即全站不可用。这四条里中两条以上,再考虑集群。
DBServer 是重资产角色,磁盘和内存都压在它身上,扩容的代价也最高。Coordinator 相对轻量,可以随并发压力横向增减。Agency 一般固定三或五个节点(Raft 要求奇数),配置不用高,但要绝对稳定。合理的起步比例通常是 DBServer 数量最多、Coordinator 次之、Agency 三台固定。反过来配——Coordinator 一大堆、DBServer 只有两台——是很多"集群上了但没变快"的典型现场。
还有一点值得单独说:分片键选错了,集群等于白上。图数据的分片要尽量让"一次遍历触及的顶点和边落在同一个分片"上,否则每跳都可能跨节点,网络往返次数随深度线性增长。ArangoDB 允许为边集合指定分片键(常见做法是让边按 _from 分片),配合 smart graph 这类机制把关联数据放一起。这件事在建集合的时候就要定,事后改分片键基本等于重灌数据。
ArangoDB 对 edge collection 会自动创建 edge index(基于 _from / _to 的组合索引),你不用建,也删不掉。它的好处是邻边查找有索引可用,遍历不至于退化成全集合扫描;代价是每次写入一条边,都要维护这条索引,写放大上再加一层,磁盘占用也实实在在多一份。边数上亿之后,光是 edge index 本身的磁盘占用就能到几十 GB 量级,而且这部分数据同样要参与 compaction、同样要跟 block cache 抢内存。
容易被忽视的是布隆过滤器和索引元数据的内存。RocksDB 为每个 SSTable 维护布隆过滤器,这些过滤器是常驻内存的。文件数量越多(compaction 跟不上时 L0 文件会堆积),布隆过滤器占的内存越大。这也是为什么"磁盘快满导致 compaction 积压"最后会以"内存不够"的形式表现出来——两种资源在这里是连着的。
普通属性索引(skiplist / 持久化的有序索引)和哈希索引的行为差别,在图库里会被放大。哈希索引只适合等值匹配,内存开销相对可控;持久化索引支持范围查询和排序,但每个条目要存完整的键值,磁盘和内存占用都比哈希索引大。全文索引(ArangoSearch 视角)另算一套,它要维护倒排表和相关的分析器结构,内存占用跟被索引文本的词汇量正相关,给长文本字段建全文索引是最容易把内存吃干的做法之一。
图库里真正值钱的是"顶点中心索引"——在边集合上建 _from 加某个业务属性(比如时间、权重、类型)的组合索引。它的作用是让"从某个顶点出发、按某属性排序取前 N 条边"这件事完全在索引里完成,不需要把所有邻边都读出来再排序。对高热度顶点来说,这一条索引能把一条查询从"读十万条边再排序"变成"走索引拿 20 条"。代价是每加一条这样的索引,写入成本上升、磁盘占用上升、block cache 的竞争也更激烈。
这句话听起来像废话,但在图库上它是有具体机制的。索引少,遍历要读的 SSTable 块就多,block cache 命中率下降,随机读 IOPS 上升,最后卡在磁盘上;索引多,每份索引都要在 block cache 里争一席之地,热数据被索引挤出去,命中率同样下降,而且写入端的放大系数同步上升,compaction 更忙,形成恶性循环。两边都会走到同一条死路上:block cache 不够用。
判断的抓手是命中率和写入放大这两个指标,而不是"索引数量"这个数字。一个简单的做法是:把 block cache 的命中率作为观察对象,每加一条索引之后跑一轮典型负载,如果命中率掉了而查询延迟没改善,说明这条索引在跟热数据抢内存,性价比是负的。反过来,如果加上索引后磁盘随机读 IOPS 明显下降、命中率持平,那这条索引就是赚的。
还有一类索引是纯粹的历史遗留——业务早期建的、后来查询模式变了再没人用。这类索引在图库上尤其该清理,因为它每天都在参与写入放大。定期把建了但从没出现在执行计划里的索引列出来删掉,是成本最低的一次性优化。
RocksDB 的写入路径是"先写 WAL 保证持久性,再写 memtable"。WAL 是顺序追加写,本身很快,但它决定了两件事:一是崩溃恢复的能力边界,二是复制的数据来源。ArangoDB 早期版本维护了自己的一套 WAL 用于复制,较新版本则直接依托 RocksDB 的 WAL 体系做主从同步——版本差异导致配置项差别很大,做容灾方案之前务必先确认你用的版本走的是哪条路径。
同步复制与异步复制的选择,直接影响写入延迟。同步复制要求主节点等从节点确认后才向客户端返回成功,数据安全性高,但写入延迟里多了一段网络往返,而且从节点一旦抖动或者慢,主节点这边就跟着排队。异步复制是发完就返回,延迟低,代价是主从之间存在复制滞后,故障切换时可能丢掉最后一段写入。图库场景里常见的是混合策略:核心的边数据用同步、日志类和可重建的数据用异步。
这一点在算容量时最容易算错。副本数设成 3,不是"多两份备份",而是磁盘占用乘 3,写入放大乘 3(每个副本各自跑自己的 compaction),网络流量乘 3。也就是说你把副本从 2 提到 3,不只是多买一份盘,还要多买一份能扛住对应写入放大的 IO 能力和一份网络带宽。
集群里还有一类特殊集合叫 satellite collection(卫星集合)。它会被完整复制到每一个 DBServer 上,好处是 join 的时候不用跨节点取数据,代价是它的存储占用等于"数据量 × DBServer 数量"。卫星集合只适合小而稳定的维度表(比如枚举、地区字典、类型定义),要是有人把一个会持续增长的集合设成了卫星集合,随着 DBServer 扩容,它的存储占用会跟着节点数线性膨胀,而且每个节点都要为它跑一遍写入。
为什么坑:深度从 3 改到 5,在写查询的人看来只是改个数字,在引擎看来中间结果集要乘以平均出度的平方。业务侧往往因为"偶尔需要深一点"就把上限放宽,结果日常查询也被拖着按上限执行,内存峰值按最坏情况来。
怎么避:给不同深度的查询拆成不同的接口,别用一个万能查询覆盖所有场景;给深度上限设硬约束,超过就走离线任务而不是在线查询;能用 SHORTEST_PATH 或者带 PRUNE 的定向遍历,就别用无目标的 1..N 范围遍历。
为什么坑:按边数估出来的内存需求跟实际差着数量级。一个三百万边的高密度图,三跳遍历的内存可能比一个一亿边的稀疏图高几个数量级。采购时按数据量报上去的配置,上线第一天就会被打脸。
怎么避:评估时先统计出度分布,重点看 P99 和最大度数,用最大度数而不是平均度数算最坏情况;对超级节点单独处理(拆边、加剪枝条件、或者把它排除在在线遍历之外);把"单查询内存峰值 × 预期并发"作为采购依据。
为什么坑:block cache 是常驻且不让步的,剩下的内存要同时应付 AQL 中间结果、进程常驻开销和 OS 页缓存。图库的内存尖峰恰恰出现在中间结果这一块,把大头顶掉之后,最先 OOM 的就是查询。
怎么避:block cache 控制在物理内存的三到四成,留出至少一半给查询与页缓存;按"并发数 × 单查询峰值"而不是"单查询峰值"来验算;上线前用真实并发压一轮,观察内存峰值而不是平均值;确认配置项名与当前版本匹配后再改。
为什么坑:SATA 的随机读 IOPS 和队列深度本来就撑不住图遍历,compaction 的后台顺序 IO 再一抢,随机读延迟直接翻几倍。表现出来是查询时快时慢、没有规律,查监控才发现 IO 等待居高不下。
怎么避:数据盘一律 NVMe;把磁盘使用率红线定在七成,给 compaction 留空间;根据业务低峰调整 compaction 的触发策略与并发度,别让它在业务高峰满负荷跑;观察 L0 文件堆积数量,堆积就是 compaction 跟不上的信号。
为什么坑:Coordinator 负责解析和执行计划、做跨节点归并,它吃的是 CPU 和网络。图库查询慢的常见根因在 DBServer 的随机读和 block cache 命中率上。加了 Coordinator 只是多了几个分发的入口,真正干活的 DBServer 依然是那么几台,瓶颈纹丝不动,钱倒是花出去了。
怎么避:先用执行计划和监控判断瓶颈在哪一层:Coordinator 的 CPU 长期高位、网络打满,才加 Coordinator;DBServer 的 IO 等待高、命中率低,就该加 DBServer 或者加内存换 NVMe。顺手检查一下分片键,跨分片遍历会把 Coordinator 的归并压力人为放大。
为什么坑:图库的性能高度依赖数据形状——度数分布、连通性、超级节点位置、查询并发模式。别人的"典型配置"在他的数据形状上成立,在你的数据上可能连一半性能都跑不出来。租用服务器时按通用清单下单,是图库项目最常见的预算浪费来源。
怎么避:上线前用真实数据抽样(哪怕只是全量的百分之十,但要保留度数分布特征)跑一轮典型负载;把内存峰值、block cache 命中率、IO 等待、compaction 积压四个指标同时观察;先用月付或短周期验证形状,摸清曲线再转长期。以下为典型部署思路,并非特指某一真实客户。
如果已经决定上集群,配置上最直接的切法是:DBServer 走大内存加 NVMe(内存给到能装下热数据规模的两倍左右,盘按七成使用率红线倒推容量,还要预留写放大需要的余量);Coordinator 走多核 CPU 加万兆内网,内存中等即可;Agency 三台固定,配置不高但必须独立、盘必须低延迟。混布在小规模环境里可行,但 Agency 一定要先独立出来——它最怕的是被别人的 IO 抖动拖累。
如果还在单实例阶段,我的建议是先加内存和换 NVMe,别急着上集群。单实例加内存的边际成本远低于搭建和维护一个集群,而大多数"跑不动"的问题根源是遍历写法和深度控制,不是架构。等内存加到位、查询收紧之后仍然卡在 IO 或者 CPU 上,再谈拆分也不迟。
具体到租机器这件事,找一家能按角色分开配、又能快速调整的服务商,比单纯比价重要得多。图库项目在摸形状阶段经常要变配置——今天发现内存不够、明天发现盘 IO 不行、后天要加一台 DBServer——如果每次调整都要走漫长的流程,项目节奏就被拖没了。一万网络深耕 IDC 19 年(成立于 2007 年),在这一点上比较顺手:NVMe 与高频内存机型可以按 DBServer 和 Coordinator 分别配,深圳自营机柜,最快 1 分钟上架,后期调整配置或加机器走 7×24 中文工单就能推进。价格方面一律需实时询价,价格主要受 CPU、内存、存储、带宽和 IP 影响,具体以签约时最新报价与合同为准。
下单前把这几项写清楚,能让配置一次到位:图的顶点数与边数、平均出度与最大出度、日常查询的深度上限与并发数、写入吞吐(每秒多少条边更新)、数据保留周期与备份策略。这几项里最容易被漏掉的是出度分布和并发数,而它们恰恰是决定内存的那两个。
顶点数本身很少成为单机上限,真正决定单机能不能扛住的是"边的数量乘以副本数"带来的磁盘占用,以及"度数乘以深度"带来的内存压力。边数在亿级以内、平均度数不高的图,单机配足 NVMe 和大内存完全可以跑;一旦边数上到十亿级,或者磁盘使用率接近七成,就该考虑分片了。判断的顺序是先算磁盘(含写放大和副本乘数),再算内存(按最大度数算最坏情况),再看 CPU 能不能扛住并发。
粗略的量级估算方法是"起点数 × 平均出度的三次方 × 单个顶点文档大小",再乘以一个路径对象的开销系数(如果遍历语句里写了路径变量,路径会保存沿途的顶点和边,系数通常在二到五倍之间)。举个典型场景:一万个起点、平均出度 20、顶点文档 1KB,理论中间结果量级在万级顶点、几十 MB 量级;同样的起点数把出度换成 100,量级就到千万级顶点、十 GB 以上。所以与其问"多少内存",不如先去统计出度分布。
建议把日常使用率红线定在七成,不要超过八成。理由不只是"留着扩容",更重要的是 compaction 需要空闲空间才能顺利完成:使用率越高,compaction 越容易积压,L0 文件堆积又会导致读放大上升和布隆过滤器内存占用上升,结果同时表现为"变慢"和"内存涨"。另外要按写放大预留 IO 寿命——业务写 1GB 磁盘上可能写十几 GB,选盘时关注写入耐久度指标,别只看容量。
能用,但只能在两种情况下:数据量很小、并发很低的分析型场景,或者纯粹做冷备和归档节点。在线服务的图库我不建议用 SATA。瓶颈不在顺序带宽而在随机读 IOPS 与延迟稳定性,而且这两项在叠加 compaction 后台 IO 之后会急剧恶化。如果预算有限,宁可先减数据量(归档冷边、压缩文档)把热数据压进小容量 NVMe,也不要用大容量 SATA 铺满。
技术上可以,小规模环境里也常见,但要有优先级。Agency 最应该先独立出来——它跑 Raft,对 fsync 延迟极其敏感,和重 IO 的 DBServer 混布会因为一次 IO 抖动触发选举超时,表现为整个集群莫名其妙抖一下。Coordinator 和 DBServer 混布在起步阶段可以接受,但要注意 Coordinator 的 AQL 归并缓冲会和 DBServer 的 block cache 抢内存,内存规划时要按两者峰值之和算,而不是分别算。
不停写也能做,ArangoDB 的热备份机制可以在不停服的情况下生成一致性快照。但要注意两件事:一是快照生成过程本身会产生额外 IO,最好放在业务低峰;二是快照占用的空间要单独算,别和数据库的日常空间挤在同一块盘的使用率红线内。真正需要停写的场景是跨大版本升级和分片键调整——这类操作通常要求先停写、再迁移、再校验。
先判断内存在谁身上。如果内存峰值出现在 DBServer 上(block cache 命中率低、IO 等待高),拆 Coordinator 一点用都没有,要么加内存、要么优化查询和索引。如果 Coordinator 的 CPU 和网络是瓶颈而内存也紧,拆 Coordinator 才有意义——它把单台上的并发归并压力摊开。最常见的错误顺序是"内存报警 → 加机器 → 仍然报警",因为加的是不扛数据的那类角色。
问:为什么我按数据量买的配置,跑起来还是 OOM?
答:因为你买的是"能装下多少数据"的配置,而图库吃的是"遍历时能膨胀到多大"。这两件事由不同的参数决定:前者看边数和文档大小,后者看平均出度、最大出度和遍历深度。一亿条边、平均出度 4 的图,三跳遍历的中间结果是几十到几百个顶点;三百万条边、平均出度 200 的图,同样的三跳是百万量级顶点。后者的数据量只有前者的百分之三,内存需求却是前者的几千倍。所以在下单之前,先把出度分布统计出来,用最大度数算最坏情况,再乘以预期并发,这个数字才是你要买的内存。
问:那磁盘呢,是不是也该按度数算?
答:磁盘主要按数据量和写放大算,跟度数关系没那么直接,但有两处间接影响。一是度数高的顶点意味着边集合更密集、索引更大,edge index 的磁盘占用跟着涨;二是高密度图的更新更频繁,写放大更明显,compaction 压力更大,需要留更多空闲空间。所以盘这一侧的关键数字是"逻辑数据量 × 空间放大系数 × 副本数",再除以七成的使用率红线,得到该买的物理容量。
问:如果两样都配足了,还跑不动怎么办?
答:那多半不是机器的问题,是查询的问题。回到遍历语句本身看三件事:深度是不是放宽了、LIMIT 是不是夹在 SORT 或 COLLECT 之后、有没有用 PRUNE 做提前剪枝。这三处改完通常能省下比加内存多得多的开销。机器配置决定的是上限,查询写法决定的是你离上限有多远。
本文涉及的执行机制、配置项命名与集群角色划分,以 ArangoDB 官方文档的对应版本说明为准(不同版本在 block cache 配置项命名、WAL 复制实现、遍历默认选项上存在差异,动手改配置前请核对自己实际使用的版本号)。文中出现的量级估算(写放大系数、出度与中间结果的关系、IOPS 量级差异)为工程经验的典型区间,用于辅助选型判断,不构成任何性能承诺,也不对应任何特定环境下的测量结果。文中标注的"典型场景""示例架构"均为假设性说明,并非特指某一真实客户或某一真实部署。
服务器租用相关的配置建议与能力描述,可参考 https://www.idc10000.net/ 的机型与 IDC 服务页面;文中涉及的价格一律需实时询价,价格主要受 CPU、内存、存储、带宽和 IP 影响,具体以签约时最新报价与合同为准。
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品