开发者深度解析:运营商匹配 SMS 路由架构与算法

引言:运营商匹配 SMS 路由的幕后机制

大多数 SMS 服务商把路由描述成一个黑箱:

"我们会根据质量和价格选用最佳路由。"

如果你是负责系统正常运行的工程师或架构师,这样的说法远远不够。你需要知道:

  • 一条消息走了哪条路径
  • 用的是哪个发送号
  • 系统为什么选择了这种组合。
  • 在故障和流量高峰下它会如何表现。

运营商匹配路由是部分网关能在高难度行业垂直领域持续实现 99.4% 以上送达率,而其他网关却长期停滞在 90% 出头到中段水平的关键原因之一。在这篇文章中,我们将深入剖析这套架构:

  • 智能层(运营商及号码类型识别)。
  • 路由决策引擎
  • 号池/网格选择与轮换逻辑。
  • 故障回退策略
  • 可观测性与调试

这不是厂商的营销话术,而是我们在每天数百万条消息的实践中验证有效的实战架构。


第 1 节:"运营商匹配"到底意味着什么

从高层次来看,运营商匹配是:

针对每一个目标号码,选择最匹配该号码所属运营商及上下文环境的发送号和路由。

而不是:

  • 把所有消息都通过最便宜的通用路由发送。
  • 把所有运营商和使用场景混杂在同一批发送号上。

运营商匹配的目标是:

  • 对 Verizon 用户使用经过 Verizon 审核认可的发送号
  • 对 AT&T 用户使用经过 AT&T 审核认可的发送号
  • 各运营商的信誉度保持隔离且可预测。

我们在实践中观察到的好处:

  • 相较于通用路由,在特定运营商上可带来3 到 12 个百分点的送达率提升
  • 长期性能表现的波动更小
  • 出现问题时能进行更清晰的根因分析(一个运营商对应一个网格)。

第 2 节:高层架构

一个具备运营商匹配能力的 SMS 网关通常包含以下组件:

  1. 入口 API

    • 接收消息请求(/messages)。
    • 校验请求体、身份认证以及基本的数据格式。
  2. 号码标准化与信息增强

    • 将电话号码标准化为 E.164 格式。
    • 为号码补充以下信息:
      • 运营商信息。
      • 国家/地区。
      • 号码类型(手机、VoIP,若可获取还包括固话)。
      • 风险信号。
  3. 路由决策引擎

    • 基于已增强的上下文信息以及应用元数据:
      • 选择路由方案(例如 OTP_US、Promo_EU)。
      • 选定一个号池/网格
      • 在该网格内挑选一个发送号
    • 应用针对各运营商和各网格的规则
  4. 排队与调度

    • 将消息放入按路由划分的队列中。
    • 应用:
      • 速率限制。
      • 突发流量控制。
      • 重试策略。
  5. 送达回执与反馈

    • 接收 DLR(送达回执)。
    • 更新:
      • 号池/网格的健康状态。
      • 发送号的信誉指标。
    • 将结果反馈回路由决策环节。
  6. 可观测性平面

    • 指标、日志、追踪链路。
    • 支持按以下维度查询:
      • 运营商。
      • 号池/网格。
      • 发送号。
      • 营销活动。

第 3 节:运营商情报层

在你能够进行运营商匹配之前,首先需要了解这些运营商。

输入

  • E.164 格式的电话号码。
  • 可选地:
    • 来自应用上下文的国家代码。
    • 已知的用户元数据(例如此前已解析过的运营商信息)。

数据来源

  • HLR / 运营商查询服务商
  • 电话号码情报 API。
  • 内部缓存(近期已解析过的号码)。

输出

对于一个给定的目标号码:

  • carrier_id:例如 verizon_usatt_ustmobile_uso2_uk 等。
  • country_codeUSGBDE 等。
  • line_typemobile(手机)、fixed(固话)、voip(若可获取)。
  • risk_flags:近期是否携号转网、号段是否存在可疑风险等(可选)。

缓存策略

  • 对以下对象预热缓存:
    • 高流量的目标号码。
    • 已知的高频发送场景(例如大量发送 OTP 的用户)。
  • 同时需遵守:
    • 查询服务商的速率限制。
    • 数据新鲜度的约束条件。

示例(伪代码):

type CarrierInfo = {
  carrierId: string;
  country: string;
  lineType?: string;
  lastUpdated: number;
};

async function resolveCarrier(msisdn: string): Promise<CarrierInfo> {
  const cached = await carrierCache.get(msisdn);
  if (cached && Date.now() - cached.lastUpdated < CACHE_TTL_MS) {
    return cached;
  }

  const lookup = await externalLookup(msisdn);

  const info: CarrierInfo = {
    carrierId: lookup.carrierId,
    country: lookup.countryCode,
    lineType: lookup.lineType,
    lastUpdated: Date.now(),
  };

  carrierCache.set(msisdn, info);
  return info;
}

第 4 节:路由决策引擎设计

给定以下信息:

  • 已增强的消息上下文(CarrierInfo、国家信息、应用元数据)。
  • 消息类型(OTP、交易类、营销类)。
  • 客户/账户配置。

路由引擎必须选定:

  1. 路由方案

    • 例如 OTP_USPROMO_USALERT_EU 等。
    • 其中封装了:
      • 优先使用的运营商/路由。
      • 吞吐量上限。
      • 允许使用的发送号类型。
  2. 号池 / 网格

    • 例如 US_OTP_VERIZON_GRID_AUS_PROMO_ATT_GRID_B
    • 每个网格:
      • 代表一组 SIM 卡/号码的集合。
      • 拥有各自针对不同运营商的容量与健康指标。
  3. 网格内的具体发送号

    • 选定依据:
      • 轮换策略。
      • 健康状况。
      • 本地约束条件。

决策流程(简化版)

function routeMessage(msg: Message, carrier: CarrierInfo): RouteDecision {
  const profile = selectProfile(msg, carrier);

  const candidateGrids = findEligibleGrids(profile, carrier);

  const grid = selectBestGrid(candidateGrids);

  const sender = pickSenderFromGrid(grid, msg);

  return { profileId: profile.id, gridId: grid.id, senderId: sender.id };
}

其中:

  • selectProfile 依据以下因素:

    • 消息类型(OTP 还是促销类)。
    • 国家/地区。
    • 风险等级/行业垂直(例如加密货币、成人内容等高风险类目)。
  • findEligibleGrids 按以下条件过滤:

    • 国家。
    • 运营商兼容性。
    • 健康度门槛。
  • selectBestGrid 可能会:

    • 优先选择满足以下条件的网格:
      • 错误率/投诉率处于健康水平。
      • 有可用容量。
    • 避开:
      • 已接近阈值的网格。
  • pickSenderFromGrid

    • 实现轮换逻辑,例如:
      • 轮询(Round-robin)。
      • 加权轮换。
      • 健康度感知(避开表现不佳的发送号)。

第 5 节:号池/网格与轮换逻辑

网格作为隔离的基本单元

一个网格可以由以下维度来定义:

  • 地区:US
  • 运营商组合:仅 Verizon、仅 AT&T,或多运营商混合。
  • 使用场景:OTPPROMO(促销)、ALERT(提醒)。
  • 优先级级别。

每个网格都会追踪:

  • 总发送量。
  • 送达/失败的明细拆分。
  • 硬失败错误码。
  • 投诉/退订率。

轮换策略

最简单的方式:

  • 在所有活跃发送号之间轮询

更优的方式:

  • 健康度感知轮换
    • 跳过满足以下条件的发送号:
      • 近期错误率偏高。
      • 投诉比例偏高。
    • 优先倾斜权重给:
      • 较新且健康状况良好的发送号。

示例:

function pickSenderFromGrid(grid: GridState): Sender {
  const healthy = grid.senders.filter((s) => s.healthScore > MIN_HEALTH);
  const weighted = buildWeightedList(healthy, (s) => s.weight);
  return randomChoice(weighted);
}

其中:

  • healthScore(健康分数)基于:
    • 近期送达率。
    • 硬失败率。
    • 投诉率。
    • 自上次验证/预热以来经过的时间。

退役与冷却

可以实施如下规则:

  • 在以下情况下将发送号退役或进行冷却处理:
    • 在最近 N 条消息中,硬失败率超过 1% 到 2%。
    • 某一时间段内投诉率超过 0.3% 到 0.5%。
    • 运营商特定的错误码出现激增。

被退役的发送号:

  • 会被移出活跃轮换池。
  • 之后可能会用少量、安全的流量重新进行测试。

第 6 节:故障回退、重试与失败场景

即便路由设计得再好,问题依然会发生:

  • 运营商出现宕机。
  • 特定路由出现质量下滑。
  • 某个网格暂时被"烧坏"(信誉受损)。

故障回退原则

  1. 优先选择同族系内的回退方案

    • 在同一方案/同一国家内,从网格 A 切换到网格 B。
    • 让 OTP 始终留在 OTP 专用网格上,促销消息留在促销专用网格上。
  2. 避免在同一条已损坏的路径上立即反复重试

    • 主动采取激进的退避策略:
      • 指数退避或线性退避。
    • 将出现故障的路由/网格标记为**质量下滑(degraded)**状态。
  3. 优雅降级

    • 对于 OTP:
      • 在同一运营商族系内尝试其他发送号。
      • 考虑使用速度较慢但更可靠的备选方案。
    • 对于促销类消息:
      • 降低发送速率。
      • 若运营商明显不稳定,则推迟发送。

重试逻辑示例(简化版)

async function dispatchMessage(decision: RouteDecision, msg: Message) {
  try {
    const result = await sendToCarrier(decision, msg);

    updateMetrics(decision, result);
    return result;
  } catch (err) {
    markRouteAsDegraded(decision, err);

    const fallbackDecision = findFallback(decision, msg);
    if (!fallbackDecision) throw err;

    const fallbackResult = await sendToCarrier(fallbackDecision, msg);
    updateMetrics(fallbackDecision, fallbackResult);
    return fallbackResult;
  }
}

第 7 节:可观测性、日志与调试

运营商匹配路由的效果,取决于其可观测性的好坏。

你应该能够提出这样的问题:

  • "给我看过去 24 小时内通过网格 A 和网格 B 路由到 Verizon 的所有消息。"
  • "网格 C 中哪些发送号的硬失败率最高?"
  • "送达率下降的那个时间点附近,系统发生了什么变化?"

最基础的日志字段

针对每一条消息,至少应记录:

  • message_id
  • timestamp(时间戳)
  • customer_id(或项目/应用 ID)
  • destination_msisdn(如有需要应进行哈希/匿名化处理)
  • carrier_id
  • country_code
  • profile_id
  • grid_id
  • sender_id
  • route_id / 上游 ID
  • status(排队中、已发送、已送达、失败、未知)
  • error_code(如有)
  • dlr_timestamp
  • latency_ms
  • campaign_idflow_id(如适用)

仪表盘

  • 运营商 × 网格热力图
    • 送达率。
    • 硬失败率。
  • 发送号排行榜
    • 按健康度和吞吐量排序。
  • 异常检测
    • 在以下情况触发告警:
      • 运营商 X、网格 Y 的送达率跌破阈值。
      • 错误码出现激增。

事故处理流程示例

  1. 告警:"Verizon 在网格 US_PROMO_A 上的送达率下降超过 3 个百分点。"
  2. 查看日志:
    • 检查错误码及对应的数量。
    • 与其他网格进行对比。
  3. 缓解措施:
    • 暂时将 Verizon 的促销流量切换到网格 US_PROMO_B。
    • 降低发送速率。
  4. 深入调查:
    • 近期的内容/模板变更。
    • 路由配置方面的改动。

常见问题:面向开发者的运营商匹配路由

1. 每条消息都需要进行 HLR/号码查询吗?

不一定。

可选方案:

  • 将结果缓存一段合理的 TTL 时长。
  • 对高流量用户提前进行解析。
  • 在为网格填充号码池时进行批量查询。

2. 如何处理号码携转(携号转网)问题?

携号转网后的号码可能会更换运营商。良好的实践包括:

  • 定期刷新以下对象的运营商信息:
    • 高频目标号码。
    • 反复出现发送失败的号码。

3. 运营商匹配是否只在美国市场有意义?

并非如此。它在以下场景中尤其有用:

  • 任何存在多家运营商且各自表现差异明显的市场。
  • 发送号 ID 和消息模板具有运营商专属性的市场(许多欧洲/亚太市场都是如此)。

4. 这与 A2P 10DLC 及已注册的营销活动(campaign)之间是什么关系?

运营商匹配能够:

  • 针对每个运营商,正确使用已注册的营销活动和发送号
  • 帮助你将发送行为控制在每个营销活动所允许的吞吐量和内容规范之内。

5. 隐私和个人信息(PII)方面如何处理?

一套以隐私为先的实现方案应当:

  • 在日志中对 MSISDN(手机号)进行哈希处理。
  • 只存储最少必要的数据。
  • 仅保留运营商和路由相关的元数据,而不保留原始消息内容。

6. 我们能否在现有 CPaaS 之上叠加一层运营商匹配能力?

在某些情况下可以:

  • 如果该 CPaaS 对外提供:
    • 按运营商划分的控制能力。
    • 按发送号划分的统计数据。
  • 你就可以在其之上构建一个元路由层(meta-routing layer)

但效果最强的实现方式,依然是基于自有基础设施(自有 SIM 卡、私有网格)。


结语:从尽力而为的路由,走向工程化的路由

大多数 SMS 项目依赖的都是尽力而为式的路由

  • 服务商选择便宜或当下可用的路由。
  • 你只能拿到一两个指标。
  • 剩下的就只能靠运气。

运营商匹配路由把 SMS 变成了一套工程化的系统

  • 针对每个运营商的路径选择都是确定性的。
  • 网格和号池彼此隔离。
  • 具备健康度感知的轮换机制与故障回退能力。
  • 为事故排查提供丰富的可观测性数据。

如果你关心以下这些事情:

  • 实现并持续保持 99.4% 以上的送达率。
  • 在促销流量高峰和高风险使用场景下依然稳定运行。
  • 为你的 SRE/基础设施团队提供他们能够理解、值得信任的可控手段。

……那么,实施或选择一套具备扎实运营商匹配架构的网关,就不是一个"可有可无"的加分项,而是唯一理性的长期策略。

Dach SMS Lab

Dach SMS Lab