起因
话说2009年12月4日的下午,博客园团队的一位在数据库服务器中修改了一个设置:
将"Use query governor to prevent long-running queries"设置为500,500代表的是查询成本,不是查询执行时间,如果SQL Server认为查询成本超过500,就会返回超时错误。
之前我们经常使用这个设置,这样可以避免一些执行时间过长的查询影响整体数据库的性能。但这不是解决问题的办法,只是让局部问题不去影响整体,真正解决问题还是要优化执行时间长的查询。
而就在修改过这个设置之后某个时候,担任缓冲重任的memcached服务突然偷偷地罢工了,血案就这样开始了...
发现问题
有些用户在博客中发评论时出现了超时错误,有人在闪存上进行反映,我们在闪存看到并进行测试,也遇到了超时问题,当时我们以为是同时连接过多造成服务器压力大造成的,从日志中发现有过于频繁访问的IP,屏蔽了该IP之后,可以正常发表评论了,我们以为问题已经解决了,从最新评论看,大家也能发表评论。
实际上问题依然存在,会在特定的条件下发生,只不过我们的测试时没有遇到这样的条件。如果没有设置"Use query governor to prevent long-running queries",这个问题更难发现,因为这时发评论只会速度变慢,不会出现超时,大家不会去反馈这个速度慢的问题。后来,超时出现越来越频繁,有用户在闪存上进行反馈,我们才知道了这个问题。
采取措施
我们怎么也没想到memcached服务已经罢工了,之前memcached服务还从来没罢工过,于是把解决问题的焦点放在评论功能相关的数据库优化。在SQL Server 2005管理工具中执行添加评论的存储过程,执行速度也很慢,查看执行计划,发现成本在聚集索引的插入上,于是我们以为是评论表的聚集索引设置有问题(看来不能仅从执行计划去分析问题)。评论表的聚集索引本来是建立在自增ID上的(博客文章表的聚集索引建立在发表时间上),因为评论表的查询主要就是根据ParentID进行查询(查询结果根据ID进行排序),之外最多的操作就是插入记录。既然成本在聚集索引的插入上,我们就取消了聚集索引,这样可以避免在插入记录时的聚集索引插入。这样操作之后,发评论的速度提升了,但速度还是不理想,还是会出现超时...
柳暗花明
后来,无意间发现memcached服务停掉了,立即想到这才是罪魁祸首,不是聚集索引的问题,为什么会这么想呢,看一下添加评论的存储过程就知道了:
BEGIN TRANSACTION
UPDATE blog_Content
SET FeedBackCount = FeedBackCount+1,LastCommentTime = GETDATE()
WHERE [ID] = @ParentID
INSERT INTO blog_Comment([Text],Author)VALUES(@Text,@Author)
Select @ID = @@Identity
COMMIT TRANSACTION
之前发表评论的性能就不怎么理想,瓶颈就在对blog_Content表FeedBackCount字段的更新上,因为blog_Content是查询最频繁的表,我们几乎对所有查询都使用了WITH(NOLOCK),但是发表评论的性能还是存在问题。memcached服务罢工,直接的结果就是对blog_Content的查询大量增加,在发表评论时的Update操作成本就很高,从而造成超时。
启动memcached服务,问题立即解决。
解决问题
但是问题并没有真正解决,真正的问题是在添加评论时对blog_Content表FeedBackCount字段的更新。
针对这个问题,我采用一种解决方法:在插入评论时不进行Update操作,而是将评论所属的文章ID插入另外一张表中,表结构如下:
CREATE TABLE [dbo].[blog_Comment_CountLog](
[EntryID] [int] NOT NULL,
[FeedbackCount] [int] NOT NULL
)
插入评论时,进行下面的操作:
INSERT INTO blog_Comment_CountLog(EntryID,FeedbackCount)VALUES(@ParentID,1)
显然,这个插入操作速度很快。
删除评论时,进行下面的操作:
INSERT INTO blog_Comment_CountLog(EntryID,FeedbackCount)VALUES(@ParentID,-1)
那么怎么更新文章的评论数呢?
然后通过SQL Server的任务计划定时执行存储过程进行更新:
SET XACT_ABORT ON
BEGIN TRANSACTION
UPDATE [blog_Content]
SET [FeedBackCount] = [FeedBackCount]+CommentCount
FROM [CNBlogs].[dbo].[blog_Content] A
INNER JOIN
(SELECT EntryID,SUM(FeedbackCount) AS CommentCount FROM blog_Comment_CountLog GROUP BY EntryID ) AS B
ON A.ID=B.EntryID
DELETE blog_Comment_CountLog
COMMIT TRANSACTION
这样还会带来批量更新的好处,在这个任务计划的间隔时间内添加评论,如果属于同一篇文章,只要执行一次更新操作。
采用这个方法后,发表评论的性能有了明显提升,现在大家发评论可以看到评论提交的执行时间。
小结
在处理问题时,不要着急,要全面分析,把可能引起问题的因素尽量多地考虑到,在动手解决问题之前,多花些时间考虑从何处下手。
在开发中,异步是一个提高性能的有效方法。
在我们解决各种问题的过程中,从网上获得了很多启发,如果有人遇到过类似问题并分享了自己的处理经验,会节省我们很多时间。
大家遇到问题时多数会先Google一下吧,如果大家都只想着Google一下,而不去分享,这个生态链就很难维系。别人的分享帮助了你,你的分享帮助了别人,在这种互相帮助中,大家都会进步。
在刚解决一个问题的时候,是分享的最佳时期,如果此时不分享,也许就永远不会分享,分享的不仅是解决问题的方法,更是解决问题时的那种兴奋。这种兴奋是程序人生的乐趣所在!