
去年接手了一个内部的后台管理系统,Python + Flask 写的,跑了快三年。注册和登录用的是邮箱验证码,发送逻辑从最初到现在改了三次,每次都是被线上问题逼的。
记录一下这三次重构的过程和踩过的坑。
最初的实现很直接,在注册接口里同步调 smtplib 发邮件:
import smtplib
from email.mime.text import MIMEText
def send_code(to_email, code):
msg = MIMEText(f'您的验证码是 {code},5 分钟内有效。')
msg['Subject'] = '登录验证码'
msg['From'] = 'noreply@company.com'
msg['To'] = to_email
with smtplib.SMTP('localhost', 25) as server:
server.send_message(msg)本地装了 postfix 做 MTA,出去的邮件走 ECS 公网 IP。
这版能用,但有几个问题:
第一,同步调用。SMTP 握手 + 发送一封邮件要 2-5 秒,用户点"获取验证码"之后要等这么久才能看到提示,体验很差。如果 SMTP 连接超时,接口直接 500。
第二,到达率不稳定。Gmail 基本全进垃圾箱,QQ 邮箱也经常进。后来才知道是 SPF、DKIM 没配,ECS 的 IP 信誉也不好。
第三,邮件发送和业务逻辑耦合在一起,注册接口既要生成验证码、又要发邮件、还要写缓存,一个函数干了太多事。
为了解决同步阻塞的问题,加了 Celery + Redis 做异步任务队列:
from celery import Celery
celery_app = Celery('mail', broker='redis://localhost:6379/0')
@celery_app.task(bind=True, max_retries=3)
def send_verification_email(self, to_email, code):
try:
msg = MIMEText(f'您的验证码是 {code},5 分钟内有效。')
msg['Subject'] = '登录验证码'
msg['From'] = 'noreply@company.com'
msg['To'] = to_email
with smtplib.SMTP('localhost', 25) as server:
server.send_message(msg)
except Exception as exc:
raise self.retry(exc=exc, countdown=10)接口里只负责生成验证码、写 Redis、把发送任务丢进队列,响应时间降到了 200ms 以内。
同时在 postfix 上配了 SPF 和 DKIM,到达率确实好了一些。但维护成本上来了:
Celery worker 要单独跑一个进程,偶尔会挂,得加进 supervisor 管理。Redis 也得维护,虽然本来就在用,但队列堆积的时候还是得关注。
postfix 的 DKIM 密钥要管理,DNS 记录改完之后等生效的过程让人焦虑。最烦的是 IP 黑名单问题——ECS 的公网 IP 是共享的,前面用这个 IP 的人发过垃圾邮件,导致 IP 上了好几个黑名单。去一个个申请解除,有的平台要等几周。
而且还有一个隐蔽的问题:发送失败了怎么知道?Celery 的重试机制能应对临时性故障,但如果邮件发出去了、对方服务器拒收了或者进了垃圾箱,从 Celery 这边是看不出来的。用户说"没收到验证码",我只能去查 postfix 的日志,一条条翻。
折腾了大半年之后,决定把自建 SMTP 这套东西全扔掉。验证码邮件的量不大,一天也就几十封,不值得为它维护一整套邮件基础设施。
换成了第三方的 HTTP 发送接口,代码变成了这样:
import requests
def send_verification_email(to_email, code):
resp = requests.post(
'https://push.spug.cc/mail/你的模板编码',
json={
'to': to_email,
'code': code
},
timeout=5
)
result = resp.json()
# code=200 只代表请求入库成功,邮件是异步发的
return result.get('request_id')几个变化:
不需要 Celery 了。HTTP 请求本身就很快(通常 200-500ms),直接在接口里同步调就行。如果担心偶尔超时影响注册流程,包一层 try-except,发送失败就让用户重新获取。
不需要维护 postfix 了。SPF、DKIM、DMARC 都是发送方那边维护的,IP 信誉也不用操心。之前 Gmail 全进垃圾箱的问题直接消失了。
发送结果可追踪。接口返回一个 request_id,如果用户反馈没收到,拿这个 ID 去日志接口查,能看到邮件的实际发送状态——是发出去了还是被退回了。比以前翻 postfix 日志方便很多。
有一点要注意:接口返回 code: 200 只是请求入库成功,邮件是异步发送的。如果你的业务逻辑需要确认送达,得额外写一个查询逻辑。不过验证码场景一般不需要,用户收到就用,没收到就重发。
第一版的问题是同步阻塞和邮件到达率,用 smtplib 直接发是最快的实现方式,但不适合生产环境。
第二版解决了同步问题,但引入了 Celery + SMTP 的运维成本。对于一天几十封验证码的场景,这个架构太重了。
第三版去掉了所有中间件,一个 HTTP 请求搞定。代码最少,运维成本最低,到达率反而最好。
如果让我重新来做这个需求,大概率直接从第三版开始。自建 SMTP 发邮件这条路,除非你的邮件量大到需要精细控制每一个环节,否则投入产出不成正比。
验证码邮件说到底就是个工具——用户注册的时候能收到就行,不需要花太多工程精力在"怎么把邮件发出去"这件事上。
邮件发送接口用的是 push.spug.cc,管理后台在 sms.spug.cc,有类似需求可以看看。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。