
TL; DRO artigo descreve o uso do projeto pet como uma maneira de manter e melhorar as habilidades. O autor criou uma biblioteca PHP para instalar o FIAS a partir de arquivos XML.
Finalidade
Raramente troco de emprego, portanto, dado o desejo natural de cada organização por processos fixos, qualquer tarefa se transforma em rotina. Por um lado, é benéfico para uma empresa manter esse estado, por outro lado, para mim isso significa perda completa ou obsolescência de habilidades. O PHP está se desenvolvendo rapidamente e, consequentemente, o atraso em potencial também está crescendo rapidamente. Por fim, todos sabemos que hoje é difícil para um programador encontrar um bom emprego sem o conhecimento de Elasticsearch, RabbitMQ, Kafka e outras tecnologias que nem sempre aparecem no meu trabalho diário.
Depois de iniciar o próximo site típico, decidi que era hora de mudar alguma coisa. Não queria mudar meu trabalho, mas lembrei-me de como em uma das conferências o palestrante recomendou o uso de seu próprio projeto opcional, o chamado projeto de estimação, para treinamento. O método parecia apropriado e eu decidi experimentá-lo.
Seleção de tarefas
A escolha da tarefa acabou sendo a parte mais difícil do empreendimento. Nada de especial veio à mente: alguns serviços, como um analisador de tarefas que podem ser facilmente implementados em uma pilha familiar. Abandonei a idéia do projeto por vários meses, até que vi acidentalmente as notícias sobre o hackathon do Ministério das Finanças. Sugeriu o uso de uma das listas de fontes de dados abertas para criar um serviço. Entre outros, também foi indicado o FIAS (Federal Information Address System). Infelizmente, o hackathon já havia terminado até então.
Eu aprendi sobre o FIAS pela primeira vez, mas a tarefa parecia interessante. Julgue por si mesmo: cerca de 30 GB de arquivos XML, cerca de 60 milhões de linhas no banco de dados e, além disso, a biblioteca poderá mais tarde ser útil no trabalho. Havia algumas soluções prontas no Github, mas isso não me impediu. Pelo contrário, com base em sua análise, fiz requisitos adicionais que destacariam minha implementação.
No futuro, observo que encontrei muito menos dificuldades do que esperava.
90% do sucesso é a afirmação correta do problema. Após vários anos de trabalho com modelos, era bastante difícil conseguir formular claramente o problema. Eu só queria começar a trabalhar, mas já no processo tudo teria esclarecido por si só. Após uma hora de luta com a procrastinação, finalmente escrevi: crie uma biblioteca em PHP para importar dados do FIAS.
Mais tarde, depois de um gosto, adicionei alguns requisitos adicionais:
- implementação em PHP sem o uso de utilitários de terceiros, código exclusivo em PHP e extensões do PECL,
- importação de todos os dados do conjunto FIAS,
- ciclo completo de instalação e atualização: pesquisando a versão necessária, recebendo o arquivo morto, desembalando, gravando no banco de dados,
- flexibilidade máxima: a capacidade de alterar o local de armazenamento, modificar os dados antes da gravação, filtrar o necessário etc.,
- A biblioteca deve ser facilmente integrada aos projetos existentes.
FIAS
O FIAS possui um site oficial que nos dá a definição e o objetivo de criar um sistema
O Sistema Federal de Informações de Endereço (FIAS) é o sistema federal de informações estaduais que fornece a formação, manutenção e uso do registro de endereços estaduais.
O objetivo da criação do FIAS é a formação de um único recurso federal que contém informações de endereço confiáveis, uniformes, disponíveis ao público e estruturadas. Graças à implementação do FIAS, essas informações podem ser obtidas gratuitamente através da Internet no portal FIAS registrado oficialmente.
Os materiais com uma descrição são suficientes, tanto no site do FIAS quanto no Habré , portanto, não vou me concentrar nisso.
Em suma. O FIAS vem em dois formatos: FIAS e KLADR. O segundo está obsoleto e não está mais em uso. As informações são armazenadas no DBF ou no XML. Cada alteração na composição do FIAS é marcada com uma nova versão. Você pode solicitar um pacote com dados completos atualizados no momento ou contendo apenas alterações entre as duas versões. Links fornece um serviço SOAP. O pacote é um arquivo RAR contendo arquivos com nomes especialmente formados. Eles consistem em um prefixo, um nome de conjunto de dados e uma data de geração. Existem dois tipos de prefixos: AS_ para arquivos dos quais os dados devem ser adicionados ao banco de dados e AS DEL para arquivos cujos dados devem ser excluídos do banco de dados.
O FIAS contém os seguintes dados:
- registro de elementos formadores de endereço (este é o gráfico de endereços: regiões, cidades e ruas),
- elementos de endereço que identificam objetos endereçáveis (número e dados da casa),
- informações sobre terrenos
- informações sobre as instalações (apartamentos, escritórios, salas, etc.),
- informações no documento normativo, que é a base para a atribuição do nome ao elemento de endereço.
Bem como vários dicionários- uma lista de possíveis valores dos intervalos das casas (regulares, pares, ímpares),
- uma lista de status relevantes para uma entrada de um elemento de endereço pelo classificador KLADR4.0,
- uma lista de status relevantes para uma entrada de um elemento de endereço de acordo com o FIAS,
- uma lista de nomes completos e abreviados dos tipos de elementos de endereço e seus níveis de classificação,
- lista de tipos de edifícios,
- lista de possíveis tipos de propriedade
- lista de códigos de operações com objetos endereçáveis,
- uma lista de possíveis condições imobiliárias,
- lista de tipos de instalações ou escritórios,
- lista de tipos de quartos,
- lista de possíveis status (centros) de objetos de endereço de unidades administrativas,
- tipos de documentos regulamentares.
A estrutura de dados é descrita no documento, que pode ser encontrado na seção de atualizações .
Por fim, temos um algoritmo de instalação FIAS bastante simples e linear:
- obtenha do serviço SOAP um link para o archive e o número da versão atual,
- arquivo de download,
- desembalar
- escreva no banco de dados todos os dados dos arquivos com o prefixo AS_,
- exclua do banco de dados todos os dados dos arquivos com o prefixo AS DEL (sim, isso mesmo, durante a instalação, você também deve excluir alguns dados),
- anote o número da versão instalada.
E não menos simples algoritmo de atualização:
- obtenha do serviço SOAP uma lista com números de versão e links para arquivos com alterações,
- se a versão atual no banco de dados local for a mais recente, pare a execução,
- obtenha um link para o arquivo com as alterações para a próxima versão,
- arquivo de download,
- desembalar
- escreva no banco de dados todos os dados dos arquivos com o prefixo AS_,
- remova do banco de dados todos os dados dos arquivos com o prefixo AS DEL ,
- anote o número da versão atualizada,
- retorne ao primeiro passo.
O FIAS deixa impressões conflitantes. Por um lado: automação completa de todo o processo, formatos abertos, boa documentação. Por outro lado: uma decisão estranha de usar o RAR proprietário para dados abertos; diferenças entre documentação e realidade (principalmente relacionadas a atributos obrigatórios), que causam muitos problemas pequenos, mas desagradáveis; ocasionalmente vêm arquivos que não podem ser descompactados no Linux; alguns deltas entre versões ocupam 4-5 GB.
Arquitetura
Cada biblioteca deve se basear em uma idéia básica, um núcleo em torno do qual o restante da funcionalidade crescerá. O padrão da "cadeia de deveres" me pareceu a melhor escolha para o papel de tal idéia. Primeiro, é ideal: várias operações seqüenciais que uma pessoa teria feito se quisesse instalar o FIAS manualmente são óbvias para o desenvolvedor e se encaixam bem em pequenas classes escritas no estilo SOLID. Em segundo lugar, essa cadeia é facilmente expandida com novas operações em praticamente qualquer estágio, o que fornece uma boa flexibilidade. Em terceiro lugar, há muito tempo eu queria escrever minha própria implementação.
Além das operações, criei vários serviços que podem ser transferidos usando o DI. Eles permitem que você reutilize o código, substitua facilmente a implementação para tarefas de sistema de baixo nível (baixar um arquivo, descompactar um arquivo morto, gravar em um banco de dados e outros) e fornecer uma boa cobertura de teste graças a zombarias.
Como resultado, a biblioteca contém quatro tipos principais de objetos, para cada um dos quais a área de responsabilidade está claramente definida:
- serviços - fornece ferramentas para executar tarefas de baixo nível do sistema,
- objeto de estado - armazena informações para transmissão entre operações,
- operações - usando os serviços e declarando que implementam a parte atômica da lógica de negócios,
- cadeia de operações - realiza operações e transfere o estado entre elas.
Usando a vinculação de operações e serviços fornecidos pela biblioteca, você pode facilmente obter qualquer nova cadeia ou complementar a existente usando apenas arquivos de configuração.
Frameworks
Com longos intervalos e refatoração constante, trabalhei na biblioteca por um ano e meio.
A primeira versão relativamente estável ficou pronta em dois meses de trabalho à noite. De fato, ele poderia existir separadamente da estrutura e continha tudo o que era necessário: um script de entrada para executar no console, um contêiner de DI, um complemento sobre o PDO, suas próprias migrações de logger e estrutura de banco de dados - das quais eu tinha muito orgulho.
É claro que seus colegas a rejeitaram sem piedade.
O principal argumento contra isso foi a falta de suporte para estruturas populares. Ninguém queria escrever um invólucro separado para a biblioteca. Por causa disso, cometi o erro mais caro a tempo: comecei a dar suporte à versão autônoma e aos invólucros individuais de cada estrutura. Arquivos FIAS reais são diferentes do que está escrito na documentação. Cada vez que era necessário remover ou adicionar, por exemplo, não nulo na descrição da coluna, era necessário fazer alterações em três repositórios. Devido ao tédio do processo, o trabalho parou por mais seis meses.
O sentimento de incompletude me atormentou todo esse tempo e depois de uma batalha sangrenta com preguiça me forçou a voltar a criar uma nova versão. Para começar, decidi que ninguém precisa de uma biblioteca independente, o que significa que você deve descartar todos os serviços que fornecem estruturas do pacote, substituindo-os por interfaces. Por isso, fomos à loucura: um script de entrada para executar no console, um contêiner de DI, um complemento sobre o PDO, nossas próprias migrações de logger e estrutura de banco de dados. Em seguida, decidi criar pacotes separados para cada estrutura, que conectariam todas as partes do principal a um script de trabalho e fornecer implementações específicas de serviços.
O ponto chave foi o modelo. A atualização constante de conjuntos heterogêneos de objetos em vários repositórios não desejava. Ao mesmo tempo, no trabalho principal, consegui um projeto no Symfony. Após um rápido conhecimento, decidi que o recurso mais útil do SF é a geração de código e resolverá todos os meus problemas. Criei um arquivo yaml no pacote principal, que contém uma descrição declarativa dos dados do FIAS. Em seguida, adicionei geradores de código que criam classes específicas para modelos com base nesta descrição: Entidades de doutrina para objetos Symfony e Eloquent para Laravel. Durante o desenvolvimento de geradores, percebi que os modelos de galho não eram adequados para isso e decidi por uma solução especializada - o Nette PHP Generator .
Como prova de conceito, criei pacotes para o Laravel e o Symfony . Já que trabalhei mais com o segundo, descreverei tudo subsequente em seu contexto.
A infraestrutura
A maioria dos meus projetos de combate foi escrita em tecnologias desatualizadas, então não pude usar analisadores de código modernos em nenhum deles. Para me livrar da opressão herdada, instalei e configurei todas as ferramentas de controle de qualidade de código que pude:
Validação integrada no Github usando Travis . Como toque final, ele adicionou um arquivo Docker para criar um ambiente de desenvolvedor local completo com um arquivo make que contém os comandos básicos do contêiner (lançamento de verificações, testes, criação de modelos e outros).
Resultados de aprendizagem
PHP 7
Antes de iniciar o trabalho na biblioteca, nunca usei realmente os novos recursos do PHP 7 . Eles são lindos: de tipos estritos a um aumento significativo na produtividade. Agradecimentos especiais aos desenvolvedores pelo operador coalescente nulo. Não vi uma diminuição tão séria na base de código após a introdução de um operador.
Rar
Surpreendentemente, no PECL havia um pacote para trabalhar com o RAR . Normalmente, essas extensões não são confiáveis e tento evitá-las. Acabou sendo suspeitamente estável: foi instalado no 7.2 sem problemas, conseguiu descompactar arquivos enormes de forma relativamente rápida e com baixo consumo de RAM (6 GB são descompactados em 10 a 20 minutos, dependendo dos recursos disponíveis do sistema). Ainda temo que isso seja alguma manifestação da lei de Murphy.
Xmlreader
Ler arquivos xml gigantes não é uma tarefa trivial. E novamente a extensão PECL veio em socorro - XmlReader . Eu não percebi imediatamente todo o seu poder, mas em várias abordagens o adaptei em conjunto com o serializador Symfony para obter rápida e economicamente dados de arquivos FIAS. No lado da biblioteca, o objeto leitor implementa a interface do iterador, que retorna seqüencialmente seqüências xml correspondentes a um registro no arquivo. Usando o serializador Symfony, essas seqüências são convertidas em objetos. Um arquivo de 20 GB pode ser lido em 3-4 minutos enquanto não for usado mais de 50 MB de RAM.
Gravar no banco de dados
Obviamente, comecei com matrizes associativas com dados e descrições de tabelas volumosas. O código rapidamente se transformou em um hash de configurações e classes de conversor.
A magia das entidades de Doutrina mostrou como os objetos podem ser autoexplicativos. Decidi usar a mesma abordagem, mas, ao mesmo tempo, me livre da minha própria implementação de gravar dados no banco de dados usando o PDO. Em vez disso, criei uma interface de armazenamento que descreve métodos para processar objetos. Com base na classe da entidade, uma implementação específica de armazenamento decide exatamente como e onde gravar os dados. Essa abordagem facilitou a conexão de uma grande variedade de armazenamentos: do MySql aos arquivos csv.
Otimização de inserção de dados
Interrompi a primeira importação depois que ela excedeu em 48 horas. Tornou-se óbvio que você precisa otimizar o processo de inserção de dados.
Primeiro, mudei para as colunas do tipo ugid para chaves primárias incorporadas ao PostgreSql . Gravar em uma coluna uuid com um índice é muito mais rápido do que gravar em uma string.
Depois disso, abandonei todos os índices não críticos e chaves estrangeiras, pois a preocupação com a integridade dos dados está completamente do lado da equipe do FIAS.
Depois refiz a interface de armazenamento para que o script externo pudesse informá-lo explicitamente sobre a conclusão da importação. Isso permitiu o uso de inserção em massa, o que acelerou a gravação às vezes. Procurando informações, também encontrei o comando copy
junto com o query_to_xml
. Ele tinha duas desvantagens principais: primeiro, o usuário do PostgreSql deve ter permissões de leitura para o arquivo, o que eu não pude garantir e, segundo, a capacidade de modificar os dados dentro do script antes de perder a gravação.
Apesar dessas alterações, o tempo de importação excedeu 30 horas. Era necessária uma mudança radical de abordagem.
Processos paralelos
A Internet está repleta de artigos sobre assincronia no PHP. Minha escolha caiu em Amp . Simplesmente não funcionou de forma assíncrona. Primeiro, o código rapidamente se transformou em uma terrível lista de retornos de chamada e chamadas não óbvias (isso provavelmente é culpa minha, não a abordagem assíncrona). Em segundo lugar, tive que abandonar o uso de ORMs padrão, porque são necessárias chamadas sem bloqueio do banco de dados por meio de uma estrutura especial . Em terceiro lugar, embora existam condições sob as quais o PostgreSql possa inserir linhas em paralelo, elas são extremamente difíceis de cumprir. Como resultado, após 5 horas de trabalho, observei como todas as minhas solicitações assíncronas eram forçosamente "sincronizadas" no lado do banco de dados.
Mas a importação é bem dividida em processos paralelos: várias tarefas completamente independentes que não possuem recursos comuns pelos quais eles poderiam competir e dados que eles poderiam trocar. Além disso, no âmbito de um segmento, recebi um código bonito e linear.
O primeiro eu decidi tentar a extensão Parallel . Ele tem uma falha fatal - o intérprete deve ser construído com suporte para ZTS (Zend Thread Safety). Como o ZTS não funciona em scripts regulares da web, seria necessário ter duas versões diferentes do intérprete. Um, sem ZTS, para web, o segundo, com ZTS, para instalação do FIAS. O potencial aumento de desempenho superou esse inconveniente, principalmente considerando a facilidade de montar um novo contêiner Docker e usá-lo em conjunto com o antigo. Infelizmente, iniciar o Symfony dentro de um novo encadeamento causou um estouro na pilha do PHP, e eu não estava pronto para recusar o contêiner de DI e a configuração conveniente.
Finalmente, encontrei o processo symfony . De fato, ele inicia um novo processo para o comando do console especificado e monitora sua conclusão. Eu tive que adicionar duas correntes adicionais. O primeiro baixa o arquivo, descompacta e inicia processos paralelos para processamento de dados. O segundo pega uma lista de arquivos do argumento da linha de comando e grava seu conteúdo no banco de dados.
Devido à falta de experiência com processos paralelos, parece que cometi todos os erros de novato.
Por exemplo, meu processo de inicialização verificou a conclusão dos filhos usando um loop infinito e, é claro, estava gastando indecentemente muitos recursos do processador nisso. A chamada de sono entre iterações ajudou.
Na primeira implementação, os arquivos foram distribuídos de maneira desigual entre os processos. Os dois maiores caíram em um fluxo, que foi processado por mais de 20 horas. Na segunda implementação, adicionei um gerenciador especial que distribui arquivos com base no tempo necessário para importá-los. Agora os processos são carregados uniformemente.
Após essas edições, consegui importar a versão completa do FIAS em 16 a 20 horas, dependendo dos recursos do servidor. Não é tão bom quanto gostaríamos, mas continuo trabalhando na otimização. O próximo passo é uma rejeição completa do PostgreSql em favor da Elasticsearch.
Conclusões
Valeu a pena? Dois anos de trabalho em uma biblioteca que nunca entrou em nenhum projeto de combate?
Sim completamente.
Eu mudei de emprego de qualquer maneira. Durante uma turnê de uma dúzia de entrevistas, respondi a muitas perguntas complicadas apenas graças ao meu projeto de estimação.
O pânico de que o PHP está morrendo está constantemente se fortalecendo. Não vou esconder o fato de que eu estava pensando em migrar para outro idioma.
Depois que vi o enorme trabalho que a equipe do PHP colocou na versão 7; ele foi convencido pelo exemplo pessoal de quão madura a linguagem se tornou e de quão rico foi o ecossistema que cresceu; Posso dizer com segurança que os rumores sobre a morte do PHP são muito exagerados. E este é apenas o começo: no futuro, estamos aguardando JIT, assincronia fora da caixa e muito mais.