【问题标题】:Correct way to stop custom logback async appender停止自定义 logback 异步附加程序的正确方法
【发布时间】:2015-10-03 15:18:13
【问题描述】:

我使用 Amazon 的 Java 开发工具包创建了 Amazon SQS 和 SNS logback appender。基本的附加程序使用同步 Java API,但我还通过扩展 ch.qos.logback.classic.AsyncAppender 类创建了两者的异步版本。

虽然使用异步附加程序停止 logback 记录器上下文并不能按预期工作。当上下文停止时,所有异步附加器都会在退出之前尝试刷新剩余事件。问题源于ch.qos.logback.core.AsyncAppenderBase#stop 方法,该方法中断了工作线程。当 Amazon SDK 仍在处理排队的事件并导致com.amazonaws.AbortedException 时触发中断。在我的测试中,AbortedException 发生在 SDK 处理来自 API 的响应时,因此实际消息通过了,但情况可能并非总是如此。

即使工作人员仍应处理剩余的事件队列,logback 是否会中断工作线程?如果是这样,我该如何解决由中断引起的AbortedException?我可以覆盖整个停止方法并删除中断,但这需要复制粘贴大部分实现。

【问题讨论】:

    标签: java logback aws-sdk appender


    【解决方案1】:

    我终于想出了一个解决方案,我认为这不是最佳的,而且远非简单,但它确实有效。

    我的第一次尝试是使用异步版本的 AWS 开发工具包 API 和 logback 提供的执行器,因为使用内部执行器可以避免中断问题。但这没有成功,因为工作队列是共享的,在这种情况下,队列必须是特定于 appender 的,以允许正确停止它。所以我需要对每个 appender 使用自己的 executor。

    首先,我需要一个 AWS 客户端的执行器。 executor 的问题是提供的线程工厂必须创建守护线程,否则如果使用 logback 的 JVM 关闭钩子,它将无限期阻塞。

    public static ExecutorService newExecutor(Appender<?> appender, int threadPoolSize) {
        final String name = appender.getName();
        return Executors.newFixedThreadPool(threadPoolSize, new ThreadFactory() {
    
            private final AtomicInteger idx = new AtomicInteger(1);
    
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName(name + "-" + idx.getAndIncrement());
                thread.setDaemon(true);
                return thread;
            }
        });
    }
    

    下一个问题是如何通过中断正确停止 appender?这需要通过重试处理中断的异常,否则执行程序将跳过等待队列刷新。

    public static void shutdown(Appender<?> appender, ExecutorService executor, long waitMillis) {
        executor.shutdown();
        boolean completed = awaitTermination(appender, executor, waitMillis);
        if (!completed) {
            appender.addWarn(format("Executor for %s did not shut down in %d milliseconds, " +
                                    "logging events might have been discarded",
                                    appender.getName(), waitMillis));
        }
    }
    
    private static boolean awaitTermination(Appender<?> appender, ExecutorService executor, long waitMillis) {
        long started = System.currentTimeMillis();
        try {
            return executor.awaitTermination(waitMillis, TimeUnit.MILLISECONDS);
        } catch (InterruptedException ie1) {
            // the worker loop is stopped by interrupt, but the remaining queue should still be handled
            long waited = System.currentTimeMillis() - started;
            if (waited < waitMillis) {
                try {
                    return executor.awaitTermination(waitMillis - waited, TimeUnit.MILLISECONDS);
                } catch (InterruptedException ie2) {
                    appender.addError(format("Shut down of executor for %s was interrupted",
                                             appender.getName()));
                }
            }
            Thread.currentThread().interrupt();
        }
        return false;
    }
    

    正常的 logback appender 应该以同步方式工作,因此即使没有适当的关闭钩子也不应该丢失日志事件。这是当前异步 AWS 开发工具包 API 调用的问题。我决定使用倒计时锁存器来提供阻塞的 appender 行为。

    public class LoggingEventHandler<REQUEST extends AmazonWebServiceRequest, RESULT> implements AsyncHandler<REQUEST, RESULT> {
    
        private final ContextAware contextAware;
        private final CountDownLatch latch;
        private final String errorMessage;
    
        public LoggingEventHandler(ContextAware contextAware, CountDownLatch latch, String errorMessage) {
            this.contextAware = contextAware;
            this.latch = latch;
            this.errorMessage = errorMessage;
        }
    
        @Override
        public void onError(Exception exception) {
            contextAware.addWarn(errorMessage, exception);
            latch.countDown();
        }
    
        @Override
        public void onSuccess(REQUEST request, RESULT result) {
            latch.countDown();
        }
    }
    

    并使用闩锁处理等待。

    public static void awaitLatch(Appender<?> appender, CountDownLatch latch, long waitMillis) {
        if (latch.getCount() > 0) {
            try {
                boolean completed = latch.await(waitMillis, TimeUnit.MILLISECONDS);
                if (!completed) {
                    appender.addWarn(format("Appender '%s' did not complete sending event in %d milliseconds, " +
                                            "the event might have been lost",
                                            appender.getName(), waitMillis));
                }
            } catch (InterruptedException ex) {
                appender.addWarn(format("Appender '%s' was interrupted, " +
                                        "a logging event might have been lost or shutdown was initiated",
                                        appender.getName()));
                Thread.currentThread().interrupt();
            }
        }
    }
    

    然后全部捆绑在一起。下面的例子是真实实现的简化版,只是展示了这个问题的相关部分。

    public class SqsAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
    
        private AmazonSQSAsyncClient sqs;
    
        @Override
        public void start() {
            sqs = new AmazonSQSAsyncClient(
                    getCredentials(),
                    getClientConfiguration(),
                    Executors.newFixedThreadPool(getThreadPoolSize())
            );
            super.start();
        }
    
        @Override
        public void stop() {
            super.stop();
            if (sqs != null) {
                AppenderExecutors.shutdown(this, sqs.getExecutorService(), getMaxFlushTime());
                sqs.shutdown();
                sqs = null;
            }
        }
    
        @Override
        protected void append(final ILoggingEvent eventObject) {
            SendMessageRequest request = ...
            CountDownLatch latch = new CountDownLatch(1);
            sqs.sendMessageAsync(request, new LoggingEventHandler<SendMessageRequest, SendMessageResult>(this, latch, "Error"));
            AppenderExecutors.awaitLatch(this, latch, getMaxFlushTime());
        }
    }
    

    所有这些都是正确处理以下情况所必需的:

    • 使用异步 appender 包装器时,在 logback 上下文停止或关闭挂钩上刷新剩余的事件队列
    • 在使用 logback 的延迟关闭挂钩时不要无限期阻塞
    • 在不使用异步追加器时提供阻塞行为
    • 从导致所有 AWS 开发工具包流实施中断的异步附加程序停止中断中幸存

    以上在我作为维护者的开源项目Logback extensions中使用。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-02-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-07-16
      相关资源
      最近更新 更多