http://ro4tub.blogspot.com/2008/10/mochiwebcomet.html
译者: oscar
译者注:作者Richard Jones是Last.fm的创始人之一。原文简单易懂,且最后的留言也颇有参考价值。
在这系列,我将向大家详细描述,我所知道的mochiweb支持那么多的连接的原因。教你们使用mochiweb构建comet程序,每个mochiweb连接都注册在给不同用户分发消息的路由器上。
最后,我会给出一个可以运行的,且能支持百万并发连接的程序。关键之处是,我们需要知道支持那多并发连接需要占用多少内存。
本作:
- 实现一个简单的comet mochiweb程序,每10秒向客户端发送一条消息。
- 设置linux内核参数,使其能处理更多的TCP连接。
- 实现一个压力测试工具(flood-testing tool),不断建立连接(C10K 测试)。
- 检查每条连接占用的内存。
在续作中,我将介绍如何实现一个实时消息路由系统、降低内存使用的技巧和进一步的测试(10万、100万并发连接)。
我假定,你会使用linux命令行,并且了解Erlang。
编译Mochiweb测试程序
简而言之:
- 安装、编译Mochiweb
- 运行: /your-mochiweb-path/scripts/new_mochiweb.erl mochiconntest
- cd mochiconntest 然后编辑src/mochiconntest_web.erl
这段代码(mochiconntest_web.erl)只是接受连接,并使用块传输(chunked transfer)发送一条初始的欢迎消息,每10秒给每个客户端发送一条。
-
- module (mochiconntest_web ).
-
- export ( [start/ 1, stop/ 0, loop/ 2 ] ).
-
%% External API
-
start ( Options ) ->
-
{ DocRoot, Options1 } = get_option (docroot, Options ),
-
Loop = fun ( Req ) ->
-
? MODULE: loop ( Req, DocRoot )
-
end,
-
% we’ll set our maximum to 1 million connections. (default: 2048)
-
mochiweb_http: start ( [ {max, 1000000 }, {name, ? MODULE }, {loop, Loop } | Options1 ] ).
-
-
stop ( ) ->
-
mochiweb_http: stop (? MODULE ).
-
-
loop ( Req, DocRoot ) ->
-
"/" ++ Path = Req: get (path ),
-
case Req: get (method ) of
-
Method when Method =:= ‘GET’; Method =:= ‘HEAD’ ->
-
case Path of
-
"test/" ++ Id ->
-
Response = Req: ok ( { "text/html; charset=utf-8",
-
[ { "Server", "Mochiweb-Test" } ],
-
chunked } ),
-
Response: write_chunk ( "Mochiconntest welcomes you! Your Id: " ++ Id ++ "/n" ),
-
%% router:login(list_to_atom(Id), self()),
-
feed ( Response, Id, 1 );
-
_ ->
-
Req: not_found ( )
-
end;
-
‘POST’ ->
-
case Path of
-
_ ->
-
Req: not_found ( )
-
end;
-
_ ->
-
Req: respond ( { 501, [ ], [ ] } )
-
end.
-
-
feed ( Response, Path, N ) ->
-
receive
-
%{router_msg, Msg} ->
-
% Html = io_lib:format("Recvd msg #~w: ‘~s’
", [N, Msg]), -
% Response:write_chunk(Html);
-
after 10000 ->
-
Msg = io_lib: format ( "Chunk ~w for id ~s/n", [ N, Path ] ),
-
Response: write_chunk ( Msg )
-
end,
-
feed ( Response, Path, N +1 ).
-
-
%% Internal API
-
get_option ( Option, Options ) ->
-
{proplists: get_value ( Option, Options ), proplists: delete ( Option, Options ) }.
启动你的mochiweb程序
make && ./start-dev.sh
默认情况下,mochiweb在所有网卡的8000端口上侦听。如果你在桌面环境下工作,那么你可以打开任何浏览
器进行测试,输入http://localhost:8000/test/foo。
而命令行测试如下:
$ lynx --source "http://localhost:8000/test/foo"
Mochiconntest welcomes you! Your Id: foo
Chunk 1 for id foo
Chunk 2 for id foo
Chunk 3 for id foo
^C
好的,让我们继续吧。
设置Linux内核参数,以支持更多连接
为节省你的时间,在进行大规模测试之前,我们先调节内核的tcp设置。否则,你的测试将会失败,你会看到许
多『out of socket memory』的消息(最后将是nf_conntrack: table
full, dropping packet.)
下面是最终的sysctl设置(你的配置可能不一样,但应该差不多):
# General gigabit tuning:
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
net.ipv4.tcp_syncookies = 1
# this gives the kernel more memory for tcp
# which you need with many (100k+) open socket connections
net.ipv4.tcp_mem = 50576 64768 98152
net.core.netdev_max_backlog = 2500
# I was also masquerading the port comet was on, you might not need this
net.ipv4.netfilter.ip_conntrack_max = 1048576
在/etc/sysctl.conf中配置,然后运行-p。不需要重启电脑,现在你的内核可以处理大量的连接了。
创建大量连接
有多种实现方法。Tsung 是其中最为不错的,其他不错的还有ab、httperf、httpload等等。不过对于测试
comet程序,们都显得不那么理想化。正好我也想找个借口尝试一下Erlang http客户端,所以我写了个简单
的测试程序,进行大量的连接。
在这里一个连接一个进程确实有点浪费。所以,我用一个进程从文件中加载URL,另一个进程建立连接、接收
数据(还有一个进程每10秒定时打印报告)。
服务器不处理接收到的数据,直接丢弃,但是会增加计数器,所以我们可以跟踪http块(HTTP chunks)的传输
量。
floodtest.erl
-
- module (floodtest ).
-
- export ( [start/ 2, timer/ 2, recv/ 1 ] ).
-
-
start ( Filename, Wait ) ->
-
inets: start ( ),
-
spawn (? MODULE, timer, [ 10000, self ( ) ] ),
-
This = self ( ),
-
spawn (fun ( )-> loadurls ( Filename, fun ( U )-> This ! {loadurl, U } end, Wait ) end ),
-
recv ( { 0, 0, 0 } ).
-
-
recv ( Stats ) ->
-
{ Active, Closed, Chunks } = Stats,
-
receive
-
{stats } -> io: format ( "Stats: ~w/n", [ Stats ] )
-
after 0 -> noop
-
end,
-
receive
-
{http, {_ Ref,stream_start,_ X } } -> recv ( { Active +1, Closed, Chunks } );
-
{http, {_ Ref,stream,_ X } } -> recv ( { Active, Closed, Chunks +1 } );
-
{http, {_ Ref,stream_end,_ X } } -> recv ( { Active -1, Closed +1, Chunks } );
-
{http, {_ Ref, {error, Why } } } ->
-
io: format ( "Closed: ~w/n", [ Why ] ),
-
recv ( { Active -1, Closed +1, Chunks } );
-
{loadurl, Url } ->
-
http: request (get, { Url, [ ] }, [ ], [ {sync, false }, {stream, self }, {version, 1.1 }, {body_format, binary } ] ),
-
recv ( Stats )
-
end.
-
-
timer ( T, Who ) ->
-
receive
-
after T ->
-
Who ! {stats }
-
end,
-
timer ( T, Who ).
-
-
% Read lines from a file with a specified delay between lines:
-
for_each_line_in_file ( Name, Proc, Mode, Accum0 ) ->
-
{ok, Device } = file: open ( Name, Mode ),
-
for_each_line ( Device, Proc, Accum0 ).
-
-
for_each_line ( Device, Proc, Accum ) ->
-
case io: get_line ( Device, "" ) of
-
eof -> file: close ( Device ), Accum;
-
Line -> NewAccum = Proc ( Line, Accum ),
-
for_each_line ( Device, Proc, NewAccum )
-
end.
-
-
loadurls ( Filename, Callback, Wait ) ->
-
for_each_line_in_file ( Filename,
-
fun ( Line, List ) ->
-
Callback (string: strip ( Line, right, $/n ) ),
-
receive
-
after Wait ->
-
noop
-
end,
-
List
-
end,
-
[read ], [ ] ).
我 们建立的每个连接都需要一个端口,即文件描述符(file descriptor)。默认情况下,文件描述符的上限为1024。为了避免『too many open files』问题,你需要修改ulimit,可以在/etc/security/limits.conf中
修改,但是需要重启。现在你可以sudo,修改当前的shell:
$ sudo bash
# ulimit -n 999999
# erl
你也需要增加端口范围:
# echo "1024 65535" > /proc/sys/net/ipv4/ip_local_port_range
为floodtest程序批量生成URL:
( for i in `seq 1 10000`; do echo "http://localhost:8000/$i" ; done ) >
/tmp/mochi-urls.txt
现在你可以编译、运行floodtest.erl:erl> c(floodtest).
erl> floodtest:start("/tmp/mochi-urls.txt", 100).
它将每秒建立10条新的连接(例如,每100毫秒一条连接)。
它将以{Active, Closed, Chunks}格式输出状态信息,其中Active是并发连接的数目,Closed是由于某种
原因断开的连接,Chunks是mochiweb以块方式传输的块数目。Closed应该为0,Chunks应该大于Active,因
为每个活跃的连接将接收多个块(每10秒1个)。
1万个活跃连接占用450MB的内存,也就是说每个连接45KB。CPU占用率几乎没有。
现阶段总结
每个连接占用45KB内存,似乎有点高。用C + libevent库,我想我可以做到每连接占用4.5KB(只是个猜想,
谁有这方面的经历,请讲讲)。如果从代码量和实现时间比较Erlang和C,我想多一点内存消耗是可以理解的。
在之后的文章中,我将实现一个消息路由(取消mochiconntest_web.erl的25行和41-43行的注释),讨论一些降低内存使用量的方法。
我也会和大家一起分享10万和100万连接的结果。