Spiga

Lab:体会ASP.NET异步处理请求的效果

2009-01-19 13:21 by Jeffrey Zhao, 8231 visits, 收藏, 编辑

关注我的朋友们一定记得,我不止一次强调过在ASP.NET应用程序中使用异步方式处理请求对于提高吞吐量的作用。不过似乎很多朋友们一直没有理解这样做的原因,亦或是对这样做的效果没有一个实际的“体会”,甚至在质疑这么做的功效。现在我将向大家进行一个演示,我们一起来看一下这么做的实际效果如何。

限制最大工作线程数量

对于ASP.NET 2.0应用程序来说,一个工作线程即为一个客户端请求的处理单位,如果所有工作线程被占完,那么站点就无法处理其他请求。使用异步方式处理请求是ASP.NET 2.0中新增的高级特性,它充分利用操作系统和CLR的功能,使得应用程序在等待IO-Bound操作完成时不会占用线程池中的工作线程(Worker Thread)。关于这一点,我曾经在《正确使用异步操作》一文中进行了较为详细的描述。在CLR 2.0 SP1之后,最大工作线程的数量变成了CPU数 * 250——不过不同托管环境(如IIS或SQL Server),均可有不同体现,而我们的试验很难占用如此多的工作线程,因此进行试验的第一步便是限制应用程序中最大工作线程的数量。在.NET应用程序中,可以通过ThreadPool.SetMaxThreads静态方法设置线程池中最大工作线程数量。

与此同时,我们还应该使用ThreadPool.SetMinThreads方法来设置线程池中“必须保留”的最小线程数量。该值默认为1,它意味着在初始情况下线程池中只保留1个线程。如果同时来访多个请求,那么线程池就必须创建额外的线程。线程池创建线程的最大速度为500毫秒一个,因为实际上一个线程的工作往往能够很快完成,这样线程就能够“复用”了。如果没有这个限制,那么线程池就可能在短时间内分配太多线程反而导致性能降低。当空闲时,线程池也会逐渐销毁线程,以避免系统维护太多线程而导致的多余开销。在我们的试验中,必须马上能够动用足够的工作线程来处理请求,否则就会把大量的时间耗费在等待线程创建上,降低了试验结果的代表性。

ThreadPool.Get/SetMaxThreads方法都会涉及到Complete I/O Port Threads这个值,它在我们试验中并不会影响什么。具体原因目前我也不清楚,原本以为它应该限制了异步IO的数据,但是实验下来却不然。

我们可以使用以下方法来修改线程池中最大及最小线程数量:

void SetThreads(int min, int max)
{
    int worker, io;

    ThreadPool.GetMaxThreads(out worker, out io);
    ThreadPool.SetMinThreads(min, io);
    ThreadPool.SetMaxThreads(max, io);
}

试验同步请求

我的测试环境为Windows Server 2008 x86 Enterprise Edition下的IIS 7。当然,这个试验在IIS 6中也能进行——不过,Vista下的IIS 7限制了10个并发连接数量,因此您无法在Vista下进行这个试验。

我们使用最为普通的工具来进行测试:Tinyget、Powershell以及perfmon。Tinyget是IIS Resource Toolkit中的工具之一,可以用于模拟数量不多的并发请求,常常用于重现一些简单并发环境下出现的问题。Powershell,我们主要是使用它的Measure-Command命令来测试执行一条Tinyget语句所消耗的时间。Measure-Command最简单的语法是Measure-Command {...},其中大括号里包含的是被测量的脚本。permon自然广为人知,我们主要用其来检测ASP.NET Applications\Requests Executing的值,它表示了同时执行请求的数量。

现在我们准备一个Sync.ashx,它将会访问数据库,并执行一个WAITFOR函数,其目的是停留3秒钟:

public class Sync : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        using (SqlConnection conn = new SqlConnection("..."))
        {
            SqlCommand cmd = new SqlCommand("WAITFOR DELAY '00:00:03';", conn);
            conn.Open();

            cmd.ExecuteNonQuery();
        }

        context.Response.ContentType = "text/plain";
        context.Response.Write("Sync");
    }

    public bool IsReusable { get { return false; } }
}

将最大及最小工作线程数量设为10,20,30,分别执行以下脚本:

Measure-Command {.\tinyget -srv:localhost -uri:/Sync.ashx -threads:30 -loop:1}

tinyget命令threads参数表明同时使用多少个线程进行请求,而loop参数表明“每个线程”将请求多少次。试验结果如下:

Max Worker Threads 10 15 20
Max Request Executing 6 11 16
Execution Time (s) 15.14 9.10 6.13
permon Snapshot 10 10 10

从试验结果中我们可以发现,可同时执行的请求数比最大工作线程少4(思考题:另外4个在做什么呢?),而同时执行的请求的数量越多,执行所有请求所消耗的时间也在越小。这和我们之前的想法基本一致。

试验异步请求

构建一个异步Handler:

public class Async : IHttpHandler, IHttpAsyncHandler
{
    public void ProcessRequest(HttpContext context) { }

    public bool IsReusable { get { return false; } }

    private SqlConnection m_conn;
    private SqlCommand m_cmd;
    private HttpContext m_context;

    public IAsyncResult BeginProcessRequest(
        HttpContext context, AsyncCallback cb, object extraData)
    {
        this.m_context = context;
        this.m_conn = new SqlConnection("Data Source=...;...;Asynchronous Processing=true");
        this.m_cmd = new SqlCommand("WAITFOR DELAY '00:00:03';", this.m_conn);
        this.m_conn.Open();

        return this.m_cmd.BeginExecuteNonQuery(cb, extraData);
    }

    public void EndProcessRequest(IAsyncResult result)
    {
        this.m_cmd.EndExecuteNonQuery(result);
        this.m_conn.Dispose();

        this.m_context.Response.ContentType = "text/plain";
        this.m_context.Response.Write("Hello World");
    }
}

唯一可能值得提到的是,如果要对SQL Server进行异步数据访问,则必须在连接字符串里加上Asynchronous Processing标记。那么我们把最大和最小工作线程数量设为10个,并使用以下脚本进行测试:

Measure-Command {.\tinyget -srv:localhost -r:7788 -uri:/Sync.ashx -threads:30 -loop:1}
[System.Threading.Thread]::Sleep(2000)
Measure-Command {.\tinyget -srv:localhost -r:7788 -uri:/Async.ashx -threads:30 -loop:1}
[System.Threading.Thread]::Sleep(2000)
Measure-Command {.\tinyget -srv:localhost -r:7788 -uri:/Async.ashx -threads:40 -loop:1}
[System.Threading.Thread]::Sleep(2000)
Measure-Command {.\tinyget -srv:localhost -r:7788 -uri:/Async.ashx -threads:50 -loop:1}

上述脚本首先将同时发起30次同步请求,再发起三次异步请求,数目分别是30、40和50。试验结果如下:

Max Request Executing 6 30 40 50
Execution Time (s) 15.06 3.10 3.11 3.10
permon Snapshot 10

结果再明显不过了:应用程序还是只能每次处理6个同步请求,但是对于异步请求来说似乎就“丝毫不受限制”了。为了更好的说明问题,我们再进行最后一个试验。

降低最小线程数量

之前提过,最小线程数量代表了线程池中所维护的最少线程数量。线程池将会根据需要来创建或销毁线程。

我们现在将最小线程数量设为1,最大线程数量设为20,使用同时发起50个请求。试验结果如下:

Max Request Executing 9 50
Execution Time (s) 18.27 3.37
perfmon Snapshot min-sync min-async

对于同步请求,同时处理的请求数目从1开始以每秒两个的速度增长,最终受限于“保护机制”而停止在9个线程。而对于异步请求,则是瞬间飙升至50个——因为这样的请求不需要占用工作线程,自然无需等待线程慢慢分配了。

看了以上的试验,不知道您是否有所感受?不如您也在自己的机器上试试看呢?

Add your comment

37 条回复

  1. #1楼 welshem      2009-01-19 13:25
    不错
     回复 引用 查看   
  2. #2楼 张明海      2009-01-19 13:27
    沙发
     回复 引用 查看   
  3. #3楼 Such Cloud      2009-01-19 13:35
    使用了异步 客户端有什么变化吗
     回复 引用 查看   
  4. #4楼 韦恩卑鄙      2009-01-19 13:40
    @Such Cloud 完全没有
    这里是指服务器端 iis 把请求转向到 clr以后的操作
     回复 引用 查看   
  5. #5楼 xq.cheng      2009-01-19 15:20
    有些东西不太懂,顶一下慢慢看
     回复 引用 查看   
  6. #6楼 1184[未注册用户]2009-01-19 15:31
    为何你的网页浏览时候老是提示有脚本错误呢?
     回复 引用   
  7. #7楼 kkun      2009-01-19 15:34
    坐下来慢慢消化
     回复 引用 查看   
  8. #8楼[楼主] Jeffrey Zhao      2009-01-19 15:53
    --引用--------------------------------------------------
    1184: 为何你的网页浏览时候老是提示有脚本错误呢?
    --------------------------------------------------------
    什么错误,我一直在阿?
     回复 引用 查看   
  9. #9楼 andy.wu      2009-01-19 18:14
    很好。
     回复 引用 查看   
  10. #10楼 123321[未注册用户]2009-01-19 19:30
    好文章.
    请问楼主能不能提供那几个软件的下载地址啊?
     回复 引用   
  11. #11楼 123321[未注册用户]2009-01-19 20:11
    是perfmon吧,是不是笔误啊
     回复 引用   
  12. #12楼[楼主] Jeffrey Zhao      2009-01-19 23:24
    --引用--------------------------------------------------
    123321: 是perfmon吧,是不是笔误啊
    --------------------------------------------------------
    嗯,笔误
     回复 引用 查看   
  13. #13楼 装配脑袋      2009-01-20 08:09
    老赵介绍下怎么让大家一下子把自己的程序改成真正的异步方式呀?
     回复 引用 查看   
  14. #14楼[楼主] Jeffrey Zhao      2009-01-20 09:11
    --引用--------------------------------------------------
    装配脑袋: 老赵介绍下怎么让大家一下子把自己的程序改成真正的异步方式呀?
    --------------------------------------------------------
    说实话,要“改”且“方便”,不容易啊,我正在想是否应该有这么一个辅助类库出来可以方便开发……
     回复 引用 查看   
  15. #15楼 小鬼00[未注册用户]2009-01-20 09:38
    public bool IsReusable { get { return false; } }

    问一个题外话,这里的return false; 和return true;有什么区别?
     回复 引用   
  16. #16楼[楼主] Jeffrey Zhao      2009-01-20 09:49
    @小鬼00
    是共享一个Handler处理多个请求,还是为每个请求使用不同的Handler
     回复 引用 查看   
  17. #17楼 装配脑袋      2009-01-20 10:08
    从前,记得有人告诉我古代的ASP.NET可以用Page.AddOnPreRenderCompleteAsync方法实现ASP.NET流水线中插入异步操作。不知道现代还能用否?
     回复 引用 查看   
  18. #18楼 感動常在      2009-01-20 10:09
    study
     回复 引用 查看   
  19. #19楼[楼主] Jeffrey Zhao      2009-01-20 10:27
    @装配脑袋
    啥叫古代的asp.net,本来就是asp.net 2.0里才有的功能吧。
    现在当然也有。
     回复 引用 查看   
  20. #20楼 Ivan Jiang      2009-01-20 10:59
    这个异步操作一定要在Handler处实现吗,能不能不改动Handler而直接放到数据层去呢?

    就以那个Article评论分页那实例。

    我们实际的应用程序大概是:

    1:前台页面用Ajax去请求一个“aspx”,"ashx"或Webservice页面,让他们去取数据。
    2:然后在取的数据访问层里面用异步操作提高性能(而不是在"ashx"上写数据访问或者异步的相关代码)

    可以这样吗?还是非得在Handler上继承IHttpHandler, IHttpAsyncHandler这些接口。
     回复 引用 查看   
  21. #21楼[楼主] Jeffrey Zhao      2009-01-20 11:57
    @Ivan Jiang
    嗯,ASP.NET里面可以异步的东西很多。
    HttpHandler,HttpModule,Page都可以异步。
    当然关键还是HttpHandler,HttpModule
    Page是HttpHandler的一个实现。
     回复 引用 查看   
  22. #22楼 mervin[未注册用户]2009-01-20 17:18
    麻烦作者把字体调小写吧,看着太累了。。
     回复 引用   
  23. #23楼[楼主] Jeffrey Zhao      2009-01-20 17:19
    @mervin
    字体很大?你用的Text Size难道不是Medium?
     回复 引用 查看   
  24. #24楼 airwolf2026      2009-01-21 09:00
    vista 里面的IIS7连接数应该是没有限制的吧,而是由于vista系统本身的tcp/ip连接数限制,间接造成IIS7连接数限制吧?
    前不久用xp和vista部署了一个相同站点,结果xp的没有几个人访问就报连接过多,而vista倒不会,系统日志倒是有警告说超过tcp/ip连接数限制...
    不知道是不是俺的错觉哈.
     回复 引用 查看   
  25. #25楼[楼主] Jeffrey Zhao      2009-01-21 09:09
    @airwolf2026
    我不知道啊,我是看一些资料和测试,它们说vista下iis7有限制,忘了具体来源是什么了。
     回复 引用 查看   
  26. #26楼 Wencui      2009-01-21 13:55
    Jeffrey的文章,顶一个!

    @airwolf2026
    不是IIS7的限制,是IE的限制。使用Tinyget的时候,我们需要将IE的最大连接数增大(改注册表),不然Tinyget不能模拟IE同时对同一个站点的发出许多请求。

    至于异步,Jeffrey提到了两种异步 1)ASP.NET application pool中的thread 异步 2)数据库 connection pool 的异步。他们是不同的东西。个人认为,没有必要大量使用异步,只有如下一些情况:

    1. 长时间的IO操作。可以使用异步的handler/page。
    2. 长时间SQL操作。我们可以同时使用以上两种异步。
     回复 引用 查看   
  27. #27楼[楼主] Jeffrey Zhao      2009-01-21 13:57
    @Wencui
    和IE有关?Tinyget不是用IE的吧。
    至于异步,其实只要
    1、长时间
    2、能利用到IOCP
    用异步肯定是好的。
     回复 引用 查看   
  28. #28楼 准备放假[未注册用户]2009-01-21 23:44
    对于数据库为主的应用,在数据库优化方面控制很好(绝大多数SQL语句执行时间较短)后,异步能明显提高吞吐量。目前痛苦的是经常发现数据查询时间长,导致线程堆积。
     回复 引用   
  29. #29楼[楼主] Jeffrey Zhao      2009-01-22 00:16
    @准备放假
    查询时间长更不能让它阻塞了啊,呵呵
     回复 引用 查看   
  30. #30楼 james-brook[未注册用户]2009-01-23 14:05
    其他4个线程可能负责其他任务吧!总不会所有的线程来处理一个任务的。
     回复 引用   
  31. #31楼[楼主] Jeffrey Zhao      2009-01-23 14:09
    @james-brook
    我的问题就是,它们在负责什么任务?
     回复 引用 查看   
  32. #32楼 WizardWu      2009-01-24 18:25
    great~
     回复 引用 查看   
  33. #33楼 Ivan Jiang      2009-02-06 23:54
    BlogEngine里面为了实现异步发送邮件只有一行代码(前三行是他以前的一个版本,Utils.SendMailMessage是它一个方法。):

    //ThreadStart threadStart = delegate { Utils.SendMailMessage(message); };
    //Thread thread = new Thread(threadStart);
    //thread.IsBackground = true;
    //thread.Start();
    ThreadPool.QueueUserWorkItem(delegate { Utils.SendMailMessage(message); });

    他的注释是:Sends the mail message asynchronously in another thread.

    老赵,怎么去分析它这种开启另一个线程来异步执行的做法,这样写道是很轻松。貌似我们如果要改进手头的程序,只要在现有的方法外套上一个ThreadPool.QueueUserWorkItem就行了。这样做有什么优劣吗?
     回复 引用 查看   
  34. #34楼[楼主] Jeffrey Zhao      2009-02-07 00:12
    @Ivan Jiang
    它是使用了一个额外的工作线程来发邮件,这样不会阻塞调用线程,仅此而已。
    我说的方法是利用IOCP省下工作线程,也就是比如在请求数据库的时候,是不占任何工作线程的。
     回复 引用 查看   
  35. #35楼 Ivan Jiang      2009-02-07 01:43
    明白,也就是说BE那样写的开销还是不小的是吧
     回复 引用 查看   
  36. #36楼[楼主] Jeffrey Zhao      2009-02-07 02:26
    --引用--------------------------------------------------
    Ivan Jiang: 明白,也就是说BE那样写的开销还是不小的是吧
    --------------------------------------------------------
    他的目的本不就是为了节省,其实他也没法节省……
     回复 引用 查看   
  37. #37楼 Cat Chen      2009-03-07 13:40
    其实做comet的时候,异步是最有必要的,必须把thread还给pool,然后等消息来了再获取一个thread执行下去。lighttpd的优势在于此,lighttpd难以调试也因为这个。
     回复 引用 查看   
发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 1377234 B00W52HE2DU=