何謂高併發?

第一次的讀書會首先由高併發作為討論主題的開頭,在正式開始討論這個主題之前,通常會定義什麼是高併發? 其實高併發這個詞相較於中國,因為先天使用者人口基數的差別,在台灣除了特定的產業或是使用情境(直播、雙十一搶購、搶票等),對比之下是比較不被廣泛討論的一個主題。

那什麼是高併發呢?通常指的就是服務同時間能撐起高請求量就叫做高併發,通常我們在評估一個服務能夠同時承受多少的請求量有幾個可供參考的數據指標:

  • QPS: Queries Per Second,意即每秒能回應的查詢次數,通常用來表示服務的吞吐能力。
  • TPS: Transactions Per Secon,指 client 向 server 請求後得到最終結果的過程,與 QPS 不同的是 TPS 是對於一個頁面的請求,一個頁面請求可能包含著若干個對 server 的請求。
  • PV: Page View,指頁面流覽量,單一使用者可以計入多個 PV 的計算中。
  • UV: Unique Visitor,獨立訪問數,單一使用者只計入一個計數中,UV 記錄的是不重複的訪問人數。

至於多少的數量才稱得上高併發其實沒有一個明確定義的數字。但是能試想若今天一個服務能承受的吞吐量為 10 qps,我們能稱做他是高併發嗎?很顯然是不行的。那 10,000 qps 呢?這個數字毋庸置疑可以算的上高併發,而且以台灣目前來說,能稱得起這個量級請求的線上服務還是不多的,因為大多數時候也不會有這樣的場景存在。

壓力測試

通常我們在測量一個服務能承受的併發數時,都會透過一些工具或是第三方服務來輔助我們達成壓力測試,以下是我們所常見的工具:

  • ab
  • wrk
  • wrk2
  • vegeta
  • JMeter

這裡就有一個值得思考的問題,我們在選用這些測量工具時,會不會因為使用不同的工具而有不同的結果呢?通常會影響最終結果數字的有可能是設定參數或是本身工具的實作。例如:併發數、長短連線、thread 數量、timeout 時長等都會影響到最終測量出來的結果,又像是 ab 工具本身在壓測請求前會先將相關的 thread 都建立完成後才一次請求,比較容易得到不好的成績。

除了以上因素外,通常我們在進行壓力測試時,會避免在服務主機上進行,執行壓測的機器會分開,才能夠確保服務的機器資源沒有受到壓測工具所占用。而壓測的機器位置因為 latency 的不同也會影響測試結果,應該盡量符合真實的情境,結果會越接近真實的數字。

討論到真實的情境,導師提出了一個名詞叫做 Coordinated Omission,他是一個使壓測工具較能接近真實情境的演算法,上述的工具中支援這個演算法的工具目前只有 wrk2vegeta

事前估算請求量

大部分的服務並不是先天就能夠支援高併發的請求量,通常需要經過一連串架構的演進、複雜的架構堆疊與足夠的機器資源才能撐起巨大的請求量。以購物網站為例,平時的請求量可能不高,但在像雙十一或是動森主機開賣的這種場景才會有突然飆升的請求量。

一般來說,我們在估算在一時間區間內服務的併發量時,最簡單也最常被參考的就是二八法則,意即 80% 的請求量會集中在 20% 的時間區間內。但像上述的搶購極端場景下,可能會是 99.99% 的量集中在 0.001% 的時間區間內。

如果今天業務/行銷單位提出需求說接下來的活動預計會有一百萬的 MAU (Monthly Active Users),那我們如何根據這樣的數字來預估可能的併發數呢?

假設每個使用者單次操作都會有 3 個 API 請求,則根據前面的法則,就能推算出:

(100,000,0 x 3 API Requests) / (30 x 0.2 x 86,400) x 0.8 ≒ 4.6 QPS

架構的演進

為了能夠提升服務的併發量與可靠的穩定度,通常就需要一連串架構的堆疊。通常在服務的發展初期很常見的會是傳統的單體架構,並且可能 File Server、API Server、DB、Cache Server 等都集中在同一台主機上。這種做法最大的缺點就是,當一台機器掛掉,全部的服務就全部掛掉,而且容易因為單一服務資源競爭到其他服務的資源。

通常隨著服務量的增加,當一台機器撐不住的時候,在不考量改變單體架構的前提下,我們通常會對機器進行資源的擴充:

  • 垂直擴充
  • 水平擴充

垂直擴充為最簡單、最暴力的擴充方式,意即直接提升單一主機的硬體能力來達成更高的負荷量。但這種作法缺點也很明顯,隨著硬體成本的一直上升,但負荷量的提升不一定能符合比例,而且當這台機器掛掉時,便所有的服務都停擺。這個時候最常見也最容易的拆法就是將 DB、Cache Server 等拆成各自的機器。

水平擴充則是透過增加機器的數量來達成整體吞吐量的提升,通常在這群機器的前面會有負載平衡器(LB)來進行流量的轉導。而且當機器數量足夠時,通常前面的負載平衡器會進行定期的 health check 確保背後運作的機器可提供服務,即便是其中一台機器掛掉,也會避免將請求轉發至那台有問題的機器上,確保其他正常的機器能繼續提供服務。

通常隨著請求量的增加,一些常用、複雜的查詢結果,我們會加入 Cache Server 來減緩壓力,Cache 雖然是方便的機制,但也須注意以下幾點:

  • 快取失效
  • 緩存穿透
  • 雪崩效應

隨著業務發展,當每個服務的 Domain 逐漸清晰且較穩定時可以嘗試將單體服務切分成各自不同 Domain 的微服務。微服務解決了傳統單體架構服務資源分配不均的問題、去中心化及避免單一服務的故障而影響到其他正常的服務。但是微服務相對來說開發較為複雜,對於初期業務劃分不清、需求時常大改、開發人力不足的狀況之下,微服務的成本可能會高於實際所獲得的好處。

當討論到微服務,我們又討論到關於 Service Mesh 與 API Gateway 兩種不同的實踐方式以及各自的優缺點,可以參考:

通常在規劃微服務架構時,首先面臨的難題就是該如何拆分服務?服務的系統邊界該如何定義?再來微服務實質上可能無法提升單一請求的回應時間,以往的單一請求可能在微服務的架構下反而還會變得比起以前的單體架構來的更加耗時。

在這次的討論中,導師也提到了 Performance 金三角的概念:

  • Throughput
  • Latency
  • Memory

他們之間會相互影響,若選擇其中的某項作為目標,其他項目則可能就無法全部兼顧。在選用任何的技術架構前,都應先考量面臨的問題是什麼?選用的架構解決了什麼問題?他背後所帶來的代價/犧牲掉的東西是什麼?

資料庫的選擇

如何根據適合的業務場景選擇合適的資料庫之前,要先了解各種類型的資料庫之間的差異

  • RDBMS
  • NoSQL
  • New SQL

RDBMS 很講求資料正確性,通常都會提供 ACID 的保證,NoSQL 本身則難保證 ACID,尤其是 Consistency,所以通常都會只達成 Eventually Consistent,也就是當下可能會不一致,要過了一段時間之後資料才會一致,所以 NoSQL 就有可能出現同一筆資料在現在與晚一點的時間讀取時結果不一樣的狀況產生。

其中討論到 MySQL 的近代發展與 MariaDB 之間的比較,MariaDB 是基於 MySQL 5.6 後 fork 出來由社群維護的版本。所以在 MySQL 5.7 之後所提供的功能在 MariaDB 上是沒有的(例如:JSON 的支援)。在 MySQL 從 5.6 到 8.0 的寫入速度基本上是:

8.0 > 5.7 > 5.6

但在讀取速度上則是:

5.6 > 5.7 > 8.0

這樣的發展不難看出由於 RDBMS 的瓶頸多數還是在寫入效能上,讀取容易擴充,但是寫入相較之下則困難許多,所以這樣的發展也不難理解了。

NewSQL 為新一代關聯式資料庫(例如:TiDB),意在整合 RDBMS 所提供的 ACID 以及 NoSQL 提供的橫向可擴展性。

會後問題發想:

  • ORM 優缺點
  • 資料庫中 null 欄位設計的好壞
  • store procedure 與 trigger 的好壞

Migration

在本次的討論中有學員提出如何在正式環境中資料庫 migration 的過程中確保服務不中斷。畢竟資料庫的 migration 不像是程式的版本控制一樣,可以在線上服務中隨時切換到任一版本。在線上環境對資料庫進行 migration 有一些做法能盡量避免非預期的錯誤:

  • 避免對 table 進行欄位的破壞性更改/刪除
  • 對複製的 table 進行修改,並用 trigger 同步新增的操作,完成後重新命名 table

然而若是 table 中的欄位不進行刪除,久而久之就會遺留下很多 deprecated 的欄位,對於團隊的維護造成額外的同步成本。另外由於 RDBMS 多以 Row Based 設計,所以在新增/查詢資料時會造成性能的額外消耗,反之 NoSQL 多以 Column Based,因此較沒有這個問題。

Ant 也分享了一個開源的線上 MySQL 資料庫 migration 工具 gh-ost,可以幫助完成這些複雜的操作:

架構演進的時機

我們都知道一個服務的架構通常應該是逐步演進的,原因很簡單,架構之所以會演進一定是因為目前的架構無法有效率的去解決既有的問題或是已經跟不上業務發展的速度。但通常架構的演進通常是越來越複雜,甚至有一些高成本的異動方式(例如:將某個服務換一個語言重新實作)。

若單純站在技術的角度來說,我們當然會希望用最有效率的方式將事情做好,但以公司整體立場而言,若今天的架構演進需要大量的金錢/時間成本來進行迭代,那就必須去考量實際上執行完成後的效益是否符合成本。

心得

在參加 TGO Next 計畫之前曾經參加過幾次 TGO 所舉辦的內部分享,每次的分享內容都讓我覺得受益良多。因緣際會之下知道 TGO Next 這個計畫,一得知在這個計畫裡面有機會能和許多業界的前輩和其他來自不同領域的學員一起交流就讓我二話不說的報名參加。

很幸運地在面試流程結束後有機會加入 Ant 所帶領的技術架構組,我們組別的討論進行方式非常的開放,每一次討論的主題都是由學員們自行票選決定,如果臨時有什麼想了解的技術議題也都可以在中途提出來和導師學員一起進行討論。短短的幾次討論中我們的主題從高併發架構、線上 migration、微服務、資料庫選型到 logging 涵蓋各個面向。

對我來說 TGO Next 給我最大的收穫除了在業界經驗豐富的導師外,學員本身也來自許多各個不同的領域,因此都有各自在特定產業、專案經驗的 know how。每次的討論議題都能夠聽到各個成員不同角度的看法,雖然大家都是圍繞著同一個主題,但是內容卻不會過於單一化,經過先發散後收斂的方式讓討論的面向更加的寬廣。

四個月的時間隨著每一次的討論很快就要結束了,這期間特別感謝 Ant 與同組的組員們,還有其他組別的學員們偶爾也會一同加入參與討論,讓每一次的討論都有滿滿的收穫。期許自己之後不管是在工作或是社群上也能繼續延續這樣的精神,將自己與別人的經驗透過各種不同的模式將經驗繼續傳承下去!