开一个系列坑,记录使用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。