跳转至

XFS Logging Design

原文档地址

1 Preamble

This document describes the design and algorithms that the XFS journalling subsystem is based on. This document describes the design and algorithms that the XFS journalling subsystem is based on so that readers may familiarize themselves with the general concepts of how transaction processing in XFS works.

本文档描述了XFS日志记录子系统所基于的设计和算法。本文档描述XFS日志子系统所依据的设计和计算方法,以便读者熟悉XFS中事务处理的一般概念。

We begin with an overview of transactions in XFS, followed by describing how transaction reservations are structured and accounted, and then move into how we guarantee forwards progress for long running transactions with finite initial reservations bounds. At this point we need to explain how relogging works. With the basic concepts covered, the design of the delayed logging mechanism is documented.

我们首先概述了XFS中的事务,然后描述了事务保留是如何构造和记账的,然后进入如何保证在有限的初始保留边界下长期运行事务的向前进展。此时,我们需要解释重新记录的工作原理。在涵盖基本概念后,将记录延迟日志记录机制的设计。

2 Introduction

XFS uses Write Ahead Logging for ensuring changes to the filesystem metadata are atomic and recoverable. For reasons of space and time efficiency, the logging mechanisms are varied and complex, combining intents, logical and physical logging mechanisms to provide the necessary recovery guarantees the filesystem requires.

XFS使用预写日志记录来确保对文件系统元数据的更改是原子的和可恢复的。由于空间和时间效率的原因,日志机制多种多样且复杂,将意图、逻辑和物理日志机制结合起来,以提供文件系统所需的必要恢复保证。

Some objects, such as inodes and dquots, are logged in logical format where the details logged are made up of the changes to in-core structures rather than on-disk structures. Other objects - typically buffers - have their physical changes logged. Long running atomic modifications have individual changes chained together by intents, ensuring that journal recovery can restart and finish an operation that was only partially done when the system stopped functioning.

某些对象(如inode和dquot)以逻辑格式记录,其中记录的详细信息由内核结构而非磁盘结构的更改组成。其他对象(通常是缓冲区)会记录其物理更改。长期运行的原子修改通过意图将单个更改链接在一起,确保日志恢复可以重新启动并完成系统停止运行时仅部分完成的操作。

The reason for these differences is to keep the amount of log space and CPU time required to process objects being modified as small as possible and hence the logging overhead as low as possible. Some items are very frequently modified, and some parts of objects are more frequently modified than others, so keeping the overhead of metadata logging low is of prime importance.

这些差异的原因是为了尽可能减少处理被修改对象所需的日志空间和CPU时间,从而尽可能降低日志开销。有些项非常频繁地被修改,对象的某些部分比其他部分更频繁地被更改,因此保持元数据日志记录的开销低是最重要的。

The method used to log an item or chain modifications together isn’t particularly important in the scope of this document. It suffices to know that the method used for logging a particular object or chaining modifications together are different and are dependent on the object and/or modification being performed. The logging subsystem only cares that certain specific rules are followed to guarantee forwards progress and prevent deadlocks.

在本文档的范围内,用于一起记录项或链修改的方法并不特别重要。只要知道,用于记录特定对象或将修改链接在一起的方法是不同的,并且取决于正在执行的对象和/或修改。日志记录子系统只关心遵循某些特定的规则来保证转发进度并防止死锁。

3 Transactions in XFS

XFS has two types of high level transactions, defined by the type of log space reservation they take. These are known as “one shot” and “permanent” transactions. Permanent transaction reservations can take reservations that span commit boundaries, whilst “one shot” transactions are for a single atomic modification.

XFS有两种类型的高级事务,由它们保留的日志空间类型定义。这些交易被称为“一次性”和“永久性”交易。永久事务保留可以接受跨越提交边界的保留,而“一次性”事务用于单个原子修改。

The type and size of reservation must be matched to the modification taking place. This means that permanent transactions can be used for one-shot modifications, but one-shot reservations cannot be used for permanent transactions.

预订的类型和大小必须与正在进行的修改相匹配。这意味着永久事务可以用于一次性修改,但一次性保留不能用于永久事务。

In the code, a one-shot transaction pattern looks somewhat like this:

tp = xfs_trans_alloc(<reservation>)
<lock items>
<join item to transaction>
<do modification>
xfs_trans_commit(tp);

As items are modified in the transaction, the dirty regions in those items are tracked via the transaction handle. Once the transaction is committed, all resources joined to it are released, along with the remaining unused reservation space that was taken at the transaction allocation time.

在事务中修改项目时,通过事务句柄跟踪这些项目中的脏区域。提交事务后,将释放与之连接的所有资源,以及在事务分配时占用的剩余未使用保留空间。

In contrast, a permanent transaction is made up of multiple linked individual transactions, and the pattern looks like this:

tp = xfs_trans_alloc(<reservation>)
xfs_ilock(ip, XFS_ILOCK_EXCL)

loop {
        xfs_trans_ijoin(tp, 0);
        <do modification>
        xfs_trans_log_inode(tp, ip);
        xfs_trans_roll(&tp);
}

xfs_trans_commit(tp);
xfs_iunlock(ip, XFS_ILOCK_EXCL);

While this might look similar to a one-shot transaction, there is an important difference: xfs_trans_roll() performs a specific operation that links two transactions together:

虽然这看起来类似于一次性事务,但有一个重要的区别:xfs_trans_roll()执行将两个事务链接在一起的特定操作:

ntp = xfs_trans_dup(tp);
xfs_trans_commit(tp);
xfs_trans_reserve(ntp);

This results in a series of “rolling transactions” where the inode is locked across the entire chain of transactions. Hence while this series of rolling transactions is running, nothing else can read from or write to the inode and this provides a mechanism for complex changes to appear atomic from an external observer’s point of view.

这导致了一系列“滚动事务”,其中inode在整个事务链中被锁定。因此,当这一系列滚动事务正在运行时,其他任何事务都无法读取或写入inode,这为复杂的更改提供了一种机制,从外部观察者的角度来看,这些更改似乎是原子的。

It is important to note that a series of rolling transactions in a permanent transaction does not form an atomic change in the journal. While each individual modification is atomic, the chain is not atomic. If we crash half way through, then recovery will only replay up to the last transactional modification the loop made that was committed to the journal.

需要注意的是,永久事务中的一系列滚动事务不会在日志中形成原子变化。虽然每个单独的修改都是原子的,但链不是原子的。如果我们中途崩溃,那么恢复将只回放到提交到日志的循环所做的最后一次事务修改。

This affects long running permanent transactions in that it is not possible to predict how much of a long running operation will actually be recovered because there is no guarantee of how much of the operation reached stale storage. Hence if a long running operation requires multiple transactions to fully complete, the high level operation must use intents and deferred operations to guarantee recovery can complete the operation once the first transactions is persisted in the on-disk journal.

这会影响长时间运行的永久事务,因为无法预测实际恢复的长时间运行操作的数量,因为无法保证有多少操作到达了过时的存储。因此,如果长时间运行的操作需要多个事务才能完全完成,则高级操作必须使用意图和延迟操作,以确保在磁盘日志中保留第一个事务后,恢复可以完成操作。

4 Transactions are Asynchronous

In XFS, all high level transactions are asynchronous by default. This means that xfs_trans_commit() does not guarantee that the modification has been committed to stable storage when it returns. Hence when a system crashes, not all the completed transactions will be replayed during recovery.

在XFS中,默认情况下,所有高级事务都是异步的。这意味着xfs_trans_commit()不能保证修改返回时已提交到稳定的存储中。因此,当系统崩溃时,并不是所有已完成的事务都会在恢复期间重播。

However, the logging subsystem does provide global ordering guarantees, such that if a specific change is seen after recovery, all metadata modifications that were committed prior to that change will also be seen.

然而,日志记录子系统确实提供了全局排序保证,这样,如果在恢复后看到特定的更改,那么也将看到在该更改之前提交的所有元数据修改。

For single shot operations that need to reach stable storage immediately, or ensuring that a long running permanent transaction is fully committed once it is complete, we can explicitly tag a transaction as synchronous. This will trigger a “log force” to flush the outstanding committed transactions to stable storage in the journal and wait for that to complete.

对于需要立即达到稳定存储的单次快照操作,或者确保一个长期运行的永久事务一旦完成就被完全提交,我们可以显式地将一个事务标记为同步。这将触发“日志强制”,将未完成的已提交事务刷新到日志中的稳定存储中,并等待其完成。

Synchronous transactions are rarely used, however, because they limit logging throughput to the IO latency limitations of the underlying storage. Instead, we tend to use log forces to ensure modifications are on stable storage only when a user operation requires a synchronisation point to occur (e.g. fsync).

然而,很少使用同步事务,因为它们将日志吞吐量限制在底层存储的IO延迟限制之内。相反,我们倾向于使用日志强制,以确保只有在用户操作需要同步点时(例如fsync),修改才会在稳定的存储上进行。

5 Transaction Reservations

It has been mentioned a number of times now that the logging subsystem needs to provide a forwards progress guarantee so that no modification ever stalls because it can’t be written to the journal due to a lack of space in the journal. This is achieved by the transaction reservations that are made when a transaction is first allocated. For permanent transactions, these reservations are maintained as part of the transaction rolling mechanism.

现在已经多次提到,日志记录子系统需要提供一个转发进度保证,这样就不会因为日志中缺少空间而无法写入日志而导致修改暂停。这是通过在首次分配事务时进行的事务预留来实现的。对于永久交易,这些保留作为交易滚动机制的一部分进行维护。

A transaction reservation provides a guarantee that there is physical log space available to write the modification into the journal before we start making modifications to objects and items. As such, the reservation needs to be large enough to take into account the amount of metadata that the change might need to log in the worst case. This means that if we are modifying a btree in the transaction, we have to reserve enough space to record a full leaf-to-root split of the btree. As such, the reservations are quite complex because we have to take into account all the hidden changes that might occur.

事务保留提供了一种保证,即在我们开始修改对象和项目之前,有物理日志空间可用于将修改写入日志。因此,保留需要足够大,以考虑在最坏情况下更改可能需要记录的元数据量。这意味着,如果我们在事务中修改btree,我们必须保留足够的空间来记录btree的完整叶到根分割。因此,保留意见非常复杂,因为我们必须考虑到可能发生的所有隐藏变化。

For example, a user data extent allocation involves allocating an extent from free space, which modifies the free space trees. That’s two btrees. Inserting the extent into the inode’s extent map might require a split of the extent map btree, which requires another allocation that can modify the free space trees again. Then we might have to update reverse mappings, which modifies yet another btree which might require more space. And so on. Hence the amount of metadata that a “simple” operation can modify can be quite large.

例如,用户数据区段分配涉及从空闲空间分配区段,这会修改空闲空间树。这是两个btree。将数据块插入inode的数据块映射可能需要拆分数据块映射btree,这需要另一个可以再次修改空闲空间树的分配。然后我们可能需要更新反向映射,这将修改另一个可能需要更多空间的btree。因此,“简单”操作可以修改的元数据量可能非常大。

This “worst case” calculation provides us with the static “unit reservation” for the transaction that is calculated at mount time. We must guarantee that the log has this much space available before the transaction is allowed to proceed so that when we come to write the dirty metadata into the log we don’t run out of log space half way through the write.

这种“最坏情况”计算为我们提供了在装载时计算的事务的静态“单元预留”。我们必须确保在允许事务继续之前,日志有足够的可用空间,以便在将脏元数据写入日志时,不会在写入过程中耗尽日志空间。

For one-shot transactions, a single unit space reservation is all that is required for the transaction to proceed. For permanent transactions, however, we also have a “log count” that affects the size of the reservation that is to be made.

对于一次性事务,只需保留一个单元空间即可进行事务。然而,对于永久事务,我们也有一个“日志计数”,它会影响要进行的保留的大小。

While a permanent transaction can get by with a single unit of space reservation, it is somewhat inefficient to do this as it requires the transaction rolling mechanism to re-reserve space on every transaction roll. We know from the implementation of the permanent transactions how many transaction rolls are likely for the common modifications that need to be made.

虽然永久事务可以通过单个空间保留单元来处理,但这样做有些低效,因为它需要事务滚动机制在每个事务滚动上重新保留空间。我们从永久事务的实现中知道,对于需要进行的常见修改,可能有多少事务滚动。

For example, an inode allocation is typically two transactions - one to physically allocate a free inode chunk on disk, and another to allocate an inode from an inode chunk that has free inodes in it. Hence for an inode allocation transaction, we might set the reservation log count to a value of 2 to indicate that the common/fast path transaction will commit two linked transactions in a chain. Each time a permanent transaction rolls, it consumes an entire unit reservation.

例如,一个inode分配通常是两个事务——一个用于物理分配磁盘上的空闲inode块,另一个用于从其中有空闲inode的inode块分配inode。因此,对于inode分配事务,我们可以将保留日志计数设置为2,以指示公共/快速路径事务将在一个链中提交两个链接的事务。每次一个永久事务滚动时,它都会消耗整个单元预留。

Hence when the permanent transaction is first allocated, the log space reservation is increased from a single unit reservation to multiple unit reservations. That multiple is defined by the reservation log count, and this means we can roll the transaction multiple times before we have to re-reserve log space when we roll the transaction. This ensures that the common modifications we make only need to reserve log space once.

因此,当首次分配永久事务时,日志空间保留从单个单元保留增加到多个单元保留。该倍数由保留日志计数定义,这意味着我们可以在回滚事务时重新保留日志空间之前多次回滚事务。这确保了我们所做的常见修改只需要保留一次日志空间。

If the log count for a permanent transaction reaches zero, then it needs to re-reserve physical space in the log. This is somewhat complex, and requires an understanding of how the log accounts for space that has been reserved.

如果永久事务的日志计数为零,则需要在日志中重新保留物理空间。这有点复杂,需要了解日志如何记录已保留的空间。

6 Log Space Accounting

The position in the log is typically referred to as a Log Sequence Number (LSN). The log is circular, so the positions in the log are defined by the combination of a cycle number - the number of times the log has been overwritten - and the offset into the log. A LSN carries the cycle in the upper 32 bits and the offset in the lower 32 bits. The offset is in units of “basic blocks” (512 bytes). Hence we can do realtively simple LSN based math to keep track of available space in the log.

日志中的位置通常称为日志序列号(LSN)。日志是循环的,因此日志中的位置由循环数(日志被覆盖的次数)和日志中的偏移量的组合来定义。LSN在高位32位中携带周期,在低位32位中承载偏移。偏移量以“基本块”(512字节)为单位。因此,我们可以进行非常简单的基于LSN的数学来跟踪日志中的可用空间。

Log space accounting is done via a pair of constructs called “grant heads”. The position of the grant heads is an absolute value, so the amount of space available in the log is defined by the distance between the position of the grant head and the current log tail. That is, how much space can be reserved/consumed before the grant heads would fully wrap the log and overtake the tail position.

日志空间核算是通过一对称为“授权头”的结构完成的。授权头的位置是一个绝对值,因此日志中可用的空间量由授权头位置和当前日志尾部之间的距离定义。也就是说,在授予者完全包裹原木并超越尾部位置之前,可以保留/消耗多少空间。

The first grant head is the “reserve” head. This tracks the byte count of the reservations currently held by active transactions. It is a purely in-memory accounting of the space reservation and, as such, actually tracks byte offsets into the log rather than basic blocks. Hence it technically isn’t using LSNs to represent the log position, but it is still treated like a split {cycle,offset} tuple for the purposes of tracking reservation space.

第一个赠款项目是“储备”项目。它跟踪活动事务当前保留的字节数。它是对空间保留的一种纯粹的内存内核算,因此,它实际上跟踪日志中的字节偏移量,而不是基本块。因此,从技术上讲,它不使用LSN来表示日志位置,但为了跟踪保留空间,它仍然被视为拆分{循环,偏移}元组。

The reserve grant head is used to accurately account for exact transaction reservations amounts and the exact byte count that modifications actually make and need to write into the log. The reserve head is used to prevent new transactions from taking new reservations when the head reaches the current tail. It will block new reservations in a FIFO queue and as the log tail moves forward it will wake them in order once sufficient space is available. This FIFO mechanism ensures no transaction is starved of resources when log space shortages occur.

保留授权头用于准确地说明确切的事务保留量和实际进行的修改以及需要写入日志的确切字节数。保留头部用于防止新事务在头部到达当前尾部时进行新的保留。它将阻止FIFO队列中的新保留,当日志尾部向前移动时,一旦有足够的空间可用,它将按顺序唤醒它们。这种FIFO机制确保在日志空间不足时不会出现事务资源不足的情况。

The other grant head is the “write” head. Unlike the reserve head, this grant head contains an LSN and it tracks the physical space usage in the log. While this might sound like it is accounting the same state as the reserve grant head - and it mostly does track exactly the same location as the reserve grant head - there are critical differences in behaviour between them that provides the forwards progress guarantees that rolling permanent transactions require.

另一个赠款负责人是“写”负责人。与保留标头不同,此授权标头包含LSN,并跟踪日志中的物理空间使用情况。虽然这听起来像是在计算与储备赠款负责人相同的状态-而且它大多数情况下确实跟踪与储备赠款主管完全相同的位置-但它们之间的行为存在重大差异,提供了滚动永久交易所需的远期进度保证。

These differences when a permanent transaction is rolled and the internal “log count” reaches zero and the initial set of unit reservations have been exhausted. At this point, we still require a log space reservation to continue the next transaction in the sequeunce, but we have none remaining. We cannot sleep during the transaction commit process waiting for new log space to become available, as we may end up on the end of the FIFO queue and the items we have locked while we sleep could end up pinning the tail of the log before there is enough free space in the log to fulfill all of the pending reservations and then wake up transaction commit in progress.

当一个永久事务被回滚,内部“日志计数”为零,并且初始的单元保留集已用完时,这些差异就会出现。此时,我们仍然需要保留日志空间以继续后续的下一个事务,但我们没有剩余的事务。在事务提交过程中,我们不能在等待新的日志空间变为可用时睡觉,因为我们可能会在FIFO队列的末尾结束,而我们在睡觉时锁定的项目可能会在日志中有足够的可用空间来完成所有待决保留之前锁定日志的尾部,然后唤醒正在进行的事务提交。

To take a new reservation without sleeping requires us to be able to take a reservation even if there is no reservation space currently available. That is, we need to be able to overcommit the log reservation space. As has already been detailed, we cannot overcommit physical log space. However, the reserve grant head does not track physical space - it only accounts for the amount of reservations we currently have outstanding. Hence if the reserve head passes over the tail of the log all it means is that new reservations will be throttled immediately and remain throttled until the log tail is moved forward far enough to remove the overcommit and start taking new reservations. In other words, we can overcommit the reserve head without violating the physical log head and tail rules.

要在不睡觉的情况下进行新的预订,我们需要能够进行预订,即使当前没有可用的预订空间。也就是说,我们需要能够“过度使用”日志保留空间。如前所述,我们不能过度使用物理日志空间。然而,储备赠款负责人并不跟踪实际空间——它只说明我们目前尚未完成的储备数量。因此,如果保留头部越过日志尾部,则意味着新的保留将立即被抑制,并保持抑制状态,直到日志尾部向前移动足够远,以消除过度承诺并开始采取新的保留。换言之,我们可以在不违反物理日志头和尾规则的情况下过度使用保留头。

As a result, permanent transactions only “regrant” reservation space during xfs_trans_commit() calls, while the physical log space reservation - tracked by the write head - is then reserved separately by a call to xfs_log_reserve() after the commit completes. Once the commit completes, we can sleep waiting for physical log space to be reserved from the write grant head, but only if one critical rule has been observed:

因此,在xfs_trans_commit()调用期间,永久事务只“重新分配”保留空间,而物理日志空间保留(由写头跟踪)则在提交完成后通过调用xfs_log_reserve()单独保留。提交完成后,我们可以休眠,等待从写入授权头保留物理日志空间,但前提是必须遵守一条关键规则:

Code using permanent reservations must always log the items they hold
locked across each transaction they roll in the chain.

“Re-logging” the locked items on every transaction roll ensures that the items attached to the transaction chain being rolled are always relocated to the physical head of the log and so do not pin the tail of the log. If a locked item pins the tail of the log when we sleep on the write reservation, then we will deadlock the log as we cannot take the locks needed to write back that item and move the tail of the log forwards to free up write grant space. Re-logging the locked items avoids this deadlock and guarantees that the log reservation we are making cannot self-deadlock.

“重新记录”每个事务卷上的锁定项可确保附加到正在滚动的事务链的项始终重新定位到日志的物理头部,因此不会固定日志的尾部。如果当我们在写保留上休眠时,锁定项锁定了日志的尾部,那么我们将死锁日志,因为我们无法获取写回该项所需的锁,并向前移动日志尾部以释放写授权空间。重新记录锁定项可以避免这种死锁,并确保我们正在进行的日志保留不会自死锁。

If all rolling transactions obey this rule, then they can all make forwards progress independently because nothing will block the progress of the log tail moving forwards and hence ensuring that write grant space is always (eventually) made available to permanent transactions no matter how many times they roll.

如果所有滚动事务都遵守此规则,那么它们都可以独立地向前移动,因为没有任何东西会阻止日志尾部向前移动的进程,因此确保无论永久事务滚动多少次,写入授权空间始终(最终)可用于永久事务。

7 Re-logging Explained

XFS allows multiple separate modifications to a single object to be carried in the log at any given time. This allows the log to avoid needing to flush each change to disk before recording a new change to the object. XFS does this via a method called “re-logging”. Conceptually, this is quite simple - all it requires is that any new change to the object is recorded with a new copy of all the existing changes in the new transaction that is written to the log.

XFS允许在任何给定时间在日志中携带对单个对象的多个单独修改。这允许日志避免在记录对象的新更改之前将每个更改刷新到磁盘。XFS通过一种叫做“重新记录”的方法来实现这一点。从概念上讲,这非常简单——它所需要的就是,对对象的任何新更改都要记录到新事务中所有现有更改的新副本中,这些更改将写入日志。

That is, if we have a sequence of changes A through to F, and the object was written to disk after change D, we would see in the log the following series of transactions, their contents and the log sequence number (LSN) of the transaction:

也就是说,如果我们有一系列从a到F的更改,并且对象在更改D之后被写入磁盘,我们将在日志中看到以下一系列事务、它们的内容和事务的日志序列号(LSN):

Transaction             Contents        LSN
   A                       A               X
   B                      A+B             X+n
   C                     A+B+C           X+n+m
   D                    A+B+C+D         X+n+m+o
    <object written to disk>
   E                       E               Y (> X+n+m+o)
   F                      E+F             Y+p

In other words, each time an object is relogged, the new transaction contains the aggregation of all the previous changes currently held only in the log.

换句话说,每次重新记录对象时,新事务都包含当前仅保存在日志中的所有先前更改的聚合。

This relogging technique allows objects to be moved forward in the log so that an object being relogged does not prevent the tail of the log from ever moving forward. This can be seen in the table above by the changing (increasing) LSN of each subsequent transaction, and it’s the technique that allows us to implement long-running, multiple-commit permanent transactions.

这种重新记录技术允许对象在日志中向前移动,以便重新记录的对象不会阻止日志的尾部向前移动。从上表中可以看出,每个后续事务的LSN都在变化(增加),这是一种允许我们实现长时间运行、多次提交永久事务的技术。

A typical example of a rolling transaction is the removal of extents from an inode which can only be done at a rate of two extents per transaction because of reservation size limitations. Hence a rolling extent removal transaction keeps relogging the inode and btree buffers as they get modified in each removal operation. This keeps them moving forward in the log as the operation progresses, ensuring that current operation never gets blocked by itself if the log wraps around.

滚动事务的一个典型示例是从inode中删除扩展数据块,由于保留大小的限制,只能以每个事务两个扩展数据块的速率进行删除。因此,滚动数据块删除事务会在每次删除操作中修改索引节点和btree缓冲区时不断重新记录它们。这会使它们在操作进行过程中在日志中继续前进,确保在日志结束时当前操作不会被自身阻止。

Hence it can be seen that the relogging operation is fundamental to the correct working of the XFS journalling subsystem. From the above description, most people should be able to see why the XFS metadata operations writes so much to the log - repeated operations to the same objects write the same changes to the log over and over again. Worse is the fact that objects tend to get dirtier as they get relogged, so each subsequent transaction is writing more metadata into the log.

因此可以看出,重新记录操作是XFS日志子系统正确工作的基础。从上面的描述中,大多数人应该能够理解为什么XFS元数据操作会向日志写入这么多内容——对相同对象的重复操作会一次又一次地向日志写入相同的更改。更糟糕的是,当对象被重新记录时,它们往往会变得更脏,因此每个后续事务都会向日志中写入更多元数据。

It should now also be obvious how relogging and asynchronous transactions go hand in hand. That is, transactions don’t get written to the physical journal until either a log buffer is filled (a log buffer can hold multiple transactions) or a synchronous operation forces the log buffers holding the transactions to disk. This means that XFS is doing aggregation of transactions in memory - batching them, if you like - to minimise the impact of the log IO on transaction throughput.

现在,重新记录和异步事务是如何并行的,这一点也应该很明显。也就是说,直到日志缓冲区被填满(日志缓冲区可以容纳多个事务),或者同步操作强制将保存事务的日志缓冲区写入磁盘,事务才会写入物理日志。这意味着XFS正在内存中聚合事务——如果您愿意,可以对它们进行批处理——以最大限度地减少日志IO对事务吞吐量的影响。

The limitation on asynchronous transaction throughput is the number and size of log buffers made available by the log manager. By default there are 8 log buffers available and the size of each is 32kB - the size can be increased up to 256kB by use of a mount option.

异步事务吞吐量的限制是日志管理器提供的日志缓冲区的数量和大小。默认情况下,有8个可用的日志缓冲区,每个缓冲区的大小为32kB-通过使用装载选项,大小可以增加到256kB。

Effectively, this gives us the maximum bound of outstanding metadata changes that can be made to the filesystem at any point in time - if all the log buffers are full and under IO, then no more transactions can be committed until the current batch completes. It is now common for a single current CPU core to be to able to issue enough transactions to keep the log buffers full and under IO permanently. Hence the XFS journalling subsystem can be considered to be IO bound.

实际上,这为我们提供了在任何时间点都可以对文件系统进行的未完成元数据更改的最大限度——如果所有日志缓冲区都已满且处于IO状态,则在当前批处理完成之前,无法提交更多事务。现在,单个当前CPU内核能够发出足够的事务,以保持日志缓冲区已满并永久处于IO状态是很常见的。因此,XFS日志子系统可以被认为是IO绑定的。

8 Delayed Logging: Concepts

The key thing to note about the asynchronous logging combined with the relogging technique XFS uses is that we can be relogging changed objects multiple times before they are committed to disk in the log buffers. If we return to the previous relogging example, it is entirely possible that transactions A through D are committed to disk in the same log buffer.

关于与XFS使用的重新记录技术相结合的异步日志记录,需要注意的一点是,在将更改的对象提交到日志缓冲区中的磁盘之前,我们可以多次重新记录它们。如果我们回到前面的重新记录示例,事务A到D完全有可能被提交到同一日志缓冲区中的磁盘。

That is, a single log buffer may contain multiple copies of the same object, but only one of those copies needs to be there - the last one “D”, as it contains all the changes from the previous changes. In other words, we have one necessary copy in the log buffer, and three stale copies that are simply wasting space. When we are doing repeated operations on the same set of objects, these “stale objects” can be over 90% of the space used in the log buffers. It is clear that reducing the number of stale objects written to the log would greatly reduce the amount of metadata we write to the log, and this is the fundamental goal of delayed logging.

也就是说,一个日志缓冲区可能包含同一个对象的多个副本,但这些副本中只需要有一个副本,即最后一个“D”,因为它包含以前更改的所有更改。换句话说,我们在日志缓冲区中有一个必要的副本,还有三个过时的副本,这只是在浪费空间。当我们对同一组对象执行重复操作时,这些“过时对象”可能占日志缓冲区中使用空间的90%以上。很明显,减少写入日志的过时对象的数量将大大减少我们写入日志的元数据量,这是延迟日志的基本目标。

From a conceptual point of view, XFS is already doing relogging in memory (where memory == log buffer), only it is doing it extremely inefficiently. It is using logical to physical formatting to do the relogging because there is no infrastructure to keep track of logical changes in memory prior to physically formatting the changes in a transaction to the log buffer. Hence we cannot avoid accumulating stale objects in the log buffers.

从概念的角度来看,XFS已经在内存中进行了重新记录(其中内存==日志缓冲区),但效率极低。它使用逻辑到物理格式化来进行重新记录,因为在将事务中的更改物理格式化到日志缓冲区之前,没有基础设施来跟踪内存中的逻辑更改。因此,我们无法避免在日志缓冲区中累积过时的对象。

Delayed logging is the name we’ve given to keeping and tracking transactional changes to objects in memory outside the log buffer infrastructure. Because of the relogging concept fundamental to the XFS journalling subsystem, this is actually relatively easy to do - all the changes to logged items are already tracked in the current infrastructure. The big problem is how to accumulate them and get them to the log in a consistent, recoverable manner. Describing the problems and how they have been solved is the focus of this document.

延迟日志记录是我们为在日志缓冲区基础结构之外的内存中保存和跟踪对象的事务性更改所取的名称。由于XFS日志记录子系统基本的重新记录概念,这实际上是相对容易做到的——对记录项的所有更改都已经在当前基础结构中进行了跟踪。最大的问题是如何累积它们,并以一致、可恢复的方式将它们写入日志。描述问题及其解决方法是本文件的重点。

One of the key changes that delayed logging makes to the operation of the journalling subsystem is that it disassociates the amount of outstanding metadata changes from the size and number of log buffers available. In other words, instead of there only being a maximum of 2MB of transaction changes not written to the log at any point in time, there may be a much greater amount being accumulated in memory. Hence the potential for loss of metadata on a crash is much greater than for the existing logging mechanism.

延迟日志记录对日志记录子系统的操作造成的一个关键变化是,它将未完成的元数据更改量与可用日志缓冲区的大小和数量分离。换言之,在任何时间点,没有写入日志的事务更改最多为2MB,而内存中的累积量可能更大。因此,与现有的日志记录机制相比,崩溃时元数据丢失的可能性要大得多。

It should be noted that this does not change the guarantee that log recovery will result in a consistent filesystem. What it does mean is that as far as the recovered filesystem is concerned, there may be many thousands of transactions that simply did not occur as a result of the crash. This makes it even more important that applications that care about their data use fsync() where they need to ensure application level data integrity is maintained.

应该注意,这不会改变日志恢复将导致一致文件系统的保证。这意味着,就恢复的文件系统而言,可能有成千上万的事务根本没有因为崩溃而发生。这使得关心数据的应用程序使用fsync()来确保应用程序级数据的完整性变得更加重要。

It should be noted that delayed logging is not an innovative new concept that warrants rigorous proofs to determine whether it is correct or not. The method of accumulating changes in memory for some period before writing them to the log is used effectively in many filesystems including ext3 and ext4. Hence no time is spent in this document trying to convince the reader that the concept is sound. Instead it is simply considered a “solved problem” and as such implementing it in XFS is purely an exercise in software engineering.

应该注意的是,延迟日志记录不是一个创新的新概念,它需要严格的证据来确定它是否正确。在将更改写入日志之前在内存中累积一段时间的方法在包括ext3和ext4在内的许多文件系统中得到了有效的使用。因此,在本文档中没有花费任何时间试图说服读者该概念是正确的。相反,它只是被认为是一个“已解决的问题”,因此在XFS中实现它纯粹是软件工程中的练习。

The fundamental requirements for delayed logging in XFS are simple:

  1. Reduce the amount of metadata written to the log by at least an order of magnitude.
  2. Supply sufficient statistics to validate Requirement #1.
  3. Supply sufficient new tracing infrastructure to be able to debug problems with the new code.
  4. No on-disk format change (metadata or log format).
  5. Enable and disable with a mount option.
  6. No performance regressions for synchronous transaction workloads.

XFS中延迟日志记录的基本要求很简单:

  1. 将写入日志的元数据量至少减少一个数量级。
  2. 提供足够的统计数据以验证要求1。
  3. 提供足够的新跟踪基础设施,以便能够使用新代码调试问题。
  4. 没有磁盘格式更改(元数据或日志格式)。
  5. 使用装载选项启用和禁用。
  6. 同步事务工作负载没有性能回归。

9 Delayed Logging: Design

9.1 Storing Changes

The problem with accumulating changes at a logical level (i.e. just using the existing log item dirty region tracking) is that when it comes to writing the changes to the log buffers, we need to ensure that the object we are formatting is not changing while we do this. This requires locking the object to prevent concurrent modification. Hence flushing the logical changes to the log would require us to lock every object, format them, and then unlock them again.

在逻辑级别累积更改(即仅使用现有的日志项脏区域跟踪)的问题是,在将更改写入日志缓冲区时,我们需要确保我们正在格式化的对象在执行此操作时不会发生更改。这需要锁定对象以防止并发修改。因此,将逻辑更改刷新到日志需要我们锁定每个对象,格式化它们,然后再次解锁它们。

This introduces lots of scope for deadlocks with transactions that are already running. For example, a transaction has object A locked and modified, but needs the delayed logging tracking lock to commit the transaction. However, the flushing thread has the delayed logging tracking lock already held, and is trying to get the lock on object A to flush it to the log buffer. This appears to be an unsolvable deadlock condition, and it was solving this problem that was the barrier to implementing delayed logging for so long.

这为已经在运行的事务带来了很多死锁的空间。例如,事务锁定并修改了对象a,但需要延迟日志跟踪锁来提交事务。然而,刷新线程已经持有延迟日志跟踪锁,并且正在尝试获取对象A上的锁,以将其刷新到日志缓冲区。这似乎是一个无法解决的死锁状态,而正是解决了这个问题,这是实现延迟日志记录的障碍。

The solution is relatively simple - it just took a long time to recognise it. Put simply, the current logging code formats the changes to each item into an vector array that points to the changed regions in the item. The log write code simply copies the memory these vectors point to into the log buffer during transaction commit while the item is locked in the transaction. Instead of using the log buffer as the destination of the formatting code, we can use an allocated memory buffer big enough to fit the formatted vector.

解决方案相对简单-只是花了很长时间才识别出来。简单地说,当前的日志代码将每个项目的更改格式化为指向项目中更改区域的矢量数组。日志写入代码只需在事务提交期间将这些向量指向的内存复制到日志缓冲区中,同时将项锁定在事务中。我们可以使用足够大的内存缓冲区来容纳格式化的向量,而不是使用日志缓冲区作为格式化代码的目标。

If we then copy the vector into the memory buffer and rewrite the vector to point to the memory buffer rather than the object itself, we now have a copy of the changes in a format that is compatible with the log buffer writing code. that does not require us to lock the item to access. This formatting and rewriting can all be done while the object is locked during transaction commit, resulting in a vector that is transactionally consistent and can be accessed without needing to lock the owning item.

如果我们随后将向量复制到内存缓冲区,并重写向量以指向内存缓冲区而不是对象本身,那么我们现在就有了与日志缓冲区编写代码兼容的格式的更改副本。这不需要我们锁定项目才能访问。这种格式化和重写都可以在事务提交期间锁定对象时完成,从而生成一个事务一致的向量,无需锁定所属项即可访问。

Hence we avoid the need to lock items when we need to flush outstanding asynchronous transactions to the log. The differences between the existing formatting method and the delayed logging formatting can be seen in the diagram below.

因此,当我们需要将未完成的异步事务刷新到日志时,我们避免了锁定项的需要。现有格式方法和延迟日志格式之间的差异可以在下图中看到。

Current format log vector:

Object    +---------------------------------------------+
Vector 1      +----+
Vector 2                    +----+
Vector 3                                   +----------+

After formatting:

Log Buffer    +-V1-+-V2-+----V3----+

Delayed logging vector:

Object    +---------------------------------------------+
Vector 1      +----+
Vector 2                    +----+
Vector 3                                   +----------+

After formatting:

Memory Buffer +-V1-+-V2-+----V3----+
Vector 1      +----+
Vector 2           +----+
Vector 3                +----------+

The memory buffer and associated vector need to be passed as a single object, but still need to be associated with the parent object so if the object is relogged we can replace the current memory buffer with a new memory buffer that contains the latest changes.

内存缓冲区和关联向量需要作为单个对象传递,但仍需要与父对象关联,因此如果对象被重新记录,我们可以用包含最新更改的新内存缓冲区替换当前内存缓冲区。

The reason for keeping the vector around after we’ve formatted the memory buffer is to support splitting vectors across log buffer boundaries correctly. If we don’t keep the vector around, we do not know where the region boundaries are in the item, so we’d need a new encapsulation method for regions in the log buffer writing (i.e. double encapsulation). This would be an on-disk format change and as such is not desirable. It also means we’d have to write the log region headers in the formatting stage, which is problematic as there is per region state that needs to be placed into the headers during the log write.

格式化内存缓冲区后保持矢量不变的原因是为了支持在日志缓冲区边界上正确分割矢量。如果我们不保留向量,我们就不知道区域边界在项目中的位置,因此我们需要为日志缓冲区写入中的区域提供一种新的封装方法(即双重封装)。这将是磁盘上的格式更改,因此是不可取的。这也意味着我们必须在格式化阶段写入日志区域标头,这是有问题的,因为在日志写入期间需要将每个区域的状态放入标头中。

Hence we need to keep the vector, but by attaching the memory buffer to it and rewriting the vector addresses to point at the memory buffer we end up with a self-describing object that can be passed to the log buffer write code to be handled in exactly the same manner as the existing log vectors are handled. Hence we avoid needing a new on-disk format to handle items that have been relogged in memory.

因此,我们需要保留向量,但通过将内存缓冲区附加到它并重写向量地址以指向内存缓冲区,我们最终得到了一个自我描述的对象,该对象可以传递给日志缓冲区写代码,以与处理现有日志向量完全相同的方式进行处理。因此,我们避免了需要新的磁盘格式来处理已重新记录在内存中的项目。

9.2 Tracking Changes

Now that we can record transactional changes in memory in a form that allows them to be used without limitations, we need to be able to track and accumulate them so that they can be written to the log at some later point in time. The log item is the natural place to store this vector and buffer, and also makes sense to be the object that is used to track committed objects as it will always exist once the object has been included in a transaction.

现在,我们可以在内存中以允许无限制使用的形式记录事务性更改,我们需要能够跟踪和累积它们,以便在稍后的某个时间点将它们写入日志。日志项是存储此向量和缓冲区的自然位置,也是用于跟踪已提交对象的对象,因为一旦对象包含在事务中,它将始终存在。

The log item is already used to track the log items that have been written to the log but not yet written to disk. Such log items are considered “active” and as such are stored in the Active Item List (AIL) which is a LSN-ordered double linked list. Items are inserted into this list during log buffer IO completion, after which they are unpinned and can be written to disk. An object that is in the AIL can be relogged, which causes the object to be pinned again and then moved forward in the AIL when the log buffer IO completes for that transaction.

日志项已用于跟踪已写入日志但尚未写入磁盘的日志项。此类日志项被视为“活动”,因此存储在活动项列表(AIL)中,该列表是一个LSN排序的双链接列表。在日志缓冲区IO完成期间,项目将插入此列表,之后将取消固定,并可以写入磁盘。AIL中的对象可以重新记录,这会导致该对象再次固定,然后在该事务的日志缓冲区IO完成时在AIL中向前移动。

Essentially, this shows that an item that is in the AIL can still be modified and relogged, so any tracking must be separate to the AIL infrastructure. As such, we cannot reuse the AIL list pointers for tracking committed items, nor can we store state in any field that is protected by the AIL lock. Hence the committed item tracking needs its own locks, lists and state fields in the log item.

本质上,这表明AIL中的项目仍然可以被修改和重新记录,因此任何跟踪都必须与AIL基础设施分开。因此,我们不能重用AIL列表指针来跟踪提交的项目,也不能将状态存储在受AIL锁保护的任何字段中。因此,提交的项跟踪需要日志项中自己的锁、列表和状态字段。

Similar to the AIL, tracking of committed items is done through a new list called the Committed Item List (CIL). The list tracks log items that have been committed and have formatted memory buffers attached to them. It tracks objects in transaction commit order, so when an object is relogged it is removed from its place in the list and re-inserted at the tail. This is entirely arbitrary and done to make it easy for debugging - the last items in the list are the ones that are most recently modified. Ordering of the CIL is not necessary for transactional integrity (as discussed in the next section) so the ordering is done for convenience/sanity of the developers.

与AIL类似,通过名为committed Item list(CIL)的新列表来跟踪提交的项目。该列表跟踪已提交的日志项,并将格式化的内存缓冲区附加到日志项。它按照事务提交顺序跟踪对象,因此当对象被重新记录时,它将从列表中的位置移除,并在尾部重新插入。这完全是任意的,而且这样做是为了便于调试——列表中的最后一项是最近修改过的。CIL的排序对于事务完整性来说是不必要的(如下一节所讨论的),所以排序是为了开发人员的方便/健全。

9.3 Delayed Logging: Checkpoints

When we have a log synchronisation event, commonly known as a “log force”, all the items in the CIL must be written into the log via the log buffers. We need to write these items in the order that they exist in the CIL, and they need to be written as an atomic transaction. The need for all the objects to be written as an atomic transaction comes from the requirements of relogging and log replay - all the changes in all the objects in a given transaction must either be completely replayed during log recovery, or not replayed at all. If a transaction is not replayed because it is not complete in the log, then no later transactions should be replayed, either.

当发生日志同步事件(通常称为“日志强制”)时,CIL中的所有项目都必须通过日志缓冲区写入日志。我们需要按照这些项目在CIL中的存在顺序来编写这些项目,并且它们需要作为一个原子事务来编写。需要将所有对象作为原子事务写入,这源于重新记录和日志重放的要求——给定事务中所有对象的所有更改必须在日志恢复期间完全重放,或者根本不重放。如果某个事务由于在日志中未完成而未被重放,则也不应重放以后的事务。

To fulfill this requirement, we need to write the entire CIL in a single log transaction. Fortunately, the XFS log code has no fixed limit on the size of a transaction, nor does the log replay code. The only fundamental limit is that the transaction cannot be larger than just under half the size of the log. The reason for this limit is that to find the head and tail of the log, there must be at least one complete transaction in the log at any given time. If a transaction is larger than half the log, then there is the possibility that a crash during the write of a such a transaction could partially overwrite the only complete previous transaction in the log. This will result in a recovery failure and an inconsistent filesystem and hence we must enforce the maximum size of a checkpoint to be slightly less than a half the log.

为了满足这一要求,我们需要在一个日志事务中写入整个CIL。幸运的是,XFS日志代码对事务的大小没有固定的限制,日志回放代码也没有。唯一的基本限制是事务大小不能超过日志大小的一半。这个限制的原因是,为了找到日志的开头和结尾,在任何给定的时间,日志中必须至少有一个完整的事务。如果事务大于日志的一半,则在写入此类事务期间发生崩溃可能会部分覆盖日志中唯一完整的先前事务。这将导致恢复失败和文件系统不一致,因此我们必须强制检查点的最大大小略小于日志的一半。

Apart from this size requirement, a checkpoint transaction looks no different to any other transaction - it contains a transaction header, a series of formatted log items and a commit record at the tail. From a recovery perspective, the checkpoint transaction is also no different - just a lot bigger with a lot more items in it. The worst case effect of this is that we might need to tune the recovery transaction object hash size.

除了这个大小要求之外,检查点事务看起来与任何其他事务都没有什么不同——它包含一个事务头、一系列格式化的日志项和尾部的提交记录。从恢复的角度来看,检查点事务也没有什么不同-只是更大了,其中有更多的项目。最坏的情况是,我们可能需要调整恢复事务对象哈希大小。

Because the checkpoint is just another transaction and all the changes to log items are stored as log vectors, we can use the existing log buffer writing code to write the changes into the log. To do this efficiently, we need to minimise the time we hold the CIL locked while writing the checkpoint transaction. The current log write code enables us to do this easily with the way it separates the writing of the transaction contents (the log vectors) from the transaction commit record, but tracking this requires us to have a per-checkpoint context that travels through the log write process through to checkpoint completion.

因为检查点只是另一个事务,对日志项的所有更改都存储为日志向量,所以我们可以使用现有的日志缓冲区编写代码将更改写入日志。为了有效地做到这一点,我们需要尽量减少在写入检查点事务时锁定CIL的时间。当前的日志写入代码使我们能够通过将事务内容(日志向量)的写入与事务提交记录分开的方式轻松地完成这一点,但跟踪这一点需要我们有一个贯穿日志写入过程直至检查点完成的每个检查点上下文。

Hence a checkpoint has a context that tracks the state of the current checkpoint from initiation to checkpoint completion. A new context is initiated at the same time a checkpoint transaction is started. That is, when we remove all the current items from the CIL during a checkpoint operation, we move all those changes into the current checkpoint context. We then initialise a new context and attach that to the CIL for aggregation of new transactions.

因此,检查点具有跟踪当前检查点从启动到检查点完成的状态的上下文。在启动检查点事务的同时启动新上下文。也就是说,当我们在检查点操作期间从CIL中删除所有当前项时,我们将所有这些更改移动到当前检查点上下文中。然后,我们初始化一个新的上下文,并将其附加到CIL以聚合新事务。

This allows us to unlock the CIL immediately after transfer of all the committed items and effectively allows new transactions to be issued while we are formatting the checkpoint into the log. It also allows concurrent checkpoints to be written into the log buffers in the case of log force heavy workloads, just like the existing transaction commit code does. This, however, requires that we strictly order the commit records in the log so that checkpoint sequence order is maintained during log replay.

这允许我们在传输所有提交的项目后立即解锁CIL,并有效地允许在将检查点格式化到日志中时发出新的事务。它还允许在日志强制繁重的工作负载情况下将并发检查点写入日志缓冲区,就像现有的事务提交代码一样。然而,这要求我们严格排序日志中的提交记录,以便在日志回放期间保持检查点顺序。

To ensure that we can be writing an item into a checkpoint transaction at the same time another transaction modifies the item and inserts the log item into the new CIL, then checkpoint transaction commit code cannot use log items to store the list of log vectors that need to be written into the transaction. Hence log vectors need to be able to be chained together to allow them to be detached from the log items. That is, when the CIL is flushed the memory buffer and log vector attached to each log item needs to be attached to the checkpoint context so that the log item can be released. In diagrammatic form, the CIL would look like this before the flush:

为了确保我们可以在另一个事务修改项并将日志项插入新CIL的同时将项写入检查点事务,检查点事务提交代码不能使用日志项来存储需要写入事务的日志向量列表。因此,日志向量需要能够链接在一起,以允许它们与日志项分离。也就是说,当刷新CIL时,需要将附加到每个日志项的内存缓冲区和日志向量附加到检查点上下文,以便可以释放日志项。以图解形式,冲洗前的CIL如下所示:

CIL Head
   |
   V
Log Item <-> log vector 1       -> memory buffer
   |                            -> vector array
   V
Log Item <-> log vector 2       -> memory buffer
   |                            -> vector array
   V
......
   |
   V
Log Item <-> log vector N-1     -> memory buffer
   |                            -> vector array
   V
Log Item <-> log vector N       -> memory buffer
                                -> vector array

And after the flush the CIL head is empty, and the checkpoint context log vector list would look like:

刷新后,CIL头为空,检查点上下文日志向量列表如下所示:

Checkpoint Context
   |
   V
log vector 1    -> memory buffer
   |            -> vector array
   |            -> Log Item
   V
log vector 2    -> memory buffer
   |            -> vector array
   |            -> Log Item
   V
......
   |
   V
log vector N-1  -> memory buffer
   |            -> vector array
   |            -> Log Item
   V
log vector N    -> memory buffer
                -> vector array
                -> Log Item

Once this transfer is done, the CIL can be unlocked and new transactions can start, while the checkpoint flush code works over the log vector chain to commit the checkpoint.

传输完成后,CIL可以解锁,新的事务可以启动,而检查点刷新代码在日志向量链上工作以提交检查点。

Once the checkpoint is written into the log buffers, the checkpoint context is attached to the log buffer that the commit record was written to along with a completion callback. Log IO completion will call that callback, which can then run transaction committed processing for the log items (i.e. insert into AIL and unpin) in the log vector chain and then free the log vector chain and checkpoint context.

将检查点写入日志缓冲区后,检查点上下文将与完成回调一起附加到写入提交记录的日志缓冲区。日志IO完成将调用该回调,然后可以对日志向量链中的日志项运行事务提交处理(即插入AIL和取消锁定),然后释放日志向量链和检查点上下文。

Discussion Point: I am uncertain as to whether the log item is the most efficient way to track vectors, even though it seems like the natural way to do it. The fact that we walk the log items (in the CIL) just to chain the log vectors and break the link between the log item and the log vector means that we take a cache line hit for the log item list modification, then another for the log vector chaining. If we track by the log vectors, then we only need to break the link between the log item and the log vector, which means we should dirty only the log item cachelines. Normally I wouldn’t be concerned about one vs two dirty cachelines except for the fact I’ve seen upwards of 80,000 log vectors in one checkpoint transaction. I’d guess this is a “measure and compare” situation that can be done after a working and reviewed implementation is in the dev tree….

讨论点:我不确定日志项是否是跟踪向量的最有效方式,尽管这看起来是最自然的方式。事实上,我们遍历日志项(在CIL中)只是为了链接日志向量并断开日志项和日志向量之间的链接,这意味着我们对日志项列表修改进行缓存行命中,然后另一个用于对数向量链。如果我们通过日志向量进行跟踪,那么我们只需要断开日志项和日志向量之间的链接,这意味着我们应该只清理日志项缓存行。通常情况下,我不会担心一个或两个脏缓存行,除非我在一个检查点事务中看到了超过80000个日志向量。我想这是一种“衡量和比较”的情况,可以在开发树中找到一个经过工作和审查的实现之后完成…。

9.4 Delayed Logging: Checkpoint Sequencing

One of the key aspects of the XFS transaction subsystem is that it tags committed transactions with the log sequence number of the transaction commit. This allows transactions to be issued asynchronously even though there may be future operations that cannot be completed until that transaction is fully committed to the log. In the rare case that a dependent operation occurs (e.g. re-using a freed metadata extent for a data extent), a special, optimised log force can be issued to force the dependent transaction to disk immediately.

XFS事务子系统的一个关键方面是,它用事务提交的日志序列号标记提交的事务。这允许异步发出事务,即使在事务完全提交到日志之前可能会有无法完成的未来操作。在发生从属操作的罕见情况下(例如,将释放的元数据数据块重新用于数据数据块),可以发出特殊的优化日志强制,以强制将从属事务立即写入磁盘。

To do this, transactions need to record the LSN of the commit record of the transaction. This LSN comes directly from the log buffer the transaction is written into. While this works just fine for the existing transaction mechanism, it does not work for delayed logging because transactions are not written directly into the log buffers. Hence some other method of sequencing transactions is required.

为此,事务需要记录事务提交记录的LSN。该LSN直接来自事务写入的日志缓冲区。虽然这对现有的事务机制很好,但对延迟日志记录不起作用,因为事务不会直接写入日志缓冲区。因此,需要一些其他排序事务的方法。

As discussed in the checkpoint section, delayed logging uses per-checkpoint contexts, and as such it is simple to assign a sequence number to each checkpoint. Because the switching of checkpoint contexts must be done atomically, it is simple to ensure that each new context has a monotonically increasing sequence number assigned to it without the need for an external atomic counter - we can just take the current context sequence number and add one to it for the new context.

正如检查点部分所讨论的,延迟日志记录使用每个检查点上下文,因此,为每个检查点分配序列号很简单。由于检查点上下文的切换必须以原子方式进行,因此很容易确保每个新上下文都分配了单调递增的序列号,而不需要外部原子计数器-我们只需获取当前上下文序列号,并为新上下文添加一个序列号。

Then, instead of assigning a log buffer LSN to the transaction commit LSN during the commit, we can assign the current checkpoint sequence. This allows operations that track transactions that have not yet completed know what checkpoint sequence needs to be committed before they can continue. As a result, the code that forces the log to a specific LSN now needs to ensure that the log forces to a specific checkpoint.

然后,我们可以指定当前检查点序列,而不是在提交期间为事务提交LSN分配日志缓冲区LSN。这允许跟踪尚未完成的事务的操作知道需要提交什么检查点序列才能继续。因此,强制日志到特定LSN的代码现在需要确保日志强制到特定检查点。

To ensure that we can do this, we need to track all the checkpoint contexts that are currently committing to the log. When we flush a checkpoint, the context gets added to a “committing” list which can be searched. When a checkpoint commit completes, it is removed from the committing list. Because the checkpoint context records the LSN of the commit record for the checkpoint, we can also wait on the log buffer that contains the commit record, thereby using the existing log force mechanisms to execute synchronous forces.

为了确保我们能够做到这一点,我们需要跟踪当前提交到日志的所有检查点上下文。当我们刷新检查点时,上下文被添加到可以搜索的“提交”列表中。当检查点提交完成时,它将从提交列表中删除。因为检查点上下文记录了检查点提交记录的LSN,所以我们还可以等待包含提交记录的日志缓冲区,从而使用现有的日志强制机制来执行同步强制。

It should be noted that the synchronous forces may need to be extended with mitigation algorithms similar to the current log buffer code to allow aggregation of multiple synchronous transactions if there are already synchronous transactions being flushed. Investigation of the performance of the current design is needed before making any decisions here.

应该注意,如果已经有同步事务被刷新,则可能需要使用类似于当前日志缓冲代码的缓解算法来扩展同步力,以允许聚合多个同步事务。在做出任何决定之前,需要对当前设计的性能进行调查。

The main concern with log forces is to ensure that all the previous checkpoints are also committed to disk before the one we need to wait for. Therefore we need to check that all the prior contexts in the committing list are also complete before waiting on the one we need to complete. We do this synchronisation in the log force code so that we don’t need to wait anywhere else for such serialisation - it only matters when we do a log force.

日志力量的主要关注点是确保在我们需要等待的检查点之前,所有先前的检查点都已提交到磁盘。因此,在等待需要完成的上下文之前,我们需要检查提交列表中的所有先前上下文是否也已完成。我们在log force代码中进行同步,这样我们就不需要在其他地方等待这样的串行化了——这只在我们进行log force时才重要。

The only remaining complexity is that a log force now also has to handle the case where the forcing sequence number is the same as the current context. That is, we need to flush the CIL and potentially wait for it to complete. This is a simple addition to the existing log forcing code to check the sequence numbers and push if required. Indeed, placing the current sequence checkpoint flush in the log force code enables the current mechanism for issuing synchronous transactions to remain untouched (i.e. commit an asynchronous transaction, then force the log at the LSN of that transaction) and so the higher level code behaves the same regardless of whether delayed logging is being used or not.

唯一剩下的复杂性是,日志强制现在还必须处理强制序列号与当前上下文相同的情况。也就是说,我们需要刷新CIL,并可能等待其完成。这是对现有日志强制代码的简单添加,用于检查序列号并在需要时进行推送。事实上,将当前序列检查点刷新放在日志强制代码中,可以使当前发布同步事务的机制保持不变(即提交异步事务,然后在该事务的LSN处强制日志),因此无论是否使用延迟日志记录,更高级别的代码都表现相同。

9.5 Delayed Logging: Checkpoint Log Space Accounting

The big issue for a checkpoint transaction is the log space reservation for the transaction. We don’t know how big a checkpoint transaction is going to be ahead of time, nor how many log buffers it will take to write out, nor the number of split log vector regions are going to be used. We can track the amount of log space required as we add items to the commit item list, but we still need to reserve the space in the log for the checkpoint.

检查点事务的最大问题是事务的日志空间保留。我们不知道检查点事务提前会有多大,也不知道写出来需要多少日志缓冲区,也不清楚要使用的分割日志向量区域的数量。当我们向提交项列表中添加项时,我们可以跟踪所需的日志空间量,但我们仍然需要在日志中为检查点保留空间。

A typical transaction reserves enough space in the log for the worst case space usage of the transaction. The reservation accounts for log record headers, transaction and region headers, headers for split regions, buffer tail padding, etc. as well as the actual space for all the changed metadata in the transaction. While some of this is fixed overhead, much of it is dependent on the size of the transaction and the number of regions being logged (the number of log vectors in the transaction).

典型的事务在日志中保留了足够的空间,以便在最坏的情况下使用事务的空间。保留日志记录头、事务和区域头、拆分区域头、缓冲区尾填充等,以及事务中所有更改元数据的实际空间。虽然其中一些开销是固定的,但大部分开销取决于事务的大小和记录的区域数(事务中的日志向量数)。

An example of the differences would be logging directory changes versus logging inode changes. If you modify lots of inode cores (e.g. chmod -R g+w *), then there are lots of transactions that only contain an inode core and an inode log format structure. That is, two vectors totaling roughly 150 bytes. If we modify 10,000 inodes, we have about 1.5MB of metadata to write in 20,000 vectors. Each vector is 12 bytes, so the total to be logged is approximately 1.75MB. In comparison, if we are logging full directory buffers, they are typically 4KB each, so we in 1.5MB of directory buffers we’d have roughly 400 buffers and a buffer format structure for each buffer - roughly 800 vectors or 1.51MB total space. From this, it should be obvious that a static log space reservation is not particularly flexible and is difficult to select the “optimal value” for all workloads.

区别的一个例子是记录目录更改与记录inode更改。如果您修改了许多inode核心(例如“chmod-R g+w*”),那么有很多事务只包含inode核心和inode日志格式结构。也就是说,两个矢量总计约150字节。如果我们修改10000个索引节点,那么我们有大约1.5MB的元数据可以写入20000个向量。每个向量是12个字节,因此要记录的总大小大约为1.75MB。相比之下,如果我们记录的是完整的目录缓冲区,它们通常每个都是4KB,因此在1.5MB的目录缓冲中,我们将有大约400个缓冲区,每个缓冲区都有一个缓冲区格式结构-大约800个向量或1.51MB的总空间。由此可见,静态日志空间保留不是特别灵活,很难为所有工作负载选择“最佳值”。

Further, if we are going to use a static reservation, which bit of the entire reservation does it cover? We account for space used by the transaction reservation by tracking the space currently used by the object in the CIL and then calculating the increase or decrease in space used as the object is relogged. This allows for a checkpoint reservation to only have to account for log buffer metadata used such as log header records.

此外,如果我们要使用静态保留,它将覆盖整个保留的哪一部分?我们通过跟踪CIL中对象当前使用的空间,然后在重新记录对象时计算所使用空间的增加或减少,来计算事务保留使用的空间。这允许检查点保留仅考虑使用的日志缓冲区元数据,如日志头记录。

However, even using a static reservation for just the log metadata is problematic. Typically log record headers use at least 16KB of log space per 1MB of log space consumed (512 bytes per 32k) and the reservation needs to be large enough to handle arbitrary sized checkpoint transactions. This reservation needs to be made before the checkpoint is started, and we need to be able to reserve the space without sleeping. For a 8MB checkpoint, we need a reservation of around 150KB, which is a non-trivial amount of space.

然而,即使只对日志元数据使用静态保留也是有问题的。通常,日志记录头每消耗1MB的日志空间至少使用16KB的日志空间(每32k 512字节),并且保留空间需要足够大,以处理任意大小的检查点事务。这个预留需要在检查点启动之前进行,我们需要能够在不睡觉的情况下预留空间。对于8MB的检查点,我们需要大约150KB的预留空间,这是一个非常小的空间量。

A static reservation needs to manipulate the log grant counters - we can take a permanent reservation on the space, but we still need to make sure we refresh the write reservation (the actual space available to the transaction) after every checkpoint transaction completion. Unfortunately, if this space is not available when required, then the regrant code will sleep waiting for it.

静态保留需要操作日志授予计数器-我们可以对空间进行永久保留,但仍需要确保在每个检查点事务完成后刷新写保留(事务的实际可用空间)。不幸的是,如果这个空间在需要时不可用,那么再生代码将休眠等待它。

The problem with this is that it can lead to deadlocks as we may need to commit checkpoints to be able to free up log space (refer back to the description of rolling transactions for an example of this). Hence we must always have space available in the log if we are to use static reservations, and that is very difficult and complex to arrange. It is possible to do, but there is a simpler way.

这样做的问题是,它可能会导致死锁,因为我们可能需要提交检查点来释放日志空间(有关此示例,请参阅滚动事务的描述)。因此,如果要使用静态预订,我们必须在日志中始终有可用空间,这是非常困难和复杂的。这是可能的,但有一种更简单的方法。

The simpler way of doing this is tracking the entire log space used by the items in the CIL and using this to dynamically calculate the amount of log space required by the log metadata. If this log metadata space changes as a result of a transaction commit inserting a new memory buffer into the CIL, then the difference in space required is removed from the transaction that causes the change. Transactions at this level will always have enough space available in their reservation for this as they have already reserved the maximal amount of log metadata space they require, and such a delta reservation will always be less than or equal to the maximal amount in the reservation.

更简单的方法是跟踪CIL中项目所使用的整个日志空间,并使用该方法动态计算日志元数据所需的日志空间量。如果由于事务提交将新的内存缓冲区插入CIL而导致日志元数据空间发生更改,则所需空间的差异将从导致更改的事务中删除。此级别的事务在其保留中将始终有足够的空间可用,因为它们已经保留了所需的最大日志元数据空间量,并且这种增量保留将始终小于或等于保留中的最大量。

Hence we can grow the checkpoint transaction reservation dynamically as items are added to the CIL and avoid the need for reserving and regranting log space up front. This avoids deadlocks and removes a blocking point from the checkpoint flush code.

因此,我们可以在将项目添加到CIL时动态增加检查点事务保留,并避免预先保留和重新分配日志空间的需要。这避免了死锁,并从检查点清除代码中删除了一个阻塞点。

As mentioned early, transactions can’t grow to more than half the size of the log. Hence as part of the reservation growing, we need to also check the size of the reservation against the maximum allowed transaction size. If we reach the maximum threshold, we need to push the CIL to the log. This is effectively a “background flush” and is done on demand. This is identical to a CIL push triggered by a log force, only that there is no waiting for the checkpoint commit to complete. This background push is checked and executed by transaction commit code.

如前所述,事务不能增长到日志大小的一半以上。因此,作为预订增长的一部分,我们还需要根据允许的最大交易大小检查预订的大小。如果达到最大阈值,我们需要将CIL推到日志中。这实际上是一种“后台刷新”,并且是按需完成的。这与日志强制触发的CIL推送相同,只是不需要等待检查点提交完成。此后台推送由事务提交代码检查和执行。

If the transaction subsystem goes idle while we still have items in the CIL, they will be flushed by the periodic log force issued by the xfssyncd. This log force will push the CIL to disk, and if the transaction subsystem stays idle, allow the idle log to be covered (effectively marked clean) in exactly the same manner that is done for the existing logging method. A discussion point is whether this log force needs to be done more frequently than the current rate which is once every 30s.

如果事务子系统在CIL中仍有项目时处于空闲状态,那么xfssyncd发出的周期性日志强制将刷新这些项目。此日志强制将CIL推送到磁盘,如果事务子系统保持空闲,则允许以与现有日志记录方法完全相同的方式覆盖空闲日志(有效标记为清除)。一个讨论点是,是否需要比当前的频率(每30秒一次)更频繁地执行此对数强制。

9.6 Delayed Logging: Log Item Pinning

Currently log items are pinned during transaction commit while the items are still locked. This happens just after the items are formatted, though it could be done any time before the items are unlocked. The result of this mechanism is that items get pinned once for every transaction that is committed to the log buffers. Hence items that are relogged in the log buffers will have a pin count for every outstanding transaction they were dirtied in. When each of these transactions is completed, they will unpin the item once. As a result, the item only becomes unpinned when all the transactions complete and there are no pending transactions. Thus the pinning and unpinning of a log item is symmetric as there is a 1:1 relationship with transaction commit and log item completion.

当前日志项在事务提交期间被锁定,而这些项仍被锁定。这发生在项目格式化之后,尽管可以在项目解锁之前随时进行。这种机制的结果是,对于提交到日志缓冲区的每个事务,项都被固定一次。因此,重新记录在日志缓冲区中的项目将对它们被清除的每个未完成事务都有一个pin计数。当这些事务中的每个事务完成时,它们将取消固定该项目一次。因此,只有当所有事务都完成并且没有挂起的事务时,项目才会变为未固定。因此,日志项的固定和取消固定是对称的,因为事务提交和日志项完成之间存在1:1的关系。

For delayed logging, however, we have an asymmetric transaction commit to completion relationship. Every time an object is relogged in the CIL it goes through the commit process without a corresponding completion being registered. That is, we now have a many-to-one relationship between transaction commit and log item completion. The result of this is that pinning and unpinning of the log items becomes unbalanced if we retain the “pin on transaction commit, unpin on transaction completion” model.

然而,对于延迟日志记录,我们有一个不对称的事务提交完成关系。每次在CIL中重新记录对象时,它都会经过提交过程,而不会注册相应的完成。也就是说,我们现在在事务提交和日志项完成之间有一个多对一的关系。这样做的结果是,如果我们保留“事务提交时固定,事务完成时取消固定”模型,则日志项的固定和取消固定将变得不平衡。

To keep pin/unpin symmetry, the algorithm needs to change to a “pin on insertion into the CIL, unpin on checkpoint completion”. In other words, the pinning and unpinning becomes symmetric around a checkpoint context. We have to pin the object the first time it is inserted into the CIL - if it is already in the CIL during a transaction commit, then we do not pin it again. Because there can be multiple outstanding checkpoint contexts, we can still see elevated pin counts, but as each checkpoint completes the pin count will retain the correct value according to its context.

为了保持固定/取消固定对称,算法需要更改为“插入CIL时固定,检查点完成时取消固定”。换句话说,固定和取消固定在检查点上下文周围变得对称。我们必须在对象第一次插入CIL时锁定它——如果在事务提交期间它已经在CIL中,那么我们就不再锁定它。因为可能有多个未完成的检查点上下文,我们仍然可以看到提升的pin计数,但随着每个检查点完成,pin计数将根据其上下文保持正确的值。

Just to make matters slightly more complex, this checkpoint level context for the pin count means that the pinning of an item must take place under the CIL commit/flush lock. If we pin the object outside this lock, we cannot guarantee which context the pin count is associated with. This is because of the fact pinning the item is dependent on whether the item is present in the current CIL or not. If we don’t pin the CIL first before we check and pin the object, we have a race with CIL being flushed between the check and the pin (or not pinning, as the case may be). Hence we must hold the CIL flush/commit lock to guarantee that we pin the items correctly.

为了让事情稍微复杂一点,pin计数的检查点级别上下文意味着项目的锁定必须在CIL提交/刷新锁下进行。如果我们将对象锁定在这个锁之外,我们无法保证锁定计数与哪个上下文相关联。这是因为固定项目取决于当前CIL中是否存在该项目。如果我们在检查和锁定对象之前没有首先锁定CIL,那么我们会在检查和引脚之间产生CIL被刷新的竞争(或者不锁定,视情况而定)。因此,我们必须持有CIL刷新/提交锁,以确保正确锁定项目。

9.7 Delayed Logging: Concurrent Scalability

A fundamental requirement for the CIL is that accesses through transaction commits must scale to many concurrent commits. The current transaction commit code does not break down even when there are transactions coming from 2048 processors at once. The current transaction code does not go any faster than if there was only one CPU using it, but it does not slow down either.

CIL的一个基本要求是,通过事务提交进行的访问必须扩展到许多并发提交。即使同时有2048个处理器发出的事务,当前事务提交代码也不会崩溃。当前事务代码的速度不会比只有一个CPU使用它时快,但也不会减慢速度。

As a result, the delayed logging transaction commit code needs to be designed for concurrency from the ground up. It is obvious that there are serialisation points in the design - the three important ones are:

  1. Locking out new transaction commits while flushing the CIL
  2. Adding items to the CIL and updating item space accounting
  3. Checkpoint commit ordering

因此,延迟日志记录事务提交代码需要从头开始为并发性而设计。很明显,设计中存在串行化点——三个重要点是:

  1. 刷新CIL时锁定新事务提交
  2. 向CIL添加项目并更新项目空间核算
  3. 检查点提交顺序

Looking at the transaction commit and CIL flushing interactions, it is clear that we have a many-to-one interaction here. That is, the only restriction on the number of concurrent transactions that can be trying to commit at once is the amount of space available in the log for their reservations. The practical limit here is in the order of several hundred concurrent transactions for a 128MB log, which means that it is generally one per CPU in a machine.

看看事务提交和CIL刷新交互,很明显我们这里有一个多对一的交互。也就是说,对可以尝试一次提交的并发事务数量的唯一限制是日志中可用于保留的空间量。这里的实际限制是128MB日志的数百个并发事务数量级,这意味着在一台机器中,每个CPU通常有一个并发事务。

The amount of time a transaction commit needs to hold out a flush is a relatively long period of time - the pinning of log items needs to be done while we are holding out a CIL flush, so at the moment that means it is held across the formatting of the objects into memory buffers (i.e. while memcpy()s are in progress). Ultimately a two pass algorithm where the formatting is done separately to the pinning of objects could be used to reduce the hold time of the transaction commit side. 事务提交执行刷新所需的时间相对较长-在执行CIL刷新时需要锁定日志项,因此目前这意味着在将对象格式化为内存缓冲区时(即memcpy()s正在进行中)。最终,可以使用两次传递算法来减少事务提交端的保持时间,其中格式化是单独对对象进行固定的。

Because of the number of potential transaction commit side holders, the lock really needs to be a sleeping lock - if the CIL flush takes the lock, we do not want every other CPU in the machine spinning on the CIL lock. Given that flushing the CIL could involve walking a list of tens of thousands of log items, it will get held for a significant time and so spin contention is a significant concern. Preventing lots of CPUs spinning doing nothing is the main reason for choosing a sleeping lock even though nothing in either the transaction commit or CIL flush side sleeps with the lock held.

由于潜在的事务提交方持有者的数量,锁确实需要是休眠锁-如果CIL刷新占用了锁,我们不希望机器中的其他CPU都在CIL锁上旋转。考虑到刷新CIL可能需要遍历数万个日志项的列表,它将被保存很长时间,因此旋转争用是一个重要的问题。即使事务提交或CIL刷新端中的任何一个都不会在锁保持的情况下休眠,但防止大量CPU空转,这是选择休眠锁的主要原因。

It should also be noted that CIL flushing is also a relatively rare operation compared to transaction commit for asynchronous transaction workloads - only time will tell if using a read-write semaphore for exclusion will limit transaction commit concurrency due to cache line bouncing of the lock on the read side.

还应注意,与异步事务工作负载的事务提交相比,CIL刷新也是一种相对罕见的操作——只有时间才能判断使用读写信号量进行排除是否会由于读取端锁的缓存线跳动而限制事务提交并发性。

The second serialisation point is on the transaction commit side where items are inserted into the CIL. Because transactions can enter this code concurrently, the CIL needs to be protected separately from the above commit/flush exclusion. It also needs to be an exclusive lock but it is only held for a very short time and so a spin lock is appropriate here. It is possible that this lock will become a contention point, but given the short hold time once per transaction I think that contention is unlikely.

第二个串行化点位于事务提交端,项目插入CIL。因为事务可以同时输入此代码,所以CIL需要与上述提交/刷新排除分开保护。它还需要是一个独占锁,但它只保持很短的时间,因此旋转锁在这里是合适的。这个锁可能会成为争用点,但考虑到每个事务一次的保持时间很短,我认为争用是不可能的。

The final serialisation point is the checkpoint commit record ordering code that is run as part of the checkpoint commit and log force sequencing. The code path that triggers a CIL flush (i.e. whatever triggers the log force) will enter an ordering loop after writing all the log vectors into the log buffers but before writing the commit record. This loop walks the list of committing checkpoints and needs to block waiting for checkpoints to complete their commit record write. As a result it needs a lock and a wait variable. Log force sequencing also requires the same lock, list walk, and blocking mechanism to ensure completion of checkpoints.

最后一个串行化点是检查点提交记录排序代码,它作为检查点提交和日志强制排序的一部分运行。触发CIL刷新的代码路径(即任何触发日志强制的代码)将在将所有日志向量写入日志缓冲区之后但在写入提交记录之前进入排序循环。此循环遍历提交检查点列表,并需要阻止等待检查点完成提交记录写入。因此,它需要一个锁和一个等待变量。日志强制排序还需要相同的锁定、列表遍历和阻塞机制,以确保完成检查点。

These two sequencing operations can use the mechanism even though the events they are waiting for are different. The checkpoint commit record sequencing needs to wait until checkpoint contexts contain a commit LSN (obtained through completion of a commit record write) while log force sequencing needs to wait until previous checkpoint contexts are removed from the committing list (i.e. they’ve completed). A simple wait variable and broadcast wakeups (thundering herds) has been used to implement these two serialisation queues. They use the same lock as the CIL, too. If we see too much contention on the CIL lock, or too many context switches as a result of the broadcast wakeups these operations can be put under a new spinlock and given separate wait lists to reduce lock contention and the number of processes woken by the wrong event.

这两个排序操作可以使用该机制,即使它们等待的事件不同。检查点提交记录排序需要等待直到检查点上下文包含提交LSN(通过完成提交记录写入获得),而日志强制排序需要等待,直到之前的检查点上下文从提交列表中删除(即,它们已完成)。一个简单的等待变量和广播唤醒(雷霆群)已用于实现这两个串行化队列。他们也使用与CIL相同的锁。如果我们在CIL锁上看到太多争用,或者由于广播唤醒而导致太多上下文切换,则可以将这些操作置于新的自旋锁下,并给出单独的等待列表,以减少锁争用和被错误事件唤醒的进程数量。

9.8 Lifecycle Changes

The existing log item life cycle is as follows:

现有日志项生命周期如下:

1. Transaction allocate
2. Transaction reserve
3. Lock item
4. Join item to transaction
        If not already attached,
                Allocate log item
                Attach log item to owner item
        Attach log item to transaction
5. Modify item
        Record modifications in log item
6. Transaction commit
        Pin item in memory
        Format item into log buffer
        Write commit LSN into transaction
        Unlock item
        Attach transaction to log buffer

<log buffer IO dispatched>
<log buffer IO completes>

7. Transaction completion
        Mark log item committed
        Insert log item into AIL
                Write commit LSN into log item
        Unpin log item
8. AIL traversal
        Lock item
        Mark log item clean
        Flush item to disk

<item IO completion>

9. Log item removed from AIL
        Moves log tail
        Item unlocked

Essentially, steps 1-6 operate independently from step 7, which is also independent of steps 8-9. An item can be locked in steps 1-6 or steps 8-9 at the same time step 7 is occurring, but only steps 1-6 or 8-9 can occur at the same time. If the log item is in the AIL or between steps 6 and 7 and steps 1-6 are re-entered, then the item is relogged. Only when steps 8-9 are entered and completed is the object considered clean.

基本上,步骤1-6独立于步骤7操作,步骤7也独立于步骤8-9。在步骤7发生的同时,可以在步骤1-6或步骤8-9中锁定项目,但同时只能发生步骤1-6或8-9。如果日志项在AIL中或在步骤6和7之间,并且重新输入步骤1-6,则重新记录该项。只有当进入并完成步骤8-9时,对象才被认为是干净的。

With delayed logging, there are new steps inserted into the life cycle:

对于延迟日志记录,在生命周期中插入了新步骤:

1. Transaction allocate
2. Transaction reserve
3. Lock item
4. Join item to transaction
        If not already attached,
                Allocate log item
                Attach log item to owner item
        Attach log item to transaction
5. Modify item
        Record modifications in log item
6. Transaction commit
        Pin item in memory if not pinned in CIL
        Format item into log vector + buffer
        Attach log vector and buffer to log item
        Insert log item into CIL
        Write CIL context sequence into transaction
        Unlock item

<next log force>

7. CIL push
        lock CIL flush
        Chain log vectors and buffers together
        Remove items from CIL
        unlock CIL flush
        write log vectors into log
        sequence commit records
        attach checkpoint context to log buffer

<log buffer IO dispatched>
<log buffer IO completes>

8. Checkpoint completion
        Mark log item committed
        Insert item into AIL
                Write commit LSN into log item
        Unpin log item
9. AIL traversal
        Lock item
        Mark log item clean
        Flush item to disk
<item IO completion>
10. Log item removed from AIL
        Moves log tail
        Item unlocked

From this, it can be seen that the only life cycle differences between the two logging methods are in the middle of the life cycle - they still have the same beginning and end and execution constraints. The only differences are in the committing of the log items to the log itself and the completion processing. Hence delayed logging should not introduce any constraints on log item behaviour, allocation or freeing that don’t already exist.

由此可以看出,这两种日志记录方法之间唯一的生命周期差异是在生命周期的中间——它们仍然具有相同的开始、结束和执行约束。唯一的区别在于将日志项提交到日志本身和完成处理。因此,延迟日志记录不应在日志项行为、分配或释放方面引入任何现有的约束。

As a result of this zero-impact “insertion” of delayed logging infrastructure and the design of the internal structures to avoid on disk format changes, we can basically switch between delayed logging and the existing mechanism with a mount option. Fundamentally, there is no reason why the log manager would not be able to swap methods automatically and transparently depending on load characteristics, but this should not be necessary if delayed logging works as designed.

由于延迟日志记录基础设施的零影响“插入”以及内部结构的设计以避免磁盘格式更改,我们基本上可以通过装载选项在延迟日志记录和现有机制之间切换。从根本上讲,日志管理器没有理由不能根据负载特性自动和透明地交换方法,但如果延迟日志按设计工作,则不需要这样做。