返回

GKE Autopilot 上 MySQL GCSFuse 权限被拒绝问题解决

mysql

MySQL on GKE Autopilot with GCSFuse: /var/lib/mysql/ 权限被拒绝

这问题看着头大,在 GKE Autopilot 上用 GCSFuse 挂载 MySQL,结果 /var/lib/mysql/ 目录权限不对,MySQL 起不来。别慌,咱来一步步分析解决。

问题原因分析

核心问题就一个:MySQL 启动时,发现数据目录 /var/lib/mysql/ 没法写入,报 "Permission denied"。 这通常有几个可能的原因:

  1. GCSFuse 挂载问题: 即使 Kubernetes 里 PVC 看着是 ReadWriteMany,GCSFuse 实际挂载时可能还是只读的,或者权限设置不对。
  2. 用户权限问题: MySQL 容器内部运行的用户(通常是 mysql,UID 999)可能对 GCSFuse 挂载的目录没有写权限。虽然你用了 initContainerchown,但可能没生效,或者 GCSFuse 有自己的权限控制机制。
  3. GCSFuse 配置问题: GCSFuse 本身的一些参数可能影响了挂载后的权限。
  4. 底层 GCS 权限问题: 确保你的服务账号真的有对 GCS 存储桶的读写权限。 尽管你配置了服务账号的权限, 也要复查。

解决方案

下面针对上面几个原因,给出具体的解决方案。

1. 显式指定 GCSFuse 挂载选项

GCSFuse 提供了一些挂载选项,可以更精细地控制权限。咱可以在 Deployment 里通过 gke-gcsfuse-sidecar 的参数来指定这些选项。

  • 原理: 通过 --file-mode--dir-mode 选项,可以明确指定挂载后的文件和目录权限。 --uid--gid 可以指定挂载目录所属的用户和组。
  • 操作: 修改 Deployment YAML,给 gke-gcsfuse-sidecar 容器添加 args
    • 如果在sidecar 模式下,就需要在应用的 container 的YAML 里面加。
    • 如果在init container模式下,需要在deployment 对应的 initcontainer 里面添加如下信息.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-mysql-deployment
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
      annotations:
        gke-gcsfuse/volumes: "true"
    spec:
      serviceAccountName: spock
      initContainers:
        - name: fix-permissions
          image: busybox
          command: ["sh", "-c", "chown -R 999:999 /var/lib/mysql && chmod -R 777 /var/lib/mysql"]
          securityContext:
            runAsUser: 0
          volumeMounts:
            - name: shared-storage
              mountPath: "/var/lib/mysql"
              subPath: "databases/api"
      containers:
        - name: gke-gcsfuse-sidecar #修改这个容器
          image: gke-gcsfuse-sidecar #镜像名字要对应实际使用的版本
          args:
            - "--file-mode=777"
            - "--dir-mode=777"
            - "--uid=999"
            - "--gid=999"
            #  下面这些设置根据实际情况添加或删除
            - "--implicit-dirs"  # 如果要自动创建不存在的父目录,就加上
            - "--app-name=mysql"
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop:
                - ALL
            runAsUser: 0
            seccompProfile:
              type: RuntimeDefault
          volumeMounts:
          - mountPath: /var/lib/mysql
            mountPropagation: Bidirectional
            name: shared-storage
        - name: mysql
          image: mysql:8.0
          args:
            - "--skip-log-bin"
            - "--skip-ssl"
            - "--ssl=0"
            - "--innodb-flush-method=fsync"
            - "--innodb-use-native-aio=0"
          env:
            - name: MYSQL_ROOT_PASSWORD
              value: "password"
            - name: MYSQL_DATABASE
              value: "api"
            - name: MYSQL_USER
              value: "user"
            - name: MYSQL_PASSWORD
              value: "password"
          volumeMounts:
            - name: shared-storage
              mountPath: "/var/lib/mysql"
              subPath: "databases/api"
      volumes:
        - name: shared-storage
          persistentVolumeClaim:
            claimName: shared-storage-pvc

  • 说明:

    • --file-mode=777--dir-mode=777:给所有用户读、写、执行权限。
    • --uid=999--gid=999:让挂载目录属于 mysql 用户和组(假设你的 MySQL 容器里 mysql 用户的 UID 和 GID 是 999)。
    • --implicit-dirs: GCSFuse 会自动创建需要的父目录。
    • --app-name: 可以设置一个应用名称,方便调试。
  • 安全建议: 把权限设置为 777 虽然简单粗暴,但不太安全。 生产环境最好根据实际需要,设置更精细的权限(比如 770,只允许所属用户和组有全部权限,其他用户没有任何权限)。

2. 调整 initContainer 的策略

你现在的 initContainer 只是简单地 chown,可能不够。 咱可以尝试更全面的权限设置方法。

  • 原理: initContainer 在 MySQL 容器启动 之前 运行,可以用来做一些准备工作。 我们可以在这里更细致地设置权限。
  • 操作: 修改 Deployment YAML,调整 fix-permissions 这个 initContainer
      initContainers:
        - name: fix-permissions
          image: busybox
          command:
            - sh
            - -c
            - |
              mkdir -p /var/lib/mysql/data
              chown -R 999:999 /var/lib/mysql
              chmod -R 777 /var/lib/mysql  # 或者更安全的权限,如 750
              # 确保数据目录存在并且权限正确
              chown 999:999 /var/lib/mysql/data
              chmod 750 /var/lib/mysql/data
          volumeMounts:
            - name: shared-storage
              mountPath: /var/lib/mysql
              subPath: "databases/api" # 确保 initContainer 也挂载了相同的 subPath

  • 说明:
    • mkdir -p /var/lib/mysql/data: 有时候 MySQL 需要 /var/lib/mysql 下面有个 data 子目录。 咱在 initContainer 里提前创建好。
    • 多次 chownchmod: 确保各个目录的权限都设置正确。

3. 使用 GCSFuse 的 --static-mount(如果适用)

如果你的 GCS 存储桶里的文件和目录结构 不会 频繁变化(比如,你不会在 GCS 网页控制台里直接修改数据库文件),可以考虑用 --static-mount

  • 原理: 默认情况下,GCSFuse 是动态挂载的。 每次访问文件时,都会去 GCS 检查一下。 --static-mount 会在启动时把 GCS 存储桶的目录结构 缓存 到本地,后续访问就直接从本地缓存读取,性能更好,也更不容易出现权限问题。
  • 操作:gke-gcsfuse-sidecar 容器添加 --static-mount 参数(跟上面的 --file-mode 等参数一起加):
        - name: gke-gcsfuse-sidecar
          ... # 其他配置
          args:
            - "--file-mode=777"
            - "--dir-mode=777"
            - "--uid=999"
            - "--gid=999"
            - "--implicit-dirs"
            - "--static-mount"  # 加这行
          ... # 其他配置
  • 注意: 用了 --static-mount 后,如果你在 GCS 网页控制台里直接修改了文件或目录,GCSFuse 挂载的目录里 不会 立即看到变化。 需要重启 Pod 才能刷新。

4. 检查服务账号权限 (复查)

  • 原理: 确保 Kubernetes Pod 使用的服务账号,在 Google Cloud 上有足够的权限访问 GCS 存储桶。
  • 操作:
    1. 在 Google Cloud Console 里,找到你的 GKE 集群。
    2. 找到节点池使用的服务账号。
    3. 确保这个服务账号有 roles/storage.objectAdminroles/storage.admin 角色。

也可以使用 gcloud 命令行工具检查:

gcloud iam service-accounts describe YOUR_SERVICE_ACCOUNT_EMAIL --format="value(displayName)"
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
    --member="serviceAccount:YOUR_SERVICE_ACCOUNT_EMAIL" \
    --role="roles/storage.objectAdmin"

(把 YOUR_SERVICE_ACCOUNT_EMAILYOUR_PROJECT_ID 替换成实际的值)

  • 如果你使用了Workload Identity。 检查Kubernetes Service Account 和 Google Service Account之间的绑定关系:
gcloud iam service-accounts get-iam-policy \
  GOOGLE_SERVICE_ACCOUNT@PROJECT_ID.iam.gserviceaccount.com \
  --project=PROJECT_ID

确认有类似下面的输出:

- members:
  - serviceAccount:PROJECT_ID.svc.id.goog[NAMESPACE/KUBERNETES_SERVICE_ACCOUNT]
  role: roles/iam.workloadIdentityUser

5. GCSFuse 进阶:单独的 Sidecar 容器(可选)

如果上面这些方法还不行,可以考虑把 GCSFuse 放到一个 单独的 Sidecar 容器里,而不是跟 MySQL 容器放一起。 这样可以更精细地控制 GCSFuse 的启动和配置。

  • 原理: 将 GCSFuse 的职责分离出来,可以更好地隔离问题,也方便单独调试。

  • 这个操作比较复杂, 而且要充分了解sidecar 模式的配置。 一般情况下不需要这样做.

总结

解决 MySQL on GKE Autopilot with GCSFuse 权限问题,关键是搞清楚 GCSFuse 的挂载机制,以及 Kubernetes 里的权限控制。 上面这几招,一般来说组合起来用,就能解决问题。
记住每次修改后要重启 Pod(kubectl rollout restart deployment/api-mysql-deployment)才能生效。

Good luck! 祝你早日搞定!