WCF – Throttling e Pooling


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();
}

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>

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();
    }
}

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) { }
}

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();
}

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.

ThrottlingPooling.zip (180.90 kb)

Anúncios

Um comentário sobre “WCF – Throttling e Pooling

  1. Boas Marcel,

    Se você quer contabilizar os proxies, então provavelmente está utilizando o modelo PerSession, que é a única forma de manter "conectado", ou seja, haverá uma instância da classe do lado do serviço para atender as requisições daquele proxy.

    Neste caso, você pode utilizar os Performance Counters do WCF, que te permitirá monitorar quantas sessões ativas você tem, mas conseguir saber quem são esses clientes, até onde sei, nada há nada nativo que te informe isso.

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s