+86 189 8218 1436 周一至五 10:00-22:00

AirAsia使用GCP Storage建置内容资源服务器

TL;DR:本文借 AirAsia 使用 GCP Storage 建置内容资源服务器的场景,带小白理解对象存储托管、权限、缓存与成本优化。

它是什么

把「AirAsia 使用 GCP Storage 建置内容资源服务器」翻成大白话:GCP Storage 通常指 Google Cloud 的 Cloud Storage 对象存储。先建一个存储桶,把图片、视频、文件、CSS、JS 等网页素材放进去,Google 负责保存和扩容;不用先维护一台 Linux VM、装 Nginx、挂硬盘,也不用半夜清磁盘。

思路是把静态内容交给托管服务:网站服务器只管登录、查票价、下单;图片和活动页用 Storage 网址或 Cloud CDN 分发,不再全塞进网站服务器硬盘。这样做是因为促销流量来了硬盘不会先满,用户能就近读缓存,权限和备份也集中在控制台,扩容不用自己搬机器。

第一次用先单独复制这一行,按提示登录并选项目;别和下面整段一起粘贴,交互命令会吃掉后面的输入。

gcloud init

再复制这段:自动读取当前项目、生成唯一桶名、上传 index.html,并打印访问网址,不用改项目名。

export PROJECT_ID="$(gcloud config list --format='value(core.project)')"
if [ -z "$PROJECT_ID" ]; then
  echo "请先完成初始化并选择项目,再重新复制执行本段命令"
else
  export BUCKET="airasia-demo-$PROJECT_ID-$(date +%s)-$$"
  gcloud storage buckets create "gs://$BUCKET" --project="$PROJECT_ID" --location=asia-southeast1 --uniform-bucket-level-access
  echo 'Hello AirAsia static content' > index.html
  gcloud storage cp index.html "gs://$BUCKET/index.html"
  gcloud storage buckets add-iam-policy-binding "gs://$BUCKET" --member=allUsers --role=roles/storage.objectViewer
  echo "https://storage.googleapis.com/$BUCKET/index.html"
fi

小白可把存储桶理解成一个可扩容文件夹;每个文件都是「对象」,控制台会按对象看名称、权限,开启版本控制和审计日志后,版本与访问记录也跟着对象留痕。它不是 Linux 目录,对象名里的斜杠只是名字前缀,不能进去改文件中间几个字节,通常只能整份重传覆盖。

  • 公开要克制:演示命令给了所有人读取权,只放能公开的图片和 HTML;订单、护照、密钥别放,练完删桶或撤权限。若报 public access prevention,是组织在保护你,正式站改用 CDN 回源或签名网址。
  • 历史不会自动全有:不开对象版本控制,覆盖后旧文件难找回;不开访问日志,谁何时读过也难查。
  • 别忽略账单和桶名:存储、请求、跨区流量、CDN 出站都可能计费;桶名全球唯一,所以命令用时间戳加进程号避重。

解决什么问题

做机票页面时,很多人一开始把图片、CSS、JS 直接塞进 VM 的系统盘。平时没事,一到活动、促销或机票查询高峰,读请求全压在同一台虚机上,磁盘 IO 和带宽先被打满,页面就开始转圈。AirAsia 使用 GCP Storage 建置內容資源伺服器,首先就是拆掉这个单点:对象存储天然横向扩展,流量涨了由服务侧自动分摊,你不用提前猜要加几台 VM、挂几块盘,VM 只安心跑业务逻辑。

第二个麻烦是「同步」。以前多台 VM 要保持一致,得自己守着 rsync 或挂 NFS,目录一深就常出现某台少个文件、权限还对不上。换到 Storage 后全站只认桶里这一份,权限、生命周期、版本控制和访问日志都交给服务侧管理,不用写定时同步、不用修挂掉的挂载点,运维轻一大截。

它最擅长托管不常变化、却要被高并发读取的内容:图片、脚本、样式、下载包、宣传素材。记住是「读多写少」,别拿它当数据库或日志盘反复改。

先建个桶感受一下,只需把第一行换成你自己的项目 ID,桶名会自动生成,整段可直接粘贴:

gcloud config set project YOUR_PROJECT_ID
BUCKET="content-res-$(date +%Y%m%d)-$RANDOM"
gcloud storage buckets create "gs://$BUCKET" --location=asia-southeast1 --uniform-bucket-level-access
gcloud storage buckets update "gs://$BUCKET" --soft-delete-duration=7d
gcloud storage buckets add-iam-policy-binding "gs://$BUCKET" --member=allUsers --role=roles/storage.objectViewer
echo 'body{font-family:sans-serif}' > ./style.css
gcloud storage cp ./style.css "gs://$BUCKET/static/style.css"
echo "访问:https://storage.googleapis.com/$BUCKET/static/style.css"

为什么这么写:选 asia-southeast1 是让资源离东南亚用户近一点;开统一桶级访问,权限只在桶这一层管,正好对应前面说的「服务侧管权限」;soft-delete 给误删留 7 天后悔药;绑定 objectViewer 是给所有人只读,不是可写。传个 style.css 而不是改数据库,就是在演示「读多写少」的正确用法。

小白最容易踩的坑:

  • 照老教程写 --add-acl=publicRead 或 gsutil acl ch,开了统一访问后会直接报「不能用 ACL」,改用上面那行 IAM 绑定就好。
  • 图省事给 allUsers 加了 objectAdmin,路人就能删掉你的宣传图,只给 objectViewer。
  • 文件名带中文或空格,引用时变成一长串 % 编码,容易 404,统一用小写英文加短横线。
  • 促销素材一版版堆着不清,账单会悄悄涨;旧版本记得配生命周期自动删,别只靠手动清理。

怎么上手

先当小实验:只放一张 SVG 和一个 CSS,不搬全站。控制台选好项目,进「Cloud Storage → 存储桶 → 创建」:名称全局唯一、只用小写字母数字和连字符;区域选离用户近且合规的,例如 asia-southeast1;类别先选 Standard。别纠结 Nearline/Coldline/Archive,它们有最短存放期和取回费,频繁读的图片反而贵。访问控制保持「统一」,先别公开。手动传 logo.svg 点预览能打开。先单独跑 gcloud init 按提示登录、选项目,再复制后面一整块命令。

gcloud init
export LOCATION="asia-southeast1"
export BUCKET_NAME="airasia-demo-$(date +%s)-$$"
gcloud storage buckets create "gs://${BUCKET_NAME}" --location="${LOCATION}" --default-storage-class=STANDARD --uniform-bucket-level-access
mkdir -p ./site-assets
printf 'body{color:#e53935}n' > ./site-assets/style.css
printf '<svg xmlns="http://www.w3.org/2000/svg" width="120" height="40"><rect width="120" height="40" fill="#e53935"/><text x="10" y="25" fill="white">AirAsia</text></svg>n' > ./site-assets/logo.svg
gsutil -m -h "Cache-Control:public, max-age=3600" cp -r ./site-assets/* "gs://${BUCKET_NAME}/static/"
export MEMBER="user:$(gcloud config get-value account 2>/dev/null)"
gcloud storage buckets add-iam-policy-binding "gs://${BUCKET_NAME}" --member="${MEMBER}" --role="roles/storage.objectViewer"
export PUBLIC_BUCKET_NAME="airasia-public-$(date +%s)-$$"
gcloud storage buckets create "gs://${PUBLIC_BUCKET_NAME}" --location="${LOCATION}" --default-storage-class=STANDARD --uniform-bucket-level-access
gsutil -h "Cache-Control:public, max-age=3600" cp ./site-assets/logo.svg "gs://${PUBLIC_BUCKET_NAME}/logo.svg"
gcloud storage buckets add-iam-policy-binding "gs://${PUBLIC_BUCKET_NAME}" --member="allUsers" --role="roles/storage.objectViewer"
export ASSET_BASE_URL="https://storage.googleapis.com/${PUBLIC_BUCKET_NAME}"
export OLD_ASSET_BASE_URL="https://old.example.com/assets"
curl -I "${ASSET_BASE_URL}/logo.svg"
printf 'new=%snold=%sn' "${ASSET_BASE_URL}" "${OLD_ASSET_BASE_URL}"

为什么这样分:控制台只用来验路,批量交给 gsutil 才方便写进脚本。IAM 先只给当前账号 objectViewer,真实项目换成服务账号或群组,别给 Editor/Owner。主桶不公开;统一访问下不能只给单个对象加 allUsers,所以另建公开桶,只放必须公开的 logo.svg。再到控制台「负载均衡」勾 Cloud CDN、配托管 SSL 和 HTTPS 域名,前端用 https://cdn.你的域名/logo.svg,别把 storage.googleapis.com 硬编码到生产页面。缓存头先一小时,设一年会导致改图不生效,要换文件名或加版本号;第一次 curl 返回 403 等几秒再试,若报「禁止公开访问」,找管理员看组织策略,别硬关。

切流前:Linux 服务或前端只改 ASSET_BASE_URL 这一个配置,OLD_ASSET_BASE_URL 先填示例、正式用换成现有地址;旧目录和旧解析都留着,回滚就是把变量改回去。先单页面或 10% 流量,打开日志、结算预算和监控,看 4xx/5xx、缓存命中率、请求数、出网流量和 24 小时账单,再全量。常见坑:桶名重名、拿项目 ID 当桶名、一上来删旧文件、忽略 CDN 与出网费用。

与替代方案对比

先别急着在「自己搭一台」和「用托管服务」之间选边站。对 AirAsia 这类内容资源服务器,最常见的内容其实是图片、CSS、JS、活动页素材、下载包:写一次,后面被大量用户反复读。可以先复制下面这段到已经完成登录和项目选择的终端,做一个最小实验:建一个测试 bucket、放一个小文件、给它公开读权限,再查看 HTTP 响应头。这么做不是为了上线,而是让你看清 GCP Storage 提供的是「按文件名取对象」。

PROJECT_ID="$(gcloud config get-value project)"
if [ -z "$PROJECT_ID" ] || [ "$PROJECT_ID" = "(unset)" ]; then
  echo "请先运行 gcloud init 并选定项目,再复制本段。"
else
  BUCKET="airasia-demo-$PROJECT_ID-$(date +%s)"
  if echo "$BUCKET" | tee bucket.txt; then
    if gcloud storage buckets create "gs://$BUCKET" --location=asia-southeast1 --uniform-bucket-level-access; then
      if printf 'hello static filen' | tee demo.txt; then
        if gcloud storage cp demo.txt "gs://$BUCKET/demo.txt"; then
          if gcloud storage buckets add-iam-policy-binding "gs://$BUCKET" --member=allUsers --role=roles/storage.objectViewer; then
            curl -I "https://storage.googleapis.com/$BUCKET/demo.txt"
          else
            echo "公开授权失败,可能被组织策略拦截,请找管理员。"
          fi
        else
          echo "上传失败,请检查权限和结算状态。"
        fi
      else
        echo "写入 demo.txt 失败,请检查当前目录是否可写。"
      fi
    else
      echo "创建 bucket 失败,请检查项目、权限和结算状态。"
    fi
  else
    echo "写入 bucket.txt 失败,请检查当前目录是否可写。"
  fi
fi

如果看到 200 OK 和响应头,就说明浏览器拿到的是一份对象;它不会像服务器那样执行 PHP、Node.js 或 Java。小白坑:如果提示找不到命令行工具,先安装 Google Cloud CLI 和常用 HTTP 工具;如果提示没权限或没开通结算,找管理员,别反复重跑,否则会留下多个 bucket。刚设置公开权限就出现 403,通常是权限生效有延迟,等几十秒再看;组织若禁止公开访问,别硬改策略。bucket 名全局唯一,公开读权限会作用在 bucket 内对象上,千万别放身份证、订单或后台配置。

if [ -f bucket.txt ]; then
  BUCKET="$(cat bucket.txt)"
  if gcloud storage rm -r "gs://$BUCKET"; then
    rm -f demo.txt bucket.txt
  else
    echo "删除失败,请稍后在控制台检查。"
  fi
else
  echo "没有找到 bucket.txt,可能还没创建测试 bucket。"
fi

用完一定清理,因为公开 bucket 既危险也会产生少量存储费;如果前面失败在半路,也要去控制台按 airasia-demo- 前缀检查残留。

  • 和 Linux VM 自建 Nginx 静态目录相比:VM 像自己租一台电脑,要维护 SSH 登录、系统补丁、Nginx 升级、磁盘扩容、备份和监控;换成 GCP Storage,SSH、补丁、磁盘扩容和备份这些维护量大减,你主要管对象、权限、生命周期。代价很直接:它不支持直接运行后端程序,不能登录上去装环境、改端口或跑接口。
  • 和 Cloud CDN 搭配不是二选一:GCP Storage 负责存源文件,Cloud CDN 负责就近缓存和加速;没命中时才回到 Storage 取原件。二者常一起用,缓存时间、刷新、命中率要在 CDN 层看,别怪 Storage「没变」。
  • 和数据库或文件共享盘相比:对象存储适合一次写入、多次读取的大文件,不适合频繁修改小块数据,也不适合需要 POSIX 挂载的场景。对象名里的斜杠看起来像目录,其实多是命名习惯;改一个字通常要重新覆盖整份文件,不能像本地盘那样加锁、随机写或挂给多个程序。

最佳实践

先把「谁都能写」这扇门堵死。新建桶本来就不公开,但小白最容易照抄旧教程给 allUsers 授权,结果整桶被传盗版图、流量被刷到账单爆表。做法是统一桶级权限加公开访问阻止,只让 Cloud CDN 的回源服务账号读文件,并且每月体检一次 IAM、服务账号和公开链接,看到 allUsers 就问一句「真的需要吗」。跑下面命令前先完成 gcloud init 登录并选好项目。

export PROJECT_ID=$(gcloud config get-value project)
export REGION="asia-southeast1"
export BUCKET="aa-content-$PROJECT_ID"
export LOG_BUCKET="aa-logs-$PROJECT_ID"
gcloud config set project "$PROJECT_ID"
gsutil mb -c STANDARD -l "$REGION" "gs://$BUCKET"
gsutil mb -c STANDARD -l "$REGION" "gs://$LOG_BUCKET"
gsutil uniformbucketlevelaccess set on "gs://$BUCKET"
gcloud storage buckets update "gs://$BUCKET" --public-access-prevention=enforced
export PROJECT_NUMBER=$(gcloud projects describe "$PROJECT_ID" --format="value(projectNumber)")
gcloud storage buckets add-iam-policy-binding "gs://$BUCKET" --member="serviceAccount:service-$PROJECT_NUMBER@cloud-cdn-fill.iam.gserviceaccount.com" --role="roles/storage.objectViewer"
gsutil iam get "gs://$BUCKET"
gcloud iam service-accounts list --project="$PROJECT_ID" --format="value(email,disabled)"
gsutil ls -L "gs://$BUCKET"

取项目号是为了拼出回源专用账号。千万别把 objectAdmin 给 allUsers,也别单独把某个对象改成公开,统一桶级权限就是防这种漏网之鱼;停用却还留着密钥的服务账号也要顺手清掉。

再给误操作留后路:版本控制、生命周期、访问日志、预算提醒一起开。

gsutil versioning set on "gs://$BUCKET"
printf '%sn' '{' '  "rule": [' '    {"action": {"type": "SetStorageClass", "storageClass": "NEARLINE"}, "condition": {"age": 30}},' '    {"action": {"type": "Delete"}, "condition": {"numNewerVersions": 3}},' '    {"action": {"type": "Delete"}, "condition": {"age": 365}}' '  ]' '}' > lifecycle.json
gsutil lifecycle set lifecycle.json "gs://$BUCKET"
gsutil iam ch group:cloud-storage-analytics@google.com:objectCreator "gs://$LOG_BUCKET"
gsutil logging set on -b "gs://$LOG_BUCKET" -o AccessLog "gs://$BUCKET"
export BILLING_ACCOUNT=$(gcloud billing accounts list --format="value(name)" --limit=1)
gcloud billing budgets create --billing-account="$BILLING_ACCOUNT" --display-name="aa-storage-guard" --budget-amount=100USD --threshold-rule=percent=80

版本让误删、覆盖都能按代次找回;30 天没人碰的旧图自动降成 NEARLINE,只留最近 3 版。常见坑:只开版本不设清理,旧版本无限堆积反而更贵。日志桶要先授权,否则日志静悄悄不落盘;没绑结算账号就先跳过最后一行。预算设在 80%,异常刷流量时邮件会比账单先到。

静态资源按「/static/版本号/类型/文件.哈希」放,配好缓存再让 CDN 回源,重复请求就打不到源站。

mkdir -p ./dist/img
echo "demo" > ./dist/img/logo.a1b2c3.png
echo "index" > ./dist/img/index.html
gsutil -m cp -r ./dist/img "gs://$BUCKET/static/v2.3.1/"
gsutil -m setmeta -h "Cache-Control:public, max-age=31536000, immutable" "gs://$BUCKET/static/**"
gsutil setmeta -h "Cache-Control:public, max-age=300" "gs://$BUCKET/static/v2.3.1/img/index.html"
gcloud compute backend-buckets create aa-cdn-origin --gcs-bucket-name="$BUCKET" --enable-cdn
gcloud compute url-maps create aa-cdn-map --default-backend-bucket=aa-cdn-origin
gcloud compute ssl-certificates create aa-cert --domains=static.example.com --global
gcloud compute target-https-proxies create aa-https-proxy --url-map=aa-cdn-map --ssl-certificates=aa-cert
gcloud compute forwarding-rules create aa-https-rule --global --target-https-proxy=aa-https-proxy --ports=443
gsutil ls -a "gs://$BUCKET/static/v2.3.1/img/logo.a1b2c3.png"

路径带版本、名字带哈希,内容一变路径就变,才敢设 immutable,也不会出现用户死活刷不到新图。坑:入口页别设长缓存,否则发版后还卡在旧页面,所以上面单独给了 300 秒;托管证书要等域名解析指向转发规则 IP 才签发,示例域名记得换成自己的。

上线前照清单走一遍,再按 5%、30%、100% 逐步放量:

  1. 费用估算:算存储、CDN 出向流量和请求数,流量比容量更容易超支。
  2. 权限复核:确认桶不公开写,密钥没有硬编码在前端。
  3. 监控告警:请求数、错误率、出向带宽、预算阈值都要有通知渠道。
  4. 恢复演练:真删一个测试文件,把上面列出的带井号数字的代次拷回来,别等出事才第一次试。
  5. 域名证书:CNAME、证书状态、回源域名、缓存键逐项核对。

放量后盯一两天日志和账单,确认没有陌生来源疯狂拉取,再全量切过去。

常见问题(FAQ)

小白一定要会 Linux 才能用 GCP Storage 吗?

不用。控制台可完成建桶、上传和授权;会 gcloud 后更适合批量运维。

GCP Storage 能完全替代 VM 吗?

不能。它适合静态资源和文件分发,动态网站、后台进程仍需 VM 或其他计算服务。

公开图片会不会产生高额流量费?

可能会。建议配 CDN、缓存和预算提醒,并限制不必要的公开访问。

AirAsia 案例对普通网站有什么启发?

启发是把静态内容从服务器剥离,用托管存储扛并发,再补齐权限、监控和成本治理。

上传后访问报错先查什么?

先查对象是否存在、IAM 权限、公开读取设置、缓存状态和域名解析。

不想自己折腾 GCP?我们提供云平台代维护、上云迁移与账单代付服务,一对一技术支持。

查看代维服务 →

发表评论