很多高性能的web框架,例如沈崴的euraisa,fackbook的tornado(这两个都是python)框架,都是http的。这和我们的印象相反,python,或者其他高级语言不是都很慢么?为什么都用这个来做http服务器呢?
Tag Archives: webserver
C10K的卡通解释
以前有一帮医生,帮一个城市看病。当然,医生少人多,政府就开始动脑筋,怎么样让医生给更多的人看病。
最开始是医生去病人那里看病的,医生花在路上的时间很长,于是成立了医院。让病人过来,节省医生的时间。当然,病人肯定比医生多的,这是整个文章的假定。为了保持原来的模式,病人到了医院后,会有自己独立的一间屋子,完全模拟在家的感觉。这样会有什么问题呢?问题在于病人独占了医生,在病人抽血,验血的时候,医生无所事事,因此效率很低。
后来转换了一个模式,医生过一段时间就离开当前病人,看看哪个病人那里空着就过去。这样的目地是为了防止一个病人拉住医生不放,将医生的时间平均分配到多个病人头上。这样的动作快多了,但是医院受不了了。原本8个医生,一人一个病房。现在8个医生要在N间屋子里穿梭,万一每个屋子里的病人都是在抽血,那这个N就会无穷大了,现在是屋子不足了。
然后又换了个模式,对不起,现在不是一人一间屋子了,是一堆人一间屋子。每个人只要一个床和一个病例记录,其他的设备可以有限的共享。这样屋子不足的问题得到了部分的缓解。问题是医生又不干了。一方面离开病人再找空病人费事又费精力,另一方面抢设备也是个困扰。医生需要设备的时候会让护士去看看,如果有就拿过来。可是两个医生一起下这个医嘱就会出问题,一个护士看看还有,回去说有,再去拿的时候另一个护士已经拿着最后一个离开了。就算是同一个医生,下这个医嘱的时候,两个执行的护士也会这么打起来。
医院方面动了动脑筋,干脆这样吧。一个病房里只能一个医生负责,多个病房公用的设备看到有就可以预定起来。这样病房里的设备是不会抢起来的,而病房外的设备先到先得,也算公平。医生在病人去抽血等等活动的时候再离开病人,而不是每隔固定的时间。每隔一个很长的时间护士会去巡房,如果医生还在被同一个病人纠缠,护士就会让这个病人强制休息。
不知道有多少人看懂了?下面是答案。
第一种模式叫做服务队列模式。医生是资源池,病人是待处理请求。这个模式的问题是请求过程中往往会有大量IO出现,此时CPU陷入等待,很不合算。
第二种模式叫做多线程。医生是CPU,病房是进程。一个病人新建一个进程,系统将CPU在多个进程间调度。此时的问题是进程对系统资源的消耗比较大。
第三种模式叫做多线程,医生是CPU,病房是进程,床是线程。每个请求新建一个线程,CPU在多个线程间调度。此时系统资源消耗的问题得到一定缓解,问题变成上下文切换和资源锁定造成的浪费。
第四种模式叫做协程。CPU只在必要的时候离开当前请求。什么是必要的时候呢?就是大规模IO之前。IO完成后,CPU会再度调度回来,这样避免了频繁的上下文切换。而在一个CPU的情况下,这样的模式不会造成竞争。(多线程模式就算只有一个CPU一样竞争,因为CPU可能在任何时间离开线程,包括原子操作内部)
沈游侠曾说过,好的构架是让瓶颈只出现的CPU上。当然,从更广义的来说是只让瓶颈出现在最紧张的资源上。显然,如果是服务器,CPU和总线带宽多数是最紧张的资源。
以nginx作为subversion前端的一些细节
本文系电脑资料,同步到blog上。小黄姐姐不必看了,可以帮我留个言。
nginx性能不错,可惜不支持WebDAV,因此没法拿来作为subversion的http服务。于是考虑开一个nginx作为前端,后端就跑一个apache来作为容器。配置这么写的(节选):
=========/etc/nginx/sites-enabled/default=========
server {
listen 443;
server_name OOXX
ssl on;
ssl_certificate keys/server.crt;
ssl_certificate_key keys/server.key;
ssl_session_timeout 5m;
ssl_protocols SSLv2 SSLv3 TLSv1;
ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/localhost.access.log;
include /etc/nginx/mapping-ssl;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /var/www/nginx-default;
}
}
========================================
打开了一个https的服务,这是当然的,svn传输的数据使用http很危险。
===========/etc/nginx/mapping-ssl=============
location ^~ /svn {
proxy_set_header Destination $http_destination;
proxy_pass http://apache_svr;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
========================================
将/svn下面的所有请求交给apache2。
=====/etc/apache2/mods-enabled/dav_svn.conf=====
<Location /svn/main>
DAV svn
SVNPath /var/web/svn/main
AuthType Basic
AuthName “Subversion Repository”
Require valid-user
AuthUserFile /var/web/svn/main/conf/passwd
AuthzSVNAccessFile /var/web/svn/main/conf/authz
</Location>
========================================
看起来很美,但是在使用中会发生502错误,原因是来自文件移动后,svn会使用COPY作为Verb去请求服务器端,这时候发生了这样一条日志:
==========/var/log/apache2/access.log==========
127.0.0.1 – {user} [02/Apr/2010:11:07:31 +0800] “COPY {path} HTTP/1.0″ 502 546 “-” “SVN/1.5.4 (r33841)/TortoiseSVN-1.5.5.14361 neon/0.28.3″
========================================
搜索了一下,这是因为使用https作为http服务的前端造成的,这里(https://secure.bonkabonka.com/blog/2008/01/04/nginx_fronting_for_subversion.html)提到了解决方案,而它又引用了另一个网页(http://silmor.de/49)解释细节。不幸的是,这个细节是错误的。关键在于这句上
LoadModule headers_module /usr/lib/apache2/modules/mod_headers_too.so
仔细看一下就会发现,mod_headers_too应当是mod_headers。在debian下,应当执行这几条指令。
cd /etc/apache2/mods-enabled
ln -s ../mods-available/headers.load headers.load
然后,在/etc/apache2/httpd.conf中写入以下内容:
RequestHeader edit Destination ^https http early
问题解决,Q.E.D。
用python实现webserver(二)――Thread
我们上面说过,Prefork模式有着先天的缺陷。针对http这种大量短请求的应用(当然,http1.1以来,有不少客户端使用了长连接),Prefork的最高并发很让人不满。并且,无论是否高并发,Prefork的性能都非常不好。现在我们介绍一下Thread模式。
和Prefork非常类似,每Thread模式通过新建的线程来控制对象的传输。和Prefork模式不同的是,一个用户能够建立多少个线程并没有限制。在系统上似乎有限制,65535个,但是同样,文件句柄最高也就能打开65535个,因此通常而言一个服务器最高也就能顶50000并发,无法再高了(nginx就能够支撑5W并发,再高要使用一些特殊手法来均衡负载)。而且线程的建立和销毁的开销非常小——没有独立的空间,不用复制句柄,只要复制一份栈和上下文对象就可以。但是,由于所有线程运行在同一个进程空间中,因此每线程模式有几个非常麻烦的瓶颈。
首先是对象锁定和同步,在每进程模式中,由于进程空间独立,因此一个对象被两个进程使用的时候,他们使用了两个完全不同的对象。而线程模式下,他们访问的是同一个对象。如果两个线程需要进行排他性访问,就必须使用锁,或者其他线程同步工具来进行线程同步。其次,由于使用同一个进程空间,因此一旦有一个连接处理的时候发生错误,整个程序就会崩溃。对于这一问题,可以通过watchdog方式来进行部分规避。原理是通过一个父进程启动子进程,子进程使用每线程处理请求。如果子进程崩溃,父进程的wait就会返回结果。此时父进程重启子进程。使用了watchdog后,服务不会中断,但是程序崩溃时正在处理的连接会全部丢失。最后,是python特有的问题——GIL。由于GIL的存在,因此无论多少线程,实际上只有一个线程可以处理请求,这无形中降低了效率。下面我们看一下Thread模式的测试结果:
测试指令: ab -n 1000 -c 100 http://localhost:8000/py-web-server
返回结果:
Document Path: /py-web-server
Document Length: 1682 bytes
Concurrency Level: 100
Time taken for tests: 3.834 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 1723000 bytes
HTML transferred: 1682000 bytes
Requests per second: 260.85 [#/sec] (mean)
Time per request: 383.362 [ms] (mean)
Time per request: 3.834 [ms] (mean, across all concurrent requests)
Transfer rate: 438.91 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 75 468.4 0 3001
Processing: 2 32 84.0 20 1593
Waiting: 1 30 84.0 18 1592
Total: 2 107 511.0 20 3828
测试指令: ab -n 10000 -c 1000 http://localhost:8000/py-web-server
返回结果:
Document Path: /py-web-server
Document Length: 1682 bytes
Concurrency Level: 1000
Time taken for tests: 37.510 seconds
Complete requests: 10000
Failed requests: 0
Write errors: 0
Total transferred: 17231723 bytes
HTML transferred: 16821682 bytes
Requests per second: 266.60 [#/sec] (mean)
Time per request: 3751.004 [ms] (mean)
Time per request: 3.751 [ms] (mean, across all concurrent requests)
Transfer rate: 448.62 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 695 2422.8 0 21004
Processing: 0 67 341.1 28 9855
Waiting: 0 64 340.5 26 9855
Total: 0 762 2516.6 29 30856
根据结果可以看到,Thread模式在1000并发的时候还工作良好,每秒处理请求数在250-300req/sec,但是每个请求的总处理时间已经高达760ms。并且从中我们可以看出,大量的时间都是消耗在等待上,说明线程的建立逐渐成为问题(为什么?下面说明)。实际性能测试的结果,也表明大约一半的时间花费在了等待上,而另一半花费在了线程建立上。加上销毁的开销,整个系统主要的瓶颈在于由于大量线程建立和销毁造成的CPU开销上。
结合上述情况,我们同时也想到一些问题,Thread模式在一个进程中,到底能创建多少个线程?上文上说大约是5W个,其实太理论了。实际上如果按照Windows来计算,最高不超过1000个,Linux下也在这个数量级上。为什么?由于进程内存空间的问题。一个线程在创建时,默认需要1M的内存空间来作为栈。对于专用的高速系统,我们建议将这个值调整到500K,一般一个session的内存消耗就在500K上下,考虑还有堆消耗,500K是一个比较安全的值。单一进程,32位访问的寻址空间是4G,然而系统需要使用其中的2G作为系统空间——这一状态可以经由启动时的3G参数调整(针对Windows)。然而由于使用了系统空间,因此系统中很多表项空间不足,对稳定性也有不利影响,通常我们建议不要进行这种“优化”。而系统的基础使用和库使用需要数百M的空间,安全起见,能够自由的用于栈的可分配自由空间只有1G的大小。这1G的空间,以创建1M的栈计算,只能同时开1000线程。这就是单一进程中线程的极限。
实际上,是根本做不出这么多的线程的。贝壳的小型测试机上只观测到过10-20个线程/每进程。这是由于线程的建立也需要时间,在创建下一个线程之前,工作进程已经跑了一些了。创建几个工作线程后,第一个工作线程已经完成工作。因此我们在实际中看到的是,压力越高,连接建立的速度越慢。因为负责创立新线程的线程获得越少的CPU时间用于工作。为了增加处理速度,通常我们可以采取一个CPU建立一个进程的策略,这被称为多线程/多进程模式。下面我们来测量其工作效率:
测试指令: ab -n 10000 -c 1000 http://localhost:8000/
返回结果:
Document Path: /
Document Length: 3318 bytes
Concurrency Level: 1000
Time taken for tests: 20.319 seconds
Complete requests: 10000
Failed requests: 0
Write errors: 0
Total transferred: 33590000 bytes
HTML transferred: 33180000 bytes
Requests per second: 492.14 [#/sec] (mean)
Time per request: 2031.939 [ms] (mean)
Time per request: 2.032 [ms] (mean, across all concurrent requests)
Transfer rate: 1614.36 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 160 912.4 0 9008
Processing: 0 28 174.7 12 4738
Waiting: 0 27 174.3 12 4738
Total: 0 188 987.5 13 12758
这是台双核的机器,性能差不多提升了一倍,这就是多进程/多线程模式的威力。按照这个数据外推,在常见的8核处理器上,将达到2000req/sec的处理速度,甚至更高。
当然,也不用高兴太早,我们看一下apache2的每线程模式:
测试指令: ab -n 10000 -c 1000 http://localhost:8000/
返回结果:
Concurrency Level: 1000
Time taken for tests: 6.147 seconds
Complete requests: 10000
Failed requests: 0
Write errors: 0
Total transferred: 3213925 bytes
HTML transferred: 453375 bytes
Requests per second: 1626.87 [#/sec] (mean)
Time per request: 614.678 [ms] (mean)
Time per request: 0.615 [ms] (mean, across all concurrent requests)
Transfer rate: 510.61 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 111 398.3 59 3087
Processing: 28 126 99.0 113 1772
Waiting: 10 104 98.7 92 1745
Total: 78 237 423.1 175 3691
Percentage of the requests served within a certain time (ms)
50% 175
66% 188
75% 207
80% 217
90% 235
95% 256
98% 1016
99% 3222
100% 3691 (longest request)
这种效率在8路的CPU上,性能将达到6400req/sec。所以想用python实现真正高效的前端本身就是个错误的逻辑。
用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
–
与其相濡以沫,不如相忘于江湖