58同城數據庫架構最佳實踐

http://geek.csdn.net/news/detail/52070

數據庫的基本概念

基本概念這一塊,主要是讓大家就一些數據庫方面的概念達成一致。

圖片描述

首先是“單庫”,最初的時候數據庫都是這麼玩的,幾乎所有的業務都有這樣的一個庫。

圖片描述

接下來是“分片”,數據庫的分片是解決數據量大的問題。如果數據量非常大,就要做水平切分,有一些數據庫支持auto sharding。之前58同城也用過兩年mongoDB,後來發現auto sharding功能不太可控,不知道什麼時間進行遷移數據,數據遷移過程中會有大粒度的鎖,讀寫被阻塞,業務會有抖動和毛刺,這些是業務不能接受的,因此現在又遷移回了MySQL。

一旦進行分片,就會面臨“數據路由”的問題:來了一個請求,要將請求路由到對應的數據庫分片上。互聯網常用的數據路由方法有三種:

(1)第一個是按照數據範圍路由,比如有兩個分片,一個範圍是0-1億,一個範圍是1億-2億,這樣來路由。 
這個方式的優點是非常的簡單,並且擴展性好,假如兩個分片不夠了,增加一個2億-3億的分片即可。 
這個方式的缺點是:雖然數據的分佈是均衡的,每一個庫的數據量差不多,但請求的負載會不均衡。例如有一些業務場景,新註冊的用戶活躍度更高,大範圍的分片請求負載會更高。

(2)第二個是按照hash路由,比如有兩個分片,數據模2尋庫即可。 
這個方式的優點是路由方式很簡單,數據分佈也是均衡的,請求負載也是均衡的。 
這個方式的缺點是如果兩個分片數據量過大,要變成三個分片,數據遷移會比較麻煩,即擴展性會受限。

(3)第三個是路由服務。前面兩個數據路由方法均有一個缺點,業務線需要耦合路由規則,如果路由規則發生變化,業務線是需要配合升級的。路由服務可以實現業務線與路由規則的解耦,業務線每次訪問數據庫之前先調用路由服務,來知道數據究竟存放在哪個分庫上。

圖片描述

接下來是“分組”“複製”,這解決的是擴展讀性能,讀高可用的問題。

根據經驗,大部分互聯網的業務都是讀多寫少。淘寶、京東查詢商品,搜索商品的請求可能佔了99%,只有下單和支付的時候有寫請求。

58同城搜索帖子,察看列表頁,查看詳情頁都是讀請求,發佈帖子是寫請求,寫請求的量也是比較少的。

可見,大部分互聯網的場景都讀多寫少,所以讀性能會最先成爲瓶頸,怎麼快速解決這個問題呢?

通常來說,會使用讀寫分離,擴充讀庫的方式來提升讀性能。同時也保證了讀可用性,一臺讀庫掛了,另外一臺讀庫可以持續的提供服務。

圖片描述

常見數據庫的玩法綜合了“分片”和“分組”,數據量大進行分片,爲了提高讀性能,保證讀的高可用,進行了分組,80%互聯網公司數據庫都是這種軟件架構

可用性架構實踐

數據庫大家都用,平時除了根據業務設計表結構,根據訪問來設計索引之外,還應該在設計時考慮數據的可用性,可用性又分爲讀的高可用寫的高可用

圖片描述

上圖是“讀”高可用的常見玩法。怎麼樣保證數據庫讀庫的高可用呢?解決高可用這個問題的思路是冗餘。

解決站點的可用性問題冗餘多個站點,解決服務的可用性問題冗餘多個服務,解決數據的可用性問題冗餘多份數據。

如果用一個讀庫,保證不了讀高可用,就複製讀庫,一個讀庫掛了另一個仍然可以提供服務,這麼用複製的方式來保證讀的可用性。

數據的冗餘會引發一個副作用,就是一致性的問題。

如果是單庫,讀和寫都落在同一個庫上,每次讀到的都是最新的數據庫,不存在一致性的問題。

但是爲了保證可用性將數據複製到多個地方,而這多個地方的數據絕對不是實時同步的,會有同步時延,所以有可能會讀到舊的數據。如何解決主從數據庫一致性問題後面再來闡述。

很多互聯網公司的數據庫軟件架構都是一主兩從或者一主三從,不能夠保證“寫”的高可用,因爲寫其實還是隻有一個庫,仍是單點,如果這個庫掛了的話,寫會受影響。那小夥伴們爲什麼還使用這個架構呢?

剛纔提到大部分互聯網公司99%的業務都是“讀”業務,寫庫不是主要矛盾,寫庫掛了,可能只有1%的用戶會受影響。

如果要做到“寫”的高可用,對數據庫軟件架構的衝擊比較大,不一定值得,爲了解決1%的問題引入了80%的複雜度,所以很多互聯網公司都沒有解決寫數據庫的高可用的問題。

圖片描述

怎麼來解決這個問題呢?思路還是冗餘,讀的高可用是冗餘讀庫,寫的高可用是冗餘寫庫。把一個寫變成兩個寫,做一個雙主同步,一個掛了的話,我可以將寫的流量自動切到另外一個,寫庫的高可用性。

用雙主同步的方式保證寫高可用性會存在什麼樣的問題?

上文提到,用冗餘的方式解保證可用性會存在一致性問題。因爲兩個主相互同步,這個同步是有時延的,很多公司用到auto-increment-id這樣的一些數據庫的特性,如果用雙組同步的架構,一個主id由10變成11,在沒有同步過去之前,另一個主又來了一個寫請求,也由10變成11,雙向同步會同步失敗,就會有數據丟失。

解決這個雙主同步id衝突的方案有兩種:

(1)一個是雙主使用不同的初始值,相同的步長來生成id,一個庫從0開始(生成02468),一個庫從1開始(生成13579),步長都爲2,這樣兩邊同步數據就不會衝突。 
(2)另一個方式是不要使用數據庫的auto-increment-id,而由業務層來保證生成的id不衝突。

圖片描述

58同城沒有使用上述兩種方式來保證讀寫的可用性。58同城使用的“雙主”當“主從”的方式來保證數據庫的讀寫可用性。

雖然看上去是雙主同步,但是讀寫都在一個主上,另一個主庫沒有讀寫流量,完全standby。當一個主庫掛掉的時候,流量會自動的切換到另外一個主庫上,這一切對業務線都是透明的,自動完成。

58同城的這種方案,讀寫都在一個主庫上,就不存同步延時而引發的一致性問題了,但缺點有兩個:

第一是數據庫資源的利用率只有50%; 
第二是沒有辦法通過增加讀庫的方式來擴展系統的讀性能。 
58同城的數據庫軟件架構如何來擴展讀性能呢,來看下面一章。

讀性能架構實踐

如何增加數據庫的讀性能,先看下傳統的玩法:

圖片描述

(1)第一種玩法是增加從庫,通過增加從庫來提升讀性能,存在的問題是什麼呢?從庫越多,寫的性能越慢,同步的時間越長,不一致的可能性越高。

圖片描述

(2)第二種常見的玩法是增加緩存,緩存是大家用的非常多的一種提高系統讀性能的一種方法,特別是對於讀多寫少的互聯網的場景非常的有效。常用的緩存玩法如上圖,上游是業務線的,底下是讀寫分離主從同步,然後會加一個cache。

對於寫操作:會先淘汰cache,再寫數據庫。

對於讀操作:先讀cache,如果cache hit則返回數據,如果cachemiss則讀從庫,然後把讀出來的數據再入緩存。

這是常見的cache玩法。

傳統的cache玩法在一種異常時序下,會引發嚴重的一致性問題,考慮這樣一個特殊的時序:

(1)先來了一個寫請求,淘汰了cache,寫了數據庫; 
(2)又來了一個讀請求,讀cache,cache miss了,然後讀從庫,此時寫請求還沒有同步到從庫上,於是讀了一個髒數據,接着髒數據入緩存; 
(3)最後主從同步完成;

這個時序會導致髒數據一直在緩存中沒有辦法被淘汰掉,數據庫和緩存中的數據嚴重不一致。

58同城也是採用緩存的方式來提升讀性能的,那就會不會有數據一致性問題呢,請往下看。

一致性架構實踐

圖片描述

58同城採用“服務+緩存+數據庫”一套的方式來保證數據的一致性,由於58同城使用“雙主當主從用”的數據庫讀寫高可用架構,讀寫都在一個主庫上,不會讀到所謂“讀庫的髒數據”,所以數據庫與緩存的不一致情況也不會存在。

傳統玩法中,主從不一致的問題有一些什麼樣的解決方案呢?一起來看一下。

圖片描述

主從爲什麼會不一致?上文提到讀寫會有時延,有可能讀到從庫上的舊數據。常見的方法是引入中間件,業務層不直接訪問數據庫,而是通過中間件訪問數據庫,這個中間件會記錄哪一些key上發生了寫請求,在數據主從同步時間窗口之內,如果key上又出了讀請求,就將這個請求也路由到主庫上去(因爲此時從庫可能還沒有同步完成,是舊數據),使用這個方法來保證數據的一致性。

中間件的方案很理想,那爲什麼大部分的互聯網的公司都沒有使用這種方案來保證主從數據的一致性呢?那是因爲數據庫中間件的技術門檻比較高,有一些大公司,例如百度,騰訊,阿里他們可能有自己的中間件,並不是所有的創業公司互聯網公司有自己的中間件產品,況且很多互聯網公司的業務對數據一致性的要求並沒有那麼高。比如說同城搜一個帖子,可能5秒鐘之後才搜出來,對用戶的體驗並沒有多大的影響。

圖片描述

除了中間件,讀寫都路由到主庫,58同城就是這麼做的,也是一種解決主從不一致的常用方案。

解決完主從不一致,第二個要解決的是數據庫和緩存的不一致,剛纔提到cache傳統的玩法,髒數據有可能入cache,怎麼解決呢?

兩個實踐:第一個是緩存雙淘汰機制,第二個是建議爲所有item設定過期時間(前提是允許cache miss)。

(1)緩存雙淘汰,傳統的玩法在進行寫操作的時候,先淘汰cache再寫主庫。上文提到,在主從同步時間窗口之內可能有髒數據入cache,此時如果再發起一個異步的淘汰,即使不一致時間窗內臟數據入了cache,也會再次淘汰掉。 
(2)爲所有item設定超時時間,例如10分鐘。極限時序下,即使有髒數據入cache,這個髒數據也最多存在十分鐘。帶來的副作用是,可能每十分鐘,這個key上有一個讀請求會穿透到數據庫上,但我們認爲這對數據庫的從庫壓力增加是非常小的。

擴展性架構實踐

擴展性也是架構師在做數據庫架構設計的時候需要考慮的一點。首先分享一個58同城非常帥氣的秒級數據擴容的方案。這個方案解決什麼問題呢?原來數據庫水平切分成N個庫,現在要擴容成2N個庫,解決的就是這個問題。

圖片描述

假設原來分成兩個庫,假設按照hash的方式分片。如上圖,分爲奇數庫和偶數庫。

圖片描述

第一個步驟提升從庫,底下一個從庫放到上面來(其實什麼動作都沒有做); 
第二個步驟修改配置,此時擴容完成,原來是2個分片,修改配置後變成4個分片,這個過程沒有數據的遷移。原來偶數的那一部分現在變成了兩個部分,一部分是0,一部分是2,奇數的部分現在變成1和3。0庫和2庫沒有數據衝突,只是擴容之後在短時間內雙主的可用性這個特性丟失掉了。

圖片描述

第三個步驟還要做一些收尾操作:把舊的雙主給解除掉,爲了保證可用性增加新的雙主同步,原來擁有全部的數據,現在只爲一半的數據提供服務了,我們把多餘的數據刪除掉,結尾這三個步驟可以事後慢慢操作。整個擴容在過程在第二步提升從庫,修改配置其實就秒級完成了,非常的帥氣。

這個方案的缺點是只能實現N庫到2N 庫的擴容,2變4、4變8,不能實現2庫變3庫,2庫變5庫的擴容。那麼,如何能夠實現這種擴容呢?

數據庫擴展性方面有很多的需求,例如剛纔說的2庫擴3庫,2庫擴5庫。產品經理經常變化需求,擴充表的屬性也是經常的事情。今年的數據庫大會同行也介紹了一些使用觸發器來做online schema change的方案,但是觸發器的侷限性在於:

第一、觸發器對數據庫性能的影響比較大; 
第二、觸發器只能在同一個庫上纔有效,而互聯網的場景特點是數據量非常大,併發量非常大,庫都分佈在不同的物理機器上,觸發器沒法弄。

最後還有一類擴展性需求,底層存儲介質發生變化,原來是MongoDB存儲,現在要變爲MySQL存儲,這也是擴展性需求(雖然很少),這三類需求怎麼擴展?

方法是導庫,遷移數據,遷移數據有幾種做法,第一種停服務,如果大家的業務能夠接受這種方法,強烈建議使用這種方法,例如有一些遊戲公司,晚上一點到兩點服務器維護,可能就是在幹分區或者合區這類導庫的事情。

圖片描述

如果業務上不允許停服務,想做到平滑遷移,雙寫法可以解決這類問題。

(1)雙寫法遷移數據的第一步是升級服務,原來的服務是寫一個庫,現在建立新的數據庫,雙寫。比如底層存儲介質的變化,我們原來是mongo數據庫,現在建立好新的mysql數據庫,然後對服務的所有寫接口進行雙庫寫升級。

(2)第二步寫一個小程序去進行數據的遷移。比如寫一個離線的程序,把兩個庫的數據重新分片,分到三個庫裏。也可能是把一個只有三個屬性的用戶表導到五個屬性的數據表裏面。這個數據遷移要限速,導完之後兩個庫的數據一致嗎?只要提前雙寫,如果沒有什麼意外,兩邊的數據應該是一致的。

什麼時候會有意外呢?在導某一條數據的過程當中正好發生了一個刪除操作,這個數據剛被服務雙寫刪除,又被遷移數據的程序插入到了新庫中,這種非常極限的情況下會造成兩邊的數據不一致。

(3)建議第三步再開發一個小腳本,對兩邊的數據進行比對,如果發現了不一致,就將數據修復。當修復完成之後,我們認爲數據是一致的,再將雙寫又變成單寫,數據完成遷移。

這個方式的優點:

第一、改動是非常小的,對服務的影響比較小,單寫變雙寫,開發兩個小工具,一個是遷移程序,從一個庫讀數據,另外一個庫插進去;還有一個數據校驗程序,兩個數據進行比對,改動是比較小的。

第二、隨時可回滾的,方案風險比較小,在任何一個步驟如果發現問題,可以隨時停止操作。比如遷移數據的過程當中發現不對,就把新的數據庫幹掉,重新再遷。因爲在切換之前,所有線上的讀服務和寫服務都是舊庫提供,只有切了以後,纔是新庫提供的服務。這是我們非常帥氣的一個平滑導庫的方式。

總結

本次分享首先介紹了單庫、分片、複製、分組、路由規則的概念。分片解決的是數據量大的問題,複製和分組解決的是提高讀性能,保證讀的可用性的問題。分片會引入路由,常用的三種路由的方法,按照範圍、按照hash,或者新增服務來路由。

如何保證數據的可用性?思路是冗餘,但會引發數據的不一致,58同城保證可用性的實踐是雙主當主從用,讀寫流量都在一個庫上,另一個庫standby,一個主庫掛掉流量自動遷移到另外一個主庫,只是資源利用率是50%,並且不能通過增加從庫的方式提高讀性。

讀性能的實踐,傳統的玩法是增加從庫或者增加緩存。存在的問題是,主從可能不一致,同城的玩法是服務加數據庫加緩存一套的方式來解決這些問題。

一致性的實踐,解決主從不一致性有兩種方法,一種是增加中間件,中間件記錄哪些key上發生了寫操作,在主從同步時間窗口之內的讀操作也路由到主庫。第二種方法是強制讀主。數據庫與緩存的一致性,我們的實踐是雙淘汰,在發生寫請求的時候,淘汰緩存,寫入數據庫,再做一個延時的緩存淘汰操作。第二個實踐是建議爲所有的item設置一個超時時間。

擴展性方面,今天分享了58同城一個非常帥氣的N庫擴2N庫的秒級擴容方案,還分享了一個平滑雙寫導庫的方案,解決兩庫擴三庫,數據庫字段的增加,以及底層介質的變化的問題。

分享人:沈劍,58到家技術總監、58同城高級架構師,58同城技術委員會負責人。曾任百度高級工程師、曾參與過多個百度hi重大項目的研發,加盟58同城以後,負責過58同城即時通訊,支付系統與攤銷系統的重構。還曾參與數據庫中間件、58同城推薦系統、58同城商戶平臺App以及58同城二手交易平臺APP等多個系統與項目的設計與實現。

架構技術實踐系列文章(部分):

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章