[Serie de tecnología de middleware de Alibaba] "Tema especial de tecnología RocketMQ" Análisis de todo el proceso de envío de mensajes RocketMQ y el principio de ubicación

RocketMQ debería ser un MQ relativamente popular en China en la actualidad. Actualmente lo estoy usando e investigando en los proyectos de la compañía. Aprovechando esta oportunidad, analizaré el proceso desde el que RocketMQ envía un mensaje hasta el almacenamiento de un mensaje. Esto ayudará a todos a analizar y estudiar. en el futuro. Investigar problemas relacionados con RocketMQ será de alguna ayuda.

El alcance técnico general del análisis se envía al almacenamiento. El objetivo principal de este artículo es principalmente comprender un mensaje y analizar el código que se envía y almacena, con respecto a la optimización y diseño del sistema de archivos MQ.

Un fragmento de código de envío del ejemplo del código fuente oficial:

DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
producer.start();
Message msg = new Message("TopicTest", "TagA", "OrderID188", "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
producer.shutdown();

Mire directamente el método de envío, el método de envío establecerá un tiempo de espera predeterminado: 3 segundos. El modo SYNC se utiliza de forma predeterminada y existen modos Async y OneWay. Es necesario manejar las excepciones del lado del cliente, las excepciones de la red, las excepciones del lado del intermediario y las excepciones de interrupción de subprocesos en la firma del método.

El método sendDefaultImpl de DefaultMQProducerImpl es la lógica principal de envío.

En el código hay un lugar que se puede mencionar: con respecto a la estrategia para actualizar el tiempo de falla, RocketMQ tiene una clase MQFaultStrategy, que se usa para manejar errores de MQ y luego degradar el servicio del servidor MQ.

Si se envía un mensaje dentro de los 550 ms, no es necesario degradarlo. Si supera los 550 ms, se realizará una degradación tolerante a fallas (disyuntor) durante 30 segundos, y así sucesivamente.

Mire la implementación de SendKernelImpl de DefaultMQProducerImpl para enviar al kernel.

Primero busque la dirección del corredor. Intenta comprimir mensajes de más de 4 M (los mensajes masivos no se comprimen) y luego ejecuta varios enlaces.

  • Objeto de solicitud (datos de almacenamiento)
  • Objeto de contexto de contexto (almacena el contexto de llamada).

Aquí se establecerá una hora de generación de mensaje, es decir, bornTimestamp, que se podrá ver más tarde cuando se utilice el seguimiento de mensajes.

De forma predeterminada: si se utiliza el modo SYNC, se llama a MQClientAPIImpl para enviar mensajes. Esta capa todavía está en el módulo Cliente. En esta capa, se establecerán detalles más detallados del mensaje y se construirá el objeto de comando. Finalmente, llame a invokeSync de remotingClient para enviar el mensaje.

La capa sendMessage de MQClientAPIImpl establecerá un CmdCode para el objeto de comando, llamado SEND_MESSAGE. Esto es un contrato con el Broker. El Broker implementará diferentes estrategias basadas en este Código.

Netty utilizará el Handler para procesar los datos salientes y los datos devueltos. Veamos qué Handlers tiene Netty en el lado del cliente.

Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class)
            .option(ChannelOption.TCP_NODELAY, true)
            .option(ChannelOption.SO_KEEPALIVE, false)
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, nettyClientConfig.getConnectTimeoutMillis())
            .option(ChannelOption.SO_SNDBUF, nettyClientConfig.getClientSocketSndBufSize())
            .option(ChannelOption.SO_RCVBUF, nettyClientConfig.getClientSocketRcvBufSize())
            .handler(new ChannelInitializer() {
    
    

                public void initChannel(SocketChannel ch) throws Exception {
    
    
                    ChannelPipeline pipeline = ch.pipeline();
                    if (nettyClientConfig.isUseTLS()) {
    
    
                        if (null != sslContext) {
    
    
                            pipeline.addFirst(defaultEventExecutorGroup, "sslHandler", sslContext.newHandler(ch.alloc()));
                            log.info("Prepend SSL handler");
                        } else {
    
    
                            log.warn("Connections are insecure as SSLContext is null!");
                        }
                    }
                    pipeline.addLast(
                        defaultEventExecutorGroup,
                        new NettyEncoder(),
                        new NettyDecoder(),
                        new IdleStateHandler(0, 0, nettyClientConfig.getClientChannelMaxIdleTimeSeconds()),
                        new NettyConnectManageHandler(),
                        new NettyClientHandler());
                }
            });

Se utilizan un codificador, un decodificador, un controlador inactivo, un administrador de conexiones y un ClientHandler.

XXCoder serializa y deserializa objetos Cmd. El tiempo máximo de inactividad para lectura y escritura utilizado aquí es de 120 s. Si excede esto, se activará el evento de inactividad.

  • RocketMQ cerrará la conexión del canal. El administrador de conexiones maneja eventos inactivos.
  • El administrador de conexiones maneja eventos inactivos, cerrados, conectados, de excepción y otros, utilizando el modo de escucha, y diferentes oyentes manejan diferentes eventos. Además, aquí podemos aprender de EventBus y se pueden configurar varios oyentes para cada evento.

Después de observar el diseño de Netty en RocketMQ, es simple observar el procesamiento del valor de retorno: NettyClientHandler procesará el valor de retorno de Netty Server en el método channelRead0. Correspondiente a RMQ, es el método ProcessMessageReceived. El método es muy sencillo:

public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception {
    
    
        final RemotingCommand cmd = msg;
        if (cmd != null) {
    
    
            switch (cmd.getType()) {
    
    
                case REQUEST_COMMAND:
                    processRequestCommand(ctx, cmd);
                    break;
                case RESPONSE_COMMAND:
                    processResponseCommand(ctx, cmd);
                    break;
                default:
                    break;
            }
        }
    }

De hecho, este es un método de plantilla, un algoritmo fijo, implementado por subclases, dividido en implementación de Solicitud e implementación de Respuesta. Veamos la implementación de Response.

public void processResponseCommand(ChannelHandlerContext ctx, RemotingCommand cmd) {
    
    
        final int opaque = cmd.getOpaque();

        final ResponseFuture responseFuture = responseTable.get(opaque);
        if (responseFuture != null) {
    
    
            responseFuture.setResponseCommand(cmd);
            responseTable.remove(opaque);
            if (responseFuture.getInvokeCallback() != null) {
    
    
                executeInvokeCallback(responseFuture);
            } else {
    
    
                responseFuture.putResponse(cmd);
                responseFuture.release();
            }
        } else {
    
    
            log.warn("receive response, but not matched any request, " + RemotingHelper.parseChannelRemoteAddr(ctx.channel()));
            log.warn(cmd.toString());
        }
    }

Encuentre la característica a través del ID de solicitud del objeto cmd, ejecute ResponseFuture.putResponse, establezca el valor de retorno y active el hilo de envío que está bloqueado y esperando.

También hay una llamada de lanzamiento aquí, que está relacionada con el envío asincrónico. El número máximo predeterminado de solicitudes asincrónicas al mismo tiempo es 65535. Los detalles no se ampliarán.

En este punto, active el hilo de envío bloqueado, devuelva los datos y el envío a nivel de cliente finalizará.

Mirando el código fuente, vemos un Código SEND_MESSAGE, que es un código acordado entre el Cliente y el Servidor Broker. Veamos dónde se usa este código.

En la clase BrokerController del módulo broker, hay un método RegisterProcessor que vincula el código SEND_MESSAGE a un objeto SendMessageProcessor.

NettyRemotingServer es una clase que maneja solicitudes. ServerBootstrap agregará un procesador NettyServerHandler a la canalización. El método channelRead0 de este procesador llamará al método ProcessMessageReceived de la clase principal de NettyRemotingServer.

De la tabla de procesador, obtenga el Procesador correspondiente según el Código Cmd, que es SEND_MESSAGE

Parte es el objeto que procesa datos y parte es el grupo de subprocesos correspondiente a este objeto. Se utiliza para la lógica de procesamiento asincrónico para evitar el bloqueo del subproceso Netty IO.

doBeforeRpcHooks(RemotingHelper.parseChannelRemoteAddr(ctx.channel()), cmd);
final RemotingCommand response = pair.getObject1().processRequest(ctx, cmd);
doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(ctx.channel()), cmd, response);

Algunos ganchos se ejecutan antes y después, como ACL.

RocketMQ tendrá una clase BrokerController que registrará la relación vinculante entre el Código y el Procesador. BrokerController también los vinculará y los registrará en Netty Server. Cuando Netty Server recibe el objeto Cmd del Socket, se puede encontrar de acuerdo con el Código del Objeto cmd.Corresponde a la clase Procesador para procesar datos.

El medio es procesar la solicitud de Solicitud. Hay muchas implementaciones de este método ProcessRequest. SendMessage de SendMessageProcessor es la lógica principal para procesar mensajes.

Motor de almacenamiento de mensajes, aquí analizamos la implementación putMessage de DefaultMessageStore.

putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner);

Dado que RocketMQ escribe datos en PageCache, si la escritura es lenta, significa que PageCache está ocupado. El estándar de ocupación aquí es que si el tiempo de bloqueo del archivo excede 1 segundo, significa que está ocupado.

if (this.isOSPageCacheBusy()) {
    
    
    return new PutMessageResult(PutMessageStatus.OS_PAGECACHE_BUSY, null);
}

Finalmente, llame a PutMessageResult result = this.commitLog.putMessage(msg) para escribir datos. Si tarda más de 500 milisegundos, se imprimirá un registro. De esta manera, cuando solucionamos problemas, podemos consultar los registros de storeStats.

result = mappedFile.appendMessage(msg, this.appendMessageCallback)

Después de escribir, libere el bloqueo, si tarda más de 500 milisegundos, imprima el registro de tiempo de costo.

Para lidiar con el cepillado de disco y la sincronización de esclavos, aquí está la estrategia de cepillado y sincronización de disco, ya sea SYNC o ASYNC. Después de mi prueba, la diferencia de rendimiento entre el cepillado sincrónico y asincrónico es 10 veces mayor.

En cuanto a la sincronización de datos del esclavo, si se utiliza el modo SYNC, el tps más alto es poco más de 2000. ¿Por qué? En la intranet, dos máquinas tardan 0,2 milisegundos en hacer ping entre sí, hasta 5.000 veces por segundo. Junto con la lógica de procesamiento, 2.000 han llegado a la cima y la red se ha convertido en un cuello de botella.

Veamos la implementación del método mappedFile.appendMessage. Seguimiento completo, hay una lógica clave en appendMessagesInner:

int currentPos = this.wrotePosition.get();
if (currentPos < this.fileSize) {
    
    
    ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice();
    byteBuffer.position(currentPos);
    AppendMessageResult result = null;
    if (messageExt instanceof MessageExtBrokerInner) {
    
    

        result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBrokerInner) messageExt);
    } else if (messageExt instanceof MessageExtBatch) {
    
    
        result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBatch) messageExt);
    } else {
    
    
        return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
    }
    this.wrotePosition.addAndGet(result.getWroteBytes());
    this.storeTimestamp = result.getStoreTimestamp();
    return result;
}

En el código, mappedFile se usa para escribir datos desde el búfer MMap mapeado desde Linux. Veamos el método doAppend.

  • Si está en modo SYNC, cuando se ejecuta el método handleDiskFlush de CommitLog, el disco se vaciará inmediatamente y esperará el resultado del vaciado.
  • Si está en modo ASYNC, cuando se ejecuta el método handleDiskFlush de CommitLog, se notificará al hilo asíncrono que vacíe el disco, pero no se esperará el resultado.

Si no hay datos nuevos, la estrategia de vaciado del disco se ejecutará cada 500 ms.

Hablemos brevemente sobre el flasheo de disco asíncrono:

De forma predeterminada, se actualizan 4 páginas. En Linux, una página tiene 4 kb de datos y 4 páginas son 16 kb.

Si los datos escritos menos los datos borrados y los datos restantes son mayores o iguales a 4 páginas, vacíe el disco y ejecute mappedByteBuffer.force() o fileChannel.force(false);

Compartir recursos

La transferencia de la imagen del enlace externo falló. El sitio de origen puede tener un mecanismo anti-leeching. Se recomienda guardar la imagen y cargarla directamente.
Para obtener los recursos anteriores, visite el proyecto de código abierto y haga clic para saltar.

Supongo que te gusta

Origin blog.csdn.net/star20100906/article/details/132595508
Recomendado
Clasificación