Русский
Русский
English
Статистика
Реклама

Wcf

Опыт портирования legacy enterprise проекта c Net Framwork на Net Core

10.08.2020 12:13:56 | Автор: admin

Опыт портирования legacy enterprise проекта c Net Framwork на Net Core


net framework to net core


Вводная часть


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


Что имеем


Дано: Enterprise система, которая написана с использованием следующих технологий (всё написано на C# под Net Framework):


  • Множество ASMX web-служб
  • Множество WCF служб
  • Бэкграунд задачи на Workflow Foundation
  • Web-приложения на WebForms и частично на ASP.NET MVC 4
  • Отчёты SQL Server Reporting Services
  • Утилиты на Windows Forms и консольные приложения

Зачем переходим на Net Core:


  • Обновляем стек технологий
  • Избавляемся от старых технологий (разработчики счастливы, а новые нанимаемые разработчики не пугаются)
  • Используем все преимущества NetCore: мультиплатформенность, масштабируемость, контейнеризация

Есть статья Выбор между .NET Core и .NET Framework для серверных приложений, которая вам поможет что выбрать.


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


Для того, чтобы добиться переносимости кода между различными средами исполнения (Framework и Core), нам на помощью приходит NetStandard (а конкретнее netstandard2.0).


Ещё нужно быть готовым к тому, что часть технологий частично или полностью отсутствует в NetCore, а конкретно:


  • Windows Communication Foundation. Нет API для служб, только API для клиентской стороны
  • Workflow Foundation
  • ASP.NET Web Forms (отсутствует System.Web)
  • Отсутствует API работы с очередями MSMQ

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


Детали переноса компонентов, служб, подсистем


Директивы препроцессора и условная компиляция


Директивы препроцессора (условная компиляция #if) помогает в тех случаях, если нет нет необходимых API и часть методов или целиком классы нельзя портировать с .Net Framewrk на .Net Core.


Когда приходилось использовать #if:


  • Часть API переехало в другие namespace'ы и приходилось их импортировать для .Net Core реализации
  • Частично переписывать реализацию методов в связи с отсутствующем API
  • Полносстью отказываться от нектороых классов в связи с невозможностью их портирования на .Net Core

Примеры условных директив, которые приходилось применять на проекте:


Часто используемая проверка: реализация кода для Net Framework иначе реализуем для NetStandard или NetCore:


#if NETFRAMEWORK#elif NETSTANDARD || NETCOREAPP#endif

В очень редких случаях если нужна была реализация для NetCore или NetFramework (связано с тем, что использовались API, которых нет в .NetStandard2.0):


#if NETFRAMEWORK#elif NETCOREAPP#endif

Как вы заметили, специально не применялись проверки на конкретные версии runtime'ов для упрощения кода.


Target'ы в *.csproj проектах выглядят так:


    <TargetFrameworks>netstandard2.0;net471;netcoreapp3.1</TargetFrameworks>

Больше про Кроссплатформенное нацеливание


Реализация заглушек API


В редких случаях вам придётся скопировать часть интрерфейсного API, которая отсутствует в NetStandard и NetCore реализации. Например, чтобы избежать большого числа условных директив в коде, пришлось скопировать часть атрибутов WCF, конфигурации, а также некоторые классы в виде заглушек.


Например, в наших контрактах часто используется WCFный атрибут TransactionFlowAttribute, но он будет использоваться, но интерфесы, нак которые он навешивается используется повсеместно, поэтому далем так:


#if NETFRAMEWORK#elif NETSTANDARD || NETCOREAPPnamespace System.ServiceModel{    [System.AttributeUsage(System.AttributeTargets.Method)]    public sealed class TransactionFlowAttribute : Attribute, System.ServiceModel.Description.IOperationBehavior    {        public TransactionFlowAttribute(TransactionFlowOption transactions)        {            Transactions = transactions;        }        public TransactionFlowOption Transactions { get; }        public void AddBindingParameters(OperationDescription operationDescription,            BindingParameterCollection bindingParameters)        {        }        public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)        {        }        public void ApplyDispatchBehavior(OperationDescription operationDescription,            DispatchOperation dispatchOperation)        {        }        public void Validate(OperationDescription operationDescription)        {        }    }    public enum TransactionFlowOption    {        NotAllowed,        Allowed,        Mandatory,    }}#endif

Заворачиваем всё в NuGet пакеты


Для возможности подключения кода на разных рантаймах рекомендую заворачивать ваши сборки в NuGet пакеты, так что будет возможность иметь сразу несколько реализаций одновременно для NetFramework and NetCore.


http://personeltest.ru/aways/docs.microsoft.com/ru-ru/dotnet/standard/library-guidance/media/cross-platform-targeting/nuget-package-multiple-assemblies.png


Инфраструктурная часть


К инфраструктурной части приложения относится:


  • Хостинг приложения (Реализация служб)
  • Логирование
  • Обработка ошибок
  • Конфигурация
  • Загрузочная часть (Bootstrapper)
  • Мониторинг

Конфигурация приложений


Конфигурация классических приложений Net Framework основывается на файлах app.config, и web.config, которые представляют из себя XML файлы и API работы с ними: System.Configuration и класс System.Configuration.ConfigurationManager. Например, часто приходится считывать данные из AppSettings и гораздо реже делать свои классы конфигурации в ConfigurationSection.


В NetCore появилось новое API, которое позволяет работать с конфигурацией в различных форматах (JSON, INI, XML) и использовать различные источники (Файлы, Командная строка, Переменные окружения и т. д.)


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


Кстати, в StackOverflow имеется куча вопросов как организовать конфигурацию


Если всё-таки нужно использовать старую реализацию, то имеется NuGet пакет System.Configuration.ConfigurationManager.


NetCore реализация конфигурации более интуитивная и простая. Здесь вы работаете с конфигурацией по конкретному пути в конфигурационном файле, либо как с объектом (и не нужно описывать сложных ConfigurationSection)


Логирование


В старом проекте применялось логирование в EventLog с использованием API из System.Diagnostics, но а так же была абстракция в виде интерфейса ILogger, которая позволяла логировать с различными уровнями messageLevel (Debug, Info, Warning, Error) и указанием категорий. В более новых проектах уже применялся NLog c той же самой абстракцией ILogger.


В NetCore появилось новое универсальное API: Microsoft.Extensions.Logging, которое предоставляет интерфейс ILogger<T>.


Мы же продолжили использовать нашу абстракцию ILogger, потому что она везде, но конкретная реализация уже использует Microsoft.Extensions.Logging.ILogger<T>, а также она легко позволяет подключить и сконфигурировать кучу существующих логеров, например: NLog, log4Net, Serilog и т.д.


Внедрение зависимостей и инверсия управления


В наших проектах использовались IoC-контейнеры Unity, а в более новых AutoFac, либо вовсе отсутствовали.


В NetCore добавлена абстракция Microsoft.Extensions.DependencyInjection с использованием класса ServiceCollection, которая позволяет регистрировать типы с уровнями:


  • Singleton создаст инстанс один раз и будет всегда переиспользовать его
  • Scoped в рамках некого контекста, например на время Http-запроса
  • Transient будет создавать инстанс каждый раз

Также имеется класс IServiceProvider, который обеспечивающий получение нужной регистрации.


Более подробнее читаем тут: Внедрение зависимостей в ASP.NET Core.


Советы:


  • Всегда используйте внедрение зависимостей через конструктор, затем методы или свойства. Если необходимо что-то конструировать в конкретном методе, то можете внедрить какую-нибудь фабрику и создавать что-либо вызывая её методы, саму фабрику регистриуем отдельно и поближе к конфигурации
  • Не пробрасывать контекст IoC-контейнера в код (это заставило нас попотеть, чтобы вынести код из классов)
  • Сосредоточьте регистрацию в одном месте

Избавляемся от Global Assembly Cache


Очень давно на проекте было принято решение использовать Global Assembly Cache, чтобы приложения не искали сборки и было централизованное место где они лежат.


Net Core не умеет в GAC, поэтому было принято решение написать кастомный AssemblyResolver, который искал бы в заданной директории используя конфигурацию.


Модели приложений


Консольные приложения


Принципиальных изменений в консольных приложениях нет, кроме того что нужно использовать новое API конфигурации, логгиирования.


Достаточно поменять *.cproj файл в формат SDK: <Project Sdk="Microsoft.NET.Sdk"> ну и соответственно использовать <TargetFramework>netcoreapp3.1</TargetFramework>


Windows Forms


Начиная с версии NetCore 3.0 появилась возможность запускать приложения Windows Forms, но после перехода на версию Net Core 3.1 часть legacy контроллов было удалено, поэтому
придётся немного переписать приложения.


Вот список контроллов, которые были "выпилены":


  • DataGrid и связанные с ним типы. Можно заменить на DataGridView;
  • ToolBar. Заменяем на ToolStrip;
  • MainMenu. Заменяем на MenuStrip;
  • ContextMenu. Заменяем на ContextMenuStrip.

Более подробно про изменения можно почитать Критические изменения в Windows Forms.


Позднее Microsoft выпустили руководство Процесс переноса классического приложения Windows Forms в .NET Core,
которое сильно помогло нам.


На первых этапах существования Windows Forms для NetCore 3.0 отсутствовал дизайнер форм для Visual Studio 2019, поэтому приходилось рисовать GUI в Net Framework, а потом переключаться на NetCore 3.0, но более поздних редакция появилась такая возможность.


Переносим ASP.NET MVC приложения в Asp Net Core MVC


Перенос будет чуть сложнее чем Windows Form приложения, но всё-равно всё происходит достаточно безболезненно.


Первое большое изменение убран бутрстраппер Global.asax и заменён на класс Startup.


Второе изменение отсутствует сборка System.Web и все типы HttpSession, Cookies, HttpRequest и т. д. соответственно.


Для общего представления читаем тут: Запуск приложения в ASP.NET Core


Третье большое изменение изменена модель аутентификации, авторизации, обработка ошибок, фильтры, отсутствуют HTTP модули.


Подробнее можно прочитать здесь: Миграция обработчиков и модулей HTTP в ASP.NET Core по промежуточного слоя. Для этого всего используется новая модель Middleware. Теперь можно организовать конвейер запроса и вклиниваться на каждом этапе прохождения запроса:


Конвейер ПО промежуточного слоя ASP.NET Core


Конвейер фильтра ASP.NET Core


Пример простейшего Middleware:


public class Startup{    public void Configure(IApplicationBuilder app)    {        app.Use(async (context, next) =>        {            // Do work that doesn't write to the Response.            await next.Invoke();            // Do logging or other work that doesn't write to the Response.        });        app.Run(async context =>        {            await context.Response.WriteAsync("Hello");        });    }}

API контроллеров практически не поменялось, разметка представлений Razor также практически не поменялась, смотрим тут: Обработка запросов с помощью контроллеров в ASP.NET Core MVC и тут: Представления в ASP.NET Core MVC.


Следующее изменение отсутствие Bundler'а для объединения и минификации js и css файлов, поэтому читаем тут: Объединение и минификация статических ресурсов в ASP.NET Core


ASPNET ASMX переносим на AspNetCore WebAPI


В данном случае всё-таки придётся отказаться от использования SOAP и использовать HTTP с JSON или XML.


На каждый asmx сервис создаём контроллер WebAPI и на каждый WebMethod создаём
Action POST метод и переносим соответствующий код с реализацией из asmx сервиса. Недостаток заключается в том, что мы полностью отходим от SOAP модели и вам также придётся переписывать клиентов. Если хотите, то можете оформить в виде Rest-служб.


Другой вариант придётся использовать сторонние библиотеки, которые могут в SOAP, например: SoapCore.


Ещё один вариант JSON-RPC, имеется куча различных библиотек под NetCore, все они хорошо внедряются в AspNet Core через Middleware.


Также у Microsoft имеется guide как написать с помощью AspNetCore Middleware свой SOAP обработчик: Custom ASP.NET Core Middleware Example.


ASPNET WebApi переносим на AspNetCore WebAPI


Достаточно простая задача, потому что идеология простоя: есть контроллеры и экшны.


Следуем этому guide'у: Переход с веб-API ASP.NET на ASP.NET Core


шаги почти аналогичные с переносом MVC:


  • Global.asax и заменяем на класс Startup
  • Настраиваем авторизация и аутентификацию
  • Настраиваем Logger, Exception Handler
  • Портируем фильтры и т.д.

ASPNET Web Forms переносим на Blazor


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


Почему было решено портировать Web Forms на Blazor:


  • Компонентная структура кода (контроллы)
  • Code-behind стиль: есть разметка и есть логика и они друг с другом сильно связаны
  • Минимум Java-Script разработки (для кастомизации заказчики и разработчики предпочитают кодить на C#)

Для переноса было написано 2 утилиты:


  • Первая парсила aspx страницы, конвертировала в XML код (так удобнее работать на C#) и описывала иерархию контроллов в виде дерева в достаточно абстрактном виде (тут кнопка, grid и т.д.), также генерировала html-разметку отделённую от aspx
  • Вторая утилита делала обратную работу: генерировала Blazor-страницы и code-behind мы дописывали сами

В связи с тем, что у нас интерфейсы достаточно однотипные и простые, нам достаточно легко удалось портировать ASPNET Web Forms приложения.


Избавляемся от WCF


В NetCore есть частичная реализация WCF Client API:


  • Имеются только простые BasicHttpBinding, NetTcpBinding
  • Нет security на Massage уровне, только на Transport
  • Нет поддержки распределённых транзакций

Так как серверная сторона WCF полностью отсутствует в Net Core, то есть несколько вариантов:


  • Портируем как AspNetCore WebAPI
  • Портируем как AspNetCore gRPC
  • Используем стороннюю библиотеку CoreWCF с многими ограничениям
  • Портируем как AspNetCore + JSON-RPC

Перенос WCF сервисов на AspNetCore WebAPI аналогичен с asmx службами: на каждый сервис свой контроллер и свой action-метод на каждый метод WCF. Способ реализации полностью ложится на Вас: каждый метод может принимать Post-запрос, где URL будет иметь название соответствующего метода как https://you-service.com/MethodName, ну или выбираем JSON-RPC.


Другой вариант AspNetCore gRPC: Перенос службы WCF "запрос ответ" в gRPC унарный RPC


Пример WCF службы:


[ServiceContract]public interface IItemService{    [OperationContract]    Task<Item> GetItem(int id);}[DataContract]public class Item{    [DataMember]    public int Id { get; set; }    [DataMember]    public string Name { get; set; }}

Пример gRPC контракта в protobuf формате:


message GetItemRequest {    int32 id = 1;}message Item {    int32 id = 1;    string name = 2;}service ItemService {    rpc GetItem(GetItemRequest) returns (Item);}

Какой вариант реализации решать вам, но я предпочитаю следующее:


  • Все внешние службы реализовать в виде WebAPI в стиле REST, либо JSON-RPC
  • Внутренние службы взаимодействуют по gRPC

Избавляемся от Workflow Foundation


Службы на Workflow Foundation полностью отсутствуют (в Microsoft, видимо, поняли что графическое представление службы никому не удобно и проще всё писать кодом), поэтому у вас есть такие варианты:



Лично мы решили просто избавиться от Workflow Foundation, он нам всегда доставлял неудобства и сделали старым добрым кодом. А что же может быть лучше старого доброго кода?


Подводим итоги


В итоге к чему приходим? А приходим к тому, что часть подсистем переносятся в лоб, часть с небольшими доработками, но часть придётся глубоко перерабатывать,
что может потребовать много усилий. Но у нас уже выработался стиль реализации подсистем: для внешних систем службы всегда делать HTTP Rest like службы, если нужна производительность
(а для бизнес задач стандартных средств хватает с головой) gRPC лучший подход. И да Побыстрее избавляйтесь (по возможности) от Web Forms.

Подробнее..

Как WCF сам себе в ногу стреляет посредством TraceSource

21.06.2021 14:12:41 | Автор: admin

Не так часто удается написать что-то интересное про проблемы, связанные с параллельным программированием. В этот же раз "повезло". Из-за особенностей реализации стандартного метода TraceEvent произошла ошибка с блокировкой нескольких потоков. Хочется предупредить о существующем нюансе и рассказать об интересном случае из поддержки наших пользователей. Причем тут поддержка? Это вы узнаете из статьи. Приятного чтения.

Предыстория

В дистрибутиве PVS-Studio есть одна утилита под названием CLMonitor.exe, или система мониторинга компиляции. Она предназначена для "бесшовной" интеграции статического анализа PVS-Studio для языков C и C++ в любую сборочную систему. Сборочная система должна использовать для сборки файлов один из компиляторов, поддерживаемых анализатором PVS-Studio. Например: gcc, clang, cl, и т.п.

Стандартный сценарий работы данной Windows утилиты очень простой, всего 3 шага:

  1. Выполняем 'CLMonitor.exe monitor';

  2. Выполняем сборку проекта;

  3. Выполняем 'CLMonitor.exe analyze'.

Первый шаг запускает 'сервер', который начинает отслеживать все процессы компиляторов в системе до тех пор, пока его не остановят. Как только мы запустили сервер выполняем сборку проекта, который мы хотим проанализировать. Если сборка прошла, то нужно запустить анализ. Для этого мы исполняем третий шаг. 'CLMonitor.exe analyze' запускает 'клиент', который говорит серверу: "Всё, хватит, выключайся и давай сюда результаты мониторинга за процессами". В этот момент сервер должен завершить свою работу, а клиент запустить анализ. Подробнее о том, как внутри работает система мониторинга и зачем сервер вообще собирает процессы, мы поговорим чуть позже.

И вот в один прекрасный момент описанный сценарий не заработал, анализ просто-напросто не запустился. Ну и чтобы было поинтереснее, возникла эта проблема не у нас, а у пользователя, который обратился к нам в поддержку. У него стабильно после запуска анализа происходило десятиминутное ожидание ответа от сервера с дальнейшим выходом из программы по timeout'у. В чём причина непонятно. Проблема не воспроизводится. Да... беда. Пришлось запросить дампфайл для процесса нашей утилиты, чтобы посмотреть, что там происходит внутри.

Примечание. Проблема у пользователя возникла при использовании Windows утилиты CLMonitor.exe. Поэтому все дальнейшие примеры будут актуальны именно для Windows.

Как работает CLMonitor.exe

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

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

Зачем мы вообще отлавливаем процессы

Как вы поняли, история начинается с того, что нужно запустить сервер, который будет отлавливать все процессы. Делаем мы это не просто так. Вообще, более удобный способ проанализировать C++ проект это прямой запуск анализатора через утилиту командной строки PVS-Studio_Cmd. У неё, однако, есть существенное ограничение она может проверять только проекты для Visual Studio. Дело в том, что для анализа требуется вызывать компилятор, чтобы он препроцессировал проверяемые исходные файлы, ведь анализатор работает именно с препроцессированными файлами. А чтобы вызвать препроцессор, нужно знать:

  • какой конкретно компилятор вызывать;

  • какой файл препроцессировать;

  • параметры препроцессирования.

Утилита PVS-Studio_Cmd узнает все необходимое из проектного файла (*.vcxproj). Однако это работает только для "обычных" MSBuild проектов Visual Studio. Даже для тех же NMake проектов мы не можем получить необходимую анализатору информацию, потому что она не хранится в самом проектном файле. И это несмотря на то, что NMake также является .vcxproj. Сам проект при этом является как бы обёрткой для другой сборочной системы. Тут в игру и вступают всяческие ухищрения. Например, для анализа Unreal Engine проектов мы используем прямую интеграцию с *Unreal Build Tool * сборочной системой, используемой "под капотом". Подробнее здесь.

Поэтому, для того чтобы можно было использовать PVS-Studio независимо сборочной системы, даже самой экзотической, у нас и появилась утилита CLMonitor.exe. Она отслеживает все процессы во время сборки проекта и отлавливает вызовы компиляторов. А уже из вызовов компиляторов мы получаем всю необходимую информацию для дальнейшего препроцессирования и анализа. Теперь вы знаете, зачем нам нужно мониторить процессы.

Как клиент запускает анализ

Для обмена данными между сервером и клиентом мы используем программный фреймворк WCF (Windows Communication Foundation). Давайте далее кратко опишем, как мы с ним работаем.

С помощью класса *ServiceHost *создается именованный канал, по которому будет происходить обмен сообщениями между процессами клиента и сервера. Вот как это выглядит на стороне сервера:

static ErrorLevels PerformMonitoring(....) {  using (ServiceHost host = new ServiceHost(                       typeof(CLMonitoringContract),                          new Uri[]{new Uri(PipeCredentials.PipeRoot)}))   {    ....    host.AddServiceEndpoint(typeof(ICLMonitoringContract),                             pipe,                             PipeCredentials.PipeName);    host.Open();         ....  }}

Обратите тут внимание на две вещи: *CLMonitoringContract *и ICLMonitoringContract.

*ICLMonitoringContract * это сервисный контракт. *CLMonitoringContract * реализация сервисного контракта. Выглядит это так:

[ServiceContract(SessionMode = SessionMode.Required,                  CallbackContract = typeof(ICLMonitoringContractCallback))]interface ICLMonitoringContract{  [OperationContract]  void StopMonitoring(string dumpPath = null);} [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]class CLMonitoringContract : ICLMonitoringContract{  public void StopMonitoring(string dumpPath = null)  {    ....    CLMonitoringServer.CompilerMonitor.StopMonitoring(dumpPath);  } }

Когда мы запускаем клиент, нам нужно остановить работу сервера и забрать у него все необходимые данные. Благодаря данному интерфейсу мы это и делаем. Вот как выглядит остановка сервера со стороны клиента:

public void FinishMonitor(){  CLMonitoringContractCallback сallback = new CLMonitoringContractCallback();  var pipeFactory = new DuplexChannelFactory<ICLMonitoringContract>(           сallback,            pipe,            new EndpointAddress(....));  ICLMonitoringContract pipeProxy = pipeFactory.CreateChannel();  ((IContextChannel)pipeProxy).OperationTimeout = new TimeSpan(24, 0, 0);  ((IContextChannel)pipeProxy).Faulted += CLMonitoringServer_Faulted;  pipeProxy.StopMonitoring(dumpPath);}

Когда клиент выполняет метод StopMonitoring, он на самом деле выполняется у сервера и вызывает его остановку. А клиент получает данные для запуска анализа.

Всё, теперь вы, хоть немного, имеете представление о внутренней работе утилиты CLMonitor.exe.

Просмотр дамп файла и осознание проблемы

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

**Интересный факт. **Откуда вообще взялись эти 10 минут? Дело в том, что мы задаем время ожидания ответа от сервера намного больше, а именно - 24 часа, как видно в примере кода, приведённом выше. Однако для некоторых операций фреймворк сам решает, что это слишком много и он успеет быстрее. Поэтому берет только часть от изначального значения.

Мы попросили у пользователя снять дамп с двух процессов (клиент и сервер) минуток через 5 после запуска клиента, чтобы посмотреть, что там происходит.

Тут небольшая пауза. Хочется быть честным по отношению к моему коллеге Павлу и упомянуть, что это именно он разобрался в данной проблеме. Я же ее просто чинил, ну и вот сейчас описываю :) Конец паузы.

Дамп 'клиента'

Так вот, когда мы открыли дамп файл клиента, перед глазами предстал следующий список потоков:

Нас интересует главный поток. Он висит на методе, отвечающем за запрос остановки сервера:

public void FinishMonitor(){  ....  ICLMonitoringContract pipeProxy = pipeFactory.CreateChannel();  ((IContextChannel)pipeProxy).OperationTimeout = new TimeSpan(24, 0, 0);  ((IContextChannel)pipeProxy).Faulted += CLMonitoringServer_Faulted;  pipeProxy.StopMonitoring(dumpPath);            // <=  ....}

Получается, что клиент попросил сервер завершить работу, а ответа не последовало. И это странное поведение, так как обычно данная операция занимает доли секунды. А я напомню, что в данном случае дамп был снят спустя 5 минут от начала работы клиента. Что ж, давайте смотреть, что там на сервере происходит.

Дамп 'сервера'

Открываем его и видим следующий список потоков:

Воу-воу, откуда так много TraceEvent'ов? Кстати, на скриншоте не уместилось, но всего их более 50. Ну давайте подумаем. Данный метод у нас используется, чтобы логировать различную информацию. Например, если отловленный процесс является компилятором, который не поддерживается, произошла ошибка считывания какого-либо параметра процесса и т.д. Посмотрев стеки данных потоков, мы выяснили, что все они ведут в один и тот же метод в нашем коде. А метод этот смотрит, является ли отловленный нашей утилитой процесс компилятором или это нечто иное и неинтересное, и, если мы отловили такой неинтересный процесс, мы это логируем.

Получается, что у пользователя запускается очень много процессов, которые, конкретно для нас, являются 'мусором'. Ну допустим, что это так. Однако данная картина все равно выглядит очень подозрительно. Почему же таких потоков так много? Ведь, по идее, логирование должно происходить быстро. Очень похоже на то, что все эти потоки висят на какой-то точке синхронизации или критической секции и чего-то ждут. Давайте зайдем на ReferenceSource и посмотрим исходный код метода TraceEvent.

Открываем исходники и действительно видим в методе TraceEvent оператор lock:

Мы предположили, что из-за постоянной синхронизации и логирования накапливается большое количество вызовов методов TraceEvent, ждущих освобождения TraceInternal.critSec. Хм, ну допустим. Однако это пока не объясняет, почему сервер не может ответить клиенту. Посмотрим еще раз в дамп файл сервера и заметим один одинокий поток, который висит на методе DiagnosticsConfiguration.Initialize:

В данный метод мы попадаем из метода NegotiateStream.AuthenticateAsServer, выполняющего проверку подлинности со стороны сервера в соединении клиент-сервер:

В нашем случае клиент-серверное взаимодействие происходит с помощью WCF. Плюс напоминаю, что клиент ждет ответ от сервера. По этому стеку очень похоже, что метод DiagnosticsConfiguration.Initialize был вызван при запросе от клиента и теперь висит и ждет. Хм... а давайте-ка зайдем в его исходный код:

И тут мы замечаем, что в данном методе имеется критическая секция, да еще и на ту же самую переменную. Посмотрев, что вообще такое этот critSec, увидим следующее:

Собственно, у нас уже есть достаточно информации, чтобы подвести итоги.

Интересный факт. Изучая просторы интернета в поисках информации про данную проблему с TraceEvent была обнаружена интересная тема на GitHub. Она немного о другом, но есть один занимательный комментарий от сотрудника компании Microsoft:

"Also one of the locks, TraceInternal.critSec, is only present if the TraceListener asks for it. Generally speaking such 'global' locks are not a good idea for a high performance logging system (indeed we don't recommend TraceSource for high performance logging at all, it is really there only for compatibility reasons)".

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

Итоги изучения дампов

Итак, что мы имеем:

  1. Клиент общается с сервером с помощью фреймворка WCF.

  2. Клиент не может получить ответа от сервера. После 10 минут ожидания он падает по тайм-ауту.

  3. На сервере висит множество потоков на методе TraceEvent и всего один - на Initialize.

  4. Оба метода зависят от одной и той же переменной в критической секции, притом это статическое поле.

  5. Потоки, в которых выполняется метод TraceEvent, бесконечно появляются и из-за lock не могут быстро сделать свое дело и исчезнуть. Тем самым они долго не отпускают объект в lock.

  6. Метод Initialize возникает при попытке клиента завершить работу сервера и висит бесконечно на lock.

Из этого можно сделать вывод, что сервер получил команду завершения от клиента. Чтобы начать выполнять метод остановки работы сервера, необходимо установить соединение и выполнить метод Initialize. Данный метод не может выполниться из-за того, что объект в критической секции держат методы TraceEvent, которые в этот момент выполняются на сервере. Появление новых TraceEvent'ов не прекратится, потому что сервер продолжает работать и отлавливать новые 'мусорные' процессы. Получается, что клиент никогда не получит ответа от сервера, потому что сервер бесконечно логирует отловленные процессы с помощью TraceEvent. Проблема найдена!

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

Теперь остается только воспроизвести и починить проблему.

Воспроизведение проблемы

Само воспроизведение получается крайне простое. Все, что нам нужно, это сделать так, чтобы сервер постоянно что-то логировал. Потому создаем метод с говорящим названием CrazyLogging, который и будет это делать:

private void CrazyLogging(){  for (var i = 0; i < 30; i++)  {    var j = i;    new Thread(new ThreadStart(() =>    {      while (!Program.isStopMonitor)        Logger.TraceEvent(TraceEventType.Error, 0, j.ToString());    })).Start();  }}

За работу сервера у нас отвечает метод Trace, поэтому добавляем наше логирование в него. Например, вот сюда:

public void Trace(){  ListenersInitialization();  CrazyLogging();  ....}

Готово. Запускаем сервер (я буду это делать с помощью Visual Studio 2019), приостанавливаем секунд через 5 процесс и смотрим что у нас там с потоками:

Отлично! Теперь запускаем клиент (TestTraceSource.exe analyze), который должен установить связь с сервером и остановить его работу.

Запустив, мы увидим, что анализ не начинается. Поэтому опять останавливаем потоки в Visual Studio и видим ту же самую картину из дамп файла сервера. А именно появился поток, который висит на методе DiagnosticsConfiguration.Initialize. Проблема воспроизведена.

Как же её чинить? Ну для начала стоит сказать, что TraceSource это класс, который предоставляет набор методов и свойств, позволяющих приложениям делать трассировку выполнения кода и связывать сообщения трассировки с их источником. Используем мы его потому, что сервер может быть запущен не приаттаченным к консоли, и консольное логирование будет бессмысленно. В этом случае мы логировали всё в Event'ы операционной системы с помощью метода TraceSource.TraceEvent.

Проблему мы "решили" следующим образом. По умолчанию теперь вся информация логируется в консоль, используя метод Console.WriteLine. В большинстве случаев, даже если эта логируемая информация и будет потеряна из-за не приаттаченной консоли, она всё равно не требуется для решения задач в работе утилиты, а проблема исчезла. Плюс изменения заняли какие-то минуты. Однако мы оставили возможность старого способа логирования с помощью специального флага enableLogger.

Код, воспроизводящий проблему

Здесь я просто привожу весь исходный код, для того чтобы вы могли наглядно у себя попробовать воспроизвести проблему.

Чтобы запустить имитирование работы сервера, запустите .exe с флагом trace. Чтобы запустить клиент, воспользуйтесь флагом analyze.

**Примечание: **количество потоков в методе CrazyLogging следует подбирать индивидуально. Если проблема у вас не воспроизводится, то попробуйте поиграться с этим значением. Также можете запустить данный проект в Visual Studio в режиме отладки.

Точка входа в программу:

using System.Linq;namespace TestTraceSource{  class Program  {    public static bool isStopMonitor = false;    static void Main(string[] args)    {      if (!args.Any())        return;      if (args[0] == "trace")      {        Server server = new Server();        server.Trace();      }      if (args[0] == "analyze")      {        Client client = new Client();        client.FinishMonitor();      }    }    }}

Сервер:

using System;using System.Diagnostics;using System.ServiceModel;using System.Threading;namespace TestTraceSource{  class Server  {    private static TraceSource Logger;    public void Trace()    {      ListenersInitialization();      CrazyLogging();      using (ServiceHost host = new ServiceHost(                          typeof(TestTraceContract),                           new Uri[]{new Uri(PipeCredentials.PipeRoot)}))      {        host.AddServiceEndpoint(typeof(IContract),                                 new NetNamedPipeBinding(),                                 PipeCredentials.PipeName);        host.Open();        while (!Program.isStopMonitor)        {          // We catch all processes, process them, and so on        }        host.Close();      }      Console.WriteLine("Complited.");    }    private void ListenersInitialization()    {      Logger = new TraceSource("PVS-Studio CLMonitoring");      Logger.Switch.Level = SourceLevels.Verbose;      Logger.Listeners.Add(new ConsoleTraceListener());      String EventSourceName = "PVS-Studio CL Monitoring";      EventLog log = new EventLog();      log.Source = EventSourceName;      Logger.Listeners.Add(new EventLogTraceListener(log));    }    private void CrazyLogging()    {      for (var i = 0; i < 30; i++)      {        var j = i;        new Thread(new ThreadStart(() =>        {          var start = DateTime.Now;          while (!Program.isStopMonitor)            Logger.TraceEvent(TraceEventType.Error, 0, j.ToString());        })).Start();      }    }   }}

Клиент:

using System;using System.ServiceModel;namespace TestTraceSource{  class Client  {    public void FinishMonitor()    {      TestTraceContractCallback сallback = new TestTraceContractCallback();      var pipeFactory = new DuplexChannelFactory<IContract>(                                сallback,                                new NetNamedPipeBinding(),                                new EndpointAddress(PipeCredentials.PipeRoot                                                   + PipeCredentials.PipeName));      IContract pipeProxy = pipeFactory.CreateChannel();      pipeProxy.StopServer();      Console.WriteLine("Complited.");        }  }}

Прокси:

using System;using System.ServiceModel;namespace TestTraceSource{  class PipeCredentials  {    public const String PipeName = "PipeCLMonitoring";    public const String PipeRoot = "net.pipe://localhost/";    public const long MaxMessageSize = 500 * 1024 * 1024; //bytes  }  class TestTraceContractCallback : IContractCallback  {    public void JobComplete()    {      Console.WriteLine("Job Completed.");    }  }  [ServiceContract(SessionMode = SessionMode.Required,                    CallbackContract = typeof(IContractCallback))]  interface IContract  {    [OperationContract]    void StopServer();  }  interface IContractCallback  {    [OperationContract(IsOneWay = true)]    void JobComplete();  }  [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]  class TestTraceContract : IContract  {    public void StopServer()    {      Program.isStopMonitor = true;    }  }}

Вывод

Будьте осторожны со стандартным методом TraceSource.TraceEvent. Если у вас теоретически возможно частое использование данного метода в программе, то вы можете столкнуться с подобной проблемой. Особенно если у вас высоконагруженная система. Сами разработчики в таком случае не рекомендуют использовать всё, что связано с классом TraceSource. Если вы уже сталкивались с чем-то подобным, то не стесняйтесь рассказать об этом в комментариях.

Спасибо за просмотр. Незаметно рекламирую свой Twitter.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Nikolay Mironov. How WCF Shoots Itself in the Foot With TraceSource.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru