08-Modelo IO para optimización de la comunicación de red: ¿cómo resolver el cuello de botella de IO en condiciones de alta concurrencia?

Cuando se trata de E/S de Java, creo que debes estar familiarizado con él. Puede usar operaciones de E/S para leer y escribir archivos, o puede usarlas para implementar la transmisión de información de Socket... Estas son las operaciones relacionadas con E/S que encontramos con más frecuencia en el sistema.

Todos sabemos que la velocidad de E/S es más lenta que la velocidad de la memoria, especialmente en la era actual de big data, el problema de rendimiento de E/S es particularmente prominente y la lectura y escritura de E/S se ha convertido en un sistema en En muchos escenarios de aplicación, los cuellos de botella en el rendimiento no se pueden ignorar.

Hoy, echemos un vistazo más profundo a los problemas de rendimiento expuestos por Java I/O en escenarios comerciales de big data y alta concurrencia, comencemos desde la fuente y aprendamos métodos de optimización.

1. ¿Qué es la E/S?

La E/S es el canal principal para que las máquinas obtengan e intercambien información, y los flujos son la forma principal de completar las operaciones de E/S.

En informática, una corriente es una transformación de información. El flujo está ordenado, por lo que, en comparación con una determinada máquina o aplicación, generalmente llamamos a la información recibida por la máquina o aplicación del mundo exterior como flujo de entrada (InputStream), y la información generada por la máquina o aplicación se llama flujo de salida ( OutputStream), denominados colectivamente flujo de entrada/salida (flujos de E/S).

Al intercambiar información o datos entre máquinas o programas, el objeto o los datos siempre se convierten en una determinada forma de flujo, y luego, a través de la transmisión del flujo, después de llegar a la máquina o programa especificado, el flujo se convierte en datos de objeto. Por lo tanto, un flujo puede considerarse como un soporte de datos a través del cual se puede realizar el intercambio y la transmisión de datos.

Las clases de operación de E/S de Java se encuentran en el paquete java.io, donde InputStream, OutputStream, Reader y Writer son las cuatro clases básicas en el paquete de E/S, que se ocupan de flujos de bytes y flujos de caracteres respectivamente. Como se muestra abajo:

 Mirando hacia atrás en mi experiencia, recuerdo que cuando leí por primera vez la documentación del flujo de E/S de Java, tuve esa pregunta y la compartiré con ustedes aquí, es decir: "Ya sea lectura o escritura de archivos o envío de red". y recibir, la información mínima Las unidades de almacenamiento son todas bytes, entonces, ¿por qué las operaciones de flujo de E/S se dividen en operaciones de flujo de bytes y operaciones de flujo de caracteres? "

Sabemos que los caracteres deben transcodificarse a bytes. Este proceso lleva mucho tiempo. Si no conocemos el tipo de codificación, es fácil generar caracteres confusos. Por lo tanto, el flujo de E/S proporciona una interfaz para manipular caracteres directamente, lo que nos resulta conveniente para realizar operaciones de flujo en caracteres en momentos normales. Entendamos el "flujo de bytes" y el "flujo de caracteres" respectivamente.

1.1, flujo de bytes

InputStream/OutputStream es una clase abstracta de flujo de bytes. Estas dos clases abstractas han derivado varias subclases y diferentes subclases manejan diferentes tipos de operaciones. Si es una operación de lectura y escritura de archivos, use FileInputStream/FileOutputStream; si es una operación de lectura y escritura de matriz, use ByteArrayInputStream/ByteArrayOutputStream; si es una operación de lectura y escritura de cadenas ordinaria, use BufferedInputStream/BufferedOutputStream. El contenido específico se muestra en la siguiente figura:

1.2, flujo de personajes

Lector/Escritor es una clase abstracta de flujos de caracteres. Estas dos clases abstractas también derivan varias subclases. Diferentes subclases manejan diferentes tipos de operaciones. El contenido específico se muestra en la siguiente figura:

2. Problemas de rendimiento de las E/S tradicionales

Sabemos que las operaciones de E/S se dividen en operaciones de E/S de disco y operaciones de E/S de red. El primero lee la fuente de datos del disco y la ingresa en la memoria, y luego persiste la información leída en el disco físico; el segundo lee la información de la red, la ingresa en la memoria y finalmente envía la información a la red. . Pero ya sea que se trate de E/S de disco o de red, existen serios problemas de rendimiento en la E/S tradicional.

2.1, múltiples copias de memoria

En la E/S tradicional, podemos leer el flujo de datos desde los datos de origen al búfer a través de InputStream y enviar los datos a dispositivos externos (incluidos discos y redes) a través de OutputStream. Primero puede observar el proceso específico de la operación de entrada en el sistema operativo, como se muestra en la siguiente figura:

  • La JVM emitirá una llamada al sistema read () e iniciará una solicitud de lectura al kernel a través de la llamada al sistema read;
  • El kernel envía un comando de lectura al hardware y espera a que la lectura esté lista;
  • El kernel copia los datos que se van a leer en la memoria caché del kernel apuntado;
  • El núcleo del sistema operativo copia los datos en el búfer del espacio de usuario y regresa la llamada al sistema de lectura.

En este proceso, los datos se copian primero desde el dispositivo externo al espacio del kernel y luego desde el espacio del kernel al espacio del usuario, lo que resulta en dos operaciones de copia de memoria. Esta operación provocará una copia de datos y un cambio de contexto innecesarios, lo que reducirá el rendimiento de E/S.

2.2, bloqueo

En la E/S tradicional, la lectura () de InputStream es una operación de bucle while, esperará a que se lean los datos y no regresará hasta que los datos estén listos. Esto significa que si no hay datos listos, la operación de lectura siempre se suspenderá y el hilo del usuario se bloqueará.

En el caso de una pequeña cantidad de solicitudes de conexión, no hay problema al utilizar este método y la velocidad de respuesta también es alta. Pero cuando ocurre una gran cantidad de solicitudes de conexión, es necesario crear una gran cantidad de subprocesos de escucha. En este momento, si el subproceso no tiene datos listos, se suspenderá y luego entrará en el estado bloqueado. Una vez que se bloquea un subproceso, estos subprocesos continuarán apoderándose de recursos de la CPU, lo que dará como resultado una gran cantidad de cambios de contexto de la CPU y un aumento de la sobrecarga del rendimiento del sistema.

3. Cómo optimizar las operaciones de E/S

Frente a los dos problemas de rendimiento anteriores, no solo se ha optimizado el lenguaje de programación, sino que cada sistema operativo también ha optimizado aún más la E/S. JDK1.4 lanzó el paquete java.nio (abreviatura de nueva E/S) y el lanzamiento de NIO optimizó los graves problemas de rendimiento causados ​​por la copia y el bloqueo de memoria. JDK1.7 lanzó NIO2 nuevamente, que propuso E / S asincrónicas realizadas desde el nivel del sistema operativo. Echemos un vistazo a la implementación de optimización específica.

3.1 Utilice buffers para optimizar las operaciones de flujo de lectura y escritura

En la E/S tradicional, se proporcionan implementaciones de E/S basadas en flujos, InputStream y OutputStream. Esta implementación basada en flujos procesa datos en unidades de bytes.

NIO se diferencia de las E / S tradicionales: se basa en bloques (Block) y procesa datos con bloques como unidad básica. En NIO, los dos componentes más importantes son el búfer (búfer) y el canal (canal). El búfer es un bloque continuo de memoria, que es el punto de tránsito para que NIO lea y escriba datos. El canal representa el origen o destino de los datos almacenados en el búfer, que se utiliza para leer o escribir datos almacenados en el búfer, y es la interfaz para acceder a los datos almacenados en el búfer.

La mayor diferencia entre la E/S tradicional y NIO es que la E/S tradicional está orientada al flujo, mientras que NIO está orientada al búfer. El búfer puede leer archivos en la memoria a la vez y luego realizar un procesamiento posterior, mientras que el método tradicional es procesar datos mientras se leen archivos. Aunque la E/S tradicional también utiliza bloques de búfer, como BufferedInputStream, todavía no es comparable a NIO. El uso de NIO para reemplazar las operaciones de E/S tradicionales puede mejorar el rendimiento general del sistema y el efecto es inmediato.

3.2 Utilice DirectBuffer para reducir la copia de memoria

Además de la optimización del bloque de búfer, Buffer de NIO también proporciona una clase DirectBuffer que puede acceder directamente a la memoria física. Ordinary Buffer asigna memoria de montón JVM, mientras que DirectBuffer asigna directamente memoria física.

Sabemos que para enviar datos a un dispositivo externo, primero se deben copiar del espacio del usuario al espacio del kernel y luego al dispositivo de salida, mientras que DirectBuffer simplifica directamente los pasos para copiar del espacio del kernel al dispositivo externo, lo que reduce la copia de datos.

Para ampliar aquí, dado que DirectBuffer se aplica a memoria física que no es JVM, el costo de creación y destrucción es muy alto. La memoria solicitada por DirectBuffer no es directamente responsable de la recolección de basura por parte de la JVM, pero cuando se recicla la clase contenedora DirectBuffer, el bloque de memoria se liberará a través del mecanismo de referencia de Java.

3.3 Evitar bloquear y optimizar las operaciones de E/S

Mucha gente en NIO también lo llama E / S sin bloqueo, es decir, E / S sin bloqueo, porque puede reflejar mejor sus características. ¿Por qué dices eso?

Incluso si la E/S tradicional utiliza bloques de búfer, todavía existen problemas de bloqueo. Debido a la cantidad limitada de subprocesos en el grupo de subprocesos, una vez que ocurre una gran cantidad de solicitudes simultáneas, los subprocesos que exceden el número máximo solo pueden esperar hasta que haya subprocesos inactivos en el grupo de subprocesos que puedan reutilizarse. Al leer el flujo de entrada del Socket, el flujo de lectura se bloqueará hasta que ocurra cualquiera de las siguientes tres situaciones:

  • tener datos para leer;
  • liberación de conexión;
  • Puntero nulo o excepción de E/S.

El problema del bloqueo es la mayor desventaja de la E/S tradicional. Después del lanzamiento de NIO, los dos componentes básicos de canales y multiplexores se han dado cuenta del no bloqueo de NIO. Echemos un vistazo a los principios de optimización de estos dos componentes juntos.

3.3.1 Canal (Canal)

Como comentamos anteriormente, la lectura y escritura de datos de las E/S tradicionales se copian desde el espacio del usuario al espacio del kernel, mientras que los datos en el espacio del kernel se leen o escriben desde el disco a través de la interfaz de E/S en el nivel del sistema operativo.

Al principio, cuando el programa de aplicación llama a la interfaz de E/S del sistema operativo, la asignación la realiza la CPU. El mayor problema con este método es "cuando ocurre una gran cantidad de solicitudes de E/S, la CPU se consume mucho". ; más tarde, el sistema operativo introduce DMA (almacenamiento de memoria directa), el acceso entre el espacio del kernel y el disco es completamente responsable del DMA, pero este método aún necesita solicitar permiso de la CPU y usar el bus DMA. Para completar la operación de copia de datos, si hay demasiados buses DMA, se producirá un conflicto de bus.

La aparición de canales resuelve los problemas anteriores: el canal tiene su propio procesador, que puede completar operaciones de E / S entre el espacio del kernel y el disco. En NIO leemos y escribimos datos a través del Canal, dado que el Canal es bidireccional, la lectura y escritura se pueden realizar al mismo tiempo.

3.3.2, multiplexor (Selector)

El selector es la base de la programación Java NIO. Se utiliza para comprobar si el estado de uno o más canales NIO es legible y escribible.

El selector se basa en una implementación basada en eventos. Podemos registrar, aceptar y leer eventos de monitoreo en el Selector, y el Selector sondeará continuamente el canal registrado en él. Si ocurre un evento de monitoreo en un canal, el canal estará en el estado listo y luego proceda con las operaciones de E/S.

Un hilo utiliza un Selector para escuchar eventos en múltiples canales mediante sondeo. Podemos configurar el canal para que no se bloquee al registrar el canal. Cuando no hay operaciones de E/S en el canal, el hilo no esperará para siempre, sino que sondeará continuamente todos los canales para evitar el bloqueo.

En la actualidad, el mecanismo de multiplexación de E/S del sistema operativo utiliza epoll. En comparación con el mecanismo de selección tradicional, epoll no tiene un límite de 1024 identificadores de conexión máximos. Entonces, en teoría, Selector puede sondear a miles de clientes.

3.3.3 Ejemplos

Permítanme usar una escena realista como ejemplo. Después de leerla, será más consciente de los roles y funciones que desempeñan el canal y el selector en las E/S sin bloqueo.

Podemos comparar la escucha de múltiples solicitudes de conexión de E/S con la entrada de una estación de tren. En el pasado, solo los pasajeros del tren de salida más cercano podían ingresar a la estación con anticipación y solo había un revisor de boletos. En ese momento, si los pasajeros de otros trenes querían ingresar a la estación, tenían que hacer cola en la entrada de la estación. Esto es equivalente a las primeras operaciones de E/S que no implementaban el grupo de subprocesos.

Posteriormente, se mejoró la estación de tren y se agregaron varias puertas de venta más, lo que permitió a los pasajeros de diferentes trenes ingresar a la estación a través de sus puertas de venta correspondientes. Esto equivale a crear múltiples subprocesos de escucha con subprocesos múltiples y monitorear las solicitudes de E/S de cada cliente al mismo tiempo.

Al final, la estación de tren se mejoró para acomodar a más pasajeros. Cada tren puede transportar más pasajeros y los trenes están organizados de manera razonable. Los pasajeros ya no hacen cola en grupos y pueden ingresar a la estación a través de una gran puerta de entrada unificada. Puede consultar billetes de varios trenes al mismo tiempo. Esta gran puerta de entrada equivale a Selector, el número de tren equivale a Channel y los pasajeros equivalen a flujo de E/S.

4. Resumen

La E/S tradicional de Java se implementa inicialmente en función de dos flujos de operación, InputStream y OutputStream. Esta operación de flujo está en bytes. Si se encuentra en un escenario de alta concurrencia y grandes datos, es fácil causar bloqueo. Por lo tanto, esta operación El rendimiento es muy pobre. Además, los datos de salida se copian del espacio del usuario al espacio del kernel y luego se copian al dispositivo de salida, lo que aumentará la sobrecarga de rendimiento del sistema.

Posteriormente, la E / S tradicional utilizó Buffer para optimizar el problema de rendimiento del "bloqueo". El bloque de buffer se utilizó como la unidad más pequeña, pero en comparación con el rendimiento general, todavía no era satisfactorio.

Entonces se lanzó NIO. Es una operación de flujo basada en bloques de búfer. Sobre la base de Buffer, se agregan dos nuevos componentes "pipeline y multiplexor" para realizar E/S sin bloqueo. NIO es adecuado para una gran cantidad de En el En el caso de solicitudes de conexión de E/S, estos tres componentes juntos mejoran el rendimiento general de E/S.

5. Preguntas para pensar

En la versión JDK1.7, Java lanzó el paquete de actualización NIO NIO2, que es AIO. AIO implementa E/S asincrónicas en el verdadero sentido, que transfiere directamente las operaciones de E/S al sistema operativo para su procesamiento asincrónico. Esto también es una optimización de las operaciones de E/S, entonces, ¿por qué muchos marcos de comunicación de contenedores todavía usan NIO?

Supongo que te gusta

Origin blog.csdn.net/qq_34272760/article/details/132323633
Recomendado
Clasificación