在 Kubernetes 部署

架构概述

DataAgent 在 K8s 环境部署时需要注意以下有状态组件:

组件存储路径说明
应用数据文件/app/nocobase/storage应用数据文件,包括上传文件、日志文件、AI生成的数据报告等
应用运行时目录/app/nocobase/应用启动过程中的临时运行时文件,包含 PM2 运行时数据

关键点/app/nocobase/storage 必须使用 PVC 持久化存储,否则 Pod 重启会导致上传文件等数据丢失; 如果要求镜像不允许写入文件系统,那么/app/nocobase 目录本身应挂载到 emptyDir,不然可以不挂载。

部署配置

1. 存储声明 (PVC)

创建支持多 Pod 共享读写的存储卷:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: yiask-storage
  namespace: default
spec:
  accessModes:
    - ReadWriteMany  # 多 Pod 共享读写
  resources:
    requests:
      storage: 100Gi  # 根据实际需求调整
  # storageClassName: nfs  # 使用支持 RWX 的 StorageClass

StorageClass 选择

  • NFS:适合小规模部署
  • Ceph Rook:适合生产环境,高可用
  • 云存储:AWS EFS、阿里云 NAS 等

2. 部署 (Deployment)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: yiask
  namespace: default
  labels:
    app: yiask
spec:
  replicas: 2  # 根据负载调整
  selector:
    matchLabels:
      app: yiask
  template:
    metadata:
      labels:
        app: yiask
    spec:
      volumes:
        - name: nocobase-root
          emptyDir: {}  # 2.0.6 起 /app/nocobase 根目录使用临时可写卷
        - name: storage
          persistentVolumeClaim:
            claimName: yiask-storage
      containers:
        - name: yiask
          image: your-registry/yiask/dataagent:latest
          ports:
            - containerPort: 13000
          env:
            # ===== 必需环境变量 =====
            - name: APP_ENV
              value: production
            - name: APP_KEY
              valueFrom:
                secretKeyRef:
                  name: yiask-secrets
                  key: app-key
            - name: DB_DIALECT
              value: postgres
            - name: DB_HOST
              value: postgres-service  # 替换为实际数据库地址
            - name: DB_PORT
              value: "5432"
            - name: DB_DATABASE
              valueFrom:
                secretKeyRef:
                  name: yiask-secrets
                  key: db-database
            - name: DB_USER
              valueFrom:
                secretKeyRef:
                  name: yiask-secrets
                  key: db-user
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: yiask-secrets
                  key: db-password

            # ===== 集群模式配置(K8s 环境必需)=====
            - name: CLUSTER_MODE
              value: "1"
            # 说明:K8s 环境中多进程能力由 Pod 副本提供,因此 CLUSTER_MODE 建议设为 1

            # ===== 缓存配置(多 Pod 必需使用 Redis)=====
            - name: CACHE_DEFAULT_STORE
              value: redis

            # 方案 A:单机 / Service 暴露的 Redis
            - name: REDIS_URL
              value: redis://redis-service:6379/0

            # 方案 B:外部 Redis Sentinel(二选一;启用时删除上面的 REDIS_URL)
            # - name: REDIS_MODE
            #   value: sentinel
            # - name: REDIS_SENTINEL_NAME
            #   value: mymaster
            # - name: REDIS_SENTINEL_NODES
            #   value: redis-sentinel-0:26379,redis-sentinel-1:26379,redis-sentinel-2:26379
            # - name: REDIS_USERNAME
            #   valueFrom:
            #     secretKeyRef:
            #       name: yiask-secrets
            #       key: redis-username
            # - name: REDIS_PASSWORD
            #   valueFrom:
            #     secretKeyRef:
            #       name: yiask-secrets
            #       key: redis-password
            # - name: REDIS_DB
            #   value: "0"

            # ===== 日志配置(输出到 stdout,配合 K8s 日志采集)=====
            - name: LOGGER_TRANSPORT
              value: console
            - name: LOGGER_LEVEL
              value: info

          volumeMounts:
            - name: storage
              mountPath: /app/nocobase/storage  # 先挂载子目录 PVC
            - name: nocobase-root
              mountPath: /app/nocobase          # 再挂载根目录 emptyDir
          resources:
            requests:
              memory: "1Gi"
              cpu: "500m"
            limits:
              memory: "4Gi"
              cpu: "2000m"
          livenessProbe:
            httpGet:
              path: /api/v1/__health_check
              port: 13000
            initialDelaySeconds: 60
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /api/v1/__health_check
              port: 13000
            initialDelaySeconds: 30
            periodSeconds: 5

3. Service

apiVersion: v1
kind: Service
metadata:
  name: yiask-service
  namespace: default
spec:
  selector:
    app: yiask
  ports:
    - port: 80
      targetPort: 13000
  type: ClusterIP

4. Ingress

注意:DataAgent 使用 SSE (Server-Sent Events) 进行流式响应,需要增加超时时间。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: yiask-ingress
  namespace: default
  annotations:
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - yiask.example.com
      secretName: yiask-tls
  rules:
    - host: yiask.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: yiask-service
                port:
                  number: 80

5. Secret

apiVersion: v1
kind: Secret
metadata:
  name: yiask-secrets
  namespace: default
type: Opaque
stringData:
  app-key: "your-random-app-key-min-32-chars"
  db-database: "yiask"
  db-user: "yiask"
  db-password: "your-db-password"

会话亲和性

为了优化性能,可以配置同一会话的请求路由到同一个 Pod:

# 在 Service 中添加
spec:
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 3600  # 1 小时

说明:这不是必需的,因为 PVC 已经实现了数据共享。但配置会话亲和性可以减少跨 Pod 访问文件的开销。

依赖服务

PostgreSQL (必需)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:17
          env:
            - name: POSTGRES_DB
              value: yiask
            - name: POSTGRES_USER
              value: yiask
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: yiask-secrets
                  key: db-password
          volumeMounts:
            - name: postgres-data
              mountPath: /var/lib/postgresql/data
      volumes:
        - name: postgres-data
          persistentVolumeClaim:
            claimName: postgres-storage
---
apiVersion: v1
kind: Service
metadata:
  name: postgres-service
spec:
  selector:
    app: postgres
  ports:
    - port: 5432

Redis (必需)

用于多 Pod 间缓存共享,必需。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
        - name: redis
          image: redis:7-alpine
          ports:
            - containerPort: 6379
---
apiVersion: v1
kind: Service
metadata:
  name: redis-service
spec:
  selector:
    app: redis
  ports:
    - port: 6379

如果使用外部 Redis Sentinel,而不是在当前集群内自行部署单节点 Redis,则可以跳过本节的 redis Deployment / Service,改为在应用 Deployment 中配置 REDIS_MODE=sentinelREDIS_SENTINEL_NAMEREDIS_SENTINEL_NODES 等环境变量。变量说明见 环境变量

只读文件系统的容器

部分客户的生产环境会将容器文件系统设置为只读。对于 2.0.6 及以上版本,建议按下面方式拆分挂载:

注意/app/nocobase/storage/app/nocobase 是父子目录,配置 volumeMounts 时必须先挂载 /app/nocobase/storage,再挂载 /app/nocobase,否则子目录挂载可能被父目录覆盖。新版本中 PM2 运行时已经位于 /app/nocobase 下,不需要再单独配置 pm2-home

1. 映射 /app/nocobase/storage 到 PVC

/app/nocobase/storage 继续挂载到原有 PVC,可持久化上传文件、日志等业务数据:

volumes:
  - name: storage
    persistentVolumeClaim:
      claimName: yiask-storage
---
volumeMounts:
  - name: storage
    mountPath: /app/nocobase/storage

2. 映射 /app/nocobaseemptyDir

/app/nocobase 根目录挂载到 emptyDir,用于存储应用启动和运行过程中的临时可写文件:

volumes:
  - name: nocobase-root
    emptyDir: {}
---
volumeMounts:
  - name: nocobase-root
    mountPath: /app/nocobase

完整配置示例

spec:
  volumes:
    - name: storage
      persistentVolumeClaim:
        claimName: yiask-storage
    - name: nocobase-root
      emptyDir: {}
  containers:
    - name: yiask
      # ... 其他配置
      volumeMounts:
        - name: storage
          mountPath: /app/nocobase/storage
        - name: nocobase-root
          mountPath: /app/nocobase

故障排查

Pod 无法启动

  1. 检查 PVC 是否已绑定:

    kubectl get pvc
    kubectl describe pvc yiask-storage
  2. 检查存储类是否支持 ReadWriteMany

Pod 报错 Pod ephemeral local storage usage exceeds the total limit of containers 1Gi

这个报错通常表示容器可用的临时本地存储(ephemeral local storage)不够。DataAgent 在启动时会先解压运行所需的代码和运行时文件,如果 /app/nocobase 落在容器的临时本地存储上,而 ephemeral-storage 限制又比较小,就可能在启动阶段直接触发这个错误。

可以按下面两种方式处理:

  1. 提升容器的 ephemeral-storage 限制,建议至少调到 5Gi

    resources:
      requests:
        memory: "1Gi"
        cpu: "500m"
        ephemeral-storage: "1Gi"
      limits:
        memory: "4Gi"
        cpu: "2000m"
        ephemeral-storage: "5Gi"
  2. /app/nocobase 直接挂载到 PVC,避免解压后的代码和运行文件继续占用容器本地临时存储。

    这种方式下,/app/nocobase/storage 不需要再单独挂载到 PVC,因为它已经包含在 /app/nocobase 里面了;但是 /app/nocobase/runtime 仍然建议单独挂载到 emptyDir,用于保存运行时临时文件。

    注意/app/nocobase/runtime/app/nocobase 是父子目录,配置 volumeMounts 时必须先挂载 /app/nocobase/runtime,再挂载 /app/nocobase,保证子目录挂载优先级更高,不会被父目录覆盖。

    spec:
      volumes:
        - name: nocobase-runtime
          emptyDir: {}
        - name: nocobase-data
          persistentVolumeClaim:
            claimName: yiask-storage
      containers:
        - name: yiask
          # ... 其他配置
          volumeMounts:
            - name: nocobase-runtime
              mountPath: /app/nocobase/runtime
            - name: nocobase-data
              mountPath: /app/nocobase

如果当前环境已经有可用 PVC,优先建议采用第二种方式。这样既能绕开临时存储容量不足的问题,也能减少启动阶段因为解压导致的本地磁盘占用。

文件丢失

  1. 确认 /app/nocobase 已挂载到 emptyDir/app/nocobase/storage 已挂载到 PVC
  2. 检查 Pod 重启后文件是否存在

SSE 连接中断

  1. 检查 Ingress timeout 配置
  2. 检查负载均衡器(如 AWS ALB)的 idle timeout 设置