Как стать автором
Обновить

Comet–приложение для Mochiweb c нагрузкой в 1 000 000 пользователей. Часть 1/3

Время на прочтение6 мин
Количество просмотров7.5K
Автор оригинала: Richard Jones
Часть 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
Теги:
Хабы:
+50
Комментарии67

Публикации

Изменить настройки темы

Истории

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн