LINUX.ORG.RU

tcp/udp/OTHER сервер: таймауты, закрытие коннектов

 


0

4

TCP или не TCP в целом не ясно - это может быть и самодельный UDP шайтан-протокол. Connection - абстракция. В целом ясно, что стурктура данных heap AKA priority queue. Хочется простого: 10 сек не было данных от клиента - close() ему на сокет. Архитектура традиционная - однопоточный евент луп - сидит в epoll_wait() с пробуждениями, скажем, 10 раз в сек, чтобы поделать служебное (позакрывать по таймауту кого-то, другие таймеры попроцессить). Входящих коннектов могут быть тыщи. Обычно для такого юзают heap (min-heap например): пихаешь в эту структуру коннекшены, впереди лежат те, у которых значение time_close_nanosec меньше. time_close_nanosec - это такое uint64_t значение у объекта-connection, которое апдейтится каждый раз, когда из коннекшена выпали байтики и означает «monitonic время, при наступлении которого коннект надо закрыть». Соответственно апдейтится оно на now_nanosec + 10 * 1000 * 1000 * 1000 всякий раз, когда от клиента что-то прилетело.

Проблема в том, что time_close_nanosec таки апдейтится. Прилетели из сокет байты - время close() этого сокета надо отложить до now + 10 * 1000 * 1000 * 1000 как выше сказано. А это значит, в heap его надо перевставить, ведь старая позиция в priority queue уже не валидна. А значит в объекте Connection будет лежить индекс этого Connection в нашем heap (чтобы сходить по этому индексу в этот heap для удаления этого Connection). А ещё, heap должен быть так реализован, что при каждом перетасовывании объектов в нём, heap ходит по указателям в тасуемых и апдейтит им индексы-в-себе на новые (ведь могут вставить кого-то на позицию 0 и я стану допустим 1). То есть, если из клиентов просто регулярно спокойно летят байтики, то эта heap постоянно шевелится и идёт возня с памятью. Это не православно.

Вот пришла в голову оптимизация: time_close_nanosec в объекте Connection апдейтить, а в heap этот Connection не перевставлять. Тогда heap будет постоянно «ложно» срабатывать. И вот в этом ложном срабатывании мы и будем делать всю работу в heap - если срабатывание реально ложное (time_close_nanosec ещё не достигло текущего момента), то передобавлять его в heap, иначе это не ложное и делать close(). Ясно, что в такой heap надо будет хранить не просто указатели на Connection * (по которым будет лазить компаратор, чтобы посмотреть на time_close_nanosec (это вредно, они постоянно меняются и структура развалится)), а ещё и копию time_close_nanosec_COPY которая верна на момент вставки и по которой компаратор работает.

В результате этой оптимизации число операций с heap наверное минимально возможное и постоянно нормально льющиеся байтики от клиентов никак её не трогают, но всё работает как задумано. «Срабатывание heap» - это не что-то плохое, это один if в евент лупе: текущее время с корнем кучи сравнить.

(UPDATE: сделал эту идейку, работает на первый взгляд норм)

Штош, может кто что посоветует по этой теме? Как то же самое сделано в ядре например или где-то ещё в похожем месте. Додумался ли я до чего-то крутого или есть ещё круче оптимизации?



Последнее исправление: lesopilorama (всего исправлений: 16)

Хочется чего: не было 10 секунд данных от клиента - close() вызвать на сокете коннекта

[километров е описание нагороженного огорода, структуры-шмуктуры]

ЯННП, а разве нельзя на опциях сокета все порешать?

t184256 ★★★★★
()
Ответ на: комментарий от t184256

Надо в тред чуваков которые чо-то поняли, ЯНПП я и сам могу. Вкратце: нет таких опций. Те, которые есть, они про блокирующие вызовы, а тут про epoll речь, всё модно молодёжно неблокируемого, евентлупно.

lesopilorama
() автор топика
Последнее исправление: lesopilorama (всего исправлений: 1)
Ответ на: комментарий от lesopilorama

Не, ты только огород городить можешь.

Клиент и сервер всегда коннектятся через прямое соединение? Никаких socks’ов, корявых порт-форвардингов, создающих ещё одно соединение нет?

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

t184256 ★★★★★
()
Ответ на: комментарий от lesopilorama

а тут про epoll речь, всё модно молодёжно неблокируемого, евентлупно.

Начиная с какого года рождения на молодёжь перестают распространяться таймауты TCP?

t184256 ★★★★★
()
Ответ на: комментарий от t184256

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

lesopilorama
() автор топика
Ответ на: комментарий от u5er

Смотреть в сторону SO_RCVTIMEO.

Простба не постить в тред не опробованный кал.

Это про блокирующие операции же.

lesopilorama
() автор топика
Последнее исправление: lesopilorama (всего исправлений: 2)
Ответ на: комментарий от etwrq

может, пальцем в небо, тебе другой алгоритм выбрать/потестить для tcp? их с десяток точно есть, если не два десятка.

Как уже говорилось, пальцем в небо в треде не надо.

lesopilorama
() автор топика

А это значит, в heap его надо перевставить, ведь старая позиция в priority queue уже не валидна.

Binary heap умеет вставлять/удалять произвольный элемент за логарифмическое время, так что не вижу проблемы. Вы бы сначала попробовали сделать и посмотреть хватает ли производительности или нет прежде чем думать об оптимизации.

Вот пришла в голову оптимизация: time_close_nanosec в объекте Connection апдейтить, а в heap этот Connection не перевставлять.

Подозреваю что в итоге в лучшем случае будет тоже самое что и первым вариантом по эффективности.

X512 ★★★★★
()

Мне почему-то кажется, что ты какой-то ерундой страдаешь. На доставке пакета там столько машинерии задействовано, что твой десяток обновлений указателей на этом фоне вообще не заметен будет.

Ты прям уже померил и замерил серьёзный расход чего-то на этой операции?

Альтернатива дереву это что-то вроде array[timestamp - offset]. Т.е. создаёшь массив из указателей, вставляешь по нужному индексу свой указатель. Если там уже вставлен - сдвигаешься куда-то (или делаешь связный список). Таких массивов держишь две штуки, к примеру первый на 0-1 секунды вперёд, второй на 1-2 секунды вперёд и меняешь их местами по мере истечения времени. Тут сложность O(1) и по памяти может быть будет лучше в кеши укладываться. Но вряд ли это всё имеет смысл.

vbr ★★★
()
Последнее исправление: vbr (всего исправлений: 1)
Ответ на: комментарий от X512

Да, этот коммент и следующий примерно понятны. Да, хватит производительности. Но кажется моя оптимизация довольно очевидна и в ней даже надо меньше кодить. Плюс, есть ещё некий спортивный интерес на уровне «а понял ли я как это сделать реально минимумом операций».

lesopilorama
() автор топика
Последнее исправление: lesopilorama (всего исправлений: 1)

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

firkax ★★★★★
()

Ох, тяжело ТС понять, тяжело.

Что понял я. Есть некий TCP/UDP сервер, который обслуживает кучу соединений. При наступлении некоего события (обработали запрос, или что-то такое) надо отложено, через некое время, сделать некое действие (закрыть сокет или т.п.)

Как этот момент времени поймать? Сейчас есть структура типа «куча (heap)», которая хранит объекты ожидаемых по времени событий. У каждого свое время пробуждения. Достаем из кучи того, кто проснется раньше всех и рассчитываем таймаут на следующий сон в epoll_timeout().

Что-то есть сомнения, что долго считаем таймаут для epoll_timeout(). Как бы это дело оптимизировать?

Я правильно понял?

pathfinder ★★★★
()
Ответ на: комментарий от X512

Binary heap умеет вставлять/удалять произвольный элемент за логарифмическое время

Есть мысль, что проблема не в сложности двоичной кучи, а в том, что не те структуры в неё пихают. Меня смущает, что эта куча содержит объекты типа Connection.

pathfinder ★★★★
()
Ответ на: комментарий от pathfinder

Вкратце: не правильно. Поймёт чувак, реализовывавший в жизни таймауты на коннектах с еполлом.

lesopilorama
() автор топика
Последнее исправление: lesopilorama (всего исправлений: 1)
Ответ на: комментарий от lesopilorama

Поймёт чувак, реализовывавший в жизни таймауты на коннектах с еполлом.

Не поймет. Либо тебе надо говорить яснее, либо смириться с тем, что тебя никто не понимает. А так, я делал таймауты на коннектах с еполлом. Даже бинарная куча у меня была, как раз для работы этих таймаутов.

pathfinder ★★★★
()
Ответ на: комментарий от lesopilorama

Нахрен его тогда с пляжа да и всё, делоффта.

Опять не понял. Кого ты гнать с пляжа собрался? Зачем кого-то куда-то гнать?

pathfinder ★★★★
()
Последнее исправление: pathfinder (всего исправлений: 1)
Ответ на: комментарий от pathfinder

Опять не понял. Кого ты гнать с пляжа собрался? Зачем кого-то куда-то гнать?

Гнать всё, что генерит вот такие посты, не содержащие ничего по сабжу. Не понял - нахрен. Не работал с вопросом - ты бесполезен, не задерживайся. Это достаточно тупо и просто, зато топик не надо пролистывать от говна. Это тематический пхорум же вроде, а не политота и не «трёп» и не «флуд», мне искренне непонятны комменты в треде в духе «я нечетал, но…», «мне лень читать» и прочие там тычки в небо.

lesopilorama
() автор топика
Последнее исправление: lesopilorama (всего исправлений: 2)

Если я верно понимаю что ты хочешь, то это классический LRU кеш.
Данные хранятся в hashmap + linked list. Значение в карте указывает на элемент списка. Элемент списка это ключ в карте + последнее время использования.

Обновление времени последнего использования происходит достаточно просто: по ключу (conn ID) в карте находим элемент в списке, удаляем его, добавляем в начало обновив время. O(1).

Удаление протухших тоже просто: бежим из конца списка удаляя слишком старые. Бежим до первого не протухшего элемента.

urxvt ★★★★★
()

Хочется простого: 10 сек не было данных от клиента - close() ему на сокет. Архитектура традиционная - однопоточный евент луп - сидит в epoll_wait() с пробуждениями, скажем, 10 раз в сек, чтобы поделать служебное

Раз в 10 сек ты можешь пробежаться по всем своим тыщам соединений и посмотреть когда в каждом последний раз были данные. Городить структуры данных и оптимизации на пустом месте - профнепригодность в чистом виде.

anonymous
()
Ответ на: комментарий от anonymous

Раз в 10 сек ты можешь пробежаться по всем своим тыщам соединений и посмотреть когда в каждом последний раз были данные. Городить структуры данных и оптимизации на пустом месте - профнепригодность в чистом виде.

Идея вообще огонь. Причём, обычно бегут не сразу по всем, а кусками штук по 100. Постепенно достаточно быстро оббегается всё в пределах секунд и ничего не тормозит.

Вот только на корень heap посмотреть всё ещё в разы быстрее…

lesopilorama
() автор топика
Ответ на: комментарий от urxvt

то это классический LRU кеш.

Не совсем, потому что LRU выкидывает ПОСЛЕДНЕГО, а мне надо того, у кого таймаут быстрее наступит. А таймауты могут быть произвольные в миллисекундах заданы на каждый коннект. Поэтому есть момент адского перетасовалова. Никогда не знаешь кто там быстрее должен сдохнуть.

lesopilorama
() автор топика
Последнее исправление: lesopilorama (всего исправлений: 2)
Ответ на: комментарий от lesopilorama

Вот только на корень heap посмотреть всё ещё в разы быстрее…

Кто тебе это сказал? Джунские курсы где рассказывают про асимптотику, а думать не учат? Чтобы сказать «в разы быстрее», нужно померять. А перед тем как мерять, нужно сначала убедиться что это узкое место. Ты ничего из этого не сделал, а сразу начал пихать хип куда не просят. Постарайся над этим подумать.

anonymous
()
Ответ на: комментарий от lesopilorama

Вот только на корень heap посмотреть всё ещё в разы быстрее…

Кто тебе это сказал? Джунские курсы где рассказывают про асимптотику, а думать не учат?

Корень хипа лежит по адресу ноль постоянно, алло йопта.

Так и почему ты думаешь что от этого у тебя что-то становится быстрее?

anonymous
()
Ответ на: комментарий от lesopilorama

Ну оттого, что быстрее одного доступа к постоянному адресу нет ничего)

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

anonymous
()
Ответ на: комментарий от anonymous

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

Да, всё верно. И вот на эту тему и была моя супергениальная оптимизация: «не трогать heap». Не переставлять таймаут вперёд у сокета, который что-то принял. То есть, такой сокет будет в нашей куче постепенно приближаться к срабатыванию таймаута на нём. И таки таймаут будет срабатывать. И только в этот момент мы будем смотреть на него: «а правда ли, что это реальный таймаут»? Мы посмотрим на Connection и увидим, что таймаут фейковый, что данные были совсем недавно и что его надо передобавить в heap.

То есть, heap будет переколбашиваться только в моменты срабатываний таймаутов на коннекшенах, то есть в среднем один коннекшен будет переколбашивать heap один раз в таймаут, то есть скажем раз в минуту. Это супернормально.

А приход байтиков из сокета будет просто апдейтить поле «time_last_data» на текущее время, чтобы когда сработает таймаут понять по этому полю, что таймаут был фейковый и его на самом деле не было.

lesopilorama
() автор топика

Штош, может кто что посоветует по этой теме?

Если timeouts одинаковые для всех connections не нужен Вам никакой heap. Нужен «intrusive indexed queue» с операцией «rotate to back». По активности на сокете двигаете его в конец очереди, разбираете «с головы» по таймеру. Таймер можно взводить на фиксированные интервалы, можно привязаться к head expiry (по при этом слишком часто взводить тоже не стОит, как по мне так 0.25 .. 1 сек довольно разумный «tick»). Я думаю - разберётесь. Удачи!

bugfixer ★★★★
()
Последнее исправление: bugfixer (всего исправлений: 1)
Ответ на: комментарий от nazar222

У меня практики с ним не было, поэтому я только в общих чертах представляю. Но как правильно тс написал, я дичь советовал. Хотя в своё время, когда я писал сетевой код, я был уверен, что таймаут работает и на «асинхронщине». Я даже не поверил и накидал тестовый код. Век живи и век учись, чё.

u5er
()
Последнее исправление: u5er (всего исправлений: 1)

сколько готовых reactor/proactor pollerов было изучено в

преддверии этого длинного опуса велосипедостроения ?

тонко намекаю, что если бы были способы это как то оптимизировать

то наиболее известные asio,tokio,folly уже бы это имплементировали

anonymous
()