最近開始把腦筋動到每個月要吃掉我20鎂的linode,看看能不能用Google Compute Platform來取代掉單純的VM。GCP本身有提供Free Tier, 所以只要小心地控制用量,搬遷應該是可以省下一筆,而且也可以玩玩GCP的一些新功能。
需求分析
我這次先搬遷之前寫的一個爬蟲服務,這個爬蟲服務應該是我現有服務裡面最簡單的一個,首先先分析一下他有什麼特性:
- 需要定時被喚醒(設計上大概10分鐘一次就夠了)
- 需要一個地方存放爬下來的分析資料
- 不需要實體上的Database
- 程式本體相當小
舊有的方案以及缺點
之前的方案是一個純application,走infinite for loop,在裡面放ticker來計時10分鐘一次,然後把他包成docker image用daemon mode跑起來。
這個跑法算是滿傳統的,在非雲端時代也是一個很標準的做法。只是這樣跑了幾個月,發現了有以下這些問題:
- 需要寫一個service script來確保host vm被重啟的時候,這個服務能夠在被拉起來。
- 還是得佔著一塊不算小的記憶體,只為了10分鐘跑一次。
- up了幾個月有時候會卡死,也許有bug也許是memory leak,但是從code其實看不太出問題,而且重現也滿困難的。
docker.io
掛點過幾次,新的image推不上去拉不下來
這些問題其實都能以傳統手段解決,比方說最簡單的方式就是把它改成cron job,從OS層面來排程而非code層面,docker.io
也不是很常掛點,真的掛掉的話改用其他免費的image registry也是可以的。
選擇Serverless
Google提供了三種不同的方法來作Serverless:
- Google App Engine
- 最老牌
- 如果你用gin-gonic寫的話,需要的改動相當小
- 說serverless….其實比較像是他幫你host一個server,只是這個server會自動按照需求啓閉而已,所以他可以是stateful
- 支援PubSub,但是是以push的形式走http route進來
- Debug/Deploy相比於Cloud Function都簡單一點
- 按run quota付費,也就是可能會因為container跑在上面比預期的久,所以需要付的錢比預期多一點。
- Cloud Functions
- 最輕量
- 很像AWS Lambda
- 原生支援PUB/SUB
- Deploy很難搞,command line頗複雜,雖然弄好以後不太會碰到
- 不過deploy不需要by container去deploy,完全就是build from source code
- Cloud Run
- 很新的服務
- 其實就是Cloud Functions的container版本
- 可以想成是Knative的POD,跟k8s的pod設定可以通用
這個爬蟲本身吃的資源不多,也不需要http跟外界溝通,用pubsub也挺方便的,所以就選擇了用Cloud Functions來當作deploy的方法。重點是,Cloud Functions免費額度應該算是最彈性的,應該怎麼打都不太會超過免費額度上限,所以就用吧!
實作
結構
預期中的結構會是像這樣
- PubSub定義一個message
- SecretManager把Config放在裡面
- Cloud Scheduler定期(十分鐘)發送這個message
- Cloud Functions吃到這個PubSub就被喚醒執行
而爬蟲的config由於有credential,所以由Secret Manager來存,然後用mount的方式掛進Cloud Functions。
Scheduler
首先我們需要一個定時器。幸運的是,GCP本身就有提供Cloud Scheduler來提供定時的功能。
Frequency基本上就是Cron的format,比方說例子中的*/15 * * * *
就是每15分鐘一次。Target Type選擇Pub/Sub
,下面的message道是隨意,給個空Body {}
就好。不過,這邊有個限制,就是message / message attributes至少要有一個,就隨便放一個就好。Pub/Sub topic
則是直接選擇Create a topic就好。
Cloud Source Repositories
Functions有很多種deploy方式,不過最方便的方式應該是用git。但是,使用git的話,必須要用GCP自己的Cloud Source Repositories(CSR)託管。幸運的是,CSR是可以作為GitHub的mirror存在的,也就是說,當我們把code push到GitHub後,很快該commit就會被mirror到CSR。
首先,先進入CSR,按下右上角的Add Repository,然後選擇Connect External Repository。
接下來就是行禮如儀,選擇Project,選擇GitHub當作Git Provider,然後經過一連串的Authorization後,把這個repo給Mirror進來就可以了。
Secret Manager
由於我們的config包含了一些credential,所以比較適合存放在Secret Manager內。這東西使用起來非常直觀,就建立一個key,把config塞進去就好
他可以upload檔案,所以甚至可以把gcp credential(JSON)塞進去。
Cloud Functions
他基本原理還滿簡單的,不過有些眉角:
- 他的package name要是FQDN。比方說
AppleTwNCCSpy
不行,github.com/Rayer/AppleTwNCCSpy
才行。 - 目前來講放置EntryPoint的
.go
必須要在最上層。如果是放子目錄的話,無論該子目錄有沒有獨立的go.mod
, 都會在fetch dependency的階段有錯誤。這問題很隱晦,如果不想浪費時間的話,請務必把它放在最上層。 - 我會建議先推一個絕對可以過的版本再慢慢改。Functions Deploy非常麻煩,而且所有輸入的數據都要成功deploy才會儲存——亦即,如果你deploy failed,所有東西都要重打!超煩的 orz
首先,建立一個任何名字的.go
檔案,裡面放一個這種簽名的function : func Func(ctx context.Context, m PubSubMessage) error
,記得函數名稱開頭要大寫export,不然會讀取不到。事實上,他一開始建立的範例也會提供這個簽名給你參考。我個人是建議開一個新的branch把它給push上去,接下來就可以開始設定了。
然後進入Cloud Functions,選Create Function。
Basics沒啥好注意的,就Trigger Type選擇剛建立好的Pub/Sub
以及Topic選擇剛建立好的Topic即可。
Runtime倒是隨意,要是工作loading很低的話建議把Memory選低一點。像是我這種Crawler的話,下面的Autoscaling的min
設定0,max
設定1是最好的。注意,如果min不是設定0的話,會一直有一個instance長在上面,很貴!有這種需求的話App Engine或者VM會是更好的選擇。
Secrets就是重頭戲了,我們會把Config寫成yaml放在Secret裡面。舉例來說,我們Secret是剛剛設定的DemoConfig
Reference Method選擇Mounted as volumn(建議不要用env expose),Mount Path/secrets
, 最後的path選擇/secrets/DemoConfig.yml
這樣設定的話,我們在function就可以這樣得到這個值:
confByte, err := ioutil.ReadFile("/secrets/DemoConfig.yml")
if err != nil {
return err
}
接下來看你要用json parse, yaml parse還是raw text就都可以了。
最後,最麻煩的地方來了
Runtime當然選Go 1.16,Source Code我是先建議先選擇Inline Editor然後存一次,再回來改,不然deploy的話你剛打的東西都要重打一次!存好回來以後,Source Code選擇Cloud Source Repository,右邊的Repository的名字…請多開一個窗去CSR,把repository name拷貝回來貼上,這邊沒有自動完成 …
還記得我建議嗎?把function寫在根目錄,多開一個branch,所以這邊就行裡如宜的選Branch,填入你剛寫好的branch name,然後Directory with source code就維持/
,最後Entry Point填入function name,按下deploy! 順利的話應該就會建立出來了。
Re-Deploy
目前來講command line還缺了一些東西,所以強烈建議先用UI Deploy一次後,再用command line redeploy。以我自己的例子來講 ,如果要deploy from git:
gcloud functions deploy apple-ncc-crawler-dev --region=us-central1 --entry-point CrawlAndAnalyze --memory 128MB --runtime go116 --source "https://source.developers.google.com/projects/iris-303620/repos/github_rayer_appletwnccspy/moveable-aliases/master/paths//"
如果要從local deploy,拿掉–source標籤即可。其實deploy from local應該比較方便,只是command line缺不少設定,比方說目前secret->mount沒辦法從command line去設定(但是很妙的是,command line會告訴你相關訊息)。
Command deploy有些要注意的地方,最坑的就是--source
這個parameter。他看起來很像是一個https
開頭的網址,事實上不是。比方說,他是https://source.developers.google.com
開頭,不是CSR的網址https://source.cloud.google.com
。所以最保險的方法就是拷貝這條一個個自己改:
https://source.developers.google.com/projects/<ProjectID>/repos/<Repository名字>/moveable-aliases/<Branch>/paths//
這樣應該就可以大功告成了。建議把deploy指令寫在project裡面一個.sh
裡面方便以後使用。