又记一次AWS SES 邮件服务账号被禁的情况

前言

时隔快两年,我们的 SES 账号又被封了,有运营在群里面说了,有用户反馈验证账号的邮件和重置密码的邮件又不能发送了。 我查了一下邮件服务队列的日志,果然又看到报错了(不要问我为什么是运营提示的,因为之前的那一次挂掉的经验,我还不涨记性,还没有把这种情况放到 cacti 报警里面):

1
MessageRejected: Sending paused for this account. For more information, please check the inbox of the email address associated with your AWS account

看了一下被禁的理由,擦,又是反弹率超过阀值了(10%)
1
看了一下,又是无效邮件的占比太高了(反弹率) 达到了 12% , 预警值是 10%, 所以就被 AWS 禁了。要解禁,只能先提交工单了。因为之前就有出现这种情况了,具体看: 记一次AWS SES 邮件服务账号被禁的情况, 发 SES 邮件的账号被 AWS 禁了, 因此就先用之前的备用账号的的那个 SES 账号来代替。 换上了之后, 就正常了。

备用账号也挂了

因为当天是周日下午,处于放假中,所以换上备用账号之后,看了一下反弹率,虽然也挺高的(也差不多一直都在 10% 左右),好像随时可能也会被封掉,但是勉强撑个半天应该没问题,等周一上班的时候,再排查下反弹率增高的原因。
谁知道当我周一刚过来上班的时候,还挺正常的,刚要排查为什么主账号的反弹率那么高的时候,备用账号竟然也挂了,赶紧去看了一下 log:

1
2
[Throttling: Daily message quota exceeded.
status code: 400, request id: 85dea96e-8028-11e9-97e6-xxx]

这次挂掉的原因,竟然是达到了24小时发送量的极限值,也就是 5 万封,所以这个备用账号当下也不能发了。
1
原来备用账号每24小时最大发送量只有 5 万封,而随着我们的业务量的增加了,每天要发送的邮件的量,都要在 5-10 万封之间(主账号的每日最大发送数比这个大很多,达到了 500 万封 每24小时),所以我们疏忽了这个点,导致备用账号当下也不能发送了。
备用账号要恢复额度,要等24小时之后,因为我们没有其他账号了,而且重新申请一个 SES 账号,也要准备很多材料, 而且也不可能一开始就给你 5 万封的额度,这个 5 万封还是之前那个项目组好几次提交工单申请提额才逐渐涨起来的。
当然我们有考虑到其他发邮件服务商,但是其他的邮件服务商,达不到这个量级,网易腾讯,最多,一天1万, 还有期限,而且还要重新接入 SDK, 时间成本更高。

排除反弹率

所以一方面我们赶紧催 AWS 的中国区的销售人员,赶紧过主账号的解禁工单。一方面就等待备用账号的额度恢复。 同时还要查一下服务有没有被人刷接口的嫌疑,为什么反弹率会突然增高?

排查接口被刷的嫌疑

刚开始以为是接口被刷了,所以就把这两天的 nginx 的 access log 分析了一下,参照这个: 使用 GoAccess 来分析 Nginx log 记录, 然后看了一下 log 里面的接口请求,发现有涉及到发送邮件的接口,量都没有增多,跟之前差不多,而且占总的请求的量都占不到 1%, 看了一下 IP 分布,也没有明显的异常。
看了一下我们的邮件类别分布,发现每天要发送 3-4 万封的验证账号的邮件,而这个比例占到了总邮件比例的 50% 左右,然后查了一下往期的发送量,发现也都差不多。所以可以排除是人为刷接口的原因。
那么问题来了,为啥没人刷接口,反弹率会增高,这个是因为我们每天的量都一般是账号验证邮件,而一旦这些邮件里面无效邮件的个数多一点,那么反弹率就上去了。所以我看了一下 SES 后台的趋势图,发现其实一段时间之内的反弹率都挺高的,高峰期都超过 10% 的阀值,只不过 AWS 都没有把我们禁掉,一直到25号,才禁掉我们。
1

前端的防抖操作

但是我在观察邮件列表的时候,经常看到同一时间有相同的两条邮件发送记录,所以我就再猜,会不会前端没有对有触发发送邮件的按钮进行防抖操作,如果用户一直快速点的话,会不会一直触发好几条??
结果我亲测了一下,发现还真是这样子,防抖没有做好啊? 那只能去改代码了,我用的 lodash 的防抖函数,当然也可以自己写 前端工具集(15) -- 节流和防抖,只不过刚好这个项目也有用到 lodash, 也就直接用了:

1
2
3
4
5
6
doHandle:  _.debounce(function (e) {
// do something
}, 500, {
leading: true,
trailing: false
}),

不过这边要注意一个细节,为了保证用户体验,要设置为首次触发。

后端的防抖操作

除了前端之后,后端那边也要加上防抖,简单的来说,就是短时间之内的,同 ip 的相同请求,直接不处理, 直接从 redis 里面取出返回值,然后返回,不进行其他处理。
简单的来说,如果收到一个请求,要把请求的参数,连同 ip 地址合并起来,然后生成一个md5 值,response 之前就把结果值缓存在redis 里面,然后时间设置得短一点,比如 5 s,如果在这个 5 s 的时候,还有相同参数,ip 的请求过来,就直接从 redis 里面取出 value,并返回就可以了。
当然这边要注意一个细节,因为接口要有处理时间的,比如 1s,才会给接口,如果我两个相同请求,不分前后的几乎同时过来,这时候就会出现第一个的response还没有给的时候,第二个就过来了,这时候就找不到 redis 缓存了,所以要换成,如果请求刚到的时候,先缓存请求,但是 value 设置成一个固定值,比如 1,如果后面发现有redis缓存,并且 value 为 1 的时候,那么说明第一个的请求处理还没有结束,所以这时候直接 sleep 2s ,然后再重新取一次看看,如果还没有的话,就当做没有缓存处理了。
当然这层处理,只能处理非常短时间的重复请求,是没办法做到防刷的,防刷的机制比这个复杂很多,而且要考虑很多因素。后面有机会会讲到如何对一个接口进行防刷处理。

优化措施

就这样等了24小时,主账号的解禁工单还没有过(这效率我都不想吐槽了,当然如果是土豪还有一种选择,可以提交付费工单,听说优先级很高,问了一下,按照我们的量,一次付费工单,要 3000 rmb,惹不起),但是备用账号的额度解禁了。
为了怕备用账号又因为发送太多邮件(超过 5 万)而被禁。所以我们又做了优化措施:

邮件模板黑名单

因为我们每天要发送很多类型的邮件模板,但是有些邮件模板频率很高,比如账号验证邮件,新注册用户欢迎邮件,有些邮件模板频率很低。所以我们就在邮件服务那边做了一个邮件黑名单的机制。简单的来说,就是我们有一个邮件模板黑名单,只要这个邮件模板上了这个名单,那么邮件服务在发送的时候,如果判断是在黑名单中,那么就不允许发送。
这样我们就只保留了一些优先级比较高的邮件,比如重置密码邮件,账号验证邮件,将一些优先级比较低的邮件,比如欢迎邮件,普通操作提醒邮件 先设置为黑名单。先降低邮件的发送量。

1
2
3
4
5
6
7
8
9
10
11
tpl_black_list = "regtip,signout,signin"

// 初始化模板黑名单数组
TplBlackListArr = strings.Split(conf.TplBlackList, ",")

// 邮件模板黑名单
if util.InStringArray(strings.ToLower(q.Template[:templateLngIndex]), TplBlackListArr) != -1 {
log.Infof("Mail to %v, tpl in blacklist : %v", q.MailTo, q.Template)
q.SetStatus(StatusTplBlackMenu)
continue
}

通过这种方式,发送邮件数果然下架了很多,但是反弹率其实并没有下降多少,而且这种方式只能是临时,等主账号解禁了,还是得全部模板都要开放。

模板有效性校验名单

如果要降低反弹率,要么多发一些有效的邮件,要么少发一些无效的邮件,或者是未知的邮件(就是这个邮箱我们也不知道到底是不是有效的)。因为这个用户有没有验证过邮箱,我们系统是知道的。
所以后面想了一种方式,针对一些跟用户紧密性比较相关的邮件(一些操作邮件,比如登录,登出,购买等等),这些邮件的发送要建立在当前用户的邮箱已经验证的情况下,才去发送,如果当前用户没有验证过邮箱的话,那么就不发送。这样就可以大大提高了发送有效邮箱的概率,而且一定程度上也可以减少邮件的发送量:

1
2
3
4
5
6
7
8
9
// 如果 mail_to 邮箱未验证,同时该邮件模板在"邮箱未验证模板黑名单" 中,不发送
arr = []string{}
for _, tplname := range TplBlackListUnverifiedArr {
arr = append(arr, strings.ToLower(tplname))
}
if util.InStringArray(tplNameSimple, arr) != -1 && MailIsVerified(q.MailTo) == false {
log.Infof("Mail to %v, mail unverified and tpl in unverified blacklist : %v", q.MailTo, q.Template)
return StatusTplBlackMenuUnverified
}

当这个优化更新之后,果然反弹率开始下降了。

主账号恢复之后

又过了几个小时,终于收到 AWS 的 SES 账号解禁的邮件了,主账号终于恢复了,既然额度恢复了,所以就把模板黑名单清空了。

复盘

邮件服务进行了优化之后,反弹率终于降到了正常值了(5%)
1
除了之前做的几个优化操作之外:

  • 前端操作按钮防抖
  • 后端接口做了一层接口缓存
  • 邮件服务增加模板黑名单机制
  • 邮件服务增加模板有效性校验机制

除了这些之后,之后还要做:

  • 增加一封报警邮件,以防这种因为 SES 账号问题导致的错误(之前有防服务挂掉和邮件堆积的报警,但是忽略了这种情况)
  • 后面为了保证我们的邮件服务,可能会采用两个 SES 账号的形式,一个账号用来发送我们的业务服务,这些业务服务应该大部分邮箱都要有效的。 另一个账号用来发送我们的验证账号服务。
    这样子,就算验证账号的那个 SES 服务挂了,也不会影响我们的其他服务。之所以要有双账号的机制,是因为 SES 的反弹率机制,就是无效邮箱的比例(阀值是10%,这个是不能改的),而验证账号邮件这种东西,本身无效邮件的比率就不可能太低,而我们的其他业务邮件,大部分都是建立在有效邮箱的基础上,所以无效邮箱的比例是会比较低的,所以最好把这两种类型的邮件要分开用不同的账号来发送,这样就算万一哪一天又因为无效邮箱太多而被禁掉了,也可以保证我们的其他邮件业务正常。
  • 还要有一个备用账号,所以最稳妥的方式就是最好有三个 SES 账号,可以随时切换,备用账号。

SNS 通知

后面发现了原来 SES 也可以做硬反弹邮件的 webhook 机制,因此还可以再优化,看这个: AWS 的邮件发送服务 SES 订阅 SNS 来处理硬反弹邮件