异步非阻塞机制与多线程阻塞机制在处理并发耗时等待任务上的效率对比分析

mallon 发布于 2014/04/20 22:11
阅读 2K+
收藏 13

应用服务器的性能分析是复杂的,关注点很多。比如典型场景Web服务器+数据库,底层网络链路和网络硬件性能姑且不论,单看:Web服务器对静态文件的读写与磁盘和文件系统IO性能紧密相关;对数据的处理和数据库性能相关;而高并发访问则关系到操作系统的线程、网络套接字以及异步网络模型的效率。

在数据量大的情况下,数据库的性能成为一个至关重要的因素,随之带来Web服务器等待数据库的时间。在此基础上如果有大量的用户同时访问,那么会对Web服务器带来什么样的影响?以下主要讨论这个问题。

对于并发访问的处理,一般有两种处理机制:异步非阻塞机制、多线程阻塞机制(介绍略)。在测试选择上,前者使用基于Python的Tornado服务器,而后者使用基于Java的Tomcat服务器。注意:本文并非讨论开发语言的优劣,事实上,新版本的Java也支持异步机制,甚至高性能的epoll等。

测试工具:变态级的http_load

测试方法:使用该工具模拟1、10、100、1000个客户端并发访问以下场景,每次测试时间1分钟,得到服务器端每秒的总响应数。注意:由于Tomcat最大线程的限制(下面有提到)以及操作系统对端口数量的限制,1000个并发已经能够得到明显的结论了。

测试场景:

  1. 静态文件的读写。一个html文件和一大一小两个图片,大小分别为676k、1.6M和12k,使用http_load工具随机读取。静态文件读写的耗时可以忽略不计的。
  2. 模拟一个耗时操作,比如数据库操作。注意:耗时操作并不占用Web服务器本身的资源,它更多地体现的是Web服务器对并发访问处理的“合理”性。

以下是Java Servlet和Tornado服务的源代码:

Servlet

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name = "Dispatcher", urlPatterns = "/index")
public class index  extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        PrintWriter out = resp.getWriter();
        resp.setContentType("text/plain;charset=UTF-8");
        try {
            // 超时设置
            Thread.sleep(10000);
            out.println("OK");
        } catch (Exception ex) {
            resp.setStatus(500);
            ex.printStackTrace(out);
        } finally {
            out.flush();
            out.close();
        }
    }
}

Tornado

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-


import logging
import os
import sys

import tornado.gen
import tornado.ioloop
import tornado.web


def log_function(handler):
    pass


class IndexHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self, *args, **kwargs):
        ioloop = tornado.ioloop.IOLoop.instance()
        # 超时设置
        ioloop.add_timeout(ioloop.time() + 10, self.done)

    def done(self):
        self.set_header('Content-Type', 'text/plain;charset=UTF-8')
        self.write('OK')
        self.finish()


log_format = '%(levelname)s %(module)s.%(funcName)s[%(lineno)d] %(message)s'
logging.basicConfig(format=log_format, level=logging.INFO)

root_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
os.chdir(root_dir)

handlers = [
    ('/', tornado.web.RedirectHandler, {'url': '/index.html'}),
    ('/index', IndexHandler),
    ('/(.*)', tornado.web.StaticFileHandler, {'path': ''}),
]

settings = {
    'log_function': log_function,
    'debug': False,
}

application = tornado.web.Application(handlers, **settings)
port = 8080
application.listen(port)
tornado.ioloop.IOLoop.instance().start()

测试结果如下:

单看数据总是云里雾里的。下面进行分析。

首先分析静态内容,如果作成图表,那么是这样的:

对于单客户端访问,两者效率相差无几,从这一点上可以看出:开发语言的效率所占的比重是微乎其微的:大凡非计算密集型的应用,IO操作总是短板,相比CPU的速度有数量级的差距。

当客户端数量增多,图表呈现出以下两个特点:

  1. 两者的每秒响应数趋稳;
  2. Tomcat的效率大致是Tornado的3-4倍。
如果查看CPU占用:

Tomcat

Tornado

可以看到,Tomcat基本把四个核都占满了,而Tornado只占用了一个核。其中的道理其实是很清楚的:

  1. Tomcat是多线程处理并发访问的,势必会最大限度地占用CPU,而Tornado异步机制是单线程的。
  2. 每秒处理能力还是有上限的,这就是每秒响应数趋稳的原因。如果没有缓存直接访问磁盘,那么Tomcat和Tornado的性能应该相差无几,事实上操作系统和Web服务器对文件的读写不可能没有缓存。而这个结果告诉我们在适当的线程数量范围内,文件的缓存读效率是恒定的。Tomcat和Tornado3-4倍的性能差距原因大致就在此。
  3. 静态文件的耗时非常少,所以Tomcat的每一个线程均能够很快执行完毕,线程带来的问题并不是那么明显。那么把耗时增大,会发生什么?

把程序中的延时加到1、3、10秒,每秒总响应发生了非常大的变化。此时如果再像上面静态文件分析,已经没有意义了。换一个分析方式:

把并发连接数除以服务器端每秒总响应数,可以得到单个客户端单次访问的平均时间。把这个平均时间再除以人为添加的那个延时,就得到一个比率。这个比率反映出Web服务器作为客户端和数据库(典型场景)之间的“中介”“拉皮条”的效率。这个效率的最佳值是1,如果大于1,那么就表示该“中介”把时间浪费在毫无意义的地方。

这样得到的图就很有意思了:

可以看到,100并发以内,Tomcat和Tornado的比率均在1左右;100到1000并发,Tornado继续保持1,而Tomcat在某个点之后比率就急剧增加了。这是什么原因?

如果使用VisualVM查看Tomcat的线程会发现,它的线程数达到220之后,就不再增加了:

很明显,Tomcat限制了线程数量(应该有参数可以配置的,我没有查资料)。

我没有更进一步做测试,但是可以相信,图表中Tomcat的拐点就在220处。

那么能不能一味地增加线程呢?这已经不需要我多讲了,学过操作系统的同志都知道,CPU的每一颗核上只能执行一条指令序列,线程只是CPU频繁切换造成的“假象”。随着线程的增加,切换时间占的比重将会越来越大。更多的线程除了给系统带来毫无意义的消耗没有其它任何用处。这也是Tomcat不敢把默认线程数设得太大的原因吧。

网上也有其它异步非阻塞与多线程性能的比较,这里就不转载了。

所以呢,以下就是结论了:

  1. 对并发耗时等待任务的处理,单线程异步非阻塞方式明显比无限制的阻塞多线程更“合理”,注意这里只谈合理性:用一个线程就能达到同样的效果,为什么要开多线程呢?
  2. 对非耗时任务,多线程能不能完全发挥效率也得看场景。即便IO不是短板,理论上线程数也应该低于CPU核数。




加载中
0
乌龟壳
乌龟壳

发内存占用会更有意思的样子

0
mallon
mallon

引用来自“郭煜”的评论

发内存占用会更有意思的样子

内存占用其实都不多,也就是上千个套接字而已

0
乌龟壳
乌龟壳

引用来自“郭煜”的评论

发内存占用会更有意思的样子

引用来自“Mallon”的评论

内存占用其实都不多,也就是上千个套接字而已

印象中java超级占用内存。
返回顶部
顶部