Будет ли создание MSSqlServerSink в веб-запросе вызвать утечку памяти или подобный тип проблемы? - PullRequest
2 голосов
/ 17 марта 2020

У меня есть довольно уникальное мультитенантное приложение, где каждый клиент получает свою собственную базу данных. В настоящее время я использую Serilog с MSSqlServerSink для записи всего в одну базу данных. Я только что получил запрос / требование также войти в отдельные базы данных арендаторов.

Я создал новую реализацию ILogEventSink, которая использует ConcurrentDictionary и ищет существующий приемник, а затем создает новый, если он не существует. .

После развертывания мы получили около 503 ошибок, и единственными симптомами, которые наблюдают наши администраторы, является количество подключений к веб-серверу, которые остаются открытыми в состоянии CLOSE_WAIT. Я ищу дополнительную информацию о причинах CLOSE_WAIT, но единственным большим изменением стало развертывание этого нового приемника журналирования.

Поскольку единственная ссылка на новый приемник сохраняется в моем объекте приемника (и этот объект создается во время app_start), я бы думал , что это сработало бы, но возможно ли, что этот новый приемник каким-то образом привязан к потоку, который обрабатывает текущий запрос и поддерживает этот запрос / соединение живым?

using Serilog.Core;
using Serilog.Events;
using Serilog.Sinks.MSSqlServer;
using System;
using System.Collections.Concurrent;
using System.Linq;
using Context = Logging.Constants.Context;

namespace Web.Logging
{
    public class CustomerSink : ILogEventSink
    {
        private readonly ConcurrentDictionary<string, SinkCache> Sinks;

        private readonly ICustomerProvider CustomerProvider;

        public CustomerSink( ICustomerProvider customerProvider = null )
        {
            CustomerProvider = customerProvider ?? Customer.GetProvider();
            Sinks = new ConcurrentDictionary<string, SinkCache>();
        }

        private ILogEventSink CreateSink( Customer customer )
        {
            var columnOptions = SqlServerOptions.DefaultColumnOptions();
            var remove = columnOptions.AdditionalColumns.Where( column => column.ColumnName == Context.ApplicationName || column.ColumnName == Context.CustomerId ).ToList();
            foreach( var column in remove )
                columnOptions.AdditionalColumns.Remove( column );

            columnOptions.Store.Remove( StandardColumn.MessageTemplate );

            columnOptions.LogEvent.ExcludeAdditionalProperties = true;
            columnOptions.LogEvent.ExcludeStandardColumns = true;

            return new MSSqlServerSink(
                connectionString: customer.ConnectionString,
                tableName: "Event",
                batchPostingLimit: 50,
                period: TimeSpan.FromSeconds( 5 ),
                formatProvider: null,
                autoCreateSqlTable: true,
                columnOptions: columnOptions,
                schemaName: "log"
                );
        }

        private SinkCache CreateSink( string customerId )
        {
            if( CustomerProvider.GetCustomer( customerId, out var customer ) )
                return new SinkCache( customer.ConnectionString, CreateSink( customer ) );

            return new SinkCache( null, null );
        }

        public void Emit( LogEvent logEvent )
        {
            if( logEvent.Properties.TryGetValue( Context.CustomerId, out var value ) && value is ScalarValue scalar && scalar.Value != null )
            {
                var cache = Sinks.AddOrUpdate( scalar.Value.ToString(), CreateSink,
                    ( customerId, existing ) =>
                    {
                        if( existing.Expiration < DateTime.UtcNow )
                        {
                            if( CustomerProvider.GetCustomer( customerId, out var customer ) )
                            {
                                if( customer.ConnectionString == existing.ConnectionString )
                                    return new SinkCache( existing.ConnectionString, existing.Sink );   //Just refresh the expiration

                                if( existing.Sink is IDisposable disposable )
                                    disposable.Dispose();

                                return new SinkCache( customer.ConnectionString, CreateSink( customer ) );
                            }
                            else if( existing.Sink is IDisposable disposable )
                                disposable.Dispose();

                            return new SinkCache( null, null );
                        }

                        //No change
                        return existing;
                    } );

                cache.Sink?.Emit( logEvent );
            }
        }

        private class SinkCache
        {
            public string ConnectionString { get; }

            public DateTime Expiration { get; }

            public ILogEventSink Sink { get; }

            public SinkCache( string connectionString, ILogEventSink sink )
            {
                ConnectionString = connectionString;
                Sink = sink;
                Expiration = DateTime.UtcNow.AddMinutes( 2 );
            }
        }
    }
}

1 Ответ

1 голос
/ 17 марта 2020

Раковины Serilog часто необходимо утилизировать, чтобы их можно было быстро очистить. MSSqlServerSink равно IDisposable, и хотя ресурсы, удерживаемые нераспределенными экземплярами, будут в конечном итоге очищены потоком финализатора. NET, ресурсы будут связаны в течение неопределенного периода времени, прежде чем завершится запуск , что приводит к утечке.

Необходимо изменить решение, чтобы утилизировать раковины, или вы можете использовать Serilog.Sinks.Map вместо этого необходимо направить журналы c, определяемые арендатором, в правильный приемник и внедрить для вас кэширование / удаление приемников.

...