Depois eu leio

Parallel, F# e no fim, de volta ao básico

Recentemente, reescrevemos aqui na empresa onde trabalho um serviço de instalações. O fluxo era mais ou menos o seguinte (ligeiramente simplificado):

1 – Identificar se o pedido foi aprovado
2 – Obter a lista de serviços inclusos no pedido
3 – Chamar o WS de instalação correspondente para cada serviço
4 – Logar todo o processo para gerar estatísticas e identificar erros de instalação

As instalações devem ser processadas ininterruptamente e o volume diário é imenso. Após um spike sobre o projeto, resolvemos utilizar o Parallels na camada de infra-estrutura para simplificar o processamento simultâneo por pacotes de serviços e escalar melhor aproveitando os múltiplos cores do servidor.

O desenvolvimento foi tranquilo, o projeto foi pro ar e observamos uma melhora significativa no ciclo de instalações. Agora a parte boa, cadê o problema? Monitorando o negócio depois de uns dias no ar, percebemos que as chamadas para os Web Services externos viraram o freio-de-mão puxado da aplicação.

“Mas como, isso é tão básico!” – pois é, tão sem graça que resolvemos variar e implementar em F# para fazer as chamadas assíncronas, saiu algo mais ou menos assim:

   1: /// Executa um POST para a url especificada.
   2: let AsyncPost (url:string, httpMethod:PostType, contentType:string, postData:string) =
   3:     Async.Run(AsyncHttpPost(url, httpMethod, contentType, postData))
   4:
   5: /// Immplementação para o método AsyncPost.
   6: let private AsyncHttpPost(url:string, httpMethod:PostType, contentType:string, postData:string) =
   7:         async {
   8:             let asyncRequest = WebRequest.Create(url)
   9:             asyncRequest.Method <- httpMethod.ToString()
  10:
  11:             do
  12:                 match httpMethod with
  13:                 |PostType.POST | PostType.PUT   ->
  14:                     asyncRequest.ContentType <- contentType
  15:                     use writeStream = asyncRequest.GetRequestStream()
  16:                     let encoding = new UTF8Encoding()
  17:                     let bytes = encoding.GetBytes(postData)
  18:                     writeStream.Write(bytes, 0, bytes.Length)
  19:                  |_ -> null
  20:
  21:             use! response = asyncRequest.AsyncGetResponse()
  22:
  23:             use stream = response.GetResponseStream()
  24:             use reader = new StreamReader(stream)
  25:
  26:             return! reader.AsyncReadToEnd()
  27:         }
  28:
  29: /// Verbos aceitos para atualizações HTTP
  30: type PostType =
  31: |    POST = 1
  32: |    PUT = 2
  33: |    DELETE = 3

Teoricamente não havia motivos para as chamadas http se tornarem o gargalo do fluxo, então decidimos rever do básico como as aplicações .Net processam chamadas http.

Depois de perder um tempo no MSDN, o Google me fez parar neste e neste post. Ambos têm a resposta que eu procurava, mas no primeiro ainda têm uma mega aula de debug e o segundo dá várias outras dicas importantes. Enfim, a solução principal estava no atributo do app.config:

<add address="*" maxconnection="20" />

Onde 20 é o número máximo de conexões simultâneas permitidas por url.

A configuração padrão no Framework permite apenas duas conexões simultâneas por endereço, e aí não têm sistema mega-blaster-multi-thread que resolva – quando chegar nas chamadas http vai enfileirar tudo neste limite.

Como o autor do segundo post explica, esse limite é uma cautela para proteger endereços de ataques DoS não intencionais e para seguir a RFC 2616, que diz que “um cliente não deve manter mais de duas conexões abertas para o mesmo servidor ou proxy”.

Ok… mas precisava uma engenheira de escalabilidade com poderes de debug sobrenaturais – da própria Microsoft, e mais um monte de gente passar pela mesma saga de “caça ao gargalo” pra chegar nessa informação? O MSDN melhorou muito nos últimos anos, mas ainda está longe de ser o ideal.

No comments

No comments yet. Be the first.

Leave a reply