Almacenamento de datos duradeiro e API de ficheiros de Linux

Eu, investigando a estabilidade do almacenamento de datos nos sistemas na nube, decidín probarme, para asegurarme de que entendo as cousas básicas. eu comezou lendo as especificacións de NVMe para comprender que garantías de persistencia dos datos (é dicir, garantías de que os datos estarán dispoñibles despois dun fallo do sistema) nos ofrecen os discos NMVe. Fixen as seguintes conclusións principais: cómpre considerar os datos danados desde o momento en que se dá o comando de escritura de datos e ata o momento en que se escriben no medio de almacenamento. Non obstante, na maioría dos programas, as chamadas do sistema úsanse con bastante seguridade para escribir datos.

Neste artigo, exploro os mecanismos de persistencia proporcionados polas API de ficheiros de Linux. Parece que aquí todo debería ser sinxelo: o programa chama ao comando write(), e despois de completar a operación deste comando, os datos almacenaranse de forma segura no disco. Pero write() só copia os datos da aplicación na caché do núcleo situada na RAM. Para forzar o sistema a escribir datos no disco, débense utilizar algúns mecanismos adicionais.

Almacenamento de datos duradeiro e API de ficheiros de Linux

En xeral, este material é un conxunto de notas relacionadas co que aprendín sobre un tema do meu interese. Se falamos moi brevemente do máis importante, resulta que para organizar un almacenamento de datos sostible é necesario utilizar o comando fdatasync() ou abrir ficheiros con bandeira O_DSYNC. Se estás interesado en saber máis sobre o que ocorre cos datos no camiño do código ao disco, bótalle unha ollada isto artigo.

Características do uso da función write().

Chamada do sistema write() definido na norma IEEE POSIX como un intento de escribir datos nun descritor de ficheiros. Despois de rematar con éxito o traballo write() As operacións de lectura de datos deben devolver exactamente os bytes que se escribiron anteriormente, aínda que se acceda aos datos desde outros procesos ou fíos (velaquí sección correspondente do estándar POSIX). Aquí, na sección sobre a interacción dos fíos coas operacións normais de ficheiros, hai unha nota que di que se dous fíos chaman cada un a estas funcións, entón cada chamada debe ver todas as consecuencias indicadas ás que leva a execución da outra chamada, ou ben. non veo nada sen consecuencias. Isto leva á conclusión de que todas as operacións de E/S de ficheiros deben manter un bloqueo no recurso no que se está a traballar.

Significa isto que a operación write() é atómico? Dende o punto de vista técnico, si. As operacións de lectura de datos deben devolver todo ou nada do que se escribiu write(). Pero a operación write(), de acordo coa norma, non ten por que rematar, tendo anotado todo o que lle pediron que anotase. Permítese escribir só parte dos datos. Por exemplo, podemos ter dous fluxos cada un engadindo 1024 bytes a un ficheiro descrito polo mesmo descritor de ficheiros. Desde o punto de vista do estándar, o resultado será aceptable cando cada unha das operacións de escritura poida engadir só un byte ao ficheiro. Estas operacións permanecerán atómicas, pero despois de completarse, os datos que escriben no ficheiro mesturaranse. Aquí discusión moi interesante sobre este tema en Stack Overflow.

funcións fsync() e fdatasync().

A forma máis sinxela de limpar os datos no disco é chamar á función fsync(). Esta función pídelle ao sistema operativo que mova todos os bloques modificados da caché ao disco. Isto inclúe todos os metadatos do ficheiro (tempo de acceso, tempo de modificación do ficheiro, etc.). Creo que estes metadatos son raramente necesarios, polo que se sabes que non é importante para ti, podes usar a función fdatasync(). En axuda en fdatasync() di que durante o funcionamento desta función, tal cantidade de metadatos gárdase no disco, o que é "necesario para a correcta execución das seguintes operacións de lectura de datos". E isto é exactamente o que lle importa á maioría das aplicacións.

Un problema que pode xurdir aquí é que estes mecanismos non garanten que se poida atopar o ficheiro despois dun posible fallo. En particular, cando se crea un ficheiro novo, débese chamar fsync() para o directorio que o contén. Se non, despois dun fallo, pode resultar que este ficheiro non existe. A razón disto é que baixo UNIX, debido ao uso de ligazóns duras, un ficheiro pode existir en varios directorios. Polo tanto, ao chamar fsync() non hai forma de que un ficheiro saiba que datos do directorio tamén se deben lavar ao disco (aquí podes ler máis sobre isto). Parece que o sistema de ficheiros ext4 é capaz automaticamente para aplicar fsync() a directorios que conteñan os ficheiros correspondentes, pero isto pode non ser o caso doutros sistemas de ficheiros.

Este mecanismo pódese implementar de forma diferente en diferentes sistemas de ficheiros. usei traza negra para saber que operacións de disco se usan nos sistemas de ficheiros ext4 e XFS. Ambos emiten os comandos habituais de escritura no disco tanto para o contido dos ficheiros como para o diario do sistema de ficheiros, limpa a caché e saen realizando unha escritura FUA (Force Unit Access, escribindo datos directamente no disco, evitando a caché) escribir no diario. Probablemente o fagan para confirmar o feito da transacción. Nas unidades que non admiten FUA, isto provoca dous vaciados da caché. Os meus experimentos demostrárono fdatasync() un pouco máis rápido fsync(). Utilidade blktrace indica que fdatasync() normalmente escribe menos datos no disco (en ext4 fsync() escribe 20 KiB, e fdatasync() - 16 KiB). Ademais, descubrín que XFS é un pouco máis rápido que ext4. E aquí coa axuda blktrace puido descubrir iso fdatasync() descarga menos datos no disco (4 KiB en XFS).

Situacións ambiguas ao usar fsync()

Pódese pensar en tres situacións ambiguas fsync()que atopei na práctica.

O primeiro incidente deste tipo ocorreu en 2008. Nese momento, a interface de Firefox 3 "conxelaba" se se estaba a escribir un gran número de ficheiros no disco. O problema foi que a implementación da interface utilizou unha base de datos SQLite para almacenar información sobre o seu estado. Despois de cada cambio que ocorreu na interface, chamouse a función fsync(), que daba boas garantías de almacenamento de datos estable. No sistema de ficheiros ext3 usado entón, a función fsync() borrou no disco todas as páxinas "sucias" do sistema, e non só as que estaban relacionadas co ficheiro correspondente. Isto significaba que facer clic nun botón en Firefox podería provocar que se escribiran megabytes de datos nun disco magnético, o que podía levar moitos segundos. A solución ao problema, ata onde eu entendín el material, era mover o traballo coa base de datos a tarefas de fondo asíncronas. Isto significa que Firefox adoitaba implementar requisitos de persistencia de almacenamento máis estrictos dos que realmente se necesitaban, e as funcións do sistema de ficheiros ext3 só agravaron este problema.

O segundo problema ocorreu en 2009. Despois, despois dunha falla do sistema, os usuarios do novo sistema de ficheiros ext4 descubriron que moitos ficheiros recén creados eran de lonxitude cero, pero isto non ocorreu co sistema de ficheiros ext3 máis antigo. No parágrafo anterior, falei de como ext3 volcaba demasiados datos no disco, o que ralentizaba moito as cousas. fsync(). Para mellorar a situación, ext4 limpa só aquelas páxinas "sucias" que sexan relevantes para un ficheiro en particular. E os datos doutros ficheiros permanecen na memoria moito máis tempo que con ext3. Isto fíxose para mellorar o rendemento (por defecto, os datos permanecen neste estado durante 30 segundos, podes configuralo usando dirty_expire_centisecs; aquí podes atopar máis información sobre isto). Isto significa que unha gran cantidade de datos pode perderse irremediablemente despois dun accidente. A solución a este problema é usar fsync() en aplicacións que precisan proporcionar un almacenamento de datos estable e protexelos na medida do posible das consecuencias dos fallos. Función fsync() funciona moito máis eficientemente con ext4 que con ext3. A desvantaxe deste enfoque é que o seu uso, como antes, ralentiza algunhas operacións, como a instalación de programas. Ver detalles sobre isto aquí и aquí.

O terceiro problema relativo fsync(), orixinado en 2018. Entón, no marco do proxecto PostgreSQL, descubriuse que se a función fsync() atopa un erro, marca as páxinas "sucias" como "limpas". Como resultado, as seguintes convocatorias fsync() non facer nada con esas páxinas. Por iso, as páxinas modificadas gárdanse na memoria e nunca se escriben no disco. Este é un verdadeiro desastre, porque a aplicación pensará que algúns datos están escritos no disco, pero de feito non o será. Tales fracasos fsync() son raros, a aplicación en tales situacións non pode facer case nada para combater o problema. Nestes días, cando isto ocorre, PostgreSQL e outras aplicacións fallan. Aquí, no artigo "¿Poden as aplicacións recuperarse de fallos de fsync?", explórase este problema en detalle. Actualmente, a mellor solución a este problema é usar Direct I/O coa bandeira O_SYNC ou cunha bandeira O_DSYNC. Con este enfoque, o sistema informará dos erros que poidan ocorrer ao realizar operacións específicas de escritura de datos, pero este enfoque require que a aplicación xestione os propios búfers. Ler máis sobre iso aquí и aquí.

Apertura de ficheiros usando as marcas O_SYNC e O_DSYNC

Volvamos á discusión dos mecanismos de Linux que proporcionan almacenamento de datos persistente. É dicir, estamos a falar de usar a bandeira O_SYNC ou bandeira O_DSYNC ao abrir ficheiros mediante a chamada do sistema abrir(). Con este enfoque, cada operación de escritura de datos realízase coma despois de cada comando write() o sistema recibe, respectivamente, comandos fsync() и fdatasync(). En Especificacións de POSIX isto chámase "Finalización da integridade do ficheiro de E/S sincronizada" e "Finalización da integridade dos datos". A principal vantaxe deste enfoque é que só se debe executar unha chamada ao sistema para garantir a integridade dos datos, e non dúas (por exemplo - write() и fdatasync()). A principal desvantaxe deste enfoque é que todas as operacións de escritura que utilicen o descritor de ficheiros correspondente estarán sincronizadas, o que pode limitar a capacidade de estruturar o código da aplicación.

Usando Direct I/O coa marca O_DIRECT

Chamada do sistema open() apoia a bandeira O_DIRECT, que está deseñado para evitar a caché do sistema operativo, realizar operacións de E/S, interactuando directamente co disco. Isto, en moitos casos, significa que os comandos de escritura emitidos polo programa traduciranse directamente en comandos destinados a traballar co disco. Pero, en xeral, este mecanismo non é un substituto das funcións fsync() ou fdatasync(). O feito é que o propio disco pode atraso ou caché comandos adecuados para escribir datos. E, aínda peor, nalgúns casos especiais, as operacións de E/S realizadas ao usar a bandeira O_DIRECT, emisión en operacións tradicionais de buffer. A forma máis sinxela de resolver este problema é usar a bandeira para abrir ficheiros O_DSYNC, o que significará que cada operación de escritura será seguida dunha chamada fdatasync().

Resultou que o sistema de ficheiros XFS engadira recentemente un "camiño rápido" para O_DIRECT|O_DSYNC-Rexistros de datos. Se o bloque se sobrescribe usando O_DIRECT|O_DSYNC, entón XFS, en lugar de limpar a caché, executará o comando de escritura FUA se o dispositivo o admite. Verifiqueino usando a utilidade blktrace nun sistema Linux 5.4/Ubuntu 20.04. Este enfoque debería ser máis eficiente, xa que escribe a cantidade mínima de datos no disco e usa unha operación, non dúas (escribir e limpar a caché). Atopei unha ligazón para parche 2018 núcleo que implementa este mecanismo. Hai algunha discusión sobre a aplicación desta optimización a outros sistemas de ficheiros, pero polo que sei, XFS é o único sistema de ficheiros que o admite ata agora.

función sync_file_range().

Linux ten unha chamada ao sistema intervalo_de_ficheros de sincronización(), o que lle permite limpar só parte do ficheiro no disco, non o ficheiro completo. Esta chamada inicia unha descarga asíncrona e non espera a que se complete. Pero na referencia a sync_file_range() dise que este comando é "moi perigoso". Non se recomenda usalo. Características e perigos sync_file_range() moi ben descrito en isto material. En particular, esta chamada parece usar RocksDB para controlar cando o núcleo laxa os datos "sucios" no disco. Pero ao mesmo tempo alí, para garantir o almacenamento de datos estable, tamén se usa fdatasync(). En código RocksDB ten algúns comentarios interesantes sobre este tema. Por exemplo, parece a chamada sync_file_range() ao usar ZFS non limpa os datos no disco. A experiencia dime que o código que se usa raramente pode conter erros. Polo tanto, desaconsello usar esta chamada de sistema a menos que sexa absolutamente necesario.

Chamadas ao sistema para axudar a garantir a persistencia dos datos

Cheguei á conclusión de que hai tres enfoques que se poden usar para realizar operacións de E/S persistentes. Todos eles requiren unha chamada de función fsync() para o directorio onde se creou o ficheiro. Estes son os enfoques:

  1. Chamada de funcións fdatasync() ou fsync() despois da función write() (mellor usar fdatasync()).
  2. Traballar cun descritor de ficheiro aberto cunha marca O_DSYNC ou O_SYNC (mellor - cunha bandeira O_DSYNC).
  3. Uso de comandos pwritev2() con bandeira RWF_DSYNC ou RWF_SYNC (preferentemente cunha bandeira RWF_DSYNC).

Notas de actuación

Non medii coidadosamente o rendemento dos distintos mecanismos que investiguei. As diferenzas que notei na velocidade do seu traballo son moi pequenas. Isto significa que podo estar equivocado, e que noutras condicións o mesmo pode mostrar resultados diferentes. En primeiro lugar, falarei do que afecta máis ao rendemento e, despois, do que afecta menos ao rendemento.

  1. Sobrescribir os datos do ficheiro é máis rápido que engadir datos a un ficheiro (a ganancia de rendemento pode ser do 2 ao 100 %). Achegar datos a un ficheiro require cambios adicionais nos metadatos do ficheiro, mesmo despois da chamada do sistema fallocate(), pero a magnitude deste efecto pode variar. Recomendo, para o mellor rendemento, chamar fallocate() para asignar previamente o espazo necesario. Entón, este espazo debe encherse explícitamente con ceros e chamalo fsync(). Isto fará que os bloques correspondentes no sistema de ficheiros se marquen como "asignados" en lugar de "non asignados". Isto dá unha pequena mellora do rendemento (aproximadamente un 2%). Ademais, algúns discos poden ter unha operación de acceso ao primeiro bloque máis lenta que outros. Isto significa que encher o espazo con ceros pode levar a unha mellora significativa (aproximadamente do 100%) do rendemento. En particular, isto pode ocorrer cos discos. AWS EBS (Este son datos non oficiais, non puiden confirmalos). O mesmo ocorre co almacenamento. Disco persistente GCP (e isto xa é información oficial, confirmada por probas). Outros expertos fixeron o mesmo observaciónrelacionados con diferentes discos.
  2. Cantas menos chamadas ao sistema, maior será o rendemento (a ganancia pode ser dun 5%). Parece unha chamada open() con bandeira O_DSYNC ou chamar pwritev2() con bandeira RWF_SYNC chamada máis rápida fdatasync(). Sospeito que o punto aquí é que con este enfoque, o feito de que teñan que realizar menos chamadas ao sistema para resolver a mesma tarefa (unha chamada en lugar de dúas) xoga un papel importante. Pero a diferenza de rendemento é moi pequena, polo que pode ignorala facilmente e usar algo na aplicación que non leve a complicación da súa lóxica.

Se estás interesado no tema do almacenamento de datos sostible, aquí tes algúns materiais útiles:

  • Métodos de acceso E/S — Unha visión xeral dos conceptos básicos dos mecanismos de entrada/saída.
  • Garantir que os datos cheguen ao disco - unha historia sobre o que acontece cos datos no camiño da aplicación ao disco.
  • Cando debería fsync o directorio que contén - a resposta á pregunta de cando solicitar fsync() para directorios. En poucas palabras, resulta que cómpre facelo ao crear un ficheiro novo, e o motivo desta recomendación é que en Linux pode haber moitas referencias ao mesmo ficheiro.
  • SQL Server en Linux: FUA Internals - Aquí tes unha descrición de como se implementa o almacenamento de datos persistente en SQL Server na plataforma Linux. Aquí hai algunhas comparacións interesantes entre as chamadas de sistema de Windows e Linux. Estou case seguro de que foi grazas a este material que aprendín sobre a optimización FUA de XFS.

Algunha vez perdeches datos que pensabas que estaban almacenados de forma segura no disco?

Almacenamento de datos duradeiro e API de ficheiros de Linux

Almacenamento de datos duradeiro e API de ficheiros de Linux

Fonte: www.habr.com