zoukankan      html  css  js  c++  java
  • .net core实践系列之短信服务-架构优化

    前言

    通过前面的几篇文章,讲解了一个短信服务的架构设计与实现。然而初始方案并非100%完美的,我们仍可以对该架构做一些优化与调整。

    同时我也希望通过这篇文章与大家分享一下,我的架构设计理念。

    源码地址:https://github.com/SkyChenSky/Sikiro.SMS/tree/optimize (与之前的是另外的分支)

    架构是设计的还是演变的?

    架构

    该词出自于建筑学。软件架构定义是指软件系统的基础结构,是系统中的实体及实体(服务)之间的关系所进行的抽象描述。而架构设计的目的是为了解决软件系统复杂度带来的问题。

    复杂度

    系统复杂度主要有下面几点:

    • 高可用
    • 高性能
    • 可扩展
    • 安全性
    • 维护成本
    • 用户规模

    业务规模

    系统的复杂度导致的直接原因是业务规模。为了用户流畅放心的使用产品,不得不提高系统性能与安全。当系统成为人们生活不可缺一部分时,避免机房停电、挖掘机挖断电缆导致的系统不可用,不得不去思考同城跨机房同步、异地多活的高可用方案。

    答案并非二选一

    我认为架构,需要在已知可见的业务复杂度与用户规模的基础上进行架构设计;伴随着技术积累与成长而对系统进行架构优化;用户的日益增长,业务的不断扩充,迫使了系统的复杂度增加,为了解决系统带来新的复杂度而进行架构演变。

    因此,架构方案是在已有的业务复杂度、用户规模、技术积累度、人力时间成本等几个方面的取舍决策后的结果体现。

    原架构

    缺点分析

    • 一般情况下,调度任务轮询数据库,90%的动作是无用功,频繁的数据库访问会对数据库增加不少压力。
    • 为了让调度任务服务进行轮循数据,需要在API优先进行数据持久化,这无疑是降低了API的性能。
    • MongoDB的Update操作相比于Insert操作时低效的,对于日志类数据应增量添加。

    因此从上述可见,调度任务服务这块是优化关键点所在。

    新架构图

    • 使用了RabbitMQ的队列定时任务代替调度任务来实现定时发送。
    • 抛弃了调度任务,减少了调用链,同时也减少了应用服务数据量。
    • 对SMS集合在MongoDB里进行按年月的时间划分,对于日志类数据可以在有效的时间范围外进行方便的归档、删除。同时也避免了同集合的数据量过大导致的查询效率缓慢。

    队列定时任务

    RabbitMQ自身并没有定时任务,然而可以通过消息的Time-To-Live(过期时间)与Dead Letter Exchange(死信交换机)的结合模拟定时发布的功能。具体原理如下:

    • 生产者发布消息,并发布到已申明消息过期时间(TTL)的缓存队列(非真正业务消费队列)
    • 消息在缓存队列等待消息过期,然后由Dead Letter Exchange将消息重新分配到实际消费队列
    • 消费者再从实际消费队列消费并完成业务

     

    Dead Letter Exchange

    Dead Letter Exchange与平常的Exchange无异,主要用于消息死亡后通过Dead Letter Exchange与x-dead-letter-routing-key重新分配到新的队列进行消费处理。

    消息死亡的方式有三种:

    • 消息进入了一条已经达到最大长度的队列
    • 消息因为设置了Time-To-Live的导致过期
    • 消息因basic.reject或者basic.nack动作而拒绝

    Time-To-Live

    两种消息过期的方式:

    队列申明x-message-ttl参数

    var args = new Dictionary<string, object>();
    args.Add("x-message-ttl", 60000);
    model.QueueDeclare("myqueue", false, false, false, args);

    每条消息发布声明Expiration参数

    byte[] messageBodyBytes = System.Text.Encoding.UTF8.GetBytes("Hello, world!");
    
    IBasicProperties props = model.CreateBasicProperties();
    props.ContentType = "text/plain";
    props.DeliveryMode = 2;
    props.Expiration = "36000000"
    
    model.BasicPublish(exchangeName,
                       routingKey, props,
                       messageBodyBytes);

    RabbitMQ.Client队列定时任务Demo

    class Program
        {
            static void Main(string[] args)
            {
                var factory = new ConnectionFactory
                {
                    HostName = "10.1.20.140",
                    UserName = "admin",
                    Password = "admin@ucsmy"
                };
    
                using (var connection = factory.CreateConnection())
                using (var channel = connection.CreateModel())
                {
                    var queueName = "Queue.SMS.Test";
                    var exchangeName = "Exchange.SMS.Test";
                    var key = "Route.SMS.Test";
    
                    DeclareDelayQueue(channel, exchangeName, queueName, key);
    
                    DeclareReallyConsumeQueue(channel, exchangeName, queueName, key);
    
                    var body = Encoding.UTF8.GetBytes("info: test dely publish!");
                    channel.BasicPublish(exchangeName + ".Delay", key, null, body);
                }
            }
    
            private static void DeclareDelayQueue(IModel channel, string exchangeName, string queueName, string key)
            {
                var retryDic = new Dictionary<string, object>
                {
                    {"x-dead-letter-exchange", exchangeName+".dl"},
                    {"x-dead-letter-routing-key", key},
                    {"x-message-ttl", 30000}
                };
    
                var ex = exchangeName + ".Delay";
                var qu = queueName + ".Delay";
                channel.ExchangeDeclare(ex, "topic");
                channel.QueueDeclare(qu, false, false, false, retryDic);
                channel.QueueBind(qu, ex, key);
            }
    
            private static void DeclareReallyConsumeQueue(IModel channel, string exchangeName, string queueName, string key)
            {
                var ex = exchangeName + ".dl";
                channel.ExchangeDeclare(ex, "topic");
                channel.QueueDeclare(queueName, false, false, false);
                channel.QueueBind(queueName, ex, key);
            }
        }

    Sikiro.SMS实现优化

    上面介绍了队列定时任务基本原理,然而我们需要自己的项目进行修改优化。

    API消息发布

    EasyNetQ是一款非常良好使用性的RabbitMQ.Client封装。对队列定时任务他也已经提供了相应的方法FuturePublish给我们使用。

    然而他的FuturePublish由有三种调度方式:

    • DeadLetterExchangeAndMessageTtlScheduler
    • DelayedExchangeScheduler
    • ExternalScheduler

    DelayedExchangeScheduler是需要EasyNetQ项目提供的调度程序,本质上也是轮询

    ExternalScheduler是通过使用MQ的插件。

    DeadLetterExchangeAndMessageTtlScheduler才是我们之前通过DEMO实现的方式,在EasyNetQ组件上通过下面代码进行启用。

    services.RegisterEasyNetQ(_infrastructureConfig.Infrastructure.RabbitMQ, a =>
                {
                    a.EnableDeadLetterExchangeAndMessageTtlScheduler();
                });

    下面代码是Sikiro.SMS.Api的优化改造:

    /// <summary>
            /// 添加短信记录
            /// </summary>
            /// <param name="model"></param>
            /// <returns></returns>
            [HttpPost]
            public ActionResult Post([FromBody] List<PostModel> model)
            {
                _smsService.Page(model.MapTo<List<PostModel>, List<AddSmsModel>>());
    
                ImmediatelyPublish();
    
                TimingPublish();
    
                return Ok();
            }
    
            /// <summary>
            /// 及时发送
            /// </summary>
            private void ImmediatelyPublish()
            {
                _smsService.SmsList.Where(a => a.TimeSendDateTime == null).ToList().MapTo<List<SmsModel>, List<SmsQueueModel>>()
                    .ForEach(
                        item =>
                        {
                            _bus.Publish(item, SmsQueueModelKey.Topic);
                        });
            }
    
            /// <summary>
            /// 定时发送
            /// </summary>
            private void TimingPublish()
            {
                _smsService.SmsList.Where(a => a.TimeSendDateTime != null).ToList()
                    .ForEach(
                        item =>
                        {
                            _bus.FuturePublish(item.TimeSendDateTime.Value.ToUniversalTime(), item.MapTo<SmsModel, SmsQueueModel>(),
                                SmsQueueModelKey.Topic);
                        });
            }

    重发机制

    重发一般是请求服务超时的情况下使用。而导致这种原因的主要几点是网络波动、服务压力过大。因为前面任意一种原因都无法在短时间恢复,因此对于简单的重试 类似while(i<3)ReSend() 是没有什么意义的。

    因此我们需要借助队列定时任务+发送次数*延迟时间来完成有效的非频繁的重发。

     public void Start()
            {
                Console.WriteLine("I started");
    
                _bus.Subscribe<SmsQueueModel>("", msg =>
                {
                    try
                    {
                        _smsService.Send(msg.MapTo<SmsQueueModel, SmsModel>());
                    }
                    catch (WebException e)
                    {
                        e.WriteToFile();
    
                        ReSend();
                    }
                    catch (Exception e)
                    {
                        e.WriteToFile();
                    }
                }, a =>
                {
                    a.WithTopic(SmsQueueModelKey.Topic);
                });
            }
    
            private void ReSend()
            {
                var model = _smsService.Sms.MapTo<SmsModel, SmsQueueModel>();
                model.SendCount++;
    
                _bus.FuturePublish(TimeSpan.FromSeconds(30 * model.SendCount), model, SmsQueueModelKey.Topic);
            }

    SMS日志集合维度

    SMS日志作为非必要业务的运维型监控数据,在需要的时候随时可以对此进行删除或者归档处理。因此以时间(年月)作为集合维度,可以很好的对日志数据进行管理。

    mongoProxy.Add(MongoKey.SmsDataBase, MongoKey.SmsCollection + "_" + DateTime.Now.ToString("yyyyMM"), model);

    结束

    经过本系列6篇的文章,介绍了以短信服务为业务场景,基于.net core平台的一个简单架构设计、架构优化与服务实现的实践例子。希望我的分享能帮助有需要的朋友。如果有任何好的建议请到下方给我留言。

  • 相关阅读:
    倍增 思想与操作
    JZOJ 4307. 【NOIP2015模拟11.3晚】喝喝喝
    快速幂 简单快速的乘方运算
    GDKOI2018总结
    myeclipse连接oracle数据库调试方法
    Error filterStart Context [/toucher] startup failed due to previous errors
    Win8初体验
    09Java项目比武大会
    学习笔记Oracle常见错误
    关于小黑双系统问题
  • 原文地址:https://www.cnblogs.com/skychen1218/p/9565198.html
Copyright © 2011-2022 走看看