为什么个人站长也该认真对待 MongoDB
很多个人站长习惯了 MySQL,觉得 NoSQL 是"大厂才用得上的东西"。但如果你的站点有这些需求:评论系统要存嵌套结构、爬虫抓下来的 JSON 数据要塞进库、日志或埋点数据写多读少、想给博客加个全文搜索的辅助索引——硬用 MySQL 会写得很别扭。MongoDB 的文档模型天然适合"一条记录结构不固定"的场景,而且单机部署的资源占用并不比 MySQL 高多少,1 核 2G 的小 VPS 跑起来毫无压力。
但 MongoDB 的坑也不少,而且和 MySQL 的思维方式完全不同:它默认绑定 127.0.0.1、默认不开认证、数据目录一旦权限不对会拒绝启动、连接数模型是"每连接一线程"、写入默认是"安全但慢"的模式。这些默认值放在生产环境上,每一个都能变成一个事故。这篇文章把单机从部署到日常运维的完整链路讲清楚,全部是在 Ubuntu 22.04 / Debian 12 上实测过的步骤,不存在"理论上可以"。
部署前的三个硬性决定
在敲第一条命令之前,先把下面三件事定下来,否则后面改起来很痛苦。
第一,存储引擎选 WiredTiger(默认就是它)。老教程里提到的 MMAPv1 早在 MongoDB 4.0 就被移除了,现在不需要选。WiredTiger 支持文档级并发控制、压缩和检查点,是唯一选择。需要注意的是它默认会对集合数据做 Snappy 压缩,日志做 zlib 压缩——对 CPU 紧张的机器可以考虑关掉压缩换性能,但个人站基本用不上这个优化。
第二,数据目录独立挂载。把 /var/lib/mongodb 放到单独的数据盘或者单独的分区,不要和系统盘混在一起。原因有两条:一是 MongoDB 的 oplog 和 journal 写入很频繁,和系统盘抢 IO 会拖慢一切;二是备份时可以对整个目录做文件系统级快照,比逻辑导出快得多,也一致得多。如果暂时没有独立分区,至少确认 /var/lib/mongodb 所在分区留了足够的空间——MongoDB 在磁盘剩余空间低于某个阈值时会直接拒绝写入,这个阈值默认和你库的大小相关。
第三,决定要不要开认证。答案是:一定要开。MongoDB 4.0 之后默认不启用访问控制,任何人只要能连上端口就能读写所有数据。虽然默认只监听回环地址,但一旦哪天为了远程连接把 bindIp 改成 0.0.0.0,而认证又没开,那就是裸奔。正确顺序是:先装、先不配认证、用本地回环创建管理员账号、然后改配置开启 security.authorization: enabled、重启。顺序不能颠倒,否则会出现"开启认证后自己都进不去"的经典锁死。
安装与初始化:五步走完
以 Ubuntu 22.04 为例,官方源安装是最稳的路径。不要用系统自带的 apt install mongodb——那个包在很多发行版里是空的或者是很久以前的版本。
# 1. 导入官方 GPG 公钥
curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | \
sudo gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg --dearmor
# 2. 添加软件源(Ubuntu 22.04 对应 jammy)
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] \
https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | \
sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
# 3. 安装
sudo apt update && sudo apt install -y mongodb-org
# 4. 创建管理员(此时还没开认证,直接连就行)
mongosh --quiet --eval '
db.getSiblingDB("admin").createUser({
user: "siteadmin",
pwd: passwordPrompt(),
roles: [ { role: "userAdminAnyDatabase", db: "admin" } ]
})
'
# 5. 创建业务库的读写账号
mongosh --quiet --eval '
db.getSiblingDB("blog").createUser({
user: "bloguser",
pwd: "换成一个足够长的随机密码",
roles: [ { role: "readWrite", db: "blog" } ]
})
'注意第 4 步用了 passwordPrompt(),这样密码不会出现在 shell 历史和进程列表里。如果你在脚本里跑,那就用 heredoc 或者从环境变量读取,千万不要把明文密码写进 .bash_history。
创建完账号之后,编辑 /etc/mongod.conf,改动只有两处:
net:
port: 27017
bindIp: 127.0.0.1 # 除非确实需要远程访问,否则保持回环
security:
authorization: enabled # 这一行默认是注释掉的,去掉注释然后 sudo systemctl restart mongod。重启后验证:mongosh 进去执行 show dbs 应该报权限错误,加 -u siteadmin -p --authenticationDatabase admin 才能列出数据库。如果你连 show dbs 都不报错,说明认证没生效,检查配置文件里的缩进——YAML 对缩进敏感,authorization 必须和 security 保持正确的层级关系。
连接串怎么写给应用用
认证开启后,应用侧的连接串格式是:
mongodb://bloguser:密码@127.0.0.1:27017/blog?authSource=blog&retryWrites=true&w=majority三个参数必须理解:
authSource 指定到哪个库里做认证。这里账号是建在 blog 库里的,所以 authSource=blog。如果账号建在 admin 库,那就得写 authSource=admin。这是新手最常踩的坑:账号明明是对的,就是认证失败,99% 是 authSource 写错了。
retryWrites=true 让驱动在遇到网络抖动或主节点切换时自动重试可重试的写操作。单机环境看起来没用,但它能避免"网络瞬断导致一条写入静默丢失"的情况,保持开启。
w=majority 是写关注级别,表示"写入被多数节点确认后才返回"。单机环境下其实就是"写入被这一个节点确认",语义上比默认的 w=1 更严格一点点。真正的价值在于:如果你以后要加副本集,这个参数不用改,行为会自动升级成真正的多数确认。
日常运维:四个必须会的操作
一、看当前连接数和慢操作。
mongosh -u siteadmin -p --authenticationDatabase admin --eval '
print("连接数: " + db.serverStatus().connections.current);
print("可用连接: " + db.serverStatus().connections.available);
printjson(db.currentOp({ "secs_running": { $gt: 2 }, "active": true }));
'MongoDB 默认最大连接数是 65536,看起来很大,但每个连接都会占用一个线程和大约 1MB 的栈空间。个人站上如果看到 current 长期在几百以上,先怀疑应用侧的连接池配置——很多驱动的默认池大小是 100,而框架可能在每个请求里创建一个新客户端。正确的做法是全局单例一个客户端实例,整个应用共享。
二、看集合的存储占用和索引大小。
mongosh -u bloguser -p --authenticationDatabase blog blog --eval '
db.stats(1024*1024);
db.comments.stats(1024*1024).indexSizes;
'MongoDB 有个和 MySQL 不同的特点:删除文档不会释放磁盘空间,只是把空间标记为可复用。所以你会看到"库里明明只剩几千条数据,磁盘却占了 2G"。这不是故障,是正常行为。真要回收空间得用 compact 命令,但它会阻塞该集合的读写,要在维护窗口做,而且对 WiredTiger 来说效果通常有限——最彻底的方式是 mongodump 出来再 mongorestore 回去。
三、清理过期数据用 TTL 索引,不要写定时任务。这是 MongoDB 相对 MySQL 的一个明显优势。给存日志、验证码、会话的集合加一个 TTL 索引,MongoDB 会自己后台清理过期文档:
db.sessions.createIndex(
{ "createdAt": 1 },
{ expireAfterSeconds: 86400, name: "ttl_session_1d" }
)注意三个细节:TTL 索引只对 Date 类型字段生效,存成字符串或时间戳整数都不会清理;后台清理线程每 60 秒跑一次,所以文档会在过期后一分钟内被删除,不保证精确;TTL 索引不能建在已经有 TTL 索引的字段上,重复建会报错。有了它,你就不用再写那些容易忘、容易失败的 deleteMany 定时任务了。
四、用 mongodump 做逻辑备份。
mongodump --uri="mongodb://bloguser:密码@127.0.0.1:27017/blog?authSource=blog" \
--gzip --archive=/backup/mongo/blog-$(date +%F).archive
# 恢复(先确认目标库名,--drop 会清空现有数据)
mongorestore --uri="mongodb://bloguser:密码@127.0.0.1:27017/blog?authSource=blog" \
--gzip --archive=/backup/mongo/blog-2026-09-30.archive --drop--gzip --archive 组合把整个库打成单个压缩文件,比默认的"每个集合一个 bson 文件"好管理得多,也方便走 rsync 或对象存储。恢复时 --drop 会先删掉目标库里的同名集合再导入,做演练时一定要用另一个库名试,别在线上直接跑。
备份脚本里务必加一步校验:mongorestore --archive=... --dryRun 可以在不写入的情况下验证归档是否可读。一个从来没验证过能恢复的备份,等于没有备份。
权限最小化:不要用管理员账号跑应用
很多教程图省事,直接让应用用 admin 库的 root 角色连库。这是最危险的习惯。一旦应用的某处查询存在注入漏洞,攻击者拿到的是整个实例的完全控制权,可以 db.dropDatabase()、可以读走其他所有库。
正确的做法是给每个应用一个专属账号,权限只覆盖它需要的那一个库:
// 只读账号,给报表或者只读接口用
db.getSiblingDB("blog").createUser({
user: "blogreader",
pwd: "另一个强密码",
roles: [ { role: "read", db: "blog" } ]
})
// 需要读写但不需要管理权限的账号
db.getSiblingDB("blog").createUser({
user: "blogapp",
pwd: "应用专用密码",
roles: [ { role: "readWrite", db: "blog" } ]
})角色选择上记住这几档:read 只能查;readWrite 能增删改但只能操作数据;dbAdmin 能管索引和集合结构但不碰数据;userAdmin 管账号;clusterMonitor 只能看监控指标。应用只需要 readWrite,监控采集器只需要 clusterMonitor 加 read。把审计和告警用的账号也单独分开,一旦出事,日志能告诉你到底是哪个账号在做什么。
几个容易误判的"故障"
"启动失败,数据目录权限不对"。MongoDB 的 systemd 单元里指定了 User=mongodb,如果 /var/lib/mongodb 或 /var/log/mongodb 的属主被改成了 root(比如你手动复制过数据,或者用 root 跑过恢复),它就会拒绝启动。sudo chown -R mongodb:mongodb /var/lib/mongodb /var/log/mongodb 即可。SELinux 或 AppArmor 环境还要额外处理上下文,Ubuntu 的 AppArmor 策略通常随包自动安装,不用手动配置。
"连接被拒绝但端口是通的"。先确认 bindIp。默认是 127.0.0.1,从另一台机器连必然是 Connection refused。如果确实需要远程访问,正确做法是不要改 bindIp,而是在应用服务器上用 SSH 隧道:ssh -N -L 27018:127.0.0.1:27017 user@dbserver,然后应用连本地 27018。这样数据库端口永远不暴露在公网上,比开防火墙规则安全得多,也避免了 MongoDB 暴露公网被扫描到然后勒索的经典事故。
"写入变慢了"。先看是不是索引缺失导致的集合扫描。db.comments.find({postId: 123}).explain("executionStats"),如果 totalDocsExamined 远大于 nReturned,就是缺索引。给每个高频查询字段建索引,但记住:索引会拖慢写入,因为每次写都要更新索引。个人站的经验法则是"查询慢就加索引,写入慢就查索引是否过多",一般单表 5 个以内的索引是安全的。另外 explain 之外还要看 db.currentOp(),排除是不是有长事务或全表扫描卡住了锁——WiredTiger 是文档级锁,不同文档的写入不会互相阻塞,但集合级的 DDL(比如 createIndex)会。
和 MySQL 共存的资源分配建议
如果同一台机器上已经跑了 MySQL,再装 MongoDB,需要留意内存。MongoDB 的 WiredTiger 默认会使用"可用内存的一半减 1GB"作为缓存上限,最少 256MB。在 2G 内存的机器上,如果 MySQL 已经吃掉了 800M,加上 PHP-FPM 和 Nginx,MongoDB 很容易触发 OOM Killer。
解决办法是在配置文件里显式限制缓存大小:
storage:
wiredTiger:
engineConfig:
cacheSizeGB: 0.5给 MongoDB 分 512MB 缓存,对个人站的文档量来说完全够用。同时把 swappiness 调低(vm.swappiness=10),避免系统在内存紧张时把数据库的热数据换出到磁盘导致性能断崖。做完这两件事,再观察几天的 free -m 和 dmesg | grep -i oom,确认没有 OOM 记录,这套共存配置就算稳了。
小结
MongoDB 在个人站场景下最大的价值不是"性能",而是"数据模型的自由度":嵌套的评论、结构不固定的爬虫数据、随时要加字段的埋点记录,用文档模型写起来比关系表舒服一个数量级。代价是你必须自己把安全关做扎实——开认证、限权限、不开公网端口,这三条是底线。备份上优先用 mongodump --gzip --archive 出单文件,配合一次真恢复演练;清理过期数据优先用 TTL 索引而不是定时任务。把这几点做到位,MongoDB 就是一个非常省心的组件。