最近逛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,相信也不会很难。