Switching from S3 to Tigris on Fly.io

Ben Hoyt

在 Fly.io 上從 S3 切換至 Tigris

原文由 Ben Hoyt 發布,訂閱此部落格

一年前我把我的 side project GiftyWeddings.com 從 Amazon EC2 遷移到 Fly.io 上。過程還滿有趣的,每個月也省了幾塊錢。

上週 Tigris 的人聯絡我,問我是否願意試用他們在 Fly.io 基礎設施上打造、並已整合進 fly CLI 的 S3 相容儲存服務的私人測試版。(Tigris 有付我一小筆費用請我試用並撰寫心得,但他們沒有干涉內容。)

我其實之前沒聽過 Tigris,但這引起了我的興趣——我對 Fly.io 的體驗一直不錯,也覺得有更多小公司出來跟 AWS 這頭「貝佐斯巨獸」(Bezomoth)競爭是件好事。

我已經用 Fly.io 來跑 Gifty 的伺服器(用 Go 寫的),不過就在幾天前,我的檔案儲存——使用者上傳的圖片和 SQLite 備份——還是放在 Amazon S3 上。

切換過程很直觀:把檔案複製過去、把伺服器的設定改成指向 Tigris 的 endpoint 而不是 S3,然後 fly deploy 就好。因為 Tigris 在驗證和快取的處理方式不同,我得做一點小幅的程式修改,也遇到了幾個小怪癖,但整體來說非常順利。

建立儲存桶

第一步是為使用者上傳的圖片建立一個測試用的儲存桶。我直接下了一道 fly storage create 指令:

$ cd gifty  # change to app directory
$ fly storage create --public
? Choose a name, use the default, or leave blank to generate one:
    test-gifty-registry-images
Your Tigris project (test-gifty-registry-images) is ready. See details and
next steps with: https://fly.io/docs/reference/tigris/

Setting the following secrets on gifty:
BUCKET_NAME
AWS_ENDPOINT_URL_S3
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
AWS_REGION

Updating existing machines in 'gifty' with rolling strategy

-------
 ✔ Machine 4d89601c6e9487 [app] update succeeded
-------
Checking DNS configuration for gifty.fly.dev

讓我有點驚訝的是,光是建立一個儲存桶,居然就自動設定了一堆環境變數並重新部署了我的應用程式。會這樣是因為我當時就在放著應用程式 fly.toml 設定檔的目錄下執行指令,不過這個行為感覺有點出乎意料、過於神奇——畢竟我只是想為本地開發建立一個測試用的儲存桶而已。

這部分其實在文件裡有寫,所以某種程度上算是我沒仔細看——不過我覺得他們應該要加上「並重新部署」才對:

在 Fly.io 應用程式的脈絡下執行以下指令——在應用程式目錄內,或是指定 -a yourapp ——將會自動在你的應用程式上設定 secrets。

這個功能可能會導致停機,所以我覺得應該要改成可選擇啟用,例如提供 --update-app--add-secrets 這樣的指令列選項。

而且也沒有簡單的方法可以查看那些自動設定的 secrets 的值。這是基於安全性的刻意設計,但也意味著我得在應用程式目錄外再建立一個新的測試儲存桶,才能拿到本地測試用的憑證:

$ cd ..  # change out of app directory
$ fly storage create --public --org ben-hoyt --name test-gifty-registry-images2
Your Tigris project (test-gifty-registry-images2) is ready. See details and
next steps with: https://fly.io/docs/reference/tigris/

Set one or more of the following secrets on your target app.
AWS_REGION: auto
BUCKET_NAME: test-gifty-registry-images2
AWS_ENDPOINT_URL_S3: https://fly.storage.tigris.dev
AWS_ACCESS_KEY_ID: ***hidden***
AWS_SECRET_ACCESS_KEY: ***hidden***

好,這樣好多了。接著我用那組 access key 和 secret 更新了本地的應用程式設定,結果……大致上能動了。我得先把幾個小問題修掉,下面會說明。

建立儲存桶時還有另一個小怪癖——如果 fly CLI 的開發者有看到這篇——就是當你執行 fly storage create 時,它印出的環境變數順序是不固定的。你可以從上面的例子看到:第一個範例先印出 BUCKET_NAME,第二個卻是 AWS_REGION 先出現。這不是什麼大問題,但有點奇怪,我還愣了一下確認兩次印的其實是同一組 key。

端點變更

很明顯,我需要改用新的 Tigris API 端點,而不是預設的 AWS 端點。原本建立 S3 client 時,我是這樣寫的:

cfg := aws.NewConfig().WithCredentials(creds).WithRegion(region)

為了使用正確的端點,我加了這幾行:

endpoint := os.Getenv("GIFTY_S3_BACKUP_ENDPOINT")
if endpoint != "" {
	cfg = cfg.WithEndpoint(endpoint)
}

要注意我用的還是 AWS SDK for Go 的 v1 版。這個做法在 v2 版中有點不同

快取差異

在使用 Amazon S3 時,當使用者為他們的婚禮禮物清單上傳新圖片,我發現直接用同一個檔名(key)覆蓋上傳也能過關。頁面重新載入、圖片重新抓取後,新圖片就會出現。

老實說,就算在 S3 上這樣做也不見得是好主意,因為早期的 S3 只有最終一致性。不過在 2020 年他們改成了寫入後讀取的強一致性,所以在 PUT 之後的所有 GET 都會立刻拿到更新後的內容。

改用 Tigris 之後,當我重新上傳一張圖片,必須按 Ctrl-Shift-R(略過快取重新整理)新圖片才會出現。Tigris 似乎做了更積極的快取:查看從 Tigris 上傳的圖片回應標頭,會發現它預設就送出了 Cache-Control: max-age=3600,即使我根本沒設定這個標頭。

我不確定這是不是個好的預設值,但因為他們想同時兼具CDN 和儲存系統的功能,我可以理解為什麼會這樣設計。這會迫使大家使用像基於內容的檔名這類快取技巧,而這其實是好事。

更新:在讀完這篇文章後,Tigris 補上了一個很棒的快取說明頁面,詳細記錄了這個行為與他們在這方面的設計初衷。

無論如何,這倒是個讓我改善 Gifty 上傳處理的好機會。我改了程式碼,在檔名中加入一小段基於內容的 hash:原本用 {registryID}.jpg 當作 key,現在改用 {registryID}-{contentHash}.jpg,就解決了快取問題:

// Add content-based hash to filename to break caching (12 hex digits).
hash := sha1.New()
_, _ = hash.Write(data)
key := fmt.Sprintf("%d-%x.jpg", registry.ID, hash.Sum(nil)[:6])

我也把原本寫死的 amazonaws.com 完整圖片 URL 改成可設定的 URL 格式:

// Old version
path := fmt.Sprintf("https://%s.s3-%s.amazonaws.com/%s",
    h.config.S3ImageBucket, h.config.S3ImageRegion, key)

// New version; under Tigris, I set S3ImageURLFormat to
// "https://fly.storage.tigris.dev/gifty-registry-images/%[1]s"
path := fmt.Sprintf(h.config.S3ImageURLFormat, key)

驗證方式的差異

使用 S3 時,我有兩個儲存桶——一個放禮物清單的圖片,一個放備份——兩者都用同一組 AWS key 和 secret 來驗證。這做法大概稱不上好(我本來應該為備份用不同的憑證),但以我的經驗來說,小型網站用單一金鑰其實還滿常見的。

改用 Tigris 後,每建立一個儲存桶就會拿到一組新的 key 和 secret。所以我得稍微改一下程式碼,多加兩個與 S3 相關的環境變數。改了幾行程式碼後,備份功能就正常了。

複製檔案與「影子儲存桶」

既然測試環境都跑通了,我就想把正式站也切換過去。

針對從 S3 遷移,Tigris 有個很酷的功能叫影子儲存桶(shadow buckets),可以讓你把 Tigris 儲存桶指向原本的 S3 儲存桶,如果檔案在 Tigris 儲存桶中不存在,就會無縫地從「影子儲存桶」拉取(並複製過去,讓之後的請求可以直接從 Tigris 取得)。

我本來應該試試這個功能,但最後決定不用,因為我最大的儲存桶裡也只有幾百個檔案,而且我想要乾淨俐落地一次切換完成,結束後就完全擺脫對 S3 的依賴。

所以我用 aws s3 sync 指令先把檔案從 S3 複製到本地磁碟,再從本地複製到新的 Tigris 儲存桶。我的網站規模很小,在切換那幾分鐘內有檔案遺漏的機率極低。

我為新的 Tigris 儲存桶建立了兩個 aws CLI 設定檔(profile),分別叫做 gifty-registry-imagesgifty-backup,憑證各不相同。預設的 profile 則仍指向 Amazon S3。

以下是我用來把檔案從 S3 複製到 Tigris 的指令(我有先用方便的 --dryrun 選項預覽):

# Copy the user-uploaded images
$ mkdir gifty-registry-images
$ cd gifty-registry-images
$ aws s3 sync s3://gifty-registry-images .
...
$ aws s3 sync . s3://gifty-registry-images --profile gifty-registry-images
...

# Copy the backup files
$ cd ..
$ mkdir gifty-backup
$ cd gifty-backup
$ aws s3 sync s3://gifty-backup .
...
$ aws s3 sync . s3://gifty-backup --profile gifty-backup
...

資料庫遷移

只要我繼續付錢給 AWS,原有的 S3 URL 還是可以正常使用。不過如前面所說,我想徹底移除對 S3 的依賴。

我在網站的 SQLite 資料庫裡儲存了完整的圖片 URL。所以複製完檔案後,我用一個簡單的 SQLite 查詢來更新資料庫中的圖片 URL:

-- First check how many S3 image URLs there are
sqlite> SELECT count(*) FROM registry
        WHERE image_url LIKE 'https://gifty-registry-images.s3%';
389

-- Perform the update
sqlite> UPDATE registry SET image_url = replace(image_url,
        	'https://gifty-registry-images.s3-us-west-1.amazonaws.com/',
        	'https://fly.storage.tigris.dev/gifty-registry-images/')
        WHERE image_url LIKE 'https://gifty-registry-images.s3%';

-- Ensure there are no more S3 URLs
sqlite> SELECT count(*) FROM registry
        WHERE image_url LIKE 'https://gifty-registry-images.s3%';
0

這樣一來,網站就完全跑在 Tigris 和 Fly.io 上,百分之百擺脫 AWS 了!

最後心得

效能

我做了一個快速、非嚴謹的測試,比較從舊的 AWS 儲存桶和新的 Tigris 儲存桶抓取圖片所需的時間。Tigris 的儲存桶預設就是「全球性」的,所以會使用最近的 Fly.io 節點(雪梨),比起 AWS 儲存桶所在的 us-west-1 區域,離紐西蘭近得多。

如預期,Tigris 的下載速度明顯快很多:小圖片大約是 AWS 的 1 秒對上 Tigris 的 300 毫秒;中等大小的圖片則大約是 AWS 的 1.3 秒對上 Tigris 的 600 毫秒。

價格

Gifty 在 AWS 上的帳單——這時已經只有 S3 的費用——每個月才幾美分。我敢打賭光是處理我的帳單,AWS 的行政成本就比我付的錢還高。所以價格對我來說不是什麼問題,不過 Tigris 的定價和 S3 還是很接近:

  • Tigris:儲存費用為每月每 GB 0.02 美元,GET 請求為每 1000 次 0.0005 美元。
  • S3:儲存費用為每月每 GB 0.023 美元,GET 請求為每 1000 次 0.0004 美元。

不管怎麼說,Tigris 在 beta 註冊過程中給了我 150 美元的免費額度。這相當於 7500 GB-月或 3 億次 GET 請求——幾乎可以肯定比我這個小網站一輩子會用到的量還多。

免責聲明與耐久性

有一件事你可能會想知道:當我註冊他們的搶先體驗 beta 時,Tigris 寄了一封信說:「在 beta 期間請避免將此服務用於正式環境的工作負載!」

我問他們這只是為了自保的免責聲明,還是真的像「檔案會隨機消失,我們會在 beta 期間每晚刪除所有資料」那樣嚴重。他們說這比較像是免責聲明,Fly.io 和 Tigris 都是把它當作正式環境的平台在對待。顯然還是有風險,但我可以接受,而且他們很快就會結束 beta 了。

我也很好奇 Tigris 的耐久性跟 S3 相比如何,S3 可是宣傳物件耐久性高達 99.999999999%。Tigris 在架構上顯然下了很多功夫,但不知道是否有辦法實際衡量它的耐久性,並拿來跟 S3 那驚人的數字比較?話說回來,S3 那 11 個 9 的數字真的有意義嗎

結語

總的來說,這次切換的體驗非常好,到目前為止也還沒遇到任何問題——不過還是要讓時間來證明!我希望 Tigris 和 Fly.io 都能持續發展,為大型雲端業者帶來一些急需的競爭。

本文章由 muse-spark-1.2-contributor 進行翻譯

留言