1. 程式人生 > >研發環境容器化實施過程(docker + docker-compose + jenkins)

研發環境容器化實施過程(docker + docker-compose + jenkins)

目錄

  • 背景介紹
  • 改造思路
  • 容器構建
    • 基礎準備
    • 中介軟體容器
    • 外部依賴容器
    • 業務應用容器
  • 容器整合
  • 自動構建容器
    • Maven相關
    • 非Maven專案
  • 總結

背景介紹

目前公司內部系統(代號GMS)研發團隊,專案整體微服務規模大概是4+9+3的規模,4個內部業務微服務,9個是外部平臺或者基礎服務(檔案資源/使用者中心/閘道器/加密等),3箇中間件服務(資料庫/Redis/Nacos)。
分為2個組,迭代週期為2周。需求和排期都是會有交叉,會保證每週都有迭代內容交付,另外技術部門也在進行效能優化以及程式碼規約的重構。我們的Git管理模型使用的是AoneFlow,意味著同一時間可能會有多個研發特性分支進行中。出現的問題就是CI,我們整合使用的Jenkins,原本研發環境就只有一套Jenkins來構建,後來出現並行的特性分支,為了支援開發聯調工作就重新搭建了一套環境,但是後面出現了更多的並行需求(例如對介面壓測的效能分支,底層基礎架構的升級分支,程式碼規約調整的分支)。
現在的痛點是需要部署一個環境的成本太高,基本需要一個高階研發對於所有元件都瞭解,對於Linux系統瞭解。整套環境部署可能需要2天左右,而且過程特別複雜容易出錯。

改造思路

考慮是需要進行容器化改造,目前整個環境的管理還沒有基於容器化來實施,所以我們希望這次也是給團隊一個基本概念和練兵的機會。
因為我們主要的訴求是環境部署,所以並沒有按照容器推薦的那樣,每個服務都單獨建立docker,而是為了能夠快速的部署和構建將所有服務和中介軟體進行分塊。
目前分塊主要是分為中介軟體服務,業務服務,依賴/底層服務這麼三大塊。這麼分的原因有下面一些:

  • 中介軟體包含資料庫、Nacos、Redis。這麼做的目的是因為Nacos強依賴資料庫,資料庫也是所有微服務的基礎依賴之一。資料庫結構和Nacos的配置實際上每個迭代會有一些變化,所以將這些內容打包在一起,以版本區分會更簡單一些。
  • 依賴/底層服務包含非業務的服務(檔案資源/使用者中心/閘道器/加密)。這些都是外部服務,迭代過程中的變化是比較少的,可以每隔幾個迭代打包一次。所以為了操作便利所以統一打包成了一個映象。
  • 業務微服務,業務的微服務就是迭代開發過程中不斷修改和測試的內容,所以這塊是應該是要單獨的容器,並且還要和Jenkins關聯能夠更新。

這樣基本的容器劃分就確認了,整體使用docker-compose來進行容器管理,因為實際的映象數量會稍微多一些,而且還有很多如埠等配置。

容器構建

思路確認之後就開始執行,我們將比較詳細的描述各個映象的構建過程。

基礎準備

伺服器上首先需要安裝好 docker和docker-compose依賴。我們的docker的私服使用的Harbor。
接下來我們基本都是在準備所有的dockerfile,所以會建立一個基礎目錄,在/root/docker/下建立 gms 資料夾用於存放各個映象的dockerfile,以及docker-compose檔案。
提供一個基礎映象用於其他映象生成,基礎映象需要包含java環境,以及一些環境基礎外掛和工具,我們來看一下Dockerfile

FROM centos:7

RUN mkdir -p /home/project/vv/log

ADD jdk-8u211-linux-x64.tar.gz /home/project/
RUN mv /home/project/jdk1.8.0_211 /home/project/java
COPY entrypoint.sh /home/project/vv/
ENV JAVA_HOME /home/project/java
ENV CLASSPATH .:$JAVA_HOME/lib:$JAVA_HOME/jre/lib:$CLASSPATH
ENV PATH $JAVA_HOME/bin:$JAVA_HOME/jre/bin:$PATH
# running required command
WORKDIR /home/project/vv
RUN yum install -y wget curl net-tools openssh-server telnet nc && chmod +x entrypoint.sh

這裡可以關注的點在於,我們在這個基礎映象的資料夾內實際上是要把我們需要使用的檔案都儲存好的,意思就是你需要複製進映象的檔案都必須在你當前執行docker build命令的目錄中。
然後就是這裡會需要存在設定環境變數。

接下來執行 docker build -t images:tag .
不要漏掉最後的那個. 那個實際上就是指定當前目錄。
完成後執行 docker push
這樣基礎映象就構建完成,我們的命名為 172.16.6.248/gms-service/gms-base
172.16.6.248是我們內部的Harbor伺服器。

中介軟體容器

中介軟體容器需要包含資料庫、Nacos、Redis。
上面也說過,Nacos依賴與資料庫,由於是研發環境Nacos使用的standalone模式。其中有一點需要注意,在Nacos中可能會設定資料庫、Redis等連線,無論原本使用的是ip還是域名,在這裡都需要改成是服務名稱,由於我們是使用類docker-compose並且採用network組網的形式將相關的服務都放在同一個網路內進行多例項之間的隔離。Nacos指向的資料庫連線改為本地。
我們來看一下目錄結構:

-rw-r--r--. 1 root root 336 12月 17 17:58 Dockerfile
-rw-r--r--. 1 root root 205 12月 17 18:34 gmsstore.sh
-rw-r--r--. 1 root root 532 12月 6 15:31 my.cnf
drwxr-xr-x. 12 root root 206 12月 6 15:24 mysql
drwxr-xr-x. 17 root root 8192 12月 26 14:19 mysqldata
drwxr-xr-x. 9 root root 125 12月 4 11:10 nacos
drwxrwxr-x. 6 root root 4096 12月 11 15:21 redis

Dockerfile不用解釋,由於包含多箇中間件,所以啟動命令打包成了shell。
mysql涉及到3個檔案/資料夾,my.cnf是配置檔案,mysql是程式本體,mysqldata是打包了所有相關庫資料。
nacos和redis資料夾也不用解釋了。

我們來看看這個Dockerfile:

FROM 172.16.6.248/gms-service/gms-base

RUN yum -y install libaio numactl

COPY mysql /home/project/mysql
COPY mysqldata /home/project/mysqldata
COPY my.cnf /etc/my.cnf
COPY nacos /home/project/nacos
COPY redis /home/project/redis
COPY gmsstore.sh /home/project
WORKDIR /home/project/
RUN chmod +x gmsstore.sh
ENTRYPOINT ./gmsstore.sh

這裡特別的地方在於mysql8需要安裝一些依賴才可以執行,所以我們安裝了libaio numactl。

外部依賴容器

先來看下目錄結構

-rw-r--r--. 1 root root 358 12月 17 19:26 Dockerfile
-rw-r--r--. 1 root root 764 12月 16 17:29 gmsdependency.sh
-rw-r--r--. 1 root root 71509153 12月 16 17:30 vv-dict.jar
-rw-r--r--. 1 root root 63880862 12月 16 17:29 vv-encryption.jar
-rw-r--r--. 1 root root 51465237 12月 16 17:30 vv-gateway.jar
-rw-r--r--. 1 root root 69535661 12月 16 17:29 vv-message.jar
-rw-r--r--. 1 root root 171366034 12月 16 17:30 vv-resource.jar
-rw-r--r--. 1 root root 78130738 12月 16 17:29 vv-user.jar

這個套路和之前一樣,相關的服務已經打出了jar包放到打包目錄下,編寫shell指令碼作為所有應用啟動的統一入口。
接下來看下Dockerfile:

FROM 172.16.6.248/gms-service/gms-base

LABEL version="1.0"
LABEL description="vv-gms-den"
LABEL maintainer="[email protected]"
COPY jar/* /home/project/vv/
ENV JVM=""
ENV NACOS="127.0.0.1:9002"
RUN chmod +x /home/project/vv/gmsdependency.sh
EXPOSE 7003
EXPOSE 8100
EXPOSE 7002
EXPOSE 7004
EXPOSE 7001
WORKDIR /home/project/vv/
ENTRYPOINT ./gmsdependency.sh

在這裡我們環境變數中設定Nacos的地址,實際上Nacos的地址會使用服務名的方式進行訪問,在使用 java -jar 命令時直接設定到引數中,類似這樣:

java -jar vv-dict.jar --spring.cloud.nacos.config.server-addr=$NACOS --spring.cloud.nacos.discovery.server-addr=$NACOS

業務應用容器

業務應用容器反而是最沒啥好說的,只有一個單jar檔案,然後一個啟動指令碼。
這裡可能唯一需要注意一下的就是第一次啟動的問題,由於業務應用依賴於中介軟體,當啟動時mysql和Nacos可能還沒有那麼快啟動起來,所以可能會引發業務應用連線不上中介軟體自動退出,需要寫指令碼檢測。
給出Dockerfile

FROM 172.16.6.248/gms-service/gms-base

LABEL version="1.0"
LABEL description="vv-gms-core"
LABEL maintainer="[email protected]"
COPY jar/* /home/project/vv/
ENV JVM=""
ENV NACOS="127.0.0.1:9002"
RUN chmod +x /home/project/vv/*.sh
EXPOSE 8102
WORKDIR /home/project/vv/

沒啥多解釋的了。

容器整合

所有的docker映象都已經構建完畢並且已經傳輸到了映象伺服器上。接下來就是如何整合容器了。
之前已經說過本次的選型是docker-compose,沒有上k8s是因為還沒有和運維同學協調好,我們使用docker-compose先做可行性測試。
docker-compose 的安裝很多教程,我列一下基本命令

yum -y install epel-release
yum -y install python-pip
yum -y install python-devel
pip --version
pip install --upgrade pip
pip install docker-compose 
docker-compose --version

接下來看一下 docker-compose.yml

version: '3'
services:
  gms-dependency:
    image: 172.16.6.248/gms-service/gms-dependency
    ports:
      - 13004:7001
      - 13005:7003
      - 13006:7004
      - 13007:17002
      - 13008:8100
    networks:
      - gmsnetwork
    environment:
      JVM:
      NACOS: gms-store:9002
    depends_on:
      - gms-gateway
    volumes:
      - "/home/project/vv/log/:/home/project/vv/log/"
    entrypoint: ./entrypoint.sh -d gms-store:3306,gms-store:9002 -c './gmsdependency.sh'
  gms-gateway:
    image: 172.16.6.248/gms-service/gms-gateway
    ports:
      - 13010:9001
    networks:
      - gmsnetwork
    depends_on:
      - gms-store
    volumes:
      - "/home/project/vv/log/:/home/project/vv/log/"
    entrypoint: ./entrypoint.sh -d gms-store:3306,gms-store:9002 -c 'java -jar vv-gateway.jar --spring.cloud.nacos.config.server-addr=gms-store:9002 --spring.cloud.nacos.discovery.server-addr=gms-store:9002 >/dev/null 2>&1'
  gms-oacore:
    image: 172.16.6.248/gms-service/gms-oacore:1.2.7
    ports:
      - 13009:8102
    networks:
      - gmsnetwork
    environment:
      JVM:
      NACOS: gms-store:9002
    depends_on:
      - gms-gateway
    volumes:
      - "/home/project/vv/log/:/home/project/vv/log/"
    entrypoint: ./entrypoint.sh -d gms-store:3306,gms-store:9002 -c 'java -jar vv-oa-core.jar --spring.cloud.nacos.config.server-addr=gms-store:9002 --spring.cloud.nacos.discovery.server-addr=gms-store:9002 >/dev/null 2>&1'
  gms-store:
    image: 172.16.6.248/gms-service/gms-store:1.2.6
    ports:
      - 13001:3306
      - 13002:9002
      - 13003:6379
    networks:
      - gmsnetwork
    volumes:
      - "/home/project/vv/log/:/home/project/vv/log/"
  gms-oaweb:
    image: 172.16.6.248/gms-service/gms-oaweb:1.2.6
    ports:
      - 13018:80
    networks:
      - gmsnetwork
    depends_on:
      - gms-oacore
      - gms-dependency
      - gms-gateway
    volumes:
      - "/home/project/vv/log/:/home/project/vv/log/"
    entrypoint: /home/project/vv/entrypoint.sh -d gms-oacore:8102,gms-dependency:17002,gms-gateway:9001,gms-xxladmin:8103 -c 'nginx -g "daemon off;"'
networks:
  gmsnetwork:
    driver: bridge

這份檔案中,其實是比較常規的docker-compose的格式,由於各個容器之間相互可能都有依賴,所以我們使用了內部網路,networks 這個特性,將相關應用放在同一個內部網路中互相訪問。depends_on這個屬性支援了啟動的先後順序,但是這個屬性僅僅基於容器級別。也就是前置的容器只要啟動後續就會啟動,但是內部依賴的應用可能還沒有啟動完成,所以我們使用了shell指令碼來檢測應用啟動完成後再實際的啟動應用。最後就是我們使用volumes開放了可掛載目錄,輸出所有的日誌檔案便於檢視。environment設定環境變數,將依賴的服務名和內部網路埠傳遞給不同容器中的應用。
這樣就完成了docker-compose的設計,然後我們使用 docker-compose up -d 就可以啟動 docker-compose stop 可以關閉。但是切記docker-compose命令必須在存在docker-compose.yml檔案的目錄下執行。

自動構建容器

我們使用docker-compose已經啟動了完整的環境,但是記得本次實踐的目的在於研發環境的部署,研發環境是需要不斷的更新程式碼進行除錯的。所以我們需要引入jenkins來進行容器重新構建、推送、環境更新。

Maven相關

我們的專案是一個父子的Maven專案,父目錄下會包含業務核心程式碼(core)、對外暴露API(api)等包。需要打包的映象實際上是core中的完整jar包。
Maven的外掛我們使用的是

<build>
    <plugins>
        <plugin>
                <groupId>com.spotify</groupId>
                <artifactId>dockerfile-maven-plugin</artifactId>
                <version>1.4.10</version>
                <configuration>
                    <repository>172.16.6.248/gms-service/gms-oacore</repository>
                    <force>true</force>
                    <forceCreation>true</forceCreation>
                    <tag>${dev.docker.tag}</tag>
                    <buildArgs>
                        <JAR_FILE>vv-oa-core/target/${project.build.finalName}.jar</JAR_FILE>
                    </buildArgs>
                </configuration>
            </plugin>
    </plugins>
</build>

這是github上的外掛地址,這裡force這個標籤是可以對同tag的映象在構建時進行覆蓋。
Dockerfile建議放在子專案根目錄下。
在Jenkins構建時,我們是以父專案作為根目錄執行 package 命令的。打包完成後,不能直接執行 dockerfile:build 命令。而是在Post Steps中定製指令碼,cd進入到子專案之後,分別執行 dockerfile:build dockerfile:push,我們來看一下Jenkins中配置的指令碼:

cd gms-oacore

nowtag=1.3.1
nowpath=/root/docker/dockerbase/feature-VV-443
nowprefix=feature-VV-443

mvn -Dmaven.test.skip=true dockerfile:build -Ddev.docker.tag=$nowtag
mvn -Dmaven.test.skip=true dockerfile:push -DpushImageTag

ssh [email protected] "/root/docker/dockerbase/jenkins-rebuild.sh $nowpath "$nowprefix"_gms-oacore_1 172.16.6.248/gms-service/gms-oacore:$nowtag gms-oacore"

這裡實際上是在Jenkins先打包,並且build和push映象,ssh到目標伺服器通過遠端指令碼來進行拉取構建的一些操作。
針對docker-compose啟動的容器,如果是要單獨更新一個映象,可以將容器stop之後rm掉,同時rmi對應映象,最終使用 docker-compose --scale images:tag=1 重新拉取啟動這個映象。

非Maven專案

由於是前後端分離專案,所以我們還會有一個單獨的前端專案,是直接掛載Nginx容器內。所以這部分沒辦法使用Maven外掛,我們就採用shell直接呼叫docker命令的形式,這裡放上差異的部分:

docker build -t 172.16.6.248/gms-service/gms-oaweb:$nowtag .
docker push 172.16.6.248/gms-service/gms-oaweb:$nowtag

替換來mvn dockerfile相關的命令,其他基本相同。

總結

在這樣的實踐中,我們將專案拆分為合理粒度建立docker映象,使用docker-compose將多個容器打包為一個完整環境執行,同時用內部網路的概念隔離多個環境在同一個宿主機器時的影響,最後使用Jenkins來進行自動化的構建和釋出,完成了研發環境的完整閉環。效果也大大提高,原本需要花幾天時間還不能很完整的部署好,現在只需要一個人15-30分鐘就可以完整部署好一個環境。
但是實際上問題也很多,由於整合來大量的環境,所以單個環境啟動後,佔用記憶體10G左右,實際上比較難單個宿主機器直接部署多套。
另外大家也能發現,我們存在部分訪問是通過ip來的,這是一個不好的習慣,建議儘量都改為內部域名的形式,避免後續伺服器變更造成複雜影響。
在實施過程中,我們還是手寫了很多shell指令碼作為中間粘合,這個對於環境的依賴會比較大,而且複用性其實是很低的,後續我們會考慮如何提高可複用性。
最後還是要考慮實施k8s,這個應該在2020的Q1就會實施。

容器化是為了能夠讓研發和運維對於應用的把握程度更高,避免大家花太多的時間在環境、部署之類問題上,也能夠大大提高系統的穩定性和擴充套件性。但是會對DevOps提出更高的要求,研發和運維要更加緊密的配合,架構設計、部署方案等都需要共同討論理解之後才能實施,但是我堅信這就是趨勢,我們越早迎合越早能提升自己、整個團隊和我們的產