3月23日(周日)下午16:30左右,博客园主站负载均衡中的2台Web服务器CPU玩起了爬楼梯的游戏(见上图),一直爬到了接近100%。发现这个状况后,我们立即将这2台阿里云临时磁盘云服务器从负载均衡中摘下来,挂上1台云盘云服务器,恢复了正常。
由于曾经多次遇到过阿里云云服务器CPU问题,现在对阿里云云服务器产生了一种偏见,只要出现CPU问题,就会首先怀疑云服务器的问题。而这次出现问题时,换上云盘云服务器立即恢复正常,我们就坚定地认为临时磁盘云服务器存在某种问题。于是,我们提交了工单,向阿里云客服抱怨这个问题。
。。。
接着突然发生的状况让我们的“坚定”产生了动摇,刚加上去的那台云盘云服务器也出现了CPU跑高的问题。
阿里云云服务器连续出问题的可能性很小,也许是其他原因引起的。这个突发情况让我们冷静下来去回想出问题之前进行过什么操作。
想起来了——
出现问题之前,我们进行过清空OCS实例缓存的操作(注:OCS是阿里云提供的Memcached缓存服务)。
缓存不仅能缓解数据库的压力,而且能缓解CPU的压力。比如有些数据从数据库中读取出来后需要进行一些正则表达式的处理(耗CPU的大户),如果缓存中存在,直接读取就行;如果缓存中不存在,需要先从数据库中读取,接着进行正则处理,然后放入缓存。清空缓存后会引发大量这样的操作,从而给CPU带来压力。
但是以前我们多次在周末访问低峰的时候进行过同样的清空OCS缓存的操作,增加的这点压力对Web服务器的CPU来说是小菜一碟。
为什么这次却有天壤之别?
- 这个周末的访问量的确比之前的周末要高一些,但不致于影响这么大。
- 在CPU跑高时,日志中记录了很多OCS缓存客户端读取数据慢的情况。难道是OCS的问题?是OCS读取缓存慢引发CPU高,还是CPU高引发OCS缓存读取速度慢?分析之后,还是觉得后者的可能性大一些。
- 现在与之前相比,哪些变化可能引发在缓存失效的情况下需要更多的CPU消耗?
想起来了——Markdown!
1月份的时候我们发布了简陋的Markdown功能,现在比以前有了更多Markdown写的博文,而这些博文转换成HTML用了复杂的正则表达式。当访问一篇使用Mardown写的博文时,如果缓存中没有,会从数据库读取原始的Markdown内容,用正则表达式转换成HTML后放入缓存,后续的访问就直接从缓存中读取HMTL内容。当清空OCS缓存后,大量的Markdown内容需要重建缓存,进行大量的复杂的正则表达式处理,这会给CPU带来很大的压力!
这是就是问题的真相?难道是我们自己导演的缓存雪崩?。。。没这么简单!在访问低峰,共16个核的CPU竟然都没有撑住,不可思议!凭我们的经验,这16个核没这么弱不禁风!
继续回想。。。
又想起来了!我们曾经实际在另外一个ASP.NET应用程序中遇到过类似的情况——
在C#中用正则表达式处理大文本时,某种条件会触发CPU高上去,而且会一直高居不下,只有回收应用程序池才能让CPU下去。当时怎么优化正则表达式也没有用,后来没办法,使用磁盘文件进行大量缓存,减少了触发这个问题的几率。
难道.NET在正则表达式处理上隐藏着不为人所知的坑?微软从.NET Framework 4.5开始给正则表达式增加了超时设置(matchTimeout),似乎验证了这一点。
// matchTimeout: // A time-out interval, or System.Text.RegularExpressions.Regex.InfiniteMatchTimeout // to indicate that the method should not time out. public Regex(string pattern, RegexOptions options, TimeSpan matchTimeout);
虽然我们的应用程序已经升级到了.NET Framework 4.5,但是还没有去使用这个特性,现在实际遇到的问题将之呼唤出来。
解决方法一:给Markdow转换所用的所有正则表达式加上超时设置——TimeSpan.FromSeconds(1),如果某个正则表达式处理超过1秒就会引发异常,从而不让任何一只老鼠坏了一碗汤。
示例代码如下:
private static Regex _newlinesLeadingTrailing = new Regex(@"^ +| +z", RegexOptions.Compiled, TimeSpan.FromSeconds(1));
但是,这样一个一个正则表达式进行修改,好麻烦!
于是有了“解决方法一”的改进版:
在Global.asax.cs中Application_Start添加如下的代码:
protected void Application_Start(object sender, EventArgs e) { AppDomain.CurrentDomain.SetData("REGEX_DEFAULT_MATCH_TIMEOUT", TimeSpan.FromSeconds(1)); }
这样就可以全局设置所有正则表达式的默认超时时间。
采用了解决方法一之后,我们又仔细考虑了一下,学得这不是最终解决方案。解决方法一虽然解决了一只老鼠坏一锅汤的问题,但是假如一百只、一千只老鼠接连出现呢?也会给CPU带来压力,这种压力会影响主站对其他请求的响应速度。
更好的解决方法应该是——不管Markdown的正则表达式处理消耗多少CPU,即使把CPU跑爆了,也不要影响主站。所以,将这部分处理分出去,隔离开来,才是最终解决方法。
最终解决方法
将Markdown的正则表达式处理放在独立的站点、独立的服务器,然后在博客程序中需要处理Markdown的时候,将文本内容post给这个独立站点进行处理。
之前在博客程序中是这样处理Markdown的:
if (entry.IsMarkdown) { body = new MarkdownSharp.Markdown().Transform(body); }
现在用了一台单独的云服务器跑ASP.NET MVC程序进行Markdown处理,MVC代码如下:
public class MarkdownController : Controller { [HttpPost] public ActionResult Transform() { using (var reader = new StreamReader(Request.InputStream)) { var bodyText = reader.ReadToEnd(); return Content(new MarkdownSharp.Markdown().Transform(bodyText)); } } }
上面的代码中,为了减少MVC的处理工作,直接从http post body中获取Markdown文本。
然后博客程序中用HttpClient将Markdown文本post给这个独立MVC站点进行处理。示例代码如下:
if (entry.IsMarkdown) { var httpClient = new HttpClient(); var httpContent = new StringContent(body); var response = httpClient.PostAsync("http://markdown.s.cnblogs.com/markdown/transform", httpContent).Result; if (response.StatusCode == System.Net.HttpStatusCode.OK) { body = response.Content.ReadAsStringAsync().Result; } else { body = new MarkdownSharp.Markdown().Transform(body); } }
上面的代码也考虑了一定的容错,假如处理Markdown的站点down掉了,博客程序会暂时辛苦一下,自己进行Markdown的正则处理。
这个最终解决方案已经实际部署到我们的主站(www.cnblogs.com)中。