mongofiles 不适合迁移大量小文件,因其单线程、无重试、不支持自定义元数据,且路径含特殊字符时会静默失败;应使用 GridFSBucket 实现可控流式写入,并为多线程单独创建 MongoClient。

为什么不能直接用 mongofiles 迁移大量小文件
因为 mongofiles 是单线程、无重试、不支持自定义元数据写入的命令行工具,面对上万个小于 1MB 的日志或图片文件时,吞吐极低,且无法注入 filename 以外的业务字段(比如 upload_time、source_path)。更关键的是,它会把原始路径当作 _id 字符串处理,一旦路径含特殊字符(如 /、\0),插入直接失败并静默跳过。
用 gridfs.GridFSBucket 而非 gridfs.GridFS 写入更可控
GridFS 类封装了 chunk 自动切分逻辑,但隐藏了文件句柄和元数据写入时机,出错时难以定位是 metadata 还是 chunk 写失败;GridFSBucket 则暴露 open_upload_stream() 和 upload_from_stream(),允许你精确控制流式写入、设置 chunk_size_bytes、传入完整 metadata 字典,并在异常时安全关闭未完成的上传。
实操建议:
- 用
GridFSBucket(db, bucket_name="assets")显式指定 bucket 名,避免默认fs冲突 - 对每个文件调用
bucket.open_upload_stream(filename=..., metadata={...}),而非先构造BytesIO再传入 —— 后者会把整个文件读进内存,对 >100MB 文件极易 OOM -
metadata中避免嵌套 dict 或 datetime 对象,MongoDB 驱动只自动序列化顶层的datetime、ObjectId,其余需手动转成字符串或 ISO 格式
多线程下必须为每个线程创建独立 MongoClient
PyMongo 的 MongoClient 实例是线程安全的,但内部连接池共享 socket 和认证状态。当多个线程共用一个 client 并并发调用 open_upload_stream() 时,会出现 write concern 丢失、chunk 插入乱序、甚至部分文件写入后 files 文档存在而 chunks 文档为空 —— 这是驱动在高并发下对同一个 socket 复用 chunk 写请求导致的状态竞争。
正确做法:
- 在线程函数内初始化
client = MongoClient(host, maxPoolSize=1),设maxPoolSize=1防止单线程占满连接池 - 不要把 client 当参数传入线程,也不要使用全局 client 变量
- 迁移完务必调用
client.close(),否则进程退出前可能残留未 flush 的 chunk 缓冲
校验阶段最容易忽略 uploadDate 和 length 的一致性
迁移脚本跑完不代表数据可靠:常见错误是文件内容写入成功,但 files 集合里 length 字段比实际字节数少几个字节(因流未完全 flush),或 uploadDate 比系统时间晚几分钟(因 MongoDB 服务端时钟不同步)。这类问题在应用层读取时表现为“文件能打开但末尾截断”或“按时间范围查询漏掉最新文件”。
建议在校验逻辑中加入:
- 对每个已迁移文件,用
bucket.open_download_stream(file_id)读取全部内容并计算len(),与files.length比对 - 用
bucket.find({"filename": ...}, limit=1)查出文档,检查uploadDate是否在当前时间 ±30 秒内 - 跳过软链接和权限不足的源文件,记录到
migration_errors.log,而不是抛出异常中断整个线程
真正麻烦的是跨时区路径名和 UTF-8 BOM 文件头 —— 它们不会报错,但会导致后续应用层 gridfs.find_one({"filename": "测试.txt"}) 查不到,因为存储时 filename 字段实际是 b'\xef\xbb\xbf\xe6\xb5\x8b\xe8\xaf\x95.txt'。这种只能靠预扫描清洗,没法在写入时自动修正。











