Por que é mais lento gravar os mesmos dados em um arquivo * maior * pré-alocado?

3

Estou escrevendo blocos de 4 * 4 KB em um arquivo. É consistentemente cerca de 50% mais lento se eu usei fallocate() para pré-alocar o arquivo com 9 blocos, em vez de apenas pré-alocar os 4 blocos. Por quê?

Parece haver um ponto de corte entre a pré-alocação de 8 e 9 blocos. Também estou me perguntando por que as gravações de primeiro e segundo bloco são consistentemente mais lentas.

Este teste é resumido em algum código de cópia de arquivo com o qual estou jogando. Inspirado por esta pergunta sobre dd , eu Estou usando O_DSYNC escreve para que eu possa medir o progresso real das gravações em disco. (A ideia era começar a copiar um bloco pequeno para medir a latência mínima e, em seguida, aumentar o tamanho do bloco de forma adaptável para melhorar o rendimento).

Estou testando o Fedora 28, em um laptop com um disco rígido giratório. Ele foi atualizado de um Fedora anterior, então o sistema de arquivos não é novinho em folha. Eu não acho que estou mexendo nos padrões do sistema de arquivos.

  • Kernel: 4.17.19-200.fc28.x86_64
  • Sistema de arquivos: ext4, no LVM.
  • Opções de montagem: rw, relatime, seclabel
  • Campos de tune2fs -l
    • Opções de montagem padrão: user_xattr acl
    • Recursos do sistema de arquivos: has_journal ext_attr resize_inode dir_index tipo de arquivo needs_recovery extent 64 bits flex_bg sparse_super large_file huge_file dir_nlink extra_isize
    • Sinalizadores do sistema de arquivos: signed_directory_hash
    • Tamanho do bloco: 4096
    • Blocos gratuitos: 7866091

Horários de strace -s3 -T test-program.py :

openat(AT_FDCWD, "out.tmp", O_WRONLY|O_CREAT|O_TRUNC|O_DSYNC|O_CLOEXEC, 0777) = 3 <0.000048>
write(3, "
#! /usr/bin/python3
import os

# Required third party module,
# install with "pip3 install --user fallocate".
from fallocate import fallocate

block = b'
openat(AT_FDCWD, "out.tmp", O_WRONLY|O_CREAT|O_TRUNC|O_DSYNC|O_CLOEXEC, 0777) = 3 <0.000048>
write(3, "
#! /usr/bin/python3
import os

# Required third party module,
# install with "pip3 install --user fallocate".
from fallocate import fallocate

block = b'%pre%' * 4096

for alloc in [0, 4, 8, 9]:
    # Open file for writing, with implicit fdatasync().
    fd = os.open("out.tmp", os.O_WRONLY | os.O_DSYNC |
                            os.O_CREAT | os.O_TRUNC)

    # Try to pre-allocate space
    if alloc:
        fallocate(fd, 0, alloc * 4096)

    os.write(fd, block)
    os.write(fd, block)
    os.write(fd, block)
    os.write(fd, block)

    os.close(fd)
%pre%%pre%"..., 4096) = 4096 <0.036378> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.033380> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.033359> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.033399> close(3) = 0 <0.000033> openat(AT_FDCWD, "out.tmp", O_WRONLY|O_CREAT|O_TRUNC|O_DSYNC|O_CLOEXEC, 0777) = 3 <0.000110> fallocate(3, 0, 0, 16384) = 0 <0.016467> fsync(3) = 0 <0.000201> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.033062> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.013806> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.008324> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.008346> close(3) = 0 <0.000025> openat(AT_FDCWD, "out.tmp", O_WRONLY|O_CREAT|O_TRUNC|O_DSYNC|O_CLOEXEC, 0777) = 3 <0.000070> fallocate(3, 0, 0, 32768) = 0 <0.019096> fsync(3) = 0 <0.000311> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.032882> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.010824> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.008188> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.008266> close(3) = 0 <0.000012> openat(AT_FDCWD, "out.tmp", O_WRONLY|O_CREAT|O_TRUNC|O_DSYNC|O_CLOEXEC, 0777) = 3 <0.000050> fallocate(3, 0, 0, 36864) = 0 <0.022417> fsync(3) = 0 <0.000260> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.032953> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.033265> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.033317> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.033237> close(3) = 0 <0.000019>
' * 4096 for alloc in [0, 4, 8, 9]: # Open file for writing, with implicit fdatasync(). fd = os.open("out.tmp", os.O_WRONLY | os.O_DSYNC | os.O_CREAT | os.O_TRUNC) # Try to pre-allocate space if alloc: fallocate(fd, 0, alloc * 4096) os.write(fd, block) os.write(fd, block) os.write(fd, block) os.write(fd, block) os.close(fd)
%pre%%pre%"..., 4096) = 4096 <0.036378> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.033380> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.033359> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.033399> close(3) = 0 <0.000033> openat(AT_FDCWD, "out.tmp", O_WRONLY|O_CREAT|O_TRUNC|O_DSYNC|O_CLOEXEC, 0777) = 3 <0.000110> fallocate(3, 0, 0, 16384) = 0 <0.016467> fsync(3) = 0 <0.000201> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.033062> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.013806> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.008324> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.008346> close(3) = 0 <0.000025> openat(AT_FDCWD, "out.tmp", O_WRONLY|O_CREAT|O_TRUNC|O_DSYNC|O_CLOEXEC, 0777) = 3 <0.000070> fallocate(3, 0, 0, 32768) = 0 <0.019096> fsync(3) = 0 <0.000311> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.032882> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.010824> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.008188> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.008266> close(3) = 0 <0.000012> openat(AT_FDCWD, "out.tmp", O_WRONLY|O_CREAT|O_TRUNC|O_DSYNC|O_CLOEXEC, 0777) = 3 <0.000050> fallocate(3, 0, 0, 36864) = 0 <0.022417> fsync(3) = 0 <0.000260> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.032953> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.033265> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.033317> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.033237> close(3) = 0 <0.000019>

test-program.py:

%pre%     
por sourcejedi 15.09.2018 / 19:50

2 respostas

3

O motivo da diferença entre os blocos de 8 e 9 de 4 KB é porque o ext4 tem uma heurística ao converter uma extensão não alocada criada por fallocate() para uma extensão alocada. Para extensões não alocadas de 32KB ou menos, ela preenche toda a extensão com zeros e reescreve tudo, enquanto extensões maiores são divididas em duas ou três extensões menores e escritas.

No caso de 8 blocos, toda a extensão de 32KB é convertida em uma extensão normal, os primeiros 16KB são gravados com seus dados e o restante é preenchido com zero e escrito. No caso de 9 blocos, a extensão de 36 KB é dividida (porque tem mais de 32 KB), e você fica com uma extensão de 16 KB para seus dados e uma extensão não escrita de 20 KB.

Estritamente falando, a extensão não escrita de 20 KB também deve ser preenchida e anotada, mas suspeito que isso não seja feito. No entanto, isso apenas alteraria o ponto de equilíbrio um pouco (para 16 KB + 32 KB = 12 blocos no seu caso), mas não mudaria o comportamento subjacente.

Você pode usar filefrag -v out.tmp após a primeira gravação para ver o layout de alocação de blocos no disco.

Dito isso, você poderia evitar completamente o fallocate e o O_DSYNC e deixar o sistema de arquivos fazer o trabalho de escrever os dados o mais rápido possível, em vez de tornar o layout do arquivo pior do que o necessário ...

    
por 16.09.2018 / 09:48
2

Essa diferença pode parecer interessante, mas o mais importante é entender que você está abusando de fallocate() . fallocate() só é garantido para reservar espaço no disco. Não é garantido melhorar o desempenho de gravações síncronas, ou seja, evitar gravações em metadados do sistema de arquivos que exigem uma busca de disco.

Você pode ilustrar isso modificando test-program.py para pré-gravar alguns blocos de dados em vez de usar fallocate() . No meu sistema de arquivos ext4 , isso fornece a menor medição de "latência mínima" para qualquer tamanho de pré-alocação. Devo observar que outros sistemas de arquivos terão perfis de desempenho diferentes. Especificamente, isso não funcionaria se eles fossem implementados usando copy-on-write como btrfs .

Alteração de código:

     # Try to pre-allocate space
     if alloc:
-        fallocate(fd, 0, alloc * 4096)
+        os.pwrite(fd, block * alloc, 0)
+        os.fsync(fd)

Resultados:

openat(AT_FDCWD, "out.tmp", O_WRONLY|O_CREAT|O_TRUNC|O_DSYNC|O_CLOEXEC, 0777) = 3 <0.000088>
pwrite64(3, "
     # Try to pre-allocate space
     if alloc:
-        fallocate(fd, 0, alloc * 4096)
+        os.pwrite(fd, block * alloc, 0)
+        os.fsync(fd)
openat(AT_FDCWD, "out.tmp", O_WRONLY|O_CREAT|O_TRUNC|O_DSYNC|O_CLOEXEC, 0777) = 3 <0.000088>
pwrite64(3, "%pre%%pre%%pre%"..., 36864, 0)      = 36864 <0.035337>
fsync(3)                                = 0 <0.000366>
write(3, "%pre%%pre%%pre%"..., 4096)             = 4096 <0.015217>
write(3, "%pre%%pre%%pre%"..., 4096)             = 4096 <0.008194>
write(3, "%pre%%pre%%pre%"..., 4096)             = 4096 <0.008371>
write(3, "%pre%%pre%%pre%"..., 4096)             = 4096 <0.008299>
close(3)                                = 0 <0.000034>
%pre%"..., 36864, 0) = 36864 <0.035337> fsync(3) = 0 <0.000366> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.015217> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.008194> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.008371> write(3, "%pre%%pre%%pre%"..., 4096) = 4096 <0.008299> close(3) = 0 <0.000034>
    
por 15.09.2018 / 20:36