从手工高可用到全容器化:我的 Keepalived+Nginx+Tomcat+MySQL 项目迁移实战

背景

去年寒假,我用一个月时间搭建了一套基于 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 漂移后能正常服务。


四、最终验证与高可用测试

  1. 直接访问 Tomcat1:http://192.168.211.128:8081/testdb.jsp → 显示数据库数据 ✅

  2. 通过 VIP 访问:http://192.168.211.100/testdb.jsp → 同样显示数据,刷新会轮询两台 Tomcat ✅

  3. 高可用切换:停止 Master 上的 nginx 容器,VIP 漂移到 Backup,访问 VIP 依然正常 ✅

  4. 模拟 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 外的所有容器,并尝试引入监控和日志收集。

相关推荐
captain3762 小时前
初识MySQL(My structured query language)
数据库·mysql
sugar15692 小时前
Trae快速构建自己项目的docker镜像
docker·容器·trae
新时代牛马2 小时前
Autoexecra — 嵌入式设备的轻量级智能网关
linux
DevilSeagull2 小时前
Linux Vim 文本编辑器基础指南
linux·运维·vim
无忧智库2 小时前
制造业的中枢神经:MES系统如何驱动智慧工厂从“自动化”迈向“自主化”(PPT)
运维·自动化
子木HAPPY阳VIP2 小时前
Ubuntu 22.04 换源+Docker安装+镜像加速
linux·ubuntu·docker
Johnstons2 小时前
多节点网络流量对比分析:优化网络性能的关键策略
运维·网络·网络流量监控·网络流量分析
Elastic 中国社区官方博客2 小时前
Observabilty:自动化错误分诊 - 从被动到自主
大数据·运维·人工智能·elasticsearch·搜索引擎·自动化·全文检索
ShineWinsu2 小时前
对于Linux:基础开发工具(vim、gcc/g++)的介绍
linux·运维·服务器·c++·面试·编辑器·vim