HeadlinesBriefing HeadlinesBriefing.com

Pico de pausa de GC de Go por intercambio

Hacker News •
×

Estoy escribiendo esto para que no te des un golpe en la frente como casi me pasó a mí cuando decidí ejecutar swap en producción para absorber picos de memoria. Tenía un cgroup con dos procesos: uno es un proceso de Go que llama a io. ReadAll y luego proto.

Unmarshal, creando un blob y luego una estructura de gráfico (que está marcada como escaneo por el asignador de Go). El otro proceso es un servidor HTTP que normalmente permanece en silencio. Cada vez que el recolector se ejecuta, lee esos spans de escaneo, puntero por puntero, y decide qué hacer con ellos.

Así que pensé: bien, bajo presión de memoria, el kernel va a expulsar páginas al dispositivo de intercambio, pero como la expulsión es por cgroup y no por proceso, las páginas de ambos procesos van a ser expulsadas, así que solo hay una pequeña probabilidad de que esto se convierta en una triste danza de intercambio de entrada y salida entre el kernel y el recolector de basura. Me equivoqué. Mientras experimentaba con esto, encontré un problema que podría haberme causado daño: el recolector de basura de Go lee sus metadatos (fuera del montón, en una región que no se libera) durante una pausa de detener-el-mundo, y esos metadatos pueden estar en el intercambio.

Hice una simulación en una máquina de Hetzner usando el kernel 6.8 con MGLRU habilitado. La pausa mediana fue de alrededor de 51 us. Con los metadatos en el NVMe, la peor pausa fue de 40 ms.

Para ver dónde se iban esos 40 ms, escribí un pequeño script de bpf que cuenta los fallos de página mientras el mundo está detenido. Esta fue la peor: 39902 us, fallos durante ese tiempo 228, 39013 us en fallos. 39 de esos 40 ms se gastaron en 228 fallos de página. Esos fallos ocurrieron dentro de la contabilidad del GC.

Ese es un modo de fallo potencial. El GC de Go tiene que detener el mundo en dos puntos: cuando realiza una terminación de barrido, y cuando realiza una terminación de marcaje. Tuvimos 312 de esas pausas en 30 minutos.

Así que aquí está por qué sucede esto: el entorno de ejecución asigna esas páginas. No se liberan, pero se reutilizan. Esas páginas se leen en ciclos de GC.

Porque el kernel expulsa páginas por antigüedad, envía las páginas menos recientemente accedidas al intercambio. El GC se ejecuta, detiene el mundo, intenta leer esas páginas, pero ahora tenemos un fallo de página importante. El kernel necesita leer las PTE, luego llamar a do_swap_page, encontrar un nuevo marco, cargarlo al cgroup, leer las páginas, enviar un bio, esperar al disco y devolverlas a la memoria, solo para resumir.

Esos 40 ms parecen inofensivos al principio. Pero estamos hablando de una pausa de detener-el-mundo. Esos 40 ms significan que todo se ha detenido, en términos de Go, cada P se ha detenido, así que, por ejemplo, si una goroutine estaba esperando una operación de E/S, durante esa pausa la E/S podría devolverse y no habría nadie para manejarla. 40 ms es 800 veces la pausa mediana.

Ocurre dos o tres veces por pico de memoria durante la prueba. Es mucho.

Fuente: Hacker News · Resumido por HeadlinesBriefing