Долгоживущий журнал ConnectionMultiplexer - PullRequest
0 голосов
/ 26 марта 2019

У меня есть служба мониторинга, которую я пишу для 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());
    }
}

1 Ответ

1 голос
/ 26 марта 2019

Проблема не в потоке памяти. Проблема в Console.WriteLine. намного быстрее записывать / читать материал в / из MemoryStream, чем записывать в консоль, например. типичная конфигурация Windows. Возможно, вы обнуляете поток памяти каждый раз, когда читаете его, но как только вы очистили его, вы снимаете блокировку, и программа записи журнала начинает вращаться очень быстро .

В первой итерации предположим, что поток записи журнала имеет 5 мс для записи некоторых журналов. Чтобы записать это на консоль, потребуется больше , чем 5 мс, поэтому к тому времени, когда поток записи на консоль обойдется один раз, он будет иметь больше журналов на 5 мс займет больше времени, чем потребовалось для записи первых 5 мс журналов ... поэтому каждый раз, когда поток записи на консоль завершает запись предыдущего состояния журналов, он обнаруживает, что он имеет даже больше , и это занимает еще дольше , чтобы записать это: да, поток памяти занимал всю память, но это потому, что ему требовалась память для хранения всех журналов, пока поток записи на консоли был занят, потребляя последнюю загрузку.

Вот немного математики, просто для удовольствия:

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, то x растет в геометрической прогрессии: плохо для использования памяти (хотя он все еще может расти только линейно во времени, потому что 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 оставляет приложение на моем компьютере около 10 МБ. Вы также можете увидеть проблему в использовании памяти: время от времени она переходит, что является (нечастым) событием программы записи консоли, запускающей новую итерацию и копирующей весь поток (byte[]) в char[] ( ReadToEnd) и, наконец, получается string: не имеет значения, что byte[] может быть немедленно освобождено, потому что у вас есть 2 объекта одинакового размера, чтобы восполнить слабину.

<ч />

Кстати, использование SetLength(0) только запутает проблему, создав больше байтовых массивов, и, возможно, фактически увеличит пиковое использование памяти, потому что это не уменьшает максимальную емкость потока памяти и означает, что есть выброшенные предметы лежат в ожидании сбора мусора.

<ч />

Как уже говорилось в комментариях, вам не следует обращаться к монитору между потоками; использование await означает, что контекст будет сохранен при возврате элемента управления методу записи журнала, но нет гарантии, что вы получите тот же поток.

...