Scrivere programmi per far fronte a errori I / O che causano scritture perse su Linux


138

TL; DR: se il kernel Linux perde una scrittura I / O bufferizzata , c'è modo che l'applicazione lo scopra?

So che devi conservare fsync()il file (e la sua directory principale) per durare nel tempo . La domanda è se il kernel perde buffer sporchi in attesa di scrittura a causa di un errore I / O, come può l'applicazione rilevarlo e ripristinarlo o interromperlo?

Pensa alle applicazioni di database, ecc., Dove l'ordine di scrittura e la durata della scrittura possono essere cruciali.

Scritte perse? Come?

Il layer di blocchi del kernel Linux in alcune circostanze può perdere richieste di I / O bufferizzate che sono state inviate correttamente da write(), pwrite()ecc., Con un errore come:

Buffer I/O error on device dm-0, logical block 12345
lost page write due to I/O error on dm-0

(Vedi end_buffer_write_sync(...)e end_buffer_async_write(...)dentrofs/buffer.c ).

Nei kernel più recenti l'errore conterrà invece "scrittura asincrona persa della pagina" , come:

Buffer I/O error on dev dm-0, logical block 12345, lost async page write

Dato che l'applicazione write()sarà già tornata senza errori, non sembra esserci modo di riportare un errore all'applicazione.

Li stai rilevando?

Non ho molta familiarità con i sorgenti del kernel, ma penso che sia impostato AS_EIOsul buffer che non è stato scritto se sta eseguendo una scrittura asincrona:

    set_bit(AS_EIO, &page->mapping->flags);
    set_buffer_write_io_error(bh);
    clear_buffer_uptodate(bh);
    SetPageError(page);

ma non mi è chiaro se o come l'applicazione possa scoprirlo quando in seguito fsync()è il file per confermare che è sul disco.

Sembra che wait_on_page_writeback_range(...)inmm/filemap.c potenza per mezzo do_sync_mapping_range(...)infs/sync.c cui è il turno chiamato da sys_sync_file_range(...). Restituisce -EIOse non è possibile scrivere uno o più buffer.

Se, come suppongo, questo si propaga al fsync()risultato, allora se l'app va in panico e si blocca se riceve un errore I / O fsync()e sa come fare di nuovo il suo lavoro al riavvio, dovrebbe essere una protezione sufficiente?

Presumibilmente non c'è modo per l'app di sapere quale offset di byte in un file corrisponde alle pagine perse, quindi può riscriverle se sa come, ma se l'app ripete tutto il suo lavoro in sospeso dall'ultimo successo fsync()del file e che riscrive eventuali buffer del kernel sporchi corrispondenti a scritture perse sul file, che dovrebbero cancellare eventuali flag di errore I / O sulle pagine perse e consentire il fsync()completamento del successivo - giusto?

Vi sono quindi altre circostanze, innocue, in cui fsync()potrebbe tornare -EIOdove salvare e rifare il lavoro sarebbe troppo drastico?

Perché?

Naturalmente tali errori non dovrebbero accadere. In questo caso l'errore è nato da una sfortunata interazione tra ildm-multipath impostazioni predefinite del driver e il codice di rilevamento utilizzato dalla SAN per segnalare un errore nell'allocazione dell'archiviazione con thin provisioning. Ma questa non è l'unica circostanza in cui possono accadere: ne ho anche visto delle notizie da LVM con thin provisioning, come quelle usate da libvirt, Docker e altro. Un'applicazione critica come un database dovrebbe cercare di far fronte a tali errori, piuttosto che continuare ciecamente come se tutto andasse bene.

Se la kernel pensa che sia OK perdere scritture senza morire con il panico del kernel, le applicazioni devono trovare un modo per far fronte.

L'impatto pratico è che ho trovato un caso in cui un problema multipath con una SAN ha causato scritture perse che si sono verificate causando la corruzione del database perché il DBMS non sapeva che le sue scritture erano fallite. Non è divertente.


1
Temo che ciò richiederebbe campi aggiuntivi nella tabella SystemFile per memorizzare e ricordare queste condizioni di errore. E una possibilità per il processo userspace di riceverli o ispezionarli nelle chiamate successive. (fsync () e close () restituiscono questo tipo di informazioni storiche ?)
joop

@joop Grazie. Ho appena pubblicato una risposta con ciò che penso stia succedendo, mente avendo un controllo di integrità poiché sembra che tu sappia di più su ciò che sta succedendo rispetto alle persone che hanno pubblicato ovvie varianti di "write () ha bisogno di close () o fsync ( ) per durabilità "senza leggere la domanda?
Craig Ringer,

BTW: Penso che dovresti davvero approfondire i sorgenti del kernel. I filesystem su journal probabilmente soffrirebbero dello stesso tipo di problemi. Per non parlare della gestione delle partizioni di swap. Dato che vivono nello spazio del kernel, la gestione di queste condizioni sarà probabilmente un po 'più rigida. writev (), che è visibile dallo spazio utente, sembra anche un posto dove guardare. [a Craig: sì perché conosco il tuo nome e so che non sei un completo idiota; -]
joop

1
Sono d'accordo, non ero così giusto. Purtroppo la tua risposta non è molto soddisfacente, intendo dire che non esiste una soluzione facile (sorprendente?).
Jean-Baptiste Yunès

1
@ Jean-BaptisteYunès True. Per il DBMS con cui sto lavorando, "crash and enter redo" è accettabile. Per la maggior parte delle app non è un'opzione e potrebbero dover tollerare le orribili prestazioni dell'I / O sincrono o semplicemente accettare comportamenti e corruzione poco definiti in caso di errori I / O.
Craig Ringer

Risposte:


91

fsync()ritorna -EIOse il kernel ha perso una scrittura

(Nota: la prima parte fa riferimento ai kernel più vecchi; aggiornata di seguito per riflettere i kernel moderni)

Sembra che la scrittura del buffer asincrono negli end_buffer_async_write(...)errori abbia impostato un -EIOflag sulla pagina del buffer sporco non riuscita per il file :

set_bit(AS_EIO, &page->mapping->flags);
set_buffer_write_io_error(bh);
clear_buffer_uptodate(bh);
SetPageError(page);

che viene poi rilevata da wait_on_page_writeback_range(...)come richiesto dalla do_sync_mapping_range(...)chiamati da sys_sync_file_range(...)come richiesto dalla sys_sync_file_range2(...)per attuare la chiamata di libreria C fsync().

Ma solo una volta!

Questo commento su sys_sync_file_range

168  * SYNC_FILE_RANGE_WAIT_BEFORE and SYNC_FILE_RANGE_WAIT_AFTER will detect any
169  * I/O errors or ENOSPC conditions and will return those to the caller, after
170  * clearing the EIO and ENOSPC flags in the address_space.

suggerisce che quando fsync()ritorna -EIOo (non documentato nella manpage) -ENOSPC, cancella lo stato di errore in modo che un successivo fsync()riporti il ​​successo anche se le pagine non sono mai state scritte.

Abbastanza sicuro wait_on_page_writeback_range(...) cancella i bit di errore quando li verifica :

301         /* Check for outstanding write errors */
302         if (test_and_clear_bit(AS_ENOSPC, &mapping->flags))
303                 ret = -ENOSPC;
304         if (test_and_clear_bit(AS_EIO, &mapping->flags))
305                 ret = -EIO;

Quindi, se l'applicazione si aspetta che possa riprovare fsync()fino a quando non riesce e si fida che i dati siano su disco, è terribilmente sbagliato.

Sono abbastanza sicuro che questa sia la fonte della corruzione dei dati che ho trovato nel DBMS. Riprovafsync() e pensa che tutto andrà bene quando avrà successo.

È permesso?

I documenti POSIX / SuS sufsync() non specificano in realtà in entrambi i casi:

Se la funzione fsync () ha esito negativo, non è garantito che le operazioni di I / O in sospeso siano state completate.

La pagina man di Linux perfsync() non dice nulla su ciò che accade in caso di fallimento.

Quindi sembra che il significato degli fsync()errori sia "non so cosa sia successo alle tue scritture, potrebbe aver funzionato o meno, meglio riprovare per esserne sicuro".

Kernel più recenti

Su 4.9 end_buffer_async_writeimposta -EIOsulla pagina, semplicemente via mapping_set_error.

    buffer_io_error(bh, ", lost async page write");
    mapping_set_error(page->mapping, -EIO);
    set_buffer_write_io_error(bh);
    clear_buffer_uptodate(bh);
    SetPageError(page);

Per quanto riguarda la sincronizzazione, penso che sia simile, anche se ora la struttura è piuttosto complessa da seguire. filemap_check_errorsin mm/filemap.cora fa:

    if (test_bit(AS_EIO, &mapping->flags) &&
        test_and_clear_bit(AS_EIO, &mapping->flags))
            ret = -EIO;

che ha più o meno lo stesso effetto. Sembra che tutti i controlli degli errori passino attraverso filemap_check_errorsun test-and-clear:

    if (test_bit(AS_EIO, &mapping->flags) &&
        test_and_clear_bit(AS_EIO, &mapping->flags))
            ret = -EIO;
    return ret;

Sto usando btrfssul mio laptop, ma quando creo un ext4loopback per testare /mnt/tmpe impostare una sonda perf su di esso:

sudo dd if=/dev/zero of=/tmp/ext bs=1M count=100
sudo mke2fs -j -T ext4 /tmp/ext
sudo mount -o loop /tmp/ext /mnt/tmp

sudo perf probe filemap_check_errors

sudo perf record -g -e probe:end_buffer_async_write -e probe:filemap_check_errors dd if=/dev/zero of=/mnt/tmp/test bs=4k count=1 conv=fsync

Trovo il seguente stack di chiamate in perf report -T:

        ---__GI___libc_fsync
           entry_SYSCALL_64_fastpath
           sys_fsync
           do_fsync
           vfs_fsync_range
           ext4_sync_file
           filemap_write_and_wait_range
           filemap_check_errors

Un read-through suggerisce che sì, i kernel moderni si comportano allo stesso modo.

Questo sembra significare che se fsync()(o presumibilmente write()o close()) ritorna -EIO, il file è in uno stato indefinito tra ultima volta che hai con successo fsync()D o close()D e il suo più recente write()stato di dieci.

Test

Ho implementato un test case per dimostrare questo comportamento .

implicazioni

Un DBMS può far fronte inserendo il ripristino di arresto anomalo. Come mai una normale applicazione utente dovrebbe farcela? La fsync()pagina man non avvisa che significa "fsync-if-you-like-it-it" e mi aspetto che molte app non riescano a gestire bene questo comportamento.

Segnalazioni di bug

Ulteriori letture

lwn.net lo ha toccato nell'articolo "Gestione degli errori a livello di blocco migliorata" .

postgresql.org thread della mailing list .


3
lxr.free-electrons.com/source/fs/buffer.c?v=2.6.26#L598 è una gara possibile, perché attende {I / O in sospeso e pianificato}, non {I / O non ancora pianificato}. Questo ovviamente per evitare ulteriori round trip per il dispositivo. (Presumo che l'utente scrive () non ritorna fino a quando l'I / O non è programmato, per mmap (), questo è diverso)
joop

3
È possibile che la chiamata di qualche altro processo a fsync per qualche altro file sullo stesso disco ottenga la restituzione dell'errore?
Casuale 832,

3
@ Random832 Molto rilevante per un DB multi-elaborazione come PostgreSQL, quindi una buona domanda. Sembra probabilmente, ma non conosco abbastanza bene il codice del kernel per capirlo. È meglio che i tuoi proc cooperino se entrambi hanno comunque lo stesso file aperto.
Craig Ringer,

1
@DavidFoerster: le syscalls restituiscono errori usando codici errno negativi; errnoè completamente un costrutto della libreria C degli spazi utente. È comune ignorare le differenze di valore di ritorno tra le syscalls e la libreria C in questo modo (come fa Craig Ringer, sopra), poiché il valore di ritorno dell'errore identifica in modo affidabile quale (funzione syscall o libreria C) viene indicato: " -1con errno==EIO"si riferisce a una funzione di libreria C, mentre" -EIO"si riferisce a una syscall. Infine, le pagine man di Linux online sono il riferimento più aggiornato per le pagine man di Linux.
Animale nominale

2
@CraigRinger: per rispondere alla tua domanda finale: "Usando I / O di basso livello e fsync()/ fdatasync()quando la dimensione della transazione è un file completo; usando mmap()/ msync()quando la dimensione della transazione è un record allineato alla pagina; e usando I di basso livello / O, fdatasync()e più descrittori di file simultanei (un descrittore e un thread per transazione) nello stesso file altrimenti " . I blocchi di descrizione dei file aperti specifici di Linux ( fcntl(), F_OFD_) sono molto utili con l'ultimo.
Animale nominale

22

Poiché write () dell'applicazione sarà già tornato senza errori, non sembra esserci modo di riportare un errore all'applicazione.

Non sono d'accordo. writepuò restituire senza errori se la scrittura viene semplicemente messa in coda, ma l'errore verrà segnalato all'operazione successiva che richiederà la scrittura effettiva sul disco, ovvero alla successiva fsync, possibilmente su una scrittura successiva se il sistema decide di svuotare la cache e at almeno sull'ultima chiusura del file.

Questo è il motivo per cui è essenziale che l'applicazione verifichi il valore di ritorno di close per rilevare possibili errori di scrittura.

Se hai davvero bisogno di essere in grado di eseguire un'elaborazione intelligente degli errori, devi presumere che tutto ciò che è stato scritto dall'ultimo successo fsync potrebbe non essere riuscito e che in tutto ciò almeno qualcosa non è riuscito.


4
Sì, penso che lo inchioda. Questo sarebbe davvero suggerire che l'applicazione deve rifare tutto il suo lavoro dopo l'ultimo confermato-successo fsync()o close()del file se si ottiene un -EIOda write(), fsync()o close(). Beh, è ​​divertente.
Craig Ringer,

1

write(2) fornisce meno di quanto ti aspetti. La pagina man è molto aperta sulla semantica di una write()chiamata riuscita :

Un ritorno riuscito da write()non garantisce che i dati siano stati impegnati su disco. In effetti, su alcune implementazioni errate, non garantisce nemmeno che lo spazio sia stato riservato con successo per i dati. L'unico modo per essere sicuri è chiamare fsync(2) dopo aver finito di scrivere tutti i tuoi dati.

Possiamo concludere che un successo write()significa semplicemente che i dati hanno raggiunto le strutture di buffering del kernel. Se il persistere del buffer ha esito negativo, un successivo accesso al descrittore di file restituirà il codice di errore. Come ultima risorsa che potrebbe essere close(). La pagina man della close(2) chiamata di sistema contiene la seguente frase:

È del tutto possibile che gli errori di un'operazione precedente write(2) vengano segnalati per la prima volta in finale close().

Se la tua applicazione deve persistere nella scrittura dei dati, deve utilizzare fsync/ fsyncdatasu base regolare:

fsync()trasferisce ("svuota") tutti i dati in-core modificati di (ovvero pagine cache buffer modificate per) il file a cui fa riferimento il descrittore di file fd sul dispositivo disco (o altro dispositivo di archiviazione permanente) in modo che tutte le informazioni modificate possano essere recuperate anche dopo il crash del sistema o il riavvio. Ciò include la scrittura o lo svuotamento di una cache del disco, se presente. La chiamata si blocca fino a quando il dispositivo segnala che il trasferimento è stato completato.


4
Sì, sono consapevole che fsync()è richiesto. Ma nel caso specifico in cui il kernel perde le pagine a causa di un errore di I / O sarà fsync()fallire? In quali circostanze può poi avere successo in seguito?
Craig Ringer,

Neanche io conosco l'origine del kernel. Supponiamo che fsync()ritorni -EIOsui problemi di I / O (cosa sarebbe utile altrimenti?). Quindi il database sa che una delle precedenti scritture non è riuscita e potrebbe andare in modalità di recupero. Non è quello che vuoi? Qual è la motivazione della tua ultima domanda? Vuoi sapere quale scrittura non è riuscita o ripristinare il descrittore di file per un ulteriore utilizzo?
fzgregor,

Idealmente un DBMS preferirà non avviare il ripristino di emergenza (dando il via a tutti gli utenti e diventando temporaneamente inaccessibile o almeno di sola lettura) se è possibile evitarlo. Ma anche se il kernel ci potesse dire "byte da 4096 a 8191 di fd X", sarebbe difficile capire cosa (ri) scrivere lì senza praticamente fare il crash crash. Quindi immagino che la domanda principale sia se ci sono altre circostanze innocenti in cui fsync()può tornare -EIOdove è sicuro riprovare e se è possibile dire la differenza.
Craig Ringer,

Il recupero sicuro è l'ultima risorsa. Ma come hai già detto, questi problemi dovrebbero essere molto rari. Pertanto, non vedo alcun problema con il recupero su qualsiasi -EIO. Se ogni descrittore di file viene utilizzato da un solo thread alla volta, questo thread potrebbe tornare all'ultimo fsync()e ripetere le write()chiamate. Tuttavia, se quelli write()scrivono solo parte di un settore, la parte non modificata potrebbe essere comunque corrotta.
fzgregor

1
Hai ragione nel ritenere che il ripristino di emergenza sia ragionevole. Per quanto riguarda i settori parzialmente corrotti, il DBMS (PostgreSQL) memorizza un'immagine dell'intera pagina la prima volta che la tocca dopo un determinato checkpoint per questo motivo, quindi dovrebbe andare bene :)
Craig Ringer

0

Utilizzare il flag O_SYNC quando si apre il file. Assicura che i dati vengano scritti sul disco.

Se questo non ti soddisfa, non ci sarà nulla.


17
O_SYNCè un incubo per le prestazioni. Significa che l'applicazione non può fare nient'altro mentre si sta verificando l'I / O del disco a meno che non venga generato thread I / O. Si potrebbe anche dire che l'interfaccia I / O bufferizzata non è sicura e che tutti dovrebbero usare AIO. Le scritture perse silenziosamente non possono essere accettabili nell'I / O bufferato?
Craig Ringer,

3
( O_DATASYNCè solo leggermente migliore in tal senso)
Craig Ringer

@CraigRinger Si consiglia di utilizzare AIO se avete questa necessità e bisogno di alcun tipo di prestazioni. O semplicemente usa un DBMS; gestisce tutto per te.
Demi,

10
@Demi L'applicazione qui è un dbms (postgresql). Sono sicuro che puoi immaginare che riscrivere l'intera applicazione per utilizzare AIO anziché l'I / O bufferizzato non sia pratico. Né dovrebbe essere necessario.
Craig Ringer,

-5

Controlla il valore di ritorno di close. la chiusura può fallire mentre le scritture bufferizzate sembrano avere successo.


8
Bene, difficilmente vogliamo che il file venga open()importato e modificato close()ogni pochi secondi. ecco perché abbiamo fsync()...
Craig Ringer,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.