Inside SQL Server, Microsoft Press.
Traduction personnelle. But pédagogique.


La Gestion de la Mémoire

SQL Server 7 existe pour Windows 95 ou Windows 98 jusqu'au serveur SMP sur Windows NT Enterprise Edition. Cela requiert une gestion mémoire particulière. Par défaut, SQL Server 7 ajuste son utilisation de mémoire système par rapport aux autres applications s'exécutant sur la machine et ses propres composants internes. SQL Server peut être configuré pour utiliser un montant fixe de mémoire. Que le type d'allocation mémoire soit fixé ou dynamique, l'espace de mémoire total est considéré comme un cache unifié et géré comme une collection de différents pools avec ses propres règles de sécurité. La mémoire est mise à la disposition de tous les composants internes.

Les Pools de Mémoire et le Gestionnaire de Buffers

Le pool de buffer est un pool de mémoire qui est le composant de mémoire principal du serveur; toute la mémoire non utilisé par les autres composants mémoires reste dans le pool de buffer. Le gestionnaire de buffer gère les opérations de I/O disque pour amener les données et les pages d'index en mémoire afin que les données soient partagées entre les utilisateurs. Lorsque d'autres composants demandent de la mémoire, ils peuvent obtenir un buffer depuis le pool de buffers.

Un autre pool de mémoire est le système d'exploitation lui-même. Quelques fois, SQL Server doit obtenir de la mémoire contiguë en blocks plus large qu'une page de 8 KB que fournit le pool de buffer. L'utilisation de larges blocks de mémoire est réduite au minimum.

Le cache de procédure peut être considéré comme un autre pool de mémoire, dans lequel les arbres de requêtes, les plans des procédures stockées, les déclencheurs et les autres requêtes sont stockées. D'autres pools sont utilisés pour les requêtes qui utilisent intensivement la mémoire, comme les tris, les regroupements et les objets mémoire qui ont un besoin de moins d'une page de 8 KB.

L'Accès aux Pages en Mémoire

L'accès aux pages dans le pool de buffer doit être rapide. Même avec de la mémoire réelle, ce serait inefficace d'avoir un scan due cache entier d'une page lorsque les données se comptent en MB, GB ou TB. Pour éviter cela, les pages du pool de buffers sont organiser dans une table de hachage pour rendre l'accès plus rapide. Hashing est une technique qui associe une clé (dans ce cas, dbid-fileno-pageno id) via une fonction de hachage à un ensemble de hash buckets. Un hash bucket est une page de mémoire qui contient un tableau de pointeurs (implémentée avec une liste chaînée) vers les pages de buffers. Si tous les pointeurs aux pages de buffers ne tiennent pas dans une seule page de hachage une liste chaînée donne les autres pages.

Avec une valeur dbid-fileno-pageno donnée, la fonction de hachage converti la clé. Le hash bucket est un index vers la page spécifique. En utilisant cette technique de table de hachage, même les grandes quantités de mémoire sont présentes,  et vous pouvez trouver une page de données dans le cache avec peu d'opérations de lecture mémoire (un ou deux).

L'Accès aux pages Libres (lazywriter)

Une page de données ou une page d'index peut être utilisée uniquement si elle existe en mémoire. Cependant, un buffer dans le pool de buffers doit être disponible pour la page qui doit être lu. Des buffers sont immédiatement disponibles pour les besoins immédiats. Si un buffer n'est pas disponible de suite, plusieurs pages de mémoire peuvent être recherchées pour localiser un buffer à utiliser comme un espace de travail.

Le pool de buffer est géré par un processus appelé lazywriter qui utilise un algorithme d'horloge. Le thread lazywriter maintient un pointeur dans le pool de buffer qui se positionne régulièrement. A chaque visite du buffer, il détermine si le buffer a été référencé depuis la dernière fois en regardant le compteur de référence dans l'entête du buffer. Si la référence ne vaut pas 0, le buffer reste dans le pool et son compteur de référence est ajusté en préparation du prochain passage; autrement, le buffer est réutilisé: il est écrit sur disque, supprimé des tables de hachage et mis dans une la liste des buffers vide. 

NOTE
L'ensemble des buffers que le lazywriter visite est appelé liste LRU (least recently used). 

Le compteur de référence d'un buffer est incrémenté chaque fois que le contenu du buffer est accédé par un processus. Pour les pages d'index et de données, c'est un simple incrément de un. Mais pour les objets qui sont coûteux à créer comme les plans des procédures stockées, ils ont un compteur de référence plus important qui représente leur coût de remplacement. Lorsque le lazywriter visite chaque page qui a été référencée, il n'utilise pas un simple décrément. Il divise le compteur de référence par 4. Cela veut dire que les pages qui sont souvent référencées (celles avec un haut compteur de référence) et celles qui ont un haut coût de remplacement sont favorisées et leur compteur n'atteindra pas 0 de si tôt, restant ainsi dans le pool pour une prochaine utilisation.

Le lazywriter visite le pool de buffer lorsque le nombre de pages de la liste vide tombe en dessous de sa taille minimum. La taille minimum est calculée comme un pourcentage de la taille totale du pool de buffer entre 128 KB et 4 MB. Par défaut, le pourcentage est positionné à 3 %, mais il peut être amené à changer avec les autres versions.

Les threads utilisateur réalisent les mêmes fonctions de recherche de pages pour la liste vide. Cela arrive lorsque qu'un processus utilisateur a besoin de lire un page du disque vers un buffer. Une fois la lecture initialisée, le thread utilisateur regarde si la liste vide n'est pas trop petite. (Ce processus consomme une page de la liste pour sa propre lecture.) Si c'est le cas, le thread utilisateur effectue la même opération que le lazywriter: il recherche les buffers à libérer. Il visite 16 buffers quel que soit le nombre qu'il en trouve à libérer dans un groupe de 16. La raison de ce partage de travail entre les threads utilisateur et le lazywriter est que le coût peut être distribué entre tous les CPU dans un environnement SMP.

Conserver les Pages dans le Cache en Permanence

Les tables être marquées pour que leurs pages ne soient jamais mises dans la liste vide et conserver en mémoire indéfiniment. Ce processus est le pinning d'une table. N'importe quelle page( données, index ou text) appartient une telle table n'est jamais positionnée comme libre tant qu'elle n'est pas unpinned. Le Pinning et l'unpinning est réalisé avec l'option pintable de la procédure stockée sp_tableoption. Positionner cette option à VRAI pour une table ne provoque pas le chargement de la table dans le cache, ni le fait de marquer les pages comme candidate au déplacement; à la place, cela évite le surcoût non nécessaire et simplement ne permet pas aux pages appartenant à la table pinned d'être misent sur la liste vide pour un remplacement possible.

Le pinning de trop de tables peut résulter que peu ou pas de pages ne soient disponibles lorsqu'un buffer est demandé. Vous devez utiliser cette option avec attention.

Checkpoints

Les opérations de Checkpoints minimisent la quantité de travail que SQL Server réalise pour récupérer les bases de données lors du démarrage du système. Les Checkpoints écrivent les dirty pages sur disque pour que les changements ne soient pas réappliqués lors de la récupération de la base. (Une page dirty est une page qui a été modifiée depuis qu'elle a été déplacée du disque vers le pool de buffer.) Lorsqu'un checkpoint survient, SQL Server écrit un enregistrement au journal des transactions, qui liste toutes les transactions qui sont actives. Cela permet au processus de récupération de reconstruire la table contenant une liste de toutes les dirty pages potentielles. 

Les Checkpoints sont déclenchés lorsque:

L'Accès aux Pages via le Gestionnaire de Buffer

Le gestionnaire de buffer gère la version en mémoire de chaque page disque physique et y fournit tous les modules d'accès. L'image mémoire dans le pool de buffers, si elle existe, est préférée par rapport à l'image disque. La copie des données en mémoire peut inclure des modifications qui n'ont pas été affectées sur disque.(Elle est dirty.) Lorsqu'une page est demandée par un processus, elle doit exister en mémoire dans le pool de buffers. Si elle n'y est pas, une opération I/O physique est réalisée pour l'obtenir. Les I/O physiques sont coûteuses, et moins il y en a, mieux c'est. Plus il y a de mémoire, plus le pool de buffers est grand, plus le nombre de pages peut y résider et plus de pages peuvent y être trouver.

Une base peut être vue simplement comment un ensemble de pages. Le dbid (database ID), le fileno (file number) et le pageno (page number) spécifient de manière unique une page dans l'environnement SQL Server. Lorsqu'un autre module (comme le gestionnaire de méthodes d'accès, le gestionnaire d'enregistrements, le gestionnaire d'index, le gestionnaire de text) a besoin d'accéder à une page, il demande l'accès au gestionnaire de buffers en spécifiant le dbid, fileno, et pageno.

Le gestionnaire de buffers répond au module appelant avec un pointeur vers le buffer mémoire contenant la page. La réponse peut être immédiate si la page est dans le cache, ou peut prend un instant pour effectuer l'opération de I/O disque et charger la page en mémoire. Le module appelant demande aussi au gestionnaire de verrous de réaliser le niveau de verrouillage approprié sur la page. Le module appelant notifie le gestionnaire de buffers quand il a fini. Le gestionnaire de buffer est responsable d'écrire ces changements sur disque en coordonnant la gestion du journal et des transactions.

Utilisation de grande mémoire

Les systèmes avec des centaines de MB de RAM ne sont pas communs. En fait, pour les activités de benchmarks, Microsoft utilise des configurations de plus de 2 GB de RAM physique. L'utilisation de SQL Server sur un processeur DEC Alpha, ou avec la version Entreprise de SQL Server, permet d'utiliser encore plus de mémoire. La raison d'utiliser plus de mémoire réside dans le fait de réduire les opérations de I/O physique en utilisant d'avantage le cache de pages de données.

Lecture anticipée

SQL Server utilise un mécanisme de lecture anticipée qui charge les pages d'index et de données dans le buffer de pool avant que celles-ci ne soit demandées. Cette optimisation permet de traiter les opérations sur de grande quantité de données en mémoire. Les lectures anticipées n'utilisent pas de threads séparées.

Il existe deux types de lectures anticipées: une pour les scan de tables et une pour les rangés d'index. Pour les scan de table, les structures d'allocation de table sont examinées pour les lire les données dans l'ordre du disque. Il y a plus de 32 entends (32 * 8 pages/extent * 8192 octets/page = 2MB) de lecture anticipée en un même instant. Les extends sont lus par incrément de 64 KB (scatter read; I/O inclus dans Windows NT 4, Service Pack 2, avec les fonctions Win32 ReadFileScatter et WriteFileScatter. Ces fonctions permettent à SQL Server de faire une seule opération de lecture ou d'écriture pour transférer jusqu'à 8 pages de données depuis ou vers le pool de buffer.) Si la table est répartie dur plusieurs fichiers dans un groupe de fichiers, SQL Server garde les au moins huit de ces fichiers ouverts pour la lecture anticipée au lieu d'un traitement séquentiel des fichiers.

Pour les séries d'index, le scan utilise un niveau de structure d'index, avec le niveau immédiatement au-dessus de la feuille, pour déterminer quelques pages à lire à l'avance. Il effectue un certain nombre de lectures anticipées, 40 plus la configuration de la valeur de max async I/O. Lorsque le scan d'index démarre, la lecture anticipée est déclenchée sur la descente initiale de l'index pour minimiser le nombre de lectures. Par exemple, si un scan de WHERE state = 'WA', la lecture anticipée cherche l'index pour la key = 'WA', et il peut déterminer depuis les noeuds du niveau 1 combien de pages doivent être examinées pour satisfaire à la recherche. Si le nombre de pages est faible, toutes les pages sont lues par la séquence initiale; si les pages sont contiguës, elles sont lues par des lectures groupées (scatter reads). Si le nombre de pages est grand, la lecture initiale se fait et une fois que 16 pages sont lues par le scan, l'index est consulté pour lire 16 nouvelles pages. Cela produit des effets intéressants:

Le Gestionnaire du Journal

Tous les changements sont écrits avant dans le journal de transactions par le gestionnaire de buffers. L'écriture avant permet de restaurer les bases juste avant un crash système, tant que les disques survivent. Un processus ne reçoit pas la confirmation qu'une transaction est validée tant qu'elle n'est pas sur disque dans le journal. Les écritures au journal sont synchrones. Les écritures aux pages de données peuvent être asynchrones car étant enregistrés dans le journal, les transactions peuvent être validées ou abandonnées.

Les enregistrements au journal sont formatés en mémoire avant écriture sur disque. Pour gérer ces buffers, le journal maintient des régions de mémoire contiguë appelées log caches.

Le Gestionnaire du journal gère deux ou plusieurs caches. Un pour le journal courant, dans lequel les nouveaux enregistrements sont ajoutés. Le gestionnaire de journal a aussi deux queues de caches : une flushQueue, qui contient les entrées à écrire, et une freeQueue qui contient les entrées vides à réutiliser.

Lorsqu'un utilisateur demande à forcer les écritures de la flushQueue (par exemple, lorsqu'une transaction est validée), le cache est placé dans une flushQueue. Alors le thread (ou fiber) est mis dans la liste des connexions en attente tant que le cache n'est pas écrit. 

Le log writer est un thread dédié qui parcours la flushQueue pour réaliser les écritures sur disque. Les entrées sont écrites une par une.