在現代軟體開發中,資料函式庫、快取等支援服務至關重要。本文將探討在 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...)
}

內容解密:

  1. Redis 結構體:定義了 Redis 連線池和伺服器組態的相關欄位。
  2. Flags 方法:允許註冊多個 Redis 伺服器的組態,以便在連線池中使用。
  3. Get 方法:傳回一個 Redis 連線,如果連線池尚未初始化,則會建立一個新的連線池。
  4. 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)

內容解密:

  1. 平行執行:透過在不同的 goroutine 中執行 Redis 命令,實作了平行處理。
  2. 使用 channel:利用 channel 等待 goroutine 的執行結果,實作了同步。
  3. 結果處理:接收並列印了每個命令的執行結果。

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

內容解密:

  1. docker login:此命令用於登入 Docker Hub,讓您能夠推播和提取映像檔。
  2. docker push titpetric/gotwitter:此命令將本地的 gotwitter 映像檔推播到 Docker Hub 上名為 titpetric 的帳戶下。

成功推播後,您的映像檔就可以在任何地方被提取和使用。這對於持續整合和持續佈署(CI/CD)流程至關重要。

自建私有 Docker 倉函式庫

相較於使用 Docker Hub,自建私有 Docker 倉函式庫提供了更多的控制權和靈活性。當您使用自建倉函式庫時,可以享受到以下好處:

  1. 使用自訂的網域名稱(例如 registry.example.com)。
  2. 無需處理不友好的 ID(例如 AWS 帳戶 ID 或區域)。
  3. 統一且易記的映像檔 URL。

登入自建倉函式庫的命令如下:

# docker login registry.example.com

推播映像檔到自建倉函式庫:

# docker push registry.example.com/gotwitter

內容解密:

  1. docker login registry.example.com:登入自建的私有 Docker 倉函式庫。
  2. docker push registry.example.com/gotwitter:將映像檔推播到自建倉函式庫。

自建私有倉函式庫雖然提供了更高的自主性,但也意味著您需要自行管理倉函式庫的維護工作,例如映像檔的刪除和更新。

私有 Docker 倉函式庫服務

如果您不想自行維護私有倉函式庫,可以選擇使用第三方提供的私有 Docker 倉函式庫服務。主要的服務提供商包括:

  1. Docker Hub:提供私有映像檔儲存,並支援自動化建置。
  2. Amazon EC2 Container Registry (Amazon ECR):與 AWS 生態系統緊密整合,提供強大的映像檔管理功能。
  3. Google Container Registry (GCR):與 Google Cloud 平台無縫整合,提供高效的映像檔儲存和管理。
  4. 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

內容解密:

  1. docker run -d -p 9000:9000:在背景執行 Portainer,並將容器的 9000 連結埠對應到主機的 9000 連結埠。
  2. -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_ENABLEDGOOSGOARCH 的資訊,請參閱可選環境變數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 提交更改時,都會發生以下情況:

  1. GitHub 將透過 Webhook 通知 Codeship 我進行了推播。
  2. Codeship 將進行 git 簽出。
  3. 根據 codeship-services.yml,建立建置環境(安裝軟體等)。
  4. 根據 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 用於開發頁面。