用Mochiweb打造百万级Comet应用,第一部分

来源:互联网 发布:linux tomcat 内存设置 编辑:程序博客网 时间:2024/04/30 12:34
提示:如有转载请注明作者 独舞 及出处

原文:A Million-user Comet Application with Mochiweb, Part 1

参考资料:Comet--基于 HTTP 长连接、无须在浏览器端安装插件的“服务器推”技术为“Comet”

               MochiWeb--建立轻量级HTTP服务器的Erlang库

 

     在这个系列中,我将详述我所发现的mochiweb是怎样支持那么巨大的网络连接的,为大家展示怎样用mochiweb构建一个comet应用,这个应用中每个mochiweb连接都被注册到负责为不同用户派送消息的路由器上。最后我们建立一个能够承受百万并发连接的可工作的应用,更重要的我们将知道这样的应用需要多少内存才能使它跑起来。

 

本部分内容如下:

  • 建立一个基本的comet应用, 它每10秒钟给客户端发送一条消息
  • 调整linux内核参数,使它能够处理大量的TCP连接
  • 写一个能够建立大量网络连接的压力测试工具 (也就是 C10k测试)
  • 检查每个连接到底需要多少内存.

本系列续作将包括怎样建立一个真正的信息路由系统,降低内存使用的技巧,100K和1m并发连接的测试。

基础是你需要知道一些linux命令行操作和一点Erlang知识,否则看不懂别怪我呀,呵呵

写一个Mochiweb测试程序

概括如下:

  1. 安装编译Mochiweb
  2. 运行: /your-mochiweb-path/scripts/new_mochiweb.erl mochiconntest
  3. cd mochiconntest 之后编辑 src/mochiconntest_web.erl

这部分代码(mochiconntest_web.erl)只是接收连接并且每十秒用块传输方式给客户端发送一个初始的欢迎信息。

 

mochiconntest_web.erl

  1. -module(mochiconntest_web).
  2. -export([start/1, stop/0, loop/2]).
  3. %% 外部API
  4. start(Options) ->
  5.     {DocRoot, Options1} = get_option(docroot, Options),
  6.     Loop = fun (Req) ->
  7.                    ?MODULE:loop(Req, DocRoot)
  8.            end,
  9.     % 设置最大连接数为一百万,缺省2048
  10.     mochiweb_http:start([{max, 1000000}, {name, ?MODULE}, {loop, Loop} | Options1]).
  11.  
  12. stop() ->
  13.     mochiweb_http:stop(?MODULE).
  14.  
  15. loop(Req, DocRoot) ->
  16.     "/" ++ Path = Req:get(path),
  17.     case Req:get(method) of
  18.         Method when Method =:= ‘GET’; Method =:= ‘HEAD’ ->
  19.             case Path of
  20.                 "test/" ++ Id ->
  21.                     Response = Req:ok({"text/html; charset=utf-8",
  22.                                       [{"Server","Mochiweb-Test"}],
  23.                                       chunked}),
  24.                     Response:write_chunk("Mochiconntest welcomes you! Your Id: " ++ Id ++ "/n"),
  25.                     %% router:login(list_to_atom(Id), self()),
  26.                     feed(Response, Id, 1);
  27.                 _ ->
  28.                     Req:not_found()
  29.             end;
  30.         ‘POST’ ->
  31.             case Path of
  32.                 _ ->
  33.                     Req:not_found()
  34.             end;
  35.         _ ->
  36.             Req:respond({501, [], []})
  37.     end.
  38.  
  39. feed(Response, Path, N) ->
  40.     receive
  41.         %{router_msg, Msg} ->
  42.         %    Html = io_lib:format("Recvd msg #~w: ‘~s’<br/>", [N, Msg]),
  43.         %    Response:write_chunk(Html);
  44.     after 10000 ->
  45.         Msg = io_lib:format("Chunk ~w for id ~s/n", [N, Path]),
  46.         Response:write_chunk(Msg)
  47.     end,
  48.     feed(Response, Path, N+1).
  49.  
  50. %%内部API
  51. get_option(Option, Options) ->
  52.     {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<br/>
Chunk 1 for id foo<br/>
Chunk 2 for id foo<br/>
Chunk 3 for id foo<br/>
^C

是的,它可以工作。 现在,让我们使劲整它,呵呵。

调整linux内核参数,使它能够处理大量的TCP连接

为节省时间我们需要在进行大量并发连接测试之前调整内核的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中然后运行sysctl -p使其生效。不需要重启,现在你的内核能够处理大量的连接了,yay。

建立大量连接

有很多方法可以用. Tsung就十分好, 也有很多其他比较好的工具如ab, httperf, httpload等等可以生成大量的无用请求。但是它们中任何一款都不适合测试comet应用, 正好我也想找个借口测试一下Erlang的http客户端,因此我写了一个基本的测试程序用以发起大量的连接。
只是因为你可以但并不意味着你就这样做..一个连接就用一个进程确实有点浪费。我用一个进程从文件中调入一批url链接,另一个进程建立连接并接收数据(当定时器的进程每10秒打印一份报告)。所有从服务器接收来的数据都被丢弃,但是它增加计数,这样我们能够跟踪到底有多少http数据块被传输了。

floodtest.erl

  1. -module(floodtest).
  2. -export([start/2, timer/2, recv/1]).
  3.  
  4. start(Filename, Wait) ->
  5.     inets:start(),
  6.     spawn(?MODULE, timer, [10000, self()]),
  7.     This = self(),
  8.     spawn(fun()-> loadurls(Filename, fun(U)-> This ! {loadurl, U} end, Wait) end),
  9.     recv({0,0,0}).
  10.  
  11. recv(Stats) ->
  12.     {Active, Closed, Chunks} = Stats,
  13.     receive
  14.         {stats} -> io:format("Stats: ~w/n",[Stats])
  15.         after 0 -> noop
  16.     end,
  17.     receive
  18.         {http,{_Ref,stream_start,_X}} ->  recv({Active+1,Closed,Chunks});
  19.         {http,{_Ref,stream,_X}} ->          recv({Active, Closed, Chunks+1});
  20.         {http,{_Ref,stream_end,_X}} ->  recv({Active-1, Closed+1, Chunks});
  21.         {http,{_Ref,{error,Why}}} ->
  22.             io:format("Closed: ~w/n",[Why]),
  23.             recv({Active-1, Closed+1, Chunks});
  24.         {loadurl, Url} ->
  25.             http:request(get, {Url, []}, [], [{sync, false}, {stream, self}, {version, 1.1}, {body_format, binary}]),
  26.                 recv(Stats)
  27.     end.
  28.  
  29. timer(T, Who) ->
  30.     receive
  31.     after T ->
  32.         Who ! {stats}
  33.     end,
  34.     timer(T, Who).
  35.  
  36. % Read lines from a file with a specified delay between lines:
  37. for_each_line_in_file(Name, Proc, Mode, Accum0) ->
  38.     {ok, Device} = file:open(Name, Mode),
  39.     for_each_line(Device, Proc, Accum0).
  40.  
  41. for_each_line(Device, Proc, Accum) ->
  42.     case io:get_line(Device, "") of
  43.         eof  -> file:close(Device), Accum;
  44.         Line -> NewAccum = Proc(Line, Accum),
  45.                     for_each_line(Device, Proc, NewAccum)
  46.     end.
  47.  
  48. loadurls(Filename, Callback, Wait) ->
  49.     for_each_line_in_file(Filename,
  50.         fun(Line, List) ->
  51.             Callback(string:strip(Line, right, $/n)),
  52.             receive
  53.             after Wait ->
  54.                 noop
  55.             end,
  56.             List
  57.         end,
  58.         [read], []).
每个连接我们都要用一个临时的端口,每个端口也是一个文件描述符, 缺省情况下这被限制为1024。为了避免Too many open files问题出现,你需要为你当前shell更改这个限制,可以通过修改/etc/security/limits.conf,但是这需要注销再登陆。目前你只需要用sudo修改当前shell就可以了(假如你不想运行在root状态下,调用ulimit后请su回非权限用户):
udo bash
# ulimit -n 999999
# erl

你也可以把临时端口的范围区间增到最大:
# echo "1024 65535" > /proc/sys/net/ipv4/ip_local_port_range

为压力测试程序生成一个url列表文件
( 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).

这将每秒钟建立十个连接 (也就是每个连接100毫秒).

它将以{Active, Closed, Chunks}的形式输出状态信息,Active表示已建立连接数, Closed表示因每种原因被终止的连接数,Chunks是mochiweb以块传输模式处理的数据块数。 Closed应该为0,Chunks应该大于Active,因为每个活跃连接接收多个数据块 (10秒一个)。


10,000个活跃连接的mochiweb进程的固定大小是450MB-也就是每个连接45KB。 CPU占用率就好像预想中的一样微乎其微.

总结

第一次尝试是可以理解的。每个连接45KB内存看起来有些高 - 用libevent再做些调整我可以把它做到将近4.5KB每个连接 (只是猜猜, 谁有这方面的经验请留个回复). 如果就代码量和时间效率上对erlang和c做下考量,我想多花点内存还是有情可原的。


后续中,我将建立一个消息路由器 (我们可以把mochiconntest_web.erl中的25行和41-43行的注释取消)也探讨一下减少内存用量的方法。我也会分享当100k和1M个连接时的测试结果。