Docker容器實戰(zhàn)(七) - 容器眼光下的文件系統(tǒng)

前兩文中,講了Linux容器最基礎(chǔ)的兩種技術(shù)

  • Namespace
    作用是“隔離”,它讓應(yīng)用進(jìn)程只能看到該Namespace內(nèi)的“世界”
  • Cgroups
    作用是“限制”,它給這個“世界”圍上了一圈看不見的墻

這么一搞,進(jìn)程就真的被“裝”在了一個與世隔絕的房間里,而這些房間就是PaaS項目賴以生存的應(yīng)用“沙盒”。

還有一個問題是:墻外的我們知道他的處境了,墻內(nèi)的他呢?

1 容器里的進(jìn)程眼中的文件系統(tǒng)

也許你會認(rèn)為這是一個關(guān)于Mount Namespace的問題
容器里的應(yīng)用進(jìn)程,理應(yīng)看到一份完全獨立的文件系統(tǒng)。這樣,它就可以在自己的容器目錄(比如/tmp)下進(jìn)行操作,而完全不會受宿主機(jī)以及其他容器的影響。

那么,真實情況是這樣嗎?

“左耳朵耗子”叔在多年前寫的一篇關(guān)于Docker基礎(chǔ)知識的博客里,曾經(jīng)介紹過一段小程序。
這段小程序的作用是,在創(chuàng)建子進(jìn)程時開啟指定的Namespace。

下面,我們不妨使用它來驗證一下剛剛提到的問題。

#define _GNU_SOURCE
#include <sys/mount.h> 
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];
char* const container_args[] = {
  "/bin/bash",
  NULL
};

int container_main(void* arg)
{  
  printf("Container - inside the container!\n");
  execv(container_args[0], container_args);
  printf("Something's wrong!\n");
  return 1;
}

int main()
{
  printf("Parent - start a container!\n");
  int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWNS | SIGCHLD , NULL);
  waitpid(container_pid, NULL, 0);
  printf("Parent - container stopped!\n");
  return 0;
}

在main函數(shù)里,通過clone()系統(tǒng)調(diào)用創(chuàng)建了一個新的子進(jìn)程container_main,并且聲明要為它啟用Mount Namespace(即:CLONE_NEWNS標(biāo)志)。

而這個子進(jìn)程執(zhí)行的,是一個“/bin/bash”程序,也就是一個shell。所以這個shell就運行在了Mount Namespace的隔離環(huán)境中。

我們來一起編譯一下這個程序:


image

這樣,我們就進(jìn)入了這個“容器”當(dāng)中??墒?,如果在“容器”里執(zhí)行一下ls指令的話,我們就會發(fā)現(xiàn)一個有趣的現(xiàn)象: /tmp目錄下的內(nèi)容跟宿主機(jī)的內(nèi)容是一樣的。


image

即使開啟了Mount Namespace,容器進(jìn)程看到的文件系統(tǒng)也跟宿主機(jī)完全一樣。
這是怎么回事呢?

Mount Namespace修改的,是容器進(jìn)程對文件系統(tǒng)“掛載點”的認(rèn)知
但是,這也就意味著,只有在“掛載”這個操作發(fā)生之后,進(jìn)程的視圖才會被改變。而在此之前,新創(chuàng)建的容器會直接繼承宿主機(jī)的各個掛載點。

這時,你可能已經(jīng)想到了一個解決辦法:創(chuàng)建新進(jìn)程時,除了聲明要啟用Mount Namespace之外,我們還可以告訴容器進(jìn)程,有哪些目錄需要重新掛載,就比如這個/tmp目錄。于是,我們在容器進(jìn)程執(zhí)行前可以添加一步重新掛載 /tmp目錄的操作:

int container_main(void* arg)
{
  printf("Container - inside the container!\n");
  // 如果你的機(jī)器的根目錄的掛載類型是shared,那必須先重新掛載根目錄
  // mount("", "/", NULL, MS_PRIVATE, "");
  mount("none", "/tmp", "tmpfs", 0, "");
  execv(container_args[0], container_args);
  printf("Something's wrong!\n");
  return 1;
}

可以看到,在修改后的代碼里,我在容器進(jìn)程啟動之前,加上了一句mount(“none”, “/tmp”, “tmpfs”, 0, “”)語句。就這樣,我告訴了容器以tmpfs(內(nèi)存盤)格式,重新掛載了/tmp目錄。

這段修改后的代碼,編譯執(zhí)行后的結(jié)果又如何呢?我們可以試驗一下:


image

可以看到,這次/tmp變成了一個空目錄,這意味著重新掛載生效了。我們可以用mount -l檢查一下:

image

可以看到,容器里的/tmp目錄是以tmpfs方式單獨掛載的。

更重要的是,因為我們創(chuàng)建的新進(jìn)程啟用了Mount Namespace,所以這次重新掛載的操作,只在容器進(jìn)程的Mount Namespace中有效。如果在宿主機(jī)上用mount -l來檢查一下這個掛載,你會發(fā)現(xiàn)它是不存在的:


image

這就是Mount Namespace跟其他Namespace的使用略有不同的地方:

它對容器進(jìn)程視圖的改變,一定是伴隨著掛載操作(mount)才能生效。

可作為用戶,希望每當(dāng)創(chuàng)建一個新容器,容器進(jìn)程看到的文件系統(tǒng)就是一個獨立的隔離環(huán)境,而不是繼承自宿主機(jī)的文件系統(tǒng)。怎么才能做到這一點呢?

可以在容器進(jìn)程啟動之前重新掛載它的整個根目錄“/”。
而由于Mount Namespace的存在,這個掛載對宿主機(jī)不可見,所以容器進(jìn)程就可以在里面隨便折騰了。

在Linux操作系統(tǒng)里,有一個名為

chroot(change root file system)

的命令, 改變進(jìn)程的根目錄到指定的位置

假設(shè),我們現(xiàn)在有一個$HOME/test目錄,想要把它作為一個/bin/bash進(jìn)程的根目錄。

  • 首先,創(chuàng)建一個test目錄和幾個lib文件夾:
$ mkdir -p $HOME/test
$ mkdir -p $HOME/test/{bin,lib64,lib}
$ cd $T
  • 然后,把bash命令拷貝到test目錄對應(yīng)的bin路徑下:
$ cp -v /bin/{bash,ls} $HOME/test/bin

接下來,把bash命令需要的所有so文件,也拷貝到test目錄對應(yīng)的lib路徑下。找到so文件可以用ldd 命令:

$ T=$HOME/test
$ list="$(ldd /bin/ls | egrep -o '/lib.*\.[0-9]')"
$ for i in $list; do cp -v "$i" "${T}${i}"; done

最后,執(zhí)行chroot命令,告訴操作系統(tǒng),我們將使用$HOME/test目錄作為/bin/bash進(jìn)程的根目錄:

$ chroot $HOME/test /bin/bash

這時,你如果執(zhí)行ls /,就會看到,它返回的都是$HOME/test目錄下面的內(nèi)容,而不是宿主機(jī)的內(nèi)容。

更重要的是,對于被chroot的進(jìn)程來說,它并不會感受到自己的根目錄已經(jīng)被“修改”成$HOME/test了。

這種視圖被修改的原理,是不是跟我之前介紹的Linux Namespace很類似呢?
沒錯!實際上,Mount Namespace正是基于對chroot的不斷改良才被發(fā)明出來的,它也是Linux操作系統(tǒng)里的第一個Namespace。

當(dāng)然,為了能夠讓容器的這個根目錄看起來更“真實”,我們一般會在這個容器的根目錄下掛載一個完整操作系統(tǒng)的文件系統(tǒng), 比如Ubuntu16.04的ISO。這樣,在容器啟動之后,我們在容器里通過執(zhí)行"ls /"查看根目錄下的內(nèi)容,就是Ubuntu 16.04的所有目錄和文件。
而這個掛載在容器根目錄上、用來為容器進(jìn)程提供隔離后執(zhí)行環(huán)境的文件系統(tǒng),就是所謂的“容器鏡像”。它還有一個更為專業(yè)的名字,叫作:rootfs(根文件系統(tǒng))。

所以,一個最常見的rootfs,或者說容器鏡像,會包括如下所示的一些目錄和文件,比如/bin,/etc,/proc等等:

$ ls /
bin dev etc home lib lib64 mnt opt proc root run sbin sys tmp usr var

而你進(jìn)入容器之后執(zhí)行的/bin/bash,就是/bin目錄下的可執(zhí)行文件,與宿主機(jī)的/bin/bash完全不同。
對Docker項目來說,它最核心的原理實際上就是為待創(chuàng)建的用戶進(jìn)程:

  • 啟用Linux Namespace配置
  • 設(shè)置指定的Cgroups參數(shù)
  • 切換進(jìn)程的根目錄(Change Root)

Docker項目在最后一步的切換上會優(yōu)先使用pivot_root系統(tǒng)調(diào)用,如果系統(tǒng)不支持,才會使用chroot

這兩個系統(tǒng)調(diào)用雖然功能類似,但是也有細(xì)微的區(qū)別

rootfs只是一個操作系統(tǒng)所包含的文件、配置和目錄,并不包括操作系統(tǒng)內(nèi)核。只包括了操作系統(tǒng)的“軀殼”,并沒有包括操作系統(tǒng)的“靈魂”。
在Linux操作系統(tǒng)中,這兩部分是分開存放的,操作系統(tǒng)只有在開機(jī)啟動時才會加載指定版本的內(nèi)核鏡像。

那么,對于容器來說,這個

操作系統(tǒng)的“靈魂”在哪

同一臺機(jī)器上的所有容器,都共享宿主機(jī)操作系統(tǒng)的內(nèi)核。
如果你的應(yīng)用程序需要配置內(nèi)核參數(shù)、加載額外的內(nèi)核模塊,以及跟內(nèi)核進(jìn)行直接的交互
這些操作和依賴的對象,都是宿主機(jī)操作系統(tǒng)的內(nèi)核,它對于該機(jī)器上的所有容器來說是一個“全局變量”,牽一發(fā)動全身。

這也是容器相比于虛擬機(jī)的主要缺陷之一
畢竟后者不僅有模擬出來的硬件機(jī)器充當(dāng)沙盒,而且每個沙盒里還運行著一個完整的Guest OS給應(yīng)用隨便折騰。

不過,正是由于rootfs的存在,容器才有了一個被反復(fù)宣傳至今的重要特性:

一致性

什么是容器的“一致性”呢?

由于云端與本地服務(wù)器環(huán)境不同,應(yīng)用的打包過程,一直是使用PaaS時最“痛苦”的一個步驟。
但有了容器鏡像(即rootfs)之后,這個問題被非常優(yōu)雅地解決了。
由于rootfs里打包的不只是應(yīng)用,而是整個操作系統(tǒng)的文件和目錄,也就意味著,應(yīng)用以及它運行所需要的所有依賴,都被封裝在了一起。

事實上,對于大多數(shù)開發(fā)者而言,他們對應(yīng)用依賴的理解,一直局限在編程語言層面。比如Golang的Godeps.json。
但實際上,一個一直以來很容易被忽視的事實是,對一個應(yīng)用來說,操作系統(tǒng)本身才是它運行所需要的最完整的“依賴庫”。

有了容器鏡像“打包操作系統(tǒng)”的能力,這個最基礎(chǔ)的依賴環(huán)境也終于變成了應(yīng)用沙盒的一部分。這就賦予了容器所謂的一致性
無論在本地、云端,還是在一臺任何地方的機(jī)器上,用戶只需要解壓打包好的容器鏡像,那么這個應(yīng)用運行所需要的完整的執(zhí)行環(huán)境就被重現(xiàn)出來了。

這種深入到操作系統(tǒng)級別的運行環(huán)境一致性,打通了應(yīng)用在本地開發(fā)和遠(yuǎn)端執(zhí)行環(huán)境之間難以逾越的鴻溝。
不過,這時你可能已經(jīng)發(fā)現(xiàn)了另一個非常棘手的問題:難道我每開發(fā)一個應(yīng)用,或者升級一下現(xiàn)有的應(yīng)用,都要重復(fù)制作一次rootfs嗎?
比如,我現(xiàn)在用Ubuntu操作系統(tǒng)的ISO做了一個rootfs,然后又在里面安裝了Java環(huán)境,用來部署應(yīng)用。那么,我的另一個同事在發(fā)布他的Java應(yīng)用時,顯然希望能夠直接使用我安裝過Java環(huán)境的rootfs,而不是重復(fù)這個流程。

一種比較直觀的解決辦法是,我在制作rootfs的時候,每做一步“有意義”的操作,就保存一個rootfs出來,這樣其他同事就可以按需求去用他需要的rootfs了。
但是,這個解決辦法并不具備推廣性。原因在于,一旦你的同事們修改了這個rootfs,新舊兩個rootfs之間就沒有任何關(guān)系了。這樣做的結(jié)果就是極度的碎片化。
那么,既然這些修改都基于一個舊的rootfs,我們能不能以增量的方式去做這些修改呢?
這樣做的好處是,所有人都只需要維護(hù)相對于base rootfs修改的增量內(nèi)容,而不是每次修改都制造一個“fork”。
答案當(dāng)然是肯定的。

這也正是為何,Docker公司在實現(xiàn)Docker鏡像時并沒有沿用以前制作rootfs的標(biāo)準(zhǔn)流程,而是做了一個小小的創(chuàng)新:
Docker在鏡像的設(shè)計中,引入了層(layer)的概念。也就是說,用戶制作鏡像的每一步操作,都會生成一個層,也就是一個增量rootfs。
當(dāng)然,這個想法不是憑空臆造出來的,而是用到

聯(lián)合文件系統(tǒng)(Union File System)

UnionFS,最主要的功能是將多個不同位置的目錄聯(lián)合掛載(union mount)到同一個目錄下。比如,我現(xiàn)在有兩個目錄A和B,它們分別有兩個文件:

$ tree
.
├── A
│  ├── a
│  └── x
└── B
  ├── b
  └── x

然后,我使用聯(lián)合掛載的方式,將這兩個目錄掛載到一個公共的目錄C上:

$ mkdir C
$ mount -t aufs -o dirs=./A:./B none ./C

這時,我再查看目錄C的內(nèi)容,就能看到目錄A和B下的文件被合并到了一起:

$ tree ./C
./C
├── a
├── b
└── x

可以看到,在這個合并后的目錄C里,有a、b、x三個文件,并且x文件只有一份。這,就是“合并”的含義。此外,如果你在目錄C里對a、b、x文件做修改,這些修改也會在對應(yīng)的目錄A、B中生效。

我的環(huán)境是Ubuntu 16.04和Docker CE 18.05,這對組合默認(rèn)使用的是AuFS這個聯(lián)合文件系統(tǒng)的實現(xiàn)。
可以通過docker info命令,查看到這個信息。

AuFS的全稱是Another UnionFS,后改名為Alternative UnionFS,再后來干脆改名叫作Advance UnionFS,從這些名字中你應(yīng)該能看出這樣兩個事實:

  • 對Linux原生UnionFS的重寫和改進(jìn)
  • 它的作者怨氣好像很大。我猜是Linus Torvalds(Linux之父)一直不讓AuFS進(jìn)入Linux內(nèi)核主干的緣故,所以我們只能在Ubuntu和Debian這些發(fā)行版上使用它。

對于AuFS來說,它最關(guān)鍵的目錄結(jié)構(gòu)在/var/lib/docker路徑下的diff目錄:

/var/lib/docker/aufs/diff/<layer_id>

現(xiàn)在,我們啟動一個容器,比如:

$ docker run -d ubuntu:latest sleep 3600

這時候,Docker就會從Docker Hub上拉取一個Ubuntu鏡像到本地。

這個所謂的“鏡像”,實際上就是一個Ubuntu操作系統(tǒng)的rootfs,內(nèi)容是Ubuntu操作系統(tǒng)的所有文件和目錄。
不過,與之前我們講述的rootfs稍微不同的是,Docker鏡像使用的rootfs,往往由多個“層”組成:

$ docker image inspect ubuntu:latest
...
     "RootFS": {
      "Type": "layers",
      "Layers": [
        "sha256:f49017d4d5ce9c0f544c...",
        "sha256:8f2b771487e9d6354080...",
        "sha256:ccd4d61916aaa2159429...",
        "sha256:c01d74f99de40e097c73...",
        "sha256:268a067217b5fe78e000..."
      ]
    }

可以看到,這個Ubuntu鏡像,實際上由五個層組成。
這五個層就是五個增量rootfs,每一層都是Ubuntu操作系統(tǒng)文件與目錄的一部分;而在使用鏡像時,Docker會把這些增量聯(lián)合掛載在一個統(tǒng)一的掛載點上(等價于前面例子里的“/C”目錄)。

這個掛載點就是/var/lib/docker/aufs/mnt/,比如:

/var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fcfa2a2f5c89dc21ee30e166be823ceaeba15dce645b3e

不出意外的,這個目錄里面正是一個完整的Ubuntu操作系統(tǒng):

$ ls /var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fcfa2a2f5c89dc21ee30e166be823ceaeba15dce645b3e
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

那么,前面提到的五個鏡像層,又是如何被聯(lián)合掛載成這樣一個完整的Ubuntu文件系統(tǒng)的呢?

這個信息記錄在AuFS的系統(tǒng)目錄/sys/fs/aufs下面。

首先,通過查看AuFS的掛載信息,我們可以找到這個目錄對應(yīng)的AuFS的內(nèi)部ID(也叫:si):

$ cat /proc/mounts| grep aufs
none /var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fc... aufs rw,relatime,si=972c6d361e6b32ba,dio,dirperm1 0 0

即,si=972c6d361e6b32ba。

然后使用這個ID,你就可以在/sys/fs/aufs下查看被聯(lián)合掛載在一起的各個層的信息:

$ cat /sys/fs/aufs/si_972c6d361e6b32ba/br[0-9]*
/var/lib/docker/aufs/diff/6e3be5d2ecccae7cc...=rw
/var/lib/docker/aufs/diff/6e3be5d2ecccae7cc...-init=ro+wh
/var/lib/docker/aufs/diff/32e8e20064858c0f2...=ro+wh
/var/lib/docker/aufs/diff/2b8858809bce62e62...=ro+wh
/var/lib/docker/aufs/diff/20707dce8efc0d267...=ro+wh
/var/lib/docker/aufs/diff/72b0744e06247c7d0...=ro+wh
/var/lib/docker/aufs/diff/a524a729adadedb90...=ro+wh

從這些信息里,我們可以看到,鏡像的層都放置在/var/lib/docker/aufs/diff目錄下,然后被聯(lián)合掛載在/var/lib/docker/aufs/mnt里面。

分層

而且,從這個結(jié)構(gòu)可以看出來,這個容器的rootfs由如下圖所示的三部分組成:

image

只讀層

容器的rootfs最下面的五層,對應(yīng)的正是ubuntu:latest鏡像的五層。
它們的掛載方式都是只讀的(ro+wh,即readonly+whiteout)

這時,我們可以分別查看一下這些層的內(nèi)容:

$ ls /var/lib/docker/aufs/diff/72b0744e06247c7d0...
etc sbin usr var
$ ls /var/lib/docker/aufs/diff/32e8e20064858c0f2...
run
$ ls /var/lib/docker/aufs/diff/a524a729adadedb900...
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

可以看到,這些層,都以增量的方式分別包含了Ubuntu操作系統(tǒng)的一部分。

可讀寫層

容器的rootfs最上面的一層(6e3be5d2ecccae7cc),它的掛載方式為:rw
在沒有寫入文件之前,這個目錄是空的。而一旦在容器里做了寫操作,你修改產(chǎn)生的內(nèi)容就會以增量的方式出現(xiàn)在這個層中。

如果我現(xiàn)在要做的,是刪除只讀層里的一個文件呢?
為了實現(xiàn)這樣的刪除操作,AuFS會在可讀寫層創(chuàng)建一個whiteout文件,把只讀層里的文件“遮擋”起來。
比如,你要刪除只讀層里一個名叫foo的文件,那么這個刪除操作實際上是在可讀寫層創(chuàng)建了一個名叫.wh.foo的文件。這樣,當(dāng)這兩個層被聯(lián)合掛載之后,foo文件就會被.wh.foo文件“遮擋”起來,“消失”了。這個功能,就是“ro+wh”的掛載方式,即只讀+whiteout的含義。我喜歡把whiteout形象地翻譯為:“白障”。

所以,最上面這個可讀寫層的作用,就是專門用來存放你修改rootfs后產(chǎn)生的增量,無論是增、刪、改,都發(fā)生在這里。而當(dāng)我們使用完了這個被修改過的容器之后,還可以使用docker commit和push指令,保存這個被修改過的可讀寫層,并上傳到Docker Hub上,供其他人使用;而與此同時,原先的只讀層里的內(nèi)容則不會有任何變化。這,就是增量rootfs的好處。

Init層

它是一個以“-init”結(jié)尾的層,夾在只讀層和讀寫層之間
Init層是Docker項目單獨生成的一個內(nèi)部層,專門用來存放/etc/hosts、/etc/resolv.conf等信息。

需要這樣一層的原因是,這些文件本來屬于只讀的Ubuntu鏡像的一部分,但是用戶往往需要在啟動容器時寫入一些指定的值比如hostname,所以就需要在可讀寫層對它們進(jìn)行修改。

可是,這些修改往往只對當(dāng)前的容器有效,我們并不希望執(zhí)行docker commit時,把這些信息連同可讀寫層一起提交掉。
所以,Docker做法是,在修改了這些文件之后,以一個單獨的層掛載了出來。而用戶執(zhí)行docker commit只會提交可讀寫層,所以是不包含這些內(nèi)容的。

最終,這7個層都被聯(lián)合掛載到/var/lib/docker/aufs/mnt目錄下,表現(xiàn)為一個完整的Ubuntu操作系統(tǒng)供容器使用。

總結(jié)

本文介紹了Linux容器文件系統(tǒng)的實現(xiàn)方式。即容器鏡像,也叫作:rootfs。
它只是一個操作系統(tǒng)的所有文件和目錄,并不包含內(nèi)核,最多也就幾百兆。而相比之下,傳統(tǒng)虛擬機(jī)的鏡像大多是一個磁盤的“快照”,磁盤有多大,鏡像就至少有多大。

通過結(jié)合使用Mount Namespacerootfs,容器就能夠為進(jìn)程構(gòu)建出一個完善的文件系統(tǒng)隔離環(huán)境。當(dāng)然,這個功能的實現(xiàn)還必須感謝chrootpivot_root這兩個系統(tǒng)調(diào)用切換進(jìn)程根目錄的能力。

而在rootfs的基礎(chǔ)上,Docker公司創(chuàng)新性地提出了使用多個增量rootfs聯(lián)合掛載一個完整rootfs的方案,這就是容器鏡像中“層”的概念。

通過“分層鏡像”的設(shè)計,以Docker鏡像為核心,來自不同公司、不同團(tuán)隊的技術(shù)人員被緊密地聯(lián)系在了一起。而且,由于容器鏡像的操作是增量式的,這樣每次鏡像拉取、推送的內(nèi)容,比原本多個完整的操作系統(tǒng)的大小要小得多;
而共享層的存在,可以使得所有這些容器鏡像需要的總空間,也比每個鏡像的總和要小。
這樣就使得基于容器鏡像的團(tuán)隊協(xié)作,要比基于動則幾個GB的虛擬機(jī)磁盤鏡像的協(xié)作要敏捷得多。

更重要的是,一旦這個鏡像被發(fā)布,那么你在全世界的任何一個地方下載這個鏡像,得到的內(nèi)容都完全一致,可以完全復(fù)現(xiàn)這個鏡像制作者當(dāng)初的完整環(huán)境。這,就是容器技術(shù)“強(qiáng)一致性”的重要體現(xiàn)。

而這種價值正是支撐Docker公司在2014~2016年間迅猛發(fā)展的核心動力。容器鏡像的發(fā)明,不僅打通了“開發(fā)-測試-部署”流程的每一個環(huán)節(jié),更重要的是:

容器鏡像將會成為未來軟件的主流發(fā)布方式。

參考

深入剖析Kubernetes

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • docker常用命令原理圖概覽: 按照docker官網(wǎng)上的說法,docker的文件系統(tǒng)分為兩層:bootfs和ro...
    燕京博士閱讀 3,005評論 2 32
  • Docker容器技術(shù)已經(jīng)發(fā)展了好些年,在很多項目都有應(yīng)用,線上運行也很穩(wěn)定。整理了部分Docker的學(xué)習(xí)筆記以及新...
    __七把刀__閱讀 11,480評論 0 58
  • 【Day5】今日閱讀《非暴力溝通》P164--P190 成長過程中我們接受了很多無益的知識和一些不良的積習(xí),滲透到...
    橘子669閱讀 328評論 1 1
  • 人不狠話不多,就一句,好記性不如爛筆頭。 在用vscode打開vue項目的時候,通常會這樣 然后每次都要去右下角的...
    芒果大餅閱讀 8,722評論 0 5
  • 是日出還是日落? 判斷日出日落沒有想象的那么簡單,一般說來如果熟悉拍攝的地點,可以根據(jù)光線的方向來判斷,如果不熟悉...
    單福福閱讀 453評論 0 0