老闆:把系統從單體架構升級到叢集架構!

問北發表於2021-09-08

首發於公眾號:BiggerBoy

如題,本文針對工作中實際經驗,整理了把一個單體架構的系統升級成叢集架構需要做的準備工作,以及為叢集架構的升級做指導方針。

本文首先分析了單體架構存在的問題,然後介紹了叢集架構(好處、注意的問題、架構圖),接著分析了目前系統的主要功能以及叢集后需要做哪些調整,然後對叢集架構涉及的技術做橫向對比,最後確定技術選型。從這幾個方面介紹了從單體架構到叢集架構的改造過程,希望對你有幫助。

背景

單機存在單點故障的隱患

Jvm記憶體頻繁在某時段報警

單體架構存在的問題

專案目前的架構是單體垂直架構,只有一個服務節點,存在一些問題,以下是對存在問題的分析:

1、服務可用性差

單機部署只有一個節點提供服務,如果服務程式掛掉或伺服器當機導致服務不可用,將會影響使用者的正常使用,如果服務重新上線的時間很長,將會嚴重公司業務開展,對於下單等常規業務帶來的損失無法估量。這個問題就是我們常說的單點故障。

2、服務效能存在瓶頸

單機所能承載的讀寫壓力、請求數都是有限的,當系統業務增長到一定程度的時候,單機的硬體資源將無法滿足你的業務需求,增加伺服器配置所帶來的的效能提升與昂貴的成本不成正比,價效比不高。

3、不可伸縮性

單體架構的弊端之一就是伸縮性不強。隨著需求和負荷的增長,單體架構的效能滿足不了現有需求時,增加伺服器資源的手段收效甚微,服務的效能可擴充套件性低是單體架構的致命缺點。

4、程式碼量龐大,系統臃腫,牽一髮動全身

隨著業務功能增加,系統程式碼量不斷增加,系統變得龐大而臃腫,開發人員修改一處程式碼往往擔心會牽涉到其他功能。

據統計,專案後端程式碼行數高達12萬行(Java),前端程式碼行數高達34萬行(html+css+js),日常維護、版本迭代、發版上線的成本也相應增加。每次專案上線前都要對系統進行迴歸測試,上線時要把專案進行全量打包釋出,上線後監測系統日誌是否正常,擔心會不會影響其他功能模組。

可規劃系統拆分。

叢集架構

3.1 為什麼要叢集部署?

1、單點故障
單機部署很容易出現服務掛了之後,沒有備用節點,從而影響使用者使用。單機對外提供服務,風險很大,伺服器任何故障都可能引起整個服務的不可用。

2、效能瓶頸
單機遇到資源瓶頸時,要想支援更大的使用者量,效能有較大的提升,一般是優化業務和增加伺服器配置。然而這麼做只能是杯水車薪,成本巨大並且效果非常有限。而叢集部署通過部署多個服務節點水平擴充套件服務的效能,成倍的增加伺服器效能,而且支援動態擴充套件。

3.2 叢集的好處

1、高可用性。提高服務的可用性,只要有一個服務可用就能對外提供服務。高可用性是指,在不需要操作者干預的情況下,防止系統發生故障或從故障中自動恢復的能力。通過把故障伺服器上的應用程式轉移到備份伺服器上執行,叢集系統能夠把正常執行時間提高到大於99.9%,大大減少伺服器和應用程式的停機時間。

2、吞吐量。增加吞吐量,併發量,支援更大的使用者量。

3、易擴充套件。也叫可伸縮性,可伸縮性體現在節點數量的調整,在預知流量增大的情況下,可以提前增加節點。

3.3 叢集部署需要注意的問題

1、負載均衡問題。

請求應該由哪個節點處理?--應用叢集需要有一個元件來管理請求的分發。--常見的如Nginx

2、session失效問題。

登入請求被節點1處理後session儲存在節點1,後續的請求分發到節點2則需要重新登入。

--叢集環境下session需要同步。方案:Tomcat自帶的session複製、spring session+redis實現分散式session

3、定時任務執行問題。

定時任務分配給哪臺機器執行?

確保不會重複執行,最簡單的辦法就是定時任務拆分出來,只部署一個節點。方案:分散式job框架、分散式鎖實現job的分配。

4、快取一致性問題。

原來快取在本地的資料,需要保證資料的一致性,實現共享,只儲存一份。(比如兩個節點各快取了一份不同版本的資料,就會出現同一個頁面重新整理,交替展示不一樣的資料直到快取失效)。--Redis

3.4 叢集架構

站點部署多個節點,叢集前面架設Nginx做負載均衡,Tomcat之間通過Redis實現session共享。

圖片

前後端分離
圖片

系統功能點及叢集部署後需作何調整

4.1 功能點

主要分為這幾個大類:使用者登入登出、許可權控制、業務功能、定時任務。

4.2 使用者登入登出的處理

登入涉及到使用者資訊的儲存,目前單機部署,session交給web容器Tomcat管理,儲存在記憶體中。叢集環境多個Tomcat,當同一個使用者的多次請求被分發到不同的伺服器上,假設第一次請求訪問的A伺服器,建立了一個session,但是第二次請求訪問到了B伺服器,這時就會出現取不到session的情況,認為使用者沒有登入,跳到登入頁再次讓使用者登入,如此反覆。於是,叢集環境中,session共享就成了一個必須要解決的問題。

解決方案:

1.不要有session:大家可能覺得我說了句廢話,但是確實在某些場景下,是可以沒有session的,在很多介面類系統當中,都提倡【API無狀態服務】;也就是每一次的介面訪問,都不依賴於session、不依賴於前一次的介面訪問;

2.存入cookie中:將session儲存到cookie中,但是缺點也很明顯,例如:每次請求都得帶著session;session資料儲存在客戶端本地,是有風險的;

3.session同步:多個伺服器之間同步session,這樣可以保證每個伺服器上都有全部的session資訊。不過當伺服器數量比較多的時候,同步是會有延遲甚至同步失敗;

4.粘滯會話:使用Nginx(或其他負載均衡軟硬體)中的ip繫結策略,同一個ip只能在指定的同一個機器訪問,但是這樣做風險也比較大,而且也失去了負載均衡的意義;

5.session分散式儲存:把session放到Redis中儲存,雖然架構上變得複雜,並且需要多訪問一次Redis,但是這種方案帶來的好處也是很大的:實現session共享,可以水平擴充套件(增加Redis伺服器);伺服器重啟session不丟失(不過也要注意session在Redis中的重新整理/失效機制);不僅可以跨伺服器session共享,甚至可以跨平臺(例如網頁端和APP端)。

4.3 許可權控制的處理

系統中許可權控制基於資料庫許可權表實現,無需調整。

4.4 業務功能的處理

梳理出哪些業務功能會受到影響?影響包括變數的使用。

通過梳理系統中程式碼,發現目前系統中對資料的快取其實是快取在本地jvm記憶體中的,自己實現了一套快取過期機制,但是這種方式並未對快取佔據記憶體大小進行控制,這樣快取使用的記憶體有無限增長的可能,甚至導致記憶體洩漏。

這些變數用做本地快取,存在jvm中,若叢集部署,則在各自的jvm程式中都會存一份,不能共享,可能存在如下問題:第一次請求,由伺服器A處理,其查詢後存了一份資料V1,第二次請求由伺服器B處理,剛好資料發生變化,查詢後存了一份資料V2,後續請求如果均勻的分發到AB伺服器,那麼使用者看到的資料將一會兒是V1一會兒是V2(在快取未過期時),這樣就造成了資料不一致。

4.5 定時任務的處理

叢集部署的初衷是解決JVM記憶體頻繁告警,記憶體告警的原因可能是定時任務比較耗記憶體,本次討論不展開jvm記憶體頻繁告警的問題。另外,系統一旦做叢集部署就需要考慮叢集環境下的定時任務不能重複執行。

1. 程式碼拆分

JOB任務耗時耗記憶體佔用伺服器資源,對使用者操作有一定的影響,將定時任務從專案中拆分出來,單獨做個站點跑定時任務,採用單機/叢集部署。
在這裡插入圖片描述

拆分的好處:由定時任務耗時耗記憶體引起的記憶體告警,可能會影響正常業務進行,拆分的好處之一就是業務隔離。若不拆分,叢集部署只能將同一時間的定時任務分散到不同節點執行,分攤記憶體壓力。但若要徹底解決定時任務引起的記憶體報警,光靠叢集部署是不能徹底解決的,因為有可能某一時刻的定時任務都由同一個節點執行,這樣又回到單機的狀態,還是會發生記憶體告警問題。若要徹底解決,首先是拆分定時任務獨立執行,觀察記憶體情況後再做後續優化。

2. 任務防重複執行

如果將定時任務程式碼拆分且叢集部署或不拆分(原系統叢集部署),那麼定時執行的任務,需要控制同一個任務觸發時只有一個節點執行,可用分散式鎖實現、或quartz框架自身支援。

分散式鎖方式:執行前先嚐試獲取鎖,獲取到則執行,否則不執行。缺點:需引入分散式鎖,修改現有業務程式碼。

quartz自帶叢集功能的支援:需修改配置檔案,同時資料庫匯入quartz官方的11張表。優點:自帶功能,對外透明,不需要改程式碼,對現有業務影響較小。

叢集部署涉及的技術方案對比

5.1 負載均衡方案

在伺服器叢集中,需要有一臺伺服器充當排程者的角色,使用者的所有請求都會首先由它接收,排程者再根據每臺伺服器的負載情況將請求分配給某一臺後端伺服器去處理。

那麼在這個過程中,排程者如何合理分配任務,保證所有後端伺服器都將效能充分發揮,從而保持伺服器叢集的整體效能最優,這就是負載均衡問題。

負載均衡種類:DNS、硬體、軟體

參考:

負載均衡種類及優缺點

高併發解決方案之一 ——負載均衡

5.2 session共享

Tomcat叢集session複製
簡介:將一臺機器上的Session資料廣播複製到叢集中其餘機器上
使用場景:機器較少,網路流量較小
優點:實現簡單、配置較少、當網路中有機器Down掉時不影響使用者訪問
缺點:廣播式複製到其餘機器有一定延時,帶來一定網路開銷

多個伺服器之間同步session,這樣可以保證每個伺服器上都有全部的session資訊,不過當伺服器數量比較多的時候,同步是會有延遲甚至同步失敗;

實現方式參考:Tomcat叢集和Session複製說明

spring session+redis實現分散式session
圖片

簡介:將Session存入分散式快取叢集中的某臺機器上,當使用者訪問不同節點時先從快取中拿Session資訊
使用場景:叢集中機器數多、網路環境複雜
優點:可靠性好
缺點:實現複雜、穩定性依賴於快取的穩定性、Session資訊放入快取時要有合理的策略寫入

把session放到Redis中儲存,雖然架構上變得複雜,並且需要多訪問一次Redis,但是這種方案帶來的好處也是很大的:實現session共享,可以水平擴充套件(增加Redis伺服器),應用伺服器重啟session不丟失(不過也要注意session在Redis中的重新整理/失效機制),不僅可以跨伺服器session共享,甚至可以跨平臺(例如網頁端和APP端)。

5.3 定時任務防重複執行

指定某一個節點執行
通過特定IP限制,在定時任務的程式碼上加一段邏輯:僅某個ip的伺服器能執行該定時任務。

優點:解決方法容易理解,部署簡單,不需要多套程式碼。

缺點:

需要修改現有程式碼;

存在單點問題,只能規定一臺伺服器執行,發生故障時需要人工介入。

通過鎖控制
鎖的性質需滿足悲觀、獨佔、非自旋、分散式。

在定時任務業務邏輯執行前先嚐試獲取鎖,誰獲取到誰執行,拿不到鎖就直接放棄,或者進行其他的處理邏輯。

優點:解決單點問題

缺點:無法故障轉移

利用quartz叢集分散式(併發)部署解決方案

quartz自身提供了叢集分散式(併發)部署的一套解決方案,主要解決思路是通過資料庫鎖的方式實現。

實現原理 和 解決方案 和 Quartz-cluster最佳實踐
特性:

1.持久化任務:當應用程式停止執行時,所有排程資訊不被丟失,當你重新啟動時,排程資訊還存在,這就是持久化任務(儲存到資料庫表中)。

2.叢集和分散式處理:當在叢集環境下,當有配置Quartz的多個客戶端時(節點),採用Quartz的叢集和分散式處理時,我們要了解幾點好處

  1. 一個節點無法完成的任務,會被叢集中擁有相同的任務的節點取代執行。

2) Quartz排程是通過觸發器的類別來識別不同的任務,在不同的節點定義相同的觸發器的類別,這樣在叢集下能穩定的執行,一個節點無法完成的任務,會被叢集中擁有相同的任務的節點取代執行。

3)分散式體現在當相同的任務定時在一個時間點,在那個時間點,不會被兩個節點同時執行。

有兩點需要注意:

1)叢集配置檔案quartz.properties部署的時候必須要一致

2)叢集建立起來之後,如果執行過程中需要修改quartz排程器的策略,例如:原來每5天執行一次任務,現在要改成每半個月執行一次,這個時候要修改所有的配置檔案,並且要重新執行資料庫指令碼,或者手動修改資料庫中存的corn表示式(容易改錯),或找到那條記錄刪除(多表主外來鍵關聯)。

實現步驟:

1)修改quartz.properties配置檔案

配置檔案詳解 和 quartz配置詳解

org.quartz.jobStore.misfireThreshold = 120000
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass =org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.dataSource = myDS
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.acquireTriggersWithinLock=true
org.quartz.jobStore.clusterCheckinInterval = 15000
org.quartz.jobStore.maxMisfiresToHandleAtATime = 1

2)匯入表結構,

quartz-2.3.0.jar!\org\quartz\impl\jdbcjobstore\tablesmysqlinnodb.sql

DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
DROP TABLE IF EXISTS QRTZ_LOCKS;
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
DROP TABLE IF EXISTS QRTZ_CALENDARS;
......

圖片

優點:

1)框架自帶功能,實現方式對外透明

2)不需要改程式碼,對現有業務影響較小。

缺點:

1)後期如果修改定時任務的執行時間,資料庫不會重新整理,需手動改庫。

2)後期想停掉定時任務需要從資料庫中刪除或修改下次執行時間為無窮大(只在程式碼中注掉定時任務的註冊不起作用)

5.4 快取(可選)

第4.4章節中,使用jvm記憶體快取了一些基礎資料,當叢集部署後,這些資料會在每個jvm都快取一份,無法做到資料唯一性。

Redis
Redis 是完全開源的,遵守 BSD 協議,是一個高效能的 key-value 資料庫。

Redis 與其他 key - value 快取產品有以下三個特點:

•Redis支援資料的持久化,可以將記憶體中的資料儲存在磁碟中,重啟的時候可以再次載入進行使用。

•Redis不僅僅支援簡單的key-value型別的資料,同時還提供list,set,zset,hash等資料結構的儲存。

•Redis支援資料的備份,即master-slave模式的資料備份。

Redis 優勢

•效能極高 – Redis能讀的速度是110000次/s,寫的速度是81000次/s 。

•豐富的資料型別 – Redis支援二進位制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 資料型別操作。

•原子 – Redis的所有操作都是原子性的,意思就是要麼成功執行要麼失敗完全不執行。單個操作是原子性的。多個操作也支援事務,即原子性,通過MULTI和EXEC指令包起來。

•豐富的特性 – Redis還支援 publish/subscribe, 通知, key 過期等等特性。

Redis與其他key-value儲存有什麼不同?

•Redis有著更為複雜的資料結構並且提供對他們的原子性操作,這是一個不同於其他資料庫的進化路徑。Redis的資料型別都是基於基本資料結構的同時對程式設計師透明,無需進行額外的抽象。

•Redis執行在記憶體中但是可以持久化到磁碟,所以在對不同資料集進行高速讀寫時需要權衡記憶體,因為資料量不能大於硬體記憶體。在記憶體資料庫方面的另一個優點是,相比在磁碟上相同的複雜的資料結構,在記憶體中操作起來非常簡單,這樣Redis可以做很多內部複雜性很強的事情。同時,在磁碟格式方面他們是緊湊的以追加的方式產生的,因為他們並不需要進行隨機訪問。

Memcache
Memcache特點
Memcache是一套開放原始碼的分散式快取記憶體系統。Memcache通過在記憶體裡維護一個統一的巨大的hash表,它能夠用來儲存各種格式的資料,包括影像、視訊、檔案以及資料庫檢索的結果等。簡單的說就是將資料呼叫到記憶體中,然後從記憶體中讀取,從而大大提高讀取速度。
1、協議簡單:Memcached的伺服器客戶端通訊使用簡單的基於文字的協議。
2、基於libevent的事件處理:libevent是個程式庫,他將Linux 的epoll、BSD類作業系統的kqueue等時間處理功能封裝成統一的介面,能在Linux、BSD、Solaris等作業系統上發揮其高效能。
3、內建記憶體儲存方式:Memcached的資料都儲存在內建的記憶體儲存空間中,因此重啟Memcached,重啟作業系統會導致全部資料消失。另外,內容容量達到指定的值之後Memcached會自動刪除不適用的快取。
4、兩階段雜湊結構:Memcached就像一個巨大的、儲存了很多對的雜湊表,客戶端可以把資料儲存在多臺memcached上。查詢資料時,客戶端首先計算出階段一雜湊,選中一個節點;客戶端將請求傳送給選中的節點,然後memcached節點通過計算出階段二雜湊,查詢真正的資料(item)並返回給客戶端。從實現的角度看,Memcached是一個非阻塞的、基於事件的伺服器程式。
5、不互通訊的分散式:伺服器端並沒有分散式功能,不會互相通訊以共享資訊。分散式是通過客戶端實現。

Redis和Memcache區別,優缺點對比
圖片

1、 Redis和Memcache都是將資料存放在記憶體中,都是記憶體資料庫。不過memcache還可用於快取其他東西,例如圖片、視訊等等。
2、Redis不僅僅支援簡單的k/v型別的資料,同時還提供list,set,hash等資料結構的儲存。
3、分散式–設定memcache叢集,利用magent做一主多從;redis可以做一主多從。都可以一主一從
4、儲存資料安全–memcache掛掉後,資料沒了;redis可以定期儲存到磁碟(持久化)
5、災難恢復–memcache掛掉後,資料不可恢復; redis資料丟失後可以通過aof恢復
6、Redis支援資料的備份,即master-slave模式的資料備份。

redis和memecache的不同在於:
1、儲存方式:
memecache 把資料全部存在記憶體之中,斷電後會掛掉,資料不能超過記憶體大小
redis有部份存在硬碟上,這樣能保證資料的永續性,支援資料的持久化(有快照和AOF日誌兩種持久化方式,在實際應用的時候,要特別注意配置檔案快照引數,要不就很有可能伺服器頻繁滿載做dump)。
2、資料支援型別:
redis在資料支援上要比memecache多的多。
3、使用底層模型不同:
新版本的redis直接自己構建了VM 機制 ,因為一般的系統呼叫系統函式的話,會浪費一定的時間去移動和請求。
4、執行環境不同:
redis目前官方只支援LINUX 上執行,從而省去了對於其它系統的支援,這樣的話可以更好的把精力用於本系統環境上的優化,雖然後來微軟有一個小組為其寫了補丁,但是沒有放到主幹上。

總結一下,有持久化需求或者對資料結構和處理有高階要求的應用,選擇redis,其他簡單的key/value儲存,選擇memcache。

5.5 分散式鎖(可選)

用於控制定時任務由哪個節點執行,若4.5節中採用quartz自帶功能解決,則不需引入分散式鎖。

分散式鎖三種實現方式:

1、基於資料庫實現分散式鎖;

2、基於快取(Redis等)實現分散式鎖;

3、基於Zookeeper實現分散式鎖。

資料庫實現分散式鎖
基於資料庫實現的分散式鎖,是最容易理解的。但是,因為資料庫需要落到硬碟上,頻繁讀取資料庫會導致 IO 開銷大,因此這種分散式鎖適用於併發量低,對效能要求低的場景。

基於資料庫實現分散式鎖比較簡單,絕招在於建立一張鎖表,為申請者在鎖表裡建立一條記錄,記錄建立成功則獲得鎖,消除記錄則釋放鎖。

該方法依賴於資料庫,優點就是容易理解,實現簡單,但缺點也很明顯:

1)單點故障問題。一旦資料庫不可用,會導致整個系統崩潰。

2)死鎖問題。資料庫鎖沒有失效時間,未獲得鎖的程式只能一直等待已獲得鎖的程式主動釋放鎖。倘若已獲得共享資源訪問許可權的程式突然掛掉、或者解鎖操作失敗,使得鎖記錄一直存在資料庫中,無法被刪除,而其他程式也無法獲得鎖,從而產生死鎖現象。

Redis(快取)實現分散式鎖
基於快取實現分散式鎖的方式,也就是說把資料存放在計算機記憶體中,不需要寫入磁碟,減少了 IO 讀寫。

使用Redis實現分散式鎖,通常可以使用 setnx(key, value) 函式來實現分散式鎖, 當程式通過 setnx 函式返回 1 時,表示已經獲得鎖。排在後面的程式只能等待前面的程式主動釋放鎖,或者等到時間超時才能獲得鎖。

相對於基於資料庫實現分散式鎖的方案來說,基於快取實現的分散式鎖的優勢表現在以下幾個方面:

1)效能更好。資料被存放在記憶體,而不是磁碟,避免了頻繁的 IO 操作。

2)很多快取可以跨叢集部署,避免了單點故障問題。

3)使用方便。很多快取服務都提供了可以用來實現分散式鎖的方法,比如 Redis 的 setnx 和 delete 方法等。

4)可以直接設定超時時間(例如 expire key timeout)來控制鎖的釋放,因為這些快取服務一般支援自動刪除過期資料。

缺點:

1)鎖刪除失敗、過期時間不好控制

ZK分散式鎖實現
ZooKeeper 基於樹形資料儲存結構實現分散式鎖,來解決多個程式同時訪問同一臨界資源時,資料的一致性問題。ZooKeeper 基於臨時順序節點實現了分佈鎖 。

臨時節點(EPHEMERAL):當客戶端與 Zookeeper 連線時臨時建立的節點。與持久節點不同,當客戶端與 ZooKeeper 斷開連線後,該程式建立的臨時節點就會被刪除。

臨時順序節點(EPHEMERAL_SEQUENTIAL):就是按時間順序編號的臨時節點。

可以解決前兩種方法提到的各種問題,比如單點故障、不可重入、死鎖等問題。但該方法實現較複雜,且需要頻繁地新增和刪除節點,所以效能不如基於快取實現的分散式鎖。

缺點:效能不如Redis分散式鎖實現。

圖片

這裡的實現複雜性,是針對同樣的分散式鎖的實現複雜性,與之前提到的基於資料庫的實現非常簡易不一樣。

基於資料庫實現的分散式鎖存在單點故障和死鎖問題,僅僅利用資料庫技術去解決單點故障和死鎖問題,是非常複雜的。而 ZooKeeper 已定義相關的功能元件,因此可以很輕易地解決設計分散式鎖時遇到的各種問題。所以說,要實現一個完整的、無任何缺陷的分散式鎖,ZooKeeper 是一個最簡單的選擇。

總結來說,ZooKeeper 分散式鎖的可靠性最高,有封裝好的框架,很容易實現分散式鎖的功能,並且幾乎解決了資料庫鎖和快取式鎖的不足,因此是實現分散式鎖的首選方法。

總結

技術選擇

負載均衡:使用哪種負載均衡策略不需要我們關心,由運維支援,告知需負載均衡即可。(若公司需自己搞,推薦Nginx)

session共享:spring session+redis

定時任務防重複執行:quartz-cluster

快取:Redis(公司大面積使用)

分散式鎖:Redis(公司提供了Redis分散式鎖實現)

相關文章