.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平臺的一個簡單架構設計、架構優化與服務實現的實踐例子。但願個人分享能幫助有須要的朋友。若是有任何好的建議請到下方給我留言。

相關文章
相關標籤/搜索