是如何在SQLServer中處理天天四億三千萬記錄的

項目背景數據庫

這是給某數據中心作的一個項目,項目難度之大使人髮指,這個項目真正的讓我感受到了,商場如戰場,而我只是其中的一個小兵,太多的戰術,太多的高層之間的較量,太多的內幕了。具體這個項目的狀況,我有空再寫相關的博文出來。緩存

這個項目是要求作環境監控,咱們暫且把受監控的設備稱爲採集設備,採集設備的屬性稱爲監控指標。項目要求:系統支持很多於10w個監控指標,每一個監控指標的數據更新不大於20秒,存儲延遲不超過120秒。那麼,咱們能夠經過簡單的計算得出較理想的狀態——要存儲的數據爲:每分鐘30w,每一個小時1800w,也就是天天4億3千兩百萬。而實際,數據量會比這個大5%左右。(實際上大部分是信息垃圾,能夠經過數據壓縮進行處理的,可是別人就是要搞你,能咋辦)服務器

上面是項目要求的指標,我想不少有很多大數據處理經驗的同窗都會呲之以鼻,就這麼點?嗯,我也看了不少大數據處理的東西,可是以前沒處理過,看別人是頭頭是道,什麼分佈式,什麼讀寫分離,看起來確實很容易解決。可是,問題沒這麼簡單,上面我說了,這是一個很是惡劣的項目,是一個行業惡性競爭典型的項目。數據結構

  1. 沒有更多的服務器,而是這個服務器除了搭配數據庫、集中採集器(就是數據解析、告警、存儲的程序),還要支持30w點的北向接口(SNMP),在程序沒有優化以前CPU常年佔用80%以上。由於項目要求要使用雙機熱備,爲了省事,減小沒必要要的麻煩,咱們把相關的服務放在一塊兒,以便可以充分利用HA的特性(外部購買的HA系統)
  2. 系統數據正確性要求極其變態,要求從底層採集系統到最上層的監控系統,一條數據都不能差
  3. 咱們的系統架構以下,能夠看到,其中數據庫壓力很是之大,尤爲在LevelA節點:

是如何在SQLServer中處理天天四億三千萬記錄的

 

  1.  
  2. 硬件配置以下:
  3. CPU:英特爾® 至強® 處理器 E5-2609 (4核, 2.40GHz, 10MB, 6.4 GT/s)
  4. 內存:4GB (2x2GB) DDR3 RDIMM Memory, 1333MHz,ECC
  5. 硬盤:500GB 7200 RPM 3.5'' SATA3 硬盤,Raid5.
  6. 數據庫版本
  7. 採用的是SQLServer2012標準版,HP提供的正版軟件,缺乏不少企業版的NB功能。

寫入瓶頸架構

首先遇到的第一個攔路虎就是,咱們發現現有的程序下,SQLServer根本處理不了這麼多的數據量,具體狀況是怎樣的呢?app

咱們的存儲結構分佈式

通常爲了存儲大量的歷史數據,咱們都會進行一個物理的分表,不然天天上百萬條的記錄,一年下來就是幾億條。所以,原來咱們的表結構是這樣的:函數

CREATE TABLE [dbo].[His20140822](
 [No] [bigint] IDENTITY(1,1) NOT NULL,
 [Dtime] [datetime] NOT NULL,
 [MgrObjId] [varchar](36) NOT NULL,
 [Id] [varchar](50) NOT NULL,
 [Value] [varchar](50) NOT NULL,
 CONSTRAINT [PK_His20140822] PRIMARY KEY CLUSTERED 
(
 [No] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

No做爲惟一的標識、採集設備Id(Guid)、監控指標Id(varchar(50))、記錄時間、記錄值。並以採集設備Id和監控指標Id做爲索引,以便快速查找。微服務

批量寫入源碼分析

寫入當時是用BulKCopy,沒錯,就是它,號稱寫入百萬條記錄都是秒級的

public static int BatchInert(string connectionString, string desTable, DataTable dt, int batchSize = 500)
 {
 using (var sbc = new SqlBulkCopy(connectionString, SqlBulkCopyOptions.UseInternalTransaction)
 {
 BulkCopyTimeout = 300,
 NotifyAfter = dt.Rows.Count,
 BatchSize = batchSize,
 DestinationTableName = desTable
 })
 {
 foreach (DataColumn column in dt.Columns)
 sbc.ColumnMappings.Add(column.ColumnName, column.ColumnName);
 sbc.WriteToServer(dt);
 }
 return dt.Rows.Count;
 }

存在什麼問題?

上面的架構,在天天4千萬的數據都是OK的。可是,調整爲上述背景下的配置時,集中監控程序就內存溢出了,分析得知,接收的太多數據,放在了內存中,可是沒有來得及寫入到數據庫中,最終致使了生成的數據大於消費的數據,致使內存溢出,程序沒法工做。

瓶頸到底在哪裏?

是由於RAID磁盤的問題?是數據結構的問題?是硬件的問題?是SQLServer版本的問題?是沒有分區表的問題?仍是程序的問題?

當時時間只有一個星期,一個星期搞很差,項目監管就要咱們滾蛋了,因而,有了連續工做48小時的壯舉,有了處處打電話求人的抓雞……

可是,這個時候須要的是冷靜,再冷靜……SQLServer版本?硬件?目前都不大可能換的。RAID磁盤陣列,應該不是。那麼究竟是什麼,真TM的冷靜不下來。

你們可能體會不到現場那種緊張的氣氛,其實過了這麼久,我本身也都很難再回到那種情境。可是能夠這麼說,或許咱們如今有了各類方法,或者處於局外人咱們有更多思考,可是當一個項目壓迫你快到放棄的時候,你那時的想法、考慮在現場環境因素的制約下,均可能出現重大的誤差。有可能讓你快速的思考,也有可能思惟停滯。有些同事在這種高壓的環境下,甚至出現了更多的低級錯誤,思惟已經徹底亂了,效率更低了……36小時沒有閤眼,或者只在工地上(下雨天處處都是泥巴,幹了的話到時都是泥灰)眯兩三個小時,而後繼續幹,連續這麼一個星期!或者還要繼續!

不少人給了不少想法,可是好像有用,又好像沒用。等等,爲何是「好像有用,又好像沒用」?我隱隱約約中,好像抓住了一絲方向,究竟是什麼?對了,驗證,咱們如今是跑在現場環境下,以前沒有問題,不表明如今的壓力下沒有問題,要在一個大型系統中分析這麼個小功能,影響太大了,咱們應該分解它。是的,是「單元測試」,就是單個方法的測試,咱們須要驗證每一個函數,每一個獨立的步驟到底耗時在哪裏?

逐步測試驗證系統瓶頸

修改BulkCopy的參數

首先,我想到的是,修噶BulkCopy的各項參數,BulkCopyTimeout、BatchSize,不斷的測試調整,結果老是在某個範圍波動,實際並無影響。或許會影響一些CPU計數,可是遠遠沒有達到個人指望,寫入的速度仍是在5秒1w~2w波動,遠遠達不到要求20秒內要寫20w的記錄。

按採集設備存儲

是的,上述結構按每一個指標每一個值爲一條記錄,是否是太多的浪費?那麼按採集設備+採集時間做爲一條記錄是否可行?問題是,怎麼解決不一樣採集設備屬性不同的問題?這時,一個同事發揮才能了,監控指標+監控值能夠按XML格式存儲。哇,還能這樣?查詢呢,能夠用for XML這種形式。

因而有了這種結構:No、MgrObjId、Dtime、XMLData

結果驗證,比上面的稍微好點,可是不是太明顯。

數據表分區???

那個時候尚未學會這個技能,看了下網上的文章,好像挺複雜的,時間很少了,不敢嘗試。

中止其餘程序

我知道這個確定是不行的,由於軟件、硬件的架構暫時無法修改。可是我但願驗證是否是這些因素影響的。結果發現,提示確實明顯,可是仍是沒有達到要求。

難道是SQLServer的瓶頸?

沒轍了,難道這就是SQLServer的瓶頸?上網查了下相關的資料,多是IO的瓶頸,尼瑪,還能怎麼辦,要升級服務器,要更換數據庫了嗎,可是,項目方給嗎?

等等,好像還有個東西,索引,對索引!索引的存在會影響插入、更新

去掉索引

是的,去掉索引以後查詢確定慢,可是我必須先驗證去掉索引是否會加快寫入。若是果斷把MgrObjId和Id兩個字段的索引去掉。

運行,奇蹟出現了,每次寫入10w條記錄,在7~9秒內徹底能夠寫入,這樣就達到了系統的要求。

查詢怎麼解決?

一個表一天要4億多的記錄,這是不可能查詢的,在沒有索引的狀況下。怎麼辦!?我又想到了咱們的老辦法,物理分表。是的,原來咱們按天分表,那麼咱們如今按小時分表。那麼24個表,每一個表只需存儲1800w條記錄左右。

而後查詢,一個屬性在一個小時或者幾個小時的歷史記錄。結果是:慢!慢!!慢!!!去掉索引的狀況下查詢1000多萬的記錄根本是不可想象的。還能怎麼辦?

繼續分表,我想到了,咱們還能夠按底層的採集器繼續分表,由於採集設備在不一樣的採集器中是不一樣的,那麼咱們查詢歷史曲線時,只有查單個指標的歷史曲線,那麼這樣就能夠分散在不一樣的表中了。

說幹就幹,結果,經過按10個採集嵌入式並按24小時分表,天天生成240張表(歷史表名相似這樣:His_001_2014112615),終於把一天寫入4億多條記錄並支持簡單的查詢這個問題給解決掉了!!!

查詢優化

在上述問題解決以後,這個項目的難點已經解決了一半,項目監管也很差意思過來找茬,不知道是出於什麼樣的戰術安排吧。

過了很長一段時間,到如今快年末了,問題又來了,就是要拖死你讓你在年末不能驗收其餘項目。

此次要求是這樣的:由於上述是模擬10w個監控指標,而如今實際上線了,卻只有5w個左右的設備。那麼這個明顯是不能達到標書要求的,不能驗收。那麼怎麼辦呢?這些聰明的人就想,既然監控指標減半,那麼咱們把時間也減半,不就達到了嗎:就是說按如今5w的設備,那你要10s以內入庫存儲。我勒個去啊,按你這個邏輯,咱們若是隻有500個監控指標,豈不是要在0.1秒內入庫?你不考慮下那些受監控設備的感想嗎?

可是別人要玩你,你能怎麼辦?接招唄。結果把時間降到10秒以後,問題來了,你們仔細分析上面邏輯能夠知道,分表是按採集器分的,如今採集器減小,可是數量增長了,發生什麼事情呢,寫入能夠支持,可是,每張表的記錄接近了400w,有些採集設備監控指標多的,要接近600w,怎麼破?

因而技術相關人員開會討論相關的舉措。

在不加索引的狀況下怎麼優化查詢?

有同事提出了,where子句的順序,會影響查詢的結果,由於按你刷選以後的結果再處理,能夠先刷選出一部分數據,而後繼續進行下一個條件的過濾。聽起來好像頗有道理,可是SQLServer查詢分析器不會自動優化嗎?原諒我是個小白,我也是感受而已,感受應該跟VS的編譯器同樣,應該會自動優化吧。

具體怎樣,仍是要用事實來講話:

結果同事修改了客戶端以後,測試反饋,有較大的改善。我查看了代碼:

是如何在SQLServer中處理天天四億三千萬記錄的

 

難道真的有這麼大的影響?等等,是否是忘記清空緩存,形成了假象?

因而讓同事執行下述語句以便得出更多的信息:

--優化以前
DBCC FREEPROCCACHE
DBCC DROPCLEANBUFFERS
SET STATISTICS IO ON
select Dtime,Value from dbo.his20140825 WHERE Dtime>='' AND Dtime<='' AND MgrObjId='' AND Id=''
SET STATISTICS IO OFF
--優化以後
DBCC FREEPROCCACHE
DBCC DROPCLEANBUFFERS
SET STATISTICS IO ON
select Dtime,Value from dbo.his20140825 WHERE MgrObjId='' AND Id='' AND Dtime>='' AND Dtime<=''
SET STATISTICS IO OFF

結果以下:

是如何在SQLServer中處理天天四億三千萬記錄的

 

優化以前反而更好了?

仔細查看IO數據,發現,預讀是同樣的,就是說咱們要查詢的數據記錄都是一致的,物理讀、表掃描也是一直的。而邏輯讀取稍有區別,應該是緩存命中數致使的。也就是說,在不創建索引的狀況下,where子句的條件順序,對查詢結果優化做用不明顯

那麼,就只能經過索引的辦法了。

創建索引的嘗試

創建索引不是簡單的事情,是須要了解一些基本的知識的,在這個過程當中,我走了很多彎路,最終才把索引創建起來。

下面的實驗基於如下記錄總數作的驗證:

是如何在SQLServer中處理天天四億三千萬記錄的

 

按單個字段創建索引

這個想法,主要是受我創建數據結構影響的,我內存中的數據結構爲:Dictionary<MgrObjId,Dictionary<Id,Property>>。我覺得先創建MgrObjId的索引,再創建Id的索引,SQLServer查詢時,就會更快。

是如何在SQLServer中處理天天四億三千萬記錄的

 

先按MgrObjId創建索引,索引大小爲550M,耗時5分25秒。結果,如上圖的預估計劃同樣,根本沒有起做用,反而更慢了。

按多個條件創建索引

OK,既然上面的不行,那麼咱們按多個條件創建索引又如何?CREATE NONCLUSTERED INDEX Idx_His20141008 ON dbo.his20141008(MgrObjId,Id,Dtime)

結果,查詢速度確實提升了一倍:

是如何在SQLServer中處理天天四億三千萬記錄的

 

等等,難道這就是索引的好處?花費7分25秒,用1.1G的空間換取來的就是這些?確定是有什麼地方不對了,因而開始翻查資料,查看一些相關書籍,最終,有了較大的進展。

正確的創建索引

首先,咱們須要明白幾個索引的要點:

  • 索引以後,按索引字段重複最少的來排序,會達到最優的效果。以咱們的表來講,若是創建了No的彙集索引,把No放在where子句的第一位是最佳的,其次是Id,而後是MgrObjId,最後是時間,時間索引若是表是一個小時的,最好不要用
  • where子句的順序決定了查詢分析器是否使用索引來查詢。好比創建了MgrObjId和Id的索引,那麼where MgrObjId='' and Id='' and Dtime=''就會採用索引查找,而where Dtime='' and MgrObjId='' and Id=''則不必定會採用索引查找。
  • 把非索引列的結果列放在包含列中。由於咱們條件是MgrObjId和Id以及Dtime,所以返回結果中只需包含Dtime和Value便可,所以把Dtime和Value放在包含列中,返回的索引結果就有這個值,不用再查物理表,能夠達到最優的速度。

跟上述幾點原則,咱們創建如下的索引:CREATE NONCLUSTERED INDEX Idx_His20141008 ON dbo.his20141008(MgrObjId,Id) INCLUDE(Value,Dtime)

耗費時間爲:6分多鐘,索引大小爲903M。

咱們看看預估計劃:

是如何在SQLServer中處理天天四億三千萬記錄的

 

能夠看到,這裏徹底使用了索引,沒有額外的消耗。而實際執行的結果,1秒都不到,居然不用一秒就在1100w的記錄中把結果篩選了出來!!帥呆了!!

怎麼應用索引?

既然寫入完成了、讀取完成了,怎麼結合呢?咱們能夠把一個小時以前的數據創建索引,當前一個小時的數據就不創建索引。也就是,不要再建立表的時候創建索引!!

還能怎麼優化

能夠嘗試讀寫分離,寫兩個庫,一個是實時庫,一個是隻讀庫。一個小時內的數據查詢實時庫,一個小時以前的數據查詢只讀庫;只讀庫定時存儲,而後創建索引;超過一個星期的數據,進行分析處理再存儲。這樣,不管查詢什麼時間段的數據,都可以正確處理了——一個小時以內的查詢實時庫,一個小時到一個星期內的查詢只讀庫,一個星期以前的查詢報表庫。

若是不須要物理分表,則在只讀庫中,定時重建索引便可。

總結

如何在SQLServer中處理億萬級別的數據(歷史數據),能夠按如下方面進行:

  • 去掉表的全部索引
  • 用SqlBulkCopy進行插入
  • 分表或者分區,減小每一個表的數據總量
  • 在某個表徹底寫完以後再創建索引
  • 正確的指定索引字段
  • 把須要用到的字段放到包含索引中(在返回的索引中就包含了一切)
  • 查詢的時候只返回所需的字段
  • 若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java高級交流:787707172,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。
相關文章
相關標籤/搜索