.net core实践系列之短信服务-架构优化

前言

经过前面的几篇文章,讲解了一个短信服务的架构设计与实现。然而初始方案并不是100%完美的,咱们仍能够对该架构作一些优化与调整。git

同时我也但愿经过这篇文章与你们分享一下,个人架构设计理念。github

源码地址: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平台的一个简单架构设计、架构优化与服务实现的实践例子。但愿个人分享能帮助有须要的朋友。若是有任何好的建议请到下方给我留言。

相关文章
相关标签/搜索