Наш продукт разрабатывается на платформе .Net Core 2.2 с использованием WCF 4.5 для взаимодействия с SOAP сервисом клиента. В процессе работы сервиса разработчики шины данных заметили высокую нагрузку на сервер. Далее стали появляться проблемы с доступом к сервису. В результате выяснили, что причина кроется в количестве активных соединений.
Существует такая проблема как connection exhaustion. Она может возникать из-за нехватки доступных портов при установлении соединения или ограничения на количество соединений с внешним или внутренним сервисом. Есть два варианта решения:
• Увеличение доступных ресурсов,
• Уменьшение количества соединений.
Первый вариант нам недоступен, так как увеличение ресурсов может производится только на стороне сервис-провайдера. Поэтому решили искать варианты оптимизации количества соединений. В этой статье расскажем о найденном решении.
Идея
Как выяснилось, проблема заключалась в том, что на каждый запрос мы создавали новый экземпляр WCF клиента. Это делало невозможным использование уже реализованного в WCF пула соединений, потому что пул создаётся для каждого канала, а у нас для каждого запроса создаётся новый канал. Конечно, можно было переписать сервис, отвечающий за взаимодействие с WCF под использование статичного WCF клиента. Но в таком случае пул тоже бы оказался статичным, из-за чего могла возникнуть проблема со сменой DNS, обсуждаемая в этой статье. Там же говорилось о решении – HttpClientFactory. Суть решения заключается в том, что фабрика умеет работать с собственным пулом, соединения в котором периодически обновляются. Период обновления по умолчанию составляет две минуты, но может быть изменен.
В нашем продукте мы уже использовали HttpClientFactory для взаимодействия с другими сервисами, и использование фабрики в WCF выглядело хорошей альтернативой статичному WCF клиенту. При этом нам не пришлось бы вносить изменения в реализацию WCF сервиса. Зато смогли бы использовать пул, с которым умеет работать фабрика. Плюс ко всему это позволяло нам решить проблему с NTLM авторизацией в Linux, описанной в этой статье, так как при конфигурации http клиента можно задать схему аутентификации для обработчика сообщений.
Реализация
Для работы с HttpClientFactory достаточно добавить в ConfigureServices описание конфигурации клиента. Там есть возможность добавить нескольких именованных или типизированных клиентов с собственной конфигурацией. В таком случае для каждого клиента будет использоваться собственный пул соединений. В примере мы используем именованный клиент.
services.AddHttpClient("ClientName");
В WCF можно добавить собственные обработчики сообщений для http клиента. Для этого в параметры биндинга добавляем делегат, инициализированный методом. Там в качестве входного параметра мы получаем обработчик, созданный на стороне WCF, и возвращаем собственный обработчик. В результате в конструктор http клиента на стороне WCF будет передан обработчик, полученный из метода делегата.
Таким образом, вернув обработчик из пула фабрики, мы заменим им входящий обработчик. Для получения обработчика из пула фабрики мы используем HttpMessageHandlerFactory. А чтобы получить доступ к параметрам биндинга, необходимо будет реализовать класс, унаследованный от IEndpointBehavior. И затем добавить его к нашему WCF клиенту.
Схематически алгоритм действий по созданию нового клиента на стороне WCF выглядит так.
Реализуем CustomEndpointBehaviour.
public class CustomEndpointBehavior : IEndpointBehavior
{
private readonly Func<HttpMessageHandler> _httpHandler;
public CustomEndpointBehavior(IHttpMessageHandlerFactory factory)
{
//Определяем метод создания обработчика для именованного клиента
_httpHandler = () => factory.CreateHandler("ClientName");
}
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
//Добавляем наш метод в параметры биндинга
bindingParameters.Add(new Func<HttpClientHandler, HttpMessageHandler>(handler => _httpHandler()));
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { }
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { }
public void Validate(ServiceEndpoint endpoint) { }
}
Далее добавляем наш EndpointBehavior к WCF клиенту.
var httpMessageHandler = serviceProvider.GetRequiredService<IHttpMessageHandlerFactory>();
var client = new WcfClient();
client.Endpoint.EndpointBehaviors.Add(new CustomEndpointBehavior(httpMessageHandler));
Теперь при создании соединений через WCF будут по возможности использоваться экземпляры обработчиков из пула. Это сократит количество активных соединений.
Тест
Для проверки мы отправили 100 одинаковых запросов. В результате без пула пик соединений достигал 53, а с пулом не превысил 7.
Мониторинг соединений без пула:
Мониторинг соединений с пулом:
Заключение
Мы в True Engineering реализовали пул соединений в WCF, который не зависит от реализации работы с WCF клиентом. Также он эффективно экономит ресурсы как на стороне сервера, где запущено приложение, так и на стороне сервис-провайдера.
Мы потратили много времени на поиск вариантов оптимизации, но само решение получилось лаконичное и простое. Берите, пока горячее)
Автор: true_engineering