前些日子买了相机，于是我开始随地大小拍。拍了半天，总不能只把照片放在NAS里留着传家，总归要发出来给大家看的。虽然我的服务器租用的是CN2线路，能够在海外为国内用户提供良好的访问体验，但问题是它流量贵啊，我这一张照片10MB，这一个月下来看不了几张照片就没了。想来想去，还是得整个CDN。本文记述如何使用BackBalze B2存储和CloudFlare Worker搭建一个四舍五入就是不要钱的CDN。计费机制在开始搭建之前我想首先介绍一下我这个方案的计费机制。首先我们的大慈善家CloudFlare允许免费用户使用CDN和缓存机制，虽然你没办法控制缓存的细节，但对于我的需求来说，只要能缓存就行，完全不需要这种细粒度的控制和其他任何花哨的功能。其次为了保证我们的桶不被滥用，我们需要使用Worker来访问私有桶，而Worker的免费计划也非常慷慨：每天10万请求，每个调用限制10毫秒CPU时间。对于我的需求来说，Worker需要做的就是将用户访问的URL转换成对桶的签名URL访问，然后让CloudFlare去抓取即可。抓取的耗时并不计算在调用的CPU时间内，实际计算的时间只是解析URL、生成签名URL。除非我的照片每天被访问10万次以上，否则不会有任何账单产生。对于存储来说，除了付存储的账单，最要命的实际是流量费。而BackBlaze和CloudFlare是带宽联盟，这就意味着从BackBlaze传送到CloudFlare的流量是不计费的。因此我们只需要付出最基本的存储费用即可。当然CloudFlare本身也提供R2存储，但R2每月每TB需要15美元，而B2只需要6美元。另外我喜欢BackBlaze的一点是，他们有月均存储量x3的免费出站流量。比如说你一个月平均下来每天存储了1TB数据，那么你从BackBlaze下载，每个月有3TB的免费出站流量，这个和带宽联盟是单独计算的。带宽联盟的流量就是免费的，不占用你的三倍免费额度。这是什么？这是大善人啊！你再看看AWS，传入是免费的，但传出的每一个比特都要计费。没有一个人能看着自己的AWS账单笑得出来。搭建CDN其实BackBlaze官方有提供如何使用CloudFlare来提供对B2桶的访问。根据桶的访问权限，主要分为两种：公开和私有。公开桶意味着所有人都可以不需要任何许可地访问你其中的文件，而你要为此付钱。私有桶则需要使用API Key之类的东西鉴权后才能访问，这通常要求实现了特定协议的客户端（比如S3协议），而浏览器是不具备这种功能的。我不是百万富翁，我怕被别人刷流量刷爆账单，所以我选择私有桶。搭建过程主要参考这篇文章：https://www.backblaze.com/docs/cloud-storage-deliver-private-backblaze-b2-content-through-cloudflare-cdn第一步是去BackBlaze创建一个私有桶，然后创建一个Application Key用来给Worker访问这个桶（推荐使用只读权限，并仅限于这个桶，并且不允许list bucket）。你也可以上传一些测试图片用于验证。接下来我们要配置CloudFlare Worker。由于CloudFlare Worker使用JavaScript（虽然他们也支持别的语言，但从费用上来说还是JS最划算），因此NodeJS必不可少。我电脑上有安装nvm（视窗操作系统用户可以考虑使用nvm-windows），使用它可以方便地管理多个NodeJS版本。要使用Worker的sdk，NodeJS版本要在16.17.0之后。我使用的是20.17.0版本，之前安装过的。首先使用npx wrangler安装这个名叫wrangler的工具，它是我们和Worker交互的命令行工具。我不善JS，索性BackBlaze官方给出了一个示例仓库，我们直接clone即可：git clone https://github.com/backblaze-b2-samples/cloudflare-b2.git cd cloudflare-b2 npm install cp wrangler.toml.template wrangler.toml现在我们就可以编辑wrangler.toml这个文件了。按照里面的注释，填写B2_APPLICATION_KEY_ID和B2_ENDPOINT等字段，这些信息都可以在BackBlaze的控制台找到。我这里没有开list bucket和rclone下载，因为我只是想用它做个分发图片的CDN。至于桶的名称，由于我的域名中不包含任何桶的名称，因此我直接填写的固定值。根据这个字段的注释，你可以实现一些更高端的功能。如果你想追求极致的精简（比如通过减少一些分支判断来节省几纳秒的CPU时间），你也可以直接修改源代码来删掉这些花哨的功能。如果你只是修改了那些变量，还没完。如果你注释读得仔细，你就知道还得用echo "<your b2 application key>" | wrangler secret put B2_APPLICATION_KEY这个命令把你的B2 API Key secret作为机密信息存储到Worker配置里。当然，如果你不想这样，比如说你想随时随地查看你的密钥，那么你把密钥作为B2_APPLICATION_KEY写入到刚才的toml文件即可。最后，如果你想修改Worker的名字，你可以修改这个toml文件最上面的name字段。全部完成后即可运行npx wrangler deploy，部署成功之后会自动给你分配一个*.workers.dev的域名。如果你自己没有域名的话，可以就用人家给的。想要用自己的域名的话，你就得让CloudFlare接管你整个域的DNS，并不能像AWS的Route53那样只接管一个自域名。我的解决办法是再买一个域名。这个时候还没完——数据从B2出发来到CloudFlare，但是现在还不会缓存。换句话说，现在每一次对CDN域名的访问，都要调用Worker进行一次签名。不信你就打开浏览器的控制台看，找到响应头中的cf-cache-status，应该是BYPASS，就是没有经过缓存。虽然每天10万的配额很富裕，但本着为众人抱薪者，不可使其冻毙于风雪的想法，我们还是打开缓存给大善人省点是点。在BackBlaze的控制台中，我们可以配置桶信息，填写{"Cache-Control":"public, max-age=86400"}，这里我们设定缓存有效期是1天。这个取值要根据使用情况来看。我的文件上传了就不再变动，1天对于我来说很好。更新桶信息之后等待一段时间使之生效，随后再去访问我们的CDN，同样找到响应的cf-cache-status头，第一次应该是MISS，多刷新几次就会变成了HIT。所有命中缓存的请求都不再调用Worker，而是直接从缓存里面返回结果。这样可以大大延长免费配额的使用寿命。照片发布流程当然了，照片不能乱发，得处理一下才行。原始的RAW文件中会包含许多数据，比如相机的型号，镜头的型号，焦距、光圈、ISO和快门时间等信息。这些数据有助于提供关于照片的额外信息，但如果处理不当，可能会带来隐私风险。比如尼康会在文件中记录相机和镜头的序列号，以及GPS座标等。这就意味着我们在发布之前需要检查一下元数据有没有泄露我们的隐私。这里使用名为exiftool的工具来进行过滤。以下命令会处理当前文件夹下的所有文件，如果需要处理其他文件夹，可以将.替换为路径。 注意：以下命令会修改源文件，请确保你有备份。 如果要删除所有元数据：exiftool -overwrite_original -EXIF= -XMP= .如果只需要删除相机和镜头序列号，并保留其余所有信息：exiftool -overwrite_original -*Serial*= -SerialNumber=0000000 .若需要删除GPS信息：exiftool -overwrite_original -*gps*= -*GPS*= .检查文件夹内所有照片的元数据：exiftool -t -G . | sort | uniq完成后可以将文件名修改为哈希，避免了取名字的烦恼：./rename-b3sum.sh source/ output/这个rename-b3sum.sh是我自己写的脚本：#!/bin/bash # Green printSuccess() { echo -e "\e[32m$1\e[0m" } # Yellow printWarning() { echo -e "\e[33m$1\e[0m" } # Red printError() { echo -e "\e[31m$1\e[0m" >> /dev/stderr } # test b3sum b3sum --version >> /dev/null if [ ! "$?" = "0" ]; then printError "b3sum not found" exit 1 fi # get input dir from param inputDir=${1%/} # check input dir not empty if [ "x$inputDir" = "x" ]; then printError "Missing input dir" printError "usage: $0 <inputDir> <outputDir>" exit 1 fi # check input dir is valid if [ ! -d "$inputDir" ]; then printError "$inputDir is not a folder, or not exists" exit 1 fi # get output dir from param outputDir=${2%/} # check input dir not empty if [ "x$outputDir" = "x" ]; then printError "Missing output dir" printError "usage: $0 <inputDir> <outputDir>" exit 1 fi # try to create the dir mkdir -p "$outputDir" if [ ! "$?" = "0" ]; then printError "Failed to create output dir $outputDir" exit 1 fi # print input and output folder printSuccess "Renaming files from $inputDir to $outputDir" # skip if the input folder is empty if [ -z "$(ls -A "$inputDir")" ]; then exit 0 fi # loop through the files for file in "$inputDir"/* ; do # do non-empty folders recursively if [ -d "$file" ]; then $0 "$file" $2 status=$? # check the status if [ ! "$status" = "0" ]; then exit $status fi continue fi # ignore none file items if [ ! -f "$file" ] && [ ! -d "$file" ]; then printWarning "Ignoring non-file and non-folder item $file" continue fi # calculate the hash hash=$(b3sum "${file}" | cut -d' ' -f1) # get extension ext=".${file##*.}" if [ "$ext" = ".$file" ]; then # if ext is exactly same as file, then there is no extension for that file ext="" fi newName="${outputDir}"/${hash}${ext} # check target if [ -f "$newName" ]; then printError "Existing output file $newName for $file" exit 1 else # doesn't exist, do the renaming echo -ne "\e[32m" # don't override cp -v "$file" "$newName" echo -ne "\e[0m" fi done这个脚本调用b3sum来计算文件的哈希，并且保留文件后缀名。如果遇到重名文件则会报错，以防哈希冲突时意外覆盖文件。后记至此我终于可以放心大胆地分享我的图片，而不必担心账单爆炸了。关于B2的账单，这里有个小插曲：我以为带宽联盟的流量是不计费的，但实际上它是先计入每日账单，然后在每月的账单中抵消掉。这就意味着如果你每日下载限额调的很低，即便去往CF的流量是免费的，你也很快会耗尽每日配额并得到一个错误。我和B2的客服确认过，你要确保你的每日限额能够支撑你各种出站的流量，其中免费的部分会分别在阅读账单中抵消。-全文完-