使用 rclone 将本地文件通过 OpenList 的 WebDAV 接口上传到夸克网盘时,OpenList 的内存占用可能明显增长,甚至触发容器 OOM。排查后发现,关键在于夸克驱动上传前的整文件缓存,以及容器统计中包含的文件页缓存。
本文分析针对 OpenList v4.2.7、普通夸克驱动、Docker 和 cgroup v2。其他驱动、其他版本应重新核对实现。
夸克上传为什么需要缓存
OpenList 的 WebDAV 入口把 HTTP 请求体包装成文件流,再交给目标存储驱动。这一步本身不统一将整个文件读入内存。
但普通夸克驱动会先取得文件的 MD5 和 SHA1。WebDAV 上传路径没有向文件对象提供这两个哈希,因此驱动调用:
stream.CacheFullAndWriter(&up, io.MultiWriter(writers...))
这个调用在计算哈希的同时,缓存完整文件。随后,驱动向夸克提交预上传和哈希请求;如果不能秒传,再从缓存中读取数据、分片上传。
哈希计算可以使用很小的内存,真正占空间的是为后续回读而保留的文件内容。 即使夸克支持分片上传,也不代表前面的整文件缓存不存在。
v4.2.7 的 HybridCache 有内存和文件两类后备方式。允许使用内存时,会尝试扩充缓存,分配受限后再切换至文件;其中部分内存通过匿名 mmap 管理。因此不能仅靠调整 Go GC 参数约束所有缓存占用。
rclone 的 --transfers 控制同时传输的文件数。两个文件并发上传,意味着两份整文件缓存可能同时存在。降低 rclone 自身的 --buffer-size 并不能直接限制 OpenList 进程中的这部分缓存。
为什么有临时目录仍占用内存
例如:
"temp_dir": "data/temp"
这只指定临时文件保存位置,不强制每次缓存都写入磁盘。该相对路径还要结合 OpenList 的工作目录、Docker 挂载判断实际位置。
此外,v4.2.7 的缓存扩容检查使用系统可用内存统计,没有在该检查中直接读取容器的 memory.max 和 memory.current。这使得宿主机尚有空闲内存时,容器仍可能先撞上自己的内存限额。
改用磁盘临时文件
先暂停上传并备份配置,在 config.json 顶层设置:
"min_free_memory": -1
这是字段片段,不是完整配置文件,修改时应保留其他内容和合法的 JSON 结构。
v4.2.7 的初始化代码将负值解释为关闭 HybridCache 内存缓存。0 会自动计算阈值,并不表示禁用。这个调整只改变对应缓存机制,不是整个进程的内存硬上限。
确保 temp_dir 落在真实磁盘上,并有足够空间。若临时目录位于 tmpfs,所谓文件缓存仍会消耗内存,达不到预期效果。缓存完整文件还意味着需要为并发文件预留足够的磁盘空间。
确认 Docker 工作目录和挂载:
container='openlist'
docker inspect -f '工作目录={{.Config.WorkingDir}}
{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}' "$container"
在已确认工作目录为 /opt/openlist、临时目录为 data/temp 的部署中,可检查:
docker exec openlist df -h /opt/openlist/data/temp
其他部署应替换为实际路径。完成配置后重启:
docker restart openlist
docker logs --tail 100 openlist 2>&1 |
grep -E 'disable memory cache|min free memory|auto memory limit'
看到以下日志表示配置已生效:
disable memory cache
此时即使仍打印 auto memory limit: 4MB,也不代表重新启用了该缓存。若没有生效,应检查是否编辑了实际使用的文件,以及是否有环境变量覆盖;变量前缀应以实际启动配置为准。
如何判断内存是否真的改善
不能只看 memory.current。它包含匿名内存、文件页缓存以及其他计费内存;改用磁盘文件后,Linux 仍会利用 RAM 缓存文件数据。
以下命令在 Docker 宿主机执行,适用于 cgroup v2。容器名按实际修改:
(
container='openlist'
pid=$(docker inspect -f '{{.State.Pid}}' "$container") || exit 1
[ "${pid:-0}" -gt 0 ] || { echo '容器未运行'; exit 1; }
cg=$(awk -F: '$1 == "0" {print $3}' "/proc/$pid/cgroup")
[ -n "$cg" ] || { echo '未找到 cgroup v2 路径'; exit 1; }
dir="/sys/fs/cgroup$cg"
awk '
$1 ~ /^(anon|file|shmem|file_dirty|file_writeback|kernel|sock)$/ {
printf "%-16s %.1f MiB\n", $1, $2/1048576
}
' "$dir/memory.stat"
cat "$dir/memory.events"
docker stats --no-stream "$container"
)
一次优化后的实测结果为:
| 指标 | 用量 |
|---|---|
anon | 26.8 MiB |
file | 131.4 MiB |
sock | 5.5 MiB |
shmem | 0.0 MiB |
file_dirty | 131.3 MiB |
file_writeback | 0.0 MiB |
| Docker 显示内存 | 36.2 MiB |
这说明该时刻匿名内存较低,主要占用来自文件页缓存,其中大部分尚未写回磁盘。file_dirty 和 shmem 等是细分项,不能再次加到 file 上。
file_writeback=0 仅代表采样瞬间正在写回的数据为零,不代表文件未使用磁盘。脏页需要先写回,才能被回收,不能将所有文件缓存视为可立即释放。
Docker CLI 在 Linux 上会扣除部分文件缓存,因此它显示的内存可能明显低于 memory.current。两者用途不同,应结合 memory.stat 判断。
这组结果支持缓存策略调整有效,但单次样本不能证明整个运行周期都没有泄漏。如果怀疑异常,应比较相同任务多次运行时的 anon、总用量和 OOM 事件,而不是仅凭上传结束后内存未立即下降下结论。
根据负载设置容器限额
先完成有代表性的高负载同步,记录总内存峰值。如果内核提供 memory.peak,可读取它;否则每秒采样 memory.current 只能得到观察期间的近似峰值,可能漏过短暂尖峰。
本次调整后曾观测到约 478 MiB 的采样峰值,因此可以将 768 MiB 作为试运行起点:
docker update --memory 768m openlist
该限额尚需完整负载验证,不是所有环境的推荐固定值。若 Docker 提示现有交换空间配置冲突,应先检查 memory-swap 设置并决定策略。
使用 Compose 时,将限额合并到原有服务:
services:
openlist:
# 保留原有镜像、挂载和网络设置
mem_limit: 768m
不要按 Docker 显示的几十 MiB 将限额压得过低,文件缓存同样计入 cgroup 内存限额。设置后应观察 memory.events 中的 oom、oom_kill 是否增加,以及上传是否失败或明显变慢。若负载较重,可先将 rclone Transfers 降为 1 做对比。