其實大部分問題我們之前也遇到過,這些問題當時也困擾着我們,後來我們經過讨論和思考,發現其實很多時候我們困擾的主要原因是過于“追求完美的異地多活方案”,這樣導緻“異地多活”設計中出現很多了的思維誤區,而如果不意識到這些思維誤區,就會陷入死胡同,導緻無法實作真正的“異地多活”方案。
接下來我将總結常見的思維誤區,看看你踩中了哪個坑?
“異地多活”是為了保證業務的高可用,但很多朋友在考慮這個“業務”的時候,會不自覺的陷入一個思維誤區:我要保證所有業務的“異地多活”!
比如說假設我們需要做一個“使用者子系統”,這個子系統負責“注冊”、“登入”、“使用者資訊”三個業務。為了支援海量使用者,我們設計了一個“使用者分區”的架構,即:正常情況下使用者屬于某個主分區,每個分區都有其它資料的備份,使用者用郵箱或者手機号注冊,路由層拿到郵箱或者手機号後,通過hash計算屬于哪個中心,然後請求對應的業務中心。基本的架構如下:
考慮這樣一個系統,如果3個業務要同時實作異地多活,我們會發現如下一些難以解決的問題:
【注冊】
a中心注冊了使用者,資料還未同步到b中心,此時a中心當機,為了支援注冊業務多活,那我們可以挑選b中心讓使用者去重新注冊。看起來很容易就支援多活了,但仔細思考一下會發現這樣做會有問題:一個手機号隻能注冊一個賬号,a中心的資料沒有同步過來,b中心無法判斷這個手機号是否重複,如果b中心讓使用者注冊,後來a中心恢複了,發現資料有沖突,怎麼解決?實際上是無法解決的,因為新增賬號不能說挑選最後一個生效;而如果b中心不支援本來屬于a中心的業務進行注冊,注冊業務的雙活又成了空談。
有的朋友可能會說:那我修改業務規則,允許一個手機号注冊多個賬号不就可以了麼?
這樣做是不可行的,類似一個手機号隻能注冊一個賬号這種規則,是核心業務規則,修改核心業務規則的代價非常大,幾乎所有的業務都要重新設計,為了架構設計去改變業務規則,而且是這麼核心的業務規則是得不償失的。
【使用者資訊】
使用者資訊的修改和注冊有類似的問題,即:a、b兩個中心在異常的情況下都修改了使用者資訊,如何處理沖突?
由于使用者資訊并沒有賬号那麼關鍵,一種簡單的處理方式是按照時間合并,即:最後修改的生效。業務邏輯上沒問題,但實際操作也有一個很關鍵的坑:怎麼保證多個中心所有機器時間絕對一緻?在異地多中心的網絡下,這個是無法保證的,即使有時間同步也無法完全保證,隻要兩個中心的時間誤差超過1s,資料就可能出現混亂,即:先修改的反而生效。
還有一種方式是生成全局唯一遞增id,這個方案的成本很高,因為這個全局唯一遞增id的系統本身又要考慮異地多活,同樣涉及資料一緻性和沖突的問題。
綜合上面的簡單分析,我們可以發現,如果“注冊”“登入”、“使用者資訊”全部都要支援異地多活的話,實際上是挺難的,有的問題甚至是無解的。那這種情況下我們應該如何考慮“異地多活”的方案設計呢?答案其實很簡單:優先實作核心業務的異地多活方案!
對于我們的這個模拟案例來說,“登入”才是最核心的業務,“注冊”和“使用者資訊”雖然也是主要業務,但并不一定要實作異地多活。主要原因在于業務影響。對于一個日活1000萬的業務來說,每天注冊使用者可能是幾萬,修改使用者資訊的可能還不到1萬,但登入使用者是1000萬,很明顯我們應該保證登入的異地多活。對于新使用者來說,注冊不了影響并不很明顯,因為他還沒有真正開始業務;使用者資訊修改也類似,使用者暫時修改不了使用者資訊,對于其業務不會有很大影響,而如果有幾百萬使用者登入不了,就相當于幾百萬使用者無法使用業務,對業務的影響就非常大了:公司的客服熱線很快就被打爆了,微網誌微信上到處都在傳業務當機,論壇裡面到處是在罵娘的使用者,那就是網際網路大事件了!
而登入實作“異地多活”恰恰是最簡單的,因為每個中心都有所有使用者的賬号和密碼資訊,使用者在哪個中心都可以登入。使用者在a中心登入,a中心當機後,使用者到b中心重新登入即可。
有的朋友可能會問,如果某個使用者在a中心修改了密碼,此時資料還沒有同步到b中心,使用者到b中心登入是無法登入的,這個怎麼處理?這個問題其實就涉及另外一個思維誤區了,我們稍後再談。
異地多活本質上是通過異地的資料備援,來保證在極端異常的情況下業務也能夠正常提供給使用者,是以資料同步是異地多活設計方案的核心,但我們大部分人在考慮資料同步方案的時候,也會不知不覺的陷入完美主義誤區:我要所有資料都實時同步!
資料備援就要将資料從a地同步到b地,從業務的角度來看是越快越好,最好和本地機房一樣的速度最好,但讓人頭疼的問題正在這裡:異地多活理論上就不可能很快,因為這是實體定律決定的,即:光速真空傳播是每秒30萬公裡,在光纖中傳輸的速度大約是每秒20萬公裡,再加上傳輸中的各種網絡裝置的處理,實際還遠遠達不到光速的速度。
除了距離上的限制外,中間傳輸各種不可控的因素也非常多,例如挖掘機把光纖挖斷,中美海底電纜被拖船扯斷、骨幹網故障等,這些故障是第三方維護,我們根本無能為力也無法預知。例如廣州機房到北京機房,正常情況下rtt大約是50ms左右,遇到網絡波動之類的情況,rtt可能飙升到500ms甚至1s,更不用說經常發生的線路丢包問題,那延遲可能就是幾秒幾十秒了。
是以異地多活方案面臨一個無法徹底解決的沖突:業務上要求資料快速同步,實體上正好做不到資料快速同步,是以所有資料都實時同步,實際上是一個無法達到的目标。
既然是無法徹底解決的沖突,那就隻能想辦法盡量減少影響。有幾種方法可以參考:
盡量減少異地多活機房的距離,搭建高速網絡;
盡量減少資料同步;
保證最終一緻性,不保證明時一緻性;
【減少距離:同城多中心】
為了減少兩個業務中心的距離,選擇在同一個城市不同的區搭建機房,機房間通過高速網絡連通,例如在北京的海定區和通州區各搭建一個機房,兩個機房間采用高速光纖網絡連通,能夠達到近似在一個機房的性能。
這個方案的優勢在于對業務幾乎沒有影響,業務可以無縫的切換到同城多中心方案;缺點就是無法應對例如新奧爾良全城被水淹,或者2003美加大停電這種極端情況。是以即使采用這種方案,也還必須有一個其它城市的業務中心作為備份,最終的方案同樣還是要考慮遠距離的資料傳輸問題。
【減少資料同步】
另外一種方式就是減少需要同步的資料。簡單來說就是不重要的資料不要同步,同步後沒用的資料不同步。
以前面的“使用者子系統”為例,使用者登入所産生的token或者session資訊,資料量很大,但其實并不需要同步到其它業務中心,因為這些資料丢失後重新登入就可以了。
有的朋友會問:這些資料丢失後要求使用者重新登入,影響使用者體驗的呀!
确實如此,畢竟需要使用者重新輸入賬戶和密碼資訊,或者至少要彈出登入界面讓使用者點選一次,但相比為了同步所有資料帶來的代價,這個影響完全可以接受,其實這個問題也涉及了一個異地多活設計的典型思維誤區,後面我們會詳細講到。
【保證最終一緻性】
第三種方式就是業務不依賴資料同步的實時性,隻要資料最終能一緻即可。例如:a機房注冊了一個使用者,業務上不要求能夠在50ms内就同步到所有機房,正常情況下要求5分鐘同步到所有機房即可,異常情況下甚至可以允許1小時或者1天後能夠一緻。
最終一緻性在具體實作的時候,還需要根據不同的資料特征,進行差異化的處理,以滿足業務需要。例如對“賬号”資訊來說,如果在a機房新注冊的使用者5分鐘内正好跑到b機房了,此時b機房還沒有這個使用者的資訊,為了保證業務的正确,b機房就需要根據路由規則到a機房請求資料(這種處理方式其實就是後面講的“二次讀取”)。
而對“使用者資訊”來說,5分鐘後同步也沒有問題,也不需要采取其它措施來彌補,但還是會影響使用者體驗,即使用者看到了舊的使用者資訊,這個問題怎麼解決呢?這個問題實際上也涉及到了一個思維誤區,在最後我們統一分析。
資料同步是異地多活方案設計的核心,幸運的是基本上存儲系統本身都會有同步的功能,例如mysql的主備複制、redis的cluster功能、elasticsearch的叢集功能。這些系統本身的同步功能已經比較強大,能夠直接拿來就用,但這也無形中将我們引入了一個思維誤區:隻使用存儲系統的同步功能!
既然說存儲系統本身就有同步功能,而且同步功能還很強大,為何說隻使用存儲系統是一個思維誤區呢?因為雖然絕大部分場景下,存儲系統本身的同步功能基本上也夠用了,但在某些比較極端的情況下,存儲系統本身的同步功能可能難以滿足業務需求。
以mysql為例,mysql5.1版本的複制是單線程的複制,在網絡抖動或者大量資料同步的時候,經常發生延遲較長的問題,短則延遲十幾秒,長則可能達到十幾分鐘。而且即使我們通過監控的手段知道了mysql同步時延較長,也難以采取什麼措施,隻能幹等。
redis又是另外一個問題,redis 3.0之前沒有cluster功能,隻有主從複制功能,而為了設計上的簡單,redis主從複制有一個比較大的隐患:從機當機或者和主機斷開連接配接都需要重新連接配接主機,重新連接配接主機都會觸發全量的主從複制,這時候主機會生成記憶體快照,主機依然可以對外提供服務,但是作為讀的從機,就無法提供對外服務了,如果資料量大,恢複的時間會相當的長。
綜合上述的案例可以看出,存儲系統本身自帶的同步功能,在某些場景下是無法滿足我們業務需要的。尤其是異地多機房這種部署,各種各樣的異常都可能出現,當我們隻考慮存儲系統本身的同步功能時,就會發現無法做到真正的異地多活。
解決的方案就是拓開思路,避免隻使用存儲系統的同步功能,可以将多種手段配合存儲系統的同步來使用,甚至可以不采用存儲系統的同步方案,改用自己的同步方案。
例如,還是以前面的“使用者子系統”為例,我們可以采用如下幾種方式同步資料:
消息隊列方式:對于賬号資料,由于賬号隻會建立,不會修改和删除(假設我們不提供删除功能),我們可以将賬号資料通過消息隊列同步到其它業務中心。
二次讀取方式:某些情況下可能出現消息隊列同步也延遲了,使用者在a中心注冊,然後通路b中心的業務,此時b中心本地拿不到使用者的賬号資料。為了解決這個問題,b中心在讀取本地資料失敗的時候,可以根據路由規則,再去a中心通路一次(這就是所謂的二次讀取,第一次讀取本地,本地失敗後第二次讀取對端),這樣就能夠解決異常情況下同步延遲的問題。
存儲系統同步方式:對于密碼資料,由于使用者改密碼頻率較低,而且使用者不可能在1s内連續改多次密碼,是以通過資料庫的同步機制将資料複制到其它業務中心即可,使用者資訊資料和密碼類似。
回源讀取方式:對于登入的session資料,由于資料量很大,我們可以不同步資料;但當使用者在a中心登入後,然後又在b中心登入,b中心拿到使用者上傳的session id後,根據路由判斷session屬于a中心,直接去a中心請求session資料即可,反之亦然,a中心也可以到b中心去拿取session資料。
重新生成資料方式:對于第4中場景,如果異常情況下,a中心當機了,b中心請求session資料失敗,此時就隻能登入失敗,讓使用者重新在b中心登入,生成新的session資料。
(注意:以上方案僅僅是示意,實際的設計方案要比這個複雜一些,還有很多細節要考慮)
綜合上述的各種措施,最後我們的“使用者子系統”同步方式整體如下:
前面我們在給出每個思維誤區對應的解決方案的時候,其實都遺留了一些小尾巴:某些場景下我們無法保證100%的業務可用性,總是會有一定的損失。例如密碼不同步導緻無法登入、使用者資訊不同步導緻使用者看到舊的使用者資訊等等,這個問題怎麼解決?
其實這個問題涉及異地多活設計方案中一個典型的思維誤區:我要保證業務100%可用!但極端情況下就是會丢一部分資料,就是會有一部分資料不能同步,怎麼辦呢,有沒有什麼巧妙和神通的辦法能做到?
很遺憾,答案是沒有!異地多活也無法保證100%的業務可用,這是由實體規律決定的,光速和網絡的傳播速度、硬碟的讀寫速度、極端異常情況的不可控等,都是無法100%解決的。是以針對這個思維誤區,我的答案是“忍”!也就是說我們要忍受這一小部分使用者或者業務上的損失,否則本來想為了保證最後的0.01%的使用者的可用性,做個完美方案,結果卻發現99.99%的使用者都保證不了了。
對于某些實時強一緻性的業務,實際上受影響的使用者會更多,甚至可能達到1/3的使用者。以銀行轉賬這個業務為例,假設小明在北京xx銀行開了賬号,如果小明要轉賬,一定要北京的銀行業務中心是可用的,否則就不允許小明自己轉賬。如果不這樣的話,假設在北京和上海兩個業務中心實作了實時轉賬的異地多活,某些異常情況下就可能出現小明隻有1萬存款,他在北京轉給了張三1萬,然後又到上海轉給了李四1萬,兩次轉賬都成功了。這種漏洞如果被人利用,後果不堪設想。
當然,針對銀行轉賬這個業務,可以有很多特殊的業務手段來實作異地多活。例如分為“實時轉賬”和“轉賬申請”。實時轉賬就是我們上述的案例,是無法做到“異地多活”的;但“轉賬申請”是可以做到“異地多活”的,即:小明在上海業務中心送出轉賬請求,但上海的業務中心并不立即轉賬,而是記錄這個轉賬請求,然後背景異步發起真正的轉賬操作,如果此時北京業務中心不可用,轉賬請求就可以繼續等待重試;假設等待2個小時後北京業務中心恢複了,此時上海業務中心去請求轉賬,發現餘額不夠,這個轉賬請求就失敗了。小明再登入上來就會看到轉賬申請失敗,原因是“餘額不足”。不過需要注意的是“轉賬申請”的這種方式雖然有助于實作異地多活,但其實還是犧牲了使用者體驗的,對于小明來說,本來一次操作的事情,需要分為兩次:一次送出轉賬申請,另外一次要确認是否轉賬成功。
雖然我們無法做到100%可用性,但并不意味着我們什麼都不能做,為了讓使用者心裡更好受一些,我們可以采取一些措施進行安撫或者補償,例如:
挂公告:說明現在有問題和基本的問題原因,如果不明确原因或者不友善說出原因,可以說“技術哥哥正在緊急處理”比較輕松和有趣的公告。
事後對使用者進行補償:例如送一些業務上可用的代金券、小禮包等,降低使用者的抱怨。
補充體驗:對于為了做異地多活而帶來的體驗損失,可以想一些方法減少或者規避。以“轉賬申請”為例,為了讓使用者不用确認轉賬申請是否成功,我們可以在轉賬成功或者失敗後直接給使用者發個短信,告訴他轉賬結果,這樣使用者就不用不時的登入系統來确認轉賬是否成功了。
綜合前面的分析,異地多活設計的理念可以總結為一句話:采用多種手段,保證絕大部分使用者的核心業務異地多活!