FreeAndNil opened a new issue, #255: URL: https://github.com/apache/logging-log4net/issues/255
# Summary This document proposes an enhancement to the RemoteSyslogAppender in Apache log4net to resolve severe performance degradation observed under high load conditions due to synchronous network I/O. # Problem Statement When using RemoteSyslogAppender in log4net version 3.1.0.0, we observed significant performance degradation under load, particularly during high throughput API requests in a production-scale ASP.NET application. Symptoms: * API response time increases drastically with RemoteSyslogAppender enabled. * When the appender is disabled, APIs perform consistently faster. * The bottleneck was traced to the following line in the Append() method: Client.SendAsync(buffer, buffer.Length, RemoteEndPoint).Wait(); # Root Cause * This line blocks the thread by waiting on an asynchronous UDP send operation. * In high-load scenarios, this creates thread contention, as logging becomes I/O- bound and synchronous. * UDP is inherently unreliable and fast; blocking to wait for its completion defeats the purpose and affects overall throughput # Proposed Solution: Refactor the RemoteSyslogAppender to decouple the UDP send operation from the calling thread by using a producer-consumer pattern backed by BlockingCollection<byte[]>. ## Key Enhancements 1. Asynchronous Background Worker: A background task consumes queued log messages and sends them via UDP. 2. Non-blocking Logging Path: Append() only queues the message—does not wait for UDP transmission. 3. Graceful Shutdown: Ensures buffered logs are flushed before shutdown. # Code Changes ```csharp private readonly BlockingCollection<byte[]> _sendQueue = new(); private CancellationTokenSource? _cts; private Task? _pumpTask; public override void ActivateOptions() { base.ActivateOptions(); _cts = new CancellationTokenSource(); _pumpTask = Task.Run(() => ProcessQueueAsync(_cts.Token), CancellationToken.None); } protected override void OnClose() { _cts?.Cancel(); _pumpTask?.Wait(TimeSpan.FromSeconds(5)); base.OnClose(); } protected override void Append(LoggingEvent loggingEvent) { var buffer = FormatMessage(loggingEvent); // Assuming your existing logic _sendQueue.Add(buffer); } private async Task ProcessQueueAsync(CancellationToken token) { // We create our own UdpClient here, so that client lifetime is tied to this task using (var udp = new UdpClient()) { udp.Connect(RemoteAddress?.ToString(), RemotePort); try { while (!token.IsCancellationRequested) { // Take next message or throw when cancelled byte[] datagram = _sendQueue.Take(token); try { await udp.SendAsync(datagram, datagram.Length); } catch (Exception ex) when (!ex.IsFatal()) { ErrorHandler.Error("RemoteSyslogAppender: send failed", ex, ErrorCode.WriteFailure); } } } catch (OperationCanceledException) { // Clean shutdown: drain remaining items if desired while (_sendQueue.TryTake(out var leftover)) { try { await udp.SendAsync(leftover, leftover.Length); } catch { /* ignore */ } } } } } ``` # Technical Justification * BlockingCollection<T> with a dedicated task ensures high throughput and thread safety. * UdpClient.SendAsync() is naturally asynchronous and works well in an async context. * This pattern avoids .Wait() and prevents thread pool starvation under high loads. * Maintains the contract of RemoteSyslogAppender without changing external behavior. # Performance Evidence ## Environment: * .NET framework 4.7.2 application * log4net 3.1.0.0 * Remote syslog server running on a separate host * Load test using Apache JMeter with 200 concurrent users ## Test Configuration * Tool: Apache JMeter * API Endpoint: High-throughput production endpoint * Threads: 1000 * Ramp-Up Period: 100 seconds * Request Rate: 10 requests/second ## Scenario 1: Default log4net with RemoteSyslogAppender Enabled (Synchronous.Wait()) |Metric|Value| |---------|--------| |Average Response Time|120590| |Minimum Response Time|6322| |Maximum Response Time|167883| |Errors/Failures|Some API errors due to timeouts or long waits| ### Issues Observed RemoteSyslogAppender causes the API thread to block due to .Wait(), resulting in slow responses. ## Scenario 2: log4net with Asynchronous RemoteSyslogAppender (Proposed Change) |Metric (for major api ConnectDesktop call)|Value| |---------|--------| |Average Response Time|2866| |Minimum Response Time|1134| |Maximum Response Time|8052| |Errors/Failures|Some API errors due to long waits| ### Observation: Avg, min max time taken is improved and less with With Async Queueing 10req/sec (thread 1000, ramp up 100sec) as compare to results with the Default log4net (sync .Wait()) # Summary The proposed update improves the performance and responsiveness of applications using RemoteSyslogAppender, especially under load. By adopting an asynchronous, queue-based architecture, we eliminate the unnecessary blocking behavior caused by .Wait(), aligning better with modern asynchronous .NET patterns. # Next Steps: We respectfully request the following actions from the log4net maintainers: * Review the proposed enhancement for performance improvement in RemoteSyslogAppender. * Verify compatibility with existing appender behavior. * Incorporate the fix in the next official release of log4net. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: notifications-unsubscr...@logging.apache.org.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org