在互联网项目迅速膨胀的背景下,我们公司,一个传统的金融公司,也开始风风火火地开展互联网项目,最近被派到一个手机APP的项目做后台架构支 持。首先面临一个问题就是如何选择一个合适的WEB中间件产品来满足高并发的场景,也就是要构建一个IO密集型的应用,而且还要面向手机客户端(低带 宽)。
在网上搜罗了一下相关的产品,主要就是tomcat,nodejs,nginx三种产品,但是对这三个产品从原理上对比分析的文章巨少无比,都是泛泛地做一个压力测试。
先说一下我们公司的技术背景,我们公司技术都以JAVA为主,所以在选择WEB中间件的时候,我会以不改变公司基础技术为前提做选择,nodejs来佐证。
首先我们有一个很关键的诉求,客户端是手机,而手机往往是低带宽的,我们需要先编程测试代码,模拟低带宽的场景,下面是我写的模拟低带宽的测试代码
package http.client; import java.io.InputStream; import java.io.PrintWriter; import java.net.Socket; public class BIOHttpClient { int taskAccount=1; static BIOHttpClient inst=new BIOHttpClient(); public static void main(String args[]) { inst.doTasksTest(); } private void doTasksTest(){ for(int i=0;i<taskAccount;i++ ){ new RequestTask().start(); } } } class RequestTask extends Thread{ String host="localhost"; int port=8080; public void run() { this.doRequest(); } private void doRequest(){ try { Socket socket=createNewSocket(); this.sendMsg(socket); //Thread.currentThread().sleep(10*1000); this.getResponseMsg(socket); socket.close(); // 关闭Socket }catch (Exception e) { throw new RuntimeException(e); } } private Socket createNewSocket()throws Exception{ Socket socket= new Socket(host,port );// 向本机的端口发出客户请求 socket.setReceiveBufferSize(1);//似乎不起作用。 socket.setSendBufferSize(1);//似乎不起作用。 System.out.println(this+"::客户端连接服务端成功!" ); return socket; } private void sendMsg(Socket socket)throws Exception{ String postStr = "account=abc&password=123456789"; int postStrLen = postStr.length(); StringBuffer post = new StringBuffer("POST /test/test HTTP/1.1\r\n"); post.append("Host: 127.0.0.1:8080\r\n"); post.append("Accept: text/html\r\n"); post.append("Connection: Close\r\n"); post.append("Content-Length: " + postStrLen + "\r\n"); post.append("Content-Type: application/x-www-form-urlencoded\r\n"); post.append("\r\n"); post.append(postStr); PrintWriter os = new PrintWriter(socket.getOutputStream());// 由Socket对象得到输出流,并构造PrintWriter对象 os.println(post); os.flush();// 刷新输出流,使Server马上收到该字符串 System.out.println(this+"::send msg to server:" + post+"::"+System.currentTimeMillis() ); } private void getResponseMsg(Socket socket)throws Exception{ StringBuilder sb=new StringBuilder(); InputStream reader = socket.getInputStream() ; // 由Socket对象得到输入流,并构造相应的BufferedReader对象 int size=1;//通常是8K~64K,如果设置为1,则明显可以模拟低带宽。 byte[] bs=new byte[size];//通过改变size来模拟低带宽读写。当size=1,且服务端数据量足够大(100万个字符以上),这样可以明显看到对BIOServer是有write阻塞的,但对NIOServer就不会阻塞。 String tmp=null; while ( (reader.read(bs)) != -1) {//读完后会阻塞 tmp=new String(bs).trim(); if( tmp.lastIndexOf("#endflag#")>0 ){//为防止读完后,跳到循环阻塞住导致不退出循环,要求服务端必须返回特定字符作为双方结束通信的协议。 break; }else{ //Thread.currentThread().sleep(10); //System.out.println(tmp); sb.append(tmp); } } System.out.println(this+"::get response msg from server,数据长度:"+sb.length()+"::"+System.currentTimeMillis() ); } }
通过控制读取的byte数组的长度来模拟低带宽,先通过这个测试代码来测试一下tomcat6,tomcat6默认配置采用的bio的模式来处理请求的,下面是服务端代码
package example; import java.io.IOException; import java.io.InputStream; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.io.IOUtils; public class TestServlet extends HttpServlet { private byte[] data; public TestServlet(){ try { InputStream in = this.getClass().getClassLoader().getResourceAsStream("data.txt"); data = IOUtils.toByteArray(in); System.out.println("数据读取完毕"); } catch (IOException e) { e.printStackTrace(); } } @Override public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { long start = System.currentTimeMillis(); resp.getOutputStream().write(data); resp.getOutputStream().flush(); System.out.println(Thread.currentThread() + "写数据结束 ,回写的数据长度为:" + data.length); System.out.println("use time : " + (System.currentTimeMillis() - start)); } }
在byte数组长度设置为1的时候,服务端回写数据(数据大小为5M)耗时为7944毫秒,当byte数组长度设置为1024的时候,耗时 为167毫秒,也就是说,tomcat在bio模式下,处理客户端为低带宽的场景的能力是非常糟糕的!tomcat的bio模式是传统中间件的一类代表, 正由于这类阻塞式的IO模式的设计,导致这类中间件无法满足互联网高并发请求的需求。在高并发场景需求的驱动下,java nio 诞生了,对于java nio封装实现较好的产品有netty和mina两个产品,但是很遗憾,这两个产品不算一个完整WEB中间件,所以这里我就不做详细讨论了。通过寻找 tomcat的官方文档,发现tomcat6以后,可以通过修改connector的协议配置,能支持nio模式。发现新大陆,麻烦修改配置测试之!修改 配置如下。
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" executor="tomcatThreadPool" connectionTimeout="20000" redirectPort="8443" />
非常遗憾,测试结果是byte数组长度为1时耗时为8413毫秒,长度为1024时,耗时为2190。what!不是nio了么?继续查看 tomcat官方文档以及源码,发现这个tomcat6的nio是伪nio,因为它在读写io的时候仍然采用的blocking的方式的,下面附上 tomcat官网上的对比说明图。
继续寻找,发现真正实现异步IO的产品有三个,tomcat6.x/tomcat7.x+nginx,tomcat8.x(servlet3.1),nodejs。针对这三个产品做测试,测试数据如下
tomcat6 bio(tomcat6不支持 nio技术) | tomcat6 bio + nginx | Nodejs | tomcat8 nio | |
低带宽场景 | 7944毫秒 | 147毫秒 | 111毫秒 | 78毫秒 |
正常带宽场景 | 167毫秒 | 141毫秒 | 118毫秒 | 32毫秒 |
从 测试数据就可以看到,在低带宽场景下,传统的中间件的处理能力是远远小于其它三个产品。这组测试数据只是想证明后面三个产品是真正实现异步IO的。对于产 品的选择,个人意见是如果你们的技术平台已经背负了沉重的java的历史包袱,那就选择在已有的WEB中间件前增加代理服务器nginx,如果是新的项 目,则采用tomcat8的servlet3.1来实现。仅仅在处理异步IO上,三个产品都差不多,没有哪个最好,只有哪个更适合
http://m.oschina.net/blog/340539