數據庫優化-從設計超過1億用戶的網站說起

文章首次發表於程序員·凌雲雜誌2014年第6期:《從“如何設計用戶超過1億的應用”說起----數據庫調優實戰》
文中介紹了分佈式架構及數據層的初步優化策略,包括拆分、業務改造、執行計劃優化等。

本文不涉及具體實現代碼,需自行開發。
關於MySQL數據庫優化,可參考《MySQL性能分析與優化》《MySQL基本原理和函數》《MySQL常見問題及解決方案》


杭州湖畔網絡技術有限公司是一家專業提供SaaS化電商ERP服務的創業公司,主要用戶羣體爲經營淘寶、天貓、京東等主流電商平臺、自建商城、線下渠道的商家及中小企業。作爲SaaS服務提供商,服務數萬乃至數十萬級用戶是業務架構初期就必須考慮的問題。龐大的用戶羣以及海量的用戶數據意味着基礎設施的構建必須兼顧高效與穩定,而按照通用的基礎設施建設方案的話,需要面對成本過高、實現複雜、需要投入太多精力等問題,這對當時的湖畔網絡這樣的初創公司來說,完全不能承受。因此,更經濟、更方便擴展的雲服務平臺成爲首選。在對比現有各家雲服務後,我們選擇了穩定性與成熟度都經過大量用戶檢驗的阿里雲。

但要構建高性能的SaaS應用,僅憑雲服務基礎設施是不夠的。如何基於雲服務平臺設計並實施符合自身業務特點的系統架構,也是決定產品性能的關鍵。本文將講述我們如何利用雲服務,使用相對經濟的方案,解決海量用戶的數據庫使用問題。

架構

我們的SaaS化電商ERP服務的整體架構是基於阿里雲服務平臺實施的,如圖1所示。
在這裏插入圖片描述

  • 採用SLB(Server Load Balance,負載均衡)作爲Web集羣訪問入口,負責爲Web端的多臺服務器進行流量分發。SLB是基於集羣建設的,並且可以隨時變配,按量付費。它不僅爲我們實現了成熟的負載均衡方案,其穩定性與靈活性也爲Web集羣提供了更多可能。
  • 後端配置多臺ECS(Elastic Compute Service,雲服務器)實例,將主要應用服務都部署在ECS上。除了可彈性擴容這一特性,ECS提供的安全防護和快照備份爲服務器安全和容災提供了非常成熟的解決方案,這恰恰是我們這種業務型創業團隊積累相對最薄弱的方面。另外,ECS多線接入骨幹網絡能保證網絡的穩定和性能,使得任何網絡的用戶訪問應用服務都非常順暢。
  • DB集羣由多臺RDS(Relational Database Service,關係型數據庫服務)實例組成。RDS是雲數據庫,簡單易用,使用方法與自行部署的數據庫完全一樣。其成熟的雙機熱備與底層資源隔離,保證了我們這兩年來數據庫的平穩運行。另外,強大的iDB Cloud控制檯、專業的DBA團隊支持,爲我們監控數據庫運行狀況、定位和解決數據庫問題,提供了非常多的建議和幫助。
  • 集羣之間的共享資源統一存放在OCS(Open Cache Service,開放緩存服務)中。我們用OCS來存放數據路由和實時性不高的業務數據。緩存作爲我們架構性能中非常重要的一個環節,在承受了來自整個集羣各方面壓力的同時,還要保證響應穩定高速。

通過該方案,不僅發揮了阿里雲的優勢(不涉及物理機器的維護和折損,靈活地配置升級,成熟的備份與快照方案),而且通過集羣,避免了系統可能會遇到的單點故障,提高了系統彈性擴容的靈活性和可用性。


數據庫優化

作爲一個SaaS化、數據更集中、數據體量龐大的企業應用,數據庫是我們整體架構中的關鍵節點,如何保證其穩定與性能,是本文講述的重點。

分片

當用戶進入快速增長期後,隨着業務量迅速增加,核心業務表的存量數據和增長速度絕對不是單個DB所能承受的(幾乎所有單DB配置都存在性能物理上限瓶頸,即使選擇升級配置也會受到成本和資源上限的約束)。因此,我們一開始就將數據庫分庫分片(Sharding)作爲一個可行方案優先考慮,主要分析如下。

  • 場景:業務熱點數據持續增加,團隊有一定的數據庫架構積累能支撐獨立研發或熟悉成熟的中間件(如Cobar)。
  • 優點:成本低(可以利用開源免費的數據庫集羣替代大型商業DB);可靈活擴容(不斷增加新的數據庫切片即可);相對均勻分佈的數據讀寫,避免單點障礙。
  • 缺點:需要研發團隊在數據庫架構上投入大量精力;數據庫集羣維護需要成本投入。

考慮到業務特性,我們最終採用了行業比較通用的水平拆分+垂直拆分策略,並自主完成DAO與JDBC之間的數據訪問封裝層開發工作。

水平拆分:按用戶將數據拆分到多個庫的相同表中

水平拆分的思路,就是將原本存放在單個RDS數據庫中的數據,根據業務ID不同,拆分到多個數據庫中(參見圖2)。拆分後,各庫的表數量及表結構都保持一致。水平拆分首先需要確立唯一的業務主表,即其他所有表的數據都與主表ID(前文所說的業務ID)存在直接或間接的主從關係,可以通過主表ID對全部數據做很好的切分。我們選擇的業務主表爲用戶表,其他業務表或表的父表都包含一個用戶ID。因此,我們切分的目標就是將不同用戶數據存放到不同的數據庫中。
在這裏插入圖片描述
確定了拆分規則後,下一步是着手封裝Sping數據訪問封裝層(DBWrapper)。DBWrapper介於DAO與JDBC之間,每個業務DAO進行數據庫基本操作,都會經過DBWrapper。它的主要作用是將數據庫架構的變化對業務層透明,業務層可以如同操作單個DB一樣,調用DBWrapper提供的數據庫操作接口,而判斷操作哪個數據庫的邏輯,則全部交由DBWrapper封裝完成(參見圖3)。
在這裏插入圖片描述
DBWrapper主要提供新用戶初始化和數據庫操作接口。在新增用戶初始化到系統時,需先動態判斷系統各庫的負載分佈情況。粗略一點的算法就是判斷各庫的用戶數,如共有4個庫,可以根據user_id%4的情況決定目標庫;再精細一點可以挖掘下核心業務數據的分佈情況,具體分配算法需要基於業務設定(如考慮不同用戶的平均訂單量)。通過各庫壓力綜合計算後,分析出壓力最小的目標數據庫,並將該新增用戶數據存放到指定的目標庫,同時更新路由信息(Router)。

當用戶完成初始化進行業務操作時,則需由業務層調用DBWrapper的操作接口。DBWrapper接收到請求後,會根據業務層傳入的User_id匹配Router,判斷最終需要操作的RDS實例和數據庫。判斷完成後,只需要按部就班地開連接執行就可以了。具體的代碼實現,需要結合自身的持久層框架,找一名研究過持久層框架實現的開發人員即可完成。

這樣就將系統用戶整體數據壓力,相對均勻地分佈到多個RDS實例與數據庫上。事實證明,這確實是一個非常有效的方案,尤其是對於數據量大、增長迅猛的表。只是在後續實施過程中,我們發現有時會有單個用戶的業務壓力比較突出,針對這種情況,我們可以通過一些人工干預(如遷移數據到單獨的庫)進行微調,當然最終的解決方案還是要不斷調優路由算法。

切分後,不可避免地需要考慮數據字典(DD)和數據路由(Router)的處理。暫時我們採用的方法是將所有數據字典與路由放入獨立的庫,這也是後文中垂直拆分的一種應用。需要說明的是,數據庫僅是這兩個業務的一種實現方式,一般還可以通過或結合分佈式緩存來處理這些業務(我們選用了OCS)。而對於可能出現的單點障礙,預留的擴展方案爲水平拆分或創建只讀節點(只讀節點可以使用RDS最新提供的只讀實例,目前還在內測階段)。

垂直拆分:按業務將表分組拆分到多個庫中

與水平拆分相比,垂直拆分要更簡單一些。其基本思路就是將存放在單個數據庫的表分組,把其中業務耦合度較高、聯繫緊密的表分爲一組,拆分到其他DB中(參見圖4)。拆分後,各庫的表結構及其業務意義將完全不同。雖然規則簡單、實施方便,但垂直拆分總是需打斷些關聯,因爲實際操作中,基礎資源常常出現在各個業務場景,在切分時又不得不切分到兩個庫中,此時就需要業務層多次查詢後,在內存處理數據,實現數據庫Join的效果。
在這裏插入圖片描述

垂直拆分同樣需要DBWrapper,但封裝規則與水平拆分略有不同,需要針對不同的業務,建立不同的DBWrapper。此時不再是完全業務層無感知,需要業務層根據業務場景有針對性使用。單個DBWrapper的實現與水平拆分一致。

垂直拆分的好處在於,將整體業務數據切分成相對獨立的幾塊,隔離了不同業務之間的性能影響。而由於拆分後的數據庫業務比較集中,也更容易找到業務主表,更有利於水平拆分。

對於垂直拆分,目前我們主要用於解決數據路由(包含了用戶的基本信息)、數據字典模塊,以及常見的冷數據問題。冷數據的處理一直是行業的常見問題(其實對於冷數據的劃分,也是水平拆分),目前我們採用的方案是集中存儲,即按自己的冷數據切分方式,通過自行開發的遷移程序將判定的冷數據增量遷移到一個庫中。這個方案既能夠分離冷數據對熱點數據的操作影響,也可以爲大數據的挖掘提供比較便利的條件。使用相對獨立的冷數據存儲結構,能方便以後採用更高效、成本更低廉的存儲介質。當然該方案存在一些潛在問題,如果冷數據庫滿了該怎麼辦?目前我們預留的設計方案是,歷史庫的水平拆分,也可以考慮其他存儲形式。

水平拆分與垂直拆分組合使用

拆分一直是數據庫優化的關鍵詞(無論是庫表結構還是SQL寫法),它是每個高併發產品最終都要經歷的一步。拆分方案的核心主要在於可以通過添加更多RDS實例和數據庫(常常爲了節約成本,多個數據庫可以部署在一個RDS實例上),靈活擴容系統的負載能力。在數據庫架構中,水平拆分和垂直拆分一般都是搭配使用的,兩者的先後順序視具體情況而定。一般而言,垂直拆分更容易,也可以爲水平拆分做鋪墊,一是業務集中,便於提取主表,二是垂直拆分後,可以只水平拆分壓力高的表,而業務增長緩慢的表則可以保留單DB,從而提高拆分效率以及降低實施成本(參見圖5)。

我們之所以優先水平拆分,主要原因還是成本和效率及當時的一些侷限性。只按業務ID(用戶)做好路由配置,這樣各個庫中的結構完全一致,保留了原本的業務邏輯與實現,避免了跨庫關聯,能大大節省實現成本。

儘管拆分有種種好處,但由於分佈式事務及跨庫Join的實現複雜度較高及可用性較差,所以分佈式事務一般都通過業務層使用樂觀鎖控制。而跨庫的表間關聯一定要打斷,否則性能和實現複雜度都會超出可接受範圍。對於跨庫的Join、Group by等問題,都需要在業務層處理。目前我們採用的是分批查詢,在業務層組裝結果的方式。

有些遺憾的是,由於我們早期使用RDS時,阿里雲尚未推出DRDS(分佈式數據庫)產品,所以上述拆分的數據庫底層架構均是由我們自行研發的,投入了大量的精力。而現在有了DRDS,正準備做拆分的團隊,則無需再自己造輪子,直接拿來用即可,這樣團隊可以將更多的精力放在業務上。

積沙成塔,小處大有可爲

雖然我們在架構上做了優化,但在產品發展過程中還是會出現性能不太理想的情況。在阿里雲支持中心和論壇上,也可以看到其他業務型團隊反饋使用RDS時遇到類似的情況。最初大家都懷疑是不是RDS的底層資源隔離有問題,多個用戶共享資源時發生爭搶,導致RDS的性能問題。但在阿里雲DBA的指導和協助下,發現是由於產品設計時對數據庫的使用太“不拘小節”,而隨着併發壓力與數據量增加,大量細小的性能問題被放大,集中暴露出來。

解決燈下黑:修正業務層的數據庫操作陋習

  • 場景:數據庫性能有問題,應優先從業務層分析。
  • 優點:減輕數據庫的直接壓力,比執行數據庫優化方案更加迅速有效。
  • 缺點:業務研發需要關注一些數據庫操作內容;有時會犧牲一些業務;產品規模越大實施越困難。

在數據庫的優化過程中,研發團隊最容易忽視的往往是業務層中的數據庫使用。一些優化方案可以作爲開發的常態化準則。下面僅列舉幾個常用的優化方案。

  • 延遲加載。很多頁面展現時,單個實體實際只展現部分內容,因此可按需加載,減輕數據庫壓力,又節省網絡流量。延遲加載也可體現在數據庫表的拆分設計上。

  • 適當緩存,以空間換時間。對於很多實時性較低或乾脆就是數據字典的內容,無需實時到數據庫中加載,只需在使用前加載到內存中,實際使用時到內存中獲取即可。

  • 減少不必要的開連接(連接池、批量查詢及提交)。對於大部分的Web應用,連接池大大減少了系統因開數據庫連接產生的開銷。而在查詢和提交中,將多個任務合併到一次數據庫操作中,也可以大大提高數據庫使用效率。

  • 樂觀鎖是高併發下不錯的解決方式。相比於數據庫的悲觀鎖,業務層實現的樂觀鎖,不僅能減少鎖爭搶,還可以減少數據庫的鎖開銷,進而提高數據庫使用效率。

  • 分解大事務。數據庫對於大事務的原子性保證,也是不容忽視的開銷。業務使用時,儘量將大事務切分爲小事務,或者適當利用異步提交,精簡事務體積。

  • 合理使用Join。數據庫執行計劃中,有一條準則是越簡單越快速。所以通過適當冗餘數據設計或業務層分批查詢後內存組裝數據,減少數據庫Join語句及SQL複雜度,對於數據庫執行效率和執行計劃的優化都有不可忽視的好處。

擠掉海綿裏的水:優化數據庫執行計劃

  • 場景:併發不多、數據量並不很大,或系統整體壓力較低,只有某幾個業務點性能較差。
  • 優點:在不改變基本條件的情況下,挖掘數據庫更大潛力。
  • 缺點:需要DBA協助或研發團隊對數據庫執行計劃做研究。

由於執行計劃的優化往往涉及到數據庫的運行機制與底層設計,此處實難三言兩語說清。所以下面僅列舉幾個我們受益頗深的優化方案。建議大家優化執行計劃時,多關注、分析iDB Cloud控制檯中的性能報告和建議,也儘量多向阿里雲DBA們請教,一般可以通過提工單的方式。有條件或興趣的話,DBA可以通過預約到阿里雲現場學習。另外,執行計劃的優化需要大量的調試工作,通過在阿里雲控制檯創建生產數據庫的臨時實例,可以準確模擬當前系統的數據結構、分佈與壓力。

字段類型選擇

選擇合理的字段,往往可以大大減少數據庫行數據的大小,並提高索引匹配的效率,進而大大提升數據庫性能。使用更小的數據類型,如日期採用date代替datetime、類型或標記使用tinyint代替smallint和int、使用定長字段代替非定長字段(如char代替varchar),都能或多或少減少數據行大小,提高數據庫緩衝池的命中率。而作爲表字段中特殊的一員―主鍵,其選型更會對錶索引的穩定和效率帶來很大的影響,一般建議考慮數據庫自增或自主維護的唯一數值。

高分離度字段建立索引

對於查詢來講,高分離度字段往往意味着精準或部分精準的條件。相對來講是最好優化的一種場景,只需要對分離度較高的字段單獨建立索引即可。當然實際使用中會有更多細節需要摸索。精確條件在各業務中基本都會用到,在越複雜的業務場景中,精確條件優先的原則,將是最有效的優化方案。需要注意的是,儘管高分離度字段單獨建立索引效率很高,但過多的索引會影響表寫入的效率,所以需要謹慎添加。這一點iDB Cloud中有大表索引的建議可以參考。

覆蓋索引(Covering Index)

通俗一點理解,就是執行計劃可以通過索引完成數據查找和結果集獲取,而無需回表(去緩衝池或磁盤查找數據)。而由於MySQL的索引機制限制,一次查詢時,將只用到一個索引或將兩個索引聚合(index_merge)起來使用,所以意味着複雜的業務場景中,單獨對每個字段建立索引可能沒有什麼用處。所以對於一些特定的查詢場景,建立合適的組合索引,應用覆蓋索引方法可以避免大量隨機I/O,是更爲推薦的優化方案(如果執行計劃Explain的Extra中有Using Index,就說明使用了覆蓋索引)。但實際業務總是會比索引本身更復雜,業務中需要查找或者獲取的字段信息往往是很多的,而組合索引並不能涵蓋所有的字段(否則我們將擁有一個比數據還要龐大的索引)。此時,爲了應用覆蓋索引,就需要使用主鍵延遲關聯(Deferred Join),即先通過組合索引中包含的字段條件,初步查詢出相對較小的結果集(面向結果集原則),該結果集只包含主鍵字段;然後通過獲取到的這個主鍵隊列,再對數據表做關聯。

適當妥協:用有限的精力,做最重要的事

見招拆招:升配置

  • 場景:性能問題緊迫,團隊時間資源有限。
  • 優點:簡單粗暴見效快,基本適用於任何優化階段。
  • 缺點:增加成本;治標不治本,只是延遲問題再次爆發時間;資源總有上限,遲早升無可升。

一般業務型的研發團隊,很難有額外的精力投入到數據庫方面,也沒有專業的DBA來不斷調優數據庫配置、優化數據庫服務器性能。所以早期團隊可以選擇的方案不多,也很難在技術上深挖下去,只能用成本換時間:性能配置不夠,那就升級服務器配置。

那麼問題來了:自己部署的數據庫要升級配置,除了調整數據庫配置參數,還會受到物理機的限制,因此就要考慮更加複雜的數據庫備份和同步策略。但這是業務團隊所不能接受,甚至短期內無法實現的,升配置也就變成了一個複雜的問題。不過我們使用了RDS,其彈性升級策略,正是這個問題的最佳解決方案。

在多年發展過程中,我們也有多次數據庫優化效果很差、不得已升級配置,結果發現升級後服務器開銷很平穩、曲線很穩定。這種情況其實就是業務壓力確實已經達到了一定程度,是需要增加資源了。這種情況如果仍投入大量人力物力去優化SQL,幾乎不會有太大提升。

二八原則

在長期的數據庫乃至整個產品的優化過程中,我感受最深刻的就是:完美的方案可遇不可求。選擇方案時,如果能解決80%的問題,並規避或保留剩下的20%,則將大大提升團隊的整體效率。產品與架構都是在不斷優化演變的,我們要循序漸進、不斷努力,將今天的終點留作明天的起點。

總結與展望

作爲一位創業公司的技術開發人員,通過實際使用阿里雲產品,我總結了幾點關於使用雲計算產品的優勢。

  1. 便利的服務器彈性升級功能,可隨時應付像“雙十一”這樣的大促。而通過使用傳統IDC託管模式,物理機的維護、升級以及升級後的數據遷移都是比較頭疼的。

  2. 成熟可靠的數據備份與快照、數據庫主從分離與同步的底層方案。創業團隊無須承受自己造輪子的代價,可專注於業務開發。

  3. 雲計算產品經過檢驗、值得信賴的安全防護。

  4. 精簡了創業團隊人員規模。雲計算平臺具備專業的技術支持與服務,使得創業團隊不再需要數據庫和服務器管理員。

除了使用雲產品的心得,數據庫調優實踐是本文的重點。在數據庫的架構設計與性能優化方面,我秉承的原則是解決主要問題,按先分而擊之、再挖掘細節的步驟,周返往復不斷進行,同時系統架構也在這個過程中不斷演變。相信隨着時間推移,會有更多優秀的方案出現。尤其隨着雲服務不斷髮展,業務研發團隊投入到基礎設施的精力與成本,將會無限減少。會有越來越多專注於業務研發的團隊,推出更多優秀的互聯網產品,用互聯網服務推動企業創新,重塑中小企業信息化形態。

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