背景
去年寒假,我用一个月时间搭建了一套基于 Keepalived + Nginx + Tomcat + MySQL 的高可用集群,并写成了博客。这套架构运行在五台虚拟机上,实现了 VIP 漂移、Nginx 反向代理、Tomcat 集群和 MySQL 数据持久化。
当时我觉得"能跑起来"就是成功。但真正进入实习、投递大厂后,我意识到:手工部署是基础,容器化才是企业级门槛。于是决定把这个项目完全容器化,只保留 Keepalived 运行在宿主机。整个过程踩了无数坑,今天记录下完整的迁移过程和排错经验。
项目原架构
· VIP: 192.168.211.100 · Master Nginx: 192.168.211.134 · Backup Nginx: 192.168.211.135 · Tomcat1: 192.168.211.128,部署了 index.jsp 和 testdb.jsp · Tomcat2: 192.168.211.133 · MySQL: 192.168.211.136,数据库 mydb,表 servers
所有组件均运行在宿主机上,通过物理 IP 通信。目标是:将 MySQL、Tomcat、Nginx 全部容器化,Keepalived 继续留在宿主机。
一、MySQL 容器化(第一波踩坑)
1.1 安装 Docker & 拉取镜像
在 MySQL 主机上:
yum install -y yum-utils
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum install -y docker-ce docker-ce-cli containerd.io
systemctl start docker
docker pull mysql:5.7
1.2 停止宿主机 MySQL 并备份数据
systemctl stop mysqld
cp -a /var/lib/mysql /var/lib/mysql.bak
1.3 启动 MySQL 容器
想保留原有数据,直接挂载数据目录:
docker run -d --name mysql --net=host \
-e MYSQL_ROOT_PASSWORD=AppPass123! \
-v /var/lib/mysql:/var/lib/mysql \
mysql:5.7
第一个错误:容器启动后立即退出,docker logs mysql 报错:
[ERROR] [FATAL] InnoDB: Table flags are 0 in the data dictionary but the flags in file ./ibdata1 are 0x4800!
原因是宿主机 MySQL 版本是 8.0,而容器用了 5.7,数据文件不兼容。改用 MySQL 8.0 镜像:
docker run -d --name mysql --net=host \
-e MYSQL_ROOT_PASSWORD=AppPass123! \
-v /var/lib/mysql:/var/lib/mysql \
mysql:8.0
第二个错误:日志显示:
[ERROR] [MY-012526] [InnoDB] Upgrade is not supported after a crash or shutdown with innodb_fast_shutdown = 2.
数据目录被 5.7 和 8.0 混用,redo log 损坏。教训:数据目录一旦被高版本打开,低版本无法读取;混用会导致不可逆损坏。
1.4 最终方案:放弃旧数据,全新初始化
docker rm -f mysql
rm -rf /var/lib/mysql
mkdir /var/lib/mysql
chown 999:999 /var/lib/mysql # 容器内 mysql 用户 UID=999
docker run -d --name mysql --net=host \
-e MYSQL_ROOT_PASSWORD=AppPass123! \
-v /var/lib/mysql:/var/lib/mysql \
mysql:5.7
这次容器正常启动。手动创建数据库和表(按 JSP 需求):
CREATE DATABASE testdb;
USE testdb;
CREATE TABLE server (
id INT PRIMARY KEY,
hostname VARCHAR(50),
status VARCHAR(20),
last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO server VALUES (1, 'tomcat1', 'active', NOW());
INSERT INTO server VALUES (2, 'tomcat2', 'active', NOW());
GRANT ALL PRIVILEGES ON testdb.* TO 'appuser'@'%' IDENTIFIED BY 'AppPass123!';
FLUSH PRIVILEGES;
二、Tomcat 容器化(驱动与 JSP 打包)
2.1 准备 JSP 文件和 Dockerfile
在 Tomcat1 主机上创建 /dockerdemo1/ROOT/,放入 index.jsp 和 testdb.jsp。testdb.jsp 最初连接 localhost:3306,但容器化后应指向 MySQL 主机 IP:
String url = "jdbc:mysql://192.168.211.136:3306/testdb?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true";
String user = "appuser";
String password = "AppPass123!";
查询表名为 server。
Dockerfile 内容:
FROM tomcat:11.0.18-jdk17
RUN rm -rf /usr/local/tomcat/webapps/ROOT
COPY ROOT /usr/local/tomcat/webapps/ROOT
ADD https://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.49/mysql-connector-java-5.1.49.jar /usr/local/tomcat/lib/
EXPOSE 8080
CMD ["catalina.sh", "run"]
2.2 构建并启动
docker build -t my-tomcat:1.0 .
docker run -d --name tomcat1 --restart always -p 8081:8080 my-tomcat:1.0
遇到的错误:
· 启动后访问 /testdb.jsp 报 ClassNotFoundException: com.mysql.jdbc.Driver → 添加驱动 JAR 到 lib 解决。 · 访问 /testdb.jsp 报 Access denied for user 'appuser'@'...' → MySQL 未创建该用户或密码错误,创建用户并授权。 · 表不存在:JSP 中查的是 server 表,但 MySQL 里是 servers → 统一表名为 server,重新建表。
2.3 Tomcat2 相同操作
将 /dockerdemo1 目录及镜像拷贝到 Tomcat2 主机,重复构建和启动,使用相同镜像名和端口映射。
三、Nginx 容器化(反向代理配置)
3.1 准备配置文件
在 Master 主机上创建 /dockerdemo1/nginx.conf(全局配置)和 /dockerdemo1/tomcat.conf。
tomcat.conf 关键内容:
upstream web {
server 192.168.211.128:8081;
server 192.168.211.133:8081;
}
server {
listen 80;
server_name 192.168.211.100;
location / {
proxy_pass http://web;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
Dockerfile:
FROM nginx:1.28.1
COPY nginx.conf /etc/nginx/nginx.conf
COPY tomcat.conf /etc/nginx/conf.d/
EXPOSE 80
3.2 构建镜像并启动
docker build -t my-nginx:1.0 .
docker run -d --name nginx --net=host --restart always my-nginx:1.0
错误:启动后访问 VIP 超时。检查发现宿主机 nginx 服务还在运行,占用 80 端口。彻底停止并屏蔽:
systemctl stop nginx
systemctl disable nginx
systemctl mask nginx
pkill -9 nginx
再次启动容器,curl http://192.168.211.100 成功返回 Tomcat 页面。
3.3 Backup 主机相同操作
在 Backup 主机上重复构建和启动,确保 VIP 漂移后能正常服务。
四、最终验证与高可用测试
-
直接访问 Tomcat1:http://192.168.211.128:8081/testdb.jsp → 显示数据库数据 ✅
-
通过 VIP 访问:http://192.168.211.100/testdb.jsp → 同样显示数据,刷新会轮询两台 Tomcat ✅
-
高可用切换:停止 Master 上的 nginx 容器,VIP 漂移到 Backup,访问 VIP 依然正常 ✅
-
模拟 Tomcat 故障:停止 Tomcat1 容器,刷新页面只看到 Tomcat2 响应 ✅
五、踩坑总结
问题 原因 解决方案 MySQL 容器启动失败 数据目录版本不兼容(5.7 vs 8.0) 清空目录,全新初始化 MySQL 权限错误 用户未创建或密码不匹配 创建 appuser 并授权 Tomcat 缺少 JDBC 驱动 镜像未包含驱动 JAR ADD 驱动到 /usr/local/tomcat/lib/ 容器间网络不通 JSP 使用 localhost:3306 改为 MySQL 主机 IP 表不存在 JSP 查询表名与数据库表名不一致 统一为 server 表 Nginx 容器无法监听 80 宿主机 nginx 占用端口 停止并 mask 宿主机服务 Nginx 代理超时 默认超时过短 增加 proxy_read_timeout
六、最终架构
· Keepalived:宿主机,管理 VIP · Nginx:容器化,--net=host,监听 80 端口 · Tomcat:容器化,端口映射 8081:8080,内嵌 MySQL 驱动 · MySQL:容器化,--net=host,数据目录持久化
所有容器通过物理 IP 通信,Keepalived 保证高可用。
七、写在最后
从手工部署到容器化,我花了整整一周时间(大部分在踩坑)。最大的收获不是技术本身,而是如何系统地排查问题:看日志、拆解现象、逐一验证假设。容器化让环境变得可移植、可复现,但也带来了网络、权限、存储的新挑战。如果你也在做类似迁移,希望这篇记录能帮你少踩几个坑。
下一步我打算加入 Docker Compose 管理除 Keepalived 外的所有容器,并尝试引入监控和日志收集。