asio и блокировка вызывающего
От: niXman Ниоткуда https://github.com/niXman
Дата: 03.04.13 06:34
Оценка:
приветствую!

есть самописный класс сокета поверх asio.
предвидя вопросы типа "а зачем оно надо?", объясняю суть: внутренняя обработка очереди задач. т.е. в пользовательской коде можно несколько раз вызывать socket::write(..., handler)/socket::read(..., handler), и, при этом, сокет сам будет выполнять чтение/запись при завершении предыдущей задачи. сокет работает отлично(проверенно в нескольких проектах, в том числе и коммерческих). на самом деле, за мои несколько лет работы разработчиком протоколов/серверов, собралась некоторая порция кода, который был унифицирован, и который используется как "шаблон" построения серверов.
сейчас, подумываю выложить этот код на опенсорс, и занялся рефаком, чтоб стереть схожесть с коммерческими проектами, в которых этот код использовался.

вопрос в следующем:
сейчас, при использовании этого сокета есть вероятность того, что очередь может вырасти до очень больших размеров, ибо нет ограничения на размер очереди.
идей решения у меня две:
1. методы socket::write(..., handler)/socket::read(..., handler) изменить так, чтоб они возвращали true в случае успешного добавления задачи, и false если очередь переполнена.
2. socket::write(..., handler)/socket::read(..., handler) изменить так, чтоб они блокировали вызывающего ровно на одну задачу(io_service::run_one()). (неважно какую, ибо io_service может быть один для тысяч сокетов(и не только))
3. дополнительный аргумент(установленный в какое-то значение по умолчанию), который и будет означать, как вести себя при переполнении: пункт 1, пункт 2.

и я немогу решить, какой из способов более предпочтительным окажется.
я склоняюсь ко второму. но тут мне не нравится то, что поведение при переполнении очереди, навязывается.
наверное все же третий...или кто-то предложит четвертый...

благодарен за мысли.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Re: asio и блокировка вызывающего
От: niXman Ниоткуда https://github.com/niXman
Дата: 03.04.13 06:36
Оценка:
Здравствуйте, niXman, Вы писали:

X>(неважно какую, ибо io_service может быть один для тысяч сокетов(и не только))

глупость же =)
очередью владеет сокет, а не io_service. тогда возможность реализации второго пункта, под вопросом...
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Re: asio и блокировка вызывающего
От: Abyx Россия  
Дата: 03.04.13 08:16
Оценка:
Здравствуйте, niXman, Вы писали:

X>вопрос в следующем:

X>сейчас, при использовании этого сокета есть вероятность того, что очередь может вырасти до очень больших размеров, ибо нет ограничения на размер очереди.
X>идей решения у меня две:
X>1. методы socket::write(..., handler)/socket::read(..., handler) изменить так, чтоб они возвращали true в случае успешного добавления задачи, и false если очередь переполнена.
это называется "возврат кода ошибки".
некоторые люди считают что исключения все же лучше.

X>2. socket::write(..., handler)/socket::read(..., handler) изменить так, чтоб они блокировали вызывающего ровно на одну задачу(io_service::run_one()). (неважно какую, ибо io_service может быть один для тысяч сокетов(и не только))

если нужна асинхронность (wait-free) то это очевидно не вариант.
тогда придется писать еще одну обертку поверх этой обертки

тут не хватает use-case.
если очередь переполнилась, это означает что клиент перестал принимать данные.
т.е. либо он повис, либо намеренно делает DoS атаку.
может конечно у юзера очень медленное соединение, но серверу-то от этого не легче.
в любом случае, если очередь переполнилась, клиента надо дисконнектить.
In Zen We Trust
Re[2]: asio и блокировка вызывающего
От: niXman Ниоткуда https://github.com/niXman
Дата: 03.04.13 08:23
Оценка:
Здравствуйте, Abyx, Вы писали:

A>это называется "возврат кода ошибки".

тут нет ошибки. false в данном случае лишь сигнализирует о том, что очередь полна. но сокет от этого не ломается, он успешно может читать/писать данные, просто медленнее чем ответная сторона.

A>некоторые люди считают что исключения все же лучше.

тем более это не исключительная ситуация.

X>>2. socket::write(..., handler)/socket::read(..., handler) изменить так, чтоб они блокировали вызывающего ровно на одну задачу(io_service::run_one()). (неважно какую, ибо io_service может быть один для тысяч сокетов(и не только))

A>если нужна асинхронность (wait-free) то это очевидно не вариант.
...
A>тут не хватает use-case.

так а третий вариант?
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Re[3]: asio и блокировка вызывающего
От: niXman Ниоткуда https://github.com/niXman
Дата: 03.04.13 08:24
Оценка:
Здравствуйте, niXman, Вы писали:

X>но сокет от этого не ломается, он успешно может читать/писать данные, просто медленнее чем ответная сторона.

т.е. так: "но сокет от этого не ломается, он успешно может читать/писать данные, просто медленнее, из-за ответной стороны."
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Re[2]: asio и блокировка вызывающего
От: niXman Ниоткуда https://github.com/niXman
Дата: 03.04.13 08:39
Оценка:
Здравствуйте, Abyx, Вы писали:

A>т.е. либо он повис, либо намеренно делает DoS атаку.

похоже, но не факт.
к примеру, мой алгоритм производит данные, и производит он их быстрее, чем аппаратура/канал может передать. в этом случае, к примеру, если алгоритм произведет 400 порций данных, что я в таком же порядке зову socket::write() 400 раз, и не заморачиваюсь. если при отправке данных произойдет ошибка — хендлер узнает об этом, и в нем я могу сохранить данные в sqlite к примеру, очистить очередь сокета, и при повторном соединении — попытаться передать оставшиеся данные.

A>может конечно у юзера очень медленное соединение, но серверу-то от этого не легче.

проверку/контроль скорости чтения/записи нужно реализовывать пользователю сокета. благо, у сокета есть хендлеры.

A>в любом случае, если очередь переполнилась, клиента надо дисконнектить.

опять же, не обязательно. но в общем, это зависит от типа клиент-сервера и от того, что они образуют.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Re[3]: asio и блокировка вызывающего
От: Abyx Россия  
Дата: 03.04.13 11:27
Оценка:
Здравствуйте, niXman, Вы писали:

A>>это называется "возврат кода ошибки".

X>тут нет ошибки. false в данном случае лишь сигнализирует о том, что очередь полна. но сокет от этого не ломается, он успешно может читать/писать данные, просто медленнее чем ответная сторона.

A>>некоторые люди считают что исключения все же лучше.

X>тем более это не исключительная ситуация.

это "ошибка добавления элемента в очередь".
и происходит это когда закончилась память. определенно это должно быть исключительной ситуацией.

т.к. если это нормальная ситуация — значит с дизайном программы что-то не то.
если алгоритм производит данные быстрее чем они уходят клиенту, это значит что он зря жрет память и процессорное время.
это значит что либо сам алгоритм неправильный, либо его используют неправильно.

конечно если у сервера 1 клиент, то тогда такое можно позволить,
но если клиентов сотни/тысячи и нужна низкая latency, то как-то не получается выделить по сотне Мб на очередь пакетов каждому клиенту, и уж тем более нету времени нагенерить столько данных "про запас".

я так и представляю как клиент отправляет запрос, а сервер ему говорит, счя, погоди, я тут нагенерю 100500 пакетов Васе и Коле (хотя они отошли покурить), пару раз сделаю swap на диск и обратно (а то физ. память закончилась), ну и с SQLite немного пообщаюсь, вобщем жди.
In Zen We Trust
Re[4]: asio и блокировка вызывающего
От: niXman Ниоткуда https://github.com/niXman
Дата: 03.04.13 11:35
Оценка:
Здравствуйте, Abyx, Вы писали:

A>это "ошибка добавления элемента в очередь".

A>и происходит это когда закончилась память. определенно это должно быть исключительной ситуацией.
да нет же. при создании сокета, ты указываешь размер очереди.
и, раз уж так получилось, что указанный размер оказался недостаточным — то это скорее логическая несостыковка.
я бы ошибкой это не назвал, ведь ничего не потерялось и ничего не сломалось. просто сокет и использующая его логика, не согласованы.

A>т.к. если это нормальная ситуация — значит с дизайном программы что-то не то.

ну так это не сокету решать.

A>если алгоритм производит данные быстрее чем они уходят клиенту, это значит что он зря жрет память и процессорное время.

так же, не сокету решать.

A>это значит что либо сам алгоритм неправильный, либо его используют неправильно.

опять же.

A>конечно если у сервера 1 клиент, то тогда такое можно позволить,

я можно сокету указать размер очереди равный одному. и никаких расхождений с API asio не будет.

A>но если клиентов сотни/тысячи и нужна низкая latency, то как-то не получается выделить по сотне Мб на очередь пакетов каждому клиенту, и уж тем более нету времени нагенерить столько данных "про запас".

ну...сотни Мб — это ооочень много

A>я так и представляю как клиент отправляет запрос, а сервер ему говорит, счя, погоди, я тут нагенерю 100500 пакетов Васе и Коле (хотя они отошли покурить), пару раз сделаю swap на диск и обратно (а то физ. память закончилась), ну и с SQLite немного пообщаюсь, вобщем жди.

сильно утрировано, но такое возможно сделать даже без моей обертки =)
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Re[5]: asio и блокировка вызывающего
От: niXman Ниоткуда https://github.com/niXman
Дата: 03.04.13 11:38
Оценка:
Здравствуйте, niXman, Вы писали:

X>никаких расхождений с API asio не будет.

никаких расхождений с поведением asio API не будет.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Re: asio и блокировка вызывающего
От: Abyx Россия  
Дата: 03.04.13 12:29
Оценка:
Здравствуйте, niXman, Вы писали:

X>2. socket::write(..., handler)/socket::read(..., handler) изменить так, чтоб они блокировали вызывающего ровно на одну задачу(io_service::run_one()). (неважно какую, ибо io_service может быть один для тысяч сокетов(и не только))


тут надо учесть, это этот run_one тоже может поймать переполнение очереди, опять вызвать run_one и так пока сервер не упадет от переполнения стека.
In Zen We Trust
Re[2]: asio и блокировка вызывающего
От: niXman Ниоткуда https://github.com/niXman
Дата: 03.04.13 13:18
Оценка:
Здравствуйте, Abyx, Вы писали:

A>тут надо учесть, это этот run_one тоже может поймать переполнение очереди, опять вызвать run_one и так пока сервер не упадет от переполнения стека.

кстати да. значит все сильно упрощается, и остается только первый способ описанный в топике.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Re[3]: asio и блокировка вызывающего
От: Abyx Россия  
Дата: 03.04.13 13:30
Оценка:
Здравствуйте, niXman, Вы писали:

X>Здравствуйте, Abyx, Вы писали:


A>>тут надо учесть, это этот run_one тоже может поймать переполнение очереди, опять вызвать run_one и так пока сервер не упадет от переполнения стека.

X>кстати да. значит все сильно упрощается, и остается только первый способ описанный в топике.

IMO, есть только два способа.
1) бросать исключение (или использовать параметр error_code& ec, если исключения запрещены).
2) передавать ошибку в handler.
In Zen We Trust
Re: asio и блокировка вызывающего
От: zaufi Земля  
Дата: 03.04.13 14:35
Оценка:
Здравствуйте, niXman, Вы писали:

X>приветствую!


X>есть самописный класс сокета поверх asio.

X>предвидя вопросы типа "а зачем оно надо?", объясняю суть: внутренняя обработка очереди задач. т.е. в пользовательской коде можно несколько раз вызывать socket::write(..., handler)/socket::read(..., handler), и, при этом, сокет сам будет выполнять чтение/запись при завершении предыдущей задачи.

т.е. ты переизобрел strand чтоли?

X>вопрос в следующем:

X>сейчас, при использовании этого сокета есть вероятность того, что очередь может вырасти до очень больших размеров, ибо нет ограничения на размер очереди.
X>идей решения у меня две:

<skipped>

X>и я немогу решить, какой из способов более предпочтительным окажется.


на основании предоставленной информации вообще довольно трудно что-то советовать.

X>я склоняюсь ко второму. но тут мне не нравится то, что поведение при переполнении очереди, навязывается.

X>наверное все же третий...или кто-то предложит четвертый...

довольно существенно тут следующее: что за протокол ты реализуешь? в смысле гоняемые данные что из себя представляют? что делают handler'ы?
Re[4]: asio и блокировка вызывающего
От: niXman Ниоткуда https://github.com/niXman
Дата: 03.04.13 14:45
Оценка:
Здравствуйте, Abyx, Вы писали:

A>IMO, есть только два способа.

A>1) бросать исключение (или использовать параметр error_code& ec, если исключения запрещены).
A>2) передавать ошибку в handler.
я так и не понял, почему ты считаешь переполнение очереди ошибкой?
в эту очередь заложено такое поведение. как по мне, я не вижу тут ни ошибки, ни тем более исключительной ситуации.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Re[2]: asio и блокировка вызывающего
От: niXman Ниоткуда https://github.com/niXman
Дата: 03.04.13 14:48
Оценка:
Здравствуйте, zaufi, Вы писали:

Z>т.е. ты переизобрел strand чтоли?

=)
нет, strand не управляем.
у моего "изобретения" есть возможность отменить любую не выполняющуюся задачу.

Z>на основании предоставленной информации вообще довольно трудно что-то советовать.

какой информации не хватает?

Z>довольно существенно тут следующее: что за протокол ты реализуешь? в смысле гоняемые данные что из себя представляют? что делают handler'ы?

массивы байт. сам протокол я не реализую.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Re[5]: asio и блокировка вызывающего
От: Abyx Россия  
Дата: 03.04.13 15:50
Оценка:
Здравствуйте, niXman, Вы писали:

X>Здравствуйте, Abyx, Вы писали:


A>>IMO, есть только два способа.

A>>1) бросать исключение (или использовать параметр error_code& ec, если исключения запрещены).
A>>2) передавать ошибку в handler.
X>я так и не понял, почему ты считаешь переполнение очереди ошибкой?
X>в эту очередь заложено такое поведение. как по мне, я не вижу тут ни ошибки, ни тем более исключительной ситуации.
потому что переполнение очереди это исключительная ситуация. такого не должно происходить.

и что самое главное, непонятно как это обрабатывать.
вот ты в 1м посте писал,
> в пользовательском коде можно несколько раз вызывать socket::write
т.е. код может выглядеть так:

socket.write(packet1);
socket.write(packet2);
foo();


если сделать так что write может вернуть false, надо писать так:

if (socket.write(packet1))
{
    if (socket.write(packet2))
    {
        foo();

это называется arrowhead antipattern, по русски — говнокод.
кроме того, программист может забыть проверить возвращаемое значение write и пакеты будут молча дропаться.

но это еще пол истории, надо же как-то обработать ошибкуотказ добавления в очередь

    if (socket.write(packet2))
    {
        foo();
    }
    else
    {
        // что тут писать?
    }


если бы код был синхронный, мы бы написали

while(!socket.write(packet1)) socket.wait_write();
while(!socket.write(packet2)) socket.wait_write();
foo();


но он у нас асинхронный, по этому мы должны написать

if (socket.write(packet1))
{
    if (socket.write(packet2))
    {
        foo();
    }
    else
    {
        socket.async_wait_write([&]
        {
            socket.write(packet2);
            foo();
        });
    }    
}
else
{
    socket.async_wait_write([&]
    {
        socket.write(packet1);
        if (socket.write(packet2))
        {
            foo();
        }
        else
        {
            socket.async_wait_write([&]
            {
                socket.write(packet2);
                foo();
            });
        }    
    });
}
In Zen We Trust
Re[3]: asio и блокировка вызывающего
От: zaufi Земля  
Дата: 03.04.13 16:10
Оценка:
Здравствуйте, niXman, Вы писали:

X>Здравствуйте, zaufi, Вы писали:


Z>>т.е. ты переизобрел strand чтоли?

X>=)
X>нет, strand не управляем.
X>у моего "изобретения" есть возможность отменить любую не выполняющуюся задачу.

Z>>на основании предоставленной информации вообще довольно трудно что-то советовать.

X>какой информации не хватает?

Z>>довольно существенно тут следующее: что за протокол ты реализуешь? в смысле гоняемые данные что из себя представляют? что делают handler'ы?

X>массивы байт. сам протокол я не реализую.

решать подобную задачу абстрагируясь от протокола довольно не просто. потому что ты не обладаешь знаниями о том что это за "массив байт", как его обрабытывает вызывающий код? является ли он датаграммой или потоком? и фактически все что ты можешь тут сделать, уже сделано в ядерной реализации TCP/IP стэка... а ты просто пытаешься переизобрести тоже самое, но в userspace с учетом каких то несущественных дополнительных знаний о том, что нужно сериализовать операции чтения/записи в данный сокет... это кардинально ничего не решает -- у тебя по прежнему нет самого важноно: знаний о протоколе.

допустим например, ты реализуешь протокол вида запрос-ответ, где каждый запрос "самостоятельная единица работы" для сервера и все клиенты независимы. я бы делал примерна так:
0) чтение из сокета (после разбора команды) ставит запросы в очередь. задача читалки разгребать сокет как можно быстрее и все!
1) выделяем пул воркеров (допустим, N = количество процов — 1) -- каждый из них берет блок "задач" из очереди. блок в данном случае это функция от текущей длины очереди и количество работников в пуле: f(QueueLen, N) -- к примеру QueueLen/N. таким образом, если возрастает нагрузка, воркеры просто начинают работать над большим числом задач быстрее разгребая очередь и не дрюча ее, дергая по одной "задаче". результаты обработки воркеры складывают в выходную очередь (возможно тоже блоками).
2) задача писалки результатов как можно быстрее разгребать выходную очередь отправляя данные клиентам
3) т.к. очереди не бывают бесконечными при переполнении входной/выходной очереди возможны варианты обработки этого события, и они опять таки сильно зависят от того, что это за протокол и его клиенты. к примеру, при переполнении входной очереди можно начать дропать попытки новых клиентов подключиться (отдавая им "Serivce Termporary Unavailable"), притормаживать чтение из сокетов уже подключенных клиентов или, допустим, например, начать дропать их запросы (возможно возвращая что-то в ответ типа "эй чувак, притормози там!")

т.е. как видишь вариантов развития событий может быть куча разных и они существенно помогают в принятии решений. на уровне сокета оперирующего массивом байт, ничего из этого не известно, и возможностей для унификации/оптимизации поведения тут imho крайне мало, а то что возможно и так уже сделано в ядре...
Re[6]: asio и блокировка вызывающего
От: niXman Ниоткуда https://github.com/niXman
Дата: 03.04.13 18:04
Оценка:
Здравствуйте, Abyx, Вы писали:

A>потому что переполнение очереди это исключительная ситуация. такого не должно происходить.

так в том-то и фишка, что эта очередь предусматривает такое поведение.

A>но это еще пол истории, надо же как-то обработать ошибкуотказ добавления в очередь

A>
A>    if (socket.write(packet2))
A>    {
A>        foo();
A>    }
A>    else
A>    {
A>        // что тут писать?
A>    }
A>

я думаю, как-то так:
if (socket.write(packet2, handler))
{
   foo();
}
else
{
   session.packet_buffer.push_back(packet2);
}

...

void handler(...) {
   if ( socket.write_queue_not_full() ) {
      if ( socket.write(session.packet_buffer.front()) ) {
         session.packet_buffer.pop_front();
      }
   }
}

?
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Re[4]: asio и блокировка вызывающего
От: niXman Ниоткуда https://github.com/niXman
Дата: 03.04.13 18:19
Оценка:
Здравствуйте, zaufi, Вы писали:

Z>решать подобную задачу абстрагируясь от протокола довольно не просто. потому что ты не обладаешь знаниями о том что это за "массив байт", как его обрабытывает вызывающий код? является ли он датаграммой или потоком?

поток.
в датаграммах пока что необходимости небыло, но думаю, что ту же самую логику можно реализовать и для них.

Z>и фактически все что ты можешь тут сделать, уже сделано в ядерной реализации TCP/IP стэка... а ты просто пытаешься переизобрести тоже самое, но в userspace с учетом каких то несущественных дополнительных знаний о том, что нужно сериализовать операции чтения/записи в данный сокет... это кардинально ничего не решает -- у тебя по прежнему нет самого важноно: знаний о протоколе.

я могу контролировать очередь. добавлять/удалять/заблокироваться до момента выполнения какой-то конкретной задачи или группы задач.

Z>допустим например, ты реализуешь протокол вида запрос-ответ, где каждый запрос "самостоятельная единица работы" для сервера и все клиенты независимы. я бы делал примерна так:

Z>0) чтение из сокета (после разбора команды) ставит запросы в очередь. задача читалки разгребать сокет как можно быстрее и все!
Z>1) выделяем пул воркеров (допустим, N = количество процов — 1) -- каждый из них берет блок "задач" из очереди. блок в данном случае это функция от текущей длины очереди и количество работников в пуле: f(QueueLen, N) -- к примеру QueueLen/N. таким образом, если возрастает нагрузка, воркеры просто начинают работать над большим числом задач быстрее разгребая очередь и не дрюча ее, дергая по одной "задаче". результаты обработки воркеры складывают в выходную очередь (возможно тоже блоками).
Z>2) задача писалки результатов как можно быстрее разгребать выходную очередь отправляя данные клиентам
Z>3) т.к. очереди не бывают бесконечными при переполнении входной/выходной очереди возможны варианты обработки этого события, и они опять таки сильно зависят от того, что это за протокол и его клиенты. к примеру, при переполнении входной очереди можно начать дропать попытки новых клиентов подключиться (отдавая им "Serivce Termporary Unavailable"), притормаживать чтение из сокетов уже подключенных клиентов или, допустим, например, начать дропать их запросы (возможно возвращая что-то в ответ типа "эй чувак, притормози там!")

Z>т.е. как видишь вариантов развития событий может быть куча разных и они существенно помогают в принятии решений. на уровне сокета оперирующего массивом байт, ничего из этого не известно, и возможностей для унификации/оптимизации поведения тут imho крайне мало, а то что возможно и так уже сделано в ядре...

ты все правильно говоришь. но ты говоришь про протокол. я же говорю только про сокет и то, как поступать при переполнении очереди.
разумеется, сокет неможет предвидеть все варианты его использования, и адаптироваться под них. эта задача ложится на реализатора протокола.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Re[7]: asio и блокировка вызывающего
От: Abyx Россия  
Дата: 03.04.13 18:33
Оценка:
Здравствуйте, niXman, Вы писали:


X>я думаю, как-то так:

X>
X>if (socket.write(packet2, handler))
X>{
X>   foo();
X>}
X>else
X>{
X>   session.packet_buffer.push_back(packet2);
X>}

X>...

X>void handler(...) {
X>   if ( socket.write_queue_not_full() ) {
X>      if ( socket.write(session.packet_buffer.front()) ) {
X>         session.packet_buffer.pop_front();
X>      }
X>   }
X>}
X>

X>?

а foo() кто вызовет, после того как packet2 наконец отправят?
In Zen We Trust
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.