向应用程序中添加日志记录功能

本主题描述如何使用 System.IO.Log 提供的功能向应用程序中添加日志记录功能。

FileRecordSequence 与 LogRecordSequence

若要选择基于简单文件的日志和基于公用日志文件系统 (CLFS) 的日志,您必须考虑以下四个条件:平台支持、丰富的功能、可靠性和性能。

基于简单文件的日志可以在支持 System.IO.Log 的所有平台上使用,而基于 CLFS 的日志只能在 Windows Server 2003 R2 和 Windows Vista 平台上使用。 此外,在 Windows Server 2003 R2 上,某些 CLFS 功能被禁用。 这会影响应用程序对特定条件的处理,例如自动增长处理。 有关这类限制的更多信息,请参见 LogRecordSequence

与基于简单文件的 FileRecordSequence 类相比,策略和多路复用这两种功能成就了基于 CLFS 的 LogRecordSequence 类的丰富功能。 使用 LogPolicy 结构设置策略将允许您对以下维护功能进行极其精细的控制,例如,日志大小的自动增长和自动缩小、最小和最大范围容器以及结尾固定的阈值等等。 这些功能还允许您使用基于 CLFS 的 LogRecordSequence 类创建循环日志。 多路复用或者操作 NTFS 文件流的功能可以在单个应用程序或作为一个整体协作的多个应用程序内部提供增强的性能和便利性。 因此,与使用 FileRecordSequence 类相比,专为长时间运行的方案或极高的性能而设计的应用程序可最大程度地获益于 LogRecordSequence 类的使用。

此外,CLFS 的优势还包括可靠性和性能。 CLFS 专门供高性能应用程序使用或在企业环境中使用。 如果应用程序约束考虑在支持 CLFS 的平台上运行,则 LogRecordSequence 类不但将在通过 LogPolicy 类控制日志文件维护时考虑更多选项,而且还会使 IO 性能得到提高。

System.IO.Log 中的主要类

下面是 System.IO.Log 中的三个最重要的类。 有关其用法的更多信息,请参见它们各自的参考文档。

使用 System.IO.Log

打开日志和添加范围

打开日志和添加范围通常是向应用程序中添加日志记录功能的第一项任务。 请注意,只有在使用基于 CLFS 的 LogRecordSequence 类时才能执行添加范围。

您应当考虑日志的位置,以及最初添加到日志中的范围的数量和大小。 存储位置仅受应用程序运行时所用用户帐户的限制。 有效位置可以是此用户在本地系统上具有写访问权限的任意位置。 但是,对于范围的数量和大小,则需要进行更多特定于应用程序的考虑。 首次向日志中添加范围时,必须提供范围的大小。 这个大小将用于其他所有手动添加或通过自动增长行为添加到日志中的范围。 而且,为了利用 LogPolicy 类提供的众多功能,对于给定的序列始终必须存在至少两个范围。

下面的示例演示如何创建一个日志,并向其中添加两个 1MB 的范围容器。

LogRecordSequence recordSequence = new LogRecordSequence("application.log", FileMode.CreateNew);
recordSequence.LogStore.Extents.Add("app_extent0", 1024 * 1024);
recordSequence.LogStore.Extents.Add("app_extent1");

设置策略

使用 LogPolicy 结构设置日志策略应为日志记录应用程序开发过程中的第二项任务。 请注意,只能在使用基于 CLFS 的 LogRecordSequence 类时执行此任务,并且由于策略不持久,因此在每次打开日志时都必须设置日志策略。

策略包含一些重要选项,如自动增长和最大范围数。 在应用程序的整个生存期中,不断变化的负载会导致最初的一组范围被耗尽,并有可能在执行过程导致 SequenceFullException。 为了防止这种情况发生,一般情况下,最好允许日志自动增长以便以透明方式容纳这种额外的负载。 请注意,在 SequenceFullException 期间手动添加范围是受支持的,因此可以使用手动添加而不使用透明的自动增长。

在处理循环日志时,您还应该设置最大范围数。 另外,LogPolicy 结构提供了一些常用的辅助设置,如自动缩小和增长率设置。 对这些值进行操作可以显著地影响应用程序的性能,应该根据系统内任何给定日志的 I/O 率进行调整。

下面的示例演示如何设置一个自动增长至最多 100 个范围的日志策略,增长幅度为每次 5 个范围。

recordSequence.LogStore.Policy.AutoGrow = true;
recordSequence.LogStore.Policy.MaximumExtentCount = 100;
recordSequence.LogStore.Policy.GrowthRate = new PolicyUnit(5, PolicyUnitType.Extents);
recordSequence.LogStore.Policy.Commit();
recordSequence.LogStore.Policy.Refresh();

追加记录

应该考虑各种以 Append 方法可理解的格式提供数据的技术。

当前的 Append 方法已经针对处理字节数组和字节数组列表进行了优化。 但是,还可以将其与更高级别的序列化类结合使用。 产生的记录格式必须仍是可理解的,以便有效地利用 IRecordSequence 类提供的功能。 下面的内容演示高级序列化代码示例,它使用基于 System.Runtime.Serialization.Formatters 中的 DataContractAttribute 的功能。

SomeDataContractClass someClassInstance = new SomeDataContractClass(…);
ArraySegment<byte>[] recordData = new ArraySegment<byte>[1];

using (MemoryStream formatStream = new MemoryStream())
{
   IFormatter formatter = new NetDataContractSerializer();
   formatter.Serialize(formatStream, someClassInstance);
   formatStream.Flush();
   recordData[0] = new ArraySegment<byte>(formatStream.GetBuffer());
}

recordSequence.Append(recordData, …);

将记录追加到记录序列时,您应当仔细考虑应用程序的空间和时间约束。 例如,应用程序可能会使用 ReservationCollection 类强制以下行为:仅当日志中有足够空间用于在以后写入更多记录时追加操作才成功。 基于 ARIES 的事务处理系统可能通常将这类记录称为补偿记录或撤消记录,在需要对工作进行回滚时将使用这些记录。 同样,一些系统具有严格的规则,用来控制应写入记录的时间。 对哪些记录要刷新到磁盘以及哪些记录允许惰性刷新的选择完全取决于应用程序要求和系统约束。 这些考虑因素既影响性能,又影响正确性。

空间和时间选项都通过各种 Append 方法向用户公开。 下面的示例通过追加三条记录并确保为以后的记录留出空间来演示如何设置这类选项。 它仅在最后一条记录写入时强制进行刷新。

// Assume recordData is smaller or equal to 1000bytes 
// Reserve space in the log for three 1000byte records ReservationCollection reservations = recordSequence.CreateReservationCollection();
reservations.Add(1000);
reservations.Add(1000);
reservations.Add(1000);

// The following three appends are guaranteed to succeed if execution 
// flows to here
recordSequence.Append(recordData1,
                                   userSqn,
                                   previousSqn,
                                   RecordAppendOptions.None,    // No flush
                                   reservations);
recordSequence.Append(recordData2,
                                   userSqn,
                                   previousSqn,
                                   RecordAppendOptions.None,    // No flush
                                   reservations);
recordSequence.Append(recordData3,
                                   userSqn,
                                   previousSqn,
                                   RecordAppendOptions.ForceFlush,    // Flush
                                   reservations);

如果 RetryAppend 属性为 true,且 Append 操作因为序列中没有空间而失败,则记录序列将尝试释放空间,然后重试该操作。 释放空间的具体实现方式各不相同。 例如,LogRecordSequence 类将调用 CLFS 策略引擎,如下面的“使用 TailPinnedEvent 释放空间”一节所述。

保留

如下面的示例所示,可以通过两种方式执行保留。 您可以采用示例中的做法可靠地进行处理。 请注意,只有在使用基于 CLFS 的 LogRecordSequence 类时才能执行此任务。

使用 ReserveAndAppend 方法

ReservationCollection reservations = recordSequence.CreateReservationCollection();
long[] lengthOfUndoRecords = new long[] { 1000 };
recordSequence.ReserveAndAppend(recordData,
                                                     userSqn,
                                                     previousSqn,
                                                     RecordSequenceAppendOptions.None,
                                                     reservations,
                                                     lengthOfUndoRecords);
recordSequence.Append(undoRecordData,    // If necessary …
                                    userSqn,
                                    previousSqn,
                                    RecordSequenceAppendOptions.ForceFlush,
                                    reservations);

使用手动方式

ReservationCollection reservations = recordSequence.CreateReservationCollection();
reservations.Add(lengthOfUndoRecord);
try
{
   recordSequence.Append(recordData, userSqn, previousSqn, RecordAppendOptions.None);
}
catch (Exception)
{
   reservations.Remove(lengthOfUndoRecord);
   throw;
}

recordSequence.Append(undoRecordData, userSqn, previousSqn, RecordAppendOptions.ForceFlush, reservations);

读取日志

在应用程序生存期中,可以随时读取日志。 然而,根据具体情况,可能有必要以不同顺序循环访问存储在日志中的记录。 除了日志的 NextPrevious 指定的标准方向外,您还可以用在各个记录追加到序列时指定的用户定义的顺序来循环访问。 还应该注意到在物理日志方面,Previous 并非总是有序的,因为用户可以指定在 Append 操作期间由当前执行线程追加的前一条记录。

下面的示例从序列号为“startSqn”的记录开始,按顺序向前读取日志。

foreach (LogRecord record in recordSequence.ReadLogRecords(startSqn, LogRecordEnumeratorType.Next))
{
   Stream dataStream = record.Data;
   // Process dataStream, which can be done using deserialization
}

使用 TailPinnedEvent 释放空间

记录序列可以用来释放空间的方法之一是激发 TailPinned 事件。 此事件指示,需要将序列的结尾(即基序列号)前移以释放空间。

当记录序列决定必须释放空间时,可以借任何理由随时激发该事件。 例如,当 CLFS 策略引擎确定共享同一日志文件的两个日志客户端的结尾相距过远时,它可能会决定激发该事件。 可以通过写入重新开始区域、截断日志及使用 AdvanceBaseSequenceNumber 方法清除空间来释放空间。 下面的示例演示第二种方式。

recordSequence.RetryAppend = true;
recordSequence.TailPinned += new EventHandler<TailPinnedEventArgs>(HandleTailPinned);

void HandleTailPinned(object sender, TailPinnedEventArgs tailPinnedEventArgs)
{
   // tailPinnedEventArgs.TargetSequenceNumber is the target 
   // sequence number to free up space to.  
   // However, this sequence number is not necessarily valid.  We have
   // to use this sequence number as a starting point for finding a
   // valid point within the log to advance toward. You need to
   // identify a record with a sequence number equal to, or greater
   // than TargetSequenceNumber; let's call this 
   // realTargetSequenceNumber. Once found, move the base

   recordSequence.AdvanceBaseSequenceNumber(realTargetSequenceNumber);

}

还可以在 TailPinned 事件之外调用 WriteRestartArea 方法以释放空间。 重新开始区域类似于其他日志处理系统内的检查点。 调用此方法则表示,在重新开始区域彻底完成以及可用于在以后追加记录之前,应用程序将考虑以前的所有记录。 与其他任何记录类似,此方法写入的记录需要使用日志中实际的可用空间才有效。

多路复用

当您具有作为一个整体协作的多个应用程序时,您可以使用基于 CLFS 的 LogRecordSequence 类操作单个 NTFS 文件流。 下面的示例演示如何创建包含两个流的多路日志。 将对日志记录交替执行追加和读取操作。

namespace MyMultiplexLog
{
    class MyMultiplexLog
    {
        static void Main(string[] args)
        {
            try
            {
                string myLog = "MyMultiplexLog";
                string logStream1 = "MyMultiplexLog::MyLogStream1";
                string logStream2 = "MyMultiplexLog::MyLogStream2";
                int containerSize = 32 * 1024;

                LogRecordSequence sequence1 = null;
                LogRecordSequence sequence2 = null;

                Console.WriteLine("Creating Multiplexed log with two streams");

                // Create log stream 1
                sequence1 = new LogRecordSequence(logStream1, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);

                // Log Extents are shared between the two streams. 
                // Add two extents to sequence1.
                sequence1.LogStore.Extents.Add("MyExtent0", containerSize);
                sequence1.LogStore.Extents.Add("MyExtent1");

                // Create log stream 2
                sequence2 = new LogRecordSequence(logStream2, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);

                // Start Appending in two streams with interleaving appends.

                SequenceNumber previous1 = SequenceNumber.Invalid;
                SequenceNumber previous2 = SequenceNumber.Invalid;

                Console.WriteLine("Appending interleaving records in stream1 and stream2...");
                Console.WriteLine();

                // Append two records in stream1
                previous1 = sequence1.Append(CreateData("MyLogStream1: Hello World!"), SequenceNumber.Invalid, SequenceNumber.Invalid, RecordAppendOptions.ForceFlush);
                previous1 = sequence1.Append(CreateData("MyLogStream1: This is my first Logging App"), previous1, previous1, RecordAppendOptions.ForceFlush);

                // Append two records in stream2
                previous2 = sequence2.Append(CreateData("MyLogStream2: Hello World!"), SequenceNumber.Invalid, SequenceNumber.Invalid, RecordAppendOptions.ForceFlush);
                previous2 = sequence2.Append(CreateData("MyLogStream2: This is my first Logging App"), previous2, previous2, RecordAppendOptions.ForceFlush);

                // Append the third record in stream1
                previous1 = sequence1.Append(CreateData("MyLogStream1: Using LogRecordSequence..."), previous1, previous1, RecordAppendOptions.ForceFlush);
                
                // Append the third record in stream2
                previous2 = sequence2.Append(CreateData("MyLogStream2: Using LogRecordSequence..."), previous2, previous2, RecordAppendOptions.ForceFlush);
                
                // Read log records from stream1 and stream2

                Encoding enc = Encoding.Unicode;
                Console.WriteLine();
                Console.WriteLine("Reading Log Records from stream1...");
                foreach (LogRecord record in sequence1.ReadLogRecords(sequence1.BaseSequenceNumber, LogRecordEnumeratorType.Next))
                {
                    byte[] data = new byte[record.Data.Length];
                    record.Data.Read(data, 0, (int)record.Data.Length);
                    string mystr = enc.GetString(data);
                    Console.WriteLine("    {0}", mystr);
                }

                Console.WriteLine();             
                Console.WriteLine("Reading the log records from stream2...");
                foreach (LogRecord record in sequence2.ReadLogRecords(sequence2.BaseSequenceNumber, LogRecordEnumeratorType.Next))
                {
                    byte[] data = new byte[record.Data.Length];
                    record.Data.Read(data, 0, (int)record.Data.Length);
                    string mystr = enc.GetString(data);
                    Console.WriteLine("    {0}", mystr);
                }

                Console.WriteLine();

                // Cleanup...
                sequence1.Dispose();
                sequence2.Dispose();

                LogStore.Delete(myLog);

                Console.WriteLine("Done...");
            }
            catch (Exception e)
            {
                Console.WriteLine("Exception thrown {0} {1}", e.GetType(), e.Message);
            }
        }

        // Converts the given data to an Array of ArraySegment<byte> 
        public static IList<ArraySegment<byte>> CreateData(string str)
        {
            Encoding enc = Encoding.Unicode;

            byte[] array = enc.GetBytes(str);

            ArraySegment<byte>[] segments = new ArraySegment<byte>[1];
            segments[0] = new ArraySegment<byte>(array);

            return Array.AsReadOnly<ArraySegment<byte>>(segments);
        }

    }
}

处理异常

使用 System.IO.Log 的应用程序必须准备好处理由基础结构产生的故障快速报警。 在某些情况下,System.IO.Log 不使用异常向应用程序传达错误。 而是将异常主要用于应用程序开发人员可依据其执行操作的可恢复错误。 但是,当进一步执行将导致损坏的或可能无法使用的日志文件时,故障快速报警将发生。 当故障快速报警发生时,没有其他应用程序操作可用来纠正该问题,应用程序必须做好终止的准备。

有关故障快速报警的更多信息,请参见 FailFast

System.IO.Log 引发的许多异常都源自内部日志实现。 下面列出了一些您应该注意的重要异常。

  • SequenceFullException:此异常可能严重,也可能不严重。 在 AutoGrow 设置为 true 且有足够的增长空间的情况下,仍然可能引发 SequenceFullException。 这是因为自动增长实现本质上是一种异步的非自动操作,并且增长率无法保证一次完成所有操作。 例如,如果将增长率设置为 100 个范围,则将全部 100 个范围都添加到日志存储区需要相当长的时间。 这可能会导致在这段时间内间歇性地引发此异常。

  • TailPinned 处理程序:将向内部日志实现报告在 TailPinned 事件内部引发的异常。 这表示应用程序无法移动基序列号。 由于 TailPinned 事件是应用程序阻止日志已满条件的最后一个机会,因此将引发 SequenceFullException

  • ReservationNotFoundException:当您尝试使用保留集合追加记录并且所有大小适当的保留都已被占用时,将发生此异常。

请参见

参考

System.IO.Log
LogRecordSequence
FileRecordSequence

其他资源

System.IO.Log 中的日志记录支持

Footer image

向 Microsoft 发送对本主题的评论。

版权所有 (C) 2007 Microsoft Corporation。保留所有权利。