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流压缩,于是我们声明一个压缩器接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 |
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
29 |
using 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压缩器也实现这个接口。
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
29 |
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); } } } } |
如果浏览器不支持流压缩,那我们只能直接输出内容了,因此我们还需要一个不进行压缩的处理类。
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 |
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了。
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110 |
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; #endregion public 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/