内容简介:最近逛github无意发现了一个很好地项目我当时一看
最近逛github无意发现了一个很好地项目 bocker , 用上百行的代码就实现了一个简易的docker,然后我看了一下,觉得挺有趣的,简单的玩了一下,也做一些更改(项目很久不更新了,有不支持的地方),简单分析了一下分享出来。
前言
我当时一看 100 行写docker, 肯定是不可能,以前看像最简化的 python 加上依赖也得几百行代码如 moker ,还有 go 实现的完善一点的也有上千行 mydocker ,可是这个项目看了一下,还真是只有100多行,不过看使用的是 shell , 不过想起来100多行应该也只能用 shell 完成了吧,不熟悉shell的可以去看一些shell的基本知识就可以了。
目前这个项目主要实现里 镜像拉取,镜像查看,容器启动,容器删除,容器查看,容器资源限制,镜像删除 ,功能都是一些最基本的,也有很多不完善的,我这里大致分析一下他们是的实现原理,分析各个流程,按照操作的顺序正常分析,首先这里讨论的情况是 linux 环境,推荐使用centos7和ubuntu14以上的系统,流程其实比较简单,底层实现依赖于linux的一些基础组件iptables,cgroup和linux namespace完成网络,资源限制,资源隔离,利用shell去管理这些资源。
开始操作!!
配置环境
最好是vagrant (如果是mac和windows建议使用该环境,如果linux,系统内核较高则可直接操作), vagrant 可以帮我们实现轻量级的开发环境,个人非常喜欢,它操作和管理vm,处理更重环境会比较方便,这里需要提前配置好环境,我在链接中附上了官方地址,按照教程配置即可。
官方Vagrantfile的epel数据源有问题,而且网络依赖,整个过程是自动化的,不过不方便调试,这里为了方便个人调试,我将流程写为一步一步的了,操作起来也会比较方便。
加载虚拟环境(vagrant配置文件)
生成Vagrant配置文件
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 (直接进入) 复制代码
安装依赖
- 安装rpm源:
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 复制代码
- 创建挂载文件系统:(docker镜像支持的一种文件结构) 具体细节可以看链接btrfs wiki
fallocate -l 10G ~/btrfs.img mkdir /var/bocker mkfs.btrfs ~/btrfs.img mount -o loop ~/btrfs.img /var/bocker 复制代码
- 安装base:
pip install git+https://github.com/larsks/undocker systemctl start docker.service docker pull centos docker save centos | undocker -o base-image 复制代码
- 安装linux-utils 一个linux的工具
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函数,根据传入的参数执行不同的函数。
- 运行环境 ? 镜像拉去 bocker pull ()
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转存到自己的唯一镜像目录中,同时保存一个镜像名为一个文件。
- bocker保存镜像
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是支持多种存储系统的,具体详情可以到这里看
。
- 有了镜像就可以进行重要的bocker run 了(第一部分)
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中, 对其 绑定i p,mac地址,然后绑定路由,启动网卡,网络接口,这里用到的veth对,你可以再简单的理解为一跟网线连接,图解一下。
那么这根网线的 两端这里一端是namespace中的设备 , 另外一端则是宿主机 ,这里结构图解析一下,可以看到docker有个eth0,主机有个veth,他们就是一个veth对。
这样就能让容器里边的bocker正常上网了。
- bocker run 资源限制(第二部分)
# @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出相应网卡删除对应的网络接口即可
- 查看容器日志 bocker logs
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,相信也不会很难。
以上所述就是小编给大家介绍的《关于如何用100行如何实现docker》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- php如何实现session,自己实现session,laravel如何实现session
- AOP如何实现及实现原理
- webpack 实现 HMR 及其实现原理
- Docker实现原理之 - OverlayFS实现原理
- 为什么实现 .NET 的 ICollection 集合时需要实现 SyncRoot 属性?如何正确实现这个属性?
- 自己实现集合框架(十):顺序栈的实现
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Pro JavaScript Techniques
John Resig / Apress / 2006-12-13 / USD 44.99
Pro JavaScript Techniques is the ultimate JavaScript book for the modern web developer. It provides everything you need to know about modern JavaScript, and shows what JavaScript can do for your web s......一起来看看 《Pro JavaScript Techniques》 这本书的介绍吧!