老板:把系統從單體架構升級到集群架構!


首發於公眾號: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分布式鎖實現)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM