Высокопроизводительное программирование сокетов TCP в .NET C # - PullRequest
0 голосов
/ 05 сентября 2018

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

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

Я очень хорошо знаю, что должен использовать async методы , и я уже реализовал все найденные мной решения и протестировал их.

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

Случай очень прост: один сокет TCP прослушивает localhost, другой сокет TCP подключается к сокету прослушивания (из той же программы, на той же машине oc.) , затем один бесконечный цикл начинает отправлять пакеты размером 256kB с клиентским сокетом в сокет сервера.

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

Я понял, что для размера пакета - 256 КБ , а размер буфера сокета - 64 КБ для максимальной пропускной способности.

С помощью методов типа async/await я смог достичь

~370MB/s (~3.2gbps) on Windows, ~680MB/s (~5.8gbps) on Linux with mono

С помощью методов типа BeginReceive/EndReceive/BeginSend/EndSend я смог достичь

~580MB/s (~5.0gbps) on Windows, ~9GB/s (~77.3gbps) on Linux with mono

С помощью методов типа SocketAsyncEventArgs/ReceiveAsync/SendAsync я смог достичь

~1.4GB/s (~12gbps) on Windows, ~1.1GB/s (~9.4gbps) on Linux with mono

Проблемы заключаются в следующем:

  1. async/await методы были самыми медленными , поэтому я не буду с ними работать
  2. BeginReceive/EndReceive методы запускали новый асинхронный поток вместе с методами BeginAccept/EndAccept, в Linux / mono каждый новый экземпляр сокета был очень медленным (когда в ThreadPool не было больше потока mono запустил новые потоки, но для создания 25 экземпляра соединений потребовалось около 5 минут , создание 50 соединений было невозможно (программа просто перестала делать что-либо после ~ 30 подключений).
  3. Изменение размера ThreadPool совсем не помогло, и я бы не стал его менять (это был просто отладочный ход)
  4. На сегодняшний день лучшим решением является SocketAsyncEventArgs, и это обеспечивает наивысшую пропускную способность в Windows, но в Linux / mono она медленнее, чем в Windows, и раньше была наоборот.

Я проверил на своем компьютере Windows и Linux iperf ,

Windows machine produced ~1GB/s (~8.58gbps), Linux machine produced ~8.5GB/s (~73.0gbps)

Странно то, что iperf может дать более слабый результат, чем мое приложение, но в Linux оно намного выше.

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

Если я решу использовать методы BeginReceive/EndReceive (они дали относительно высокий результат в Linux / mono), как я могу исправить проблему с многопоточностью, ускорить создание экземпляра соединения и устранить остановленное состояние после создания нескольких экземпляры?

Я продолжаю делать дополнительные тесты и поделюсь результатами, если появятся новые.

======================================= ОБНОВЛЕНИЕ ============= =====================

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

Я должен был понять, в Windows 7, устройство обратной связи работает медленно , не может получить более высокий результат, чем 1 ГБ / с с iperf или NTttcp , только Windows 8 и более новые версии имеют быструю обратную связь , поэтому меня больше не волнуют результаты Windows, пока я не смогу протестировать более новую версию. SIO_LOOPBACK_FAST_PATH должен быть включен через Socket.IOControl , но он вызывает исключение в Windows 7.

Оказалось, что самым мощным решением является реализация на основе событий Completion SocketAsyncEventArgs как в Windows, так и в Linux / Mono. Создание нескольких тысяч экземпляров клиентов никогда не испортило ThreadPool , программа не остановилась внезапно, как я упоминал выше. Эта реализация очень хороша для многопоточности.

Создание 10 подключений к прослушивающему сокету и передача данных из 10 отдельных потоков из ThreadPool вместе с клиентами может привести к ~2GB/s трафику данных в Windows и ~6GB/s в Linux / Mono.

Увеличение количества клиентских соединений не улучшило общую пропускную способность, но общий трафик стал распределяться между соединениями, возможно, это связано с тем, что загрузка ЦП составляла 100% на всех ядрах / потоках даже с 5, 10 или 200 клиентами.

Я думаю, что общая производительность неплохая, 100 клиентов могут генерировать около ~500mbit/s трафика каждый. (Конечно, это измеряется в локальных соединениях, сценарий реальной жизни в сети будет другим.)

Единственное наблюдение, которым я хотел бы поделиться: эксперименты с размерами буфера входа / выхода Socket и с размерами буфера чтения / записи программы / циклами цикла сильно повлияли на производительность и очень по-разному в Windows и в Linux / Mono.

В Windows наилучшая производительность достигается при использовании буферов 128kB socket-receive, 32kB socket-send, 16kB program-read и 64kB program-write.

В Linux предыдущие настройки давали очень слабую производительность, но 512 КБ socket-receive and -send оба, 256kB program-read и 128kB program-write размеры буфера работали лучше всего.

Теперь моя единственная проблема - если я пытаюсь создать 10000 соединительных сокетов, то после примерно 7005 он просто прекращает создавать экземпляры, не выдает никаких исключений, и программа работает, так как не было никаких проблем, но я не знаю как он может выйти из определенного цикла for без break, но это так.

Любая помощь будет благодарна за все, о чем я говорил!

1 Ответ

0 голосов
/ 09 июля 2019

Поскольку этот вопрос получает много просмотров, я решил опубликовать «ответ», но технически это не ответ, а мое окончательное заключение, поэтому я отмечу его как ответ.

О подходах:

Функции 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.

Я надеюсь, что мой опыт поможет некоторым из Вас!

...