c# - 长寿命连接多路复用器日志记录
问题描述
我有一个正在为 Redis 编写的监视服务,StackExchange.Redis
并且正在订阅某些事件。我面临的问题是日志记录。它需要一个TextWriter
. 理想情况下,我想把它分流到一个,EventLog
所以我在一个基于任务的计时器上使用一个MemoryStream
支持StreamWriter
和转储到EventLog
一个。StreamReader
这个实现的问题是,在我的测试中MemoryStream
泄漏很严重,即使我MemoryStream.SetLength(int)
在每次阅读后都使用清除。该ConnectionMultipler.Connect()
方法只需要一个对象,我无法看到如何替换该对象,这意味着我也必须ConnectionMultiplexer
定期更新。
这听起来不好吗?我错过了什么吗?更简单的方法似乎是管理一个对象,但我没有看到如何控制它。这是一个示例控制台应用程序来演示。
class Program
{
private static MemoryStream _loggingStream;
private static StreamReader _reader;
private static object _padlock = new object();
static async Task Main(string[] args)
{
_loggingStream = new MemoryStream();
_reader = new StreamReader(_loggingStream);
var logWriter = new StreamWriter(_loggingStream);
ThreadPool.QueueUserWorkItem(async state => await WriteLog());
while (true)
{
Monitor.Enter(_padlock);
try
{
await logWriter.WriteLineAsync("hello world " + DateTime.Now.ToLongTimeString());
await logWriter.FlushAsync();
}
finally
{
Monitor.Exit(_padlock);
}
}
}
private static async Task WriteLog()
{
while (_loggingStream.Length == 0)
{
await Task.Delay(TimeSpan.FromMilliseconds(5));
}
string log;
lock (_padlock)
{
_loggingStream.Position = 0;
log = _reader.ReadToEnd();
_reader.DiscardBufferedData();
_loggingStream.SetLength(0);
}
Console.WriteLine(log);
ThreadPool.QueueUserWorkItem(async state => await WriteLog());
}
}
解决方案
问题不在于内存流。问题是Console.WriteLine
. 向/从 a 写入/读取内容比在典型的 Windows 配置中写入控制台要快得多。MemoryStream
您可能会在每次读取内存流时将其归零,但是一旦您清除它,您就会放弃锁定,并且日志写入器开始快速旋转。
在第一次迭代中,假设写日志的线程有 5ms 的时间来写一些日志。将其写入控制台将花费超过5 毫秒,因此当控制台写入线程运行一次时,它的日志价值超过5毫秒,这将比写入前 5 毫秒的日志花费的时间更长。 logs... 所以每次写控制台的线程写完之前的日志状态,它发现它有更多,写出来需要更长的时间:是的,内存流正在消耗所有的内存,但那是因为它需要内存来存储所有日志,而控制台编写线程正忙于消耗最后一次加载。
这里有一些数学,只是为了好玩:
d is rate at which logs are produced
c is how long it takes to consume a unit of logs
x(i) is the volume of logs produced during iteration i of the log-consumer
y(i) is how long it takes to consume the logs produced in iteration i
我们可以写一些很好的简单方程:
y(i) = c*x(i) (time to consume logs is a linear function of volume)
x(i+1) = d*y(i) (volume is a linear function of time between iterations)
适当地,我们可以确定日志量(与内存使用量成正比)如何随着每次迭代而变化
x(i+1) = d*c*x(i)
如果d*c > 1
, thenx
呈指数增长:对内存使用不利(尽管它仍然只能随时间线性增长,因为这d
是限制因素(回想一下,我们关注的是每次迭代的成本,而不是时间))
如果我们考虑1/c
(消耗日志的速率),那么很明显,当
d > 1/c (i.e. rate at which logs are produced is greater than the rate at which logs are consumed)
写入内存流比写入控制台便宜:d > 1/c
,所以我们有一个基本问题,再怎么聪明也解决不了:你不能将这么多的日志写入控制台。
您可以在输出中看到这个问题,因为时间戳无法与时钟时间保持一致:它立即落后。删除Console.WriteLine
叶子应用程序在我的机器上解决了大约 10MB。您还可以在内存使用中看到问题:它时不时地跳转,这是控制台编写器开始新迭代并将整个流 ( byte[]
) 复制到char[]
( ReadToEnd
) 并最终生成 a的 (不频繁) 事件string
:可以立即释放它并不重要byte[]
,因为您有 2 个相同大小的对象来填补空缺。
顺便说一句, usingSetLength(0)
只会通过创建更多字节数组来掩盖问题,并且实际上可能增加了峰值内存使用量,因为它不会减少内存流的最大容量,并且意味着周围有丢弃的对象等待被垃圾收集。
正如评论中所讨论的,您不应该在线程之间访问监视器;您的使用await
意味着当控制权返回到日志写入方法时上下文将被保留,但不能保证您将获得相同的线程。
推荐阅读
- node.js - GRPC双向流配置
- python - pd.to_datetime 带有时间戳和 PM
- openstack - 无法访问 openstack 中的实例
- sql - 仅获取特定位置条件的 LHS 数据
- 3d - 使用 FirstPersonCameraController 在透视相机上重置(滚动)旋转
- .net - .Net (clrjit.dll) 的 JIT 编译器崩溃
- database - RANKX 针对不同的值显示相同的排名号
- performance - 是否有可能使用 Puppeteer 获得 Chrome DevTools 的性能总结秒数?
- android - 高度较小的Android View闪烁
- c# - 当玩家与触发器发生碰撞时,如何启用游戏对象的组件?