1. 程式人生 > 程式設計 >Go語言實現微服務工具鏈(一) - 藍綠部署

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服務。選擇部署環境的邏輯如下:

  1. 部署環境的status不是production狀態
  2. 部署環境的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(&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)
}
複製程式碼

四、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
	}
	// 獲取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)
}
複製程式碼

五、使用效果

  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 |
+----+------------+-------+------------+---------+-----------------------------------------------------------+
複製程式碼

六、結語

當服務被部署到blue或green環境之後,會把自己的ip和port註冊到註冊中心。在使用藍綠部署的時候,部署工具可以把每對ip/port當前是production還是staging同步給閘道器。API閘道器基於服務註冊發現和部署工作同步過來的資訊,即可知道測試流量和外部正常流量分別應該轉發到哪個ip/port。