閱讀430 返回首頁    go 阿裏雲 go 技術社區[雲棲]


簡單的web server性能測試

最近一直在讀《java並發編程實踐》,書是絕對的好書,翻譯不能說差,也談不上好,特別是第一部分的前麵幾章,有的地方翻譯的南轅北轍了,還是要對照著英文版來看。我關注並發編程是從學習Erlang開始的,在多核來臨的時代,有人說並發將是下一個10年的關鍵技術。java5之前的多線程編程很複雜,況且我也沒有從事此類應用的開發,了解不多,而從jdk5引入了讓人流口水的concurrent包之後,java的並發編程開始變的有趣起來。
   書中第6章以編寫一個web server為例子,引出了幾種不同版本的寫法:單線程、多線程以及采用jdk5提供的線程池實現。我就用apache自帶的ab工具測試了下各個版本的性能,在redhat9 p4 2g內存的機器上進行了測試。
ab -50000 -1000 http://localhost/index.html >benchmark

單線程模式,順序性地處理每一個請求,50000並發很快就沒有響應了,不參與比較了。再來看看我們自己寫的多線程方式處理每個請求:
package net.rubyeye.concurrency.chapter6;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class ThreadPerTaskWebServer {
    
public static void main(String[] args) throws IOException {
        ServerSocket server 
= new ServerSocket(80);
        
while (true) {
            
final Socket connection = server.accept();
            Runnable task 
= new Runnable() {
                
public void run() {
                    
try {
                        handleRequest(connection);
                    } 
catch (IOException e) {
                        e.printStackTrace();
                    }

                }
            };
            
new Thread(task).start();
        }
    }

    
public static void handleRequest(Socket socket) throws IOException {
        
try {
            InetAddress client 
= socket.getInetAddress();
            
// and print it to gui
            s(client.getHostName() + " connected to server.\n");
            
// Read the http request from the client from the socket interface
            
// into a buffer.
            BufferedReader input = new BufferedReader(new InputStreamReader(
                    socket.getInputStream()));
            
// Prepare a outputstream from us to the client,
            
// this will be used sending back our response
            
// (header + requested file) to the client.
            DataOutputStream output = new DataOutputStream(socket
                    .getOutputStream());

            
// as the name suggest this method handles the http request, see
            
// further down.
            
// abstraction rules
            http_handler(input, output);
            socket.close();
        } 
catch (Exception e) { // catch any errors, and print them
            s("\nError:" + e.getMessage());
        }

    } 
// go back in loop, wait for next request

    
// our implementation of the hypertext transfer protocol
    
// its very basic and stripped down
    private static void http_handler(BufferedReader input,
            DataOutputStream output) {
        
int method = 0// 1 get, 2 head, 0 not supported
        String http = new String(); // a bunch of strings to hold
        String path = new String(); // the various things, what http v, what
        
// path,
        String file = new String(); // what file
        String user_agent = new String(); // what user_agent
        try {
            
// This is the two types of request we can handle
            
// GET /index.html HTTP/1.0
            
// HEAD /index.html HTTP/1.0
            String tmp = input.readLine(); // read from the stream
            String tmp2 = new String(tmp);
            tmp.toUpperCase(); 
// convert it to uppercase
            if (tmp.startsWith("GET")) { // compare it is it GET
                method = 1;
            } 
// if we set it to method 1
            if (tmp.startsWith("HEAD")) { // same here is it HEAD
                method = 2;
            } 
// set method to 2

            
if (method == 0) { // not supported
                try {
                    output.writeBytes(construct_http_header(
5010));
                    output.close();
                    
return;
                } 
catch (Exception e3) { // if some error happened catch it
                    s("error:" + e3.getMessage());
                } 
// and display error
            }
            
// }

            
// tmp contains "GET /index.html HTTP/1.0 dot.gifdot.gif."
            
// find first space
            
// find next space
            
// copy whats between minus slash, then you get "index.html"
            
// it's a bit of dirty code, but bear with medot.gif
            int start = 0;
            
int end = 0;
            
for (int a = 0; a < tmp2.length(); a++) {
                
if (tmp2.charAt(a) == ' ' && start != 0) {
                    end 
= a;
                    
break;
                }
                
if (tmp2.charAt(a) == ' ' && start == 0) {
                    start 
= a;
                }
            }
            path 
= tmp2.substring(start + 2, end); // fill in the path
        } catch (Exception e) {
            s(
"errorr" + e.getMessage());
        } 
// catch any exception

        
// path do now have the filename to what to the file it wants to open
        s("\nClient requested:" + new File(path).getAbsolutePath() + "\n");
        FileInputStream requestedfile 
= null;

        
try {
            
// NOTE that there are several security consideration when passing
            
// the untrusted string "path" to FileInputStream.
            
// You can access all files the current user has read access to!!!
            
// current user is the user running the javaprogram.
            
// you can do this by passing "../" in the url or specify absoulute
            
// path
            
// or change drive (win)

            
// try to open the file,
            requestedfile = new FileInputStream(path);
        } 
catch (Exception e) {
            
try {
                
// if you could not open the file send a 404
                output.writeBytes(construct_http_header(4040));
                
// close the stream
                output.close();
            } 
catch (Exception e2) {
            }
            ;
            s(
"error" + e.getMessage());
        } 
// print error to gui

        
// happy day scenario
        try {
            
int type_is = 0;
            
// find out what the filename ends with,
            
// so you can construct a the right content type
            if (path.endsWith(".zip"|| path.endsWith(".exe")
                    
|| path.endsWith(".tar")) {
                type_is 
= 3;
            }
            
if (path.endsWith(".jpg"|| path.endsWith(".jpeg")) {
                type_is 
= 1;
            }
            
if (path.endsWith(".gif")) {
                type_is 
= 2;
                
// write out the header, 200 ->everything is ok we are all
                
// happy.
            }
            output.writeBytes(construct_http_header(
2005));

            
// if it was a HEAD request, we don't print any BODY
            if (method == 1) { // 1 is GET 2 is head and skips the body
                while (true) {
                    
// read the file from filestream, and print out through the
                    
// client-outputstream on a byte per byte base.
                    int b = requestedfile.read();
                    
if (b == -1) {
                        
break// end of file
                    }
                    output.write(b);
                }

            }
            
// clean up the files, close open handles
            output.close();
            requestedfile.close();
        }

        
catch (Exception e) {
        }

    }

    
private static void s(String s) {
    
//    System.out.println(s);
    }

    
// this method makes the HTTP header for the response
    
// the headers job is to tell the browser the result of the request
    
// among if it was successful or not.
    private static String construct_http_header(int return_code, int file_type) {
        String s 
= "HTTP/1.0 ";
        
// you probably have seen these if you have been surfing the web a while
        switch (return_code) {
        
case 200:
            s 
= s + "200 OK";
            
break;
        
case 400:
            s 
= s + "400 Bad Request";
            
break;
        
case 403:
            s 
= s + "403 Forbidden";
            
break;
        
case 404:
            s 
= s + "404 Not Found";
            
break;
        
case 500:
            s 
= s + "500 Internal Server Error";
            
break;
        
case 501:
            s 
= s + "501 Not Implemented";
            
break;
        }

        s 
= s + "\r\n"// other header fields,
        s = s + "Connection: close\r\n"// we can't handle persistent
        
// connections
        s = s + "Server: SimpleHTTPtutorial v0\r\n"// server name

        
// Construct the right Content-Type for the header.
        
// This is so the browser knows what to do with the
        
// file, you may know the browser dosen't look on the file
        
// extension, it is the servers job to let the browser know
        
// what kind of file is being transmitted. You may have experienced
        
// if the server is miss configured it may result in
        
// pictures displayed as text!
        switch (file_type) {
        
// plenty of types for you to fill in
        case 0:
            
break;
        
case 1:
            s 
= s + "Content-Type: image/jpeg\r\n";
            
break;
        
case 2:
            s 
= s + "Content-Type: image/gif\r\n";
        
case 3:
            s 
= s + "Content-Type: application/x-zip-compressed\r\n";
        
default:
            s 
= s + "Content-Type: text/html\r\n";
            
break;
        }

        
// //so on and so ondot.gifdot.gif
        s = s + "\r\n"// this marks the end of the httpheader
        
// and the start of the body
        
// ok return our newly created header!
        return s;
    }
}
測試結果如下:
Concurrency Level:      1000
Time taken for tests:   111.869356 seconds
Complete requests:      50000
Failed requests:        0
Write errors:           0
Total transferred:      4950000 bytes
HTML transferred:       250000 bytes
Requests per second:    446.95 [#/sec] (mean)
Time per request:       2237.387 [ms] (mean)
Time per request:       2.237 [ms] (mean, across all concurrent requests)
Transfer rate:          43.20 [Kbytes/sec] received

修改下上麵的程序,采用jdk5提供的線程池:
    private static final int NTHREADS = 5;

    
private static Executor exec;

    
public static void main(String[] args) throws IOException {
        ServerSocket server 
= new ServerSocket(80);
        
if (args.length == 0)
            exec 
= Executors.newFixedThreadPool(NTHREADS);
        
else
            exec 
= Executors.newFixedThreadPool(Integer.parseInt(args[0]));
        
while (true) {
            
final Socket connection = server.accept();
            Runnable task 
= new Runnable() {
                
public void run() {
                    
try {
                        handleRequest(connection);
                    } 
catch (IOException e) {
                        e.printStackTrace();
                    }

                }
            };
            exec.execute(task);
        }
    }
默認線程池大小取5,後經過反複測試,線程池大小在5左右,測試結果達到最佳。測試采用線程池的結果如下:

Concurrency Level:      1000
Time taken for tests:   51.648142 seconds
Complete requests:      50000
Failed requests:        0
Write errors:           0
Total transferred:      4978908 bytes
HTML transferred:       251460 bytes
Requests per second:    968.09 [#/sec] (mean)
Time per request:       1032.963 [ms] (mean)
Time per request:       1.033 [ms] (mean, across all concurrent requests)
Transfer rate:          94.14 [Kbytes/sec] received

與上麵結果一比較,牛人寫的線程池終究是大大不一樣。當連接數增加到10W以上,兩個版本之間的性能差異就更明顯了。這裏采用的是固定線程池,如果采用緩衝線程池會怎麼樣呢?newFixedThreadPool改為newCachedThreadPool方法,測試可以發現結果與固定線程池的最佳結果相似。CachedThreadPool更適合此處短連接、高並發的場景。後來,我想Erlang寫一個簡單的web server,性能上會不會超過采用線程池的這個版本呢?試試:
%% httpd.erl - MicroHttpd 
-module(httpd).
-export([start/0,start/1,start/2,process/2]).
-import(regexp,[split/2]). 
-define(defPort,80). 
-define(docRoot,"."). 
start() 
-> start(?defPort,?docRoot).
start(Port) 
-> start(Port,?docRoot). 
start(Port,DocRoot) 
-> 
      
case gen_tcp:listen(Port, [binary,{packet, 0},{active, false}]) of 
          {ok, LSock}     
-> 
               server_loop(LSock,DocRoot);   
          {error, Reason}     
-> 
              exit({Port,Reason}) 
      end.
      
%% main server loop - wait for next connection, spawn child to process it
      server_loop(LSock,DocRoot) 
->   
          
case gen_tcp:accept(LSock) of   
                    {ok, Sock}     
->  
                          spawn(
?MODULE,process,[Sock,DocRoot]),  
                          server_loop(LSock,DocRoot);    
                  {error, Reason}     
->    
          exit({accept,Reason})  
  end.
  
%% process current connection
process(Sock,DocRoot) 
->  
      Req 
= do_recv(Sock),  
      {ok,[Cmd
|[Name|[Vers|_]]]} = split(Req,"[ \r\n]"),  
      FileName 
= DocRoot ++ Name, 
      LogReq 
= Cmd ++ " " ++ Name ++ " " ++ Vers, 
      Resp 
= case file:read_file(FileName) of  
                {ok, Data}     
->    
                     io:format(
"~p ~p ok~n",[LogReq,FileName]), 
                    Data;   
                {error, Reason}     
->   
                     io:format(
"~p ~p failed ~p~n",[LogReq,FileName,Reason]),   
                   error_response(LogReq,file:format_error(Reason))  
         end, 
        do_send(Sock,Resp),
        gen_tcp:close(Sock). 
        
%% construct HTML for failure message 
error_response(LogReq,Reason) 
->  
  
"<html><head><title>Request Failed</title></head><body>\n" ++
      
"<h1>Request Failed</h1>\n" ++ 
      
"Your request to " ++ LogReq ++ 
    
" failed due to: " ++ Reason ++  "\n</body></html>\n"
.
      
%% send a line of text to the 
do_send(Sock,Msg) 
->  
      
case gen_tcp:send(Sock, Msg) of  
      ok        
->
          ok;  
      {error, Reason}     
-> 
          exit(Reason)  
  end. 
          
%% receive data from the socket
do_recv(Sock) 
->  
      
case gen_tcp:recv(Sock, 0) of    
           {ok, Bin}     
-> 
                  binary_to_list(Bin);   
           {error, closed}     
-> 
                  exit(closed);    
           {error, Reason}     
-> 
                  exit(Reason)  
  end.
執行:
 erl -noshell +5000 -s httpd start


+P參數是將係統允許創建的process數目增加到50000,默認是3萬多。測試結果:

Concurrency Level:      1000
Time taken for tests:   106.35735 seconds
Complete requests:      50000
Failed requests:        0
Write errors:           0
Total transferred:      250000 bytes
HTML transferred:       0 bytes
Requests per second:    471.54 [#/sec] (mean)
Time per request:       2120.715 [ms] (mean)
Time per request:       2.121 [ms] (mean, across all concurrent requests)
Transfer rate:          2.30 [Kbytes/sec] received
    結果讓人大失所望,這個結果與我們自己寫的多線程java版本差不多,與采用線程池的版本就差多了,減少並發的話,倒是比java版本的快點。側麵驗證了這個討論的結論:erlang的優勢就是高並發而非高性能。當然,這三者都比不上C語言寫的多線程web server。測試了unix/linux編程實踐中的例子,速度是遠遠超過前三者,不過支持的並發有限,因為係統創建的線程在超過5000時就崩潰了。如果采用jdk5進行開發,應當充分利用新的並發包,可惜我們公司還停留在1.4。

文章轉自莊周夢蝶  ,原文發布時間2007-08-29

最後更新:2017-05-17 16:01:42

  上一篇:go  斷開的管道?
  下一篇:go  比較C語言標準I/O與*nix係統I/O的異同