1萬字長文高速你千萬級並發架構下如何提高數據庫存儲性能


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

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學架構]公眾號,獲取更多精品原創


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM