本系列代碼地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford

Log4j2 异步日志核心通過 RingBuffer 實現,如果某一時刻產生大量日志並且寫的速度不及時導致 RingBuffer 滿了,業務代碼中調用日志記錄的地方就會阻塞。所以我們需要對 RingBuffer 進行監控。Log4j2 對於每一個 AsyncLogger 配置,都會創建一個獨立的 RingBuffer,例如下面的 Log4j2 配置:

<!--省略了除了 loggers 以外的其他配置-->
<loggers>
<!--default logger -->
<Asyncroot level="info" includeLocation="true">
<appender-ref ref="console"/>
</Asyncroot>
<AsyncLogger name="RocketmqClient" level="error" additivity="false" includeLocation="true">
<appender-ref ref="console"/>
</AsyncLogger>
<AsyncLogger name="com.alibaba.druid.pool.DruidDataSourceStatLoggerImpl" level="error" additivity="false" includeLocation="true">
<appender-ref ref="console"/>
</AsyncLogger>
<AsyncLogger name="org.mybatis" level="error" additivity="false" includeLocation="true">
<appender-ref ref="console"/>
</AsyncLogger>
</loggers>

這個配置包含 4 個 AsyncLogger,對於每個 AsyncLogger 都會創建一個 RingBuffer。Log4j2 也考慮到了監控 AsyncLogger 這種情况,所以將 AsyncLogger 的監控暴露成為一個 MBean(JMX Managed Bean)。

相關源碼如下:

Server.java

private static void registerLoggerConfigs(final LoggerContext ctx, final MBeanServer mbs, final Executor executor)
throws InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException { //獲取 log4j2.xml 配置中的 loggers 標簽下的所有配置值
final Map<String, LoggerConfig> map = ctx.getConfiguration().getLoggers();
//遍曆每個 key,其實就是 logger 的 name
for (final String name : map.keySet()) {
final LoggerConfig cfg = map.get(name);
final LoggerConfigAdmin mbean = new LoggerConfigAdmin(ctx, cfg);
//對於每個 logger 注册一個 LoggerConfigAdmin
register(mbs, mbean, mbean.getObjectName());
//如果是异步日志配置,則注册一個 RingBufferAdmin
if (cfg instanceof AsyncLoggerConfig) {
final AsyncLoggerConfig async = (AsyncLoggerConfig) cfg;
final RingBufferAdmin rbmbean = async.createRingBufferAdmin(ctx.getName());
register(mbs, rbmbean, rbmbean.getObjectName());
}
}
}

創建的 MBean 的類源碼:RingBufferAdmin.java

public class RingBufferAdmin implements RingBufferAdminMBean {
private final RingBuffer<?> ringBuffer;
private final ObjectName objectName;
//... 省略其他我們不關心的代碼 public static final String DOMAIN = "org.apache.logging.log4j2";
String PATTERN_ASYNC_LOGGER_CONFIG = DOMAIN + ":type=%s,component=Loggers,name=%s,subtype=RingBuffer"; //創建 RingBufferAdmin,名稱格式符合 Mbean 的名稱格式
public static RingBufferAdmin forAsyncLoggerConfig(final RingBuffer<?> ringBuffer,
final String contextName, final String configName) {
final String ctxName = Server.escape(contextName);
//對於 RootLogger,這裏 cfgName 為空字符串
final String cfgName = Server.escape(configName);
final String name = String.format(PATTERN_ASYNC_LOGGER_CONFIG, ctxName, cfgName);
return new RingBufferAdmin(ringBuffer, name);
} //獲取 RingBuffer 的大小
@Override
public long getBufferSize() {
return ringBuffer == null ? 0 : ringBuffer.getBufferSize();
}
//獲取 RingBuffer 剩餘的大小
@Override
public long getRemainingCapacity() {
return ringBuffer == null ? 0 : ringBuffer.remainingCapacity();
}
public ObjectName getObjectName() {
return objectName;
}
}

我們的微服務項目中使用了 spring boot,並且集成了 prometheus。我們可以通過將 Log4j2 RingBuffer 大小作為指標暴露到 prometheus 中,通過如下代碼:

對應源碼:Log4j2Configuration.java

import io.micrometer.core.instrument.Gauge;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.jmx.RingBufferAdminMBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener; import javax.annotation.PostConstruct;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory; @Log4j2
@Configuration(proxyBeanMethods = false)
//需要在引入了 prometheus 並且 actuator 暴露了 prometheus 端口的情况下才加載
@ConditionalOnEnabledMetricsExport("prometheus")
public class Log4j2Configuration {
@Autowired
private ObjectProvider<PrometheusMeterRegistry> meterRegistry;
//只初始化一次
private volatile boolean isInitialized = false; //需要在 ApplicationContext 刷新之後進行注册
//在加載 ApplicationContext 之前,日志配置就已經初始化好了
//但是 prometheus 的相關 Bean 加載比較複雜,並且隨著版本更迭改動比較多,所以就直接偷懶,在整個 ApplicationContext 刷新之後再注册
// ApplicationContext 可能 refresh 多次,例如調用 /actuator/refresh,還有就是多 ApplicationContext 的場景
// 這裏為了簡單,通過一個簡單的 isInitialized 判斷是否是第一次初始化,保證只初始化一次
@EventListener(ContextRefreshedEvent.class)
public synchronized void init() {
if (!isInitialized) {
//通過 LogManager 獲取 LoggerContext,從而獲取配置
LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
org.apache.logging.log4j.core.config.Configuration configuration = loggerContext.getConfiguration();
//獲取 LoggerContext 的名稱,因為 Mbean 的名稱包含這個
String ctxName = loggerContext.getName();
configuration.getLoggers().keySet().forEach(k -> {
try {
//針對 RootLogger,它的 cfgName 是空字符串,為了顯示好看,我們在 prometheus 中將它命名為 root
String cfgName = StringUtils.isBlank(k) ? "" : k;
String gaugeName = StringUtils.isBlank(k) ? "root" : k;
Gauge.builder(gaugeName + "_logger_ring_buffer_remaining_capacity", () ->
{
try {
return (Number) ManagementFactory.getPlatformMBeanServer()
.getAttribute(new ObjectName(
//按照 Log4j2 源碼中的命名方式組裝名稱
String.format(RingBufferAdminMBean.PATTERN_ASYNC_LOGGER_CONFIG, ctxName, cfgName)
//獲取剩餘大小,注意這個是嚴格區分大小寫的
), "RemainingCapacity");
} catch (Exception e) {
log.error("get {} ring buffer remaining size error", k, e);
}
return -1;
}).register(meterRegistry.getIfAvailable());
} catch (Exception e) {
log.error("Log4j2Configuration-init error: {}", e.getMessage(), e);
}
});
isInitialized = true;
}
}
}

增加這個代碼之後,請求 /actuator/prometheus 之後,可以看到對應的返回:

//省略其他的
# HELP root_logger_ring_buffer_remaining_capacity
# TYPE root_logger_ring_buffer_remaining_capacity gauge
root_logger_ring_buffer_remaining_capacity 262144.0
# HELP org_mybatis_logger_ring_buffer_remaining_capacity
# TYPE org_mybatis_logger_ring_buffer_remaining_capacity gauge
org_mybatis_logger_ring_buffer_remaining_capacity 262144.0
# HELP com_alibaba_druid_pool_DruidDataSourceStatLoggerImpl_logger_ring_buffer_remaining_capacity
# TYPE com_alibaba_druid_pool_DruidDataSourceStatLoggerImpl_logger_ring_buffer_remaining_capacity gauge
com_alibaba_druid_pool_DruidDataSourceStatLoggerImpl_logger_ring_buffer_remaining_capacity 262144.0
# HELP RocketmqClient_logger_ring_buffer_remaining_capacity
# TYPE RocketmqClient_logger_ring_buffer_remaining_capacity gauge
RocketmqClient_logger_ring_buffer_remaining_capacity 262144.0

這樣,當這個值為 0 持續一段時間後(就代錶 RingBuffer 滿了,日志生成速度已經遠大於消費寫入 Appender 的速度了),我們就認為這個應用日志負載過高了。

其實可以通過 JMX 直接查看動態修改 Log4j2 的各種配置,Log4j2 中暴露了很多 JMX Bean,例如通過 JConsole 可以查看並修改:

但是,JMX 裏面包含的信息太多,並且我們的服務器在世界各地,遠程 JMX 很不穩定,所以我們還是通過 actuator 暴露 http 接口進行操作。

首先,要先配置 actuator 要通過 HTTP 暴露出日志 API,我們這裏的配置是:

management:
endpoints:
# 不通過 JMX 暴露任何 actuator 接口
jmx:
exposure:
exclude: '*'
# 通過 JMX 暴露所有 actuator 接口
web:
exposure:
include: '*'

請求接口 GET /actuator/loggers,可以看到如下的返回,可以知道當前日志框架支持哪些級別的日志配置,以及每個 Logger 的級別配置。

{
"levels": [
"OFF",
"FATAL",
"ERROR",
"WARN",
"INFO",
"DEBUG",
"TRACE"
],
"loggers": {
"ROOT": {
"configuredLevel": "WARN",
"effectiveLevel": "WARN"
},
"org.mybatis": {
"configuredLevel": "ERROR",
"effectiveLevel": "ERROR"
}
},
"groups": {
}
}

如果我們想增加或者修改某一 Logger 的配置,可以通過 POST /actuator/loggers/自定義logger名稱,Body 為:

{
"configuredLevel": "WARN"
}

我們這一節詳細分析了我們微服務框架中日志相關的各種配置,包括基礎配置,鏈路追踪實現與配置以及如果沒有鏈路追踪信息時候的解决辦法,並且針對一些影響性能的核心配置做了詳細說明。然後針對日志的 RingBuffer 監控做了個性化定制,並且說明了通過 actuator 查看並動態修改日志配置。下一節我們將會開始分析基於 spring-mvc 同步微服務使用的 web 容器 - Undertow。

微信搜索“我的編程喵”關注公眾號,每日一刷,輕松提昇技術,斬獲各種offer

SpringCloud昇級之路2020.0.x版-11.Log4j2 監控相關的更多相關文章

  1. Spring Cloud 昇級之路 - 2020.0.x - 1. 背景知識、需求描述與公共依賴

    1. 背景知識.需求描述與公共依賴 1.1. 背景知識 & 需求描述 Spring Cloud 官方文檔說了,它是一個完整的微服務體系,用戶可以通過使用 Spring Cloud 快速搭建一個 ...

  2. Spring Cloud 昇級之路 - 2020.0.x - 4. 使用 Eureka 作為注册中心

    Eureka 目前的狀態:Eureka 目前 1.x 版本還在更新,但是應該不會更新新的功能了,只是對現有功能進行維護,昇級並兼容所需的依賴. Eureka 2.x 已經胎死腹中了.但是,這也不代錶 ...

  3. Spring Cloud 昇級之路 - 2020.0.x - 2. 使用 Undertow 作為我們的 Web 服務容器

    本項目代碼地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 在我們的項目中,我 ...

  4. Spring Cloud 昇級之路 - 2020.0.x - 3. Undertow 的 accesslog 配置

    上一節我們講述了如何使用 Undertow 作為我們的 Web 服務容器,本小節我們來分析使用 Undertow 的另一個問題,也就是如何配置 accesslog,以及 accesslog 的各種占比特 ...

  5. Spring Cloud 昇級之路 - 2020.0.x - 6. 使用 Spring Cloud LoadBalancer (1)

    本項目代碼地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 我們使用 Spri ...

  6. Spring Cloud 昇級之路 - 2020.0.x - 7. 使用 Spring Cloud LoadBalancer (2)

    本項目代碼地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 我們使用 Spri ...

  7. Spring Cloud 昇級之路 - 2020.0.x - 5. 理解 NamedContextFactory

    spring-cloud-commons 中參考了 spring-cloud-netflix 的設計,引入了 NamedContextFactory 機制,一般用於對於不同微服務的客戶端模塊使用不同的 ...

  8. 【Android】將Xamarin For VS昇級為4.1.0.530版

    分類:C#.Android.VS2015(自帶Update2).Win10 創建日期:2016-06-10 2016-08-03說明:該版本已過時,新版本詳見本博客置頂的更新. 一.Xamarin f ...

  9. 運維工程師打怪昇級進階之路 V2.0

    在此之前,發布過兩個版本: 運維工程師打怪昇級之路 V1.0 版本發布 運維工程師打怪昇級必經之路 V1.0.1 很多讀者夥伴們反應總結的很系統.很全面,無論是0基礎初學者,還是有基礎的入門者,或者是 ...

  10. 《.NET 5.0 背鍋案》第1集:驗證 .NET 5.0 正式版 docker 鏡像問題

    今天我們分析了博客站點的2次故障(故障一.故障二),發現一個巧合的地方,.NET 5.0 正式版的 docker 鏡像是在11月10日提前發布上線的. 而在11月10日下午4點左右,由於 CI 服務器 ...

隨機推薦

  1. iframe 跨域相互操作

    我們在開發後臺管理系統時可能會經常要跟 iframe 打交道,因為現在大部分後臺管理系統都是頁面內嵌iframe,所以有時候兩者之間就難免要互相通信,但瀏覽器為了安全的原因,所以就禁止了不同域的訪問, ...

  2. Leetcode Copy List with Random Pointer

    A linked list is given such that each node contains an additional random pointer which could point t ...

  3. sae flask 微信公眾平臺開發

    index.wsgi啟動服務文件 import sae from evilxr import app application = sae.create_wsgi_app(app) evilxr.py ...

  4. HBase與MongDB等NoSQL數據庫對照

    HBase概念學習(十)HBase與MongDB等NoSQL數據庫對照 轉載請注明出處: jiq•欽's technical Blog - 季義欽 一.開篇 淘寶之前使用的存儲層架構一直是MySQL數 ...

  5. 學習配置vsftp 進行ftp文件的傳輸

    一. FTP 說明 linux 系統下常用的FTP 是vsftp, 即Very Security File Transfer Protocol. 還有一個是proftp(Profession ftp) ...

  6. centos 忘記 root 密碼

    采用單用戶維護模式可以重設置新密碼 系統重啟,按任意鍵進入如下所示的菜單: 選擇“kernel /.....”根據提示,按下 "e" 就能進入grup 編輯模式,此時出現的畫面類似 ...

  7. 關於driver_register做了些什麼

    現在進入driver_register()函數去看看.在driver_register() 中,調用了driver_find(drv->name, drv->bus)函數,這裏是幹啥呢?這 ...

  8. Linux下SVN安裝配置全程實錄(轉)

    一.安裝SVN默認安裝到/usr/local/bin下面 二.創建倉庫 svnadmin create /home/svnrepo /root/svnrepo為所創建倉庫的路徑,理論上可以是任何目錄 ...

  9. 基於epoll實現簡單的web服務器

    1. 簡介 epoll 是 Linux 平臺下特有的一種 I/O 複用模型實現,於 2002 年在 Linux kernel 2.5.44 中被引入.在 epoll 之前,Unix/Linux 平臺下 ...

  10. ThreadLoacl,InheritableThreadLocal,原理,以及配合線程池使用的一些坑

    雖然使用AOP可以獲取方法簽名,但是如果要獲取方法中計算得出的數據,那麼就得使用ThreadLocal,如果還涉及父線程,那麼可以選擇InheritableThreadLocal. 注意:理解一些原理 ...