在現代 Web 開發中,高效的資料儲存和容器化佈署至關重要。本文首先介紹如何利用 Redis 的特性提升應用程式效能,特別是如何透過 Redigo 客戶端函式庫實作 Redis 的平行處理。接著,文章會引導讀者建立私有的 Docker Registry,讓映像檔管理更安全、便捷,並推薦 Portainer 作為圖形化管理工具。最後,文章將會說明如何利用 Codeship 建立 CI/CD 流程,自動化建置、測試和佈署應用程式,以提升開發效率和程式碼品質。
使用Redis作為支援服務
在眾多的NoSQL儲存方案中,Redis因其強大的功能和靈活性而備受青睞。相較於簡單的鍵值儲存如Memcached,Redis提供了磁碟儲存、資料複製以及多種資料結構,使其成為真正的資料儲存而非僅僅是快取。
啟動Redis容器
啟動Redis容器相對簡單,以下是一個範例指令碼:
#!/bin/bash
MYPATH=$(dirname $(dirname $(readlink -f $0)))
NAME=$(basename $MYPATH)
DOCKER_IMAGE=$(<$(dirname $(dirname $(readlink -f $0)))/docker.image)
# 移除Redis警告
sysctl vm.overcommit_memory=1
echo never > /sys/kernel/mm/transparent_hugepage/enabled
SYSCTL="--sysctl net.core.somaxconn=4096"
echo -n "$NAME: "
docker stop redis && docker rm redis
docker run --restart=always -h $NAME --name $NAME $SYSCTL --net=party -d $DOCKER_IMAGE </dev/null
內容解密:
此指令碼首先設定必要的系統引數以最佳化Redis效能,然後停止並移除任何現有的Redis容器,最後以特定的設定啟動新的Redis容器。其中,--sysctl net.core.somaxconn=4096用於增加系統的最大連線數,以支援高並發的連線請求。
使用Redigo連線Redis
Redigo是Go語言中一個流行的Redis客戶端函式庫,它提供了連線池功能,有助於高效管理與Redis的連線。由於Redis是單執行緒的,因此對單一伺服器的命令操作不會實作平行處理,但可以透過連線多個Redis伺服器來達到平行處理的效果。
以下是如何使用Redigo建立連線池的範例:
type Redis struct {
pool *redis.Pool
dsn []*string
serverIndex int
}
// Flags註冊新的引數
func (r *Redis) Flags(name, value, usage string, flag redisFlags) {
r.dsn = append(r.dsn, flag.String(name, value, usage))
}
// Get傳回一個需要被關閉的連線
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執行命令對臨時池連線
func (r *Redis) Do(commandName string, params ...interface{}) (interface{}, error) {
conn := r.Get()
defer conn.Close()
return conn.Do(commandName, params...)
}
內容解密:
Redis結構體封裝了連線池和多個Redis伺服器的DSN(資料來源名稱)。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)
fmt.Printf("[%.4f] Start\n", service.Now())
sleep1_chan := make(chan string, 1)
sleep2_chan := make(chan string, 1)
go func() {
conn := redis.Get()
defer conn.Close()
fmt.Printf("[%.4f] Run sleep 100ms\n", service.Now())
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()
fmt.Printf("[%.4f] Run sleep 200ms\n", service.Now())
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命令(DEBUG SLEEP),並透過channel收集結果。由於這兩個命令是對不同的Redis伺服器發出的,因此可以實作平行處理。輸出結果顯示了命令的執行時間,從而驗證了平行處理的效果。
自建 Docker Registry 伺服器與管理工具
在容器化的世界中,Docker Registry 扮演著至關重要的角色,負責儲存和分發 Docker 映像檔。本文將探討如何建立自己的 Docker Registry 伺服器,以及如何使用各種管理工具來簡化操作流程。
建立 Docker Registry 伺服器
首先,我們需要啟動一個 Docker Registry 伺服器。這可以透過執行一個簡單的 Bash 指令碼來完成。
#!/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 映像檔。然後,它設定了一個儲存路徑,用於儲存 Docker 映像檔。如果該路徑不存在,則建立它。
- 停止並移除現有的 Registry 容器:指令碼嘗試停止並移除任何已存在的同名容器,以確保新的容器可以正確啟動。
- 啟動新的 Registry 容器:使用
docker run命令啟動一個新的容器,將主機的儲存路徑掛載到容器的/var/lib/registry目錄,並對映 5000 埠。
登入 Docker Registry
要推播映像檔到 Docker Registry,首先需要登入。
# docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: titpetric
Password:
Login Succeeded
內容解密:
- 登入流程:使用
docker login命令登入 Docker Registry。需要提供使用者名稱和密碼。 - 成功登入:登入成功後,可以推播和提取映像檔。
推播映像檔到 Docker Registry
登入後,可以推播映像檔到 Docker Registry。
# docker push titpetric/gotwitter
The push refers to a repository [docker.io/titpetric/gotwitter]
1592f0386889: Pushed
c32cc9520ad2: Pushed
60ab55d3379d: Pushed
latest: digest: sha256:e0a7ad8d994273710d92c92fdc3c30244b13fb519638bfefb84da1aae616bec1 size: 949
內容解密:
- 推播映像檔:使用
docker push命令將映像檔推播到指定的 Docker Registry。 - 推播結果:命令輸出顯示了推播的進度和結果,包括映像檔的 Digest 和大小。
使用自建 Docker Registry 的優勢
使用自建的 Docker Registry 有多個優勢,包括:
- 可以使用自己的網域名稱,如
registry.example.com。 - 不涉及難以閱讀的 ID,如 AWS 帳戶 ID 或地區。
- 統一且易記的映像檔 URL。
管理 Docker 主機的工具:Portainer
對於喜歡圖形介面的使用者,Portainer 提供了一個友好的介面來管理 Docker 主機。
# docker run -d -p 9000:9000 \
-v /var/run/docker.sock:/var/run/docker.sock \
portainer/portainer
內容解密:
- 啟動 Portainer:透過執行上述命令,可以啟動 Portainer 容器,並將主機的 Docker Socket 掛載到容器內。
- 存取 Portainer:在瀏覽器中存取
http://<主機IP>:9000,即可進入 Portainer 的管理介面。
建構、釋出、執行 - 嚴格區分建構和執行階段
12 Factor 應用程式提倡將軟體開發流程分為三個週期:
- 建構(build):準備程式碼成為可執行檔案或 Docker 映像檔
- 釋出(release):釋出這個可執行檔案或 Docker 映像檔(例如釋出到 GitHub 或推播到 Docker Hub)
- 執行(run):最終階段,執行任何釋出版本
這些階段之間的區分確保了開發在正確的環境中進行。如果使用像 Node 或 PHP 這樣的指令碼語言,就有可能在執行環境中修改它,但這會增加諸如如何將這些變更傳回建構階段的複雜性。透過建立軟體的釋出版本,也可以實作對生產環境中可能造成幹擾的變更進行回復。
由於 Go 是一種具有編譯器的程式語言,「建構」階段負責將原始碼轉換為機器碼可執行檔,而「釋出」階段則將這些可執行檔釋出到 GitHub 或作為 Docker 容器釋出到 Docker Hub。
建構 Go 應用程式
使用 Go 語言,可以為多個架構和平台建構可執行檔。例如,架構包括:
- amd64:64 位元 x86 架構
- 386:32 位元 x86 架構
- arm:ARM 架構(例如 Raspberry PI)
在建構應用程式時,可以傳遞 GOARCH 變數以指定目標平台。同樣,可以傳遞 GOOS 變數以指定目標作業系統:支援的選項包括 linux、windows、freebsd 和 darwin 等。這意味著只要能夠為其建構可執行檔,就可以將應用程式封裝到不同的平台上。
當應用程式被編譯時,它會使用系統中可用的函式庫。這個過程稱為動態連結。當將可執行檔從一個系統傳輸到另一個系統(或從一個 Docker 映像檔傳輸到另一個)時,它很可能具有不同的函式庫,從而導致應用程式無法運作。
在建構 Docker 映像檔時,應該使用 golang:1.8-alpine 之類別的映像檔來建構映像檔,然後使用底層基礎映像檔 alpine:3.5 來執行產生的二進位檔。如果使用其他 Go 語言的 Docker 映像檔,可以檢查 Docker Hub 頁面上的 Dockerfile,以檢視哪個基礎映像檔對於應用程式是安全的。
檢查可執行檔型別的典型方法是:
# 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
內容解密:
上述指令用於檢查 ./gotwitter 可執行檔的型別。輸出結果顯示該檔案是一個 64 位元的 ELF 可執行檔,使用動態連結,並指定了解譯器。這個資訊對於瞭解可執行檔的相容性和執行環境至關重要。
另一種(更好的)方法是使用靜態連結,這樣產生的應用程式會嵌入所需的函式庫。這樣,二進位檔就真正具有可移植性,可以將其移動到其他相容的作業系統和平台上,無論它們提供哪些函式庫。
實作 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
內容解密:
上述指令顯示了靜態連結後的 gotwitter 可執行檔資訊。輸出結果中的 “statically linked” 表示該檔案是靜態連結的,這使得它在不同環境中的可移植性更強。這種方法避免了動態連結可能帶來的相容性問題。
建構 Docker 映像檔
在建構 Docker 映像檔時,建議使用特定的版本來標記映像檔。這可以是日期/時間字串、任何遞增的數字或版本控制系統(如 semver)或 Git 儲存函式庫中的提交 ID。如果使用提交 ID,就意味著可以根據該提交中的原始碼重建完全相同的 Docker 映像檔。
GITVERSION=$(git rev-list HEAD | head -n 1 | cut -c1-6)
內容解密:
上述指令用於取得最新的 Git 提交 ID 的前 6 個字元,並將其儲存在 GITVERSION 變數中。這種做法使得映像檔可以與特定的程式碼版本對應,便於追蹤和管理。
通常會在 Dockerfile 中設定一個引數和環境變數來儲存這個資訊。
ARG GITVERSION=development
ENV GITVERSION ${GITVERSION}
內容解密:
在 Dockerfile 中,ARG 用於定義一個變數,可以在建構時透過 --build-arg 選項傳遞。ENV 用於設定環境變數,使得該值在容器執行時仍然可用。這樣可以在容器內部檢查 GITVERSION 的值,以確定當前的程式碼版本。
檢查容器內 GITVERSION 的值的方法如下:
# docker exec -it monocms-cache-nutcracker env | grep GITVERSION
GITVERSION=85990e
內容解密:
上述指令用於在執行的容器中檢查 GITVERSION 環境變數的值。這使得管理員可以輕鬆地將容器內的版本資訊與 Git 儲存函式庫中的提交 ID 對應起來,從而方便地進行版本控制和問題排查。
使用 Codeship 實作持續整合與佈署
在軟體開發過程中,持續整合(Continuous Integration, CI)是一種被廣泛接受的實踐方法。當開發者將程式碼變更推播到 Git 儲存函式庫時,CI 系統會自動觸發建置流程。本文將介紹如何使用 Codeship 實作持續整合與佈署,並以一個 ID 生成服務(sonyflake)為例進行說明。
設定 Codeship
首先,我們需要在 Codeship 上建立一個新的專案。Codeship 提供了簡單的註冊流程,其免費方案允許每月進行 100 次建置。我們將使用「Codeship Pro」產品,該產品支援 Docker、可自訂的 CI 環境以及本地建置執行器。
建立新專案
- 登入 Codeship 後台,點選大綠按鈕建立新專案。
- 選擇要連線的 Git 儲存函式庫(支援 GitHub、Bitbucket 和 GitLab)。本文以 GitHub 為例。
- 連線儲存函式庫時,請務必選擇「Codeship Pro」方案。
安裝 Jet CLI
為了在本地端除錯和測試建置流程,我們需要安裝 Codeship 的 Jet CLI。Jet CLI 可以幫助我們加密安全憑證並在本地執行建置步驟。
- 依照官方檔案指示安裝 Jet CLI。
- 安裝完成後,可以在本地端執行
jet steps指令來測試建置流程。
設定環境變數與加密憑證
為了將建置好的 Docker 映像檔推播到 Docker Hub 並發布到 GitHub,我們需要設定一些環境變數。這些變數包括 Docker Hub 登入憑證和 GitHub Token。
設定環境變數
- 將以下環境變數儲存到名為
.env的檔案中:DOCKER_REGISTRY_USERNAME=titpetric DOCKER_REGISTRY_PASSWORD=ImNotTellingYou GITHUB_TOKEN=somethingsecret - 將
.env檔案新增到.gitignore中,以避免將敏感資訊提交到 Git 儲存函式庫。
加密環境變數
- 使用
jet encrypt指令加密.env檔案:jet encrypt .env .env.encrypted - 將加密後的
.env.encrypted檔案提交到 Git 儲存函式庫。
定義 Codeship 服務與建置步驟
在 Codeship 中,我們需要定義 codeship-services.yml 和 codeship-steps.yml 兩個檔案來設定建置流程。
codeship-services.yml
此檔案定義了用於建置和佈署的服務。對於我們的 Go 語言專案,我們將使用 Docker 建置模式來產生包含靜態編譯執行檔的 Docker 映像檔。
# Dockerfile.build
FROM golang:1.8-alpine
MAINTAINER Tit Petric <black@scene-si.org>
RUN apk --update add bash make docker && go get -u github.com/aktau/github-release
WORKDIR /go/src/app
codeship-steps.yml
此檔案定義了建置和佈署的步驟。Codeship 將根據此檔案中的定義執行建置、測試和佈署操作。
# 簡化的 codeship-steps.yml 範例
steps:
- name: 建置與佈署
# 建置和佈署指令...
此圖示
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title Redis平行處理與DockerRegistry管理
package "Docker 架構" {
actor "開發者" as dev
package "Docker Engine" {
component [Docker Daemon] as daemon
component [Docker CLI] as cli
component [REST API] as api
}
package "容器運行時" {
component [containerd] as containerd
component [runc] as runc
}
package "儲存" {
database [Images] as images
database [Volumes] as volumes
database [Networks] as networks
}
cloud "Registry" as registry
}
dev --> cli : 命令操作
cli --> api : API 呼叫
api --> daemon : 處理請求
daemon --> containerd : 容器管理
containerd --> runc : 執行容器
daemon --> images : 映像檔管理
daemon --> registry : 拉取/推送
daemon --> volumes : 資料持久化
daemon --> networks : 網路配置
@enduml