2. 应用容器化改造:传统应用拆解、无状态化改造、配置外置、日志处理标准化
好,咱们进入正题。这一章聊的是容器化改造中最核心的四个动作:拆解、无状态化、配置外置、日志标准化。说白了,就是把一个“老古董”应用,改造成能在K8s里跑得欢的“新青年”。我这些年经手过不少改造项目,踩过的坑比吃过的盐还多,今天把这些经验掰开揉碎了讲给你听。
2.1 传统应用拆解:从“大泥球”到“微服务”
很多传统应用,就是一个巨大的单体。所有功能揉在一起,耦合度极高。你想想看,改一行代码,整个应用都得重新部署,风险大、效率低。
拆解的原则是什么?
- 按业务边界拆:比如电商系统,可以拆成用户、商品、订单、支付等独立服务。每个服务有自己的数据库,别共享。
- 按变更频率拆:经常变动的模块(比如营销活动)单独拆出来,稳定模块(比如基础数据)保持不动。
- 按资源消耗拆:CPU密集型的计算模块和IO密集型的查询模块,拆开后可以独立扩缩容。
核心要点:拆解不是越细越好。我见过一个项目,硬生生拆了上百个微服务,结果运维成本爆炸。建议先从2-3个服务开始,逐步演进。
举个例子,一个传统的Java单体应用,里面既有用户登录,又有订单处理,还有报表生成。改造时,我会先把报表生成拆出来,因为它最耗资源,而且变更少。拆出来后,报表服务可以独立部署,用独立的资源池,互不影响。
2.2 无状态化改造:让应用“随遇而安”
容器化最大的敌人是什么?是状态。容器随时可能被销毁、重建、迁移。如果你的应用把用户会话、临时文件、缓存数据都留在本地,那容器一重启,数据就丢了。
无状态化的核心思想:应用本身不存储任何持久化数据,所有状态都交给外部中间件。
- 会话数据:从本地内存迁移到Redis或Memcached。我习惯用Redis,性能好,还支持持久化。
- 文件上传:别存本地磁盘,用对象存储(比如MinIO、AWS S3)或共享文件系统(NFS)。
- 临时缓存:用分布式缓存,别用本地HashMap。
小技巧:改造时,可以先用环境变量控制“是否启用本地存储”。测试环境用本地,生产环境切到外部存储。这样风险可控,逐步验证。
我曾经遇到一个项目,应用里大量使用本地文件系统做临时存储。改造时,我把这些临时文件全部改成了写到Redis的临时Key,设置TTL自动过期。嗯,效果不错,容器随便重启,数据不丢。
2.3 配置外置:把“硬编码”赶出代码
传统应用里,数据库连接、API密钥、日志级别这些配置,经常硬编码在代码里,或者写在配置文件中,和代码一起打包。容器化后,这种做法的弊端就暴露了:
- 不同环境(开发、测试、生产)需要不同配置,改代码太麻烦。
- 敏感信息(密码、密钥)暴露在镜像里,不安全。
配置外置的标准做法:
- 环境变量:最简单的方式。K8s里通过ConfigMap和Secret注入。比如数据库地址、端口。
- 配置文件挂载:复杂的配置(比如Nginx、Logback)用ConfigMap挂载成文件。
- 配置中心:大型项目用Apollo、Nacos等配置中心,支持动态刷新,不用重启应用。
注意:环境变量不要太多,超过20个就不好管理了。我建议把核心参数用环境变量,复杂配置用配置文件挂载。
举个例子,改造前,代码里写死了数据库连接:
// 改造前:硬编码
String dbUrl = "jdbc:mysql://192.168.1.100:3306/mydb";
String dbUser = "root";
String dbPass = "123456";
改造后,从环境变量读取:
// 改造后:配置外置
String dbUrl = System.getenv("DB_URL");
String dbUser = System.getenv("DB_USER");
String dbPass = System.getenv("DB_PASS");
然后在K8s的Deployment里,通过ConfigMap和Secret注入:
env:
- name: DB_URL
valueFrom:
configMapKeyRef:
name: app-config
key: db.url
- name: DB_PASS
valueFrom:
secretKeyRef:
name: app-secret
key: db.password
2.4 日志处理标准化:让日志“流”起来
传统应用里,日志通常写到本地文件。容器化后,容器一销毁,日志就没了。而且你没法用`kubectl logs`查看多个Pod的日志。所以,日志必须标准化。
标准化的三个步骤:
- 输出到标准输出/标准错误:应用把日志写到stdout/stderr,Docker会自动收集。这是最推荐的方式。
- 结构化日志:用JSON格式输出,包含时间、级别、模块、请求ID等字段。方便后续用ELK、Loki等工具检索。
- 日志轮转与归档:如果必须写文件,确保日志文件有大小和时间的轮转策略,避免磁盘写满。
避坑指南:我曾经遇到一个项目,日志量巨大,直接写stdout导致Docker日志驱动撑爆磁盘。后来加了日志轮转,并限制每个容器日志最大100MB,问题解决。
举个例子,改造前,日志可能是这样的:
2024-01-15 10:30:00 INFO 用户登录成功,用户ID: 12345
改造后,输出JSON格式:
{"timestamp":"2024-01-15T10:30:00Z","level":"INFO","module":"user","message":"用户登录成功","userId":12345,"requestId":"abc-123"}
这样,用`kubectl logs`查看时,可以配合`grep`或`jq`快速过滤。配合Loki或Elasticsearch,还能做全文检索和可视化。
2.5 改造实战:一个完整的例子
咱们用一个简单的Java Spring Boot应用来演示整个改造流程。
改造前:单体应用,本地存储会话,硬编码配置,日志写文件。
改造后:拆成用户服务和订单服务,会话存Redis,配置用环境变量,日志输出到stdout。
| 改造项 | 改造前 | 改造后 |
|---|---|---|
| 应用架构 | 单体 | 用户服务 + 订单服务 |
| 会话存储 | 本地内存 | Redis |
| 数据库配置 | 硬编码在application.properties | 环境变量注入 |
| 日志输出 | 写入/var/log/app.log | 输出到stdout,JSON格式 |
改造后的Dockerfile示例:
FROM openjdk:11-jre-slim
COPY target/user-service.jar /app/
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/user-service.jar"]
注意,这里没有配置文件和日志路径。所有配置通过环境变量传入,日志直接输出到控制台。
个人建议:改造时,不要一次性全改完。先改一个模块,验证没问题了,再改下一个。我习惯用“绞杀者模式”,逐步替换,风险最低。
好了,这一章的内容就这些。核心就是四个字:拆、无、配、日。记住了,容器化改造不是技术炫技,而是为了让应用更稳定、更弹性、更易运维。下一章,咱们聊聊镜像构建的优化技巧。