1萬字長文高速你千萬級併發架構下如何提高資料庫儲存效能

跟著Mic學架構發表於2021-10-20

如圖所示,表示發起一個請求時,涉及到資料庫的相關操作,在前面的文章中我們說過,如果服務端要提升整體的吞吐量,就必須要減少每一次請求的處理時長,那麼在當前這個場景中,資料庫層面哪些因素會影響到效能呢?

image-20210624225017321
圖2-1

池化技術,減少頻繁建立資料庫連線

遇到這樣的問題,解決辦法就是順著當前整體的邏輯去思考,首先,應用要和資料庫打交道,必然會設計到資料庫連結的建立。然後在當前連線中完成資料庫的相關操作,最後再關閉連線。

在這種場景下,客戶端每次發起請求,都需要重新建立連線,如果頻繁的建立連線是否會影響到效能呢?答案是一定的,我們通過下面這樣一個方式來驗證一下

# -i指定網路卡名稱
tcpdump -i eth0 -nn -tttt port 3306

當我們向資料庫發起一次連線時,上述抓包命令會列印連線的相關資訊如下。(通過Navicat 的連結測試工具測試)

關注前面8行資料即可。

2021-06-24 23:15:50.130812 IP 218.76.8.219.57423 > 172.17.136.216.3306: Flags [S], seq 759743325, win 64240, options [mss 1448,nop,wscale 8,nop,nop,sackOK], length 0
2021-06-24 23:15:50.130901 IP 172.17.136.216.3306 > 218.76.8.219.57423: Flags [S.], seq 3058334924, ack 759743326, win 29200, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
2021-06-24 23:15:50.160730 IP 218.76.8.219.57423 > 172.17.136.216.3306: Flags [.], ack 1, win 260, length 0
    
2021-06-24 23:15:50.161037 IP 172.17.136.216.3306 > 218.76.8.219.57423: Flags [P.], seq 1:79, ack 1, win 229, length 78
2021-06-24 23:15:50.190126 IP 218.76.8.219.57423 > 172.17.136.216.3306: Flags [P.], seq 1:63, ack 79, win 259, length 62
2021-06-24 23:15:50.190193 IP 172.17.136.216.3306 > 218.76.8.219.57423: Flags [.], ack 63, win 229, length 0
2021-06-24 23:15:50.190306 IP 172.17.136.216.3306 > 218.76.8.219.57423: Flags [P.], seq 79:90, ack 63, win 229, length 11
2021-06-24 23:15:50.219256 IP 218.76.8.219.57423 > 172.17.136.216.3306: Flags [P.], seq 63:82, ack 90, win 259, length 19
2021-06-24 23:15:50.219412 IP 172.17.136.216.3306 > 218.76.8.219.57423: Flags [P.], seq 90:101, ack 82, win 229, length 11
2021-06-24 23:15:50.288721 IP 218.76.8.219.57423 > 172.17.136.216.3306: Flags [.], ack 101, win 259, length 0
  • 第一部分是TCP三次握手建立連線的資料包

    • 第一個資料包是客戶端向服務區段傳送一個SYN包
    • 第二個資料包是服務端返回給客戶端的ACK包以及一個SYN包
    • 第三個資料包是客戶端返回給服務端的ACK包
  • 第二個部分是Mysql服務端校驗客戶端密碼的過程

從開始建立連線的時間130812到最終完成連線288721, 總共耗時157909,接近158ms時間,這個時間看起來很小,而且在請求量較小的情況下,對系統的影響不是很大。但是請求量上來之後,這個請求耗時的影響就非常大了。

而對於這個問題的解決辦法大家都已經知道,就是利用池化技術,預先建立好資料庫連線,當應用需要使用連線時,直接從預先建立好的連線中來獲取進行呼叫,如圖2-2所示。

image-20210625134607312

圖2-2

資料庫連線池的工作原理和執行緒池類似,資料庫連線池有兩個最重要的配置: 最小連線數和最大連線數, 它們控制著從連線池中獲取連線的流程:

  • 如果當前連線數小於最小連線數,則建立新的連線處理資料庫請求;
  • 如果連線池中有空閒連線則複用空閒連線;
  • 如果空閒池中沒有連線並且當前連線數小於最大連線數,則建立新的連線處理請求;
  • 如果當前連線數已經大於等於最大連線數,則按照配置中設定的時間(maxWait)等待舊的連線可用;
  • 如果等待超過了這個設定時間則向使用者丟擲錯誤。

總的來說,連線池核心思想是空間換時間,期望使用預先建立好的物件來減少頻繁建立物件的效能開銷,同時還可以對物件進行統一的管理,降低了物件的使用的成本。

資料庫本身的效能優化

資料庫本身的效能優化也很重要,常見的優化手段

  • 建立並正確使用索引,儘量只通過索引訪問資料
  • 優化SQL執行計劃,SQL執行計劃是關係型資料庫最核心的技術之一,它表示SQL執行時的資料訪問演算法,優化執行計劃也就能夠提升sql查詢的效能
  • 每次資料互動時,儘可能返回更少的資料,因為更大的資料意味著會增大網路通訊延遲。常見的方式是通過分頁來查詢資料、只返回當前場景需要的欄位
  • 減少和資料庫的互動次數,比如批量提交、批量查詢
  • ...

資料庫讀寫操作的效能問題

如果老闆說公司準備在下個月搞一場運營活動,使用者數量會快速增加,導致對資料庫的讀壓力增加,假設在4 核 8G 的機器上運 MySQL 5.7 時,大概可以支撐 500 的 TPS 和 10000 的 QPS,而實際的QPS可能是10W,那怎麼解決呢?

首先分析一下這個問題,在絕大部分面向使用者的系統中,都是讀多寫少的模型,比如電商,大部分的時候是在搜尋和瀏覽,比如抖音,大部分是在載入短視訊,所以我們需要考慮的問題是,資料庫如何扛住查詢請求。一般的解決方法是讀寫分離,

所謂讀寫分離,就是把同一個資料庫分離成兩份,一份專門用來做事務操作,另一份專門用來做讀操作,如圖2-3所示。

image-20210625142110740

圖2-3

做了主從複製之後,我們就可以在寫入時只寫主庫,在讀資料時只讀從庫,這樣即使寫請求會鎖表或者鎖記錄,也不會影響到讀請求的執行。同時呢,在讀流量比較大的情況下,我們可以部署多個從庫共同承擔讀流量,這就是所說的 一主多從 部署方式,在你的垂直電商專案中就可以通過這種方式來抵禦較高的併發讀流量。另外,從庫也可以當成一個備庫來使用,以避免主庫故障導致資料丟失。

那麼你可能會說,是不是我無限制地增加從庫的數量就可以抵抗大量的併發呢? 實際上並不是的。因為隨著從庫數量增加,從庫連線上來的 IO 執行緒比較多,主庫也需要建立同樣多的 log dump 執行緒來處理複製的請求,對於主庫資源消耗比較高,同時受限於主庫的網路頻寬,所以在實際使用中,一般一個主庫最多掛 3~5 個從庫

當然,主從複製也有一些缺陷, 除了帶來了部署上的複雜度,還有就是會帶來一定的主從同步的延遲,這種延遲有時候會對業務產生一定的影響

資料量增加帶來的效能問題

隨著業務的增長,資料庫中的資料量也會隨著增加,由於最早開發時主要是為了趕進度,資料都是單表儲存,因此單表資料量增加之後,導致資料庫的查詢和寫入都造成非常大的效能開銷,具體體現在。

  • 單表資料量過大,千萬級別到上億級別,這時即使你使用了索引,索引佔用的空間也隨著資料量的增長而增大,資料庫就無法快取全量的索引資訊,那麼就需要從磁碟上讀取索引資料,就會影響到查詢的效能。
  • 資料量的增加也佔據了磁碟的空間,資料庫在備份和恢復的時間變長
  • 不同模組的資料,比如使用者資料和使用者關係資料,全都儲存在一個主庫中,一旦主庫發生故障,所有的模組兒都會受到影響
  • 在 4 核 8G 的雲伺服器上對 MySQL5.7 做 Benchmark,大概可以支撐 500TPS 和 10000QPS,你可以看到資料庫對於寫入效能要弱於資料查詢的能力,那麼隨著系統寫入請求量的增長,對於寫請求的耗時也會增加(更新資料操作需要同步更新索引,資料量較大的情況下更新索引耗時較長)

在這類場景中,解決方案就是對資料進行分片,也就是分庫分表的機制,如圖2-4所示。資料拆分的核心降低單表和單庫的資料IO壓力,從而提升對資料庫相關操作的效能。

image-20210625144502558

圖2-4

不同儲存裝置帶來的效能提升

前面我們瞭解了對於傳統關係型資料庫的一些優化思路,整體來說,通過優化之後能夠提升程式訪問資料庫的計算效能。但是還是有一些情況,即便是優化之後,使用傳統關係型資料庫無法解決的,比如。

  • 當資料量達到TB級別時,傳統關係型資料庫基本做了分庫分表,單表資料量也是非常大的。
  • 對於一些不適合用關係型資料庫儲存的資料,傳統資料庫無法做到,所以資料庫本身的特性限制了多樣性資料的管理。

所以nosql出現了,大家對nosql這個概念已經不陌生了,它是指不同於傳統關係型資料庫的其他資料庫系統的一個統稱,它不使用SQL作為查詢語言,並且相對於傳統關係型資料庫來說,

它提供了更高的效能以及橫向擴充套件能力,非常適合網際網路專案中高併發且資料量較大的場景中,如圖25所示,表示目前比較主流的不同型別的nosql資料庫。

image-20210625164052410

圖2-5 不同的NoSql資料庫

這個網站上記錄了所有的Nosql框架

https://hostingdata.co.uk/nosql-database/

Key-Value資料庫

key-value資料庫,典型的代表就是Redis、Memcached,也是目前業內非常主流的Nosq資料庫。

之所以在IO效能方面比傳統關係型資料庫高,有兩個點

  • 資料基於記憶體,讀寫效率高
  • KV型資料,時間複雜度為O(1),查詢速度快

KV型NoSql最大的優點就是高效能,利用Redis自帶的BenchMark做基準測試,TPS可達達到接近10W的級別,效能非常強勁。同樣的Redis也有所有KV型NoSql都有的比較明顯的缺點:

  • 查詢方式單一,只有KV的方式,不支援條件查詢,多條件查詢唯一的做法就是資料冗餘,但這會極大的浪費儲存空間
  • 記憶體是有限的,無法支援海量資料儲存
  • 同樣的,由於KV型NoSql的儲存是基於記憶體的,會有丟失資料的風險

基於Key-Value資料庫的特性,這類資料庫比較適用於快取的場景。

  • 讀多寫少
  • 讀取能力強
  • 可以接受資料丟失

這類儲存相比於傳統的資料庫的優勢是極高的讀寫效能,一般對效能有比較高的要求的場景會使用,主要使用場景。

  • 用來做分散式快取,提升程式處理效率。
  • 用來做會話資料儲存
  • 其他功能性特性,比如訊息通訊、分散式鎖、布隆過濾器
  • 微博的feed流,早期就是用了redis實現。(持續更新並呈現給使用者內容的資訊流。每個人的朋友圈,微博關注頁等等都是一個 Feed 流)

列式資料庫

我們最早學習資料庫,都是基於以二維表形式儲存,每一行代表一條完整的資料。大部分傳統的關係型資料庫中,都是以行來儲存資料。不過最近幾年,列式儲存也逐步被廣泛運用在大資料框架中。

行儲存和列儲存,是資料庫底層資料組織的形式的區別,如圖2-6所示,資料庫表中所有列一次排成一行,以行位單位儲存,再配合B+樹或者SS-Table作為索引,就能快速通過主鍵找到相應的行資料。

image-20210625202700946

圖2-6

在實際應用中,大部分的操作都是以實體(Entity)為單位,也就是大部分CRUD操作都是針對一整行記錄,如果需要儲存一行資料,只需要在原來的資料後追加一行資料即可,所以資料的寫入非常快。

但是對於查詢來說,一個典型的查詢操作需要遍歷整個表,分組、排序、聚合等,對於行儲存來說,這樣的操作的優勢就不存在了,更慘的是,分析型SQL可能不需要用到所有的列,僅僅只需要對某些列進行運算即可,但是那一行中和本次操作無關的列也必須要參與到資料掃描中。

比如,如圖2-7所示,現在我想統計所有文章的總的點贊數量,作為行儲存的系統,資料庫會怎麼操作呢?

  • 首先需要把所有行的資料載入到記憶體
  • 然後對like_num列做sum操作

image-20210625204523910

圖2-7

行式儲存對於OLAP場景而言,優勢就不存在了,所以就引入了列式儲存。

OLTP(on-line transaction processing)翻譯為聯機事務處理, OLAP(On-Line Analytical Processing)翻譯為聯機分析處理,從字面上來看OLTP是做事務處理,OLAP是做分析處理。從對資料庫操作來看,OLTP主要是對資料的增刪改,OLAP是對資料的查詢

如圖2-8所示,列式儲存是將每一列資料組織在一起,它方便對於列的操作,比如前面說的統計like_num之和,按列儲存之後只需要一次磁碟操作就可以完成三個資料的彙總,所以非常適合OLAP的場景。

  • 當查詢語句只涉及部分列時,只需要掃描相關列
  • 每一列資料都是相同型別,彼此間的關聯性更大,對列資料壓縮的效率較高。

但是對於OLTP來說不是很友好,因為一行資料的寫入需要修改多個列。

image-20210625221307500

圖2-8

列式儲存在大資料分析中使用非常多,比如推薦畫像(螞蟻金服的風控)、是空資料(滴滴叫車的歸集資料)、訊息/訂單(電信領域、銀行領域)不少訂單查詢底層的儲存。 Feeds流(朋友圈類似的應用)等等。

image-20210625220018008

圖2-9

文件型資料庫

傳統的資料庫,所有資訊會被分割成離散的資料欄位,儲存在關係型資料庫中,甚至對於一些複雜的場景,還會分散在不同的表結構中。

舉個例子,在一個技術論壇中,假設對於使用者、文章、文章評論表的關係圖如圖2-10所示。

image-20210625225801200

圖2-10

那使用者點一篇文章,裡面要顯示該文章的建立者、文章詳情、文章的評論,那麼服務端要做什麼呢?

  • 查詢文章詳情
  • 根據文章中的uid查詢使用者資訊
  • 查詢該文章的所有評論列表
  • 查詢每個評論的建立者名字

這個過程要麼就是多次資料庫查詢,要麼就是使用一個複雜關聯查詢來檢索,不管怎麼做,都不是很方便。而文件資料庫就可以解決這樣的問題。

文件資料庫是以文件單位,具體的文件形式有很多種,比如(XML、YAML、JSON、BSON)等,文件中儲存具體的欄位和值,應用可以使用這些欄位進行查詢和資料篩選。

一般情況下,文件中包含了實體中的全部資料,比如圖2-10的結構,我們可以直接把一篇文章的基本要素資訊構建成一個完整的文件儲存到文件資料庫中,應用程式只需要發起一次請求就可以獲取所有資料。b

Article:{
    Creator:{
        uid: '',
        username: ''
    },
    Topic: {
        title: '',
        content: ''
    },
    Reply: [
        {
            replyId:,
            content:''
        },
        {
            replyId:,
            content:''
        }
    ]
}

MongoDB是目前最流行的Nosql資料庫,它是一種面向集合、與模式(Schema Free)無關的文件型資料庫。它的資料是以“集合”的方式進行分組,每個集合都有單獨的名稱並可以包含無線數量的文件,這種集合與關係型資料庫中的表類似,唯一的區別就是它並沒有任何明確的schema。

在資料庫中,schema(發音 “skee-muh” 或者“skee-mah”,中文叫模式)是資料庫的組織和結構,schemasschemata都可以作為複數形式。模式中包含了schema物件,可以是(table)、(column)、資料型別(data type)、檢視(view)、儲存過程(stored procedures)、關係(relationships)、主鍵(primary key)、外來鍵(foreign key)等。資料庫模式可以用一個視覺化的圖來表示,它顯示了資料庫物件及其相互之間的關係

如圖2-11所示, 將資料儲存在類似 JSON 的靈活文件中,這意味著欄位可能因具體文件而異,並且資料結構可能隨著時間的推移而變化。

img

圖2-11

MongoDB沒有“資料一致性檢查”、“事務”等,不適合儲存對資料事務要求較高的場景,只適合放一些非關鍵性資料,常見應用場景如下:

  • 使用Mongodb對應用日誌進行記錄
  • 儲存監控資料,比如應用的埋點資訊,可以直接上報儲存到mongoDB中
  • MongoDB可以用來實現O2O快遞應用,比如快遞騎手、快遞商家的資訊儲存在MongoDB,然後通過MongoDB的地理位置查詢,方便用來查詢附近的商家、騎手等功能。

圖形資料庫

圖形資料庫,表示以資料結構“圖”作為儲存的資料庫。
圖形資料儲存管理兩類資訊:節點資訊和邊緣資訊。 節點表示實體,邊緣表示這些實體之間的關係。 節點和邊緣都可以包含一些屬性用於提供有關該節點或邊緣的資訊(類似於表中的列)。

邊緣還可以包含一個方向用於指示關係的性質。

圖形資料儲存的用途是讓應用程式有效執行需遍歷節點和邊緣網路的查詢,以及分析實體之間的關係。 如圖2-12所示,顯示了已結構化為圖形的組織人員資料。

實體為員工和部門,邊緣指示隸屬關係以及員工所在的部門。 在此圖中,邊緣上的箭頭表示關係的方向。

image-20210625180249817

圖2-12

使用此結構可以簡單直接地執行類似於“查詢 Sarah 的直接或間接下屬”或“誰與 John 在同一個部門工作?”的查詢。 對於包含大量實體和關係的大型圖形,可以快速執行復雜的分析。 多個圖形資料庫提供一種可用於高效遍歷關係網路的查詢語言。比如:關係、地圖、網路拓撲、交通路線等場景。

NewSql

NewSql也是最近幾年出來的概念,想必大家或多或少都有聽過,NewSql是Nosql發展之後的下一代資料儲存方案。

前面我們瞭解了Nosql的優勢。

  • 高可用性和可擴充套件性,自動分割槽,輕鬆擴充套件
  • 不保證強一致性,效能大幅提升
  • 沒有關係模型的限制,極其靈活

但是有些優勢在某些場景下不是很適合,比如不保證強一致性,對於普通應用來說沒有問題,但是對於一些金融級的企業應用來說,

強一致的需求會比較高。另外,Nosql不支援SQL語句,不同的Nosql資料庫都是有自己獨立的API來進行資料操作,相對來說比較麻煩和複雜。

所以NewSql出現了,簡單來說,newSQL 就是在傳統關係型資料庫上整合了 noSQL 強大的可擴充套件性,傳統的SQL架構設計基因中是沒有分散式的,而 newSQL 生於雲時代,天生就是分散式架構。

NewSQL 的主要特性:

  • SQL 支援,支援複雜查詢和大資料分析。
  • 支援 ACID 事務,支援隔離級別。
  • 彈性伸縮,擴容縮容對於業務層完全透明。
  • 高可用,自動容災

商用NewSql

  • Spanner、F1:谷歌

  • OceanBase:阿里

  • TDSQL:騰訊

  • UDDB:UCloud

總結

在 NoSQL 資料庫剛剛被應用時,它被認為是可以替代關係型資料庫的銀彈,在我看來,也許因為以下幾個方面的原因:

  • 彌補了傳統資料庫在效能方面的不足;
  • 資料庫變更方便,不需要更改原先的資料結構;
  • 適合網際網路專案常見的大資料量的場景;

不過,這種看法是個誤區,因為慢慢地我們發現在業務開發的場景下還是需要利用 SQL 語句的強大的查詢功能以及傳統資料庫事務和靈活的索引等功能,NoSQL 只能作為一些場景的補充。

使用Redis優化效能問題

Redis是目前用得非常多的一種Key-Vlaue資料庫,我們先來通過一個壓測資料瞭解一下redis和mysql的效能差距。

演示專案: springboot-redis-example

通過jmeter工具分別壓測這個專案中的兩個url。

其中,基於mysql訪問的介面,吞吐量資料如下,qps=4735/s。

image-20210628143407508

圖2-13

基於redis的壓測資料,如圖2-14所示。

image-20210628143634472

圖2-14

可以很明顯的看到,在同樣的程式中,Redis的QPS要比Mysql的多了1000。

瞭解Redis

08年的時候有一個義大利西西里島的小夥子,筆名antirez(http://invece.org/),建立了一個訪客資訊網站LLOOGG.COM。如果有自己做過網站的同學應該知道,

有的時候我們需要知道網站的訪問情況,比如訪客的IP、作業系統、瀏覽器、使用的搜尋關鍵詞、所在地區、訪問的網頁地址等等。在國內,有很多網站提供了這個功能,比如CNZZ,百度統計,國外也有谷歌的Google Analytics。

也就是說,我們不用自己寫程式碼去實現這個功能,只需要在全域性的footer裡面嵌入一段JS程式碼就行了,當頁面被訪問的時候,就會自動把訪客的資訊傳送到這些網站統計的伺服器,然後我們登入後臺就可以檢視資料了。

LLOOGG.COM提供的就是這種功能,它可以檢視最多10000條的最新瀏覽記錄。這樣的話,它需要為每一個網站建立一個列表(List),不同網站的訪問記錄進入到不同的列表。如果列表的長度超過了使用者指定的長度,它需要把最早的記錄刪除(先進先出)。

img

圖2-15

當LLOOGG.COM的使用者越來越多的時候,它需要維護的列表數量也越來越多,這種記錄最新的請求和刪除最早的請求的操作也越來越多。LLOOGG.COM最初使用的資料庫是MySQL,可想而知,因為每一次記錄和刪除都要讀寫磁碟,因為資料量和併發量太大,在這種情況下無論怎麼去優化資料庫都不管用了。

考慮到最終限制資料庫效能的瓶頸在於磁碟,所以antirez打算放棄磁碟,自己去實現一個具有列表結構的資料庫的原型,把資料放在記憶體而不是磁碟,這樣可以大大地提升列表的push和pop的效率。antirez發現這種思路確實能解決這個問題,所以用C語言重寫了這個記憶體資料庫,並且加上了持久化的功能,09年,Redis橫空出世了。從最開始只支援列表的資料庫,到現在支援多種資料型別,並且提供了一系列的高階特性,Redis已經成為一個在全世界被廣泛使用的開源專案。

為什麼叫REDIS呢?它的全稱是Remote Dictionary Service,直接翻譯過來是遠端字典服務。

key-value資料庫使用排名

對於Redis,我們大部分時候的認識是一個快取的元件,當然從它的發展歷史我們也可以看到,它最開始並不是作為快取使用的。只是在很多的網際網路應用裡面,它作為快取發揮了最大的作用。所以下面我們來聊一下,Redis的主要特性有哪些,我們為什麼要使用它作為資料庫的快取。

大家對於快取應該不陌生,比如我們有硬體層面的CPU的快取,瀏覽器的快取,手機的應用也有快取。我們把資料快取起來的原因就是從原始位置取資料的代價太大了,放在一個臨時儲存起來,取回就可以快一些。

如果要了解Redis的特性,我們必須回答幾個問題:

1、為什麼要把資料放在記憶體中?

  1. 記憶體的速度更快,10w QPS

  2. 減少計算的時間

2、如果是用記憶體的資料結構作為快取,為什麼不用HashMap或者Memcache?

  1. 更豐富的資料型別

  2. 程式內與跨程式;單機與分散式

  3. 功能豐富:持久化機制、過期策略

  4. 支援多種程式語言

  5. 高可用,叢集

https://db-engines.com/en/ranking/key-value+store

image-20210626202902337

圖2-16

關注[跟著Mic學架構]公眾號,獲取更多精品原創

相關文章