2009年12月22日星期二

replay

发现最近的生活,越来越重复了。怎么说呢?每天早上起来,照例刷牙洗脸,吃饭出门。走在南泉路上,会看到一辆119。拐到兰村路上,会看到一个黑衣MM,对面走过来。人很漂亮,一般是在打手机,否则就是拿手机听歌。前走一点,有个保安在和卖煎饼的聊天,一个抽烟一个翻煎饼,每天的差异差不多就是有客户和没客户而已。再往前,在东方路前会碰到一个辫子MM走出来,辫子很长,从后面看很漂亮。不过记住,如果你不想对世界失去信心,一定要走在她后面。走在前面千万不要回头,无论是有人喊你还是后面有车要70码你。我头次很好奇的回了个头,结果一天都无法正常工作。走到东方路,有八成概率是一堆人在等红灯。恭喜,这代表你会在20秒内碰到绿灯――平均值通常是5秒。
上地铁后就更规律了,如果一堆人在等车,那车上肯定也是一堆人,你如果被挤上去就算是走运了。相反,如果只有小猫两三只,那么车上多半也没有人,慢慢走上去就好。到了世纪大道换车,就可以开电脑放音乐,头一首歌照例是《钢之炼金术士2009》的片头。到了张江,有车停左边和车停右边两个选项。不过没有关系,凡是在进站前停车的,就是左边,否则是右边。因为车要靠右开,停靠右边站台的车在开出时,换到左边(相对他是右边),而我们的车在进站时要靠左,也需要换边。不停车怕又碰到昨天一号线的事故,因此惯例是要停车的。
地铁下来后,有两个红绿灯,第一个灯是20秒横走40秒直行,因此多数会直行过去。第二个灯是50秒横走20秒直行,因此多数会横向过街。联合以上数据计算概率,两次直行的概率是19%,先直行后过街的概率是47.6%,先横后直行的概率是33.3%。没有觉得什么不对?看来又多一个走傻掉的。计算以上概率的方法是分支法,联合概率乘法?我没事先过个街,第二个路口走回来?那不真傻了?
最后,到办公室。一般这时候是王菲在唱《只有我自己》。如果刚唱到"走过千山万水",那说明地铁走的还是挺快的。如果已经到了"失去你,就失去,面对孤独的勇气",那――要么是地铁走的慢了,要么是你走的慢了。

2009年12月7日星期一

论BTchina的倒掉

    BTchina倒了,死的很惨。被广电总局直接勒令关闭,连整改的机会都没有。无疑,大家都知道,VeryCD会紧随其后。无论是非如何,请允许我先向这两家陪伴我多年的网站道声感谢,一路走好。
    就打击这个问题来说,我无疑是赞成的。广电总局打击集中向两个关键词,盗版和非法。无论哪个,都是应该打击的对象。但是这次打击本身却值得怀疑,主要集中在三点,是否允许整改,选择性执法,还有实际效果。
    首先,拿整改问题来说。BTchina和VeryCD做的是下载业务,其中有盗版下载再正常不过。一般打击都是先责令整改,然后再关服务器——虽然我们知道责令也不见什么效果,BTchina肯定改不过来的。不过样子还得做啊。不过从某种意义上说,样子做不做也就是政府内部的事情。无论做不做都是合理合法的,所以这点问题还不大。
    其次,我质疑比较大的一点,就是选择性执法。有个朋友说的比较精辟,每只猫都有他的目的。我总是不惮以最大的恶意来揣测咱们的政府——他们这回想干啥?如果要打击盗版,那问题严重了去了。中国桌面上,十有八九都是盗版Windows。哪怕你买了个笔记本,上面带了个正版Vista,一般也非要装个番茄花园不可。中国人看的电影,听的歌,乃至于用的手机,都是盗版产品。所以从这个角度说,我到真的很愿意国家打击盗版。盗版没了我们的软件才更好卖,盗版没了才有人被逼用Linux,盗版没了我们这行才能赚钱。不过咱得看事实,不能听风就是雨,更不能YY。广电总局要真有决心打击盗版,首先应该奔着卖盗版盘和卖盗版书的去——虽然他们也被互联网下载整的生不如死。问题是,他们没有。
    那,会不会是打击非法音像呢?这话说的更搞笑了。BTchina和VeryCD上有没有色情我不敢说,不过要有也绝对少于新华网。这不是指责新华网有多色情,而是阐述两者身为不同主体的无奈事实。BTchina和VeryCD要是碰到色情门那是沾上就死抡上就亡。作为半国企的新华网,就算偶尔行为出格,只要不出大差错无非就是检讨一下而已。这种情况下,前两家的审查力度,和后一家怎么可能同日而语?实话说,我当初还非常努力的试图在VeryCD上找什么色情资料。只能说他们的版主比较尽责,连疑似的都没有。要打击非法音像,还得找小网站——就是那种准备烧一把就走的。除非碰上严打,否则等查处来的时候,人都没了。
    那我就得怀疑了,广电总局想干啥呢?说打击盗版,不像,说打击非法音像,也不像。这事情我怎么看怎么觉得像上海政府查处黑车,黑车不黑车不重要,重要的是规范正当市场——说白了就是交钱。百度百科也涉嫌抄袭Wikipedia啊,怎么没看广电局找他们麻烦呢?人家3000万春晚赞助可不是白交的。
    那这事靠不靠谱呢?我只能说,越来越不靠谱了。从本心来说,我希望打击盗版,但是从现实而言,无论是美国政府,欧洲诸国政府,还是中国政府,对打击盗版都没啥办法。前两者有海盗党——当然,他们也被人盗版了他们的logo,真是讽刺。后者则有1亿多网民,每天发明各种奇怪的办法。现在的网民水准是越来越高了,或者说软件是越来越傻瓜了。原来下载必须在中心节点上投入大量资金,而且政府一来就玩完了。后来则是中心节点上只要搭个论坛,剩下的自然有P2P软件搞定。现在,根本就没中心节点。DHT的普及,使得去中心化的优势体现的淋漓尽致。Emule里面可以用kad搜索,直接搜索你需要的资源——正常情况下不比VeryCD差。而torrent里面则有个种子市场,可以直接搜索你要的种子。
    好家伙,连服务器都不要了,这次广电局再要下手,只有从网络传输上下手了——中国为了解决轮子问题,为GFW投资了不少钱。不知道广电局是否能收到足够的钱,把这个系统扩大个几倍,把P2P的混淆协议也全概括进去。作为一个老程序员,我劝诫所有的下载者一点。下次再弄的时候,用emule的kad搜索来找你需要的资源,你会发现其实verycd也不是必须的——虽然对他们来说,这是个比广电局查处更不利的消息。

2009年12月6日星期日

用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实现真正高效的前端本身就是个错误的逻辑。