2

Consistence des écritures avec SATA

twitterlinkedinmail

A l’origine, cet autre post de James Hamilton qui s’interroge sur le support de FUA par le protocole ATA/IDE/SATA.

C’est quoi FUA ?

FUA = Force Unit Access. Il s’agit d’un bit au sein d’un Command Disk Block Read ou Write SCSI qui permet d’indiquer au driver de ne pas lire ou écrire depuis ou à partir d’un cache mais bien depuis ou à partir du média physique, c’est à dire du disque magnétique. Par exemple un CDB de Write(10) (source wikipedia):

C’est important d’avoir cette garantie pour les moteurs de bases de données car elle leur permet de supporter ce qu’on appelle le WAL protocol , le pilier qui soutient  la durabilité des transactions dans un SGBD. Lorsqu’une transaction est validée, quoi qu’il advienne ensuite cette information doit être physiquement inscrite quelque part sur un média persistant. Même si on perd un disque, l’alimentation de la machine ou toute la salle d’un seul coup, tout ce qui a été modifié avant la validation de cette transaction doit se retrouver sur disque avant fin du COMMIT, d’où le nom (WAL = Write Ahead Logging).

SQL Server utilise deux commutateurs lors de l’ouverture des fichiers de données et des journaux de transactions au démarrage de l’instance pour valider ce comportement (FILE_FLAG_WRITE_THROUGH  |  FILE_FLAG_NO_BUFFERING). Le premier indique que l’OS a le droit de bufferiser une écriture mais qu’il ne doit pas renvoyer d’acquittement avant que cette écriture n’ait touché le support physique (l’équivalent de O_DSYNC sur Linux, cf ce post). La seconde indique que l’Os n’a pas le droit de lire une donnée depuis le cache, mais de forcer la lecture à partir du disque. Donc vous l’aurez compris, l’association des deux permet de bypasser complètement le cache.

FUA et SATA:

Ce bit FUA fait partie intégrante du protocole SCSI mais reste ignoré par ATA et SATA. Lorsque l’information arrive au driver atapi.sys, elle est tout simplement non interprétée, les écritures vont dans le cache et l’acquittement OK revient au moteur. Pas terrible pour la durabilité des transactions. La seule arme face au problème reste de désactiver le cache en écriture au niveau du disque (sous windows une petite case à décocher dans les propriétés du disque, voir au bas de cet article), mais le problème, c’est qu’il n’y a encore aujourd’hui aucun moyen de savoir réellement si le driver a bien pris en compte cette modification ou non.

Depuis, SATA II a annoncé le support de FUA, mais dans la pratique je vois encore beaucoup de :

sd 0:0:0:0: [sda] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA

sur les forums linux, même avec des contrôleurs SATA II. Après une petite discussion avec Bob Dorr du support MS, je suis parti à la recherche de preuves. J’ai un laptop avec un disque SATA, un SQL Server 2005 SP3, je voudrais savoir si les écritures envoyées vers le disque seront écrites dans le cache ou réellement sur le disque. Capture du matériel pour mémo:

La Descente vers le disque:

Avant d’aller plus loin, il faut rappeler un peu la théorie des IOs sous Windows, et la relation avec SQL Server.

Lorsque SQL Server démarre, il ouvre les fichiers de données et les journaux de transactions des bases qu’il monte en ligne avec CreateFile. Il récupère un type HANDLE qui sera utilisé ensuite pour toutes les opérations sur le fichier (lectures / écritures). Lorsqu’un processus demande l’ouverture ou la création d’un fichier, le noyau transfère le contrôle à l’Object Manager. Celui-ci créé un objet de type FILE_OBJECT pour décrire le fichier, ses attributs, ses modes d’accès, la liste des IOs en cours, etc.. Lorsque notre CreateFile ouvre un fichier en FILE_FLAG_WRITE_THROUGH, le bit FO_WRITE_THROUGH (0x00000010 *) va être ajouté au membre .Flags du FILE_OBJECT.

Ensuite, lorsque le programme doit  écrire dans le fichier, il utilisera une des primitives WriteFile() / WriteFileEx() / WriteFileGather().  Lors de l’écriture, le noyau transfère le contrôle à l’IO Manager pour l’initialisation de l’IO. Celui-ci va créer un IRP (IO Request Packet) pour décrire l’opération.  Chaque IRP a une structure en ascenceur. Elle contient un entête et plusieurs piles (une par driver dans la pile de drivers jusqu’au disque) qu’on appelle des IO_STACK_LOCATIONs. L’entête sert à pointer vers l’IO_STACK_LOCATION correspondant au driver en cours dans la pile. Lorsque le driver courant a terminé son opération, il passe l’IRP au driver suivant dans la stack et fait pointer l’entête de l’IRP vers l’IO_STACK_LOCATION suivante, etc…(cf Handling IRPS : what every driver writer needs to know).

Chaque IRP renseigne un bitmask (.Flags) pour indiquer quels sont les commutateurs actifs. Parmi ceux-ci, SL_WRITE_THROUGH (0x04 *) propage notre contrainte d’écriture sur le média au driver, et c’est là que les choses se gâtent.

L’idée est donc de contrôler la propagation du writethrough à travers les différentes IO_STACK_LOCATIONS et voir quel driver réinitialise le bit.

J’ai tenté pendant trois ou quatre jours de traquer la bête avec livekd, mais c’est assez difficile de prendre SQL Server la main dans le sac. Les IOs sont assez rapides et les IRP vers les disques sont supprimées par l’IO Manager avant que je n’aie le temps de les inspecter. J’ai donc décidé (pas borné du tout le garçon 😉 ) d’écrire un petit programme très simple qui me ferait en boucle des écritures alignées sur les secteurs disque en write-through exactement comme SQL Server, avec le côté asynchrone en moins (pas important pour ce que l’on cherche, et en plus pas très simple à gérer, mais promis ce sera l’objet d’un prochain post).

Le source:

// ------------------------------------------------------------------------------------------------------------
// writeThrough.cpp : Ecrit en boucle des caractères dans un fichier en mode Writethrough / No Buffering
// Utilisé avec livekd, permet de scruter le contenu des IO_STACK_LOCATION dans l'IRP correspondante
// pour contrôler si le bit SL_WRITE_THROUGH est bien propagé jusqu'au driver (pb ATA / IDE /S-ATA).
//
// On utilisera un IOCTL (IOCTL_DISK_GET_DRIVE_GEOMETRY) pour récupérer la taille du secteur
//
// dbaffaleuf@capdata.fr (c) CapData Consulting 2011
//
// ------------------------------------------------------------------------------------------------------------

#include <stdafx.h>
#include <windows.h>
#include <stdlib.h>
#include <winioctl.h>
#include <direct.h>

int _tmain(int argc, _TCHAR* argv[])
{
     if (argc!=2)
    {
        printf("Usage is:  writeThrough outputfilename\n");
        return 1;
    }

 // Récupération de la taille du secteur disque avec un IOCTL ---------------------------------------------------
 DISK_GEOMETRY dg;
 HANDLE hDrive;
 DWORD ioctlJnk; 

 hDrive=CreateFile(TEXT("\\\\.\\PhysicalDrive0")
                          ,0
                          ,FILE_SHARE_READ
                          | FILE_SHARE_WRITE
                          ,NULL
                          ,OPEN_EXISTING
                          ,0
                          ,NULL);

 if (INVALID_HANDLE_VALUE==hDrive)
 {
     printf("Unable to open drive PhysicalDrive0. Last error=%d\n",GetLastError());
     return 1;
 }

 if (! DeviceIoControl(hDrive
                          ,IOCTL_DISK_GET_DRIVE_GEOMETRY
                          ,NULL
                          ,0
                          ,&dg
                          ,sizeof(dg)
                          ,&ioctlJnk
                          ,NULL) )
 {
     printf("Error on IOCTL (IOCTL_DISK_GET_DRIVE_GEOMETRY) to %s.  Last error=%d\n",argv[1],GetLastError());
     return 1;
 }

 CloseHandle(hDrive);

 // Ouverture du fichier Writethrough + No Buffering ---------------------------------------------------------
 HANDLE hOutputFile=CreateFile(argv[1]
                      ,GENERIC_READ
                      | GENERIC_WRITE
                      ,0
                      ,NULL
                      ,CREATE_ALWAYS
                      ,FILE_ATTRIBUTE_NORMAL
                      | FILE_FLAG_WRITE_THROUGH
                      | FILE_FLAG_NO_BUFFERING
                      ,NULL);

 if (INVALID_HANDLE_VALUE==hOutputFile)
 {
     printf("Unable to open file %s.  Last error=%d\n",argv[1],GetLastError());
     return 1;
 }

 // Initialisation du tampon et écriture --------------------------------------------------------------------
 wchar_t *csBuffer = (wchar_t *)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,dg.BytesPerSector * sizeof(wchar_t));
 int len = swprintf_s(csBuffer,dg.BytesPerSector,L"abcdefghijklmnopqrstuvwxyz");    
 DWORD dwBytesWritten;
 DWORD dwTotalBytesWritten=0;

 do
 {
     if (! WriteFile(hOutputFile
                       ,csBuffer
                       ,dg.BytesPerSector
                       ,&dwBytesWritten
                       ,NULL))
     {
         printf ("WriteFile failed with error %d.\n", GetLastError());
         return 1;
     }

    dwTotalBytesWritten+=dwBytesWritten;
    printf("Operation terminée, %d octets écrits\n", dwTotalBytesWritten);
    Sleep (1);

 } while (TRUE);

 HeapFree(GetProcessHeap(),0,csBuffer);
 CloseHandle(hOutputFile);

 return 0;
}

La seule contrainte d’utilisation de (FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH) est d’écrire en s’alignant sur les secteurs du disque, il nous faut donc récupérer la taille du secteur avec un IOCTL (en l’occurence IOCTL_DISK_GET_DRIVE_GEOMETRY). Dans la plupart des cas ce sera 512 octets, mais des tailles de 4K vont commencer à arriver sur le marché, donc il ne faut pas présumer et bien calculer tout ça proprement. Dans la seconde partie, on initialise un buffer sur une taille multiple de la taille du secteur et on écrit dans notre fichier hOutputFile en boucle continue, avec une pause de 1 ms entre chaque écriture.

On lance le programme en boucle et on va passer à la partie livekd:

C:\DBA\DEV\CPP\writeThrough\Debug>writeThrough.exe test.txt
Operation terminée, 512 octets écrits
Operation terminée, 1024 octets écrits
Operation terminée, 1536 octets écrits
Operation terminée, 2048 octets écrits
Operation terminée, 2560 octets écrits
Operation terminée, 3072 octets écrits
...

Observation des IRPs:

Première chose à faire si vous souhaitez reproduire ce POC, il vous faudra récupérer les outils de debug de Windows et les symboles pour votre plateforme. Il vous faudra aussi récupérer LiveKD.

Ensuite pointer vers le serveur de symboles MS:

set _NT_SYMBOL_PATH=srv*c:\temp\symbols*http://msdl.microsoft.com/download/symbols   

Et lancer livekd. Pour assurer un peu plus la cohérence des résultats, il peut être nécessaire de passer par un bcdedit /debug on + reboot pour activer le débogage du noyau. La stratégie est de retrouver les IRPs actives associées au fichier dans lequel on écrit par le biais d’un !irpfind en lui passant l’adresse du DEVICE_OBJECT associé au disque. Dans un foreach, ensuite on affiche le contenu détaillé de toutes les irps que l’on retrouve. Attention, vous devrez sans doute le faire un certain nombre de fois pour pouvoir observer un IRP décrivant une écriture (MAJOR= 4 => IRP_MJ_WRITE).

Première chose, on va récupérer l’adresse de notre processus writeThrough.exe, et le passer en process par défaut sous livekd:

lkd> !sym quiet
quiet mode - symbol prompts on
lkd> !process 0 0 writeThrough.exe
PROCESS 881b39d0  SessionId: 0  Cid: 0c08    Peb: 7ffde000  ParentCid: 0960
 DirBase: 0aa80740  ObjectTable: e4168638  HandleCount:   9.
 Image: writeThrough.exe

lkd> .process 881b39d0  
Implicit process is now 881b39d0

Ensuite, nous allons nous intéresser au fichier test.txt. Il nous faut récupérer le FILE_OBJECT créé par l’Object Manager lors du CreateFile():

0: kd> !handle 0 3
(...)
07dc: Object: 8701f1b0  GrantedAccess: 0012019f Entry: e3baefb8
Object: 8701f1b0  Type: (8a57a900) File
 ObjectHeader: 8701f198 (old version)
 HandleCount: 1  PointerCount: 1
 Directory Object: 00000000  Name: \CAPDATA\DEV\DEV++\writeThrough\Debug\test.txt {HarddiskVolume3}
(...)

0: kd> dt nt!_FILE_OBJECT 8701f1b0
 +0x000 Type             : 0n5
 +0x002 Size             : 0n112
 +0x004 DeviceObject     : 0x8a4a5a30_DEVICE_OBJECT
 +0x008 Vpb              : 0x8a4a71e8 _VPB
 +0x00c FsContext        : 0xe6be4928 Void
 +0x010 FsContext2       : 0xe6be4a80 Void
 +0x014 SectionObjectPointer : 0x8839f5cc _SECTION_OBJECT_POINTERS
 +0x018 PrivateCacheMap  : (null)
 +0x01c FinalStatus      : 0n0
 +0x020 RelatedFileObject : 0x88752ec0 _FILE_OBJECT
 +0x024 LockOperation    : 0 ''
 +0x025 DeletePending    : 0 ''
 +0x026 ReadAccess       : 0x1 ''
 +0x027 WriteAccess      : 0x1 ''
 +0x028 DeleteAccess     : 0 ''
 +0x029 SharedRead       : 0 ''
 +0x02a SharedWrite      : 0 ''
 +0x02b SharedDelete     : 0 ''
 +0x02c Flags            : 0x4101a
 +0x030 FileName         : _UNICODE_STRING "\CAPDATA\DEV\DEV++\writeThrough\Debug\test.txt"
 +0x038 CurrentByteOffset : _LARGE_INTEGER 0x7c00
 +0x040 Waiters          : 0
 +0x044 Busy             : 0
 +0x048 LastLock         : (null)
 +0x04c Lock             : _KEVENT
 +0x05c Event            : _KEVENT
 +0x06c CompletionContext : (null)

Par la même occasion, on va vérifier si notre CreateFile() a bien propagé FO_WRITE_THROUGH au FILE_OBJECT:

0: kd> ? (0x4101a & 0x00000010)
Evaluate expression: 16 = 00000010

OK. On peut donc poursuivre en gardant sous le coude l’adresse du DEVICE OBJECT renvoyé par le dt nt!_FILE_OBJECT… :

(...)
+0x004 DeviceObject     : 0x8a4a5a30_DEVICE_OBJECT
(...)

Courage !

Et c’est là que la partie de pêche commence vraiment. Je vous laisse le soin de regarder comment les instructions sous un déboggeur fonctionnent mais l’idée est de récupérer l’adresse de chaque IRP associée au DEVICE OBJECT de notre disque et d’afficher son contenu, en vérifiant qu’il provient bien de notre petit programme. Il faut être patient. Au bout d’un certain nombre de tentatives, on arrive à en attraper une :

lkd> .foreach /pS 1 /ps 3 (irpaddr {.foreach /pS 21 /ps 9 (irpaddressblk {!irpfind 0 0 device 0x8a4a5a30}) { .echo ${irpaddressblk} } }) { !irp ${irpaddr} 1 }
Irp is active with 10 stacks 6 is current (= 0x882a312c)
 Mdl=88195a60: No System Buffer: Thread 881059e8:  Irp stack trace.  
Flags = 00000043
ThreadListEntry.Flink = 882a3018
ThreadListEntry.Blink = 882a3018
IoStatus.Status = 00000000
IoStatus.Information = 00000000
RequestorMode = 00000000
Cancel = 00
CancelIrql = 0
ApcEnvironment = 00
UserIosb = a0e60878
UserEvent = a0e60820
Overlay.AsynchronousParameters.UserApcRoutine = 00000000
Overlay.AsynchronousParameters.UserApcContext = 00000000
Overlay.AllocationSize = 00000000 - 00000000
CancelRoutine = 00000000   
UserBuffer = 88083000
&Tail.Overlay.DeviceQueueEntry = 882a3048
Tail.Overlay.Thread = 881059e8
Tail.Overlay.AuxiliaryBuffer = 00000000
Tail.Overlay.ListEntry.Flink = 00000000
Tail.Overlay.ListEntry.Blink = 00000000
Tail.Overlay.CurrentStackLocation = 882a312c
Tail.Overlay.OriginalFileObject = 899ef028
Tail.Apc = 00000000
Tail.CompletionKey = 00000000
 cmd  flg cl Device   File     Completion-Context
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    

 Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    

 Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    

 Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    

 Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    

 Args: 00000000 00000000 00000000 00000000
>[  4, 0]   0  0 8a483770 00000000 f77077ca-8a483548    
 \Driver\Disk    
 Args: 00000000 00000000 00000000 00000000
 [  4, 0]   0  0 8a483548 00000000 f7238962-8a4a5ae8    
 \Driver\PartMgr    
 Args: 00000000 00000000 00000000 00000000
 [  4, 0]   0  0 8a4a5a30 00000000 f74a7680-8a48a7b8    
 \Driver\Ftdisk    
 Args: 00000000 00000000 00000000 00000000
 [  4, 0]   0  0 8a48a700 00000000 f7063e3f-a0e6045c    
 \Driver\VolSnap    
 Args: 00000000 00000000 00000000 00000000
 [  4, 0]   0  0 8979c020 00000000 00000000-00000000    
 \FileSystem\Ntfs
 Args: 00000000 00000000 00000000 00000000

J’ai volontairement colorié le membre .Flags et l’IO_STACK_LOCATION courante. On voit bien ici l’empilement des drivers: Ntfs -> VolSnap -> Ftdisk -> PartMgr -> Disk. Entre crochets, notre couple [MAJOR,Minor] qui indique le type d’opération en cours (pour plus d’infos cf l’aide de la commande !irp dans le DDK). [4,0] indique que la commande en cours est un IRP_MJ_WRITE, donc une écriture, et la petite indirection à l’extrême gauche montre que la besogne est entre les mains du driver le plus bas dans la stack (c:\Windows\system32\Disk.sys). Quant à la valeur de .Flags:

lkd> ? (0x00000043 & 0x4)
Evaluate expression: 0 = 00000000

Bingo. Pas de SL_WRITE_THROUGH. J’ai pû faire le test avec et sans cache activé au niveau du disque, et le résultat est le même:

Donc potentiellement si je débranche ma batterie et que je coupe mon alimentation au moment d’une grosse écriture il y a un risque de perdre une transaction validée. Je vous avoue que je ne l’ai testé qu’une seule fois (je tiens aussi un peu à mon matériel), mais la coupure est intervenue quelques millisecondes avant le commit, et SQL Server a rollbacké tout ça proprement. Il faudrait faire une batterie de tests mais je n’ai pas les moyens de cramer du disque comme ça. Après, celà jette tout de même de lourds soupçons sur la capacité de SATA à répondre aux besoins des SGBD.

Ce n’est plus qu’une question de coût ou de performance que de comparer SCSI à SATA. Celà devient une question d’intégrité des données, et donc un problème fondamental. Affaire à suivre…

A+. David B.

* Sources: winddk.h

Références:
http://support.microsoft.com/kb/234656
http://support.microsoft.com/kb/46091
http://support.microsoft.com/kb/86903

Continuez votre lecture sur le blog :

twitterlinkedinmail

David Baffaleuf

2 commentaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.