0%

Maven

  • 插件编写

  • 构建模板

phase and goal

Introduction to the Build Lifecycle

一个lifecycle有多个phase,一个phase可以有多个goal

Mojo

三种内置的lifecycle:default、clean和site

列出所有的plugin、phase、id、goal

1
mvn fr.jcgay.maven.plugins:buildplan-maven-plugin:list
1
2
# 打印插件的所有命令详情
mvn help:describe -Dplugin=javafx -Ddetail
1
2
3
4
5
6
7
8
9
10
11
12
13
mvn clean install -DskipTests -Dfast -Drat.skip=true -Dhaoop.version=2.6.0-cdh5.15.1 -Pvendor-repos -Dinclude-hadoop -Dscala-2.11 -T2C

# -Dfast #在flink根目录下pom.xml文件中fast配置项目中含快速设置,其中包含了多项构建时的跳过参数. #例如apache的文件头(rat)合法校验,代码风格检查,javadoc生成的跳过等,详细可阅读pom.xml
# install maven的安装命令
# -T2C #支持多处理器或者处理器核数参数,加快构建速度,推荐Maven3.3及以上
# -Pinclude-hadoop 将hadoop的 jar包,打入到lib/中
# -Pvendor-repos # 如果需要指定hadoop的发行商,如CDH,需要使用-Pvendor-repos
# -Dscala-2.11 # 指定scala的版本为2.11
# -Dhadoop.version=2.6.0-cdh5.15.1 指定 hadoop 的版本

————————————————
版权声明:本文为CSDN博主「实力不允许偷懒」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_17310871/article/details/106677165
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<version>2.2.5</version>
<executions>
<execution>
<id>get-the-git-infos</id>
<!-- 默认绑定阶段initialize -->
<phase>initialize</phase>
<goals>
<!-- 目标:revision -->
<goal>revision</goal>
</goals>
</execution>
</executions>
<configuration>
<!-- 检查的仓库根目录,${project.basedir}:项目根目录,即包含pom.xml文件的目录 -->
<dotGitDirectory>${project.basedir}/.git</dotGitDirectory>
<!-- false:扫描路径时不打印更多信息,默认值false,可以不配置 -->
<verbose>false</verbose>
<!-- 定义插件中所有时间格式,默认值:yyyy-MM-dd’T’HH:mm:ssZ -->
<dateFormat>yyyy-MM-dd HH:mm:ss</dateFormat>
<!-- git属性文件中各属性前缀,默认值git,可以不配置 -->
<prefix>git</prefix>
<!-- 生成git属性文件,默认false:不生成 -->
<generateGitPropertiesFile>true</generateGitPropertiesFile>
<!-- 生成git属性文件路径及文件名,默认${project.build.outputDirectory}/git.properties -->
<generateGitPropertiesFilename>${project.build.outputDirectory}/conf/git.properties
</generateGitPropertiesFilename>
<!-- 生成git属性文件格式,默认值properties -->
<format>json</format>
<!-- 配置git-describe命令 -->
<gitDescribe>
<skip>false</skip>
<always>false</always>
<dirty>-dirty</dirty>
</gitDescribe>
</configuration>
</plugin>

shade

  1. 打包文件增加时间戳,并指定时间戳格式
  2. 排除代码路径与排除文件路径, 排除整个目录需要增加**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<maven.build.timestamp.format>yyyyMMdd-HHmmss</maven.build.timestamp.format>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<id>shade</id>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>

<configuration>
<finalName>
${project.artifactId}-${project.version}-${maven.build.timestamp}-shaded
</finalName>
<!-- <shadeTestJar>true</shadeTestJar>-->
<!-- <shadedArtifactAttached>false</shadedArtifactAttached>-->
<createDependencyReducedPom>true</createDependencyReducedPom>
<transformers combine.children="append">
<!-- The service transformer is needed to merge META-INF/services files -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
<transformer implementation="org.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer" />

</transformers>
<minimizeJar>true</minimizeJar>
<shadeTestJar>false</shadeTestJar>
<shadedArtifactAttached>false</shadedArtifactAttached>
<createDependencyReducedPom>false</createDependencyReducedPom>
<!-- <dependencyReducedPomLocation>-->
<!-- ${project.basedir}/target/dependency-reduced-pom.xml-->
<!-- </dependencyReducedPomLocation>-->

<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>log4j.properties</exclude>
<exclude>version-info.properties</exclude>
<exclude>org/slf4j/**</exclude>
<exclude>flake/**</exclude>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
<exclude>org/apache/commons/**</exclude>
</excludes>
</filter>
</filters>
<relocations>
<relocation>
<pattern>com.tencent.oceanus.common</pattern>
<shadedPattern>shaded.flake.com.tencent.oceanus.common
</shadedPattern>
</relocation>
<relocation>
<pattern>com.tencent.oceanus.util</pattern>
<shadedPattern>shaded.flake.com.tencent.oceanus.util
</shadedPattern>
</relocation>
<relocation>
<pattern>com.tencent.oceanus.exception</pattern>
<shadedPattern>shaded.flake.com.tencent.oceanus.exception
</shadedPattern>
</relocation>
<relocation>
<pattern>com.tencent.oceanus.dto</pattern>
<shadedPattern>shaded.flake.com.tencent.oceanus.dto
</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
<configuration>
<!-- <createSourcesJar>true</createSourcesJar>-->
</configuration>
</plugin>

发布test jar

对那些有着良好设计,能够重复使用在项目的不同模块中、甚至不同项目中的测试代码,也需要打包成构件重复使用,从而减少编写测试代码的工作量。而 mvn package 只会对主代码和资源文件进行打包安装与部署,对测试代码和资源文件是不会处理的

发布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<executions>
<execution>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

使用

maven-jar-plugin 有两个目标:一个是 jar;另一个是 test-jar。
jar 目标有内置绑定到 Maven 的 default 生命周期的 package 阶段,会在 Maven 工程进行构建的时候自动执行,将项目的主代码和资源文件进行打包。

test-jar 目标没有内置绑定,所以需要用户在插件配置中声明该目标,从而达到在 Maven 工程构建的时候将测试代码和资源文件打包。
test-jar 目标是默认绑定到 default 生命周期的 package 阶段,
所以当运行 mvn clean package 命令的时候,能同时将主代码和测试代码分别打包。

1
2
3
4
5
6
7
8
<dependency>
<groupId>com.dalong</groupId>
<artifactId>testpacakge</artifactId>
<version>1.0-SNAPSHOT</version>
<classifier>tests</classifier>
<type>test-jar</type>
<scope>test</scope>
</dependency>

module-dependency-tree

https://github.com/ferstl/depgraph-maven-plugin
https://ferstl.github.io/depgraph-maven-plugin/plugin-info.html

https://ferstl.github.io/depgraph-maven-plugin/graph-mojo.html#graphFormat

1
2
3
4
mvn com.github.ferstl:depgraph-maven-plugin:3.0.1:aggregate \
-DcreateImage=true \
-DreduceEdges=false \
-Dscope=compile \

archetype

resources-filtered

在 Maven 项目中,resources-filtered 是一个配置选项,用于指示 Maven 是否应该对资源文件(如 *.properties 或 *.xml 文件)中的变量进行过滤和替换

资源过滤的作用

资源过滤的主要目的是允许开发者在构建过程中动态地替换资源文件中的占位符(例如 ${project.version})为实际的值。这在以下场景中非常有用:

  1. 版本管理:可以在资源文件中使用 ${project.version} 占位符,Maven 会在构建时自动将其替换为项目的实际版本号。
  2. 环境特定配置:可以为不同的部署环境(如开发、测试、生产)创建不同的配置文件,并在构建时选择合适的文件进行替换。
  3. 避免硬编码:通过使用占位符而不是硬编码的值,可以使项目更加灵活和可维护。

如何启用资源过滤

在 Maven 的 pom.xml 文件中,可以通过以下方式启用资源过滤:

1
2
3
4
5
6
7
8
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>

在这个例子中,<filtering> 元素被设置为 true,表示 Maven 应该对 src/main/resources 目录下的所有资源文件进行过滤。

注意事项

  • 资源过滤可能会导致构建过程中的性能开销,特别是在处理大量资源文件时。
  • 如果资源文件中的某些内容不应该被过滤(例如,包含敏感信息的密码字段),可以使用 Maven 的 delimiters 配置来指定自定义的分隔符,以避免不必要的替换。

样例

可以替换的属性,包含maven properties和git属性

1
2
3
4
5
6
7
project.version=${project.version}  
scala.binary.version=${scala.binary.version}

git.commit.id=${git.commit.id}
git.commit.id.abbrev=${git.commit.id.abbrev}
git.commit.time=${git.commit.time}
git.build.time=${git.build.time}

logback

configuration

logback_configuration_01

appender

类名 描述
ConsoleAppender 将日志通过 System.out 或者 System.err 来进行输出,即输出到控制台。
FileAppender 将日志输出到文件中。
RollingFileAppender 继承自 FileAppender,也是将日志输出到文件,但文件具有轮转功能。
DBAppender 将日志输出到数据库
SocketAppender 将日志以明文方式输出到远程机器
SSLSocketAppender 将日志以加密方式输出到远程机器
SMTPAppender 将日志输出到邮件

Filter

image-20211026212759209

EvaluatorFilter

1
2
3
4
5
6
<!-- groovy -->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>3.0.0-rc-3</version>
</dependency>

GEventEvaluator

ch.qos.logback.classic.boolex.GEventEvaluator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<configuration scan="true" scanPeriod="10 seconds" debug="true">
<!-- 定义变量 -->
<property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<target>system.err</target>
<encoder charset="utf-8">
<pattern>${LOG_PATTERN}</pattern>
</encoder>
<!-- 设置过滤器 -->
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator class="ch.qos.logback.classic.boolex.GEventEvaluator">
<expression>
e.level.toInt() >= ERROR.toInt() &amp;&amp;
!(e.mdc?.get("req.userAgent") =~ /Googlebot|msnbot|Yahoo/ )
</expression>
</evaluator>
<OnMismatch>DENY</OnMismatch>
<OnMatch>NEUTRAL</OnMatch>
</filter>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>

JaninoEventEvalutor

JaninoEventEvaluator

Logback-classic 附带了另一个名为JaninoEventEvaluator的具体EventEvaluator实现,它采用任意 Java 语言块返回布尔值作为评估标准。我们将此类 Java 语言布尔表达式称为“ * evaluation expressions *”。评估表达式为事件过滤提供了极大的灵 Active。 JaninoEventEvaluator要求Janino library。请参阅安装文档的corresponding section。与JaninoEventEvaluator相比,借助 Groovy 语言,GEventEvaluator更加方便使用,但是JaninoEventEvaluator通常对于等效表达式运行(快得多)。

评估表达式是在配置文件的解释过程中即时编译的。作为用户,您不必担心实际的管道问题。但是,您有责任确保 Java 语言表达式返回一个布尔值,即它的计算结果为 true 或 false。

评估表达式是在当前日志记录事件上评估的。 Logback-classic 自动将日志记录事件的各个字段导出为可从评估表达式访问的变量。下面列出了这些导出变量的区分大小写的名称。

Name Type Description
event LoggingEvent 与日志记录请求关联的原始日志记录事件。事件中还提供以下所有变量。例如,event.getMessage()返回与下面描述的* message *变量相同的 String 值。
message String 日志记录请求的原始消息。对于某些 Logger* l *,当您编写 l.info(“ Hello{}”,name);其中为名称分配了值“ Alice”,则消息为“ Hello{}”。
formattedMessage String 日志记录请求中的格式化消息。对于某些 Logger* l *,当您编写 l.info(“ Hello{}”,name);其中为名称分配了值“ Alice”,则“ Hello Alice”是格式化的消息。
logger String Logger 的名称。
loggerContext LoggerContextVO 记录事件所属的 Logger 上下文的受限(值对象)视图。
level int 对应于级别的 int 值。为了帮助轻松创建涉及级别的表达式,还提供了默认值* DEBUG INFO WARN ERROR 。因此,使用 level> INFO *是正确的表达式。
timeStamp long 与记录事件的创建相对应的时间戳。
marker Marker 与日志记录请求关联的Marker对象。请注意,标记可以为 null,您有责任检查此条件以避免NullPointerException
mdc Map 在创建日志记录事件时包含所有 MDC 值的 Map。可以使用以下表达式访问值:* mdc.get(“ myKey”)*。从经典 logback 版本 0.9.30 开始,“ mdc”变量将永远不会为 null。java.util.Map类型是非参数化的,因为 Janino 不支持泛型。因此,由mdc.get()返回的类型是Object而不是String。要对返回的值调用String方法,必须将其强制转换为String。例如((String) mdc.get("k")).contains("val")
throwable java.lang.Throwable 如果没有异常与事件关联,则“ throwable”变量的值将为 null。不幸的是,“ throwable”无法在序列化中幸免。因此,在远程系统上,其值将始终为 null。对于位置无关的表达式,请使用下面描述的throwableProxy变量。
throwableProxy IThrowableProxy 与日志记录事件关联的异常的代理。如果没有异常与事件关联,则“ throwableProxy”变量的值将为 null。与“ throwable”相反,当异常与事件相关联时,即使在远程系统上(即在序列化之后),“ throwableProxy”的值也不会为空。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<configuration scan="true" scanPeriod="10 seconds" debug="true">
<!-- 定义变量 -->
<property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<target>system.err</target>
<encoder charset="utf-8">
<pattern>${LOG_PATTERN}</pattern>
</encoder>
<!-- 设置过滤器 -->
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator> <!-- defaults to type ch.qos.logback.classic.boolex.JaninoEventEvaluator -->
<expression>return message.contains("ERROR");</expression>
</evaluator>
<OnMismatch>DENY</OnMismatch>
<OnMatch>NEUTRAL</OnMatch>
</filter>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>

源码解析

初始化

slf4j 通过 StaticLoggerBinder 类与具体日志实现进行关联,从而实现门面模式

加载StaticLoggerBinder

image-20211026205834511

LoggerFactory.performInitialization(),会执行初始化,所谓的初始化就是查找 StaticLoggerBinder 这个类是不是存在,如果存在会将该类绑定到当前应用,同时,根据不同情况修改INITIALIZATION_STATE。代码比较多,我概括下执行的步骤:

  1. 如果 StaticLoggerBinder 存在且唯一,修改初始化状态为 SUCCESSFUL_INITIALIZATION;
  2. 如果 StaticLoggerBinder 存在但为多个,由 JVM 决定绑定哪个 StaticLoggerBinder,修改初始化状态为 SUCCESSFUL_INITIALIZATION,同时,会在控制台打印存在哪几个 StaticLoggerBinder,并提醒用户最终选择了哪一个 ;
  3. 如果 StaticLoggerBinder 不存在,打印提醒,并修改初始化状态为 NOP_FALLBACK_INITIALIZATION;
  4. 如果 StaticLoggerBinder 存在但 getSingleton() 方法不存在,打印提醒,并修改初始化状态为 FAILED_INITIALIZATION;

加载配置

支持采用 xml、grovy 和 SPI 的方式配置文件

Joran(一个成熟的,灵活的并且强大的配置框架 )

logback_joran

获取logger

LoggerContext.getLogger(String)

JaninoEventEvaluator

https://github.com/qos-ch/logback/blob/master/logback-core/src/main/java/ch/qos/logback/core/boolex/JaninoEventEvaluatorBase.java

minLog

最小开销的Java日志


overview

MinLog: 一个很小的Java日志库:

零开销
低于给定级别的日志记录在编译时被javac自动删除。这意味着应用程序可以有详细的跟踪和调试日志,而不会对最终产品产生任何影响。

极致轻薄
整个项目由一个Java文件和大约100行非注释代码组成。
简单有效 简单的API,高效的运行时。

minlog-slf4j适配slf4j的替代实现.


Usage:

1
2
3
<groupId>com.esotericsoftware</groupId>
<artifactId>minlog</artifactId>
<version>1.3.2-SNAPSHOT</version>

Usage

Messages are logged using static methods:

Log.info("Some message.");  
Log.debug("Error reading file: " + file, ex);  

A static import can be used to make the logging more concise:

import static com.esotericsoftware.minlog.Log.*;  
// ...  
info("Some message.");  
debug("Error reading file: " + file, ex);  

While optional, for brevity the rest of this documentation assumes this static import is in place.

If log statements from different libraries or areas of an application need to be differentiated, a category can be specified as the first argument:

info("some lib", "Some message.");  
debug("some lib", "Error reading file: " + file, ex);  

Log level

Setting the level will log that level, as well as all higher levels. There are multiple ways to set the current level:

Log.set(LEVEL_INFO);  
Log.INFO();  
INFO();  

The levels are:

  • NONE disables all logging.
  • ERROR is for critical errors. The application may no longer work correctly.
  • WARN is for important warnings. The application will continue to work correctly.
  • INFO is for informative messages. Typically used for deployment.
  • DEBUG is for debug messages. This level is useful during development.
  • TRACE is for trace messages. A lot of information is logged, so this level is usually only needed when debugging a problem.

Conditional logging

If a logging method below the current level is called, it will return without logging the message. In order to avoid string concatenation, the current log level can be checked before the message is logged:

if (ERROR) error("Error reading file: " + file, ex);  
if (TRACE) {  
   StringBuilder builder = new StringBuilder();  
   // Do work, append to the builder.  
   trace(builder);  
}  

Fixed logging levels

MinLog users can choose from the regular “minlog.jar” or from from a JAR file like “minlog-info.jar” which has a fixed logging level that cannot be changed at runtime. When a fixed level JAR is used, code that changes the level will have no affect. During compilation, any conditional logging statements below the fixed level will be automatically removed by the Java compiler. This means they will have absolutely no impact on the application.

Output customization

The default logger outputs messages in this format:

time level: [category] message  

Where “time” is the time elapsed since the application started. For example:

00:00 TRACE: [kryo] Wrote string: moo  
00:00 TRACE: [kryo] Wrote object: NonNullTestClass  
00:01 TRACE: [kryo] Wrote string: this is some data  
00:01 TRACE: [kryo] Compressed to 7.97% using: DeflateCompressor  
00:12 TRACE: [kryo] Decompressed using: DeflateCompressor  
00:12 TRACE: [kryo] Read string: this is some data  

The output can be customized:

static public class MyLogger extends Logger {  
   public void log (int level, String category, String message, Throwable ex) {  
	  StringBuilder builder = new StringBuilder(256);  
	  builder.append(new Date());  
	  builder.append(' ');  
	  builder.append(level);  
	  builder.append('[');  
	  builder.append(category);  
	  builder.append("] ");  
	  builder.append(message);  
	  if (ex != null) {  
		 StringWriter writer = new StringWriter(256);  
		 ex.printStackTrace(new PrintWriter(writer));  
		 builder.append('\n');  
		 builder.append(writer.toString().trim());  
	  }  
	  System.out.println(builder);  
   }  
}    // ...  
Log.setLogger(new MyLogger());  

Using this mechanism, log messages can be filtered (eg, by category), written to a file, etc.

MAT

为了演示MAT的使用方法,本文采用jamp生成了一个Java继承的dump文件。

4.1 Overview选项

当成功启动MAT后,通过菜单选项“File->Open heap dump…”打开指定的dump文件后,将会生成Overview选项,如下所示:

在Overview选项中,以饼状图的形式列举出了程序内存消耗的一些基本信息,其中每一种不同颜色的饼块都代表了不同比例的内存消耗情况。

4.2 Dominator Tree

如果说需要定位内存泄露的代码点,我们可以通过Dominator Tree菜单选项来进行排查。Dominator Tree提供了一个列表。
Dominator Tree:对象之间dominator关系树。如果从GC Root到达Y的的所有path都经过X,那么我们称X dominates Y,或者X是Y的Dominator 。
Dominator Tree由系统中复杂的对象图计算而来。从MAT的dominator tree中可以看到占用内存最大的对象以及每个对象的dominator,如下所示:

点开“+”符号,可以进一步查看内层应用情况,同时还可以看到对应类对象的属性值,如下所示:

4.3 Histogram选项

进一步,可以通过Histogram分析,Histogram列出了每个类的实例数量,点击Action下的Histogram,得到以下结果:

如果需要查询特性的某个类,我们可以在第一行输入类名或者关键词进行正则匹配查找,如查找“netty”:

可以看出,查找“netty”输出的结果列表是无序的,如果匹配到的结果很多,查找起来比较困难,因此,我们可以对结果进行排序:选中结果列表的任意一行,鼠标右键-》Colums->Sort By->如Class Name,结果如下:

当我们找到疑似存在泄漏的类之后,我们可以进行进一步分析。比较重要的一点,选中疑似类,右键出来选中List Objects,得到的结果再右键选中"Paths to GC Roots",我们可以通过它快速找到GC ROOT,如果存在GC ROOT,它就不会被回收。

4.4 Path to GC Roots

查看一个对象到RC Roots的引用链  

通常在排查内存泄漏的时候,我们会选择exclude all phantom/weak/soft etc.references,意思是查看排除虚引用/弱引用/软引用等的引用链,因为被虚引用/弱引用/软引用的对象可以直接被GC给回收,我们要看的就是某个对象否还存在Strong 引用链(在导出HeapDump之前要手动出发GC来保证),如果有,则说明存在内存泄漏,然后再去排查具体引用。 

其它重要选项:

1. List objects :
with incoming references 引用到该对象的对象
with outcoming references 被该对象引用的对象

2. Show objects by class :
incoming references 引用到该对象的对象
outcoming references 被该对象引用的对象

4.5 OQL(Object Query Language)

类似SQL查询语言
Classes:Table
Objects:Rows
Fileds: Cols

select * from com.example.mat.Listener

查找size=0并且未使用过的ArrayList
select * from java.util.ArrayList where size=0 and modCount=0

查找所有的Activity
select * from instanceof android.app.Activity

4.6 利用Histogram和Dominator Tree分析内存泄露

在分析内存泄露时,必须要掌握粒度,所谓粒度就是你此刻dump的hprof文件究竟是分析谁的泄露,如果你在开始前心中没有个目标,最后取出来的hprof也分析不出什么原因。粒度越小,对你分析问题也就越有利,当你把一个个小粒度问题解决后,整个App的泄露就迎刃而解了。也许这么说,大家心中有点迷糊。下面就举例来说吧:

假如现在有个项目包含Module几十个,每个Module包含的Activity数以百计,现在让你分析它是否内存泄露,如果你只是胡乱抓个hprof根本分析不出什么。假如你就针对某个Activity分析这样问题就简单多了。比如你现在分析ActivityA的内存泄露问题,你可以参考如下步骤:

Step1:进入ActivityA之前,你先dump个hprof文件HprofA;

Step2:进入ActivityA操作一会,再退出ActivityA后dump个hprof文件HprofB;

Step3:采用Histogram和Dominator Tree对比分析这两个Hprof文件,即可得出ActivityA是否泄露

现在以分析TestActivity为例,按上述步骤实战分析,先抓取进入TestActivity前后的hprof文件,按如下步骤对比两个hprof的异同,如下图1,2:

图1 选择所需比较的hprof

图2 比较两个hprof

正如图2所示,易知在执行进出TestActivity后,多出了个TestActivity对象,按理论上来说在进入Activity后会创建个Activity,但是按Back键返回后这个Activity就会被销毁进而从Task栈上被移除,也就是说这个操作前后不应该会多出个Activity,因此可以断定TestActivity存在泄漏。

TestActivity存在泄漏,那我们应该怎么解决呢?因此我们就需要找到为何泄漏,为什么本该销毁的Activity却没有被销毁?如知真相如何,请看下图3-4

图3 获取TestActivity的Reference chain

图4 TestActivity的引用关系

从图4易知TestActivity没有被释放就是因为GC Root(TestActivity$1)引用着TestActivity,到此原因也一目了然。找到了只是开始,解决才是关键。这时让我们查看下TestActivity代码:  
1
public class TestActivity extends Activity {       private static final Object mLock = new Object();     @Override    protected void onCreate(Bundle savedInstanceState) {                super.onCreate(savedInstanceState);        DebugUtil.StrictModeDebug();        setContentView(R.layout.test_main);           new Thread(){//匿名线程            public void run() {                synchronized (mLock) {                    try {                        mLock.wait();                    } catch (InterruptedException e) {                        // TODO Auto-generated catch block                        e.printStackTrace();                    }                }            }        }.start();    }}
从代码上可以发现TestActivity里存在个匿名线程,且一直处于等待状态,直到退出TestActivity仍未被唤醒,进而导致该线程就一直没有结束,它所持有的TestActivity也就无法被释放了(可能大家听到此处会很疑惑,线程没有结束可以理解,但是它并没有持有TestActivity呀?我只能说是隐含this,如还不明白,请自行参阅java内部类相关内容),如要解决此泄露,只需在Activity的onDestory里将线程唤醒让其可以正常结束就OK了。

优化建议

  1. 使用线程时,一定要确保线程在周期性对象(如Activity)销毁时能正常结束,如能正常结束,但是Activity销毁后还需执行一段时间,也可能造成泄露,此时可采用WeakReference方法来解决,另外在使用Handler的时候,如存在Delay操作,也可以采用WeakReference;
  2. 使用Handler + HandlerThread时,记住在周期性对象销毁时调用looper.quit()方法;
  3. 建议少使用匿名类或内部类,可考虑使用嵌套类(带static那种类),减少对周期性对象的隐性持有;

栈上分配与TLAB

逃逸分析(Escape Analysis)

栈上分配

针对那些作用域不会逃逸出方法的对象,在分配内存时不再将对象分配在堆内存中,而是将对象属性打散后分配在栈(线程私有的,属于栈内存)上,这样,随着方法的调用结束,栈空间的回收就会随着将栈上分配的打散后的对象回收掉,不再给gc增加额外的无用负担,从而提升应用程序整体的性能

优点:

    1)可以在函数调用结束后自行销毁对象,不需要垃圾回收器的介入,有效避免垃圾回收带来的负面影响

    2)栈上分配速度快,提高系统性能

线程私有变量,大对象虚拟机会分配到TLAB中,TLAB(Thread Local Allocation Buffer)要不要了解下?

在栈上分配该对象的内存,当栈帧从Java虚拟机栈中弹出,就自动销毁这个对象。减小垃圾回收器压力。

TLAB

TLAB全称ThreadLocalAllocBuffer,是线程的一块私有内存,如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个Buffer,如果需要分配内存,就在自己的Buffer上分配,这样就不存在竞争的情况,可以大大提升分配效率,当Buffer容量不够的时候,再重新从Eden区域申请一块继续使用,这个申请动作还是需要原子操作的。

TLAB的目的是在为新对象分配内存空间时,让每个Java应用线程能使用自己专属的分配指针来分配空间,均摊对GC堆(eden区)里共享的分配指针做更新而带来的同步开销。

TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB,而在老TLAB里的对象还留在原地什么都不用管——它们无法感知自己是否是曾经从TLAB分配出来的,而只关心自己是在eden里分配的。

启动参数 JVM内存分配模式 Eden区 YoungGC 耗时
-XX:+DoEscapeAnalysis(开逃逸分析)-XX:+UseTLAB (开启TLAB) 虚拟机栈上分配模式(小对象) 较少使用 较少使用 很低
-XX:-DoEscapeAnalysis(关闭逃逸分析)-XX:+UseTLAB(开启TLAB) TLAB区分配模式 大量使用 大量使用 较高
-XX:-DoEscapeAnalysis(关闭逃逸分析)-XX:-UseTLAB(关闭TLAB) Eden区分配模式 大量使用 大量使用 特别高

在学习Java的过程中,一般认为new出来的对象都是被分配在堆上的,其实这个结论不完全正确,因为是大部分new出来的对象被分配在堆上,而不是全部。通过对Java对象分配的过程分析,可以知道有另外两个地方也是可以存放对象的。这两个地方分别栈 (涉及逃逸分析相关知识)和TLAB(Thread Local Allocation Buffer)。我们首先对这两者进行介绍,而后对Java对象分配过程进行介绍。

栈上分配

在JVM中,堆是线程共享的,因此堆上的对象对于各个线程都是共享和可见的,只要持有对象的引用,就可以访问堆中存储的对象数据。虚拟机的垃圾收集系统可以回收堆中不再使用的对象,但对于垃圾收集器来说,无论筛选可回收对象,还是回收和整理内存都需要耗费时间。

如果确定一个对象的作用域不会逃逸出方法之外,那可以将这个对象分配在栈上,这样,对象所占用的内存空间就可以随栈帧出栈而销毁。在一般应用中,不会逃逸的局部对象所占的比例很大,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,无须通过垃圾收集器回收,可以减小垃圾收集器的负载。

JVM允许将线程私有的对象打散分配在栈上,而不是分配在堆上。分配在栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统性能。
栈上分配的技术基础:
**一是逃逸分析:**逃逸分析的目的是判断对象的作用域是否有可能逃逸出函数体。关于逃逸分析的问题可以看我另一篇文章:

**二是标量替换:**允许将对象打散分配在栈上,比如若一个对象拥有两个字段,会将这两个字段视作局部变量进行分配。

只能在server模式下才能启用逃逸分析,参数-XX:DoEscapeAnalysis启用逃逸分析,参数-XX:+EliminateAllocations开启标量替换(默认打开)。Java SE 6u23版本之后,HotSpot中默认就开启了逃逸分析,可以通过选项-XX:+PrintEscapeAnalysis查看逃逸分析的筛选结果。

TLAB

TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。
由于对象一般会分配在堆上,而堆是全局共享的。因此在同一时间,可能会有多个线程在堆上申请空间。因此,每次对象分配都必须要进行同步(虚拟机采用CAS配上失败重试的方式保证更新操作的原子性),而在竞争激烈的场合分配的效率又会进一步下降。JVM使用TLAB来避免多线程冲突,在给对象分配内存时,每个线程使用自己的TLAB,这样可以避免线程同步,提高了对象分配的效率。

TLAB本身占用eEden区空间,在开启TLAB的情况下,虚拟机会为每个Java线程分配一块TLAB空间。参数-XX:+UseTLAB开启TLAB,默认是开启的。TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,当然可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。
由于TLAB空间一般不会很大,因此大对象无法在TLAB上进行分配,总是会直接分配在堆上。TLAB空间由于比较小,因此很容易装满。比如,一个100K的空间,已经使用了80KB,当需要再分配一个30KB的对象时,肯定就无能为力了。这时虚拟机会有两种选择,第一,废弃当前TLAB,这样就会浪费20KB空间;第二,将这30KB的对象直接分配在堆上,保留当前的TLAB,这样可以希望将来有小于20KB的对象分配请求可以直接使用这块空间。实际上虚拟机内部会维护一个叫作refill_waste的值,当请求对象大于refill_waste时,会选择在堆中分配,若小于该值,则会废弃当前TLAB,新建TLAB来分配对象。这个阈值可以使用TLABRefillWasteFraction来调整,它表示TLAB中允许产生这种浪费的比例。默认值为64,即表示使用约为1/64的TLAB空间作为refill_waste。默认情况下,TLAB和refill_waste都会在运行时不断调整的,使系统的运行状态达到最优。如果想要禁用自动调整TLAB的大小,可以使用-XX:-ResizeTLAB禁用ResizeTLAB,并使用-XX:TLABSize手工指定一个TLAB的大小。
-XX:+PrintTLAB可以跟踪TLAB的使用情况。一般不建议手工修改TLAB相关参数,推荐使用虚拟机默认行为。

对象内存分配的两种方法

为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

指针碰撞(Serial、ParNew等带Compact过程的收集器)
假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。
空闲列表(CMS这种基于Mark-Sweep算法的收集器)
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。   

总结

总体流程
这里写图片描述

对象分配流程
这里写图片描述
如果开启栈上分配,JVM会先进行栈上分配,如果没有开启栈上分配或则不符合条件的则会进行TLAB分配,如果TLAB分配不成功,再尝试在eden区分配,如果对象满足了直接进入老年代的条件,那就直接分配在老年代。

对象在内存的引用方式
这里写图片描述

对象在内存中的结构
这里写图片描述

参考

查看组

list_rsgroups

创建表

create ‘rtc_activity_dispatch_act_test’,{NAME => ‘i’, VERSIONS => 1 , TTL => 2592000 , BLOCKCACHE => true}

HFile创建:Spark、MapReduce、Flink

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
object CreateHfile {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("CreateHfile").setMaster(args(0))
val sc = new SparkContext(conf)
val hbaseConf = HBaseConfiguration.create()
//
val rdd = sc.textFile(args(1))
.flatMap(v =>{
val x = new javaList[String]()
for( a <- 1 to 9999){
x.add(v + "%04d".format(a))
}
x.toArray
}
)
.sortBy(v=>v.toString)
.map(r =>(
new ImmutableBytesWritable(Bytes.toBytes(r.toString)),
new KeyValue(
Bytes.toBytes(r.toString), Bytes.toBytes("phoneFamliy"), Bytes.toBytes("phoneCol"),
System.currentTimeMillis(),
KeyValue.Type.DeleteColumn)
))
rdd.saveAsNewAPIHadoopFile(args(2), classOf[ImmutableBytesWritable],classOf[KeyValue],classOf[HFileOutputFormat2], hbaseConf)
sc.stop()
}
}

Kafka Schema Registy

背景

读写Kafka的数据时,需要维护数据的序列化方式和Schema,每个topic有独立的schema,如何管理schema信息呢?

Confluent创建了Kafka Schema Registy项目

架构

img

向 kafka 发送数据时,需要先向 Schema Registry 注册 schema,然后序列化发送到 kafka 里。当我们需要从 kafka 消费数据时,也需要先从 Schema Registry 获取 schema,然后才能解析数据。

Schema的保存

Registry 服务端将数据格式存储到 Kafka 中,对应的 topic 名称为 _schemas。存储消息的格式如下:

  • Key 部分,包含数据格式名称,版本号,由 SchemaRegistryKey 类表示。
  • Value部分,包含数据格式名称,版本号, 数据格式 id 号,数据格式的内容,是否被删除, 由 SchemaRegistryValue 类表示。

Registry 服务端在存储Kafka之前,还会将上述的 Key 和 Value 序列化,目前序列化由两种方式:

  • json 序列化,由 ZkStringSerializer 类负责
  • 将 SchemaRegistryKey 或 SchemaRegistryValue 强制转换为 String 类型保存起来

处理请求

Registry 服务端主要负责两种请求,注册数据格式 schema 请求和 获取数据格式 schema 请求。

如果 Registry 服务端启动了高可用,说明有多个服务端在运行。如果注册 schema 请求发送给了 follower,那么 follower 会将请求转发给 leader。

读写分离

至于获取 schema 请求,follower 和 leader 都能处理,因为 schema 最后都存在了 kafka 中,它们直接从 kafka 里读取。

高可用

如果要实现高可用,需要运行多个 Registry 服务,这些服务中必须选择出一个 leader,所有的请求都是最终 由 leader 来负责。当 leader 挂掉之后,就会触发选举操作,来选举出新的 leader。选举的实现有两种方式: 基于kafka 和 基于 zookeeper。

基于 kafka

原理是利用消费组,因为消费组的每个成员都需要和 kafka coordinator 服务端保持心跳,如果有成员挂了,那么就会触发组的重分配操作。重分配操作会从存活的成员中,选出 leader 角色。

KafkaGroupMasterElector 启动了一个心跳线程,定期发送心跳请求。它 实现了监听器的接口,当出发开始选举时会调用onRevoked方法,当选举完之后会调用onAssigned方法。

基于 zookeeper

更加简单,效率也更高。因为只有 leader 挂掉,zookeeper 才会触发重新选举。而基于 kafka 的方式,只要是有一个成员挂掉,不管它是不是 leader,都会触发重新选举。如果这个成员不是 leader,则会造成不必要的选举。

使用zookeeper方式的原理是,所有 Registry 服务都会监听一个临时节点,而只有 leader 才会占有这个节点。当 leader 挂掉之后,临时节点会消失。其余的服务发现临时节点不存在,就会立即尝试重新创建,而只有一个服务能够创建成功,成为 leader。

[参考文献]

  1. https://zhmin.github.io/2019/04/23/kafka-schema-registry/

Kafka-offset-delivery-guarantee

概念

什么是 offset?

offset 是 consumer position,Topic 的每个 Partition 都有各自的 offset

消费者需要自己保留一个 offset,从 kafka 获取消息时,只拉去当前 offset 以后的消息

Kafka 的 scala/java 版的 client 已经实现了这部分的逻辑

之前 offset 保存到 zookeeper 上,broker 存放 offset 是 kafka 从 0.9 版本开始,提供的新的消费方式原因是zookeeper来存放,还是有许多弊端,不方便灵活控制,效率不高

offset 记录位置

kafka 消费者在会保存其消费的进度,也就是offset,存储的位置根据选用的 kafka api 不同而不同。具体可以参看kafka 消费者offset记录位置和方式

Kafka 支持三种消息投递语义

  • At most once 消息可能会丢,但绝不会重复传递
  • At least one 消息绝不会丢,但可能会重复传递
  • Exactly once 每条消息肯定会被传输一次且仅传输一次

consumer在从broker读取消息后,可以选择commit,该操作会在Zookeeper中存下该consumer在该partition下读取的消息的offset,该consumer下一次再读该partition时会从下一条开始读取。如未commit,下一次读取的开始位置会跟上一次commit之后的开始位置相同。

可以将consumer设置为autocommit,即consumer一旦读到数据立即自动commit。如果只讨论这一读取消息的过程,那Kafka是确保了Exactly once。但实际上实际使用中consumer并非读取完数据就结束了,而是要进行进一步处理,而数据处理与commit的顺序在很大程度上决定了消息从broker和consumer的delivery guarantee semantic。

  • 读完消息先commit再处理消息。这种模式下,如果consumer在commit后还没来得及处理消息就crash了,下次重新开始工作后就无法读到刚刚已提交而未处理的消息,这就对应于At most once。
  • 读完消息先处理再commit消费状态(保存offset)。这种模式下,如果在处理完消息之后commit之前Consumer crash了,下次重新开始工作时还会处理刚刚未commit的消息,实际上该消息已经被处理过了,这就对应于At least once。
  • 如果一定要做到Exactly once,就需要协调offset和实际操作的输出。经典的做法是引入两阶段提交,但由于许多输出系统不支持两阶段提交,更为通用的方式是将offset和操作输入存在同一个地方。比如,consumer拿到数据后可能把数据放到HDFS,如果把最新的offset和数据本身一起写到HDFS,那就可以保证数据的输出和offset的更新要么都完成,要么都不完成,间接实现Exactly once。(目前就high level API而言,offset是存于Zookeeper中的,无法存于HDFS,而low level API的offset是由自己去维护的,可以将之存于HDFS中)。

总之,Kafka默认保证At least once,并且允许通过设置producer异步提交来实现At most once,而Exactly once要求与目标存储系统协作,Kafka提供的offset可以较为容易地实现这种方式。


参考链接

kafka-producer-ack

Kafka有两个很重要的配置参数,acksmin.insync.replicas .其中acks是producer的配置参数,min.insync.replicas是Broker端的配置参数,这两个参数对于生产者不丢失数据起到了很大的作用.接下来,本文会以图示的方式讲解这两个参数的含义和使用方式。通过本文,你可以了解到:

  • Kafka的分区副本
  • 什么是同步副本(In-sync replicas)
  • 什么是acks确认机制
  • 什么是最小同步副本
  • ack=all与最小同步副本是如何发挥作用的

分区副本

Kafka的topic是可以分区的,并且可以为分区配置多个副本,改配置可以通过replication.factor参数实现. Kafka中的分区副本包括两种类型:领导者副本(Leader Replica)和追随者副本(Follower Replica),每个分区在创建时都要选举一个副本作为领导者副本,其余的副本自动变为追随者副本. 在 Kafka 中,追随者副本是不对外提供服务的,也就是说,任何一个追随者副本都不能响应消费者和生产者的读写请求. 所有的请求都必须由领导者副本来处理. 换句话说,所有的读写请求都必须发往领导者副本所在的 Broker,由该 Broker 负责处理. 追随者副本不处理客户端请求,它唯一的任务就是从领导者副本异步拉取消息,并写入到自己的提交日志中,从而实现与领导者副本的同步.

Kafka默认的副本因子是3,即每个分区只有1个leader副本和2个follower副本.具体如下图所示:

上面提到生产者客户端仅写入Leader broker,跟随者异步复制数据。由于Kafka是一个分布式系统,必然会存在与 Leader 不能实时同步的风险,所以需要一种方法来判断这些追随者是否跟上了领导者的步伐, 即追随者是否同步了最新的数据.换句话说,Kafka 要明确地告诉我们,追随者副本到底在什么条件下才算与 Leader 同步?这就是下面所要说的ISR同步副本机制.

同步副本(In-sync replicas)

In-sync replica(ISR)称之为同步副本,ISR中的副本都是与Leader进行同步的副本,所以不在该列表的follower会被认为与Leader是不同步的. 那么,ISR中存在是什么副本呢?首先可以明确的是:Leader副本总是存在于ISR中. 而follower副本是否在ISR中,取决于该follower副本是否与Leader副本保持了“同步”.

尖叫提示:对于”follower副本是否与Leader副本保持了同步”的理解如下:

(1)上面所说的同步不是指完全的同步,即并不是说一旦follower副本同步滞后与Leader副本,就会被踢出ISR列表.

(2)Kafka的broker端有一个参数replica.lag.time.max.ms, 该参数表示follower副本滞后与Leader副本的最长时间间隔,默认是10秒. 这就意味着,只要follower副本落后于leader副本的时间间隔不超过10秒,就可以认为该follower副本与leader副本是同步的,所以哪怕当前follower副本落后于Leader副本几条消息,只要在10秒之内赶上Leader副本,就不会被踢出出局.

(3)如果follower副本被踢出ISR列表,等到该副本追上了Leader副本的进度,该副本会被再次加入到ISR列表中,所以ISR是一个动态列表,并不是静态不变的。

如上图所示:Broker3上的partition1副本超过了规定时间,未与Leader副本同步,所以被踢出ISR列表,此时的ISR为[1,3].

acks确认机制

acks参数指定了必须要有多少个分区副本收到消息,生产者才认为该消息是写入成功的,这个参数对于消息是否丢失起着重要作用,该参数的配置具体如下:

  • acks=0,表示生产者在成功写入消息之前不会等待任何来自服务器的响应. 换句话说,一旦出现了问题导致服务器没有收到消息,那么生产者就无从得知,消息也就丢失了. 改配置由于不需要等到服务器的响应,所以可以以网络支持的最大速度发送消息,从而达到很高的吞吐量。

  • acks=1,表示只要集群的leader分区副本接收到了消息,就会向生产者发送一个成功响应的ack,此时生产者接收到ack之后就可以认为该消息是写入成功的. 一旦消息无法写入leader分区副本(比如网络原因、leader节点崩溃),生产者会收到一个错误响应,当生产者接收到该错误响应之后,为了避免数据丢失,会重新发送数据.这种方式的吞吐量取决于使用的是异步发送还是同步发送.

    尖叫提示:如果生产者收到了错误响应,即便是重新发消息,还是会有可能出现丢数据的现象. 比如,如果一个没有收到消息的节点成为了新的Leader,消息就会丢失.

  • acks =all,表示只有所有参与复制的节点(ISR列表的副本)全部收到消息时,生产者才会接收到来自服务器的响应. 这种模式是最高级别的,也是最安全的,可以确保不止一个Broker接收到了消息. 该模式的延迟会很高.

最小同步副本

上面提到,当acks=all时,需要所有的副本都同步了才会发送成功响应到生产者. 其实这里面存在一个问题:如果Leader副本是唯一的同步副本时会发生什么呢?此时相当于acks=1.所以是不安全的.

Kafka的Broker端提供了一个参数**min.insync.replicas**,该参数控制的是消息至少被写入到多少个副本才算是”真正写入”,该值默认值为1,生产环境设定为一个大于1的值可以提升消息的持久性. 因为如果同步副本的数量低于该配置值,则生产者会收到错误响应,从而确保消息不丢失.

Case 1

如下图,当min.insync.replicas=2且acks=all时,如果此时ISR列表只有[1,2],3被踢出ISR列表,只需要保证两个副本同步了,生产者就会收到成功响应.

Case 2

如下图,当min.insync.replicas=2,如果此时ISR列表只有[1],2和3被踢出ISR列表,那么当acks=all时,则不能成功写入数;当acks=0或者acks=1可以成功写入数据.

Case 3

这种情况是很容易引起误解的,如果acks=all且min.insync.replicas=2,此时ISR列表为[1,2,3],那么还是会等到所有的同步副本都同步了消息,才会向生产者发送成功响应的ack.因为min.insync.replicas=2只是一个最低限制,即同步副本少于该配置值,则会抛异常,而acks=all,是需要保证所有的ISR列表的副本都同步了才可以发送成功响应. 如下图所示:

总结

acks=0,生产者在成功写入消息之前不会等待任何来自服务器的响应.

acks=1,只要集群的leader分区副本接收到了消息,就会向生产者发送一个成功响应的ack.

acks=all,表示只有所有参与复制的节点(ISR列表的副本)全部收到消息时,生产者才会接收到来自服务器的响应,此时如果ISR同步副本的个数小于min.insync.replicas的值,消息不会被写入.