如題,本文針對工作中實際經驗,整理了把一個單體架構的系統更新成叢集架構需要做的準備工作,以及為叢集架構的更新做指導方針。
本文首先分析了單體架構存在的問題,然後介紹了叢集架構(好處、注意的問題、架構圖),接着分析了目前系統的主要功能以及叢集後需要做哪些調整,然後對叢集架構涉及的技術做橫向對比,最後确定技術選型。從這幾個方面介紹了從單體架構到叢集架構的改造過程,希望對你有幫助。
背景
單機存在單點故障的隐患
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