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.
Cómo se ha medido
Sección titulada «Cómo se ha medido»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.
Mediciones
Sección titulada «Mediciones»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.
Memoria, goroutines y tiempo de procesador por escenario
Sección titulada «Memoria, goroutines y tiempo de procesador por escenario»| Escenario | Clientes | En reposo | Un cliente | Todos los clientes | Por cliente extra | Pico | Goroutines | CPU, % de un núcleo |
|---|---|---|---|---|---|---|---|---|
| stdio, dynamic | 8 | n/a | 106 | 849 | 106 | 995 | 34 | 1039% |
| stdio, meta | 8 | n/a | 107 | 857 | 107 | 1018 | 36 | 794% |
| stdio, individual | 8 | n/a | 277 | 2134 | 265 | 2291 | 36 | 1120% |
| stdio, dynamic, telemetry | 8 | n/a | 111 | 893 | 112 | 1048 | 43 | 1080% |
| http, dynamic | 64 | 35 | 108 | 118 | 0 | 375 | 291 | 974% |
| http, meta | 64 | 37 | 112 | 118 | 0 | 324 | 293 | 928% |
| http, individual | 64 | 36 | 282 | 248 | -1 | 1422 | 165 | 1147% |
| http, dynamic, telemetry | 64 | 38 | 113 | 122 | 0 | 383 | 300 | 1018% |
Lo que espera un cliente, por escenario
Sección titulada «Lo que espera un cliente, por escenario»| Escenario | Proceso listo | Primer tools/list | tools/list en caliente (p50) | Tamaño de tools/list |
|---|---|---|---|---|
| stdio, dynamic | 0.26 | 351 | 1.9 | 12 KB |
| stdio, meta | 0.17 | 341 | 42 | 598 KB |
| stdio, individual | 0.18 | 1279 | 379 | 3.2 MB |
| stdio, dynamic, telemetry | 0.24 | 333 | 1.7 | 12 KB |
| http, dynamic | 59 | 319 | 21 | 12 KB |
| http, meta | 51 | 324 | 270 | 599 KB |
| http, individual | 51 | 1277 | 2394 | 3.2 MB |
| http, dynamic, telemetry | 51 | 319 | 20 | 12 KB |
Percentiles de latencia por método
Sección titulada «Percentiles de latencia por método»| Escenario | Método | Llamada | p50 | p90 | p99 | Máx |
|---|---|---|---|---|---|---|
| stdio, dynamic | resources/list | el listado más pequeño | 0.65 | 7.6 | 7.7 | 7.7 |
| stdio, dynamic | tools/call | gitlab_find_action | 30 | 41 | 49 | 49 |
| stdio, dynamic | tools/list | la superficie completa | 1.9 | 6.0 | 6.2 | 6.2 |
| stdio, meta | resources/list | el listado más pequeño | 0.64 | 0.99 | 1.2 | 1.2 |
| stdio, meta | tools/call | gitlab_server (status) | 1.7 | 40 | 41 | 41 |
| stdio, meta | tools/list | la superficie completa | 42 | 51 | 55 | 55 |
| stdio, individual | resources/list | el listado más pequeño | 0.57 | 0.78 | 0.93 | 0.93 |
| stdio, individual | tools/call | gitlab_server_status | 0.87 | 1.4 | 1.8 | 1.8 |
| stdio, individual | tools/list | la superficie completa | 379 | 405 | 424 | 424 |
| stdio, dynamic, telemetry | resources/list | el listado más pequeño | 0.79 | 0.87 | 0.95 | 0.95 |
| stdio, dynamic, telemetry | tools/call | gitlab_find_action | 30 | 47 | 50 | 50 |
| stdio, dynamic, telemetry | tools/list | la superficie completa | 1.7 | 2.3 | 2.6 | 2.6 |
| http, dynamic | resources/list | el listado más pequeño | 33 | 48 | 80 | 85 |
| http, dynamic | tools/call | gitlab_find_action | 213 | 307 | 314 | 314 |
| http, dynamic | tools/list | la superficie completa | 21 | 35 | 38 | 38 |
| http, meta | resources/list | el listado más pequeño | 10 | 55 | 62 | 66 |
| http, meta | tools/call | gitlab_server (status) | 23 | 31 | 33 | 34 |
| http, meta | tools/list | la superficie completa | 270 | 388 | 403 | 404 |
| http, individual | resources/list | el listado más pequeño | 9.8 | 13 | 30 | 30 |
| http, individual | tools/call | gitlab_server_status | 8.4 | 16 | 18 | 18 |
| http, individual | tools/list | la superficie completa | 2394 | 2855 | 2929 | 2949 |
| http, dynamic, telemetry | resources/list | el listado más pequeño | 14 | 53 | 59 | 61 |
| http, dynamic, telemetry | tools/call | gitlab_find_action | 213 | 301 | 314 | 315 |
| http, dynamic, telemetry | tools/list | la superficie completa | 20 | 35 | 36 | 37 |
Serie de concurrencia
Sección titulada «Serie de concurrencia»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»| Credenciales | Residente, media | Residente, pico | Heap en reposo | Residente en reposo | CPU por llamada | Llamadas | tools/call p50 | tools/call p99 | tools/list p50 | tools/list p99 | Goroutines |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 143 | 154 | 39.0 | 106 | 7.394 | 6895 | 11 | 14 | 0.96 | 3.4 | 18 |
| 2 | 172 | 192 | 39.1 | 107 | 7.633 | 11070 | 13 | 18 | 1.1 | 4.7 | 22 |
| 5 | 217 | 232 | 39.2 | 109 | 7.910 | 13684 | 23 | 43 | 3.9 | 16 | 36 |
| 10 | 241 | 277 | 39.5 | 110 | 7.971 | 13660 | 43 | 118 | 7.5 | 53 | 56 |
| 20 | 289 | 364 | 40.0 | 115 | 7.843 | 14254 | 75 | 274 | 11 | 147 | 96 |
| 50 | 414 | 481 | 41.5 | 122 | 7.629 | 16777 | 151 | 766 | 11 | 471 | 216 |
| 100 | 547 | 608 | 44.0 | 133 | 7.639 | 17482 | 269 | 1578 | 15 | 956 | 416 |
| 200 | 869 | 1070 | 48.9 | 157 | 7.674 | 17851 | 406 | 3572 | 20 | 1757 | 816 |
| 500 | 1958 | 3089 | 64.0 | 232 | 7.851 | 17187 | 911 | 9313 | 52 | 4188 | 2015 |
| 1000 | 897 | 1454 | 88.3 | 288 | 9.176 | 16119 | 3106 | 8857 | 2216 | 6357 | 4015 |
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»| Credenciales | Residente, media | Residente, pico | Heap en reposo | Residente en reposo | CPU por llamada | Llamadas | tools/call p50 | tools/call p99 | tools/list p50 | tools/list p99 | Goroutines |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 134 | 146 | 32.6 | 93.0 | 8.014 | 6323 | 0.97 | 4.0 | 11 | 16 | 20 |
| 2 | 162 | 179 | 32.7 | 94.7 | 8.001 | 9544 | 1.4 | 5.3 | 15 | 20 | 24 |
| 5 | 185 | 203 | 32.8 | 94.7 | 8.512 | 10810 | 6.9 | 20 | 29 | 49 | 36 |
| 10 | 199 | 226 | 33.1 | 96.0 | 8.489 | 11025 | 16 | 53 | 51 | 117 | 56 |
| 20 | 232 | 257 | 33.6 | 100 | 8.234 | 11486 | 34 | 146 | 91 | 247 | 96 |
| 50 | 298 | 388 | 35.2 | 107 | 7.953 | 12021 | 138 | 451 | 165 | 530 | 216 |
| 100 | 295 | 406 | 37.9 | 116 | 8.517 | 11842 | 349 | 775 | 348 | 700 | 416 |
| 200 | 376 | 531 | 43.2 | 135 | 8.458 | 12427 | 747 | 1481 | 648 | 1250 | 816 |
| 500 | 514 | 689 | 59.5 | 184 | 8.599 | 14008 | 1414 | 3252 | 1763 | 3077 | 2015 |
| 1000 | 749 | 1009 | 83.5 | 243 | 8.330 | 16130 | 1791 | 6808 | 3180 | 6028 | 4015 |
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»| Credenciales | Residente, media | Residente, pico | Heap en reposo | Residente en reposo | CPU por llamada | Llamadas | tools/call p50 | tools/call p99 | tools/list p50 | tools/list p99 | Goroutines |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 273 | 296 | 88.1 | 186 | 111.908 | 262 | 1.1 | 4.9 | 151 | 268 | 18 |
| 2 | 296 | 338 | 88.3 | 183 | 116.938 | 480 | 1.1 | 6.9 | 164 | 201 | 20 |
| 5 | 429 | 530 | 88.4 | 191 | 133.564 | 766 | 1.3 | 11 | 262 | 291 | 26 |
| 10 | 538 | 673 | 88.5 | 194 | 137.591 | 855 | 17 | 61 | 454 | 593 | 36 |
| 20 | 731 | 1073 | 88.8 | 200 | 134.659 | 953 | 38 | 257 | 784 | 1319 | 56 |
| 50 | 1081 | 1303 | 89.7 | 211 | 127.568 | 1110 | 68 | 804 | 1685 | 3502 | 116 |
| 100 | 1266 | 1724 | 91.1 | 215 | 126.672 | 1292 | 213 | 2572 | 2828 | 7144 | 216 |
| 200 | 2244 | 3274 | 94.0 | 248 | 122.502 | 1499 | 239 | 3428 | 5430 | 12328 | 415 |
| 500 | 2522 | 3427 | 103 | 277 | 110.330 | 2361 | 285 | 11643 | 11082 | 18163 | 1015 |
| 1000 | 3330 | 4677 | 117 | 326 | 98.265 | 3637 | 4974 | 16559 | 16796 | 25229 | 2015 |
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.
Dimensionar un despliegue
Sección titulada «Dimensionar un despliegue»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.
Qué espera un cliente
Sección titulada «Qué espera un cliente»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.
Telemetría
Sección titulada «Telemetría»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.
Reproducirlo
Sección titulada «Reproducirlo»make bench-resources # medir y redibujarlo todomake bench-resources-render # redibujar desde el registro, sin medirmake check-bench-resources # comprobar que las gráficas cuadran con el registroLos 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.