RBKmoney Payments under the hood - microservicios, protocolos y configuración de plataforma

Hola habr RBKmoney nuevamente se pone en contacto y continúa una serie de artículos sobre cómo escribir el procesamiento de pagos de bricolaje.



Quería sumergirme de inmediato en los detalles de la descripción de la implementación de un proceso comercial de pago como máquina de estado, mostrar ejemplos de una máquina de este tipo con un conjunto de eventos, características de implementación ... Pero parece que no puede prescindir de un par de artículos de revisión más. El área temática resultó ser demasiado grande. Esta publicación revelará los matices del trabajo y la interacción entre los microservicios de nuestra plataforma, la interacción con sistemas externos y cómo gestionamos la configuración del negocio.


Servicio de macro


Nuestro sistema consta de muchos microservicios que, implementando cada una de sus partes terminadas de la lógica empresarial, interactúan entre sí y juntos forman un macro servicio. En realidad, el macro servicio implementado en el centro de datos, conectado a bancos y otros sistemas de pago, es nuestro procesamiento de pagos.


Plantilla de microservicio


Utilizamos un enfoque unificado para el desarrollo de cualquier microservicio en cualquier idioma que esté escrito. Cada microservicio es un contenedor Docker que contiene:


  • la aplicación en sí que implementa la lógica de negocios escrita en Erlang o Java;
  • RPClib: una biblioteca que implementa la comunicación entre microservicios;
    • utilizamos Apache Thrift, sus principales ventajas son las bibliotecas cliente-servidor listas para usar y la capacidad de tipificar estrictamente la descripción de todos los métodos públicos que ofrece cada microservicio;
    • La segunda característica de la biblioteca es nuestra implementación de Google Dapper , que nos permite rastrear rápidamente las solicitudes con una simple búsqueda en Elasticsearch. El primer microservicio que recibió una solicitud de un sistema externo genera un único trace_id , que se guarda en cada cadena de solicitud posterior. Además, generamos y span_id y span_id , lo que le permite crear un árbol de consultas, monitoreando visualmente toda la cadena de microservicios involucrados en el procesamiento de la solicitud;
    • La tercera característica: utilizamos activamente la transferencia a nivel de transporte de información diferente sobre el contexto de la solicitud. Por ejemplo, los plazos (la vida útil esperada de la solicitud establecida en el cliente), o en nombre de quién hacemos una llamada a un método;
  • La plantilla de cónsul es un agente de descubrimiento de servicios que mantiene información sobre la ubicación, disponibilidad y estado de un microservicio. Los microservicios se encuentran entre sí por nombres DNS, la zona TTL es cero, el servicio que ha muerto o no ha pasado el chequeo de salud deja de resolverse y por lo tanto recibe tráfico;
  • los registros que la aplicación escribe en un formato comprensible para Elasticsearch en el archivo contenedor local y filebeat , que se ejecuta en la máquina host en relación con el contenedor, recoge estos registros y los envía al clúster Elasticsearch;
    • Como implementamos la plataforma de acuerdo con el modelo de Event Sourcing, las cadenas de registro resultantes también se utilizan para la visualización en forma de diferentes paneles de Grafana, lo que nos permite reducir el tiempo para implementar diferentes métricas (también utilizamos métricas separadas).


Al desarrollar microservicios, utilizamos las limitaciones que hemos inventado especialmente, que están diseñadas para resolver el problema de la alta disponibilidad de la plataforma y su tolerancia a fallas:


  • límites estrictos de memoria para cada contenedor, cuando va más allá de los límites: OOM, la mayoría de los microservicios viven dentro de 256-512M. Esto hace que la implementación de la lógica empresarial sea más finamente fragmentada, protege contra la deriva hacia el monolito, reduce el costo del punto de falla, brinda una ventaja adicional de la capacidad de trabajar en hardware barato (la plataforma se implementa y se ejecuta en servidores económicos de un solo procesador);
  • la menor cantidad posible de microservicios con estado y tantas implementaciones sin estado como sea posible. Esto nos permite resolver los problemas de tolerancia a fallas, velocidad de recuperación y, en general, minimizar lugares con un comportamiento potencialmente incomprensible. Esto se vuelve especialmente importante con un aumento en la vida del sistema cuando se acumula un gran legado;
  • deje que se bloquee y se acerca "definitivamente se romperá". Sabemos que cualquier parte de nuestro sistema necesariamente fallará, por lo que lo diseñamos para que esto no afecte la corrección general de la información acumulada en la plataforma. Ayuda a minimizar la cantidad de estados indefinidos en el sistema.

Seguramente familiar para muchos que se integran con terceros, la situación. Esperábamos una respuesta de un tercero a la solicitud de cancelar el dinero de acuerdo con el protocolo, y llegó una respuesta completamente diferente, no descrita en ninguna especificación, que se desconoce cómo interpretarlo.


En esta situación, matamos la máquina de estado que sirve este pago, cualquier acción desde afuera recibirá un error de 500. Y en el interior descubrimos el estado actual del pago, alineamos el estado de la máquina con la realidad y revivimos la máquina de estado.


Desarrollo Orientado a Protocolo



Al momento de escribir, se registraron 636 cheques diferentes en nuestro Servicio de Descubrimiento de servicios que aseguran el funcionamiento de la plataforma. Incluso teniendo en cuenta que se están realizando varias verificaciones en un servicio, y también que la mayoría de los servicios sin estado funcionan en al menos una instancia triple, aún puede obtener cincuenta aplicaciones que de alguna manera deben estar conectadas entre sí y no fallar en el infierno RPC.


La situación se complica por el hecho de que tenemos tres lenguajes de desarrollo en la pila: Erlang, Java, JS, y todos deben poder comunicarse de manera transparente entre sí.


La primera tarea que debía resolverse era diseñar la arquitectura correcta para el intercambio de datos entre microservicios. Como base, tomamos Apache Thrift. Todos los microservicios intercambian binarios trift; utilizamos HTTP como transporte.


Colocamos las especificaciones de fuente en forma de repositorios separados en nuestro github, para que estén disponibles para cualquier desarrollador que tenga acceso a ellas. Inicialmente, utilizaron un repositorio común para todos los protocolos, pero con el tiempo llegaron a la conclusión de que esto es inconveniente: el trabajo paralelo en conjunto sobre los protocolos se convirtió en un dolor de cabeza constante. Diferentes equipos e incluso diferentes desarrolladores se vieron obligados a ponerse de acuerdo sobre el nombre de las variables, un intento de dividirse en el espacio de nombres tampoco ayudó.


En general, podemos decir que tenemos un desarrollo basado en protocolos. Antes de comenzar cualquier implementación, desarrollamos el futuro protocolo de microservicios en forma de una especificación de elevación, revisamos 7 círculos de revisión, atraemos a futuros clientes de este microservicio y tenemos la oportunidad de comenzar simultáneamente a desarrollar varios microservicios en paralelo, porque conocemos todos sus métodos futuros y ya podemos escribir sus manejadores, opcionalmente usando moki.


Un paso separado en el proceso de desarrollo del protocolo es una revisión de seguridad, donde los chicos miran desde el punto de vista de Pentester los matices de la especificación que se está desarrollando.


También consideramos apropiado destacar una función separada del propietario del protocolo en el equipo. La tarea es difícil, una persona tiene que tener en cuenta los detalles de todos los microservicios, pero vale la pena en un gran orden y la presencia de un único punto de escalada.


Sin la aprobación final de la solicitud de extracción por parte de estos empleados, el protocolo no puede fusionarse en una rama maestra. Hay una funcionalidad muy conveniente en el github para esto: propietarios de códigos , lo usamos con gusto.


Por lo tanto, resolvimos el problema de la comunicación entre microservicios, posibles problemas de malentendido qué tipo de microservicio apareció en la plataforma y por qué es necesario. Este conjunto de protocolos es quizás la única parte de la plataforma en la que elegimos incondicionalmente la calidad frente al costo y la velocidad de desarrollo, porque la implementación de un microservicio puede reescribirse con relativa facilidad, y el protocolo en el que varias docenas ya es costoso y doloroso.


En el camino, el registro preciso ayuda a resolver el problema de la documentación. ¡Nombres de métodos y parámetros elegidos razonablemente, algunos comentarios y una especificación auto documentada ahorran mucho tiempo!


Por ejemplo, así es como se ve la especificación del método de uno de nuestros microservicios, permitiéndole obtener una lista de eventos que ocurrieron en la plataforma:


 /**    */ typedef i64 EventID /* Event sink service definitions */ service EventSink { /** *       ,   *    ,  ,  `range`.  *      `0`  `range.limit` . * *   `range.after`    ,   * ,        , *   `EventNotFound`. */ Events GetEvents (1: EventRange range) throws (1: EventNotFound ex1, 2: base.InvalidRequest ex2) /** *         *  . */ base.EventID GetLastEventID () throws (1: NoLastEvent ex1) } /* Events */ typedef list<Event> Events /** * ,    -,  . */ struct Event { /** *  . *    ,     *      (total order). */ 1: required base.EventID id /** *   . */ 2: required base.Timestamp created_at /** *  -,  . */ 3: required EventSource source /** *  ,    ( ) *   -,  . */ 4: required EventPayload payload /** *      . *    . */ 5: optional base.SequenceID sequence } // Exceptions exception EventNotFound {} exception NoLastEvent {} /** * ,       -   */ exception InvalidRequest { /**          */ 1: required list<string> errors } 

Cliente de consola de ahorro


A veces nos enfrentamos a la tarea de llamar a ciertos métodos del microservicio necesario directamente, por ejemplo, con nuestras manos desde la terminal. Esto puede ser útil para depurar, obtener un conjunto de datos sin procesar o cuando la tarea es tan rara que no es práctico desarrollar una interfaz de usuario separada.


Por lo tanto, desarrollamos una herramienta para nosotros que combina funciones de curl , pero le permite realizar solicitudes de trift en forma de estructuras JSON. Lo llamamos en consecuencia: woorl . La utilidad es universal, es suficiente para transferirle la ubicación de cualquier especificación de elevación utilizando el parámetro de línea de comando, hará el resto por sí mismo. Una utilidad muy conveniente, puede comenzar un pago directamente desde la terminal, por ejemplo.


Así es como se ve una apelación directamente al microservicio de la plataforma, que es responsable de administrar las aplicaciones (por ejemplo, para crear una tienda). Solicité datos en mi cuenta de prueba:



Los lectores observadores probablemente notaron una característica en la captura de pantalla. Eso tampoco nos gusta. Es necesario asegurar la autorización de llamadas trift entre microservicios, es necesario pegar TLS de una buena manera. Pero aunque los recursos, como siempre, no son suficientes. Nos limitamos al recinto total del perímetro en el que viven los microservicios de procesamiento.


Protocolos para comunicarse con sistemas externos.


Para publicar las especificaciones de elevación externas y obligar a nuestros comerciantes a comunicarse utilizando el protocolo binario, lo consideramos demasiado cruel para ellos. Era necesario elegir un protocolo legible por humanos que nos permitiera integrarnos convenientemente, depurar y poder documentar convenientemente. Elegimos el estándar Open API, también conocido como Swagger .


Volviendo al problema de documentar protocolos, Swagger le permite resolver este problema de manera rápida y económica. La red tiene muchas implementaciones del hermoso diseño de la especificación Swagger en forma de documentación para desarrolladores. Analizamos todo lo que pudimos encontrar y finalmente elegimos ReDoc , una biblioteca JS que acepta swagger.json como entrada, y genera dicha documentación de tres columnas en la salida: https://developer.rbk.money/api/ .


Los enfoques para el desarrollo de ambos protocolos, Thrift interno y Swagger externo, son absolutamente idénticos para nosotros. Esto agrega tiempo al desarrollo, pero vale la pena a largo plazo.


También necesitábamos resolver otro problema importante: no solo aceptamos solicitudes para cancelar dinero, sino que también las enviamos más lejos, a bancos y sistemas de pago.


Forzarlos a implementar nuestro ascensor sería una tarea aún más impracticable que enviarlo a API públicas.


Por lo tanto, se nos ocurrió e implementamos el concepto de adaptadores de protocolo. Este es solo otro microservicio que implementa nuestra especificación de elevación interna en un lado, que es lo mismo para toda la plataforma, y ​​el segundo es un protocolo externo específico para un banco o subestación en particular.


Los problemas que surgen al escribir dichos adaptadores cuando tiene que interactuar con terceros es un tema rico en historias diferentes. En nuestra práctica, hemos encontrado diferentes cosas, respuestas de la forma: "usted, por supuesto, puede implementar esta función como se describe en el protocolo que le dimos, pero no doy ninguna garantía. Aquí viene nuestro paciente que todo esto responde, y le pides confirmación ". Además, tales situaciones no son infrecuentes: "aquí está el nombre de usuario y la contraseña de nuestro servidor, vaya allí y configure todo usted mismo".


Me resulta especialmente interesante cuando nos integramos con un socio de pagos, que, a su vez, se había integrado previamente con nuestra plataforma y había realizado pagos con éxito a través de nosotros (esto a menudo sucede, los detalles comerciales de la industria de pagos). En respuesta a nuestra solicitud de un entorno de prueba, el socio respondió que no tenía un entorno de prueba como tal, pero que podía obtener tráfico para la integración con RBC, es decir, con nuestra plataforma, donde podríamos involucrarnos. Así es como nosotros, a través de un socio, nos integramos con nosotros mismos una vez.


Por lo tanto, simplemente resolvimos el problema de implementar una conexión paralela masiva de varios sistemas de pago y otros terceros. En la gran mayoría de los casos, no necesita tocar el código de la plataforma para esto, solo escriba los adaptadores y agregue más instrumentos de pago a la enumeración.


Como resultado, obtuvimos ese esquema de trabajo: buscamos fuera de los microservicios de API RBKmoney (los llamamos API común, o capi *, los viste en el cónsul anterior), que validan los datos de entrada de acuerdo con la especificación pública de Swagger, autorizan a los clientes, transmiten estos métodos a nuestras llamadas de elevación internas y envían solicitudes por la cadena al siguiente microservicio. Además, estos servicios implementan otro requisito de plataforma, cuya especificación técnica se formuló como: "el sistema siempre debe tener la oportunidad de obtener un gato".


Cuando necesitamos hacer una llamada a algún sistema externo, los microservicios internos extraen los métodos de elevación del adaptador de protocolo correspondiente, los traducen al idioma de un banco o sistema de pago específico y los envían.


Protocolo Dificultades de compatibilidad con versiones anteriores


La plataforma está en constante evolución, se agregan nuevas funciones, se cambian las antiguas. En tales circunstancias, debe invertir en compatibilidad con versiones anteriores o actualizar constantemente microservicios dependientes. Y si la situación cuando el campo requerido se convierte en opcional es simple, no puede hacer nada en absoluto, entonces en el caso contrario debe gastar recursos adicionales.


Con un conjunto de protocolos internos, las cosas se vuelven más fáciles. La industria de pagos rara vez cambia, por lo que aparecen algunos métodos de interacción fundamentalmente nuevos. Tomemos, por ejemplo, una tarea común para nosotros: conectar un nuevo proveedor con un nuevo instrumento de pago. Por ejemplo, el procesamiento de billetera local, que le permite procesar pagos en Kazajstán. Esta es una nueva billetera para nuestra plataforma, pero en principio no difiere de la misma billetera Qiwi: siempre tiene un identificador único y métodos que le permiten debitar / cancelar el débito.


En consecuencia, nuestra especificación de elevación para todos los proveedores de billeteras se ve así:


 typedef string DigitalWalletID struct DigitalWallet { 1: required DigitalWalletProvider provider 2: required DigitalWalletID id } enum DigitalWalletProvider { qiwi rbkmoney } 

y agregar un nuevo medio de pago en forma de una nueva billetera simplemente complementa enum:


 enum DigitalWalletProvider { qiwi rbkmoney newwallet } 

Ahora queda por superar todos los microservicios utilizando esta especificación, sincronizándose con el asistente de repositorio con la especificación y desplegándolos a través de CI / CD.


Los protocolos externos son más complicados. Cada actualización de la especificación Swagger, especialmente sin compatibilidad con versiones anteriores, es casi imposible de aplicar dentro de un plazo razonable; es poco probable que nuestros socios conserven recursos de desarrollador gratuitos específicamente para actualizar nuestra plataforma.


Y a veces esto es simplemente imposible, ocasionalmente encontramos situaciones como: "el programador nos escribió y se fue, se llevó el código fuente con él, cómo trabajamos, no lo sabemos, funciona y no lo tocamos".


Por lo tanto, invertimos en admitir compatibilidad con versiones anteriores en protocolos externos. Esto es un poco más fácil en nuestra arquitectura, dado que usamos adaptadores de protocolo separados para cada versión específica de la API común, simplemente dejamos que funcionen los microservicios de capi antiguos, cambiando solo la parte que parece un trift dentro de la plataforma, si es necesario. Entonces, los microservicios capi-v1 , capi-v2 , capi-v3 etc., aparecen y permanecen con nosotros para siempre.


Lo que sucederá cuando capi-v33 tendremos que desaprobar algunas versiones antiguas, probablemente.


En este punto, generalmente empiezo a entender muy bien a compañías como Microsoft y todo su dolor al apoyar la compatibilidad con versiones anteriores de soluciones que han estado funcionando durante décadas.


Personaliza el sistema


Y, al finalizar el tema, le diremos cómo gestionamos la configuración de la plataforma específica de la empresa.


Simplemente hacer un pago no es tan fácil como parece. Para cada pago, el cliente comercial desea adjuntar una gran cantidad de condiciones, desde la comisión hasta, en principio, la posibilidad de una implementación exitosa dependiendo de la hora del día. Nos propusimos digitalizar todo el conjunto de condiciones que un cliente comercial puede presentar ahora y en el futuro y aplicar este conjunto a cada pago recién lanzado.


Como resultado, decidimos desarrollar nuestro propio DSL, en el cual jodimos herramientas para una gestión conveniente que nos permite describir el modelo de negocio de la manera correcta: la elección de adaptadores de protocolo, una descripción del plan de contabilización, según el cual el dinero se distribuirá en las cuentas dentro del sistema, estableciendo límites, comisiones, categorías y Otras cosas específicas del sistema de pago.


Por ejemplo, cuando queremos tomar una comisión del 1% por adquirir tarjetas del maestro y MS y dispersarla en cuentas dentro del sistema, configuramos el dominio de esta manera:


 { "cash_flow": { "decisions": [ { "if_": { "any_of": [ { "condition": { "payment_tool": { "bank_card": { "definition": { "payment_system_is": "maestro" } } } } }, { "condition": { "payment_tool": { "bank_card": { "definition": { "payment_system_is": "mastercard" } } } } } ] }, "then_": { "value": [ { "source": { "system": "settlement" }, "destination": { "provider": "settlement" }, "volume": { "share": { "parts": { "p": 1, "q": 100 }, "of": "operation_amount" } }, "details": "1% processing fee" } ] } } ] } } 

, , . , JSON. , , , . , , . , CVS/SVN-.


" ". , , , 1%, , , . , , . , .


cvs-like , . , — stateless, , . . .


- . , , . , , .


. , 10 , , .


, , , -, woorl-. - JSON- . - JS, , UX:



, , , .


, , .


, , SaltStack.


, !

Source: https://habr.com/ru/post/443518/


All Articles