Sobre el tamaño de bloque en dd
16 de marzo de 2021 • fjbalon
dd es una de las herramientas invaluables de cualquier arsenal Unix, y quizás una de las herramientas más potentes que existen en esta familia. Forma parte de las core utils y su objetivo principal es convertir y copiar archivos -de manera similar a cp-, dd hace una copia bit a bit con funciones de control de flujo de entrada y salida a bajo nivel.
Permite entonces realizar tareas generales, como quemar una imagen -generalmente ISO- en un medio de instalación USB.
dd bs=512K if=/image.iso of=/dev/sdx conv=fsync oflag=direct status=progress
El uso de dd es simple, indicamos como parámetros un archivo de entrada (input file - if) y uno de salida (output file - of). De tal forma, también permite clonar un disco completo en el sistema de ficheros, siempre que indiquemos como if el disco físico (por ejemplo
dd bs=512K if=/dev/sdx status=progress | gzip -c > /image.img.gz
Del mismo modo, intercambiando if y of podemos quemar esta imagen comprimida en cualquier otro disco, sea
gunzip -c /image.img.gz | dd bs=512K of=/dev/sdy status=progress
Si lo único que deseamos es clonar de un disco a otro, podemos evitar el intermediario y hacerlo directamente jugando con if y of, dados los discos
dd bs=512K if=/dev/sdx of=/dev/sdy status=progress
Quizá sea porque hay que ser extremadamente cauteloso utilizando dd, ya que es sencillo destruir datos de forma irreversible en caso de negligencia; lo que hace que muchas veces nos enfoquemos únicamente en acertar con el parámetro of (archivo de salida - output file), o quizá sea pereza mental, pero un aspecto que se pasa normalmente por alto es el relativo a los tres argumentos de tamaño de bloque: ibs (tamaño de bloque de entrada), obd (tamaño de bloque de salida) y bs (tamaño de bloque de entrada y salida, abarcándolo todo).
Partamos de la base de que un bloque es una unidad que mide el número de bytes que se leen, escriben o convierten a la vez. De forma general y predeterminada, dd utiliza un tamaño de bloque de 512 bytes, tanto para la entrada como para la salida. Esto quizás ha sido correcto entre 1980 y 1999, cuando la mayoría de discos tenían un tamaño de sector de 512 bytes, sin embargo en los últimos años la mayoría de los discos tienen un tamaño de sector de al menos 4096 bytes (4K). Aunque el cambio puede parecer intrascendente, puede conducir a ineficiencia e inestabilidad, más teniendo en cuenta que los discos actuales pueden tener más de 1TB de capacidad, lo que requiere un tamaño de bloque más óptimo.
Por convención, hoy en día se recomienda utilizar un tamaño de bloque de 64K (en ocasiones se indica como 128K), asegurándose como más óptimos. Sin embargo, al no existir contexto ni explicación, lo considero un argumento viciado y arbitrario, teniendo en cuenta que no existe un tamaño de bloque universalmente correcto: lo que es óptimo para una situación puede ser terriblemente ineficiente para otra.
Si tomamos como ejemplo de lectura el fichero
Para ilustrarnos técnicamente, dado el script:
#!/bin/bash
target=$1
origin=/dev/urandom # /dev/zero
file_SIZE=134217728 # 128MB
human_bytes () {
unidades="b,K,M,G,T,P,E,Z,Y"
i=1
base=$(echo "$1 / 1" | bc)
variable=$( echo $base / 1024 | bc )
while [ $variable -gt 0 ]
do
let i=i+1
base=$(echo "$base / 1024" | bc)
variable=$(echo $base / 1024 | bc )
done
echo $base$(echo $unidades | cut -f$i -d, )
}
for BLOCK_SIZE in 512 1024 2048 4096 8192 16384 32768 65536 131072 262144 524288 1048576 2097152 4194304 8388608 16777216 33554432 67108864
do
COUNT=$(($file_SIZE / $BLOCK_SIZE))
if [ $COUNT -le 0 ]; then
echo "Block size of $BLOCK_SIZE estimated to require $COUNT blocks, aborting further tests."
break
fi
# Clear kernel cache to ensure more accurate test
[ $EUID -eq 0 ] && [ -e /proc/sys/vm/drop_caches ] && echo 3 > /proc/sys/vm/drop_caches
DD_RESULT=$(dd if=$origin of=$target bs=$BLOCK_SIZE count=$COUNT conv=fsync 2>&1 1>/dev/null)
TRANSFER_RATE=$(echo $DD_RESULT | \grep --only-matching -E '[0-9.]+ ([MGk]?B|bytes)/s(ec)?')
printf "$BLOCK_SIZE ($(human_bytes $BLOCK_SIZE)) : $TRANSFER_RATE \n"
done
Donde
unidades=" bytes, KB, MB, GB, TB, PB, EB, ZB, YB"
i=1
base=$(echo "scale=2; $1 / 1" | bc)
variable=$( echo $base / 1024 | bc )
while [ $variable -gt 0 ]
do
let i=i+1
base=$(echo "scale=2; $base / 1024" | bc)
variable=$(echo $base / 1024 | bc )
done
echo $base$(echo $unidades | cut -f$i -d, )
Cuyo resultado se basa en la escritura de datos provenientes de
sudo ./dd-obs.sh /dev/sdd
512 (512b) : 7 MB/s
1024 (1K) : 9 MB/s
2048 (2K) : 0 MB/s
4096 (4K) : 192 MB/s
8192 (8K) : 205 MB/s
16384 (16K) : 204 MB/s
32768 (32K) : 202 MB/s
65536 (64K) : 195 MB/s
131072 (128K) : 201 MB/s
262144 (256K) : 203 MB/s
524288 (512K) : 200 MB/s
1048576 (1M) : 200 MB/s
2097152 (2M) : 197 MB/s
4194304 (4M) : 203 MB/s
8388608 (8M) : 193 MB/s
16777216 (16M) : 197 MB/s
33554432 (32M) : 189 MB/s
67108864 (64M) : 194 MB/s
Lo que lleva a analizar que un tamaño de bloques definido entre 64K y 512K es suficientemente óptimo, tomando como referencia valores más altos en los que apenas se notaría la optimización (y como detallaré más adelante ofrecerá más riesgo) y valores inferiores, contra los que sí que se observa una clara diferencia, especialmente tomando el valor por defecto.
Para analizar el ibs, debemos indicar el disco como if y copiar el contenido of a
BLOCK_SIZE=65536
COUNT=$(($file_SIZE / $BLOCK_SIZE))
dd if=/dev/urandom of=$target bs=$BLOCK_SIZE count=$COUNT > /dev/null 2>&1
for BLOCK_SIZE in 512 1024 2048 4096 8192 16384 32768 65536 131072 262144 524288 1048576 2097152 4194304 8388608 16777216 33554432 67108864
do
DD_RESULT=$(dd if=$target of=/dev/null bs=$BLOCK_SIZE 2>&1 1>/dev/null)
TRANSFER_RATE=$(echo $DD_RESULT | \grep --only-matching -E '[0-9.]+ ([MGk]?B|bytes)/s(ec)?')
printf "$BLOCK_SIZE ($(human_bytes $BLOCK_SIZE)) : $TRANSFER_RATE \n"
done
Ofreciendo un resultado similar, permitiéndonos ajustar el tamaño del bloque con mayor inteligencia:
sudo ./dd-ibs.sh example
512 (512b) : 275 MB/s
1024 (1K) : 373 MB/s
2048 (2K) : 276 MB/s
4096 (4K) : 491 MB/s
8192 (8K) : 134 MB/s
16384 (16K) : 348 MB/s
32768 (32K) : 451 MB/s
65536 (64K) : 486 MB/s
131072 (128K) : 333 MB/s
262144 (256K) : 406 MB/s
524288 (512K) : 257 MB/s
1048576 (1M) : 439 MB/s
2097152 (2M) : 188 MB/s
4194304 (4M) : 152 MB/s
8388608 (8M) : 135 MB/s
16777216 (16M) : 193 MB/s
33554432 (32M) : 120 MB/s
67108864 (64M) : 8 MB/s
Hay que tener en cuenta que, si el tamaño de bloque es, digamos, 1M, dd leerá 1024x1024 bytes y escribirá bytes iguales. Pero si ocurriera un error de lectura, las cosas saldrán mal. Se tiende a pensar que dd llenará los errores de lectura con ceros si usa las opciones
Si tenemos la certeza de que el disco no contiene ningún error, podríamos continuar usando un tamaño de bloque más grande, lo que aumentará la velocidad de su copia varias veces, especialmente siguiendo los parámetros anteriores para una mayor eficiencia. Por ejemplo, en teoría cambiar el bs de 512 a 64K cambiará la velocidad de copia de 35MB/s a 120MB/s, tomando como ejemplo un sistema Celeron de 2.7 GHz. Pero tenga en cuenta que los errores de lectura en el disco de origen terminarán como «errores de bloque» en el disco de destino, es decir, un solo error de lectura de 512 bytes dañará todo el bloque de salida de 64K.