一次由于网络套接字文件描述符泄露导致线上服务事故原因的排查经历

最近,线上服务遭遇了一次事故。该服务(记为 A 服务)是一个 Java 的网络服务,间接使用了 Ignite 作为内存数 据库和 RPC 基础件;服务对 Ignite 的访问操作通过公司另外部门维护的一个二次封装接口(公共 JAR 包形式)进行。

在生产环境中,运维按计划下线 Ignite 服务之后,A 服务的正常业务流程未受到影响;按计划继续下线 Ignite 服务所在的服务器之后,A 服务很快开始报 java.net.SocketException: Too many open files 错误,导致 A 服务很快不可用。

我在测试环境部署模拟复现了该现象,并排查到此次事故是由 Ignite 的一个 BUG 导 致的。事实上,该 BUG 已经在 Ignite 的主线版本中被修复,但由于隔壁部门自己 FORK 了 Ignite 的较低版本并且没有及时地同步主线更新,使得 A 服务在生产环境出现了该事故。

现记录下事故原因排查的经历,以鉴后事。

Dependency Injection - Guice VS Dagger 2

做 Java 后台开发的同学,基本上都使用过 Dependency Injection 依赖注入框架。大名鼎鼎的 Spring Framework 就是从依赖注入起家的,然后一路奔向了全家桶的不归路;Guice 是 Google 公司的依赖注入解决方案,重视类型安全胜过于使用便利性,相比 Spring Framework 能提供 更精准的控制和更翔实的注入失败错误信息;而由 Google 接盘 SquareDagger 2 则比 Guice 更进一步,通过在编译时构建依赖注入关系,使得依赖错误在编译阶 段尽量暴露出来,同时,原生类型安全的 Java 代码相较基于反射的注入代码也有或多或少的性能提升(尤其是在 Android 上)。

本文通过同时使用 GuiceDagger 2 来实现相同的功能,对比二者在实现依赖注入方法上 的异同。

JOOQ 的使用 - 从生成代码执行 SQL 命令 (SQL Executor)

jOOQ 是基于 JDBC 之上的一个抽象层,提供了多种多样的模型来与关系型数据库进行互操作;其使用与 mybatisHibernate ORM 不同的思路来实现 对象关系映射 ORM

JOOQ 的使用 - 代码生成配置 (PostgreSQL & DDL Driven) 介绍了使用 jOOQ 为数据库表生成实体类代码;JOOQ 的使用 - 执行 SQL 语句 (SQL Executor) 介绍了基于 jOOQ 的 SQL 命令执行;作为对比,本篇主要介绍基于生成代码的 SQL 命令执行 (SQL Executor) 。

JOOQ 的使用 - 从生成代码拼接 SQL 语句 (SQL Builder)

jOOQ 是基于 JDBC 之上的一个抽象层,提供了多种多样的模型来与关系型数据库进行互操作;其使用与 mybatisHibernate ORM 不同的思路来实现 对象关系映射 ORM

JOOQ 的使用 - 代码生成配置 (PostgreSQL & DDL Driven) 介绍了使用 jOOQ 为数据库表生成实体类代码;JOOQ 的使用 - 拼接 SQL 语句 (SQL Builder) 介绍了基于 jOOQ 的 SQL 语句拼接;作为对比,本篇主要介绍基于生成代码的 SQL 语句拼接 (SQL Builder) 。

JOOQ 的使用 - 代码生成配置 (PostgreSQL & DDL Driven)

jOOQ 是基于 JDBC 之上的一个抽象层,提供了多种多样的模型来与关系型数据库进行互操作;其使用与 mybatisHibernate ORM 不同的思路来实现 对象关系映射 ORM

jOOQ 的一个最主要的特性就是基于代码生成的类型安全 SQL。其主要是从已有的数据库配置信息中收集 到足够的表结构、字段结构和索引等信息,生成对应的 Java 类型;业务使用生成的 Java 类型来进行 SQL 操作 ,可以得到足够的类型安全保证。同时,由于代码生成避免了基于反射的对象关系映射,在调试时会有更好的表现 。

截止到版本 3.12.1, jOOQ 支持从以下数据库配置源生成代码:

  1. 关系数据库实例,包括(注:免费版本的 jOOQ 可能不支持特定的数据库提供商的产品):
    • Aurora MySQL Edition
    • Aurora PostgreSQL Edition
    • Azure SQL Data Warehouse
    • Azure SQL Database
    • CUBRID
    • DB2 LUW
    • Derby
    • Firebird
    • H2
    • HANA
    • HSQLDB
    • Informix
    • Ingres
    • MariaDB
    • Microsoft Access
    • MySQL
    • Oracle
    • PostgreSQL
    • Redshift
    • SQL Server
    • SQLite
    • Sybase Adaptive Server Enterprise
    • Sybase SQL Anywhere
    • Teradata
    • Vertica
  2. Java Persistence API (JPA) 相关的实体类型(带注解)
  3. 符合约定条件的描述表结构信息等的 XML 文件
  4. 符合标准的用于创建表结构等的 SQL DDL 语句文件

本篇主要介绍基于 jOOQ 的数据库实例和 SQL DDL 文件驱动的代码生成实践。

Mapper Classes Generated by MapStruct

对象实体转换操作在分层应用中很常见,比如数据库层的对象实体和表现层的实体很可能具有不同的属性集,需要在互操作时进行属性的映射(或拷贝)。

MapStruct 是一个辅助进行 Java 实体类之间相互转换的类库,与其他具有相似功能的工具库之间的最大区别在于其使用了 Java 注解处理器 APT 来实现实体间属性的映射而不是使用反射技术。

在 Maven 中支持 Java 的注解处理器 APT

Java 中有很多基于 注解处理器 (Annotation Processing Tool, APT) 技术的类库,如 AutoValueFreeBuilder 等。

Maven 中支持 APT ,需要在 Apache Maven Compiler Plugin 的配置部分添加 annotationProcessorPaths 的配置,如下:

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-compiler-plugin</artifactId>
	<version>3.6.1</version>
	<configuration>
		<source>1.8</source>
		<target>1.8</target>
		<testSource>1.8</testSource>
		<testTarget>1.8</testTarget>
		<encoding>UTF-8</encoding>
		<optimize>true</optimize>
		<!-- Slightly faster builds, see https://issues.apache.org/jira/browse/MCOMPILER-209 -->
		<useIncrementalCompilation>false</useIncrementalCompilation>
		<annotationProcessorPaths>
			<path>
				<groupId>com.google.auto.value</groupId>
				<artifactId>auto-value</artifactId>
				<version>${auto-value.version}</version>
			</path>
		</annotationProcessorPaths>
	</configuration>
</plugin>

上述配置对于 Maven 3.5 以上版本有效。

对于低于 3.5 的版本,可以在 dependencies 块中添加依赖项,并设置 optional 属性。

<dependency>
	<groupId>org.inferred</groupId>
	<artifactId>freebuilder</artifactId>
	<version>${freebuilder_version}</version>
	<optional>true</optional>
</dependency>

如果是可执行的工程,也可以设置 scopeprovided

<dependency>
	<groupId>org.inferred</groupId>
	<artifactId>freebuilder</artifactId>
	<version>${freebuilder_version}</version>
	<scope>provided</scope>
</dependency>

Value Class Generated by FreeBuilder

FreeBuilder 是一个非常好用的基于 JAVA 注解处理器 APT (Annotation Processing Tool) 技术的数据实体类生成器,可以 通过简单的注解来生成 Builder 模式的实体类。AutoValue 是另一个非常好用的基于 APT 的类库,参见 Value Class Generated by Google AutoValue。二者的区别在于:

  1. AutoValue 根据开发者提供的一个 Builder 接口来实现具体的 Builder 类;FreeBuilder 则对给定的数据实体类生成符合固定规则的 Builder 类。前者可以控制生成的 Builder 类的方法列表,后者无法控制生成的 Builder 类的方法列表,但是可以通过继承生成的 Builder 类限制对外暴露的方法列表。
  2. AutoValue 需要数据实体类是一个抽象类;FreeBuilder 则同时支持抽象类和接口。

详细对比可以参考 FreeBuilder Alternatives

Value Class Generated by Google AutoValue

Java 中组织数据对象的方式中,最简单也是最通用的是所谓的 POJO (Plain Ordinary Java Object) 即 Java 简单对象。POJO 大法好,好就好在简单实用:只需用遵循简单的命名约定和构造规则,就可以满足数据实体 的需求,而且大多数的序列化方案和持久化方案都能提供 POJO 的支持;如果追求代码精简的话,使用 POJO 可以 和反射方案一起极大地简化重复代码。

POJO 的好处吾等皆非常清楚,然而本文却在讲 AutoValue,因为:

  1. 人是善变的。吾等吃够了鸡蛋牛奶,也会想要尝尝豆浆油条。
  2. equals toString hash 。此三种样板代码,即使是有 IDE 的协助,维护起来仍会非常痛苦。
  3. Mutable shared state is the root of all evil in concurrent systems 。可变性在并发系统中的共享是所有 BUG 的源头,不可变性是救赎之道;而 POJO 不能满足不可变性约束。
  4. Google 大法好。AutoValue 是 Google Java 团队的作品。

AutoValue 是一个基于 Java 注解处理器 APT (Annotation Processing Tool) 的代码生成库,网络上关于 AutoValue 的介绍不要太少,毋庸再讲;其理念和实际用法可见 官网文档,十分钟入门,老少咸 宜。

本文对于 AutoValue 的用法做一个简单汇总,并引申介绍一下基于 AutoValue 的对 象 JSON 序列化方案。

Spring Cloud Gateway Load Testing using Wrk

Spring Cloud GatewaySpring 官方为了完善 Spring Cloud 的版图 而推出的网关服务组件,使用了非阻塞式网络模式和目前流行的响应式编程模型,吸引了很多公司和开发者的注意 力。

网络上有一些现成的对 Spring Cloud Gateway 的性能测试的案例,比如 纠错帖:Zuul & Spring Cloud Gateway & Linkerd性能对比。根据 Simple benchmark comparing zuul and spring cloud gateway 的数据,Spring Cloud Gateway 的性能测试结果参考如下:

ProxyAvg LatencyAvg Req/Sec/Thread
gateway6.61ms3.24k
linkered7.62ms2.82k
zuul12.56ms2.09k
none2.09ms11.77k

但是,由于:

  1. 上述测试只是简单的对后端反向代理测试,而我们知道 Spring Cloud Gateway 对于路由的匹配是顺序的,匹配链后面的路由的性能并没有被关注到。
  2. 测试数据被进行了简化,测试的细节被(有意地)忽略了。

所以,在根据 Spring Cloud Gateway 改造出了初版的公司网关系统之后,我使用 wrkwrk2 对网关进行了多轮的压测,测试结果汇总为本文。

Usage of Benchmarking Tool WRK and WRK2

wrk 是一款短小精悍又备受赞誉的开源性能测试工具,能够用来对 HTTP 服务进行压测;wrk2 是 对 wrk 的改进,增加了压测结果的直方图输出。

网路上有不少介绍 wrkwrk2 的文章,但大多泛泛而谈,对于压测的结果输出项的解释也是云 里雾里或者简单跳过,殊为遗憾。

本文主要介绍 wrkwrk2 的使用,并在阅读源码的基础上对输出结果的项进行解释。