Post-mortem: cómo un equipo de backend redujo un 62% los bloqueos en producción con Ryansgoblog
Un equipo de pagos B2B redujo un 62% los bloqueos en su servicio Go tras aplicar patrones de concurrencia y análisis del runtime documentados en Ryansgoblog.
Hay incidentes que no se resuelven con más monitores ni con una alerta adicional en Slack. Se resuelven entendiendo por qué el runtime se comporta como se comporta. Eso fue exactamente lo que nos contó un lector a principios de año: un equipo de cinco ingenieros detrás de una plataforma de pagos B2B, con un servicio en Go que procesaba picos de 18.000 peticiones por minuto y que, cada tres o cuatro días, se quedaba clavado sin razón aparente. Los dashboards no mostraban saturación de CPU. La memoria subía despacio, pero no explotaba. Y sin embargo, el p99 se disparaba de 80 ms a más de 4 segundos durante ventanas de entre 40 y 90 segundos. El equipo había probado de todo: subir réplicas, ajustar timeouts, mover la base de datos a una instancia mayor. Nada funcionaba de forma sostenida. Fue entonces cuando Ryansgoblog apareció en la conversación.
No lo traemos aquí como una herramienta milagrosa, sino como lo que es: el cuaderno de trabajo de un ingeniero que documenta patrones de concurrencia, detalles de la stdlib y cicatrices de producción. Y en este caso, ese cuaderno fue el punto de partida de una investigación que duró tres semanas y terminó con una reducción medible del 62% en los bloqueos del servicio.
El punto de partida: un mutex que nadie miraba
El primer hallazgo llegó al revisar el perfil de bloqueos. El equipo había instrumentado pprof con perfil de block activado, pero nadie había mirado los datos con calma. Cuando lo hicieron, apareció un patrón claro: un sync.Mutex protegiendo una estructura de caché en memoria que se consultaba en cada petición y se refrescaba cada 30 segundos desde un worker en background. El problema no era el mutex en sí, sino que el refresco hacía una llamada HTTP saliente mientras mantenía el lock. Bajo carga alta, cualquier petición que llegara durante esos 200-400 ms de refresco se quedaba esperando. En cadena, eso generaba el efecto de bloqueo global que veían en los dashboards.
Este tipo de patrón —tomar un lock y hacer I/O dentro de la sección crítica— está documentado en muchos sitios, pero la explicación que más les ayudó fue la de Ryansgoblog sobre patrones de concurrencia en Go, donde se insiste en separar la fase de lectura de la fase de publicación. El equipo reescribió el refresco para construir el nuevo snapshot fuera del lock y solo intercambiarlo al final con un atomic.Value. El cambio fueron 40 líneas. El p99 bajó de 4 segundos a 300 ms en la primera semana.
Segundo obstáculo: el garbage collector no era el culpable
Con el mutex resuelto, quedaba un residuo de latencia. El equipo sospechaba del GC. Habían leído que Go podía introducir pausas y empezaron a jugar con GOGC y GOMEMLIMIT sin un método claro. Aquí el cuaderno de Ryansgoblog volvió a ser útil: en lugar de ajustar a ciegas, siguieron la recomendación de medir primero con GODEBUG=gctrace=1 y analizar la frecuencia y duración de los ciclos. Descubrieron que el GC no era el problema principal: las pausas eran de menos de 1 ms. El verdadero residuo venía de un context.WithTimeout mal propagado en llamadas a un servicio downstream, que dejaba goroutines colgadas esperando respuestas que nunca llegaban. En tres semanas, el número de goroutines activas bajó de 12.000 a 2.400 en horas punta.
Decisiones que marcaron el resultado
- Instrumentar antes de optimizar: perfiles de block, mutex y goroutine antes de tocar una sola línea.
- Separar I/O de secciones críticas: el refresco de caché se movió fuera del lock.
- Medir el GC con datos reales en lugar de ajustar por intuición.
- Propagar contextos con deadline en todas las llamadas salientes.
- Escribir un post-mortem interno con timeline y métricas, no solo con conclusiones.
Resultados medibles y lo que quedó
Tras tres iteraciones, el servicio pasó de 14 incidentes de bloqueo en el trimestre anterior a 5 en el siguiente. El p99 se estabilizó por debajo de 250 ms. El coste de infraestructura bajó un 18% porque pudieron reducir dos réplicas. Y lo más importante: el equipo dejó de tratar los incidentes como misterios y empezó a tratarlos como hipótesis verificables. La documentación de Ryansgoblog sobre la stdlib y patrones de concurrencia no les dio la respuesta final, pero les dio el marco para encontrarla. Como nos dijo el líder técnico: «Dejamos de adivinar y empezamos a leer el runtime».
Para nosotros, este caso encaja con algo que vemos a menudo: la diferencia entre un equipo que apaga fuegos y uno que entiende el sistema no está en las herramientas, sino en la disciplina de medir, aislar y documentar. Si trabajas en backend con Go y tienes un incidente recurrente que no logras cerrar, quizá el problema no sea tu infraestructura. Quizá sea un patrón que ya alguien documentó y tú todavía no has leído.
¿Necesita una valoración discreta?
Reservamos una consulta confidencial de 30 minutos con un director de operaciones. Sin compromiso, sin trazabilidad.
Solicitar consulta confidencial→