Deja de preocuparte por el tamaño del grupo de subprocesos y la cantidad de subprocesos. No existe una fórmula fija | Equipo de tecnología de JD Cloud

Es posible que muchas personas hayan visto una teoría sobre cómo establecer el número de subprocesos:

  • Programas intensivos de CPU: número de núcleos + 1

  • Programas intensivos de E/S - número de núcleos * 2

No, no, ¿alguien realmente planifica el número de subprocesos según esta teoría?

Pequeña prueba de número de subprocesos y utilización de CPU

Dejando de lado algunos sistemas operativos y principios informáticos, hablemos de una teoría básica (no hay necesidad de preocuparse por si es rigurosa, solo para una fácil comprensión): un núcleo de CPU solo puede ejecutar las instrucciones de un hilo por unidad de tiempo.

Entonces, en teoría, un subproceso solo necesita seguir ejecutando instrucciones para alcanzar la utilización completa de un núcleo.

Escribamos un ejemplo de ejecución en un bucle sin fin para verificar:

Entorno de prueba: AMD Ryzen 5 3600, 6 núcleos, 12 subprocesos

public class CPUUtilizationTest {
	public static void main(String[] args) {
		//死循环,什么都不做
		while (true){
		}
	}
}

Después de ejecutar este ejemplo, echemos un vistazo a la utilización actual de la CPU:
imagen.png
como puede ver en la imagen, mi utilización del núcleo número 3 ya está completa.

Según la teoría anterior, ¿debería intentar abrir algunos hilos más?

public class CPUUtilizationTest {
	public static void main(String[] args) {

		for (int j = 0; j < 6; j++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					while (true){
					}
				}
			}).start();
		}
	}
}

En cuanto a la utilización de la CPU en este momento, las tasas de utilización de varios núcleos el 1/2/5/7/9/11 ya están completas:
imagen.png

Entonces, si se abren 12 subprocesos, ¿se utilizarán por completo todos los núcleos? La respuesta debe ser si.
imagen.png

Si continúo aumentando la cantidad de subprocesos en el ejemplo anterior a 24 subprocesos en este momento, ¿cuál será el resultado?
imagen.png

Como puede ver en la imagen de arriba, la utilización de la CPU es la misma que en el paso anterior, todavía 100% para todos los núcleos, pero en este momento la carga ha aumentado de 11.x a 22.x (para la explicación de la carga promedio). , consulte https://scoutapm.com/blog/understanding-load-averages ), lo que indica que la CPU está más ocupada en este momento y las tareas del subproceso no se pueden ejecutar a tiempo.

Las CPU modernas son básicamente multinúcleo. Por ejemplo, la AMD 3600 que probé aquí tiene 6 núcleos y 12 subprocesos (hyper-threading). Simplemente podemos pensar en ella como una CPU de 12 núcleos. Entonces mi CPU puede hacer 12 cosas al mismo tiempo sin molestarse entre sí.

Si la cantidad de subprocesos a ejecutar es mayor que la cantidad de núcleos, entonces el sistema operativo debe programarlo. El sistema operativo asigna recursos de intervalo de tiempo de CPU a cada subproceso y luego cambia continuamente para lograr el efecto de ejecución "paralela".

¿Pero es esto realmente más rápido? Como se puede ver en el ejemplo anterior, un subproceso puede utilizar completamente la utilización de un núcleo . Si cada subproceso es muy "autoritario" y sigue ejecutando instrucciones sin darle tiempo de inactividad a la CPU, y la cantidad de subprocesos que se ejecutan al mismo tiempo es mayor que la cantidad de núcleos de la CPU, hará que el sistema operativo cambie la ejecución del subproceso. con más frecuencia para garantizar que todos los subprocesos puedan ejecutarse.

Sin embargo, el cambio tiene un costo: cada cambio irá acompañado de operaciones como actualizaciones de datos de registro y actualizaciones de la tabla de páginas de memoria . Aunque el costo de un cambio es insignificante en comparación con las operaciones de E/S, si hay demasiados subprocesos, los cambios de subprocesos son demasiado frecuentes o incluso el tiempo de conmutación por unidad de tiempo es mayor que el tiempo de ejecución del programa, provocará un exceso de CPU. Recursos: desperdiciados en cambiar de contexto en lugar de ejecutar el programa, la ganancia supera la ganancia.

El ejemplo anterior de ejecución en un bucle sin fin es demasiado extremo y es poco probable que dicho programa exista en circunstancias normales.

La mayoría de los programas tendrán algunas operaciones de E/S cuando se estén ejecutando, que pueden ser leer y escribir archivos, enviar y recibir mensajes a través de la red, etc. Estas operaciones de E/S deben esperar comentarios mientras están en progreso. Por ejemplo, al leer y escribir en la red, debe esperar a que se envíen o reciban mensajes. Durante este proceso de espera, el hilo está en estado de espera y la CPU no funciona. En este momento, el sistema operativo programará la CPU para ejecutar instrucciones de otros subprocesos, aprovechando así perfectamente el período de inactividad de la CPU y mejorando la utilización de la CPU.

En el ejemplo anterior, el programa sigue repitiendo y no hace nada, y la CPU tiene que seguir ejecutando instrucciones, sin dejar casi tiempo libre. ¿Qué pasa si se inserta una operación de E/S y la CPU está inactiva durante la operación de E/S? ¿Qué pasará con la utilización de la CPU? Veamos primero los resultados en un solo hilo:

public class CPUUtilizationTest {
	public static void main(String[] args) throws InterruptedException {

		for (int n = 0; n < 1; n++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					while (true){
                        //每次空循环 1亿 次后,sleep 50ms,模拟 I/O等待、切换
						for (int i = 0; i < 100_000_000l; i++) { 
						}
						try {
							Thread.sleep(50);
						}
						catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			}).start();
		}
	}
}

imagen.png

Vaya, la única tasa de utilización del núcleo No. 9 es solo del 50%, en comparación con el 100% anterior sin dormir, ya es la mitad menor. Ahora ajuste el número de hilos a 12 y vea:
imagen.png

La tasa de utilización de un solo núcleo es de aproximadamente 60, que no es muy diferente del resultado de un solo subproceso en este momento. La utilización de la CPU aún no se ha alcanzado por completo. Ahora aumente el número de subprocesos a 18:
imagen.png

En este momento, la utilización de un solo núcleo es cercana al 100%. Se puede ver que cuando hay E / S y otras operaciones en el subproceso que no ocupan recursos de la CPU, el sistema operativo puede programar la CPU para ejecutar más subprocesos al mismo tiempo.

Ahora aumente la frecuencia de los eventos de E/S y reduzca el número de bucles a la mitad, 50_000_000, los mismos 18 subprocesos:


imagen.png

En este momento, la tasa de utilización de cada núcleo es solo de alrededor del 70%.

Un breve resumen del número de subprocesos y la utilización de la CPU

El ejemplo anterior es solo un auxiliar. Para comprender mejor la relación entre el número de subprocesos/comportamiento del programa/estado de la CPU, resumamos brevemente:

  1. Un subproceso extremo (cuando ejecuta constantemente operaciones "informáticas") puede aprovechar al máximo la utilización de un solo núcleo. Una CPU de varios núcleos solo puede ejecutar un número máximo de subprocesos "extremos" igual al número de núcleos al mismo tiempo.

  2. Si cada subproceso es tan "extremo" y la cantidad de subprocesos que se ejecutan al mismo tiempo excede la cantidad de núcleos, provocará cambios innecesarios, hará que la carga sea demasiado alta y solo hará que la ejecución sea más lenta.

  3. Durante operaciones de pausa como E/S, la CPU está en un estado inactivo y el sistema operativo programa la CPU para ejecutar otros subprocesos, lo que puede mejorar la utilización de la CPU y ejecutar más subprocesos al mismo tiempo.

  4. Cuanto mayor sea la frecuencia de los eventos de E/S o mayor sea el tiempo de espera/pausa, mayor será el tiempo de inactividad de la CPU y cuanto menor sea la tasa de utilización, el sistema operativo puede programar la CPU para ejecutar más subprocesos.

La fórmula para la planificación del número de hilos.

El presagio anterior es todo para ayudar a comprender. Ahora veamos la definición en el libro. "Programación concurrente de Java en la práctica" presenta una fórmula para calcular el número de subprocesos:

CleanShot 2023-09-07 a las 12.41.41@2x.png

Si desea que el programa se ejecute hasta la utilización de CPU objetivo, la fórmula para la cantidad de subprocesos necesarios es:

CleanShot 2023-09-07 a las 12.42.02@2x.png

La fórmula es muy clara, ahora probémosla con el ejemplo anterior:

Si espero una utilización objetivo del 90% (multinúcleo 90), entonces la cantidad de subprocesos necesarios es:

Número de núcleos 12 * Tasa de utilización 0,9 * (1 + 50 (tiempo de suspensión)/50 (ciclo 50_000_000 que consume mucho tiempo)) ≈ 22

Ahora ajuste el número de hilos a 22 y vea los resultados:
imagen.png

La utilización de la CPU ahora es de aproximadamente 80+, lo que está cerca de las expectativas. Debido a la cantidad excesiva de subprocesos, cierta sobrecarga de cambio de contexto y la falta de casos de prueba rigurosos, es normal que la utilización real sea menor.

Al cambiar la fórmula, también puede calcular la utilización de la CPU por la cantidad de subprocesos:

CleanShot 2023-09-07 a las 12.41.11@2x.png

Número de subprocesos 22 / (Número de núcleos 12 * (1 + 50 (tiempo de suspensión) / 50 (ciclo 50_000_000 que consume mucho tiempo))) ≈ 0,9

Aunque la fórmula es buena, en un programa real, generalmente es difícil obtener el tiempo de espera y el tiempo de cálculo precisos, porque el programa es complejo y no solo "calcula" . Habrá mucha lectura y escritura de memoria, cálculo, E / S y otras operaciones compuestas en un fragmento de código. Es difícil obtener con precisión estos dos indicadores, por lo que calcular el número de subprocesos en función de fórmulas es demasiado ideal.

Número de hilos en el programa real.

Entonces, en programas reales, o en algunos sistemas empresariales Java, ¿cuál es la cantidad adecuada de subprocesos (tamaño del grupo de subprocesos) que se debe planificar?

Permítanme hablar primero de la conclusión: no hay una respuesta fija. Primero establezca expectativas, como cuál espero que sea la utilización de la CPU, cuál es la carga, cuál es la frecuencia del GC y otros indicadores, y luego ajústelos continuamente a un número razonable de subprocesos mediante pruebas.

Por ejemplo, para un sistema empresarial normal basado en SpringBoot, el contenedor Tomcat predeterminado + grupo de conexiones HikariCP + reciclador G1, si en este momento el proyecto también necesita un subproceso múltiple (o grupo de subprocesos) para que el escenario empresarial ejecute el negocio procesar de forma asincrónica/en paralelo.

En este momento, si planifico la cantidad de subprocesos de acuerdo con la fórmula anterior, el error será muy grande. Debido a que ya hay muchos subprocesos en ejecución en este host en este momento, Tomcat tiene su propio grupo de subprocesos, HikariCP también tiene su propio subproceso en segundo plano, JVM también tiene algunos subprocesos compilados e incluso G1 tiene su propio subproceso en segundo plano. Estos subprocesos también se ejecutan en el proceso actual y en el host actual, y también ocuparán recursos de la CPU.

Por lo tanto, debido a la interferencia ambiental, es difícil planificar con precisión la cantidad de subprocesos basándose únicamente en fórmulas y debe verificarse mediante pruebas.

El proceso generalmente es el siguiente:

  1. Analizar si hay interferencia de otros procesos en el host actual

  2. Analizar si hay otros subprocesos en ejecución o posibles en ejecución en el proceso JVM actual

  3. Pon una meta

    1. Utilización objetivo de la CPU: ¿hasta qué punto puedo tolerar que mi CPU funcione?

    2. Frecuencia objetivo de GC/tiempo de pausa: después de la ejecución de subprocesos múltiples, la frecuencia de GC aumentará. ¿Cuál es la frecuencia máxima que se puede tolerar y cuánto dura cada tiempo de pausa?

    3. Eficiencia de ejecución: por ejemplo, durante el procesamiento por lotes, ¿cuántos subprocesos necesito abrir por unidad de tiempo para completar el procesamiento a tiempo?

    4. ……

  4. Clasifique los puntos clave del enlace para ver si hay puntos atascados, porque si hay demasiados subprocesos, los recursos limitados de algunos nodos en el enlace pueden hacer que una gran cantidad de subprocesos esperen recursos (como tres- límite de corriente de la interfaz de parte, número limitado de grupos de conexiones, intermedio La presión sobre las piezas es demasiado alta y no se puede soportar, etc.)

  5. Aumente/disminuya continuamente la cantidad de subprocesos para probar, pruebe de acuerdo con los requisitos más altos y finalmente obtenga una cantidad de subprocesos que "satisfagan los requisitos"**

¡Y y y! El concepto de número de hilo en diferentes escenarios también es diferente:

  1. maxThreads en Tomcat es diferente en E/S con bloqueo y E/S sin bloqueo

  2. Dubbo todavía tiene una única conexión de forma predeterminada. También hay una distinción entre subprocesos de E/S (grupos) y subprocesos comerciales (grupos). Los subprocesos de E/S generalmente no son cuellos de botella, por lo que no es necesario tener demasiados, pero los negocios Los hilos pueden fácilmente denominarse cuellos de botella.

  3. Redis también es multiproceso después de 6.0, pero solo es de E/S multiproceso y el procesamiento "comercial" sigue siendo de un solo subproceso.

Por lo tanto, no se preocupe por cuántos subprocesos configurar. No existe una respuesta estándar, debes combinar el escenario, los objetivos y realizar pruebas para encontrar el número más adecuado de hilos.

Algunos estudiantes pueden tener preguntas: "No hay presión sobre nuestro sistema. No necesitamos una cantidad tan adecuada de subprocesos. Es simplemente un escenario asincrónico simple que no afecta otras funciones del sistema".

Es normal, muchos sistemas empresariales internos no requieren mucho rendimiento, siempre que sean estables, fáciles de usar y satisfagan las necesidades. Entonces mi número recomendado de subprocesos es: Número de núcleos de CPU

apéndice

Java obtiene la cantidad de núcleos de CPU

Runtime.getRuntime().availableProcessors()//获取逻辑核心数,如6核心12线程,那么返回的是12

Linux Obtenga la cantidad de núcleos de CPU

# 总核数 = 物理CPU个数 X 每颗物理CPU的核数 
# 总逻辑CPU数 = 物理CPU个数 X 每颗物理CPU的核数 X 超线程数

# 查看物理CPU个数
cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l

# 查看每个物理CPU中core的个数(即核数)
cat /proc/cpuinfo| grep "cpu cores"| uniq

# 查看逻辑CPU的个数
cat /proc/cpuinfo| grep "processor"| wc -l

Si mi artículo te resulta útil, dale Me gusta/recopila/sigue para animarlo y apoyarlo❤❤❤❤❤❤

 

Autor: JD Seguros Jiang Xin

Fuente: Comunidad de desarrolladores de JD Cloud Indique la fuente al reimprimir

JetBrains lanza Rust IDE: RustRover Java 21 / JDK 21 (LTS) GA Con tantos desarrolladores de Java en China, debería nacer un marco de desarrollo de aplicaciones de nivel ecológico .NET 8. El rendimiento ha mejorado enormemente y está muy por delante de . NET 7. PostgreSQL 16 es lanzado por un ex miembro del equipo de Rust. Lo lamento profundamente y pedí cancelar mi nombre. Ayer completé la eliminación de Nue JS en el front-end. El autor dijo que crearé un nuevo ecosistema web. NetEase Fuxi respondió a la muerte de un empleado que fue "amenazado por RR.HH. debido a un ERROR" Ren Zhengfei: Estamos a punto de entrar en la cuarta revolución industrial, Apple es el nuevo producto "v0" del maestro de Huawei Vercel: genera código de interfaz de usuario basado en texto
{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/u/4090830/blog/10112038
Recomendado
Clasificación