Часть 2
Часть 3
В этом цикле статей я поделюсь тем, что я узнал опытным путем о том, как Mochiweb обрабатывает большое количество открытых соединений, и покажу, как создать Comet-приложение, используя Mochiweb, где каждое соединение зарегистрировано в маршрутизаторе. Мы закончим рабочим приложением, которое в состоянии справиться с 1000 000 параллельных соединенией, и узнаем, как много памяти нам для этого потребуется.
В части первой:
• Создание простого Comet — приложение, которое посылает клиентам сообщение каждые 10 секунд.
• Настройка ядра Linux для поддержки большого количества соединений.
• Создание тестирующей утилиты для создания большого количества соединений.
• Определение необходимого количества памяти.
Следующие части этого цикла расскажут, как построить реальную систему, покажут дополнительные уловки, чтобы уменьшить использование памяти, и содержат тесты с 100 000 и 1 000 000 параллельных соединений.
Предполагается, что Вы знакомы с bash, и немного с Erlang.
Вкратце:
1. Установите Mochiweb.
2. Запустите: /your-mochiweb-path/scripts/new_mochiweb.erl mochiconntest
3. cd mochiconntest и измените src/mochiconntest_web.erl
Этот код (mochiconntest_web.erl) только принимает соединения и посылает приветственное сообщение, и одно сообщение каждые 10 секунд каждому клиенту.
По умолчанию mochiweb слушает порт 8000. Если Вы работаете за домашним компьютером, Вы можете проверить с помощью любого веб-браузера: http://localhost:8000/test/foo.
Ниже тест для командной строки:
Да, это работает.
Сэкономим себе немного время и настроим ядро прежде, чем проводить тесты с большим количеством соединений, или Ваш тест не заработает, и Вы будете видеть много «Out of socket memory» сообщений.
Вот sysctl настройки:
Поместим их в/etc/sysctl.conf, затем выполним sysctl -p, чтобы применить их. Нет необходимости перезагружать систему, теперь ядро должно быть в состоянии обработать намного больше открытых соединений.
Есть много способов сделать это. Tsung довольно сексуален, да и так достаточно менее сексуальных способов создания спамма httpd с большим количеством запросов (ab, httperf, httpload и т.д.). Ни один из них идеально не подходит для того, чтобы протестировать наше приложение, поэтому я написал базовый тест на Erlang для этих целей.
Только, потому что мы можем, не означает, что мы должны… Один процесс для каждого подключения определенно был бы затратным. Я использовал один процесс, чтобы загрузить URL из файла, и другой процесс, чтобы установить соединение и получить сообщения от всех http соединений (и один процесс как таймер, чтобы печатать отчет каждые 10 секунд). Все данные, получаемые от сервера, отбрасываются, но счетчик увеличивается, таким образом, мы можем отследить, сколько HTTP сообщений было доставлено.
Каждое соединение, которое мы создаем, требует порта, и по умолчанию их количество ограничено 1024. Чтобы избежать проблем, мы должны изменить ulimit-параметр для оболочки. Это сделать в/etc/security/limits.conf, но требует перезагрузки системы.
Мы также можем увеличить диапазон доступных портов до максимума:
Сгенерируем файл с URL для нашей тестирующей утилиты:
Далее скомпилируем и запустим нашу утилиту с помощью консоли Erlang:
Код будет устанавливать 10 новых соединений в секунду (то есть, 1 соединение каждые 100 мс).
Статистика будет выводится в виде {Active, Closed, Chunks}, где Active — число соединений, в настоящий момент установленных, Closed – число соединение, которые были завершены по некоторым причинам, и Chunks — число сообщений, переданных от Mochiweb. Closed должен остаться на 0, а Chunks должны быть больше, чем Active, потому что каждое активное соединение влечет много сообщений (1 каждые 10 секунд).
Размер Mochiweb процесса с 10 000 активных соединений составлял 450 МБ – это 45 Кб для каждого подключения. Использование CPU на машине было фактически нулевым.
Это было первой попыткой. 45 Кб для каждого подключения кажутся довольно большими – вероятно, можно собрать что-нибудь на C, использующем libevent, который мог проделать похожее, затратив допустим 4.5Кб для каждого подключения (это только предположение, если у кого-либо есть подобный опыт, пожалуйста, оставьте комментарий). Если принять во внимание количестве кода и время, которое потребовалось, чтобы сделать это в Erlang по сравнению с C, я думаю, что увеличенное использованной памяти более простительно.
В будущих статьях я покажу маршрутизатор сообщений (таким образом, мы сможем раскомментировать строки 25 и 41-43 в mochiconntest_web.erl), и расскажу о некоторых способах уменьшения использования памяти. Я также покажу результаты тестирования с 100 000 и 1 000 000 соединений.
Update: Часть 2
Часть 3
В этом цикле статей я поделюсь тем, что я узнал опытным путем о том, как Mochiweb обрабатывает большое количество открытых соединений, и покажу, как создать Comet-приложение, используя Mochiweb, где каждое соединение зарегистрировано в маршрутизаторе. Мы закончим рабочим приложением, которое в состоянии справиться с 1000 000 параллельных соединенией, и узнаем, как много памяти нам для этого потребуется.
В части первой:
• Создание простого Comet — приложение, которое посылает клиентам сообщение каждые 10 секунд.
• Настройка ядра Linux для поддержки большого количества соединений.
• Создание тестирующей утилиты для создания большого количества соединений.
• Определение необходимого количества памяти.
Следующие части этого цикла расскажут, как построить реальную систему, покажут дополнительные уловки, чтобы уменьшить использование памяти, и содержат тесты с 100 000 и 1 000 000 параллельных соединений.
Предполагается, что Вы знакомы с bash, и немного с Erlang.
Создание тестирующего приложения
Вкратце:
1. Установите Mochiweb.
2. Запустите: /your-mochiweb-path/scripts/new_mochiweb.erl mochiconntest
3. 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.
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'
", [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. Если Вы работаете за домашним компьютером, Вы можете проверить с помощью любого веб-браузера: 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 соединений
Сэкономим себе немного время и настроим ядро прежде, чем проводить тесты с большим количеством соединений, или Ваш тест не заработает, и Вы будете видеть много «Out of socket memory» сообщений.
Вот 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, чтобы применить их. Нет необходимости перезагружать систему, теперь ядро должно быть в состоянии обработать намного больше открытых соединений.
Создание большого количества соединений
Есть много способов сделать это. Tsung довольно сексуален, да и так достаточно менее сексуальных способов создания спамма httpd с большим количеством запросов (ab, httperf, httpload и т.д.). Ни один из них идеально не подходит для того, чтобы протестировать наше приложение, поэтому я написал базовый тест на Erlang для этих целей.
Только, потому что мы можем, не означает, что мы должны… Один процесс для каждого подключения определенно был бы затратным. Я использовал один процесс, чтобы загрузить URL из файла, и другой процесс, чтобы установить соединение и получить сообщения от всех http соединений (и один процесс как таймер, чтобы печатать отчет каждые 10 секунд). Все данные, получаемые от сервера, отбрасываются, но счетчик увеличивается, таким образом, мы можем отследить, сколько HTTP сообщений было доставлено.
-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. Чтобы избежать проблем, мы должны изменить ulimit-параметр для оболочки. Это сделать в/etc/security/limits.conf, но требует перезагрузки системы.
$ sudo bash
# ulimit -n 999999
Мы также можем увеличить диапазон доступных портов до максимума:
# 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:
erl> c(floodtest).
erl> floodtest:start("/tmp/mochi-urls.txt", 100).
Код будет устанавливать 10 новых соединений в секунду (то есть, 1 соединение каждые 100 мс).
Статистика будет выводится в виде {Active, Closed, Chunks}, где Active — число соединений, в настоящий момент установленных, Closed – число соединение, которые были завершены по некоторым причинам, и Chunks — число сообщений, переданных от Mochiweb. Closed должен остаться на 0, а Chunks должны быть больше, чем Active, потому что каждое активное соединение влечет много сообщений (1 каждые 10 секунд).
Размер Mochiweb процесса с 10 000 активных соединений составлял 450 МБ – это 45 Кб для каждого подключения. Использование CPU на машине было фактически нулевым.
Выводы.
Это было первой попыткой. 45 Кб для каждого подключения кажутся довольно большими – вероятно, можно собрать что-нибудь на C, использующем libevent, который мог проделать похожее, затратив допустим 4.5Кб для каждого подключения (это только предположение, если у кого-либо есть подобный опыт, пожалуйста, оставьте комментарий). Если принять во внимание количестве кода и время, которое потребовалось, чтобы сделать это в Erlang по сравнению с C, я думаю, что увеличенное использованной памяти более простительно.
В будущих статьях я покажу маршрутизатор сообщений (таким образом, мы сможем раскомментировать строки 25 и 41-43 в mochiconntest_web.erl), и расскажу о некоторых способах уменьшения использования памяти. Я также покажу результаты тестирования с 100 000 и 1 000 000 соединений.
Update: Часть 2