大家好,我是58沈劍,今天我分享的主題是《58怎么玩數據庫架構》,我的PPT頁數非常少,討論的問題非常的聚焦。
一、數據庫的基本概念
基本概念就一頁PPT,讓大家就一些數據庫方面的概念達成一致。
首先是“單庫”,最開始的時候數據庫都是這么玩的,幾乎所有公司都會經歷這個階段。
接下來是“分片”,也就是水平切分,它是用來解決數據量大的問題。有一些數據庫支持auto sharding,自動分片,例如mongoDB,58同城也用過兩年mongoDB,后來發現auto sharding不太可控,不知道什么時候會進行數據遷移,數據遷移過程中會有大粒度的鎖,讀寫被阻塞,業務會有抖動和毛刺,這些是業務不能接受的,于是后來又被我們棄用。
一旦進行分片,就會面臨“數據路由”的問題,來了一個請求,要將請求路由到對應的數據庫分片上?;ヂ摼W常用的數據路由方法有三種:
(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同城沒有使用上述兩種方式來保證讀寫的可用性,我們使用的是“雙主”當“主從”的方式來保證數據庫的讀寫可用性。
雖然看上去是雙主同步,但是讀寫都在一個主上,另一個主庫沒有讀寫流量,完全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庫的秒級擴容方案,還分享了一個平滑雙寫導庫的方案,解決兩庫擴三庫,數據庫字段的增加,以及底層介質的變化的問題。
我今天分享的內容就這么多,謝謝大家,希望大家有收獲。
===【完】===
回【趕集】趕集mysql軍規
回【join】30秒懂SQL中的join
歡迎大伙留言提問,有問必答。
如果有收獲幫忙隨手轉一下哈。