Docker技術(shù)原理之Linux UnionFS(容器鏡像)

0.前言

前面我們討論了Docker容器實現(xiàn)隔離和資源限制用到的技術(shù)Linux namespace 、Linux CGroups,本篇我們來討論Docker容器鏡像用到的技術(shù)UnionFS缓熟。

1.關(guān)于UnionFS

1)什么是UnionFS

聯(lián)合文件系統(tǒng)(Union File System):2004年由紐約州立大學(xué)石溪分校開發(fā)移稳,它可以把多個目錄(也叫分支)內(nèi)容聯(lián)合掛載到同一個目錄下,而目錄的物理位置是分開的屎飘。UnionFS允許只讀和可讀寫目錄并存妥曲,就是說可同時刪除和增加內(nèi)容。UnionFS應(yīng)用的地方很多钦购,比如在多個磁盤分區(qū)上合并不同文件系統(tǒng)的主目錄檐盟,或把幾張CD光盤合并成一個統(tǒng)一的光盤目錄(歸檔)。另外押桃,具有寫時復(fù)制(copy-on-write)功能UnionFS可以把只讀和可讀寫文件系統(tǒng)合并在一起葵萎,虛擬上允許只讀文件系統(tǒng)的修改可以保存到可寫文件系統(tǒng)當中。

2)docker的鏡像rootfs唱凯,和layer的設(shè)計

任何程序運行時都會有依賴羡忘,無論是開發(fā)語言層的依賴庫,還是各種系統(tǒng)lib磕昼、操作系統(tǒng)等卷雕,不同的系統(tǒng)上這些庫可能是不一樣的,或者有缺失的票从。為了讓容器運行時一致漫雕,docker將依賴的操作系統(tǒng)、各種lib依賴整合打包在一起(即鏡像)峰鄙,然后容器啟動時浸间,作為它的根目錄(根文件系統(tǒng)rootfs),使得容器進程的各種依賴調(diào)用都在這個根目錄里吟榴,這樣就做到了環(huán)境的一致性魁蒜。

不過,這時你可能已經(jīng)發(fā)現(xiàn)了另一個問題:難道每開發(fā)一個應(yīng)用吩翻,都要重復(fù)制作一次rootfs嗎(那每次pull/push一個系統(tǒng)豈不瘋掉)兜看?

比如,我現(xiàn)在用Debian操作系統(tǒng)的ISO做了一個rootfs仿野,然后又在里面安裝了Golang環(huán)境铣减,用來部署我的應(yīng)用A。那么脚作,我的另一個同事在發(fā)布他的Golang應(yīng)用B時葫哗,希望能夠直接使用我安裝過Golang環(huán)境的rootfs缔刹,而不是重復(fù)這個流程,那么本文的主角UnionFS就派上用場了劣针。

Docker鏡像的設(shè)計中校镐,引入了層(layer)的概念,也就是說捺典,用戶制作鏡像的每一步操作鸟廓,都會生成一個層,也就是一個增量rootfs(一個目錄)襟己,這樣應(yīng)用A和應(yīng)用B所在的容器共同引用相同的Debian操作系統(tǒng)層引谜、Golang環(huán)境層(作為只讀層),而各自有各自應(yīng)用程序?qū)忧嬖。涂蓪憣釉毖省尤萜鞯臅r候通過UnionFS把相關(guān)的層掛載到一個目錄,作為容器的根文件系統(tǒng)贮预。

需要注意的是贝室,rootfs只是一個操作系統(tǒng)所包含的文件、配置和目錄仿吞,并不包括操作系統(tǒng)內(nèi)核滑频。這就意味著,如果你的應(yīng)用程序需要配置內(nèi)核參數(shù)唤冈、加載額外的內(nèi)核模塊峡迷,以及跟內(nèi)核進行直接的交互,你就需要注意了:這些操作和依賴的對象务傲,都是宿主機操作系統(tǒng)的內(nèi)核凉当,它對于該機器上的所有容器來說是一個“全局變量”枣申,牽一發(fā)而動全身售葡。

3)各Linux版本的UnionFS不同

由于各種原因(有興趣的可自行谷歌),Linux各發(fā)行版實現(xiàn)的UnionFS各不相同忠藤,所以Docker在不同linux發(fā)行版中使用的也不同挟伙。你可以通過docker info來查看docker使用的是哪種,比如:

  • centos, docker18.03.1-ce: Storage Driver: overlay2
  • debain, docker17.03.2-ce: Storage Driver: aufs

2.舉個例子(debain aufs)

1)準備如下目錄和文件

$ tree
.
|-- a
|   |-- a.log
|   `-- x.log
`-- b
    |-- b.log
    `-- x.log

2)執(zhí)行掛載命令

$ mkdir mnt
$ mount -t aufs -o dirs=./a:./b none ./mnt
$ tree ./mnt
./mnt
|-- a.log
|-- b.log
`-- x.log

可以看到被掛載的mnt目錄合并了目錄a和目錄b

3)修改

$ echo test > mnt/x.log 
$ cat mnt/x.log 
test
$ cat a/x.log 
test
$ cat b/x.log 

你會發(fā)現(xiàn)x.log在a模孩、b目錄都存在尖阔,在修改后只有a目錄生效了,原因是我們在mount aufs命令中榨咐,沒有指a介却、b目錄的權(quán)限,默認上來說块茁,命令行上第一個(最左邊)的目錄是可讀可寫的齿坷,后面的全都是只讀的桂肌,所以會出現(xiàn)上面這種情況,你也可以在掛載的時候自己指定權(quán)限(mount -t aufs -o dirs=./a=rw:./b=rw none ./mnt)永淌,如果你有興趣可以去嘗試一下崎场,這里就不再演示了。

那么再試一下修改b目錄(只讀目錄)才有的b.log文件試一下呢:

$ echo test > mnt/b.log 
$ cat mnt/b.log 
test
$ cat b/b.log 
$ cat a/b.log 
test

你會發(fā)現(xiàn)遂蛀,b目錄下的文件沒有被修改谭跨,而是在a目錄(可讀寫目錄)創(chuàng)建了一個b.log。

4)刪除

$ touch b/bb.log
$ rm mnt/a.log
$ rm mnt/bb.log
$ ls -al mnt
-rw-r--r-- 1 root root    0 Sep 19 23:11 b.log
-rw-r--r-- 1 root root    0 Sep 19 23:11 x.log
$ ls -al a
-rw-r--r-- 1 root root    0 Sep 19 23:15 .wh.bb.log
-rw-r--r-- 1 root root    0 Sep 19 23:11 b.log
-rw-r--r-- 1 root root    0 Sep 19 23:11 x.log
$ ls -al b
-rw-r--r-- 1 root root    0 Sep 19 23:11 b.log
-rw-r--r-- 1 root root    0 Sep 19 23:14 bb.log
-rw-r--r-- 1 root root    0 Sep 19 23:11 x.log

你會看到在mnt目錄中刪除a.log和bb.log后李滴,a目錄(可讀寫)中的a.log真的刪除了螃宙,而b目錄(只讀)中的bb.log還在,只是a目錄中多個.wh.bb.log這個文件所坯。

一般來說只讀目錄都會有whiteout的屬性污呼,所謂whiteout的意思,就是如果在union中刪除的某個文件包竹,實際上是位于一個readonly的目錄上燕酷,那么,在mount的union這個目錄中你將看不到這個文件,但是readonly這個層上我們無法做任何的修改锥涕,所以相寇,我們就需要對這個readonly目錄里的文件作whiteout。AUFS的whiteout的實現(xiàn)是通過在上層的可寫的目錄下建立對應(yīng)的whiteout隱藏文件來實現(xiàn)的酱讶。 所以上面的rm mnt/bb.log操作和touch a/.wh.bb.log效果相同。

5)來看一個docker容器

我們一起來執(zhí)行如下命令:

#啟動一個容器
$ docker run -dt golang:1.8.3 /bin/sh
7bcd61b6ccd79a7367cb9872015ad20871be5b44f8bad74d35e045c89b610f34

#通過上面容器id查看掛載點
$ ls /var/lib/docker/image/aufs/layerdb/mounts/7bcd61b6ccd79a7367cb9872015ad20871be5b44f8bad74d35e045c89b610f34 
init-id  mount-id  parent
$ cat /var/lib/docker/image/aufs/layerdb/mounts/7bcd61b6ccd79a7367cb9872015ad20871be5b44f8bad74d35e045c89b610f34/mount-id 
e4e2f1159f512ab74a6afbfeca51413cc3b6a24e86caccf91e40a9d611ce0a9b# 

可以看到容器掛載的目錄是e4e2f1159f512ab74a6afbfeca51413cc3b6a24e86caccf91e40a9d611ce0a9b彼乌,那么找到該目錄泻肯,看看里面的文件都有些什么:

$ ls /var/lib/docker/aufs/mnt/e4e2f1159f512ab74a6afbfeca51413cc3b6a24e86caccf91e40a9d611ce0a9b                   
bin  dev  etc  go  go%  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

一個完整的操作系統(tǒng)根目錄出現(xiàn)在里面。我們再來看看這個rootfs聯(lián)合掛載的層級結(jié)構(gòu):

# 通過上面找到的mount-id查看aufs的內(nèi)部id(也叫si)
$ cat /proc/mounts |grep e4e2f1159f512ab74a6afbfeca51413cc3b6a24e86caccf91e40a9d611ce0a9b
none /var/lib/docker/aufs/mnt/e4e2f1159f512ab74a6afbfeca51413cc3b6a24e86caccf91e40a9d611ce0a9b aufs rw,relatime,si=63e50947768841ec,dio,dirperm1 0 0

# 然后通過si查看layer
$ cat /sys/fs/aufs/si_63e50947768841ec/br[0-9]*
/var/lib/docker/aufs/diff/e4e2f1159f512ab74a6afbfeca51413cc3b6a24e86caccf91e40a9d611ce0a9b=rw
/var/lib/docker/aufs/diff/e4e2f1159f512ab74a6afbfeca51413cc3b6a24e86caccf91e40a9d611ce0a9b-init=ro
/var/lib/docker/aufs/diff/974a7e81b15c1eb6ea6c3c66dfb50dfcdf7b99b1e6458e2d3dca9451e2414106=ro
/var/lib/docker/aufs/diff/fd68755d715f47edc7f5ceaa2e5dc6788d4ca36a4d50f51a92a53045cd0b9fb1=ro
/var/lib/docker/aufs/diff/0e1237afa6d0fff72d9fdd5f84ef7275b1a49448d7523d590686131a3b129496=ro
/var/lib/docker/aufs/diff/440bf3d93514f6a35bd99d4ac098d9b709e878146e355c670bd8f1f533c185c5=ro
/var/lib/docker/aufs/diff/57e27832290597d0c5f2dc2ab55d1c53a7aa8a2a40eb6d21d014ad1210b1bb6f=ro
/var/lib/docker/aufs/diff/55da955ef5752f9c3d1810a7b23e0325dd7947a0c0aaecf6ae373f3e33979143=ro

由此我們找到了每個增量rootfs(即layer)所在的目錄慰照,那么現(xiàn)在你可以在容器里執(zhí)行上面UnionFS中實驗過的增刪改灶挟,看看在最終被修改的layer是哪個,這里就不一一實驗了毒租。從上面可以看到容器的layer一共有8層:

第一部分 只讀層

它是這個容器的rootfs最下面的6層(xxx=ro結(jié)尾)稚铣。可以看到墅垮,它們的掛載方式都是只讀的(ro+wh惕医,即readonly+whiteout,上面已經(jīng)講過一般來說只讀目錄都會有whiteout屬性)算色。

第二部分 Init層

它是一個以“-init”結(jié)尾的層抬伺,夾在只讀層和讀寫層之間。Init層是Docker項目單獨生成的一個內(nèi)部層灾梦,專門用來存放/etc/hosts峡钓、/etc/resolv.conf等信息齐鲤。需要這樣一層的原因是,這些文件本來屬于只讀的系統(tǒng)鏡像層的一部分椒楣,但是用戶往往需要在啟動容器時寫入一些指定的值比如hostname给郊,所以就需要在可讀寫層對它們進行修改∨趸遥可是淆九,這些修改往往只對當前的容器有效,我們并不希望執(zhí)行docker commit時毛俏,把這些信息連同可讀寫層一起提交掉炭庙。所以,Docker做法是煌寇,在修改了這些文件之后焕蹄,以一個單獨的層掛載了出來。而用戶執(zhí)行docker commit只會提交可讀寫層阀溶,所以是不包含這些內(nèi)容的腻脏。

第三部分 可讀寫層

它是這個容器的rootfs最上面的一層,它的掛載方式為:rw银锻,即read write永品。在沒有寫入文件之前,這個目錄是空的击纬。而一旦在容器里做了寫操作鼎姐,你修改產(chǎn)生的內(nèi)容就會以增量的方式出現(xiàn)在這個層中。刪除ro-wh層等文件時更振,也會在rw層創(chuàng)建對應(yīng)的個whiteout文件炕桨,把只讀層里的文件“遮擋”起來。最上面這個可讀寫層的作用肯腕,就是專門用來存放你修改rootfs后產(chǎn)生的增量献宫,無論是增刪改,都發(fā)生在這里乎芳。而當我們使用完了這個被修改過的容器之后遵蚜,還可以使用docker commit和push指令,保存這個被修改過的可讀寫層奈惑,并上傳到Docker Hub上,供其他人使用睡汹。而與此同時肴甸,原先的只讀層里的內(nèi)容則不會有任何變化。這囚巴,就是增量rootfs的好處原在。

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

6)性能

IBM的研究中心對Docker的性能給了一份非常不錯的性能報告(PDF)《An Updated Performance Comparison of Virtual Machinesand Linux Containers》村怪。

這里扒了兩張圖下來,順序讀寫和隨機讀寫:


順序讀寫

隨機讀寫

3.對照Docker源碼

1)在啟動docker daemon時會根據(jù)系統(tǒng)初始化好能使用的unionfs
func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.Store) (daemon *Daemon, err error) {
    //...
    for operatingSystem, gd := range d.graphDrivers {
        layerStores[operatingSystem], err = layer.NewStoreFromOptions(layer.StoreOptions{
            Root:                      config.Root,
            MetadataStorePathTemplate: filepath.Join(config.Root, "image", "%s", "layerdb"),
            GraphDriver:               gd,
            GraphDriverOptions:        config.GraphOptions,
            IDMapping:                 idMapping,
            PluginGetter:              d.PluginStore,
            ExperimentalEnabled:       config.Experimental,
            OS:                        operatingSystem,
        })
    }
    //...
}

func NewStoreFromOptions(options StoreOptions) (Store, error) {
    driver, err := graphdriver.New(options.GraphDriver, options.PluginGetter, graphdriver.Options{
        Root:                options.Root,
        DriverOptions:       options.GraphDriverOptions,
        UIDMaps:             options.IDMapping.UIDs(),
        GIDMaps:             options.IDMapping.GIDs(),
        ExperimentalEnabled: options.ExperimentalEnabled,
    })
    //...
}

// New creates the driver and initializes it at the specified root.
func New(name string, pg plugingetter.PluginGetter, config Options) (Driver, error) {
    //...
    driversMap := scanPriorDrivers(config.Root)
    list := strings.Split(priority, ",")
    logrus.Debugf("[graphdriver] priority list: %v", list)
    for _, name := range list {
        if name == "vfs" {
            // don't use vfs even if there is state present.
            continue
        }
        if _, prior := driversMap[name]; prior {
            driver, err := getBuiltinDriver(name, config.Root, config.DriverOptions, config.UIDMaps, config.GIDMaps)
            //...
            return driver, nil
        }
    }
    //...
}
2)再來看創(chuàng)建容器時浮庐,如何使用這個driver的
//docker daemon創(chuàng)建容器api的http handler
router.NewPostRoute("/containers/create", r.postContainersCreate)

//handler 挨著往下扒~
func (s *containerRouter) postContainersCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
    //...
    ccr, err := s.backend.ContainerCreate(types.ContainerCreateConfig{
        Name:             name,
        Config:           config,
        HostConfig:       hostConfig,
        NetworkingConfig: networkingConfig,
        AdjustCPUShares:  adjustCPUShares,
    })
    //...
}

func (daemon *Daemon) ContainerCreate(params types.ContainerCreateConfig) (containertypes.ContainerCreateCreatedBody, error) {
    return daemon.containerCreate(params, false)
}

func (daemon *Daemon) containerCreate(params types.ContainerCreateConfig, managed bool) (containertypes.ContainerCreateCreatedBody, error) {
    //...
    container, err := daemon.create(params, managed)
    //...
}

func (daemon *Daemon) create(params types.ContainerCreateConfig, managed bool) (retC *container.Container, retErr error) {
    //...
    //創(chuàng)建init和rw層
    // Set RWLayer for container after mount labels have been set
    rwLayer, err := daemon.imageService.CreateLayer(container, setupInitLayer(daemon.idMapping))
    //...
}

func (i *ImageService) CreateLayer(container *container.Container, initFunc layer.MountInit) (layer.RWLayer, error) {
    var layerID layer.ChainID
    if container.ImageID != "" {
        img, err := i.imageStore.Get(container.ImageID)
        if err != nil {
            return nil, err
        }
        layerID = img.RootFS.ChainID()
    }

    rwLayerOpts := &layer.CreateRWLayerOpts{
        MountLabel: container.MountLabel,
        InitFunc:   initFunc,
        StorageOpt: container.HostConfig.StorageOpt,
    }

    // Indexing by OS is safe here as validation of OS has already been performed in create() (the only
    // caller), and guaranteed non-nil
    //這里的layerStores正式NewDaemon時 layerStores[operatingSystem], err = layer.NewStoreFromOptions(...)這里的這個
    return i.layerStores[container.OS].CreateRWLayer(container.ID, layerID, rwLayerOpts)
}
3)接下來我們來追溯CreateRWLayer的實現(xiàn):
func (ls *layerStore) CreateRWLayer(name string, parent ChainID, opts *CreateRWLayerOpts) (RWLayer, error) {
    //...
    //這里driver不同環(huán)境有不同的實現(xiàn)甚负,下面我們主要來看aufs的實現(xiàn)
    if err = ls.driver.CreateReadWrite(m.mountID, pid, createOpts); err != nil {
        return nil, err
    }
    //
    //這里的saveMount正是save上面2.5里面查看掛載點的mount-id,init-id审残,parent
    //
    if err = ls.saveMount(m); err != nil {
        return nil, err
    }

    return m.getReference(), nil
}

//我們這里來看aufs的實現(xiàn)
//
// CreateReadWrite creates a layer that is writable for use as a container
// file system.
func (a *Driver) CreateReadWrite(id, parent string, opts *graphdriver.CreateOpts) error {
    return a.Create(id, parent, opts)
}

// Create three folders for each id
// mnt, layers, and diff
func (a *Driver) Create(id, parent string, opts *graphdriver.CreateOpts) error {

    if opts != nil && len(opts.StorageOpt) != 0 {
        return fmt.Errorf("--storage-opt is not supported for aufs")
    }

    if err := a.createDirsFor(id); err != nil {
        return err
    }
    // Write the layers metadata
    f, err := os.Create(path.Join(a.rootPath(), "layers", id))
    if err != nil {
        return err
    }
    defer f.Close()

    if parent != "" {
        ids, err := getParentIDs(a.rootPath(), parent)
        if err != nil {
            return err
        }

        if _, err := fmt.Fprintln(f, parent); err != nil {
            return err
        }
        for _, i := range ids {
            if _, err := fmt.Fprintln(f, i); err != nil {
                return err
            }
        }
    }

    return nil
}

至此梭域,容器鏡像的實現(xiàn)我們就討論的差不多了,有興趣的朋友搅轿,可以在去看看devicemapper病涨、overlay2等驅(qū)動,這就不一一展開討論了璧坟。

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末既穆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子雀鹃,更是在濱河造成了極大的恐慌循衰,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件褐澎,死亡現(xiàn)場離奇詭異会钝,居然都是意外死亡,警方通過查閱死者的電腦和手機工三,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門迁酸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人俭正,你說我怎么就攤上這事奸鬓。” “怎么了掸读?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵串远,是天一觀的道長。 經(jīng)常有香客問我儿惫,道長澡罚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任肾请,我火速辦了婚禮留搔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘铛铁。我一直安慰自己隔显,他們只是感情好却妨,可當我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著括眠,像睡著了一般彪标。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上掷豺,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天捞烟,我揣著相機與錄音,去河邊找鬼萌业。 笑死坷襟,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的生年。 我是一名探鬼主播婴程,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼抱婉!你這毒婦竟也來了档叔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤蒸绩,失蹤者是張志新(化名)和其女友劉穎衙四,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體患亿,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡传蹈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了步藕。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片惦界。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖咙冗,靈堂內(nèi)的尸體忽然破棺而出沾歪,到底是詐尸還是另有隱情,我是刑警寧澤雾消,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布灾搏,位于F島的核電站,受9級特大地震影響立润,放射性物質(zhì)發(fā)生泄漏狂窑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一范删、第九天 我趴在偏房一處隱蔽的房頂上張望蕾域。 院中可真熱鬧,春花似錦到旦、人聲如沸旨巷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽采呐。三九已至,卻和暖如春搁骑,著一層夾襖步出監(jiān)牢的瞬間斧吐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工仲器, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留煤率,地道東北人。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓乏冀,卻偏偏與公主長得像蝶糯,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子辆沦,可洞房花燭夜當晚...
    茶點故事閱讀 42,828評論 2 345

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

  • Docker容器技術(shù)已經(jīng)發(fā)展了好些年昼捍,在很多項目都有應(yīng)用,線上運行也很穩(wěn)定肢扯。整理了部分Docker的學(xué)習(xí)筆記以及新...
    __七把刀__閱讀 11,427評論 0 59
  • docker常用命令原理圖概覽: 按照docker官網(wǎng)上的說法妒茬,docker的文件系統(tǒng)分為兩層:bootfs和ro...
    燕京博士閱讀 2,950評論 2 32
  • 今天閱讀《管教的智慧》P55-P76,第二部分----澆水蔚晨。固定的適量的給植物澆水乍钻,是植物生長良好的必備條件。孩子...
    阿樂的后花園閱讀 185評論 0 1
  • 研路慢慢長铭腕,今日晃彷徨银择。 下午為幫聽,隨是忘壽辰谨履。 猶如往日否欢摄,旁聽上周同。 下午最后節(jié)笋粟,為聽高數(shù)去怀挠。 講者徳修歐...
    遠龍大詩閱讀 535評論 1 7
  • 寫在開頭 今天有個相熟的保險經(jīng)紀人,打算推薦我一款消費型的重疾險害捕,但被我委婉的拒絕了绿淋。同時,我告訴他尝盼,之前經(jīng)他買的...
    自在的蝸牛閱讀 392評論 3 2