開一個系列坑,記錄使用Go語言練習實現微服務工具鏈的過程,第一篇是藍綠部署的實現。mysql
藍綠部署是不停老版本,部署新版本而後進行測試,確認OK,將流量切到新版本。git
項目的git地址爲 github.com/mikellxy/mk… (藍綠部署的實如今api目錄的deploy目錄下)github
在藍綠部署中,上線的過程當中,不會停用老版本,而是另外部署新的服務來運行新版本,並導入測試流量進行測試。下文中,把一個項目正在對外提供服務的稱做production服務,部署了新版本正在進行測試的稱做staging服務。當測試經過後,咱們執行swtich操做,把staging和production的身份對調,此時運行新版本代碼的服務變成production對外提供服務。golang
因而可知,咱們在描述一個項目的時候,必需要指定兩套部署環境,由於須要支持同時運行production和staging的服務。sql
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"`
}
複製代碼
建立一個叫test_project的項目指定兩套部署環境docker
+----+--------------+-----------+-----------+-----------+------------+
| 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端口。json
在給項目定義了描述項目部署環境的參數以後,下一步須要定義一個數據結構來描述兩個部署環境的狀態。api
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"`
}
複製代碼
當須要上線新版本代碼的時候,首先咱們須要找到一個合適的部署環境,來部署staging服務。選擇部署環境的邏輯以下:數據結構
首先根據project id查出對應的deployment數據,而後進行部署環境的選擇,實現代碼以下:tcp
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, nil
}
return nil, errors.New("deploying")
}
// 第一使用blue, 建立數據
blue, err := (&models.Deployment{}).Create(project.ID, BLUE)
if err != nil {
return nil, err
}
return blue, nil
} else {
// 新項目第一次部署,建立blue,用於部署
blue, err := (&models.Deployment{}).Create(project.ID, BLUE)
if err != nil {
return nil, err
}
return blue, nil
}
}
複製代碼
通常在進行部署以前,新版本的代碼已經經過ci工具打包成了docker image。當肯定了用於部署staging服務的環境以後,咱們須要得到最新的docker image的信息,而後經過docker api來部署docker image。
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"`
}
複製代碼
部署的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)
}
複製代碼
當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
}
// 獲取project
project := &models.Project{}
project, err := project.FindOneByID(uint(param.ID), true)
if err != nil {
c.JSON(http.StatusNotFound, 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)
}
複製代碼
mysql> select * from package where project_id = 1 order by id desc;
+----+------------+-----------------------------------------------------------+------+
| id | project_id | tag | port |
+----+------------+-----------------------------------------------------------+------+
| 1 | 1 | mikellxy/test-ci:ef95f0d98da9d5f76cb75c49e3541f8c98a9327f | 0 |
+----+------------+-----------------------------------------------------------+------+
複製代碼
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 |
+----+------------+-------+---------+---------+-----------------------------------------------------------+
複製代碼
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 |
+----+------------+-------+------------+---------+-----------------------------------------------------------+
複製代碼
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 |
+----+------------+-----------------------------------------------------------+------+
複製代碼
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。