问题现象

线上某个应用的某个实例突然出现某些次请求服务响应极慢的情况,有几次请求超过 60s 才返回,并且通过日志发现,服务线程并没有做什么很重的操作。这种情况断断续续持续了半小时左右。

核心问题定位

由于半小时后,服务实例恢复正常,并且出现请求极慢的次数并不太多,没有能及时打印出现问题的时候线程堆栈采集相关信息。但是,我们有自己的法宝,JFR(关于 JFR,请参考我的另一系列JFR全解)。

JFR 非常适合用来事后复盘定位问题,并且配置得当的话,性能损耗极小,并且不像 APM 系统一样需要额外的采集以及需要统一的服务进程进行整合展现。我们随用随取即可,但是 JFR 面向的是单进程的问题定位,找到问题进程,定位跨进程业务问题链路追踪还是需要 APM 系统的。

我们通过如下命令采集对应时间段的 JFR:

jcmd 进程pid JFR.dump begin=2021-07-04T17:30:00.000Z end=2021-07-04T18:30:00.000Z

我们选择的时间跨度比出问题的时间跨度大一些,这样保证底层问题出现的时候也被采集写入这个 dump 文件。

对于历史某些请求响应慢,我一般按照如下流程去看:

  1. 是否有 STW(Stop-the-world,参考我的另一篇文章:JVM相关 - SafePoint 与 Stop The World 全解):
  2. 是否有 GC 导致的长时间 STW
  3. 是否有其他原因导致进程所有线程进入 safepoint 导致 STW
  4. 是否 IO 花了太长时间,例如调用其他微服务,访问各种存储(硬盘,数据库,缓存等等)
  5. 是否在某些锁上面阻塞太长时间?
  6. 是否 CPU 占用过高,哪些线程导致的?

接下来我们来详细分析,首先是 GC,从下图看,并没有能严重影响线程 STW 很长时间的:

然后查看 safepoint 相关,发现也没有什么影响:

接着,我们查看 IO 相关的事件,也并没有发现业务相关的阻塞读:

然后,我们查看是否在某些锁上面阻塞太长时间,终于发现异常:

通过堆栈,我们发现是** log4j2 打印日志卡住了**。

问题分析

1. Java Monitor Blocked 的 Monitor Address 对于同一个对象也是会改变的

首先,对于 Java Monitor Blocked 这个事件,我们可以通过 Monitor Address 来判断是否是同一个锁。例如这里我们发现这些线程都是阻塞在 FFFF 4C09 3188 这个地址的对象,这个地址是基于对象在 Java 堆中的相对地址得出的。

但是对于同一个对象,这个地址并不会是不变的,在每次 GC 扫描整理到这个对象对象的时候,如果有回收内存,一般这个地址就会改变。由于我们使用的是 G1,所以并不是每次 GC 都会扫描到这个对象,但是如果地址发生改变,一定是因为发生了 GC

2. log4j2 异步日志原理简介

在我们的应用配置中,采用的日志框架是 log4j2,采用的配置是异步日志配置。这里简单说一下 Log4j2 异步日志的原理:Log4j2 异步日志基于高性能数据结构 Disruptor,Disruptor 是一个环形 buffer,做了很多性能优化(具体原理可以参考我的另一系列:高并发数据结构(disruptor)),Log4j2 对此的应用如下所示:

简单来说,多线程通过 log4j2 的门面类 org.apache.logging.log4j.Logger 进行日志输出,被封装成为一个 org.apache.logging.log4j.core.LogEvent,放入到 Disruptor 的环形 buffer 中。在消费端有一个单线程消费这些 LogEvent 写入对应的 Appender,我们这里只有一个 Appender,其配置是:

<RollingFile name="file" append="true"filePattern="./app.log-%d{yyyy.MM.dd.HH}"><PatternLayout pattern="${logFormat}"/><Policies><TimeBasedTriggeringPolicy interval="1" modulate="true"/></Policies><DirectWriteRolloverStrategy maxFiles="72"/>
</RollingFile>

异步日志 logger 配置为(配置 includeLocation 为 false,避免每次打印日志需要获取调用堆栈的性能损耗):

<Asyncroot level="info" includeLocation="false"><appender-ref ref="file"/>
</Asyncroot>

log4j component 额外配置为:

log4j2.component.properties:

# 当没有日志的时候,消费线程通过 Block 等待日志事件到来,这样 CPU 占用最少
AsyncLoggerConfig.WaitStrategy=Block

3. log4j2 disruptor 的 RingBuffer 的大小

既然是一个环形 Buffer,它的容量是有限的,我们这里没有修改它的大小,走的是默认配置,查看其源码:

AsyncLoggerConfigDisruptor.java
public synchronized void start() {//省略其他代码ringBufferSize = DisruptorUtil.calculateRingBufferSize("AsyncLoggerConfig.RingBufferSize");disruptor = new Disruptor<>(factory, ringBufferSize, threadFactory, ProducerType.MULTI, waitStrategy);
}
DisruptorUtil.java
private static final int RINGBUFFER_MIN_SIZE = 128;
private static final int RINGBUFFER_DEFAULT_SIZE = 256 * 1024;
private static final int RINGBUFFER_NO_GC_DEFAULT_SIZE = 4 * 1024;static int calculateRingBufferSize(final String propertyName) {//是否启用了 ThreadLocal,如果是则为 4 kB,不是则为 256 kBint ringBufferSize = Constants.ENABLE_THREADLOCALS ? RINGBUFFER_NO_GC_DEFAULT_SIZE : RINGBUFFER_DEFAULT_SIZE;//读取系统变量,以及 log4j2.component.properties 文件获取 propertyName(这里是 AsyncLoggerConfig.RingBufferSize) 这个配置final String userPreferredRBSize = PropertiesUtil.getProperties().getStringProperty(propertyName,String.valueOf(ringBufferSize));try {int size = Integer.parseInt(userPreferredRBSize);//如果小于 128 字节则按照 128 字节设置if (size < RINGBUFFER_MIN_SIZE) {size = RINGBUFFER_MIN_SIZE;LOGGER.warn("Invalid RingBufferSize {}, using minimum size {}.", userPreferredRBSize,RINGBUFFER_MIN_SIZE);}ringBufferSize = size;} catch (final Exception ex) {LOGGER.warn("Invalid RingBufferSize {}, using default size {}.", userPreferredRBSize, ringBufferSize);}//取最近的 2 的 n 次方,因为对于 2 的 n 次方取余等于对于 2^n-1 取与运算,这样更快return Integers.ceilingNextPowerOfTwo(ringBufferSize);
}

如果启用了 ThreadLocal 这种方式生成 LogEvent,每次不新生成的 LogEvent 用之前的,用 ThreadLocal 存储的,这样避免了创建新的 LogEvent。但是考虑下面这种情况:

logger.info("{}", someObj);

这样会造成强引用,导致如果线程没有新的日志,这个 someObj 一直不能回收。所以针对 Web 应用,log4j2 默认是不启用 ThreadLocal 的 方式生成 LogEvent

Constants.java
public static final boolean IS_WEB_APP = PropertiesUtil.getProperties().getBooleanProperty("log4j2.is.webapp", isClassAvailable("javax.servlet.Servlet"));
public static final boolean ENABLE_THREADLOCALS = !IS_WEB_APP && PropertiesUtil.getProperties().getBooleanProperty("log4j2.enable.threadlocals", true);

由此,可以看出,我们的 RingBuffer 的大小为 256 kB

4. RingBuffer 满了 log4j2 会发生什么?

当 RingBuffer 满了,如果在 log4j2.component.properties 配置了 AsyncLoggerConfig.SynchronizeEnqueueWhenQueueFull=false,则会 Wait(其实是 park) 在 Disruptor 的 produce 方法上,等待消费出下一个可以生产的环形 buffer 槽;默认这个配置为 true,即所有生产日志的线程尝试获取全局中的同一个锁(private final Object queueFullEnqueueLock = new Object();):

DisruptorUtil.java
static final boolean ASYNC_CONFIG_SYNCHRONIZE_ENQUEUE_WHEN_QUEUE_FULL = PropertiesUtil.getProperties().getBooleanProperty("AsyncLoggerConfig.SynchronizeEnqueueWhenQueueFull", true);
private boolean synchronizeEnqueueWhenQueueFull() {return DisruptorUtil.ASYNC_CONFIG_SYNCHRONIZE_ENQUEUE_WHEN_QUEUE_FULL// Background thread must never block&& backgroundThreadId != Thread.currentThread().getId();
}private final Object queueFullEnqueueLock = new Object();private void enqueue(final LogEvent logEvent, final AsyncLoggerConfig asyncLoggerConfig) {//如果 AsyncLoggerConfig.SynchronizeEnqueueWhenQueueFull=true,默认就是 trueif (synchronizeEnqueueWhenQueueFull()) {synchronized (queueFullEnqueueLock) {disruptor.getRingBuffer().publishEvent(translator, logEvent, asyncLoggerConfig);}} else {//如果 AsyncLoggerConfig.SynchronizeEnqueueWhenQueueFull=falsedisruptor.getRingBuffer().publishEvent(translator, logEvent, asyncLoggerConfig);}
}

默认配置的时候,异常堆栈和我们 JFR 中看到的一样,举个例子:

"Thread-0" #27 [13136] prio=5 os_prio=0 cpu=0.00ms elapsed=141.08s tid=0x0000022d6f2fbcc0 nid=0x3350 waiting for monitor entry  [0x000000399bcfe000]java.lang.Thread.State: BLOCKED (on object monitor)at org.apache.logging.log4j.core.async.AsyncLoggerConfigDisruptor.enqueue(AsyncLoggerConfigDisruptor.java:375)- waiting to lock <merged>(a java.lang.Object)at org.apache.logging.log4j.core.async.AsyncLoggerConfigDisruptor.enqueueEvent(AsyncLoggerConfigDisruptor.java:330)at org.apache.logging.log4j.core.async.AsyncLoggerConfig.logInBackgroundThread(AsyncLoggerConfig.java:159)at org.apache.logging.log4j.core.async.EventRoute$1.logMessage(EventRoute.java:46)at org.apache.logging.log4j.core.async.AsyncLoggerConfig.handleQueueFull(AsyncLoggerConfig.java:149)at org.apache.logging.log4j.core.async.AsyncLoggerConfig.logToAsyncDelegate(AsyncLoggerConfig.java:136)at org.apache.logging.log4j.core.async.AsyncLoggerConfig.log(AsyncLoggerConfig.java:116)at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:460)at org.apache.logging.log4j.core.config.AwaitCompletionReliabilityStrategy.log(AwaitCompletionReliabilityStrategy.java:82)at org.apache.logging.log4j.core.Logger.log(Logger.java:162)at org.apache.logging.log4j.spi.AbstractLogger.tryLogMessage(AbstractLogger.java:2190)at org.apache.logging.log4j.spi.AbstractLogger.logMessageTrackRecursion(AbstractLogger.java:2144)at org.apache.logging.log4j.spi.AbstractLogger.logMessageSafely(AbstractLogger.java:2127)at org.apache.logging.log4j.spi.AbstractLogger.logMessage(AbstractLogger.java:2003)at org.apache.logging.log4j.spi.AbstractLogger.logIfEnabled(AbstractLogger.java:1975)at org.apache.logging.log4j.spi.AbstractLogger.info(AbstractLogger.java:1312)省略业务方法堆栈

配置为 false 的时候,堆栈是这个样子的:

"Thread-0" #27 [18152] prio=5 os_prio=0 cpu=0.00ms elapsed=5.68s tid=0x000002c1fa120e00 nid=0x46e8 runnable  [0x000000eda8efe000]java.lang.Thread.State: TIMED_WAITING (parking)at jdk.internal.misc.Unsafe.park(java.base@17-loom/Native Method)at java.util.concurrent.locks.LockSupport.parkNanos(java.base@17-loom/LockSupport.java:410)at com.lmax.disruptor.MultiProducerSequencer.next(MultiProducerSequencer.java:136)at com.lmax.disruptor.MultiProducerSequencer.next(MultiProducerSequencer.java:105)at com.lmax.disruptor.RingBuffer.publishEvent(RingBuffer.java:524)at org.apache.logging.log4j.core.async.AsyncLoggerConfigDisruptor.enqueue(AsyncLoggerConfigDisruptor.java:379)at org.apache.logging.log4j.core.async.AsyncLoggerConfigDisruptor.enqueueEvent(AsyncLoggerConfigDisruptor.java:330)at org.apache.logging.log4j.core.async.AsyncLoggerConfig.logInBackgroundThread(AsyncLoggerConfig.java:159)at org.apache.logging.log4j.core.async.EventRoute$1.logMessage(EventRoute.java:46)at org.apache.logging.log4j.core.async.AsyncLoggerConfig.handleQueueFull(AsyncLoggerConfig.java:149)at org.apache.logging.log4j.core.async.AsyncLoggerConfig.logToAsyncDelegate(AsyncLoggerConfig.java:136)at org.apache.logging.log4j.core.async.AsyncLoggerConfig.log(AsyncLoggerConfig.java:116)at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:460)at org.apache.logging.log4j.core.config.AwaitCompletionReliabilityStrategy.log(AwaitCompletionReliabilityStrategy.java:82)at org.apache.logging.log4j.core.Logger.log(Logger.java:162)at org.apache.logging.log4j.spi.AbstractLogger.tryLogMessage(AbstractLogger.java:2190)at org.apache.logging.log4j.spi.AbstractLogger.logMessageTrackRecursion(AbstractLogger.java:2144)at org.apache.logging.log4j.spi.AbstractLogger.logMessageSafely(AbstractLogger.java:2127)at org.apache.logging.log4j.spi.AbstractLogger.logMessage(AbstractLogger.java:2003)at org.apache.logging.log4j.spi.AbstractLogger.logIfEnabled(AbstractLogger.java:1975)at org.apache.logging.log4j.spi.AbstractLogger.info(AbstractLogger.java:1312)

5. 为何会满,我们的消费线程当时在干啥?

我们来看下当时的消费线程是否有异常,由于当时硬盘 io 看系统监控并没有异常所以这个线程很大可能是 Runnable 的,不断的在写入日志。同时,我们知道,写入文件 java 底层基于 native 调用,所以我们查看 JFR 的 native 方法采集。使用事件查看器中的 Method Profiling Sample Native,右键点击,并选择“使用所选类型事件创建新页”:

在创建出来的新页中,按照线程分组查看,查看 Log4j2 的 disruptor 消费线程,可以得出下图:

可以看出:在出问题的时间段,采集到的写入日志文件的事件数量明显增多很多,并且,通过堆栈可以看到:

"Log4j2-TF-2-AsyncLoggerConfig-1" #26 [23680] daemon prio=5 os_prio=0 cpu=406.25ms elapsed=2.89s tid=0x0000022d6f3d4080 nid=0x5c80 runnable  [0x000000399bbfe000]java.lang.Thread.State: RUNNABLEat java.io.FileOutputStream.writeBytes(java.base@17-loom/Native Method)at java.io.FileOutputStream.write(java.base@17-loom/FileOutputStream.java:365)at org.apache.logging.log4j.core.appender.OutputStreamManager.writeToDestination(OutputStreamManager.java:261)- eliminated <0x000000070ee0af40> (a org.apache.logging.log4j.core.appender.rolling.RollingFileManager)at org.apache.logging.log4j.core.appender.FileManager.writeToDestination(FileManager.java:272)- eliminated <0x000000070ee0af40> (a org.apache.logging.log4j.core.appender.rolling.RollingFileManager)at org.apache.logging.log4j.core.appender.rolling.RollingFileManager.writeToDestination(RollingFileManager.java:236)- eliminated <0x000000070ee0af40> (a org.apache.logging.log4j.core.appender.rolling.RollingFileManager)at org.apache.logging.log4j.core.appender.OutputStreamManager.flushBuffer(OutputStreamManager.java:293)- locked <0x000000070ee0af40> (a org.apache.logging.log4j.core.appender.rolling.RollingFileManager)at org.apache.logging.log4j.core.appender.OutputStreamManager.flush(OutputStreamManager.java:302)- locked <0x000000070ee0af40> (a org.apache.logging.log4j.core.appender.rolling.RollingFileManager)at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.directEncodeEvent(AbstractOutputStreamAppender.java:199)at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.tryAppend(AbstractOutputStreamAppender.java:190)at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.append(AbstractOutputStreamAppender.java:181)at org.apache.logging.log4j.core.appender.RollingFileAppender.append(RollingFileAppender.java:312)at org.apache.logging.log4j.core.config.AppenderControl.tryCallAppender(AppenderControl.java:156)at org.apache.logging.log4j.core.config.AppenderControl.callAppender0(AppenderControl.java:129)at org.apache.logging.log4j.core.config.AppenderControl.callAppenderPreventRecursion(AppenderControl.java:120)at org.apache.logging.log4j.core.config.AppenderControl.callAppender(AppenderControl.java:84)at org.apache.logging.log4j.core.config.LoggerConfig.callAppenders(LoggerConfig.java:543)at org.apache.logging.log4j.core.async.AsyncLoggerConfig.callAppenders(AsyncLoggerConfig.java:127)at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:502)at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:485)at org.apache.logging.log4j.core.async.AsyncLoggerConfig.log(AsyncLoggerConfig.java:121)at org.apache.logging.log4j.core.async.AsyncLoggerConfig.logToAsyncLoggerConfigsOnCurrentThread(AsyncLoggerConfig.java:169)at org.apache.logging.log4j.core.async.AsyncLoggerConfigDisruptor$Log4jEventWrapperHandler.onEvent(AsyncLoggerConfigDisruptor.java:111)at org.apache.logging.log4j.core.async.AsyncLoggerConfigDisruptor$Log4jEventWrapperHandler.onEvent(AsyncLoggerConfigDisruptor.java:97)at com.lmax.disruptor.BatchEventProcessor.processEvents(BatchEventProcessor.java:168)at com.lmax.disruptor.BatchEventProcessor.run(BatchEventProcessor.java:125)at java.lang.Thread.run(java.base@17-loom/Thread.java:1521)

log4j2 调用了很多次 flush, JFR 显示采集到的事件,每次都调用了 flush。每次调用 flush,都会造成文件真正写的 native 调用。而消费慢的原因,其实就是这种 native 调用太多,系统写入不过来了。

问题解决

我们可以通过以下四个方向解决这个问题:

  1. 减少日志输出,精简日志,这是比较基础的方式,也是比较简单的方式,但是身为一个技术人,我们不能满足于此
  2. 增加硬盘 io,这个也是比较基础简单的解决方式
  3. 我们是否可以减少这个 flush 呢?答案是可以的,我们可以配置 Appender 的**immediateFlush**为 false
  4. 增加监控,针对堆栈包含 org.apache.logging.log4j.core.async.AsyncLoggerConfigDisruptor.enqueue 的 java monitor block 事件进行监控,如果发现时间过长或者数量很多的事件则报警或者重建进程

1. 配置 Appender 的 immediateFlush 为 false

我们可以配置 Appender 的 immediateFlush 为 false,例如:

<RollingFile name="file" append="true"filePattern="./app.log-%d{yyyy.MM.dd.HH}"immediateFlush="false"><PatternLayout pattern="${logFormat}"/><Policies><TimeBasedTriggeringPolicy interval="1" modulate="true"/></Policies><DirectWriteRolloverStrategy maxFiles="72"/>
</RollingFile>

这里的原理对应源码:

AbstractOutputStreamAppender.java
protected void directEncodeEvent(final LogEvent event) {getLayout().encode(event, manager);//如果配置了 immdiateFlush (默认为 true)或者当前事件是 EndOfBatchif (this.immediateFlush || event.isEndOfBatch()) {manager.flush();}
}

那么对于 Log4j2 Disruptor 异步日志来说,什么时候 LogEventEndOfBatch 呢?是在消费到的 index 等于生产发布到的最大 index 的时候,这也是比较符合性能设计考虑,即在没有消费完的时候,尽可能地不 flush,消费完当前所有的时候再去 flush:

BatchEventProcessor.java
private void processEvents()
{T event = null;long nextSequence = sequence.get() + 1L;while (true){try{final long availableSequence = sequenceBarrier.waitFor(nextSequence);if (batchStartAware != null){batchStartAware.onBatchStart(availableSequence - nextSequence + 1);}while (nextSequence <= availableSequence){event = dataProvider.get(nextSequence);//这里 nextSequence == availableSequence 就是 EndOfBatcheventHandler.onEvent(event, nextSequence, nextSequence == availableSequence);nextSequence++;}sequence.set(availableSequence);}catch (final TimeoutException e){notifyTimeout(sequence.get());}catch (final AlertException ex){if (running.get() != RUNNING){break;}}catch (final Throwable ex){exceptionHandler.handleEventException(ex, nextSequence, event);sequence.set(nextSequence);nextSequence++;}}
}

2. 增加基于 JFR 事件监控

这个需要 Java 14 以上的版本

Configuration config = Configuration.getConfiguration("default");
//设置监控的锁 block 时间超过多少就会采集
config.getSettings().put("jdk.JavaMonitorEnter#threshold", "1s");
try (var es = new RecordingStream(config)) {es.onEvent("jdk.JavaMonitorEnter", recordedEvent -> {//如果堆栈包含我们关注的,则报警if (recordedEvent.getStackTrace().getFrames().stream().anyMatch(recordedFrame -> recordedFrame.toString().contains("org.apache.logging.log4j.core.async.AsyncLoggerConfigDisruptor.enqueue")))  {System.out.println("Alarm: " + recordedEvent);}});es.start();
}

一次Log4j2日志输出阻塞问题的定位相关推荐

  1. 一次鞭辟入里的 Log4j2 异步日志输出阻塞问题的定位

    一次鞭辟入里的 Log4j2 日志输出阻塞问题的定位 问题现象 线上某个应用的某个实例突然出现某些次请求服务响应极慢的情况,有几次请求超过 60s 才返回,并且通过日志发现,服务线程并没有做什么很重的 ...

  2. log4j2日志输出到控制台-Maven工程

    log4j2简介: Apache Log4j 2 is an upgrade to Log4j that provides significant improvements over its pred ...

  3. Spring Boot系列教程六:日志输出配置log4j2

    一.前言  spring boot支持的日志框架有,logback,Log4j2,Log4j和Java Util  Logging,默认使用的是logback日志框架,笔者一直在使用log4j2,并且 ...

  4. Spring Boot 使用 Log4j2 Logback 输出日志到 EKL

    文章目录 1.ELK 介绍 2.环境.软件准备 3.ELK 环境搭建 4.Spring Boot 配置示例 4.1.Log4j2 方式配置 4.2.Logback 方式配置 1.ELK 介绍 ELK ...

  5. 使用log4j2,控制台可以正常输出日志,但是log4j2日志文件为空?

    Log4j-2-does-not-write-to-file.md 问题: 使用log4j2, 控制台可以正常输出日志,但是log4j2日志文件为空? 配置文件log4j2.xml配置如下: < ...

  6. log4j2 在开发环境(win)和测试环境(win)下均可以输出日志,但在生产环境(linux)无日志输出

    一.问题说明:在开发环境和测试环境下均可以输出日志,但在生产环境无日志输出. 二.发现问题:          1,系统不同:         开发环境和测试环境(windows)         生 ...

  7. springboot的log4j2日志不打印,且控制台不输出!!!

    不打印日志,并且连简单的nullporint等异常都不输出!!! 不报错 问题出在哪呢?  碰到这种问题  . 检查日志的配置文件 具体配置      ,可自行百度...... 情况1: 在配置都正确 ...

  8. 在被线上大量日志输出导致性能瓶颈毒打了很多次之后总结出的经验

    由于线上业务量级比较大(日请求上亿,日活用户几十万),同时业务涉及逻辑很复杂,线上日志级别我们采用的是 info 级别,导致线上日志量非常庞大,经常遇到因为日志写入太慢导致的性能瓶颈(各微服务每小时日 ...

  9. 近期业务大量突增微服务性能优化总结-2.开发日志输出异常堆栈的过滤插件

    最近,业务增长的很迅猛,对于我们后台这块也是一个不小的挑战,这次遇到的核心业务接口的性能瓶颈,并不是单独的一个问题导致的,而是几个问题揉在一起:我们解决一个之后,发上线,之后发现还有另一个的性能瓶颈问 ...

最新文章

  1. 手动删除EXCHANGE2010
  2. node 使用 download-git-repo 下载 github 代码
  3. 动态设置 GridView Web 服务器控件列宽
  4. ANSYS Products 19.1 安装教程
  5. Net 消息中间件 知识小结
  6. 如何从eclipse迁移到idea
  7. 华为主题锁屏壁纸换不掉_快来看看华为与荣耀手机的这16款主题!别一直用系统默认主题啦!...
  8. linux内核的I2C子系统详解5——i2c_driver的注册、i2c_client的来源
  9. vue-router配置介绍和使用方法(一)
  10. 现代软件工程—构建之法---第三章:练习与讨论
  11. iOS 图片拉伸、拉伸两端保留中间
  12. 用计算机字符写名字,NSIS:把计算机名转换为大写或小写字符
  13. 禅道项目管理工具环境搭建
  14. echar2 ,3 自定义右键菜单 右击事件
  15. SpringBoot笔记(五)Linux系统与项目部署
  16. Eclipse Mars2中Augular2开发环境的搭建过程记录
  17. concurrent包
  18. LINUX内核编译(ZT)
  19. break在java语言中什么意思_在Java语言的控制结构中,break语句是经常用到的语句。在switch语句中break语句的作用是( )_学小易找答案...
  20. C# Math.Abs()取“绝对值”方法使用过程中需要注意的地方

热门文章

  1. Python学习笔记(一):编程和Python
  2. 计算机控制器输入设备的功能是,计算机5大部件之一的控制器的作用是什么?...
  3. .net5 查询Access数据库数据
  4. 字典树c语言,字典树的应用 单词意义查找-C语言实现
  5. python conda无法安装包 报错An HTTP error occurred when trying to retrieve this URL.
  6. SpringBoot后台搭建-创建restful接口,使用mybatisPlus实现分页
  7. 一个女程序员的奋斗经历
  8. Hdoj 2188.悼念512汶川大地震遇难同胞——选拔志愿者 题解
  9. 使用TexturePacker命令行的一个坑
  10. 安全修复之Web——Spring Framework 远程代码执行漏洞