短信系统开发 (短信平台系统搭建流程)

短信系统相信大家并不陌生,在日常生活中,用户注册登录、密码找回、账户变更、确认支付、活动认证、营销推广等场景都会出现它的身影。本文将着重从短信的高可用和可观测两个方面去介绍短信系统。

短信系统的核心流程目前强依赖于一些外部资源,如下游服务、MySQL等,外部资源异常时往往会导致短信服务完全不可用,这极大地影响了短信系统的稳定性。另外短信系统也强依赖多家三方服务商的发送能力,由于缺少对每家服务商的质量评估,至少需要在一天后才能感知到三方通道异常的状态,期间会造成不必要的业务损失。

基于以上问题,我们期望本次优化能实现如下两个目标:

  1. 提高短信短信服务稳定性,使其具有快速感知故障的能力,并保障接口可用性在99.99% 以上
  2. 提高短信系统的可观测性,完善对多家服务商的质量观测与评估体系,实现渠道异常的及时感知

现状

发短信平台系统搭建,短信系统开发

短信系统整体由短信服务和短信指标观测模块两部分构成。

短信服务提供基础的短信验证码发送、校验、回执、上行能力,其中短信验证码发送链路中存在稳定性风险点如下:

【服务强依赖】下游加解密服务异常,导致手机号加密失败,验证码流程结束

【 Redis 强依赖】Redis 异常,导致 requestID 生成失败,验证码流程直接结束

【 MySQL 强依赖】MySQL 异常,导致查询、更新、保存验证记录失败,验证码流程直接结束

短信指标观测模块提供短信指标计算、短信指标可视化、短信指标告警能力。短信的可观测性不足体现在如下几个方面:

【指标缺失】短信回填率、到达率等核心指标缺失,无法评估接入的三方服务商质量优劣

【指标可视化】目前可视化展示方式对业务方不够友好、展示维度不足

【告警缺失】指标异常时,没有告警机制,无法及时感知其质量变化

整体方案

优化思路

  • 针对 Redis 强依赖,使用 UUID 算法直接进行替换
  • 针对服务强依赖,使用消息队列来做服务解耦。
  • 针对 MySQL 强依赖,通过引入冗余存储介质如 Redis 实现容灾
  • 针对质量观测体系不完善,通过梳理观测指标,优化采集方案,实现对整个短信服务质量的观测

设计实践

消除 Redis 的强依赖

发短信平台系统搭建,短信系统开发

Redis 存在的意义只是为了生成全局唯一的ID,可以使用 UUID 算法直接进行替换

消除对服务的强依赖

发短信平台系统搭建,短信系统开发

对手机号使用 MD5 加密算法作为直接结果入库,同时通过投入 Kafka 队列,异步调用加解密服务来实现手机号对称加密,异步更新数据库中的对称加密手机号字段,保证此字段优化前后逻辑的一致性

消除 MySQL 的强依赖

发短信平台系统搭建,短信系统开发

通过新增 Redis 冗余存储,在故障期间通过执行等效 MySQL 语句的 Redis 命令来实现容灾。引入新的存储对象后,我们需要考虑4中存储状态,依据不同的状态执行不同的故障处理逻辑。下面列出了一些本次存储层改造的难点和对应的解决方案:

01

MySQL 故障感知&恢复

MySQL 执行 SQL 语句异常时,会同时返回错误码和错误信息,我们可以依据错误码来感知 MySQL Server 的状态,当然对于网络不稳定的情况,我们需要对相关的错误码做频率检测。

MySQL的恢复,依赖于人工控制。MySQL 故障发生后,会触发相关告警,DEV 和 SRE 会进行紧急修复,恢复后,手动去修改远程配置使其恢复。

02

Redis 故障感知&恢复

Redis 执行相关命令异常时,会返回错误信息,我们可以依据错误信息是否包含某些关键词来感知 Redis Server 的状态,同理对于网络不稳定的情况,我们需要对相关的错误频率做检测。

Redis 的恢复,依赖于人工控制。Redis 故障发生后,会触发相关告警,DEV 和 SRE 会进行紧急修复,恢复后,手动去修改远程配置使其恢复。

03

MySQL、Redis 的切换时机

发短信平台系统搭建,短信系统开发

服务通过实时监听远程配置中 MySQL故障开关和 Reids 故障开关来控制不同执行流程。当故障发生时,触发告警,同时更新相关远程配置状态,切换执行流程;当故障恢复时,人工将相关配置重置为恢复状态即可。

04

SQL 容灾

我们可以使用状态模式,定义故障转移对象,依据其内部当前所处的不同状态触发不同的行为,以此来避免工程中出现大量重复的条件语句块。接下来我将从故障转移对象的定义、处理流程、使用三个方面来讲解

故障转移对象的定义如下

// State 故障转移状态对象
type State struct {
    acquireStatus func() Status // 获取当前的故障状态
 
    setMySQLFatalFlag func(context.Context) error // 打开 mysql 故障标志位
    setRedisFatalFlag func(context.Context) error // 打开 redis 故障标志位
 
    runMaster func() error // mysql 正常时,相关的执行函数
    runBackup func() error // redis 正常时,相关的执行函数
    recordSQL func()       // 当 mysql异常时,执行的函数; 如:大数据日志记录, sql 语句记录
}
 
 
// Run 依据当前内存中开关状态,执行不同处理流程
func (s *State) Run(ctx context.Context) error {
    var fn func(context.Context) error
    switch s.acquireStatus() {
    case StatusHealthy:
        fn = s.runFlow
    case StatusMysqlFatal:
        fn = s.runRedisFlow
    case StatusRedisFatal:
        fn = s.runMySQLFlow
    default:
        fn = s.runDBFatalFlow
    }
    return fn(ctx)
}

故障转移对象的处理流程如下

发短信平台系统搭建,短信系统开发

故障转移对象调用

// insertSmsCodeRecordProcess 验证码短信发送记录入库
func (s *Service) insertSmsCodeRecordProcess(ctx context.Context, req *model.SmsSendReq, record *dao.SmsTable) error {
    tableName := dao.GenMonthTable(record.MsgType, 0)
    err := failover.Option{
        AcquireStatus:     s.watcher.AcquireFailoverStatus,
        SetMySQLFatalFlag: s.setMySQLFatalFlag,
        SetRedisFatalFlag: s.setRedisFatalFlag,
 
        RunMaster: func() error {
            // SQL 语句
            err := s.insertSmsCodeRecordToMySQL(ctx, tableName, record)
            if err != nil {
                return err
            }
            if s.watcher.AcquireFailoverStatus().GetRedisStatus() == model.SwitchON {
                return nil
            }
            // redis 执行函数
            errRedis := s.insertSmsCodeRecordToRedis(ctx, record)
            if failover.IsRedisFatalErr(errRedis) {
                s.setRedisFatalFlag(ctx)
            }
            return nil
        },
        RunBackup: func() error {
            // redis 执行函数
            return s.insertSmsCodeRecordToRedis(ctx, record)
        },
        RecordSQL: func() {
            // redis 故障期间的日志记录
            logging.Log(sendCodeLoggerName).Info(s.dao.MakeInsertUserAccountSQL(tableName, record))  // 记录对于的SQL语句
            bigdata.BigData(model.SmsSendMysqlFatalTopic, req.AppKey, req.Atom, record.BigDataSendInfo(req.PhoneValid, req.Resend)) // 记录 bigdata 数据,确保SLA数据不受影响
        },
    }.New().Run(ctx)
    if err != nil {
        logging.Error(err)
    }
    return err
}

以上可以看出,RunMaster 函数负责执行 SQL 语句以及故障发生瞬间的 SQL 容灾逻辑;RunBackup 函数负责执行故障发生后的 SQL 容灾逻辑;RecordSQL 函数负责将故障发生瞬间、以及故障发送后的相关 SQL 语句记录下来,以便故障恢复后进行MySQL 数据恢复。

05

故障记录恢复

故障期间,通过 RecordSQL 函数将需要执行的 SQL 语句写到磁盘上,数据恢复时可以直接导入 MySQL 执行

质量观测体系的搭建

以上我们完成了短信服务相关改造的介绍,接下来我将从指标梳理和指标采集架构两个方面来介绍短信指标观测模块的改造

指标梳理

发短信平台系统搭建,短信系统开发

我们本次额外新增了10个指标,其中三方接口成功率、回执率、到达率、回填率是需要核心关注的指标。其计算方式和含义如下:

三方接口成功率

公式:请求三方接口成功数 / 请求总数

含义:用来衡量三方接口的稳定性

回执率:

公式:短信回执数 / 短信发送数

含义:用来衡量三方服务商的回执情况

到达率:

公式:短信回执成功数 / 短信发送数

含义:短信发送到客户端的成功率

回填率:

公式:用户真实校验数 / 短信发送数

含义:短信真实发送到客户端且用户进行校验的比率

指标采集、展示架构图

发短信平台系统搭建,短信系统开发

通过分析采集到的记录表,为每个业务输出当日质量日报、提供不同运营商近期的服务质量变化趋势图、质量监控告警等展示方式,实现对整个短信服务质量的观测

收益

  1. 当前短信服务质量已保证99.99%的可用性,近一年无重大故障
  2. 已完成对短信质量观测体系的搭建,针对短信渠道质量异常,可在20分钟内发现并解决问题

展望未来

  1. 短信地区级精细化运营,目前已实现对海外各地区的覆盖,但针对某些地区的短信质量还有较高的提升空间
  2. 自动化分析工具完善,短信质量下降后,无法快速明确下降的原因,需要耗费较多的时间去分析

作者:冯志全

来源:微信公众号:映客技术

出处:https://mp.weixin.qq.com/s/RSIyTKmNvTUDhHjPH8Rnjw