淺談Serverless Solution — 以GCP Cloud Function為例
什麼是Serverless?
字面上意思很像是沒有Server,不過真正意義是你完全不要擔心維護Server這件事情。各家大廠也有對應的產品,例如Amazon AWS Lamda, Google Cloud Platform Cloud Function。由於工作上選用GCP,因此本篇將透過GCP Cloud Function嘗試總結自己在Serverless Solution的理解以及實用小技巧。
不管是Cloud Function或是AWS Lamda,在架構上都是以一個Function為單位,引導開發者往低耦合的方向開發應用程式,設計思維需要做點調整,首先要面臨的就是一個Function最小應該負責的事情是甚麼,Function之前互相應該如何溝通。
下文會依照軟體開發不同階段,提供Cloud Function應該注意的事項以及一些實作。此外,對於程式撰寫細節不會多加著墨,主要著重分享各階段的一些小小心得。
規劃(Plan)
為何選擇Cloud Function?
Cloud Function是一種輕量級的雲端應用方式,也是一種微服務(Micro Service)適合單一目的、獨立的功能。透過各種事件的觸發,讓開發者可以不用管理Server或控制執行環境。Cloud Function含以下特色:
- 在雲端環境執行程式碼最簡單的方法
- 可自動擴充,具備高可用性和容錯性
- 無需佈建、管理、修補或更新伺服器
- 執行程式碼時才需付費
- 可連結和擴充雲端服務
計價模式是使用者付費,當Function有被執行時才有對應的費用,不用隨時開著Server並且管理。然而,在雲端環境開發服務,Cloud Function是一個簡單且便宜的解決方案,但是應當注意其限制以及使用情境,以下幾點供評估參考:
- 每個Cloud Function有time out限制,預設1分鐘內,當超過限制後會拋出錯誤狀態且return。目前能夠容許的最大time out時間是9分鐘
- 目前單一Function最大的可用Memory是2GB,如果執行期間程式超過其Memory,將會拋出錯誤中斷。就資料科學應用而言,這個情境將不適合應用大量資料的載入及運算相關功能。
- Cloud Function執行環境目前支援Node.js、Python3.7、GO等,一些主流企業愛用程式語言如Java, C#等並未提供。
- Cloud Function本身執行環境擁有唯讀的檔案系統,檔案系統的大小與Memory相依,位置在/tmp,適合用來暫存檔案。
- 多個Cloud Function間並不共享Memory, Global Variable, File System。如果有資料共享的需求,可以使用GCP上的Storage Service
先前提到目前支援幾個相對新穎的程式語言,以下各間段範例將以Python為主作為範例。
建置(Build)
設計Cloud Function架構
既然是需要多個Function的設計模式,每個Function的參數/進入點就是一個必須要被優先思考的事情。以Cloud Function來說,是Event/Trigger的模式,因此在設計上需先釐清透過甚麼事件去驅動Cloud Function。
GCP環境中有許多雲端事件可以被收集當作Trigger的條件,例如上傳檔案至Storage或是一個pub/sub訊息(類似Kafka的Message Queue), 而這些雲端事件將Cloud Function被分為兩類:
Http Functions
將(request)
(Flask Request Object)作為參數,透過自動產生的url並使用該url(send request)觸發。
Backgroud Functions
將(data, context)
作為參數,是一種可以接收各種雲端服務事件的設計方式,以下引用官方說明:
data (dict): The dictionary with data specific to this type of event.
context (google.cloud.functions.Context): The Cloud Functions event
metadata.
舉例來說,一個Function在GCP雲端環境上監聽某個Storage,當檔案被上傳到該位置後發出事件通知,Cloud Function做出對應動作,例如解析檔案並且儲存於資料庫。我在建置時還會嘗試勾勒相關應用,產出架構圖,大致上概念如下:
定義/決定你應用情境(包含應該要什麼事件觸發)、繪製設計圖後,接著就可以著手進行開發。
初始化專案開發架構
Cloud Function概念是根據Event/Trigger觸發後執行程式邏輯,程式的進入點即是一個主程式 main.py
,如果有其他相依副程式,可以包裝成package並且import至主程式中,依需求建立。概念上佈署到GCP的進入點就是main.py
以下分享一下我開發時的程式專案的架構:
- 建立專案資料夾架構
cloud function本機開發的參考專案架構如下:
myfunction/
├── main.py
├── requirements.txt
└── lib/
├── __init__.py
└── your_lib.py
是否需要lib資料夾依各人需求建立,你也可以將其他可能會用的module放在同一層直接import,分離資料夾的目的主要還是比較乾淨好管理,若是需要lib的話記得在資料夾底下建立一個無內容的__init__.py
2. 建立requirements.txt
cloud function服務要被啟動時才安裝對應的環境,requirements.txt這個描述檔目的就是在與GCP環境溝通在你的開發應用上究竟用了什麼延伸模組,也就是告訴GCP需要pip install什麼東西。
- 格式: <package_name> == version
google-cloud-error-reporting==0.30.0
lib
sqlalchemy
pandas
numpy
值得注意的是若是你跟我一樣要分離一些lib到另外一個資料夾,記得也要到requirements .txt加入描述。而依照上面架構,如果你要import自己自定義的lib的話,可以在主程式這樣寫:
from lib.your_lib import xxx
更多細節我建議參考這裡
3. 撰寫Cloud Function
會建議針對Google本身提供的sample code去改寫,框架上明確許多,跟著大神範例修改準沒錯:
git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git
clone下來後可以去參考以下路徑的程式碼:
python-docs-samples/functions
code內要做什麼就看需求了,重點還是要搞清楚你要開發的是http function還是接收各種不同事件的background function。就我經驗而言拿範例來修改真的比自己邊摸索邊寫簡單太多,滿滿血淚史,路別白走了。
以下擷取兩段範例分別是background function/http function的helloworld
# [START functions_tips_terminate]
# [START functions_helloworld_get]
def hello_get(request): return 'Hello World!'# [END functions_helloworld_get]
# [START functions_helloworld_background]
def hello_background(data, context):
if data and 'name' in data:
name = data['name']
else:
name = 'World'
return 'Hello {}!'.format(name)
# [END functions_helloworld_background]
# [END functions_tips_terminate]
http function還能夠直接解析request夾帶的資訊,這在以http cloud function作為各種cloud service接口時很實用:
# [START functions_helloworld_http]
def hello_http(request):
request_json = request.get_json(silent=True)
request_args = request.argsif request_json and 'name' in request_json:
name = request_json['name']
elif request_args and 'name' in request_args:
name = request_args['name']
else:
name = 'World'
return 'Hello {}!'.format(escape(name))# [END functions_helloworld_http]
佈署(Deploy)
佈署方法百百種,剛開始你可能會選擇透過GCP的圖形化介面上傳zip或者是將程式碼直接貼上去佈署,不過這裡我推薦可以從本機直接透過gcloud
commnad line 將本機寫完也測試好的程式碼上傳到GCP。以Mac為例,你可以在安裝好google cloud sdk後利用一下指令佈署你的cloud function:
gcloud functions deploy NAME --runtime RUNTIME TRIGGER [FLAGS...]
關於該指令相關參數說明,可以參考這裡
然而,透過gcp command line tool最棒的是可以做一定程度的自動化,減少自己在個個上傳zip檔時可能發生的低級錯誤,例如漏傳等。以下分享段落我如何自己寫shell將大量cloud function佈署至GCP環境:
- 用純文字編輯程式開啟,開檔按存成.sh
- 撰寫以下內容啟用大量佈署,格式如下:
#!/bin/bash#define your own sdk path
gcp_sdk_path="<your_gcp_sdk_path>"#change to your cloud function folder
cd <your_cloud_function_folder>#deploy your cloud function
$gcp_sdk_path/gcloud functions deploy <your_cloud_function> --entry-point xxx --runtime python37 --trigger-resource xxx &
3. 開啟terminal執行你的.sh
以上只是大概.sh架構,如果多個function只要從撰寫script重複轉換資料夾>佈署就好。分享一下小技巧,由於是透過Mac佈署,在佈署command後加上&可以讓job持續在後端跑(應該是所有unix家族都可以這樣),接著就會直接執行下個job,全部你想佈署的cloud function會一次被上傳。又不延遲!
測試(Test)
一般而言對於特定某事件例如檔案上傳至某個bucket觸發,可以直接上傳檔案並且另外開個視窗監控:
以上方式大概是background function比較簡易的觀察觸發/結果的方式。而http function在佈署後會產生url,測試時可以透過curl觸發:
curl -X POST "https://YOUR_REGION-YOUR_PROJECT_ID.cloudfunctions.net/FUNCTION_NAME" -H "Content-Type:application/json" --data '{"name":"Keyboard Cat"}'
除此之外,其實範例中也有提供利用測試程式
functions/helloworld/sample_http_test.py
from unittest.mock import Mock
import main
def test_print_name():
name = 'test'
data = {'name': name}
req = Mock(get_json=Mock(return_value=data), args=data)
# Call tested function
assert main.hello_http(req) == 'Hello {}!'.format(name)
def test_print_hello_world():
data = {}
req = Mock(get_json=Mock(return_value=data), args=data)
# Call tested function
assert main.hello_http(req) == 'Hello World!'
用以下指令測試你的http cloud function
pytest sample_http_test.py
更多測試相關資訊,可以參考這裡。
小結
以上大略是以懶人包的方式記敘了一些注意事項以及我筆記的資訊、重要參考來源。這段開發過程挑戰解耦合這件事情比想像中困難一些,心得整理以下幾點:
- 架構時設計可能是挑戰,但是設計的夠好理論上低耦合度可以在改版時變更風險變小。架構設計可以善用pub/sub服務,有相依性的subsriber只要訊息格式一樣,發出topic的程式碼更動核心邏輯也不會對有相依性的其他程式造成過大影響。
- Cloud Function其實不是非常好管理的,尤其同一project可能散落好幾個Cloud Function列成同一清單只會眼花繚亂。因此,善用自訂的命名原則以及都多利用tag是一個比較推薦的方式,幫助你快速找到你想找的目標。
- 最後我認為最大的優點是:便宜,比起開一台Compute Engine,在多數情況下(非密集存取以及耗用大量網路資源),Cloud Function所產稱的費用一個月十分低廉(常常估算一個月存取約20000次只需要5美金以下),無怪乎我很在意的難以分類管理這件事情這麼多人願意接受。