Docker 入門與實踐

mrcode發表於2018-04-18
Docker 是一個能讓程式跑在一個它無法感知的、用於隔絕外界環境裡的容器的工具。

 Docker 簡介

  最初是 dotCloud 公司創始人 Solomon Hykes 發起的一個公司內部專案,並於 2013 年 3 月以 Apache 2.0 授權協議開源,程式碼主要在 GitHub 上進行維護。Docker 專案後來還加入了 Linux 基金會,併成立推動 開放容器聯盟(OCI)

  Docker 使用 Google 推出的 Go 語言開發實現,基於 Linux 核心的 cgroupnamespace,以及 UnionFS 等技術。最初實現基於 LXC,從 0.7 版本後去除 LXC,轉而開始使用自行開發的 libcontainer,從 1.11 開始,則進一步演進為使用 runCcontainerd

  在 2017 年 4 月 21 日 Pull Request #32691 將原有的 Docker 專案更名為 Moby,由 Moby 構建出 Docker CE(社群版),而新的 Docker 專案則構建出 Docker EE(企業版本)。

 Docker 初步瞭解

  Docker 容器與虛擬機器的區別

  原理不同

  上圖是 Docker Doc 關於 Docker 和傳統虛擬機器區別的截圖。

  Docker 利用了 Linux 核心的 cgroup 和 namespace 為程式的執行創造一個隔離的環境,使得程式感知不到外界的存在,其本身仍然是跑在原有的核心上的;而虛擬機器則是通過 Hypervisor 模擬了一整套系統環境,虛擬機器裡的程式是跑在虛擬機器核心上的。由於虛擬機器需要模擬一整套作業系統環境,因此開銷比 Docker 容器要高很多很多。

  你可以把跑在容器裡的程式想象成楚門(楚門的世界男主),他並不知道自己生活在一個精心佈置的超大影棚裡,但是他仍然是活在現實世界裡的,呼吸著現實世界中的空氣,吃著和我們差不多的食物;跑在虛擬機器裡的程式就好像活在動畫片裡的小豬佩奇,他的一切都是虛擬的,雖然小豬佩奇並不知道自己活在動畫片裡,但是很顯然的是它和我們完全不在一個世界(不是同一個系統核心)。

  效能差異

特性 容器 虛擬機器
啟動 秒級 分鐘級
硬碟使用 一般為 MB 一般為 GB
效能 接近原生 弱於
系統支援量 單機上千個容器 一般幾十個

  Docker 基本概念

  映象

映象 是一個包含作業系統完整 root 檔案系統 的、只讀的,由多層檔案系統聯合而成的打包檔案。

  Docker 為了讓應用無感知的跑在容器中,提供了一套完整的 root 檔案系統,比如官方映象 library/ubuntu 就包含了一整套 root 檔案系統。像 apache、nginx 都是基於該映象構建的,由於 library/ubuntu 本身很大,所以 Docker 採用了分層儲存的方式。

  本文假裝你已經安裝了 Docker,上圖通過 docker pull nginx 從 官方 Registry(下面會提到這是啥)拉取 nginx 映象,拉取 nginx 相當於 library/nginx:latest,library 表示 nginx 是官方映象,因此可以省略,:latest 表示拉取標籤為 latest 的映象。拉取後可以看到存在兩個映象,因為 nginx 映象本身就是基於 library:ubuntu:16.04 映象的。

  上圖通過 docker pull httpd 拉取了 apache 映象,由於 ubuntu:16.04 映象已經在本地存在了,因此拉取的時候不會重複拉取。從而節約拉取時間。這就是 Docker 分層儲存的意義。

  映象的只讀可以理解成以前的光碟 CD,是不可更改的。為了模擬實現對光碟 CD 的寫的功能,會建立兩層檔案系統,一層是光碟 CD 的只讀檔案系統;另外一層是存放更改資料的可寫的檔案系統。從而實現模擬更改映象的作用。Docker 也是採用這種類似的分層的方式。

  如圖,可以看出 ubuntu:15.04 是由很多層檔案系統(映象)堆疊形成的,最底層是 root 檔案系統(d3a1f33e8a5a)。這幾層檔案系統都被設定成只讀的。多層檔案系統利用了上面提到的 UnionFS、AUFS、OverlayFS,這是一類檔案系統,這種聯合掛載檔案系統最早就是用於解決 CD 這種只讀檔案系統的修改問題,Docker 之前使用 AUFS,但是由於 AUFS 不被 linus 喜歡(被 linus 評價為稠密、不可讀,無註釋)導致 AUFS 一直沒有被合併到 Linux 的主分支中。Docker 在 1.12 以後已經將預設的檔案系統從 AUFS 替換成 OverlayFS2。因為 OverlayFS2 已經被合併進了 Linux 的主幹分支中。

  容器

  上面我們拉取了 nginx 映象到本地,我們可以使用 docker container start nginx(省略了 latest 標籤)來執行這個映象。執行之前會先建立一個容器(其實本質就是建立了一層可讀寫的檔案系統,以提供程式執行時的讀寫支援),然後就會啟動程式,讓程式跑在一個隔離環境(不是虛擬環境)裡。你還可以通過 docker container commit>來對當前層進行提交(就好像 Git 提交一樣),從而形成一個新的映象,但是這種方式是不推薦的;這是因為在程式執行過程中可能會產生一些垃圾檔案,而如果這些垃圾檔案被提交後,新的映象又是不可修改的,只會增大映象的體積。具體怎麼建立映象會在下面說到。

  可以看到上圖中在建立容器的時候其實就是建立了一個容器可讀寫層。你還可以通過 docker container stop<container ID> 停止容器的執行,相當於 kill 掉容器內的正在執行的程式,但是建立容器時建立的可讀寫的檔案系統依然存在。所以你依然可以通過 docker start <container ID> 來重啟程式。

  倉庫

  映象構建完成後,可以很容易的在宿主機器上執行,但是如果其他機器要使用這個映象,我們就需要一個集中儲存、分發映象的服務,Docker Registry 就是這樣的服務。一個 Docker Registry 可以包含多個倉庫,每個倉庫可以包含多個標籤,每個標籤對應一個映象。

  就拿上面的 library/nginx:latest 舉例,library 表示這個映象是官方映象,如果不是官方映象,這裡一般填註冊在 Docker Registry 的使用者名稱;library/nginx 是倉庫的名字,latest 是該倉庫一個標籤。

  誠然,官方的 Docker Registry 是世界上最大的映象分發服務,官方還提供了 Docker Registry 映象 用於搭建私有映象分發服務。而且 DockerHub 和社群一起製作了大量的、高質量的映象,使得我們構建映象更為方便。

 Docker 簡單實踐

  Docker 單個映象

  前面提到可以通過 docker commit 生成新的映象,但是這種方式並不推薦(原因已經說明),所以我們一般還是採用 Dockerfile 的方式。下面的實踐以 github-issue-rss 為例,demonstate how to containerization a normal project。

  首先建立一個 Dockerfile 檔案,內容如下:

FROM node:9-alpine

MAINTAINER mrcode "mrcodehang@outlook.com"

WORKDIR /src # 表示容器內的程式執行時的當前目錄

COPY . /src # 把構建 Dockerfile 檔案目錄下的檔案全部複製到映象的 /src 目錄下
RUN npm install -g yarn && yarn install # 構建時執行

EXPOSE 3000 # 暴露容器的 3000 埠到外面

ENTRYPOINT ["npm", "start"] # 執行 docker start <container ID> 時就會執行 npm start

  Dockerfile 裡的每一行開頭的大寫字母單詞叫做 Dockerfile 指令。每執行一條指令就會增加一層映象(本質是執行了一次 docker commit,而 AUFS 最大的層數是 127 層,因此 Dockerfile 裡的層數最好不要太多!

  FROM 表示基於哪一個映象構建,node:9-alpine 表示基於官方的 node 映象構建,標籤 9-alpine 表示這是一個 node 9 的映象,同時該 node9 映象是基於 alpine 映象構建的,alpine 是 Linux 的一個精簡發行版,大小隻有 5MB 左右,而 Ubuntu 映象大小接近 200MB。

  RUN 指令會在構建映象時執行,使用 && 符號是為了減少 RUN 命令的使用次數,減少最終映象的層數。

  EXPOSE 指令讓外界能通過容器的 3000 埠進行網路通訊。

  ENTRYPOINT 表示執行 docker start <container ID> 時就會執行 npm start(啟動程式);還可以寫成 ENTRYPOINT npm start 這種形式;然後就可以開始構建了。有的同學喜歡在 npm start 後加上 '&',來讓容器預設後臺執行;但這隻會導致容器無法啟動,因為容器本身的執行完全是依靠程式本身的程式的,當程式本身程式沒有掛載在 docker 容器上時,容器就會直接結束,容器結束後容器內的程式也被殺掉。所以要知道保持容器執行的正是容器內的程式本身!

  圖中執行命令最後有一個 '.',這是將當前目錄作為上下文傳遞給 Docker daemon;Docker 的工作方式是基於 C-S 架構的,你需要將構建的所在目錄傳給 docker daemon,這也是上面的 Dockerfile 檔案的 COPY 指令的當前目錄。

  接下來建立容器,一個映象可以建立多個容器(其實就是建立多個在同一層的讀寫層)。

  docker run 會拉取遠端的映象(如果本地沒有的話),接著它會建立一個容器,基於 mrcode/github-issue-rss:test 映象(只有 latest 標籤可以省略);-v 會建立一個資料卷(volume),表示當容器對 /var/log/github-issue-rss/ 寫入資料時相當於寫在了宿主機的 ~/github-issue-rss/log 目錄上,從而維持容器的無狀態特性(無狀態特性是指容器在執行時儘量不要將重要資料儲存在容器所在的讀寫層裡,雖然那是一層讀寫層,但是是用來存放程式執行時產生的臨時檔案的,不應將重要資料放在裡面);-d 表示 daemon 執行程式,否則的話容器程式會掛載在當前 shell 上,一般通過 -d 掛載到 docker daemon 程式上;—rm 表示容器退出後自動刪除容器,這是推薦的用法,也是容器的無狀態特性的體現。

容器程式具有和容器內程式本身程式相同的生命週期,容器程式用來啟動容器內程式,相當於 Linux 內的 init 程式;當容器內程式被 docker stop <container ID> 殺掉時,容器就會退出,留下一個已建立的讀寫層檔案系統,這也是容器存在的標誌。

  由於建立容器僅僅是建立了一個可讀寫的檔案系統,所以容器的存在是非常非常輕量級的。即便對一個映象建立多個容器,映象本身是不會被重新拷貝的,而是最大程度的複用,這是因為映象內的多層檔案系統的每一層都被設定成只讀的。

  你可以通過 docker container ls 檢視當前正在執行的所有容器,如果還想檢視已退出的容器,加上一個 -a 引數。使用 docker container start/stop 可以啟動/關閉容器。

  最後可以通過 docker push mrcode/github-issue-rss:test 釋出到 DockerHub 上,分享到社群。

  Docker 多個映象

  github-issue-rssis a tool converts the issues on GitHub to RSS.

  這個工具需要用到了 mysql,為了以後方便資料遷移,我決定使用 mysql 映象,mysql 映象可以把所有狀態存放在宿主機的一個資料夾下。那我現在不僅需要啟動 mysql 和 github-issue-rss 映象,還需要建立他們之間的網路連線關係,事情變得麻煩了。有一個工具叫 docker-compose (本文假裝你已經安裝了這個工具)可以把這一切自動化。下面是專案根目錄的一個 docker-compose.yml 檔案:

version: "3"
services:
    db:
        image: mysql:5.7
        volumes:
            - ~/.github-issue-rss/mysql:/var/lib/mysql
        restart: always
        environment:
            MYSQL_ROOT_PASSWORD: rootroot
            MYSQL_DATABASE: rss
            MYSQL_USER: mrcode
            MYSQL_PASSWORD: github-issue-rss
    github-issue-rss:
        image: mrcode/github-issue-rss:v0.1.0
        depends_on:
            - db
        ports:
            - "3000:3000"
        restart: always
        environment:
            MYSQL_PORT: 3306
            MYSQL_HOST: db
            MYSQL_SCHEMA: rss
            MYSQL_USERNAME: mrcode
            MYSQL_PASSWORD: github-issue-rss
            LOG_FILE: /var/log/github-issue-rss/
        volumes:
            - ~/.github-issue-rss/log/:/var/log/github-issue-rss/

  在 docker-compose 的世界裡沒有容器,只有服務。它認為它啟動了兩服務 db 和 github-issue-rss。沒有哪個是主服務,所有服務都是平等的。

  在 db service 中,設定了 volumes,將 mysql 的資料儲存在 ~/.github-issue-rss/mysql/ 裡,還可以設定更多的 volume。restart 表示只要服務執行失敗就重啟,防止依賴的 service 還沒有啟動完成時導致的錯誤引發連鎖反應。給兩個 service 配置的 environment 來建立兩者的資料連線,github-issue-rss 程式碼會讀取這個環境變數,然後連線到 db 服務,可以看到 github-issue-rss 裡的環境變數 MYSQL_HOST 設定為 db,這是因為 docker-compose 會在啟動的服務配置裡建立這個 DNS 對映關係。

  還可以通過 docker-compose down 來停止並且刪除服務對應的容器。

  現在你只需要克隆倉庫到本地,然後執行 docker-compose up 就可以啟動 github-issue-rss 了,因為 github-issue-rss 映象本身已經構建併發布到 Docker Hub 了。

 Docker 的應用

  持續整合和持續交付

  使用 Docker 可以通過定製應用映象來實現持續整合、持續交付、部署。開發人員通過 Dockerfile 進行映象構建,結合持續整合系統進行整合測試,而運維人員則可以在生產環境中快速部署該映象。甚至結合持續部署進行自動部署。

  而且使用 Dockerfile 使映象的構建透明化,不僅可以幫助開發人員理解應用執行環境,也方便運維團隊理解應用執行所需條件,幫助更好的生產環境中部署該映象。

  微服務

  Docker 和微服務架構簡直就是渾然天成,站在 Docker 的角度,軟體本質是容器的組合:業務邏輯容器、資料庫容器、儲存容器、佇列容器……Docker 使得軟體拆分成若干的標準化容器,然後像積木一樣的搭建起來。這正是微服務的思想:軟體把任務外包出去,讓各種外部服務完成這些任務,軟體本身只是底層服務的呼叫中心和組裝層。

相關文章