Ir al contenido

Consumo de recursos

Nada de lo publicado hasta ahora sobre este servidor decía lo que cuesta ejecutarlo. Esta página sí: memoria residente, tiempo de procesador, número de goroutines y latencia por método, medidos en ambos transportes y según los ajustes que de verdad mueven esas cifras.

El binario real, compilado desde la copia de trabajo actual y arrancado como lo arranca un cliente, en ambos transportes. GitLab se sustituye por un servidor HTTP dentro del propio arnés, así que una ejecución no necesita instancia, ni credenciales, ni red, y dos personas que repitan la medición comparan este servidor y no sus instalaciones de GitLab. La superficie de herramientas se pasa de forma explícita en lugar de leerse del entorno, para que una máquina de desarrollo y la integración continua midan lo mismo.

Todo lo que sigue lo regenera una sola orden, make bench-resources, que ejecuta el banco de pruebas y redibuja las gráficas a partir del registro que escribe. La máquina, la compilación y el método aparecen en el texto bajo el encabezado y en el borde inferior de cada gráfica, porque una cifra de memoria residente sin ellos no es accionable.

Esta página es una instantánea, y solo eso. Cada versión vuelve a medir en la máquina de referencia y sustituye estos valores por completo: sin columna de “antes”, sin proporción frente a una ejecución anterior, sin histórico. Una cifra comparada con una ejecución cuyo código ya no existe no dice nada sobre la instancia que estás dimensionando.

Los escenarios puntuales ejecutan 64 credenciales en HTTP y 8 procesos en stdio, admitiéndolos de uno en uno y cargándolos después todos a la vez. 64 queda muy por encima del punto en que la máquina se satura y por debajo del límite por defecto del pool, que es 100, así que el escenario ejecuta la configuración que se distribuye y mide contención real; 8 en stdio, porque cada uno es un proceso entero con su propio catálogo y ocho clientes simultáneos son una máquina de desarrollo ocupada.

Un despliegue compartido atiende a cientos, así que el banco de pruebas ejecuta además una serie de concurrencia por superficie: un solo proceso HTTP al que se le dan por turnos 1, 2, 5, 10, 20, 50, 100, 200, 500 y 1000 credenciales distintas, cada una calentada con un tools/list, y que se mide en cada recuento durante una fase estable de diez segundos en la que todas las credenciales llaman sin parar a tools/call y tools/list. Cada paso registra la memoria residente (media y pico), el tiempo de procesador por llamada, el p50 y el p99 de ambos métodos y el número de goroutines, y captura un perfil de CPU y otro de heap del servidor a través de su listener --pprof-addr, que el análisis lee y el sitio no publica.

Cada paso se mide además una segunda vez, con la carga detenida. Las dos lecturas responden a preguntas distintas: el conjunto residente de la fase estable es lo que cuestan N credenciales mientras todas están llamando, y la lectura en reposo, tomada al terminar la carga y forzando una recolección, es lo que cuesta mantenerlas. La primera se mide en megabytes por credencial y la segunda en kilobytes, así que ninguna sustituye a la otra. El conjunto residente en reposo se registra junto al heap en reposo y va por detrás de él, porque liberar el montículo no devuelve las páginas: el recolector de Go las devuelve según su propio calendario.

Una serie se detiene antes de tumbar la máquina: antes de cada paso se estima, a partir de los pasos anteriores, la memoria residente que alcanzaría, y el resto de la lista se omite cuando la estimación supera el presupuesto de memoria (el 80% de la memoria disponible del equipo salvo que se indique otro); un paso cuyo p99 de tools/call pase de 30 segundos es el último que se ejecuta. Dónde se detuvo cada serie, y por qué, se indica bajo su tabla. En la ejecución publicada aquí no se detuvo ninguna: las tres llegaron a mil credenciales.

Medido en Intel(R) Core(TM) i5-14400, 16 CPU lógicas, 62 GiB de RAM, linux/amd64, núcleo 6.12.105-production+truenas, go1.27.1, compilación 2.7.6-0.20260906081503-18bed59da189 (18bed59d), 2026-09-06T08:24:43Z. 3 rondas por método, conjunto residente muestreado cada 100 ms.

Barras agrupadas que comparan la memoria residente de las superficies dynamic, meta e individual en ambos transportes.
Líneas que muestran cómo crece la memoria residente cuando cada credencial nueva construye su catálogo en el pool HTTP.
Barras agrupadas en escala logarítmica que comparan el arranque del proceso, el primer tools/list en frío y uno en caliente.
Barras agrupadas en escala logarítmica que comparan la latencia de resources/list, tools/call y tools/list por transporte y superficie.
Líneas sobre una escala logarítmica de credenciales con el pico de memoria residente de cada superficie por paso, el presupuesto de memoria y el número de credenciales en que se detuvo cada serie.
Líneas en escalas logarítmicas con el p50 y el p99 de tools/call por superficie según crece el número de credenciales.
Líneas sobre una escala logarítmica de credenciales con el tiempo de procesador por llamada de cada superficie.

Memoria, goroutines y tiempo de procesador por escenario

Sección titulada «Memoria, goroutines y tiempo de procesador por escenario»
EscenarioClientesEn reposoUn clienteTodos los clientesPor cliente extraPicoGoroutinesCPU, % de un núcleo
stdio, dynamic8n/a106849106995341039%
stdio, meta8n/a107857107101836794%
stdio, individual8n/a27721342652291361120%
stdio, dynamic, telemetry8n/a1118931121048431080%
http, dynamic64351081180375291974%
http, meta64371121180324293928%
http, individual6436282248-114221651147%
http, dynamic, telemetry643811312203833001018%
EscenarioProceso listoPrimer tools/listtools/list en caliente (p50)Tamaño de tools/list
stdio, dynamic0.263511.912 KB
stdio, meta0.1734142598 KB
stdio, individual0.1812793793.2 MB
stdio, dynamic, telemetry0.243331.712 KB
http, dynamic593192112 KB
http, meta51324270599 KB
http, individual51127723943.2 MB
http, dynamic, telemetry513192012 KB
EscenarioMétodoLlamadap50p90p99Máx
stdio, dynamicresources/listel listado más pequeño0.657.67.77.7
stdio, dynamictools/callgitlab_find_action30414949
stdio, dynamictools/listla superficie completa1.96.06.26.2
stdio, metaresources/listel listado más pequeño0.640.991.21.2
stdio, metatools/callgitlab_server (status)1.7404141
stdio, metatools/listla superficie completa42515555
stdio, individualresources/listel listado más pequeño0.570.780.930.93
stdio, individualtools/callgitlab_server_status0.871.41.81.8
stdio, individualtools/listla superficie completa379405424424
stdio, dynamic, telemetryresources/listel listado más pequeño0.790.870.950.95
stdio, dynamic, telemetrytools/callgitlab_find_action30475050
stdio, dynamic, telemetrytools/listla superficie completa1.72.32.62.6
http, dynamicresources/listel listado más pequeño33488085
http, dynamictools/callgitlab_find_action213307314314
http, dynamictools/listla superficie completa21353838
http, metaresources/listel listado más pequeño10556266
http, metatools/callgitlab_server (status)23313334
http, metatools/listla superficie completa270388403404
http, individualresources/listel listado más pequeño9.8133030
http, individualtools/callgitlab_server_status8.4161818
http, individualtools/listla superficie completa2394285529292949
http, dynamic, telemetryresources/listel listado más pequeño14535961
http, dynamic, telemetrytools/callgitlab_find_action213301314315
http, dynamic, telemetrytools/listla superficie completa20353637

http, superficie dynamic: 4 en vuelo por credencial, 10 s por paso, presupuesto de memoria de 35000 MiB

Sección titulada «http, superficie dynamic: 4 en vuelo por credencial, 10 s por paso, presupuesto de memoria de 35000 MiB»
CredencialesResidente, mediaResidente, picoHeap en reposoResidente en reposoCPU por llamadaLlamadastools/call p50tools/call p99tools/list p50tools/list p99Goroutines
114315439.01067.394689511140.963.418
217219239.11077.6331107013181.14.722
521723239.21097.9101368423433.91636
1024127739.51107.97113660431187.55356
2028936440.01157.84314254752741114796
5041448141.51227.6291677715176611471216
10054760844.01337.63917482269157815956416
200869107048.91577.674178514063572201757816
5001958308964.02327.8511718791193135241882015
1000897145488.32889.1761611931068857221663574015

Ajustado sobre estos pasos: el pico de conjunto residente bajo carga crece 1.92 MiB por credencial, y el heap vivo en reposo, leído con la carga detenida y una recolección forzada, crece 50.6 KiB por credencial. Lo primero es lo que cuesta una credencial mientras ella y todas las demás están llamando; lo segundo es lo que cuesta mantenerla. El conjunto residente en reposo va por detrás de ambos, porque Go devuelve al sistema operativo las páginas liberadas según su propio calendario. Se ejecutaron todos los pasos previstos, hasta 1000 credenciales.

http, superficie meta: 4 en vuelo por credencial, 10 s por paso, presupuesto de memoria de 35000 MiB

Sección titulada «http, superficie meta: 4 en vuelo por credencial, 10 s por paso, presupuesto de memoria de 35000 MiB»
CredencialesResidente, mediaResidente, picoHeap en reposoResidente en reposoCPU por llamadaLlamadastools/call p50tools/call p99tools/list p50tools/list p99Goroutines
113414632.693.08.01463230.974.0111620
216217932.794.78.00195441.45.3152024
518520332.894.78.512108106.920294936
1019922633.196.08.4891102516535111756
2023225733.61008.23411486341469124796
5029838835.21077.95312021138451165530216
10029540637.91168.51711842349775348700416
20037653143.21358.4581242774714816481250816
50051468959.51848.5991400814143252176330772015
1000749100983.52438.3301613017916808318060284015

Ajustado sobre estos pasos: el pico de conjunto residente bajo carga crece 0.81 MiB por credencial, y el heap vivo en reposo, leído con la carga detenida y una recolección forzada, crece 52.6 KiB por credencial. Lo primero es lo que cuesta una credencial mientras ella y todas las demás están llamando; lo segundo es lo que cuesta mantenerla. El conjunto residente en reposo va por detrás de ambos, porque Go devuelve al sistema operativo las páginas liberadas según su propio calendario. Se ejecutaron todos los pasos previstos, hasta 1000 credenciales.

http, superficie individual: 2 en vuelo por credencial, 10 s por paso, presupuesto de memoria de 35000 MiB

Sección titulada «http, superficie individual: 2 en vuelo por credencial, 10 s por paso, presupuesto de memoria de 35000 MiB»
CredencialesResidente, mediaResidente, picoHeap en reposoResidente en reposoCPU por llamadaLlamadastools/call p50tools/call p99tools/list p50tools/list p99Goroutines
127329688.1186111.9082621.14.915126818
229633888.3183116.9384801.16.916420120
542953088.4191133.5647661.31126229126
1053867388.5194137.591855176145459336
20731107388.8200134.65995338257784131956
501081130389.7211127.56811106880416853502116
1001266172491.1215126.6721292213257228287144216
2002244327494.0248122.50214992393428543012328415
50025223427103277110.33023612851164311082181631015
10003330467711732698.265363749741655916796252292015

Ajustado sobre estos pasos: el pico de conjunto residente bajo carga crece 4.27 MiB por credencial, y el heap vivo en reposo, leído con la carga detenida y una recolección forzada, crece 29.8 KiB por credencial. Lo primero es lo que cuesta una credencial mientras ella y todas las demás están llamando; lo segundo es lo que cuesta mantenerla. El conjunto residente en reposo va por detrás de ambos, porque Go devuelve al sistema operativo las páginas liberadas según su propio calendario. Se ejecutaron todos los pasos previstos, hasta 1000 credenciales.

stdio. Un proceso por cliente, y el proceso empieza a construir su catálogo nada más arrancar, así que no hay ningún estado de reposo en el que ahorrar memoria. Entre 106 y 277 MiB por proceso según la superficie, con picos algo mayores bajo carga. Entre procesos no se comparte nada, así que el coste es una línea recta en el número de clientes: una máquina de desarrollo con un solo cliente no lo notará, y la columna “todos los clientes” es lo que pesan ocho juntos.

HTTP. Parte de entre 35 y 38 MiB para un proceso que no atiende a nadie y suma un catálogo por cada configuración distinta, que construye la primera credencial que lo pide y comparten todas las demás credenciales de esa misma configuración. Eso es lo que informa la columna “por cliente extra”: 0,15, 0,09 y -0,53 MiB en dynamic, meta e individual, es decir, nada por encima del ruido de una lectura de conjunto residente, a lo largo de una escalera que admite 64 credenciales de una en una. Lo que queda por dimensionar es el trabajo concurrente, que es la columna de pico contigua: de 324 a 1422 MiB con las 64 llamando.

Hay dos ejes, y solo uno de ellos es el pool. Mantener una credencial cuesta 50,6 KiB de heap vivo en reposo en dynamic, 52,6 KiB en meta y 29,8 KiB en individual, de modo que mil credenciales admitidas son 88, 83 y 117 MiB de heap vivo y menos de un tercio de gibibyte de conjunto residente. Servirlas es donde una instancia se queda sin memoria: con todas manteniendo entre dos y cuatro peticiones en vuelo, el pico de conjunto residente crece 1,92, 0,81 y 4,27 MiB por credencial, y en el paso de mil credenciales se situó en 1,5, 1,0 y 4,7 GiB.

Una regla práctica para HTTP: reserva entre 1 y 4 MiB, según la superficie, por llamante simultáneo, más 40 MiB para el proceso y un catálogo por configuración. --max-http-clients acota las credenciales del pool y no es un ajuste de memoria: a 50 KiB cada una, su valor por defecto de 100 son cinco mebibytes. Acorta --pool-idle-timeout si quieres que las entradas se recuperen antes, pero espera recuperar kilobytes, no gigabytes.

Rendimiento. En la serie, las llamadas completadas por escalón dejan de crecer hacia las cinco credenciales en dynamic y meta: una llamada cuesta 8 ms de tiempo de procesador, así que una máquina de dieciséis hilos completa entre 11.000 y 17.900 por escalón de diez segundos sea cual sea el número de credenciales, y a partir de ahí la latencia es cola, con la p50 de tools/call en dynamic subiendo de 11 ms con una credencial a 269 ms con cien y a 3,1 segundos con mil. individual tiene otra forma: un tools/call cuesta ahí un milisegundo, pero una llamada promedia 120 ms de tiempo de procesador, porque una de cada dos es un tools/list que serializa tres megabytes de esquemas, así que una sola credencial con dos peticiones en vuelo ya mantiene ocupados tres núcleos.

El proceso está listo en milisegundos y la superficie que hay detrás no. En HTTP, /health responde entre 51 y 59 ms mientras el pool está vacío; la primera credencial de una configuración construye su catálogo, y ahí está la espera: 0,32 segundos en dynamic, 0,32 en meta y 1,28 en individual en la máquina medida. En stdio el proceso se ejecuta en mucho menos de un milisegundo y empieza a construir de inmediato, así que su primera petición espera ese mismo trabajo: 0,35, 0,34 y 1,28 segundos.

Esa espera es por configuración y no por credencial, porque el servidor construido se comparte, y vuelve: cuando todas las entradas de una configuración se han recuperado por --pool-idle-timeout o expulsado por el límite de tamaño del pool, la siguiente petición que la necesite la reconstruye entera.

Ya en caliente la imagen se invierte. Un tools/list cuesta entre 1,9 y 21 ms en dynamic, entre 42 y 270 ms en meta y entre 0,38 y 2,4 segundos en individual, que es lo que cuesta serializar 12 KB, 599 KB y 3,2 MB respectivamente, siendo el extremo alto de cada par HTTP con las 64 credenciales llamando. Un cliente que hable el protocolo 2026-07-28 puede servir los listados repetidos desde su propia caché y pagarlo menos veces, pero lo paga al menos una vez por sesión.

Exportar por OTLP cuesta unas pocas goroutines y tiempo de procesador medible, y nada de memoria apreciable. Viene desactivada por defecto; en OpenTelemetry está qué exporta y adónde va.

Ventana de terminal
make bench-resources # medir y redibujarlo todo
make bench-resources-render # redibujar desde el registro, sin medir
make check-bench-resources # comprobar que las gráficas cuadran con el registro

Los ocho escenarios puntuales tardan entre cinco y diez minutos en la máquina de referencia: la mitad stdio se va casi entera en construir un catálogo por proceso, que es justo el coste que se está midiendo y no una sobrecarga del arnés, y la mitad HTTP se va en servir 64 credenciales a la vez. Las series tardan después lo que la memoria del equipo les permita, hasta un cuarto de hora por superficie. Las series también pueden medirse en un equipo y dibujarse en otro: compila el driver y el servidor, y en el equipo con memoria ejecuta el driver con -binary, -json, -profiles y -no-render, que no lee nada de ningún repositorio; después copia el registro de vuelta y ejecuta make bench-resources-render. Los detalles para quien desarrolle, incluido lo que el arnés deliberadamente no mide, están en la referencia de consumo de recursos.