WCF netTcpBinding - Cosa fare se: “..the socket did not complete within the allotted timeout of…”

Mi è capitato talvolta di incontrare uno strano errore quando un client cerca di connettersi ad un WCF Service che espone un endpoint TCP (netTcpBinding). Un aspetto ricorrente è che questo errore può verificarsi sopratutto quando uno o più client WCF tentano di instaurare un numero considerevole di connessioni contemporaneamente.

Sto parlando della seguente eccezione, che potete osservare nei trace del WCF Service (per capire come abilitare i trace di WCF, consiglio di dare un’occhiata ad un mio precedente post: WCF Tracing appunto):

<ExceptionType>System.TimeoutException, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</ExceptionType>

<Message>The socket was aborted because an asynchronous receive from the socket did not complete within the allotted timeout of 00:00:05. The time allotted to this operation may have been a portion of a longer timeout.</Message>

L’effetto visibile di questa eccezione (che si verifica sul server) lato client è che alcune connessioni verso il servizio non vanno a buon fine, dal momento che il server non risponde entro il tempo massimo prestabilito.

Quando si va a fare il tuning di un servizio WCF ci sono molti aspetti da tenere in considerazione: non è mia intenzione parlarne ora in modo esaustivo (tra i miei progetti c’è comunque un post in cui elencherò tutti i parametri di configurazione che possono impattare sulle performance di un servizio WCF, perciò restate sintonizzati smile_regular), in quanto mi vorrei soffermare su un parametro in particolare, spesso trascurato, ma che può darci qualche mal di testa quando andiamo a fare i test di carico.

In sostanza, quando avete un burst di connessioni entranti nel vostro netTcpBinding WCF service e alcune falliscono in fase di instaurazione, abilitate i trace sul service e vi trovate di fronte all’eccezione di cui sopra, ci sono buone probabilità che il parametro da andare a toccare sia il ChannelInitializationTimeout.

Il ChannelInitializationTimeout (valore di default: 5 sec.) imposta infatti il tempo massimo in cui un canale può trovarsi in stato di inizializzazione, prima di essere disconnesso.

Come si fa ad impostarlo? Supponiamo che il vostro file di configurazione sia più o meno come questo:

 <services>
  <service name="Microsoft.ServiceModel.Samples.CalculatorService"
           behaviorConfiguration="CalculatorServiceBehavior">
    ...
    <endpoint address=""
              binding="netTcpBinding"
              bindingConfiguration="Binding1" 
              contract="Microsoft.ServiceModel.Samples.ICalculator" />
    ...
  </service>
</services>

<bindings>
  <netTcpBinding>
    <binding name="Binding1" 
             closeTimeout="00:01:00"
             openTimeout="00:01:00" 
             receiveTimeout="00:10:00" 
             sendTimeout="00:01:00"
             transactionFlow="false" 
             transferMode="Buffered" 
             transactionProtocol="OleTransactions"
             hostNameComparisonMode="StrongWildcard" 
             listenBacklog="10"
             maxBufferPoolSize="524288" 
             maxBufferSize="65536" 
             maxConnections="10"
             maxReceivedMessageSize="65536">
      <security mode="None">
        <transport clientCredentialType="None" />
      </security>
    </binding>
  </netTcpBinding>
</bindings>

Purtroppo la proprietà ChannelInitializationTimeout non è direttamente esposta nel netTcpBinding, per tale ragione dobbiamo definire un custom binding equivalente, ma che in più contiene l’impostazione di tale proprietà ad un valore maggiore di 5 sec. (per esempio: 1 minuto).

 <services>
  <service name="Microsoft.ServiceModel.Samples.CalculatorService"
           behaviorConfiguration="CalculatorServiceBehavior">
    ...
    <endpoint address=""
              binding="customBinding"
              bindingConfiguration="Binding1" 
              contract="Microsoft.ServiceModel.Samples.ICalculator" />
    ...
  </service>
</services>

<bindings>
        <customBinding>
           <binding name="Binding1"
        closeTimeout="00:01:00"
        openTimeout="00:01:00"
        receiveTimeout="00:10:00"
        sendTimeout="00:01:00">
             <binaryMessageEncoding />
             <tcpTransport maxBufferPoolSize="524288" maxReceivedMessageSize="65536" hostNameComparisonMode="StrongWildcard"
                           maxBufferSize="65536" maxPendingConnections="10" channelInitializationTimeout="00:01:00"
                           transferMode="Buffered" listenBacklog="10" portSharingEnabled="false" teredoEnabled="false"
                   >
             </tcpTransport>
           </binding>           
         </customBinding>
</bindings>

Ci tengo a far presente che proprietà come il ChannelInitializationTimeout hanno dei valori di default piuttosto “stretti” perché il loro fine è di mettere in sicurezza il servizio prevenendo possibili attacchi DoS. Un esempio in cui il ChannelInitializationTimeout previene un attacco DoS può essere il seguente: supponiamo che sia prevista l’autenticazione tra client e service, il primo deve inviare un certo numero di byte al servizio affinché l’autenticazione possa essere effettuata, e tale numero di byte è molto inferiore alla dimensione del messaggio completo. A questo punto, invece di attendere lo scadere del ReceiveTimeout, il servizio chiude la connessione se i byte finalizzati all’autenticazione non sono stati ricevuti entro il ChannelInitializationTimeout, impedendo che il client possa mantenere aperte le connessioni in fase di inizializzazione più del dovuto.

In particolare condizioni di carico, se ci accorgiamo che il ChannelInitializationTimeout deve essere aumentato, facciamolo pure, ma scegliamo il nuovo valore con attenzione!

Andrea Liberatore

Senior Support Engineer

Developer Support Core