菜宝钱包(caibao.it)是使用TRC-20协议的Usdt第三方支付平台,Usdt收款平台、Usdt自动充提平台、usdt跑分平台。免费提供入金通道、Usdt钱包支付接口、Usdt自动充值接口、Usdt无需实名寄售回收。菜宝Usdt钱包一键生成Usdt钱包、一键调用API接口、一键无实名出售Usdt。
本文要先容的是一个发生在我们线上环境的真实案例,问题发生在某次大促时代,对我们的线上集群造成了对照大的影响,这篇文章简朴复盘一下这个问题。
为了利便人人明白,现实排查和解决历程可能和本文形貌的并不完全一致,然则思绪是一样的。
一、问题历程
某次大促时代,某一个线上应用突然发生大量报警,提醒磁盘占用率过高,一度到达了80%多。
这种情形我们第一时间登录线上机械,查看线上机械的磁盘使用情形。使用下令:df查看磁盘占用情形。
$df
Filesystem 1K-blocks Used Available Use% Mounted on
/ 62914560 58911440 4003120 93% /
/dev/sda2 62914560 58911440 4003120 93% /home/admin
发现机械磁盘确实花费的对照严重,由于大促时代请求量对照多,于是我们最先最先嫌疑是不是日志太多了,导致磁盘耗尽。
这里插播一个靠山,我们的线上机械是设置了日志的自动压缩和清算的,单个文件到达一定的巨细,或者机械内容到达一定的阈值之后,就会自动触发。
然则大促当天并没有触发日志的清算,导致机械磁盘一度被耗尽。
经由排查,我们发现是应用的某一些Log文件,占用了极大的磁盘空间,而且还在不停的增大。
du -sm * | sort -nr
512 service.log.20201105193331
256 service.log
428 service.log.20201106151311
286 service.log.20201107195011
356 service.log.20201108155838
du -sm * | sort -nr :统计当前目录下文件巨细,并凭据巨细排序代码文本框
于是经由和运维同砚相同,我们决议举行紧要处置。
首先接纳的手段就是手动清算日志文件,运维同砚登录到服务器上面之后,手动的清算了一些不太主要的日志文件。
rm service.log.20201105193331
然则执行了清算下令之后,发现机械上面的磁盘使用率并没有削减,而且照样在不停的增添。
$df
Filesystem 1K-blocks Used Available Use% Mounted on
/ 62914560 58911440 4003120 93% /
/dev/sda2 62914560 58911440 4003120 93% /home/admin
于是我们最先排查为什么日志被删除之后,内存空间没有被释放,通过下令,我们查到了是有一个历程还在对文件举行读取。
lsof |grep deleted
SLS 11526 root 3r REG 253,0 2665433605 104181296 /home/admin/****/service.log.20201205193331 (deleted)
lsof |grep deleted 的作用是:查看所有已打开文件并筛选出其中已删除状态的文件
经由排查,这个历程是一个SLS历程,在不停的从机械上读取日志内容。
LS是阿里的一个日志服务,提供一站式提供数据网络、洗濯、剖析、可视化和告警功效。简朴点说就是会把服务器上面的日志采集到,持久化,然后供查询、剖析等。
我们线上日志都市通过SLS举行采集,以是,通过剖析,我们发现磁盘空间没释放,和SLS的日志读取有关。
到这里,问题基本已经定位到了,那么我们插播一下原理,先容一下这背后的靠山知识。
二、靠山知识
Linux系统中是通过link的数目来控制文件删除的,只有当一个文件不存在任何link的时刻,这个文件才会被删除。
一样平常来说,每个文件都有2个link计数器:i_count 和 i_nlink,也就是说:Linux系统中只有i_nlink及i_count都为0的时刻,这个文件才会真正被删除。
当一个文件被某一个历程引用时,对应i_count数就会增添;当建立文件的硬链接的时刻,对应i_nlink数就会增添。
在Linux或者Unix系统中,通过rm或者文件治理器删除文件,只是将它会从文件系统的目录结构上排除链接(unlink),现实上就是削减磁盘引用计数i_nlink,然则并不会削减i_count数。
若是一个文件正在被某个历程挪用,用户使用rm下令把文件"删除"了,这时刻通过ls等文件治理下令就无法找到这个文件了,然则并不意味着这个文件真正的从磁盘上删除了。
由于另有一个历程在正常的执行,在向文件中读取或写入,也就是说文件实在并没有被真正的"删除",以是磁盘空间也就会一直被占用。
而我们的线上问题就是这个原理,由于有一个历程正在对日志文件举行操作,以是实在rm操作并没有将文件真正的删除,以是磁盘空间并未释放。
三、问题解决
在了解了线上的问题征象以及以上的相关靠山知识之后,我们就可以想到设施来解决这个问题了。
那就是想设施把SLS历程对这个日志文件的引用干掉,文件就可以真正的被删除,磁盘空间就能真正的被释放掉了。
kill -9 11526
$df
Filesystem 1K-blocks Used Available Use% Mounted on
/ 62914560 50331648 12582912 80% /
/dev/sda2 62914560 50331648 12582912 80% /home/admin
稀奇提醒下,在执行kill -9 之前,一定要思量下执行的结果是什么,背后原理可以参考:我到服务器执行kill -9后,就被通知第二天别来了!
事后,我们经由复盘,发现之以是泛起这样的问题,主要有两个缘故原由:
- SLS日志拉取速率太慢
深入剖析后我们发现,这个应用打印了许多历程日志,最初日志打印是为了利便排查线上的问题,或者做数据剖析用的,然则大促时代日志量激增,导致磁盘空间占用极速上升。
另外,由于该应用和几个其他的大应用共用了一份SLS的project,导致SLS拉取速率被拉低,进而导致历程一直无法竣事。
事后,我们也总结了一些改进项,对于第二个问题,我们对于该应用的SLS设置做拆分,自力出来举行治理。
对于第一个问题,那就是大促时代引入日志降级的计谋,一旦发生磁盘爆满,就是将日志降级掉。
关于日志降级,我开发了一个通用的工具,就是通过设置的方式,动态推送日志级别,动态修改线上的日志输出级别。而且把这份设置的修改设置到我们的预案平台上,大促时代举行准时或者紧要预案处置,即可制止这个问题。
下面和人人详细分享一下,日志降级工具的开发思绪和相关代码:
1)日志级别
在最先正文前简朴先容下日志级别,差别的日志框架支持差别的日志级别,其中对照常见的就是Log4j和Logback。
在Log4j中支持8种日志级别,优先级从高到低依次为:OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE、 ALL。
Logback中支持7种日志级别,优先级从高到低分别是:OFF、ERROR、WARN、INFO、DEBUG、TRACE、ALL。
可以看到常见的ERROR、WARN、INFO、DEBUG,这两者都是支持的。
所谓设置日志的输出级别示意的是输出的日志的最低级别,也就是说,若是我们把级别设置成INFO,那么包罗INFO在内以及比INFO优先级高的级别的日志都可以输出。
无论是Log4j照样Logback,都是通过日志的设置文件来控制日志输出级别的。这里就不详述了。
2)日志框架
上面我们提到了Log4j和Logback,这两种都是对照常用的日志框架。
然则许多时刻,我们在代码中打印日志并不是直接使用这种日志框架来举行的,而是依赖了一个日志门面来举行的,如slf4j、commons-logging等。
一样平常最最常用的方式就是通过slf4j提供的LoggerFactory的getLogger来获取Logger,然后举行日志打印
private static final Logger LOGGER = LoggerFactory.getLogger(LoggerService.class);
public void test(){
LOGGER.info("hollis log test");
}
当我们使用LoggerFactory.getLogger方式建立一个Logger工具的时刻,会给他传入一个loggerName,通过这个loggerName来唯一识别一个Logger,如上面的方式就是使用LoggerService这个类的全路径名作为其loggerName。
loggerName是每一个Logger的设置信息一部门,除此之外另有日志输出级别等信息。
关于为什么不直接使用log4j和logback打印日志,我在《为什么阿里巴巴克制工程师直接使用日志系统(Log4j、Logback)中的 API》中剖析过。
3)Arthas改变日志级别
在最先先容代码实现之前,先先容一个工具,也可以辅助我们的动态修改日志级别。
那就是阿里开源的神器——Arthas (https://arthas.aliyun.com/doc/ )。
Arthas提供了一个logger下令,这个下令可以查看和更新logger信息,包罗日志级别。
查看指定名字的logger信息
[arthas@2062]$ logger -n org.springframework.web
name org.springframework.web
class ch.qos.logback.classic.Logger
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
level null
effectiveLevel INFO
additivity true
codeSource file:/Users/hengyunabc/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar
更新logger level
[arthas@2062]$ logger --name ROOT --level debug
update logger level success.
简朴吧,使用一个下令就可以修改机日志级别了。
然则Arthas现在对于集群的支持并不是稀奇的友好,虽然他支持了通过Arthas Tunnel Server/Client 来远程治理/毗邻多个Agent,然则使用起来还不是很利便,而且对于下令的使用要求对照高。
另有就是我们系统通过一个工具,利便我们在大促时代通过预案方式动态调整日志级别,这方面使用arthas就不是很利便了。
4)代码实现
我写的这个工具功效很简朴,就是提供动态修改日志级别的入口,利便用户动态修改级别。
而且为了利便使用,我将他封装在一个Spring Boot Starter内里了,另有就是将他直接对接到公司内部的设置中央中,可以利便的通过设置中央一键修改日志级别。
首先看下其中最焦点的功效,那就是动态修改日志级别的部门,代码如下
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggerConfiguration;
import org.springframework.boot.logging.LoggingSystem;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.springframework.boot.logging.LoggingSystem.ROOT_LOGGER_NAME;
/**
* 日志级别设置服务类
*
* @author Hollis
*/
public class LoggerLevelSettingService {
@Autowired
private LoggingSystem loggingSystem;
private static final Logger LOGGER = LoggerFactory.getLogger(LoggerLevelSettingService.class);
public void setRootLoggerLevel(String level) {
LoggerConfiguration loggerConfiguration = loggingSystem.getLoggerConfiguration(ROOT_LOGGER_NAME);
if (loggerConfiguration == null) {
if (LOGGER.isErrorEnabled()) {
LOGGER.error("no loggerConfiguration with loggerName " + level);
}
return;
}
if (!supportLevels().contains(level)) {
if (LOGGER.isErrorEnabled()) {
LOGGER.error("current Level is not support : " + level);
}
return;
}
if (!loggerConfiguration.getEffectiveLevel().equals(LogLevel.valueOf(level))) {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("setRootLoggerLevel success,old level is '" + loggerConfiguration.getEffectiveLevel()
+ "' , new level is '" + level + "'");
}
loggingSystem.setLogLevel(ROOT_LOGGER_NAME, LogLevel.valueOf(level));
}
}
private List<String> supportLevels() {
return loggingSystem.getSupportedLogLevels().stream().map(Enum::name).collect(Collectors.toList());
}
}
以上代码,就是凭据用户传入的level的级别,将应用的ROOT日志输出级别修改掉。
这内里用到了一个要害的服务:
org.springframework.boot.logging.LoggingSystem
LoggingSystem服务是SpringBoot对日志系统的抽象,是一个顶层的抽象类。他有许多详细的实现
通过上图,我们可以发现现在SpringBoot现在支持4种类型的日志,分别是JDK内置的Log(JavaLoggingSystem)以及Log4j(Log4JLoggingSystem)、Log4j2(Log4J2LoggingSystem)以及Logback(LogbackLoggingSystem)。
LoggingSystem是个抽象类,内部有这几个方式:
- beforeInitialize方式:日志系统初始化之前需要处置的事情。抽象方式,差别的日志架构举行差别的处置
- initialize方式:初始化日志系统。默认不举行任何处置,需子类举行初始化事情
- cleanUp方式:日志系统的消灭事情。默认不举行任何处置,需子类举行消灭事情
- getShutdownHandler方式:返回一个Runnable用于当jvm退出的时刻处置日志系统关闭后需要举行的操作,默认返回null,也就是什么都不做
- setLogLevel方式:抽象方式,用于设置对应logger的级别
SpringBoot在启动时,会完成LoggingSystem的初始化,这部门代码是在LoggingApplicationListener中实现的
/**
* 执行LoggingSystem初始化的前置操作
*/
private void onApplicationStartingEvent(ApplicationStartingEvent event) {
//获取LoggingSystem的真实实现,
// 此处会凭据差别的日志框架获取差别的实现,
// logback :LogbackLoggingSystem
// log4j2:Log4J2LoggingSystem
// javalog:JavaLoggingSystem
this.loggingSystem = LoggingSystem
.get(event.getSpringApplication().getClassLoader());
//执行beforeInitialize方式完成初始化前置操作
this.loggingSystem.beforeInitialize();
,,菜宝钱包(caibao.it)是使用TRC-20协议的Usdt第三方支付平台,Usdt收款平台、Usdt自动充提平台、usdt跑分平台。免费提供入金通道、Usdt钱包支付接口、Usdt自动充值接口、Usdt无需实名寄售回收。菜宝Usdt钱包一键生成Usdt钱包、一键调用API接口、一键无实名出售Usdt。
}
有了LoggingSystem以后,我们就可以通过他来动态的修改日志级别。他帮我们屏障掉了底层的详细日志框架。
除了支持修改ROOT级别的日志以外,还可以支持用户自界说的日志的级别修改,代码实现如下:
先界说一个LoggerConfig,用来封装日志的设置
/**
* the config of logger
*
* @author Hollis
*/
public class LoggerConfig {
/**
* the name of the logger
*/
private String loggerName;
/**
* the log level
*
* @see LogLevel
*/
private String level;
public String getLoggerName() {
return loggerName;
}
public void setLoggerName(String loggerName) {
this.loggerName = loggerName;
}
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
}
接着提供方式动态修改日志级别:
public void setLoggerLevel(List<LoggerConfig> configList) {
Optional.ofNullable(configList).orElse(Collections.emptyList()).forEach(
config -> {
LoggerConfiguration loggerConfiguration = loggingSystem.getLoggerConfiguration(config.getLoggerName());
if (loggerConfiguration == null) {
if (LOGGER.isErrorEnabled()) {
LOGGER.error("no loggerConfiguration with loggerName " + config.getLoggerName());
}
return;
}
if (!supportLevels().contains(config.getLevel())) {
if (LOGGER.isErrorEnabled()) {
LOGGER.error("current Level is not support : " + config.getLevel());
}
return;
}
if (LOGGER.isInfoEnabled()) {
LOGGER.info("setLoggerLevel success for logger '" + config.getLoggerName() + "' ,old level is '"
+ loggerConfiguration.getEffectiveLevel()
+ "' , new level is '" + config.getLevel() + "'");
}
loggingSystem.setLogLevel(config.getLoggerName(), LogLevel.valueOf(config.getLevel()));
}
);
}
以上,凭据用户传入的LoggerConfig,修改指定的loggerName对应的loggerLevel。至于LoggerLevel是怎么来的,就可以通过设置的方式传入,好比剖析JSON花样的设置或者YML文件等。
如我们可以在设置中央中接纳以下设置来控制日志级别,并推送:
[{'loggerName':'com.hollis.degradation.core.logger.LoggerLevelSettingService','level':'WARN'}]
以上设置,会使得loggerName为com.hollis.degradation.core.logger.LoggerLevelSettingService的日志的级别动态修改为WARN,另外,若是设置信息如下:
[{'loggerName':'com.hollis.degradation.core.logger','level':'WARN'}]
固然,这个设置也支持设置多个Logger的级别,若是是以下设置内容:
[
{'loggerName':'com.hollis.degradation.core.logger','level':'WARN'}
,{'loggerName':'com.hollis.degradation.core.logger.LoggerLevelSettingService','level':'INFO'}
]
加入代码中有多个日志,他们的界说方式分别为
private static final Logger LOGGER1 = LoggerFactory.getLogger(LoggerLevelSettingService.class);
private static final Logger LOGGER2 = LoggerFactory.getLogger(TestService.class);
private static final Logger LOGGER3 = LoggerFactory.getLogger(DebugService.class);
那么,设置生效后,会使得以上的LOGGER1的输出级别为INFO,而LOGGER2和LOGGER3的级别为WARN。
除此以外,上面的日志级别修改,可能会影响到我们自己这个工具自己的日志输出,以是,我们提供了一个方式,可以直接修改我们自己这个日志服务的日志级别
public void setDegradationLoggerLevel(String level) {
LoggerConfiguration loggerConfiguration = loggingSystem.getLoggerConfiguration(
this.getClass().getName());
if (loggerConfiguration == null) {
if (LOGGER.isWarnEnabled()) {
LOGGER.warn("no loggerConfiguration with loggerName " + level);
}
return;
}
if (!supportLevels().contains(level)) {
if (LOGGER.isErrorEnabled()) {
LOGGER.error("current Level is not support : " + level);
}
return;
}
if (!loggerConfiguration.getEffectiveLevel().equals(LogLevel.valueOf(level))) {
loggingSystem.setLogLevel(this.getClass().getName(), LogLevel.valueOf(level));
}
}
有了以上的LoggerLevelSettingService类以后,基本具备了动态修改日志的能力,接下来就是想设施通过设置中央动态修改日志级别了。
这内里由于差别的设置中央用法差别,我只是拿我们自己的设置中央简朴举例
/**
* 降级开关注册器
*
* @author Hollis
*/
public class DegradationSwitchInitializer implements Listener, InitializingBean {
//从设置项中读取应用名,利便注册到设置中央
@Value("${project.name}")
private String appName;
@Autowired
private LoggerLevelSettingService loggerLevelSettingService;
//设置中央值发生转变会自动回调该方式
@Override
public void valueChange(String appName, String nameSpace, String name,
String value) {
if (name.equals(rootLogLevel.name())) {
loggerLevelSettingService.setRootLoggerLevel(value);
}
if (name.equals(logLevelConfig.name())) {
List<LoggerConfig> loggerConfigs = JSON.parseArray(value, LoggerConfig.class);
loggerLevelSettingService.setLoggerLevel(loggerConfigs);
}
//将降级工具的日志输出级别设置成INFO,保证其日志可以正常输出
loggerLevelSettingService.setDegradationLoggerLevel("INFO");
}
@Override
public void afterPropertiesSet() {
//将服务设置到设置中央
ConfigCenterManager.addListener(this);
ConfigCenterManager.init(appName, DegradationConfig.class);
}
}
以上,我们实现了监听设置中央的值的转变,动态修改日志级别。
基本功效就都完成了,接下来可以思量若何让其他应用快速接入,那就是界说一个Starter,可以利便快速接入。主要代码如下:
先界说一个Configuration类:
/**
* @author Hollis
*/
@Configuration
@ConditionalOnProperty(prefix = "hollis.degradation", name = "enable", havingValue = "true")
public class HollisDegradationAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "project.name")
public LoggerLevelSettingService loggerLevelSettingService() {
return new LoggerLevelSettingService();
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(value = LoggerLevelSettingService.class)
public DegradationSwitchInitializer degradationSwitchInitializer() {
return new DegradationSwitchInitializer();
}
}
在这个类内里界说两个bean,而且bean界说的条件是应用中设置了以下两个设置项:
hollis.degradation.enable = true
project.name = test
接下来就是定一个spring.factories文件,界说内容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.hollis.degradation.starter.autoconfiguration.HollisDegradationAutoConfiguration
以上,只需要在需要引入降级工具的应用中,引入我们的这个starter,而且设置两个设置项即可。
接入后,可以利便的在设置中央中动态修改单机或者集群的日志输出级别,而且可以在大促时代设置到预案平台上,通过紧要预案快速执行。
以上,基本实现了许多基本的功效,实现时思量的因素主要有以下几个:
1、通用性。要同时可以支持差别的日志框架,客户端使用的日志框架不影响我们的功效,而且客户端不需要体贴自己的日志框架的区别。
2、可设置性。可以将设置信息通过外部设置中央推送,可以快速举行调整。
3、易用性。通过封装到SpringBoot Starter中,利便客户端快速接入。
4、无侵入性。框架的使用不应该影响到应用的正常运行。
固然,这个工具只是我花了几个小时撸出来的,其中另有许多不足,实在另有许多事情可以优化,好比设置的花样可以支持多种、支持通过EndPoint查看日志设置情形等,这些都还没有实现。
本文只是提供一个思绪,希望人人都能学会用工具化的方式解决一样平常事情中遇到的问题,学会造轮子。
四、思索
每次大促之后我们复盘,都市发现实在大多数问题都是由几个不起眼的小问题堆积到一起而引发的。
在问题剖析历程中往往会需要运用到许多非开发技术相关的知识,如操作系统、计算机网络、数据库、甚至硬件相关的知识。
以是我一直以为,判断一个程序员是否牛X,就看他的解决问题的能力!
作者丨Hollis Hollis
泉源丨民众号:Hollis (ID:hollischuang)
网友评论