请看:我是如何在SQLServer中处理每天四亿三千万记录的
2019獨(dú)角獸企業(yè)重金招聘Python工程師標(biāo)準(zhǔn)>>>
? ???小中午,無意間讀到這篇博客,覺得,文中作者在處理海量數(shù)據(jù)寫入方面,所做的一些嘗試,或許可以給同道朋友提供一些幫助,與君共勉。?? ?--題記
?【正文部分】
? ? 首先聲明,我只是個程序員,不是專業(yè)的DBA,以下這篇文章是從一個問題的解決過程去寫的,而不是一開始就給大家一個正確的結(jié)果,如果文中有不對的地方,請各位數(shù)據(jù)庫大牛給予指正,以便我能夠更好的處理此次業(yè)務(wù)。
項目背景
? ? ?這是給某數(shù)據(jù)中心做的一個項目,項目難度之大令人發(fā)指,這個項目真正的讓我感覺到了,商場如戰(zhàn)場,而我只是其中的一個小兵,太多的戰(zhàn)術(shù),太多的高層之間的較量,太多的內(nèi)幕了。具體這個項目的情況,我有空再寫相關(guān)的博文出來。
這個項目是要求做環(huán)境監(jiān)控,我們暫且把受監(jiān)控的設(shè)備稱為采集設(shè)備,采集設(shè)備的屬性稱為監(jiān)控指標(biāo)。項目要求:系統(tǒng)支持不少于10w個監(jiān)控指標(biāo),每個監(jiān)控指標(biāo)的數(shù)據(jù)更新不大于20秒,存儲延遲不超過120秒。那么,我們可以通過簡單的計算得出較理想的狀態(tài)——要存儲的數(shù)據(jù)為:每分鐘30w,每個小時1800w,也就是每天4億3千兩百萬。而實際,數(shù)據(jù)量會比這個大5%左右。(實際上大部分是信息垃圾,可以通過數(shù)據(jù)壓縮進(jìn)行處理的,但是別人就是要搞你,能咋辦)
上面是項目要求的指標(biāo),我想很多有不少大數(shù)據(jù)處理經(jīng)驗的同學(xué)都會呲之以鼻,就這么點?嗯,我也看了很多大數(shù)據(jù)處理的東西,但是之前沒處理過,看別人是頭頭是道,什么分布式,什么讀寫分離,看起來確實很容易解決。但是,問題沒這么簡單,上面我說了,這是一個非常惡劣的項目,是一個行業(yè)惡性競爭典型的項目。
我們的系統(tǒng)架構(gòu)如下,可以看到,其中數(shù)據(jù)庫壓力非常之大,尤其在LevelA節(jié)點:
CPU:英特爾? 至強(qiáng)? 處理器 E5-2609 (4核, 2.40GHz, 10MB, 6.4 GT/s)
內(nèi)存:4GB (2x2GB) DDR3 RDIMM Memory, 1333MHz,ECC
硬盤:500GB 7200 RPM 3.5'' SATA3 硬盤,Raid5.
采用的是SQLServer2012標(biāo)準(zhǔn)版,HP提供的正版軟件,缺少很多企業(yè)版的NB功能。
寫入瓶頸
首先遇到的第一個攔路虎就是,我們發(fā)現(xiàn)現(xiàn)有的程序下,SQLServer根本處理不了這么多的數(shù)據(jù)量,具體情況是怎樣的呢?
我們的存儲結(jié)構(gòu)
一般為了存儲大量的歷史數(shù)據(jù),我們都會進(jìn)行一個物理的分表,否則每天上百萬條的記錄,一年下來就是幾億條。因此,原來我們的表結(jié)構(gòu)是這樣的:
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?KEYCLUSTERED ( [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作為唯一的標(biāo)識、采集設(shè)備Id(Guid)、監(jiān)控指標(biāo)Id(varchar(50))、記錄時間、記錄值。并以采集設(shè)備Id和監(jiān)控指標(biāo)Id作為索引,以便快速查找。
批量寫入
寫入當(dāng)時是用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;}存在什么問題?
上面的架構(gòu),在每天4千萬的數(shù)據(jù)都是OK的。但是,調(diào)整為上述背景下的配置時,集中監(jiān)控程序就內(nèi)存溢出了,分析得知,接收的太多數(shù)據(jù),放在了內(nèi)存中,但是沒有來得及寫入到數(shù)據(jù)庫中,最終導(dǎo)致了生成的數(shù)據(jù)大于消費(fèi)的數(shù)據(jù),導(dǎo)致內(nèi)存溢出,程序無法工作。
瓶頸到底在哪里?
是因為RAID磁盤的問題?是數(shù)據(jù)結(jié)構(gòu)的問題?是硬件的問題?是SQLServer版本的問題?是沒有分區(qū)表的問題?還是程序的問題?
當(dāng)時時間只有一個星期,一個星期搞不好,項目監(jiān)管就要我們滾蛋了,于是,有了連續(xù)工作48小時的壯舉,有了到處打電話求人的抓雞……
但是,這個時候需要的是冷靜,再冷靜……SQLServer版本?硬件?目前都不大可能換的。RAID磁盤陣列,應(yīng)該不是。那么到底是什么,真TM的冷靜不下來。
大家可能體會不到現(xiàn)場那種緊張的氣氛,其實過了這么久,我自己也都很難再回到那種情境。但是可以這么說,或許我們現(xiàn)在有了各種方法,或者處于局外人我們有更多思考,但是當(dāng)一個項目壓迫你快到放棄的時候,你那時的想法、考慮在現(xiàn)場環(huán)境因素的制約下,都可能出現(xiàn)重大的偏差。有可能讓你快速的思考,也有可能思維停滯。有些同事在這種高壓的環(huán)境下,甚至出現(xiàn)了更多的低級錯誤,思維已經(jīng)完全亂了,效率更低了……36小時沒有合眼,或者只在工地上(下雨天到處都是泥巴,干了的話到時都是泥灰)瞇兩三個小時,然后繼續(xù)干,連續(xù)這么一個星期!或者還要繼續(xù)!
很多人給了很多想法,但是好像有用,又好像沒用。等等,為什么是“好像有用,又好像沒用”?我隱隱約約中,好像抓住了一絲方向,到底是什么?對了,驗證,我們現(xiàn)在是跑在現(xiàn)場環(huán)境下,之前沒有問題,不代表現(xiàn)在的壓力下沒有問題,要在一個大型系統(tǒng)中分析這么個小功能,影響太大了,我們應(yīng)該分解它。是的,是“單元測試”,就是單個方法的測試,我們需要驗證每個函數(shù),每個獨(dú)立的步驟到底耗時在哪里?
逐步測試驗證系統(tǒng)瓶頸
修改BulkCopy的參數(shù)
首先,我想到的是,修噶BulkCopy的各項參數(shù),BulkCopyTimeout、BatchSize,不斷的測試調(diào)整,結(jié)果總是在某個范圍波動,實際并沒有影響?;蛟S會影響一些CPU計數(shù),但是遠(yuǎn)遠(yuǎn)沒有達(dá)到我的期望,寫入的速度還是在5秒1w~2w波動,遠(yuǎn)遠(yuǎn)達(dá)不到要求20秒內(nèi)要寫20w的記錄。
按采集設(shè)備存儲
是的,上述結(jié)構(gòu)按每個指標(biāo)每個值為一條記錄,是不是太多的浪費(fèi)?那么按采集設(shè)備+采集時間作為一條記錄是否可行?問題是,怎么解決不同采集設(shè)備屬性不一樣的問題?這時,一個同事發(fā)揮才能了,監(jiān)控指標(biāo)+監(jiān)控值可以按XML格式存儲。哇,還能這樣?查詢呢,可以用for XML這種形式。
于是有了這種結(jié)構(gòu):No、MgrObjId、Dtime、XMLData
結(jié)果驗證,比上面的稍微好點,但是不是太明顯。
數(shù)據(jù)表分區(qū)???
那個時候還沒有學(xué)會這個技能,看了下網(wǎng)上的文章,好像挺復(fù)雜的,時間不多了,不敢嘗試。
停止其他程序
我知道這個肯定是不行的,因為軟件、硬件的架構(gòu)暫時沒法修改。但是我希望驗證是不是這些因素影響的。結(jié)果發(fā)現(xiàn),提示確實明顯,但是還是沒有達(dá)到要求。
難道是SQLServer的瓶頸?
沒轍了,難道這就是SQLServer的瓶頸?上網(wǎng)查了下相關(guān)的資料,可能是IO的瓶頸,尼瑪,還能怎么辦,要升級服務(wù)器,要更換數(shù)據(jù)庫了嗎,但是,項目方給嗎?
等等,好像還有個東西,索引,對索引!索引的存在會影響插入、更新
去掉索引
是的,去掉索引之后查詢肯定慢,但是我必須先驗證去掉索引是否會加快寫入。如果果斷把MgrObjId和Id兩個字段的索引去掉。
運(yùn)行,奇跡出現(xiàn)了,每次寫入10w條記錄,在7~9秒內(nèi)完全可以寫入,這樣就達(dá)到了系統(tǒng)的要求。
查詢怎么解決?
一個表一天要4億多的記錄,這是不可能查詢的,在沒有索引的情況下。怎么辦!?我又想到了我們的老辦法,物理分表。是的,原來我們按天分表,那么我們現(xiàn)在按小時分表。那么24個表,每個表只需存儲1800w條記錄左右。
然后查詢,一個屬性在一個小時或者幾個小時的歷史記錄。結(jié)果是:慢!慢!!慢!!!去掉索引的情況下查詢1000多萬的記錄根本是不可想象的。還能怎么辦?
繼續(xù)分表,我想到了,我們還可以按底層的采集器繼續(xù)分表,因為采集設(shè)備在不同的采集器中是不同的,那么我們查詢歷史曲線時,只有查單個指標(biāo)的歷史曲線,那么這樣就可以分散在不同的表中了。
說干就干,結(jié)果,通過按10個采集嵌入式并按24小時分表,每天生成240張表(歷史表名類似這樣:His_001_2014112615),終于把一天寫入4億多條記錄并支持簡單的查詢這個問題給解決掉了!!!
查詢優(yōu)化
在上述問題解決之后,這個項目的難點已經(jīng)解決了一半,項目監(jiān)管也不好意思過來找茬,不知道是出于什么樣的戰(zhàn)術(shù)安排吧。
過了很長一段時間,到現(xiàn)在快年底了,問題又來了,就是要拖死你讓你在年底不能驗收其他項目。
這次要求是這樣的:因為上述是模擬10w個監(jiān)控指標(biāo),而現(xiàn)在實際上線了,卻只有5w個左右的設(shè)備。那么這個明顯是不能達(dá)到標(biāo)書要求的,不能驗收。那么怎么辦呢?這些聰明的人就想,既然監(jiān)控指標(biāo)減半,那么我們把時間也減半,不就達(dá)到了嗎:就是說按現(xiàn)在5w的設(shè)備,那你要10s之內(nèi)入庫存儲。我勒個去啊,按你這個邏輯,我們?nèi)绻挥?00個監(jiān)控指標(biāo),豈不是要在0.1秒內(nèi)入庫?你不考慮下那些受監(jiān)控設(shè)備的感想嗎?
但是別人要玩你,你能怎么辦?接招唄。結(jié)果把時間降到10秒之后,問題來了,大家仔細(xì)分析上面邏輯可以知道,分表是按采集器分的,現(xiàn)在采集器減少,但是數(shù)量增加了,發(fā)生什么事情呢,寫入可以支持,但是,每張表的記錄接近了400w,有些采集設(shè)備監(jiān)控指標(biāo)多的,要接近600w,怎么破?
于是技術(shù)相關(guān)人員開會討論相關(guān)的舉措。
在不加索引的情況下怎么優(yōu)化查詢?
有同事提出了,where子句的順序,會影響查詢的結(jié)果,因為按你刷選之后的結(jié)果再處理,可以先刷選出一部分?jǐn)?shù)據(jù),然后繼續(xù)進(jìn)行下一個條件的過濾。聽起來好像很有道理,但是SQLServer查詢分析器不會自動優(yōu)化嗎?原諒我是個小白,我也是感覺而已,感覺應(yīng)該跟VS的編譯器一樣,應(yīng)該會自動優(yōu)化吧。
具體怎樣,還是要用事實來說話:
結(jié)果同事修改了客戶端之后,測試反饋,有較大的改善。我查看了代碼:
難道真的有這么大的影響?等等,是不是忘記清空緩存,造成了假象?
于是讓同事執(zhí)行下述語句以便得出更多的信息:
結(jié)果如下:
優(yōu)化之前反而更好了?
仔細(xì)查看IO數(shù)據(jù),發(fā)現(xiàn),預(yù)讀是一樣的,就是說我們要查詢的數(shù)據(jù)記錄都是一致的,物理讀、表掃描也是一直的。而邏輯讀取稍有區(qū)別,應(yīng)該是緩存命中數(shù)導(dǎo)致的。也就是說,在不建立索引的情況下,where子句的條件順序,對查詢結(jié)果優(yōu)化作用不明顯。
那么,就只能通過索引的辦法了。
建立索引的嘗試
建立索引不是簡單的事情,是需要了解一些基本的知識的,在這個過程中,我走了不少彎路,最終才把索引建立起來。
下面的實驗基于以下記錄總數(shù)做的驗證:
按單個字段建立索引
這個想法,主要是受我建立數(shù)據(jù)結(jié)構(gòu)影響的,我內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)為:Dictionary<MgrObjId,Dictionary<Id,Property>>。我以為先建立MgrObjId的索引,再建立Id的索引,SQLServer查詢時,就會更快。
先按MgrObjId建立索引,索引大小為550M,耗時5分25秒。結(jié)果,如上圖的預(yù)估計劃一樣,根本沒有起作用,反而更慢了。
按多個條件建立索引
OK,既然上面的不行,那么我們按多個條件建立索引又如何?CREATE NONCLUSTERED INDEX Idx_His20141008 ON dbo.his20141008(MgrObjId,Id,Dtime)
結(jié)果,查詢速度確實提高了一倍:
等等,難道這就是索引的好處?花費(fèi)7分25秒,用1.1G的空間換取來的就是這些?肯定是有什么地方不對了,于是開始翻查資料,查看一些相關(guān)書籍,最終,有了較大的進(jìn)展。
正確的建立索引
首先,我們需要明白幾個索引的要點:
- 索引之后,按索引字段重復(fù)最少的來排序,會達(dá)到最優(yōu)的效果。以我們的表來說,如果建立了No的聚集索引,把No放在where子句的第一位是最佳的,其次是Id,然后是MgrObjId,最后是時間,時間索引如果表是一個小時的,最好不要用
- where子句的順序決定了查詢分析器是否使用索引來查詢。比如建立了MgrObjId和Id的索引,那么where MgrObjId='' and Id='' and Dtime=''就會采用索引查找,而where Dtime='' and MgrObjId='' and Id=''則不一定會采用索引查找。
- 把非索引列的結(jié)果列放在包含列中。因為我們條件是MgrObjId和Id以及Dtime,因此返回結(jié)果中只需包含Dtime和Value即可,因此把Dtime和Value放在包含列中,返回的索引結(jié)果就有這個值,不用再查物理表,可以達(dá)到最優(yōu)的速度。
跟上述幾點原則,我們建立以下的索引:CREATE NONCLUSTERED INDEX Idx_His20141008 ON dbo.his20141008(MgrObjId,Id) INCLUDE(Value,Dtime)
耗費(fèi)時間為:6分多鐘,索引大小為903M。
我們看看預(yù)估計劃:
可以看到,這里完全使用了索引,沒有額外的消耗。而實際執(zhí)行的結(jié)果,1秒都不到,竟然不用一秒就在1100w的記錄中把結(jié)果篩選了出來!!帥呆了!!
怎么應(yīng)用索引?
既然寫入完成了、讀取完成了,怎么結(jié)合呢?我們可以把一個小時之前的數(shù)據(jù)建立索引,當(dāng)前一個小時的數(shù)據(jù)就不建立索引。也就是,不要再創(chuàng)建表的時候建立索引!!
還能怎么優(yōu)化
可以嘗試讀寫分離,寫兩個庫,一個是實時庫,一個是只讀庫。一個小時內(nèi)的數(shù)據(jù)查詢實時庫,一個小時之前的數(shù)據(jù)查詢只讀庫;只讀庫定時存儲,然后建立索引;超過一個星期的數(shù)據(jù),進(jìn)行分析處理再存儲。這樣,無論查詢什么時間段的數(shù)據(jù),都能夠正確處理了——一個小時之內(nèi)的查詢實時庫,一個小時到一個星期內(nèi)的查詢只讀庫,一個星期之前的查詢報表庫。
如果不需要物理分表,則在只讀庫中,定時重建索引即可。
總結(jié)
如何在SQLServer中處理億萬級別的數(shù)據(jù)(歷史數(shù)據(jù)),可以按以下方面進(jìn)行:
- 去掉表的所有索引
- 用SqlBulkCopy進(jìn)行插入
- 分表或者分區(qū),減少每個表的數(shù)據(jù)總量
- 在某個表完全寫完之后再建立索引
- 正確的指定索引字段
- 把需要用到的字段放到包含索引中(在返回的索引中就包含了一切)
- 查詢的時候只返回所需的字段
轉(zhuǎn)載于:https://my.oschina.net/lichaoqiang/blog/757406
總結(jié)
以上是生活随笔為你收集整理的请看:我是如何在SQLServer中处理每天四亿三千万记录的的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: shell常见的文件属性检查
- 下一篇: centos系统rpm命令