愛流浪的小風

技術隨手寫

使用Asp.Net MVC打造Web Api (26) - 使用Azure Service Bus處理瞬間大量請求

| Comments

在我們網站開發時,有時候會遇到瞬間大量請求,而系統卻來不及處理,導致網頁發生無法回應的情況,例如常常出現在活動開始報名,或是開放登記新手機等等場景,問題發生的原因是有可能網頁伺服器或資料庫面對Request時,必須要花時間處理,而一次能夠處理的Request數量也有限,就會造成網頁伺服器的Request Queue越來越多沒辦法消化的情況,更甚至是資料庫發生Dead lock,那就慘不忍睹了。

某些時候這種情境我們不需要馬上對資料進行變更或是即時回應給使用者,可以考慮先將傳入的訊息存入Service Bus的佇列中,讓完成請求的時間變短,然後系統在慢慢對佇列中的內容進行消化,已確保使用者的請求有被處理到,今天我就要向大家介紹如何使用Azure Service Bus來處理訊息佇列。

使用情境說明

假設我們網站的API開放給使用者進行新手機登記活動,假設因為伺服器等級的關係,我們處理每一個使用者進行登記請求,由於還要操作資料庫,透過Third Party發送簡訊等等行為,大約要五秒鐘才能回應給使用者是否登記成功,在一般流量的情況下是沒有什麼問題的。

但如果今天是熱門手機上市了,我們同時要面對幾千人進行登記,如果一個人要5秒鐘,那第一百個人要等待500秒伺服器才會回應它的話,那麼可能使用者都要抱怨連連了。

為了改善這種很不好的使用者體驗,我們可以利用佇列的特性,因為它是First In First Out,就算進入佇列中,我們還是可以保持使用者的先後順序,先讓使用者得到登記的回應,我們在透過後段的程式慢慢處理登記的流程。

這樣的模式讓使用者進行登記,而事後再接受到登記確認,使用者不會因為沒辦法送出Request而抱怨,而後續的動作就算需花比較久的時間,但也能夠確實的完成了!

建立Azure Service Bus服務

我們首先先在Azure建立一個Service Bus服務,用來儲存請求資料佇列

  1. 進入Azure入口,選擇建立命名空間

  2. 加入新的命名空間

  3. 建立完成後,可以看到下方有連接資訊,記住連接字串

撰寫登記API

接下來我們要來修改我們的API,提供使用者登記新產品,並儲存到Azure Service Bus的Queue中

  1. 在ViewModels新增登記資料的Class

    public class RegisterProductUserInfoModel
    {        
        public string Name { get; set; }
    
        public string CellPhone { get; set; }
    
        public string Email { get; set; }
    }    
    
  2. 在網站的ProductController新增登記商品Action

    public class ProductController : JsonNetController
    {            
        public IProductService ProductService { get; set; }
    
        public ILogger Logger { get; set; }
    
        public ProductController(IProductService productService, ILogger logger)
        {
            this.ProductService = productService;
            this.Logger = logger;
        }            
    
        [AuthorizeByToken(Roles = "Administrator")]
        public ActionResult RegisterNewProduct(RegisterProductUserInfoModel model)
        {
            this.ProductService.RegisterNewProduct(model);
    
            return Json(ApiStatusEnum.Success);
        }
    }
    
  3. 使用Nuget在BL中安裝Azure Service Bus的Library

  4. 在BL將資料送到RegisterProduct的佇列中

    public interface IProductService
    {            
        void RegisterNewProduct(RegisterProductUserInfoModel model);
    } 
    
    public class ProductService : IProductService
    {            
        public void RegisterNewProduct(RegisterProductUserInfoModel model)
        {            
            string connectionString = CloudConfigurationManager.GetSetting("azure.servicebus.connectionstring");
    
            // Create the queue if it does not exist already
            var namespaceManager = NamespaceManager.CreateFromConnectionString(connectionString);            
            if (!namespaceManager.QueueExists("RegisterProduct"))
            {
                namespaceManager.CreateQueue("RegisterProduct");
            }                
    
            QueueClient client = QueueClient.CreateFromConnectionString(connectionString, "RegisterProduct");
            client.Send(new BrokeredMessage(model));            
        }
    }
    
  5. 嘗試送出資料給API

  6. 送出成功後可以到Azure看到訊息有確實增加

撰寫佇列處理處理程式

在我們可以使用佇列來先儲存使用者送進來的登記需求之後,我們還需要一個程式來幫我們處理佇列中的需求,讓使用者的資料可以確實寫入資料庫,並發送登記成功的訊息

  1. 我們新增一個Cloud專案

  2. 選擇Service Bus的Worker role,會建立一個Cloud專案,和一個類別庫專案

  3. 其實到這邊大部分的程式碼都已經建立好了,我們先點擊Cloud專案角色,輸入Service Bus的連線字串

  4. 在類別庫專案加入我們需要的程式碼邏輯

    public class WorkerRole : RoleEntryPoint
    {
        // 佇列的名稱
        const string QueueName = "RegisterProduct";
    
        // QueueClient 可進行安全對話。已建議您進行快取, 
        // 而非在每次要求時重新建立
        QueueClient Client;
        ManualResetEvent CompletedEvent = new ManualResetEvent(false);
    
        public override void Run()
        {
            Trace.WriteLine("開始處理訊息");
    
            // 起始訊息幫浦,且已叫用每則已收到的訊息回呼,用戶端結果的呼叫會停止幫浦。
            Client.OnMessage((receivedMessage) =>
                {
                    try
                    {
                        var model = receivedMessage.GetBody<RegisterProductUserInfoModel>();
    
                        // 寫入資料庫
                        Trace.WriteLine("寫入資料庫...");
                        Trace.WriteLine("姓名: "+model.Name);
                        Trace.WriteLine("Email: " + model.Email);
    
                        // 發送訊息
                        Trace.WriteLine("發送訊息...");
                        Trace.WriteLine("發送簡訊給:" + model.CellPhone);
    
                        receivedMessage.Complete();
                    }
                    catch
                    {
                        receivedMessage.Abandon();
                    }
                });
    
            CompletedEvent.WaitOne();
        }
    
        public override bool OnStart()
        {
            // 設定並行連線數目上限 
            ServicePointManager.DefaultConnectionLimit = 12;
    
            // 若不存在,請建立佇列
            string connectionString = CloudConfigurationManager.GetSetting("Microsoft.ServiceBus.ConnectionString");
            var namespaceManager = NamespaceManager.CreateFromConnectionString(connectionString);
            if (!namespaceManager.QueueExists(QueueName))
            {
                namespaceManager.CreateQueue(QueueName);
            }
    
            // 起始到 Service Bus 佇列的連線
            Client = QueueClient.CreateFromConnectionString(connectionString, QueueName);
            return base.OnStart();
        }
    
        public override void OnStop()
        {
            // 關閉到 Service Bus 佇列的連線
            Client.Close();
            CompletedEvent.Set();
            base.OnStop();
        }
    }
    
  5. 執行程式之後,透過模擬器可以看到有確實處理訊息

本日小結

Service Bus適用於很多可允許非同步處理的場景,我們可以使用Queue來面對大量流量的部分,再透過Daemon來慢慢消化Queue中的資料,還可以依據伺服器的乘載量來調整Daemon的實體,讓我們提供給使用者較佳的使用者體驗,也是讓我們伺服器面對大流量時多了一層防火牆,避免瞬間的衝擊。關於今天的內容歡迎大家一起討論喔^_^

Comments

comments powered by Disqus