快狗叫車CTO沈劍:資料庫架構一致性最佳實踐

伺服器頻道發表於2018-11-13

   本文根據沈劍在2018年10月18日【第十屆中國系統架構師大會(SACC2018)】現場演講內容整理而成。

   講師介紹:

  沈劍,快狗叫車CTO,網際網路架構技術專家,“架構師之路”公眾號作者。曾任百度高階工程師,58同城高階架構師,58同城技術委員會主席。2015年調至58到家任高階總監,技術委員會主席,負責基礎架構,技術平臺,運維安全,資訊系統等後端技術體系搭建。現任快狗叫車CTO,負責快狗叫車技術體系的搭建,本質是技術人一枚。

   本文摘要:

  沈劍分享了快狗叫車資料庫架構的一致性實踐,在一致性實踐的過程中,能夠體現快狗叫車資料庫架構的演進歷程。從單庫到多庫再到高可用等等,包括在研究的過程中,每個階段可能會碰到不同的問題,快狗叫車是採用一些什麼樣的技術手段去解決這些問題?以快狗叫車的實踐跟大家做一些分享。

   分享大綱:

  主從不一致,最佳化實踐

  快取不一致,最佳化實踐

  資料冗餘不一致,最佳化實踐

  多庫事務不一致,最佳化實踐

  總結

   演講正文:

  快狗叫車(原58速運)是一個創業型公司,技術架構、技術體系、資料庫架構的變遷,和在座很多公司是很相近的,今天和大家聊一聊,我們在快狗叫車資料庫架構一致性方面碰到一些問題。

   不一致的最佳化歷程,也是資料庫架構演進的過程

  主線是我們的資料庫架構變化的過程,在這個過程中,我列出了四個跟一致性相關的節點,主從會不一致、快取會不一致、冗餘資料會不一致、多庫多例項會不一致。不一致的最佳化歷程,也是我們資料庫架構演進的一個過程。從單庫到現在,有哪些坑在等著我們呢?

  先看一下,最初的資料庫架構,最早是這個樣子的。那個時候沒有什麼微服務分層, web透過DAO訪問一個單庫資料庫,最早我這麼玩的。單庫,它不具備什麼高可用,高併發特性,擴充套件性也比較差。我相信很多創業公司初期也是這樣。

  單庫最早會遇到什麼樣的瓶頸呢?在創業的時候,資料量變大了,併發量大了,業務變複雜了,整個系統的瓶頸最先出現在哪裡?我的經驗是資料庫。資料庫的瓶頸又會在哪裡?我的經驗是讀。因為絕大部分的業務是讀多寫少的業務,讀,最容易稱為系統的瓶頸。

  最早在資料庫讀扛不住的時候,最先想到的最佳化方式是什麼?網際網路公司都講快,今天出問題,能不能明天后天給我搞定?最先想到的方案是什麼,如何能快速擴充資料庫的讀效能呢?

  加兩個例項,主從同步,讀寫分離,這是創業型公司,當資料庫讀成為瓶頸的時候,最先想到的方案,快速擴充讀效能。主從同步碰到的問題是什麼?這就是本主題要講的第一個問題,主從一致性的問題。

  當資料量越來越多,吞吐量越來越大的時候,寫到了主庫,主庫同步到從庫,主從同步存在延時,在延時視窗期內,讀寫分離去讀從庫,就有可能讀到一箇舊資料。這個問題,我相信大家也會碰到。

  對於這個問題,不少接業務的解法方案是,忍,有些業務如果對一致性的要求沒這麼高。但有沒有最佳化方案呢?

  這兩個圖是我們的兩個常見的實踐。

  第一個是中介軟體,我們的服務層或者站點層不直接調資料庫,透過一箇中間層,去調資料庫。中間層它能夠知道哪一個庫,哪一個表,哪一個KEY發生了寫操作,如果說接下來的這一段時間(假設主從同步一秒鐘完成),有讀請求落到從庫上,就會讀到舊資料。那麼此時,中介軟體就要將讀請求,路由到主庫上去,讀新資料。

  第二個是強制讀主。第二個圖,雙主同步,強制讀主有什麼好處?第一解決了高可用問題,雙主使用同一個VIP,一個主庫如果掛了,另一個主庫能隨時頂上,保障高可用。第二避免了主從之間的不一致。

  強制讀主它帶來的新的問題是什麼呢?解決了一致性問題,但讀效能擴充套件的問題又來了,主庫抗讀寫,還是沒有解決讀性的擴大的問題。

  除了增加從庫,網際網路公司還有一種常見的提升系統讀效能的方式,快取加服務化。抽象出服務層,向呼叫方遮蔽底層資料庫的複雜性,遮蔽資料庫的高可用的複雜性,遮蔽快取的複雜性,對業務層提供服務。

  服務化加快取確實是提升系統讀容量的架構方案。透過快取來提升讀性,又會遇到什麼新的問題呢?用主從架構,有主從不一致問題;用快取架構,當然也有快取不一致的問題。只要你把同一份資料放在了多個地方,多個地方的修改有時間差,這個時間差就會有資料訪問不一致的問題。

  當我們出現資料庫與快取中的資料不一致的時候,我們怎麼來解決?

  首先來看一下為什麼會不一致。快取的常用玩法是“Cache Aside Pattern”。Cache Aside Pattern,旁路快取,一般是怎麼玩的?淘汰快取,而不是更新快取,這是Cache Aside Pattern的結論。

  讀寫時序是什麼樣的?對於讀請求有快取,毫無爭議的,先讀快取,如果資料命中我就直接返回,如果資料沒有命中,讀從庫讀寫分離,把這個資料從從庫裡拿出,放到快取裡,這是讀請求的一個流程。

  對於寫請求,Cache Aside Pattern的做法是,先寫資料庫,再淘汰快取。在什麼情況下會出現不一致?當併發量相對會比較高時,對於同一個KEY做了一個寫操作,馬上又來了一個讀操作,會出現什麼樣的情況?先發生一個寫操作,先更新到資料庫,淘汰了Cache,馬上又來了一個讀操作,這個時候主從同步還沒同步完成,先讀快取,快取被剛剛的寫操作已經淘汰掉了,又去讀從庫,把從庫的髒資料拿過來放到快取裡去,不一致就出現。

  高併發狀態下,寫後立即讀的場景,容易出現髒資料入Cache。

  大家發現沒有,這裡的資料不一致,比主從的資料不一致的情況更嚴重。主從不一致,只有一個主動同步時間差不一致,同步之後,從庫就能讀到新資料了。但是快取與資料庫的不一致,它會導致後續一直不一致,一旦髒資料入了快取,髒資料會延續到下一個寫發生的時候才會被淘汰掉,所以它其實更嚴重。

  如何來解決呢?快取和資料庫的資料不一致,我們的兩個實踐:非同步淘汰快取,確保從庫已經同步成功;設定超時時間,極限情況下有機會修正。

  第一個,等從庫已經完全同步成功,再去非同步淘汰快取?只要監聽從庫的binlog,從庫binlog完成,一定是寫操作執行完畢,此時再淘汰快取,就能避免時間差。

  第二個,就是如果允許Cache miss,不要將快取過期時間設為永久,如果你設定為無限長的過期時間,就沒有一個機會去修正不一致了。

  隨著業務的發展,除了流量的增加,我們要提升系統的讀效能,我們要提升系統的資料庫高可用,還會面臨一個什麼問題?對了,資料量會增大。我們業務資料量越來越大了,通常採用什麼樣的方式去解決?創業型公司,這兩個方案應該是大家用得最多的。

  第一個,分庫。降低每個庫,降低每個例項的資料量,這樣就能夠承載更多的資料。分庫又帶來什麼新的問題?舉了個例子,訂單一個庫,它有多個維度的查詢,有訂單ID的查詢,有使用者ID的查詢,有司機ID的查詢,一個庫沒有任何問題。

  但分庫以後,變成多個庫以後,一旦用了一個維度分庫,你會發現其他的維度的查詢就要變成多個庫了,是不是?

  一般來說是透過使用者的ID去分庫,在訂單ID裡去放上分庫因子,這樣透過使用者ID以及訂單ID都能夠定位到相關資料。但是對於司機ID就不同了,司機ID和使用者ID是一個多對多的關係。一個使用者他可能下了多個司機的單,一個司機接了多個使用者的單,透過司機ID去查詢,並不能一次性查詢到所有的資料,同一個司機的訂單一定是分佈在多個庫裡。怎麼辦呢?此時最常用解決方案是,資料冗餘。

  我用一個儲存後設資料,用一個儲存關係資料,後設資料透過使用者ID來分庫,保證同一個使用者的所有訂單在一個庫裡。關係資料用司機ID來分庫,保證同一個司機的所有訂單在一個庫裡。同一份資料,由於它存在兩個維度的查詢,這兩個維度查詢都可以不誇庫,而透過資料冗餘來實現,這個在業內屬於很常見的方案。

  資料冗餘,又會出現什麼問題?一起來看一下。上面是應用,中間是服務,一個資料存在兩個庫裡,一個庫是透過使用者ID分庫,一個庫是透過司機ID去分庫,呼叫方來了一個請求,先要往第一份資料裡寫一個資料,再往另外一個庫裡寫一個冗餘資料。能保證冗餘資料的一致性麼?是不能夠保證,這兩個庫同時寫成功的,那怎麼辦呢?

  這就是冗餘資料的一致性問題。資料冗餘資料的不一致最佳化,今天介紹三種方法,其實本質的方法論都是最終一致性。

  第一個方案是掃全量。怎麼發現冗餘資料不一致?寫個指令碼,每天晚上跑,理論上A庫裡有的B庫裡面也有,一旦掃庫發現怎麼A庫有B庫裡沒有,就是出現不一致了,就要根據業務特性來做補償。到底是將後一半補進去,還是把前一半刪掉,跟業務特性相關,不過思路大致是這樣的,一個非同步的方式,最終來保證一致性。

  第二個方案是掃增量。透過服務操作兩個庫,寫成功第一個庫寫一條日誌,寫成功第二個庫再寫一條日誌。這些日誌裡的就是每天改變的資料,每天不用掃描全量,只要掃描每天改變的資料就行了。如果掃描日誌不匹配,就透過非同步的方式修復,保證最終一致性。

  第三個方式,比前兩種方式更加實時。不寫日誌了,而是發訊息。用一個訊息元件,資料庫正向表操作成功了,發一個訊息,冗餘表操作成功了,發另一個訊息。用一個非同步的服務去監聽這兩個訊息,如果只有一條訊息到達,就去資料庫檢測一致性,並用非同步的方式來補償。

  最後是多例項多庫,這也是解決資料量大的一個常見方案。它會帶來什麼樣的不一致呢?這裡有一個案例,下單的一個操作,可能有三個資料要修改,一個是餘額的資料,我可能要扣減一些餘額;一個是訂單的資料,要新增一條訂單;一個是流水的資料,要新增一條流水。原來是單庫事務來保證一致性,現在資料量大了,變成多個庫,餘額是一個單獨的例項,訂單是一個單獨的例項,流水是一個單獨的例項,所以原來的一個事務,在多庫狀態下,就變成三個事務。

  多例項,多庫事務,不一致,怎麼辦?這一塊我們有兩個最佳化實踐。

  第一個是補償事務,業內應該也經常用到補償事務。

  餘額操作,正向的操作是扣減餘額,補償事務就是把餘額加回來。

  訂單操作,正向的操作是新增訂單,補償事務就是把訂單刪除掉。

  流水操作,正向的操作是新增流水,補償事務就是把流水刪除。

  總之,補償事務就是當你發現前面的事務執行失敗的時候,要執行一個應用層的事務,回滾一個動作。

  另外一種方式,偽分散式事務的解決方案,是後置提交。

  先細化的看一下三個事務是怎麼執行的?第一個事務先執行再提交,第二個事務執行再提交,第三個事務執行再提交。事務的執行過程很慢,事務的提交過程很快。上圖這個例子,可能執行時間200毫秒,提交時間幾毫秒,什麼時候會出現不一致呢?第一個事務提交成功之後,最後一個事務提交成功之前的中間,任何一個地方出現異常都會導致不一致。

  最佳化其實也很簡單,後置提交。第一個事務執行,第二個事務執行,第三個事務執行;第一個事務提交,第二個事務提交,第三個事務提交。什麼時候會出現不一致呢?仍然是第一個事務提交成功之後,第三個事務提交成功之前的時間間隔,如果出現了,網路異常,伺服器掛了,就會不一致。但是這個間隔就只有後面的兩毫秒,所以整個不一致的機率是降低了百倍左右。

  最後做一個簡單的總結。根據我的經驗,40分鐘50分鐘的一個技術分享,第二天能夠記住的只有10%。如果只記住10%,那我希望大家能夠記住這一頁的內容,並希望自己的邏輯是清晰的。

  資料庫架構最初是單庫,單庫會碰到什麼問題?會碰到讀效能瓶頸的問題。讀效能瓶頸最早用什麼樣的方式去解決?主從同步讀寫分離,它會帶來什麼問題?主從的不一致,用什麼方案解決?我們的實踐是中介軟體,以及強制讀主。

  提升讀效能,服務化加快取也是常見方案,帶來什麼新的問題?快取和資料庫的不一致。在Cache Aside Pattern的情況下,有寫後立即讀的問題,舊資料可能入快取。我們的實踐,可以透過非同步淘汰的方式,當寫操作在從庫上真正完成的時候再去淘汰快取。同時,我們建議為所有允許Cache miss的資料設定超時時間。

  資料庫架構,資料量大的問題,怎麼解決?常用的解決方案是分庫,多例項。分庫帶來什麼新的問題?記得我的例子麼,分了庫之後,可以保證同一個使用者的資料在同一個庫裡,不能夠保證同一個司機資料也在同一個庫裡,怎麼解決?使用資料冗餘。冗餘帶來什麼問題?冗餘資料的不一致問題,方向是最終一致性。怎麼最終保證一致性?掃全量,掃增量,實時訊息對。除了多庫,多例項也可以擴充套件資料儲存量,會遇到什麼問題?多庫的事務不能在保證原則性,補償事務,後置提交,都是我們的最佳化實踐。

  今天的內容這麼多,希望大家有收穫,謝謝大家。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31545803/viewspace-2219723/,如需轉載,請註明出處,否則將追究法律責任。

相關文章