2009年10月20日星期二

用python实现webserver(一)――Prefork

    要实现webserver,首先需要一个tcp server。作为python的设计原则,最好是使用SocketServer或者封装更好的BaseHTTPServer来复用。不过既然我们的目的 是为了学习,那么就不能用这两个内置对象。我们先实现一个最古典的每进程模式实现。而我们标题上的Prefork,则是apache服务器对这个模式的称 呼。
    每进程模式,顾名思义,就是每个新连接开启一个进程进行处理。首先创建一个socket,bind到一个套接字上。当有请求时,accept。(好多英 文,不是我有意cheglish,全是api的名称)accept会返回一个通讯用的socket,这时fork出一个新的进程,处理这个socket。 主进程在每次进入accept后阻塞,子进程在每次进入recv后阻塞。这样会带来几方面的好处。首先是模型分离,即使一个子进程崩溃,也不会影响到其他 子进程。其次是身份分离,当你需要让http server以高于常规运行(常规都是以apache, www-data, nobody运行的)用户的权限进行工作时,每进程模式是唯一安全的模式。其他模式都会造成同一进程内的其他session也暂时获得这个权限的问题。但 是同样,这样有几方面的问题,主要就是性能问题。
    由于每个连接都需要fork出一个新进程去处理。因此针对大量小连接的时候,fork和exit消耗了大量CPU。问题更严重的是,由于用户进程总数是有 限的(PEM或者ulimit都会限制这个数量),因此压力大到一定程度时(通常是1024或者2048),就会出现无法创建连接的情况。而对小型服务器 而言,在压力还没大道这个程度以前,服务器就会由于性能达到限制而造成段错误。以下是实际试验指令和结果:
测试指令: ab -n 10000 -c 100 http://localhost:8000/py-web-server
服务器报错:
[20090924 05:51:18]: Traceback (most recent call last):
[20090924 05:51:18]:   File "main.py", line 19, in <module>
[20090924 05:51:18]:
[20090924 05:51:18]: sock.run ();
[20090924 05:51:18]:   File "/home/shell/py-web-server/server.py", line 30, in run
[20090924 05:51:18]:
[20090924 05:51:18]: while loop_func (): pass
[20090924 05:51:18]:   File "/home/shell/py-web-server/server.py", line 56, in do_loop
[20090924 05:51:18]:
[20090924 05:51:18]: if os.fork () == 0:
[20090924 05:51:18]: OSError
[20090924 05:51:18]: :
[20090924 05:51:18]: [Errno 11] Resource temporarily unavailable

测试指令: ab -n 1000 -c 100 http://localhost:8000/py-web-server
返回结果:
Document Path:          /py-web-server
Document Length:        1320 bytes

Concurrency Level:      100
Time taken for tests:   14.189 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      1361000 bytes
HTML transferred:       1320000 bytes
Requests per second:    70.48 [#/sec] (mean)
Time per request:       1418.851 [ms] (mean)
Time per request:       14.189 [ms] (mean, across all concurrent requests)
Transfer rate:          93.67 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0   18 328.4      0    9000
Processing:     4  109 211.0     95    3335
Waiting:        4  100 211.1     86    3324
Total:          8  127 498.3     95   12332

    为什么只有100并发?因为200并发的时候测试机上的服务器已经崩溃了。而且我们看到服务器的效率大约是70req/sec。等过两天,讲到 Thread模式的时候,大家可以对比一下Thread模式的效率。基本上说,针对普通服务器,个人觉得Prefork模式并发数量尽量控制在 100-800这个级别比较合适。更高的也许能承受,可是就可能发生不稳定(原因上面有说,就是进程数量限制)。那么Prefork模式通常用在哪里呢? 数据库!Oracle和Postgresql全是Prefork模式的(其中Oracle的Prefork模式还经过一次分发,更复杂一些)。压力通常在 100-200这个级别,连接基本不断开,连接的请求和销毁开销很小,但是处理的过程开销很大。并且,由于处理过程复杂,一个链接的处理错误不能殃及整个 系统。对于这种问题,最好采用Prefork模式进行处理。同类的问题还有一些EJB服务器,复杂中间件等等。
    那么反过来,作为客户,我在面对Prefork模式的时候,如何才能高效处理呢?——对,大家都想到了,连接池模式。通过连接池存放空闲连接,避免连接的建立和释放开销,从而增加服务器性能。
    另外,python实现其实性能是很有问题的,我们对比一下apache2的测试结果:
测试指令: ab -n 1000 -c 100 http://localhost:8000/py-web-server
返回结果:
Document Path:          /
Document Length:        45 bytes

Concurrency Level:      1000
Time taken for tests:   7.914 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      3204355 bytes
HTML transferred:       452025 bytes
Requests per second:    1263.56 [#/sec] (mean)
Time per request:       791.413 [ms] (mean)
Time per request:       0.791 [ms] (mean, across all concurrent requests)
Transfer rate:          395.40 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0   70 432.8      3    3028
Processing:     0  193 733.0     88    6629
Waiting:        0  190 732.4     85    6621
Total:         46  263 950.7     93    7895

--
与其相濡以沫,不如相忘于江湖

没有评论: