Поскольку этот вопрос получает много просмотров, я решил опубликовать «ответ», но технически это не ответ, а мое окончательное заключение, поэтому я отмечу его как ответ.
О подходах:
Функции async/await
, как правило, создают ожидаемый асинхронный Tasks
, назначенный для TaskScheduler
времени выполнения dotnet, поэтому тысячи одновременных соединения, поэтому тысячи или операции чтения / записи запустят тысячи задач. Насколько я знаю, это создает тысячи StateMachines, хранящихся в оперативной памяти, и бесчисленное количество переключений контекста в потоках, которым они назначены, что приводит к очень высокой загрузке ЦП. С несколькими соединениями / асинхронными вызовами он лучше сбалансирован, но по мере того, как количество ожидаемых задач растет, оно замедляется в геометрической прогрессии.
Методы сокетов BeginReceive/EndReceive/BeginSend/EndSend
являются технически асинхронными методами без ожидаемых Задач, но с обратными вызовами в конце вызова, что фактически оптимизирует многопоточность, но все же является ограничением дизайна многоточечной сети. эти методы сокетов, на мой взгляд, плохие, но для простых решений (или ограниченного числа соединений) это путь.
Тип сокета SocketAsyncEventArgs/ReceiveAsync/SendAsync
является лучшим в Windows по какой-то причине. Он использует Windows IOCP в фоновом режиме для достижения самых быстрых асинхронных вызовов сокетов и использования перекрывающегося ввода-вывода и специального режима сокетов. Это решение является самым простым и быстрым под Windows. Но в случае mono / linux это никогда не будет таким быстрым, потому что mono эмулирует Windows IOCP с помощью linux epoll
, который на самом деле намного быстрее, чем IOCP, но он должен эмулировать IOCP для достижения dotnet совместимость, это вызывает некоторые накладные расходы.
О размерах буфера:
Существует множество способов обработки данных в сокетах. Чтение простое, данные поступают, вы знаете их длину, вы просто копируете байты из буфера сокета в ваше приложение и обрабатываете его.
Отправка данных немного отличается.
- Вы можете передать свои полные данные в сокет, и он будет разрезать их на порции, копировать патроны в буфер сокета до тех пор, пока не останется больше отправки, и метод отправки сокета вернется, когда будут отправлены все данные (или когда происходит ошибка).
- Вы можете взять Ваши данные, разрезать их на порции и вызвать метод отправки через сокет с порцией, а когда она вернется, отправить следующий порцию, пока не останется больше.
В любом случае Вы должны учитывать, какой размер буфера сокета Вы должны выбрать. Если вы отправляете большой объем данных, то чем больше буфер, тем меньше нужно отправлять чанков, поэтому нужно вызывать меньше вызовов в вашем (или во внутреннем) цикле, меньше копий памяти, меньше накладных расходов.
Но выделение больших буферов сокетов и программных буферов данных приведет к большому использованию памяти, особенно если у вас тысячи подключений, а многократное выделение (и освобождение) большой памяти всегда дорого.
На стороне отправки размер буфера сокета 1-2-4-8 КБ идеален для большинства случаев, но если вы планируете регулярно отправлять большие файлы (более нескольких МБ), то размер буфера 16-32-64 КБ - это путь , Более 64 КБ, как правило, нет смысла идти.
Но это имеет преимущество только в том случае, если на стороне получателя тоже есть относительно большие приемные буферы.
Обычно через интернет-соединения (не по локальной сети) нет смысла превышать 32 КБ, даже 16 КБ идеально.
Значение ниже 4-8 КБ может привести к экспоненциально увеличенному количеству вызовов в цикле чтения / записи, вызывая большую загрузку ЦП и медленную обработку данных в приложении.
Пропускать меньше 4 КБ, только если Вы знаете, что Ваши сообщения обычно меньше 4 КБ или очень редко превышают 4 КБ.
Мой вывод:
Что касается моих экспериментов, встроенные классы сокетов / методы / решения в dotnet в порядке, но не эффективны вообще. Мои простые тестовые программы linux C, использующие неблокирующие сокеты, могут превзойти самое быстрое и «высокопроизводительное» решение для сокетов dotnet (SocketAsyncEventArgs
).
Это не означает, что невозможно быстрое программирование сокетов в dotnet, но в Windows мне пришлось создавать собственную реализацию Windows IOCP, напрямую связываясь с ядром Windows через InteropServices / Marshaling, прямой вызов методов Winsock2 , использование множества небезопасных кодов для передачи контекстных структур моих соединений в качестве указателей между моими классами / вызовами, создание собственного ThreadPool, создание потоков обработчика событий ввода-вывода, создание собственного TaskScheduler для ограничения количество одновременных асинхронных вызовов, чтобы избежать бессмысленного переключения контекста.
Это была большая работа с большим количеством исследований, экспериментов и испытаний. Если Вы хотите сделать это самостоятельно, делайте это только в том случае, если Вы действительно думаете, что оно того стоит. Смешивать небезопасный / неуправляемый код с управляемым кодом - трудная задача, но цель того стоит, потому что с этим решением я мог достичь с моим собственным http-сервером около 36000 http-запросов / сек на 1-гигабитной локальной сети в Windows 7 с i7 4790.
Это такая высокая производительность, которую я никогда не смог достичь с помощью встроенных сокетов dotnet.
Когда мой сервер dotnet работает на i9 7900X в Windows 10, подключенном к 4c / 8t Intel Atom NAS в Linux через локальную сеть 10 Гбит, я могу использовать всю пропускную способность (следовательно, копирование данных с 1 ГБ / с) независимо от того, У меня только 1 или 10000 одновременных подключений.
Моя библиотека сокетов также определяет, выполняется ли код в Linux, и затем вместо Windows IOCP (очевидно) использует вызовы ядра Linux через InteropServices / Marshalling для создания, использования сокетов и обработки событий сокетов непосредственно с помощью linux epoll. удалось максимизировать производительность тестовых машин.
Совет по дизайну:
Как оказалось, сложно спроектировать сетевую библиотеку, особенно одну, которая, вероятно, очень универсальна для всех целей. Вы должны сделать так, чтобы у вас было много настроек, или особенно для нужной вам задачи.
Это означает, что нужно найти правильные размеры буфера сокета, количество потоков обработки ввода-вывода, количество рабочих потоков, количество разрешенных асинхронных задач, все это должно быть настроено на машину, на которой выполняется приложение, и на число подключений, и тип данных. Вы хотите передать через сеть. Вот почему встроенные сокеты не работают так хорошо, потому что они должны быть универсальными, и они не позволяют Вам устанавливать эти параметры.
В моем случае использование более двух выделенных потоков для обработки событий ввода-вывода фактически ухудшает общую производительность, поскольку используется только две очереди RSS и вызывает большее переключение контекста, чем идеальное.
Выбор неправильных размеров буфера приведет к снижению производительности.
Всегда сравнивайте различные реализации для смоделированной задачи. Вам необходимо выяснить, какое решение или настройка являются лучшими.
Различные настройки могут давать разные результаты производительности на разных машинах и / или в операционных системах!
Моно против ядра Dotnet:
Так как я запрограммировал свою библиотеку сокетов в FW / Core-совместимом способе, я смог протестировать их под linux с mono и с внутренней компиляцией ядра. Самое интересное, что я не заметил каких-либо значительных различий в производительности, они оба были быстрыми, но, конечно, лучше оставить моно и компилировать ядро.
Совет по бонусному исполнению:
Если ваша сетевая карта поддерживает RSS (масштабирование на стороне приема), включите ее в Windows в настройках сетевого устройства в дополнительных свойствах и задайте для очереди RSS значение от 1 до максимально возможного / максимально высокого значения для ваше выступление.
Если он поддерживается вашей сетевой картой, то обычно он равен 1, это назначает сетевое событие для обработки только одним ядром ЦП ядром. Если вы можете увеличить этот счетчик очередей до больших чисел, это распределит сетевые события между большим количеством ядер ЦП и приведет к гораздо лучшей производительности.
В linux это также можно настроить, но по-разному лучше искать информацию о вашем драйвере linux distro / lan.
Я надеюсь, что мой опыт поможет некоторым из Вас!