Postal 日常运维与排障手册

Postal 3.3.7 日常运维与排障手册:日常巡检、队列管理(重投/清空/挂起释放)、日志与审计、监控指标、备份与恢复、升级回滚、投递失败排查(SoftFail/HardFail/抑制名单),全部操作经 3.3.7 实测

Postal邮件服务器运维2026/10/19

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 指标(节选):

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_executions10 分钟无增长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 真实恢复步骤

线上恢复(库丢失或迁移到新数据库服务器)按以下顺序执行:

  1. 建库导入备份:mysql -h<数据库地址> -u<账号> -p < postal-all-<日期>.sql(备份文件自带 CREATE DATABASE,无需先建库)
  2. 恢复配置目录:tar xzf postal-config-<日期>.tar.gz -C /opt/data/quzuo/postal-3.3.7,核对 postal.yml 中数据库地址与新环境一致
  3. 结构补齐:postal initialize(幂等,已存在则跳过)
  4. 启动服务: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 返回错误。排查顺序:

  1. 发件域名是否已加入邮件服务器 Domains 且 verified(内网环境的本地模拟验证口径见《Postal 快速入门与实战示例》5.3 节)
  2. SMTP 凭据是否正确(用户名固定 x,密码为 SMTP 凭据 key)
  3. API 场景核对 X-Server-API-Key 是否为 API 类型凭据(错误回显 code: InvalidServerAPIKey,见《Postal 快速入门与实战示例》8.2 节)

8.6 案例五:worker 不取队列

现象:消息长期停在队列,attempts 不增长。排查顺序:

  1. postal status 确认 worker 在 Up
  2. curl -s http://127.0.0.1:9090/health 确认健康端点
  3. postal logs worker 看调度任务是否在运行(正常时可见 scheduling task to next run at … 周期日志)
  4. 数据库可达性: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 在未来几分钟属正常重试等待。

十、参考链接