欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

教你用100?行shell實(shí)現(xiàn)Docker詳解

 更新時(shí)間:2023年02月12日 08:28:43   作者:vivo?互聯(lián)網(wǎng)技術(shù)  
這篇文章主要為大家介紹了教你用100?行shell實(shí)現(xiàn)Docker詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

引言

作者:vivo 互聯(lián)網(wǎng)運(yùn)維團(tuán)隊(duì)- Hou Dengfeng

本文主要介紹使用shell實(shí)現(xiàn)一個(gè)簡易的Docker。

一、目的

在初接觸Docker的時(shí)候,我們必須要了解的幾個(gè)概念就是Cgroup、Namespace、RootFs,如果本身對(duì)虛擬化的發(fā)展沒有深入的了解,那么很難對(duì)這幾個(gè)概念有深入的理解,本文的目的就是通過在操作系統(tǒng)中以交互式的方式去理解,Cgroup/Namespace/Rootfs到底實(shí)現(xiàn)了什么,能做到哪些事情,然后通過shell這種直觀的命令行方式把我們的理解組合起來,去模仿Docker實(shí)現(xiàn)一個(gè)縮減的版本。

二、技術(shù)拆解

2.1 Namespace

2.1.1 簡介

Linux Namespace是Linux提供的一種內(nèi)核級(jí)別環(huán)境隔離的方法。學(xué)習(xí)過Linux的同學(xué)應(yīng)該對(duì)chroot命令比較熟悉(通過修改根目錄把用戶限制在一個(gè)特定目錄下),chroot提供了一種簡單的隔離模式:chroot內(nèi)部的文件系統(tǒng)無法訪問外部的內(nèi)容。Linux Namespace在此基礎(chǔ)上,提供了對(duì)UTS、IPC、mount、PID、network、User等的隔離機(jī)制。Namespace是對(duì)全局系統(tǒng)資源的一種封裝隔離,使得處于不同namespace的進(jìn)程擁有獨(dú)立的全局系統(tǒng)資源,改變一個(gè)namespace中的系統(tǒng)資源只會(huì)影響當(dāng)前namespace里的進(jìn)程,對(duì)其他namespace中的進(jìn)程沒有影響。

Linux Namespace有如下種類:

2.1.2 Namespace相關(guān)系統(tǒng)調(diào)用

amespace相關(guān)的系統(tǒng)調(diào)用有3個(gè),分別是clone(),setns(),unshare()。

  • clone: 創(chuàng)建一個(gè)新的進(jìn)程并把這個(gè)新進(jìn)程放到新的namespace中

  • setns: 將當(dāng)前進(jìn)程加入到已有的namespace中

  • unshare: 使當(dāng)前進(jìn)程退出指定類型的namespace,并加入到新創(chuàng)建的namespace中

2.1.3 查看進(jìn)程所屬Namespace

上面的概念都比較抽象,我們來看看在Linux系統(tǒng)中怎么樣去get namespace。

系統(tǒng)中的每個(gè)進(jìn)程都有/proc/[pid]/ns/這樣一個(gè)目錄,里面包含了這個(gè)進(jìn)程所屬namespace的信息,里面每個(gè)文件的描述符都可以用來作為setns函數(shù)(2.1.2)的fd參數(shù)。

#查看當(dāng)前bash進(jìn)程關(guān)聯(lián)的Namespace
# ls -l /proc/$$/ns
total 0
lrwxrwxrwx 1 root root 0 Jan 17 21:43 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Jan 17 21:43 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Jan 17 21:43 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0 Jan 17 21:43 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Jan 17 21:43 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Jan 17 21:43 uts -> uts:[4026531838]
#這些 namespace 文件都是鏈接文件。鏈接文件的內(nèi)容的格式為 xxx:[inode number]。
    其中的 xxx 為 namespace 的類型,inode number 則用來標(biāo)識(shí)一個(gè) namespace,我們也可以把它理解為 namespace 的 ID。
    如果兩個(gè)進(jìn)程的某個(gè) namespace 文件指向同一個(gè)鏈接文件,說明其相關(guān)資源在同一個(gè) namespace 中。以ipc:[4026531839]例,
    ipc是namespace的類型,4026531839是inode number,如果兩個(gè)進(jìn)程的ipc namespace的inode number一樣,說明他們屬于同一個(gè)namespace。
    這條規(guī)則對(duì)其他類型的namespace也同樣適用。
#從上面的輸出可以看出,對(duì)于每種類型的namespace,進(jìn)程都會(huì)與一個(gè)namespace ID關(guān)聯(lián)。
#當(dāng)一個(gè)namespace中的所有進(jìn)程都退出時(shí),該namespace將會(huì)被銷毀。在 /proc/[pid]/ns 里放置這些鏈接文件的作用就是,一旦這些鏈接文件被打開,
    只要打開的文件描述符(fd)存在,那么就算該 namespace 下的所有進(jìn)程都結(jié)束了,但這個(gè) namespace 也會(huì)一直存在,后續(xù)的進(jìn)程還可以再加入進(jìn)來。

2.1.4 相關(guān)命令及操作示例

本節(jié)會(huì)用UTS/IPC/NET 3個(gè)Namespace作為示例演示如何在linux系統(tǒng)中創(chuàng)建Namespace,并介紹相關(guān)命令。

2.1.4.1 IPC Namespace

IPC namespace用來隔離System V IPC objects和POSIX message queues。其中System V IPC objects包含消息列表Message queues、信號(hào)量Semaphore sets和共享內(nèi)存Shared memory segments。為了展現(xiàn)區(qū)分IPC Namespace我們這里會(huì)使用到ipc相關(guān)命令:

#    nsenter: 加入指定進(jìn)程的指定類型的namespace中,然后執(zhí)行參數(shù)中指定的命令。
#       命令格式:nsenter [options] [program [arguments]]
#       示例:nsenter –t 27668 –u –I /bin/bash
#
#    unshare: 離開當(dāng)前指定類型的namespace,創(chuàng)建且加入新的namesapce,然后執(zhí)行參數(shù)中執(zhí)行的命令。
#       命令格式:unshare [options] program [arguments]
#       示例:unshare --fork --pid --mount-proc readlink /proc/self
#
#    ipcmk:創(chuàng)建shared memory segments, message queues, 和semaphore arrays
#       參數(shù)-Q:創(chuàng)建message queues
#    ipcs:查看shared memory segments, message queues, 和semaphore arrays的相關(guān)信息
#      參數(shù)-a:顯示全部可顯示的信息
#      參數(shù)-q:顯示活動(dòng)的消息隊(duì)列信息

下面將以消息隊(duì)列為例,演示一下隔離效果,為了使演示更直觀,我們?cè)趧?chuàng)建新的ipc namespace的時(shí)候,同時(shí)也創(chuàng)建新的uts namespace,然后為新的uts namespace設(shè)置新hostname,這樣就能通過shell提示符一眼看出這是屬于新的namespace的bash。示例中我們用兩個(gè)shell來展示:

shell A

#查看當(dāng)前shell的uts / ipc namespace number
# readlink /proc/$$/ns/uts /proc/$$/ns/ipc
uts:[4026531838]
ipc:[4026531839]
#查看當(dāng)前主機(jī)名
# hostname
myCentos
#查看ipc message queues,默認(rèn)情況下沒有message queue
# ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages   
#創(chuàng)建一個(gè)message queue
# ipcmk -Q
Message queue id: 131072
# ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages   
0x82a1d963 131072     root       644        0            0 
-----> 切換至shell B執(zhí)行
------------------------------------------------------------------
#回到shell A之后我們可以看下hostname、ipc等有沒有收到影響
# hostname
myCentos
# ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages   
0x82a1d963 131072     root       644        0            0          
#接下來我們嘗試加入shell B中新的Namespace
# nsenter -t 30372 -u -i /bin/bash
[root@shell-B:/root]
# hostname
shell-B
# readlink /proc/$$/ns/uts /proc/$$/ns/ipc
uts:[4026532382]
ipc:[4026532383]
# ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages   
#可以看到我們已經(jīng)成功的加入到了新的Namespace中

shell B

#確認(rèn)當(dāng)前shell和shell A屬于相同Namespace
# readlink /proc/$$/ns/uts /proc/$$/ns/ipc
uts:[4026531838]
ipc:[4026531839]
# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0x82a1d963 131072 root 644 0 0
#使用unshare創(chuàng)建新的uts和ipc Namespace,并在新的Namespace中啟動(dòng)bash
# unshare -iu /bin/bash
#確認(rèn)新的bash uts/ipc Namespace Number
# readlink /proc/$$/ns/uts /proc/$$/ns/ipc
uts:[4026532382]
ipc:[4026532383]
#設(shè)置新的hostname與shell A做區(qū)分
# hostname shell-B
# hostname
shell-B
#查看之前的ipc message queue
# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
#查看當(dāng)前bash進(jìn)程的PID
# echo $$
30372
切換回shell A <-----

2.1.4.2 Net Namespace

Network namespace用來隔離網(wǎng)絡(luò)設(shè)備, IP地址, 端口等. 每個(gè)namespace將會(huì)有自己獨(dú)立的網(wǎng)絡(luò)棧,路由表,防火墻規(guī)則,socket等。每個(gè)新的network namespace默認(rèn)有一個(gè)本地環(huán)回接口,除了lo接口外,所有的其他網(wǎng)絡(luò)設(shè)備(物理/虛擬網(wǎng)絡(luò)接口,網(wǎng)橋等)只能屬于一個(gè)network namespace。每個(gè)socket也只能屬于一個(gè)network namespace。當(dāng)新的network namespace被創(chuàng)建時(shí),lo接口默認(rèn)是關(guān)閉的,需要自己手動(dòng)啟動(dòng)起。標(biāo)記為"local devices"的設(shè)備不能從一個(gè)namespace移動(dòng)到另一個(gè)namespace,比如loopback, bridge, ppp等,我們可以通過ethtool -k命令來查看設(shè)備的netns-local屬性。

我們使用以下命令來創(chuàng)建net namespace。

相關(guān)命令:
    ip netns: 管理網(wǎng)絡(luò)namespace
    用法:
       ip netns list
       ip netns add NAME
       ip netns set NAME NETNSID
       ip [-all] netns delete [NAME]

下面使用ip netns來演示創(chuàng)建net Namespace。

shell A

#創(chuàng)建一對(duì)網(wǎng)卡,分別命名為veth0_11/veth2_11
# ip link add veth0_11 type veth peer name veth2_11
#查看已經(jīng)創(chuàng)建的網(wǎng)卡
#ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 5e:75:97:0d:54:17 brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.1/24 brd 192.168.1.255 scope global eth0
       valid_lft forever preferred_lft forever
3: br1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN qlen 1000
    link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/24 scope global br1
       valid_lft forever preferred_lft forever
96: veth2_11@veth0_11: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN qlen 1000
    link/ether 5e:75:97:0d:54:0e brd ff:ff:ff:ff:ff:ff
97: veth0_11@veth2_11: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN qlen 1000
    link/ether a6:c7:1f:79:a6:a6 brd ff:ff:ff:ff:ff:ff
#使用ip netns創(chuàng)建兩個(gè)net namespace
# ip netns add r1
# ip netns add r2
# ip netns list
r2
r1 (id: 0)
#將兩個(gè)網(wǎng)卡分別加入到對(duì)應(yīng)的netns中
# ip link set veth0_11 netns r1
# ip link set veth2_11 netns r2
#再次查看網(wǎng)卡,在bash當(dāng)前的namespace中已經(jīng)看不到veth0_11和veth2_11了
# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 5e:75:97:0d:54:17 brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.1/24 brd 192.168.1.255 scope global eth0
       valid_lft forever preferred_lft forever
3: br1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN qlen 1000
    link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/24 scope global br1
       valid_lft forever preferred_lft forever
#接下來我們切換到對(duì)應(yīng)的netns中對(duì)網(wǎng)卡進(jìn)行配置
#通過nsenter --net可以切換到對(duì)應(yīng)的netns中,ip a展示了我們上面加入到r1中的網(wǎng)卡
# nsenter --net=/var/run/netns/r1 /bin/bash
# ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
97: veth0_11@if96: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN qlen 1000
    link/ether a6:c7:1f:79:a6:a6 brd ff:ff:ff:ff:ff:ff link-netnsid 1
#對(duì)網(wǎng)卡配置ip并啟動(dòng)
# ip addr add 172.18.0.11/24 dev veth0_11
# ip link set veth0_11 up
# ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
97: veth0_11@if96: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state LOWERLAYERDOWN qlen 1000
    link/ether a6:c7:1f:79:a6:a6 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet 172.18.0.11/24 scope global veth0_11
       valid_lft forever preferred_lft forever
-----> 切換至shell B執(zhí)行
------------------------------------------------------------------
#在r1中ping veth2_11
# ping 172.18.0.12
PING 172.18.0.12 (172.18.0.12) 56(84) bytes of data.
64 bytes from 172.18.0.12: icmp_seq=1 ttl=64 time=0.033 ms
64 bytes from 172.18.0.12: icmp_seq=2 ttl=64 time=0.049 ms
...
#至此我們通過netns完成了創(chuàng)建net Namespace的小實(shí)驗(yàn)

shell B

#在shell B中我們同樣切換到netns r2中進(jìn)行配置
#通過nsenter --net可以切換到r2,ip a展示了我們上面加入到r2中的網(wǎng)卡
# nsenter --net=/var/run/netns/r2 /bin/bash
#  ip a
1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
96: veth2_11@if97: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN qlen 1000
    link/ether 5e:75:97:0d:54:0e brd ff:ff:ff:ff:ff:ff link-netnsid 0
#對(duì)網(wǎng)卡配置ip并啟動(dòng)
# ip addr add 172.18.0.12/24 dev veth2_11
# ip link set veth2_11 up
# ip a
1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
96: veth2_11@if97: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UP qlen 1000
    link/ether 5e:75:97:0d:54:0e brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.18.0.12/24 scope global veth2_11
       valid_lft forever preferred_lft forever
    inet6 fe80::5c75:97ff:fe0d:540e/64 scope link
       valid_lft forever preferred_lft forever
#嘗試ping r1中的網(wǎng)卡
# ping 172.18.0.11
PING 172.18.0.11 (172.18.0.11) 56(84) bytes of data.
64 bytes from 172.18.0.11: icmp_seq=1 ttl=64 time=0.046 ms
64 bytes from 172.18.0.11: icmp_seq=2 ttl=64 time=0.040 ms
...
#可以完成通信
切換至shell A執(zhí)行 &lt;-----

示意圖

2.2 Cgroup

2.2.1 簡介

Cgroup和namespace類似,也是將進(jìn)程進(jìn)行分組,但它的目的和namespace不一樣,namespace是為了隔離進(jìn)程組之間的資源,而cgroup是為了對(duì)一組進(jìn)程進(jìn)行統(tǒng)一的資源監(jiān)控和限制。

Cgroup作用:

資源限制(Resource limiting): Cgroups可以對(duì)進(jìn)程組使用的資源總額進(jìn)行限制。如對(duì)特定的進(jìn)程進(jìn)行內(nèi)存使用上限限制,當(dāng)超出上限時(shí),會(huì)觸發(fā)OOM。

優(yōu)先級(jí)分配(Prioritization): 通過分配的CPU時(shí)間片數(shù)量及硬盤IO帶寬大小,實(shí)際上就相當(dāng)于控制了進(jìn)程運(yùn)行的優(yōu)先級(jí)。

資源統(tǒng)計(jì)(Accounting): Cgroups可以統(tǒng)計(jì)系統(tǒng)的資源使用量,如CPU使用時(shí)長、內(nèi)存用量等等,這個(gè)功能非常適用于計(jì)費(fèi)。

**進(jìn)程控制(Control):**Cgroups可以對(duì)進(jìn)程組執(zhí)行掛起、恢復(fù)等操作。

Cgroups的組成:

task: 在Cgroups中,task就是系統(tǒng)的一個(gè)進(jìn)程。

cgroup: Cgroups中的資源控制都以cgroup為單位實(shí)現(xiàn)的。cgroup表示按照某種資源控制標(biāo)準(zhǔn)劃分而成的任務(wù)組,包含一個(gè)或多個(gè)子系統(tǒng)。一個(gè)任務(wù)可以加入某個(gè)cgroup,也可以從某個(gè)cgroup遷移到另外一個(gè)cgroup。

subsystem: 一個(gè)subsystem就是一個(gè)內(nèi)核模塊,被關(guān)聯(lián)到一顆cgroup樹之后,就會(huì)在樹的每個(gè)節(jié)點(diǎn)(進(jìn)程組)上做具體的操作。subsystem經(jīng)常被稱作"resource controller",因?yàn)樗饕挥脕碚{(diào)度或者限制每個(gè)進(jìn)程組的資源,但是這個(gè)說法不完全準(zhǔn)確,因?yàn)橛袝r(shí)我們將進(jìn)程分組只是為了做一些監(jiān)控,觀察一下他們的狀態(tài),比如perf_event subsystem。到目前為止,Linux支持13種subsystem(Cgroup v1),比如限制CPU的使用時(shí)間,限制使用的內(nèi)存,統(tǒng)計(jì)CPU的使用情況,凍結(jié)和恢復(fù)一組進(jìn)程等。

hierarchy: 一個(gè)hierarchy可以理解為一棵cgroup樹,樹的每個(gè)節(jié)點(diǎn)就是一個(gè)進(jìn)程組,每棵樹都會(huì)與零到多個(gè)subsystem關(guān)聯(lián)。在一顆樹里面,會(huì)包含Linux系統(tǒng)中的所有進(jìn)程,但每個(gè)進(jìn)程只能屬于一個(gè)節(jié)點(diǎn)(進(jìn)程組)。系統(tǒng)中可以有很多顆cgroup樹,每棵樹都和不同的subsystem關(guān)聯(lián),一個(gè)進(jìn)程可以屬于多顆樹,即一個(gè)進(jìn)程可以屬于多個(gè)進(jìn)程組,只是這些進(jìn)程組和不同的subsystem關(guān)聯(lián)。如果不考慮不與任何subsystem關(guān)聯(lián)的情況(systemd就屬于這種情況),Linux里面最多可以建13顆cgroup樹,每棵樹關(guān)聯(lián)一個(gè)subsystem,當(dāng)然也可以只建一棵樹,然后讓這棵樹關(guān)聯(lián)所有的subsystem。當(dāng)一顆cgroup樹不和任何subsystem關(guān)聯(lián)的時(shí)候,意味著這棵樹只是將進(jìn)程進(jìn)行分組,至于要在分組的基礎(chǔ)上做些什么,將由應(yīng)用程序自己決定,systemd就是一個(gè)這樣的例子。

2.2.2 查看Cgroup信息

查看當(dāng)前系統(tǒng)支持的subsystem

#通過/proc/cgroups查看當(dāng)前系統(tǒng)支持哪些subsystem
# cat /proc/cgroups
#subsys_name    hierarchy       num_cgroups     enabled
cpuset              11              1           1
cpu                 4               67          1
cpuacct             4               67          1
memory              5               69          1
devices             7               62          1
freezer             8               1           1
net_cls             6               1           1
blkio               9               62          1
perf_event          3               1           1
hugetlb             2               1           1
pids                10              62          1
net_prio            6               1           1
#字段含義
#subsys_name: subsystem的名稱
#hierarchy:subsystem所關(guān)聯(lián)到的cgroup樹的ID,如果多個(gè)subsystem關(guān)聯(lián)到同一顆cgroup樹,那么他們的這個(gè)字段將一樣,比如這里的cpu和cpuacct就一樣,表示他們綁定到了同一顆樹。如果出現(xiàn)下面的情況,這個(gè)字段將為0:
        當(dāng)前subsystem沒有和任何cgroup樹綁定
        當(dāng)前subsystem已經(jīng)和cgroup v2的樹綁定
        當(dāng)前subsystem沒有被內(nèi)核開啟
#num_cgroups: subsystem所關(guān)聯(lián)的cgroup樹中進(jìn)程組的個(gè)數(shù),也即樹上節(jié)點(diǎn)的個(gè)數(shù)
#enabled: 1表示開啟,0表示沒有被開啟(可以通過設(shè)置內(nèi)核的啟動(dòng)參數(shù)“cgroup_disable”來控制subsystem的開啟).

查看進(jìn)程所屬cgroup

#查看當(dāng)前shell進(jìn)程所屬的cgroup
# cat /proc/$$/cgroup
11:cpuset:/
10:pids:/system.slice/sshd.service
9:blkio:/system.slice/sshd.service
8:freezer:/
7:devices:/system.slice/sshd.service
6:net_prio,net_cls:/
5:memory:/system.slice/sshd.service
4:cpuacct,cpu:/system.slice/sshd.service
3:perf_event:/
2:hugetlb:/
1:name=systemd:/system.slice/sshd.service
#字段含義(以冒號(hào)分為3列):
# 1. cgroup樹ID,對(duì)應(yīng)/proc/cgroups中的hierachy
# 2. cgroup所綁定的subsystem,多個(gè)subsystem使用逗號(hào)分隔。name=systemd表示沒有和任何subsystem綁定,只是給他起了個(gè)名字叫systemd。
# 3. 進(jìn)程在cgroup樹中的路徑,即進(jìn)程所屬的cgroup,這個(gè)路徑是相對(duì)于掛載點(diǎn)的相對(duì)路徑。

2.2.3 相關(guān)命令

使用cgroup

cgroup相關(guān)的所有操作都是基于內(nèi)核中的cgroup virtual filesystem,使用cgroup很簡單,掛載這個(gè)文件系統(tǒng)就可以了。一般情況下都是掛載到/sys/fs/cgroup目錄下,當(dāng)然掛載到其它任何目錄都沒關(guān)系。

查看下當(dāng)前系統(tǒng)cgroup掛載情況。

#過濾系統(tǒng)掛載可以查看cgroup
# mount |grep cgroup
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct,cpu)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
#如果系統(tǒng)中沒有掛載cgroup,可以使用mount命令創(chuàng)建cgroup
#掛載根cgroup
# mkdir /sys/fs/cgroup
# mount -t tmpfs cgroup_root /sys/fs/cgroup
#將cpuset subsystem關(guān)聯(lián)到/sys/fs/cgroup/cpu_memory
# mkdir /sys/fs/cgroup/cpuset
# sudo mount -t cgroup cpuset -o cgroup /sys/fs/cgroup/cpuset/
#將cpu和memory subsystem關(guān)聯(lián)到/sys/fs/cgroup/cpu_memory
# mkdir /sys/fs/cgroup/cpu_memory
# sudo mount -n -t cgroup -o cpu,memory cgroup /sys/fs/cgroup/cpu_memory

除了mount命令之外我們還可以使用以下命令對(duì)cgroup進(jìn)行創(chuàng)建、屬性設(shè)置等操作,這也是我們后面腳本中用于創(chuàng)建和管理cgroup的命令。

# Centos操作系統(tǒng)可以通過yum install cgroup-tools 來安裝以下命令
    cgcreate: 在層級(jí)中創(chuàng)建新cgroup。
        用法: cgcreate [-h] [-t &lt;tuid&gt;:&lt;tgid&gt;] [-a &lt;agid&gt;:&lt;auid&gt;] [-f mode] [-d mode] [-s mode]
                -g &lt;controllers&gt;:&lt;path&gt; [-g ...]
        示例: cgcreate -g *:student -g devices:teacher //在所有的掛載hierarchy中創(chuàng)建student cgroup,在devices   
             hierarchy掛載點(diǎn)創(chuàng)建teacher cgroup
    cgset: 設(shè)置指定cgroup(s)的參數(shù)
        用法: cgset [-r &lt;name=value&gt;] &lt;cgroup_path&gt; ...
        示例: cgset -r cpuset.cpus=0-1 student //將student cgroup的cpuset控制器中的cpus限制為0-1
    cgexec: 在指定的cgroup中運(yùn)行任務(wù)
        用法: cgexec [-h] [-g &lt;controllers&gt;:&lt;path&gt;] [--sticky] command [arguments]
        示例: cgexec -g cpu,memory:test1 ls -l //在cpu和memory控制器下的test1 cgroup中運(yùn)行l(wèi)s -l命令

2.3 Rootfs

2.3.1 簡介

Rootfs 是 Docker 容器在啟動(dòng)時(shí)內(nèi)部進(jìn)程可見的文件系統(tǒng),即 Docker容器的根目錄。rootfs 通常包含一個(gè)操作系統(tǒng)運(yùn)行所需的文件系統(tǒng),例如可能包含典型的類 Unix 操作系統(tǒng)中的目錄系統(tǒng),如 /dev、/proc、/bin、/etc、/lib、/usr、/tmp 及運(yùn)行 Docker 容器所需的配置文件、工具等。

就像Linux啟動(dòng)會(huì)先用只讀模式掛載rootfs,運(yùn)行完完整性檢查之后,再切換成讀寫模式一樣。Docker deamon為container掛載rootfs時(shí),也會(huì)先掛載為只讀模式,但是與Linux做法不同的是,在掛載完只讀的rootfs之后,Docker deamon會(huì)利用聯(lián)合掛載技術(shù)(Union Mount)在已有的rootfs上再掛一個(gè)讀寫層。container在運(yùn)行過程中文件系統(tǒng)發(fā)生的變化只會(huì)寫到讀寫層,并通過whiteout技術(shù)隱藏只讀層中的舊版本文件。

Docker支持不同的存儲(chǔ)驅(qū)動(dòng),包括 aufs、devicemapper、overlay2、zfs 和 vfs 等,目前在 Docker 中,overlay2 取代了 aufs 成為了推薦的存儲(chǔ)驅(qū)動(dòng)。

2.3.2 overlayfs

overlayFS是聯(lián)合掛載技術(shù)的一種實(shí)現(xiàn)。除了overlayFS以外還有aufs,VFS,Brtfs,device mapper等技術(shù)。雖然實(shí)現(xiàn)細(xì)節(jié)不同,但是他們做的事情都是相同的。Linux內(nèi)核為Docker提供的overalyFS驅(qū)動(dòng)有2種:overlay2和overlay,overlay2是相對(duì)于overlay的一種改進(jìn),在inode利用率方面比overlay更有效。

overlayfs通過三個(gè)目錄來實(shí)現(xiàn):lower目錄、upper目錄、以及work目錄。三種目錄合并出來的目錄稱為merged目錄。

**lower:**可以是多個(gè),是處于最底層的目錄,作為只讀層。

**upper:**只有一個(gè),作為讀寫層。

**work:**為工作基礎(chǔ)目錄,掛載后內(nèi)容會(huì)被清空,且在使用過程中其內(nèi)容用戶不可見。

**merged:**為最后聯(lián)合掛載完成給用戶呈現(xiàn)的統(tǒng)一視圖,也就是說merged目錄里面本身并沒有任何實(shí)體文件,給我們展示的只是參與聯(lián)合掛載的目錄里面文件而已,真正的文件還是在lower和upper中。所以,在merged目錄下編輯文件,或者直接編輯lower或upper目錄里面的文件都會(huì)影響到merged里面的視圖展示。

2.3.3 文件規(guī)則

merged層目錄會(huì)顯示離它最近層的文件。層級(jí)關(guān)系中upperdir比lowerdir更靠近merged層,而多個(gè)lowerdir的情況下,寫的越靠前的目錄離merged層目錄越近。相同文件名的文件會(huì)依照層級(jí)規(guī)則進(jìn)行“覆蓋”。

2.3.4 overlayFS如何工作

讀:

如果文件在容器層(upperdir),直接讀取文件;

如果文件不在容器層(upperdir),則從鏡像層(lowerdir)讀取;

寫:

①首次寫入: 如果在upperdir中不存在,overlay執(zhí)行cow操作,把文件從lowdir拷貝到upperdir,由于overlayfs是文件級(jí)別的(即使文件只有很少的一點(diǎn)修改,也會(huì)產(chǎn)生的cow的行為),后續(xù)對(duì)同一文件的在此寫入操作將對(duì)已經(jīng)復(fù)制到容器的文件的副本進(jìn)行操作。值得注意的是,cow操作只發(fā)生在文件首次寫入,以后都是只修改副本。

②刪除文件和目錄: 當(dāng)文件在容器被刪除時(shí),在容器層(upperdir)創(chuàng)建whiteout文件,鏡像層(lowerdir)的文件是不會(huì)被刪除的,因?yàn)樗麄兪侵蛔x的,但whiteout文件會(huì)阻止他們顯示。

2.3.5 在系統(tǒng)里創(chuàng)建overlayfs

shell

# 創(chuàng)建所需的目錄
# mkdir upper lower merged work
# echo "lower" &gt; lower/in_lower.txt
# echo "upper" &gt; upper/in_upper.txt
# 在lower和upper中都創(chuàng)建 in_both文件
# echo "lower" &gt; lower/in_both.txt
# echo "upper" &gt; upper/in_both.txt
#查看下我們當(dāng)前的目錄及文件結(jié)構(gòu)
# tree .
.
|-- lower
|   |-- in_both.txt
|   `-- in_lower.txt
|-- merged
|-- upper
|   |-- in_both.txt
|   `-- in_upper.txt
`-- work
#使用mount命令將創(chuàng)建的目錄聯(lián)合掛載起來
# mount -t overlay overlay -o lowerdir=lower,upperdir=upper,workdir=work merged
#查看mount結(jié)果可以看到已經(jīng)成功掛載了
# mount |grep overlay
overlay on /data/overlay_demo/merged type overlay (rw,relatime,lowerdir=lower,upperdir=upper,workdir=work)
#此時(shí)再查看文件目錄結(jié)構(gòu)
# tree .
.
|-- lower
|   |-- in_both.txt
|   `-- in_lower.txt
|-- merged
|   |-- in_both.txt
|   |-- in_lower.txt
|   `-- in_upper.txt
|-- upper
|   |-- in_both.txt
|   `-- in_upper.txt
`-- work
    `-- work
#可以看到merged中包含了lower和upper中的文件
#然后我查看merge中的in_both文件,驗(yàn)證了上層目錄覆蓋下層的結(jié)論
# cat merged/in_both.txt
upper
#上面我們驗(yàn)證了掛載后overlayfs的讀,接下來我們?nèi)ヲ?yàn)證下寫
#我們?cè)趍erged中創(chuàng)建一個(gè)新文件,并查看
# touch merged/new_file
# tree .
.
|-- lower
|   |-- in_both.txt
|   `-- in_lower.txt
|-- merged
|   |-- in_both.txt
|   |-- in_lower.txt
|   |-- in_upper.txt
|   `-- new_file
|-- upper
|   |-- in_both.txt
|   |-- in_upper.txt
|   `-- new_file
`-- work
    `-- work
#可以看到新文件實(shí)際是放在了upper目錄中
#下面我們看下如果刪除了lower和upper中都有的文件會(huì)怎樣
# rm -f merged/in_both.txt
# tree .
.
|-- lower
|   |-- in_both.txt
|   `-- in_lower.txt
|-- merged
|   |-- in_lower.txt
|   |-- in_upper.txt
|   `-- new_file
|-- upper
|   |-- in_both.txt
|   |-- in_upper.txt
|   `-- new_file
`-- work
    `-- work
#從文件目錄上看只有merge中沒有了in_both文件,但是upper中的文件已經(jīng)發(fā)生了變化
# ll upper/in_both.txt
c--------- 1 root root 0, 0 Jan 21 19:33 upper/in_both.txt
#upper/in_both.txt已經(jīng)變成了一個(gè)空的字符文件,且覆蓋了lower層的內(nèi)容

三 、Bocker

3.1 功能演示

第二部分中我們對(duì)Namespace,cgroup,overlayfs有了一定的了解,接下來我們通過一個(gè)腳本來實(shí)現(xiàn)個(gè)建議的Docker。

3.2 完整腳本

腳本一共用130行代碼,完成了上面的功能,也算符合我們此次的標(biāo)題了。為了大家可以更深入的理解腳本內(nèi)容,這里就不再對(duì)腳本進(jìn)行拆分講解,以下是完整腳本。

#!/usr/bin/env bash
set -o errexit -o nounset -o pipefail; shopt -s nullglob
overlay_path='/var/lib/bocker/overlay' && container_path='/var/lib/bocker/containers' && cgroups='cpu,cpuacct,memory';
[[ $# -gt 0 ]] && while [ "${1:0:2}" == '--' ]; do OPTION=${1:2}; [[ $OPTION =~ = ]] && declare "BOCKER_${OPTION/=*/}=${OPTION/*=/}" || declare "BOCKER_${OPTION}=x"; shift; done
function bocker_check() {
    case ${1:0:3} in
        img) ls "$overlay_path" | grep -qw "$1" && echo 0 || echo 1;;
        ps_) ls "$container_path" | grep -qw "$1" && echo 2 || echo 3;;
    esac
}
function bocker_init() { #HELP Create an image from a directory:\nBOCKER init <directory>
    uuid="img_$(shuf -i 42002-42254 -n 1)"
    if [[ -d "$1" ]]; then
        [[ "$(bocker_check "$uuid")" == 0 ]] && bocker_run "$@"
        mkdir "$overlay_path/$uuid" > /dev/null
        cp -rf --reflink=auto "$1"/* "$overlay_path/$uuid" > /dev/null
        [[ ! -f "$overlay_path/$uuid"/img.source ]] && echo "$1" > "$overlay_path/$uuid"/img.source
        [[ ! -d "$overlay_path/$uuid"/proc ]] && mkdir "$overlay_path/$uuid"/proc
        echo "Created: $uuid"
    else
        echo "No directory named '$1' exists"
    fi
}
function bocker_pull() { #HELP Pull an image from Docker Hub:\nBOCKER pull <name> <tag>
    tmp_uuid="$(uuidgen)" && mkdir /tmp/"$tmp_uuid"
    download-frozen-image-v2 /tmp/"$tmp_uuid" "$1:$2" > /dev/null
    rm -rf /tmp/"$tmp_uuid"/repositories
    for tar in $(jq '.[].Layers[]' --raw-output < /tmp/$tmp_uuid/manifest.json); do
        tar xf /tmp/$tmp_uuid/$tar -C /tmp/$tmp_uuid && rm -rf /tmp/$tmp_uuid/$tar
    done
    for config in $(jq '.[].Config' --raw-output < /tmp/$tmp_uuid/manifest.json); do
        rm -f /tmp/$tmp_uuid/$config
    done
    echo "$1:$2" > /tmp/$tmp_uuid/img.source
    bocker_init /tmp/$tmp_uuid && rm -rf /tmp/$tmp_uuid
}
function bocker_rm() { #HELP Delete an image or container:\nBOCKER rm <image_id or container_id>
    [[ "$(bocker_check "$1")" == 3 ]] && echo "No container named '$1' exists" && exit 1
    [[ "$(bocker_check "$1")" == 1 ]] && echo "No image named '$1' exists" && exit 1
    if [[ -d "$overlay_path/$1" ]];then
        rm -rf "$overlay_path/$1" && echo "Removed: $1"
    else
        umount "$container_path/$1"/merged && rm -rf "$container_path/$1" && ip netns del netns_"$1" && ip link del dev veth0_"$1" && echo "Removed: $1"
        cgdelete -g "$cgroups:/$1" &> /dev/null
    fi
}
function bocker_images() { #HELP List images:\nBOCKER images
    echo -e "IMAGE_ID\t\tSOURCE"
    for img in "$overlay_path"/img_*; do
        img=$(basename "$img")
        echo -e "$img\t\t$(cat "$overlay_path/$img/img.source")"
    done
}
function bocker_ps() { #HELP List containers:\nBOCKER ps
    echo -e "CONTAINER_ID\t\tCOMMAND"
    for ps in "$container_path"/ps_*; do
        ps=$(basename "$ps")
        echo -e "$ps\t\t$(cat "$container_path/$ps/$ps.cmd")"
    done
}
function bocker_run() { #HELP Create a container:\nBOCKER run <image_id> <command>
    uuid="ps_$(shuf -i 42002-42254 -n 1)"
    [[ "$(bocker_check "$1")" == 1 ]] && echo "No image named '$1' exists" && exit 1
    [[ "$(bocker_check "$uuid")" == 2 ]] && echo "UUID conflict, retrying..." && bocker_run "$@" && return
    cmd="${@:2}" && ip="$(echo "${uuid: -3}" | sed 's/0//g')" && mac="${uuid: -3:1}:${uuid: -2}"
    ip link add dev veth0_"$uuid" type veth peer name veth2_"$uuid"
    ip link set dev veth0_"$uuid" up
    ip link set veth0_"$uuid" master br1
    ip netns add netns_"$uuid"
    ip link set veth2_"$uuid" netns netns_"$uuid"
    ip netns exec netns_"$uuid" ip link set dev lo up
    ip netns exec netns_"$uuid" ip link set veth2_"$uuid" address 02:42:ac:11:00"$mac"
    ip netns exec netns_"$uuid" ip addr add 172.18.0."$ip"/24 dev veth2_"$uuid"
    ip netns exec netns_"$uuid" ip link set dev veth2_"$uuid" up
    ip netns exec netns_"$uuid" ip route add default via 172.18.0.1
    mkdir -p "$container_path/$uuid"/{lower,upper,work,merged} && cp -rf --reflink=auto "$overlay_path/$1"/* "$container_path/$uuid"/lower > /dev/null && \
    mount -t overlay overlay \
        -o lowerdir="$container_path/$uuid"/lower,upperdir="$container_path/$uuid"/upper,workdir="$container_path/$uuid"/work \
        "$container_path/$uuid"/merged
    echo 'nameserver 114.114.114.114' > "$container_path/$uuid"/merged/etc/resolv.conf
    echo "$cmd" > "$container_path/$uuid/$uuid.cmd"
    cgcreate -g "$cgroups:/$uuid"
    : "${BOCKER_CPU_SHARE:=512}" && cgset -r cpu.shares="$BOCKER_CPU_SHARE" "$uuid"
    : "${BOCKER_MEM_LIMIT:=512}" && cgset -r memory.limit_in_bytes="$((BOCKER_MEM_LIMIT * 1000000))" "$uuid"
    cgexec -g "$cgroups:$uuid" \
        ip netns exec netns_"$uuid" \
        unshare -fmuip --mount-proc \
        chroot "$container_path/$uuid"/merged \
        /bin/sh -c "/bin/mount -t proc proc /proc && $cmd" \
        2>&1 | tee "$container_path/$uuid/$uuid.log" || true
    ip link del dev veth0_"$uuid"
    ip netns del netns_"$uuid"
}
function bocker_exec() { #HELP Execute a command in a running container:\nBOCKER exec <container_id> <command>
    [[ "$(bocker_check "$1")" == 3 ]] && echo "No container named '$1' exists" && exit 1
    cid="$(ps o ppid,pid | grep "^$(ps o pid,cmd | grep -E "^\ *[0-9]+ unshare.*$1" | awk '{print $1}')" | awk '{print $2}')"
    [[ ! "$cid" =~ ^\ *[0-9]+$ ]] && echo "Container '$1' exists but is not running" && exit 1
    nsenter -t "$cid" -m -u -i -n -p chroot "$container_path/$1"/merged "${@:2}"
}
function bocker_logs() { #HELP View logs from a container:\nBOCKER logs <container_id>
    [[ "$(bocker_check "$1")" == 3 ]] && echo "No container named '$1' exists" && exit 1
    cat "$container_path/$1/$1.log"
}
function bocker_commit() { #HELP Commit a container to an image:\nBOCKER commit <container_id> <image_id>
    [[ "$(bocker_check "$1")" == 3 ]] && echo "No container named '$1' exists" && exit 1
    [[ "$(bocker_check "$2")" == 0 ]] && echo "Image named '$2' exists" && exit 1
    mkdir "$overlay_path/$2" && cp -rf --reflink=auto "$container_path/$1"/merged/* "$overlay_path/$2" && sed -i "s/:.*$/:$(date +%Y%m%d-%H%M%S)/g" "$overlay_path/$2"/img.source
    echo "Created: $2"
}
function bocker_help() { #HELP Display this message:\nBOCKER help
    sed -n "s/^.*#HELP\\s//p;" < "$1" | sed "s/\\\\n/\n\t/g;s/$/\n/;s!BOCKER!${1/!/\\!}!g"
}
[[ -z "${1-}" ]] && bocker_help "$0" && exit 1
case $1 in
    pull|init|rm|images|ps|run|exec|logs|commit) bocker_"$1" "${@:2}" ;;
    *) bocker_help "$0" ;;
esac
README

Bocker

使用100行bash實(shí)現(xiàn)一個(gè)docker,本腳本是依據(jù)bocker實(shí)現(xiàn),更換了存儲(chǔ)驅(qū)動(dòng),完善了pull等功能。

前置條件

為了腳本能夠正常運(yùn)行,機(jī)器上需要具備以下組件:

overlayfs

iproute2

iptables

libcgroup-tools

util-linux >= 2.25.2

coreutils >= 7.5

大部分功能在centos7上都是滿足的,overlayfs可以通過modprobe overlay掛載。

另外你可能還要做以下設(shè)置:

  • 創(chuàng)建bocker運(yùn)行目錄 /var/lib/bocker/overlay,/var/lib/bocker/containers
  • 創(chuàng)建一個(gè)IP地址為 172.18.0.1/24 的橋接網(wǎng)卡 br1
  • 確認(rèn)開啟IP轉(zhuǎn)發(fā) /proc/sys/net/ipv4/ip_forward = 1
  • 創(chuàng)建iptables規(guī)則將橋接網(wǎng)絡(luò)流量轉(zhuǎn)發(fā)至物理網(wǎng)卡,示例:iptables -t nat -A POSTROUTING -s 172.18.0.0/24 -o eth0 -j MASQUERADE

實(shí)現(xiàn)的功能

docker build +

docker pull

docker images

docker ps

docker run

docker exec

docker logs

docker commit

docker rm / docker rmi

Networking

Quota Support / CGroups

+bocker init 提供了有限的 bocker build 能力

四、總結(jié)

到此本文要介紹的內(nèi)容就結(jié)束了,正如開篇我們提到的,寫出最終的腳本實(shí)現(xiàn)這樣一個(gè)小玩意并沒有什么實(shí)用價(jià)值,真正的價(jià)值是我們通過100行左右的腳本,以交互式的方式去理解Docker的核心技術(shù)點(diǎn)。在工作中與容器打交道時(shí)能有更多的思路去排查、解決問題。

以上就是教你用100 行shell實(shí)現(xiàn)Docker詳解的詳細(xì)內(nèi)容,更多關(guān)于shell實(shí)現(xiàn)Docker的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • docker: invalid reference format.問題

    docker: invalid reference format.問題

    這篇文章主要介紹了docker: invalid reference format.問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-01-01
  • Docker 查看鏡像信息的方法

    Docker 查看鏡像信息的方法

    這篇文章主要介紹了Docker 查看鏡像信息的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-12-12
  • 解決docker修改mysql配置文件的問題

    解決docker修改mysql配置文件的問題

    今天在用docker啟動(dòng)一個(gè)5.7的數(shù)據(jù)庫在建表時(shí)候遇到一個(gè)問題,但是很快就解決了,下面小編給大家講解下docker怎么修改mysql內(nèi)部的配置,感興趣的朋友跟隨小編一起看看吧
    2022-10-10
  • 基于Docker搭建ELK 日志系統(tǒng)的方法

    基于Docker搭建ELK 日志系統(tǒng)的方法

    Beats,它是一個(gè)輕量級(jí)的日志收集處理工具(Agent),占用資源少,適合于在各個(gè)服務(wù)器上搜集日志后傳輸給Logstash,官方也推薦此工具,本文重點(diǎn)給大家介紹Docker 搭建 ELK 日志系統(tǒng)的方法,感興趣的朋友一起看看吧
    2021-05-05
  • 基于Docker鏡像部署go項(xiàng)目的方法步驟

    基于Docker鏡像部署go項(xiàng)目的方法步驟

    這篇文章主要介紹了基于Docker鏡像部署go項(xiàng)目的方法步驟,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-04-04
  • Docker相關(guān)命令應(yīng)用匯總

    Docker相關(guān)命令應(yīng)用匯總

    如果各位看官熟悉 Git 和 GitHub ,可與 Docker 做個(gè)類比,可更加容易理解 Docker 和 Docker Hub 及兩者關(guān)系。
    2018-04-04
  • 使用dockerfile構(gòu)建nginx鏡像的方法示例

    使用dockerfile構(gòu)建nginx鏡像的方法示例

    這篇文章主要介紹了使用dockerfile構(gòu)建nginx鏡像的方法示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2018-09-09
  • Docker清理命令之如何刪除所有的鏡像和容器

    Docker清理命令之如何刪除所有的鏡像和容器

    這篇文章主要介紹了Docker清理命令之如何刪除所有的鏡像和容器問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-05-05
  • Docker配置WebSSH的實(shí)現(xiàn)

    Docker配置WebSSH的實(shí)現(xiàn)

    本文主要介紹了Docker配置WebSSH的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-03-03
  • 基于docker安裝tensorflow的完整步驟

    基于docker安裝tensorflow的完整步驟

    TensorFlow 隨著AlphaGo的勝利也火了起來。 下面這篇文章主要給大家介紹了關(guān)于基于docker安裝tensorflow的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。
    2018-02-02

最新評(píng)論