Un vistazo en 30 segundos
- PBS_Chunk_Checker calcula el espacio en disco que ocupa de verdad una VM, un contenedor o un namespace dentro de un datastore de Proxmox Backup Server.
- Lo hace leyendo los índices (
.fidx y .didx), quedándose con los digests únicos referenciados y sumando el tamaño real de esos ficheros en .chunks.
- Script de Python, licencia GPL-3.0, de Jan Paulzen (VoltKraft). Versión 2.12.0, publicada el 27 de abril de 2026. Sin dependencias fuera de la biblioteca estándar.
- Tiene modo de línea de comandos, modo interactivo con menús de texto, informe por invitado de todo el datastore (
--all-guests) con salida CSV y desglose por snapshot (--per-snapshot).
- Corre en el propio host de PBS: necesita
proxmox-backup-manager y proxmox-backup-debug, y permisos para leer el almacén de chunks.
- Ojo con la interpretación: lo que mide es el espacio referenciado, no el espacio que liberarías al borrar. Los chunks compartidos aparecen en la cuenta de todos los que los usan.
La pregunta llega siempre igual. Un cliente, o el compañero que lleva el presupuesto, señala una máquina virtual concreta y quiere saber cuánto está ocupando en las copias. Parece una pregunta sencilla, y en cualquier otro sistema de backup lo sería.
Abres la interfaz de Proxmox Backup Server y te encuentras con dos cifras que no sirven. Una es el uso total del datastore, que es la suma de todo y no reparte nada. La otra es el tamaño de cada disco dentro del snapshot, que es el tamaño provisionado del disco original, no lo que ha acabado ocupando en el almacén. Entre esas dos cifras hay un agujero del tamaño de tu factura de almacenamiento.
PBS_Chunk_Checker existe para tapar ese agujero.
Por qué PBS no te da esa cifra
No es un olvido de la interfaz, es cómo está construido el producto.
PBS trocea todo lo que respalda en chunks y los identifica por su contenido con SHA-256. Dos bloques idénticos producen el mismo identificador y se guardan una sola vez, vengan de la copia de anoche, de la de hace ocho meses, de otra máquina del mismo clúster o de otro namespace completamente distinto. Cada snapshot no guarda datos: guarda un índice con la lista ordenada de los chunks que necesita.
Hay dos tipos de índice, y conviene conocerlos porque el script trabaja justo ahí:
| Índice | Qué describe | Troceado |
|---|
.fidx | Imágenes de disco de máquinas virtuales | Tamaño fijo, normalmente 4 MiB |
.didx | Archivos pxar de contenedores y de copias de host, catálogos | Tamaño variable, decidido por un hash rodante (Buzhash) |
Y los chunks viven todos juntos en un único almacén por datastore:
<raíz-del-datastore>/
├── .chunks/
│ ├── 0000/
│ ├── 0001/
│ └── … ← 65.536 subdirectorios, por prefijo de 2 bytes del hash
├── vm/
├── ct/
└── host/
Esos 65.536 directorios se crean al montar el datastore, y dentro va cada chunk como un fichero suelto, comprimido con zstd. Por eso un chunk de 4 MiB casi nunca ocupa 4 MiB en disco: ocupa lo que pese después de comprimir, que en datos normales suele rondar la mitad.
Con ese diseño, la pregunta «¿cuánto ocupa la VM 100?» deja de tener una respuesta única. Un chunk que comparten la VM 100 y la VM 214 no pertenece a ninguna de las dos. Está ahí porque lo necesitan las dos, y seguirá ahí mientras quede una que lo referencie. Lo único que sí se puede calcular con precisión es cuánto suman en disco los chunks distintos a los que apunta un objeto concreto, y eso es exactamente lo que hace este script.
Qué hace exactamente el script
Tres pasos, y ninguno de ellos toca los datos:
- Recorre los índices que cuelgan del camino que le indiques (un namespace entero, una VM, un contenedor) y extrae de cada uno la lista de digests.
- Deduplica esa lista en memoria. Si el mismo chunk aparece en ochenta snapshots, cuenta una vez.
- Consulta el tamaño de cada fichero de chunk dentro de
.chunks y suma los bytes.
El resultado es el tamaño real sobre disco de los chunks únicos que referencia el objeto que has elegido, ya comprimidos, tal y como los ve el sistema de ficheros.
Un detalle que me parece bien resuelto: la fase de suma consulta metadatos de los ficheros, no los abre. Eso significa que no altera la fecha de último acceso de los chunks, que es justo el dato del que depende la recolección de basura de PBS para decidir qué sobra. La fase de marcado del recolector actualiza el atime de todo chunk referenciado, y la de barrido borra lo que tenga un atime anterior a un corte de 24 horas y 5 minutos. Un script que fuera leyendo chunks a lo bestia podría enturbiar esa contabilidad; este no.
Ficha del proyecto
| Dato | Valor |
|---|
| Autor | Jan Paulzen (VoltKraft) |
| Licencia | GPL-3.0 |
| Lenguaje | Python, solo módulos de la biblioteca estándar |
| Versión | 2.12.0, del 27 de abril de 2026 |
| Primer commit | Abril de 2025 |
| Popularidad | Alrededor de 30 estrellas en GitHub |
| Requisitos | Host Linux de PBS, con proxmox-backup-manager y proxmox-backup-debug |
| Interfaz | Línea de comandos y modo interactivo con curses |
El proyecto valida que esas dos herramientas de Proxmox estén disponibles antes de empezar y aborta con un mensaje claro si faltan. No hay versión para ejecutar desde tu portátil contra un PBS remoto: esto se corre en la máquina, con acceso al almacén de chunks.
Cómo se usa
La forma directa, indicando datastore y camino dentro de él:
# Resumen de un namespace entero
./pbs_chunk_checker.py --datastore MiDatastore --searchpath /ns/MiNamespace
# Una máquina virtual concreta dentro de ese namespace
./pbs_chunk_checker.py --datastore MiDatastore --searchpath /ns/MiNamespace/vm/100
# Un contenedor
./pbs_chunk_checker.py --datastore MiDatastore --searchpath /ns/MiNamespace/ct/205
Si lo lanzas sin parámetros arranca el modo interactivo: eliges el datastore de una lista, navegas la estructura de directorios con los cursores y seleccionas el objeto. Usa el módulo curses que ya trae Python, sin instalar nada, y si el terminal no lo soporta cae a un menú numérico. Dentro del menú, la tecla o abre las opciones (hilos, emojis, comentarios de invitado, carpeta del CSV) y v muestra la versión.
Para el inventario completo del datastore está --all-guests, que recorre todos los namespaces y saca una tabla por invitado ordenada por consumo:
# Todo el datastore, con informe CSV en una ruta concreta
./pbs_chunk_checker.py --datastore MiDatastore --all-guests --csv-dir /var/lib/informes-pbs
# Solo un namespace y los que cuelgan de él
./pbs_chunk_checker.py --datastore MiDatastore --searchpath /ns/Cliente-A --all-guests
El CSV sale con separador punto y coma y columnas namespace_path, last_comment y unique_size_gib, con el nombre del fichero en marca de tiempo ISO 8601 básica (20260819T223000.csv). Se escribe entero al final, así que un fallo a mitad no te deja un fichero a medias que luego alguien cargue en una hoja de cálculo sin darse cuenta.
Los parámetros que vas a usar
| Opción | Para qué |
|---|
--datastore | Nombre del datastore. Obligatorio en modo script |
--searchpath | Camino dentro del datastore: /ns/Nombre, /ns/Nombre/vm/100, /ns/Nombre/ct/205 |
--all-guests | Recorre todo (o el namespace indicado) y saca resumen por invitado más CSV |
--per-snapshot | Desglose por snapshot: único, compartido y total |
--threads N | Paralelismo. Por defecto, dos por núcleo con tope de 32 |
--csv-dir | Dónde escribir el informe. Por defecto, el directorio actual |
--no-csv | No escribir CSV. No se puede combinar con --silent |
--silent | Sin salida por pantalla, pensado para cron |
--show-comments | Añade junto al ID una etiqueta sacada del comentario del último snapshot |
--no-emoji | Cambia los iconos por etiquetas ASCII |
--version / --update | Versión instalada y comprobación de actualizaciones |
Si lo metes en un cron con --silent, pon --csv-dir con ruta absoluta. El entorno de cron viene pelado y el directorio de trabajo no es el que crees.
Leer la salida sin equivocarse
Lo interesante no es el total, es el bloque de resumen que sale debajo:
🧩 Chunk usage summary:
Unique chunks : 8505 9.59% | 12.2GiB
Duplicate refs : 80186 90.41% | 186.2GiB
Total references : 88692 | 198.4GiB
| Línea | Qué es | Para qué sirve |
|---|
| Unique chunks | Chunks distintos referenciados, y lo que pesan en disco | Es la cifra de espacio real. La que buscabas |
| Duplicate refs | Referencias adicionales a chunks ya contados | Mide cuánto trabaja la deduplicación |
| Total references | Suma de todas las referencias y su tamaño lógico | Es lo que ocuparía sin deduplicar |
En ese ejemplo, 198 GiB de datos lógicos caben en 12 GiB de chunks. Un factor de dieciséis a uno, que es lo normal cuando hay muchos puntos de restauración de un sistema que cambia poco. Si tu ratio es mucho peor, ya tienes por dónde empezar a mirar: bases de datos que reescriben ficheros enteros, discos cifrados dentro del invitado, ficheros ya comprimidos o troceado fijo enfrentado a datos que se desplazan.
Con --per-snapshot la cosa se afina y aparecen tres columnas por punto de restauración:
- Unique size: chunks que solo referencia ese snapshot. Es lo que aporta de nuevo respecto a los demás.
- Shared size: referencias que también existen en otros snapshots del mismo objeto.
- Total size: la suma, que es el tamaño lógico del snapshot y no lo que ocupa en disco.
Esa columna de únicos es oro para ajustar retención. Si los snapshots diarios de una máquina aportan 300 MiB cada uno y los mensuales aportan 40 GiB, ya sabes qué política te está costando dinero de verdad y cuál es prácticamente gratis.
El error que se comete al facturar con estos números
Aquí es donde conviene frenar, porque este es el punto que se salta casi todo el mundo.
La cifra que da el script para una VM es el tamaño de los chunks que esa VM referencia. No es el espacio que se liberaría al borrarla, y no es un trozo exclusivo del datastore. Si la VM 100 y la VM 214 comparten la mitad de sus bloques porque salen de la misma plantilla de Debian, esos bloques aparecen enteros en la cuenta de las dos.
Consecuencias prácticas:
La suma de las partes es mayor que el todo. Si ejecutas --all-guests y sumas la columna de todos los invitados, te va a dar bastante más que el uso real del datastore. No es un fallo del script: es que el solapamiento está contado varias veces. La diferencia entre esa suma y el uso real es, de hecho, una medida bastante buena de cuánto te está ahorrando la deduplicación compartida.
Borrar no libera lo que pone. Si eliminas una máquina que el informe dice que ocupa 400 GiB, el recolector de basura puede liberar 400 GiB o puede liberar 12, según cuánto de eso lo estuvieran usando otros. Y lo liberará cuando toque, no en el momento: los chunks tienen que quedar sin referencias y sobrevivir a la ventana de 24 horas y 5 minutos antes de que el barrido se los lleve.
Repartir el coste exige decidir un criterio. No hay una respuesta técnica correcta, hay un acuerdo comercial. Las tres opciones razonables:
| Criterio | Cómo se calcula | Cuándo tiene sentido |
|---|
| Tamaño referenciado | Lo que da el script tal cual | Sencillo de explicar, sobrefactura en conjunto |
| Reparto proporcional | Se escala la cifra de cada uno hasta que la suma cuadre con el uso real | Justo entre clientes de una plataforma compartida |
| Coste marginal | Uso del datastore menos el uso sin ese objeto | Responde a «¿cuánto ahorro si se va este cliente?» |
El tercero es el más honesto y el más caro de calcular, porque implica una segunda pasada. En la práctica, para un informe mensual, el reparto proporcional es el que menos discusiones genera: cada cual paga la parte de un almacén compartido que su consumo justifica.
Y hay un caso donde esto deja de ser contabilidad y pasa a ser arquitectura: si estás separando clientes por namespace dentro de un mismo datastore para no partir la deduplicación, el reparto tiene que contemplar lo compartido. Es justo el precio a pagar por la ventaja que buscabas al consolidar.
Lo que cuesta ejecutarlo
No es gratis en recursos, aunque no escriba nada.
Recorrer los índices y consultar metadatos de cientos de miles o millones de ficheros pequeños es una carga de E/S de metadatos pura. Sobre NVMe vuela; sobre discos giratorios sin caché de metadatos decente se puede hacer largo, y el ejemplo del propio proyecto ya habla de doce minutos para un namespace de algo más de un tera. Un --all-guests sobre un datastore grande es cuestión de horas, y por eso el modo interactivo te lo advierte y te pide confirmación antes de lanzarlo.
Recomendaciones de sentido común:
- Lánzalo fuera de la ventana de copias, y fuera de la de verificación y recolección de basura. Compites por los mismos IOPS de metadatos.
- Sube
--threads si el almacén es de estado sólido; bájalo si son discos mecánicos, donde pasar de cierto punto solo genera búsquedas y empeora el resultado.
- Si el datastore vive sobre ZFS, este es un trabajo que agradece un vdev special para metadatos más que ninguna otra cosa que corras en ese servidor.
- Programa el
--all-guests semanal o mensual con --silent y CSV, y consulta puntualmente por VM cuando alguien pregunte. Ejecutarlo a diario no aporta nada: estas cifras se mueven despacio.
Antes de pegarlo en tu host de producción
El proyecto ofrece ejecutarlo sin dejar ficheros, canalizando el fichero desde GitHub directamente a Python. Es cómodo y funciona:
wget -q -O - https://raw.githubusercontent.com/VoltKraft/PBS_Chunk_Checker/main/pbs_chunk_checker.py | python3 - --datastore MiDatastore --searchpath /ns/MiNamespace
Nuestro criterio es el mismo que aplicamos a cualquier script que corre con privilegios en un hipervisor o en un servidor de copias, y ya lo dijimos hablando de ProxMenux: descarga, lee, fija la versión. Ese wget | python3 ejecuta lo que haya en la rama principal en ese preciso instante, sin que nadie lo haya mirado. Clonar el repositorio, quedarte en una etiqueta concreta y revisar el diff cuando actualices cuesta cinco minutos y te deja saber qué está corriendo en la máquina que guarda tus copias.
Lo mismo vale para --update, que descarga la última versión y reemplaza el fichero dejando un .bak al lado. Verifica la suma SHA-256 cuando la publicación trae ese fichero adjunto, lo cual está bien, pero sigue siendo autoactualización en un servidor de producción. En un entorno gestionado eso lo decide el ciclo de cambios, no el script.
Dos límites más que conviene tener claros:
Necesita el almacén de chunks en local. Desde PBS 4 se puede montar un datastore con el backend de S3, donde los chunks viven como objetos en el bucket y en disco solo queda una caché LRU además de los metadatos. Ahí este planteamiento no cuadra: los índices sí estarían, pero los tamaños de los chunks no se pueden consultar con un stat local. Si tienes ese montaje, pruébalo antes en un datastore de laboratorio en lugar de fiarte de la cifra.
Necesita permisos sobre el datastore. Los ficheros de chunk pertenecen al usuario de servicio de PBS y no son legibles por cualquiera. En la práctica, esto se ejecuta como root en el servidor de copias, que es otra razón para saber exactamente qué código estás lanzando.
Cómo lo miramos nosotros
En las plataformas que gestionamos, el espacio de las copias es una conversación que aparece siempre, y casi nunca empieza por la pregunta correcta. La útil no es «cuánto ocupa esta VM», sino «qué política de retención estoy pagando y para qué me sirve». Un script como este te da los datos para tenerla con números encima de la mesa en lugar de con impresiones.
Nuestro uso sería este: informe periódico por invitado para ver la distribución y detectar al que se ha ido de madre, --per-snapshot sobre los dos o tres primeros de la lista para saber si el problema es el dato o la retención, y reparto proporcional si hay que trasladar el coste a alguien. Nunca la cifra bruta como precio, porque no es lo que ocupa el almacén.
Y una cosa que no cambia por muchos informes que saques: el espacio es la mitad barata del problema. La otra mitad es cuánto tardas en volver, que es de lo que hablamos en RTO y RPO y en la regla 3-2-1. Si estás dimensionando o revisando la plataforma de copias de tu empresa, échale un ojo a nuestra página de continuidad y recuperación o cuéntanos cómo lo tienes montado.
Preguntas frecuentes
¿Por qué la interfaz de Proxmox Backup Server no muestra ya este dato?
Porque no existe como dato único. Un chunk deduplicado pertenece a todos los snapshots, máquinas y namespaces que lo referencian, así que el espacio de una VM depende de cómo repartas lo compartido. PBS enseña lo que sí es exacto: el uso total del datastore y el tamaño provisionado de cada disco.
¿Puedo sumar los resultados de todas las VM para cuadrar el uso del datastore?
No, y esa suma siempre saldrá más alta. Cada invitado contabiliza los chunks compartidos que referencia, y esos están contados varias veces. La diferencia entre la suma y el uso real es una buena medida de lo que aporta la deduplicación entre objetos.
Solo la parte que ningún otro snapshot referencie, y no de inmediato. El recolector de basura marca los chunks en uso, y borra los que quedan sin referencias tras superar el corte de 24 horas y 5 minutos.
¿Interfiere con la recolección de basura o con las copias?
No manipula el atime de los chunks porque consulta metadatos sin abrir los ficheros, así que no confunde al recolector. Sí compite por E/S: conviene no solaparlo con la ventana de copias, verificación o recolección.
¿Hace falta instalarlo en el servidor?
Puede ejecutarse canalizado desde GitHub sin dejar fichero, pero eso ejecuta con privilegios lo que haya publicado en ese momento. Para un servidor de producción es preferible clonar el repositorio, fijar una versión concreta y revisar los cambios antes de actualizar.
¿Sirve con un datastore montado sobre S3?
El script suma tamaños de ficheros de chunk en disco local, y con el backend de S3 de PBS 4 los chunks viven en el bucket con solo una caché local. Ese escenario queda fuera de lo que la herramienta puede medir con fiabilidad: pruébalo en laboratorio antes de sacar conclusiones.
¿Y para hacer facturación por cliente?
Sirve como base, pero necesita un criterio de reparto del espacio compartido. El proporcional (escalar las cifras hasta cuadrar con el uso real del datastore) es el que menos discusiones genera; el coste marginal es el más exacto y exige una pasada extra.
Fuentes