一、为什么要把Tomcat塞进Kubernetes?
咱们先聊聊为啥要把老牌Web容器Tomcat塞进Kubernetes这个时髦的容器编排系统。想象一下你有个传统Java Web应用,以前都是直接扔在物理服务器上跑,现在要享受云原生的红利——自动扩缩容、滚动升级、故障自愈这些高级功能。
举个真实场景:某电商大促期间流量暴涨300%,传统部署方式需要提前准备大量闲置服务器,而Kubernetes只需要这样定义HPA:
# HPA自动扩缩配置示例(技术栈:Kubernetes+YAML)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: tomcat-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: tomcat-web
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
不过要注意,Tomcat默认配置可能扛不住K8s的动态调度。比如session超时设置太短会导致Pod重启时用户掉线,这就引出了我们下个要讨论的问题。
二、会话保持这个老大难问题
在K8s环境下,Pod随时可能被调度或重启。传统Tomcat的会话保持机制会面临两大挑战:
- 内存中的会话数据会随Pod消失而丢失
- 负载均衡需要确保同一用户请求总是落到同一个Pod
解决方案主要有三种:
方案A:使用Redis集中存储会话
<!-- Tomcat的context.xml配置片段(技术栈:Tomcat+Redis) -->
<Context>
<Manager className="org.apache.catalina.session.PersistentManager"
maxIdleBackup="30"
minIdleSwap="60">
<Store className="org.apache.catalina.session.RedisSessionStore"
host="redis-cluster.default.svc.cluster.local"
port="6379"
database="0"
password="${REDIS_PASSWORD}"/>
</Manager>
</Context>
方案B:使用StatefulSet+持久化存储
# StatefulSet配置示例(技术栈:Kubernetes+YAML)
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: tomcat-stateful
spec:
serviceName: "tomcat-service"
replicas: 3
selector:
matchLabels:
app: tomcat
template:
metadata:
labels:
app: tomcat
spec:
containers:
- name: tomcat
image: tomcat:9.0
volumeMounts:
- name: session-data
mountPath: /usr/local/tomcat/work/Catalina
volumeClaimTemplates:
- metadata:
name: session-data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
方案C:使用Ingress的会话亲和性
# Nginx Ingress配置示例(技术栈:Kubernetes+Nginx Ingress)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tomcat-ingress
annotations:
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/session-cookie-name: "route"
nginx.ingress.kubernetes.io/session-cookie-expires: "172800"
nginx.ingress.kubernetes.io/session-cookie-max-age: "172800"
spec:
rules:
- host: tomcat.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: tomcat-service
port:
number: 8080
这三种方案各有优劣:Redis方案最灵活但需要额外中间件,StatefulSet方案最稳定但扩展性较差,Ingress方案最简单但依赖负载均衡器能力。
三、资源配置不当引发的血案
很多团队把Tomcat扔进K8s后,发现经常出现OOM(内存溢出)或者CPU饥饿。根本原因是没根据容器环境调整JVM参数。来看个典型错误案例:
# 错误的Deployment配置示例(技术栈:Kubernetes+YAML)
containers:
- name: tomcat
image: tomcat:9.0
resources:
requests:
memory: "2Gi"
cpu: "1"
limits:
memory: "4Gi"
cpu: "2"
env:
- name: JAVA_OPTS
value: "-Xms2048m -Xmx2048m"
这里至少有三大问题:
- JVM堆内存设置与容器limit完全一致,没给非堆内存留空间
- 没考虑容器化环境的CPU限制
- 没设置MaxMetaspaceSize导致元数据区可能无限增长
正确的姿势应该是:
# 优化后的Deployment配置(技术栈:Kubernetes+YAML)
containers:
- name: tomcat
image: tomcat:9.0
resources:
requests:
memory: "3Gi" # 比JVM堆多预留30%空间
cpu: "1"
limits:
memory: "4Gi"
cpu: "2"
env:
- name: JAVA_OPTS
value: >
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0
-XX:InitialRAMPercentage=50.0
-XX:MaxMetaspaceSize=256m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/tomcat/logs
关键改进点:
- 使用UseContainerSupport让JVM自动感知容器限制
- 通过百分比参数动态适应不同规格的Pod
- 显式控制元数据区大小
- 添加OOM时的诊断支持
四、日志收集这个技术活
传统Tomcat的日志都写在本地文件,在K8s环境下会面临:
- Pod崩溃后日志丢失
- 多实例日志分散难以聚合
- 日志文件占用容器存储空间
成熟的解决方案是EFK(Elasticsearch+Fluentd+Kibana)栈。配置示例:
# 带日志收集的Deployment配置(技术栈:Kubernetes+Fluentd)
containers:
- name: tomcat
image: tomcat:9.0
volumeMounts:
- name: logs
mountPath: /usr/local/tomcat/logs
- name: fluentd-config
mountPath: /fluentd/etc
volumes:
- name: logs
emptyDir: {}
- name: fluentd-config
configMap:
name: fluentd-config
---
# Fluentd的ConfigMap配置
apiVersion: v1
kind: ConfigMap
metadata:
name: fluentd-config
data:
fluent.conf: |
<source>
@type tail
path /usr/local/tomcat/logs/catalina.*.log
pos_file /var/log/fluentd/tomcat-catalina.log.pos
tag tomcat.catalina
<parse>
@type multiline
format_firstline /^\[.*\]/
format1 /^\[(?<time>.*)\] (?<level>[^\s]+) (?<class>[^\s]+) -(?<message>.*)/
</parse>
</source>
<match tomcat.**>
@type elasticsearch
host elasticsearch-logging
port 9200
logstash_format true
</match>
这个方案实现了:
- 将容器内日志目录挂载到emptyDir卷
- 通过Fluentd实时采集日志并解析多行日志(如Java异常堆栈)
- 自动将结构化日志发送到Elasticsearch
五、健康检查不能马虎
K8s的健康检查机制是保障服务稳定的关键,但很多Tomcat部署只做了简单的端口检查:
# 不完善的健康检查配置(技术栈:Kubernetes+YAML)
livenessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
这只能证明Tomcat进程在运行,不能证明应用健康。我们应该:
- 自定义健康检查接口:
// 健康检查Controller示例(技术栈:Java+Spring Boot)
@RestController
@RequestMapping("/health")
public class HealthController {
@GetMapping("/deep")
public ResponseEntity<String> deepCheck() {
// 检查数据库连接
if(!checkDatabase()) return ResponseEntity.status(503).build();
// 检查缓存连接
if(!checkRedis()) return ResponseEntity.status(503).build();
// 检查磁盘空间
if(!checkDiskSpace()) return ResponseEntity.status(507).build();
return ResponseEntity.ok("OK");
}
}
- 配置完善的就绪和存活探针:
# 完整的健康检查配置(技术栈:Kubernetes+YAML)
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 180 # 给JVM预热时间
periodSeconds: 15
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
successThreshold: 2
failureThreshold: 3
六、总结与最佳实践
经过以上讨论,我们总结出Tomcat在K8s中的黄金法则:
- 会话管理:根据业务场景选择合适方案,电商推荐Redis方案,金融类推荐StatefulSet方案
- 资源分配:永远不要将JVM堆内存设置为等于容器内存限制,建议预留30%空间
- 日志收集:必须实现集中式日志收集,EFK栈是最成熟方案
- 健康检查:要区分存活检查和就绪检查,深度检查应该包含所有关键依赖
- 滚动更新:配置适当的maxSurge和maxUnavailable保证更新时的服务可用性
最后给个完整的Deployment模板供参考:
# Tomcat在K8s中的最佳实践配置(技术栈:Kubernetes+YAML)
apiVersion: apps/v1
kind: Deployment
metadata:
name: tomcat-prod
spec:
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 10%
selector:
matchLabels:
app: tomcat
template:
metadata:
labels:
app: tomcat
spec:
containers:
- name: tomcat
image: tomcat:9.0-jdk11
ports:
- containerPort: 8080
resources:
requests:
memory: "3Gi"
cpu: "1"
limits:
memory: "4Gi"
cpu: "2"
env:
- name: JAVA_OPTS
value: >
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0
-XX:InitialRAMPercentage=50.0
-XX:MaxMetaspaceSize=256m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/tomcat/logs
volumeMounts:
- name: logs
mountPath: /usr/local/tomcat/logs
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 180
periodSeconds: 15
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
volumes:
- name: logs
emptyDir:
sizeLimit: 1Gi
记住,没有放之四海而皆准的配置,一定要根据实际业务负载进行调优。希望这些经验能帮你避开我踩过的那些坑!