是如何在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大型互联网技术的视频免费分享给你们。
相关文章
相关标签/搜索