无需修改代码,将应用改造为使用云存储
我最近在一台服务器上安装了一个媒体分享应用。安装过程很简单,但它为长期维护埋下了一个阴险的陷阱。
每当用户上传文件时,Web 应用都会将其保存到本地文件系统。如果我以后销毁服务器并重新构建,就必须手动备份和恢复每一个文件。更好的架构应该是让应用将文件写入独立的存储服务器,但我不想花几个月重写应用来实现这一点。
借助 Docker、Google Cloud Storage 以及 gcsfuse 工具,我无需修改应用中的任何一行代码,就实现了这种分离。
在本教程中,我将展示如何以尽量少地修改源代码的方式,将传统应用改造为使用 Google Cloud Storage。
术语说明
如果你不熟悉 Google Cloud Platform,下面是本教程中需要了解的几个缩写:
| 缩写 | 全称 | 它是什么? | 类似于 |
|---|---|---|---|
| GCS | Google Cloud Storage | Google 的云存储服务 | Amazon S3 |
| GCE | Google Compute Engine | Google 的按需虚拟机服务 | Amazon EC2 |
| GCR | Google Container Registry | Google 用于托管 Docker 镜像的服务 | Docker Hub |
| GCP | Google Cloud Platform | Google 的云计算平台(GCS、GCE 和 GCR 都是 GCP 的组成部分) | Amazon Web Services 或 Microsoft Azure |
Docker 允许开发者为应用构建可以在任何地方运行的自包含环境:
- Docker 镜像是运行应用所需的全部文件,包括操作系统和所有第三方依赖项。
- Docker 容器是 Docker 镜像实际执行时所处的运行环境。
在本教程中,你可以把 Docker 容器理解为轻量级虚拟机,尽管从技术上说它们并不是虚拟机。
前置条件
要跟随我的示例操作,你需要以下工具:
我的示例应用
我为本教程创建了一个示例项目。这是一个基于 Flask 框架上传文档构建的极其简单的 Web 应用。
运行它需要输入以下命令:
git clone https://github.com/mtlynch/flask_upload_demo.git
cd flask_upload_demo
sudo pip install -r requirements.txt
gunicorn \
demo.app:app \
--bind 0.0.0.0:5000这个应用允许你选择并上传文件:

flask-upload-demo 的上传页面
随后,它会通过 URL http://[server address]:5000/uploads/[filename] 永久提供该文件:

flask-upload-demo 提供已上传的图像
在服务器中可以看到,应用将文件保存到了本地文件系统的 demo/uploads 文件夹:
$ ls -l demo/uploads/
-rw-rw-r-- 1 mike mike 230720 Nov 24 21:45 Space_Duck_Desktop_RGB_PNG.png将示例应用 Docker 化
由于这个应用只有几个简单的依赖项,为它创建 Docker 镜像很容易:
Dockerfile
FROM debian:stretch
RUN apt-get update
RUN apt-get install --yes \
git-core \
python \
python-pip \
python-virtualenv
# Create demo user system account.
ARG APP_USER="demo-user"
ARG APP_GROUP="demo-user"
ARG APP_HOME_DIR="/home/demo-user"
RUN set -x && \
groupadd "$APP_GROUP" && \
useradd \
--comment "Demo app system account" \
--home-dir "$APP_HOME_DIR" \
--create-home \
--system \
--gid "$APP_GROUP" \
"$APP_USER"
# Create directory for app source code.
ARG APP_ROOT="/srv/demo-app"
RUN mkdir --parents "$APP_ROOT" && \
chown \
--no-dereference \
--recursive \
"${APP_USER}:${APP_GROUP}" "$APP_ROOT"
USER "$APP_USER"
WORKDIR "$APP_ROOT"
# Install demo app.
ARG DEMO_APP_REPO="https://github.com/mtlynch/flask_upload_demo"
RUN set -x && \
git clone "$DEMO_APP_REPO" . && \
virtualenv VIRTUAL && \
. VIRTUAL/bin/activate && \
pip install --requirement requirements.txt
EXPOSE 5000
# Run demo app.
ENV FLASK_APP "demo/app.py"
CMD virtualenv VIRTUAL && \
. VIRTUAL/bin/activate && \
gunicorn \
demo.app:app \
--bind 0.0.0.0:5000 \
--log-level info上面的 Dockerfile 执行了几项高层次任务来准备镜像:
- 安装 Git、Python 及相关软件包
- 创建一个系统账户(
demo-user),以有限权限运行应用 - 将应用源代码仓库克隆到本地
- 添加一个
CMD,在 5000 端口启动示例应用
你可以克隆我的仓库,在本地构建 Docker 容器,以测试这个 Dockerfile:
cd ~
git clone https://github.com/mtlynch/docker-flask-upload-demo.git
cd docker-flask-upload-demo
docker build \
--tag demo-app-image \
.
docker run \
--detach \
--publish 80:5000 \
--name demo-app \
demo-app-image在浏览器中访问 http://localhost/,就能看到示例应用。
更贴近实际的 Docker 镜像
大多数 Web 应用不会直接接受来自浏览器的流量。相反,它们会使用 Nginx 或 Apache 这样的 HTTP 服务器来处理通用任务(例如负载均衡、提供静态文件),而后端则处理应用特有的逻辑。
我在我的 Docker 仓库中添加了 nginx 分支,用来演示更接近真实应用使用场景的 Docker 镜像。相关改动如下:
# Install nginx.
ARG NGINX_GROUP="www-data"
COPY nginx.conf /etc/nginx/sites-enabled/nginx.conf
RUN set -x && \
apt-get install --yes \
nginx \
sudo && \
rm /etc/nginx/sites-enabled/default && \
usermod --append --groups "$NGINX_GROUP" "$APP_USER" && \
echo "$APP_USER ALL=(ALL:ALL) NOPASSWD: /usr/sbin/nginx" >> /etc/sudoersNginx 通常以 root 身份运行,这样才能监听特权 HTTP 端口 80 和 443。因此,Dockerfile 使用 sudo 允许示例应用用户以 root 身份启动 Nginx,同时让它执行其他活动时仍保持有限权限。
最后,它会将一个 Nginx 配置文件复制到容器中:
nginx.conf
server {
listen 80;
server_name example.org; # Replace with your server's domain name
client_max_body_size 20m;
# Serve static resources directly (bypass backend).
location /uploads/ {
alias /srv/demo-app/demo/uploads/;
}
# Forward all other requests to the application backend.
location / {
proxy_pass http://127.0.0.1:5000;
}
}location /uploads { ... } 块允许 Nginx 直接从 /uploads 文件夹提供文件,而不是将请求转发给 flask-upload-demo 后端。这是一种常见配置,因为 Nginx 处理静态文件的速度和效率都高于应用后端。
你可以使用下面的命令运行带 Nginx 的 flask-upload-demo:
cd ~/docker-flask-upload-demo
git checkout nginx
docker build \
--tag demo-app-image \
.
docker run \
--detach \
--publish 80:80 \
--name demo-app \
demo-app-image应用会再次出现在 http://localhost/。其行为与前一个版本完全相同,只是提供上传文件时占用的资源更少。
准备 GCP 项目
在本地部署应用很不错,但将其发布到云端更令人兴奋,因为所有用户都可以访问它。你需要执行几步操作,配置系统以便部署到 GCP。
首先,在 gcloud 中指定 GCP 项目的名称:
PROJECT_ID="ENTER-YOUR-PROJECT-ID-HERE"
gcloud config set project "$PROJECT_ID"接下来,使用 GCP Web 控制台创建一个服务账户,并为其分配 owner 角色:
陷阱提示:由于 GCP 中一个疑似存在的 Bug,如果你使用 GCP 根账户(例如你的 @gmail.com 账户),或者使用通过 gcloud 创建的服务账户,将 Docker 镜像推送到 gcr.io(见下一节)时会失败。

从 GCP Web 控制台创建新的服务账户

为服务账户分配角色
将私钥下载为 key.json:

下载服务账户的私钥
最后,使用 gcloud 以刚创建的服务账户身份进行认证:
gcloud auth activate-service-account --key-file key.json完成认证后,还需要运行几个命令,为本教程后续步骤准备项目:
# Enable APIs you'll need for this workflow.
gcloud services enable \
cloudresourcemanager.googleapis.com \
compute.googleapis.com \
containerregistry.googleapis.com \
iam.googleapis.com
# Enable gcloud to provide credentials for Docker image pushes.
gcloud auth configure-docker --quiet将镜像上传到 Google Container Registry
在将 Docker 镜像部署到 GCE 之前,需要先将其发布到 Docker 镜像托管服务。GCR 是 Google Cloud 集成的 Docker 镜像托管服务,因此是最简单的选择。
要将 Docker 镜像上传到 GCR,先切换到示例仓库的 nginx 分支:
cd ~/docker-flask-upload-demo
git checkout nginx现在,在本地构建 Docker 镜像,然后将镜像推送到 GCR:
LOCAL_IMAGE_NAME="flask-upload-demo-image"
docker build --tag "$LOCAL_IMAGE_NAME" .
GCR_HOSTNAME="gcr.io"
GCR_IMAGE_PATH="${GCR_HOSTNAME}/${PROJECT_ID}/flask-demo-app"
docker tag "$LOCAL_IMAGE_NAME" "$GCR_IMAGE_PATH"
docker push "$GCR_IMAGE_PATH"部署 Docker 容器
部署容器需要绕一点弯路。GCP 不允许直接部署 Docker 镜像。相反,你需要使用 GCE 启动一台完整的虚拟机(VM),在该 VM 上运行 Docker,然后由 GCE 在这台 VM 的容器中运行 Docker 镜像。幸运的是,GCP 的工具简化了这一过程。
GCE VM 默认禁止入站 HTTP 流量。要允许这种流量,需要创建一条防火墙规则,接受 80 端口上的 TCP 连接(这是明文 HTTP 流量的标准端口)。
一种简单的做法是将规则应用于带有 http-server 标签的任意 VM:
VM_TAGS="http-server"
gcloud compute \
--project="$PROJECT_ID" \
firewall-rules create default-allow-http \
--direction=INGRESS \
--action=ALLOW \
--rules=tcp:80 \
--target-tags="$VM_TAGS"然后,为 GCE VM 添加 http-server 标签并部署:
VM_NAME="flask-demo-app-vm"
MACHINE_TYPE="n1-standard-1"
ZONE=us-east1-b
gcloud compute \
--project="$PROJECT_ID" \
instances create-with-container "$VM_NAME" \
--zone="$ZONE" \
--machine-type="$MACHINE_TYPE" \
--network-tier=STANDARD \
--metadata=google-logging-enabled=true \
--maintenance-policy=MIGRATE \
--scopes=https://www.googleapis.com/auth/cloud-platform \
--tags="$VM_TAGS" \
--image-family=cos-stable \
--image-project=cos-cloud \
--boot-disk-size=10GB \
--boot-disk-type=pd-standard \
--boot-disk-device-name="$VM_NAME" \
--container-image="$GCR_IMAGE_PATH" \
--container-restart-policy=on-failure下面是其中几个值得注意的标志:
--tags="$VM_TAGS" \--tags 标志会使用你为防火墙规则创建的标签启动 VM,确保 VM 能在 80 端口接收 HTTP 流量。
--image-family=cos-stable \
--image-project=cos-cloud \这些标志告诉 GCE 使用 Container-Optimized OS 运行容器。这是 Google 创建的精简版 Linux 操作系统,用于运行 Docker 容器。--image-family=cos-stable 会告诉 gcloud 使用最新的稳定版 Container-Optimized OS。
--container-image="$GCR_IMAGE_PATH" \上面的标志告诉 GCE 在 GCE VM 中运行哪个 Docker 镜像,使用的就是你之前创建的 GCR URL。
命令完成后,你会看到类似下面的输出:
Created [https://www.googleapis.com/compute/v1/projects/flask-upload-demo-2018-11-26/zones/us-east1-b/instances/flask-demo-app-vm].
NAME ZONE MACHINE_TYPE PREEMPTIBLE INTERNAL_IP EXTERNAL_IP STATUS
flask-demo-app-vm us-east1-b n1-standard-1 10.142.0.2 35.211.106.214 RUNNING在浏览器中输入 EXTERNAL_IP 中的地址,应用的运行效果就会与本地版本一样:

运行在 GCE 上的 flask-upload-demo

flask-upload-demo 从 GCE 提供图像文件
问题在于,如果你终止这台 VM,然后使用相同的 Docker 镜像启动一台新的 VM,刚才上传的文件就不见了:

新的 GCE VM 无法提供该文件,因为它存储在之前的 VM 上
当然,这正是促使我写下整篇教程的问题。容器将文件存储在自己的内部文件系统中。当你终止宿主 VM 时,所有文件都会丢失。
要解决这个问题,需要配置 Docker 容器,将所有持久化数据存储在 Google Cloud Storage(GCS)存储桶中。
规划支持 GCS 的架构
下面是能够解决该问题的目标架构:

将 Flask 应用部署到 Google Cloud Platform 的架构
- Web 浏览器只与 Web 服务器 Nginx 通信,由 Nginx 负责协调所有前端请求。
- 如果 Web 浏览器请求文件,Nginx 会通过名为 gcsfuse 的工具从 GCS 获取文件。该工具可以将 GCS 存储桶挂载为文件系统中的文件夹。
- 对于其他所有请求,Nginx 会将请求转发给 flask-upload-demo 应用。
- flask-upload-demo 也可以通过 gcsfuse 工具将新文件写入 GCS。
这个架构满足了我在文章开头提出的目标。由于 GCS 存储了所有永久状态,VM 中的一切都可以随时丢弃。所有应用代码都位于 Docker 容器中,因此部署简单且具有原子性。
创建 GCS 存储桶(可选)
如果你还没有 GCS 存储桶,可以使用下面的 gcloud 命令创建:
STORAGE_LOCATION="us-east1"
GCS_BUCKET="${PROJECT_ID}-storage"
gsutil mb \
-p "$PROJECT_ID" \
-l "$STORAGE_LOCATION" \
"gs://${GCS_BUCKET}"否则,将 GCS_BUCKET 环境变量设置为 GCS 存储桶的名称。
授予 Docker 容器访问 GCS 的权限
你需要修改 Docker 镜像,使其集成 gcsfuse 工具。幸运的是,我已经替你完成了这项工作。完整的 Dockerfile 位于我的 GitHub 仓库的 gcsfuse 分支,下面是主要改动:
# Install gcsfuse.
ARG GCSFUSE_REPO="gcsfuse-stretch"
ARG GCS_MOUNT_ROOT="/mnt/gcsfuse"
RUN set -x && \
apt-get install --yes --no-install-recommends \
ca-certificates \
curl && \
echo "deb http://packages.cloud.google.com/apt $GCSFUSE_REPO main" \
| tee /etc/apt/sources.list.d/gcsfuse.list && \
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg \
| apt-key add -
RUN apt-get update
RUN set -x && \
apt-get install --yes gcsfuse && \
echo 'user_allow_other' > /etc/fuse.conf && \
mkdir --parents "$GCS_MOUNT_ROOT" && \
chown \
--no-dereference \
"${APP_USER}:${NGINX_GROUP}" "$GCS_MOUNT_ROOT"这里有两个值得讨论的部分:
echo 'user_allow_other' > /etc/fuse.conf上面这行使得使用 gcsfuse 的 -o allow_other 选项成为可能。这是必要的,因为应用系统账户和 Nginx 系统账户都需要访问 GCS 文件夹。如果配置文件中没有 user_allow_other 这一行,就只能有一个账户访问 GCS 文件夹。
陷阱提示:如果有多个系统账户需要访问 GCS 存储桶,/etc/fuse.conf 文件必须包含 user_allow_other 这一行。
mkdir --parents "$GCS_MOUNT_ROOT" && \
chown \
--no-dereference \
"${APP_USER}:${NGINX_GROUP}" "$GCS_MOUNT_ROOT"gcsfuse 要求存在一个启动用户拥有写权限的目录。普通用户没有 /mnt 目录的写权限,因此 Dockerfile 以 root 用户身份创建 /mnt/gcsfuse 目录,并使用 chown 将其所有权分配给 Nginx 和示例应用的系统账户。
其他值得注意的改动位于 Dockerfile 的 CMD 部分,该部分定义了容器的运行时行为:
ENV GCS_BUCKET "REPLACE-WITH-YOUR-GCS-BUCKET-NAME"
ENV GCS_MOUNT_ROOT "$GCS_MOUNT_ROOT"
ENV APP_UPLOADS_DIR "${APP_ROOT}/demo/uploads"
CMD set -x && \
sudo nginx && \
gcsfuse \
-o nonempty \
-o allow_other \
--implicit-dirs \
"$GCS_BUCKET" "$GCS_MOUNT_ROOT" && \
if [ ! -d "$APP_UPLOADS_DIR" ]; then \
ln --symbolic "$GCS_MOUNT_ROOT" "$APP_UPLOADS_DIR"; \
fi && \
virtualenv VIRTUAL && \
. VIRTUAL/bin/activate && \
gunicorn \
demo.app:app \
--bind 127.0.0.1:5000 \
--log-level info再次按照值得注意的代码片段进行拆解:
gcsfuse \
-o nonempty \
-o allow_other \
--implicit-dirs \
"$GCS_BUCKET" "$GCS_MOUNT_ROOT"和上面的 user_allow_other 选项一样,-o allow_other 使多个用户可以访问挂载的文件夹,因为 nginx(以 www-data 用户身份运行)和示例应用(以 demo-user 身份运行)都需要访问它。
陷阱提示:如果多个用户账户要访问 GCS 挂载中的文件,gcsfuse 需要使用 -o allow_other 标志。
如果没有 --implicit-dirs 标志,gcsfuse 就无法访问 GCS 存储桶子文件夹中的文件。
陷阱提示:如果 GCS 存储桶包含子文件夹,gcsfuse 需要使用 --implicit-dirs 标志。
if [ ! -d "$APP_UPLOADS_DIR" ]; then \
ln --symbolic "$GCS_MOUNT_ROOT" "$APP_UPLOADS_DIR"; \
fi应用会将上传的文件写入 demo/uploads 目录。上面的代码块会创建从 demo/uploads 到 /mnt/gcsfuse 的符号链接。这样,当应用以为自己正在写入 demo/uploads 路径时,实际上写入的就是你的 GCS 存储桶。if/then 代码块可以防止容器重启等情况下重复执行这一步。
创建具有 GCS 访问权限的服务账户
在将这个镜像部署到 GCE 之前,还需要执行一个额外步骤。默认情况下,GCE 实例以标准 GCE 服务账户的身份运行。该账户只有 GCS 的只读权限,因此应用无法将新文件写入 GCS。
要解决这个问题,请创建一个具有以下两个角色的自定义服务账户:
storage.objectAdmin:允许 VM 中的进程读取和写入 GCS 中的对象。logging.logWriter:允许应用的日志输出显示在 GCP 的日志界面中。
陷阱提示:除非以自定义服务账户身份启动 GCE 实例,否则 GCE 实例无法写入 GCS 存储桶。
下面的命令会创建一个具有必要权限的服务账户:
SERVICE_ACCOUNT_NAME=flask-demo-app-service-account
SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
gcloud iam service-accounts create "$SERVICE_ACCOUNT_NAME"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member "serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \
--role roles/storage.objectAdmin
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member "serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \
--role roles/logging.logWriter部署支持 GCS 的容器
回到你克隆的 docker-flask-upload-demo 仓库,切换到 gcsfuse 分支:
cd ~/docker-flask-upload-demo
git checkout gcsfuse现在,重新构建 Docker 镜像并将其推送到 GCR:
LOCAL_IMAGE_NAME="flask-upload-demo-image"
docker build --tag "$LOCAL_IMAGE_NAME" .
GCR_HOSTNAME="gcr.io"
GCR_IMAGE_PATH="${GCR_HOSTNAME}/${PROJECT_ID}/flask-demo-app"
docker tag "$LOCAL_IMAGE_NAME" "$GCR_IMAGE_PATH"
docker push "$GCR_IMAGE_PATH"注意:如果尝试在本地运行这个镜像,它会失败。gcsfuse 版本的 Dockerfile 假设其执行环境已经完成 gcloud 认证(在 GCE 上运行的容器确实如此)。你也可以调整 Dockerfile,使其能够在 GCE 之外运行时挂载 GCS 存储桶,但这就留给读者作为练习了。
有了新的自定义 GCE 服务账户后,就可以部署支持 GCS 的容器了:
VM_NAME="flask-demo-app-vm-gcsfuse"
MACHINE_TYPE="n1-standard-1"
ZONE=us-east1-b
gcloud compute \
--project="$PROJECT_ID" \
instances create-with-container "$VM_NAME" \
--zone="$ZONE" \
--machine-type="$MACHINE_TYPE" \
--network-tier=STANDARD \
--metadata=google-logging-enabled=true \
--maintenance-policy=MIGRATE \
--scopes=https://www.googleapis.com/auth/cloud-platform \
--service-account="$SERVICE_ACCOUNT_EMAIL" \
--tags="$VM_TAGS" \
--image-family=cos-stable \
--image-project=cos-cloud \
--boot-disk-size=10GB \
--boot-disk-type=pd-standard \
--boot-disk-device-name="$VM_NAME" \
--container-image="$GCR_IMAGE_PATH" \
--container-restart-policy=on-failure \
--container-privileged \
--container-env="GCS_BUCKET=$GCS_BUCKET"这条命令与之前的部署命令相同,只多了两个标志:
--container-privileged \--container-privileged 标志是必需的,因为它允许 Docker 容器挂载 FUSE 文件系统(Docker 提供了更细粒度的实现方式,但 GCE 目前还不支持)。
陷阱提示:除非使用 --container-privileged 标志部署 VM,否则 gcsfuse 无法在 GCE 上挂载 GCS 存储桶。
--container-env="GCS_BUCKET=$GCS_BUCKET"我特意将 Docker 镜像设计为在运行时之前不依赖 GCS 存储桶的名称。--container-env 标志允许你在部署时指定 GCS_BUCKET 环境变量。
持久化的回报……就是持久化
现在,你终于拥有了一个能将状态持久化到 GCS 的 Docker 容器。你可以向已部署的应用上传文件来测试它:

flask-upload-demo 从 GCS 的永久存储中提供文件
检查你的 GCS 存储桶,就会看到刚才上传的文件:

GCS 存储桶中的图像文件
真正的考验是这些状态能否跨不同 VM 持续存在。你可以彻底终止 VM,然后重新部署,以验证这一点。之前 VM 中的图像 URL 在新服务器上仍然可以访问:

即使 VM 被销毁并重新构建,flask-upload-demo 仍能继续提供文件
额外收获:日志界面
这个方案还有一个不错的附带好处:GCP 提供了一个简洁的 Web 界面,用于查看应用日志。
你随时可以通过 SSH 登录 VM,然后运行 docker logs 手动查看日志:
$ docker logs klt-flask-demo-app-vm-gcsfuse-qnnf
...
[2018-11-26 21:28:54 +0000] [33] [INFO] Starting gunicorn 19.9.0
[2018-11-26 21:28:54 +0000] [33] [INFO] Listening at: http://127.0.0.1:5000 (33)
[2018-11-26 21:28:54 +0000] [33] [INFO] Using worker: sync
[2018-11-26 21:28:54 +0000] [37] [INFO] Booting worker with pid: 37
[2018-11-26 21:32:59 +0000] [37] [INFO] Saving uploaded file "zestful-logo.png" to "/srv/demo-app/demo/uploads/zestful-logo.png"更简单的方法是打开 GCP 的 StackDriver 日志界面。在那里,你可以通过功能丰富的 Web 界面查看所有日志:

GCP 的 StackDriver 界面显示应用的日志输出。
推送新版本
这个架构让推送新版本变得很容易。每当你想更新应用或其依赖项时,只需构建新镜像并将其推送到 GCR:
docker build --tag "$LOCAL_IMAGE_NAME" .
docker tag "$LOCAL_IMAGE_NAME" "$GCR_IMAGE_PATH"
docker push "$GCR_IMAGE_PATH"然后,使用 update-container 命令更新正在运行的 GCE 实例上的 Docker 镜像:
gcloud compute \
--project="$PROJECT_ID" \
instances update-container "$VM_NAME" \
--container-image="$GCR_IMAGE_PATH"陷阱提示:除非分配了静态 IP,否则该命令完成后 VM 的外部 IP 地址会发生变化。
如果你推送了有问题的版本,update-container 命令允许你回滚到之前已知可用的镜像。
限制
这个方案具有 gcsfuse 工具和 GCS 本身的相同限制。gcsfuse 尽最大努力让 GCS 存储桶看起来像普通文件系统文件夹,但这种抽象在两个主要方面会失效:
- GCS 不支持锁,因此如果应用尝试获取文件锁,运行就会变得混乱。
- 延迟很高,尤其是在大文件上执行小规模随机读写时。
具体来说,我发现如果将 sqlite 指向位于 gcsfuse 挂载上的数据库,它会很快失败。
结语
在本教程中,你学会了如何在不修改应用本身的情况下,将应用数据重定向到云存储。示例应用完全不了解 Google Cloud Platform,但你仍然将它部署到了 Google Compute Engine,并将其持久化数据重定向到了 Google Cloud Storage。
源代码
- flask-upload-demo:将状态保存在本地文件系统中的 Flask 示例应用。
- docker-flask-upload-demo:flask-upload-demo 的 Docker 配置,共有三个版本:
- master 分支——展示应用的基本打包方式
- nginx 分支——展示更贴近真实场景的架构,其中 nginx 为应用代理流量
- gcsfuse 分支——展示如何在 Docker 容器内部挂载 Google Cloud Storage 存储桶(假设容器运行在对 Google Cloud Storage 具有读写权限的 Google Compute Engine VM 中)。
- mediagoblin-docker(gcsfuse 分支)——一个真实媒体分享应用的 Docker 配置,我使用相同技术将该应用的永久数据重定向到了 Google Cloud Storage。
软件架构图由 Loraine Yow(洛林·尤)绘制
随机一篇博客

