Pull to refresh

Comments 51

Главный недостаток Go это несомненно обработка ошибок. Вот вы смогли как-то прикрутить нужный вам интерфейс ОС. А вот исключения в эзыке сделать не получится.

Ну и второй момент, если вам надо настолько тесно взаимодействовать с ОС на таком низком уровне - скорее всего Go вам ненужен, это неудачный выбор инструмента. Может лучше их, например, разделить. IO занимается один процесс, а Go-программа работает отдельно и занимается настройкой и мониторингом этих IO процессов.

Работа с файлами - это не низкий уровень. В общем, если бы не странное решение выносить системные вызовы в отдельный поток (да еще и не завершать его потом), то это поведение не отличалось бы от других языков.

решение выносить в отдельный поток как раз не странное, это отличный кроссплатформенный способ сделать i/o асинхронным.
вот то, что потоки не закрываются потом, это странно. впрочем, судя по всему, в типичных сценариях использования это не проблема, а вот у вас выстрелило

То, что потоки не закрываются — это тоже не странно: создавать новый поток дорого, держать неактивным дешевле.

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


omotezata


Конечно, производительность сразу снизилась, а клиенты стали падать с ошибками.

а почему конечно-то? если эти потоки были заняты дисковым i/o, то что плохого в том, что их было много?

Резко увеличилась нагрузка на память и цп. Полагаю, планировщику потоков было не очень хорошо.

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

Полагаю, планировщику потоков было не очень хорошо

сотня «лишних» процессов определенно не являются проблемой для линуксового планировщика.


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

если они находятся в iowait, то и не повредят.

Не думаю, что это хороший способ. Скажем так, это один из возможных способов, но не единственный. Поведение можно было бы регулировать хотя бы с помощью флагов компиляции: синхронные вызовы, создание нового потока, заранее аллоцированный пул потоков размера N или что-то еще.

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

Поэтому asyncfs и появился на свет. Описанное поведение рантайма было для меня неожиданностью, но мне совершенно не хотелось переписывать тонны кода из-за одной проблемы.

С титульной пикчей статьи конечно не замарачивались.

А не пробовали использовать библиотечки для io_uring, которые упоминаются в https://github.com/golang/go/issues/31908? Как они решают упомянутые проблемы, перечисленные в разделе "Замечания"?

Библиотеки из ишью проще, так как заточены только под io_uring. Они решают (вернее, просто не воспроизводят) только первую проблему. Линуксового aio там нет, а вот go vet пишет такой же ворнинг, как и у меня.
Полагаю, что я слишком заморочился с дизайном сохранения состояния асинхронной операции. uintptr, unsafe.Pointer, приведение типов и вот это все. Можно упростить, чтобы рантайм с включенным детектором гонок не ругался на прямую работу с указателями. Попробую в следующей версии пофиксить.

По идее можно сделать пул потоков для файлового I/o через пул горутин + runtime.LockOsThread и raw сисколы. А так да io_uring решает проблему с асинхронными операциями на файлах, хоть в ядре всеравно будет тот же пул воркеров :)

Согласен, runtime.LockOsThread мог бы помочь.

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

Тут весьма приятный и простой интерфейс: надо всего лишь открыть файл с флагом FILE_FLAG_OVERLAPPED

В первый раз вижу, чтобы кто-то называл WinAPI приятным и простым интерфейсом :) А так очень круто заморочились конечно.

UFO just landed and posted this here

Да они даже как представлять bool не определились, 1 или 4 байта. Когда дело доходит до маршалинга структур, там вообще кто на что горазд (из индусов, писавших этот код, no offense так сказать)

Мне нравится, что там можно юзать OVERLAPPED и горя не знать. Да и IOCP умеет одинаково работать как с сокетами, так и с файлами. Не надо заводить зоопарк интерфейсов и вспоминать, чем aio отличается от io_uring, почему нельзя использовать epoll на файлах и почему не получится асинхронно вызывать zero-copy запись в сокет.
А может у меня просто специфичные вкусы)

Ну, вообще-то в Winsock API были и свои асинхронные системные вызовы — аналоги системных вызовов socket API (родом BSD Unix).
Этот API — он родом из Win3.x с ее «колхозной» (т.е. кооперативной) многозадачностью, и наличие в нем асинхронных вызовов было необходимостью: иначе блокировался единственный на всю систему (систему, не процесс!) поток управления, и пользователь мог только созерцать песочные часы: даже переключиться на другую программу он не мог.

Меня iocp тоже соблазняет, но там нет таймеров . Приходилось сбоку прикручивать.

Довольно странно ожидать таймеров от IOCP...

Там нет таймеров, но зато есть PostQueuedCompletionStatus(). Через этот механизм можно вообще всё что угодно реализовать. Главное не забывать что количество физических потоков ограничено и не блокировать их выполнение.

Следующий уровень — асинхронное закрытие файлов...

Не совсем. RawSyscall возвращает uintptr, который является числом, не связанным с каким-либо объектом с точки зрения рантайма. KeepAlive не сможет донести до сборщика мусора, что uinptr - это не число, а указатель на что-то.

Если не делать return значения скастованого из uintptr то не должно быть проблем, ведь если данные снаружи, то GC их не тронет, а если изнутри, то keepalive (оригинальной структуры, а не новой из uintptr) в пределах блока сработает. С return конечно сложнее.

Про KeepAlive, вроде, в issue написано, что эта штука не защищает от сборщика мусора, поправьте, если ошибаюсь. Пробовали cgo.NewHandle? Он как раз для этих целей. И никаких проблем с гонкой не должно быть. Правда, сохранять нужно ссылку на сам слайс.

Если go память не выделял, то почему он должен собрать ее сборщиком мусора?

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

Если проблема именно в нагрузке при асинхронной записи многих файлов - можно попробовать ммапить файлы для записи, делать, собственно, запись данных в заммапленную память, а дальше пусть ОС разгребает dirty pages самостоятельно. Правда, все может упереться в задержки на page faults, я лично не тестил. Впрочем, для sparse файлов проблемы с пейжфолтами быть не должно. И придется ещё позаниматься тюнингом sysctl vm.dirty* и подобных, а то ОС может и не справиться самостоятельно, если прилетит большая пачка апдейтов.
На фре есть возможность сделать ммап с флагом MAP_NOSYNC - тогда можно отдельно синкать измененные файлы на диск (ОС это делать уже не будет), контролируя загрузку дисков..

Отчасти Linux вышел вперёд, благодаря выливаниям компаний не только в серверную но и десктопную среду, на нем были решены многие пользовательские проблемы (автомонтирование флешек, воспроизведение звука с нескольких устройств одновременно), которые судя по беглому гуглению, ещё присутствуют во FreeBSD. Разработчикам стало относительно удобно в нем, и по привычке проще поставить на сервер более знакомую среду. Тем более что основной софт (по количеству разработчиков) - это сервисы, web. Который работает на том же nginx, и поэтому не все сталкиваются с такими проблемами, так как они решаются другими людьми.

Жаль конечно что freebsd сдала позиции, но бывают и обратные примеры.

Старый знакомый, оказывал компаниям услуги по настройке web сайтов, решил попробовать freebsd после Linux и полностью перешёл на неё , спустя пару недель. Клиентам с его слов все равно на чем работает их сайт, а ему намного легче ставить это все и следить за этим всем. Как он говорил все есть в handbook - не надо искать как в другом дистрибутиве настраивать и устанавливать пакеты.

За статью спасибо! Очень познавательно

Удивлён, кстати, что где-то Linux уступает BSD-системам. На моей практике было ровно наоборот. Хотя и у меня задачи совсем другие были... Около-скриптового характера. Несколько примеров из того что вспомню:

  1. BSD-шный date не поддерживает %N, а значит отсутствует простая и быстрая возможность получать нано- или хотя-бы миллисекундное разрешение времени в shell

  2. BSD-шный awk гораздо медленнее Gawk

  3. В BSD нет линуксового /dev/shm - файловая система в RAM - удобно для быстрого создания временных файлов

  4. В BSD нет /dev/stderr, /dev/stdin, /dev/stdout

  5. Коробочный bash в macOS гораздо древнее актуального, а во Free он даже не установлен по дефолту

  6. Добавьте своё или поправьте меня по пунктам выше👆

Короче, для себя сделал вывод, что для разработчика Linux поприятнее, чем macOS/FreeBSD. Да и вообще GNU-окружение по сравнению с BSD-аналогами.

  1. Нет 'bsd-ного' awk, это one-true-awk от собственно авторов языка. Если нужен gawk, то его и поставьте.

  2. это называется tmpfs, обычно монтируется в /tmp

  3. конечно есть /dev/std{in,out,err}, как и /dev/fd/

  4. опять-таки, поставьте bash. Только не пишите #!/bin/bash, хотя бы #!/usr/bin/env bash

  1. Все так, но это не меняет факта. В системе по умолчанию стоит самый каноничный, но самый медленный awk, а не быстрые gawk или mawk

  2. Но по умолчанию я наблюдаю, что у меня в macOS и фре /tmp это диск

  3. Да, тут моя неправда. Сейчас перепроверил и всё работает. Значит, это была другая ошибка. Благодарю.

  4. Все так, но это не менят факта.

Еще вспомнил момент. Во Фре по дефолту нету wget и curl, но есть некий fetch.

Во Фре по дефолту нету wget и curl, но есть некий fetch.

А ещё некие sysrc, sockstat... ee вместо nano, ifconfig вместо brctl / ethtool / ip / iwconfig, ipfw вместо iptables, jail вмеcто lxc/lxcd/docker, md5 вместо md5sum, mpd5 вместо... а time вообще выводит данные в одну строку...

Ну и отлично! НО! curl, например, это все-таки такой себе серьезный стандарт, можно сказать. Даже в хроме можно скопировать запрос в синтаксисе curl.

А аналоги - они, разумеется, есть. Но, видимо, гораздо менее популярные и используемые. Потому, видимо, и сама Фря так малопопулярна теперь.

я бы назвал статью «как было плохо с асинхронным i/o в linux до io_uring» )


На сегодняшний день асинхронного sendfile в Линуксе нет

ЕМНИП рекомендуемый сейчас подход — звать splice через io_uring


Что случилось, почему все вдруг забыли про FreeBSD — некогда самую популярную серверную ОС — и начали переходить на Linux?

в какой момент самую популярную? что-то я такого не припоминаю.

я одного не мог понять с самого начала статьи и так и не увидел ответа – зачем вы вообще пишете в файлы? если у вас прямой эфир, формируйте чанки и плейлисты прямо в памяти и из неё же и отдавайте – на 100 потоков это же не так много памяти нужно. когда чанк ушёл из плейлиста, из памяти его сборщик мусора выкинет, а для архива поток пусть пишет отдельно стоящая машинка с ffmpeg. зачем вообще дисковое IO на медиасервере?

Плейлист может содержать N чанков, в зависимости от настроек сорца. N чанков - это X мб, а если там еще адаптивный стриминг, то X*Y. Данные одного плейлиста могут несколько десятков мегабайт весить.100 потоков - это норм, но не всегда будет 100 потоков. Сейчас демка держит несколько сотен паблишеров - даже если в текущей сырой версии все держать в памяти, то счет пойдет на гигабайты, которые будут расходоваться только на вот этот кэш. В будущих версиях я надеюсь добиться производительности 1000+, и там уже о десятках гигов пойдет речь. Словом, все держать в памяти - это не вариант.

Вообще в настройках есть возможность управлять этим, например, потоки с большим количеством клиентов-плееров можно будет кэшировать, но это все надо будет аккуратно отслеживать и тюнить. Держать в памяти плейлист, у которого полтора зрителя - такое себе решение.
Кроме того, дисковое I/O нужно для VoD трансляций.

Асинхронная запись, переиспользование памяти, zero-copy операции, быстрые ssd - наши лучшие друзья.

ну собственно "десятки гигабайт" – это не так уж много для сервера. кроме того с 1000+ стримов на одном сервере у вас скорее всего начнёт заканчиваться пропускная способность сетевого интерфейса, и именно сеть а не память будет ограничивать capacity сервера. а вы CDN подключаете или напрямую с сервера чанки раздаёте? если напрямую, то сеть закончится гораздо раньше, если конечно в стримах не по 1-2 зрителя…

что касается VoD, то у него вообще другие задачи и там уже чанки лучше раздавать с какого-нибудь S3-подобного хранилища, а писать их отдельным сервисом на отдельной машине, зачем всё в один процесс пытаться "утрамбовать"?

UFO just landed and posted this here

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

я думаю, что тесты были бы хорошим дополнением к статье )

Классная статья, автор явно хорошо разобрался с внутренностями Go. Прочитал с удовольствием.

Вам не кажется, что вы просто неудачный инструмент выбрали для решения задачи? Из спортивного интереса конечно можно и на Go написать, и на эрланге, но не проще было для выжимания максимума из железа взять плюсы?

Спасибо)
На плюсах не хочу. Я 7 лет работал программистом на С++, у меня все до сих пор болит от этих плюсов.

Вообще я когда-то начал этим заниматься просто чтобы выучить Go (самый лучший способ изучить новый язык - это сразу начать на нем программировать). А потом подумал, что можно свой бизнес начать, раз у меня демка почти готова. Проблема, описанная в статье, была неожиданностью, но я не стал все переписывать только из-за нее. Go нормально подходит для бэка. Бывают иногда неудобства, конечно, но они во всех языках есть.
Иногда закрадываются мысли переписать все на Rust, но я их отгоняю.

В терапевтических дозах не должно сильно болеть. Go - отличный язык для бэка. Весь control plane, все апишечки на Go писать куда приятнее. А с data plane можно потерпеть и написать и на чем-то более низкоуровневом. Обычно там не так много кода. Раз написал, и оно всю жизнь работает.

Да, это действительно было интересно — узнать, что разработчики .NET Core наконец-то прочитали документацию по Win32 API:
we have very carefully studied the profiles and WriteFile docs and got to the conclusion that we don’t need to extend the file before performing every async write operation. WriteFile() extends the file if needed.

In the past, we were doing that because we were thinking that SetFilePointer (Windows sys-call used to set file position) could not be pointing to a non-existing offset (offset > endOfFile). Docs helped us to invalidate that assumption:

Они решили посмотреть на перфоманс кода 20летней давности. Ничего удивительного, что много интересного выяснили.

Год назад io_uring валил ядро в панику. В одном из релизов после 5.4 это починили, потом бэкпортировали

POSIX AIO в glibc, помнится, был реализован на тредпуле - возможно, так и осталось. С Native AIO там были связаны относительно удобные векторные системные вызовы (что-то наподобие io_pwritev, io_preadv, на которых было достаточно удобно делать 0-copy DMA, но там была куча багов в обработке kiocb

Sign up to leave a comment.

Articles