一、为什么要把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的会话保持机制会面临两大挑战:

  1. 内存中的会话数据会随Pod消失而丢失
  2. 负载均衡需要确保同一用户请求总是落到同一个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"

这里至少有三大问题:

  1. JVM堆内存设置与容器limit完全一致,没给非堆内存留空间
  2. 没考虑容器化环境的CPU限制
  3. 没设置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

关键改进点:

  1. 使用UseContainerSupport让JVM自动感知容器限制
  2. 通过百分比参数动态适应不同规格的Pod
  3. 显式控制元数据区大小
  4. 添加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>

这个方案实现了:

  1. 将容器内日志目录挂载到emptyDir卷
  2. 通过Fluentd实时采集日志并解析多行日志(如Java异常堆栈)
  3. 自动将结构化日志发送到Elasticsearch

五、健康检查不能马虎

K8s的健康检查机制是保障服务稳定的关键,但很多Tomcat部署只做了简单的端口检查:

# 不完善的健康检查配置(技术栈:Kubernetes+YAML)
livenessProbe:
  tcpSocket:
    port: 8080
  initialDelaySeconds: 60
  periodSeconds: 10

这只能证明Tomcat进程在运行,不能证明应用健康。我们应该:

  1. 自定义健康检查接口:
// 健康检查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");
    }
}
  1. 配置完善的就绪和存活探针:
# 完整的健康检查配置(技术栈: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中的黄金法则:

  1. 会话管理:根据业务场景选择合适方案,电商推荐Redis方案,金融类推荐StatefulSet方案
  2. 资源分配:永远不要将JVM堆内存设置为等于容器内存限制,建议预留30%空间
  3. 日志收集:必须实现集中式日志收集,EFK栈是最成熟方案
  4. 健康检查:要区分存活检查和就绪检查,深度检查应该包含所有关键依赖
  5. 滚动更新:配置适当的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

记住,没有放之四海而皆准的配置,一定要根据实际业务负载进行调优。希望这些经验能帮你避开我踩过的那些坑!