一次寻找Bug的“痛苦”旅程

最近在做一个数据量很大的程序,这个程序的功能就是采集互联网上的链接,供用户查询,专业俗语叫“反链查询”或“外链查询”。

比如http://www.cnblogs.com页面内有友情链接这么多

我要做的就是把这些链接保存到数据库里,其对应的域名就是http://www.cnblogs.com
当用户查询的时候,输入chinaz.com,就会列出www.cnblogs.com
Demo地址:http://outlink.chinaz.com

中国互联网顶级域名的数量可能是200多万,加上常用二级、三级域名,数量可能在千万,如果平均每个域名上有10个链接的话,差不多会有上亿的数据,并且还要定期更新。数据库设计为两个数据库,OutUrls和OutLinks,OutUrls用来保存域名,及其上面对应的链接,链接的保存采用LinkId+表后缀,表后缀是按照域名的第一个字母。考虑到每个表的数据量不能太大,采用了水平分表,根据域名的第一个字母,相同字母的归到同一个数据表。

但当数据库文件达到10G,数据的select,insert,update就比较慢,性能监视器中显示Avg.Disk Queue Length的平均值达到20以上,程序日志记录里很多
Timeout expired.  The timeout period elapsed prior to completion of the operation or the server is not responding.   at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection)
select一条记录都要1分钟。

再说用户查询的性能,查询是通过查询视图,来获取数据,考虑到查询的性能,表的设计,字段出现了冗余,结果是每个域名第一次查询的时候很慢,像查询qq.com差不多要4s,第二次就比较快1s。这时数据量并不大,我想如果再大点的话,会更慢。
以前就听说过lucene.net,全文的搜索引擎,其实一开始并不想把程序做得很复杂,能简单点,就简单的。现在这种情况下,还是试一下lucene.net吧。所以,就开始使用lucene.net了,效果果然很棒,几乎每个查询都能在1s以内。
由于有些数据要先在数据库里更新,然后再更新到Lucene索引,所以,现在的做法是数据库保留,用户搜索是查询LuceneIndex里的数据,只要每天定时更新LuceneIndex就行了。

再回到Avg.Disk Queue Length持续很大这个问题。
由于服务器硬盘使用的是RAID,其实只有两个硬盘,同事建议说,可以把OutLinks数据库放到另外一个盘,我就按照他的建议做了,感觉并没有快多少。程序跑了一段时间,文件日志里记录,很多错误,形如:不能在具有唯一索引 'IX_Link1Q_Domain' 的对象 'dbo.Link1Q' 中插入重复键的行。

Link1Q表里有一个unique 索引,在insert之前,我已经判断是否存在,但是还是会报这个错误。
然后 DBCC CHECKDB (OutLinks) 爆出了一大堆的错误:

 1 消息 8978,级别 16,状态 1,第 1 行
 2 表错误: 对象 ID 1749581271,索引 ID 1,分区 ID 72057594148814848,分配单元 ID 72057594138525696 (类型为In-row data)。页 (1:54760) 缺少上一页 (1:563545) 对它的引用。可能是因为链链接有问题。
 3 消息 8935,级别 16,状态 1,第 1 行
 4 表错误: 对象 ID 1749581271,索引 ID 1,分区 ID 72057594148814848,分配单元 ID 72057594138525696 (类型为In-row data)。页 (1:60960) 上的上一页链接 (1:433433) 与父代 (1:50171) 槽 29 所预期的此页的上一页(1:655512) 不匹配。
 5 消息 8936,级别 16,状态 1,第 1 行
 6 表错误: 对象 ID 1749581271,索引 ID 1,分区 ID 72057594148814848,分配单元 ID 72057594138525696 (类型为In-row data)。B 树链链接不匹配。(1:655512)->next = (1:60960),但 (1:60960)->Prev = (1:433433)。
 7 消息 2533,级别 16,状态 1,第 1 行
 8 表错误: 看不到分配给对象 ID 1749581271,索引 ID 1,分区 ID 72057594148814848,分配单元 ID72057594138525696 (类型为 In-row data)的页 (1:563544)。该页可能无效,或者页头中可能包含错误的分配单元 ID。
 9 消息 2533,级别 16,状态 1,第 1 行
10 表错误: 看不到分配给对象 ID 1749581271,索引 ID 1,分区 ID 72057594148814848,分配单元 ID72057594138525696 (类型为 In-row data)的页 (1:563545)。该页可能无效,或者页头中可能包含错误的分配单元 ID。
11 消息 8976,级别 16,状态 1,第 1 行
12 表错误: 对象 ID 1749581271,索引 ID 1,分区 ID 72057594148814848,分配单元 ID 72057594138525696 (类型为In-row data)。在扫描过程中未发现页 (1:563545),但该页的父级 (1:489984) 和上一页 (1:113381) 都引用了它。请检查以前的错误消息。
13 消息 8978,级别 16,状态 1,第 1 行
14 表错误: 对象 ID 1749581271,索引 ID 1,分区 ID 72057594148814848,分配单元 ID 72057594138525696 (类型为In-row data)。页 (1:564660) 缺少上一页 (1:563544) 对它的引用。可能是因为链链接有问题。

CHECKDB 在表 'Link1P' (CHECKDB 在表 'Link1P' (对象 ID 1749581271)中发现 0 个分配错误和 8 个一致性错误。

为了这个问题,确实是寝食难安啊,本来做这个东西已经花费了很多时间,内心已有些焦急,上面的领导时不时的又来问你进度,面对这个以前从没碰到过的问题,就像一个人第一次孤零零地行走于沙漠,无助与凄凉。

不断的修复数据库:

use OutLinks
declare @dbname varchar(255)
set @dbname='OutLinks'
exec sp_dboption @dbname,'single user','true'
dbcc checkdb(dbname,REPAIR_ALLOW_DATA_LOSS)
dbcc checkdb(dbname,REPAIR_REBUILD)
exec sp_dboption @dbname,'single user','false'

只要程序运行了一会,还是会出现错误。
也怀疑是硬盘的问题,用硬盘检测工具,快速检测,没有发现问题,如果不那么急躁的话,舍得那一点时间的话,可能就找到问题了。

以前的经验告诉我,遇到事情总是先找自身的原因,并且经常也是自己的原因,如果你怀疑其他外界条件的话,好像你不能够搞定它,而把它归为外界因素。所以,有时候,走自己的路,让别人去说吧,不失为一种正确的选择,如果你坚信自己的怀疑是正确的话,就去实践吧。 

由于程序同时也在优化中,以为是程序的问题,程序是分为 服务器端和客服端,都是Console Application,采用WCF进行通信,工作过程是这样的。
 一开始考虑到并发问题,以为是多个线程同时更新一张表造成的,为了解决这个问题,服务器端改用采用队列的方式,来更新数据。服务器端先从队列里取出一批待处理的Url,然后把相同表里的数据放到同一个集合,然后多线程的处理这批集合,一个线程负责处理一个集合,这样就可以控制一张表,同时只会有一个线程在操作。这样更改之后,程序运行一段时间之后,DBCC 命令还是会出现一堆的错误,希望又一次破灭,所剩的只是绝望。

此时,硬盘有问题的想法,再次闪过我的脑海,我决定把数据库放回到原来的位置。程序跑了2天之后,dbcc checkdb没有发现错误,欣喜若狂啊,原来不是程序的问题。

虽然这次的错误,花费了很多时间,寻找其中的bug,痛苦,纠结,失望,绝望,无奈。最终还是解决了,也很兴奋,更加自信啦。在解决问题的过程中,程序也不断的得到了优化与改进,也尝试了很多新的方法。因此,还是要感谢bug。

原文地址:https://www.cnblogs.com/lhking/p/2668547.html