容器下 -XX:+HeapDumpOnOutOfMemoryError 未生成 dump 文件的问题

JVM 的启动命令一般都会加上参数 -XX:+HeapDumpOnOutOfMemoryError=/path/to/save/dump.hprof,用于在 JVM 发生 OOM 时自动生成内存 dump 文件。

应用在生产环境是运行在 Docker 容器里、由 K8S 负责管理容器。

但是有的应用发生 OOM 时,在 /path/to/save/dump.hprof 路径下并没有生成对应的 dump 文件。

优秀的运维同事小伍在测试环境进行了各种测试,得出以下两种情况没法生产文件:
1. /path/to/save/ 如果中间层的目录没有提前建好,是没法生成 dump 文件的。
2. 堆外内存不足 800M 时,也没法生成 dump 文件。

第1点没啥问题,文件所在的目录没有提前建好是会报 java.io.FileNotFoundException 异常的。

第2点其实是因为 JVM 运行在容器里,容器允许使用的内存是有上限的,比如分配给容器的是 4G 内存,JVM 堆占用 80%,那么堆外内存就只能占用 20% 即 800M。

发生 OOM 时,JVM 占用了 3.2G;对于堆外内存,线程、JVM自身、应用申请的本地内存等都要在这里分配,OOM dump 也需要利用堆外内存,容器使用的总内存达到 4G 内存上限时,触发系统的 oomkiller 机制把容器进程杀死。

这带来一个小问题:如果要保证 JVM OOM 自动 dump 机制能顺利执行,我们就需要在容器里预留出足够的堆外内存,每个容器都得考虑预留,这就带来内存利用率的问题了。如果 JVM 直接运行在宿主操作系统,没有容器的限制,能申请的堆外内存是受限于系统能分配的内存的,不同应用的 JVM 可共享这个可分配内存空间。


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

MySQL InnoDB MRR 优化

MRR 是 Multi-Range Read 的简写,目的是减少磁盘随机访问,将随机访问转化为较为顺序的访问。适用于 range/ref/eq_ref 类型的查询。

实现原理:

  1. 在二级索引查找后,根据得到的主键到聚簇索引找出需要的数据。
  2. 二级索引查找得到的主键的顺序是不确定的,因为二级索引的顺序与聚簇索引的顺序不一定一致;
  3. 如果没有 MRR,那么在聚簇索引查找时就可能出现乱序读取数据页,这对于机械硬盘是及其不友好的。
  4. MRR 的优化方式:

    • 将查找到的二级索引键值放在一个缓存中;
    • 将缓存中的键值按照 主键 进行排序;
    • 根据排序后的主键去聚簇索引访问实际的数据文件。
  5. 当优化器使用了 MRR 时,执行计划的 Extra 列会出现 “Using MRR” 。

  6. 如果查询使用的二级索引的顺序本身与结果集的顺序一致,那么使用 MRR 后需要对得到的结果集进行排序。

继续阅读

Redis Cluster

本文基于 Redis 5.0.5

首先安装 Redis

wget http://download.redis.io/releases/redis-5.0.5.tar.gz

tar xzf redis-5.0.5.tar.gz

cd redis-5.0.5/

sudo make && make install

修改配置文件 redis.conf

# 同一台机器上,每个 Redis 实例的端口要不一样
port 7000

# 开启实例的集群模式
cluster-enabled yes

# 保存节点配置文件的路径
cluster-config-file nodes.conf

# 节点间通信的超时时间
cluster-node-timeout 5000

# 采用 AOF 
appendonly yes

拷贝并配置集群实例

cd ..
mkdir -p redis-cluster/{7000,7001,7002,7003,7004,7005}

把 redis.conf 拷贝到上面建立的数字目录下,修改相应的端口号为目录名。

继续阅读

MySQL ICP 索引条件下推优化

ICP 优化的全称是 Index Condition Pushdown Optimization 。

ICP 优化适用于 MySQL 利用索引从表里检索数据的场景。

ICP 适用的场景

  • ICP 用于访问方法是 range/ref/eq_ref/ref_or_null,且需要访问表的完整行记录。
  • ICP适用于 InnoDB 和 MyISAM 的表,包括分区的表。
  • 对于 InnoDB 表,ICP只适用于二级索引。ICP 的目标是减少访问表的完整行的读数量从而减少 I/O 操作。对于 InnoDB 的聚簇索引,完整的记录已经读进 InnoDB 的缓存,使用 ICP 不能减少 I/O 。
  • ICP 不支持建立在虚拟列上的二级索引。InnoDB 支持在虚拟列上建立二级索引。
  • 引用子查询、存储函数的条件没法下推。
  • Triggered conditions 也没法下推。

继续阅读

SpringBoot 启动分析(五) — 上下文的刷新过程

1. SpringApplication.refreshContext

首先来看 SpringApplication 里刷新上下文的逻辑:

private void refreshContext(ConfigurableApplicationContext context) {
    refresh(context);
    if (this.registerShutdownHook) {
        try {
            context.registerShutdownHook();
        } catch (AccessControlException ex) {
            // Not allowed in some environments.
        }
    }
}

protected void refresh(ApplicationContext applicationContext) {
    Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
    ((AbstractApplicationContext) applicationContext).refresh();
}

刷新的逻辑是在 AbstractApplicationContext.refresh 方法完成的,刷新完后注册了 JVM 的关闭回调钩子。

2. AbstractApplicationContext.refresh

继续阅读

SpringBoot 启动分析(四) — 注解驱动的 Bean 定义加载

1. 一个 Spring 加载类的问题

先抛出个问题:SpringBoot 允许通过注解根据某个类是否存在来决定配置,如

@Bean
@ConditionalOnClass(value = HikariDataSource.class)
public DataSource hikariDataSource() {
    return new HikariDataSource();
}

@Bean
@ConditionalOnClass(value = BasicDataSource.class)
public DataSource basicDataSource() {
    return new BasicDataSource();
}

我们也知道 ClassLoader 加载一个类时,如果这个类或这个类依赖的类找不到则会抛出 ClassNotFoundException

Spring 是如何实现这样的条件加载而不会抛出 ClassNotFoundException 异常?

继续阅读

Oracle rownum 与 offset

主要来自:
On ROWNUM and Limiting Results
The result offset and fetch first clauses

rownum

rownum 主要有两类用处

  • 处理 top N ;
  • 分页查询;

rownum 的工作机制

rownum 是查询中的伪列,从 1 开始计数。

A ROWNUM value is assigned to a row after it passes the predicate phase of the query but before the query does any sorting or aggregation. Also, a ROWNUM value is incremented only after it is assigned .

rownum 的值是在行记录通过了查询的过滤阶段、在排序或聚合之前被赋值。rownum 只有在被赋值之后才会递增。select * from t where ROWNUM > 1; 是永远查不到记录的。

查询的处理步骤大概如下:
1. from/where 首先执行;
2. rownum 被递增、赋值给 from/where 子句输出的每一行;
3. 执行 select 子句;
4. 执行 group by 子句;
5. 执行 having ;
6. 执行 order by 。

继续阅读

SpringBoot 启动分析(三) — Environment 的初始化流程

1. Environment 的初始化流程

ConfigFileApplicationListener 收到 ApplicationEnvironmentPreparedEvent 事件后通过 SPI 加载所有的 EnvironmentPostProcessor 实现,触发其 postProcessEnviroment 方法。

SpringApplication.run() ->
SpringFactoriesLoader.loadFactories(ApplicationListener) ->
SpringApplication.prepareEnviroment() -> EventPublishingRunListener.enviromentPrepared(ApplicationEnviromentPraparedEvent) ->
SimpleApplicationEventMulticaster.multicastEvent() ->
ConfigFileApplicationListener.onApplicationOnEnviromentPreparedEvent() ->
EnviromentPostProcessor.postProcessEnviroment()

比较重要的 EnviromentPostProcessor 实现是 HostInfoEnvironmentPostProcessorConfigFileApplicationListener

2. HostInfoEnvironmentPostProcessor.postProcessEnviroment

获取本机的 主机名和IP地址,封装在 PropertySource 添加到 environment 里。

3. ConfigFileApplicationListener.postProcessEnviroment

ConfigFileApplicationListener 自身也实现了 EnvironmentPostProcessor,通过内部类 Loader 去加载配置文件,其主要流程如下:

  1. 从 Environment 中获取 active 和 include 的 profile 集合。进行迭代:
  2. 获取所有的搜索路径,进行迭代,默认的搜索路径是 classpath:/,classpath:/config/,file:./,file:./config/
  3. 如果某个搜索路径不以 / 结尾的则认为是一个文件,直接加载,否则,找出所有的搜索文件名 name 进行迭代搜索,默认的搜索文件名是 “application”。
  4. 通过 PropertySourcesLoader 找出支持的所有配置文件后缀进行迭代。
  5. 最终得到 location + name + "-" + profile + "." + ext 组成的一个具体的完整路径,通过 PropertiesLoader.load 方法加载该路径指向的配置文件。
  6. PropertiesLoader.load 内部又根据配置文件的后缀用不同的 PropertySourceLoader 去加载得到一个 PropertySource
  7. 对于解析得到的 PropertySource,找出里面激活的 profile,添加到 proflie 集合里进行迭代。
  8. 继续迭代下一个 profile 。

继续阅读

SpringBoot 启动分析(一)

SpringBoot 启动分析 序列文章基于 spring-boot-starter-parent 1.5.19.RELEASE 。

1. 启动一个 SpringBoot 应用

启动一个 SpringBoot 应用只需要下面几行代码即可:

@SpringBootApplication
public class SpringBootDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootDemoApplication.class, args);
    }
}

查看 SpringApplication.run 方法时会来到:

public static ConfigurableApplicationContext run(Object source, String... args) {
    return run(new Object[] { source }, args);
}

public static ConfigurableApplicationContext run(Object[] sources, String[] args) {
    return new SpringApplication(sources).run(args);
}

可以看到 SpringBoot 的魔法就是那么简单:创建一个 SpringApplication,执行其 run 方法
就像 “打开冰箱门、把大象塞进冰箱、关上冰箱门” 那么简单、有力。

当然,要了解原理是不能只看高层抽象的。

2. SPI 机制 SpringFactoriesLoader

SpringFactoriesLoader 是 Spring 提供的 SPI 实现机制,从类路径下的 META-INF/spring.factories 文件里加载指定接口的所有实现。以接口的完整类名作为 key,实现类的完整名字作为值,多个实现类用 , 分隔。

下面是 spring-boot-autoconfigure 包下 META-INF/spring.factories 文件里的 一小部分:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,\

SpringFactoriesLoader 内部通过 ClassLoader.getResources 方法来加载类路径下的文件。

继续阅读

SpringBoot 启动分析(二)–启动主流程

1. initialize 方法

SpringApplication 的构造函数只调用了 initialize 方法:

private void initialize(Object[] sources) {
    if (sources != null && sources.length > 0) {
        // 把应用指定的配置类加入配置扫描的启动来源
        this.sources.addAll(Arrays.asList(sources));
    }

    // 判断应用是否是 web 应用,主要用于决定采用哪种具体的上下文实现
    this.webEnvironment = deduceWebEnvironment();

    // 利用定制的 SPI 实现 SpringFactoriesLoader 加载 ApplicationContextInitializer 的所有实现并设置到 initializers 属性
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

    // 利用定制的 SPI 实现 SpringFactoriesLoader 加载 ApplicationListener 的所有实现并设置到 listeners 属性
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

    // 找出启动类:线程栈上 main 方法所处的类
    this.mainApplicationClass = deduceMainApplicationClass();
}

该方法的逻辑主要如下:

  1. 把启动类加入 sources 属性。
  2. 判断是否是 web 应用并设置到 webEnvironment 属性。
  3. 加载所有的 ApplicationContextInitializer 并设置到 initializers 属性。
  4. 加载所有的 ApplicationListener 并设置到 listeners 属性。
  5. 找出 main 类设置到 mainApplicationClass。

2. run 方法

继续阅读