Friday, October 17, 2008

[译]使用Mochiweb构建百万级Comet程序,第一篇

使用Mochiweb构建百万级Comet程序,第一篇

译者: oscar

译者注:作者Richard Jones是Last.fm的创始人之一。原文简单易懂,且最后的留言也颇有参考价值。




在这系列,我将向大家详细描述,我所知道的mochiweb支持那么多的连接的原因。教你们使用mochiweb构建comet程序,每个mochiweb连接都注册在给不同用户分发消息的路由器上。
最后,我会给出一个可以运行的,且能支持百万并发连接的程序。关键之处是,我们需要知道支持那多并发连接需要占用多少内存。

本作:
  • 实现一个简单的comet mochiweb程序,每10秒向客户端发送一条消息。
  • 设置linux内核参数,使其能处理更多的TCP连接。
  • 实现一个压力测试工具(flood-testing tool),不断建立连接(C10K 测试)。
  • 检查每条连接占用的内存。

在续作中,我将介绍如何实现一个实时消息路由系统、降低内存使用的技巧和进一步的测试(10万、100万并发连接)。

我假定,你会使用linux命令行,并且了解Erlang。


编译Mochiweb测试程序

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

这段代码(mochiconntest_web.erl)只是接受连接,并使用块传输(chunked transfer)发送一条初始的欢迎消息,每10秒给每个客户端发送一条。

  1. -module(mochiconntest_web).
  2. -export([start/1, stop/0, loop/2]).
  3. %% External API
  4. start(Options) ->
  5. {DocRoot, Options1} = get_option(docroot, Options),
  6. Loop = fun (Req) ->
  7. ?MODULE:loop(Req, DocRoot)
  8. end,
  9. % we’ll set our maximum to 1 million connections. (default: 2048)
  10. mochiweb_http:start([{max, 1000000}, {name, ?MODULE}, {loop, Loop} | Options1]).

  11. stop() ->
  12. mochiweb_http:stop(?MODULE).

  13. loop(Req, DocRoot) ->
  14. "/" ++ Path = Req:get(path),
  15. case Req:get(method) of
  16. Method when Method =:= ‘GET’; Method =:= ‘HEAD’ ->
  17. case Path of
  18. "test/" ++ Id ->
  19. Response = Req:ok({"text/html; charset=utf-8",
  20. [{"Server","Mochiweb-Test"}],
  21. chunked}),
  22. Response:write_chunk("Mochiconntest welcomes you! Your Id: " ++ Id ++ "\n"),
  23. %% router:login(list_to_atom(Id), self()),
  24. feed(Response, Id, 1);
  25. _ ->
  26. Req:not_found()
  27. end;
  28. ‘POST’ ->
  29. case Path of
  30. _ ->
  31. Req:not_found()
  32. end;
  33. _ ->
  34. Req:respond({501, [], []})
  35. end.

  36. feed(Response, Path, N) ->
  37. receive
  38. %{router_msg, Msg} ->
  39. % Html = io_lib:format("Recvd msg #~w: ‘~s’
    ", [N, Msg]),
  40. % Response:write_chunk(Html);
  41. after 10000 ->
  42. Msg = io_lib:format("Chunk ~w for id ~s\n", [N, Path]),
  43. Response:write_chunk(Msg)
  44. end,
  45. feed(Response, Path, N+1).

  46. %% Internal API
  47. get_option(Option, Options) ->
  48. {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

  1. -module(floodtest).
  2. -export([start/2, timer/2, recv/1]).

  3. start(Filename, Wait) ->
  4. inets:start(),
  5. spawn(?MODULE, timer, [10000, self()]),
  6. This = self(),
  7. spawn(fun()-> loadurls(Filename, fun(U)-> This ! {loadurl, U} end, Wait) end),
  8. recv({0,0,0}).

  9. recv(Stats) ->
  10. {Active, Closed, Chunks} = Stats,
  11. receive
  12. {stats} -> io:format("Stats: ~w\n",[Stats])
  13. after 0 -> noop
  14. end,
  15. receive
  16. {http,{_Ref,stream_start,_X}} -> recv({Active+1,Closed,Chunks});
  17. {http,{_Ref,stream,_X}} -> recv({Active, Closed, Chunks+1});
  18. {http,{_Ref,stream_end,_X}} -> recv({Active-1, Closed+1, Chunks});
  19. {http,{_Ref,{error,Why}}} ->
  20. io:format("Closed: ~w\n",[Why]),
  21. recv({Active-1, Closed+1, Chunks});
  22. {loadurl, Url} ->
  23. http:request(get, {Url, []}, [], [{sync, false}, {stream, self}, {version, 1.1}, {body_format, binary}]),
  24. recv(Stats)
  25. end.

  26. timer(T, Who) ->
  27. receive
  28. after T ->
  29. Who ! {stats}
  30. end,
  31. timer(T, Who).

  32. % Read lines from a file with a specified delay between lines:
  33. for_each_line_in_file(Name, Proc, Mode, Accum0) ->
  34. {ok, Device} = file:open(Name, Mode),
  35. for_each_line(Device, Proc, Accum0).

  36. for_each_line(Device, Proc, Accum) ->
  37. case io:get_line(Device, "") of
  38. eof -> file:close(Device), Accum;
  39. Line -> NewAccum = Proc(Line, Accum),
  40. for_each_line(Device, Proc, NewAccum)
  41. end.

  42. loadurls(Filename, Callback, Wait) ->
  43. for_each_line_in_file(Filename,
  44. fun(Line, List) ->
  45. Callback(string:strip(Line, right, $\n)),
  46. receive
  47. after Wait ->
  48. noop
  49. end,
  50. List
  51. end,
  52. [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: