Как стать автором
Обновить
53
0
Евгений Пешков @epeshk

Пользователь

Отправить сообщение

Хак с добавлением <GenerateBindingRedirectsOutputType> не срабатывает? У меня, на вид, получилось.


<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>

Проекты старого или нового 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 память обнуляется, как это влияет на производительность, и зачем вообще нужно обнуление, если на него нельзя полагаться?

Информация

В рейтинге
Не участвует
Работает в
Зарегистрирован
Активность