简介:这是一份关于短消息中心业务功能的技术培训PPT课件,面向通信网络运维、开发及相关学习者,系统讲解SMS Center在移动网络中的核心作用。内容从短消息提交、转发、优先级与有效期管理讲起,逐项说明重发机制、状态报告、用户鉴权、汉字短消息支持、虚拟短消息中心及多种调度方式,还涉及节日模式、网络短消息、长短消息转发、多目的地发送、网关与报表统计等扩展功能,适合作为理解短信业务架构及其处理逻辑的入门或复习材料。资源包共1个文件,为PPTX演示文稿,大小约444KB,章节结构清晰,便于按模块浏览和摘录重点。目前已有77人学习,通过流程说明和功能要点速览,可帮助读者建立短消息中心业务全景,并掌握高负荷场景下的性能优化与多中心组网思路。
1. 短消息中心业务功能:为什么说它不是“发短信”三个字能概括的
“发短信”这个词,用户看着只有三个字,短消息中心背后要处理的事却远不止三个字。一次上行短信的接收、下行短信的转发、状态报告的生成与回送、长短信的分片重组、闪信的即时呈现、黑名单拦截,都在短消息中心业务功能的范畴里。很多运维同学第一次上手时,习惯把这里当成一个比普通 HTTP 服务更能扛的消息盒子,结果一上线就被回执错位、消息重复、短信号码显示异常搞得焦头烂额。这篇笔记的定位很直接:从短消息中心的业务功能拆解开始,一路讲到 SMPP 接入参数、重试策略、故障排查和话单稽核。新手能跟着把流程走通,熟手可以重点看参数边界和踩坑记录。
2. 拆解短消息中心业务功能:从一次 MO 到状态回执的完整闭环
2.1 先说清楚短消息中心在整条链路上的位置
短消息中心一般缩写成 SMSC,它不是独立封闭的系统,而是夹在移动网络和业务系统之间的一个“存储转发”节点。移动网络侧,它通过 No.7 信令网与 MSC/VLR、HLR 交互;业务侧,它对外暴露 SMPP、SGIP、CMPP 这类协议接口,让 SP、企业应用可以在线提交下行短信,或接收用户上行短信。很多人把短信网关和短消息中心当成一回事,实际上业务功能边界并不相同:短信网关偏重协议适配和路由转发,短消息中心则真正承担了消息存储、调度重试和状态回执的生成。
如果只是做一个小规模的通知下发应用,也许连接的是短信网关而不是短消息中心;一旦网关后端的短消息中心出现积压、重复、回执丢失,业务侧看到的还是“短信发不出去”。所以,理解短消息中心业务功能,首先要建立两条链路的印象:一条是用户手机到短消息中心的 MO 上行链路,另一条是短消息中心到用户手机的 MT 下行链路,而状态报告始终作为下行链路的一个分支存在。后面排查故障,本质上就是在查这两条链路里的哪个环节吞了消息。
2.2 一次 MO 短信在短消息中心内部走了哪几个环节
用户发送一条短信,比如回复“1”到一个特服号码,消息先到 MSC,MSC 根据号段归属把它提交到短消息中心。短消息中心拿到这条 MO 消息后,并不是直接转给业务系统,而是先做五件事:第一,检查用户是否在黑名单,黑名单直接丢弃并产生拦截话单;第二,做内容合规检查,按本地策略过滤违规内容或超长内容;第三,解析被叫号码,判断是点对点短信还是业务短信号码,这决定后续路由;第四,对短消息进行分片重组与编码转换,比如把 GSM 7-bit 编码转成 UCS2;第五,生成 MO 话单并写入话单库,然后把消息经由 SMPP 等接口推送给业务侧。
在这一连串动作里,最容易出问题的其实是“编码转换”。很多业务系统的上行内容到短消息中心时变成乱码,问题常常不是网络丢包,而是短消息中心在 GSM 7-bit、8-bit、UCS2 之间转换时,data_coding 字段没按协议要求映射。我处理过一类奇怪故障:用户发来的中文短信,业务系统收到的前半段正常,后半段变成问号。后来定位发现,短消息中心把一条包含 emoji 的短信按 UCS2 解析,却在拆分时把第二个分片误标成了 GSM 7-bit,解码端拿到错误编码后只能显示出问号。这种问题在业务功能联调阶段很难暴露,往往要等到跑量之后才集中爆发。
MO 链路还有一个常被忽略的细节:短消息中心要不要向 MSC 返回确认。短消息中心接收 MO 并成功落库后,需要向 MSC 返回接收成功的响应,MSC 才不会再发起重试。如果业务系统的 SMPP 接收接口响应很慢,短消息中心的 MO 处理线程被拖住,反过来会影响它对 MSC 的确认超时。结果就是同一个用户的同一条上行短信,MSC 重发了好几次,短消息中心也重复落库了几次,最终业务侧看到的是“用户刷屏”。
2.3 MT 短信与状态报告:业务功能里最容易“回执错位”的一环
MT 下行流程从业务系统提交短信开始。业务侧通过 SMPP submit_sm 把消息发给短消息中心,短消息中心做鉴权、路由、费用控制处理,然后按 HLR 查询结果选择合适的 MSC 投递。投递成功或者失败,MSC 会返回一个投递状态,短消息中心据此生成一条状态报告,再通过 deliver_sm 回给业务侧。这里的“回执错位”是日常运维里最常见的痛点之一:业务系统认为发送成功,用户却没收到;或者用户收到了,话单里却显示失败。
错位的根源在于状态报告没有和原始提交消息建立强关联。SMPP 协议里,状态报告通过 message_id 与 submit_sm 关联,而 message_id 的生成规则各厂商实现并不统一。一些短消息中心在多节点部署后,提交时分配到节点 A,状态报告却从节点 B 异步返回,如果节点 B 没有同步消息上下文,回执里的 message_id 就可能对不上。另一个常见错位场景是手动重发:业务系统超时后人工重发相同内容,短消息中心把两条消息当成两个独立任务,用户收到重复短信,但业务侧只看到一次提交返回。
要避免这类问题,落地时需要在短消息中心与业务侧之间约定一套完整的关联字段。我的习惯是要求业务侧在 submit_sm 里自带业务消息 ID,例如写在 SMPP 可选参数 message_id 或私有 TLV 中;短消息中心生成状态报告时,必须原样返回这个 ID。这样业务系统收到状态报告后,就可以用自己的业务 ID 和短消息中心的消息 ID 做关联,而不是只知道“短信网关那边有个 ID”。如果你维护的是短消息中心自身,建议从架构上把状态报告和原始提交放进同一分区或同一消费组,尽量让回执按提交顺序返回,否则乱序会造成误判。
2.4 把功能清单翻译成系统模块:一张职责表
把 MO、MT 两条链路讲完后,可以整理出一张功能模块清单。这张表不是给你看 PPT 目录,而是后续排障和开发排期时用来对职责的。我一般会把短消息中心业务功能分成九个模块:
| 模块 | 主要职责 | 典型故障出口 |
|---|---|---|
| 接入协议处理 | SMPP/SGIP/CMPP 协议编解码、连接管理 | 连接被重置、收发超时 |
| 鉴权与黑白名单 | 主叫/被叫号段鉴权,黑名单拦截 | 批量拦截误伤 |
| 编码转换与分片 | 7-bit/UCS2 转换,长短信分组 | 乱码、分片丢失 |
| 路由寻址 | 按号段、HLR/PRN 结果决定 MSC 地址 | 路由数据过期 |
| 存储转发 | 消息落库、定时下发、队列调度 | 积压、丢失 |
| 重试调度 | 失败重试策略、退避时间、最大次数 | 风暴式重试 |
| 状态报告 | 投递结果映射、状态报告生成 | 状态错位、缺失 |
| 话单计费 | 各类话单生成、批价接口 | 话单不平 |
| 运维监控 | 话单量、队列深度、响应时延 | 告警淹没或漏报 |
这张表对应的是一个标准短消息中心的职责切分。落实到具体项目时,有的系统把“鉴权与黑白名单”放在更上游的短信网关,有的把“话单计费”拆到独立计费域,这都不影响你拿这张表做功能核对。核对的方法很简单:把短消息中心每个对外接口、每张数据库表、每个后台任务分别归到这个表里,找不到归属的功能就要警惕是不是历史遗留的“黑匣子”。如果让我重新搭一个短消息中心,我会把每一条消息的落库字段设计成至少包含:消息 ID、业务系统 ID、主叫、被叫、消息类型、优先级、提交时间、状态、重试次数、节点号。这样排障时只需要一条 SQL 就能看到消息全生命周期。很多老系统把状态字段设计成两位数字并冠以一堆自定义含义,最后排障全靠猜,这是最要命的。
3. 让业务功能可落地:SMPP 接入流程与一页纸参数表
3.1 为什么落地短消息中心业务功能要先谈 SMPP
短消息中心对外接口里,SMPP 是事实标准,几乎所有主流短消息中心和短信网关都支持。SGIP/CMPP 主要在国内某些区域使用,但从业务功能落地的角度,它们要处理的参数问题大概率是共通的。SMPP 最核心的能力就是三件事:建立一个可靠的长连接会话、提交短信、接收短信和状态报告。用 SMPP 走通一次收发,等于把短消息中心的核心链路轮了一圈。很多业务系统接入时喜欢让短消息中心按某种“内部接口”对接,理由是“效率高”,但我不太建议上来就这么干。标准协议的好处是两侧都有现成工具,出了问题还能开包抓取 PDU 对照协议文档。自定义接口一旦出了问题,两边各执一词,排障只能靠猜。
SMPP 会话通常分三种模式:transmitter(只发)、receiver(只收)、transceiver(收发一体)。业务系统接入下行发送时,多数短消息中心建议用 transceiver 模式,这样上下行和状态报告可以在同一个连接上处理,省去维护两个连接的负担。但要注意,有些短消息中心的许可证按连接数计费,如果业务系统节点多,transceiver 模式的连接数上限可能比分别建立 transmitter 和 receiver 更早触顶。接入前先问清楚短消息中心的连接数限制,这是血泪经验。
3.2 最小可用的 SMPP 收发脚本:从连接到提交 MO
下面用 Python 的 smpplib 示例跑通一个最小收发闭环。代码不依赖具体短消息中心厂商,只要对方开放 SMPP 端口即可:
import smpplib import time # 短消息中心侧配置,按实际环境修改 HOST = "192.0.2.10" PORT = 3720 SYSTEM_ID = "smpp_client" PASSWORD = "your_pass" def on_deliver_sm(pdu): # 收到状态报告或上行短信时都会进来 print("deliver_sm, msg_id:", pdu.receipted_message_id) print("stat:", pdu.message_state) if pdu.esm_class & 0x04: print("short_message:", pdu.short_message) client = smpplib.client.Client(HOST, PORT) client.set_message_received_handler(on_deliver_sm) client.connect() client.bind_transceiver(system_id=SYSTEM_ID, password=PASSWORD) # 构造一条下行文本短信 params = { "source_addr_ton": 1, "source_addr_npi": 1, "source_addr": "10086", "dest_addr_ton": 1, "dest_addr_npi": 1, "destination_addr": "13800138000", "short_message": "hello", "data_coding": 0, } client.submit_sm(**params) # 循环读取响应,让回调有机会被触发 try: while True: client.read_poll() time.sleep(1) except KeyboardInterrupt: client.unbind() client.disconnect()这段脚本的逻辑分四步:先建立 TCP 连接并绑定为 transceiver 会话;然后构造 submit_sm 请求提交一条下行短信;接着进入轮询循环,持续读取短消息中心推过来的 deliver_sm;最后收到键盘中断时主动解绑并断开连接。参数里 source_addr_ton 和 source_addr_npi 分别代表源地址类型和编号计划,填 1 表示国际号码,填 0 表示未知;真实短信中心对接时一般按对方要求填,大多数情况下短消息中心不校验源地址类型,但会校验源地址号码是否有权限发送。dest_addr_ton 和 dest_addr_npi 同理,目标号码前缀带国家码时填 1,不带时可能需要填 0。
这里最容易踩的坑是 read_poll 的循环间隔。间隔太短,CPU 空转;间隔太长,短消息中心会认为连接不活跃,先发来 enquire_link 探活,长时间没有响应就主动断开。我的经验是本地联调时 sleep(1) 够用,生产环境接入最好由短消息中心侧指定轮询间隔或直接使用异步回调模式。还要注意 submit_sm 的响应里会返回短消息中心的 message_id,这个 ID 必须和后续状态报告里的 receipted_message_id 对上,对不上就是回执错位,上一章已经说过。
3.3 参数怎么定:绑定时长、收发窗口、超时阈值的一页纸
接入短消息中心的时候,业务系统关注最多的参数往往不是短信内容本身,而是协议连接和推送方式。我一般把参数分成三层:连接层、消息层、业务层。连接层参数解决“连接能不能活”;消息层参数解决“这条短信以什么编码、什么优先级发出去”;业务层参数解决“要不要回执、多久算超时”。下面是我习惯整理的一页纸参数对照表:
| 参数 | 含义 | 常见取值 | 备注 |
|---|---|---|---|
| bind_timeout | 连接绑定后的空闲超时 | 300 秒左右 | 超时后短消息中心发 enquire_link 探活 |
| enquire_link_interval | 客户端主动探活间隔 | 30~60 秒 | 短于服务端空闲超时即可 |
| window_size | 并发未确认的 submit_sm 数量 | 16~64 | 超过后要等待响应再继续发 |
| request_timeout | submit_sm 的响应等待时间 | 5~10 秒 | 太长会拖累业务侧线程 |
| registered_delivery | 是否需要状态报告 | 1 表示需要成功和失败回执 | 不需要回执时填 0 |
| data_coding | 短信内容编码 | 0 为 GSM 7-bit,8 为 UCS2 | 中文必须用 8 或 UTF-8 映射 |
| priority_flag | 优先级 | 0~3 | 0 最低,3 最高,慎用 3 |
| validity_period | 消息在短消息中心的有效期 | 1~24 小时常见 | 过期后丢弃并生成失败话单 |
| replace_if_present_flag | 同 message_id 是否覆盖旧消息 | 0 不覆盖,1 覆盖 | 抄送场景慎用 |
这组参数里最容易翻车的是 window_size。业务系统一次性提交几千条短信时,如果 window_size 设置过小,发送速率会被响应往返时间卡住;设置过大,短消息中心的接收队列会被瞬间打满,反过来触发它的流控。比较稳妥的做法是先用 16 起步,观察短消息中心侧的平均响应时间和队列深度,再逐步上调到 32、64。validity_period 也值得注意,很多业务系统把它当成普通超时时间,填了“3 天”甚至更长。对通知类短信来说,有效期越长,重试窗口越大,用户体验反而越差;用户手机已经关机一整天,第二天开机收到一条昨天的验证码,业务上毫无意义。
3.4 长短信、闪信和优先级在 SMPP 里的真实字段
短消息中心业务功能里,长短信、闪信、优先级这三类功能最容易被当成“简单需求”,但在协议字段上各有各的坑。长短信的拆分不是简单把字符串按 70 字一段切,SMPP 里要做用户数据头(UDH)处理。短消息中心负责拆分时,submit_sm 的 esm_class 要设置 UDHI 标志,同时把每条分片打上 sar_msg_ref_num、sar_total_segments、sar_segment_seqnum 三件套。业务侧如果自己先拆好了再提交,短消息中心可能不会自动重组,而是原样把多分片发给手机,手机侧能否正确合并取决于手机实现,这就容易产生“收到三片但读不出来”的投诉。
闪信在 SMPP 里一般通过 data_coding 和相关的消息级别参数配合实现。最常用的是把消息的 priority_flag 设为 1,并配合短消息中心管理台的“闪信号段”配置。有些厂商把闪信映射成特定 data_coding 值,例如 0xF0 加特殊标记。我的观点是:闪信这类功能不要试图跨厂商统一参数,先看短消息中心支持哪些字段,再固定成自己的配置文件。优先级则比很多人想象的危险,把大量营销短信的 priority_flag 设为 3,会让普通验证码短信排队时间变长。优先级的本质是队列插队,插队越多,系统整体的公平性越差。生产环境里优先级 3 应该只留给最高级别的告警短码,不允许业务方自行指定。
4. 高并发与重试策略:短消息中心最容易翻车的三个地方
4.1 业务功能设计时最容易忽视的“消息积压”模型
短消息中心和普通 HTTP 服务最大的区别并不是吞吐量,而是它天然带有“存储转发”的异步语义。业务系统把短信提交给短消息中心,短消息中心返回 accept,并不代表短信已经到用户手机,只代表它把消息放进了内部队列。后面能不能发出去,取决于 MSC 的响应、HLR 查询、用户手机状态等一系列外部因素。很多人把短消息中心的“已接收”当成“已送达”,这是后续一系列误判的起点。
我习惯把短消息中心的内部队列看成生产-消费模型:生产速度是协议接入层接收 submit_sm 的速率,消费速度是重试调度模块从队列取消息并提交到 MSC 的速率。正常场景下消费速度大于生产速度,队列很浅;一旦 MSC 侧出现批量超时,消费速度降到接近 0,队列深度会在几十秒内涨到几十万。这时如果重试调度没有做背压,就会形成恶性循环:队列越深,重试任务越多,重试又进一步挤压正常的发送任务。
避免积压要从两个方向下手。方向一是限流,在协议接入层对每个业务系统设置每分钟最大提交条数,超过后直接返回错误或走备用队列。方向二是削峰,把重试调度设计成“按号段分桶”的独立消费者,每个桶有独立队列和独立告警,这样即使其中一个号段的 MSC 出问题,也不至于把整个短消息中心拖垮。我见过最短命的配置是全网共用一个队列,一个号段的小区短信涌入后,验证码通道被堵了几个小时,最后只能重启清理队列。这种场景下,无论参数怎么调,架构上不分桶都救不回来。
4.2 重试与退避:从固定间隔到指数退避的参数实验
重试策略是短消息中心业务功能里最考验经验的部分。固定间隔重试实现简单,但有一个明显的副作用:大量失败消息在同一时刻集中重试,形成重试风暴。特别是一次营销活动下发几十万条短信时,如果失败比例按固定 5 分钟重试,那么每隔 5 分钟,系统就要面对一整波重试洪峰。这个洪峰大概率会再次击穿 MSC,于是下一轮失败更多,下一轮洪峰更大,最终把整个短信中心打挂。
比较工程化的做法是指数退避加抖动。指数退避让每一条消息的重试间隔随重试次数增长,避免同步共振;抖动则让间隔带上随机量,避免同批次消息仍然保持步调一致。下面是一段常见的重试时间计算逻辑:
import random base_interval = 60 # 首轮重试等待 60 秒 max_interval = 3600 # 单轮等待上限 1 小时 max_retry = 6 # 最多重试 6 次 def next_retry_delay(retry_count): if retry_count <= 0: return 0 # 指数过程:2^(retry_count-1) * base delay = base_interval * (2 ** (retry_count - 1)) # 基础抖动:上下浮动 20% jitter = random.uniform(0.8, 1.2) delay = delay * jitter # 封顶 return int(min(delay, max_interval)) for retry in range(1, max_retry + 1): print("第", retry, "次重试等待", next_retry_delay(retry), "秒")这段代码的核心是把重试次数映射到一个随指数增长的等待时间,同时加 20% 抖动。要注意的是,指数退避不是避免重复,而是把重复分摊到更长的时间轴上,为系统争取恢复的外部条件。重试次数上限也不能拍脑袋。短消息的有效期只有两小时时,重试 6 次、最大间隔 1 小时,理论窗口已经超过有效期,最后几次重试即使成功,消息也可能因为过期被短消息中心丢弃。所以重试参数要和上一章说的 validity_period 联动设计。我一般会把“最长重试时间”控制在有效期的一半以内,给最后一次投递留出足够响应时间。
4.3 事务边界与幂等:断电恢复后为什么短信重了
短消息中心的高并发下,消息重复和消息丢失是硬币的两面。为了不丢消息,系统需要把消息落库并返回确认;为了不重复,落库时必须建立唯一约束或幂等机制。很多自研短消息中心在存储层用消息表存所有待发送消息,但没有唯一索引。短消息中心向 MSC 提交消息后,如果 TCP 断连,MSC 到底有没有收到是不确定的。短消息中心重发一次,用户就可能收到两条一模一样的短信;不重发,又可能彻底丢失。
解决重复一般分两层。第一层是存储去重:在消息表上对“业务消息 ID + 分片序号”建唯一索引,重复提交直接返回已存在消息的 message_id。第二层是调度去重:重试调度取任务时,用数据库行锁或 Redis 的 SETNX 抢锁,抢到锁的消息才允许进入发送流程。下面是一个简单的去重 SQL 示例:
CREATE TABLE sms_message ( id BIGINT PRIMARY KEY AUTO_INCREMENT, biz_msg_id VARCHAR(64) NOT NULL, segment_seq INT NOT NULL DEFAULT 0, msisdn VARCHAR(20) NOT NULL, content TEXT, status TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL, UNIQUE KEY uk_biz_seg (biz_msg_id, segment_seq) ) ENGINE=InnoDB; -- 插入时发现重复则返回已存在 INSERT INTO sms_message (biz_msg_id, segment_seq, msisdn, content, status, create_time) VALUES ('B20240001', 0, '13800138000', 'hello', 0, NOW()) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id);在业务提交层把业务消息 ID 和分片序号作为唯一键,可以让重复提交变成无害操作。需要注意的是,唯一键不能只放在业务系统侧,短消息中心自身也要做同样的约束,因为很多重复来自 MSC 重发和短消息中心内部重试。事务边界上,我习惯把“写消息表”和“更新发送状态”放在同一个事务里,状态迁移靠 SQL 条件更新而不是应用层 if-else,能进一步减少并发下的状态覆盖。断电恢复后短信重复,十有八九是唯一约束没建或者建得不对,这是埋得最深的一个坑。
4.4 容量评估:先算队列深度,再谈并发数
高并发压测时,很多人只看每秒能提交多少条 submit_sm,忽略了一个更重要的问题:在 MSC 响应变慢时,系统能容纳多少在途消息。在途消息数等于发送速率乘以平均响应时间,这就是 Little 定律。假设系统每秒提交 1000 条,MSC 平均响应时间 0.5 秒,那么在途消息约 500 条;如果 MSC 故障导致响应时间变成 10 秒,在途消息瞬间变成 10000 条,全部堆积在短消息中心的内存或队列里。
容量评估时我会按三档建模:正常档、降级档、故障档。正常档按 1 秒内完成的发送任务估算内存占用;降级档按 MSC 响应变慢 5 倍估算队列深度;故障档按 MSC 完全不响应估算消息堆积峰值和落库磁盘水位。短消息中心投入生产前,至少要按故障档做一次演练,把消息积压到磁盘容量的 60% 以上再看系统行为。很多系统在低水位时跑得很顺,高水位一上来,数据库连接池先耗尽,消息表的锁竞争拖死整个落库链路,这说明容量模型里少算了“消息积压带来的存储压力”。
| 场景 | 发送速率 | 平均响应时间 | 在途消息量 | 重点关注 |
|---|---|---|---|---|
| 正常档 | 1000 条/秒 | 0.5 秒 | 500 条 | 内存缓冲 |
| 降级档 | 1000 条/秒 | 5 秒 | 5000 条 | 队列水位 |
| 故障档 | 1000 条/秒 | 无响应 | 持续上涨 | 磁盘与连接池 |
这张表的价值不在于具体数字,而在于帮你确定“该为哪种场景预留资源”。把故障档给压测通过,生产环境大概率能应对绝大多数突发情况;只按正常档设计,一次网络抖动就可能让短消息中心从“高吞吐”变成“高延迟”。
5. 短消息中心常见故障与排查清单:现象、原因、解法
5.1 状态报告延迟或缺失
现象:业务系统提交短信后,短消息中心很快返回 message_id,但后续状态报告长时间不来,或者漏了某几条。用户手机已经收到了短信,业务侧却一直标记为发送中,最后超时判失败。原因通常分成两种:一是该条 submit_sm 的 registered_delivery 参数没有要求回执,短消息中心自然不会生成状态报告;二是短消息中心的状态报告模块与提交模块在两个节点上,中间通过异步队列同步,队列消费慢或消息丢失,回执就会延迟或缺失。
解法:先查消息话单,看短消息中心侧这条消息最终投递结果是否存在;再查状态报告队列消费位置有没有落后。如果结果存在但回执没送到业务侧,抓包看 SMPP 连接上短消息中心是否真的发过 deliver_sm。我遇到过的最隐蔽原因是业务侧 SMPP 连接使用 receiver 模式,但短消息中心把状态报告发到了另一个绑定模式的连接上,结果在业务侧看起来就是“丢了”。
5.2 下行短信被网关拒绝发送
现象:submit_sm 提交后,短消息中心返回错误码,常见的有拒绝、无效目的地址、源地址无权限。用户收到的不是短信,而是系统提示“发送失败”。原因可能是源号码不合法、目标号段不在短消息中心服务范围、或者该用户被加入了黑名单。还有一种情况是高峰期间短消息中心主动流控,对某些业务系统返回“限流中”,业务侧没做退避,反复提交,被短消息中心以“重复提交”拒绝。
解法:把短消息中心返回的错误码原样透传给业务侧,不要自己映射成统一的“失败”,很多错误码差异恰恰是定位关键。然后按号段白名单核对该目标号码是否属于短消息中心可服务范围。如果错误码是流控类,业务侧需要指数退避而不是立即重试。我给业务系统定的规矩是:凡是短消息中心返回明确拒绝的错误码,一律不做自动重试,转人工排查;只有超时类错误码才允许自动重试,这样能避免大量无效请求打在短消息中心上。
5.3 消息重复下发
现象:用户投诉收到两遍一模一样的短信,话单里却只有一条提交记录,或者状态报告显示两条回执。原因通常来自三处:短消息中心内部重试造成重复提交 MSC、业务系统超时后重发相同内容、以及 MO 链路上 MSC 重复投递导致重复落库。这三处的共同点是都发生在“不确定是否成功”之后,系统选择重试,但没有判重。
解法:先从话单里找出重复消息的 message_id 和提交时间,看两条记录之间的时间差。如果时间差接近业务系统超时时间,那大概率是业务侧重发;如果时间差接近短消息中心重试间隔,并且消息表中的重试次数字段累加了,那问题在短消息中心侧。两侧都要建唯一索引,同时把“业务消息 ID + 分片序号”作为幂等键贯穿整条链路。这个坑我踩过一次之后,再也不敢让任何接口不带幂等参数上生产。
5.4 时钟跳变导致调度失灵
现象:短消息中心的定时短信功能出现诡异行为,比如短信提前几分钟下发,或者重试调度突然大量触发。原因听起来有点玄学,但实际很常见:运行短消息中心的虚拟机时钟发生跳变,NTP 校时把一个只差几毫秒的时钟拨快了几秒,定时任务立即大量到期。重试调度也依赖当前时间和上次失败时间做差值,时钟跳变会让差值瞬间达到重试阈值。
解法:短消息中心的所有节点必须启用 NTP 客户端并配置可信源,同时禁止应用层直接系统时间。调度任务的时间计算尽量以数据库时间为基准,不要完全相信机器内存里的时钟。如果多次出现这一类问题,建议在短消息中心配置参数里加一个小缓冲区,例如定时任务比设定时间提前 1 秒不触发,避免边界抖动造成批处理风暴。
5.5 从日志和话单定位问题的方法
排查短消息中心问题时,最怕的是日志分散、话单不全、两侧时间对不上。我通常先把日志规范成统一格式,至少包含消息 ID、业务消息 ID、协议类型、方向、源号码、目的号码、状态、耗时、节点号。然后建一个快速检索脚本,按消息 ID 拉出全部生命周期轨迹:
#!/bin/bash # 按消息ID短消息中心日志,调用方式 ./trace_msg.sh B20240001 13800138000 msg_id=$1 msisdn=$2 grep -h "${msg_id}" /var/log/smsc/*.log | grep "${msisdn}" | sort -k4 > /tmp/trace_${msg_id}.log awk '{print $1, $2, $4, $5, $6, $7, $8}' /tmp/trace_${msg_id}.log | head -50脚本思路很简单:把消息 ID 和手机号作为两个维度的过滤条件,在短消息中心全部日志文件中找出相关记录,按时间排序后缩小到核心字段展示。实际使用时要特别注意日志时间精度,最好统一到毫秒,而且所有节点的时钟要先做同步,否则跨节点追踪会因为时间错位得出完全相反的结论。后面的 awk 只是精简展示,真实排障时我还会把状态报告里的 message_state 和 submit_sm 返回的 message_id 单独拎出来对比。
6. 把业务功能做成可运营的能力:话单稽核与用例设计
6.1 话单稽核:从“能发短信”到“账目清楚”
短消息中心上线一段时间后,业务方最关心的问题往往不是“短信能不能发出去”,而是“这个月应该结算多少条”。话单稽核就是把短消息中心产生的发送话单、状态报告话单、计费系统话单三方对平。常见做法是每小时做一次总量比对,每天做一次明细比对。总量核对公式是:MO 话单数 = 上行接收数,MT 话单数 = 下行提交数,成功回执数 = 送达数。三者各自独立,不能混着算。我发现很多“金额对不上”的争议,源头都是把 MT 提交数当成了成功计费数。按成功率计费的客户,必须用状态报告里的成功回执数作为结算基数,而不是提交数。做话单稽核前先把这层口径定义清楚,否则后面对出花来也是空转。
6.2 测试用例设计:覆盖消息生命周期
业务功能测试不能只测“提交成功”。我会按这条规则设计用例集:至少覆盖 MO 上行、MT 下行、状态报告、长短信分片、闪信、黑名单拦截、无效号段、重复提交、超时重试、重启恢复和话单生成十一类场景。其中重复提交和重启恢复是最容易发现逻辑漏洞的,测试方法也很简单:把短消息中心进程杀掉,再同时拉起一批提交请求,观察恢复后消息表和发送状态是否一致。我第一次做这种测试时,发现重启后有 3 条消息状态还是“发送中”,既没有重试,也不会失败,成了永远卡住的僵尸消息,后来在启动逻辑里补了一个状态恢复任务才解决。
最后说一个我自己的教训:刚接触短消息中心时,我总觉得把发送成功率做上去就大功告成了,直到上线第二个星期,财务拿着对账单找上门,说账目差了八千条。我才发现短消息中心话单、网关话单和计费侧话单三个口径各算各的,没有一个例行稽核任务。后来我把话单核对刷成了每小时自动跑,才发现很多所谓的“玄学故障”,其实就是话单先漏了。做短消息中心业务功能,发出去只是起点,能数清楚、能追回来,才是运营层面真正值钱的部分。这个思路后来帮我避开了不少坑,希望也能帮到你。
本文还有配套的精品资源,点击获取