Высокопроизводительное программирование сокетов TCP в .NET C#

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

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

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

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

Дело очень простое, один TCP Socket слушает на локальном хосте, другой TCP Socket подключается к слушающему сокету (из той же программы, на той же машине oc.), затем один бесконечный цикл начинает отправлять < Strong>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.

Оказалось, что наиболее эффективным решением является событие завершения на основе 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, но это так.

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


person beatcoder    schedule 05.09.2018    source источник
comment
если вы не планируете использовать локальный хост с вашим конечным продуктом, результаты ваших тестов действительно значат меньше. Если эта программа будет работать через Интернет, вам нужно запустить тест через Интернет, чтобы получить такие же накладные расходы и задержки при работе со всеми частями оборудования между сервером и клиентом.   -  person Scott Chamberlain    schedule 05.09.2018
comment
Кроме того, не видя вашего тестового кода, мы не можем сказать, будет ли другое решение работать лучше, потому что вы никогда не показывали нам с кодом, что вы сейчас делаете. Текстовые описания кода недостаточно подробные.   -  person Scott Chamberlain    schedule 05.09.2018
comment
Это хорошо продуманный вопрос, однако в значительной степени на него невозможно ответить, если только вы не встретите на следующий день кого-то, кто сравнил решение с 30000 клиентских сокетов со всеми вашими решениями. может быть, это было бы лучше для проверки кода с вашим тестовым кодом   -  person TheGeneral    schedule 05.09.2018
comment
Скотт Чемберлен - Спасибо за Ваш ответ. Я пытаюсь создать упрощенные тестовые коды и поделюсь ими. Мой вопрос в основном теоретический, я хотел бы знать, какая реализация лучше подходит для какой операционной системы или есть известный недостаток (например, моно под Linux не может обеспечить производительность Windows с помощью SocketAsyncEventArgs, возможно, потому, что он должен имитировать события Windows), или мне нужно делать что-то принципиально другое, чтобы достичь производительности iperf под linux, или управлять потоками особым образом?   -  person beatcoder    schedule 05.09.2018
comment
TheGeneral - Спасибо за ваш комментарий. Как я уже упоминал в своем предыдущем комментарии, мой вопрос в основном теоретический. Я хотел бы знать, какой метод лучше всего подходит для какой операционной системы, есть ли какие-либо известные недостатки из-за кроссплатформенности или мне нужно управлять потоками по-другому, чтобы избежать ужасных задержек и проблемы с зависанием.   -  person beatcoder    schedule 05.09.2018
comment
Поскольку вы упомянули моно, вы можете захотеть написать свою программу, ориентированную на .NET Core framework, этот фреймворк может запускаться под Linux изначально.   -  person Scott Chamberlain    schedule 06.09.2018
comment
Скотт - Спасибо за совет, я обязательно посмотрю этот фреймворк, он кажется очень многообещающим.   -  person beatcoder    schedule 10.09.2018
comment
Вы, вероятно, значительно продвинулись в своей жизни после этого поста, но мне интересно, как вы использовали событие .Completed в SocketAsyncEventArgs? Не видя вашего кода, вот пара ошибок. Я только что просматривал пример MSDN и вижу там ошибку, когда он добавляет обработчики событий, но никогда не очищает их явно. Кроме того, если вы повесите кучу вещей на один SocketAsyncEventArgs.Completed, вы также получите штраф за то, как отправляются события.   -  person paulecoyote    schedule 19.07.2020
comment
@paulecoyote Вы должны прикрепить только один обратный вызов к обработчику событий .Completed для каждого объекта SocketAsyncEventArgs и никогда не должны его отсоединять. Вы можете повторно использовать объект SAEA, и при удалении метод должен отсоединиться. Обычно вы создаете класс, в котором вы создаете методы для OnAccept, OnReceive, OnSent и прикрепляете эти 3 метода к каждому объекту SAEA, обычно 2 объекта на соединение RX/TX. Вы прикрепляете один и тот же метод к каждому объекту соединения и определяете, какое соединение вызвало его внутри метода. При закрытии сокета вы либо удаляете, либо сохраняете объект SAEA для последующего повторного использования.   -  person beatcoder    schedule 22.07.2020
comment
@beatcoder Кажется, вы получили то, что искали, если я могу спросить, просто из любопытства, для чего была ваша сетевая библиотека?   -  person Simple Fellow    schedule 05.01.2021
comment
@SimpleFellow - Извините, я только что заметил ваш комментарий. Эта библиотека все еще находится в стадии разработки и, вероятно, всегда будет, но она уже функциональна и имеет свое место в некоторых службах, в основном в веб-приложениях (обслуживающих http/s и веб-сокеты) и некоторых других онлайн-сервисах, которые требуют высокой частоты и низкой задержки. обмен данными.   -  person beatcoder    schedule 21.06.2021
comment
@beatcoder :) ты ответил. Спасибо. Я спросил, потому что в то время у меня также были высокие требования к производительности. Хотя не так много. Kestrel был лучшим выбором для меня с потоковой передачей grpc.   -  person Simple Fellow    schedule 22.06.2021
comment
@SimpleFellow - Kestrel - неплохой выбор, я копался в его коде, но, честно говоря, он мне не очень понравился, я думаю, что он в некотором роде слишком сложен, но работает нормально, так что это не так. вообще плохо. Я не знаю, насколько это надежно, никогда не пробовал ни для одного проекта.   -  person beatcoder    schedule 22.06.2021


Ответы (2)


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

О подходах:

Функции async/await имеют тенденцию создавать ожидаемые асинхронные Tasks, назначенные TaskScheduler среды выполнения dotnet, поэтому имея тысячи одновременных подключений, следовательно, тысячи или чтение /writing запустит тысячи задач. Насколько я знаю, это создает тысячи StateMachine, хранящихся в оперативной памяти, и бесчисленное количество переключений контекста в потоках, которым они назначены, что приводит к очень высокой нагрузке на ЦП. С несколькими соединениями/асинхронными вызовами он лучше сбалансирован, но по мере роста количества ожидаемых задач он замедляется в геометрической прогрессии.

Методы сокета BeginReceive/EndReceive/BeginSend/EndSend технически являются асинхронными методами без ожидаемых задач, но с обратными вызовами в конце вызова, что на самом деле больше оптимизирует многопоточность, но все же ограничения дизайна dotnet этих методов сокета плохо, на мой взгляд, но для простых решений (или ограниченного количества подключений) это путь.

Реализация сокета типа 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, управляемого максимально увеличить производительность тестовых машин.

Совет по дизайну:

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

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

Выбор неправильных размеров буфера приведет к потере производительности.

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

Различные настройки могут привести к разным результатам производительности на разных машинах и/или операционных системах!

Mono против Dotnet Core:

Поскольку я запрограммировал свою библиотеку сокетов совместимой с FW/Core, я мог протестировать их под Linux с моно и с основной компиляцией. Самое интересное, что я не заметил каких-либо заметных различий в производительности, оба были быстрыми, но, конечно, лучше оставить моно и компилировать в ядре.

Дополнительный совет по повышению эффективности:

Если ваша сетевая карта поддерживает RSS (масштабирование на стороне приема), включите ее в Windows в настройках сетевого устройства в дополнительных свойствах и установите для очереди RSS значение от 1 до максимально возможного/наилучшего для вашей производительности.

Если он поддерживается вашей сетевой картой, то он обычно устанавливается равным 1, это назначает сетевое событие для обработки ядром только одним ядром ЦП. Если вы можете увеличить это количество очередей до более высоких значений, то это распределит сетевые события между большим количеством ядер ЦП и приведет к гораздо более высокой производительности.

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

Надеюсь, мой опыт поможет кому-то из Вас!

person beatcoder    schedule 09.07.2019
comment
Нет ничего плохого в том, чтобы сделать то, что вы сделали с ответом. У меня были вопросы с самостоятельными ответами, которые длились годами, пока кто-то не пришел и не нашел действительно хорошее решение. - person Scott Chamberlain; 09.07.2019
comment
@СкоттЧемберлен | Спасибо за подсказку. Я работаю над этой библиотекой уже больше года, и до сих пор. Я отправил много вопросов и редко получал какие-либо подсказки или ответы, но я всегда получаю уведомление об этом вопросе, он очень популярен, даже если на него нет ответа. Я решил поделиться с читателями своим более чем годичным опытом, надеясь, что это поможет им, и им не придется пройти через то, что я прошел в прошлом году. Я отметил это как ответ, потому что он действительно отвечает на вопросы в исходном сообщении. Я действительно надеюсь, что это нормально и полезно. - person beatcoder; 09.07.2019
comment
Выполнив такую ​​большую задачу, вы не думали о том, чтобы как-то ее разделить? Github lib или что-то в этом роде? - person Noman_1; 20.12.2020
comment
@ Noman_1 - К сожалению, я не должен делиться им, поскольку он уже является базой для нескольких уже работающих сервисов, а совместное использование кода не разрешено из соображений безопасности и других политик. Несмотря на то, что я не планировал делиться своим кодом, я все же хотел помочь другим, столкнувшимся с этими проблемами, поэтому я сделал обзор своего прогресса. Я по-прежнему буду помогать всем, кто задает вопросы, потому что я испробовал множество способов разработки этой библиотеки, и я могу сэкономить время другим своими подсказками, но я не буду делиться точным кодом, который мы используем прямо сейчас. - person beatcoder; 22.12.2020

У меня такая же проблема. Вам следует изучить: NetCoreServer

Каждый поток в пуле потоков .NET clr может одновременно обрабатывать одну задачу. Таким образом, чтобы обрабатывать больше асинхронных подключений/чтений и т. д., вам необходимо изменить размер пула потоков, используя:

ThreadPool.SetMinThreads(Int32, Int32)

Использование EAP (асинхронный шаблон на основе событий) — это способ работы в Windows. Я бы использовал его и в Linux из-за упомянутых вами проблем и снижения производительности.

Лучше всего использовать порты завершения ввода-вывода в Windows, но они не переносимы.

PS: когда дело доходит до сериализации объектов, настоятельно рекомендуется использовать protobuf-net. Он бинарно сериализует объекты в 10 раз быстрее, чем бинарный сериализатор .NET, а также экономит немного места!

person Martin.Martinsson    schedule 11.09.2020
comment
Спасибо за совет, но мой проект уже достаточно продвинут, мой веб-сервер теперь способен обслуживать ~ 140 тыс. HTTP-запросов в секунду на Windows и около 200 тыс. / с на Linux на средней машине, соединение 10 Гбит / с уже является узким местом. Я также сделал собственный синтаксический анализатор json, который может сериализовать/десериализовать объекты в json, двоичный файл и обратно, что очень удобно, может обрабатывать потоки и примерно в 1,6 раза быстрее, чем msgpack или protobuf. Это были месяцы оптимизации и множество неуправляемого кода с использованием самых быстрых буферов и операций sse2+avx2. Это была адская работа, но оно того стоило. Тем не менее, все еще улучшая его. - person beatcoder; 11.09.2020
comment
Я вернулся к этому ответу, чтобы добавить, что изменение размера пула потоков на самом деле мало помогает. Это было в первых вещах, которые я пробовал. Когда у вас есть десятки тысяч одновременных подключений, пул потоков ведет себя по-разному в разных операционных системах. Даже при установке большого числа программа просто останавливается на несколько секунд для создания новых потоков, и очень плохо иметь сотни или даже тысячи таких потоков. Мой подход позволяет мне установить только несколько для разных задач, посвященных IOCP, уровню обработки и приложения, таким образом, он работает очень эффективно, не говоря уже о переключениях контекста. - person beatcoder; 21.11.2020
comment
beatcoder - есть ли способ связаться с вами напрямую? - person Newcomer; 17.05.2021
comment
@Newcomer - Да, почему бы и нет, но я не знаю правил размещения здесь контактов. У тебя есть что-то на уме? Электронное письмо? Есть ли здесь специальный способ обмена личными сообщениями? - person beatcoder; 21.06.2021