网站通知邮件发不出去,先分清是「发不出」还是「发着堵住了」
自建邮件服务或者用服务器直发通知的站长,迟早会遇到这种状况:网站注册验证码收不到、备份完成通知没动静、后台的告警邮件全没了。上服务器一看,postfix 服务明明在跑,systemctl status postfix 一片绿色,日志里也没有红色报错。
这类问题的共同点是:故障发生在队列里,而不是在服务里。Postfix 的设计是「收下邮件就返回成功」,然后把实际的投递拆成异步任务去做。也就是说,当上游 SMTP 连不上、被对方限速、或者 DNS 解析出问题时,Postfix 会老老实实把邮件排进队列,每隔一段时间重试一次——服务状态永远正常,邮件却永远发不出去。而队列会一直积压,直到磁盘被塞满,那时候就是「邮件故障」升级成「全站故障」了。
这篇文章按「看队列 → 判读队列里的报错 → 定位根因 → 清理与限流 → 防止复发」来讲,覆盖最常踩的几个坑。
第一步:用 mailq / postqueue 看懂队列里到底有什么
mailq
# 完全等价于
postqueue -p输出格式是这样的:
-Queue ID- --Size-- ----Arrival Time---- -Sender/Recipient-------
A3F2B41C98 4821 Tue Sep 28 10:14:02 no-reply@example.com
(connect to mx1.gmail.com[142.250.x.x]:25: Connection timed out)
someone@gmail.com
C7D1E90A33 3910 Tue Sep 28 10:15:47 no-reply@example.com
(Host or domain name not found. Name service error for name=mx.oldcustomer.com type=A: Host not found)
bad@oldcustomer.com
这里有四个字段必须会读:
- Queue ID:每个队列条目的唯一标识,后面所有针对单封邮件的操作都用它。注意它不是文件名——实际文件在
/var/spool/postfix/下按队列名分目录存放。 - Size:邮件大小(含附件)。用这个可以快速看出队列是被少数几封大邮件撑起来的,还是海量小邮件堆积。
- Arrival Time:入队时间。这是判断「正常重试」还是「已经堵死」的最重要依据。如果队列里全是几分钟前的邮件,那是正常重试;如果有一堆三天前的邮件还挂着,说明投递链路确实坏了。
- 括号里的那行:最后一次投递尝试的失败原因。这行才是真正有用的信息,下面会详细讲怎么读。
只是想知道队列长度:
postqueue -p | tail -1
# 输出形如:-- 1283 Kbytes in 47 Requests.想在监控里直接取数字(比解析 mailq 稳得多):
postqueue -p | tail -1 | awk '{print $5}'注意不同 Postfix 版本的 tail -1 行格式略有差异(有的是 -- N Kbytes in M Requests.,有 Kbytes 有的写 KiloBytes),字段位置也会变。所以监控脚本里更稳的做法是取「Requests 前面的那个数字」,而不是写死字段序号:
postqueue -p | tail -1 | grep -oE '[0-9]+ Requests' | grep -oE '^[0-9]+'如果队列是空的,postqueue -p 会输出 Mail queue is empty,这时上面的 awk 会返回空字符串,监控脚本要能容忍这个情况,否则会误报成 0 或者把空值当成整数去比较而报错。
第二步:判读失败原因——五种高频报错分别意味着什么
队列里那行括号内容,是 Postfix 从上游 SMTP 服务器或 DNS 那里拿到的真实反馈。把它按类别分清,根因基本就浮出来了。
报错一:Connection timed out / Connection refused
最常见的两类。区别很关键:
Connection timed out—— 包发出去了没有回应。这几乎必然是防火墙或云厂商封锁了 25 端口出站。AWS、GCP、阿里云、腾讯云等主流云厂商对 25 端口的出站流量默认封锁或需要提交工单解封,这不是配置问题。Connection refused—— 对方明确拒绝了。可能是对方丢弃连接(很多自建邮件服务为了反垃圾会直接 RST),也可能是你连错了端口。
验证封锁最快的方法是直接用 telnet 或 nc 测试出站 25:
nc -zv -w 5 mx1.gmail.com 25
nc -zv -w 5 mx1.gmail.com 587如果 25 卡死而 587 秒通,基本可以确认是 25 出站被封。这种情况的解法不是改 Postfix 配置,而是改用 587/465 通过第三方中继(relayhost)投递,或者申请解封(通常需要企业资质,个人很难)。
报错二:Host or domain name not found
DNS 解析失败。可能的原因有三层,从近到远排查:
# 1. Postfix 能不能解析(它有自己的 chroot 和 resolver 配置)
postmap -q example.com dns:mx1.gmail.com
# 2. 系统解析器能不能解析
dig +short MX gmail.com
dig +short MX oldcustomer.com # 看目标域是否真的没有 MX
# 3. 有没有配 DNSBL(反垃圾黑名单)导致解析路径被劫持
grep -r dnsbl /etc/postfix/一个很隐蔽的坑:Postfix 在 chroot 模式下运行时,它看到的 /etc/resolv.conf 是 /var/spool/postfix/etc/resolv.conf 的副本,而不是系统的那份。如果系统换了 DNS 服务器而没同步这个副本,就会出现「系统能解析、Postfix 不能」的诡异现象。修复方式:
cp /etc/resolv.conf /var/spool/postfix/etc/resolv.conf
postfix reload报错三:450 4.7.1 Greylisted / 451 Temporary failure
这类 4xx 是临时性拒绝,Postfix 会自动重试,属于正常行为不是故障。灰名单(greylisting)机制本身就要求发件方重试,所以你会看到队列里同一封邮件反复出现、Queue ID 不同、每次都带 450。看到 4xx 不要急着清队列,等重试周期走完(默认 5 分钟、10 分钟、20 分钟递增,最长 4000 秒)通常就自动投递出去了。
但要注意区分:如果同一封邮件重试了几十次都还是 450/451,那对方的策略可能已经把你拉黑,或者是你的 IP 信誉有问题。
报错四:550 5.7.1 SPF/DKIM/DMARC 类拒绝
被明确判定为垃圾邮件。这类 5xx 是永久性失败,Postfix 会立刻退回给发件人并生成退信,不会长时间占用队列。但它是「邮件收不到」这个问题的头号原因。三条基础检查:
# SPF 记录
dig +short TXT example.com | grep spf1
# DKIM 选择器(常见的默认选择器名)
dig +short TXT default._domainkey.example.com
# DMARC 策略
dig +short TXT _dmarc.example.com特别注意一个高频配置错误:SPF 记录里 include: 的机制数量是有上限的(RFC 规定最多 10 次 DNS 查询,超出会直接判为 permerror)。如果你的 SPF 里堆了七八个 include:,又加了第三方邮件服务,很容易超限。精简 SPF 往往是解决「有时能发有时不能」的关键——因为不同接收方对超限的容忍度不一样。
报错五:mail for xxx loops back to myself
这个报错很特别,它不是网络问题,而是本地配置把邮件又绕回自己了。典型场景:你把某个域设成了 virtual_alias_domains 同时又配了 relay_domains,或者 mydestination 里包含了应该外发的域。检查:
postconf -n | grep -E 'mydestination|relay_domains|virtual_alias_domains|virtual_mailbox_domains'原则是:同一个域只能在一个分类里出现。一个域名同时出现在 mydestination 和 virtual_mailbox_domains 里,Postfix 就会陷入「我要投给它,但它就是我自己」的循环。
第三步:定位是哪一封/哪一类邮件在拖垮队列
队列积压几百封时,逐个看 mailq 是不现实的。按维度统计才有效率。
按收件域统计(找出「堵在谁那里」)
postqueue -p | grep -oE '@[A-Za-z0-9.-]+' | sort | uniq -c | sort -rn | head -20如果前几名全是同一个域(比如某个客户的旧域名已经不存在了),那就是单一目标域导致的队列膨胀,处理起来很单纯。
按发件人统计(找出「是谁在发」)
postqueue -p | grep -oE '^[A-Z0-9]{9,15} +[0-9]+ +[A-Z][a-z]{2} [A-Z][a-z]{2} +[0-9]+ [0-9:]+ +[^ ]+@[^ ]+' | awk '{print $NF}' | sort | uniq -c | sort -rn | head如果某个发件人占了绝大多数,要考虑是不是被拿来做中继了(开放中继是严重安全问题),或者某个功能在疯狂发通知(比如某个循环 bug 每请求发一封邮件)。
按错误类型统计(找出「根因是什么」)
postqueue -p | grep -oE '\(.*\)' | sed 's/[0-9]\+/N/g' | sort | uniq -c | sort -rn | head -10上面的 sed 's/[0-9]\+/N/g' 是把报错里的数字(IP、时间、错误码)统一替换成 N,这样同一类错误会聚合成一行。这一步能在十秒内告诉你「绝大多数邮件都卡在哪个原因上」,是所有排查里性价比最高的一条命令。
第四步:清理队列——分清「删除」和「重投」
定位清楚之后,才轮到清理。这里必须按「重投」和「删除」两条路分开处理。
安全的重投
# 让 Postfix 立刻重试所有队列条目(不删除)
postqueue -f
# 只重投某一封
postqueue -i A3F2B41C98postqueue -f 是最常用的一个操作:如果你刚修好 relayhost 或 DNS,跑它比等重试周期快得多。注意它会让所有邮件同时开始投递,如果队列很大,可能瞬间打满出站带宽或被对方判定为异常——队列超过几千封时,更稳的做法是先限制并发:
postconf -e 'default_destination_concurrency_limit = 2'
postconf -e 'smtp_destination_concurrency_limit = 2'
postfix reload
postqueue -f按条件删除
Postfix 提供了 postsuper 做队列维护,但它只能按 Queue ID 或按队列分类操作,不能按收件域删除。所以按条件删除要先拿到 ID 列表:
# 找出所有发给某个不存在域名的邮件,删掉
postqueue -p | grep -B1 'oldcustomer.com' | grep -oE '^[A-Z0-9]{9,15}' > /tmp/purge.txt
wc -l /tmp/purge.txt
# 确认列表没问题后再删
postsuper -d - < /tmp/purge.txt这里的 postsuper -d - 表示从标准输入读取 Queue ID 列表。务必先 wc -l 确认数量级符合预期——这条命令没有确认提示,列表搞错了就直接删掉真实邮件。个人建议是先把列表打印出来肉眼扫一遍再执行。
还有一个更狠的操作,除非你确定所有队列邮件都没有价值,否则别用:
postsuper -d ALL # 删除所有队列邮件(危险)清理时必须同时看磁盘
队列积压往往伴随磁盘告警,而且很容易踩到一个经典陷阱:删除队列里的文件之后,df 显示的可用空间没变。原因是 Postfix 进程(或长期不重启的 qmgr)还持有已删除文件的句柄。判读方法:
df -h /
lsof +L1 | grep -i postfix | headlsof +L1 会列出所有「link count 为 0 但仍有进程打开」的文件。如果这里能看到大量 /var/spool/postfix 下的文件,那么空间是在 postfix reload 或重启之后才会真正释放。这个现象和「容器日志删了但空间不还」是同一个成因,处理思路也一样。
第五步:别让队列再撑爆——四道防线
修好故障之后,真正该做的是让它不会再演变成全站事故。
防线一:队列长度监控 + 阈值告警
最简单的做法是放进 /etc/cron.d/,每十分钟跑一次,超过阈值就推消息:
*/10 * * * * root /usr/local/bin/check-mailq.sh脚本的核心判断就是把前面那条取数字的命令包一层比较。阈值建议按业务量设:通知型站点超过 100 封就该看,超过 500 封必须处理。关键是告警要能真的送达——如果告警走邮件,而邮件链路正好是坏的那个环节,告警就等于没有。所以队列告警应该走 Telegram 机器人、钉钉这类独立通道,不要走 SMTP。
防线二:给队列本身设容量上限
Postfix 有两个参数控制队列不会无限制膨胀:
postconf -e 'message_size_limit = 10240000' # 单封邮件上限 10MB
postconf -e 'mailbox_size_limit = 0' # 0 表示不限(本地投递)
postconf -e 'qmgr_message_active_limit = 20000' # 活跃队列上限
postconf -e 'qmgr_message_recipient_limit = 20000' # 收件人总量上限message_size_limit 尤其重要:默认值只有 10MB 左右,但如果有人用它发大附件(比如网站把整个数据库备份通过邮件发出去),一封邮件就能堵住很久。反过来,如果你确实需要发大附件,也要同时检查接收方的限制,别在 25MB 上限那里反复重试。
防线三:出站走中继,别直连
对于个人站长,直连 25 端口发信基本是一条死路:家用宽带和多数云厂商都封 25,且新 IP 没有信誉积累,邮件大量进垃圾箱。正确姿势是配一个第三方中继:
postconf -e 'relayhost = [smtp.example-relay.com]:587'
postconf -e 'smtp_sasl_auth_enable = yes'
postconf -e 'smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd'
postconf -e 'smtp_sasl_security_options = noanonymous'
postconf -e 'smtp_tls_security_level = encrypt'
postconf -e 'smtp_use_tls = yes'
# 生成密码表并收紧权限(这一步不能省)
postmap /etc/postfix/sasl_passwd
chmod 600 /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db
postfix reloadchmod 600 那一步是安全底线——sasl_passwd 里是明文凭据,权限放开等于把中继账号公开。Postfix 会在权限过宽时拒绝启动或报警,这是它的保护机制,别去绕过它。
防线四:给队列目录单独规划空间
/var/spool/postfix 和网站数据放在同一个分区时,队列撑爆就是网站挂掉。有条件的话把 /var 单独分区,或者至少让监控把 /var/spool/postfix 的占用单独统计出来——df 只能告诉你分区满了,du -sh /var/spool/postfix/* 才能告诉你是不是队列干的。
小结
Postfix 队列故障的处理逻辑,可以用一句话概括:服务正常不代表邮件正常,先看队列,再看队列里的报错。
postqueue -p看队列,重点读 Arrival Time(判断是正常重试还是真堵)和括号里的失败原因。- 失败原因按类型分:
Connection timed out→ 25 端口被封,改走中继;Host not found→ 检查 chroot 下的resolv.conf;4xx→ 临时重试,别急着清;5xx SPF/DKIM→ 检查 DNS 记录和 include 次数;loops back to myself→ 同一个域别放在两个分类里。 - 清理分「重投」(
postqueue -f)和「删除」(postsuper -d),按条件删除必须先wc -l核对列表。 - 队列大不是终点,磁盘满才是。删除后
df不还空间,用lsof +L1查 Postfix 持有的已删除句柄。 - 收尾一定要做监控和限额,尤其是把队列告警放到独立于邮件的通道上——否则告警和故障一起消失。
最后提醒一句:如果你的队列里出现大量发往陌生域名的邮件,不要只当成队列问题看,那可能是开放中继被人利用,属于安全事件。这时候排查顺序要反过来——先关掉 relay_domains 和可能开放的中继权限,再清理队列,最后才是追查入口。