Проекты старого или нового SDK-style формата? Предположу, что здесь столкнулись не с особенностями проектной модели, а с тем, что всё просто не влезло в 32-битный процесс
Получается вы стартовали один таймер на каждый запрос?
Да. С таймером много решений можно придумать, но если проблема воспроизвелась в уже готовом приложении, в котором, например активно используется Task.Delay — проще сконфигурировать рантайм, прежде чем делать что-либо ещё.
я бы переключился на синхронные методы в контроллерах, тогда они будут выполняться в тредпуле. А если их слишком много, то сервер поставит поступающие запросы на ожидание
Если я правильно понял — предлагается использовать ограничение на количество потоков в тредпуле для реализации троттлинга. Это сработает, но приведёт к тому, что любой код, который хочет попасть на тредпул — будет ждать, пока в тредпуле не освободится или не создастся новый поток. И эта очередь будет обрабатываться в FIFO порядке, что снижает вероятность того, что клиенты не накидают в неё повторные запросы и она успешно разберётся.
MyThrottledTaskScheduler — это реализация TaskScheduler с тротлингом запросов. Если вдруг с шедулером не взлетает, то делается свой SynchronizationContext. С ним точно все будет ок.
Подозреваю, в MyThrottledTaskScheduler или MyThrottledSynchronizationContext придётся написать примерно такой же код, как у нас, магии же не бывает :)
Я бы сильно подумал откуда вообще такая конкуренция за буферы, почему так много потоков, которые пытаются делать IO?
Здесь проблемным приложением был сервер распределённой кастомной файловой системы, который открывает сразу множество файлов и раздаёт клиентам их фрагменты (здесь возникнет вопрос о целесообразности использования .NET в нём, но так уж получилось). В других приложениях такого не наблюдали.
К тому же интенсивное IO не было проблемой само по себе, на момент написания приложения проблемы не существовало — она возникла только после обновления рантайма .NET на серверах. .NET Framework позволяет иметь лишь одну версию рантайма (4.х) одновременно — 4.5.2 полностью заменяет 4.5.1, например. .NET Core, во избежание подобных проблем позволяет держать несколько версий рантайма (shared framework) на одной машине side-by-side, либо деплоить рантайм вместе с приложением.
Почему не immutable dictionary, кажется оно должно в этой ситуации работать на ура?
Immutable коллекции, такие как ImmutableList и ImmutableDictionary основаны на деревьях, объекты в которых переиспользуются их различными версиями. Но т.к. это деревья объектов — они дают большой оверхэд по памяти и нагрузку на GC, как и ConcurrentDictionary.
вытащить индекс в нативную память, разбить на сегменты и закрыть простым локом каждый сегмент.
Такое решение тоже должно сработать, придётся только подумать о том, как контролировать размеры сегментов и переаллоцировать их при необходимости. Другое дело, что само разбиение на сегменты уменьшит их размеры, что может сделать нецелесообразным их перенос в нативную память (не будут попадать в LOH), а с локом можно по-разному экспериментировать — тот же SpinLock использовать.
В другом проекте инфраструктуры переносили часть объектов, создающих большой memory traffic в нативную память, там дошло до использования TCMalloc, возможно, когда-нибудь и об этом опыте кто-нибудь расскажет.
Это решение не стали использовать т.к. это привело бы к формированию большой очереди на локе из запросов, при обработке которых происходит обращение к коллекции на чтение, если одновременно с этим происходит запись. Отвечая на исходный вопрос, пробовали ли — уже не помню.
Дополнительно стоит сравнить оверхед от использования ReaderWriterLockSlim по сравнению с обычным lock на Monitor, в интернетах ходят слухи, что rwlock жирнее, чтобы убедиться в целесообразно его использования вместе с Dictionary.
Проблема с реализаций Task.Delay здесь опосредована от протокола. Конкретно для этой задачи мы пробовали HTTP/2 и столкнулись с таким же lock convoy'ем и некоторыми другими проблемами.
Почему не взять голанг?
примитивы синхронизации надёжны, как швейцарские часы, и просты
Скорее всего, такая уверенность останется только до первой проблемы, которая возникнет при их использовании. Нет гарантии не набажить самому или не наткнуться на неожиданное поведение в корнер-кейсе.
Большинство имеющегося в компании кода написано на C#, поддерживается разработчиками, которые пишут на C# и знают особенности .NET. Обосновать переход на Go с отбрасыванием имеющейся кодовой базы по причине более надёжных примитивов синхронизации — сложно, с таким же успехом можно предложить использовать C++ потому что в нём нет GC и ручное управление памятью надёжнее или Python, потому что код на нём не выглядит заумно, да и любую другую технологию по любой другой причине. В новых, изолированных проектах, используется не только .NET, но останавливать разработку уже успешно работающего проекта — большого смысла не вижу.
А что будет со всеми траснляторами в sql? Они справятся с трансляцией исключения?
Фреймворки вида LINQ to SQL, в отличие от LINQ методов для коллекций, используют не делегаты, а деревья выражений (expression trees), по которым строят запрос к базе. В деревьях выражений нельзя использовать throw-выражения.
Такой код не скомпилируется:
void A(Expression<Func<int, bool>> expr) {}
A(x => throw new Exception()); // [CS8188] An expression tree may not contain a throw-expression.
Также в expression'ах нет поддержки других конструкций C# 7: кортежей (tuple literals), объявлений переменных при вызове метода с out-параметром (out var)
Иногда (например в этом докладе) stackalloc не рекомендуют использовать, говоря, что он заполняет выделенную память нулями, и из-за этого работает медленно. На гитхабе coreclr эта тема тоже обсуждалась, и там есть пример, когда stackalloc не заполняет память нулями (я немного модифицировал этот пример):
const int Size = 16384;
static unsafe void Main() {
Foo();
byte* p = stackalloc byte[Size];
Console.WriteLine(p[0]);
}
static unsafe void Foo() {
byte* p = stackalloc byte[Size];
for (int i = 0; i < Size; i++)
p[i] = 42;
}
Если в этом коде менять Size на разные значения — можно получить разный результат. На моей машине с .NET Framework 4.7.1 результаты такие:
RyuJit x64:
Size: 1-48, значение: 0
Size: 49-64 — «случайное» число,
Size >=65 — 42
LegacyJit x86:
Size 1-24 — 0
Size >=25 — 42
При этом в машинном коде Foo заполнение нулями присутствует.
Отсюда появляются вопросы — в каких случаях при использовании stackalloc память обнуляется, как это влияет на производительность, и зачем вообще нужно обнуление, если на него нельзя полагаться?
Хак с добавлением
<GenerateBindingRedirectsOutputType>
не срабатывает? У меня, на вид, получилось.Проекты старого или нового SDK-style формата? Предположу, что здесь столкнулись не с особенностями проектной модели, а с тем, что всё просто не влезло в 32-битный процесс
Да. С таймером много решений можно придумать, но если проблема воспроизвелась в уже готовом приложении, в котором, например активно используется
Task.Delay
— проще сконфигурировать рантайм, прежде чем делать что-либо ещё.Если я правильно понял — предлагается использовать ограничение на количество потоков в тредпуле для реализации троттлинга. Это сработает, но приведёт к тому, что любой код, который хочет попасть на тредпул — будет ждать, пока в тредпуле не освободится или не создастся новый поток. И эта очередь будет обрабатываться в FIFO порядке, что снижает вероятность того, что клиенты не накидают в неё повторные запросы и она успешно разберётся.
Подозреваю, в
MyThrottledTaskScheduler
илиMyThrottledSynchronizationContext
придётся написать примерно такой же код, как у нас, магии же не бывает :)Здесь проблемным приложением был сервер распределённой кастомной файловой системы, который открывает сразу множество файлов и раздаёт клиентам их фрагменты (здесь возникнет вопрос о целесообразности использования .NET в нём, но так уж получилось). В других приложениях такого не наблюдали.
К тому же интенсивное IO не было проблемой само по себе, на момент написания приложения проблемы не существовало — она возникла только после обновления рантайма .NET на серверах. .NET Framework позволяет иметь лишь одну версию рантайма (4.х) одновременно — 4.5.2 полностью заменяет 4.5.1, например. .NET Core, во избежание подобных проблем позволяет держать несколько версий рантайма (shared framework) на одной машине side-by-side, либо деплоить рантайм вместе с приложением.
Immutable коллекции, такие как
ImmutableList
иImmutableDictionary
основаны на деревьях, объекты в которых переиспользуются их различными версиями. Но т.к. это деревья объектов — они дают большой оверхэд по памяти и нагрузку на GC, как иConcurrentDictionary
.Такое решение тоже должно сработать, придётся только подумать о том, как контролировать размеры сегментов и переаллоцировать их при необходимости. Другое дело, что само разбиение на сегменты уменьшит их размеры, что может сделать нецелесообразным их перенос в нативную память (не будут попадать в LOH), а с локом можно по-разному экспериментировать — тот же SpinLock использовать.
В другом проекте инфраструктуры переносили часть объектов, создающих большой memory traffic в нативную память, там дошло до использования TCMalloc, возможно, когда-нибудь и об этом опыте кто-нибудь расскажет.
Дополнительно стоит сравнить оверхед от использования
ReaderWriterLockSlim
по сравнению с обычнымlock
наMonitor
, в интернетах ходят слухи, что rwlock жирнее, чтобы убедиться в целесообразно его использования вместе сDictionary
.Почему не взять голанг?
Скорее всего, такая уверенность останется только до первой проблемы, которая возникнет при их использовании. Нет гарантии не набажить самому или не наткнуться на неожиданное поведение в корнер-кейсе.
Большинство имеющегося в компании кода написано на C#, поддерживается разработчиками, которые пишут на C# и знают особенности .NET. Обосновать переход на Go с отбрасыванием имеющейся кодовой базы по причине более надёжных примитивов синхронизации — сложно, с таким же успехом можно предложить использовать C++ потому что в нём нет GC и ручное управление памятью надёжнее или Python, потому что код на нём не выглядит заумно, да и любую другую технологию по любой другой причине. В новых, изолированных проектах, используется не только .NET, но останавливать разработку уже успешно работающего проекта — большого смысла не вижу.
Фреймворки вида LINQ to SQL, в отличие от LINQ методов для коллекций, используют не делегаты, а деревья выражений (expression trees), по которым строят запрос к базе. В деревьях выражений нельзя использовать throw-выражения.
Такой код не скомпилируется:
Также в expression'ах нет поддержки других конструкций C# 7: кортежей (tuple literals), объявлений переменных при вызове метода с out-параметром (out var)
Иногда (например в этом докладе)
stackalloc
не рекомендуют использовать, говоря, что он заполняет выделенную память нулями, и из-за этого работает медленно. На гитхабе coreclr эта тема тоже обсуждалась, и там есть пример, когдаstackalloc
не заполняет память нулями (я немного модифицировал этот пример):Если в этом коде менять Size на разные значения — можно получить разный результат. На моей машине с .NET Framework 4.7.1 результаты такие:
RyuJit x64:
LegacyJit x86:
При этом в машинном коде
Foo
заполнение нулями присутствует.Отсюда появляются вопросы — в каких случаях при использовании
stackalloc
память обнуляется, как это влияет на производительность, и зачем вообще нужно обнуление, если на него нельзя полагаться?