Desenvolvimento - C#

WCF - Throttling e Pooling

Este artigo explicará como proceder para efetuar a configuração do Throttling e suas implicações; também falaremos supercialmente sobre a estrutura do Pooling e como implementá-lo.

por Israel Aéce



function doClick(index, numTabs, id) { document.all("tab" + id, index).className = "tab"; for (var i=1; i Faça o download do exemplo.

Através do gerenciamento de instância de um serviço podemos definir qual a forma de criação de uma instância para servir uma determinada requisição. Essa configuração que fazemos à nivel de serviço, através de um behavior, não impõe nenhuma restrição na quantidade de instância e/ou execuções concorrentes que são realizadas e, dependendo do volume de requisições que o serviço tenha ou até mesmo a quantidade de recursos que ele utiliza, podemos degradar consideravelmente a performance.

O Throttling possibilita restringirmos a quantidade de sessões, instâncias e chamadas concorrentes que são realizadas para um serviço. Além do Throttling, ainda há outra funcionalidade que pode ser utilizada em um serviço, que é o Pooling de objetos, muito comum dentro do Enterprise Services (COM+). Este artigo explicará como proceder para efetuar a configuração do Throttling e suas implicações; também falaremos supercialmente sobre a estrutura do Pooling e como implementá-lo.

Throttling

Seria muito interessante conseguir atender a todas as requisições que chegam para um serviço mas infelizmente, devido à limitação de alguns recursos, isso nem sempre será possível. Cada serviço (assim como qualquer outra aplicação) está limitado à disponibilidade do processador, memória, conexão com base de dados, etc. Um grande número de chamadas faz com que um grande número de instâncias sejam criadas ou um grande número de acesso concorrente (dependerá do modo de gerenciamento de instância escolhido). Cada instância ou cada thread exige recursos do sistema para poder realizar a operação.

Para sanar problemas como estes temos duas alternativas: a primeira deleas é ter um hardware mais potente e que consiga atender as todas as requisições mais, se a quantidade de requisições aumentar, precisará de mais hardware; já a segunda alternativa é restringir o acesso à tais recursos, limitando o número de chamadas concorrentes e/ou sessões ativas. Caso o limite for atingido, as requisições serão enfileiradas, aguardando a sua vez ou, se essa espera pelo processamento demorar muito tempo, falhará por timeout.

O WCF te possibilita controlar esses limites através do Throttling. Basicamente a idéia do Throttling é limitar o número de sessões, instâncias e execuções concorrentes de um serviço. Essa configuração é realizada a partir de um behavior de serviço, ou seja, essa configuração refletirá na(s) instância(s) do serviço, independente de endpoints. Temos três propriedades que podemos definir para especificar esse comportamento:

  • MaxConcurrentInstances: Esta propriedade especifica o número máximo de instâncias concorrentes permitidas para o serviço. Quando o modo de gerenciamento de instância está definido como Single, esta informação é irrelevante, pois existirá apenas uma única instância servindo todas as requisições; já no modo PerCall o número de instâncias será o mesmo número de chamadas concorrentes.

  • MaxConcurrentCalls: Esta propriedade especifica o número máximo de mensagens concorrentes em que o serviço poderá processar. O valor padrão é definido como 16.

  • MaxConcurrentSessions: Esta propriedade especifica o número máximo de sessões concorrentes que o serviço permitirá. É importante lembrar que os clientes são responsáveis por inicializar ou terminar as sessões, realizando várias chamadas entre esse período. O problema das sessões é que quando elas são longas, ou melhor, duram muito tempo, outros clientes podem ser bloqueados. Os bindings que estão sob HTTP e estão com a sessão desabilitada não interferem no valor, pois não mantém uma conexão ativa com o serviço e, conseqüentemente, não haverá instância servindo-o. O valor padrão é definido como 10.

A configuração do Throttling afeta diretamente o serviço, independente de que endpoint a requisição está vindo. Por isso, a configuração poderá ser realizada via arquivo de configuração ou de forma imperativa. No modelo declarativo utilizamos o elemento serviceThrottling, que faz parte de um behavior que está diretamente ligado à um serviço; já no modelo imperativo, utilizamos a classe ServiceThrottlingBehavior (namespace System.ServiceModel.Description) que, depois de configurada, a adicionamos à coleção de behaviors, exposta pela classe ServiceHost. O trecho de código abaixo exemplifica as duas formas de configurar o Throttling:

using System;
using System.ServiceModel;
using System.ServiceModel.Description;

using(ServiceHost host = new ServiceHost(typeof(ICliente), 
    new Uri[] { new Uri("net.tcp://localhost:8377") }))
{
    //Configuração dos Endpoints

    ServiceThrottlingBehavior t = new ServiceThrottlingBehavior();
    t.MaxConcurrentCalls = 40;
    t.MaxConcurrentInstances = 20;
    t.MaxConcurrentSessions = 20;
    host.Description.Behaviors.Add(t);

    host.Open();

    Console.ReadLine();
}
Imports System
Imports System.ServiceModel
Imports System.ServiceModel.Description

Using host As New ServiceHost(GetType(ICliente), _
    New Uri() {New Uri("net.tcp://localhost:8377")})

    "Configuração dos Endpoints

    Dim t As New ServiceThrottlingBehavior()
    t.MaxConcurrentCalls = 40
    t.MaxConcurrentInstances = 20
    t.MaxConcurrentSessions = 20
    host.Description.Behaviors.Add(t)

    host.Open()

    Console.ReadLine()
End Using
C# VB.NET

A configuração do Throttling deve ser realizada antes da abertura do serviço. A configuração equivalente ao que vimos acima, mas utilizando o arquivo de configuração é mostrado abaixo:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="Host.ServicoDeClientes" behaviorConfiguration="Config">
        <!-- Endpoints -->
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="Config">
          <serviceThrottling
            maxConcurrentCalls="40"
            maxConcurrentInstances="20"
            maxConcurrentSessions="20" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>
*.Config

Pooling

Já sabemos que o runtime do WCF cria instâncias da classe que representa o serviço para atender uma determinada requisição. A criação das instâncias dependerá do modo de gerenciamento escolhido, que foi constantemente abordado neste artigo. Para relembrar, o modo PerSession cria uma instância para cada cliente; já o modo PerCall cria uma instância por chamada e, finalmente, o modo Single cria uma única instância.

Criar e destruir instâncias a todo momento pode ser extremamente custoso, pois às vezes existem tarefas árduas que são realizadas no momento da sua inicialização. Para ter um ganho considerável em performance, podemos recorrer à uma técnica chamada de Pooling. Esta técnica consiste em, ao invés de destruir o objeto por completo e removê-lo da memória colocá-lo em um repositório para reutilizá-lo. Essa técnica também terá influencia do modo de gerenciamento de instâncias.

Quando uma requisição chega para um serviço que está exposto via modo PerCall, o WCF verifica se há objetos "pré-criados" no pool para atender a requisição. Caso exista, ele utilizará essa instância para servir a requisição. Quando a requisição encerrar, o objeto será devolvido para o pool, podendo ser reaproveitado para uma futura requisição. Quando utilizarmos o modo PerSession, a semântica é a mesma, mas o objeto somente será retornado ao pool quando o cliente encerrar a sessão. Já o modo Single não se aplica ao pooling. Por questões de segurança e também de consistência, ao retornar um objeto para o pool, os dados que são utilizados pelo mesmo (campos internos) devem ser reinicializados.

Por padrão, o WCF não suporta nativamente a técnica de Pooling. Com toda a estensibilidade (behavior) que o WCF possui, fica fácil acoplar uma extensão ao mesmo para suportar o Pooling. Assim como o cliente possui o proxy, o serviço possui o dispatcher*. Para cada endpoint temos um endpoint dispatcher relacionado, sendo ele o responsável por converter as mensagens que chegam para o serviço (mais especificamente para o endpoint) em chamadas para as operações que o serviço fornece e, depois disso, retornar uma mensagem contendo a resposta.

* O cliente também pode criar um dispatcher, mas isso acontece quando estamos utilizando um contrato Duplex e está fora do escopo deste artigo.

O endpoint expõe uma propriedade chamada DispatchRuntime, do tipo DispatchRuntime, que representa o dispatcher. Esta classe, por sua vez, fornece uma propriedade chamada InstanceProvider, que é onde podemos acoplar a instância do objeto que fará a extração e a devolução das classes (instâncias) do serviço ao pool. Essa propriedade recebe um objeto que implementa a Interface IInstanceProvider (namespace System.ServiceModel.Dispatcher) e a utilizaremos quando formos definir nosso próprio mecanismo de criação de instâncias.

A Interface IInstanceProvider disponibiliza três membros, os quais devem obrigatoriamente ser implementados na classe que gerenciará o pool. Abaixo temos uma tabela com a explicação para esses três membros, mas é importante dizer que a freqüência para a chamada dos métodos abaixo está condicionada ao modo de gerenciamento de instâncias utilizado pelo serviço (propriedade InstanceContextMode do atributo ServiceBehaviorAttribute):

Método Descrição
GetInstance

Quando a mensagem chega para o dispatcher ele invocará este método para criar uma nova instância da classe e, em seguida, processá-la.

GetInstance

A mesma finalidade do método GetInstance, exceto que este é invocado quando não existe uma classe Message relacionada à requisição atual.

ReleaseInstance

Quando o tempo de vida de uma instância chega ao fim, o dispatcher chama este método, passando a instância do objeto corrente para decidirmos o que iremos fazer com a mesma. Como estamos criando um pool, devemos armazená-la para uso posterior.

A implementação do pool consiste em duas etapas: a primeira delas é a criação da classe que gerenciará o pool e uma classe para acoplarmos ao runtime do WCF. Como já sabemos, a classe que irá gerenciar o pool deve implementar a Interface IInstanceProvider; para facilitar o trabalho, iremos criá-la de forma genérica. Internamente ela manterá um membro estático chamado _pool do tipo Stack (coleção do tipo LIFO (Last-In First-Out)), que armazenará as instâncias para serem reutilizadas. Como já é de se esperar, o método GetInstance irá verificar se existe ou não algum item disponível dentro desta coleção e, caso exista, ele será utilizado; caso contrário, uma nova instância será criada (você não precisa adicioná-la à coleção antes de utilizá-la, pois quem fará isso será o método ReleaseInstance).

A implementação do pool para efeito de exemplo não é muito complexa. Utilizamos os métodos Pop e Push da Stack para remover ou adicionar um item, utilizando cada um deles no momento propício (GetInstance e ReleaseInstance). Além disso, a classe está tipificada com TService, obrigando que a mesma seja uma classe e também possua um construtor padrão. Além dos métodos fornecidos pela Interface IInstanceProvider, foi criado um método chamado CreateNewInstance, que retornará uma nova instância da classe que representa o serviço, caso seja necessário. O código abaixo exibe a classe na íntegra:

using System;
using System.Diagnostics;
using System.Collections;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;

public class PoolInstanceProvider<TService> : 
    IInstanceProvider where TService : class, ICleanup, new()
{
    private static object _lock = new object();
    private static Stack _pool;

    static PoolInstanceProvider()
    {
        _pool = new Stack();
    }

    public object GetInstance(InstanceContext instanceContext, Message message)
    {
        object obj = null;

        lock (_lock)
        {
            if (_pool.Count > 0)
            {
                obj = _pool.Pop();
                Debug.WriteLine("From Pool");
            }
            else
            {
                obj = CreateNewInstance();
                Debug.WriteLine("New Object");
            }
        }

        return obj;
    }

    public object GetInstance(InstanceContext instanceContext)
    {
        throw new NotImplementedException();
    }

    public void ReleaseInstance(InstanceContext instanceContext, object instance)
    {
        lock (_lock)
        {
            ((ICleanup)instance).Cleanup();

            _pool.Push(instance);
        }
    }

    private static object CreateNewInstance()
    {
        return new TService();
    }
}
Imports System
Imports System.Diagnostics
Imports System.Collections
Imports System.ServiceModel
Imports System.ServiceModel.Channels
Imports System.ServiceModel.Dispatcher

Friend Class PoolInstanceProvider(Of TService As {Class, ICleanup, New})
    Implements IInstanceProvider

    Private Shared _lock As Object = New Object()
    Private Shared _pool As Stack

    Shared Sub New()
        _pool = New Stack()
    End Sub

    Public Function GetInstance(ByVal instanceContext As InstanceContext, _
        ByVal message As Message) As Object Implements IInstanceProvider.GetInstance

        Dim obj As Object = Nothing

        SyncLock _lock
            If _pool.Count > 0 Then
                obj = _pool.Pop()
                Debug.WriteLine("From Pool")
            Else
                obj = CreateNewInstance()
                Debug.WriteLine("New Object")
            End If
        End SyncLock

        Return obj
    End Function

    Public Function GetInstance(ByVal instanceContext As InstanceContext) As Object _
        Implements IInstanceProvider.GetInstance

        Throw New NotImplementedException()
    End Function

    Public Sub ReleaseInstance(ByVal instanceContext As InstanceContext, _
        ByVal instance As Object) Implements IInstanceProvider.ReleaseInstance

        SyncLock (_lock)
            DirectCast(instance, ICleanup).Cleanup()

            _pool.Push(instance)
        End SyncLock
    End Sub

    Private Shared Function CreateNewInstance() As TService
        Return New TService()
    End Function
End Class
C# VB.NET

Como podemos notar, a classe PoolInstanceProvider faz o uso de um tipo genérico chamado TService, onde TService deve ser tipificado com uma classe que representará o serviço. Há algumas condições que o tipo que é especificado no parâmetro TService deverá satisfazer: class (deve ser uma classe), ICleanup (deve implementar esta Interface) e new (deve ter um construtor sem parâmetros). A Interface ICleanup fornece um método chamado Cleanup, que tem a finalidade de restaurar os dados (membros internos) utilizados por uma instância antes de retorná-la para o pool.

É importante lembrar que a classe acima é apenas um exemplo que, para um mundo real, talvez precisaria ser melhorada. Com ela criada, nos resta definir o ponto de entrada para acoplarmos esta classe dentro do WCF. Assim como o Throttling, o Pooling também deverá refletir para o serviço como um todo, independente de qual endpoint a requisição venha e, devido à isso, criaremos um behavior de serviço, o que nos leva a implementar a Interface IServiceBehavior em uma classe, mais especificamente o método ApplyDispatchBehavior. Esse método fornece a possibilidade de inserir objetos de estensibilidade na execução do WCF, permitindo modificar ou inspecionar o host que está sendo construído para a execução do objeto que representará o serviço.

A instância do host é fornecida como parâmetro para este método e, através dela, temos a coleção de dispatchers relacionados ao mesmo (lembrando que existe um dispatcher para cada endpoint). Inicialmente iremos percorrer a coleção de dispatchers fornecida pelo host e, a partir daí, a coleção de endpoints. Cada endpoint fornece uma propriedade chamada DispatchRuntime que, como vimos acima, é onde o WCF transforma as mensagens em objetos. A partir dessa propriedade podemos vincular a instância do pool que criamos anteriormente, através da propriedade InstanceProvider. É importante dizer que, como o mesmo pool deverá atender à todas as requisições, você deverá definir a mesma instância para todos os endpoints expostos pelo host. O código abaixo exibe como efetuar essa configuração:

using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Description;
using System.Collections.ObjectModel;

internal class PoolServiceBehavior<TService> : 
    IServiceBehavior where TService : class, ICleanup, new()
{
    public void AddBindingParameters(ServiceDescription serviceDescription, 
        ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, 
        BindingParameterCollection bindingParameters) { }

    public void ApplyDispatchBehavior(ServiceDescription serviceDescription, 
        ServiceHostBase serviceHostBase)
    {
        PoolInstanceProvider<TService> pool = new PoolInstanceProvider<TService>();

        foreach (ChannelDispatcherBase cdb in serviceHostBase.ChannelDispatchers)
        {
            foreach (EndpointDispatcher ed in ((ChannelDispatcher)cdb).Endpoints)
            {
                ed.DispatchRuntime.InstanceProvider = pool;
            }
        }
    }

    public void Validate(ServiceDescription serviceDescription, 
        ServiceHostBase serviceHostBase) { }
}
Imports System
Imports System.ServiceModel
Imports System.ServiceModel.Channels
Imports System.ServiceModel.Dispatcher
Imports System.ServiceModel.Description
Imports System.Collections.ObjectModel

Friend Class PoolServiceBehavior(Of TService As {Class, ICleanup, New})
    Implements IServiceBehavior

    Public Sub AddBindingParameters(ByVal serviceDescription As ServiceDescription, _
                                    ByVal serviceHostBase As ServiceHostBase, _
                                    ByVal endpoints As Collection(Of ServiceEndpoint), _
                                    ByVal bindingParameters As BindingParameterCollection) _
        Implements IServiceBehavior.AddBindingParameters
    End Sub

    Public Sub ApplyDispatchBehavior(ByVal serviceDescription As ServiceDescription, _
                                     ByVal serviceHostBase As ServiceHostBase) _
        Implements IServiceBehavior.ApplyDispatchBehavior

        Dim pool As New PoolInstanceProvider(Of TService)()

        For Each cdb As ChannelDispatcherBase In serviceHostBase.ChannelDispatchers
            For Each ed In DirectCast(cdb, ChannelDispatcher).Endpoints
                ed.DispatchRuntime.InstanceProvider = pool
            Next
        Next
    End Sub

    Public Sub Validate(ByVal serviceDescription As ServiceDescription, _
                        ByVal serviceHostBase As ServiceHostBase) _
        Implements IServiceBehavior.Validate
    End Sub
End Class
C# VB.NET

Finalmente, com toda essa infraestrutura pronta, devemos adicionar a instância da classe PoolServiceBehavior à coleção de behaviors do host do serviço. Por se tratar de uma classe genérica, ao criar a sua instância você precisará especificar o mesmo tipo da classe que está sendo exposta pelo serviço. O código abaixo ilustra como devemos proceder para instalar a mesma no host:

using System;
using System.ServiceModel;
using System.ServiceModel.Description;

using (ServiceHost host = new ServiceHost(typeof(ServicoDeClientes), 
    new Uri[] { new Uri("net.tcp://localhost:8377") }))
{
    host.Description.Behaviors.Add(new PoolServiceBehavior<ServicoDeClientes>());

    //endpoints

    host.Open();
    Console.ReadLine();
}
Imports System
Imports System.ServiceModel
Imports System.ServiceModel.Description

Using host As New ServiceHost(GetType(ServicoDeClientes), _
    New Uri() {New Uri("net.tcp://localhost:8377")})

    host.Description.Behaviors.Add(New PoolServiceBehavior(Of ServicoDeClientes)())

    "endpoints

    host.Open()
    Console.ReadLine()
End Using
C# VB.NET

Observação: Como já sabemos, há outras formas de adicionar um behavior, como por exemplo via arquivo de configuração, ou até mesmo via atributo, mas isso está além do foco deste artigo.

Conclusão: Este artigo explicou como devemos proceder e quais as formas que temos para dizer ao runtime do WCF quando e como criar a instância do objeto que representará o serviço, bem como o impacto que estas técnicas causarão. As configurações que vimos neste artigo (Throttling e Pooling) melhoram consideravelmente a performance de um serviço mas, se utilizado de forma indevida, pode prejudicar ao invés de melhorar.
Israel Aéce

Israel Aéce - Especialista em tecnologias de desenvolvimento Microsoft, atua como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. Como instrutor Microsoft, leciona sobre o desenvolvimento de aplicações .NET. É palestrante em diversos eventos Microsoft no Brasil e autor de diversos artigos que podem ser lidos a partir de seu site http://www.israelaece.com/. Possui as seguintes credenciais: MVP (Connected System Developer), MCP, MCAD, MCTS (Web, Windows, Distributed, ASP.NET 3.5, ADO.NET 3.5, Windows Forms 3.5 e WCF), MCPD (Web, Windows, Enterprise, ASP.NET 3.5 e Windows 3.5) e MCT.