Комментарии 37
Рассматривали Haskell вместе с/вместо Go?
"Сложность простоты"
https://habr.com/ru/post/469441/
TL;DR поста "Сложность простоты": автору (как он сам указывает, C# (mostly) developer; хотя он Rust, Scala знает) больше понравился Haskell.
Нет, не рассматривал, да и не уверен что Haskell удобен в контексте системного программирования.
https://habr.com/ru/post/489136/
https://habr.com/ru/post/496370/
Например, системная утилита подсчёта символов, слов, строк wc на Haskell.
Ну, к примеру, я попробовал по быстрому нагуглить как в Haskell сделать кастомный syscall. И с ходу не получилось найти. Конечно я тут не претендую на объективность, но кажется что язык и комьюнити не совсем об этом.
Да, но вот ядро ОС на Haskell https://github.com/tathougies/hos
Ну я все таки рассуждал в контексте линукса. А так why not.
@0xd34df00d спасибо за ссылку на hos выше https://habr.com/ru/company/skillfactory/blog/585884/comments/#comment_23649442
Я знал только https://github.com/dls/house , единственный коммит которой был 13 лет назад.
А Вы так называемые "дополнительные секунды" (https://ru.wikipedia.org/wiki/%D0%94%D0%BE%D0%BF%D0%BE%D0%BB%D0%BD%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F_%D1%81%D0%B5%D0%BA%D1%83%D0%BD%D0%B4%D0%B0) учли?
Тоже думал написать серию статей по io_uring, но с переходом на новую работу руки так и не дошли.
Очень рад был увидеть такую статью. Технология очень интересная.
Я сейчас прикручиваю io_uring к Envoy, и мне казалось, что в случае нулевых "params" никаких воркеров в ядре не запускается. Они запускаются, если указать флаг IORING_SETUP_SQPOLL (попробуйте свой бенчмарк с ним). Без него основной выигрыш в производительности происходит за счёт экономии на syscall'ах - меньше нужно переключений контекста, если сразу несколько системных операций (accept, writev, readv, connect, close) положить в буфер и один раз позвать io_uring_submit().
Не совсем так, пул воркеров запускается в любом случае (кому то же нужно выполнять сисколы). Грепните вот так:
ps auxf | grep io_wqe_
По поводу IORING_SETUP_SQPOLL - эта опция поднимает еще один дополнительный тред на одно кольцо. Его задача разгребать SQ, освобождая нас от ручного вызова io_uring_enter для подтверждения новых SQE (ну или submit в случае liburing). Но вообще эту тему (и более подробные бенчмарки) я как раз собираюсь оставить для будущих публикаций. Там можно много и по разному конфигурировать:
один io_uring
много io_uring
один ui_uring с IORING_SETUP_SQPOLL
много io_uring с IORING_SETUP_SQPOLL
много io_uring но на ограниченном пуле воркеров
и т.д.
У меня греп ничего не показывает. Правда, и с IORING_SETUP_SQPOLL новых тредов в ps auxf тоже невидно. Возможно, у меня ps или ядро какие-то неправильные.
Я, грешным делом, думал, что сисколы выполняются в том же треде, что и приложение, но с переключением в контекст ядра. То есть контроль исполнения передаётся ядру на время выполнения сискола, а приложение стоит и ждёт возврата. С IORING_SETUP_SQPOLL нет нужды в сисколах - IO операции выполняются ядром асинхронно, по моим предположениям, в polling-треде. В моих нагрузочных тестах видно как с IORING_SETUP_SQPOLL один CPU core полностью нагружается ядром, а CPU core на который запинено приложение тоже полностью нагружено, но практически не переключается в контекст ядра. В общем, было бы здорово развеять мои заблуждения в новый статьях.
Вот ещё вопрос возник. Если IO операции выполняются отдельными ядерными тредами, они попадут в одну cgroup с приложением или нет? Если нет, то, наверно, в k8s может случиться проблема "noisy neighbor".
Ну собственно поэтому этот момент в статье описан как туманный :) Например вот в недавних версиях появилась возможность IORING_REGISTER_IOWQ_MAX_WORKERS - ограничить размер пула. Но что это за пул и как работает - в доке одни намеки, приходится копать самому.
По поводу cgroup в целом ничего не могу сказать. Но в 5.12+ появилась такая фича:
IORING_FEAT_NATIVE_WORKERS
If this flag is set, io_uring is using native workers for its async helpers. Previous kernels used kernel threads that assumed the identity of the original io_uring owning task, but
later kernels will actively create what looks more like regular process threads instead. Available since kernel 5.12.
возможно она решит проблему (если таковая имеется)?
Тоже не вижу потоков (io_wqe или чего-то похожее), только [kworker/...]. Ядро 5.15 от Ubuntu. Не знаете почему?
Почитал. Впечатлился.
Однако, я бы не согласился с тем, что это дальнейшая эволюция epoll
. У этих технологий имеется принципиальная разница: io_uring
— это надстройка над блокирующими вызовами, тогда как epoll
— над неблокирующими.
В чём заключается разница:
Блокирующие вызовы невозможно отменить. Вызов будет висеть в очереди, пока не придут/отправятся данные, либо не случится ошибка. В случае же неблокирующего вызова можно отписаться от нотификации готовности к чтению/записи в любой момент. Отсюда следует, что использовать
io_uring
в качестве бэкэнда для планировщика в C# без костылей не получится, т.к.CancellationToken
попросту не будет работать.
В Linux нет асинхронной работы с дисками. Существующие решения работают с блокирующими вызовами в пуле потоков. Очевидно, что такие решения масштабируется довольно плохо. Технология
io_uring
же решает эту проблему.
Круто что впечатлились! По первому пункту, возможно я Вас не понял, но io_uring умеет отменять операции (даже если они уже засабмиченны из SQ в ядро) - для этого есть спец операция IORING_OP_ASYNC_CANCEL.
Спасибо, тогда это решает все возможные проблемы. Просто с документацией по этой технологии пока всё плохо, и я тупо не смог найти эту информацию, т.к. функционал IORING_OP_ASYNC_CANCEL был добавлен в ядро сильно позже, в версии 5.5.
объясните человеку не в теме - как обрабатывается переполнение буферов?
О каких именно буферах речь?
кажется, не туда ответил см тут https://habr.com/ru/company/itsoft/blog/589389/comments/#comment_23773249
Очень просто:
Если у вас накопилось больше запросов, чем влазит в буфер, то вы должны ждать, пока ядро их прочитает, и только потом дописывать новые. Если вы не используете poll-режим, тогда просто достаточно вызывать io_uring_submit
после заполнения буфера.
Если же у ядра накопилось ответов больше, чем влазит в буфер, то оно заполнит столько элементов, сколько влезет в буфер, и будет ждать, пока клиент не вычитает ответы. Если вы не используете poll-режим, тогда буфер заполнится при следующем вызове io_uring_wait_*
.
Спасибо! добавлю что для в случае переполнения CQ поведение зависит от версии ядра, раньше лишние cqe дропались (надо проверять FEAT_NODROP вообщем)
а) не перезаписать свой (еще) необработанный запрос
в случае использования liburing не получится перезаписать, если работать с SQ руками то можно
Если у вас накопилось больше запросов, чем влазит в буфер, то вы должны ждать, пока ядро их прочитает, и только потом дописывать новые
По какому признаку пользователь должен это отслеживать?
Если же у ядра накопилось ответов больше, чем влазит в буфер, то оно заполнит столько элементов, сколько влезет в буфер, и будет ждать, пока клиент не вычитает ответы.
По какому признаку ядро отслеживает, какие пакеты вычитаны и могут быть безопасно перезаписаны?
По какому признаку пользователь должен это отслеживать?
В случае liburing - функция io_uring_get_sqe вернет null. Если пишите либу сами и работаете с чистыми сисколами то у Вас есть доступ к head и tail SQ + размер SQ тоже известен, так что просто сами смотрите
По какому признаку ядро отслеживает, какие пакеты вычитаны и могут быть безопасно перезаписаны?
В случае liburing - опять же мы подтверждаем прием cqe вызовом функции, под капотом - двигаем head CQ буфера (это значение шарится между юзер спейсом и ядром)
В случае liburing — опять же мы подтверждаем прием cqe вызовом функции
Мне, кстати, логика работы функции io_uring_cqe_seen
совершенно не нравится, т.к. делает она вовсе не то, что декларируется в её названии. Она двигает счётчик на 1, но никакой логики по отношению к cqe к ней нет:
Например, мы можем считать сразу несколько cqe через io_uring_peek_batch_cqe
, а затем начать обрабатывать их не по порядку, тогда вызов io_uring_cqe_seen
может привести к тому, что первые cqe могут быть затёрты.
Мне, кстати, логика работы функции
io_uring_cqe_seen
совершенно не нравится
согласен с Вами, думаю это сделано из-за красивого api
Например, мы можем считать сразу несколько cqe через
io_uring_peek_batch_cqe
, а затем начать обрабатывать их не по порядку, тогда вызовio_uring_cqe_seen
может привести к тому, что первые cqe могут быть затёрты.
я даже об это слегка спотыкался, правда теперь, немного разобравшись, везде где батчим cqe (а батчим везде где нужен высокий throughput) использую io_uring_cq_advance
Кольцевой буфер со всеми указателями (head, tail) шарится между ядром и приложением, есть общий алгоритм работы с ним, который описан в статье.
Проверил, что будет, если создать uring на 32 элемента, и стал заполнять SQ много тысяч раз без вычитывания CQ. Kworker не заблочился, в файл всё записалось. Kernel 5.15.
О кольцевых. Ring buffer имеет конечную емкость.
Я, может быть, коряво выразился. Попробую переформулировать: за какими признаками должен следить пользователь io_uring чтобы
а) не перезаписать свой (еще) необработанный запрос
б) с гарантией успевать обрабатывать все ответы ядра.
Конкретные примеры и истории косяков приветствуются.
б) с гарантией успевать обрабатывать все ответы ядра.
Мне кажется, ответа тут нет. У меня ядро 5.15, и в нём никакой проблемы с обработкой нет. Я запихивал в uring миллион различных таймеров, и весь этот миллион корректно складировался в ядре без потери ответов.
Но судя по документации, sumbit может вернуть -EBUSY. В этом случае нужно приостановить отправку запросов в ядро и вычитать результаты. Но я с таким не сталкивался.
IO_URING. Часть 1. Введение