• 100万并发连接服务器笔记之Erlang完成1M并发连接目标


    前言

    使用Erlang语言也写一个测试和前面大同小异的测试,在100万个并发连接用户情况下,就是想观察一下极显情况下的表现。
    这个测试使用了优秀的Erlang界的明星框架cowboy,加单易用的接口,避免了我们对HTTP栈再次进行闭门造车。

    测试Erlang服务器

    运行在VMWare Workstation 9中,64位Centos 6.4系统,分配14.9G内存左右,双核4个线程,服务器安装Erlang/OTP R16B,最新版本支持异步代码热加载,很赞。

    下载安装

    本系统已经提前安装JDK,只需要安装Erlang好了。

    安装依赖

    
    
    yum install build-essential m4 yum install openssl yum install openssl-devel yum install unixODBC yum install unixODBC-devel yum -y install openssl make gcc gcc-c++ kernel-devel m4 ncurses-devel openssl-devel yum install xsltproc fop

    源代码安装

    #wget https://elearning.erlang-solutions.com/binaries/sources/otp_src_R16B.tar.gz
    #tar xvf otp_src_R16B.tar.gz
    cd otp_src_R16B
    ./configure --prefix=/usr/local/erlang --enable-hipe --enable-threads --enable-smp-support --enable-kernel-poll
    make
    make install
    

    添加到环境变量中(/etc/profile)

    export ERL_HOME=/usr/local/erlang export PATH=$ERL_HOME/bin:$PATH
    

    保存生效

    source /etc/profile
    

    测试进程创建

    这里拷贝《Erlang程序设计》一书提供的processes.erl源码,稍作修改。

    1234567891011121314151617181920212223
    -module(processes).
    -export([max/1]).
     
    max(N) ->
    Max = erlang:system_info(process_limit),
    io:format("Maxmium allowed process is ~p ~n", [Max]),
    statistics(runtime),
    statistics(wall_clock),
    L = for(1, N, fun() -> spawn(fun() -> wait() end) end),
    {_, Time1} = statistics(runtime),
    {_, Time2} = statistics(wall_clock),
    lists:foreach(fun(Pid) -> Pid ! die end, L),
    U1 = Time1 * 1000 / N,
    U2 = Time2 * 1000 /N,
    io:format("Process spawn time=~p (~p) microseconds ~n", [U1, U2]).
     
    wait() ->
    receive
    die -> void
    end.
     
    for(N, N, F) -> [F()];
    for(I, N, F) -> [F()|for(I+1, N, F)].
    view rawprocesses.erl hosted with ❤ by GitHub

    创建一百万个进程,看看大概花费多少时间。

    [yongboy@base erlang]$ erl +P 10240000
    Erlang R16B (erts-5.10.1) [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]
    Eshell V5.10.1 (abort with ^G)
    1> processes:max(200000).
    Maxmium allowed process is 16777216 Process spawn time=1.1 (2.68) microseconds ok
    2> processes:max(200000).
    Maxmium allowed process is 16777216 Process spawn time=1.7 (2.33) microseconds ok
    3> processes:max(200000).
    Maxmium allowed process is 16777216 Process spawn time=1.55 (2.12) microseconds ok
    4> processes:max(1000000).
    Maxmium allowed process is 16777216 Process spawn time=2.97 (3.967) microseconds ok
    5> processes:max(1000000).
    Maxmium allowed process is 16777216 Process spawn time=2.4 (2.729) microseconds ok
    6> processes:max(1000000).
    Maxmium allowed process is 16777216 Process spawn time=2.19 (2.735) microseconds ok
    7> processes:max(10000000).
    Maxmium allowed process is 16777216 Process spawn time=3.328 (4.2777) microseconds ok
    8> processes:max(10000000).
    Maxmium allowed process is 16777216 Process spawn time=3.144 (3.1361) microseconds ok
    9> processes:max(10000000).
    Maxmium allowed process is 16777216 Process spawn time=3.394 (3.2051) microseconds ok
    

    恩,创建1000万个进程,每一个进程花费3.4微秒(μs)的CPU时间,相当于4.3微秒(μs)的消耗时间(亦即消耗的真实时间),在一定量的区间内,其值变化,可以看做是一个常量。

    初始化小问题

    Cowboy初始化需要事项

    我们做的简单程序,使用了非常受欢迎的cowboy框架,其启动函数:

    cowboy:start_http(my_http_listener, 100,
    [{port, 8000}],
    [{env, [{dispatch, Dispatch}]}]
    ),
    

    Cowboy默认支持1024个连接,服务器端输出:

    online user 1122 online user 1123 

    停滞于此,后续的连接只能排队等候了。

    这里设置支持无限个连接好了。

    {max_connections, infinity}
    

    话说,Cowboy为Erlang世界的明星产品,绝对值得一试!

    Erlang默认创建进程限制

    Erlang在我的机器上默认允许创建的线程也是有限的:

    erlang:system_info(process_limit).
    262144
    

    才26万个,不够用。在启动脚本(start.sh)处添加允许创建的最大线程支持:

    #!/bin/sh
    erl +K true +P 10240000 -sname testserver -pa ebin -pa deps/*/ebin -s htmlfilesimple
    -eval "io:format("Server start with port 8000 Success!~n")."

    脚本启动后现在在erl shell中测试一下:

    erlang:system_info(process_limit).
    16777216
    

    数量完全够用了。

    开启erlang的epoll属性

    +K true | false 是否开启kernel poll,就是epoll;
    

    不开启,测试过程中,在内存完好情况下,经常会有连接失败情况。

    使用cowboy_req:compact降低内存占用

    一旦你从request对象中获取到足够的信息,以后不再获取其附加属性时,调用compact/1函数可去除无用属性,起到节省内存作用。 
    程序里面调用如下:

    init(_Any, Req, State) -> NowCount = count_server:welcome(),
    io:format("online user ~p :))~n", [NowCount]),
    output_first(Req),
    Req2 = cowboy_req:compact(Req),
    {loop, Req2, State, hibernate}.
    

    在本例中精测压缩内存效果不明显,因为 ,测试端输出的HTTP头部压根就没有几个。

    Cowboy无法处理没有header的HTTP请求

    这里需要牢记,也不能算是BUG,前面的client2.c源码,就未曾设置HTTP Header元数据,需要做些简单修改,修改之后的测试端程序文件名为client5.c, 可以到这里 下载client.c

    Cowboy处理长连接

    Cowboy很贴心的提供了cowboy_loop_handler behaviour。在init/3函数中,可以进入休眠状态,节省内存,消息到达时,被唤醒,值得一赞! 其定义如下:

    123456789101112131415161718192021222324252627
    -module(cowboy_loop_handler).
     
    -type opts() :: any().
    -type state() :: any().
    -type terminate_reason() :: {normal, shutdown}
    | {normal, timeout}
    | {error, atom()}.
     
    %% 处理用户第一次请求,可以处理请求,为当前会话生成状态数据,进行等待或者休眠,或者关闭掉当前会话。
    -callback init({atom(), http}, Req, opts())
    -> {ok, Req, state()}
    | {loop, Req, state()}
    | {loop, Req, state(), hibernate}
    | {loop, Req, state(), timeout()}
    | {loop, Req, state(), timeout(), hibernate}
    | {shutdown, Req, state()}
    | {upgrade, protocol, module()}
    | {upgrade, protocol, module(), Req, opts()}
    when Req::cowboy_req:req().
    %% init若返回loop状态信息,等待接收消息,可以继续接收,或者继续休眠
    -callback info(any(), Req, State)
    -> {ok, Req, State}
    | {loop, Req, State}
    | {loop, Req, State, hibernate}
    when Req::cowboy_req:req(), State::state().
    %% 会话终端时被调用,可执行会话清理工作
    -callback terminate(terminate_reason(), cowboy_req:req(), state()) -> ok.
    注意hibernatetimeout参数,按照实际需求返回即可。

    htmlfile_handler示范代码如下:

    1234567891011121314151617181920212223242526272829303132
    -module(htmlfile_handler).
    -behaviour(cowboy_loop_handler).
    -export([init/3, info/3, terminate/3]).
    -define(HEARBEAT_TIMEOUT, 20*1000).
    -record(status, {count=0}).
     
    init(_Any, Req, State) ->
    NowCount = count_server:welcome(),
    io:format("online user ~p~n", [NowCount]),
    output_first(Req),
    %%Req2 = cowboy_req:compact(Req),
    {loop, Req, State, hibernate}.
     
    %% POST/Short Request
    info(_Any, Req, State) ->
    {loop, Req, State, hibernate}.
     
    output_first(Req) ->
    {ok, Reply} = cowboy_req:chunked_reply(200, [{<<"Content-Type">>, <<"text/html; charset=utf-8">>},
    {<<"Connection">>, <<"keep-alive">>}], Req),
    cowboy_req:chunk(<<"<html><body><script>var _ = function (msg) { parent.s._(msg, document); };</script> ">>,
    Reply),
    cowboy_req:chunk(gen_output("1::"), Reply).
     
    gen_output(String) ->
    DescList = io_lib:format("<script>_('~s');</script>", [String]),
    list_to_binary(DescList).
     
    terminate(Reason, _Req, _State) ->
    NowCount = count_server:bye(),
    io:format("offline user ~p :(( ~n", [NowCount]).
    view rawhtmlfile_handler.erl hosted with ❤ by GitHub

    100万并发连接达成

    测试过程跌跌撞撞的,虽然这中间因为内存问题抛出若干的异常,但也达到100W连接的数量

    online user 1022324 :))
    online user 1022325 :))
    online user 1022326 :))
    online user 1022327 :))
    online user 1022328 :))
    online user 1022329 :))
    online user 1022330 :))
    online user 1022331 :))
    online user 1022332 :))
    online user 1022333 :))
    online user 1022334 :))
    online user 1022335 :))
    online user 1022336 :))
    online user 1022337 :))
    online user 1022338 :))
    

    可以看到状态信息 

    算一下: 14987952K = 14636M 
    14987952/1022338 = 14.7K/Connection

    未启动时的内存情况:

     total used free shared buffers cached
    Mem: 14806 245 14561 0 12 60
    -/+ buffers/cache: 172 14634
    Swap: 3999 0 3999
    

    启动后的内存占用情况:

     total used free shared buffers cached
    Mem: 14806 435 14370 0 12 60
    -/+ buffers/cache: 363 14443
    Swap: 3999 0 3999
    

    用户量达到1022338数量后的内存一览:

     total used free shared buffers cached
    Mem: 14806 14641 165 0 1 5
    -/+ buffers/cache: 14634 172
    Swap: 3999 1068 2931
    

    可以看到,当前内存不够用了,需要虚拟内存配合了。

    查看一下当前进程的内存占用

    ps -o rss= -p `pgrep -f 'sname testserver'`
    4869520
    

    这样算起来,系统为每一个进程持有 4869520/1022338 = 4.8K 内存。 这个值只是计算物理内存,实际上连虚拟内存都占用了,估计在4.8K-6.8K之间吧。

    和C语言相比,内存占用相当大,我虚拟机器分配的15G内存,也仅仅处理达到100万的连接,已经接近极限时,会发现陆陆续续的有连接失败。

    不得不说的代码热加载

    运行时系统的代码热加载功能,在这个实例中,通过vi修改了htmlfile_handler.erl文件,主要修改内容如下:

    io:format("online user ~p :))~n", [NowCount]),
    ......
    io:format("offline user ~p :(( ~n", [NowCount]).
    

    执行make,编译

    [root@base htmlfilesimple]# make
    ==> ranch (get-deps)
    ==> cowboy (get-deps)
    ==> htmlfilesimple (get-deps)
    ==> ranch (compile)
    ==> cowboy (compile)
    ==> htmlfilesimple (compile)
    src/htmlfile_handler.erl:5: Warning: record status is unused
    src/htmlfile_handler.erl:29: Warning: variable 'Reason' is unused
    Compiled src/htmlfile_handler.erl
    

    很好,非常智能的rebar,自动只编译了htmlfile_handler.erl一个文件,然后通知Erlang的运行环境进行代码热替换吧。

    (testserver@base)4> code:load_file(htmlfile_handler). 

    查看日志输出控制台,可以看到已经生效,同时也保存着到状态数据等。

    非常利于运行时调试,即不伤害在线状态数据,又能即时修改,赞!但生产环境下,一般都是版本切换,OTP的版本切换,测试或马上修改bug时,着实有些复杂。

    小结

    和C相比,处理相同的事情(100万并发连接),及其简单,但Erlang会需要更多的内存,廉价的内存可以满足,只是我的搭建在Vmware中的虚拟机器已经达到了它所要求的极限。 
    完整的源代码,可点击这里下载

  • 相关阅读:
    Spring5.2.x02日志体系
    【cdh6.3.2】阿里云盘
    日本产业大溃败:拥有无数世界第一,却被全世界抛弃
    Silverlight+WCF部署到IIS
    Silverlight部署常见问题
    html中调用js脚本是加入defer="defer" 作用
    ie6 ie7 ie8 ie9 ff css 区别
    jupyter notebook 安装nbextensions后不显示
    用flash导出动画时提示 “java运行时环境初始化时出现错误,您可能需要重新安装flash"
    Stylish 样式
  • 原文地址:https://www.cnblogs.com/hzcya1995/p/13318386.html
Copyright © 2020-2023  润新知