聊架構 – 大型網站設計中多人上線指標Transactions Per Second(TPS)定義

大型網站的架構設計中,主要都是以同一時間系統能處理大量網頁請求或處理任務的能力當標準值。假設說,五月天的演唱會訂票系統瞬間會有大量搶票、支付,或是系統同時會有幾十萬客戶上線,這種有機會讓系統癱瘓掉的都屬於「高併發」。
而高併發衡量的指標主要是以”Transactions Per Second(TPS)”就是每秒處理交易數量,就如同剛才說的五月天演唱會搶票,以2024年6月統計7-11有12000家同時有人搶票,就算是高併發的一個場景。
一般而言TPS 落在1000-5000 屬於具有一定程度的併發,而5000以上就算高併發,像50000以上就屬於超大型網站會遇到的超高併發了。
要支撐這樣的場景,需要採用許多高併發的技術方案。
一、分散式架構
將系統的負載分散到多個伺服器的節點,提高擴展與可靠性。例如:增加伺服器的數量

二、微服務架構

把系統從單體拆分成相互獨立的服務,系統各自有自己的業務功能,彼此間透過RPC HTTP 做通訊。而每個微服務可以根據流量/負載做水平擴增。微服務的好處還可以獨立部署或擴增,減少耦合。

三、Cche機制

透過減少直接連接資料庫來提高回應速度,降低伺服器與資料庫的壓力。而用最多的莫過於使用redis 集群方式來擴展性能。把會頻繁查詢的資料,像商品、會員等放在記憶體中,提高併發量。

四、Load Balance 負載平衡

把客戶端的請求分配到不同伺服器,避免單一台伺服器過載,例如像nginx、HAProxy、F5等技術。而負載平衡有三種主要的策略來做分配

  • 輪詢:依順序把請求配到每一台伺服器
  • 權重:根據伺服器效能設定不同權重,能者多勞(不是過勞喔)的意思
  • IP Hash:透過請求的IP來分配,把同一個IP分配到同一台,這樣可以讓Session保持

五、流量的削峰

這是常用到的方式,避免瞬間大量沖垮服務過載。常用的方法是透過佇列的排隊來處理,比方說放到MQ這種訊息佇列來做承受大流量。

六、限流

透過限制請求的速率,主要有Token Bucket、Leaky bucket或計數器三種演算法

可參考:[Architecture] 架構設計 – 限流策略 Rate Limiting Strategies

七、熔斷

當某服務故障、過慢,熔斷機制可臨時中斷請求,避免影響

八、降級

關閉一些非核心的服務,降低系統壓力。

讓MS SQL與Redis資料同步的幾種作法

首先,ms sql是資料庫,是永久儲存資料用,保證了資料的可靠性;而redis是拿來做暫存,主要提昇資料存取的性能。

而讓資料庫與暫存達到一致是相當經典的問題。

首先先探討不一致的發生原因,通常是發生在資料有變動的時候,因為資料變更需要同時操作資料庫以及暫存,因為不同系統,無法同時操作瞭解成功與失敗,且有時間差。就導致不一致~

提出幾個作法:

一、先刪暫存,再更新資料庫(在高併發讀寫時會不一致,不建議)

二、先更新資料庫,再刪除暫存【Cache Aside Pattern】(會出現一小段不一致)

三、只更新暫存,讓暫存同步更新資料庫【Read/Write Through Pattern】(不一致的機會很低)

四、只更新暫存,讓暫存非同步更新資料庫【Write Behind Cache Pattern】(性能最高,但如果暫存更新回資料庫時暫存掛了,資料會遺

C# RabbitMQ模擬Producer與Consumer

最近系統考量可靠性問題,在中間加上一層 Message Queue(MQ)的架構,採用的是目前最多人使用的RabbitMQ當作server服務。順便直接以百萬筆資料為單位塞入MQ來測試一下效能。

Producer程式

System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();//引用stopwatch物件
            sw.Reset();//碼表歸零
            sw.Start();//碼表開始計時
            //
            string queue = "info";
            try
            {
                var factory = new RabbitMQ.Client.ConnectionFactory();
                factory.HostName = "localhost";
                factory.UserName = "guest";
                factory.Password = "guest";

                using (var connection = factory.CreateConnection())
                {
                    using (var channel = connection.CreateModel())
                    {
                        bool durable = true;
                        channel.QueueDeclare(queue, durable, false, false, null);
                        RabbitMQ.Client.IBasicProperties properties = channel.CreateBasicProperties();
                        properties.DeliveryMode = 2;
                        for (int i = 1; i <= 100; i++)
                        {
                            LogInfo info = new LogInfo();
                            info.SYS_ID = "系統";
                            info.COMPANY_ID = "公司";
                            info.STORE_ID = "店別";
                            info.Content = "訊息";
                            string m1 = Newtonsoft.Json.JsonConvert.SerializeObject(info);
                            var body = Encoding.UTF8.GetBytes(m1);
                            channel.BasicPublish("", queue, properties, body);
                        }

                    }
                }
            }
            catch (System.Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
            sw.Stop();
            string result1 = sw.Elapsed.TotalMilliseconds.ToString();
            Console.WriteLine("共花費:" + result1 + "毫秒");
            Console.ReadLine();

Consumer端程式

    string queue = "info";
            try
            {
                var factory = new ConnectionFactory();
                factory.HostName = "localhost";
                factory.UserName = "guest";
                factory.Password = "guest";
                var connection = factory.CreateConnection();
                var channel = connection.CreateModel();
                channel.QueueDeclare(queue, true, false, false, null); // 定義處理那一個queue
                channel.BasicQos(0, 1, false);  // 每次處理1則

                var consumer = new RabbitMQ.Client.Events.EventingBasicConsumer(channel); 
                // 定義收到queue的內容處理方式
                consumer.Received += (sender, e) =>
                {
                    byte[] body = e.Body.ToArray();
                    string message1 = Encoding.UTF8.GetString(body); 
                    LogInfo log = JsonConvert.DeserializeObject<LogInfo>(message1);  // 將queue中的json轉回物件
                    // 以下可以更改為自己要處理的事項
                    Console.WriteLine(log.DateTime.ToString("yyyy/MM/dd HH:mm:ss:FFF")+" "+log.Content );  // 先顯示畫面上
                    //
                    channel.BasicAck(e.DeliveryTag, false); // 處理完手動回應    

                };
                channel.BasicConsume(queue, false, consumer);  // 開始處理
                Console.ReadLine();
                connection.Close();
                channel.Close();
            }
            catch (System.Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }