Halo 官方镜像源在 Serverless(Cloudflare Workers + R2) 上的实践

  • halo-dev
  • 2022-11-14
  • 523
Halo 官方镜像源在 Serverless(Cloudflare Workers + R2) 上的实践

转载自:https://nova.moe/halo-mirror-serverless/

一转眼两年过去了,在两年前的实践中,我利用 Cloudflare 的 Serverless 平台——Workers 做出了两个小玩具 「knatnetwork/g2fs-serverless」和「knatnetwork/g2ww-serverless」,分别可以将 Grafana 的报警信息推送到飞书和企业微信上,解决了当时在 PingCAP 内部使用对应聊天工具时没法很方便收到来自 Grafana 报警的问题。

去年,Cloudflare 发布了一个新的产品——R2,Announcing Cloudflare R2 Storage: Rapid and Reliable Object Storage, minus the egress fees,一个 S3 兼容的对象存储,同时提供了 Workers 的支持,苦于那段时间一直在和公司内的 Jenkins 和 Kubectl 做斗争,没有太多时间来实践,从 PingCAP 离开后正好有些闲暇的时间开着自己的车在赛道上划船,赛道之余便开始思考 Cloudflare 的这么些个产品在我自己这边的实际应用场景。

也许大家都知道, 我有一辆十代两厢手动思域, 我运行着 Halo 的官方镜像源,这个镜像源的主要功能为提供一个在 GitHub Release 之外的一个相对稳定的 Halo Jar 包下载地,作为镜像站,它需要满足以下几个功能:

  • Jar 包需要和上游(这里是 GitHub Releases)尽量保持同步
  • 存储地不能和上游一样(比如这里不能是个 GitHub 的反代)
  • 尽量减少开销(减少被刷爆的概率)

在之前我是这么做的:在自己的集群中找了一些机器和存储空间,并部署了一个自动同步 Jar 包的脚本,用 crontab 定期运行(一小时一次),这样能保证本地文件系统的文件和 GitHub Releases 上保持尽可能一致,有了本地文件系统之后接下来只需要一个 Nginx 的 autoindex ,然后设置好 Load balancer,就可以做到类似这样的效果,并且保证一定的可用性。

做运维和建站固然是个很开心的事情,MJJ 们可能也是这么想的,但是一旦自己手上需要维护的服务多了起来,且都需要保证一定的可用性的时候,我们便不能像大学的时候一样一个个给自己的服务器取名字,然后一点点手动编写配置文件并一顿微操了。应该在条件许可的情况下能用 Managed 就用 Managed,锅能丢给服务商就丢给服务商,在这里也是如此,为了保证可用性和 Cloudflare 一致,我打算利用 Workers 和 R2 改造一下这个项目,一来减少自己维护(带来的心理)压力,二来可以以此为契机学习一下 Cloudflare 的这一套 Serverless 理论。

让我们开始吧!

Jar downloading

在前文中我们知道,要做到和上游同步并保证存储地不一样,这里我们直接使用 Cloudflare R2 作为存储,但是由于要尽量摆脱对于人工/自己机器的介入,我们并不考虑通过 CLI 或者 SDK 的方式访问 R2 并修改内容,而是使用 Workers 来完成这个操作,那么在这里,我们需要做的操作就是用 Worker 下载 Jar 包并保存到 R2 中的对应位置。

R2

R2 的存储相对来说比 S3 便宜,价格如下:

Free Paid - Rates
Storage 10 GB / month $0.015 / GB-month
Class A Operations 1 million requests / month $4.50 / million requests
Class B Operations 10 million requests / month $0.36 / million requests

其中 A 类操作是指 ListBuckets, PutBucket, ListObjects, PutObject, CopyObject, CompleteMultipartUpload, CreateMultipartUpload, ListMultipartUploads, UploadPart, UploadPartCopy and PutBucketEncryption. 这些和写更多相关的操作

B 类操作是指 HeadBucket, HeadObject, GetObject, UsageSummary, GetBucketEncryption and GetBucketLocation. 这类看上去比较只读的操作

截止本文编写时,Halo 众多 Jar 包的存储空间没有超过 6G,所以存储的部分是绝对不会超过 Free 的限额,并且只要在设置好合适的 Rate limit 的情况下,Class B 操作应该也不会太容易超过免费的限制。

(如果真玩超了那 Ryan Wang 的钱包上估计会出现个大洞…

Workers

虽然我们在 Workers 的 Pricing 页面看到 Worker 的限制是 Up to 10ms CPU time per request,但是从个人实践的角度来看,Worker 的每个 request 至少能运行 1 分钟以上,这就给了我们下载文件并保存的机会,只需要在 wrangler.toml 中配置好 binding:

r2_buckets = [
  { binding = "dl_halo_run", bucket_name = "dl-halo-run", preview_bucket_name = "" }
]

然后只要两行,我们便可以利用 Worker 下载文件并放到 R2 中的对应位置,fetch 用来下载文件,env.BUCKET_NAME.put 用来把下载下来的内容存到 R2 中,代码如下:

const response = await fetch(download_url);
await env.dl_halo_run.put(download_filname, response.body);

是不是看上去很简单?在明白了核心的部分之后接下来只需要一些简单的业务逻辑,比如判断需要下载的文件名,判断是否已经存储在 R2 中等等便可完成下载部分的处理,样例代码如下

let download_filname = ""
if (filename.includes('beta') || filename.includes('alpha')) {
  download_filname = "prerelease/" + filename;
} else {
  download_filname = "release/" + filename;
}

// Check if exist in R2
console.log("Checking if exist in R2");
const check_file = await env.dl_halo_run.get(download_filname);
console.log("Check file", check_file);
if (!check_file) {
  console.log('Downloading file' + filename);
  const response = await fetch(download_url);
  await env.dl_halo_run.put(download_filname, response.body);
} else{
  console.log('File already exist in R2 ' + filename);
}

由于我们需要定期查询 GitHub 上的情况,所以这里的函数需要写在:

async scheduled(event, env, ctx) {
}

内部,并在 wrangler.toml 中配置一个 cron 来执行:

[triggers]
crons = ["0 */1 * * *"]

至此,我们的下载逻辑已经全部完成,Cloudflare Workers 会每小时执行一遍 scheduled 函数中的操作并下载所需要的 jar 文件了,此时你的 R2 看上去应该类似这样:

File Listing API

现在我们已经有了稳定的存储和定期的下载同步了,作为一个镜像站,我们肯定需要对外提供展示页面,单纯给 R2 绑定个域名用于下载看上去没有问题,但是没有 Listing 的能力的话会让用户没法知道镜像站上有啥内容,所以这里我们需要做一个简单的 API 对外展示内容,而这个部分就更加简单了,我们继续修改上面的 Workers 中的文件,只不过这次是写在:

async fetch(request, env, ctx) {
}

函数内,编写逻辑如下:

  • 如果访问的 URI 是 /api 的话,就输出 R2 中的所有内容(的 JSON 格式)
  • 此外的所有请求就尝试通过 URI 作为 Key 去 R2 中取文件

代码样例如下:

对于访问的 URI 是 /api 的请求:

// Check if vising /api, list all the files
if (uri == "api") {
  const listed = await env.dl_halo_run.list(options);

  const listed_objects = listed['objects'];
  for (const listed_object of listed_objects) {
    delete listed_object.customMetadata;
    delete listed_object.httpMetadata;
    delete listed_object.version;
    delete listed_object.httpEtag;
    delete listed_object.etag;
  }

  return new Response(JSON.stringify(listed_objects), {
    headers: {
      'content-type': 'application/json; charset=UTF-8',
    }
  });
} 

此外的所有请求:

const file = await env.dl_halo_run.get(objectName);
if (!file) {
  return new Response('File not found', { status: 404 })
}
const headers = new Headers()
file.writeHttpMetadata(headers)
headers.set('etag', file.httpEtag)
return new Response(file.body, {
  headers
})

是不是很容易? 此时访问对应的 /api 接口就能看到类似如下的返回结果:

[
  {
    "uploaded": "2022-11-13T06:34:09.717Z",
    "checksums": {
      "md5": "10e25e056c2bea90a9386e27a9450bfb"
    },
    "size": 61,
    "key": "config/Caddyfile2.x"
  },
...
  {
    "uploaded": "2022-11-13T07:05:29.984Z",
    "checksums": {
      "md5": "44f8a1a6821dbfe69d3577c451862bac"
    },
    "size": 79495690,
    "key": "release/halo-v1.4.14.jar"
  }
]

并且 URI 改为 key 中对应的路径也可以正常下载对应的文件了。

以上完整代码被我放到了 halo-sigs/halo-dl-api,各位如果有兴趣的话可以 一键三联(Star,Fork,Watch) 来围观下。

Ratelimit

这里我们为了减少一些潜在的恶意流量,我们可以加一点 Ratelimit 在这里,今年 Cloudflare 宣布了对于所有 Plan 都有 Unmetered Rate limiting: Back in 2017 we gave you Unmetered DDoS Mitigation, here’s a birthday gift: Unmetered Rate Limiting 之后,免费用户可以针对 Path 来做一点 Ratelimit 了,设置为「对于一个 IP 而言 10 秒钟访问了超过 20 次就自动 429」 ,类似如下:

File Frontend

现在就到了整个旅程的最后一步——提供一个活人能看的页面了,由于前端方面我是几乎一窍不通,所以这里只好暂时用了一下 NextJS 的快速开始模板并加入一些魔改,在和 ESLint 以及 next 做了许多斗争,期间不厌其烦地打扰 @tukideng 询问各种奇葩问题并被白眼数次之后,往 Cloudflare Pages 上一挂,用 nextbuild,嘿,就出现了这么个神奇的页面。

网址: https://download.halo.run/ 看上去像是能用的,不是嘛?

Speedtest

最后我们来随意测个速吧,测速的机器为 Hetzner 德国的机器,分别下载以下两个地址:

速度分别如下:

wget https://github.com/halo-dev/halo/releases/download/v1.6.0/halo-1.6.0.jar
--2022-11-14 08:31:07--  https://github.com/halo-dev/halo/releases/download/v1.6.0/halo-1.6.0.jar
Resolving github.com (github.com)... 140.82.121.3
Connecting to github.com (github.com)|140.82.121.3|:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: https://objects.githubusercontent.com/github-production-release-asset-2e65be/126178683/8ee886dc-48a4-47ce-a096-47871737d506?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20221114%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221114T003107Z&X-Amz-Expires=300&X-Amz-Signature=60fa40ea63def87878b9f8c499f12bdcc7b41775b394b78d3e436b8df8963ef9&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=126178683&response-content-disposition=attachment%3B%20filename%3Dhalo-1.6.0.jar&response-content-type=application%2Foctet-stream [following]
--2022-11-14 08:31:07--  https://objects.githubusercontent.com/github-production-release-asset-2e65be/126178683/8ee886dc-48a4-47ce-a096-47871737d506?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20221114%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221114T003107Z&X-Amz-Expires=300&X-Amz-Signature=60fa40ea63def87878b9f8c499f12bdcc7b41775b394b78d3e436b8df8963ef9&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=126178683&response-content-disposition=attachment%3B%20filename%3Dhalo-1.6.0.jar&response-content-type=application%2Foctet-stream
Resolving objects.githubusercontent.com (objects.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ...
Connecting to objects.githubusercontent.com (objects.githubusercontent.com)|185.199.108.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 96866731 (92M) [application/octet-stream]
Saving to: ‘halo-1.6.0.jar’

halo-1.6.0.jar             100%[=====================================>]  92.38M  23.3MB/s    in 4.1s    

2022-11-14 08:31:12 (22.3 MB/s) - ‘halo-1.6.0.jar’ saved [96866731/96866731]

wget https://dl-r2.halo.run/release/halo-1.6.0.jar 
--2022-11-14 08:32:02--  https://dl-r2.halo.run/release/halo-1.6.0.jar
Resolving dl-r2.halo.run (dl-r2.halo.run)... 2a06:98c1:3121::3, 2a06:98c1:3120::3, 188.114.96.3, ...
Connecting to dl-r2.halo.run (dl-r2.halo.run)|2a06:98c1:3121::3|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 96866731 (92M) [application/zip]
Saving to: ‘halo-1.6.0.jar’

halo-1.6.0.jar             100%[=====================================>]  92.38M  27.6MB/s    in 3.4s    

2022-11-14 08:32:07 (27.6 MB/s) - ‘halo-1.6.0.jar’ saved [96866731/96866731]

以上,希望能给耐心看到这里的你带来一些新的灵感~

References

  1. Announcing Cloudflare R2 Storage: Rapid and Reliable Object Storage, minus the egress fees
  2. Limits · Cloudflare Workers docs
  3. How to disable ESLint for some lines, files or folders
评论