Ruslan Aromatov, desarrollador principal, ICD
Hola Habr! Trabajo como desarrollador de backend en Moscow Credit Bank, y durante mi trabajo he adquirido cierta experiencia que me gustaría compartir con la comunidad. Hoy les contaré cómo escribimos nuestro propio servicio de caché para los servidores frontales de nuestros clientes utilizando la aplicación móvil MKB Online. Este artículo puede ser útil para aquellos que están involucrados en el diseño de servicios y están familiarizados con la arquitectura de microservicios, la base de datos en memoria Tarantool y la biblioteca ZeroMQ. En el artículo no habrá prácticamente ejemplos de código y explicación de los conceptos básicos, sino solo una descripción de la lógica de los servicios y su interacción con un ejemplo específico que ha estado trabajando en nuestra batalla durante más de dos años.
Como empezó todo
Hace unos 6 años, el esquema era simple. Como legado de la empresa de outsourcing, obtuvimos dos clientes de banca móvil para iOS y Android, así como un servidor frontal que los atiende. El servidor en sí fue escrito en Java, fue a su backend de diferentes maneras (principalmente jabón) y se comunicó con los clientes transmitiendo xml a través de https.
Las aplicaciones de los clientes pudieron autenticarse de alguna manera, mostrar una lista de productos y ... parecían poder realizar algunas transferencias y pagos, pero de hecho no lo hicieron muy bien y no siempre. Por lo tanto, el servidor frontal no experimentó una gran cantidad de usuarios ni ninguna carga seria (lo que, sin embargo, no evitó que se cayera aproximadamente una vez cada dos días).
Está claro que nosotros (y en ese momento nuestro equipo estaba formado por cuatro personas), como los responsables del banco móvil, no nos adaptamos a esta situación, y para empezar, pusimos en orden las aplicaciones actuales, pero el servidor frontal resultó ser realmente malo, por lo que tuvo que reescriba rápidamente todo, reemplazando simultáneamente xml con json y pasando al servidor de aplicaciones
WildFly . Repartido en un par de años, la refactorización no se basa en una publicación separada, ya que todo se hizo principalmente para garantizar que el sistema funcionara de manera estable.
Poco a poco, las aplicaciones y el servidor desarrollados comenzaron a funcionar de manera más estable y su funcionalidad se expandió constantemente, lo que valió la pena: había cada vez más usuarios.
Al mismo tiempo, comenzaron a surgir problemas como la tolerancia a fallas, la redundancia, la replicación y, atemorizante de pensar, la alta carga.
Una solución rápida al problema fue agregar un segundo servidor WildFly, y las aplicaciones aprendieron a cambiar entre ellos. El problema del trabajo simultáneo con sesiones de clientes fue resuelto por el módulo
Infinispan integrado en WildFly.
Parecía que la vida estaba mejorando ...
No puedes vivir así
Sin embargo, esta opción de trabajar con sesiones, de hecho, no estuvo exenta de inconvenientes. Mencionaré aquellos que no nos convenían.
- Pérdida de sesiones. El menos importante. Por ejemplo, una aplicación envía dos solicitudes al servidor-1: la primera solicitud es autenticación y la segunda es una solicitud de una lista de cuentas. La autenticación es exitosa, se crea una sesión en el servidor-1. En este momento, la segunda solicitud del cliente se interrumpe repentinamente debido a una comunicación deficiente y la aplicación cambia al servidor-2, reenviando la segunda solicitud. Pero a una determinada carga de trabajo, Infinispan puede no tener tiempo para sincronizar datos entre nodos. Como resultado, el servidor 2 no puede verificar la sesión del cliente, envía una respuesta enojada al cliente, el cliente está triste y finaliza su sesión. El usuario debe iniciar sesión nuevamente. Triste
- Reiniciar el servidor también puede causar la pérdida de sesiones. Por ejemplo, después de una actualización (y esto sucede con bastante frecuencia). Cuando se inicia el servidor 2, no puede funcionar hasta que los datos estén sincronizados con el servidor 1. Parece que el servidor se inició, pero de hecho no debería aceptar solicitudes. Esto es inconveniente.
- Este es un módulo WildFly incorporado que nos impide alejarnos de este servidor de aplicaciones hacia microservicios.
A partir de aquí, una lista de lo que nos gustaría se formó de alguna manera por sí misma.
- Queremos almacenar sesiones de clientes para que cualquier servidor (sin importar cuántas haya) inmediatamente después del lanzamiento tenga acceso a ellas.
- Queremos almacenar cualquier información del cliente entre solicitudes (por ejemplo, parámetros de pago y todo eso).
- Queremos guardar cualquier información arbitraria en una clave arbitraria en general.
- Y también queremos recibir datos del cliente antes de que pase la autenticación. Por ejemplo, el usuario está autenticado y todos sus productos están allí, frescos y cálidos.
- Y queremos escalar de acuerdo con la carga.
- Y ejecutar en la ventana acoplable, y escribir registros en una sola pila, y contar las métricas, y así sucesivamente ...
- Ah, sí, y para que todo funcione rápidamente.
Harina de elección
Anteriormente, no implementamos la arquitectura de microservicios, así que para empezar nos sentamos a leer, mirar y probar diferentes opciones. De inmediato quedó claro que necesitábamos un repositorio rápido y algún tipo de complemento sobre él que se ocupe de la lógica de negocios y sea la interfaz de acceso al repositorio. Además, sería bueno asegurar el transporte rápido entre servicios.
Eligieron durante mucho tiempo, discutieron mucho y experimentaron. Ahora no describiré los pros y los contras de todos los candidatos, esto no se aplica al tema de este artículo, solo digo que el almacenamiento será
tarantool , escribiremos nuestro servicio en Java y
ZeroMQ funcionará como transporte. Ni siquiera argumentaré que la elección es muy ambigua, pero fue influenciada en gran medida por el hecho de que no nos gustan los diferentes marcos grandes y pesados (por su peso y lentitud), soluciones en caja (por su versatilidad y falta de personalización), pero al mismo tiempo Nos encanta controlar todas las partes de nuestro sistema tanto como sea posible. Y para controlar el trabajo de los servicios, elegimos el servidor de recopilación de métricas
Prometheus con sus agentes convenientes que pueden integrarse en casi cualquier código. Los registros de todo esto irán a la pila de ELK.
Bueno, me parece que ya había demasiada teoría.
Comenzar y terminar
El resultado del diseño fue aproximadamente ese esquema.
AlmacenamientoDebe ser lo más estúpido posible, solo para almacenar datos y sus estados actuales, pero siempre debe funcionar sin reinicios. Diseñado para servir diferentes versiones de servidores frontales. Mantenemos todos los datos en la memoria, recuperación en caso de reinicio a través de archivos .snap y .xlog.
Tabla (espacio) para sesiones de cliente:
- ID de sesión
- Identificación del cliente;
- versión (servicio)
- tiempo de actualización (marca de tiempo);
- tiempo de vida (ttl);
- datos de sesión serializados.
Aquí todo es simple: el cliente está autenticado, el servidor frontal crea una sesión y la guarda en el almacenamiento, recordando la hora. Con cada solicitud de datos, el tiempo se actualiza, por lo que la sesión se mantiene viva. Si, a pedido, los datos no están actualizados (o no habrá ninguno), entonces devolveremos un código de retorno especial, después de lo cual el cliente finalizará su sesión.
Tabla de caché simple (para cualquier dato de sesión):
- clave
- ID de sesión
- tipo de datos almacenados (número arbitrario);
- tiempo de actualización (marca de tiempo);
- tiempo de vida (ttl);
- Datos serializados.
Tabla de datos del cliente que debe calentarse antes de iniciar sesión:
- Identificación del cliente;
- ID de sesión
- versión (servicio)
- tipo de datos almacenados (número arbitrario);
- tiempo de actualización (marca de tiempo);
- condición
- Datos serializados.
Un campo importante aquí es la condición. En realidad, solo hay dos: inactivo y actualizando. Los coloca un servicio superpuesto que va al backend para obtener datos del cliente, de modo que otra instancia de este servicio no haga el mismo trabajo (ya inútil) y no cargue el backend.
Tabla de dispositivos:
- Identificación del cliente;
- ID del dispositivo
- tiempo de actualización (marca de tiempo);
La tabla de dispositivos es necesaria para que, incluso antes de que el cliente se autentique en el sistema, descubra su ID y comience a recibir sus productos (calentando el caché). La lógica es esta: la primera entrada siempre está fría, porque antes de la autenticación no sabemos qué tipo de cliente proviene de un dispositivo desconocido (los clientes móviles siempre transmiten ID de dispositivo en cualquier solicitud). Todas las entradas posteriores de este dispositivo irán acompañadas de un caché de calentamiento para el cliente asociado con él.
El trabajo con datos está aislado del servicio de Java mediante procedimientos del servidor. Sí, tuve que aprender lua, pero no me llevó mucho tiempo. Además de la gestión de datos en sí, los procedimientos lua también son responsables de devolver los estados actuales, las selecciones de índice, la limpieza de registros obsoletos en procesos en segundo plano (fibras) y la operación del servidor web incorporado a través del cual se lleva a cabo el servicio directo de acceso a los datos. Aquí está, el encanto de escribir todo con las manos, la posibilidad de un control ilimitado. Pero la desventaja es la misma: debe escribir todo usted mismo.
Tarantool funciona en un contenedor acoplable, todos los archivos lua necesarios se colocan allí en la etapa de ensamblaje de imágenes. Todo el ensamblaje a través de scripts gradle.
Replicación maestro-esclavo. En el otro host, se ejecuta exactamente el mismo contenedor que la réplica del almacenamiento principal. Se necesita en caso de un bloqueo de emergencia del maestro; luego, los servicios de Java cambian a esclavo y se convierte en el maestro. Hay un tercer esclavo por si acaso. Sin embargo, incluso una pérdida de datos completa en nuestro caso es triste, pero no fatal. Según el peor de los casos, los usuarios tendrán que iniciar sesión y recuperar todos los datos que vuelven a la caché.
Servicio de JavaDiseñado como un microservicio sin estado típico. No tiene configuración, todos los parámetros necesarios (y hay 6 de ellos) se pasan a través de las variables de entorno al crear el contenedor acoplable. Funciona con el servidor frontal a través del transporte ZeroMQ (org.zeromq.jzmq - la interfaz de Java para el libzmq.so.5.1.1 nativo, que nosotros mismos construimos) usando nuestro propio protocolo. Funciona con una tarántula a través de un conector java (org.tarantool.connector).
La inicialización del servicio es bastante simple:
- Iniciamos un registrador (log4j2);
- De las variables de entorno (estamos en la ventana acoplable) leemos los parámetros necesarios para el trabajo;
- Iniciamos el servidor de métricas (embarcadero);
- Conéctese a la tarántula (asincrónicamente);
- Comenzamos el número requerido de manipuladores de hilos (trabajadores);
- Comenzamos un corredor (zmq), un ciclo interminable de procesamiento de mensajes.
De todo lo anterior, solo el motor de procesamiento de mensajes es interesante. A continuación se muestra un diagrama del microservicio.
Comencemos con el inicio del corredor. Nuestro broker es un conjunto de zmq-sockets del tipo ROUTER, que acepta conexiones de varios clientes y es responsable de programar los mensajes que provienen de ellos.
En nuestro caso, tenemos un socket de escucha en la interfaz externa que recibe mensajes de clientes que usan el protocolo tcp y el otro recibe mensajes de subprocesos de los trabajadores que usan el protocolo inproc (es mucho más rápido que tcp).
Después de inicializar los sockets, comenzamos un ciclo de eventos sin fin.
public int run() { int status; try { ZMQ.Poller poller = new ZMQ.Poller(2); poller.register(workerServicePoint, ZMQ.Poller.POLLIN); poller.register(clientServicePoint, ZMQ.Poller.POLLIN); int rc; while (true) {
La lógica del trabajo es muy simple: recibimos mensajes de diferentes lugares y hacemos algo con ellos. Si algo se rompió críticamente con nosotros, salimos del bucle, lo que hace que el proceso se bloquee, lo que será reiniciado automáticamente por el demonio docker.
La idea principal es que el intermediario no maneja ninguna lógica comercial, solo analiza el encabezado del mensaje y distribuye las tareas a los hilos de trabajo que se iniciaron antes cuando comenzó el servicio. En esto, una sola cola de mensajes con priorización de una longitud fija lo ayuda.
Analicemos el algoritmo usando el ejemplo del esquema y el código anterior.
Después del inicio, los trabajadores de subprocesos que comenzaron más tarde que el intermediario se inicializan y envían un mensaje de preparación al intermediario. El corredor los acepta, los analiza y agrega a cada trabajador a la lista.
Ocurre un evento en el socket del cliente: recibimos mensaje1. El agente llama al manejador de mensajes entrantes, cuya tarea es:
- análisis del encabezado del mensaje;
- colocar un mensaje en un objeto titular con una prioridad dada (basada en el análisis del encabezado) y de por vida;
- colocar al titular en la cola de mensajes;
- si la cola no está llena, la tarea del controlador ha terminado;
- Si la cola está llena, llamamos al método para enviar un mensaje de error al cliente.
En la misma iteración del bucle, llamamos al manejador de la cola de mensajes:
- solicitamos el mensaje más reciente de la cola (la cola decide esto por sí misma en función de la prioridad y el orden de agregar el mensaje);
- verifique la vida útil del mensaje (si ha expirado, llame al método para enviar un mensaje de error al cliente);
- si el mensaje para el procesamiento es relevante, intente preparar al primer trabajador libre para trabajar;
- si no hay ninguno, vuelva a colocar el mensaje en la cola (más precisamente, simplemente no lo elimine de allí, permanecerá allí hasta que expire su vida útil);
- si tenemos un trabajador listo para trabajar, lo marcamos como ocupado y le enviamos un mensaje para que lo procese;
- Eliminar el mensaje de la cola.
Hacemos lo mismo con todos los mensajes posteriores. El trabajador de subprocesos en sí está diseñado de la misma manera que un intermediario: tiene el mismo ciclo interminable de procesamiento de mensajes. Pero en él ya no necesitamos procesamiento instantáneo, está diseñado para realizar tareas largas.
Después de que el trabajador completó su tarea (por ejemplo, fue al back-end para los productos del cliente o en la tarántula para la sesión), envía un mensaje al corredor, que el corredor envía de vuelta al cliente. La dirección del cliente a quien se debe enviar la respuesta se recuerda desde el momento en que llega el mensaje del cliente en el objeto titular, que se envía al trabajador como un mensaje en un formato ligeramente diferente, y luego regresa.
El formato de los mensajes que menciono constantemente es nuestra propia producción. Fuera de la caja, ZeroMQ nos proporciona las clases de ZMsg, el mensaje en sí mismo y el ZFrame, parte de este mensaje, esencialmente solo una matriz de bytes, que soy libre de usar si existe tal deseo. Nuestro mensaje consta de dos partes (dos ZFrames), la primera de las cuales es un encabezado binario y la segunda son datos (el cuerpo de la solicitud, por ejemplo, en forma de una cadena json representada por una matriz de bytes). El encabezado del mensaje es universal y viaja tanto de cliente a servidor como de servidor a cliente.
De hecho, no tenemos el concepto de "solicitud" o "respuesta", solo mensajes. El encabezado contiene: versión del protocolo, tipo de sistema (qué sistema se dirige), tipo de mensaje, código de error de nivel de transporte (si no es 0, algo sucedió en el motor de transferencia de mensajes), ID de solicitud (identificador de transferencia que viene del cliente - necesario para el seguimiento), ID de sesión del cliente (opcional), así como un signo de un error de nivel de datos (por ejemplo, si no se pudo analizar la respuesta del backend, configuramos este indicador para que el analizador del lado del cliente no deserialice la respuesta, pero reciba datos de error de otra manera).
Gracias a un protocolo único entre todos los microservicios y un encabezado de este tipo, podemos manipular los componentes de nuestros servicios. Por ejemplo, puede llevar el intermediario a un proceso separado y convertirlo en un único intermediario de mensajes a nivel de todo el sistema de microservicios. O, por ejemplo, ejecutar trabajadores no en forma de hilos dentro del proceso, sino como procesos independientes separados. Y aunque el código dentro de ellos no cambia. En general, hay margen para la creatividad.
Un poco sobre rendimiento y recursos
El corredor en sí mismo es rápido, y el ancho de banda total del servicio está limitado por la velocidad del backend y la cantidad de trabajadores. Convenientemente, toda la cantidad de memoria necesaria se asigna inmediatamente al inicio del servicio, y todos los subprocesos se inician de inmediato. El tamaño de la cola también es fijo. En tiempo de ejecución, solo se procesan los mensajes.
Como ejemplo: además del hilo principal, nuestro servicio de combate de caché actual lanza otros 100 hilos de trabajo, y el tamaño de la cola está limitado a tres mil mensajes. En funcionamiento normal, cada instancia procesa hasta 200 mensajes por segundo y consume aproximadamente 250 MB de memoria y aproximadamente el 2-3% de la CPU. A veces, en las cargas máximas, salta al 7-8%. Todo funciona en algún tipo de xeon virtual de doble núcleo.
El trabajo regular del servicio implica el empleo simultáneo de 3-5 trabajadores (de cada 100) con el número de mensajes en la cola 0 (es decir, pasan al procesamiento de inmediato). Si el backend comienza a disminuir, el número de trabajadores ocupados aumenta en proporción al tiempo de su respuesta. En los casos en que se produce un accidente y el back-end aumenta, todos los trabajadores terminan primero, después de lo cual la cola de mensajes comienza a obstruirse. Cuando se obstruye por completo, comenzamos a responder a los clientes con rechazos para procesar. Al mismo tiempo, no comenzamos a consumir recursos de memoria o CPU, brindando métricas de manera estable y respondiendo claramente a los clientes lo que está sucediendo.
La primera captura de pantalla muestra el funcionamiento regular del servicio.
Y en el segundo, ocurrió un accidente: el backend por alguna razón no respondió en 30 segundos. Se ve que al principio todos los trabajadores se quedaron sin trabajo, después de lo cual la cola de mensajes comenzó a obstruirse.
Pruebas de rendimiento
Las pruebas sintéticas en mi máquina de trabajo (CentOS 7, Core i5, 16Gb RAM) mostraron lo siguiente.
Trabaje con el repositorio (escribiendo en la tarántula e inmediatamente leyendo este registro de 100 bytes de tamaño, simulando el trabajo con la sesión) - 12000 rps.
Lo mismo, solo se midió la velocidad no entre los puntos de tarántula del servicio, sino entre el cliente y el servicio. Por supuesto, tuve que escribirle a un cliente para hacerme una prueba de estrés. Dentro de una máquina, fue posible obtener 7000 rps. En una red local (y tenemos muchas máquinas virtuales diferentes que no están claras cómo están físicamente conectadas), los resultados varían, pero es posible hasta 5000 rps para una instancia. Dios sabe qué tipo de rendimiento, pero cubre más de diez veces nuestras cargas máximas. Y esto es solo si se está ejecutando una instancia del servicio, pero tenemos varios de ellos, y en cualquier momento puede ejecutar tantos como necesite. Cuando los servicios bloquean la velocidad de almacenamiento, será posible escalar la tarántula horizontalmente (fragmento basado en la identificación del cliente, por ejemplo).
Servicio de inteligencia
El lector atento probablemente ya hace la pregunta: ¿cuál es la "inteligencia" de este servicio, que se menciona en el título. Ya mencioné esto de pasada, pero ahora te contaré más.
Una de las tareas principales del servicio era reducir el tiempo que lleva emitir sus productos a los usuarios (listas de cuentas, tarjetas, depósitos, préstamos, paquetes de servicios, etc.) mientras se reduce la carga en el back-end (reduciendo el número de solicitudes en Oracle grande y pesado) debido al almacenamiento en caché en la tarántula.
Y lo hizo bastante bien. La lógica para calentar el caché del cliente es la siguiente:
- el usuario inicia la aplicación móvil;
- Se envía una solicitud AppStart que contiene la ID del dispositivo al servidor frontal;
- el servidor frontal envía un mensaje con esta ID al servicio de caché;
- el servicio busca en la tabla del dispositivo la ID del cliente para este dispositivo;
- si no está allí, no pasa nada (la respuesta ni siquiera se envía, el servidor no la espera);
- Si se encuentra la ID del cliente, el trabajador crea un conjunto de mensajes para recibir listas de productos de usuario que el agente procesa inmediatamente y se distribuyen a los trabajadores en modo normal;
- cada trabajador envía una solicitud de un cierto tipo de datos al usuario, colocando el estado de "actualización" en la base de datos (este estado protege al backend de repetir las mismas solicitudes si provienen de otras instancias del servicio);
- después de recibir los datos, se registran en la tarántula;
- el usuario inicia sesión en el sistema, y la aplicación envía solicitudes para recibir sus productos, y el servidor envía estas solicitudes en forma de mensajes al servicio de caché;
- Si los datos del usuario ya han sido recibidos, simplemente los enviamos desde el caché;
- si los datos están en proceso de ser recibidos (estado de "actualización"), se inicia un ciclo de espera de datos dentro del trabajador (es igual al tiempo de espera de solicitud para el back-end);
- tan pronto como se reciben los datos (es decir, el estado de este registro (tupla) en la tabla pasa a "inactivo", el servicio se lo dará al cliente;
- Si los datos no se reciben dentro de un cierto intervalo de tiempo, se devolverá un error al cliente.
Por lo tanto, en la práctica, pudimos reducir el tiempo promedio de recepción de productos para el servidor frontal de 200 ms a 20 ms, es decir, en aproximadamente 10 veces, y el número de solicitudes al backend en aproximadamente 4 veces.
Los problemas
El servicio de caché ha estado trabajando en la batalla durante aproximadamente dos años y actualmente satisface nuestras necesidades.
Por supuesto, todavía hay problemas sin resolver, a veces ocurren problemas. Los servicios de Java en la batalla aún no han caído. La tarántula cayó un par de veces en SIGSEGV, pero era una versión antigua, y después de la actualización no volvió a suceder. Durante la prueba de esfuerzo, la replicación se cae, una tubería rota ocurrió en el maestro, después de lo cual el esclavo se cayó, aunque el maestro continuó trabajando. Se decidió reiniciando el esclavo.
Una vez que hubo algún tipo de accidente en el centro de datos, resultó que el sistema operativo (CentOS 7) dejó de ver discos duros. El sistema de archivos entró en modo de solo lectura. Lo más sorprendente fue que los servicios continuaron funcionando, ya que guardamos todos los datos en la memoria. La tarántula no pudo escribir archivos .xlog, nadie registró nada, pero de alguna manera todo funcionó. Pero el intento de reiniciar no tuvo éxito: nadie pudo comenzar.
Hay un gran problema sin resolver, y me gustaría escuchar la opinión de la comunidad sobre este tema. Cuando la tarántula maestra se bloquea, los servicios de Java pueden cambiar a esclavo, que continúa funcionando como maestro. Sin embargo, esto solo sucede si el maestro falla y no puede funcionar.
Supongamos que tenemos 3 instancias de un servicio que funcionan con datos en una tarántula maestra. Los servicios en sí no se caen, la replicación de la base de datos continúa, todo está bien. Pero de repente tenemos una red que se desmorona entre el nodo 1 y el nodo 4, donde funciona el asistente. El servicio 1 después de varios intentos fallidos decide cambiar a la base de datos de respaldo y comienza a enviar solicitudes allí.
Inmediatamente después de esto, el esclavo de tarántula comienza a aceptar solicitudes de modificación de datos, como resultado de lo cual la replicación del maestro se desmorona, y obtenemos datos inconsistentes. Al mismo tiempo, los servicios 2 y 3 funcionan perfectamente con el maestro, y el servicio 1 se comunica bien con el antiguo esclavo. Está claro que en este caso comenzamos a perder sesiones de clientes y cualquier otro dato, aunque todo funciona desde el punto de vista técnico. Todavía no hemos resuelto un problema tan potencial. Afortunadamente, esto no ha sucedido en 2 años, pero la situación es bastante real. Ahora cada servicio conoce el número de la tienda a la que va, y tenemos una alerta para esta métrica, que funcionará al cambiar de maestro a esclavo. Y tienes que reparar todo con tus manos. ¿Cómo se resuelven estos problemas?
Planes
Planeamos trabajar en el problema descrito anteriormente, limitando el número de trabajadores simultáneamente ocupados con un tipo de solicitud, seguro (sin perder las solicitudes actuales) para detener el servicio y pulir aún más.
Conclusión
Eso, quizás, es todo, aunque analicé el tema de manera bastante superficial, pero la lógica general del trabajo debería ser clara. Por lo tanto, si es posible, estoy listo para responder en los comentarios. Describí brevemente cómo un pequeño subsistema auxiliar de los servidores frontales del banco funciona para atender a clientes móviles.
Si el tema es de interés para la comunidad, puedo informarle sobre varias de nuestras soluciones que contribuyen a mejorar la calidad del servicio al cliente para el banco.