上下文:
我正在为Web API
项目实现一个日志记录机制,它将序列化对象从多个方法写入文件,而这些方法又被外部进程读取(nxLog
更准确)。该应用程序托管在IIS上,使用18个工作进程。应用程序池每天回收一次。将包含日志记录方法的服务的预期负载为10,000 req / s。简而言之,这是一个经典的produces/consumer
问题,有多个生成器(生成日志的方法)和一个使用者(从日志文件中读取的外部进程)。 更新:每个进程也使用多个线程。
我使用BlockingCollection
来存储数据(并解决竞争条件)以及将数据从集合写入磁盘的长时间运行任务。
要写入磁盘,我使用的是StreamWriter
和FileStream
因为写入频率几乎是恒定的(正如我所说的每秒写入10,000次),所以我决定在应用程序池的整个生命周期内保持流打开,并定期将日志写入磁盘。我依靠App Pool循环和我的DI框架每天处理我的记录器。另请注意,此类将是单例,因为我不希望有多个线程专用于从我的线程池写入。
显然,FileStream对象在处理之前不会写入磁盘。现在我不希望FileStream等待一整天,直到它写入磁盘。保存所有序列化对象所需的内存将是巨大的,更不用说应用程序或服务器上的任何崩溃都会导致数据丢失或文件损坏。
现在我的问题:
我如何定期将基础流(FileStream和StreamWriter)写入磁盘而不进行处理?我最初的假设是,一旦FileSteam超过其缓冲区大小(默认为4K),它将写入磁盘。
更新:答案中提到的不一致已修复。
代码:
public class EventLogger: IDisposable, ILogger
{
private readonly BlockingCollection<List<string>> _queue;
private readonly Task _consumerTask;
private FileStream _fs;
private StreamWriter _sw;
public EventLogger()
{
OpenFile();
_queue = new BlockingCollection<List<string>>(50);
_consumerTask = Task.Factory.StartNew(Write, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
private void OpenFile()
{
_fs?.Dispose();
_sw?.Dispose();
_logFilePath = $"D:\Log\log{DateTime.Now.ToString(yyyyMMdd)}{System.Diagnostic.Process.GetCurrentProcess().Id}.txt";
_fs = new FileStream(_logFilePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
_sw = new StreamWriter(_fs);
}
public void Dispose()
{
_queue?.CompleteAdding();
_consumerTask?.Wait();
_sw?.Dispose();
_fs?.Dispose();
_queue?.Dispose();
}
public void Log(List<string> list)
{
try
{
_queue.TryAdd(list, 100);
}
catch (Exception e)
{
LogError(LogLevel.Error, e);
}
}
private void Write()
{
foreach (List<string> items in _queue.GetConsumingEnumerable())
{
items.ForEach(item =>
{
_sw?.WriteLine(item);
});
}
}
}
答案 0 :(得分:2)
也许我的回答无法解决您的具体问题,但我相信您的方案可能是memory-mapped files的一个很好的用例。
持久文件是与a关联的内存映射文件 磁盘上的源文件。当最后一个进程完成后 该文件,数据保存到磁盘上的源文件中。这些 内存映射文件适合使用非常大的文件 源文件。
这可能非常有趣,因为您可以在不锁定问题的情况下从不同进程(即 IIS工作进程)进行日志记录。请参阅MemoryMappedFile.OpenExisting方法。
此外,您可以登录到非持久性共享内存映射文件,并使用任务计划程序或Windows服务,使用可持久内存映射文件将挂起日志带到其最终目标。
由于您的多进程/进程间方案,我发现使用此方法的潜力很大。
如果您不想重新发明轮子,我会选择像MSMQ这样的可靠消息队列(非常基本,但在您的场景中仍然有用)或RabbitMQ。将日志排入队列,后台进程可能会使用这些日志队列将日志写入文件系统。
这样,您可以每天创建一次,每天两次或随时创建日志文件,并且在系统中记录操作时,您不依赖于文件系统。
答案 1 :(得分:2)
您的问题存在一些“不一致”。
应用程序托管在IIS上并使用18个工作进程
_logFilePath = $“D:\ Log \ log {DateTime.Now.ToString(yyyyMMdd)} {System.Diagnostic.Process.GetCurrentProcess()。Id} .txt”;
。
将序列化对象写入多个方法的文件
将所有这些放在一起,你似乎有一个单线程的情况,而不是多线程的情况。并且由于每个进程都有一个单独的日志,因此不存在争用问题或需要同步。我的意思是,我不明白为什么需要BlockingCollection
。您可能忘记提及Web流程中存在多个线程。我会在这里做出这样的假设。
另一个问题是您的代码无法编译
Logger
,但EventLogger
函数看起来像构造函数。把所有这些放在一边,如果你真的有争用的情况并希望通过多个线程或进程写入同一个日志,那么你的类似乎拥有你需要的大部分内容。我修改了你的课程以做更多的事情。值得注意的是以下项目
lock
对象,以便不中断写操作StreamWriter
构造函数中使用了显式缓冲区大小。你应该试探性地确定哪种尺寸最适合你。此外,您应该从AutoFlush
禁用StreamWriter
,这样您就可以将写入命中缓冲区而不是文件,从而提供更好的性能。以下是带有更改的代码
public class EventLogger : IDisposable, ILogger {
private readonly BlockingCollection<List<string>> _queue;
private readonly Task _consumerTask;
private FileStream _fs;
private StreamWriter _sw;
private System.Timers.Timer _timer;
private object streamLock = new object();
private const int MAX_BUFFER = 16 * 1024; // 16K
private const int FLUSH_INTERVAL = 10 * 1000; // 10 seconds
public EventLogger() {
OpenFile();
_queue = new BlockingCollection<List<string>>(50);
_consumerTask = Task.Factory.StartNew(Write, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
void SetupFlushTimer() {
_timer = new System.Timers.Timer(FLUSH_INTERVAL);
_timer.AutoReset = true;
_timer.Elapsed += TimedFlush;
}
void TimedFlush(Object source, System.Timers.ElapsedEventArgs e) {
_sw?.Flush();
}
private void OpenFile() {
_fs?.Dispose();
_sw?.Dispose();
var _logFilePath = $"D:\\Log\\log{DateTime.Now.ToString("yyyyMMdd")}{System.Diagnostics.Process.GetCurrentProcess().Id}.txt";
_fs = new FileStream(_logFilePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
_sw = new StreamWriter(_fs, Encoding.Default, MAX_BUFFER); // TODO: use the correct encoding here
_sw.AutoFlush = false;
}
public void Dispose() {
_timer.Elapsed -= TimedFlush;
_timer.Dispose();
_queue?.CompleteAdding();
_consumerTask?.Wait();
_sw?.Dispose();
_fs?.Dispose();
_queue?.Dispose();
}
public void Log(List<string> list) {
try {
_queue.TryAdd(list, 100);
} catch (Exception e) {
LogError(LogLevel.Error, e);
}
}
private void Write() {
foreach (List<string> items in _queue.GetConsumingEnumerable()) {
lock (streamLock) {
items.ForEach(item => {
_sw?.WriteLine(item);
});
}
}
}
}
修改强>
控制这种机制的表现有4个因素,了解它们之间的关系很重要。下面的例子有希望说清楚
让我们说
List<string>
的平均大小为50字节MAX_BUFFER
是1024 * 1024字节(1兆)您每秒产生500,000字节的数据,因此1兆的缓冲区只能容纳2秒的数据。即使FLUSH_INTERVAL
设置为10秒,缓冲区将在缓冲区空间用完时每2秒(平均)自动刷新一次。
另外请记住,盲目增加MAX_BUFFER
无济于事,因为实际的刷新操作需要更长时间,因为缓冲区大小更大。
要理解的主要事情是,当传入数据速率(到您的EventLog
类)和传出数据速率(到磁盘)存在差异时,您将需要一个无限大小的缓冲区(假设连续不断)运行过程)或者你将不得不放慢你的平均速度。传入率以匹配平均值。出境率
答案 2 :(得分:1)
使用FileStream.Flush()方法 - 您可以在每次调用.Write
后执行此操作。它将清除流的缓冲区并使任何缓冲的数据写入文件。
https://msdn.microsoft.com/en-us/library/2bw4h516(v=vs.110).aspx