Go語言實現微服務工具鏈(一) - 藍綠部署
開一個系列坑,記錄使用Go語言練習實現微服務工具鏈的過程,第一篇是藍綠部署的實現。
藍綠部署是不停老版本,部署新版本然後進行測試,確認OK,將流量切到新版本。
專案的git地址為 github.com/mikellxy/mk… (藍綠部署的實現在api目錄的deploy目錄下)
一、定義專案
在藍綠部署中,上線的過程中,不會停用老版本,而是另外部署新的服務來執行新版本,並匯入測試流量進行測試。下文中,把一個專案正在對外提供服務的稱作production服務,部署了新版本正在進行測試的稱作staging服務。當測試通過後,我們執行swtich操作,把staging和production的身份對調,此時執行新版本程式碼的服務變成production對外提供服務。
由此可見,我們在描述一個專案的時候,必須要指定兩套部署環境,因為需要支援同時執行production和staging的服務。
project表設計
type Project struct {
Model
Name string `gorm:"not null;unique_index:uix_project_name;type:varchar(64)" json:"name" binding:"required"`
BlueIP string `json:"blue_ip"`
BluePort uint32 `json:"blue_port"`
GreenIP string `json:"green_ip"`
GreenPort uint32 `json:"green_port"`
Deployments []*Deployment `gorm:"association foreign:project_id"`
}
複製程式碼
- name:專案名
- blue_ip,green_ip:分別是兩套部署環境的ip地址
- blue_port,green_port: 分別是在兩套部署環境專案執行的埠
建立專案
建立一個叫test_project的專案指定兩套部署環境
+----+--------------+-----------+-----------+-----------+------------+
| id | name | blue_ip | blue_port | green_ip | green_port |
+----+--------------+-----------+-----------+-----------+------------+
| 1 | test_project | 127.0.0.1 | 8001 | 127.0.0.1 | 8002 |
+----+--------------+-----------+-----------+-----------+------------+
複製程式碼
在這個練習中,會把專案部署在本機的docker swarm上,所以兩套部署環境的ip指定為localhost,docker swarm代理專案的埠分別是本機的8001和8002埠。
二、描述部署環境
在給專案定義了描述專案部署環境的引數之後,下一步需要定義一個資料結構來描述兩個部署環境的狀態。
deployment表設計
type Deployment struct {
Model
ProjectID uint `gorm:"not null;unique_index:uix_deployment_project_id_color" json:"project_id"`
Color string `gorm:"type:varchar(16);not null;unique_index:uix_deployment_project_id_color" json:"color"`
// production or staging
Status string `gorm:"type:varchar(32);not null;default:'staging'" json:"status"`
// stop,pending or running
Stage string `gorm:"type:varchar(32);not null;default:'stop'" json:"stage"`
PackageTag string `json:"package_tag"`
}
複製程式碼
- project_id: 專案的id
- color: blue/green,用於區分兩套部署環境,和project_id聯合唯一,因為每個專案只需要有兩套部署環境
- status: production/staging,環境上部署的是對外提供服務的版本還是在staging測試的版本
- stage:running正常執行,pending部署中,stop沒有在執行
- package_tag:描述環境上正在執行的程式碼版本,在這個練習中是專案的docker image的tag
選擇部署環境
當需要上線新版本程式碼的時候,首先我們需要找到一個合適的部署環境,來部署staging服務。選擇部署環境的邏輯如下:
- 部署環境的status不是production狀態
- 部署環境的stage不是pending,避免同時進行兩次部署,出現資料競爭和衝突 (第一次上線時,兩個環境都沒部署過,需要先建立一條deployment資料描述其中一個部署環境。第二次上線的時候,之前只用了一個部署環境,建立描述另外一個部署環境的deployment資料。之後兩個deployment交替用於staging和production。類似把需要O(n*n)空間複雜度的動態規劃,優化成使用兩行的二維資料的思路)
首先根據project id查出對應的deployment資料,然後進行部署環境的選擇,實現程式碼如下:
func GetStaging(project *models.Project) (*models.Deployment,error) {
var blue,green *models.Deployment
for _,d := range project.Deployments {
if d.Color == GREEN {
green = d
} else {
blue = d
}
}
if blue != nil {
// blue可用於staging
if blue.Status == STATUS_STAG {
if blue.Stage != STAGE_PENDING {
return blue,nil
}
// 正在部署,返回錯誤
return nil,errors.New("deploying")
}
if green != nil {
// green可用於staging
if green.Stage != STAGE_PENDING {
return green,nil
}
return nil,errors.New("deploying")
}
// 第一使用green,建立資料
green,err := (&models.Deployment{}).Create(project.ID,GREEN)
if err != nil {
return nil,err
}
return green,nil
} else if green != nil {
// green可用於staging
if green.Status == STATUS_STAG {
if green.Stage != STAGE_PENDING {
return green,errors.New("deploying")
}
// 第一使用blue,建立資料
blue,BLUE)
if err != nil {
return nil,err
}
return blue,nil
} else {
// 新專案第一次部署,建立blue,用於部署
blue,nil
}
}
複製程式碼
三、描述要部署的程式碼版本
一般在進行部署之前,新版本的程式碼已經通過ci工具打包成了docker image。當確定了用於部署staging服務的環境之後,我們需要獲得最新的docker image的資訊,然後通過docker api來部署docker image。
package表設計
type Package struct {
Model
ProjectID uint `gorm:"not null;index" json:"project_id"`
Tag string `gorm:"not null;unique;" json:"tag"`
Port uint32 `gorm:"not null" json:"port"`
}
複製程式碼
- project_id: 專案的id
- tag: docker image的tag
- port:容器expose的埠
部署的api實現如下,呼叫docker api使用的是官方的Go SDK:
type IntID struct {
ID int `uri:"id" binding:"required"`
}
func deploy(c *gin.Context) {
var ip string
var port uint32
param := IntID{}
if err := c.BindUri(¶m); err != nil {
c.JSON(http.StatusUnprocessableEntity,err.Error())
return
}
// 獲取project
project := &models.Project{}
project,err := project.FindOneByID(uint(param.ID),true)
if err != nil {
c.JSON(http.StatusNotFound,err.Error())
return
}
// 選擇要部署的環境
deployment,err := service.GetStaging(project)
if err != nil {
c.JSON(http.StatusInternalServerError,err.Error())
return
}
// 根據color確定專案部署的ip和port
if deployment.Color == "green" {
ip,port = project.GreenIP,project.GreenPort
} else {
ip,port = project.BlueIP,project.BluePort
}
// 獲取專案最新的docker image版本
pkg := &models.Package{}
pkg,err = pkg.FindOneByProjectID(project.ID)
if err != nil {
c.JSON(http.StatusNotFound,err.Error())
return
}
// 獲取連線相應部署環境的docker client,使用docker api進行部署
conf := config.Conf.DockerClient
dockerClient,err := docker_api.NewDockerClient(fmt.Sprintf(conf.Host,ip))
if err != nil {
c.JSON(http.StatusInternalServerError,err.Error())
return
}
err = dockerClient.CreateSwarmService(fmt.Sprintf("%s_%s",project.Name,deployment.Color),pkg.Tag,4,map[uint32]uint32{pkg.Port: port})
if err != nil {
c.JSON(http.StatusInternalServerError,err.Error())
return
}
// 部署成功之後,把deployment的stage改成running
db,_ := database.GetDB()
deployment.Stage = "running"
deployment.PackageTag = pkg.Tag
db.Save(deployment)
if db.Error != nil {
c.JSON(http.StatusInternalServerError,db.Error.Error())
return
}
c.JSON(http.StatusCreated,deployment)
}
複製程式碼
四、production和staging切換
當staging環境的程式碼測試通過之後,可以把它修改成production(資訊同時同步到api閘道器,後續文章再討論api閘道器的實現,這裡先不展開)對外提供服務。
func deploymentSwitch(c *gin.Context) {
param := IntID{}
if err := c.BindUri(¶m); err != nil {
c.JSON(http.StatusUnprocessableEntity,err.Error())
return
}
// 獲取staging和production(第一次上線,還沒有production) deployment
var staging,other *models.Deployment
for _,d := range project.Deployments {
if d.Status == "staging" && d.Stage == "running" {
staging = d
} else {
other = d
}
}
if staging == nil {
c.JSON(http.StatusInternalServerError,"no staging project is running")
return
}
// 把staging的身份轉換成staging,把原先的production的身份轉換成staging,合適的時候可以停用老版本程式碼(呼叫docker api刪除service,並把stage改成stop)
db,_ := database.GetDB()
staging.Status = "production"
if other != nil {
other.Status = "staging"
other.Stage = "stop"
db.Save(other)
}
db.Save(staging)
c.JSON(http.StatusOK,project)
}
複製程式碼
五、使用效果
- 把打包好的專案的docker iamge資訊寫入package表
mysql> select * from package where project_id = 1 order by id desc;
+----+------------+-----------------------------------------------------------+------+
| id | project_id | tag | port |
+----+------------+-----------------------------------------------------------+------+
| 1 | 1 | mikellxy/test-ci:ef95f0d98da9d5f76cb75c49e3541f8c98a9327f | 0 |
+----+------------+-----------------------------------------------------------+------+
複製程式碼
- 呼叫api部署專案 此時使用了blue環境,status是staging
mysql> select * from deployment where project_id = 1;
+----+------------+-------+---------+---------+-----------------------------------------------------------+
| id | project_id | color | status | stage | package_tag |
+----+------------+-------+---------+---------+-----------------------------------------------------------+
| 1 | 1 | blue | staging | running | mikellxy/test-ci:ef95f0d98da9d5f76cb75c49e3541f8c98a9327f |
+----+------------+-------+---------+---------+-----------------------------------------------------------+
複製程式碼
- 測試完畢,呼叫api進行swtich 此時blue環境變成了production狀態
mysql> select * from deployment where project_id = 1;
+----+------------+-------+------------+---------+-----------------------------------------------------------+
| id | project_id | color | status | stage | package_tag |
+----+------------+-------+------------+---------+-----------------------------------------------------------+
| 1 | 1 | blue | production | running | mikellxy/test-ci:ef95f0d98da9d5f76cb75c49e3541f8c98a9327f |
+----+------------+-------+------------+---------+-----------------------------------------------------------+
複製程式碼
- 打包新的docker image,寫入package表
mysql> select * from package where project_id = 1 order by id desc;
+----+------------+-----------------------------------------------------------+------+
| id | project_id | tag | port |
+----+------------+-----------------------------------------------------------+------+
| 2 | 1 | mikellxy/test-ci:9ed308ff2e483a873e1acfd1baa7e66ae232ce19 | 8090 |
| 1 | 1 | mikellxy/test-ci:ef95f0d98da9d5f76cb75c49e3541f8c98a9327f | 8090 |
+----+------------+-----------------------------------------------------------+------+
複製程式碼
- 進行部署 此時blue環境依然是production狀態,對外提供服務。green環境變成了staging,可以進行測試.
mysql> select * from deployment where project_id = 1 order by updated_at desc;
+----+------------+-------+------------+---------+-----------------------------------------------------------+
| id | project_id | color | status | stage | package_tag |
+----+------------+-------+------------+---------+-----------------------------------------------------------+
| 2 | 1 | green | staging | running | mikellxy/test-ci:9ed308ff2e483a873e1acfd1baa7e66ae232ce19 |
| 1 | 1 | blue | production | running | mikellxy/test-ci:ef95f0d98da9d5f76cb75c49e3541f8c98a9327f |
+----+------------+-------+------------+---------+-----------------------------------------------------------+
複製程式碼
看一下docker service,兩個環境上服務執行的docker iamge跟描述的也是一致的
docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
go2nobdfy41b test_project_blue replicated 4/4 mikellxy/test-ci:ef95f0d98da9d5f76cb75c49e3541f8c98a9327f *:8001->8090/tcp
es8x3npaobhk test_project_green replicated 4/4 mikellxy/test-ci:9ed308ff2e483a873e1acfd1baa7e66ae232ce19 *:8002->8090/tcp
複製程式碼
- 測試完成,對調身份
mysql> select * from deployment where project_id = 1 order by updated_at desc;
+----+------------+-------+------------+---------+-----------------------------------------------------------+
| id | project_id | color | status | stage | package_tag |
+----+------------+-------+------------+---------+-----------------------------------------------------------+
| 1 | 1 | blue | staging | stop | mikellxy/test-ci:ef95f0d98da9d5f76cb75c49e3541f8c98a9327f |
| 2 | 1 | green | production | running | mikellxy/test-ci:9ed308ff2e483a873e1acfd1baa7e66ae232ce19 |
+----+------------+-------+------------+---------+-----------------------------------------------------------+
複製程式碼
六、結語
當服務被部署到blue或green環境之後,會把自己的ip和port註冊到註冊中心。在使用藍綠部署的時候,部署工具可以把每對ip/port當前是production還是staging同步給閘道器。API閘道器基於服務註冊發現和部署工作同步過來的資訊,即可知道測試流量和外部正常流量分別應該轉發到哪個ip/port。