第 2 章:理解 BLAST 内存模型——关键影响因素
内存溢出,说白了就是 BLAST 找系统要内存,系统给不起。但为什么有的任务给得起,有的就给不起?这背后是 BLAST 的一套内存分配逻辑。搞懂它,你就能预判什么情况下会炸,什么情况下稳如老狗。
我个人习惯先把一个工具的内存模型摸透,再去调参。不然你连 OOM 是怎么来的都不知道,谈何处理?
2.1 BLAST 内存分配的核心区域
BLAST 进程启动后,内存主要花在三个地方。我画了一张图,方便你理解它们的层次关系。
从上图你能看明白:绝大部分的内存都被数据库的索引数据吃掉了。我在项目中遇到过很多次,新手把问题归咎于查询序列太大,其实查出来的内存大头根本不在那儿。
2.2 数据库索引 —— 真正的内存大户
BLAST 要快,就必须把数据库的索引结构全部加载到内存。不然每次比对都去读磁盘,速度会慢到让你怀疑人生。
具体来说,这些索引包含以下内容:
- 序列数据本身:所有数据库序列的碱基/氨基酸序列,以紧凑格式存放。
- 哈希表或后缀数组:这是 BLAST 快速定位“种子”匹配的索引结构,大小与数据库总字符数成正比。
- 体积索引 (volume index):对于超大数据库,BLAST 会分卷建立索引,每卷的索引头也要占内存。
举个例子。我前几天处理一个 nr 数据库的格式化,本地建库后一看大小:
$ ls -lh nr.*
-rw-r--r-- 1 user group 25G Feb 18 10:00 nr.00.phr
-rw-r--r-- 1 user group 18G Feb 18 10:00 nr.00.pin
-rw-r--r-- 1 user group 12G Feb 18 10:00 nr.00.psq
-rw-r--r-- 1 user group 34G Feb 18 10:00 nr.01.phr
-rw-r--r-- 1 user group 22G Feb 18 10:00 nr.01.pin
-rw-r--r-- 1 user group 15G Feb 18 10:00 nr.01.psq
.phr 和 .pin 文件就是索引。你想想看,仅一个分卷的索引头就 25G,两个分卷加起来 59G。BLAST 要全量加载它们,机器没个 128G 物理内存,不 OOM 才怪。
这里有个重要参数控制了索引在内存中的组织形式:
关键参数:-db_soft_mask、-gil、-no_greedy、-ungapped
这些参数会改变搜索算法,间接影响索引的访问模式和缓存驻留量。但不改变“索引必须全部加载”这个前提。
2.3 查询序列缓冲区 —— 最小的一块
很多人误以为查询文件越大,内存占用越高。其实不是这样。BLAST 会把查询序列分批读入内存,一次只处理一批。
决定这批大小的参数是 -query_batch 或隐含的 -query_loc 设定。默认情况下,BLAST 会智能地划分批次,保证单批序列总碱基数不超过一个阈值。
可以这么理解:
# blastn 默认分批行为(简化逻辑)
每一批次的查询碱基总数 ≈ min(5e6, 单条序列平均长度 * 1000)
所以即使你给了 100 万条短 reads,每批也可能只处理几千条。这一块的峰值内存通常不会超过几百 MB。
一个小技巧:如果你想验证查询序列的实际缓冲区大小,可以在 blastn 命令行加上 -outfmt 11 并搭配 -verbose,日志里会打印出 batch 的划分细节。
2.4 运行时工作区 —— 线程数决定它的大小
这部分内存是 BLAST 在比对过程中动态申请的,用于存放比对得分矩阵、局部命中扩展数组、回溯路径等临时数据。
它的计算公式并不复杂:
运行时工作区 ≈ 线程数 × 每线程工作区大小
其中每线程工作区大小取决于:
- 比对的序列长度:越长所需扩展数组越大。
- 得分矩阵大小:例如 BLOSUM62 是 24×24 的矩阵,约 2.3 KB;而 PAM250 略大。
- 字长 (word size):字长越小,命中数越多,扩展步骤越多,工作区消耗也越大。
- 置换矩阵 (composition-based statistics):开启
-comp_based_stats会引入额外的权重表,每个线程多出几十 KB 开销。
用一张表格来看不同线程数下的运行时工作区估算:
| 线程数 | 每线程开销(典型值) | 运行时工作区总量 | 典型场景 |
|---|---|---|---|
| 1 | ~500 MB | ~500 MB | 单核调试,避免 OOM |
| 8 | ~500 MB | ~4 GB | 日常中等规模比对 |
| 32 | ~500 MB | ~16 GB | 高并发服务器,数据库大 |
| 64 | ~500 MB | ~32 GB | 极限吞吐,小心内存瓶颈 |
我曾经在 64 核机器上做过一次测试,数据库索引占了 80 GB,运行时工作区又吃了 32 GB,加上系统开销,直接把 128 GB 物理内存吃满,触发了 OOM Killer。后来我把线程数降到 48,才稳定住。
注意:这里的“每线程开销”不是固定不变的。如果查询序列长度波动很大(例如同时有 50 bp 和 100 kbp 的序列),BLAST 会对长序列分配更大的临时数组。此时用平均值估算会偏小。
2.5 操作系统与虚拟内存的干扰
除了 BLAST 自己申请的内存,操作系统层面还有两个因素会影响 OOM 的发生:
- mmap 机制:BLAST 在加载大数据库时会使用 mmap 将索引文件映射到虚拟地址空间。这个映射占用的虚存 (VSZ) 可能远大于物理内存 (RSS)。如果系统配置了
vm.overcommit_memory=2,BLAST 可能会因为虚拟地址空间不足而直接被拒绝分配。 - Overcommit 策略:我建议你在生产环境中检查这个值。如果是 0 或 1,BLAST 可以超额申请;如果是 2,系统会严格限制。很多 OOM 其实是这里出的问题,而不是 BLAST 本身的 bug。
检查 Overcommit 策略很简单:
$ cat /proc/sys/vm/overcommit_memory
0
$ cat /proc/sys/vm/overcommit_ratio
50
如果 overcommit_memory 是 2,系统只允许分配物理内存+交换空间的 50%(由 overcommit_ratio 控制)。这时候 BLAST 想 mmap 一个 30G 的索引文件,系统说“不行,你已经超了”,BLAST 就直接报内存分配失败。
一句话总结这个章节的核心:BLAST 内存大头在数据库索引(60%-80%),其次是运行时工作区(与线程数成正比),查询序列本身占得最少。OOM 的根源,十有八九是数据库建得太大,或者线程数拉得太高。