使用Erlang语言也写一个测试和前面大同小异的测试,在100万个并发连接用户情况下,就是想观察一下极显情况下的表现。
这个测试使用了优秀的Erlang界的明星框架cowboy,加单易用的接口,避免了我们对HTTP栈再次进行闭门造车。
运行在VMWare Workstation 9中,64位Centos 6.4系统,分配14.9G内存左右,双核4个线程,服务器安装Erlang/OTP R16B,最新版本支持异步代码热加载,很赞。
本系统已经提前安装JDK,只需要安装Erlang好了。
安装依赖
源代码安装
#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
)].
|
创建一百万个进程,看看大概花费多少时间。
[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: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: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
数量完全够用了。
+K true | false 是否开启kernel poll,就是epoll;
不开启,测试过程中,在内存完好情况下,经常会有连接失败情况。
一旦你从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头部压根就没有几个。
这里需要牢记,也不能算是BUG,前面的client2.c源码,就未曾设置HTTP Header元数据,需要做些简单修改,修改之后的测试端程序文件名为client5.c, 可以到这里 下载client.c。
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
.
|
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
(
<<
" "
>>
,
Reply
),
cowboy_req
:
chunk
(
gen_output
(
"1::"
),
Reply
).
gen_output
(
String
)
->
DescList
=
io_lib
:
format
(
""
,
[
String
]),
list_to_binary
(
DescList
).
terminate
(
Reason
,
_
Req
,
_
State
)
->
NowCount
=
count_server
:
bye
(),
io
:
format
(
"offline user
~p
:((
~n
"
,
[
NowCount
]).
|
测试过程跌跌撞撞的,虽然这中间因为内存问题抛出若干的异常,但也达到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中的虚拟机器已经达到了它所要求的极限。
完整的源代码,可点击这里下载。