iT邦幫忙

2025 iThome 鐵人賽

DAY 28
0
自我挑戰組

從零開始的AI學習之路:非本科轉職的30天挑戰記系列 第 28

D28 | 我的Side Project 每拍呷--部署篇

  • 分享至 

  • xImage
  •  

今日進度14/14:部署到GCP/images/emoticon/emoticon07.gif


原本很猶豫要不要部署,還是開NGROK就好?
但...身為天蠍座,一旦投入一件事情,就會想要做到120分!

所以就硬著頭皮從零開始研究GCP了,不得不說介面真的很、複、雜,前置作業也很多。
跟AI討論後決定用Cloud Run,是一個用來運行容器映像檔的Google服務。

它的優點是彈性高,你可以使用任何程式語言和函式庫,只要將它們打包成一個容器映像檔即可。
缺點就是學習曲線比較陡,還要輸入 gcloud 指令TwT
容器就像一個獨立的作業系統,裡面包含了你的程式碼、Python 直譯器,以及所有必要的函式庫。
例如,

  • gcloud builds submit 指令,就是用來執行容器化的過程。
  • Dockerfile: 告訴 Google Cloud 你的容器應該如何建立。
  • gcloud builds submit: 負責將你的專案檔案(包括 Dockerfile、main.py 和 requirements.txt)打包並建置成一個 Docker 映像檔。
  1. 首先要準備:Python 3.7+、Google Cloud SDK安裝CLI並初始化、建立 requirements.txt
    裡面要寫Flask、gunicorn、line-bot-sdk、google-generativeai、firebase-admin、google-cloud-firestore、google-cloud-storage、google-cloud-texttospeech;
  2. 建置並上傳 Docker 映像:
    啟用Cloud Build API,執行指令gcloud builds submit --tag gcr.io/[專案ID]/[服務名稱]
  3. 部署到 Cloud Run:
    進入Google Cloud Console=>在左側導覽列中,點選 Cloud Run=>點擊「建立服務」(Create Service)。
  4. 在設定頁面中,點選「從現有的容器映像檔部署修訂版本」(Deploy one revision from an existing container image)。

在「容器映像網址」(Container image URL)欄位中,選取剛剛成功建置的映像檔。
=>在「Authentication」部分,選擇Allow unauthenticated invocations。
(對 LINE BOT 的 Webhook 來說是必要的)
=>在底部的 「變數與密鑰」(Variables & Secrets)區塊,設定環境變數。
(原本我在本地端是用config.ini,現在設定環境變數,程式碼也要微調)

(其實對於「容器」我還是有點一知半解,之後再請教大神!)

還有一些奇妙的名詞要理解:

自動調整資源配置 (Autoscaling)
自動調整資源配置指的是 Cloud Run 服務會根據實際的流量需求,自動增加或減少執行個體的數量。

當流量增加時:如果有很多人同時使用 LINE BOT,Cloud Run 會自動啟動更多的執行個體來處理這些請求,確保服務不會因為負載過大而變慢或當掉。

當流量減少時:如果服務閒置,沒有任何用戶傳送訊息,Cloud Run 會自動將執行個體的數量減少到零(如果設定了最小執行個體數為零),這能節省費用,因為只需為實際使用的資源付費。

冷啟動 (Cold Start)

冷啟動是自動調整資源配置所帶來的一個現象。

當服務在一段時間內沒有任何請求時,所有的執行個體都會被終止,處於「休眠」狀態。
當一個新的請求(例如,用戶傳送第一則訊息)傳進來時,Cloud Run 需要花一些時間來啟動一個新的執行個體,這個啟動過程就是所謂的「冷啟動」。

發生時機: 通常在服務長時間閒置後,第一個請求會觸發冷啟動。

影響: 冷啟動會讓你的服務在第一次回應時稍微延遲一些。例如,當用戶傳送第一則訊息時,可能會等 1-2 秒甚至更久才收到回覆。但一旦執行個體啟動後,後續的請求就會非常快速。
(難怪!我想說怎麼傳了訊息沒回覆,還以為又要Debug了TwT)

如何優化冷啟動?
對於 LINE BOT 來說,冷啟動的延遲通常是可以接受的。如果你的專案需要更快的響應速度,你可以在 Cloud Run 的設定中,將最小執行個體數設定為1。這樣,你的服務會永遠有一個執行個體在運行,從而避免冷啟動,但這會讓你在服務閒置時也會產生費用。所以我還是設為0了,現階段還在學習,盡量降低成本!

要求逾時 (Request Timeout)

要求逾時 (Request Timeout) 是指服務在收到請求後,必須在指定時間內傳回回應。
如果服務在時間限制內沒有回覆,Cloud Run 就會終止這個請求,並向客戶端回傳一個錯誤。

為什麼重要?逾時設定可以防止你的服務因為處理一個永遠無法完成的任務而持續佔用資源。例如,如果你的 BOT 需要呼叫一個很慢的外部 API,這個設定可以防止你的服務一直等待下去,確保它能及時釋放資源。

推薦設定:對於大多數 LINE BOT 專案來說,預設值(通常是幾十秒)就夠用了。如果你的 BOT 執行複雜的任務,例如處理大型圖片或複雜的 AI 運算,你可能需要將這個值調高。

每個執行個體的並行要求數量上限 (Maximum concurrent requests per instance)

這是什麼? 這是指一個單一的執行個體(你的服務的一個副本)可以同時處理的請求數量。
為什麼重要? 這個設定可以幫助你控制服務的效能和資源使用。
如果你將這個值設定得很低,當流量增加時,Cloud Run 會啟動更多的執行個體來處理請求,這可能會導致更多的冷啟動,但每個請求的處理會更快。
如果你將這個值設定得很高,每個執行個體會處理更多的請求,這能減少冷啟動的頻率,但在流量高峰時,可能會導致單一執行個體因負載過重而變慢。

推薦設定: 對於大多數 Python 服務,預設值(通常是 80 或 100)是一個很好的起點。除非你的服務有特殊的並行限制(例如,你的程式碼無法處理多個並行請求),否則保持預設值即可。(最後我設定80)


還有一個新學習是要「撰寫Dockerfile」。
它就像一份詳細的「烹飪食譜」,告訴 Google Cloud 你的應用程式應該如何被打包和啟動。
在這份食譜中,有一個關鍵的指令叫做 CMD。它告訴容器在啟動時,應該要執行哪個指令。
大概是長這樣:

# 選擇一個輕量級的 Python 3.12 映像檔作為基礎
FROM python:3.12-slim

# 設定容器內的工作目錄為 /app
WORKDIR /app

# 將本地的 requirements.txt 檔案複製到容器的 /app 目錄中
COPY requirements.txt .

# 執行 pip 安裝,安裝所有在 requirements.txt 中列出的 Python 套件
# --no-cache-dir 參數可以減少映像檔的大小
RUN pip install --no-cache-dir -r requirements.txt

# 將專案中的所有檔案複製到容器的 /app 目錄中
COPY . .

# 定義容器啟動時的預設指令
# 使用 gunicorn 運行你的網頁應用程式
# --bind 0.0.0.0:8080 表示綁定到所有網路介面的 8080 連接埠
# main:app 指向你的 Python 應用程式(在 main.py 檔案中,名為 app 的 Flask 實例)
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "main:app"]

一直遇到「找不到Flask模組」錯誤

原本照著Gemini的建議:「透過現有的容器映像檔部署單一修訂版本」,但一直部署失敗,每次都報錯-「找不到Flask模組」。

這真的是太奇怪了,我的import確定有Flask,requirements文件第一行就是寫Flask。

後來我改用終端機寫指令gcloud run deploy --source . ,沒想到,一次就部署上去了,而且能跑...好開心!這個指令算是一個懶人包指令,它會自動幫你完成所有事情。

  • 自動建置映像檔:當你使用--source .(來源部署),gcloud 會自動找到你專案資料夾裡的 Dockerfile,然後執行建置 (build) 的動作,把你的程式碼和所有套件打包成一個 Docker 映像檔。

  • 自動部署服務:建置完成後,它會自動將這個新生成的映像檔,部署到你指定的 Cloud Run 服務上。

測試看看,能順利完成用戶資料設定,也收到Flex Message了...但怎麼沒有收到語音檔?
一分鐘、兩分鐘、三分鐘過去了,真的沒有語音檔,怎麼會這樣呢~
也就是:跑文字檔都正常,到後來生成語音檔的功能就壞掉了!

不像平常在VS CODE終端機查看,Cloud Run的介面有點奇妙,我找介面找了老半天,終於找到log檔...
來來回回修改就過了兩、三個小時...

可能的原因

這通常代表程式碼在運作過程中,某個環節發生了靜默錯誤 (silent error),導致回覆訊息的程式碼沒有被執行。
問題很可能發生在處理完圖片、要發送 Flex Message 或語音訊息之前。

問題分析:回覆令牌 (Reply Token) 與 API 錯誤
LINE 的 reply_token 只能使用一次。你的程式碼在處理圖片訊息時,發送了「正在辨識圖片中的營養標示...」這段文字回覆,這時回覆令牌已經被用掉了。

如果接下來的任何一個步驟出錯(例如,Gemini API 辨識失敗、TTS 語音生成失敗、或者 Firebase Storage 上傳失敗),後續的訊息發送程式碼就不會被執行。因為回覆令牌已經用過了,所以你也不會收到任何錯誤訊息。

解決方案:先處理,後回覆
最可靠的解決方案是將所有的處理步驟放在回覆之前。我們將調整程式碼,確保所有 API 呼叫、資料處理都完成後,再一次性地發送最終的 Flex Message 和語音檔案。

根據上述原理修正了 handle_image_message 函式,確保在所有處理完成後,才進行最後的回覆。

  1. 移除第一次的回覆:不再在處理圖片的一開始就發送「正在辨識圖片...」的文字。

  2. 單次回覆:將所有處理邏輯包裝在一個 try...except 區塊中,無論成功或失敗,都會在最後使用 reply_token 進行一次性回覆。

  3. 即時錯誤回報:如果中途發生任何錯誤,except 區塊會捕捉到,並立即回覆用戶「處理圖片時發生了錯誤」。

看到這邊,我有種恍然大悟的感覺!問題也終於解決了!


部署成功!

最後,把得到的 HTTP 觸發網址,網址複製並貼到 LINE Developers Console 的 Webhook URL 欄位即可。

不得不說是有點感動,這做了14天的專題雖然不完美,但仍然謝謝卯足全力的自己。

雖然因為久坐電腦打程式碼,體重又默默增加1KG了🤣

https://ithelp.ithome.com.tw/upload/images/20250902/20177974TlGslCq7gL.jpg


上一篇
D27 | 我的Side Project 每拍呷--最後優化+Debug篇
下一篇
D29 | 我的Side Project 每拍呷--更新流程架構篇
系列文
從零開始的AI學習之路:非本科轉職的30天挑戰記30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言