docker源码分析
A. docker 源码分析 怎么用
本文根据docker官方给出的docker代码编译环境搭建指团岁南做更深入的分手或谨析。官方给出的指导比较简单,但是由于国内的网络问题经常会编译失败,了解了编译步骤后,也可以结合自身遇到的网络问题进行毕基“规避”。
B. 《Docker源码分析》epub下载在线阅读全文,求百度网盘云资源
《Docker源码分析》(孙宏亮)电子书网盘下载免费在线阅读
链接:
书名:Docker源码分析
豆瓣评分:6.4
作者:孙宏亮
出版社:机械工业出版社
出版年:2015-8-1
页数:264
内容简介
本书是一本引导读者深入了解Docker实现原理的技术普及读物,主要目标是通过对Docker架构和源代码的详细讲解和解剖,帮助读者对Docker的底层实现有一个全面的理解。
作者通过大量的流程图和代码片段对Docker的架构、Docker的重要模块,特别是对Swarm、Machine和Compose这三个模块进行了详细介绍和深度剖析,无论是Docker的使用者还是开发者,通过阅读此书都可以对Docker有更深刻的理解,能够更好的使用或者开发Docker。
作者简介
孙宏亮
硕士,浙江大学毕业,现为DaoCloud软件工程师,主要负责企业级容器云平台的研发工作。数年来一直从事云计算、PaaS领域的研究与实践,是国内较早一批接触Docker的先行者,同时也是Docker技术的推广者。
C. 如何系统地学习Docker
1Docker 技术可谓是近年最火热的技术之一,铺天盖地的技术论坛和各种讲座,大家都在分享关于如何容器化及如何使用Docker优化自己运维和开发流程的经验。随着Docker技术的逐渐普及,使用Docker已经不再是一个难题。现在更加重要的是生产环境容器化的最佳实践,段姿另外就是容器的编排框架之争。但是,对于技术人员来说,除去Docker 外表的繁华外,什么是容器,容器到底是怎么创建的,容器底层的技术探秘也是非常重要的。2014年开始接触 Docker的时候,经历了从最初的新奇—感叹竟然还有Docker 这样的好工具,到逐渐熟悉Docker的各种功能,尝试在生产环境中使用Docker技术的过程。但是,每每被人问到:“Docker技术到底是怎么实现的呢”我只能粗粗浅浅地说:“Docker是使用linux Kernel的Namespace 和 Cgroups实现的一种容器技术。”那么,什么是Namespace,什么是Cgroups,Docker是怎么使用它们的,容器到底是怎么一步步被创建出来的.问到这些,我就会支支吾吾地不知所以。蚂橡由此可见,了解容器技术的底层技术,然后明白它们是如何工作的,尤为重要,这些才是整个容器技术的基石,掌握了这些基石才能更加容易地向上攀登。
2从docker的用途上来考虑,看docker能否解决你工作中遇到的问题。例如在实际开发过程中,经常遇到的持续集成问题,软件开发,测试,部署,如何做成自动化的。配合github Jenkins和docker实现自动化部署,系统持续集成。docker最重要的是编排,如何合理有效稳定的管理各个容器,调度容器。可以看看k8s,自己动手搭建握物绝跑跑看。docker本身并没有引入新的技术,都是在linux原有的基础上做的融合。可以关注docker源码分析。UCloud也支持Docker,推出了UDocker产品。UCloud - 专业云计算服务商。
D. 如何学习Docker
如何学习Docker
对于在校学生而言,应该如何去学习docker?毕竟学校没有具体的应用需求作为引导,所以应该如何去研究Docker?还有,Docker的源代码有没有必要去研究?
首先我说明下,我是一位在浙江大学VLIS实验室云计算项目组的学生,使用过Docker,研究过Docker及其源码,也定制过Docker。
对于学生如何学习Docker,我认为首先要看一下学生个人的知识背景、能利用的资源资源、以及个人兴趣和发展方向。
1.学习Docker,如果没有云计算的基本知识,以及内核的基本知识,那么学习并理解起来会稍吃力。作为容器,Docker容器的优势在哪,不足在哪,最好了解容器的实现是怎样的(简单了解);拥有镜像管理,Docker又该如何体现软件开发,集成,部署,发布,再迭代的软件生命周期管理优势。以上两点我认为最为关键,有这两方面的认识势必会对之后的工作帮助巨大。
2.关于学习资源,起码的硬件设施总是要有的。Docker及其生态的发展很快,不使用纯理论肯定收效甚微。另外,资源还包括Docker官方,各大电子媒体平台,技术论坛,开源社区等,往往大拿的观点能点破自己的困惑,或者让自己知道哪方面的认识还很欠缺,以及让自己少走很多的弯路。
3.个人兴趣的话,归结为强扭的瓜不甜。起码应该认同Docker的设计价值,以及Docker的未来潜力,当然有依据的批判Docker并带动大家的思考,也是深切关注的表现。
4.个人发展方向,我认为如果需要把Docker当作软件生命周期管理工具的话,那用好Docker最为重要,API及命令的理解与使用是必需的。如果专注系统设计方面,那么除Docker以上的知识与经验之外,若有Docker源码的学习与理解,那么这些肯定会让你的Docker水平提高一个层次。
2014-11-21 8 0
xds2000
学习Docker,最大的好处是跟进新技术发展方向。我觉得在校生应该没有多少硬性需求在Docker的研究上,这也是为什么学校没做具体应用要求的原因。最实际的做法是看一些Docker使用案例,自己实践出一些经验应该会再以后的社会实践中起到作用。
研究docker的源代码,应该到你下定决心从事云计算方面的事业或者研究,那么你就需要以研究者的身份去做仔细的源码分析的工作。
2014-11-21 3 0
刘勃GTDer
我作为参加工作的过来人来说,我认为只有你真正参加工作后,在工作中学习跟有意义,毕竟Docker知识云计算其中的一个软件平台而已,说不来等你毕业了,新的技术出现Docker不一定是唯一选择。
作为学生了解新技术确实无可厚非,一定要能把理论转化为生产力才是正道。
2014-12-15 3 0
9lives - 爱生活,爱云计算。
学习任何一个开源新技术,首先问自己几个问题:
1. 为什要学习它?
2. 学习它需要了解哪些相关知识点?
3. 如何快速学习?
4. 该技术的使用场景是什么?
拿我个人的学习经验来举例(本人之前比较了解OpenStack)
为什要学习docker?
回答:
docker是轻量级虚拟化技术,docker使linux容器技术的应用更加简单和标准化
docker的速度很快,容器启动时毫秒级的
docker将开发和运维职责分清
docker解决了依赖地狱问题
docker支持几乎所有操作系统
docker有着飞速发展的生态圈
很多IT巨头逐渐加入和支持
学习它需要了解哪些相关知识点?
回答:
云计算概念相关(restapi, 微服务,OpenStack)
Linux 系统管理(软件包管理,用户管理,进程管理等)
Linux 内核相关(Cgroup, namespace 等)
Linux 文件系统和存储相关(AUFS,BRFS,devicemapper 等)
Linux 网络(网桥,veth,iptables等)
Linux安全相关(Appmor,Selinux 等)
Linux进程管理(Supervisord,Systemd etc)
Linux容器技术(LXC等)
开发语言(Python, GO,Shell 等)
3.如何快速学习?
回答:个人体会最好有一个实际的需求或项目来边实践边学习,入门可以参考(第一本docker书)写的不错,非常适合入门。除此之外,阅读牛人的blog比如官方blog http://blog.docker.com/
最后,参与社区互动也是很好的学习方式。
该技术的使用场景是什么? 回答:docker非常适用于dev/test CI/CD 场景,用完就扔。还有就是PasS了。
欢迎大家讨论。
2015-05-21 3 0
西弗尔 - 要么牛逼,要么滚蛋
你好!我也是在校的学生,也在自己学习docker,多多交流啊!
2015-05-23 1 1
田浩浩 - wizmacau developer
https:// github.com /llitfkitfk/docker-tutorial-cn/
BTW: 熟读docker文档
2014-11-21 0 0
tuxknight
楼上各位说的都很好,我再补充一点:
找份相关的实习工作
2015-07-22 0 0
lancer
工作和研究是两个方向我个人认为,工作需要通过你的实际效能为企业带来经济效益,而研究的话可以专注某个点。但是研究离不开工作,因为工作可以让你更好的理会技术带来的价值,以及如何提供更好的服务,用户使用场景需要那些技术的突破。有了这些认识,然后更加专注的研究某个技术点,这样或许可以说技术和商业是分不开的。
2015-08-25 0 0
绿剑色影
docker现在十分火热,值得学习一下。
E. libcontainer位于哪个目录
libcontainer 是Docker中用于容器管理的包,它基于Go语言实现,通过管理namespaces、cgroups、capabilities以及文件系统来进行容器控制。你可以使用libcontainer创建容器,并对容器进行生命周期管理。
容器是一个可管理的执行环境,与主机系统共享内核,可与系统中的其他容器进行隔离。
在2013年Docker刚发布的时候,它是一款基于LXC的开源容器管理引擎。把LXC复杂的容器创建与使用方式简化为Docker自己的一套命令体系。随着Docker的不断发展,它开始有了更为远大的目标,那就是反向定义容器的实现标准,将底层实现都抽象化到libcontainer的接口。这就意味着,底层容器的实现方式变成了一种可变的方案,无论是使用namespace、cgroups技术抑或是使用systemd等其他方案,只要实现了libcontainer定义的一组接口,Docker都可以运行。这也为Docker实现全面的跨平台带来了可能。
1.libcontainer 特性
目前版本的libcontainer,功能实现上涵盖了包括namespaces使用、cgroups管理、Rootfs的配置启动、默认的Linux capability权限集、以及进程运行的环境变量配置。内核版本最低要求为2.6,最好是3.8,这与内核对namespace的支持有关。 目前除user namespace不完全支持以外,其他五个namespace都是默认开启的,通过clone系统调用进行创建。
1.1 建立文件系统
文件系统方面,容器运行需要rootfs。所有容器中要执行的指令,都需要包含在rootfs(在Docker中指令包含在其上叠加的镜像层也可以执行)所有挂载在容器销毁时都会被卸载,因为mount namespace会在容器销毁时一同消失。为了容器可以正常执行命令,以下文件系统必须在容器运行时挂载到rootfs中。
当容器冲仿的文件系统刚挂载完毕时,/dev文件系统会被一系列设备节点所填充,所以rootfs不应该管理/dev文件系统下的设备节点,libcontainer会负责处理并正确启动这些设备。设备及其权限模式如下。
容器支持伪终端TTY,当用户使用时,就会建立/dev/console设备。其他终端支持设备,如/dev/ptmx则是宿主机的/dev/ptmx 链接。容器中指向宿主机 /dev/null的IO也会被重定向到容器内的 /dev/null设备。当/proc挂载完成后,/dev/中与IO相关的链接也会建立,如下表。
pivot_root 则用于改变进程的根目录,这样可以有效的将进程控制在我们建立的rootfs中。如果rootfs是基于ramfs的(不支持pivot_root),那么会在mount时使用MS_MOVE标志位加上chroot来顶替。 当文件系统创建完毕后,umask权限被重新设置回0022。
1.2 资源管理
在《Docker背后的内核知识:cgroups资源隔离》一文中已经提到,Docker使用cgroups进行资源管理与限制,包括设备、内存、CPU、输入输出等。 目前除网络外所有内核支持的子系统都被加入到libcontainer的管理中,所以libcontainer使用cgroups原生支持的统计信息作为资源管理的监控展示。 容器中运行的第一个进程init,必须在初始化开始前放置到指定的cgroup目录中,这样就能防止初始型判旦化完成后运行的其他用户指令逃逸出cgroups的控制。父子进程的同步则通过管道来完成,在随后的运行时初始化中会进行展开描述。
1.3 可配置的容器安全
容器安全一直是被广泛探讨的话题,使用namespace对进程进行隔离是容器安全的基础,遗憾的是,usernamespace由于设计上的复杂性,还没有被libcontainer完全支持。 libcontainer目前可通过配置capabilities、SELinux、apparmor 以及seccomp进行一定的安全防范,目前除seccomp以外都有一份默认的配置项提供给用户作为参考。 在本系列的后续文章中,我们将对容器安全进行更深入的探讨,敬请期待。
1.4 运行时与初始化进程
在容器创建过程中,父进程需要与容器的init进程进行同步通信,通信的方式则通过卜扰向容器中传入管道来实现。当init启动时,他会等待管道内传入EOF信息,这就给父进程完成初始化,建立uid/gid映射,并把新进程放进新建的cgroup一定的时间。 在libcontainer中运行的应用(进程),应该是事先静态编译完成的。libcontainer在容器中并不提供任何类似Unix init这样的守护进程,用户提供的参数也是通过exec系统调用提供给用户进程。通常情况下容器中也没有长进程存在。 如果容器打开了伪终端,就会通过p2把console作为容器的输入输出(STDIN, STDOUT, STDERR)对象。 除此之外,以下4个文件也会在容器运行时自动生成。 * /etc/hosts * /etc/resolv.conf * /etc/hostname * /etc/localtime
1.5 在运行着的容器中执行新进程
用户也可以在运行着的容器中执行一条新的指令,就是我们熟悉的docker exec功能。同样,执行指令的二进制文件需要包含在容器的rootfs之内。 通过这种方式运行起来的进程会随容器的状态变化,如容器被暂停,进程也随之暂停,恢复也随之恢复。当容器进程不存在时,进程就会被销毁,重启也不会恢复。
1.6 容器热迁移(Checkpoint & Restore)
目前libcontainer已经集成了CRIU作为容器检查点保存与恢复(通常也称为热迁移)的解决方案,应该在不久之后就会被Docker使用。也就是说,通过libcontainer你已经可以把一个正在运行的进程状态保存到磁盘上,然后在本地或其他机器中重新恢复当前的运行状态。这个功能主要带来如下几个好处。
服务器需要维护(如系统升级、重启等)时,通过热迁移技术把容器转移到别的服务器继续运行,应用服务信息不会丢失。
对于初始化时间极长的应用程序来说,容器热迁移可以加快启动时间,当应用启动完成后就保存它的检查点状态,下次要重启时直接通过检查点启动即可。
在高性能计算的场景中,容器热迁移可以保证运行了许多天的计算结果不会丢失,只要周期性的进行检查点快照保存就可以了。
要使用这个功能,需要保证机器上已经安装了1.5.2或更高版本的criu工具。不同Linux发行版都有criu的安装包,你也可以在CRIU官网上找到从源码安装的方法。我们将会在nsinit的使用中介绍容器热迁移的使用方法。 CRIU(Checkpoint/Restore In Userspace)由OpenVZ项目于2005年发起,因为其涉及的内核系统繁多、代码多达数万行,其复杂性与向后兼容性都阻碍着它进入内核主线,几经周折之后决定在用户空间实现,并在2012年被Linus加并入内核主线,其后得以快速发展。 你可以在CRIU官网查看其原理,简单描述起来可以分为两部分,一是检查点的保存,其中分为3步。
收集进程与其子进程构成的树,并冻结所有进程。
收集任务(包括进程和线程)使用的所有资源,并保存。
清理我们收集资源的相关寄生代码,并与进程分离。
第二部分自然是恢复,分为4步。
读取快照文件并解析出共享的资源,对多个进程共享的资源优先恢复,其他资源则随后需要时恢复。
使用fork恢复整个进程树,注意此时并不恢复线程,在第4步恢复。
恢复所有基础任务(包括进程和线程)资源,除了内存映射、计时器、证书和线程。这一步主要打开文件、准备namespace、创建socket连接等。
恢复进程运行的上下文环境,恢复剩下的其他资源,继续运行进程。
至此,libcontainer的基本特性已经预览完毕,下面我们将从使用开始,一步步深入libcontainer的原理。
2. nsinit与libcontainer的使用
俗话说,了解一个工具最好的入门方式就是去使用它,nsinit就是一个为了方便不通过Docker就可以直接使用libcontainer而开发的命令行工具。它可以用于启动一个容器或者在已有的容器中执行命令。使用nsinit需要有 rootfs 以及相应的配置文件。
2.1 nsinit的构建
使用nsinit需要rootfs,最简单最常用的是使用Docker busybox,相关配置文件则可以参考sample_configs目录,主要配置的参数及其作用将在配置参数一节中介绍。拷贝一份命名为container.json文件到你rootfs所在目录中,这份文件就包含了你对容器做的特定配置,包括运行环境、网络以及不同的权限。这份配置对容器中的所有进程都会产生效果。 具体的构建步骤在官方的README文档中已经给出,在此为了节省篇幅不再赘述。 最终编译完成后生成nsinit二进制文件,将这个指令加入到系统的环境变量,在busybox目录下执行如下命令,即可使用,需要root权限。 nsinit exec --tty --config container.json /bin/bash 执行完成后会生成一个以容器ID命名的文件夹,上述命令没有指定容器ID,默认名为”nsinit”,在“nsinit”文件夹下会生成一个state.json文件,表示容器的状态,其中的内容与配置参数中的内容类似,展示容器的状态。
2.2 nsinit的使用
目前nsinit定义了9个指令,使用nsinit -h就可以看到,对于每个单独的指令使用--help就能获得更详细的使用参数,如nsinit config --help。 nsinit这个命令行工具是通过cli.go实现的,cli.go封装了命令行工具需要做的一些细节,包括参数解析、命令执行函数构建等等,这就使得nsinit本身的代码非常简洁明了。具体的命令功能如下。
config:使用内置的默认参数加上执行命令时用户添加的部分参数,生成一份容器可用的标准配置文件。
exec:启动容器并执行命令。除了一些共有的参数外,还有如下一些独有的参数。
--tty,-t:为容器分配一个终端显示输出内容。
--config:使用配置文件,后跟文件路径。
--id:指定容器ID,默认为nsinit。
--user,-u:指定用户,默认为“root”.
--cwd:指定当前工作目录。
--env:为进程设置环境变量。
init:这是一个内置的参数,用户并不能直接使用。这个命令是在容器内部执行,为容器进行namespace初始化,并在完成初始化后执行用户指令。所以在代码中,运行nsinit exec后,传入到容器中运行的实际上是nsinit init,把用户指令作为配置项传入。
oom:展示容器的内存超限通知。
pause/unpause:暂停/恢复容器中的进程。
stats:显示容器中的统计信息,主要包括cgroup和网络。
state:展示容器状态,就是读取state.json文件。
checkpoint:保存容器的检查点快照并结束容器进程。需要填--image-path参数,后面是检查点保存的快照文件路径。完整的命令示例如下。 nsinit checkpoint --image-path=/tmp/criu
restore:从容器检查点快照恢复容器进程的运行。参数同上。
总结起来,nsinit与Docker execdriver进行的工作基本相同,所以在Docker的源码中并不会涉及到nsinit包的调用,但是nsinit为libcontainer自身的调试和使用带来了极大的便利。
3. 配置参数解析
no_pivot_root :这个参数表示用rootfs作为文件系统挂载点,不单独设置pivot_root。
parent_death_signal: 这个参数表示当容器父进程销毁时发送给容器进程的信号。
pivot_dir:在容器root目录中指定一个目录作为容器文件系统挂载点目录。
rootfs:容器根目录位置。
readonlyfs:设定容器根目录为只读。
mounts:设定额外的挂载,填充的信息包括原路径,容器内目的路径,文件系统类型,挂载标识位,挂载的数据大小和权限,最后设定共享挂载还是非共享挂载(独立于mount_label的设定起作用)。
devices:设定在容器启动时要创建的设备,填充的信息包括设备类型、容器内设备路径、设备块号(major,minor)、cgroup文件权限、用户编号、用户组编号。
mount_label:设定共享挂载还是非共享挂载。
hostname:设定主机名。
namespaces:设定要加入的namespace,每个不同种类的namespace都可以指定,默认与父进程在同一个namespace中。
capabilities:设定在容器内的进程拥有的capabilities权限,所有没加入此配置项的capabilities会被移除,即容器内进程失去该权限。
networks:初始化容器的网络配置,包括类型(loopback、veth)、名称、网桥、物理地址、IPV4地址及网关、IPV6地址及网关、Mtu大小、传输缓冲长度txqueuelen、Hairpin Mode设置以及宿主机设备名称。
routes:配置路由表。
cgroups:配置cgroups资源限制参数,使用的参数不多,主要包括允许的设备列表、内存、交换区用量、CPU用量、块设备访问优先级、应用启停等。
apparmor_profile:配置用于SELinux的apparmor文件。
process_label:同样用于selinux的配置。
rlimits:最大文件打开数量,默认与父进程相同。
additional_groups:设定gid,添加同一用户下的其他组。
uid_mappings:用于User namespace的uid映射。
gid_mappings:用户User namespace的gid映射。
readonly_paths:在容器内设定只读部分的文件路径。
MaskPaths:配置不使用的设备,通过绑定/dev/null进行路径掩盖。
4. libcontainer实现原理
在Docker中,对容器管理的模块为execdriver,目前Docker支持的容器管理方式有两种,一种就是最初支持的LXC方式,另一种称为native,即使用libcontainer进行容器管理。在孙宏亮的《Docker源码分析系列》中,Docker Deamon启动过程中就会对execdriver进行初始化,会根据驱动的名称选择使用的容器管理方式。 虽然在execdriver中只有LXC和native两种选择,但是native(即libcontainer)通过接口的方式定义了一系列容器管理的操作,包括处理容器的创建(Factory)、容器生命周期管理(Container)、进程生命周期管理(Process)等一系列接口,相信如果Docker的热潮一直像如今这般汹涌,那么不久的将来,Docker必将实现其全平台通用的宏伟蓝图。本节也将从libcontainer的这些抽象对象开始讲解,与你一同解开Docker容器管理之谜。在介绍抽象对象的具体实现过程中会与Docker execdriver联系起来,让你充分了解整个过程。
4.1 Factory 对象
Factory对象为容器创建和初始化工作提供了一组抽象接口,目前已经具体实现的是Linux系统上的Factory对象。Factory抽象对象包含如下四个方法,我们将主要描述这四个方法的工作过程,涉及到具体实现方法则以LinuxFactory为例进行讲解。
Create():通过一个id和一份配置参数创建容器,返回一个运行的进程。容器的id由字母、数字和下划线构成,长度范围为1~1024。容器ID为每个容器独有,不能冲突。创建的最终返回一个Container类,包含这个id、状态目录(在root目录下创建的以id命名的文件夹,存state.json容器状态文件)、容器配置参数、初始化路径和参数,以及管理cgroup的方式(包含直接通过文件操作管理和systemd管理两个选择,默认选cgroup文件系统管理)。
Load():当创建的id已经存在时,即已经Create过,存在id文件目录,就会从id目录下直接读取state.json来载入容器。其中的参数在配置参数部分有详细解释。
Type():返回容器管理的类型,目前可能返回的有libcontainer和lxc,为未来支持更多容器接口做准备。
StartInitialization():容器内初始化函数。
这部分代码是在容器内部执行的,当容器创建时,如果New不加任何参数,默认在容器进程中运行的第一条命令就是nsinit init。在execdriver的初始化中,会向reexec注册初始化器,命名为native,然后在创建libcontainer以后把native作为执行参数传递到容器中执行,这个初始化器创建的libcontainer就是没有参数的。
传入的参数是一个管道文件描述符,为了保证在初始化过程中,父子进程间状态同步和配置信息传递而建立。
不管是纯粹新建的容器还是已经创建的容器执行新的命令,都是从这个入口做初始化。
第一步,通过管道获取配置信息。
第二步,从配置信息中获取环境变量并设置为容器内环境变量。
若是已经存在的容器执行新命令,则只需要配置cgroup、namespace的Capabilities以及AppArmor等信息,最后执行命令。
若是纯粹新建的容器,则还需要初始化网络、路由、namespace、主机名、配置只读路径等等,最后执行命令。
至此,容器就已经创建和初始化完毕了。
4.2 Container 对象
Container对象主要包含了容器配置、控制、状态显示等功能,是对不同平台容器功能的抽象。目前已经具体实现的是Linux平台下的Container对象。每一个Container进程内部都是线程安全的。因为Container有可能被外部的进程销毁,所以每个方法都会对容器是否存在进行检测。
ID():显示Container的ID,在Factor对象中已经说过,ID很重要,具有唯一性。
Status():返回容器内进程是运行状态还是停止状态。通过执行“SIG=0”的KILL命令对进程是否存在进行检测。
State():返回容器的状态,包括容器ID、配置信息、初始进程ID、进程启动时间、cgroup文件路径、namespace路径。通过调用Status()判断进程是否存在。
Config():返回容器的配置信息,可在“配置参数解析”部分查看有哪些方面的配置信息。
Processes():返回cgroup文件cgroup.procs中的值,在Docker背后的内核知识:cgroups资源限制部分的讲解中我们已经提过,cgroup.procs文件会罗列所有在该cgroup中的线程组ID(即若有线程创建了子线程,则子线程的PID不包含在内)。由于容器不断在运行,所以返回的结果并不能保证完全存活,除非容器处于“PAUSED”状态。
Stats():返回容器的统计信息,包括容器的cgroups中的统计以及网卡设备的统计信息。Cgroups中主要统计了cpu、memory和blkio这三个子系统的统计内容,具体了解可以通过阅读“cgroups资源限制”部分对于这三个子系统统计内容的介绍来了解。网卡设备的统计则通过读取系统中,网络网卡文件的统计信息文件/sys/class/net/<EthInterface>/statistics来实现。
Set():设置容器cgroup各子系统的文件路径。因为cgroups的配置是进程运行时也会生效的,所以我们可以通过这个方法在容器运行时改变cgroups文件从而改变资源分配。
Start():构建ParentProcess对象,用于处理启动容器进程的所有初始化工作,并作为父进程与新创建的子进程(容器)进行初始化通信。传入的Process对象可以帮助我们追踪进程的生命周期,Process对象将在后文详细介绍。
启动的过程首先会调用Status()方法的具体实现得知进程是否存活。
创建一个管道(详见Docker初始化通信——管道)为后期父子进程通信做准备。
配置子进程cmd命令模板,配置参数的值就是从factory.Create()传入进来的,包括命令执行的工作目录、命令参数、输入输出、根目录、子进程管道以及KILL信号的值。
根据容器进程是否存在确定是在已有容器中执行命令还是创建新的容器执行命令。若存在,则把配置的命令构建成一个exec.Cmd对象、cgroup路径、父子进程管道及配置保留到ParentProcess对象中;若不存在,则创建容器进程及相应namespace,目前对user namespace有了一定的支持,若配置时加入user namespace,会针对配置项进行映射,默认映射到宿主机的root用户,最后同样构建出相应的配置内容保留到ParentProcess对象中。通过在cmd.Env写入环境变量_libcontainer_INITTYPE来告诉容器进程采用的哪种方式启动。
执行ParentProcess中构建的exec.Cmd内容,即执行ParentProcess.start(),具体的执行过程在Process部分介绍。
最后如果是新建的容器进程,还会执行状态更新函数,把state.json的内容刷新。
Destroy():首先使用cgroup的freezer子系统暂停所有运行的进程,然后给所有进程发送SIGKIL信号(如果没有使用pid namespace就不对进程处理)。最后把cgroup及其子系统卸载,删除cgroup文件夹。
Pause():使用cgroup的freezer子系统暂停所有运行的进程。
Resume():使用cgroup的freezer子系统恢复所有运行的进程。
NotifyOOM():为容器内存使用超界提供只读的通道,通过向cgroup.event_control写入eventfd(用作线程间通信的消息队列)和cgroup.oom_control(用于决定内存使用超限后的处理方式)来实现。
Checkpoint():保存容器进程检查点快照,为容器热迁移做准备。通过使用CRIU的SWRK模式来实现,这种模式是CRIU另外两种模式CLI和RPC的结合体,允许用户需要的时候像使用命令行工具一样运行CRIU,并接受用户远程调用的请求,即传入的热迁移检查点保存请求,传入文件形式以Google的protobuf协议保存。
Restore():恢复检查点快照并运行,完成容器热迁移。同样通过CRIU的SWRK模式实现,恢复的时候可以传入配置文件设置恢复挂载点、网络等配置信息。
至此,Container对象中的所有函数及相关功能都已经介绍完毕,包含了容器生命周期的全部过程。
TIPs: Docker初始化通信——管道
libcontainer创建容器进程时需要做初始化工作,此时就涉及到使用了namespace隔离后的两个进程间的通信。我们把负责创建容器的进程称为父进程,容器进程称为子进程。父进程clone出子进程以后,依旧是共享内存的。但是如何让子进程知道内存中写入了新数据依旧是一个问题,一般有四种方法。
发送信号通知(signal)
对内存轮询访问(poll memory)
sockets通信(sockets)
文件和文件描述符(files and file-descriptors)
对于Signal而言,本身包含的信息有限,需要额外记录,namespace带来的上下文变化使其不易理解,并不是最佳选择。显然通过轮询内存的方式来沟通是一个非常低效的做法。另外,因为Docker会加入network namespace,实际上初始时网络栈也是完全隔离的,所以socket方式并不可行。 Docker最终选择的方式就是打开的可读可写文件描述符——管道。 Linux中,通过pipe(int fd[2])系统调用就可以创建管道,参数是一个包含两个整型的数组。调用完成后,在fd[1]端写入的数据,就可以从fd[0]端读取。
// 需要加入头文件: #include // 全局变量: int fd[2]; // 在父进程中进行初始化: pipe(fd); // 关闭管道文件描述符 close(checkpoint[1]);
调用pipe函数后,创建的子进程会内嵌这个打开的文件描述符,对fd[1]写入数据后可以在fd[0]端读取。通过管道,父子进程之间就可以通信。通信完毕的奥秘就在于EOF信号的传递。大家都知道,当打开的文件描述符都关闭时,才能读到EOF信号,所以libcontainer中父进程先关闭自己这一端的管道,然后等待子进程关闭另一端的管道文件描述符,传来EOF表示子进程已经完成了初始化的过程。
4.3 Process 对象
Process 主要分为两类,一类在源码中就叫Process,用于容器内进程的配置和IO的管理;另一类在源码中叫ParentProcess,负责处理容器启动工作,与Container对象直接进行接触,启动完成后作为Process的一部分,执行等待、发信号、获得pid等管理工作。 ParentProcess对象,主要包含以下六个函数,而根据”需要新建容器”和“在已经存在的容器中执行”的不同方式,具体的实现也有所不同。
已有容器中执行命令
pid(): 启动容器进程后通过管道从容器进程中获得,因为容器已经存在,与Docker Deamon在不同的pid namespace中,从进程所在的namespace获得的进程号才有意义。
start(): 初始化容器中的执行进程。在已有容器中执行命令一般由docker exec调用,在execdriver包中,执行exec时会引入nsenter包,从而调用其中的C语言代码,执行nsexec()函数,该函数会读取配置文件,使用setns()加入到相应的namespace,然后通过clone()在该namespace中生成一个子进程,并把子进程通过管道传递出去,使用setns()以后并没有进入pid namespace,所以还需要通过加上clone()系统调用。
开始执行进程,首先会运行C代码,通过管道获得进程pid,最后等待C代码执行完毕。
通过获得的pid把cmd中的Process替换成新生成的子进程。
把子进程加入cgroup中。
通过管道传配置文件给子进程。
等待初始化完成或出错返回,结束。
新建容器执行命令
pid():启动容器进程后通过exec.Cmd自带的pid()函数即可获得。
start():初始化及执行容器命令。
开始运行进程。
把进程pid加入到cgroup中管理。
初始化容器网络。(本部分内容丰富,将从本系列的后续文章中深入讲解)
通过管道发送配置文件给子进程。
等待初始化完成或出错返回,结束。
实现方式类似的一些函数
**terminate() **:发送SIGKILL信号结束进程。
**startTime() **:获取进程的启动时间。
signal():发送信号给进程。
wait():等待程序执行结束,返回结束的程序状态。
Process对象,主要描述了容器内进程的配置以及IO。包括参数Args,环境变量Env,用户User(由于uid、gid映射),工作目录Cwd,标准输入输出及错误输入,控制终端路径consolePath,容器权限Capabilities以及上述提到的ParentProcess对象ops(拥有上面的一些操作函数,可以直接管理进程)。
5. 总结
本文主要介绍了Docker容器管理的方式libcontainer,从libcontainer的使用到源码实现方式。我们深入到容器进程内部,感受到了libcontainer较为全面的设计。总体而言,libcontainer本身主要分为三大块工作内容,一是容器的创建及初始化,二是容器生命周期管理,三则是进程管理,调用方为Docker的execdriver。容器的监控主要通过cgroups的状态统计信息,未来会加入进程追踪等更丰富的功能。另一方面,libcontainer在安全支持方面也为用户尽可能多的提供了支持和选择。遗憾的是,容器安全的配置需要用户对系统安全本身有足够高的理解,user namespace也尚未支持,可见libcontainer依旧有很多工作要完善。但是Docker社区的火热也自然带动了大家对libcontainer的关注,相信在不久的将来,libcontainer就会变得更安全、更易用。
F. 如何编译Docker源码
本文根据docker官方给出的docker代码编译环境搭建指南做更深入的分析。官方给出的指导比较简单,但是由于国内的网络问题经常会编译失败,了解了编译步骤后,也可以结合自身遇到的网络问题进行“规避”。
docker的编译环境实际上是创建一个docker容器,在容器中对代码进行编译。 如果想快速的查看编译环境搭建指导,而不关注环境搭建的机制和细节,可以直接跳到最后一章“总结”。
前提
机器上已经安装了docker,因为编译环境是个docker容器,所以要事先有docker(daemon),后面会创建个编译环境容器,在容器里面编译代码。本文中使用物理机,物理机上运行着docker (daemon)。
机器(物理机)上安装了git 。 后续使用git下载docker源码
机器(物理机)上安装了make。
下载ubuntu 14.04的docker镜像
下载docker源码
git clone
会把代码下载到当前目录下,后面会把代码拷贝到容器中。
编译前分析
官方给的编译方法是make build 和 make binary等。下面先分析Makefile,看懂Makefile后,编译环境的准备流程就比较清楚了。
Makefile
在下载的docker源码中可以看到它的Makefile,Makefile中比较关键的几个参数:
DOCKER_MOUNT := $(if $(BIND_DIR),-v "$(CURDIR)/$(BIND_DIR):/go/src/github.com/docker/docker/$(BIND_DIR)") DOCKER_MOUNT 表示创建容器时的mount参数。因为编译环境是一个容器,在后续的步骤中启动容器时使用DOCKER_MOUNT参数,会将物理机上的目录mount给容器容器,容器中该目录是编译生成docker二进制文件的目录。
DOCKER_FLAGS := docker run --rm -i --privileged $(DOCKER_ENVS) $(DOCKER_MOUNT) 这是后面创建docker容器时的命令行的一部分,其中包含了前面的DOCKER_MOUNT参数。
DOCKER_IMAGE := docker-dev$(if $(GIT_BRANCH),:$(GIT_BRANCH)) 这是docker image参数,镜像的名字是docker-dev,以当前git中docker版本作为tag名。这个镜像是在make build一步做出来的。
DOCKER_RUN_DOCKER := $(DOCKER_FLAGS) "$(DOCKER_IMAGE)" 创建docker容器的命令行,组合了前面的DOCKER_FLAGS 和 DOCKER_IMAGE 。 从命令行中可以看出,启动容器使用的参数有 --rm -i --privileged,使用了一些环境变量,还有使用了-v参数把物理机上目录mount给容器,在容器中编译好二进制文件后放到该目录中,在物理机上就能获得docker二进制文件。启动的的docker 容器镜像名字是docker-dev。下文会介绍docker-dev镜像是怎么来的。
由于官方给出的“构建编译环境”的方法是执行 make build,下面在Makefile中看到build分支是这样的:
make build时会调用 docker build -t "$(DOCKER_IMAGE)" . 去制作一个叫做DOCKER_IMAGE的镜像。
进行源码编译的方式是执行 make binary来编译代码,在Makefile中make binary的分支如下:
make binary除了进行 make build以外,会执行$(DOCKER_RUN_DOCKER),即上文提到的docker run命令行。由于执行过了build,会build出来docker-dev镜像,所以在docker run时直接使用前面build出来的镜像。docker run时的命令行参数是hack/make.sh binary。make binary的过程实际上是创建一个容器,在容器中执行hack/make.sh binary脚本。接下来会详细介绍make build和make binary所做的内容。
make build
根据官方的指导,先执行make build来搭建编译环境。上面分析了,make build实际上是制作了一个镜像,这个镜像里会包含编译代码所需的环境。下面来介绍下这个镜像。
Dockerfile
在和Makefile相同的目录下(源码的根目录),有Dockerfile。执行make build 相当于调用docker build,使用的就是该Dockerfile。Dockerfile中的几个主要步骤(有些步骤这里略过):
FROM ubuntu:14.04 使用ubuntu 14.04作为基础镜像;在宿主机上,要事先下载好ubuntu 14.04镜像。
安装一些编译需要的软件;
用git下载lvm2源码,并编译安装;
下载并安装GO 1.5.1;
安装GO相关的tools 可以做code coverage test 、 go lint等代码检查
安装registry和notary server;
安装docker-py 后面跑集成测试用的
将物理机的contrib/download-frozen-image.sh 脚本拷贝到镜像中/go/src/github.com/docker/docker/contrib/
运行contrib/download-frozen-image.sh 制作镜像 实际上这一步只是下载了3个镜像的tar文件。注意:docker build相当于创建一个临时的容器(在临时的容器中执行Dockerfile中的每一步,最后在保存成镜像),“运行contrib/download-frozen-image.sh 制作镜像”这个动作出现在Dockerfile中,相当于在docker build所创建的临时的容器中下载docker镜像,有docker-in-docker容器嵌套的概念。下一小节会对download-frozen-image.sh脚本做详细分析。
ENTRYPOINT ["hack/dind"] 做出来的镜像,使用它启动的容器可以自动运行源码目录中的hack/dind脚本。 dind这个脚本是a wrapper script which allows docker to be run inside a docker container 。后面的小节会对hack/dind脚本做详细的分析。
COPY . /go/src/github.com/docker/docker 把物理机上的docker源码文件打入到镜像中
download-frozen-image.sh脚本
上一小节里提到,在Dockerfile中,有一步会调用contrib/download-frozen-image.sh ,它主要作用是下载3个镜像的tar包,供后续docker load。在Dockerfile中的调用方式如下:
download-frozen-image.sh脚本中会依次解析参数,其中/docker-frozen-images作为base dir,后面下载的东西全放到这里。之后的3个参数是镜像,里面包含了镜像名(例如busybox)、镜像tag(例如latest)、镜像id(例如),后面会在循环中依次下载这3个镜像的tar文件。
download-frozen-image.sh脚本中会通过curl从registry上获取如下信息:
token:获取token,后面curl获取的其他信息时都需要使用token。例如本例中 token='signature=,repository="library/busybox",access=read'
ancestryJson:把镜像相关联的历史层次的id也都获取到,因为每一层的tar都需要下载。本例中 ancestryJson='["", ""]'
这里可以看到这个镜像只有2层,两层的id这里都列了出来。 每个镜像包含的层数不同,例如。第三个镜像jess/unshare共有10层。
VERSION、json、tar: 每一层镜像id的目录下,都下载这3个文件,其中VERSION文件内容目前都是“1.0”,json文件是该层镜像的json文件,tar文件是该层镜像的真正内容,以.tar保存。
下载好的各层镜像目录结构如下:
$ls
$tree
hack/dind脚本
在Dockerfile中,ENTRYPOINT ["hack/dind"] ,表示在镜像启动后,运行该脚本,下面分析一下这个脚本的功能。
脚本在代码根目录下的hack目录中,作者对脚本的描述是 DinD: a wrapper script which allows docker to be run inside a docker container.
就是可以在docker容器中创建docker容器。它就做了一个事,那就是在容器中创建好cgroup目录,并把各个cgroup子系统mount上来。
为了方便理解,我们可以先看看物理机。在宿主机上如果创建docker容器,需要宿主机上必须事先mount cgroup子系统,因为cgroup是docker容器的一个依赖。同理docker-in-docker也要求外层的docker容器中有cgroup子系统,dind脚本在容器启动后,先去/proc/1/cgroup中获取cgroup子系统,然后依次使用mount命令,将cgroup mount上来,例如mount -n -t cgroup -o "cpuset" cgroup "/cgroup/cpuset"
最终在运行make build后,会制作出一个叫docker-dev的镜像。
make binary
执行make binary 就可以编译出docker二进制文件。编译出来的二进制文件在源码目录下的bundles/1.10.0-dev/binary/docker-1.10.0-dev ,其中还包含md5和sha256文件。
Makefile中的binary
Makefile中关于make binary流程是
先执行build,即上一节介绍的,制作docker-dev编译环境镜像。
再执行DOCKER_RUN_DOCKER,创建容器,DOCKER_RUN_DOCKER就是执行docker run,使用docker-dev镜像启动容器,并且会mount -v 将容器生成二进制文件的路径与宿主机共享。DOCKER_RUN_DOCKER在“编译前分析”一章中有介绍。启动的容器运行的命令行是 hack/make.sh binary 。docker run完整的形式如下:
docker run --rm -i --privileged -e BUILDFLAGS -e DOCKER_CLIENTONLY -e DOCKER_DEBUG -e DOCKER_EXECDRIVER -e DOCKER_EXPERIMENTAL -e DOCKER_REMAP_ROOT -e DOCKER_GRAPHDRIVER -e DOCKER_STORAGE_OPTS -e DOCKER_USERLANDPROXY -e TESTDIRS -e TESTFLAGS -e TIMEOUT -v "/home/mu/src/docker/docker/bundles:/go/src/github.com/docker/docker/bundles" -t "docker-dev:master" hack/make.sh binary
hack/make.sh脚本
上一节提到的make binary中创建的容器启动命令是hack/make.sh binary,运行容器中的(docker源码目录下的)hack/make.sh脚本,参数为binary。
make.sh中根据传入的参数组装后续编译用的flags(BUILDFLAGS),最后根据传入的参数依次调用 hack/make/目录下对应的脚本。例如我们的操作中传入的参数只有一个binary。那么在make.sh的最后,会调用hack/make/binary脚本。
hack/make/binary脚本中,就是直接调用go build进行编译了,其中会使用BUILDFLAGS LDFLAGS LDFLAGS_STATIC_DOCKER等编译选项。
如果最终生成的docker二进制文件不在bundles/1.10.0-dev/binary/目录下,那么可能是编译参数BINDDIR设置的不正确,可以在执行make binary时增加BINDDIR参数,例如
make BINDDIR=. binary , 将BINDDIR设置为当前目录。
总结
编译步骤总结:
1、编译前在物理机上安装好make、git,并下载好docker代码。下载好ubuntu:14.04镜像
2、执行make build 。这步执行完会在物理机上创建出一个docker-dev的镜像。
3、执行make binary 。 这步会使用docker-dev镜像启动一个容器,在容器中编译docker代码。编译完成后在物理机上直接可以看到二进制文件。默认二进制文件在 bundles/1.10.0-dev/binary/目录下
4、docker代码里有很多test,可以使用此套编译环境执行test,例如 make test 。 更多参数可以看Makefile
搭建环境心得:
1、在make build时,使用Dockerfile创建制作镜像,这个镜像有40多层,其中一层失败就会导致整个build过程失败。由于Dockerfile中很多步骤是要连到国外的网站去下载东西,很容易失败。好在docker build有cache机制,如果前面的层成功了,下次重新build时会使用cache跳过,节省了很多时间。所以如果make build中途失败(一般是由于国内连国外的网络原因),只要重新执行make build就会在上次失败的地方继续,多试几次可以成功。
2、如果其他人已经build出了docker-dev镜像,可以把它下载到自己的环境上。这样在自己make build时,会跳过那些已经在本地存在的层,可以节省时间。
3、每一次编译会自动删除掉前面已经生成的二进制文件,所以不用担心二进制文件不是最新的问题。
G. 五、Docker Server 的创建(摘自《Docker源码分析》)
Docker 架构中,Docker Server 是 Docker Daemon 的重要组成部分。Docker Server 最主要的功能是:
Docker Daemon 最后环节创建的并运行的serveapi的job 就是让守护进程提供API访问服务
使 Docker Server 监听某一指定地址,接受该地址上的请求,并对以上请求路由转发至相应的处理函数 Handler 处。从实现的角度来看,ListenAndServe 主要实现了设置一个服务于 HTTP 的 server,该 server 将监听指定地址上的请求,并对春穗告请求做特定的协议检查,最终完成请求的路由与分发。
路由实例的作用是:负责Docker Server 对请求进行路由以及分发。实现过程中,主要两个步骤:
第一,创建全新的router 路由实例;
第二,为router 实例添族前加路由记录。
Listener 是一种面向流协议的通用网络监听模块。
在创建 Listener 之前,先判断 Docker Server 允许的协议,若协议为 fd 形式,则直接通过 ServeFd 来服务请求;若协议不为 fd 形式,则继续往下执行。
Docker Server 同样需要创建一个 Server 对象来运行 HTTP 服务端
创建扒明 http.Server 实例之后,Docker Server 立即启动 API 服务,使 Docker Server 开始在 Listener 监听实例 l 上接受请求,并对于每一个请求都生成一个新的 goroutine 来做专属服务。对于每一个请求,goroutine 会读取请求,查询路由表中的路由记录项,找到匹配的路由记录,最终调用路由记录中的执行 Handler,执行完毕后,goroutine 对请求返回响应信息。
H. 微服务架构:基于微服务和Docker容器技术的PaaS云平台架构设计
基于微服务架构和Docker容器技术的PaaS云平台建设目标是给我们的开发人员提供一套服务快速开发、部署、运维管理、持续开发持续集成的流程。平台提供基础设施、中间件、数据服务、云服务器等资源,开发人员只需要开发业务代码并提交到平台代码库,做一些必要的配置,系统会自动构建、部署,实现应用的敏捷开发、快速迭代。在系统架构上,PaaS云平台主要分为微服务架构、Docker容器技术、DveOps三部分,这篇文章重点介绍微服务架构的实施。
如果想学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis,Netty源码分析的朋友可以加我的Java高级交流:854630135,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。
实施微服务需要投入大量的技术力量来开发基础设施,这对很多公司来说显然是不现实的,别担心,业界已经有非常优秀的开源框架供我们参考使用。目前业界比较成熟的微服务框架有Netflix、Spring Cloud和阿里的Dubbo等。Spring Cloud是基于Spring Boot的一整套实现微服务的框架,它提供了开发微服务所需的组件,跟Spring Boot一起使用的话开发微服务架构的云服务会变的很方便。Spring Cloud包含很多子框架,其中Spring Cloud Netflix是其中的一套框架,在我们的微服务架构设计中,就使用了很多Spring Cloud Netflix框架的组件。Spring Cloud Netflix项目的时间还不长,相关的文档资料很少,博主当时研究这套框架啃了很多英文文档,简直痛苦不堪。对于刚开始接触这套框架的同学,要搭建一套微服务应用架构,可能会不知道如何下手,接下来介绍我们的微服务架构搭建过程以及 需要那些 框架或组件来支持微服务架构。
为了直接明了的展示微服务架构的组成及原理,画了一张系统架构图,如下:
从上图可以看出,微服务访问大致路径为:外部请求 → 负载均衡 → 服务网关(GateWay)→ 微服务 → 数据服务/消息服务。服务网关和微服务都会用到服务注册和发现来调用依赖的其他服务,各服务集群都能通过配置中心服务来获得配置信息。
服务网关(GateWay)
网关是外界系统(如:客户端浏览器、移动设备等)和企业内部系统之间的一道门,所有的客户端请求通过网关访问后台服务。为了应对高并发访问,服务网关以集群形式部署,这就意味着需要做负载均衡,我们采用了亚马逊EC2作为虚拟云服务器,采用ELB(Elastic Load Balancing)做负载均衡。EC2具有自动配置容量功能,当用户流量达到尖峰,EC2可以自动增加更多的容量以维持虚拟主机的性能。ELB弹性负载均衡,在多个实例间自动分配应用的传入流量。为了保证安全性,客户端请求需要使用https加密保护,这就需要我们进行SSL卸载,使用Nginx对加密请求进行卸载处理。外部请求经过ELB负载均衡后路由到GateWay集群中的某个GateWay服务,由GateWay服务转发到微服务。服务网关作为内部系统的边界,它有以下基本能力:
1、动态路由:动态的将请求路由到所需要的后端服务集群。虽然内部是复杂的分布式微服务网状结构,但是外部系统从网关看就像是一个整体服务,网关屏蔽了后端服务的复杂性。
2、限流和容错:为每种类型的请求分配容量,当请求数量超过阀值时抛掉外部请求,限制流量,保护后台服务不被大流量冲垮;党内部服务出现故障时直接在边界创建一些响应,集中做容错处理,而不是将请求转发到内部集群,保证用户良好的体验。
3、身份认证和安全性控制:对每个外部请求进行用户认证,拒绝没有通过认证的请求,还能通过访问模式分析,实现反爬虫功能。
4、监控:网关可以收集有意义的数据和统计,为后台服务优化提供数据支持。
5、访问日志:网关可以收集访问日志信息,比如访问的是哪个服务?处理过程(出现什么异常)和结果?花费多少时间?通过分析日志内容,对后台系统做进一步优化。
我们采用Spring Cloud Netflix框架的开源组件Zuul来实现网关服务。Zuul使用一系列不同类型的过滤器(Filter),通过重写过滤器,使我们能够灵活的实现网关(GateWay)的各种功能。
如果想学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis,Netty源码分析的朋友可以加我的Java高级交流:854630135,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。
服务注册与发现
由于微服务架构是由一系列职责单一的细粒度服务构成的网状结构,服务之间通过轻量机制进行通信,这就引入了服务注册与发现的问题,服务的提供方要注册报告服务地址,服务调用放要能发现目标服务。我们的微服务架构中使用了Eureka组件来实现服务的注册与发现。所有的微服务(通过配置Eureka服务信息)到Eureka服务器中进行注册,并定时发送心跳进行 健康 检查,Eureka默认配置是30秒发送一次心跳,表明服务仍然处于存活状态,发送心跳的时间间隔可以通过Eureka的配置参数自行配置,Eureka服务器在接收到服务实例的最后一次心跳后,需要等待90秒(默认配置90秒,可以通过配置参数进行修改)后,才认定服务已经死亡(即连续3次没有接收到心跳),在Eureka自我保护模式关闭的情况下会清除该服务的注册信息。所谓的自我保护模式是指,出现网络分区、Eureka在短时间内丢失过多的服务时,会进入自我保护模式,即一个服务长时间没有发送心跳,Eureka也不会将其删除。自我保护模式默认为开启,可以通过配置参数将其设置为关闭状态。
Eureka服务以集群的方式部署(在博主的另一篇文章中详细介绍了Eureka集群的部署方式),集群内的所有Eureka节点会定时自动同步微服务的注册信息,这样就能保证所有的Eureka服务注册信息保持一致。那么在Eureka集群里,Eureka节点是如何发现其他节点的呢?我们通过DNS服务器来建立所有Eureka节点的关联,在部署Eureka集群之外还需要搭建DNS服务器。
当网关服务转发外部请求或者是后台微服务之间相互调用时,会去Eureka服务器上查找目标服务的注册信息,发现目标服务并进行调用,这样就形成了服务注册与发现的整个流程。Eureka的配置参数数量很多,多达上百个,博主会在另外的文章里详细说明。
微服务部署
微服务是一系列职责单一、细粒度的服务,是将我们的业务进行拆分为独立的服务单元,伸缩性好,耦合度低,不同的微服务可以用不同的语言开发,每一个服务处理的单一的业务。微服务可以划分为前端服务(也叫边缘服务)和后端服务(也叫中间服务),前端服务是对后端服务做必要的聚合和剪裁后暴露给外部不同的设备(PC、Phone等),所有的服务启动时都会到Eureka服务器进行注册,服务之间会有错综复杂的依赖关系。当网关服务转发外部请求调用前端服务时,通过查询服务注册表就可以发现目标服务进行调用,前端服务调用后端服务时也是同样的道理,一次请求可能涉及到多个服务之间的相互调用。由于每个微服务都是以集群的形式部署,服务之间相互调用的时候需要做负载均衡,因此每个服务中都有一个LB组件用来实现负载均衡。
微服务以镜像的形式,运行在Docker容器中。Docker容器技术让我们的服务部署变得简单、高效。传统的部署方式,需要在每台服务器上安装运行环境,如果我们的服务器数量庞大,在每台服务器上安装运行环境将是一项无比繁重的工作,一旦运行环境发生改变,就不得不重新安装,这简直是灾难性的。而使用Docker容器技术,我们只需要将所需的基础镜像(jdk等)和微服务生成一个新的镜像,将这个最终的镜像部署在Docker容器中运行,这种方式简单、高效,能够快速部署服务。每个Docker容器中可以运行多个微服务,Docker容器以集群的方式部署,使用Docker Swarm对这些容器进行管理。我们创建一个镜像仓库用来存放所有的基础镜像以及生成的最终交付镜像,在镜像仓库中对所有镜像进行管理。
服务容错
微服务之间存在错综复杂的依赖关系,一次请求可能会依赖多个后端服务,在实际生产中这些服务可能会产生故障或者延迟,在一个高流量的系统中,一旦某个服务产生延迟,可能会在短时间内耗尽系统资源,将整个系统拖垮,因此一个服务如果不能对其故障进行隔离和容错,这本身就是灾难性的。我们的微服务架构中使用了Hystrix组件来进行容错处理。Hystrix是Netflix的一款开源组件,它通过熔断模式、隔离模式、回退(fallback)和限流等机制对服务进行弹性容错保护,保证系统的稳定性。
1、熔断模式:熔断模式原理类似于电路熔断器,当电路发生短路时,熔断器熔断,保护电路避免遭受灾难性损失。当服务异常或者大量延时,满足熔断条件时服务调用方会主动启动熔断,执行fallback逻辑直接返回,不会继续调用服务进一步拖垮系统。熔断器默认配置服务调用错误率阀值为50%,超过阀值将自动启动熔断模式。服务隔离一段时间以后,熔断器会进入半熔断状态,即允许少量请求进行尝试,如果仍然调用失败,则回到熔断状态,如果调用成功,则关闭熔断模式。
2、隔离模式:Hystrix默认采用线程隔离,不同的服务使用不同的线程池,彼此之间不受影响,当一个服务出现故障耗尽它的线程池资源,其他的服务正常运行不受影响,达到隔离的效果。例如我们通过andThreadPoolKey配置某个服务使用命名为TestThreadPool的线程池,实现与其他命名的线程池隔离。
3、回退(fallback):fallback机制其实是一种服务故障时的容错方式,原理类似Java中的异常处理。只需要继承HystixCommand并重写getFallBack()方法,在此方法中编写处理逻辑,比如可以直接抛异常(快速失败),可以返回空值或缺省值,也可以返回备份数据等。当服务调用出现异常时,会转向执行getFallBack()。有以下几种情况会触发fallback:
1)程序抛出非HystrixBadRequestExcepption异常,当抛出HystrixBadRequestExcepption异常时,调用程序可以捕获异常,没有触发fallback,当抛出其他异常时,会触发fallback;
2)程序运行超时;
3)熔断启动;
4)线程池已满。
4、限流: 限流是指对服务的并发访问量进行限制,设置单位时间内的并发数,超出限制的请求拒绝并fallback,防止后台服务被冲垮。
Hystix使用命令模式HystrixCommand包装依赖调用逻辑,这样相关的调用就自动处于Hystrix的弹性容错保护之下。调用程序需要继承HystrixCommand并将调用逻辑写在run()中,使用execute()(同步阻塞)或queue()(异步非阻塞)来触发执行run()。
动态配置中心
微服务有很多依赖配置,某些配置参数在服务运行期间可能还要动态修改,比如:根据访问流量动态调整熔断阀值。传统的实现信息配置的方法,比如放在xml、yml等配置文件中,和应用一起打包,每次修改都要重新提交代码、打包构建、生成新的镜像、重新启动服务,效率太低,这样显然是不合理的,因此我们需要搭建一个动态配置中心服务支持微服务动态配置。我们使用Spring Cloud的configserver服务帮我们实现动态配置中心的搭建。我们开发的微服务代码都存放在git服务器私有仓库里面,所有需要动态配置的配置文件存放在git服务器下的configserver(配置中心,也是一个微服务)服务中,部署到Docker容器中的微服务从git服务器动态读取配置文件的信息。当本地git仓库修改代码后push到git服务器仓库,git服务端hooks(post-receive,在服务端完成代码更新后会自动调用)自动检测是否有配置文件更新,如果有,git服务端通过消息队列给配置中心(configserver,一个部署在容器中的微服务)发消息,通知配置中心刷新对应的配置文件。这样微服务就能获取到最新的配置文件信息,实现动态配置。
以上这些框架或组件是支撑实施微服务架构的核心,在实际生产中,我们还会用到很多其他的组件,比如日志服务组件、消息服务组件等等,根据业务需要自行选择使用。在我们的微服务架构实施案例中,参考使用了很多Spring Cloud Netflix框架的开源组件,主要包括Zuul(服务网关)、Eureka(服务注册与发现)、Hystrix(服务容错)、Ribbon(客户端负载均衡)等。这些优秀的开源组件,为我们实施微服务架构提供了捷径。
如果想学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis,Netty源码分析的朋友可以加我的Java高级交流:854630135,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。