Cuando estaba jodiendo en el trabajo, accidentalmente construí un marco RPC con las manos vacías y lo grabé rápidamente

origen

Recientemente, compartí la mano de RPC en la empresa, así que haré un resumen.

Conceptos

¿Qué es RPC?

RPC, llamada llamada de procedimiento remoto, se utiliza para resolver el problema de las llamadas entre servicios en un sistema distribuido. En términos sencillos, los desarrolladores pueden llamar a servicios remotos como si llamaran a métodos locales. Por tanto, el papel de RPC se refleja principalmente en estos dos aspectos:

  • La diferencia entre proteger las llamadas remotas y las llamadas locales nos hace sentir como métodos de llamada en el proyecto;
  • Ocultar la complejidad de la comunicación de red subyacente nos permite centrarnos más en la lógica empresarial.

Arquitectura básica del marco RPC

Hablemos de la arquitectura básica del marco RPC a través de una imagen.

El marco RPC consta de los tres componentes más importantes, a saber, el cliente, el servidor y el registro. En un proceso de llamada RPC, estos tres componentes interactúan así:

  • Una vez que se inicia el servidor, publicará la lista de servicios que proporciona en el registro y el cliente se suscribe al registro para la dirección del servicio;
  • El cliente llamará al servidor a través del módulo proxy local Proxy, y el módulo Proxy recibe y es responsable de convertir datos como métodos y parámetros en flujos de bytes de red;
  • El cliente selecciona una de las direcciones de servicio de la lista de servicios y envía los datos al servidor a través de la red;
  • Después de recibir los datos, el servidor los decodifica para obtener la información solicitada;
  • El servidor llama al servicio correspondiente de acuerdo con la información de solicitud decodificada y luego devuelve el resultado de la llamada al cliente.

Proceso de comunicación del marco RPC y roles involucrados

En la imagen de arriba, puede ver que el marco RPC generalmente tiene estos componentes: gobierno de servicios (descubrimiento de registros), equilibrio de carga, tolerancia a fallas, serialización/deserialización, códec, transmisión de red, grupo de subprocesos, proxy dinámico y otras funciones, por supuesto. algunos El marco RPC también tendrá funciones como agrupación de conexiones, registro y seguridad.

El proceso de llamada específico

  1. El consumidor del servicio (cliente) llama al servicio en el modo de llamada local
  2. Después de que el stub del cliente recibe la llamada, es responsable de encapsular métodos, parámetros, etc. en un cuerpo de mensaje que se puede transmitir a través de la red.
  3. El stub del cliente codifica el mensaje y lo envía al servidor
  4. El stub del servidor decodifica el mensaje después de recibir el mensaje
  5. El stub del servidor llama al servicio local de acuerdo con el resultado de la decodificación
  6. El servicio local se ejecuta y devuelve el resultado al código auxiliar del servidor.
  7. El stub del servidor codifica el resultado de importación devuelto y lo envía al consumidor
  8. El stub del cliente recibe el mensaje y lo decodifica
  9. El consumidor del servicio (cliente) obtiene el resultado

Protocolo de mensajes RPC

Durante el proceso de llamada de RPC, los parámetros deben clasificarse en mensajes para enviar, el receptor debe desclasificar los mensajes como parámetros y los resultados del procesamiento del proceso también deben clasificarse y desclasificarse. Las partes del mensaje y la representación del mensaje constituyen el protocolo del mensaje.
El protocolo de mensajes utilizado en el proceso de llamada RPC se denomina protocolo de mensajes RPC.

combate real

A partir de los conceptos anteriores, sabemos en qué partes consiste un marco RPC, por lo que debemos considerar estas partes al diseñar un marco RPC. A partir de la definición de RPC, podemos saber que el marco RPC necesita proteger los detalles subyacentes y hacer que los usuarios sientan que llamar a un servicio remoto es tan simple como llamar a un método local, por lo que se deben considerar estos problemas:

  • ¿Cómo pueden los usuarios usar nuestro marco RPC con la menor configuración posible?
  • Cómo dar de alta el servicio a ZK (aquí el centro de registro elige ZK) y desconocimiento del usuario
  • Cómo llamar de forma transparente (hasta donde el usuario no pueda percibir) al proveedor del servicio de llamadas
  • Cómo permitir que varios proveedores de servicios logren un equilibrio de carga dinámico
  • ¿Cómo permite el marco que los usuarios personalicen los componentes de la extensión (como ampliar las estrategias personalizadas de equilibrio de carga)?
  • Cómo definir el protocolo de mensajes y el códec
  • ...y muchos más

Los problemas anteriores se resolverán en el diseño de este marco RPC.

Selección técnica

  • Los centros de registro maduros actuales del centro de registro incluyen Zookeeper, Nacos, Consul y Eureka. Aquí, ZK se utiliza como centro de registro, y no hay función de cambio y centros de registro definidos por el usuario.
  • Marco de comunicación de IO Esta implementación utiliza Netty como el marco de comunicación subyacente, porque Netty es un marco de IO sin bloqueo (NIO) basado en eventos de alto rendimiento. No proporciona otras implementaciones y no admite marcos de comunicación definidos por el usuario.
  • Protocolo de mensajes Esta implementación utiliza un protocolo de mensajes personalizado, que se explicará más adelante.

Estructura general del proyecto

A partir de esta estructura, se puede saber que los módulos que comienzan con rpc son los módulos del marco rpc y el contenido del marco RPC de este proyecto, mientras que el consumidor es el consumidor del servicio, el proveedor es el proveedor del servicio y el proveedor -api es la API de servicio expuesta.

dependencia general

Introducción a la implementación del proyecto

Para lograr la menor configuración posible cuando los usuarios usan nuestro marco RPC, el marco RPC está diseñado como un iniciador, y los usuarios solo necesitan confiar en este iniciador, que básicamente está bien.

¿Por qué diseñar dos arrancadores (cliente-iniciador/servidor-iniciador)?

Esto es para reflejar mejor el concepto de cliente y servidor, los consumidores confían en el cliente, los proveedores de servicios confían en el servidor y minimizan las dependencias.

¿Por qué diseñar como titular?

Según el mecanismo de ensamblaje automático Spring Boot, se cargará el archivo spring.factories en el iniciador y se configurará el siguiente código en el archivo. Aquí, la clase de configuración de nuestro iniciador tendrá efecto y se agregarán algunos beans necesarios. configurado en la clase de configuración.

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.rrtv.rpc.client.config.RpcClientAutoConfiguration
复制代码

Servicios de publicación y consumo

  • Para los
    proveedores de servicios de publicación, se debe agregar la anotación @RpcService al servicio expuesto. Esta anotación personalizada se basa en @service y es una anotación compuesta con la función de la anotación @service. La interfaz del servicio y la versión del servicio se especifican en @ Anotación RpcService. , publique el servicio en ZK, se registrará de acuerdo con estos dos metadatos
    • Principio del servicio de publicación:
      después de que se inicia el proveedor de servicios, de acuerdo con el mecanismo de ensamblaje automático Spring Boot, la clase de configuración de servidor de arranque entra en vigor.En un posprocesador de bean (RpcServerProvider), se obtiene el bean decorado con la anotación @RpcService. y la anotación Los metadatos están registrados con ZK.
  • Para los servicios
    de consumo, los servicios de consumo deben identificarse mediante una anotación @RpcAutowired personalizada, que es una anotación compuesta basada en @Autowired.
    • Principio del servicio de consumo
      Para que el cliente llame al proveedor de servicios de manera imperceptible, se debe usar un proxy dinámico. Como se muestra arriba, HelloWordService no tiene una clase de implementación, y se le debe asignar una clase de proxy e iniciar una llamada de solicitud en el clase de apoderados. Basado en el ensamblado automático Spring Boot, el consumidor del servicio se inicia y el posprocesador de bean RpcClientProcessor comienza a funcionar. Principalmente atraviesa todos los beans para determinar si las propiedades de cada bean se modifican mediante la anotación @RpcAutowired y, de ser así, realiza la propiedad dinámica Asigne la clase de proxy, que llamará al método de invocación de la clase de proxy cuando se vuelva a llamar.
    • El método de invocación de clase de proxy obtiene metadatos del lado del servidor a través del descubrimiento de servicios, encapsula solicitudes e inicia llamadas a través de netty.

Centro de registro

El centro de registro de este proyecto utiliza ZK, porque el centro de registro lo utilizan tanto los consumidores como los proveedores de servicios. Así que pon ZK en el módulo rpc-core.

El módulo rpc-core se muestra en la figura anterior y todas las funciones principales se encuentran en este módulo. Los servicios se registran en el paquete de registro.

Interfaz de registro de servicios, la implementación específica se implementa utilizando ZK.

estrategia de equilibrio de carga

El equilibrio de carga se define en rpc-core, que actualmente es compatible con round-robin (FullRoundBalance) y aleatorio (RandomBalance), y utiliza una estrategia aleatoria de forma predeterminada. Especificado por rpc-client-spring-boot-starter.

Cuando se descubre a través del servicio ZK, se encontrarán varias instancias y luego se obtendrá una de las instancias a través de la estrategia de equilibrio de carga.

Puede configurar rpc.client.balance=fullRoundBalance en el consumidor para reemplazarlo, o puede personalizar la estrategia de equilibrio de carga implementando la interfaz LoadBalance y agregando la clase creada al contenedor IOC. Dado que configuramos @ConditionalOnMissingBean, los beans definidos por el usuario se cargarán primero.

Protocolo de mensaje personalizado, códec

El llamado acuerdo es que las dos partes negocian las reglas por adelantado y el servidor sabe cómo analizar los datos enviados.

  • protocolo de mensaje personalizado
    • Número mágico: El número mágico es un código secreto negociado por ambas partes de la comunicación, generalmente representado por un número fijo de bytes. La función del número mágico es evitar que alguien envíe datos al puerto del servidor de forma indiscriminada. Por ejemplo, el número mágico 0xCAFEBABE se almacena al principio del archivo de Clase Java.Al cargar el archivo de Clase, primero se verificará la corrección del número mágico.
    • Número de versión del protocolo: con el cambio de los requisitos comerciales, es posible que el protocolo deba modificar la estructura o los campos, y los métodos de análisis correspondientes a las diferentes versiones del protocolo también son diferentes.
    • Algoritmo de serialización: el campo del algoritmo de serialización indica qué método debe usar el remitente de datos para convertir el objeto solicitado en binario y cómo convertir el binario en un objeto, como JSON, Hessian y la propia serialización de Java.
    • Tipo de paquete: en diferentes escenarios comerciales, puede haber diferentes tipos de paquetes. Hay solicitudes, respuestas, latidos y otros tipos de mensajes en el marco RPC.
    • Estado: El campo de estado se utiliza para identificar si la solicitud es normal (ÉXITO, FALLO).
    • ID de mensaje: El ID único de la solicitud, a través del cual se asocia la respuesta, y el seguimiento de enlaces también se puede hacer a través de la ID de la solicitud.
    • Longitud de datos: Indique la longitud de los datos, que se utiliza para juzgar si se trata de un paquete de datos completo
    • Contenido de los datos: contenido del cuerpo de la solicitud
  • Codec
    Codec se implementa en el módulo rpc-core, bajo el paquete com.rrtv.rpc.core.codec.
  • El codificador personalizado implementa la codificación de mensajes al heredar la clase MessageToByteEncoder<MessageProtocol<T>> de netty.
  • El decodificador personalizado implementa la decodificación de mensajes al heredar la clase ByteToMessageDecoder de netty.

Al decodificar, debe prestar atención a los problemas de bloqueo y desempaquetado de TCP

¿Qué es TCP pegado y desempaquetado?

El protocolo de transporte TCP está orientado a flujos y no tiene límites de paquetes, lo que significa que los mensajes no tienen límites. Cuando el cliente envía datos al servidor, un paquete completo se puede dividir en varios paquetes pequeños para la transmisión, o varios paquetes se pueden combinar en un paquete grande para la transmisión. De ahí el desembalaje y pegado.

En el proceso de comunicación de red, el tamaño de los paquetes de datos que se pueden enviar cada vez está limitado por varios factores, como el tamaño de la unidad de transmisión MTU, la ventana deslizante, etc.
Entonces, si el tamaño de los datos del paquete de red transmitidos en un momento excede el tamaño de la unidad de transmisión, entonces nuestros datos pueden dividirse en múltiples paquetes de datos y enviarse. Si los datos del paquete de red de cada solicitud son muy pequeños, como un total de 10 000 solicitudes, TCP no enviará 10 000 veces por separado. El algoritmo Nagle (envío por lotes, utilizado principalmente para resolver el problema de congestión de la red causado por el envío frecuente de pequeños paquetes de datos) adoptado por TCP está optimizado para esto.

Entonces, la transmisión de red aparecerá así:

  1. El servidor leyó dos paquetes de datos completos A y B, y no hubo ningún problema de desempaquetado/pegado;
  2. El servidor recibe los paquetes de datos que A y B están pegados, y el servidor necesita analizar A y B;
  3. El servidor recibe la parte A y B completa del paquete de datos B-1, el servidor necesita analizar el A completo y esperar para leer el paquete de datos B completo;
  4. El servidor recibe una parte del paquete de datos A-1 de A, y necesita esperar a que se reciba el paquete de datos A completo en este momento;
  5. El paquete de datos A es grande y el servidor necesita varias veces para recibir el paquete de datos A.

Cómo resolver el problema de TCP pegado y desempaquetado

El medio fundamental para resolver el problema: encontrar el límite del mensaje:

  • Longitud fija del mensaje
    Cada datagrama requiere una longitud fija. Cuando el receptor lee acumulativamente los mensajes de longitud fija, considera que se ha obtenido un mensaje completo. Cuando los datos del remitente son menores que la longitud fija, deben llenarse con espacios en blanco.
    El método de longitud fija del mensaje es muy simple de usar, pero las desventajas también son muy obvias. Es imposible establecer muy bien el valor de la longitud fija. Si la longitud es demasiado grande, provocará una pérdida de bytes, y si la longitud es demasiado pequeña, afectará la transmisión del mensaje. Por lo tanto, en general, el mensaje tiene una longitud fija. La ley no se adoptará.
  • Separador específico
    Agregue un separador específico al final de cada mensaje enviado, y el receptor puede dividir el mensaje de acuerdo con el separador especial. Debe evitarse que la elección del delimitador sea el mismo que los caracteres del cuerpo del mensaje para evitar conflictos. De lo contrario, puede ocurrir una división incorrecta del mensaje. La práctica recomendada es codificar el mensaje, como la codificación base64, y luego elegir un carácter distinto de los 64 caracteres codificados como delimitador específico.
  • Longitud del mensaje + contenido
    del mensaje Longitud del mensaje + contenido del mensaje es el protocolo más utilizado en el desarrollo de proyectos, el receptor lee el contenido del mensaje de acuerdo con la longitud del mensaje.

Este proyecto utiliza el método de "longitud del mensaje + contenido del mensaje" para resolver el problema de bloqueo y desempaquetado de TCP. Por lo tanto, al decodificar, es necesario juzgar si los datos son lo suficientemente largos para leer. Si no es suficiente, significa que los datos no están listos. Continúe leyendo los datos y decodificándolos. Aquí, un paquete de datos completo puede obtenerse de esta manera.

Serializar y deserializar

Serialización y deserialización El paquete com.rrtv.rpc.core.serialization del módulo rpc-core proporciona serialización HessianSerialization y JsonSerialization.
La serialización predeterminada es HessianSerialization. El usuario no puede personalizar.

Rendimiento de serialización:

  • espacialmente

  • a tiempo

Transmisión de red, usando netty

El código de red es fijo. Vale la pena señalar que el orden de los controladores no se puede confundir. Tomando el servidor como ejemplo, la codificación es una operación de salida (se puede colocar después de la entrada), y la decodificación y la recepción de la respuesta son ambas. operaciones entrantes Frontal.

Método de llamada RPC del cliente

Los marcos RPC maduros generalmente brindan cuatro métodos de llamada, a saber, sincronización síncrona, futuro asíncrono, devolución de llamada y devolución unidireccional.

  • Sincronización de llamadas sincrónicas. Después de que el subproceso del cliente inicie la llamada RPC, el subproceso actual se bloqueará hasta que el servidor devuelva el resultado o maneje la excepción de tiempo de espera.
  • Llamada asíncrona futura
    Después de que el cliente inicie la llamada, no se bloqueará ni esperará, sino que obtendrá el objeto Futuro devuelto por el marco RPC. El resultado de la llamada será almacenado en caché por el servidor, y el cliente decidirá cuándo obtener el resultado devuelto. en el futuro. Cuando el cliente obtiene activamente el resultado, el proceso se bloquea y espera
  • Cuando el cliente Callback Callback inicia una llamada, el objeto Callback se pasa al marco RPC y regresa directamente sin esperar el resultado devuelto de forma sincrónica. Cuando se obtiene el resultado de la respuesta del servidor o la excepción de tiempo de espera, se ejecuta la devolución de llamada de devolución de llamada registrada por el usuario.
  • El cliente de llamadas unidireccionales regresa directamente después de iniciar la solicitud, ignorando el resultado de la devolución

El primero se usa aquí: llamadas síncronas del lado del cliente, otras no están implementadas. La lógica está en RpcFuture, usando CountDownLatch para implementar la espera de bloqueo (espera de tiempo de espera)

Arquitectura general y proceso

El proceso se divide en tres partes: proceso de inicio del proveedor de servicios, inicio del consumidor de servicios y proceso de llamada.

  • El proveedor de servicios inicia el proveedor de servicios confiará en rpc-server-spring-boot-starterProviderApplication para iniciar, de acuerdo con el mecanismo de ensamblaje automático springboot, la configuración automática de RpcServerAutoConfiguration entra en vigor.RpcServerProvider es un posprocesador de beans que publicará servicios y registrará servicios metadatos a El método RpcServerProvider.run en ZK iniciará un servicio de red
  • El consumidor del servicio inicia el consumidor del servicio el consumidor confiará en rpc-client-spring-boot-starterConsumerApplication para iniciar, de acuerdo con el mecanismo de ensamblaje automático springboot, la configuración automática de RpcClientAutoConfiguration entra en vigor.El descubrimiento del servicio, el equilibrio de carga, el proxy y otros beans son agregado al posprocesador del contenedor IOC RpcClientProcessor escaneará beans y asignará dinámicamente propiedades modificadas por @RpcAutowired a objetos proxy
  • El consumidor del servicio del proceso de llamada inicia una solicitud http://localhost:9090/hello/world?name=hello El consumidor del servicio llama al método helloWordService.sayHello() y se delegará para ejecutar el método ClientStubInvocationHandler.invoke(). El consumidor del servicio atiende a través de ZK Se encuentra que se obtienen los metadatos del servicio y no se puede encontrar el error 404. El protocolo personalizado del consumidor del servicio encapsula el encabezado de la solicitud y el cuerpo de la solicitud. El consumidor del servicio usa el codificador personalizado RpcEncoder para codificar el mensaje El consumidor del servicio obtiene la ip y el proveedor del servicio a través del puerto de descubrimiento del servicio, llama al consumidor del servicio a través de la capa de transporte de la red Netty e ingresa el resultado de devolución (tiempo de espera) a través de RpcFuture y espera a que el proveedor del servicio reciba la solicitud del consumidor. El proveedor de servicios decodifica el mensaje a través del decodificador personalizado RpcDecoder.Procesa en RpcRequestHandler, ejecuta el método local del servidor a través de una llamada de reflexión y obtiene el resultado.El resultado que ejecutará el proveedor de servicios codificará el mensaje a través del codificador RpcEncoder. (Debido a que el protocolo de la solicitud y la respuesta es el mismo, el codificador y el decodificador pueden usar un conjunto.) El consumidor del servicio decodifica el mensaje a través del decodificador personalizado RpcDecoder. RpcResponseHandler, y establece el consumidor del servicio de resultado de respuesta de RpcFuture obtiene el resultado

El proceso anterior se puede combinar con el análisis de código, que se dará más adelante en el código.

Construcción del entorno

  • Sistema Operativo: Windows
  • Herramientas de desarrollo integradas: IntelliJ IDEA
  • Pila de tecnología del proyecto: SpringBoot 2.5.2 + JDK 1.8 + Netty 4.1.42.Final
  • Herramienta de gestión de dependencias de proyectos: Maven 4.0.0
  • Centro de registro: Zookeeper 3.7.0

prueba de proyecto

  • Inicie el servidor Zookeeper: bin/zkServer.cmd
  • Inicie el módulo de proveedor ProviderApplication
  • Inicie el módulo de consumidor ConsumerApplication
  • Prueba: ingrese http://localhost:9090/hello/world?name=hello en el navegador, devuelva con éxito Hola: hola, llamada rpc con éxito

dirección del código del proyecto

gitee.com/listen_w/rp…

Supongo que te gusta

Origin blog.csdn.net/wdjnb/article/details/124449335
Recomendado
Clasificación