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(@"^\n+|\n+\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)中。