Go語言實現微服務工具鏈(一) - 藍綠部署

開一個系列坑,記錄使用Go語言練習實現微服務工具鏈的過程,第一篇是藍綠部署的實現。mysql

藍綠部署是不停老版本,部署新版本而後進行測試,確認OK,將流量切到新版本。git

項目的git地址爲 github.com/mikellxy/mk… (藍綠部署的實如今api目錄的deploy目錄下)github

1、定義項目

在藍綠部署中,上線的過程當中,不會停用老版本,而是另外部署新的服務來運行新版本,並導入測試流量進行測試。下文中,把一個項目正在對外提供服務的稱做production服務,部署了新版本正在進行測試的稱做staging服務。當測試經過後,咱們執行swtich操做,把staging和production的身份對調,此時運行新版本代碼的服務變成production對外提供服務。golang

因而可知,咱們在描述一個項目的時候,必需要指定兩套部署環境,由於須要支持同時運行production和staging的服務。sql

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的項目指定兩套部署環境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

2、描述部署環境

在給項目定義了描述項目部署環境的參數以後,下一步須要定義一個數據結構來描述兩個部署環境的狀態。api

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服務。選擇部署環境的邏輯以下:數據結構

  1. 部署環境的status不是production狀態
  2. 部署環境的stage不是pending,避免同時進行兩次部署,出現數據競爭和衝突 (第一次上線時,兩個環境都沒部署過,須要先建立一條deployment數據描述其中一個部署環境。第二次上線的時候,以前只用了一個部署環境,建立描述另一個部署環境的deployment數據。以後兩個deployment交替用於staging和production。相似把須要O(n*n)空間複雜度的動態規劃,優化成使用兩行的二維數據的思路)

首先根據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
	}
}
複製代碼

3、描述要部署的代碼版本

通常在進行部署以前,新版本的代碼已經經過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(&param); 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)
}
複製代碼

4、production和staging切換

當staging環境的代碼測試經過以後,能夠把它修改爲production(信息同時同步到api網關,後續文章再討論api網關的實現,這裏先不展開)對外提供服務。

func deploymentSwitch(c *gin.Context) {
	param := IntID{}
	if err := c.BindUri(&param); 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)
}
複製代碼

5、使用效果

  1. 把打包好的項目的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 |
+----+------------+-----------------------------------------------------------+------+
複製代碼
  1. 調用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 |
+----+------------+-------+---------+---------+-----------------------------------------------------------+
複製代碼
  1. 測試完畢,調用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 |
+----+------------+-------+------------+---------+-----------------------------------------------------------+

複製代碼
  1. 打包新的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 |
+----+------------+-----------------------------------------------------------+------+
複製代碼
  1. 進行部署 此時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

複製代碼
  1. 測試完成,對調身份
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 |
+----+------------+-------+------------+---------+-----------------------------------------------------------+
複製代碼

6、結語

當服務被部署到blue或green環境以後,會把本身的ip和port註冊到註冊中心。在使用藍綠部署的時候,部署工具能夠把每對ip/port當前是production仍是staging同步給網關。API網關基於服務註冊發現和部署工做同步過來的信息,便可知道測試流量和外部正常流量分別應該轉發到哪一個ip/port。

相關文章
相關標籤/搜索