译者: 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万连接的结果。
No comments:
Post a Comment