Retrofitting Apps for Cloud Storage with Zero Code Changes

Michael Lynch

无需改动一行代码,为应用接入云存储

原文由 Michael Lynch 发布,订阅该博客

最近,我在一台服务器上安装了一款媒体分享应用。安装很简单,但它给长期维护埋下了一个棘手的陷阱。

每当用户上传文件时,这个 Web 应用都会把它保存到本地文件系统。如果哪天我重装服务器,就得手动备份和恢复所有文件。更合理的架构是让应用把文件写到独立的存储服务器上,但我不想为此花上几个月去重写应用。

简单架构与理想架构对比

借助 Docker、Google Cloud Storage 和 gcsfuse 工具,我在没有改动应用一行代码的情况下实现了这种分离。

在本教程中,我将展示如何在尽量少改动源码的前提下,改造老应用以适配 Google Cloud Storage。

术语说明

如果你对 Google Cloud Platform 不太熟悉,下面是本教程中会用到的几个缩写:

缩写全称是什么类似于
GCSGoogle Cloud StorageGoogle 的云存储服务Amazon S3
GCEGoogle Compute EngineGoogle 的按需虚拟机服务Amazon EC2
GCRGoogle Container RegistryGoogle 的 Docker 镜像托管服务Docker Hub
GCPGoogle Cloud PlatformGoogle 的云计算平台(GCS、GCE 和 GCR 都是 GCP 的组成部分)Amazon Web Services 或 Microsoft Azure

Docker 徽标

Docker 能让开发者为应用构建自包含的运行环境,并且在任何地方都能运行:

  • Docker 镜像是运行应用所需全部文件的集合,包括操作系统和所有第三方依赖。
  • Docker 容器是 Docker 镜像运行时的实例环境。

在本教程中,你可以把 Docker 容器理解为轻量级虚拟机,虽然从技术上说它们并不是虚拟机。

准备工作

要跟着本文的示例操作,你需要准备以下工具:

示例应用

我为本教程创建了一个示例项目。这是一个极其简单的 Web 应用,基于 Flask 框架的上传文档实现。

运行方式如下:

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://[服务器地址]:5000/uploads/[文件名] 永久提供该文件的访问:

示例应用上传结果截图

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 镜像非常容易:

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 主要完成了几项任务来准备镜像:

  1. 安装 Git、Python 及相关软件包
  2. 创建一个用于以受限权限运行应用的系统账号(demo-user
  3. 在本地克隆应用源码仓库
  4. 添加 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/sudoers

Nginx 通常以 root 身份运行,以便监听 80 和 443 等特权 HTTP 端口。因此,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 处理静态文件的速度和效率都比应用后端更高。

你可以用以下命令运行 flask-upload-demo 的 Nginx 版本:

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 网页控制台创建一个具有 Owner 角色的服务账号

避坑提醒:由于 GCP 一个疑似的 Bug,如果你使用根账号(例如你的 @gmail.com 账号)或通过 gcloud 创建的服务账号,向 gcr.io 推送 Docker 镜像(见下一节)会失败。

在 GCP 网页控制台中创建新服务账号的截图

在 GCP 网页控制台中创建新的服务账号

服务账号角色选择界面截图

为服务账号分配角色

将私钥下载为 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),在该虚拟机上运行 Docker,再由 GCE 在其中的容器里运行你的 Docker 镜像。好在 GCP 的工具让这个过程变得简单。

GCE 虚拟机默认禁止入站 HTTP 流量。要允许访问,需要创建一条防火墙规则,放行 80 端口(明文 HTTP 流量的标准端口)的 TCP 连接。

一个简便的方法是将这条规则应用到所有带有 http-server 标签的虚拟机:

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"

然后,使用 http-server 标签部署你的 GCE 虚拟机:

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 标志会使用你为防火墙规则创建的标签来启动虚拟机,确保它能在 80 端口接收 HTTP 流量。

  --image-family=cos-stable \
  --image-project=cos-cloud \

这些标志告诉 GCE 在 Container-Optimized OS 下运行容器,这是 Google 为运行 Docker 容器而打造的精简版 Linux 系统。--image-family=cos-stable 表示让 gcloud 使用 Container-Optimized OS 的最新稳定版。

--container-image="$GCR_IMAGE_PATH" \

上面的标志告诉 GCE 要在 GCE 虚拟机中运行哪个 Docker 镜像,使用的是你之前创建的 GCR 地址。

命令执行完成后,你会看到类似如下的输出:

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 截图

在 GCE 上运行的 flask-upload-demo

在 GCE 上的 flask-upload-demo 中已上传文件的截图

flask-upload-demo 在 GCE 上提供图片文件

问题在于,如果你销毁这台虚拟机,再用同一个 Docker 镜像启动一台新的,之前上传的文件就不见了:

之前上传的文件出现 404 的截图

新的 GCE 虚拟机无法提供该文件,因为文件保存在了上一台虚拟机上

当然,这正是催生本教程的问题。容器将文件保存在其内部文件系统上,一旦终止宿主机虚拟机,所有文件都会丢失。

要解决这个问题,你需要配置 Docker 容器,将所有持久化数据都存储到 Google Cloud Storage(GCS)存储桶中。

规划支持 GCS 的架构

下面是解决该问题的目标架构:

flask-demo-app 架构图

在 Google Cloud Platform 上部署 Flask 应用的架构

  • Web 浏览器只与 Web 服务器 Nginx 通信,Nginx 作为所有前端请求的调度器。
  • 如果浏览器请求文件,Nginx 会通过名为 gcsfuse 的工具从 GCS 获取文件,该工具能将 GCS 存储桶挂载为文件系统中的文件夹。
  • 对于其他所有请求,Nginx 会将其转发给 flask-upload-demo 应用。
  • flask-upload-demo 同样通过 gcsfuse 工具将新文件写入 GCS。

这一架构满足了我在文章开头提出的目标。虚拟机中的一切都是可丢弃的,因为所有持久状态都由 GCS 保存。所有应用代码都在 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 目录没有写权限,因此 Dockerfileroot 身份创建了 /mnt/gcsfuse 目录,并通过 chown 将其所有权赋予 Nginx 和示例应用的系统账号。

另一处有意思的改动在 DockerfileCMD 部分,它定义了容器的运行时行为:

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:允许虚拟机中的进程对 GCS 进行读写。
  • logging.logWriter:允许应用的日志输出显示在 GCP 的日志界面中。

避坑提醒:除非使用自定义服务账号启动,否则 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 标志部署虚拟机,否则 gcsfuse 无法在 GCE 上挂载 GCS 存储桶。

  --container-env="GCS_BUCKET=$GCS_BUCKET"

我有意将 Docker 镜像设计为在运行时之前不依赖具体的 GCS 存储桶名称。--container-env 标志让你可以在部署时指定 GCS_BUCKET 环境变量。

持久化的回报,正是持久化

你终于得到了一个能将状态持久化到 GCS 的 Docker 容器。你可以通过向已部署的应用上传文件来测试:

flask-upload-demo 从 GCS 永久存储提供文件的截图

flask-upload-demo 从 GCS 上的永久存储提供文件

如果你查看 GCS 存储桶,就会看到刚才上传的文件:

GCS 存储桶中显示已上传文件的截图

GCS 存储桶中的图片文件

真正的考验是这种状态能否在不同的虚拟机之间保持。你可以通过彻底销毁虚拟机并重新部署来验证。之前虚拟机上的图片 URL 在新服务器上依然可以访问:

在不同 IP 上 flask-upload-demo 仍在提供文件的截图

即使虚拟机已被销毁并重建,flask-upload-demo 仍能继续提供该文件

番外:日志界面

这个方案还有一个不错的附加好处:GCP 提供了一个精美的网页界面来查看应用日志。

你随时可以通过 SSH 登录虚拟机,然后运行 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 日志界面。在那里,你会在一个功能丰富的网页界面中找到所有日志:

GCP Stackdriver 界面中的应用日志

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,否则执行此命令后虚拟机的外部 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 虚拟机中)。
  • mediagoblin-docker(gcsfuse 分支) - 一个真实媒体分享应用的 Docker 配置,我在其中使用了同样的技术将应用的持久化数据重定向到 Google Cloud Storage。

软件架构图由 Loraine Yow 绘制

本文章由 muse-spark-1.2-contributor 进行翻译

评论