Postal 日常运维与排障手册
Postal 3.3.7 日常运维与排障手册:日常巡检、队列管理(重投/清空/挂起释放)、日志与审计、监控指标、备份与恢复、升级回滚、投递失败排查(SoftFail/HardFail/抑制名单),全部操作经 3.3.7 实测
Postal 日常运维与排障手册
目标:为已上线的 Postal 3.3.7 提供日常运维闭环——日常巡检、消息队列管理(重投/清空/挂起释放/抑制名单)、日志与审计、Prometheus 指标、备份与恢复、升级与回滚,以及投递失败(SoftFail/HardFail)的完整排查路径。已在 Postal 3.3.7(Ubuntu 22.04,部署形态见《Postal Docker Compose 部署指南》,资源初始化见《Postal 快速入门与实战示例》)实测通过,全部命令与回显为真实执行结果。
一、环境信息
本节列出手册操作所依赖的环境基准与默认约定。
- Postal 版本:3.3.7;操作系统 Ubuntu 22.04.4 LTS(x86_64)
- 服务进程:web(127.0.0.1:8124)、smtp(*:25,健康检查 127.0.0.1:9091)、worker(健康检查 127.0.0.1:9090)
- 数据库:MariaDB 11.4(127.0.0.1:13306,账号 root / q1w2e3r4),主库 postal + 消息库 postal-server-1
- 管理员:admin@postal.example.com / q1w2e3r4;邮件服务器:Example Organization → Main Server(main-server)
- 出站口径:postal.smtp_relays 指向内部中继 127.0.0.1:2525(本环境为验证用调试接收器;无中继环境直接投递 MX,排查思路相同)
- 配置目录:/opt/data/quzuo/postal-3.3.7/config(postal.yml / signing.key)
💡 说明:手册中的队列/消息操作给出两个层面——Web 界面(适合日常人工处理)与 postal console 数据操作(适合批量与自动化),两者操作的是同一套数据。所有 Ruby 片段在
postal console中执行(官方文档收录的命令入口)。
二、日常巡检
本节给出每天应执行的最小巡检集:进程状态、监听端口、健康端点、队列水位与磁盘,全部命令可直接粘贴执行。
2.1 服务与端口巡检
postal status
NAME IMAGE COMMAND SERVICE CREATED STATUS
postal-smtp-1 ghcr.io/postalserver/postal:3.3.7 "docker-entrypoint.p…" smtp 3 hours ago Up 3 hours
postal-worker-1 ghcr.io/postalserver/postal:3.3.7 "docker-entrypoint.p…" worker 3 hours ago Up 3 hours
postal-web-1 ghcr.io/postalserver/postal:3.3.7 "docker-entrypoint.p…" web 3 hours ago Up 3 hours
ss -ltn | grep -E ':(8124|9090|9091|25)\s'
LISTEN 0 2048 127.0.0.1:8124 0.0.0.0:*
LISTEN 0 2048 127.0.0.1:9090 0.0.0.0:*
LISTEN 0 2048 127.0.0.1:9091 0.0.0.0:*
LISTEN 0 2048 *:25 *:*
两个健康端点返回 OK 即进程级健康(实测回显):
curl -s http://127.0.0.1:9090/health && echo; curl -s http://127.0.0.1:9091/health && echo
OK
OK
2.2 队列水位巡检
队列积压是投递链路异常的第一信号。查看当前队列与重试计划(实测回显):
mysql -h127.0.0.1 -P13306 -uroot -pq1w2e3r4 -e \
"SELECT id,message_id,domain,attempts,retry_after,locked_by FROM postal.queued_messages;"
id message_id domain attempts retry_after locked_by
💡 说明:查询返回空集说明队列已全部清空,属健康状态。若出现大量 locked_by 为空且 retry_after 晚于当前时间的行,说明在等待重试周期;locked_by 非空表示 worker 正在处理。本环境此前的历史队列行在投递完成后自动清除。
2.3 数据库与磁盘巡检
mysql -h127.0.0.1 -P13306 -uroot -pq1w2e3r4 -e \
"SELECT table_schema, ROUND(SUM(data_length+index_length)/1024/1024,1) AS size_mb FROM information_schema.tables WHERE table_schema LIKE 'postal%' GROUP BY table_schema;"
table_schema size_mb
postal 1.9
postal-server-1 1.3
df -h /opt/data/quzuo/postal-3.3.7 | tail -1
/dev/sda3 786G 277G 475G 37% /
⚠️ 注意:消息库随原始邮件(raw-YYYY-MM-DD 表)持续增长,是磁盘占用的主要来源;raw 表保留期默认 30 天/2048 MB(postal.yml 可调),到期由 worker 自动清理。巡检时若 size_mb 增长异常,优先核对 retention 配置(见 7.4 节)。
三、队列管理
本节覆盖队列的日常操作:查看、立即重投、挂起与释放、清空(取消)、抑制名单。队列主表在主库 postal.queued_messages,消息与投递详情在消息库 postal-server-1 的 messages/deliveries 表。
3.1 队列查看与重试机制
Postal 对投递失败的重试策略(官方默认,postal.yml 可调):最多 18 次尝试,退避因子 1.3(每次间隔 = 5 分钟 × 1.3^尝试次数),超过后按收件域做 HardFail。队列行的关键字段:
| 字段 | 含义 |
|---|---|
| message_id | 消息库中消息记录的 id |
| domain | 收件域名(按域分批投递的依据) |
| attempts | 已尝试次数 |
| retry_after | 下次尝试时间(NULL 表示待立即处理) |
| locked_by / locked_at | 当前处理的 worker 标识(非空=处理中) |
3.2 立即重投(单条)
对处于 SoftFail 等待重试的消息,可以跳过退避周期立即重投。实测背景:停掉出站中继后发送到 failtest@recv.test 的消息进入 SoftFail(复现过程见 7.2 节),恢复中继后立即重投(实测回显):
irb(main):001:0> qm = QueuedMessage.find_by(message_id: 6)
irb(main):002:0> qm.retry_now
=> true
irb(main):003:0> puts "ATTEMPTS=#{qm.reload.attempts}"
ATTEMPTS=1
irb(main):004:0> exit
worker 日志随即出现投递(实测回显):
worker-1 | 2026-10-19 11:07:39 +0000 INFO Connected to 127.0.0.1:2525 (127.0.0.1) log_id=DMHM1QBG component=worker thread=work0 original_queued_message=7 queued_message=7
worker-1 | 2026-10-19 11:07:39 +0000 INFO Sending message 1::6 to failtest@recv.test log_id=DMHM1QBG component=worker thread=work0 original_queued_message=7 queued_message=7
worker-1 | 2026-10-19 11:07:39 +0000 INFO Accepted by 127.0.0.1:2525 (127.0.0.1) for failtest@recv.test log_id=DMHM1QBG component=worker thread=work0 original_queued_message=7 queued_message=7
投递状态落库核对(实测回显):
mysql -h127.0.0.1 -P13306 -uroot -pq1w2e3r4 -e \
"SELECT id,message_id,status,details FROM \`postal-server-1\`.deliveries WHERE message_id=6;"
id message_id status details
6 6 SoftFail No SMTP servers were available for recv.test. Tried 127.0.0.1.
7 6 Sent Message for failtest@recv.test accepted by 127.0.0.1:2525 (127.0.0.1)
💡 说明:Web 界面上等价操作为消息详情页(Messages → Outgoing → 点开消息)的 Retry now 按钮。
retry_now属手工入队(manual),会跳过抑制名单检查(见 3.5 节),适合「障碍恢复后放行」场景。
3.3 挂起与释放(Held 消息)
挂起(Held)的两个常见来源:邮件服务器处于 Development 模式(全部出站挂起待审),或收件人命中抑制名单。实测 Development 模式挂起:切模式 → 发信 → 查看挂起(实测回显):
irb(main):001:0> s = Server.find_by(permalink: 'main-server')
irb(main):002:0> s.update_column(:mode, 'Development')
=> 1
irb(main):003:0> s.reload.mode
=> "Development"
irb(main):004:0> exit
swaks --server 127.0.0.1:25 \
--auth PLAIN --auth-user x --auth-password xo5g1Opn3yZYSgAXTsMOJMJQ \
--from sender@example.com --to heldtest@recv.test \
--h-Subject 'hold test in dev mode' --body 'should be held' 2>&1 | tail -1
=== Connection closed with remote host.
sleep 6
mysql -h127.0.0.1 -P13306 -uroot -pq1w2e3r4 -e \
"SELECT id,rcpt_to,subject,status,held FROM \`postal-server-1\`.messages WHERE rcpt_to='heldtest@recv.test';"
id rcpt_to subject status held
8 heldtest@recv.test hold test in dev mode Held 1
mysql -h127.0.0.1 -P13306 -uroot -pq1w2e3r4 -e \
"SELECT id,message_id,status,details FROM \`postal-server-1\`.deliveries WHERE message_id=8;"
id message_id status details
9 8 Held Server is in development mode.
切回 Live 并释放该消息(实测回显):
irb(main):001:0> s = Server.find_by(permalink: 'main-server')
irb(main):002:0> s.update_column(:mode, 'Live')
=> 1
irb(main):003:0> msg = s.message_db.message(8)
irb(main):004:0> puts "STATUS=#{msg.status} HELD=#{msg.held?}"
STATUS=Held HELD=true
irb(main):005:0> msg.add_to_message_queue(manual: true)
=> true
irb(main):006:0> exit
sleep 8
mysql -h127.0.0.1 -P13306 -uroot -pq1w2e3r4 -e \
"SELECT id,rcpt_to,status,held FROM \`postal-server-1\`.messages WHERE id=8; \
SELECT id,message_id,status,details FROM \`postal-server-1\`.deliveries WHERE message_id=8 ORDER BY id;"
id rcpt_to status held
8 heldtest@recv.test Sent 0
id message_id status details
9 8 Held Server is in development mode.
10 8 Sent Message for heldtest@recv.test accepted by 127.0.0.1:2525 (127.0.0.1)
⚠️ 注意:加载消息对象必须用
s.message_db.message(id)(消息库与主库是不同连接),不要手工 new Scope。Web 界面等价操作:消息详情页 Release 按钮。
3.4 清空队列(取消投递)
对不再需要投递的消息(如测试流量、误发),从队列中取消即清空。实测:停中继制造一条 SoftFail 队列行(id=13,message=12),随后删除队列行(实测回显):
irb(main):001:0> qm = QueuedMessage.find_by(message_id: 12)
irb(main):002:0> puts "BEFORE: id=#{qm.id} message=#{qm.message_id} attempts=#{qm.attempts}"
BEFORE: id=13 message=12 attempts=1
irb(main):003:0> qm.destroy
=> #<QueuedMessage id: 13, message_id: 12, ...>
irb(main):004:0> puts "REMAIN=#{QueuedMessage.count}"
REMAIN=0
irb(main):005:0> exit
⚠️ 注意:destroy 只移除队列行,不删除消息记录(消息仍在消息库且状态为 SoftFail,审计可查)。Web 界面等价操作为消息详情页 Cancel。若要批量清空整域积压,用
QueuedMessage.where(domain: 'recv.test').destroy_all并先 SELECT 核对范围。
3.5 抑制名单(Suppression List)
抑制名单按邮件服务器维护,命中 recipient 类型的收件人在自动投递时会被直接挂起(不是拒收),等待人工处置。添加并实测拦截(实测回显):
irb(main):001:0> s = Server.find_by(permalink: 'main-server')
irb(main):002:0> s.message_db.suppression_list.add(:recipient, 'blocked@recv.test', reason: 'lab test')
=> true
irb(main):003:0> s.message_db.suppression_list.get(:recipient, 'blocked@recv.test')['address']
=> "blocked@recv.test"
irb(main):004:0> exit
swaks --server 127.0.0.1:25 \
--auth PLAIN --auth-user x --auth-password xo5g1Opn3yZYSgAXTsMOJMJQ \
--from sender@example.com --to blocked@recv.test \
--h-Subject 'suppression hold test 2' --body 'should be held' 2>&1 | tail -1
=== Connection closed with remote host.
sleep 6
mysql -h127.0.0.1 -P13306 -uroot -pq1w2e3r4 -e \
"SELECT id,type,address,reason FROM \`postal-server-1\`.suppressions; \
SELECT id,message_id,status,details FROM \`postal-server-1\`.deliveries ORDER BY id DESC LIMIT 1;"
id type address reason
2 recipient blocked@recv.test lab test
id message_id status details
18 16 Held Recipient (blocked@recv.test) is on the suppression list (reason: lab test)
名单也会自动维护:同一收件人 24 小时内出现 HardFail 后,worker 会自动把地址加入 recipient 类型抑制(阈值由 postal.yml postal.recipient_on_suppression_list_threshold 控制,默认 1),投递成功则自动移除。移除方式(实测口径):
irb(main):001:0> s = Server.find_by(permalink: 'main-server')
irb(main):002:0> s.message_db.suppression_list.remove(:recipient, 'blocked@recv.test')
=> true
irb(main):003:0> exit
⚠️ 注意:
retry_now(manual 入队)会跳过抑制名单检查,这是「确认误抑制后放行」的正确路径;若想让消息继续走抑制检查,等待下一次自动调度即可。
四、日志与审计
本节说明三个服务日志的开启方式、内容形态与审计数据的落库位置。
4.1 服务日志
postal logs <服务名> 跟踪日志(Ctrl+C 退出)。v3 已内置结构化日志,无需另行开启;component/trace_id 字段用于关联同一条 SMTP 会话内的全部行。SMTP 会话审计样例(实测回显,对应《Postal 快速入门与实战示例》7.1 节那次发信):
postal logs smtp 2>&1 | grep 'trace_id=IM76066A' | head -8
smtp-1 | 2026-10-19 11:18:43 +0000 DEBUG Connection opened from 127.0.0.1 trace_id=IM76066A component=smtp-server
smtp-1 | 2026-10-19 11:18:43 +0000 DEBUG Client identified as 127.0.0.1 trace_id=IM76066A component=smtp-server
smtp-1 | 2026-10-19 11:18:43 +0000 DEBUG <= EHLO ops trace_id=IM76066A component=smtp-server
smtp-1 | 2026-10-19 11:18:43 +0000 DEBUG => 250-My capabilities are trace_id=IM76066A component=smtp-server
smtp-1 | 2026-10-19 11:18:43 +0000 DEBUG => 250 AUTH CRAM-MD5 PLAIN LOGIN trace_id=IM76066A component=smtp-server
smtp-1 | 2026-10-19 11:18:43 +0000 DEBUG <= AUTH PLAIN [redacted] trace_id=IM76066A component=smtp-server
Web 请求日志样例(含 API 调用审计字段,实测回显):
web-1 | 2026-10-19 11:19:28 +0000 INFO POST /api/v1/send/message (200) event=request transaction=6fbd9ae699a6644b8a51 controller=LegacyAPI::SendController action=message format=*/* method=POST path=/api/v1/send/message request_id=8cbd2ddb-79f8-4abb-93a7-0b9f7b742087 ip_address=127.0.0.1 status=200 view_runtime=0.12042699381709099 db_runtime=9.301935002440587
💡 说明:密码与凭据在日志中自动脱敏(如上例 AUTH PLAIN [redacted])。如需把日志外送集中式收集,postal.yml 支持
gelf:段落配置 GELF(Graylog)服务器地址,web/smtp/worker 三进程均会外发。
4.2 消息级审计数据
消息审计的核心在消息库四张表:messages(收发件人/主题/状态/大小)、deliveries(逐次投递状态与详情文本)、raw-YYYY-MM-DD(原始邮件全文,按天分表)、suppressions(抑制记录)。审计示例:查某收件人的完整投递历史(实测回显):
mysql -h127.0.0.1 -P13306 -uroot -pq1w2e3r4 -e \
"SELECT d.id,d.status,LEFT(d.details,58) AS detail FROM \`postal-server-1\`.deliveries d \
JOIN \`postal-server-1\`.messages m ON m.id=d.message_id \
WHERE m.rcpt_to='failtest@recv.test' ORDER BY d.id;"
id status detail
6 SoftFail No SMTP servers were available for recv.test. Tried 127.0.0.1.
7 Sent Message for failtest@recv.test accepted by 127.0.0.1:2525 (127.0.0.1)
垃圾检查与 Webhook 调用也有独立表(spam_checks / webhook_requests),Web 界面消息详情页同样可查。DKIM/SPF 的验证结论记录在域名字段与投递详情中;本环境的 DNS 校验为本地模拟口径(详见《Postal 快速入门与实战示例》5.3 节)。
五、监控指标
本节给出 Prometheus 指标接入点与关键指标口径。
- worker 指标:http://127.0.0.1:9090/metrics(postal_worker_job_executions、postal_worker_job_runtime_bucket 等)
- smtp 指标:http://127.0.0.1:9091/metrics
- 两端点各带 /health 探针,适合存活检查;指标为 Prometheus 文本格式,直接被 Prometheus 抓取
实测抓取 worker 指标(节选):
curl -s http://127.0.0.1:9090/metrics | grep '^postal_' | head -6
postal_worker_job_executions{thread="0",job="ProcessQueuedMessagesJob"} 1.0
postal_worker_job_runtime_bucket{thread="1",job="ProcessQueuedMessagesJob",le="0.005"} 0.0
postal_worker_job_runtime_bucket{thread="1",job="ProcessQueuedMessagesJob",le="0.01"} 53.0
postal_worker_job_runtime_bucket{thread="1",job="ProcessQueuedMessagesJob",le="0.025"} 93.0
postal_worker_job_runtime_bucket{thread="1",job="ProcessQueuedMessagesJob",le="0.05"} 93.0
postal_worker_job_runtime_bucket{thread="1",job="ProcessQueuedMessagesJob",le="0.1"} 93.0
建议告警规则(口径来自本次实测):
| 指标/查询 | 告警条件 | 说明 |
|---|---|---|
postal_worker_job_executions | 10 分钟无增长 | worker 调度停摆 |
| postal.queued_messages 行数(SQL) | > 100 持续 15 分钟 | 队列积压 |
| 9090/9091 /health | 非 OK | 进程级故障 |
| deliveries 中 SoftFail 计数(SQL) | 5 分钟内突增 | 上游中继/目标网络异常 |
六、备份与恢复
本节完成数据库与配置的全量备份、可恢复性验证与恢复操作步骤。Postal 的全部业务数据在 MariaDB 与配置目录两处,备份即覆盖全部状态。
6.1 备份数据库
MariaDB 11.4 的逻辑备份工具为 mariadb-dump(11.x 起不再随附 mysqldump,二者参数兼容)。备份主库与全部消息库(实测回显):
mariadb-dump -h127.0.0.1 -P13306 -uroot -pq1w2e3r4 \
--single-transaction --routines --triggers \
--databases postal postal-server-1 > postal-all-20261019.sql
ls -la postal-all-20261019.sql
grep -c 'CREATE TABLE' postal-all-20261019.sql
tail -2 postal-all-20261019.sql
-rw-r--r-- 1 root root 63332 Oct 19 19:06 postal-all-20261019.sql
44
-- Dump completed on 2026-10-19 11:06:16
⚠️ 注意:新增邮件服务器会新增消息库(postal-server-2 等),备份清单要同步更新。可用
SHOW DATABASES LIKE 'postal%'先取清单再拼 --databases 参数。--single-transaction 保证 InnoDB 一致性快照且不锁表,适合运行中备份。
6.2 备份配置目录
配置目录含 postal.yml(全部运行参数)与 signing.key(安装级 DKIM 私钥,丢失会导致回程路径签名失效),打包归档(实测回显):
tar czf postal-config-20261019.tar.gz -C /opt/data/quzuo/postal-3.3.7 config
ls -la postal-config-20261019.tar.gz
-rw-r--r-- 1 root root 3168 Oct 19 19:06 postal-config-20261019.tar.gz
6.3 恢复验证(演练)
恢复演练把备份导入临时库并比对行数,不触碰线上库。先用 sed 把库名改为 *_check,再导入(实测回显):
sed 's/`postal-server-1`/`postal-server-1-check`/g; s/`postal`/`postal-check`/g; s/DATABASE postal/DATABASE postal-check/' \
postal-all-20261019.sql > restore-check.sql
mysql -h127.0.0.1 -P13306 -uroot -pq1w2e3r4 < restore-check.sql
mysql -h127.0.0.1 -P13306 -uroot -pq1w2e3r4 -e \
"SELECT (SELECT COUNT(*) FROM \`postal-server-1\`.messages) AS live_msgs, \
(SELECT COUNT(*) FROM \`postal-server-1-check\`.messages) AS check_msgs, \
(SELECT COUNT(*) FROM postal.servers) AS live_servers, \
(SELECT COUNT(*) FROM \`postal-check\`.servers) AS check_servers;"
live_msgs check_msgs live_servers check_servers
8 8 1 1
行数一致即恢复链路有效,随后清理演练库(实测回显):
mysql -h127.0.0.1 -P13306 -uroot -pq1w2e3r4 \
-e "DROP DATABASE \`postal-check\`; DROP DATABASE \`postal-server-1-check\`;"
6.4 真实恢复步骤
线上恢复(库丢失或迁移到新数据库服务器)按以下顺序执行:
- 建库导入备份:
mysql -h<数据库地址> -u<账号> -p < postal-all-<日期>.sql(备份文件自带 CREATE DATABASE,无需先建库) - 恢复配置目录:
tar xzf postal-config-<日期>.tar.gz -C /opt/data/quzuo/postal-3.3.7,核对 postal.yml 中数据库地址与新环境一致 - 结构补齐:
postal initialize(幂等,已存在则跳过) - 启动服务:
postal restart,按 2.1 节巡检验证
七、升级与回滚
本节给出版本升级路径与回滚手段,均基于官方辅助命令。
7.1 升级
postal set-version <新版本号> # 重新生成 docker-compose.yml 中的版本标签
postal upgrade # 拉取新版本程序并执行 initialize(含数据库迁移)
postal restart # 重启三服务
postal version # 验证版本
⚠️ 注意:升级前必须完成第六章备份(尤其数据库——迁移不可逆);跨大版本(v2 → v3)按官方升级文档执行,v3 起 RabbitMQ、cron、requeuer 进程全部不再需要,可一并下线。
7.2 回滚
版本标签由 set-version 写入 docker-compose.yml,回滚即改回旧版本号后 postal upgrade + postal restart。数据库迁移若已执行,需用升级前的备份库恢复(因此升级前备份是硬性前置)。
八、故障排查手册(投递失败)
本节给出投递失败的定位路径:先判定 SoftFail 还是 HardFail,再按「中继/网络 → DNS → 域名/凭据 → 系统资源」四层排查。全部现象与结论来自本环境实测。
8.1 现象识别:SoftFail 与 HardFail
| 状态 | 含义 | 队列行为 | 典型原因 |
|---|---|---|---|
| SoftFail | 暂时性失败 | 留在队列按 5min × 1.3^n 退避重试,最多 18 次 | 目标拒连/网络不通/4xx 应答 |
| HardFail | 永久失败 | 出队并生成退信 | 域名不存在/5xx 拒收;同一收件人 24h 内 HardFail 还会自动入抑制名单 |
| Held | 挂起待审 | 出队停发,人工放行 | Development 模式/命中抑制名单/凭据 hold |
8.2 案例一:出站中继不可达(SoftFail)
现象:发信成功受理但收不到;消息状态 SoftFail。定位三步(全部实测):
第一步,看投递详情拿失败原因(实测回显):
mysql -h127.0.0.1 -P13306 -uroot -pq1w2e3r4 -e \
"SELECT id,status,details FROM \`postal-server-1\`.deliveries WHERE message_id=6 ORDER BY id DESC LIMIT 1;"
id status details
6 SoftFail No SMTP servers were available for recv.test. Tried 127.0.0.1.
第二步,看 worker 日志拿底层错误(实测回显):
worker-1 | 2026-10-19 11:05:11 +0000 ERROR Cannot connect to 127.0.0.1:2525 (127.0.0.1) (Errno::ECONNREFUSED: Connection refused - connect(2) for "127.0.0.1" port 2525) log_id=L8SLERLG component=worker thread=work0 original_queued_message=6 queued_message=6
worker-1 | 2026-10-19 11:05:11 +0000 INFO message requeued for trying later retry_after=2026-10-19 11:05:11 UTC component=worker thread=work0 original_queued_message=6 queued_message=6
第三步,验证目标端口连通性并修复(本例为出站中继进程停止,重启即可):
ss -ltn | grep ':2525' || echo "中继端口无监听"
中继端口无监听
修复后按 3.2 节 retry_now 立即重投,状态变 Sent(回显见 3.2 节)。无中继环境的同类故障是出站 25 端口被封或对方 MX 拒连,details 中会给出目标主机与错误码,排查思路相同。
8.3 案例二:Development 模式全部挂起(Held)
现象:所有出站消息状态 Held,详情 Server is in development mode.(复现与释放全过程见 3.3 节)。处理:确认是否为有意的测试模式,切回 Live 后对存量 Held 消息逐条 Release。
8.4 案例三:收件人命中抑制名单(Held)
现象:特定收件人被挂起,详情 Recipient (...) is on the suppression list (reason: ...)(实测见 3.5 节)。处理:核对原因,若为自动入榜的误抑制,suppression_list.remove 移除后等自动调度或 retry_now 放行。
8.5 案例四:提交被拒(发信域名未验证)
现象:SMTP 提交 RCPT/MAIL 阶段被拒或 API 返回错误。排查顺序:
- 发件域名是否已加入邮件服务器 Domains 且 verified(内网环境的本地模拟验证口径见《Postal 快速入门与实战示例》5.3 节)
- SMTP 凭据是否正确(用户名固定 x,密码为 SMTP 凭据 key)
- API 场景核对 X-Server-API-Key 是否为 API 类型凭据(错误回显
code: InvalidServerAPIKey,见《Postal 快速入门与实战示例》8.2 节)
8.6 案例五:worker 不取队列
现象:消息长期停在队列,attempts 不增长。排查顺序:
postal status确认 worker 在 Upcurl -s http://127.0.0.1:9090/health确认健康端点postal logs worker看调度任务是否在运行(正常时可见scheduling task to next run at …周期日志)- 数据库可达性:worker 与主库/消息库断连时队列冻结,按第六章恢复数据库连接后自动续跑
九、常见问题(FAQ)
Q1:队列里的消息会无限重试吗? 不会。默认 18 次尝试、退避 5 分钟 × 1.3^n,超过后对收件域做 HardFail 并生成退信;阈值可在 postal.yml 调整。
Q2:清空队列会丢数据吗? 不会。destroy 只删 queued_messages 行,消息与投递历史仍在消息库(状态保留为 SoftFail/Held),审计与界面均可回查。
Q3:备份需要停服吗? 不需要。--single-transaction 基于 InnoDB 一致性快照,运行中即可备份;配置目录 tar 也不影响服务。
Q4:raw 原始邮件表会无限增长吗?
不会。默认保留 30 天或 2048 MB(取先到),由 worker 定期清理;postal.yml postal.message_retention_* 与 raw_message_retention_* 系列参数可调。
Q5:如何把日志接到集中式日志平台?
postal.yml 的 gelf: 段配置 GELF 服务器(Graylog 协议),三进程日志同步外发;本手册 4.1 节的结构化字段(trace_id/component)在 GELF 中同样保留。
Q6:signing.key 丢了怎么办? 用最近一次配置备份恢复。若无备份,需为每个发信域名重新生成 DKIM 密钥并更新 DNS,回程路径签名期间会降级——因此配置目录必须与数据库同步备份。
Q7:如何确认队列没有积压?
SELECT COUNT(*) FROM postal.queued_messages; 为 0 即无积压;有少量行且 retry_after 在未来几分钟属正常重试等待。
十、参考链接
- 官方文档站(运维相关章节):https://docs.postalserver.io/
- 配置参考(retention/重试/日志/GELF 全参数):https://github.com/postalserver/postal/blob/main/doc/config/yaml.yml
- 调试与日志:https://docs.postalserver.io/other/debugging
- 升级指南:https://docs.postalserver.io/getting-started/upgrading
- MariaDB 备份工具:https://mariadb.com/kb/en/mariadb-dump/