Docker入門簡介

kobejayandy發表於2018-06-30

Docker是什麼

Docker是一種虛擬化技術,類似虛擬機器,這使得安裝在其中的程式能夠只依賴虛擬機器的環境,而不受外部作業系統環境的影響。同虛擬機器不同的是,Docker的虛擬容器佔用空間更小,使得它比虛擬機器更容易分發和多例項安裝。

Docker容器化技術的整個開發使用方式非常類似java應用開發,這裡同java應用開發做一個類比,幫助有過java開發經驗的同學快速掌握其中的核心概念

Docker入門簡介

Dockerfile

相當於Java應用開發中的Maven配置檔案pom.xml或則gradle的build.gradle檔案。java開發中的pom.xml和build.gradle是用來宣告java應用依賴的jar包,和應用的構建方式。而Dockerfile是用來宣告一個程式依賴的環境和構建執行方式。比如redis的Dockerfile如下:

# 第一部分,宣告redis程式依賴系統環境,是使用的debian
FROM debian:stretch-slim

# 第二部分,配置系統許可權,新增新的組和使用者,專供redis使用
RUN groupadd -r redis && useradd -r -g redis redis

# 第三部分,是安裝系統更新,環境變數配置,以及下載redis並安裝
ENV GOSU_VERSION 1.10
RUN set -ex; \
	\
	fetchDeps=" \
		ca-certificates \
		dirmngr \
		gnupg \
		wget \
	"; \
	apt-get update; \
	apt-get install -y --no-install-recommends $fetchDeps; \
	rm -rf /var/lib/apt/lists/*; \
	\
	dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
	wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
	wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
	export GNUPGHOME="$(mktemp -d)"; \
	gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
	gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
	gpgconf --kill all; \
	rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc; \
	chmod +x /usr/local/bin/gosu; \
	gosu nobody true; \
	\
	apt-get purge -y --auto-remove $fetchDeps

ENV REDIS_VERSION 5.0.4
ENV REDIS_DOWNLOAD_URL http://download.redis.io/releases/redis-5.0.4.tar.gz
ENV REDIS_DOWNLOAD_SHA 3ce9ceff5a23f60913e1573f6dfcd4aa53b42d4a2789e28fa53ec2bd28c987dd

# for redis-sentinel see: http://redis.io/topics/sentinel
RUN set -ex; \
	\
	buildDeps=' \
		ca-certificates \
		wget \
		\
		gcc \
		libc6-dev \
		make \
	'; \
	apt-get update; \
	apt-get install -y $buildDeps --no-install-recommends; \
	rm -rf /var/lib/apt/lists/*; \
	\
	wget -O redis.tar.gz "$REDIS_DOWNLOAD_URL"; \
	echo "$REDIS_DOWNLOAD_SHA *redis.tar.gz" | sha256sum -c -; \
	mkdir -p /usr/src/redis; \
	tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1; \
	rm redis.tar.gz; \
	\

	grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 1$' /usr/src/redis/src/server.h; \
	sed -ri 's!^(#define CONFIG_DEFAULT_PROTECTED_MODE) 1$!\1 0!' /usr/src/redis/src/server.h; \
	grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 0$' /usr/src/redis/src/server.h; \
	\
	make -C /usr/src/redis -j "$(nproc)"; \
	make -C /usr/src/redis install; \
	\
	rm -r /usr/src/redis; \
	\
	apt-get purge -y --auto-remove $buildDeps
	
# 第四部分,設定redis後續命令的工作目錄
RUN mkdir /data && chown redis:redis /data
VOLUME /data
WORKDIR /data

#第五部分,啟動redis服務,並配置向外暴露的埠
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 6379
CMD ["redis-server"]
複製程式碼

可能每個不同的Docker程式,其Dockerfile略有不同,但大致都可以總結為這麼幾步

  • 宣告執行系統環境
  • 安裝系統更新,安裝程式
  • 配置環境變數
  • 設定向外暴露的埠,並啟動程式

image

相當於java應用開發中的jar包。java中基於pom.xml或build.gradle build而成jar。而docker中,基於Dockerfile build出的是image。它可以像jar包一樣,提交到Docker的中央倉庫,並被下發指其它機器使用。一個使用Dockerfile構建image的demo如下:

  1. 先用python開發一個簡單的web服務,名為app.py

     from flask import Flask
     from redis import Redis, RedisError
     import os
     import socket
     
     # Connect to Redis
     redis = Redis(host="redis", db=0, socket_connect_timeout=2, socket_timeout=2)
     
     app = Flask(__name__)
     
     @app.route("/")
     def hello():
         try:
             visits = redis.incr("counter")
         except RedisError:
             visits = "<i>cannot connect to Redis, counter disabled</i>"
     
         html = "<h3>Hello {name}!</h3>" \
                "<b>Hostname:</b> {hostname}<br/>" \
                "<b>Visits:</b> {visits}"
         return html.format(name=os.getenv("NAME", "world"), hostname=socket.gethostname(), visits=visits)
     
     if __name__ == "__main__":
         app.run(host='0.0.0.0', port=80)
    複製程式碼
  2. 再編寫Dockerfile

     # 從程式程式碼中,我們知道使用的python,需要依賴python的環境。python環境的image在docker公共倉庫中,可以直接使用,在這個image基礎上,新增我們的應用,構建另一個image
     FROM python:2.7-slim
     
     # 把容器看做一個小型作業系統的話,這一步設定後續命令在這個容器作業系統內的路徑。名字可以任意。相當於普通linux中的cd命令。路徑不存在應該可以直接建立。Dockerfile後續的所有命令,都是在這個資料夾下執行的
     WORKDIR /app1
     
     # 將宿主機的當前路徑內容拷貝到app1下
     COPY . /app1
     
     # 從app.py程式程式碼中,可以看到其依賴FLask庫環境和Redis,這裡通過pip安裝,這一步是在python image的內部執行的,不是外部環境。相當於再給python的image系統映象安裝東西
     RUN pip install --trusted-host pypi.python.org Flask
     RUN pip install --trusted-host pypi.python.org Redis
     
     # 將容器的80埠暴露出來。
     EXPOSE 80
     
     # 在容器內設定一個環境遍歷,key為NAME, value為world。就像linux中設定環境變數一樣。只不過這裡是在容器這個作業系統內設定環境變數,相應的容器中的程式可以讀取這個環境變數
     ENV NAME World
     
     # 這一步放在最後,前面的所有命令基本上把程式要求的環境都初始化好了,這裡直接執行命令,CMD的第一個引數是程式命令,後面的是引數。這裡就是通過python來run app.py。&emsp;由於當前路徑是/app1(前面WORKDIR設定的),並且其中包含app.py,所以在該路徑下執行python app.apy當然找得到程式檔案
     CMD ["python", "app.py"]
    複製程式碼
  3. 構建image 在宿主機上建立一個資料夾,名字任意,將Dockerfile和app.py 都放置其中(因為Dockerfile中有一個命令COPY . /app1,所以要確保程式跟Dockerfile在同一的路徑下,才可以拷貝進去。當然你可以不在一個路徑下,那就需要修改Dockerfile命令,將具體app.py的路徑寫全),然後在該路徑下執行構建命令構建image,並將其取名為hellworld

    docker build --tag=helloworld .

  4. 釋出image 你可以像釋出jar一樣,將image釋出到docker中央倉庫,或公司的私有倉庫,具體方式這裡就不展開討論了。

container

類似於java應用中的jar執行。我們基於image執行後,會建立一個執行的例項,即為container,容器。比如我們可以使用以下命令,通過前面build的image,建立一個container

docker run -p 4000:80 helloworld
複製程式碼

network

container需要對外進行通訊,可能需要網路服務。有5種網路驅動可供docker配置,用來配置docker的聯網行為。

  • bridge 橋接模式,通過鏈路層裝置連結host網路,它同host使用不同的ip,一般在單節點的host使用這種方式,預設是這種方式

  • host 模式,container直接跟host公用一個ip,這也意味這container暴露什麼埠,通過host的ip可直接訪問,不推薦這種方式

  • overlay docker叢集的網路連線驅動方式

  • Macvlan 對docker配置mac地址,通過實體地址進行網路通訊

  • none 使docker沒有任何網路連線

data volumes

container中的程式執行時,可能會產生一些資料,或者需要使用一些資料,甚至希望同其它container共享資料。那麼實現這些的方式就是data volumes,它對應docker的儲存概念,後續會詳細講解。

docker daemon

類似於Java虛擬機器。它負責image構建,分發,獲取,執行,以及container、volumes、network等上述核心元件的管理,遮蔽底層作業系統的細節,使得基於docker構建的服務能夠跨平臺。我們一般通過docker CLI也即docker命令列來向docker deamon傳送命令執行上述管理。

Docker入門簡介

Docker的基本使用方式

作為普通使用者大多數時候,我們只是從中央倉庫中獲取別人製作好的image,在本地建立container來提供服務,比如獲取mysql的image,在本地建立一個mysql的servers。所以下面主要介紹對container的一些核心操作命令。

獲取image

使用如下命令去遠端倉庫中拉取,image檔案

 docker pull IMAGE[:TAG]
複製程式碼

比如我們想要獲取redis的image,在中央倉庫中我們可以看到有很多redis的image,他們用不同的tag區分

Docker入門簡介
我們可以通過指定tag來拉取特定的image,比如我們拉取tag為5.0.4-alpine的image。docker pull redis:5.0.4-alpine

如何建立一個container

docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]
複製程式碼

其命令主幹是docker run IMAGE。每一次run,都會建立一個新的container

我們基於前面拉到的redis image啟動一個containerdocker run redis:5.0.4-alpine

可以在建立的時候指定許多引數,比如建立container時,指定名字docker run --name test-redis redis:5.0.4-alpine

將容器中的程式以後臺形式執行docker run --name some-redis -d redis

檢視docker相關元件

我們可以使用ls命令,來檢視docker中container,image,network,volume等元件的id和名字,就像linux中的ls命令一樣。

docker container ls #檢視正在執行的container
docker container ls --all #檢視包括已停止和執行中的所有container
docker image ls #檢視本地擁有的image
docker network ls#檢視當前系統具有的網路驅動
docker volume ls#檢視當前系統具有的volume儲存
複製程式碼

如何停止一個container

docker container stop CONTAINER_ID|CONTAINER_NAME
複製程式碼

可以使用container的id或name來將處在run中的container停止

如何啟動一個start

通過上述的ls命令,獲取到container的名字或id,然後通過命令docker container start CONTAINER_ID|CONTAINER_NAME來啟動容器,舉例docker container start 48b24d849908

如何檢視container中的程式執行日誌

我們可以將container當做一個小的linux系統。那啟動後如何登入?有兩種方式,第一種是attach命令到指定的容器,比如

sudo docker container attach 48b24d849908
複製程式碼

但這個命令是隻將當前的host終端attach到指定的container中正在執行的程式。並顯示其輸出。但並不能任意的瀏覽container的其他系統目錄。如果僅僅是為了看當前container中的執行程式日誌,大可不必用上述方法,直接用logs命令輸出即可(當然這種方式的能看到日誌的前提是,container中的程式將日誌輸出到了STDOUT或STDERR中才行)比如:

sudo docker container logs 48b24d849908
複製程式碼

如果嫌輸出的日誌太多,也可以管道加less慢慢看

sudo docker container logs 48b24d849908 | less
複製程式碼

想要正真的直接登入container去瀏覽其系統檔案,需要使用一下命令

sudo docker container exec -it 48b24d849908 /bin/bash
複製程式碼

當然這個要容器裡確實有bash程式才行。exec還可以run程式中的其他命令

如何啟動一個一次性的container

基於image建立一個container後,如果不主動刪除,那麼該container會一直存在,若以希望container被停止後,自動刪除。那麼可以在建立命令run中加引數--rm。例如:

docker run --rm --name some-redis -d redis
複製程式碼

如何讓容器自動重啟

有時我們希望宿主機在重啟後,或docker deamon重啟後,相應的container能自動重啟。那麼在建立container時,使用引數--restart來控制重啟行為。重啟策略主要有以下幾種

  • no 預設選項,不會自動重啟container

  • on-failure 當container非正常退出時,自動重啟

  • always 無論什麼情況都自動重啟。但手動停止容器後,需要docker daemon程式重啟時,才會重啟container,也即宿主機重啟時,會重啟container

  • unless-stopped 同always類似,但是手動停止的container不會在自動重啟。

舉例docker run -dit --restart unless-stopped redis

如何做埠對映

程式執行在container中。container又被docker deamon管理。所以需要將container中的程式暴露的埠,對映到宿主機自己的指定埠,否則外部程式無法直接同container通訊。可以在建立時指定引數-p來指定。例如:docker run -p 6379:6379/udp -p6379:6379 redis:5.0.4-alpine

其中冒號左邊為宿主機的埠,右邊為container中程式暴露的埠。斜槓後面指定暴露的埠型別是UDP還是TCP,如果是TCP可以不寫。

如何對映檔案系統

container中程式可能需要讀或寫一些資料,要使得這些資料能夠被宿主機可見,需要像埠對映一樣,將container中的檔案路徑對映到外部檔案系統中。這些外部的檔案系統可以是宿主機的檔案系統,也可是docker管理的volume。這裡以宿主機的檔案系統為例

docker run -v /home/v2ray_proxy:/etc/v2ray -p 1081:1081  v2ray/official  v2ray -config=/etc/v2ray/config.json
複製程式碼

將宿主機路徑/home/v2ray_proxy對映到container的/etc/v2ray路徑,這樣宿主機在/home/v2ray_proxy中修改的內容,container可以通過其/etc/v2ray路徑獲取到。反之亦然。

如何清理所有不使用的container、image、volume、network

可以使用rm命令,刪除指定id或name的相關元件。比如:

docker container rm CONTAINER_ID
docker image rm IMAGE_ID
docker volume rm VOLUME_ID
docker network rm NETWOKR_ID
複製程式碼

可能上述手動挨個刪太麻煩,你可以使用prune命令,直接將符合需求的元件全部刪除。比如:

docker image prune#刪除未被任何容器使用的image
docker container prune#刪除所有未啟動的container
docker volume prune#刪除所有未被使用的volume
docker network prune#刪除所有未被使用的網路
docker system prune#刪除所有未被使用的container,image ,volume, network。docker 1.7以上需要顯示執行`--volumes`引數,才能一併將volume也刪除,之所以這麼做是害怕一不小心把資料給刪了。多加引數增加了誤刪資料的門檻
複製程式碼

以上所有的刪除prune命令,都可以基於過濾條件來刪除。加引數--filter即可,比如刪除過去24小時未啟動的容器

docker container prune --filter "until=24h"
複製程式碼

如何檢視container的資源使用情況

使用命令docker stats

Layer

一個Dockerfile最終會被構建成image,一個image被run後會生成一個container。為了最大化共享儲存檔案,減少儲存空間的浪費,docker引入了層的概念layer. Docerkfile中RUN, COPY, ADD三個命令會產生layer

一個dockerfile中從上下到下的命令,反應到image上是由下到上的層,每一層都是基於上一層進行構建的。layer又分為image layer和container layer,前者是image構建時,每句dockerfile命令對應生成的layer,後者是通過image 生成一個新的container 時,container所獨有的read writer 層。

container的read writer layer是container的程式讀寫檔案時,檔案的儲存的層,它會隨著container的銷燬而銷燬。通常來說,container執行生成或修改檔案內容最好不要放到其read write layer,因為不方便cotnainer間共享,又容易影響container本身的讀寫效能,所以一般通過volume或bind mount的方式,將container讀寫的檔案內容對映掛載到外部。

比如,Dockerfile

FROM ubuntu:15.04
COPY . /app
RUN make /app
CMD python /app/app.py
複製程式碼
  • 第一句是基礎層,表示基於ubuntu15的image構建
  • 第二句在ubuntu15的基礎上,將宿主機當前路徑的內容拷貝到image的/app路徑做為新的layer
  • 第三句,使用make命令,將/app中的檔案進行編譯,生成的內容為新的layer
  • 第四句,使用python命令執行上一步build的可執行檔案app.py,其對應container中的R/W layer

其對應的image層的示例為:

Docker入門簡介
多個container公用image layer的示例:

Docker入門簡介

檔案系統

Docker中的任何資料的產生,預設都是儲存在了container的write layer,這帶來了以下一些問題:

  • 不方便備份和訪問,因為資料在容器裡面
  • 資料易丟失,當容器被刪除後,資料也跟著被刪除
  • 不方便程式更新,容器跟資料繫結了,這個時候你想通過更新的image,建立新的容器來達到升級程式的目的變得很難,因為你要丟資料

為了解決這些問題,Docker提出資料更容器分離的理念。以掛載的路徑來區分,有以下三種掛載方式

  • volume mount 受docker deamon管理的檔案系統
  • bind mount 當前宿主機的檔案系統
  • tmpfs mount 記憶體

Docker入門簡介

Volume mount

建立volume的幾種方式

  1. 直接用volume命令建立例如docker volume create my-vol

  2. 在建立一個container時或service時,通過引數-v或者--mount掛載volume時,volume不存在,也會自動建立。舉例如下:

     //volume名為myvol2,掛載到container的指定目錄為/app
     $ docker run -d \
       --name devtest \
       -v myvol2:/app \
       nginx:latest
    
    
     //建立四個nginx container組成的service
     $ docker service create -d \
       --replicas=4 \
       --name devtest-service \
       --mount source=myvol2,target=/app \
       nginx:latest
    複製程式碼

-v--mount 這兩個都能指定掛載的volume(如果不存在,都會建立),建立service時,只能使用mount命令。-v引數後面直接指定所有的配置value不直觀,--mount的配置,則是以key=value的形式體現,能夠清楚的知道指定配置項意義。能通過他們配置的資訊有:

  • source container外的宿主檔案系統(bind mount時,source就是宿主的檔案路徑)或volume
  • destination path: container 內的指定路徑
  • 讀寫模式:對掛載的宿主檔案路徑或volume是否有讀寫的權利
  • driver: 如果掛載到container的是volume時,配置該volume的驅動型別。volume的驅動型別預設是local,也即宿主機所在檔案系統。但有些volume對應的儲存可能是aws,所以其驅動就不是local.

使用-v引數的大概形式為:

-v <source>:<destination>
//其中source可以忽略,忽略時,預設建立一個匿名的volume
複製程式碼

使用--mount引數的大概形式為:

     --mount 'type=volume,src=<VOLUME-NAME>,dst=<CONTAINER-PATH>,volume-driver=local,volume-opt=type=nfs,volume-opt=device=<nfs-server>:<nfs-path>'
     //其中src可以寫為source
     //dst 可以寫為destination或target
複製程式碼

像volume中填充內容 如果一個空的volume掛載到指定的container目錄,並且該目錄下已經有內容,那麼這些內容會自動被複制到volume下。舉例如下;

$ docker run -d \
  --name=nginxtest \
  --mount source=nginx-vol,destination=/usr/share/nginx/html \
  nginx:latest
//名為nginx-vol的volume,裡面會被拷貝進/usr/share/nginx/html檔案
複製程式碼

bind mount

volume是掛載一個由docker 守護程式管理的檔案系統到container。而bind mount是直接掛載宿主機的任意檔案路徑到container。這樣宿主機其他程式該掛載路徑下的檔案內容,container也會感受到,反之亦然。其掛載命令跟volume差不讀,不再贅述,只是其mount的type為bind。

簡單總結來看,希望容器間相互共享內容,使用volume掛載到container 希望容器和宿主機之間相互共享內容,使用bind mount

tmpfs mount

tmpfs是將容器指定路徑對映到記憶體,這樣當容器對指定路徑寫資料時,不會寫到容器自己的write layer。並且tmpfs不能被容器共享,即A容器mount 的tmpfs,不能被B容器讀到,這就使得tmpfs非常適合儲存一些易失的,且容器獨有的私密資訊。

tmpfs只能在linux的docker中使用

tmpfs的掛載也有兩種引數方式,一是--tmpfs,二是--mount,前者不能指定任何引數,後者則可以,後者的功能和工作範圍都比較廣。

volume和bind在掛載時,需要指定一個source,而tmpfs的掛載不需要,只用指定掛載到對應contaienr的路徑即可。

後話

容器化使得部署應用變得簡單方便。docker還提供了swarm,使得服務以叢集化形式編排和部署同樣變得簡單。這裡不再詳述。

使用容器化提供服務時,需要遵循微服務化的原則,保持服務的原子性,即一個container只提供一種服務。這樣更加方便後期管理和程式擴充套件。

參考資料

docs.docker.com/

相關文章