本文介绍基于.NET Framework4.0环境的一次接口优化全过程:当接口接收来自同一客户端(同一Session会话)的多个异步请求时,处理并发问题的优化;案例以笔者近期改进项目上处理的接口性能问题为例。
接口以实现HTTPHandler的方式接收客户端请求。主函数实现了IHttpHandler和IRequiresSessionState接口。下面开始详细的问题发现和解决步骤。
在上一章的问题现象中可以看到:
Jmeter的聚合报告中:响应平均值到了50毫秒,这显示是不可接受的。
再往后看,最小值在738毫秒,在1秒以内,属于正常的调用。但是最大值高到离谱:100毫秒;
吞吐量:几乎是1秒钟只能处理一个请求。
以上现象,很容易让人怀疑是在排队处理,先到的请求,先处理完,后面的请求一直在等待。既然有所怀疑,那我们就想办法论证一下。
在接口用stopwatch计时并生成日志,证明我们的怀疑,代码片段如下:
public class EatsunMobileNewHandler : IHttpHandler, IRequiresSessionState
{
public void ProcessRequest(HttpContext context)
{
//......
/************************计时************************/
Stopwatch sw = new Stopwatch();
int key = 0;
if (bDebug)
{
Random rd = new Random();
key = rd.Next();
sw.Start();
}
/****************************************************/
/************************业务************************/
if (bDebug)
{
BaseFunc.DebugLog(string.Format("开始业务逻辑之前的耗时,key:{0},action:{1},time:{2}", key, action, sw.ElapsedMilliseconds));
sw.Restart();
}
/****************************************************/
//......
/************************业务************************/
if (bDebug)
{
BaseFunc.DebugLog(string.Format("业务逻辑完成的耗时,key:{0},action:{1},time:{2}", key, action, sw.ElapsedMilliseconds));
sw.Stop();
}
/****************************************************/
//......
}
}
再次用jmeter发起请求,查看日志结果如下:
可以看到,相同的key为一组请求,所有的请求都是成对出现,下一次请求开始都是在上一次结束之后。这就论证了我们怀疑的点:接口只能串行处理来自同一客户端的请求,造成了性能问题。我们需要将其改成并行处理的方式。
对于web处理程序,对请求进行串行处理是没有道理的,即使是同一个客户端来的请求,也可能是并发性质的,现今用户操作界面无论是PC还是手机,界面越来越丰富,同一个界面在同一时间向服务端发起请求是很常见的,所以这个问题不符合常理,既然不符合常理,就必定有解决办法。我们从实现的IHttpHandler接口开始调查。
相对于IHttpHandler,微软官方还提供了一个异步的接口IHttpAsyncHandler,尝试使用IHttpAsyncHandler解决此问题:实现IHttpAsyncHandler接口,主类继承HttpAsyncHandler并实现BeginRqeuest抽象方法,实现异步,代码如下:
public abstract class HttpAsyncHandler : IHttpAsyncHandler, IAsyncResult
{
private HttpContext _context;
public bool IsReusable
{
get { return true; }
}
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(DoRequest), this);
return this;
}
public void EndProcessRequest(IAsyncResult result)
{
}
public abstract void BeginRqeuest(HttpContext context);
private static void DoRequest(object state)
{
HttpAsyncHandler handler = (HttpAsyncHandler)state;
handler.BeginProcess(handler._context);
}
//......
}
修改完后,实际测试,并不能达到效果,因为IHttpAsyncHandler只是在接口内部实现了逻辑的异步。来自同一个客户端的并发请求任然不能并行处理。
继续调查后发现:IRequiresSessionState这个session相关接口的一些信息:实现了IRequiresSessionState接口的类,为了保证Session的正确性,.net内部机制会保证同一个会话期间,第一次请求执行完之前,不会执行第二次请求。而相应的还有一个接口IReadOnlySessionState,这个接口因为Session是只读方式的使用,.net不对同一个会话进行控制,不管多少此请求,在服务器可承受范围内,就会同时进行处理。
在检查完代码之后,尝试将IRequiresSessionState接口更换为IReadOnlySessionState,代码如下:
public class EatsunMobileNewHandler : IHttpHandler, IReadOnlySessionState
{
public void ProcessRequest(HttpContext context)
{
//......
}
}
修改后,测试正常,同一个客户端请求可以同时进入主函数:ProcessRequest。
(注,笔者这里压力测试的线程数较多,均值耗时在5秒左右,在此环境下,相比于修改前提升了。)
应用服务器
数据库服务器
可以看到应用服务器CPU资源已经被充分利用起来了,数据库服务器CPU耗用也有所提高。
再一次论证了我们修改达到了预期。
至此,同一客户端并发请求问题已经解决,但是目前的方案仍然不是最优,还需要将IRequiresSessionState和IReadOnlySessionState相关业务拆开,并实现IHttpAsyncHandler异步的方式,才是最佳效果,笔者将在下一篇文章中进行完善。《记一次.NET HttpHandler接口性能优化过程(续)》