Recentemente, eu e o Ricardo Gomes da Fix Auditoria fizemos pair programming com a missão de melhorar o parse de EFD Fiscal que ele esta desenvolvendo em Golang.
O nosso objetivo era fazer o parser ganhar alguma performance e também resolver alguns pequenos problemas na carga dos arquivos.
O que o sistema faz até o momento é o seguinte: lê vários arquivos texto contendo centenas de milhares de registros, parseia esse conteúdo linha a linha e por fim grava tudo no banco de dados.
Essa tarefa estava levando 37 minutos, com 36 arquivos com mais de 50 mil linhas cada, e agora leva aproximadamente 2 minutos. Um ganho de performance de 95%. Nada mau para uma manhã de domingo falando besteiras e nerdices.
Essas foram as mudanças que fizemos
A primeira coisa foi melhorar o sistema de carga de arquivos, inicialmente tudo estava sendo carregado para a RAM e depois processado, isso fazia com que muita memória fosse usada apenas para um enorme volume de dados ficar lá esperando a sua vez de passar pelo parser. No lugar disso fizemos os arquivos serem lidos conforme fossem parseados. Usamos um reader do pacote bufio padrão do Go com uma rotina muito parecida com o exemplo a seguir.
scanner := bufio.NewScanner(file) for scanner.Scan() { ProcessRows(scanner.Text()) }
Dessa forma as linhas de cada arquivo são processadas conforme são carregadas e descartadas em seguida o que ajuda a manter o consumo de RAM sob controle. Uma coisa para sempre ter em mente é que memória e processador não são infinitos, temos sempre que ser cuidadosos com a quantidade de dados que estamos subindo para a RAM de uma só vez.
Em seguida fizemos com que os arquivos fossem processados via goroutines e não sequencialmente. Dessa forma ganhamos algum desempenho mas precisamos fazer alguns ajustes para que variáveis que eram globais não causassem mais conflitos, a solução foi colocar essas variáveis em structs e criar uma instancia para cada processo do parser.
Com isso ganhamos mais alguma performace mas o banco de dados não estava feliz, isso porque tínhamos apenas uma conexão com o banco e todas as goroutines estavam tentando fazer insert simultaneamente o que gerava um enorme gargalo. A solução foi simples, criamos uma conexão com o banco para cada instancia do parser. Isso trouxe uma grande melhora de desempenho.
Mas estávamos preocupados de não sobrecarregar o sistema pois o projeto demanda ler varias centenas de arquivos e não queríamos deixar o numero de goroutines crescer descontroladamente, então bolamos uma forma simples e bem primitiva de limitar o numero de instancias do parser funcionando simultaneamente. Basicamente o que fizemos foi usar uma variável para contar as rotinas, sempre que um novo parser é instanciado a variável de controle é incrementada, e sempre que um parser terminava o trabalho ele decrementava a variável de controle, e quando essa variável atinge um limite máximo simplesmente deixávamos o sistema esperando até o valor baixar novamente. Basicamente fizemos o thread pool mais simples possível, mas esta funcionando surpreendentemente bem.
Se você estiver curioso as principais mudanças foram feitas no arquivo SpedRead.go
O ultimo teste do domingo foi feito com 96 arquivos gerando 776.000 registros MySQL e o tempo de carga foi de 3 minutos aproximadamente.
O sistema ainda tem muito que evoluir agora o Ricardo esta trabalhando no parser de XML que também é importante para a auditoria que ele quer fazer e serão mais varias centenas de arquivos a serem passeados e carregados. Uma das coisas que queremos fazer é preparar o sistema para funcionar com o gofn e assim processar os arquivos sob demanda sem precisar de um serviço no ar o tempo todo esperando precisar ser usado.
Também ainda existe muito espaço para melhorias e otimizações, se você esta procurando um processo open source com grande volume de processamento de informações pode ser uma boa ideia ficar de olho nesse repositório.