第一篇
这个系列中我将详细介绍我发现的利用mochiweb如何实现海量连接的经验及演示如何建立一个使用Mochiweb的Comet应用程序 ,每个mochiweb的连接由一个给不同用户派发消息的路由器注册的。我们完成的一个可运行的应用程序能够应付100万的并发连接,并且重要的是,我们还能知道需要多少内存使他正常工作。
在第一篇中:
- 构建一个每隔10秒给客户发送消息的基本mochiweb comet应用程序。
- 调整Linux内核,使之能处理大量TCP的连接。
- 构建一个防洪测试工具打开海量连接
- 检查每个连接需要多少内存。
这个系列中后续的连载将覆盖如何构建一个实际的消息路由系统和节省内存开销的技巧,以及10万到100万之间的并发连接测试。
我假定你已经熟悉Linux的命令行指令,另外还已经了解一些Erlang的知识。
构建一个Mochiweb测试应用程序
简介:
- 安装和构建 Mochiweb
- 运行: /your-mochiweb-path/scripts/new_mochiweb.erl mochiconntest
- cd mochiconntest 然后编辑 src/mochiconntest_web.erl
这些代码 (mochiconntest_web.erl)只是接受连接和使用数据块传输用于发送的初始欢迎消息另外对每个客户端每隔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’<br/>", [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端口监听所有接口,如果你正在桌面上工作,你可以使用任何一个Web浏览器,只要浏览 http://localhost:8000/test/foo.
以下是测试命令行代码
$ lynx --source "http://localhost:8000/test/foo"
Mochiconntest welcomes you! Your Id: foo<br/>
Chunk 1 for id foo<br/>
Chunk 2 for id foo<br/>
Chunk 3 for id foo<br/>
^C
正确运行了,接下来我们让他受点痛苦
为大量tcp连接调整Linux的内核
在测试海量连接之前要留点时间来调整内核的tcp设置,否则你的测试将会失败,你会看到大量Out of socket memory 的消息(如果你情愿,nf_conntrack: table full, dropping packet.)
以下是我完成的sysctl的设置,这只是可行方法之一,可能还有其他的做法
# 调整通用千兆位:
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
# 以下让内核给更多的内存容量给tcp
# 为了你需要的大量 (100k+) 打开的 socket 连接
net.ipv4.tcp_mem = 50576 64768 98152
net.core.netdev_max_backlog = 2500
net.ipv4.netfilter.ip_conntrack_max = 1048576
把以上内容存到 /etc/sysctl.conf 然后运行 sysctl -p 来应用这些配置. 不必重起, 现在你的内核将能处理更多的连接, yay.
创建海量连接
有很多方法实现这个功能。 Tsung 是相当强的, 并有许多其他不太好的发送一个垃圾的有大量请求的httpd(ab, httperf , httpload等) 。 没有一个能理想符合一个彗星应用程序的测试,我一直试图用Erlang实现一个HTTP客户端解决方案,所以我写了一个创建大量连接的基本测试。只是因为可以这样做而不是必须这样做.. 这里每个连接对应一个进程是明显的资源浪费.我使用一个进程从文件加载网址,另一个进程,建立和接收所有HTTP连接发送的消息(及一个每隔10秒打印一次报告的定时器进程), 所有从服务器收到的数据被丢弃,但它会增加计数器的计数,这样我们可以跟踪有多少个HTTP数据块被交付了。
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], []).
每个连接,我们需要请求一个临时的端口,即文件描述符,并且默认情况下最多打开1024 个。为了避免打开太多文件的问题你需要修改你shell文件的ulimit值。这可以通过修改/etc/security/limits.conf,但是需要一次退出/登陆操作。现在您可以只要sudo和修改当前的shell文件(当你完成修改ulimit后,如果你不想以root身份运行,使用su回到您的非特权用户) :
$ sudo bash
# ulimit -n 999999
# erl
您也可能设置临时端口的范围增加的太大,会限制在最大65535 。
# echo "1024 65535" > /proc/sys/net/ipv4/ip_local_port_range
给防洪测试程序生成一个网址文件
( for i in `seq 1 10000`; do echo "http://localhost:8000/test/$i" ; done ) > /tmp/mochi-urls.txt
从 erlang 提示符下你可以编译和启动 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秒一个),
有10,000个活跃连接的Mochiweb beam 进程的常驻内存容量大小为450MB-即每个连接45KB.正如预期所料CPU的利用率几乎没有。
最后总结
这是一个合理的首次尝试。每个连接45KB似乎有点高-用c或许可以做到每个连接接近4.5KB(只是一个猜测,如果任何人有经验,请发表评论) 。如果您用c和erlang比较实现这个功能的话,考虑代码量和编码时间的因素,我想增加内存开销更加情有可原。
在后续的连载中我将覆盖构建一个消息路由(这可以通过去掉在mochiconntest_web.erl的25行及41-43 行的注释来实现)另外还将讨论一些方法用于减少整体的内存开销。我还会分享对有100k和1M数量级连接的测试结果。