最近逛github無心發現了一個很好地項目bocker, 用上百行的代碼就實現了一個簡易的docker,而後我看了一下,以爲挺有趣的,簡單的玩了一下,也作一些更改(項目好久不更新了,有不支持的地方),簡單分析了一下分享出來。php
前言html
我當時一看100行寫docker, 確定是不可能,之前看像最簡化的python加上依賴也得幾百行代碼如moker,還有go實現的完善一點的也有上千行mydocker,但是這個項目看了一下,還真是隻有100多行,不過看使用的是shell, 不過想起來100多行應該也只能用shell完成了吧,不熟悉shell的能夠去看一些shell的基本知識就能夠了。python
目前這個項目主要實現裏鏡像拉取,鏡像查看,容器啓動,容器刪除,容器查看,容器資源限制,鏡像刪除,功能都是一些最基本的,也有不少不完善的,我這裏大體分析一下他們是的實現原理,分析各個流程,按照操做的順序正常分析,首先這裏討論的狀況是linux環境,推薦使用centos7和ubuntu14以上的系統,流程其實比較簡單,底層實現依賴於linux的一些基礎組件iptables,cgroup和linux namespace完成網絡,資源限制,資源隔離,利用shell去管理這些資源。linux
開始操做!!git
配置環境github
最好是vagrant (若是是mac和windows建議使用該環境,若是linux,系統內核較高則可直接操做), vagrant能夠幫咱們實現輕量級的開發環境,我的很是喜歡,它操做和管理vm,處理更重環境會比較方便,這裏須要提早配置好環境,我在連接中附上了官方地址,按照教程配置便可。docker
官方Vagrantfile的epel數據源有問題,並且網絡依賴,整個過程是自動化的,不過不方便調試,這裏爲了方便我的調試,我將流程寫爲一步一步的了,操做起來也會比較方便。shell
加載虛擬環境(vagrant配置文件)json
生成Vagrant配置文件ubuntu
Vagrant配置啓動
$script = <<SCRIPT
(
echo "echo start---config"
) 2>&1
SCRIPT
Vagrant.configure(2) do |config|
config.vm.box = 'puppetlabs/centos-7.0-64-nocm'
config.ssh.username = 'root'
config.ssh.password = 'puppet'
config.ssh.insert_key = 'true'
config.vm.provision 'shell', inline: $script
end
拷貝上邊的文件Vim爲保存到一個文件中Vagrantfile中
vagrant up (直接啓動,這裏會去源拉去centos的鏡像,時長主要根據我的網絡)
vagrant ssh (直接進入)
複製代碼
安裝依賴
wget https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
rpm -ivh epel-release-latest-7.noarch.rpm(官方用的eprl源不存在了)
複製代碼
核心是cgourp, btrfs-progs
yum install -y -q autoconf automake btrfs-progs docker gettext-devel git libcgroup-tools libtool python-pip
jq
複製代碼
fallocate -l 10G ~/btrfs.img
mkdir /var/bocker
mkfs.btrfs ~/btrfs.img
mount -o loop ~/btrfs.img /var/bocker
複製代碼
pip install git+https://github.com/larsks/undocker
systemctl start docker.service
docker pull centos
docker save centos | undocker -o base-image
複製代碼
git clone https://github.com/karelzak/util-linux.git
cd util-linux
git checkout tags/v2.25.2
./autogen.sh
./configure --without-ncurses --without-python
make
mv unshare /usr/bin/unshare
複製代碼
echo 1 > /proc/sys/net/ipv4/ip_forward
iptables --flush
iptables -t nat -A POSTROUTING -o bridge0 -j MASQUERADE
iptables -t nat -A POSTROUTING -o enp0s3 -j MASQUERADE
ip link add bridge0 type bridge
ip addr add 10.0.0.1/24 dev bridge0
ip link set bridge0 up
複製代碼
我簡單解釋一下上邊的流程,因爲docker底層網絡會利用iptables和linux namespace實現,這裏是爲了讓容器網絡正常工做,主要分爲2部分。
1 首先須要建立一塊虛擬網卡bridge0,而後配置bridge0網卡的nat地址轉換,這裏bridge至關於docker中的docker0,bridge0至關於在網絡中的交換機二層設備,他能夠鏈接不一樣的網絡設備,當請求到達Bridge設備時,能夠經過報文的mac地址進行廣播和轉發,因此全部的容器虛擬網卡須要在bridge下,這也是鏈接namespace中的網絡設備和宿主機網絡的方式,這裏下變會有講解。(若是須要實現overlay等,須要換用更高級的轉換工具,如用ovs來作類vxlan,gre協議轉換)
2 開啓開啓內核轉發和配置iptables MASQUERADE,這是爲了用MASQUERADE規則將容器的ip轉換爲宿主機出口網卡的ip,在linux namespace中,請求宿主機外部地址時,將namespace中的原地址換成宿主機做爲原地址,這樣就能夠在namespace中進行地址正常轉換了。
環境準備完成,能夠分析下具體實現了
首先想一下,對docker來說最重要的就是幾部分,一個是鏡像,第二個是獨立的環境,ip,網絡,第三個是資源限制。
這裏我在代碼中增長了一些中文註釋方便理解,這個項目叫bocker,我也叫bocker吧
[[ -z "${1-}" ]] && bocker_help "$0"
# @1 執行與help
case $1 in
pull|init|rm|images|ps|run|exec|logs|commit|cleanup) bocker_"$1" "${@:2}" ;;
*) bocker_help "$0" ;;
esac
複製代碼
help比較簡單,程序入口,邏輯至關因而咱們程序裏面的main函數,根據傳入的參數執行不一樣的函數。
function bocker_pull() { #HELP Pull an image from Docker Hub:\nBOCKER pull <name> <tag>
# @1 獲取對應鏡像進行拉去, 源代碼老版本是v1的docker registry是無效的, 我更新爲了v2版本
token=$(curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/$1:pull" | jq '.token'| sed 's/\"//g')
registry_base='https://registry-1.docker.io/v2'
tmp_uuid="$(uuidgen)" && mkdir /tmp/"$tmp_uuid"
# @2 獲取docker鏡像每一層的layter,保存到數組中
manifest=$(curl -sL -H "Authorization: Bearer $token" "$registry_base/library/$1/manifests/$2" | jq -r '.fsLayers' | jq -r '.[].blobSum' )
[[ "${#manifest[@]}" -lt 1 ]] && echo "No image named '$1:$2' exists" && exit 1
# @3 依次獲取鏡像每一層, 而後init
for id in ${manifest[@]}; do
curl -#L -H "Authorization: Bearer $token" "$registry_base/library/$1/blobs/$id" -o /tmp/"$tmp_uuid"/layer.tar
tar xf /tmp/"$tmp_uuid"/layer.tar -C /tmp/"$tmp_uuid"
done
echo "$1:$2" > /tmp/"$tmp_uuid"/img.source
bocker_init /tmp/"$tmp_uuid" && rm -rf /tmp/"$tmp_uuid"
}
複製代碼
這個項目簡易的實現了docker,因此docker鏡像倉庫確定是沒有實現的,鏡像倉庫仍是使用官方源,這裏若是須要使用本身私有源,須要對鏡像源和代碼都作變動,這裏其實邏輯是下載對應鏡像每一個分層,而後轉存到本身的文件鏡像存儲中,這裏我更改了他的邏輯,使用了docker registry api v2版本,(由於做者源v1版本代碼已經失效,從官方不能獲取正確數據,做者其實已經三年未提交了,docker發展速度太快,也能夠理解),流程是首先是auth,獲取對應鏡像對應權限的進行一個token,而後利用token獲取到鏡像的每個layer,這裏我用了jq json解析插件,會比較方便的操做Jason,轉爲shell相關變量,而後下載全部的layer轉存到本身的惟一鏡像目錄中,同時保存一個鏡像名爲一個文件。
function bocker_init() { #HELP Create an image from a directory:\nBOCKER init
# @1 生成隨機數鏡像,就像生成docker images 惟一id
uuid="img_$(shuf -i 42002-42254 -n 1)"
if [[ -d "$1" ]]; then
[[ "$(bocker_check "$uuid")" == 0 ]] && bocker_run "$@"
# @2 建立對應image文件 btrfs volume
btrfs subvolume create "$btrfs_path/$uuid" > /dev/null
cp -rf --reflink=auto "$1"/* "$btrfs_path/$uuid" > /dev/null
[[ ! -f "$btrfs_path/$uuid"/img.source ]] && echo "$1" > "$btrfs_path/$uuid"/img.source
echo "Created: $uuid"
else
echo "No directory named '$1' exists"
fi
}
複製代碼
這裏其實就是保存從鏡像倉庫拉取下來的layer,而後建立目錄,這裏須要強調的是docker使用的鏡像目錄在這裏必須是btrfs的文件結構,而後保存對應的鏡像名到img.source文件中 ,這裏環境準備的時候經過btrfs命令建立了10g的文件系統,docker是支持多種存儲系統的,具體詳情能夠到這裏看
。
function bocker_run() { #HELP Create a container:\nBOCKER run <image_id> <command>
# @1 環境準備,生成惟一id,檢查相關鏡像,ip, mac地址
uuid="ps_$(shuf -i 42002-42254 -n 1)"
[[ "$(bocker_check "$1")" == 1 ]] && echo "No image named '$1' exists" && exit 1
[[ "$(bocker_check "$uuid")" == 0 ]] && echo "UUID conflict, retrying..." && bocker_run "$@" && return
cmd="${@:2}" && ip="$(echo "${uuid: -3}" | sed 's/0//g')" && mac="${uuid: -3:1}:${uuid: -2}"
# @2 經過ip link && ip netns 實現隔離的網絡namespace與網絡通訊
ip link add dev veth0_"$uuid" type veth peer name veth1_"$uuid"
ip link set dev veth0_"$uuid" up
ip link set veth0_"$uuid" master bridge0
ip netns add netns_"$uuid"
ip link set veth1_"$uuid" netns netns_"$uuid"
ip netns exec netns_"$uuid" ip link set dev lo up
ip netns exec netns_"$uuid" ip link set veth1_"$uuid" address 02:42:ac:11:00"$mac"
ip netns exec netns_"$uuid" ip addr add 10.0.0."$ip"/24 dev veth1_"$uuid"
ip netns exec netns_"$uuid" ip link set dev veth1_"$uuid" up
ip netns exec netns_"$uuid" ip route add default via 10.0.0.1
btrfs subvolume snapshot "$btrfs_path/$1" "$btrfs_path/$uuid" > /dev/null
複製代碼
解析:
在運行bocker run時會進行一些列配置,我在也加了也進行了註釋,第一部先生成相關配置,首先會經過shuf函數生成每一個bocker惟一的Id,進行相關合法性檢驗,而後根據生成的截取生成的隨機數id,截取部分字段組成ip地址和mac地址(注意這裏可能會有機率ip衝突,後期應該須要優化)。
第二部分,生成Linux veth對(Veth是成對的出如今虛擬網絡設備,發送動Veth虛擬設備的請求會從另外一端的虛擬設備發出,在容器的虛擬化場景中,常常會使用Veth鏈接不一樣的namespace) , 利用ip命令建立veth對 veth0_xx, veth1_xx,建立惟一uuid namespace, 綁定veth1到namespace中, 對其綁定ip,mac地址,而後綁定路由,啓動網卡,網絡接口,這裏用到的veth對,你能夠再簡單的理解爲一跟網線鏈接,圖解一下。
那麼這根網線的兩端這裏一端是namespace中的設備,另一端則是宿主機,這裏結構圖解析一下,能夠看到docker有個eth0,主機有個veth,他們就是一個veth對。
這樣就能讓容器裏邊的bocker正常上網了。
# @3 更改nameserver, 保存cmd
echo 'nameserver 8.8.8.8' > "$btrfs_path/$uuid"/etc/resolv.conf
echo "$cmd" > "$btrfs_path/$uuid/$uuid.cmd"
# @4 經過cgroup-tools工具配置cgroup資源組與調整資源限制
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"
# @5 執行
cgexec -g "$cgroups:$uuid" \
ip netns exec netns_"$uuid" \
unshare -fmuip --mount-proc \
chroot "$btrfs_path/$uuid" \
/bin/sh -c "/bin/mount -t proc proc /proc && $cmd" \
2>&1 | tee "$btrfs_path/$uuid/$uuid.log" || true
ip link del dev veth0_"$uuid"
ip netns del netns_"$uuid"
複製代碼
這裏爲了簡便操做,使用了cgroup工具進行資源限制,cgroup是linux 自帶的進程資源限制工具,連接中有對應詳情。這裏利用了cgroup-tools工具操做cgroup會比較簡便,在這裏利用cgcreate增長了CPU,set, mem進行限制,經過隨機建立的id建立cgroup組,cgset默認增長了CPU, mem的參數限制(若是是程序開發的話會對應的依賴封裝庫)
下圖能夠看到其實cgroup對應的數據都是存文件,保存在目錄中的。
最後使用 cgroup exec執行啓動執行程序,將輸出經過tee輸出到日誌目錄。
當程序執行結束,刪除對應的網絡接口和命名空間,清楚網絡接口是爲了方便將綁定在主機上的虛擬網卡刪除
這裏一個bocker run就能夠實現了,下邊的是一些細節了
function bocker_cleanup() { #HELP Delete leftovers of improperly shutdown containers:\nBOCKER cleanup
# @1 清楚全部的相關網絡接口
for ns in $(ip netns show | grep netns_ps_); do [[ ! -d "$btrfs_path/${ns#netns_}" ]] && ip netns del "$ns"; done
for iface in $(ifconfig | grep veth0_ps_ | awk '{ print $1 }'); do [[ ! -d "$btrfs_path/${iface#veth0_}" ]] && ip link del dev "$iface"; done
}
複製代碼
ps出相應網卡刪除對應的網絡接口便可
function bocker_logs() { #HELP View logs from a container:\nBOCKER logs <container_id>
# @1 查看日誌
[[ "$(bocker_check "$1")" == 1 ]] && echo "No container named '$1' exists" && exit 1
cat "$btrfs_path/$1/$1.log"
}
複製代碼
全部的日誌在都是保存在btrfs文件系統對應的子目錄中$btrfs_path/$uuid中,這裏對應到btrfs_path,因此只須要獲取到正確的目錄,cat出文件便可
還有幾個簡單命令我就不分析了,比較簡單,能夠本身去看開頭給的連接,下載源碼對應我文中的代碼更改。
總結:
總體來講,這個項目利用了shell的優點,實現了一小部分docker的主要功能,框架是有了,還有99%的功能沒有實現,好比跨主機通訊,端口轉發,端口映射,異常處理等等,不過做爲學習的項目來講,可讓人眼前一亮,你們也能夠根據這個項目的思路去實現一個簡單的docker,相信也不會很難。