Web性能优化实践——应用层性能优化
隨著公司項目的進一步推廣,用戶數量的增加,已經面臨著單臺服務器不能負載的問題。
這次的優化由于時間關系主要分兩步走,首先優化應用層代碼以提高單臺服務器的負載和吞吐率。之后再進行分表,引入隊列、MemCached等分布式應用。
項目背景:這是一個在線競賽的項目(http://race.gwy.591up.com),在競賽的時間段內數據庫的寫入壓力很大。
當前問題:1、服務器帶寬壓力。2、數據庫壓力。
下圖是Web服務器CPU使用率報表。
總體上應用層服務器的CPU使用率不高。
下圖是Web服務器帶寬報表。
從這個報表可以看到,每塊競賽帶寬的占用都會出現一個很高的峰值。
我們再看數據庫服務器的帶寬報表。
同樣的,數據庫服務器同樣的在競賽的時間點流量超大,很明顯這種情況是不正常的,查詢肯定是有問題的。
面對這樣的問題,確定了第一期主要做以下的優化。
1、用flash storage做用戶做題斷點記錄。(做題斷點:類似程序斷點,用戶做到第N題時退出做答,下次進入時依然定位到第N題。)這里原來是用數據庫存儲的,但 用戶每做一題都會執行一條UPDATE語句,而數據庫是MySQL的MyISAM引擎,更新操作經常被堵塞。
2、更改系統交卷行為。原來系統在用戶做完提交競賽后,會執行一條UPDATE語句更新用戶提交試卷的時間點。同樣的這個UPDATE也是在同一時間段內執行,和產品經理溝通后,確認在最后一分鐘的時候可以不用再執行這個更新,允許用戶的作答時間有1分鐘的誤差。
3、調整數據庫的更新語句為插入語句,這個優化點因為時間問題,推遲到第二個優化階段再處理。
4、調整應用服務器以支持LVS集群。對當前系統進行分析后,暫時可以不用調整代碼直接部署集群,問題是在多臺服務器內都會存在相同的進程內緩存,這種情況暫時是可以接受的,后期需要改到MemCached集中管理緩存。
5、等待成績頁面同一時間跳轉的壓力問題。在線競賽的提交時間點很集中,用戶做答完題目后,會統一跳轉到一個頁面等待答案(這時后臺的 Windows 服務在進行競賽統計),這里服務器的并發、帶寬壓力都非常大。因此,優化這里不進行跳轉,而是在當前的頁面等待,并且會自動給不同的用戶分配不同的等待時 間,以避免占滿服務器的帶寬。
6、每場完整的公務員考試試卷,題目資源有150K-200K,因為作答和查看解析是在不同的頁面,之前的實現會造成題目的多次加載,嚴重的浪費了 帶寬資源。于是這里優化成Handler輸入靜態資源加載,從服務器加載一次之后,后面所有的地方調用到題目都可以從瀏覽器的本地緩存中加載帶省服務器帶 寬。同時,服務器上只對靜態資源服務器開啟了GZip壓縮,對動態文件進行壓縮會浪費服務器的CPU資源,而只對Handler輸出的題目進行GZip壓 縮,一方面節省了CPU,另一方面把150K-200K的題目資源壓縮到了50K左右。
7、數據庫性能優化。調整了代碼中查詢的各個條件的位置,使查詢語句能夠更多的使用到索引。同時把原來每次一條的插入操作修改為一次插入多條等一些數據庫查詢優化。
任何一個優化都要針對已經存在的問題,從服務器監控的報表可以看到我們這個網站應用服務器帶寬壓力、數據庫服務器帶寬壓力都很大,應用服務器的CPU使用率不高,因此,主要的優化是對應用服務器帶寬和數據庫服務器的寫入壓力做的優化,因為目的很明確,效果也是比較明顯的。
文中提到了用Handler來輸出靜態資源讓瀏覽器緩存,附上這個代碼,其它的優化針對性很高,就不再啰嗦了,主要的還是記錄下這次優化的工作方式和工作方法。
Handler輸出的靜態資源使用了.NET流壓縮,于是我們聲明一個壓縮器接口。
using System.IO;namespace ND.Race.Compressor
{/// <summary>/// 壓縮器接口/// </summary>public interface ICompressor{string EncodingName { get; }bool CanHandle(string acceptEncoding);void Compress(string content, Stream targetStream);}
}GZip壓縮器實現這個接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29using System.IO;
using System.IO.Compression;
using System.Text;namespace ND.Race.Compressor
{public sealed class GZipCompressor : ICompressor{public string EncodingName{get { return "gzip"; }}public bool CanHandle(string acceptEncoding){return !string.IsNullOrEmpty(acceptEncoding) &&acceptEncoding.Contains("gzip");}public void Compress(string content, Stream targetStream){using (var writer = new GZipStream(targetStream, CompressionMode.Compress)){var bytes = Encoding.UTF8.GetBytes(content);writer.Write(bytes, 0, bytes.Length);}}}
}
同樣的Deflate壓縮器也實現這個接口。
using System.IO;
using System.IO.Compression;
using System.Text;namespace ND.Race.Compressor
{public sealed class DeflateCompressor : ICompressor{public string EncodingName{get { return "deflate"; }}public bool CanHandle(string acceptEncoding){return !string.IsNullOrEmpty(acceptEncoding) &&acceptEncoding.Contains("deflate");}public void Compress(string content, Stream targetStream){using (var writer = new DeflateStream(targetStream, CompressionMode.Compress)){var bytes = Encoding.UTF8.GetBytes(content);writer.Write(bytes, 0, bytes.Length);}}}
}
如果瀏覽器不支持流壓縮,那我們只能直接輸出內容了,因此我們還需要一個不進行壓縮的處理類。
using System.IO;
using System.Text;namespace ND.Race.Compressor
{public sealed class NullCompressor : ICompressor{public string EncodingName{get { return "utf-8"; }}public bool CanHandle(string acceptEncoding){return true;}public void Compress(string content, Stream targetStream){using (targetStream){var bytes = Encoding.UTF8.GetBytes(content);targetStream.Write(bytes, 0, bytes.Length);}}}
}
現在我們就可以開始編碼我們的Handler了。
public class QuestionCacheHandler : IHttpHandler
{#region 靜態變量/// <summary>/// 資源過期時間/// </summary>private static readonly int durationInDays = 30;/// <summary>/// 流壓縮接口/// </summary>private static readonly ICompressor[] Compressors = {new GZipCompressor(),new DeflateCompressor(),new NullCompressor()};#endregion#region 私有變量/// <summary>/// 內存流壓縮類/// </summary>private ICompressor compressor;/// <summary>/// ETAG/// </summary>private string eTagCacheKey;/// <summary>/// 競賽場次Id/// </summary>private long raceId;#endregionpublic void ProcessRequest(HttpContext context){if (context == null) return;long.TryParse(context.Request.QueryString["raceId"], out raceId);if (raceId == 0) return; var acceptEncoding = context.Request.Headers["Accept-Encoding"];compressor = Compressors.First(o => o.CanHandle(acceptEncoding));eTagCacheKey = string.Concat(raceId, "/etag");if (IsInBrowserCache(context, eTagCacheKey)) return;SendOutputToClient(context, true, eTagCacheKey);}/// <summary>/// 發送內容到客戶端/// </summary>/// <param name="context"></param>/// <param name="insertCacheHeaders"></param>/// <param name="etag"></param>private void SendOutputToClient(HttpContext context, bool insertCacheHeaders, string etag){string content = "";MemoryStream memoryStream = new MemoryStream();compressor.Compress(content, memoryStream);byte[] bytes = memoryStream.ToArray();HttpResponse response = context.Response;if (insertCacheHeaders){HttpCachePolicy cache = context.Response.Cache;cache.SetETag(etag);cache.SetOmitVaryStar(true);cache.SetMaxAge(TimeSpan.FromDays(durationInDays));cache.SetLastModified(DateTime.Now);cache.SetExpires(DateTime.Now.AddDays(durationInDays)); // HTTP 1.0 的瀏覽器使用過期時間cache.SetValidUntilExpires(true);cache.SetCacheability(HttpCacheability.Public);cache.SetRevalidation(HttpCacheRevalidation.AllCaches);cache.VaryByHeaders["Accept-Encoding"] = true;}response.AppendHeader("Content-Length", bytes.Length.ToString(System.Globalization.CultureInfo.InvariantCulture));response.ContentType = "text/plain";response.ContentType = "application/x-javascript";response.AppendHeader("Content-Encoding", compressor.EncodingName);if (bytes.Length > 0)response.OutputStream.Write(bytes, 0, bytes.Length);if (response.IsClientConnected)response.Flush();}/// <summary>/// 是否瀏覽器已經緩存/// </summary>/// <param name="context"></param>/// <param name="etag"></param>/// <returns></returns>private bool IsInBrowserCache(HttpContext context, string etag){string incomingEtag = context.Request.Headers["If-None-Match"];if (String.Equals(incomingEtag, etag, StringComparison.Ordinal)){context.Response.Cache.SetETag(etag);context.Response.AppendHeader("Content-Length", "0");context.Response.StatusCode = (int)System.Net.HttpStatusCode.NotModified;context.Response.End();return true;}return false;}public bool IsReusable{get{return false;}}
}
服務器端代碼通過Http請求Header的Accept-Encoding來判斷是否支持流壓縮,再通過Header的etag來判斷瀏覽器中是否已經有緩存副本。
轉自:http://blog.moozi.net/archives/web-performance-optimization-practice-application-optimization.html
轉載于:https://www.cnblogs.com/xiaopohou/archive/2011/09/20/2182811.html
總結
以上是生活随笔為你收集整理的Web性能优化实践——应用层性能优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 专柜纽百伦574多少钱
- 下一篇: WCF - Session 剖析