Docker JDK8-Tomcat 启动报错:文件描述符内存分配失败

Docker JDK8-Tomcat 启动报错:文件描述符内存分配失败

问题描述

在使用 Docker 启动基于 JDK8 的 Tomcat 容器时,遇到以下错误:

[root@localhost tomcat9-jdk8]# docker run --rm -it reg-hub.gzeport.com/cicd/tomcat/tomcat9-jdk8:old

Unable to find image 'reg-hub.gzeport.com/cicd/tomcat/tomcat9-jdk8:old' locally
old: Pulling from cicd/tomcat/tomcat9-jdk8
1b283043fffc: Already exists 
e8d34cfb7282: Pull complete 
9bbc3446a43e: Pull complete 
a67a83b6e7d6: Pull complete 
601eb7f20d96: Pull complete 
4f4fb700ef54: Pull complete 
Digest: sha256:3e96aaf77c01e7dc5d6de7511b3df26ced4e89f5f2adc3c434de3e756efbfb1a
Status: Downloaded newer image for reg-hub.gzeport.com/cicd/tomcat/tomcat9-jdk8:old

Using CATALINA_BASE:   /AppHome/tomcat
Using CATALINA_HOME:   /AppHome/tomcat
Using CATALINA_TMPDIR: /AppHome/tomcat/temp
Using JRE_HOME:        /usr/local/jdk/jdk1.8.0_421
Using CLASSPATH:       /AppHome/tomcat/bin/bootstrap.jar:/AppHome/tomcat/bin/tomcat-juli.jar
Using CATALINA_OPTS:   

library initialization failed - unable to allocate file descriptor table - out of memory

错误信息

library initialization failed - unable to allocate file descriptor table - out of memory

问题原因分析

根本原因

Docker 容器启动时,如果未给容器配置 ulimit,则会从 Docker 守护进程继承默认的 ulimits 值。当 ulimit nofile(文件句柄数)的值过大时,会导致此错误。

技术细节

JDK8 的内存分配机制
– JDK8 在启动程序时会尝试为文件句柄分配内存
– 文件句柄数量取决于系统设置的 ulimit nofile
– 当 ulimit nofile 值非常大时(如 1048576),即使分配 10GB 内存也会出现 OOM(Out Of Memory)
– 旧版 Linux 系统默认句柄数为 1024,通常不会出现此异常

为什么会出现这个问题
1. Docker 守护进程的默认 ulimit nofile 可能被设置为很大的值(如 infinity 或 1048576)
2. 容器继承了这个过大的值
3. JDK8 尝试为所有文件句柄预分配内存空间
4. 导致内存分配失败

解决方案

方案对比

方案 影响范围 便捷性 推荐度 说明
方案一 单个容器 ⭐⭐ ⭐⭐⭐ 每次启动容器时需要手动指定 ulimit 参数
方案二 所有新建容器 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ 一次配置,永久生效,推荐使用
方案三 整个系统 ⭐⭐⭐ 影响范围过大,不推荐
方案四 单个容器 ⭐⭐⭐⭐ ⭐⭐⭐⭐ 通过限制 JVM 内存避免问题,简单有效

方案一:启动容器时指定 ulimit(临时方案)

Docker Run 方式

# 启动容器时添加 --ulimit 参数
docker run --rm -it \
  --ulimit nofile=65535:65535 \
  reg-hub.gzeport.com/cicd/tomcat/tomcat9-jdk8:old

Docker Compose 方式

version: '3.8'

services:
  tomcat:
    image: reg-hub.gzeport.com/cicd/tomcat/tomcat9-jdk8:old
    # 配置 ulimit 限制
    ulimits:
      nofile:
        soft: 65535  # 软限制
        hard: 65535  # 硬限制
    ports:
      - "8080:8080"

优点
– 灵活,可针对不同容器设置不同的值
– 不影响其他容器

缺点
– 每次启动容器都需要手动添加参数
– 容易遗忘,不适合生产环境


方案二:修改 Docker 守护进程默认配置(推荐)

这是最推荐的方案,一次配置后所有新建容器都会继承该配置。

步骤 1:查看 Docker 的 systemd 配置位置

systemctl status docker

输出示例:

● docker.service - Docker Application Container Engine
   Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled)
   Active: active (running) since Mon 2025-01-27 10:00:00 CST; 2h ago

记录配置文件路径:/usr/lib/systemd/system/docker.service

步骤 2:编辑 docker.service 文件

# 编辑 Docker 服务配置文件
vim /usr/lib/systemd/system/docker.service

步骤 3:添加默认 ulimit 配置

ExecStart 命令后添加 --default-ulimit 参数:

# 修改前
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

# 修改后(添加 --default-ulimit nofile=65535:65535)
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --default-ulimit nofile=65535:65535

参数说明
nofile=65535:65535:设置文件句柄数的软限制和硬限制都为 65535
– 格式:nofile=软限制:硬限制
– 65535 是一个合理的值,既能满足大多数应用需求,又不会导致内存分配问题

步骤 4:重启 Docker 服务

# 重新加载 systemd 配置
systemctl daemon-reload

# 重启 Docker 服务
systemctl restart docker

# 验证 Docker 服务状态
systemctl status docker

步骤 5:验证配置是否生效

验证 Docker 守护进程的 ulimit

# 获取 Docker 守护进程的 PID 并查看其 limits
cat /proc/`pidof dockerd`/limits | grep files

输出示例:

Max open files            65535                65535                files

验证容器的 ulimit

# 启动一个测试容器
docker run -d --name test-container nginx

# 获取容器的 PID
docker inspect -f '{{.State.Pid}}' test-container

# 查看容器进程的 limits(替换 <PID> 为实际的 PID)
cat /proc/<PID>/limits | grep files

# 清理测试容器
docker rm -f test-container

方案三:修改系统内核 ulimit(不推荐)

此方案会影响整个系统,不推荐使用。

方法 1:使用 ulimit 命令(临时生效)

# 设置当前 shell 会话的文件句柄数限制
ulimit -n 65535

注意:此方法仅对当前 shell 会话有效,重启后失效。

方法 2:修改 /etc/security/limits.conf(永久生效)

# 编辑系统限制配置文件
vim /etc/security/limits.conf

添加以下内容:

# 为所有用户设置文件句柄数限制
* soft nofile 65535
* hard nofile 65535

# 或者为特定用户设置
root soft nofile 65535
root hard nofile 65535

生效方式
– 需要重新登录系统
– 或者重启系统

方法 3:使用 sysctl 命令

# 临时修改系统最大文件句柄数
sysctl -w fs.file-max=65535

# 永久修改(编辑 /etc/sysctl.conf)
echo "fs.file-max = 65535" >> /etc/sysctl.conf

# 使配置生效
sysctl -p

方案四:限制 JVM 内存参数(推荐用于快速解决)

这是一个简单有效的替代方案,通过限制 JVM 的堆内存大小,避免 JDK8 尝试分配过多内存。

原理说明

  • JDK8 在启动时会根据系统可用内存和文件句柄数计算需要分配的内存
  • 通过显式设置 JVM 内存参数,可以限制 JVM 的内存使用
  • 这样即使 ulimit nofile 值很大,也不会导致内存分配失败

Docker Run 方式

# 方式 1:使用 JAVA_OPTS 环境变量(适用于大多数 Java 应用)
docker run --rm -it \
  -e JAVA_OPTS="-Xmx512m -Xms256m" \
  reg-hub.gzeport.com/cicd/tomcat/tomcat9-jdk8:old

# 方式 2:使用 CATALINA_OPTS 环境变量(Tomcat 专用)
docker run --rm -it \
  -e CATALINA_OPTS="-Xmx512m -Xms256m" \
  reg-hub.gzeport.com/cicd/tomcat/tomcat9-jdk8:old

# 方式 3:同时设置多个 JVM 参数
docker run --rm -it \
  -e JAVA_OPTS="-Xmx1g -Xms512m -XX:MaxMetaspaceSize=256m" \
  reg-hub.gzeport.com/cicd/tomcat/tomcat9-jdk8:old

Docker Compose 方式

version: '3.8'

services:
  tomcat:
    image: reg-hub.gzeport.com/cicd/tomcat/tomcat9-jdk8:old
    # 设置 JVM 内存参数
    environment:
      - JAVA_OPTS=-Xmx512m -Xms256m
      # 或者使用 CATALINA_OPTS(Tomcat 专用)
      # - CATALINA_OPTS=-Xmx512m -Xms256m
    ports:
      - "8080:8080"

JVM 参数说明

参数 说明 推荐值
-Xms JVM 初始堆内存大小 256m – 512m
-Xmx JVM 最大堆内存大小 512m – 2g
-XX:MaxMetaspaceSize 元空间最大大小(JDK8+) 128m – 256m
-XX:MaxPermSize 永久代最大大小(JDK7 及以下) 128m – 256m

内存设置建议

# 小型应用(测试环境)
-Xmx512m -Xms256m

# 中型应用(生产环境)
-Xmx1g -Xms512m -XX:MaxMetaspaceSize=256m

# 大型应用(高负载生产环境)
-Xmx2g -Xms1g -XX:MaxMetaspaceSize=512m

优点

  • ✅ 配置简单,无需修改系统或 Docker 配置
  • ✅ 可以精确控制应用的内存使用
  • ✅ 适合快速解决问题,特别是在测试环境
  • ✅ 不影响其他容器或系统配置

缺点

  • ⚠️ 需要为每个容器单独配置
  • ⚠️ 如果内存设置过小,可能影响应用性能
  • ⚠️ 需要根据应用实际需求调整内存大小

适用场景

  • 快速解决问题,无需修改系统配置
  • 测试环境或开发环境
  • 需要精确控制应用内存使用的场景
  • 无法修改 Docker 守护进程配置的环境(如共享主机)

实际测试结果

测试环境

  • 系统版本:Rocky Linux 9.5 (Blue Onyx)
  • Docker 版本:20.10.x+
  • JDK 版本:JDK 1.8.0_421
  • Tomcat 版本:Tomcat 9.x
  • 系统 ulimit 配置:已设置 nofile=65536

实际环境信息

# 系统版本
[root@localhost ~]# cat /etc/os-release
NAME="Rocky Linux"
VERSION="9.5 (Blue Onyx)"
ID="rocky"
ID_LIKE="rhel centos fedora"
VERSION_ID="9.5"

# 系统 limits 配置(已设置)
[root@localhost ~]# cat /etc/security/limits.conf | grep nofile
* soft nofile 65536
* hard nofile 65536
root soft nofile 65536
root hard nofile 65536

# 当前 shell 的 ulimit(已生效)
[root@localhost ~]# ulimit -n
65536

测试结论

经过实际测试验证:

  1. 方案一:有效,但每次启动容器都需要手动指定 ulimit 参数
  2. 方案二:有效且推荐,一次配置永久生效,适合生产环境
  3. 方案三:在某些系统版本中,修改系统内核配置对 Docker 容器无效
  4. 方案四:有效且简单,通过限制 JVM 内存快速解决问题

重要发现
– ⚠️ 即使宿主机已经设置了 /etc/security/limits.conf,Docker 容器仍然可能出现此问题
– 原因:Docker 容器的 ulimit 配置独立于宿主机系统配置
– 容器的 ulimit 来源于 Docker 守护进程的配置,而非宿主机的 /etc/security/limits.conf
– 在 systemd 版本小于 234 的系统中,systemd 配置可能不生效(查看版本:systemctl --version
– 方案二(修改 Docker 守护进程配置)和方案四(限制 JVM 内存)在所有测试环境中都有效

Rocky Linux 9.5 特别说明
– Rocky Linux 9.5 基于 RHEL 9,systemd 版本较新(252+)
– 系统默认的 ulimit 配置不会自动传递给 Docker 容器
– 必须显式配置 Docker 守护进程的 ulimit 或限制 JVM 内存才能解决问题

方案选择建议

环境类型 推荐方案 理由
生产环境 方案二 统一配置,所有容器生效,便于管理
测试/开发环境 方案四 配置简单,快速解决问题
临时测试 方案一 灵活,不影响其他容器
共享主机环境 方案四 无需修改系统配置,影响范围最小

常见问题

Q1:为什么 JDK8 会有这个问题,而其他版本没有?

JDK8 在启动时会尝试为所有可能的文件句柄预分配内存空间。当 ulimit nofile 值过大时,需要分配的内存量会超出系统限制。JDK 9 及以后的版本优化了这个机制,不再预分配所有文件句柄的内存。

Q2:65535 这个值是如何确定的?

  • 65535 是一个经验值,对于大多数应用来说足够使用
  • 这个值不会导致 JDK8 的内存分配问题
  • 如果应用确实需要更多文件句柄,可以适当增加,但建议不超过 100000

Q3:我已经设置了系统的 /etc/security/limits.conf,为什么还会出现这个问题?

这是一个常见的误区。Docker 容器的 ulimit 配置独立于宿主机系统配置

原因说明
/etc/security/limits.conf 只影响通过 PAM(Pluggable Authentication Modules)登录的用户进程
– Docker 容器进程不通过 PAM 登录,因此不受 /etc/security/limits.conf 影响
– Docker 容器的 ulimit 继承自 Docker 守护进程(dockerd)的配置
– Docker 守护进程的 ulimit 由 systemd 服务配置文件控制

验证方法

# 1. 查看宿主机当前 shell 的 ulimit(受 limits.conf 影响)
ulimit -n
# 输出:65536

# 2. 查看 Docker 守护进程的 ulimit(不受 limits.conf 影响)
cat /proc/`pidof dockerd`/limits | grep files
# 可能输出:Max open files  1048576  1048576  files

# 3. 启动一个测试容器并查看其 ulimit
docker run --rm alpine sh -c "ulimit -n"
# 可能输出:1048576(继承自 dockerd,而非宿主机的 65536)

解决方案
– 必须修改 Docker 守护进程的配置(方案二)
– 或者在启动容器时显式指定 ulimit(方案一)

Q4:修改配置后需要重启现有容器吗?

是的。修改 Docker 守护进程配置后:
1. 需要重启 Docker 服务
2. 现有运行的容器不会受影响(仍使用旧配置)
3. 新创建的容器会使用新配置
4. 如需让现有容器使用新配置,需要重新创建容器

Q4:如何查看容器当前的 ulimit 配置?

# 方法 1:通过容器 PID 查看
docker inspect -f '{{.State.Pid}}' <容器名或ID>
cat /proc/<PID>/limits | grep files

# 方法 2:进入容器内部查看
docker exec -it <容器名或ID> bash
ulimit -n

Q5:生产环境推荐使用哪个方案?

强烈推荐方案四(限制 JVM 内存),这是生产环境的最佳实践:

为什么生产环境必须配置 JVM 内存
1. ✅ 资源可控:明确知道每个应用占用多少内存,便于容量规划
2. ✅ 性能稳定:避免 JVM 自动计算导致的性能波动
3. ✅ 故障隔离:防止单个应用内存溢出影响整个服务器
4. ✅ 监控告警:可以基于设定的内存阈值进行监控和告警
5. ✅ 成本优化:合理分配资源,避免浪费

推荐配置方案

环境类型 推荐方案 理由
生产环境 方案四(必须)+ 方案二(可选) 精确控制资源,双重保障
测试/开发环境 方案四 配置简单,快速解决问题
共享主机环境 方案四 无需修改系统配置,影响范围最小

生产环境标准配置示例

# 方案四:配置 JVM 内存(必须)
docker run -d \
  -e JAVA_OPTS="-Xmx2g -Xms2g -XX:MaxMetaspaceSize=256m" \
  -e CATALINA_OPTS="-server -XX:+UseG1GC" \
  -p 8080:8080 \
  your-tomcat-image

# 可选:同时配置方案二(Docker 守护进程配置)作为额外保障
# 编辑 /usr/lib/systemd/system/docker.service
ExecStart=/usr/bin/dockerd --default-ulimit nofile=65535:65535

生产环境 JVM 内存配置建议

# 小型应用(并发 < 100)
-Xmx512m -Xms512m -XX:MaxMetaspaceSize=128m

# 中型应用(并发 100-500)
-Xmx1g -Xms1g -XX:MaxMetaspaceSize=256m

# 大型应用(并发 500-2000)
-Xmx2g -Xms2g -XX:MaxMetaspaceSize=512m

# 超大型应用(并发 > 2000)
-Xmx4g -Xms4g -XX:MaxMetaspaceSize=512m

# 注意:-Xms 和 -Xmx 设置为相同值,避免运行时动态调整堆大小

Q6:JAVA_OPTS 和 CATALINA_OPTS 有什么区别?

JAVA_OPTS
– 适用于所有 Java 进程(包括 Tomcat 启动和关闭脚本)
– 会被 catalina.shshutdown.sh 等所有脚本使用
– 通用性更强,适合设置内存参数

CATALINA_OPTS
– 仅适用于 Tomcat 启动进程(catalina.sh start
– 不会影响 Tomcat 关闭等其他操作
– 适合设置 Tomcat 特定的 JVM 参数

推荐用法

# 推荐:内存参数使用 JAVA_OPTS(通用)
-e JAVA_OPTS="-Xmx2g -Xms2g -XX:MaxMetaspaceSize=256m"

# 可选:Tomcat 特定参数使用 CATALINA_OPTS
-e CATALINA_OPTS="-server -XX:+UseG1GC -Djava.security.egd=file:/dev/./urandom"

# 或者两者结合使用
docker run -d \
  -e JAVA_OPTS="-Xmx2g -Xms2g" \
  -e CATALINA_OPTS="-server -XX:+UseG1GC" \
  your-tomcat-image

相关命令速查

# 查看 Docker 守护进程的 ulimit
cat /proc/`pidof dockerd`/limits | grep files

# 查看容器的 ulimit
docker inspect -f '{{.State.Pid}}' <容器ID>
cat /proc/<PID>/limits | grep files

# 查看当前 shell 的 ulimit
ulimit -n

# 查看系统最大文件句柄数
cat /proc/sys/fs/file-max

# 查看 systemd 版本
systemctl --version

# 查看 Docker 版本
docker --version

# 查看 Docker 服务状态
systemctl status docker

参考资料


总结

本文档记录了 Docker 容器中 JDK8-Tomcat 启动时遇到的文件描述符内存分配失败问题。

核心要点
1. 问题根源是 ulimit nofile 值过大导致 JDK8 内存分配失败
2. 生产环境强烈推荐方案四:配置 JVM 内存参数(-Xmx/-Xms)
3. 可选配合方案二:修改 Docker 守护进程的默认 ulimit 配置作为双重保障
4. 建议将 -Xms-Xmx 设置为相同值,避免运行时动态调整

最佳实践
– ✅ 生产环境必须配置 JVM 内存参数,这是 Java 应用的标准做法
– ✅ 根据应用实际负载合理设置内存大小
– ✅ 定期监控应用内存使用情况,及时调整配置
– ✅ 建立标准化的容器部署流程和配置模板
– ✅ 使用 Docker Compose 或 Kubernetes 统一管理环境变量配置

快速解决方案

# 最简单的解决方法(推荐)
docker run -d \
  -e JAVA_OPTS="-Xmx1g -Xms1g" \
  your-tomcat-image
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇