在現代軟體開發中,資料函式庫、快取等支援服務至關重要。本文將探討在 Go 應用程式中使用 Redis 的高效能方法,包含連線池的建立與使用,並示範如何在多個 goroutine 中平行執行 Redis 命令,提升應用程式效能。同時,文章也涵蓋 Docker 映像檔的管理技巧,包含使用 Docker Registry 建立私有倉函式庫、利用 Portainer 簡化 Docker 管理流程,以及 12-Factor App 的建置、釋出、執行流程。最後,文章說明如何使用 Codeship 建立 CI/CD 流程,自動化建置、測試和佈署應用程式。
支援服務 - 將支援服務視為附加資源
在現代軟體開發中,支援服務(Backing Services)扮演著至關重要的角色。這些服務包括資料函式庫、快取系統、訊息佇列等,它們為應用程式提供了必要的功能和資料儲存。本文將探討如何將支援服務視為附加資源,並以 Redis 為例,展示如何在 Go 語言應用程式中實作對 Redis 的高效使用。
Redis 連線池的實作
為了高效地使用 Redis,我們需要實作一個連線池(Connection Pool)。連線池可以減少建立和關閉連線的開銷,提高應用程式的效能。以下是使用 Redigo 函式庫實作 Redis 連線池的範例程式碼:
type Redis struct {
pool *redis.Pool
dsn []*string
serverIndex int
}
// Flags 方法用於註冊 Redis 伺服器的組態
func (r *Redis) Flags(name, value, usage string, flag redisFlags) {
r.dsn = append(r.dsn, flag.String(name, value, usage))
}
// Get 方法傳回一個 Redis 連線
func (r *Redis) Get() redis.Conn {
if r.pool == nil {
r.pool = &redis.Pool{
MaxIdle: 3,
IdleTimeout: 240 * time.Second,
Dial: func() (redis.Conn, error) {
return redis.Dial("tcp", r.getServerName())
},
}
}
return r.pool.Get()
}
// Do 方法用於執行 Redis 命令
func (r *Redis) Do(commandName string, params ...interface{}) (interface{}, error) {
conn := r.Get()
defer conn.Close()
return conn.Do(commandName, params...)
}
內容解密:
Redis結構體:定義了 Redis 連線池和伺服器組態的相關欄位。Flags方法:允許註冊多個 Redis 伺服器的組態,以便在連線池中使用。Get方法:傳回一個 Redis 連線,如果連線池尚未初始化,則會建立一個新的連線池。Do方法:簡化了執行 Redis 命令的過程,自動處理連線的取得和關閉。
平行執行 Redis 命令
由於 Redis 是單執行緒的,執行命令是阻塞的。因此,為了實作平行執行,我們需要在不同的 goroutine 中執行命令,並使用 channel 來等待結果。
// 設定 Redis 服務
redis := &service.Redis{}
redis.Flags("redis1", "redis1:6379", "Redis DNS", flags)
redis.Flags("redis2", "redis2:6379", "Redis DNS", flags)
redis.Flags("redis3", "redis3:6379", "Redis DNS", flags)
// 平行執行 Redis 命令
sleep1_chan := make(chan string, 1)
sleep2_chan := make(chan string, 1)
go func() {
conn := redis.Get()
defer conn.Close()
// 執行 DEBUG SLEEP 命令
sleep1, err := conn.Do("DEBUG", "SLEEP", "0.1")
if err != nil {
sleep1 = "ERROR"
}
sleep1_chan <- sleep1.(string)
}()
go func() {
conn := redis.Get()
defer conn.Close()
// 執行 DEBUG SLEEP 命令
sleep2, err := conn.Do("DEBUG", "SLEEP", "0.2")
if err != nil {
sleep2 = "ERROR"
}
sleep2_chan <- sleep2.(string)
}()
// 等待結果
var result string
result = <-sleep1_chan
fmt.Printf("[%.4f] End Sleep 100ms, result %s\n", service.Now(), result)
result = <-sleep2_chan
fmt.Printf("[%.4f] End Sleep 200ms, result %s\n", service.Now(), result)
內容解密:
- 平行執行:透過在不同的 goroutine 中執行 Redis 命令,實作了平行處理。
- 使用 channel:利用 channel 等待 goroutine 的執行結果,實作了同步。
- 結果處理:接收並列印了每個命令的執行結果。
Docker Registry 的使用
Docker Registry 是用於儲存 Docker 映象的容器服務。您可以設定自己的 Registry 伺服器,就像執行 MySQL 或 Redis 一樣。
啟動 Registry 伺服器
#!/bin/bash
NAME="registry"
DOCKER_IMAGE="registry:2"
STORAGE=$(dirname $(dirname $(readlink -f $0)))/storage
if [ ! -d "$STORAGE" ]; then
mkdir $STORAGE
fi
docker stop $NAME && docker rm $NAME
docker run -d -p 5000:5000 --restart=always --name $NAME -v $STORAGE:/var/lib/registry $DOCKER_IMAGE
登入 Registry
要將映象推播到 Docker Hub 或您的私有 Registry,您需要先登入。
管理 Docker 映像檔與私有倉函式庫
在開發與佈署應用程式的過程中,Docker 映像檔的管理是至關重要的一環。無論是使用公有的 Docker Hub 還是私有倉函式庫,映像檔的安全性、存取控制以及版本管理都是需要仔細考量的議題。
登入 Docker Hub 與推播映像檔
首先,您需要登入 Docker Hub 以便推播和管理您的 Docker 映像檔。登入的過程非常簡單,只需執行以下命令:
# docker login
系統會提示您輸入 Docker ID 和密碼。成功登入後,您可以將本地的 Docker 映像檔推播到 Docker Hub:
# docker push titpetric/gotwitter
內容解密:
docker login:此命令用於登入 Docker Hub,讓您能夠推播和提取映像檔。docker push titpetric/gotwitter:此命令將本地的gotwitter映像檔推播到 Docker Hub 上名為titpetric的帳戶下。
成功推播後,您的映像檔就可以在任何地方被提取和使用。這對於持續整合和持續佈署(CI/CD)流程至關重要。
自建私有 Docker 倉函式庫
相較於使用 Docker Hub,自建私有 Docker 倉函式庫提供了更多的控制權和靈活性。當您使用自建倉函式庫時,可以享受到以下好處:
- 使用自訂的網域名稱(例如
registry.example.com)。 - 無需處理不友好的 ID(例如 AWS 帳戶 ID 或區域)。
- 統一且易記的映像檔 URL。
登入自建倉函式庫的命令如下:
# docker login registry.example.com
推播映像檔到自建倉函式庫:
# docker push registry.example.com/gotwitter
內容解密:
docker login registry.example.com:登入自建的私有 Docker 倉函式庫。docker push registry.example.com/gotwitter:將映像檔推播到自建倉函式庫。
自建私有倉函式庫雖然提供了更高的自主性,但也意味著您需要自行管理倉函式庫的維護工作,例如映像檔的刪除和更新。
私有 Docker 倉函式庫服務
如果您不想自行維護私有倉函式庫,可以選擇使用第三方提供的私有 Docker 倉函式庫服務。主要的服務提供商包括:
- Docker Hub:提供私有映像檔儲存,並支援自動化建置。
- Amazon EC2 Container Registry (Amazon ECR):與 AWS 生態系統緊密整合,提供強大的映像檔管理功能。
- Google Container Registry (GCR):與 Google Cloud 平台無縫整合,提供高效的映像檔儲存和管理。
- Quay.io:提供與 Docker Hub 相似的功能,並且在某些技術上更為先進。
使用 Portainer 管理 Docker 主機
對於那些偏好圖形介面來管理基礎設施的人來說,Portainer 是一個非常好的選擇。您可以使用以下命令輕鬆啟動 Portainer:
# docker run -d -p 9000:9000 \
-v /var/run/docker.sock:/var/run/docker.sock \
portainer/portainer
內容解密:
docker run -d -p 9000:9000:在背景執行 Portainer,並將容器的 9000 連結埠對應到主機的 9000 連結埠。-v /var/run/docker.sock:/var/run/docker.sock:將主機的 Docker socket 對應到容器內,這樣 Portainer 就能夠管理主機上的 Docker。
透過 Portainer,您可以輕鬆地管理 Docker 主機或叢集,並且使用其提供的應用程式範本快速佈署常見服務。
建置、釋出、執行 - 嚴格區分建置和執行階段
12 Factor App 方法論強調將軟體開發過程嚴格區分為三個階段:
- 建置(build):將程式碼封裝成可執行的檔案或 Docker 映像檔。
- 釋出(release):將建置好的檔案或映像檔釋出到特定的儲存位置。
- 執行(run):在生產環境中執行已釋出的版本。
這種區分確保了開發過程的有序進行,並且使得版本的回復變得更加容易。在 Go 語言中,由於其編譯特性,建置階段負責將原始碼編譯成機器碼,而釋出階段則負責將這些可執行檔案釋出到適當的位置,例如 GitHub 或 Docker Hub。
建置、釋出、執行 - 嚴格區分建置與執行階段
建置 Go 應用程式
Go 語言允許我們為多種架構和平台建置可執行檔。例如,常見的架構包括:
- amd64:64 位元 x86 架構
- 386:32 位元 x86 架構
- arm:ARM 架構(例如 Raspberry PI)
在建置應用程式時,可以傳遞 GOARCH 變數以指定目標平台。同樣地,可以傳遞 GOOS 變數以指定目標作業系統,如 linux、windows、freebsd 和 darwin 等。這意味著只要能夠為目標平台建置可執行檔,就可以為不同的平台封裝應用程式。
當應用程式被編譯時,它會使用系統中可用的函式庫。這個過程稱為動態連結。當將可執行檔從一個系統轉移到另一個系統(或從一個 Docker 映像檔轉移到另一個)時,它很可能會遇到不同的函式庫,從而導致應用程式無法運作。
建置 Docker 映像檔的最佳實踐
建置 Docker 映像檔時,應使用 golang:1.8-alpine 這樣的基礎映像檔來建置應用程式,然後使用 alpine:3.5 這樣的基礎映像檔來執行產生的二進位制檔。如果使用其他 Go 語言的 Docker 映像檔,可以在 Docker Hub 頁面上檢查 Dockerfile,以確定哪個基礎映像檔適合用於應用程式。
檢查可執行檔型別的典型方法是使用 file 命令:
# file ./gotwitter
./gotwitter: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, not stripped
靜態連結:更佳的選擇
另一種更好的方法是使用靜態連結,即產生的應用程式嵌入所需的函式庫。這樣,二進位制檔真正具有可移植性,可以將其移動到其他相容的作業系統和平台,無論它們提供哪些函式庫。
實作 Go 應用程式靜態連結的方法是在建置應用程式時傳遞一些選項,這些選項將被傳遞給底層的連結器。Go build 支援 --ldflags 引數,可以用於將 -static 選項傳遞給底層的編譯器和連結器。同時,需要傳遞環境變數 CGO_ENABLED=0 以停用 cgo 工具,並使用 gcc,就像進行跨平台/架構編譯一樣。
# file gotwitter
gotwitter: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
更多關於環境變數的資訊
更多關於 CGO_ENABLED、GOOS 和 GOARCH 的資訊,請參閱可選環境變數和cgo 工具檔案。
建置 Docker 映像檔
建置 Docker 映像檔時,建議使用特定的版本標記映像檔。這個版本可以是日期/時間字串、任何遞增的數字或版本控制系統(如 semver),或者是 Git 儲存函式庫中的提交 ID。如果使用提交 ID,則意味著可以根據該提交中的原始碼重建完全相同的 Docker 映像檔。
GITVERSION=$(git rev-list HEAD | head -n 1 | cut -c1-6)
上述命令將在 bash 中設定 GITVERSION 變數。它的工作原理是首先取得所有提交雜湊的列表。使用 head -n 1 提取最新的提交,並使用 cut -c1-6 只保留提交雜湊的前 6 個字母。這些資訊在需要檢視程式碼時也很有用——透過標記的映像檔,您可以準確知道用於建置映像檔時的原始碼版本。
使用提交 ID 進行建置
我喜歡根據建置內容的提交 ID 進行建置。我設定了一個引數和一個環境變數,用於在容器執行時儲存這些資訊。
ARG GITVERSION=development
ENV GITVERSION ${GITVERSION}
一般來說,您需要知道的是,ARG 命令提供了一個變數,可以在 docker build 時透過 --build-arg 選項傳遞。該 ARG 引數的值本身不會持久化到容器中。這就是為什麼還要使用 ENV 命令,以持久化給定的值。在執行容器時,可以透過以下方式檢查:
# docker exec -it monocms-cache-nutcracker env | grep GITVERSION
GITVERSION=85990e
透過這些資訊,我可以簽出 git 儲存函式庫中完全對應的提交,也許還會修改 Dockerfile,然後重新建置它,使其與之前完全相同(只是更好)。同樣的版本資訊也可用於還原建置,只要知道映像檔的建置或佈署到生產環境的順序即可。
使用 Codeship 進行持續整合
提到軟體交付,公認的方法是在 git 儲存函式庫發生變化時進行建置。當建置完成並測試後,您可能會釋出軟體,例如將其釋出到 GitHub 上作為版本釋出,或將 Docker 映像檔推播到 Docker Hub 或您的私有倉函式庫。這個過程稱為持續整合。
在我們的例子中,將使用一個 ID 生成器服務,我將在下一章節中更詳細地描述它。我將使用 Codeship 作為 CI,它將建立建置並將適當的二進位制檔釋出到 GitHub 和 Docker Hub。
每次我向 GitHub 提交更改時,都會發生以下情況:
- GitHub 將透過 Webhook 通知 Codeship 我進行了推播。
- Codeship 將進行 git 簽出。
- 根據
codeship-services.yml,建立建置環境(安裝軟體等)。 - 根據
codeship-steps.yml,您的軟體被建置和佈署。
入門 Codeship
首先,註冊 Codeship 很簡單,他們的免費方案每月提供 100 次建置。我們將使用「Codeship Pro」產品,它具有完整的 Docker 支援、可自定義的 CI 環境,並且可以執行本地建置執行器。在進一步介紹之前,讓我帶您完成建立第一個專案的過程:
在您的儀錶板上歡迎介面
點選大大的綠色按鈕後,Codeship 將詢問您要建立哪個專案。它們支援 GitHub、Bitbucket 和 GitLab 儲存函式庫。我們的服務 sonyflake託管在 GitHub 上,因此我使用它來連線到 Codeship。
為什麼需要 Jet?
Jet 用於本地除錯和測試 Codeship Pro 的建置,以及協助處理諸如加密安全憑證等重要任務。
簡單來說,安裝 Jet 後,您可以在程式碼簽出中執行 jet steps,以檢視您的建置在 Codeship Pro 上將如何執行。在開發過程中,您將執行它,直到對建置結果滿意為止——當您推播程式碼時,GitHub 將觸發 Codeship 的建置。
由於我正在設定釋出到 Docker Hub 和 GitHub,因此我肯定需要安全地新增一些憑證到容器中,用於建置我們的應用程式。Codeship Jet 提供了一種透過為專案提供 AES 金鑰來加密這些憑證的方法。
設定 Codeship Pro
連線儲存函式庫時,請務必選擇「Codeship Pro」方案。在此階段,我們需要安裝他們的「Jet」CLI。有完整的檔案可供參考,位於在本地執行 Codeship 的 Jet 用於開發頁面。