1. 从一条短信说起:为什么还要折腾MAS
短信这东西,现在很多人觉得它已经过时了,微信、钉钉、飞书满天飞,谁还看短信?但如果你在企业里做过系统对接,尤其是做过通知类、验证码类、营销类消息的下发,你就会发现短信依然是触达率最稳的那条通道。原因很简单:它不需要用户装任何App,不需要联网权限之外的任何授权,也不需要对方在线。只要手机有信号,短信就能到。
而中国移动MAS(Mobile Agent Server,移动代理服务器)就是企业接入中国移动短信通道的一个核心网关。说白了,它是企业IT系统和移动短信网络之间的一个中间层。你这边把要发的短信内容、目标号码提交给MAS,MAS再把它送进移动的短信中心,最终落到用户手机上。普通短信,也就是我们常说的SMS,是MAS最基础、最常用的一种业务形态。
我之所以想写这篇东西,是因为最近帮一个朋友的公司做了一套基于MAS普通短信的下发系统,从申请账号、配置IP白名单、调接口、处理回执,到后面踩了一堆坑,整个过程下来积累了不少实战经验。网上关于MAS的资料要么太老,要么太碎,要么就是厂商文档那种冷冰冰的说明书,真正从开发者角度讲清楚"怎么接、怎么调、怎么排错"的内容并不多。所以我把这次项目里的东西整理出来,给后面要接MAS的人省点时间。
这篇文章适合谁看?如果你是后端开发、系统集成工程师、运维,或者你是一个中小企业的技术负责人,需要给公司系统加上短信通知能力,那这篇内容应该能帮到你。我不讲虚的,直接讲架构、讲参数、讲代码、讲排错。看完你至少能知道MAS普通短信是怎么回事,怎么接,接的时候要注意什么。
2. MAS普通短信的整体架构与接入思路
2.1 MAS到底是个什么东西
先把这个概念说清楚。MAS不是中国移动直接暴露给所有开发者的一个公开API,它更像是一个部署在企业侧或者移动侧的服务节点。传统上,MAS有两种常见的部署形态:一种是移动侧集中部署,企业通过专线或者指定协议接入;另一种是企业侧本地部署一台MAS服务器,这台服务器再和移动的短信网关对接。不管哪种形态,企业系统要发短信,本质上都是把请求提交给MAS,由MAS完成后续的协议转换和路由。
普通短信在MAS里通常走的是CMPP协议或者SGIP协议,这两个都是国内短信网关的经典协议。CMPP是中国移动的短信网关接口协议,SGIP是中国联通的,后来也有一些互通场景。对于MAS普通短信来说,你作为企业开发者,很多时候并不需要直接去写CMPP的二进制包,因为MAS会提供更上层的接口,比如HTTP接口、WebService接口,或者提供SDK。但理解底层的CMPP结构,对你排查问题非常有帮助,因为很多错误码和状态报告都是从那一层透传上来的。
我这次项目里用的是MAS提供的HTTP接口方式。为什么选HTTP?因为团队里没有人熟悉CMPP的二进制协议,而且HTTP接口调试成本低,用Postman就能测,日志也好看。当然,HTTP接口的吞吐量不如直接走CMPP长连接,但对于我们这种日均几千条、峰值几万条的场景,完全够用。
2.2 接入前必须搞清楚的几个问题
在动手写代码之前,有几件事你必须先确认,否则后面会反复返工。
第一,你的短信是什么类型。普通短信分两种:一种是验证码/通知类,一种是营销类。这两者在运营商侧的管控力度完全不同。验证码类通常要求模板报备,内容格式固定,发送频率也有限制;营销类则需要用户明确同意,而且发送时间有严格限制,比如不能在大半夜发。你如果搞混了,轻则短信被拦截,重则通道被关停。
第二,你的目标号码覆盖哪些运营商。MAS是中国移动的网关,它发移动号码是最顺的。如果目标号码里有联通和电信的,MAS通常也能发,但走的是互通路由,到达率和延迟可能不如移动本网。如果你的用户里非移动占比很高,那就要考虑是否要接多个通道。
第三,你的发送量级和并发要求。MAS的HTTP接口一般有QPS限制,具体数值取决于你申请的通道规格。如果你要群发几十万条,那就不能一条一条同步调,必须做队列和异步处理。这个后面我会详细讲。
第四,回执怎么拿。短信发出去不等于用户收到了。运营商会返回状态报告,告诉你这条短信是DELIVRD(成功送达)还是UNDELIV(未送达)还是EXPIRED(过期)。这些回执对账、排错、统计到达率都非常重要。MAS通常提供两种回执获取方式:一种是主动推送,你提供一个回调地址;另一种是你主动去拉。我建议用推送,实时性好,但你的回调接口必须稳定,否则回执丢了就对不上账。
2.3 整体链路长什么样
我把这次项目的链路画一下,虽然不能用图,但用文字描述清楚。
企业业务系统产生短信需求,比如用户注册要发验证码。业务系统把消息丢进一个消息队列,比如RabbitMQ或者Kafka。为什么用队列?因为业务系统不应该被短信接口的延迟拖死,短信发不出去不能影响主流程。队列后面是一个短信发送服务,这个服务从队列里消费消息,组装成MAS要求的HTTP请求,提交给MAS。MAS返回一个提交结果,比如"提交成功"或者"提交失败,错误码XXX"。发送服务把这个结果记录下来,同时等待MAS推送的状态报告。状态报告回来之后,更新这条短信的最终状态,并触发后续逻辑,比如失败重试或者告警。
这个链路里,最核心的是发送服务和回执处理服务。发送服务要处理限流、重试、模板渲染、号码校验;回执处理服务要处理并发写入、幂等、对账。下面我逐个拆开讲。
3. 核心细节解析与实操要点
3.1 账号申请与IP白名单配置
MAS的接入不是你自己注册个账号就能用的,通常需要走商务流程。你需要向移动或者移动的代理商提交申请,说明你的业务场景、预计发送量、短信内容类型。审核通过后,你会拿到几个关键信息:账号、密码、接口地址、端口、企业代码(有些叫SPID或者AppID)、以及一个或者多个IP白名单。
IP白名单这个事特别重要。MAS侧通常会限制只有白名单里的IP才能调用接口。这意味着你的发送服务必须部署在固定的公网IP上,或者通过NAT网关出去。如果你用的是云服务器,弹性公网IP可能会变,那就得提前规划好,用固定IP或者NAT网关。我这次就踩了一个坑:测试环境用的是动态IP,结果调接口一直报"IP未授权",查了半天才发现是白名单没加测试机的IP。
注意:申请白名单时,要把生产环境、测试环境、灾备环境的出口IP都加上,并且预留几个备用IP。否则一旦IP变动,短信就断了。
另外,账号密码一般有有效期,有些是90天强制更换。你要把这个写进运维日历,不然到期那天短信突然发不出去,排查起来很麻烦。
3.2 短信模板报备与内容规范
普通短信的内容不是你想发什么就发什么。运营商对短信内容有严格的审核机制,尤其是验证码类短信,必须使用报备过的模板。模板里可以带变量,比如"您的验证码是{code},5分钟内有效"。报备的时候,你要把模板内容、变量规则、使用场景都写清楚。
模板报备通过后,你会拿到一个模板ID。发送的时候,你提交模板ID和变量值,MAS会帮你拼装成最终内容。这样做的好处是,运营商侧知道你在发什么,不容易被拦截。如果你绕过模板,直接发自定义内容,短期内可能没事,但一旦被运营商的风控系统抓到,轻则拦截,重则封通道。
我这次项目里,验证码模板报备花了大概两个工作日。营销类模板更久,而且需要提供用户授权证明。所以如果你有上线时间要求,模板报备一定要提前做。
内容规范方面,有几个红线不能碰:不能包含敏感政治词汇,不能包含色情暴力内容,不能包含赌博、诈骗信息,不能包含未经用户同意的营销内容。另外,短信开头一般要带签名,比如"【某某公司】",这个签名也是要报备的。签名和模板是绑定的,不能随便换。
3.3 HTTP接口的请求结构与参数详解
MAS的HTTP接口通常是一个POST请求,Content-Type一般是application/json或者application/x-www-form-urlencoded,具体看版本。我这次用的是JSON格式。请求体里一般包含以下字段:
- account:你的账号
- password:密码,有些版本要求MD5加密后传
- msgid:你生成的消息ID,用于幂等和回执关联
- phone:目标号码,多个号码用逗号分隔,但一般建议单条提交
- content:短信内容,如果用了模板,这里可能是模板ID加变量
- templateid:模板ID
- sign:签名,有些版本要求
- sendtime:定时发送时间,可选
响应体里一般包含:
- code:状态码,0或者200表示提交成功
- msg:描述信息
- msgid:你传上去的消息ID,原样返回
- taskid:MAS侧生成的任务ID,回执里会带这个
这里有个细节:msgid是你自己生成的,必须全局唯一。我建议用UUID或者"业务前缀+时间戳+序列号"的方式。为什么要唯一?因为回执回来的时候,你要靠msgid或者taskid去关联到具体是哪条短信。如果你重复了,回执就对不上了。
还有一个细节:密码的加密方式。有些MAS版本要求密码明文传,有些要求MD5,有些要求MD5后大写。这个一定要看对接文档,或者直接问移动的技术支持。我见过有人因为密码加密方式搞错,一直报"认证失败",查了一整天。
3.4 状态报告与回执处理
状态报告是MAS主动推送给你的,通常是一个HTTP POST请求,推到你预先配置的回调地址上。推送的内容一般包含:
- msgid:你提交时的消息ID
- taskid:MAS侧的任务ID
- phone:目标号码
- status:状态,比如DELIVRD、UNDELIV、EXPIRED、REJECTD
- receivetime:送达时间
- reporttime:报告时间
你的回调接口收到这个请求后,要做的第一件事是返回一个成功的响应,比如HTTP 200,body里返回"OK"或者"success"。如果你不返回或者返回错误,MAS可能会重推,重推次数和间隔取决于配置。所以你的回调接口必须幂等,同一条回执推多次,你不能重复处理。
我这次的回调接口是用Spring Boot写的,收到回执后先写进一个Redis队列,然后异步落库。为什么不在回调里直接写数据库?因为回调接口的响应时间很关键,如果你写库慢了,MAS那边超时了,它会认为推送失败,然后重推。重推多了,你的系统压力就大了。用Redis做缓冲,回调接口只负责快速接收和入队,后面的落库、对账、告警都异步做。
提示:回执里的status字段,不同MAS版本可能不一样。有的用DELIVRD表示成功,有的用0表示成功。一定要以实际对接文档为准,不要想当然。
3.5 限流、重试与幂等设计
MAS的HTTP接口一般有QPS限制,比如每秒100次。如果你超过这个限制,MAS会返回限流错误,比如"too many requests"或者特定的错误码。所以你的发送服务必须做限流。我这次用的是Guava的RateLimiter,单机限流。如果有多台发送服务,那就需要分布式限流,可以用Redis+Lua实现。
重试策略也很关键。不是所有失败都能重试。比如"号码格式错误"这种,重试一万次也没用。但"网络超时"或者"系统繁忙"这种,可以重试。我建议把错误码分类:可重试错误和不可重试错误。可重试的错误,用指数退避策略,比如第一次等1秒,第二次等2秒,第三次等4秒,最多重试3次。不可重试的错误,直接标记失败,并告警。
幂等方面,除了msgid唯一,还要考虑重复提交。比如你的发送服务在提交给MAS之后,还没收到响应就超时了,然后你重试,结果MAS其实已经收到了第一次请求。这时候如果msgid一样,MAS应该做幂等处理,不会重复发送。但有些MAS版本不支持幂等,那就需要你在业务侧做去重。我这次的做法是,在Redis里用msgid做key,设置一个过期时间,比如5分钟。提交之前先查Redis,如果存在就跳过。
4. 实操过程与核心环节实现
4.1 环境准备与依赖安装
我这次的技术栈是Java 8 + Spring Boot 2.x + Redis + MySQL + RabbitMQ。为什么用Java?因为团队熟悉,而且生态成熟。如果你用Python或者Go,思路是一样的,只是库不一样。
依赖方面,主要用到:
- HTTP客户端:OkHttp或者Apache HttpClient
- JSON处理:Jackson或者Fastjson
- 限流:Guava RateLimiter
- 队列:Spring AMQP for RabbitMQ
- 缓存:Spring Data Redis
- 数据库:MyBatis或者JPA
安装过程我就不赘述了,都是标准操作。重点讲一下配置。
在application.yml里,我把MAS的配置单独抽出来:
mas: sms: url: http://mas.example.com:8080/sms/send account: your_account password: your_password callback-url: http://your-domain.com/mas/callback qps: 100 retry: max-attempts: 3 backoff: 1000这里url、account、password都是示例,实际要用移动给你的。callback-url是你提供给MAS的回调地址,必须是公网可访问的。
4.2 发送服务的核心代码实现
发送服务的核心逻辑是:从队列里拿消息,组装请求,限流,提交,处理响应。
先看消息的实体类:
public class SmsMessage { private String msgId; private String phone; private String templateId; private Map<String, String> variables; private String sign; private Date sendTime; // getters and setters }msgId在消息进入队列之前就生成好,用UUID。phone要做格式校验,必须是11位数字,且以1开头。templateId是报备通过的模板ID。variables是模板变量,比如验证码。
发送逻辑:
@Service public class SmsSendService { @Autowired private RateLimiter rateLimiter; @Autowired private OkHttpClient httpClient; @Autowired private MasConfig masConfig; public void send(SmsMessage message) { // 限流 rateLimiter.acquire(); // 组装请求 Map<String, Object> request = new HashMap<>(); request.put("account", masConfig.getAccount()); request.put("password", md5(masConfig.getPassword())); request.put("msgid", message.getMsgId()); request.put("phone", message.getPhone()); request.put("templateid", message.getTemplateId()); request.put("content", renderContent(message)); request.put("sign", message.getSign()); // 提交 String json = objectMapper.writeValueAsString(request); Request httpRequest = new Request.Builder() .url(masConfig.getUrl()) .post(RequestBody.create(json, MediaType.parse("application/json"))) .build(); try (Response response = httpClient.newCall(httpRequest).execute()) { String body = response.body().string(); MasResponse masResponse = objectMapper.readValue(body, MasResponse.class); if (masResponse.isSuccess()) { // 记录提交成功 saveSubmitLog(message, masResponse); } else { // 处理失败,判断是否可重试 handleFailure(message, masResponse); } } catch (IOException e) { // 网络异常,进入重试 handleNetworkError(message, e); } } private String renderContent(SmsMessage message) { String template = getTemplateContent(message.getTemplateId()); for (Map.Entry<String, String> entry : message.getVariables().entrySet()) { template = template.replace("{" + entry.getKey() + "}", entry.getValue()); } return template; } }这段代码里,限流用的是Guava的RateLimiter,在Bean初始化的时候创建:
@Bean public RateLimiter rateLimiter(MasConfig masConfig) { return RateLimiter.create(masConfig.getQps()); }重试逻辑我单独抽了一个方法,用递归或者循环实现。注意,重试的时候要重新限流,不能绕过。
4.3 回执回调接口的实现
回调接口是一个Controller:
@RestController @RequestMapping("/mas") public class MasCallbackController { @Autowired private RedisTemplate<String, String> redisTemplate; @PostMapping("/callback") public String callback(@RequestBody MasReport report) { // 快速入队 String json = objectMapper.writeValueAsString(report); redisTemplate.opsForList().leftPush("mas:report:queue", json); return "OK"; } }然后有一个消费者,从Redis队列里拿数据,落库:
@Component public class MasReportConsumer { @Scheduled(fixedDelay = 100) public void consume() { String json = redisTemplate.opsForList().rightPop("mas:report:queue"); if (json != null) { MasReport report = objectMapper.readValue(json, MasReport.class); // 幂等检查 if (isProcessed(report.getMsgId())) { return; } // 更新数据库 updateSmsStatus(report); // 标记已处理 markProcessed(report.getMsgId()); } } }这里用Redis的List做队列,简单够用。如果量特别大,可以用RabbitMQ或者Kafka。幂等检查用Redis的setnx,key是msgId,过期时间设长一点,比如7天。
4.4 数据库表设计
短信记录表:
CREATE TABLE sms_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, msg_id VARCHAR(64) NOT NULL UNIQUE, phone VARCHAR(20) NOT NULL, content TEXT, template_id VARCHAR(64), status VARCHAR(20) DEFAULT 'PENDING', submit_time DATETIME, report_time DATETIME, error_code VARCHAR(20), error_msg VARCHAR(255), retry_count INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_phone (phone), INDEX idx_status (status), INDEX idx_create_time (create_time) );status字段的取值:PENDING(待提交)、SUBMITTED(已提交)、DELIVRD(已送达)、UNDELIV(未送达)、EXPIRED(过期)、REJECTD(被拒)、FAILED(提交失败)。
回执记录表:
CREATE TABLE sms_report ( id BIGINT AUTO_INCREMENT PRIMARY KEY, msg_id VARCHAR(64) NOT NULL, task_id VARCHAR(64), phone VARCHAR(20), status VARCHAR(20), receive_time DATETIME, report_time DATETIME, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_msg_id (msg_id) );这两张表通过msg_id关联。对账的时候,用sms_record左连sms_report,找出没有回执的记录,重点排查。
4.5 参数计算与容量规划
假设你的业务日均短信量是10万条,峰值集中在上午9点到10点,这一个小时的量占全天的30%,也就是3万条。那么峰值QPS大约是30000/3600≈8.3。考虑到突发流量,按3倍冗余,QPS按25设计。MAS给你的通道QPS是100,那完全够用。
但如果你要做营销群发,比如一次发50万条,那就不能同步发了。我的做法是,把50万条拆成小批次,每批1000条,丢进队列,由发送服务慢慢消费。发送服务根据MAS的QPS限制,控制消费速度。这样既能保证不超限,又能尽快发完。
存储方面,一条短信记录大概占500字节,10万条一天就是50MB,一年不到20GB。MySQL完全扛得住。但要注意定期归档,比如把3个月前的数据移到历史表,避免主表太大影响查询。
5. 常见问题与排查技巧实录
5.1 提交失败类问题
问题一:认证失败
现象:MAS返回"认证失败"或者"账号密码错误"。
排查思路:先确认账号密码是否正确,注意大小写和空格。然后确认密码的加密方式,是明文、MD5还是MD5大写。最后确认账号是否过期,有些账号有有效期。
我踩过的坑:密码里有个特殊字符,在配置文件里被转义了,导致实际传过去的密码不对。后来把密码用单引号包起来才解决。
问题二:IP未授权
现象:MAS返回"IP未授权"或者"非法IP"。
排查思路:确认你的出口IP是否在白名单里。如果你在云上,确认是弹性IP还是NAT网关IP。如果是容器环境,确认容器的出口IP。
我踩过的坑:测试环境用了动态IP,每次重启就变,后来申请了一个固定IP才稳定。
问题三:模板不存在
现象:MAS返回"模板不存在"或者"模板未审核"。
排查思路:确认templateid是否正确,模板是否已经审核通过。有些MAS要求模板ID和签名必须匹配,签名不对也会报这个错。
5.2 回执异常类问题
问题一:回执收不到
现象:短信提交成功了,但一直收不到状态报告。
排查思路:先确认回调地址是否公网可访问,用Postman模拟MAS推送,看你的接口能不能收到。然后确认MAS侧是否配置了回调地址,有些需要手动配置。最后确认防火墙是否放行了MAS的IP。
我踩过的坑:回调接口用了HTTPS,但证书是自签的,MAS侧不信任,导致推送失败。后来换成HTTP才解决。生产环境建议用正规CA签发的证书。
问题二:回执重复
现象:同一条短信收到多次回执。
排查思路:这是正常的,MAS在没收到你的成功响应时会重推。你的接口必须幂等。用msgId做去重,处理过的直接返回成功。
问题三:回执状态不对
现象:明明用户收到了短信,但回执是UNDELIV。
排查思路:状态报告是运营商侧的最终状态,有时候会有延迟或者误差。如果大量出现这种情况,联系移动的技术支持查一下路由。另外,有些手机设置了短信拦截,也会导致状态报告不准确。
5.3 性能与稳定性问题
问题一:发送服务被限流
现象:MAS返回"too many requests"。
排查思路:检查你的QPS是否超过通道限制。如果是多台发送服务,检查是否做了分布式限流。我建议在发送服务前面加一个令牌桶,用Redis实现,所有实例共享。
问题二:回调接口超时
现象:MAS推送回执时,你的接口响应太慢,导致MAS重推。
排查思路:回调接口只做入队,不做复杂逻辑。如果入队也慢,检查Redis连接池是否够大。我这次把回调接口的响应时间控制在50ms以内,基本没出现过重推。
问题三:数据库写入瓶颈
现象:短信量大了之后,数据库写入变慢,影响回执处理。
排查思路:批量写入代替单条写入。比如每100条回执批量insert一次。另外,把回执表和记录表分开,避免锁竞争。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 认证失败 | 账号密码错误、加密方式不对、账号过期 | 检查配置、确认加密方式、联系移动 | 修正配置、更新账号 |
| IP未授权 | 出口IP不在白名单 | 查出口IP、对比白名单 | 添加白名单、用固定IP |
| 模板不存在 | 模板ID错误、未审核、签名不匹配 | 核对模板ID、确认审核状态 | 修正模板ID、重新报备 |
| 回执收不到 | 回调地址不可达、防火墙拦截 | 模拟推送、检查防火墙 | 修正回调地址、放行IP |
| 回执重复 | MAS重推 | 检查接口响应时间 | 接口幂等、快速响应 |
| 限流 | QPS超限 | 检查发送频率 | 限流、降速 |
| 数据库慢 | 单条写入、表太大 | 查慢查询、看表大小 | 批量写入、归档历史数据 |
5.5 独家避坑技巧
第一个技巧:在发送服务里加一个"影子模式"。所谓影子模式,就是短信不真正提交给MAS,而是只记录日志。这个模式用于测试和压测,避免测试短信真的发到用户手机上。我这次压测的时候,就是用影子模式跑了10万条,验证了队列和数据库的性能。
第二个技巧:对账任务要每天跑。用定时任务,每天凌晨跑一次对账,找出状态是SUBMITTED但超过24小时没有回执的记录,以及回执状态是UNDELIV的记录。这些记录要人工介入排查,可能是号码问题,也可能是通道问题。
第三个技巧:监控告警要配好。关键指标包括:提交成功率、回执到达率、平均延迟、队列积压量。提交成功率低于95%要告警,回执到达率低于90%要告警,队列积压超过1万条要告警。我这次用的是Prometheus + Grafana,效果不错。
第四个技巧:号码清洗很重要。发送之前,把号码里的空格、横线、+86前缀都去掉,统一成11位纯数字。空号、停机号、黑名单号要过滤掉,不然浪费短信费。我这次接了一个号码清洗服务,把无效号码过滤掉,节省了大概5%的成本。
第五个技巧:短信内容里不要带URL短链。运营商对短链的管控很严,容易被拦截。如果一定要带链接,用正规域名,并且提前报备。
6. 成本控制与通道选型的一点经验
6.1 短信计费方式与成本构成
MAS普通短信的计费一般是按条计费,成功提交计费,失败不计费。但不同通道的单价差别很大。验证码类短信通常比营销类短信贵,因为验证码要求更高的到达率和更快的速度。营销类短信便宜,但到达率低,而且容易被拦截。
成本构成里,除了短信费,还有通道费、模板报备费、以及可能的维护费。如果你是通过代理商接的,代理商还会加价。我这次对比了几家代理商,单价从0.03元到0.05元不等。最后选了一家0.035元的,但要求月发送量不低于10万条。
6.2 如何评估通道质量
评估通道质量,不能只看单价。要看几个指标:到达率、延迟、稳定性、回执准确性。
到达率是最重要的。同样发1万条,A通道到达9500条,B通道到达9000条,那A通道的实际成本更低。延迟也很关键,验证码短信如果延迟超过10秒,用户可能已经放弃了。稳定性方面,要看通道是否经常出故障,故障恢复时间多长。
我这次的做法是,先小批量测试。每个通道发1000条,对比到达率和延迟。测试的时候要用真实的号码,覆盖移动、联通、电信。测试时间也要分散,早中晚各发一批,看不同时段的差异。
6.3 多通道冗余与切换策略
如果你的业务对短信依赖很高,建议接至少两个通道。主通道和备通道。正常情况下走主通道,主通道故障或者到达率下降时,自动切换到备通道。
切换策略可以基于实时监控数据。比如主通道的提交成功率连续5分钟低于90%,就自动切换。切换的时候要注意,正在队列里的消息要重新路由。我这次用了一个简单的策略:在发送服务里配置多个通道,每个通道有一个权重和健康状态。健康状态由定时探测任务更新。发送的时候,根据权重和健康状态选择通道。
注意:多通道切换时,msgId要保持唯一,避免回执混淆。不同通道的回执格式可能不一样,回调接口要能兼容。
6.4 短信内容优化与成本节省
短信内容越短,成本越低。普通短信按70个字符一条计费,超过70个字符按多条计费。所以内容要尽量精简。验证码短信一般很短,问题不大。通知类短信要注意,把不必要的信息去掉。
另外,签名也会占用字符数。签名一般放在开头,比如"【某某公司】",占6个字符。如果签名太长,也会增加成本。所以签名要简短。
还有一个节省成本的技巧:合并发送。如果同一个用户有多条通知,可以合并成一条短信发。比如"您有2条新消息,请登录查看"。但要注意,合并后的内容不能太长,否则反而更贵。
7. 上线部署与运维要点
7.1 部署架构与高可用
发送服务和回调服务都要做高可用。至少两台实例,前面挂负载均衡。数据库用主从架构,主库写,从库读。Redis用哨兵或者集群模式,避免单点故障。
我这次的部署架构是:两台发送服务,两台回调服务,一个MySQL主从,一个Redis哨兵。发送服务和回调服务都注册到Nacos,通过网关暴露。MAS的回调地址配置的是网关地址,网关再转发到回调服务。
7.2 日志与监控
日志要打全。每条短信的提交请求、响应、回执都要记录。日志里要包含msgId、phone、status、耗时。方便后面排查问题。
监控方面,除了前面说的业务指标,还要监控系统指标:CPU、内存、磁盘、网络。特别是回调服务的CPU,如果回执量突然增大,CPU可能会飙高。
我这次用ELK做日志收集,用Prometheus做指标监控,用Grafana做展示。告警通过钉钉机器人发送到群里。
7.3 灰度发布与回滚
短信服务上线,建议先灰度。先切10%的流量到新服务,观察一天,没问题再逐步放大。如果出问题,立即回滚。
灰度的时候,要注意新旧服务的msgId不能冲突。我这次用了一个前缀区分,新服务用"NEW_",旧服务用"OLD_"。回执处理的时候,根据前缀路由到不同的处理逻辑。
7.4 日常运维检查清单
每天检查:
- 提交成功率是否正常
- 回执到达率是否正常
- 队列是否有积压
- 数据库连接池是否正常
- Redis内存是否正常
每周检查:
- 通道到达率对比
- 失败短信原因分析
- 日志是否有异常
- 磁盘空间是否充足
每月检查:
- 账号密码是否快过期
- 模板是否需要更新
- 成本是否超出预算
- 备份是否正常
8. 一些个人体会
做短信接入这件事,技术难度其实不算高,但细节特别多。一个参数不对,一个IP没加,一个模板没报备,都会导致短信发不出去。而且短信这东西,用户感知很强,发不出去就是发不出去,没有中间状态。所以做这个系统,一定要细心,要把各种边界情况都考虑到。
我这次最大的体会是,队列和异步化是必须的。不要把短信发送和业务逻辑耦合在一起。业务系统只管把消息丢进队列,后面的限流、重试、回执处理都由专门的短信服务来做。这样业务系统稳定,短信服务也可以独立扩展。
另外,回执处理比发送更重要。发送只是提交,回执才是最终结果。很多人只关注发送成功,不关注回执,结果对账的时候发现一堆问题。回执的幂等、去重、异步落库,这些都要做好。
最后再分享一个小技巧:如果你不确定MAS的某个参数怎么传,直接抓包看。用Wireshark或者tcpdump抓一下MAS的请求,看看实际传了什么。有时候文档写得不清楚,抓包最直接。当然,抓包要在测试环境做,生产环境注意合规。
这个内容后续还可以这样扩展:比如接入多个通道做智能路由,根据号码归属地和通道质量动态选择;比如做短信内容的A/B测试,对比不同文案的到达率和点击率;比如把短信和App推送、微信模板消息打通,做一个统一的消息中台。这些方向都挺有意思,等后面有机会再展开聊。