
TL; DREl artículo describe el uso del proyecto mascota como una forma de mantener y mejorar las habilidades. El autor creó una biblioteca PHP para instalar FIAS a partir de archivos XML.
Propósito
Raramente cambio de trabajo, por lo tanto, dado el deseo natural de cada organización por procesos fijos, cualquier tarea se convierte en una rutina. Por un lado, es beneficioso para una empresa mantener ese estado, por otro lado, para mí esto significa una pérdida completa u obsolescencia de habilidades. PHP se está desarrollando rápidamente y, en consecuencia, el retraso potencial también está creciendo rápidamente. Finalmente, todos sabemos que hoy en día es difícil para un programador encontrar un buen trabajo sin el conocimiento de Elasticsearch, RabbitMQ, Kafka y otras tecnologías que no aparecen a menudo en mi trabajo diario.
Después de comenzar el siguiente sitio típico, decidí que era hora de cambiar algo. No quería cambiar mi trabajo, pero recordé cómo en una de las conferencias el orador recomendó usar su propio proyecto opcional, el llamado proyecto mascota, para la capacitación. El método parecía apropiado y decidí probarlo.
Selección de tareas
La elección de la tarea resultó ser la parte más difícil de la empresa. No se me ocurrió nada especial: algunos servicios, como un analizador de trabajos que se pueden implementar fácilmente en una pila familiar. Abandoné la idea del proyecto durante varios meses, hasta que accidentalmente vi las noticias sobre el hackathon del Ministerio de Finanzas. Sugirió usar una de la lista de fuentes de datos abiertas para crear un servicio. Entre otros, también se indicó el Sistema Federal de Direcciones de Información (FIAS). Desafortunadamente, el hackathon ya había terminado para entonces.
Aprendí sobre FIAS por primera vez, pero la tarea me pareció interesante. Juzgue usted mismo: aproximadamente 30 GB de archivos XML, aproximadamente 60 millones de líneas en la base de datos y, además, la biblioteca podría resultar útil en el trabajo. Había algunas soluciones listas para usar en Github, pero eso no me detuvo. Por el contrario, en base a su análisis, hice requisitos adicionales que destacarían mi implementación.
Mirando hacia el futuro, noto que me encontré con muchas menos dificultades de las que esperaba.
El 90% del éxito es la declaración correcta del problema. Después de varios años de trabajo de plantilla, fue bastante difícil lograr formular claramente el problema. Solo quería comenzar a trabajar, pero ya en el proceso todo se habría aclarado por sí solo. Después de una hora de lucha con la dilación, finalmente escribí: crear una biblioteca en PHP para importar datos FIAS.
Más tarde, después de probarlo, agregué algunos requisitos adicionales:
- implementación en PHP sin utilizar utilidades de terceros, código exclusivo en PHP y extensiones de PECL,
- importación de todos los datos del conjunto FIAS,
- instalación completa y ciclo de actualización: búsqueda de la versión necesaria, recepción del archivo, desempaquetado, escritura en la base de datos,
- Máxima flexibilidad: la capacidad de cambiar la ubicación de almacenamiento, modificar los datos antes de grabar, filtrar lo necesario, etc.
- La biblioteca debe integrarse fácilmente en los proyectos existentes.
FIAS
FIAS tiene un sitio web oficial que nos brinda la definición y el propósito de crear un sistema
El Sistema de Direcciones de Información Federal (FIAS) es el sistema de información del estado federal que proporciona la formación, mantenimiento y uso del registro de direcciones del estado.
El propósito de crear el FIAS es la formación de un único recurso federal que contenga información de direcciones estructurada, confiable, uniforme y públicamente disponible. Gracias a la implementación de FIAS, esta información se puede obtener de forma gratuita a través de Internet en el portal oficialmente registrado de FIAS.
Los materiales con una descripción son suficientes tanto en el sitio web de FIAS como en el Habré , por lo tanto, no me centraré en esto.
En resumen FIAS viene en dos formatos: FIAS y KLADR. El segundo está en desuso y ya no está en uso. La información se almacena en DBF o en XML. Cada cambio en la composición de FIAS está marcado con una nueva versión. Puede solicitar un paquete con datos completos que esté actualizado en este momento o que solo contenga cambios entre las dos versiones. Links proporciona un servicio SOAP. El paquete es un archivo RAR que contiene archivos con nombres especialmente formados. Consisten en un prefijo, un nombre de conjunto de datos y una fecha de generación. Hay dos tipos de prefijos: AS_ para archivos desde los que se deben agregar datos a la base de datos, y AS DEL para archivos cuyos datos se deben eliminar de la base de datos.
FIAS contiene los siguientes datos:
- registro de elementos formadores de direcciones (este es el gráfico de direcciones: regiones, ciudades y calles),
- elementos de dirección que identifican objetos direccionables (número de casa y datos de la casa),
- información sobre terrenos
- información sobre el local (apartamentos, oficinas, habitaciones, etc.),
- información sobre el documento normativo, que es la base para la asignación del nombre al elemento de dirección.
Además de varios diccionarios- una lista de posibles valores de los intervalos de las casas (regular, par, impar),
- Una lista de estados de relevancia de una entrada de un elemento de dirección por el clasificador KLADR4.0
- una lista de estados de relevancia de una entrada de un elemento de dirección de acuerdo con FIAS,
- una lista de nombres completos y abreviados de tipos de elementos de dirección y sus niveles de clasificación,
- lista de tipos de edificios,
- lista de posibles tipos de propiedad
- lista de códigos de operaciones con objetos direccionables,
- una lista de posibles condiciones de bienes inmuebles,
- lista de tipos de locales u oficinas,
- lista de tipos de habitaciones,
- lista de posibles estados (centros) de objetos de dirección de unidades administrativas,
- tipos de documentos reglamentarios.
La estructura de datos se describe en el documento, que se puede encontrar en la sección de actualizaciones .
En definitiva, tenemos un algoritmo de instalación FIAS bastante simple y lineal:
- obtener del servicio SOAP un enlace al archivo y el número de versión actual,
- descargar archivo,
- desempacar
- escribe en la base de datos todos los datos de los archivos con el prefijo AS_,
- eliminar de la base de datos todos los datos de los archivos con el prefijo AS DEL (sí, es cierto, durante la instalación también debe eliminar algunos de los datos)
- anote el número de la versión instalada.
Y no menos simple algoritmo de actualización:
- obtener del servicio SOAP una lista con números de versión y enlaces a archivos con cambios,
- si la versión actual en la base de datos local es la última, entonces detenga la ejecución,
- obtener un enlace al archivo con los cambios a la próxima versión,
- descargar archivo,
- desempacar
- escribe en la base de datos todos los datos de los archivos con el prefijo AS_,
- eliminar de la base de datos todos los datos de los archivos con el prefijo AS DEL ,
- anote el número de la versión actualizada,
- volver al primer paso
FIAS deja impresiones contradictorias. Por un lado: automatización completa de todo el proceso, formatos abiertos, buena documentación. Por otro lado: una extraña decisión de usar RAR patentado para datos abiertos; diferencias entre documentación y realidad (principalmente relacionadas con atributos obligatorios), que causan muchos problemas pequeños pero desagradables; ocasionalmente vienen archivos que no se pueden desempaquetar en Linux; algunos deltas entre versiones ocupan 4-5 GB.
Arquitectura
Cada biblioteca debe basarse en una idea básica, un núcleo alrededor del cual crecerá el resto de la funcionalidad. El patrón de "cadena de deberes" me pareció la mejor opción para el papel de tal idea. En primer lugar, es ideal: varias operaciones secuenciales que una persona hubiera realizado si hubiera querido instalar FIAS manualmente son obvias para el desarrollador y encajan bien en clases pequeñas escritas en el estilo SOLID. En segundo lugar, dicha cadena se expande muy fácilmente con nuevas operaciones en casi cualquier etapa, lo que proporciona una buena flexibilidad. En tercer lugar, hace tiempo que quería escribir mi propia implementación.
Además de las operaciones, he creado varios servicios que pueden transferirse usando DI. Le permiten reutilizar el código, reemplazar fácilmente la implementación para tareas del sistema de bajo nivel (descargar un archivo, desempaquetar un archivo, escribir en una base de datos y otros) y proporcionar una buena cobertura con las pruebas gracias a los simulacros.
Como resultado, la biblioteca contiene cuatro tipos principales de objetos, para cada uno de los cuales el área de responsabilidad está claramente definida:
- servicios: proporcionan herramientas para realizar tareas de sistema de bajo nivel,
- objeto de estado: almacena información para la transmisión entre operaciones,
- operaciones: utilizando los servicios y el estado en que implementan la parte atómica de la lógica empresarial,
- cadena de operaciones: realiza operaciones y transfiere el estado entre ellas.
Usando el enlace de operaciones y servicios proporcionados por la biblioteca, puede obtener fácilmente cualquier nueva cadena o complementar la existente utilizando solo archivos de configuración.
Marcos
Con pausas largas y refactorización constante, trabajé en la biblioteca durante un año y medio.
La primera versión relativamente estable estaba lista en dos meses de trabajo por las tardes. De hecho, podría existir por separado del marco y contener todo lo necesario: un script de entrada para ejecutar en la consola, un contenedor DI, un complemento para PDO, su propio registrador y migraciones de estructura de base de datos, de lo que estaba muy orgulloso.
Por supuesto, sus colegas la rechazaron sin piedad.
El principal argumento en contra de esto fue la falta de apoyo a los marcos populares. Nadie quería escribir un contenedor separado para la biblioteca. Debido a esto, cometí el error más costoso a tiempo: comencé a admitir tanto la versión independiente como los contenedores individuales para cada marco. Los archivos FIAS reales son diferentes de lo que está escrito en la documentación. Cada vez que era necesario eliminar o agregar, por ejemplo, no nulo en la descripción de la columna, tenía que hacer cambios en tres repositorios. Debido a lo tedioso del proceso, el trabajo se detuvo por otros seis meses.
La sensación de incompletitud me atormentó todo este tiempo y después de una sangrienta batalla contra la pereza me obligó a volver a diseñar una nueva versión. Para empezar, decidí que nadie necesita una biblioteca independiente, lo que significa que debe eliminar todos los servicios que proporcionan marcos del paquete, reemplazándolos por interfaces. Así que nos metimos debajo del cuchillo: un script de entrada para ejecutar en la consola, un contenedor DI, un complemento sobre PDO, nuestro propio registrador y migraciones de la estructura de la base de datos. A continuación, decidí hacer paquetes separados para cada marco, que conectarán todas las partes desde el principal a un script de trabajo y proporcionarán implementaciones específicas de servicios.
El punto clave fue el modelo. Actualizar constantemente conjuntos heterogéneos de objetos en varios repositorios no quería. Al mismo tiempo, en el trabajo principal, obtuve un proyecto en Symfony. Después de conocerlo rápidamente, decidí que la característica más útil de SF es la generación de código y resolverá todos mis problemas. Creé un archivo yaml en el paquete principal, que contiene una descripción declarativa de los datos FIAS. Luego agregué generadores de código que crean clases específicas para modelos basados en esta descripción: entidades de Doctrine para Symfony y objetos Eloquent para Laravel. Durante el desarrollo de los generadores, me di cuenta de que las plantillas de ramitas no eran adecuadas para esto, y me decidí por una solución especializada: Nette PHP Generator .
Como prueba de concepto, creé paquetes para Laravel y Symfony . Como trabajé más tiempo con el segundo, describiré todo lo posterior en su contexto.
La infraestructura
La mayoría de mis proyectos de combate se escribieron sobre tecnologías obsoletas, por lo que no pude usar analizadores de código modernos en ninguno de ellos. Deshaciéndome de la opresión heredada, instalé y configuré todas las herramientas de control de calidad de código que pude:
Validación integrada en Github usando Travis . Como toque final, agregó un archivo Docker para crear un entorno de desarrollador local completo con un archivo make que contiene los comandos básicos para el contenedor (lanzamiento de comprobaciones, pruebas, creación de modelos y otros).
Resultados de aprendizaje
PHP 7
Antes de comenzar a trabajar en la biblioteca, nunca usé las nuevas características de PHP 7 . Son hermosos: desde tipos estrictos hasta un aumento significativo en la productividad. Un agradecimiento especial a los desarrolladores por el operador de fusión nula. No he visto una disminución tan grave en la base del código después de la introducción de un operador.
Rar
Sorprendentemente, en PECL había un paquete para trabajar con RAR . Por lo general, tales extensiones no son confiables y trato de evitarlas. Esto resultó ser sospechosamente estable: se instaló en 7.2 sin problemas, fue capaz de descomprimir archivos enormes relativamente rápido y con un bajo consumo de RAM (6 GB se desempacan en 10-20 minutos dependiendo de los recursos del sistema disponibles). Todavía temo que esto sea una manifestación de la ley de Murphy.
Xmlreader
Leer archivos xml gigantes no es una tarea trivial. Y de nuevo la extensión PECL vino al rescate: XmlReader . No me di cuenta de inmediato de todo su poder, pero en varios enfoques lo adapté junto con el serializador de Symfony para obtener datos de archivos FIAS de manera rápida y económica. En el lado de la biblioteca, el objeto lector implementa la interfaz iteradora, que devuelve secuencialmente las cadenas xml correspondientes a un registro en el archivo. Usando el serializador de Symfony, estas cadenas se convierten en objetos. Un archivo de 20 GB se puede leer en 3-4 minutos sin usar más de 50 MB de RAM.
Escribir en la base de datos
Por supuesto, comencé con matrices asociativas con datos y descripciones de tablas voluminosas. El código se convirtió rápidamente en un hash de configuraciones y clases de convertidor.
La magia de las entidades de Doctrina mostró cómo los objetos pueden autodescribirse. Decidí usar el mismo enfoque, pero al mismo tiempo deshacerme de mi propia implementación de escribir datos en la base de datos usando PDO. En cambio, creé una interfaz de almacenamiento que describe métodos para procesar objetos. Según la clase de entidad, una implementación de almacenamiento particular decide exactamente cómo y dónde escribir los datos. Este enfoque facilitó la conexión de una amplia variedad de almacenamientos: desde MySql a archivos csv.
Optimización de inserción de datos
Interrumpí la primera importación después de que excediera en 48 horas. Se hizo evidente que necesita optimizar el proceso de inserción de datos.
Primero, cambié a las columnas de tipo ugid para claves primarias integradas en PostgreSql . Escribir en una columna uuid con un índice es mucho más rápido que escribir en una cadena.
Después de esto, abandoné todos los índices no críticos y las claves externas, ya que la preocupación por la integridad de los datos está completamente del lado del equipo de FIAS.
Luego rehice la interfaz de almacenamiento para que el script externo pueda informarle explícitamente sobre la finalización de la importación. Esto permitió el uso de inserción masiva, que a veces aceleró la grabación. Buscando información, también encontré el comando copy
junto con query_to_xml
. Tenía dos grandes inconvenientes: en primer lugar, el usuario de PostgreSql debía tener permisos de lectura para el archivo, lo que no podía garantizar, y en segundo lugar, la capacidad de modificar datos dentro del script antes de que se perdiera la escritura.
A pesar de estos cambios, el tiempo de importación superó las 30 horas. Se necesitaba un cambio radical de enfoque.
Procesos paralelos
Internet está repleto de artículos sobre asincronía en PHP. Mi elección recayó en el amplificador . Simplemente no funcionó de forma asincrónica. En primer lugar, el código se convirtió rápidamente en una aterradora hoja de devoluciones de llamadas y llamadas no obvias (probablemente sea mi culpa, no el enfoque asincrónico). En segundo lugar, tuve que abandonar el uso de ORM estándar porque se necesitan llamadas sin bloqueo a la base de datos a través de un marco especial . En tercer lugar, aunque existen condiciones bajo las cuales PostgreSql puede insertar filas en paralelo, son extremadamente difíciles de cumplir. Como resultado, después de 5 horas de trabajo, vi todas mis solicitudes asincrónicas "sincronizadas" a la fuerza en el lado de la base de datos.
Pero la importación está bien dividida en procesos paralelos: varias tareas completamente independientes que no tienen recursos comunes por los cuales podrían competir, y datos que podrían intercambiar. Además, en el marco de un hilo, recibí un código hermoso y lineal.
El primero decidí probar la extensión paralela . Tiene un defecto fatal: el intérprete debe estar construido con soporte para ZTS (Zend Thread Safety). Dado que ZTS no funciona en los scripts web normales, habría que tener dos versiones diferentes del intérprete. Uno, sin ZTS, para web, el segundo, con ZTS, para instalar FIAS. El potencial aumento del rendimiento superó este inconveniente, especialmente teniendo en cuenta lo fácil que es ensamblar un nuevo contenedor Docker y usarlo junto con el anterior. Desafortunadamente, iniciar Symfony dentro de un nuevo hilo causó que la pila de PHP se desbordara, y no estaba listo para rechazar el contenedor DI y la configuración conveniente.
Finalmente, encontré el proceso de Symfony . De hecho, inicia un nuevo proceso para el comando de consola especificado y supervisa su finalización. Tuve que agregar dos cadenas adicionales. El primero descarga el archivo, lo descomprime e inicia procesos paralelos para el procesamiento de datos. El segundo toma una lista de archivos del argumento de la línea de comando y escribe su contenido en la base de datos.
Debido a la falta de experiencia con procesos paralelos, parece que he cometido todos los errores de novato.
Por ejemplo, mi proceso de inicio verificó la finalización de los niños usando un bucle infinito y, por supuesto, estaba gastando indecentemente muchos recursos de procesador en esto. La llamada de sueño entre iteraciones ayudó.
En la primera implementación, los archivos se distribuyeron de manera desigual entre los procesos. Los dos más grandes cayeron en una secuencia, que se procesó durante más de 20 horas. En la segunda implementación, agregué un administrador especial que distribuye archivos en función del tiempo que lleva importarlos. Ahora los procesos se cargan de manera uniforme.
Después de estas ediciones, pude importar la versión completa de FIAS en 16-20 horas, dependiendo de los recursos del servidor. No es tan bueno como nos gustaría, pero sigo trabajando en la optimización. El siguiente paso es un rechazo completo de PostgreSql a favor de Elasticsearch.
Conclusiones
¿Valió la pena? ¿Dos años de trabajo en una biblioteca que nunca entró en ningún proyecto de combate?
Si completamente.
Cambié mi trabajo de todos modos. Durante un recorrido de una docena de entrevistas, respondí muchas preguntas difíciles solo gracias a mi proyecto favorito.
El pánico de que PHP está muriendo es cada vez más fuerte. No ocultaré el hecho de que yo mismo estaba pensando en migrar a otro idioma.
Después de ver el gran trabajo que el equipo de PHP realizó en la versión 7; él estaba convencido por su ejemplo personal de cuán maduro se volvió el lenguaje y cuán rico fue el ecosistema que creció; Puedo decir con seguridad que los rumores sobre la muerte de PHP son muy exagerados. Y esto es solo el comienzo: en el futuro estamos esperando JIT, asincronía fuera de la caja y mucho más.