第1章 容器虚拟化概述       * 了解容器实现原理。 * 了解Docker和Kubernetes基本原理。 * 熟悉容器虚拟化应用场景。   本章首先向读者介绍容器的概念及实现的基本原理,再进一步介绍Docker和Kubernetes,最后介绍容器虚拟化的实际应用场景。 1.1 容器的发展历史和应用场景 1.1.1 虚拟化技术与容器技术的区别及其联系   虚拟化技术最初起源于20世纪60年代末,当时美国IBM公司开发了一套被称为虚拟机管理监视器(Virtual Machine Monitor)的软件。该软件作为计算机硬件层上面的一层软件抽象层,将计算机硬件虚拟分区成一个或多个虚拟机,并提供多用户对大型计算机的交互访问。   如今,虚拟化技术已成为一种被大家广泛认可的服务器资源共享方式,它可以在按需构建操作系统实例的过程中,为系统管理员提供极大的灵活性。但Hypervisor虚拟化技术仍然存在一些性能和资源使用效率方面的问题,因此出现了一种称为容器(Container)的新型虚拟化技术来帮助解决这些问题。   如果说虚拟化技术通过Hypervisor实现VM与底层硬件的解耦,那么容器(Container)技术就是一种更加轻量级的操作系统虚拟化技术。它是将应用程序及其运行依赖环境打包封装到标准化、强移植的镜像中,通过容器带有的引擎提供进程隔离、资源可限制的运行环境,实现应用与OS平台及底层硬件的解耦,一次性打包,实现了跨区域性的移植运行。   容器本身是基于镜像实现运行的,可部署在物理机或者虚拟机上,通过容器引擎与容器编排调度平台来实现容器化应用的生命周期管理。   虚拟化技术与容器技术的对比如图1-1所示。 图1-1 虚拟化技术与容器技术的对比   VM中包含GuestOS,调度与资源占用都比较重。而容器仅仅只包含应用运行时所需要的文件,管理容器就是管理应用本身。   如表1-1所示,容器具有极其轻量、秒级部署、易于移植、敏捷弹性伸缩等多种优势。VM是OS系统级隔离,而容器则是进程级隔离,但相对于VM来说,容器的安全性更弱一些,因此需要一些额外的安全技术或安全容器方案来弥补。 表1-1 虚拟化技术和容器化技术对比 对 比 项 虚拟化VM 容器Container 镜像大小 包含GuestOS 几GB以上 只包含应用的bin/lib 资源要求 CPU 内存按核,GB分配 CPU内存按0.x核,0.0xGB分配 启动时间 分钟级 毫秒级 可移植 跨物理机迁移 跨OS平台迁移 弹性伸缩 VM自动伸缩,CPU/内存手动伸缩 实例自动伸缩,CPU/内存自动伸缩 隔离策略 OS,系统级 Cgroup进程级   作为云原生的核心技术,容器、微服务与DevOps/ CICD等技术已成为应用架构转型或实现技术中台不可或缺的组件。 1.1.2 容器虚拟化应用场景   容器技术的诞生,其主要目的是为解决PaaS层的技术实现,就像OpenStack、Cloudstack等技术为解决IaaS层的问题而诞生一样。对于IaaS层和PaaS层的区别和特性,这里不再赘述。   目前,主流的容器技术主要应用场景有以下4种。   1. 容器化传统应用   容器技术不仅能提高现有应用的安全性和可移植性,还能节约成本。每个企业的环境中都有一套较旧的应用来服务于客户或自动执行业务流程。即使是大规模的单体应用,也可以通过容器隔离来增强安全性、可移植性等特点,从Docker中获益,从而降低成本。容器化之后,这些应用可以扩展额外的服务或者转变到微服务架构上。   2. 持续集成和持续部署(CI/CD)   通过Docker加速应用管道自动化和应用部署,交付速度至少提高13倍。其现代化开发流程快速、持续且具备自动执行能力,最终目标就是为了开发出更加可靠的软件。   通过持续集成(CI)和持续部署(CD),每次开发人员签入代码并顺利测试后,IT团队都能够集成新代码。作为开发运维方法的基础,CI/CD创造了一种实时反馈回路机制,持续地传输小型迭代更改,从而达到加速更改、提高质量的目的。   CI环境通常是完全自动化的,通过git推送命令触发测试,测试成功时自动构建新镜像,然后推送到Docker镜像库。再通过后续的自动化和脚本,将新镜像的容器部署到预演环境,从而进行更深层次的测试。   3. 微服务   加速应用架构现代化进程。应用架构正在从采用瀑布模型开发法的单体代码库,转变为独立开发和部署的松耦合服务。由成千上万个这样的服务相互连接形成应用。Docker允许开发人员选择最适合于每种服务的工具或技术栈,隔离服务以消除任何潜在的冲突,从而避免“地狱式的矩阵依赖”。   这些容器可以独立于应用的其他服务组件,轻松地共享、部署、更新和瞬间扩展。Docker端到端安全功能让团队能够构建和运行最低权限的微服务模型,服务所需的资源(其他应用、涉密信息、计算资源等)会适时地被创建并访问。   4. IT基础设施优化   充分利用基础设施,节省资金。Docker和容器有助于优化IT基础设施的利用率和成本。优化不仅是指削减成本,还指能确保在适当的时间有效地使用适当的资源。   容器作为一种轻量级的打包和隔离应用工作负载的方法,它允许在同一物理或虚拟服务器上毫不冲突地运行多项工作负载。企业可以整合数据中心,将并购而来的IT资源进行整合,从而获得向云端的可迁移性,同时减少操作系统和服务器的维护工作。 1.2 从容器到Docker 1.2.1 Docker的由来   2010年,美国旧金山成立了一家名为“dotCloud”的公司。这家公司主要提供基于PaaS的云计算技术服务。具体来说,是和LXC有关的容器技术。LXC是指Linux容器虚拟技术(Linux container)。后来,dotCloud公司将自己的容器技术进行了简化和标准化,并命名为Docker。Docker技术诞生后,并没有引起行业的关注。   而dotCloud公司作为一家小型创业企业,在激烈的竞争之下,也步履维艰。正当他们快要坚持不下去的时候,想出了“开源”(Open Source)的想法。所谓“开源”,就是开放源代码,将原来内部保密的程序源代码开放给所有人,然后让大家一起参与进来,贡献代码和意见。   对于开源,有的软件从一开始设计时就开源;有的软件是因为资金不够,但它的创造者又不想放弃开发,所以选择开源。   2013年3月,dotCloud公司的创始人之一,Docker之父,28岁的Solomon Hykes正式决定将Docker项目开源。   在Docker项目开源后,越来越多的IT工程师开始发现Docker的优点,蜂拥而至Docker开源社区,Docker的人气迅速攀升,速度之快令人瞠目结舌。   开源当月,Docker 0.1版本发布,此后的每一个月Docker都会发布一个版本。2014年6月9日,Docker 1.0版本正式发布。此时的Docker已经成为行业里人气火爆的开源技术,没有之一。甚至像Google、微软、Amazon、VMware等巨头,都对它青睐有加,表示将全力支持。   Docker流行之后,dotCloud公司把公司名字改成了“Docker Inc.”。   2013年,CoreOS(Linux系统)也加入了Docker的生态建设中,在容器生态圈中贴有标签——专为容器设计的操作系统CoreOS。然而,2014年CoreOS发布了自己的开源容器引擎Rocket,从此Docker和Rocket成为容器技术的两大阵营。 1.2.2 容器的标准化   当前,Docker几乎是容器的代名词,很多人以为Docker就是容器。其实,这是错误的认知,容器除了Docker外,还有CoreOS。   当然有不同就容易出现分歧,所以任何技术的出现都需要一个标准来规范它,否则容易导致技术实现的碎片化,出现大量的冲突和冗余。   因此,在2015年,成立了由Google、Docker、CoreOS、IBM、微软、红帽等厂商联合发起的OCI(Open Container Initiative)组织,并于2016年4月推出了第一个开放容器标准。   该标准主要包括容器镜像标准(image spec)和容器运行时标准(runtime spec)。容器标准的推出有助于为成长中的市场带来稳定性,让企业能放心地采用容器技术;用户在打包、部署应用程序后,可以自由选择不同的容器Runtime;同时,镜像打包、建立、认证、部署、命名也都能按照统一的规范来做。   1. 容器镜像标准   (1)一个镜像由4部分组成:manifest、Image Index(可选)、layers和Configuration。   ①manifest文件。   manifest文件包括镜像内容的元信息和镜像层的摘要信息,这些镜像层可以解包部署成最后的运行环境、镜像的config文件索引、有哪些layer及额外的annotation信息。manifest文件中保存了很多与当前平台有关的信息。   ②Image Index文件。   从更高的角度描述了manifest信息,主要应用于镜像跨平台可选的文件,指向不同平台的manifest文件,其作用是确保镜像能跨平台使用。每个平台都拥有着不同的manifest文件,但都会使用index文件作为索引。   ③layers文件。   以layer保存的文件系统,每个layer保存了和上层之间变化的部分,layer应该保存哪些文件,以及如何表示增加、修改和删除的文件等。   ④Configuration文件。   config文件包含了应用的参数环境,保存了文件系统的层级信息(每个层级的hash值、历史信息),以及容器运行时需要的一些信息(如环境变量、工作目录、命令参数、mount列表等),指定了镜像在某个特定平台和系统的配置。比较接近使用docker inspect 时看到的内容。   (2)根据存储内容的密码学哈希值来找到镜像存储的位置,根据内容寻址描述格式。   (3)一种格式存储CAS斑点并引用它们(可选OCI层)。   (4)签名是基于签名的图像内容的地址(可选OCI层)。   (5)命名是联合基于DNS,可以授权(可选OCI层)。   2. 容器运行时标准   1)creating(创建中)   使用create命令创建容器,这个过程称为创建中。   2)created(创建后)   容器创建出来,但是还没有运行,表示镜像和配置没有错误,容器能够在当前平台运行。   3)running   容器的运行状态,里面的进程处于up状态,正在执行用户设定的任务。   4)stopped   容器运行完成,或者运行出错,或者运行stop命令后容器处于暂停状态。这个状态下,容器还有很多信息保存在平台中,并没有完全被删除。   容器标准格式也要求容器把自身运行时的状态持久化到磁盘中,这样便于外部的其他工具对此信息使用和演绎,运行时状态会以JSON格式编码存储。推荐把运行时状态的JSON文件存储在临时文件系统中,以便系统重启后会自动移除。   一个state.json文件中包含的具体信息如下。   1)版本信息   存放OCI标准的具体版本号。   2)容器ID   通常是一个哈希值,也可以是一个易读的字符串。在state.json文件中加入容器ID是为了便于之前提到的运行时hooks只需载入state.json就可以定位到容器,然后检测state.json,若发现文件不见了就判定为容器关停,再执行相应预定义的脚本操作。   3)PID   容器中运行的首个进程在宿主机上的进程号。   4)容器文件目录   存放容器rootfs及相应配置的目录。外部程序只需读取state.json即可定位到宿主机上的容器文件目录。?标准的容器生命周期应该包含3个基本过程。   (1)容器创建。   容器创建包括文件系统、namespaces、cgroups、用户权限在内的各项内容的创建。   (2)容器进程的启动。   运行容器进程,进程的可执行文件定义在config.json中的args项。   (3)容器暂停。   容器实际上作为进程可以被外部程序关停(kill),容器标准规范应该包含对容器暂停信号的捕获,并做相应资源回收的处理,避免孤儿进程的出现。   总的来说,容器镜像标准定义了容器镜像的打包形式(pack format),而容器运行时标准定义了如何运行一个容器。 1.2.3 Docker的开源项目moby   moby项目地址如下: https://github.com/moby/moby   moby项目的目标是基于开源的方式,发展成为Docker的上游,并将Docker拆分为很多模块化组件。其中包括Docker引擎的核心项目,但是引擎中的代码正在持续拆分和模块化。 1.3 容器虚拟化与Docker 1.3.1 容器虚拟化技术   容器化应用不是直接在宿主机上运行的应用,而是运行应用程序的传统方法,将应用程序直接安装在宿主机的文件系统上,并从文件系统运行它,其环境包括宿主机的进程表、文件系统、IPC设施、网络连接、端口及设备。   有时,应用程序迭代更新时,需要应用程序在系统上运行不同版本的应用。这就很容易引起应用程序间的冲突。比如,多个应用程序占用同一个端口的情况,如mysql的3306端口与网页的80端口,这些都是一台宿主机无法解决的问题。   容器化应用不是在虚拟机上运行的应用,虚拟机上的应用作为宿主机独立的操作系统运行,解决了应用直接在硬件上运行缺乏的灵活性,可以在宿主机上启动10个不同的虚拟机来运行10个应用程序,虽然每个虚拟机上的服务监听了同一个端口号3306,但是每个虚拟机有自己独立的IP,所以不会起冲突,但是一个虚拟机运行耗费了很多空间,占用大量的CPU,或者虚拟系统占用的CPU远比你的应用程序消耗的高得多。   容器化应用独立运行环境如下。   (1)文件系统。   容器拥有自己的文件系统,默认情况,它无法看到宿主系统的文件,该规则有一个例外,即有些文件(如/etc/hosts、/etc/resolv.confDNS服务文件)可能会被自动挂载到容器中。另一个例外是当容器运行镜像时,可以将显示的宿主机的目录挂载到容器中。   (2)进程表。   Linux宿主机上运行着成千上万的进程,默认情况下,容器内无法看到宿主机的进程表,因此,你的应用在容器启动时,pid为1。   (3)网络接口。   默认情况下,容器会通过DHCP从一组私有IP地址确定IP。   (4)IPC设备。   容器内运行的进程不能与宿主机系统上运行的进程通信设施交互,可以将宿主机上的IPC设备暴露给容器,每个容器都有自己的IPC设施。Linux IPC:Pipe Signal Message Semaphore Socket共享内存。   (5)设备。   容器进程无法直接看到宿主机的设备,可以设置特殊权限,在启动容器时授予权限。 1.3.2 容器造就了Docker   关于容器是否是Docker的核心技术,业界一直存在争议。有人认为Docker的核心技术是对分层镜像的创新使用,还有人认为其核心是统一了应用的打包分发和部署方式,为服务器级别的“应用商店”提供了可能,而这将会是颠覆传统行业的举措。   事实上,这一系列创新并不依赖于容器技术,基于传统的Hypervisor也可以做到,业界也由此诞生了一些开源项目,如Hyper、Clear Linux等。   另外,Docker官方对Docker核心功能的描述“Build,Ship and Run”中也没有体现与容器强相关的内容。   尽管如此,容器仍然是Docker的核心技术之一。   首先从Docker的诞生历史上来看,它主要是为了完善当时的容器项目LXC,让用户可以更方便地使用容器,让容器可以更好地应用到项目开发和部署的各个流程中。从一开始,LXC就是Docker上的唯一容器引擎也可以看出这一点,因此,可以说Docker就是为容器而生的。   另外,更重要的一点是,同Docker一起发展、众所周知的一个名为“微服务”(micro service)的设计哲学有关,而这会把容器的优势发挥得淋漓尽致。容器作为Linux平台的轻量级虚拟化,其核心优势是与内核的无缝融合,其在运行效率上的优势和极小的系统开销,与需要将各个组件单独部署的微服务应用完美融合。   而且微服务在隔离性问题上更加可控,这也避免了容器相对传统虚拟化在隔离性上的短板。所以,未来在微服务的设计哲学下,容器必将与Docker一起得到更加广泛的应用和发展。   在理解了容器、容器的核心技术Cgroup和Namespace,以及容器技术如何巧妙且轻量地实现“容器”本身的资源控制和访问隔离后,就会明白Docker和容器其实是一种完美的融合和相辅相成的关系,它们不是唯一的搭配,但一定是最完美的组合。   与其说是容器造就了Docker,不如说是它们造就了彼此,容器技术让Docker得到更多的应用和推广,Docker也使容器技术被更多人熟知。在未来,它们也一定会彼此促进,共同发展,在全新的解决方案和生态系统中扮演重要角色。 1.3.3 Docker的概念   Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用和依赖包到一个可移植的容器中,然后发布到任何流行的Linux或Windows机器上,也可以实现虚拟化,容器完全使用沙箱机制,相互之间不会有任何接口。   Docker的基本概念如下。   1. 镜像   Docker镜像(Image)相当于是一个root文件系统。比如官方镜像ubuntu:16.04就包含了完整的一套Ubuntu16.04最小系统的root文件系统。   2. 容器   镜像和容器的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。   3. 仓库   可以将仓库看成一个代码控制中心,用来保存镜像。   Docker使用客户端-服务器(C/S)架构模式,使用远程API来管理和创建Docker容器。Docker容器通过Docker镜像来创建。容器与镜像的关系类似于面向对象编程中的对象与类。 1.3.4 为什么使用Docker   1. 容器化应用面临的挑战   (1)容器化应用本质上还是各自独立的,一个应用系统的存在需要注册服务应用、登录服务应用、数据库应用、缓存应用等。   (2)如何让它们精密配合使用和互相访问?   (3)如何管理容器镜像,比如保存myql镜像?   (4)如何确保下载官方的mysql镜像时,在下载过程中镜像没有被篡改?   (5)原本Linux系统中有用于启动停止服务的机制、监控侦听、轮换日志文件的方式来确保服务的稳定运行,但是容器隔离了Linux系统。如何对容器应用进行监控侦听来确保稳定运行?   2. Docker具备的优势   (1)统一的管理服务。   使用Docker部署的应用,都会在Docker的管理范围之内。这也是Docker的另一个优点(第一个是标准化),它提供了一种隔离的空间,把服务器上零散的部署应用集中起来进行管理。   比如未使用Docker时,一个服务器上部署了多个服务,如mysql、redis、rabbitmq等。如果有一天服务器突然断电重启了,那些没有设置自动重启的应用、重启出现问题的应用,以及某些甚至都不知道隐藏在某个角落里的重要应用启动没有成功怎么办?   但使用Docker后,一眼就可以看出哪些应用正常启动了,哪些应用出问题了,然后进行处理即可。   (2)更高效的利用系统资源。   由于容器不需要进行硬件虚拟及运行完整操作系统等额外开销,Docker对系统资源的利用率更高,无论是应用执行速度、内存消耗还是文件存储速度,都比传统虚拟机技术更高效。   因此,相比虚拟机技术,一个相同配置的主机往往可以运行更多数量的应用。Docker容器的运行不需要额外的Hypervisor支持,它是内核级的虚拟化,因此可以实现更高的性能和效率。   (3)更快速的启动时间。   传统的虚拟机技术启动应用服务往往需要数分钟,而由于Docker容器应用直接运行于宿主内核,无须启动完整的操作系统,因此可以做到秒级,甚至毫秒级的启动时间,大大节约了开发、测试、部署等过程中的时间。   (4)一致的运行环境。   开发过程中一个常见的问题就是环境一致性问题,由于开发环境、测试环境和生产环境不一致,导致某些bug并未在开发过程中被发现,而Docker的镜像功能提供了除内核外完整的运行时环境,确保了应用运行环境一致性,不会再出现这类问题。   (5)持续交付和部署。   对于开发和运维人员来说,希望一次性完成创建或配置后,就可以在任意地方正常运行。使用Docker可以通过定制应用镜像来实现持续集成、持续交付和持续部署。   开发人员可以通过Dockerfile进行镜像构建,并结合持续集成系统进行集成测试,而运维人员则可以在生产环境中快速部署该镜像,甚至结合持续部署系统进行自动部署。   (6)更轻松的迁移。   由于Docker确保了执行环境的一致性,使应用的迁移更加容易。Docker可以在很多平台上运行,无论是物理机、虚拟机、公有云、私有云,甚至是比较版本,其运行结果都是一致的,因此用户可以很轻易地将一个平台上运行的应用迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行。   (7)更轻松的维护和扩展。   Docker使用的分层存数及镜像技术,可以使应用重复部分的复用更为容易,也使应用的维护更新更加简单,基于基础镜像进一步扩展镜像也变得非常简单。   此外,Docker团队同各个开源项目团队一起维护了一大批高质量的官方镜像,既可以直接在生产环境中使用,又可以作为基础进一步定制,大大降低了应用服务的镜像制作成本。 1.4 从Docker到Kubernetes 1.4.1 Kubernetes的由来   1. 传统部署时代   早期,各个组织机构在物理服务器上运行应用程序。无法为物理服务器中的应用程序定义资源边界,这会导致资源分配问题。例如,如果在物理服务器上运行多个应用程序,则可能会出现一个应用程序占用大部分资源的情况,结果可能导致其他应用程序的性能下降。   一种解决方案是在不同的物理服务器上运行每个应用程序,但是由于资源利用不足而无法扩展,并且维护许多物理服务器的成本很高。   2. 虚拟化部署时代   作为解决方案,引入了虚拟化。虚拟化技术允许用户在单个物理服务器的CPU上运行多个虚拟机(VM)。虚拟化允许应用程序在VM之间隔离,并提供一定程度的安全保障,因为一个应用程序的信息不能被另一个应用程序随意访问。   虚拟化技术能够更好地利用物理服务器上的资源,并且因为可轻松地添加或更新应用程序,可以实现更好的可伸缩性,降低硬件成本等。每个VM都是一台完整的计算机,在虚拟化硬件之上运行所有组件,包括其自己的操作系统。   3. 容器部署时代   容器类似于VM,但是它们具有被放宽的隔离属性,可以在应用程序之间共享操作系统(OS)。因此,容器被认为是轻量级的。容器与VM类似,具有自己的文件系统、CPU、内存、进程空间等。由于它们与基础架构分离,因此可以跨云和OS发行版本进行移植。   容器因具有许多优势而变得流行起来,下面列出了使用容器的几点好处。   (1)敏捷应用程序的创建和部署:与使用VM镜像相比,提高了容器镜像创建的简便性和效率。   (2)持续开发、集成和部署:通过快速简单的回滚(由于镜像不可变性),支持可靠且频繁的容器镜像构建和部署。   (3)关注开发与运维的分离:在构建/发布时而不是在部署时创建应用程序容器镜像,从而将应用程序与基础架构分离。   (4)可观察性不仅可以显示操作系统级别的信息和指标,还可以显示应用程序的运行状况和其他指标信号。   (5)跨开发、测试和生产的环境一致性:在便携式计算机上与在云中相同地运行。   (6)跨云和操作系统发行版本的可移植性:可在Ubuntu、RHEL、CoreOS、本地、Google Kubernetes Engine和其他任何平台运行。   (7)松散耦合、分布式、弹性、解放的微服务:应用程序被分解成较小的独立部分,并且可以动态部署和管理,而不是在一台大型单机上整体运行。   (8)资源隔离:可预测的应用程序性能。   (9)资源利用:高效率和高密度。 1.4.2 Kubernetes的功能   容器是打包和运行应用程序的方式。在生产环境中,需要管理运行应用程序的容器,并确保容器不会停机。例如,一个容器发生故障时,需要马上启动另一个容器替代上,这样一来就节省了修理故障的时间,也不会影响运行的服务。   这就是Kubernetes解决这些问题的方法。Kubernetes提供了一个可弹性运行分布式系统的框架,能满足扩展要求、故障转移、部署模式等。例如,Kubernetes可以轻松地管理系统的Canary部署。   Kubernetes的优势如下。   (1)服务发现和负载均衡。   Kubernetes可以使用DNS名称或自己的IP地址公开容器,如果一次性进入容器的流量数据庞大,那Kubernetes还可以更高效地负载均衡并分配适合网络流量,从而使部署变得稳定。   (2)存储编排。   Kubernetes允许自动挂载所选择的存储系统,如本地存储、公共云提供商等。   (3)自动部署和回滚。   可以使用Kubernetes描述已部署容器的所需状态,它可受控的速率将实际状态更改为期望状态。例如,可以自动化Kubernetes来为用户的部署创建新容器,删除现有容器并将它们的所有资源用于新容器。   (4)自动完成装箱计算。   Kubernetes允许用户指定每个容器所需的CPU和内存(RAM)。当容器指定了资源请求时,Kubernetes可以更好地决策出管理容器所需要的资源。   (5)自我修复。   Kubernetes能够重新启动失败的容器、替换容器、终止不响应用户定义的运行状况检查的容器,并且在准备好服务之前不将其通告给客户端。   (6)密钥与配置管理。   Kubernetes允许用户存储和管理敏感信息,如密码、OAuth令牌和ssh密钥。用户可以在不重建容器镜像的情况下部署、更新密钥和应用程序配置,无须在堆栈配置中暴露密钥。 1.5 安装VMware   步骤1:进入VMware官网(https://www.vmware.com/cn.html),单击上方导航栏中的“工作空间”菜单,再单击图中标记的“桌面Hypervisor”下的Workstation Pro,如图1-2所示。 图1-2 VMware官网界面   步骤2:进入CentOS官网(https:// www.centos.org ),单击上方导航栏中的Download菜单,如图1-3所示。 图1-3 CentOS首页   步骤3:选择自己的计算机对应的类型进行下载,如图1-4所示。 图1-4 CentOS版本类型   步骤4:选择一个镜像源。这里既可以选择最新版本的CentOS 8.5.2版本,也可以选择下载案例中的CentOS 7版本,如图1-5所示。 图1-5 选择镜像源   步骤5:选择其中一种版本的CentOS版本CD下载,如图1-6所示。 图1-6 选择版本   步骤6:创建虚拟机,如图1-7所示。 图1-7 创建虚拟机   步骤7:首先单击左上角的菜单,新建虚拟机跳转到向导界面,如图1-8所示。   步骤8:进入向导界面后,选择“自定义(高级)”单选按钮,单击“下一步”按钮,如图1-8所示。在兼容性界面中保持默认设置,如图1-9所示。单击“下一步”按钮,在映像文件界面中的“安装程序光盘映像文件”中选择已下载的CentOS-7文件,单击“下一步”按钮,进入命名虚拟机界面,如图1-10所示。在“位置”选项下选择存放虚拟机的磁盘位置和是否要修改虚拟机名称,如图1-11所示。 图1-8 向导界面 图1-9 兼容性界面 图1-10 映像文件界面 图1-11 命名虚拟机界面   步骤9:单击“下一步”按钮,进入处理器界面,如图1-12所示,设置“处理器数量”和“每个处理器的内核数量”参数,注意不能给予太多的处理器,否则会影响宿主机的运行,从而导致虚拟机的运算速率下降。完成后进入虚拟机内存界面,如图1-13所示,选择推荐内存1GB后单击“下一步”按钮。 图1-12 处理器界面 图1-13 虚拟机内存界面   步骤10:进入网络类型界面,如图1-14所示,选择“使用桥接网络”单选按钮,单击“下一步”按钮,进入选择磁盘界面,如图1-15所示。在其中选择“创建新虚拟磁盘”单选按钮,单击“下一步”按钮,进入磁盘容量界面,如图1-16所示,选择完毕后等待虚拟机的创建,如图1-17所示。   步骤11:配置完虚拟机后,在界面中选择语言中文设置后,进入到安装信息摘要界面如图1-18所示,在“软件选择”选项中选择“带GUI的服务器”选项,单击“开始安装”按钮,最后出现配置用户密码界面,配置用户和密码,等待安装完毕即可,如图1-19所示。   登录到Linux字符界面如图1-20所示。 图1-14 网络类型界面 图1-15 选择磁盘界面 图1-16 磁盘容量界面 图1-17 完成界面 图1-18 安装信息摘要 图1-19 配置用户密码界面 图1-20 Linux字符界面                第2章 Docker架构与原理       * 了解容器虚拟化和Docker。 * 了解Docker的技术结构和技术原理。 * 熟悉Docker的安装和使用。   本章首先向读者介绍容器虚拟化和Docker之间的关联,再进一步介绍Docker的技术结构和原理,最后介绍Docker的安装和使用过程。 2.1 技术架构 2.1.1 Docker技术构成   Docker软件采用客户-服务(CS架构)的技术架构模式,Docker Client和Docker Daemon交互,Docker Daemon负责创建、运行、发布容器,Docker Client和Docker Daemon可以在同一个系统中,或者Docker Client可以通过REST API远程控制Docker Daemon。Docker Compose负责控制一组应用容器,如图2-1所示。 图2-1 Linux Container 2.1.2 Docker核心技术   Docker核心技术有3类:Cgroups、LXC、AUFS。   1. Cgroups   Cgroups提供了对一组进程及将来子进程的资源限制、控制和统计的能力,这些资源包括CPU、内存、存储、网络等。通过Cgroups,可以方便地限制某个进程的资源占用,并且可以实时监测进程的监控和统计信息。Cgroups的接口通过操作一个虚拟文件系统来实现,一般挂载在(/sys/fs/cgroup)文件夹下。   2. LXC   LXC是Linux Containers的简称,是一种基于容器的操作系统层级的虚拟化技术。LXC项目位于Sourceforge上面,由一个Linux内核补丁和一些用户空间工具组成,其中内核补丁提供底层新特性,上层工具使用这些新特性,提供一套简化的工具来维护容器。   LXC在资管管理方面依赖与Linux内核密切相关的Cgroups子系统,这个子系统是Linux内核提供的一个基于进程组的资源管理框架,可以为特定的进程组限定可以使用的资源,借助Cgroups子系统,在当前Linux环境下实现一个轻量化的虚拟机。   LXC在隔离控制方面依赖于Linux内核提供的namespace特性,具体来说,就是在clone时加入相应的flag。   3. AUFS   AUFS是一种Union File System(联合文件系统),又称Another UnionFS,后来被称为Alternative UnionFS,再后来又被称为高大上的Advance UnionFS。所谓UnionFS,就是把不同物理位置的目录合并(mount)到同一个目录中。UnionFS的一个最主要应用是,把一张CD/DVD和一个硬盘目录联合(mount)在一起,然后,就可以对这个只读的CD/DVD上的文件进行修改(当然,修改的文件存储在硬盘上的目录里)。 2.1.3 Docker打包原理   在LXC的基础上,Docker额外提供的Feature包括:标准统一的打包部署运行方案,为了最大化重用Image,加快运行速度,减少内存和磁盘footprint,Docker Container运行时所构造的运行环境实际上是由具有依赖关系的多个Layer组成的。   在基础的rootfs image的基础上,叠加了包含如Emacs等各种工具的image,再叠加包含apache及其相关依赖library的image,这些image由AUFS文件系统加载合并到统一路径中,以只读的方式存在,最后再叠加加载一层可写的空白Layer,用于记录对当前运行环境所作的修改。有了层级化的image做基础,理想情况下,不同的App就可以尽可能地公用底层文件系统和相关依赖工具等,同一个App的不同示例也可以实现公用绝大多数数据,进而以copy on write的形式维护那份已修改过的数据等。 2.1.4 Docker网络模式   Docker的网络模式包括下列4类:Bridge container(桥接式网络模式)、Host(open)container(开放式网络模式)、Container(join)container(联合挂载式网络模式,是Host网络模式的延伸)、None(Close)container(封闭式网络模式)。   1)Bridge container   当Docker进程启动时,会在主机上生成一个默认的虚拟网桥,此主机上启动的Docker容器会连接到虚拟网桥中,默认的IP地址都是由虚拟网桥生成的。虚拟网桥的工作方式和物理交换机类似,这样主机上的所有容器就通过交换机连在了一个二层网络中。从一个子网中分配一个IP给容器使用,并设置子网的IP地址为容器的默认网关。在主机上创建一对虚拟网卡veth pair设备,Docker将veth pair设备的一端放在新创建的容器中,并命名为eth0(容器的网卡),另一端放在主机中,以veth-xxx这种形式命名,并将这个网络设备加入到虚拟网桥中。   2)Host(open)container   如果启动容器时使用Host模式,那么这个容器将不会获得一个独立的Network Namespace,而是和宿主机共用一个Network Namespace。容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口。但是,容器的其他方面,如文件系统、进程列表等,还是和宿主机隔离的。   3)Container(join)container   这种模式指定新创建的容器和已经存在的一个容器共享一个Network Namespace,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的IP,而是和一个指定的容器共享IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过网卡设备通信。   4)None(Close)container   使用None模式时,Docker容器拥有自己的Network Namespace,但是,并不为Docker容器进行任何网络配置。也就是说,这个Docker容器没有网卡、IP、路由等信息,只有IO网络接口,需要为Docker容器添加网卡、配置IP等。   不参与网络通信,运行于此类容器中的进程仅能访问本地回环接口,仅适用于进程无须网络通信的场景中,如备份、进程诊断及各种离线任务等。   用户可以在Linux虚拟机上使用操作命令来查看容器网络模式,代码如下。 docker network   代码运行结果如图2-2所示。   用户可以在Linux虚拟机上使用容器命令指定使用网络模式,代码如下。 docker network [指定网络模式] 图2-2 Docker网络模式 2.2 技术原理 2.2.1 镜像   镜像是一个只读模板,包含创建Docker容器的说明。通常,一个镜像基于另一个镜像附带一些额外的配置。例如,可以构建一个基于Ubuntu镜像的镜像,但在里面安装Apache Web服务器和应用程序,以及一些确保网站可以详细运行的应用配置(如开放80端口)。   用户可以创建自己的镜像,或者使用其他人发布在注册中心的公开镜像。当创建自己的镜像时需要用特定的语法创建一个Dockerfile,每一个Dockerfile描述都会创建一个层级在你的镜像里,当改变Dockerfile重建镜像时,只有对应的层级会被改变。这样可使镜像轻量化,效率高,灵活性强,如图2-3所示。 图2-3 Docker 镜像   通常使用docker container run和docker service create命令从某个镜像启动一个或多个容器。   一旦容器从镜像启动后,二者之间就变成了互相依赖的关系,并且在镜像上启动的容器全部停止之前,镜像是无法被删除的。尝试删除镜像而不停止或销毁使用它的容器,会导致出错。   镜像通常比较小,容器的目的就是运行应用或者服务,这意味着容器的镜像中必须包含应用服务运行所必需的操作系统和应用文件。   但是,容器又追求快速和小巧,这意味着构建镜像时通常需要裁剪掉不必要的部分,保持较小的体积。例如,Docker镜像通常不会包含6个不同的Shell让读者选择——通常Docker镜像中只有一个精简的Shell,甚至没有Shell。镜像中还不包含内核——容器都是共享所在Docker主机的内核。所以,有时会说容器仅包含必要的操作系统(通常只有操作系统文件和文件系统对象)。   提示:Hyper-V 容器运行在专用的轻量级VM上,同时利用VM内部的操作系统内核。   Docker官方镜像Alpine Linux大约只有4MB,可以说是Docker镜像小巧这一特点的比较典型的例子,如图2-3所示。   在Docker的术语里,一个只读层被称为镜像,一个镜像是永远不会变的。   由于Docker使用一个统一文件系统,Docker进程认为整个文件系统是以读写方式挂载的。但是所有的变更都发生顶层的可写层,而下层的原始的只读镜像文件并未变化。由于镜像不可写,所以镜像是无状态的。   1. 父镜像   每一个镜像都可能依赖于由一个或多个下层而组成的另一个镜像。可以说,下层镜像是上层镜像的父镜像。   2. 基础镜像   一个没有任何父镜像的镜像,称为基础镜像。   3. 镜像ID   所有镜像都是通过一个64位十六进制字符串(内部是一个256 bit的值)来标识的。为简化使用,前12个字符可以组成一个短ID,可以在命令行中使用。短ID有一定的碰撞几率,所以服务器总是返回长ID。镜像关系如图2-4所示。 图2-4 镜像关系 2.2.2 容器   容器是镜像的可运行实例。用户可以使用Docker API或CLI创建、启动、停止、移动或删除容器,还可以将容器连接到一个或多个网络,为其附加存储,甚至可以根据其当前状态创建新映像。   默认情况下,容器与其他容器及其主机相对隔离。用户可以控制容器的网络、存储或其他底层子系统与其他容器或主机之间的隔离程度。   容器由其映像及在创建或启动它时提供给它的任何配置选项定义。当容器被移除时,未存储在持久存储中的对其状态的任何更改都会消失。 2.2.3 数据卷   Docker的镜像是由多个只读的文件系统叠加在一起形成的。启动一个容器时,Docker会加载这些只读层,并在这些只读层的上面(栈顶)增加一个读写层。此时,如果修改正在运行的容器中已有的文件,那么这个文件将会从只读层复制到读写层。该文件的只读版本还在,只是被上面读写层的该文件的副本隐藏。当删除Docker或者重新启动时,之前的更改将会消失。在Docker中,只读层及在顶部的读写层的组合被称为Union File System(联合文件系统)。   为了更好地实现数据保存和数据共享,Docker提出了Volume这个概念,简单地说就是绕过默认的联合文件系统,而以正常的文件或者目录的形式存在于宿主机上,又被称为数据卷。 2.2.4 仓库   镜像仓库(Docker Repository)用于存储具体的Docker镜像,起到仓库存储的作用,比如Tomcat下面有很多版本的镜像,它们共同组成了Tomcat的Repository,通过tag来区分镜像版本,Registry上有很多Repositor。 2.3 安装说明 2.3.1 Docker应用场景   1. 作为云主机使用   相比虚拟机来说,容器使用的是一系列非常轻量级的虚拟化技术,使得其启动、部署、升级同管理进程一样迅速,用起来既灵活又感觉跟虚拟机一样没什么区别,所以有些人直接使用Docker的Ubuntu等镜像创建容器,当作轻量的虚拟机来使用。   特别是现在随着系统、软件越来越多,开发测试环境越来越复杂,仅依靠多用户共享这种方式节省资源带来的后果就是环境完全不可控。Docker容器的出现让每个人仅仅通过一个几KB的Dockerfile文件就能构建一个自定义的系统镜像,进而启动一个完整系统容器,使人人都能成为DevOps。   容器云主机也完全能像普通主机一样随意启动、稳定运行、关机和重启,所以在上面随意搭建博客、小网站、VPN代理服务器等也完全不在话下。除了常用的托管服务业务,完全可以自定义任何用法,包括在上面使用任何云服务提供商的云硬盘和云数据库,部署各种需要的服务。   目前,Docker容器管理服务器在Windows下运行需要借助Toolbox工具,虽然微软在2014年底就计划提供Windows Server容器镜像,但目前还没有发布,所以想要在Docker中运行Windows系统的容器的用户还需要等待,希望到时候微软能裁剪出一种轻巧的Windows基础镜像,毕竟容器本身就是一种更轻量级的系统。   2. 作为服务使用   如果仅仅把Docker容器当作一个轻量的固定虚拟机使用,其实只能算是另类用法,Docker容器最重要的价值在于提供了一整套平台无关的标准化技术,简化服务的部署、升级与维护,只要把需要运维的各种服务打包成标准的集装箱,就可以在任何能运行Docker的环境下运行,达到开箱即用的目的,这才是Docker容器风靡全球的根本原因。 2.3.2 Docker生态圈   1)Chroot Jail   就是人们常见的chroot命令的用法。它诞生于1979年,被认为是最早的容器化技术之一。它可以把一个进程的文件系统隔离起来。   2)FreeBSD Jail   FreeBSD Jail实现了操作系统级别的虚拟化,是操作系统级别虚拟化技术的先驱之一。   3)Linux VServer   使用添加到Linux内核的系统级别的虚拟化功能实现的专用虚拟服务器。   4)Solaris Containers   Solaris Containers也是操作系统级别的虚拟化技术,专为x86和SPARC系统设计。Solaris容器是系统资源控制和通过“区域”提供边界隔离的组合。   5)OpenVZ   OpenVZ是一种Linux操作系统级别的虚拟化技术。它允许创建多个安全隔离的Linux容器,即VPS。   6)Process Containers   Process容器由Google的工程师开发,一般被称为Cgroups。      7)LXC   LXC又称为Linux容器,这也是一种操作系统级别的虚拟化技术,允许使用单个Linux内核在宿主机上运行多个独立的系统。   8)Warden   在最初阶段,Warden使用LXC作为容器运行,如今已被CloudFoundy取代。   9)LMCTFY   LMCTFY是Let Me Contain That For You的缩写。它是Google的容器技术栈的开源版本。Google的工程师一直在与Docker的libertainer团队合作,并将libertainer的核心概念进行抽象并移植到此项目中。该项目的进展不明,估计会被libcontainer取代。   10)Docker   Docker是一个可以将应用程序及其依赖打包到几乎可以在任何服务器上运行的容器的工具。   11)RKT   RKT是Rocket的缩写,是一个专注于安全和开放标准的应用程序容器引擎。 2.3.3 安装Docker   步骤1:安装需要的安装包的代码如下。 yum install -y yum-utils   步骤2:设置镜像仓库的代码如下。 yum-config-manager \   输入内容如下。 --add-repo \ http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo   步骤3:安装Docker相关内容的代码如下。 yum install docker-ce docker-ce-cli containerd.io   其中,docker-ce为社区版,docker-ce-cli是企业版本。   步骤4:启动docker代码如下。 systemctl start docker   步骤5:验证是否安装成功,验证命令代码如下。 docker version   代码运行结果如图2-5和图2-6所示。    图2-5 Docker版本信息1    图2-6 Docker版本信息2 2.3.4 搭建Web服务器   步骤1:使用docker命令查看可用版本,代码如下。 docker search nginx   代码运行结果如图2-7所示。 图2-7 Docker可用版本   步骤2:使用docker命令拉取最新版本的Nginx镜像,代码如下。 docker pull nginx:latest   代码运行结果如图2-8所示。   步骤3:使用docker命令查看是否已安装了Nginx,代码如下。 docker images   代码运行结果如图2-9所示。 图2-8 Docker最新版本 图2-9 镜像仓库   步骤4:使用docker建立容器nginx,并将本地8080端口映射到容器内部的80端口,最后设置容器一直在后台运行,代码如下。 docker run --name nginx -p 8080:80 -d nginx   代码运行结果如图2-10所示。 图2-10 运行nginx容器   步骤5:最后在浏览器中输入IP地址和端口号进入nginx服务器,如图2-11所示。 图2-11 端口登录界面 2.4 基础命令   1. 基础命令   Docker容器的学习需要从最基本的了解容器的常用命令开始,只有熟悉这些命令,才能更好地操作容器,完成想要的操作,信息如表2-1所示,代码格式如下。 docker [基础命令] 表2-1 Docker基础命令 选 项 命 令 环境信息 info、version 镜像仓库命令 login、logout、pull、push、search 镜像管理 build、images、import、load、rmi、save、tag、commit 容器生命周期 create、exec、kill、pause restart、rm、run、start、stop、unpause 容器运维操作 attach、export、inspect、port、ps、top、wait、cp、diff 容器资源管理 volume、network 系统日志信息 events、history、logs   2. Docker环境   查看Docker环境也是学习Docker不可缺少的一部分,可以从容器环境中知道它的一切需求,指令代码如下。 docker info   代码运行结果如图2-12所示。 图2-12 Docker的环境信息   3. 镜像仓库   容器应用的开发和运行离不开可靠的镜像管理,虽然Docker官方也提供了公共的镜像仓库,但是从安全和效率等方面考虑,部署私有环境内的Registry非常必要,镜像仓库对应的命令解释如表2-2所示。 表2-2 镜像仓库命令 选 项 命 令 Login 登录到一个Docker镜像仓库,如果未指定镜像仓库地址,默认为官方仓库 Docker Hub logout 退出一个Docker镜像仓库,如果未指定镜像仓库地址,默认为官方仓库 Docker Hub pull 从镜像仓库中拉取或者更新指定镜像,“-a”表示拉取所有 tagged 镜像 push 将本地的镜像上传到镜像仓库,要先登录到镜像仓库 search 从Docker Hub查找镜像      只需要给出镜像的名字和标签,就能在官方仓库中定位一个镜像(采用“:”分隔)。从官方仓库拉取镜像时,docker image pull命令的格式代码如下。 docker pull :   下面的两条命令用于完成Alpine和Ubuntu镜像的拉取,意为从alpine和ubuntu仓库拉取了标有“latest”标签的镜像,代码如下。 docker pull alpine:latest docker pull ubuntu:latest   代码运行结果如图2-13所示。 图2-13 Alpine和Ubuntu镜像拉取   如何从官方仓库拉取不同的镜像呢?   示例   1)该命令会从官方Mongo库拉取标签为3.3.11的镜像 docker pull mongo:3.3.11   2)该命令会从官方Redis库拉取标签为latest的镜像 docker pull redis:latest   3)该命令会从官方Alpine库拉取标签为latest的镜像 docker pull alpine:latest   docker search命令允许通过CLI的方式搜索Docker Hub。用户可以通过“NAME”字段的内容进行匹配,并且基于返回内容中任意列的值进行过滤。   例如,下面的命令会列出所有仓库名称中包含“alpine”的镜像,代码如下。 docker search alpine   代码运行结果如图2-14所示。 图2-14 alpine所有镜像   4. 镜像管理   本地镜像管理是指通过对应的容器命令对容器本身进行创建、修改、删除、增添等操作,详细的解释说明如表2-3所示。 表2-3 镜像管理命令 选 项 命 令 bulid 用于使用 Dockerfile 创建镜像 images 列出本地镜像 import 从归档文件中创建镜像 load 导入使用docker save命令导出的镜像 rmi 删除本地一个或多少镜像“-f”表示强制删除 save 将指定镜像保存成 tar 归档文件 tag 标记本地镜像,将其归入某一仓库 commit 从容器创建一个新的镜像      5. 系统日志信息   系统日志信息对应的命令解释如表2-4所示。 表2-4 系统日志信息命令 选 项 命 令 Event 从服务器获取实时事件 history 查看指定镜像的创建历史 logs 获取容器的日志   6. 容器生命周期   容器生命周期对应的命令解释如表2-5所示。 表2-5 容器生命周期命令 选 项 命 令 Create 创建一个新的容器,但不启动它 exec 在运行的容器中执行命令 kill 终止一个运行中的容器 pause/unpause 暂停/恢复容器中所有的进程 start/restart 启动/重启一个或多个已经被停止的容 run 创建一个新的容器并运行一个命令 rm 表示删除一个或多个容器。“-l”表示移除容器间的网络连接,而非容器本身。“-v”表示删除与容器关联的卷 stop 停止一个容器      7. 容器运维操作   容器运维操作对应的命令解释如表2-6所示。 表2-6 容器运维操作命令 选 项 命 令 Attach 连接到正在运行中的容器 export 将文件系统作为一个tar归档文件导出到STDOUT inspect 获取容器/镜像的元数据 port 列出指定的容器的端口映射,或者查找将PRIVATE_PORT NAT到面向公众的端口 ps 列出容器 top 查看容器中运行的进程信息,支持ps命令参数 wait 阻塞运行直到容器停止,然后打印出它的退出代码 diff 检查容器里文件结构的更改 cp 用于容器与主机之间的数据复制      8. 容器资源管理   容器资源管理对应的命令解释如表2-7所示。 表2-7 容器资源管理命令 选 项 命 令 volume 数据卷操作 network 容器网络操作    第3章 Docker应用进阶       * 了解Docker的常用命令。 * 了解Docker的实践容器原理。 * 熟悉Docker图形化管理应用。   本章首先向读者介绍Docker的基础命令,再进一步介绍Docker的实践原理,最后介绍Docker图形化管理和监控的过程。 3.1 容器镜像实践   步骤1:创建文件夹,代码如下。 mkdir webimage cd webimage/   步骤2:创建文件,代码如下。 touch index.html vim index.html   步骤3:使用vim在index.html中编写首页代码,代码如下。 首页 我自己的 nginx 镜像 服务器首页   步骤4:生成Docker,代码如下。 touch Dockerfile vim Dockerfile   步骤5:使用vim在Dockerfile文件中编写代码,代码如下。 FROM nginx:latest COPY index.html /usr/share/nginx/html EXPOSE 80 443 CMD ["nginx", "-g", "daemon off;"]   步骤6:基于nginx作为底层镜像,将index.html复制到nginx中,暴露80 443端口,执行命令(不允许nginx后台运行,在Docker容器里直接运行),代码如下。 nginx -g daemon off   步骤7:申请一个Docker账号,界面如图3-1所示。 * 登录https://hub.docker.com/。 * 若没有账号,准备一个邮箱注册一个账号。 * 注册好之后,记住自己的dockerID,示例中注册一个名为dooxo的账号。 * 通常情况下,可在垃圾邮箱中找到激活邮件,激活邮箱之后才可以使用。   步骤8:配置环境变量Dockerid,代码如下。 export DOCKERID=dooxo echo $DOCKERID   步骤9:构建Docker镜像,代码如下。 docker image build --tag $DOCKERID/webimage:1.0 .   代码运行结果如图3-2所示。 图3-1 申请Docker账号 图3-2 构建Docker镜像   步骤10:使用镜像创建容器(-p/--publish宿主机端口号:容器端口号),代码如下。 docker container run -d -p 8090:80 --name mywebimage $DOCKERID /webimage:1.0   代码运行结果如图3-3所示。 图3-3 镜像创建容器   创建成功后打开地址,如图3-4所示。 图3-4 nginx服务器首页   步骤11:接着删除mywebimage容器,代码如下。 docker container rm --force mywebimage   步骤12:上传你的镜像,构建tag2.0的镜像,代码如下。 docker image build --tag $DOCKERID /webimage:2.0   步骤13:查看本地镜像,代码如下。 docker image ls docker image ls -f reference="$DOCKERID/*"   代码运行结果如图3-5所示。 图3-5 镜像仓库   步骤14:登录到docker hub,代码如下。 docker login   代码运行结果如图3-6所示。 图3-6 docker hub登录界面   步骤15:上传镜像,代码如下。 docker image push $DOCKERID/webimage:1.0   代码运行结果如图3-7所示。 图3-7 上传镜像 3.2 容器互联实践 3.2.1 容器互联   1. 运行Docker容器的3种常用方法   (1)执行任务:shell脚本或者自定义App。   (2)交互式运行:类似ssh登录服务器。   (3)后台运行:像长时间守护进程一样运行,如网站、数据库等。   2. Docker容器运行单次任务   步骤1:查看当前容器的容器名,代码如下。 docker container run alpine hostname   代码运行结果如图3-8所示。 图3-8 查看当前容器名   步骤2:查看当前所有容器,代码如下。 docker container ls --all   代码运行结果如图3-9所示。 图3-9 查看所有容器   可以看到容器hostname就是它的container id,而且容器执行完任务后都处于Exited状态,而之前安装的nginx网页服务器状态一直在运行。 3.2.2 运行一个交互器   步骤1:实现在Centos系统上运行Ubuntu系统容器,代码如下。 docker container run -i --tty --rm ubuntu bash   代码命令解释如下。   -i/--interactive 交互式进入Docker容器。   --tty?分配一个伪终端。   --rm?容器运行之后删除容器。   代码运行结果如图3-10所示,Bash shell模式进入容器,正常显示为root@:/#。 图3-10 Ubuntu系统容器   步骤2:使用ls命令查看容器的根目录,代码如下。 root@f1338171757d:/# ls   代码运行结果如图3-11所示。 图3-11 容器的根目录   步骤3:使用ps aux命令查看容器内进程,代码如下。 root@f1338171757d:/# ps aux   代码运行结果如图3-12所示。 图3-12 容器内进程   步骤4:使用exit命令退出容器,由于加了--rm,Docker容器结束之后会被删除,再次列出全部容器,代码如下。 docker container ls --all   代码运行结果如图3-13所示。 图3-13 所有容器界面 3.2.3 运行一个后台进程容器   步骤1:运行一个mysql数据库,代码如下。 docker run -d --name mydb -e MYSQL_ROOT_PASSWORD=123456 mysql:latest   命令解释说明如下。   --detach/-d?后台运行。   --name?容器名字设置。   -e?设置一个环境变量。   步骤2:查看运行中的容器,代码如下。 docker container ls docker ps   代码运行结果如图3-14所示。 图3-14 运行中的容器   步骤3:查看容器的日志,代码如下。 docker logs mydb   代码运行结果如图3-15所示。 图3-15 容器日志   步骤4:查看容器中的进程,代码如下。 docker top mydb   代码运行结果如图3-16所示。 图3-16 运行进程图   可以看到mysqld在容器中运行,即使mysqld在运行,但是它还是独立的,因为网络端口没有暴露出来,不能连通该容器对应的服务。   步骤5:尝试查看mysql的版本号,代码如下。 docker exec –it mydb mysql --user=root --password=$MYSQL_ROOT_PASSWORD –version   代码运行结果如图3-17所示。 图3-17 mysql版本号   步骤6:用docker exec执行sh命令,进入终端,代码如下。 docker exec -it mydb sh   代码运行结果如图3-18所示。 图3-18 进入终端 3.2.4 映射数据卷到容器   回顾上节课中的nginx命令,每当修改网站首页时,都需要进入容器,再到/usr/share/ nginx/html中进行修改。利用下面介绍的方法可以直接映射宿主机的文件到容器,这样直接修改宿主机文件即可直接修改容器数据了。   步骤1:挂载宿主机文件到容器,代码如下。 mkdir myweb cd ~/myweb touch index.html vim index.html   修改index.html内容如下。 首页 我是宿主机上的文件   步骤2:停止原来的mynginx容器,删除原来的mynginx容器(如果没有则跳过),代码如下。 docker ps docker stop mynginx docker rm mynginx   代码运行结果如图3-19所示。 图3-19 停止原来的mynginx并删除容器   步骤3:挂载宿主机文件到容器()-v宿主机目录:容器目录(),代码如下。 sudo docker run --name mynginx -p 8080:80 -d -v /root/myweb:/usr/share/nginx/html nginx   代码运行结果如图3-20所示。 图3-20 nginx登录界面   步骤4:创建数据卷映射到容器,代码如下。 docker volume create my-htmlweb   然后使用inspect命令查看my-htmlweb配置,代码如下。 docker volume inspect my-htmlweb   体积也是绕过容器的文件系统,直接将数据写到主机机器上,只是体积是被docker管理的,docker下所有的体积都在主机机器上的指定目录下/var/lib/docker/volumes。   代码运行结果如图3-21所示。   步骤5:查看my-htmlweb文件夹下的_data文件,代码如下。 cd /var/lib/docker/volumes/my-htmlweb/_data touch index.html vim index.html 图3-21 创建数据卷映射到容器   编写index.html文件内容,代码如下。 首页 我是数据卷上的文件   步骤6:创建index.html文件后,再重新启动nginx,并把原来mynginx文件中使用的rm命令删除,代码如下。 docker ps docker stop mynginx docker rm mynginx   步骤7:将修改后的数据卷文件挂载到nginx上去,代码如下。 sudo docker run --name mynginx -p 8080:80 -d -v my-htmlweb:/usr/share/nginx/html nginx   代码运行结果如图3-22所示。 图3-2 映射数据卷修改页面数据 3.3 容器网络实践 3.3.1 Docker网络   查看docker network命令的相关操作,代码如下。 docker network   代码运行结果如图3-23所示。 图3-23 Docker网络模式   docker network命令的相关操作如下。   (1)connect:将某个容器连接到一个Docker网络。   (2)create:创建一个Docker局域网络。   (3)disconnect:将某个容器退出某个局域网络。   (4)inspect:显示某个局域网络信息。   (5)ls:显示所有Docker局域网络。   (6)prune:删除所有未引用的Docker局域网络。   (7)rm:删除Docker网络。   步骤1:列出Docker网络,代码如下。 docker network ls   代码运行结果如图3-24所示。 图3-24 Docker网络   步骤2:查看网桥网络,注意桥接网络驱动命名和网络名一样的bridge,代码如下。 docker network inspect bridge   代码运行结果如图3-25所示。   步骤3:安装网桥工具,代码如下。 sudo yum install bridge-utils   代码运行结果如图3-26所示。 图3-25 网桥网络信息 图3-26 安装网桥工具   步骤4:列出宿主机的网桥信息,代码如下。 brctl show   代码运行结果如图3-27所示。 图3-27 宿主机的网桥信息 3.3.2 网络连接容量   步骤1:创建一个新的容器,代码如下。 docker run -dt ubuntu sleep infinity   代码运行结果如图3-28所示。 图3-28 创建新容器   步骤2:创建一个基于Ubuntu镜像的后台进程,查看网桥信息,代码如下。 brctl show   可以看到,新建的容器直接和docker0链接,代码运行结果如图3-29所示。 图3-29 网桥信息   步骤3:可以用inspect命令查看bridge桥接网络有多少个容器链接,代码如下。 docker network inspect bridge   代码运行结果如图3-30所示。 图3-30 桥接网络信息   步骤4:可以用ip a命令查看更多网卡信息,代码如下。 ip a   代码运行结果如图3-31所示。 图3-31 网卡信息 3.3.3 检查网络是否连接容器   步骤1:查看宿主机是否可以连通容器,代码如下。 ping -c5 192.168.10.129   代码运行结果如图3-32所示。 图3-32 ping路由IP   步骤2:说明宿主机可以连通刚才创建的容器,接下来检查容器内是否可以通过网桥模式链接到宿主机,代码如下。 docker ps   代码运行结果如图3-33所示。 图3-33 检查容器   步骤3:找到容器id并进入容器,代码如下。 docker exec -it f97aa67135e5 /bin/bash   代码运行结果如图3-34所示。 图3-34 进入容器   步骤4:在容器内部安装ping命令,代码如下。 apt-get update && apt-get install -y iputils-ping   代码运行结果如图3-35所示。 图3-35 安装ping命令   步骤5:查看是否可以连通外网,代码如下。 ping www.baidu.com   代码运行结果如图3-36所示。 图3-36 连通外网 3.3.4 创建自己的局域网   步骤1:创建自己的局域网络,代码如下。 docker network create mynet1   代码运行结果如图3-37所示。 图3-37 创建局域网络   步骤2:列出网络列表,代码如下。 docker network ls   可以发现多了一个mynet1的桥接网络,代码运行结果如图3-38所示。 图3-38 网络列表   步骤3:可以用inspect命令查看mynet1有多少个容器链接,代码如下。 docker inspect mynet1   代码运行结果如图3-39所示。 图3-39 mynet1信息   步骤4:为mynet1加入容器,生成新的nginx链接入mynet1,代码如下。 docker run -d --name mynginxnet1 -p 8091:80 --network mynet1 nginx   代码运行结果如图3-40所示。 图3-40 为mynet1加入容器   步骤5:通过查看发现它们都是桥接的,之前认证过桥接容器和宿主机的互相网络连通,代码如下。 docker network inspect mynet1   代码运行结果如图3-41所示。 图3-41 桥接容器和宿主机的互相网络连通 3.4 Docker图形化管理及监控 3.4.1 Docker常用的可视化(图形化)管理工具   1. Portainer   Portainer是一款Docker可视化管理工具,允许用户在网页中方便地查看和管理Docker容器,如图3-42和图3-43所示。 图3-42 Portainer工具   2. LazyDocker   LazyDocker是基于终端的一个可视化查询工具,支持键盘操作和鼠标点击。相比Portainer来说,LazyDocker可能不那么专业,不过对于开发者来说可能反而更好用。因为一般开发者都是使用命令行来运行Docker,偶尔需要图形化查看时就可以使用LazyDocker这个工具,如图3-44和图3-45所示。 图3-43 Portainer界面 图3-44 LazyDocker工具 图3-45 LazyDocker界面 3.4.2 Linux常用的监控工具   1. Linux Dash   Linux Dash是一个简单易用的Linux系统状态监控工具,如图3-46和图3-47所示。 图3-46 Linux Dash工具 图3-47 Linux Dash界面   2. Cockpit   Cockpit是一个免费且开源的基于Web的管理工具,系统管理员可以执行如存储管理、网络配置、检查日志、管理容器等任务,如图3-48和图3-49所示。 图3-48 Cockpit工具 图3-49 Cockpit界面    第4章 Docker容器云       * 了解Docker容器云的构建思路。 * 了解容器的编排与部署。 * 了解Machine与虚拟机软件。 * 了解Swarm集群抽象工具和Flynn,以及Deis的使用。 * 熟悉容器云搭建过程。   本章首先向读者介绍Docker容器云的构建思路,再进一步介绍容器的编排与部署,以及Swarm集群抽象工具、Flynn和Deis的使用,最后介绍容器云的搭建过程。 4.1 构建容器云 4.1.1 云平台的层次架构   云平台层次架构分为IaaS、PaaS和SaaS。   1)IaaS   IaaS层为基础设施运维人员服务,提供计算、存储、网络及其他基础资源,云平台使用者可以在上面部署和运行包括操作系统和应用程序在内的任意软件,无须再为基础设施的管理而分心。   2)PaaS   PaaS层为应用开发人员服务,提供支撑应用运行所需的软件运行环境、相关工具与服务,如数据库服务、日志服务、监控服务等,让应用开发者可以专注于核心业务的开发。   3)SaaS   SaaS层为一般用户服务,提供了一套完整可用的软件系统,让一般用户无须关注技术细节,只需通过浏览器、应用客户端等方式即可使用部署在云上的应用服务。?   云平台层次架构如图4-1所示。 图4-1 云平台层次架构 4.1.2 构建容器云的思路与步骤   1. 用Docker搭载电商App   为了实现App的功能,需要App Server(可以用Nodejs、PHP、Java、Golang等实现),用redis集群存储系统的session信息,EKL完成系统的日志转发、存储和检索功能。整体系统结构如图4-2所示。 图4-2 整体系统结构   整个系统全都用容器完成,一个Dockerfile就可以完成任意的复制操作。但是上线后如果流量太大,需要负载均衡,这里需要配置静态动态内容分离、URL重定向等。   在此基础上,应用还要被复制成多份,在负载均衡管理下统一提供对外服务,这对分流、灰度发布、高可用及弹性伸缩都是必需的。基于此,需要添加一个HAProxy,启动一个完全相同的App Server,再将两个App Server的IP和端口配置到HAProxy。   但是如何保证App Server容器失败重启或者升级扩展之后,HAProxy可以及时更新自己的配置文件呢?这就需要一个组件负责探测容器退出或者创建的事件发生后通知HAProxy修改配置文件。基于此,需要所有容器本身的IP和端口信息注册到etcd中去,再通过confd定时查询etcd中的数据变化来更新HAProxy的配置文件。   2. Docker搭载电商App时的问题   (1)容器应用的健康检查怎么处理?   (2)如何保证同一个应用的不同容器实例分布在不同或者指定的节点上?   (3)当宿主节点意外退出时,如何保证该节点的容器能在其他宿主节点上恢复?   (4)如何构建测试—开发—上线完整流程的运行机制?   (5)镜像非常多时,复杂的镜像关系会大大拖延容器的创建和启动速度,如何处理?   (6)挂载volume的数据该如何备份?是否需要实现高可用的跨主机迁移?   (7)限制CPU、内存和磁盘的资源如何才算合理?   容器云的概念就此诞生,最直观的形态就是一个颇具规模的容器集群,容器云中按功能或者依赖敏感性划分成组,不同容器之间完全隔离,组内容器允许一定程度的共享。   容器之间的关系不再简单依靠docker link这类原生命令来进行组织,而是借助全局网络管理组件来进行统一治理。   容器云用户也不需要直接面对Docker API,而是借助某种控制器来完成用户操作到Docker容器之间的调用和转译,从而保证底层容器操作对最终用户的友好性。   大多数容器云还会提供完善的容器状态健康检查和高可用支持,并尽可能做到旁路控制而非直接侵入Docker体系。容器云会提供一个高效、完善、可配置的调度器,调度器可以说是容器云系统需要解决的第一要务,运维和管理困难程度往往呈指数级上升。   3. 基础设施层   基础设施层主要为云服务运行环境以及相应的第三方依赖。   (1)构建服务(Web、App、机器学习等)之前,需要选配稳定、可靠的物理设施。   (2)CPU、内存、硬盘、带宽、视频采集、音频采集等。   (3)可以选择现有云服务或者自购裸机服务器,如图4-3所示。   4. 业务服务层   为了支持整体项目进展,理清业务,项目需要的数据存储服务、缓存服务、注册服务、日志分析和下单服务等都由该层部署,如图4-4所示。 图4-3 基础设施层 图4-4 业务服务层   5. 容器编排层   由于应用由数十个乃至数百个松散结合的容器式组件构成,而这些组件需要通过相互间的协同合作才能使既定的应用按照设计运作。容器编排是指对单独组件和应用层的工作进行组织的流程,如图4-5所示。   6. 应用层   在容器技术基础上,对容器进行管理编排,从而实现应用提供接口对外服务,如图4-6所示。 图4-5 容器编排层 图4-6 应用层 4.2 容器的编排与部署   在生产环境中,整个团队需要发布的容器数量可能非常庞大,而容器之间的联系和拓扑结构也可能非常复杂,尤其是企业内部已经运行多年的核心应用服务,往往都是集群化的,并且具备高可用设计(如同步和心跳)或者需要依赖大量复杂的缓存结构,需要关系数据库或者非关系数据库,需要调用其他组服务。   如果依赖人工记录和配置这样复杂的容器关系,并保障集群正常运行、监控、迁移、高可用等常规运维需求,实在是力不从心。   然而在生产环境,尤其是微服务架构中,业务模块一般包含若干个服务,每个服务一般都会部署多个实例。整个系统的部署或启停将涉及多个子服务的部署或启停,而且这些子服务之间还存在强依赖关系,手动操作不仅劳动强度大,而且容易出错。   因此,迫切需要一种像Dockerfile定义Docker容器一样能够定义容器集群的编排和部署工具,来协助用户解决上述棘手问题。Dockerfile重现一个容器,Compose重现容器的配置和集群。   由于Docker的快速发展和企业应用的实现,部署和管理繁多的服务非常困难,为了多容器的管理,Orchard公司开发了基于Docker的Python工具——Fig。   2014年,Docker公司收购了Orchard公司,并将Fig更名为Docker Compose。   Docker Compose并不是通过脚本和各种冗长的docker命令来将应用组件组织起来的,而是通过一个声明式的配置文件描述整个应用,从而使用一条命令完成部署。 4.2.1 Compose的原理   1. 编排   编排(orchestration)是指根据被部署的对象之间的耦合关系,以及被部署对象对环境的依赖,制定部署流程中各个动作的执行顺序,部署过程所需要的依赖文件及被部署文件的存储位置和获取方式,以及如何验证部署成功,这些信息都会在编排工具中以指定的格式(如配置文件或特定的代码)来要求运维人员定义并保存起来,从而保证这个流程能够随时在全新的环境中可靠有序地重现。   2. 部署   部署(deployment)是指按照编排所指定的内容和流程,在目标机器上执行环境初始化,存放指定的依赖文件,运行指定的部署动作,最终按照编排中的规则来确认部署成功。   所以说,编排是一个“指挥家”,它的“大脑”里存储了整个乐曲此起彼伏的演奏流程,对于每一个小节、每一段音乐的演奏方式都了然于胸。而部署就是整个“乐队”,它们严格按照“指挥家”的意图用乐器来完成乐谱的执行。最终,两者通过协作就能把每一位“演奏者”独立的演奏通过组合、重叠、衔接来形成高品位的交响乐。这也是Docker Compose所要完成的使命。   docker-compose是一个Python项目,它是通过调用Dokcer-py库与docker engine交互实现构建Docker镜像,启动、停止Docker容器等操作实现容器编排的。而Docker-py库则是通过调用docker remote API与Docker Daemon交互(可通过DOCKER_HOST配置本地或远程Docker Daemon的地址)来操作Docker镜像与容器的。   为了方便docker-compose,将所管理的对象抽象为3层:工程(project)、服务(service)与容器(container)。   project:通过Docker compose管理的一个项目被抽象成为一个project,它是由一组关联的应用容器组成的一个完整的业务单元。简而言之,就是一个docker-compose.yml文件定义一个project。   service:运行一个应用的容器,实际上可以是一个或多个运行相同镜像的容器。可以通过docker-compose up命令的--scale选项指定某个service运行的容器个数。   docker-compose启动一个工程主要经历以下几个步骤。   (1)工程初始化—解析配置文件(包括docker-compose.yml,外部配置文件extends files,环境变量配置文件env_file),并将每个服务的配置转换成Python字典,初始化docker-py客户端用于与docker engine通信。   (2)根据docker-compose的命令参数将命令分发给相应的处理函数,其中启动命令为up。   (3)调用project类的up函数,得到当前工程中的所有服务,并根据服务的依赖关系进行拓扑排序并去掉重复出现的服务。   (4)通过工程名及服务名从docker engine获取当前工程中处于运行中的容器,从而确定当前工程中各个服务的状态,再根据当前状态为每个服务制定接下来的动作。docker- compose使用labels标记启动的容器,使用docker inspect可以看到通过docker-compose启动的容器都被添加了标记。   (5)创建docker-compose工程的核心在于定义配置文件,配置文件的默认名称为docker- compose.yml,也可以用其他名称,但需要修改环境变量COMPOSE_FILE或者启动时通过-f参数指定配置文件。   (6)下列为一个docker-compose配置文件的示例,其定义的工程包含了两个service,一个是数据库服务test_db,一个是Web服务test_web。其中Web服务包含两个副本,并且要在数据库服务启动后才能启动,如图4-7~图4-9所示。 图4-7 命令解释说明1 图4-8 命令解释说明2 图4-9 命令解释说明3 4.2.2 Fleet的原理   Fleet是一个通过Systemd对CoreOS集群进行控制和管理的工具。Fleet与Systemd之间通过D-Bus API进行交互,每个Fleet Agent之间通过etcd服务来注册和同步数据。Fleet提供的功能非常丰富,包括查看集群中服务器的状态、启动或终止Docker Container、读取日志内容等。   搭建Docker集群,使用etcd作为信息存储,Fleet连接与控制所有节点服务器的systemd,然后通过相应的命令创建或者消灭节点里Docker容器。    4.3 跨平台宿主环境管理工具Machine 4.3.1 Machine与虚拟机软件   Machine的主要功能是帮助用户在不同的云主机提供商上创建和管理虚拟机,并在虚拟机中安装Docker。   用户只需要提供几项登录凭证,即可等待环境安装完成。   Machine能便捷地管理所有通过它创建的Docker宿主机,进行宿主机的启动、关闭、重启、删除等操作。   Docker Machine是一种可以让用户在虚拟主机上安装Docker的工具,并可以使用docker-machine命令来管理主机。   Docker Machine也可以集中管理所有的Docker主机,如快速为100台服务器安装上Docker。 4.3.2 Machine与IaaS平台   Machine在2016年4月发布的v0.6.0版本中支持的IaaS平台已经达到15种,其中包括主流服务提供商Amazon、Google、Microsoft的Amazon EC2、GCE和Azure,也包括开源的OpenStack。与此同时,还有一部分开源社区开发者开发的第三方驱动插件,如今已达到20余种,其中包括国内的UCloud和Aliyun ECS。 4.3.3 Machine示例   使用Docker-Machine远程安装Docker,安装Docker Machine的主机localhost(作为管理主机)上通过docker-machine create命令在另一台Linux主机上远程安装Docker,并将该主机配置为Docker机器,纳入Docker Machine管理。   步骤1:先进行Docker-Machine的安装,代码如下。 base=https://github.com/docker/machine/releases/download/v0.16.0 && curl -L $base/docker-machine-$(uname -s)-$(uname -m) >/tmp/docker-machine && sudo mv /tmp/docker-machine /usr/local/bin/docker-machine && chmod +x /usr/local/bin/docker-machine   步骤2:执行下列命令确认主机是否关闭防火墙,代码如下。 systemctl stop firewalld systemctl stop firewalld   然后还需要确认两台主机之间的网络能够相互连通,再管理主机执行ssh-keygen命令创建密钥对,代码如下。 ssh-keygen   代码运行结果如图4-10所示。 图4-10 创建密钥   步骤3:在管理主机上执行ssh-copy-id命令将密钥对远程复制到目标主机,实现SSH无密码登录,代码如下。 ssh-copy-id [目标主机id地址]   代码运行结果如图4-11所示。 图4-11 将密钥对远程复制到目标主机   可以在目标主机/root/.ssh/authorized_keys下看到控制主机的密钥文件,如图4-12所示。 图4-12 控制主机的密钥文件   步骤4:在管理主机上执行以下操作,将目标主机创建为“Machine”。其中--driver generic参数表示指定generic驱动,--generic-ip-address参数用于指定目标系统的IP地址,最后的host-b参数表示托管主机将要被设置的名称,代码如下。 docker-machine create --driver generic --generic-ip-address=192.168.5.102 host-b   代码运行结果如图4-13所示。 图4-13 配置过程   步骤5:使用docker-machine ls命令查看当前管理的Docker机器,代码如下。 docker-machine ls   代码运行结果如图4-14所示。 图4-14 当前管理的Docker机器   步骤6:登录到目标机器上,可以从/etc/systemd/system/docker.service.d/下看到一个名为10-dmachine.conf的文件,这就是Docker守护进程配置,代码如下。 cd /etc/systemd/system/docker.service.d/ vim 10-dmachine.conf   代码运行结果如图4-15所示。 图4-15 10-dmachine.conf文件   完成以上步骤后,就实现了Docker-Machine的远程安装Docker,可以在目标主机上看到已经被安装好的Docker下的nginx,如图4-16所示。 图4-16 目标主机的Docker 4.4 集群抽象工具Swarm 4.4.1 Swarm概述   在Docker应用越来越深入的今天,把调度粒度停留在单个容器上是非常没有效率的。同样,在提高对Docker宿主机管理效率和利用率的方向上,集群化管理方式是一个正确的选择。Swarm就是将多台宿主机抽象为“一台”的工具。   原来操作Docker集群时,用户必须单独对每一个容器执行命令,如图4-17所示。 图4-17 无Swarm的宿主机   有了Swarm后,使用多台Docker宿主机的方式发生了改变,如图4-18所示。 图4-18 有Swarm的宿主机   Swarm最大程度地兼容Docker的远程API,目前为止,Swarm已经能够支持95%以上的Docker远程API,这使得所有直接调用Docker远程API的程序都能方便地将后端替换为Swarm,这类程序包括Docker官方客户端,以及Fig、Flynn和Deis这类集群化管理使用Docker的工具。   Swarm除了在多台Docker宿主机(或者说多个Docker服务端)上建立一层抽象,还提供对宿主机资源的分配和管理。Swarm通过在Docker宿主机上添加的标签信息将宿主机资源进行细粒度分区,通过分区帮助用户将容器部署到目标宿主机上,同时,通过分区方式还能提供更多的资源调度策略扩展。 4.4.2 Swarm集群的多种创建方式   对于一个Swarm集群而言,集群内节点分成Swarm Agent和Swarm Manager两类。Agent节点运行Docker服务端,Docker Release的版本需要保证一致,且为1.4.0或更新的版本。   Manager节点负责与所有Agent上的Docker宿主机通信及对外提供Docker远程API服务,因此Manager需要能获取到所有Agent地址。   实现方式可以是让所有Agent到网络上的某个位置注册,让Manager到相同的地址获取最新的信息,这样Agent节点的活动就可以被实时侦测;也可以事先将所有Agent的信息写在Manager节点的一个本地文件中,但这种实现无法再动态地为集群增加Agent节点。   创建一个Swarm集群的方法如下。   (1)使用命令去获得一个独一的集群ID,代码如下: swarm create   swarm create命令的实质是向Docker Hub的服务发现地址发送POST请求的过程。   (2)使用etcd创建集群:使用etcd时,要事先获知etcd的地址和存储集群信息的具体路径,存储在etcd上的节点信息都带有一个ttl生存时间,Agent会定时(默认为25秒)更新自身生存时间,保证不会被Manager认定为该节点已不属于集群。   (3)使用静态文件创建集群:可以将所有Agent节点信息通过命令写入Manager节点上的某个文本文件中。   (4)使用Consul创建集群与etcd方式实现类似。   (5)使用ZooKeeper创建集群与etcd方式实现类似。   (6)用户自定义集群创建方式。 4.4.3 Swarm对请求的处理   Manager收到的请求主要可以分为以下4类。   (1)针对已创建容器的操作,Swarm只是起到一个转发请求到特定宿主机的作用。   (2)针对Docker镜像的操作。   (3)创建新的容器命令docker create,其中涉及的集群调度会在后面的内容中进行讲解。   (4)其他获取集群整体信息的操作,如获取所有容器信息、查看Docker版本等。 4.4.4 Swarm集群的调度策略   Swarm管理了多台Docker宿主机,用户在这些宿主机上创建容器时,就会产生究竟与哪台宿主机交互的疑问。   Swarm提供了过滤的功能,用来帮助用户筛选出符合他们条件的宿主机。以一个使用场景为例,用户需要将一个与MySQL相关、名为db的容器部署到一台装有固态硬盘的宿主机上。装有固态硬盘的宿主机在启动Docker服务端时会使用以下命令来添加适当的标签信息,代码如下。 docker -d --label storage=ssd   用户在使用Docker客户端创建db容器时,命令中会带上相应的要求,代码如下。 docker run -d -P -e constraint:storage=ssd --name db mysql   constraint环境变量会被Manager解析,然后筛选出所有带有storage:ssd这一键/值对标签的宿主机作为候选。   除了使用filter,Swarm还提供了strategy来选出最终运行容器的宿主机。   现阶段Swarm有多种调度策略,分别如下。   (1)random策略:random就是在候选宿主机中随机选择一台。   (2)binpacking策略:binpacking会在权衡候选宿主机CPU和内存的占用率后,选择能分配到最大资源的那台宿主机。   (3)spread策略:spread尝试把每个容器平均地部署到每个节点上。 4.4.5 Swarm集群高可用(HA)   在Docker Swarm集群中,Swarm Manager为整个集群服务,并管理多个Docker宿主机的资源,这就导致一个问题,一旦Swarm Manager发生故障,那么整个集群将会瘫痪。所以,对于产品级Swarm集群,高可用解决方案HA(High Availability)非常必要。幸运的是,目前Swarm已经支持leader selector,这就为Swarm集群高可用提供了可能。   HA允许Docker Swarm中的Swarm Manager采取故障转移策略,即主Swarm Manager发生故障,备用的Swarm Manager将会替代原来的Manager,任何时间都会有一台Manager正常工作,从而保证系统的稳定性。 4.5 Flynn与Deis 4.5.1 容器云的基础设施层   1. Flynn   现在的应用程序从源代码到运行阶段太复杂,没有标准的、通用的方式。整个过程及产出分为以下几个阶段。   (1)开发阶段:源代码。   (2)构建阶段:发布包/可执行程序。   (3)部署阶段:可运行的镜像(发布包+配置)。   (4)运行阶段:进程、集群、日志、监控信息、网络。   如果需要管理或者构建一个完整的服务栈,容器扮演的仅仅是一个基本工作单元的角色。在服务栈的最下层,需要有一种资源抽象来为工作单元展示一个统一的资源视图。这样容器就不必关心服务器集群资源情况和网络拓扑,即从容器视角看到的仅仅是“一台”服务器而已。还应该能够根据用户提交的容器描述文件来进行应用容器的编排和调度,为用户创建出符合预期描述的一个或多个容器,交给调度引擎放置到一台或多台物理服务器上运行。如何为容器中正在运行的服务提供负载均衡和反向代理,如何填补从用户代码制品到容器这一“鸿沟”,如何将底层的编排和调度功能API化等,这些功能看似并不“核心”,却是实现“面向应用”云平台的必经之路。   早在Docker得到普遍认同之前,就有一些敏感的极客们意识到了这一点,他们给出的解决方案被称为Flynn,一个具有Layer 0和Layer 1两层架构的类PaaS项目。   2. Flynn的工作   Flynn对宿主机集群实现一个统一的抽象,将容器化的任务进程合理调度并运行在集群上,然后对这些任务进行容器层面的生命周期管理,这一层负责的工作可以总结为以下4点。   (1)分布式配置和协调。   毋庸置疑,这是Zookeeper或etcd的工作。Flynn选择了etcd,但并没有直接依赖它,也就是说,可以方便地通过实现Flynn定义的抽象接口将分布式协同组件更换成其他的方案。   (2)任务调度。   Flynn团队曾重点关注了Mesos和Omega这两个调度方案,最后选择了更简单更易掌控的Omega1。在此基础上,Flyrni原生提供了两种调度器,一种负责调度长运行任务(Service Scheduler),另一种负责调度一次性任务(Ephemeral Scheduler)。   (3)服务发现。   引入etcd后,服务发现就水到渠成了。在Flynn中,服务发现的主要任务是观察被监控节点(包括服务实例和宿主机节点)的上线和下线事件,从而在callback回调中完成每个事件对应的处理逻辑(如更新负载均衡的serve列表)。跟分布式协调组件一样,Flynn同样有一个发现来封装etcd,对外提供统一的服务发现接口,鉴于该设计,ZookeepermDNS也可以用于Flynn的服务发现后端。   (4)宿主机抽象。   所谓宿主机抽象,是指上层系统(Layer 1)以何种方式与宿主机交互。宿主机抽象可以屏蔽不同宿主机系统和硬件带来的不一致。一般来讲,抽象实现的方式是在宿主机上运行一个Agent进程来响应上层的RPC请求,向上层的调度组件报告这台宿主机的资源情况,以及向服务发现组件注册宿主机的存活状态等。Flynn的做法与之类似,不过Flynn还将这个Agent进程作为自身服务框架托管下的一组“服务”进行管理,从而避免了从外部引入一套守护进程所带来的烦琐。 4.5.2 容器云的功能框架层   Flynn构建在Layer 0之上的一套组件统称为Layer 1,它能够基于Layer 0提供的资源,抽象实现容器云所需的上层功能。   上层功能可以总结为以下4点。   (1)API控制器。   同经典PaaS一样,Flynn也运行着一个API后端,以响应用户的HTTP管理请求。   (2)Git接收器。   Flynn使用Git来发布用户代码,Git接收器作为一个获取远程配置在用户方,所以用户推进的代码会直接交给这个接收器来制作代码制品和发布包。   (3)Buildpacks。   用户只需要上传可执行文件包(如WAR包),Buildpack就能够将这些文件按照一定的格式组织成可以运行的实体(如Tomcat+WAR组成的压缩包)。通过定义不同的Buildpack,PaaS就能实现支持不同的编程语言、运行环境、Web容器的组合。   (4)路由组件。   容器服务栈要想正常工作,一个为集群服务的负载均衡路由组件是必不可少的。Flynn的路由组件支持HTTP协议和TCP协议,它能够支持大部分用户服务的访问需求。Flynn的管理类请求也是由路由组件来转交给API控制器的。通过与服务发现组件etcd协作,路由组件可以及时地更新被代理服务的IP和端口。 4.5.3 Flynn体系架构与实现原理   用户代码是如何上传到Flynn并执行运作的呢?主要有以下两种方式。   第一种方式:用户通过Git指令直接提交代码,这时Flynn需要做的工作至少包括以下几项。   (1)接收用户上传的代码。   (2)如果需要的话,按照一定的标准编译代码,组织代码目录。   (3)按照一定的标准将编译后的可执行文件保存到预设的目录中。   如果需要的话,按照一定的标准将可执行文件目录和Web服务器目录组装起来,生成启停脚本和必要的配置信息;将上述包含了可执行文件、Web服务器、控制脚本和配置文件的目录打包保存起来;在需要运行代码时,只需在一个指定的base容器中解压上述包,然后执行启动脚本即可,从可运行实体转换为应用实例。   前文提到,当用户的应用已经被上传并在Flynn中完成了打包工作后,生成的Slug就是一个按照Flynn规定的组织方式,将可执行文件、Web服务器等应用运行所需的各种制品组织在一起的压缩包。   当服务发布完成后,作为一个类PaaS项目,Flynn还需要实现这个服务或应用的整个生命周期管理,包括应用启动和停止、状态监控和测量。   第一,整个服务的生命周期管理的实现很简单,只需要针对微光生成的启动命令和运行起来的PID进行操作即可,这里不再过多介绍,有兴趣的读者可以自行研究Buildpack的工作原理。   第二,服务和应用的状态监控。Flynn Host上的Container Manager进程负责实施健康检查,并检测本身运行着的容器数目,检测结果会更新Flynn数据库中的Formations表。另一端的Flynn Controller保持监听该表的数据变化,一旦发现预期的实例数和实际的实例数不一致,Controller就会根据差异值重新在某台Flynn Host下载并运行对应的Slug(或者删除多余的实例)。   第三,服务的水平扩展。如果需要增加实例,Flynn由用户指定某个Flynn Host来启动新的实例容器。如果用户不指定Host,那么Flynn调度器会选择一个当前运行中的实例数目最小的Host来运行。如果用户需要减少实例,Flynn会直接选择这个服务或应用的最新实例,然后把它们删除。   第二种方式:直接上传用户Docker镜像并运行起来。   当需要运行某个应用时,Flynn会从Blobstore中下载对应的Slug来运行。Flynn如果想要用户上传一个Docker镜像来运行,就要想办法把镜像制作成一个伪Slug。   回顾Flynn借助Buildpack所做的三步工作,对于Docker镜像来说,发现和编写两步是不需要的,所以Flynn处理Docker镜像的过程直接来到了释放这一步骤。   最后,就可以使用scale指令启动Docker镜像。 4.5.4 Deis的原理与使用   1. Deis基础概述   Deis是一个Django/Celery API服务器、Python CLI和一组Chef cookbooks合并起来提供一个类似Heroku的应用平台,用于公有云和私有云。Deis的口号是:Your PaaS. Your Rules。   Deis是一个开源的PaaS系统,简化和Linux Container容器和Chef节点的发布和伸缩。可用于托管应用、数据库、中间件和其他服务。Deis利用Chef、Docker和Heroku Buildpacks来提供的私有PaaS是非常轻量级和灵活的。   Deis提供开箱即用的Ruby、Python、Node.js、Java、Clojure、Scala、Play、PHP、Perl、Dart和Go语言的支持。此外,Deis可使用Heroku Buildpacks、Docker images和Chef recipes发布任何内容。Deis主要用来与不同的云提供商进行交互,支持EC2等平台。   2. Deis原理的4个阶段   (1)构建阶段。   Builder组件处理git push请求,并且在临时的Docker容器内构建应用并生成一个新的Docker镜像(image)。   (2)发布阶段。   在发布阶段,一个Build和配置结合起来创建出一个新的数字型的发行版本(release)。紧接着这个发行版本会被推送到Docker registry以便稍后执行。当一个新的Build被创建或者配置发生改变时,都会触发构建新的发行版本,这样回滚代码或配置更改都会变得很容易。   (3)运行阶段。   在运行阶段,容器会被分派到调度器(Scheduler)并且更新相应的路由。调度器负责将容器发布到主机上,并且保证它们在集群上的均衡。容器一旦处于健康状态,koi会被推送到路由组件。旧的容器只有在新的容器上线并且开始处理请求后,才会被收起来以保证零停机部署。   (4)备份服务。   Deis把数据库、缓存、存储、消息系统及其他后端服务当作附加资源,以符合十二要素应用程序的最佳实践。   应用通过使用环境变量附加后端服务,因为应用与后端服务之间没有耦合,所以应用可以任意独立地进行扩展,与由其他应用提供的服务通信,或者切换为外部服务及第三方提供的服务。 4.5.5 Deis与Flynn的比较   Flynn和Deis是Docker的两个云计算微PaaS技术,它们都可以作为一个PaaS平台,但是它们不像旧式的PaaS范式那样将Docker与其他混合装机在一起,而是寻求一种重新定义的PaaS途径。Flynn和Deis已经重新定义了微PaaS概念,也就是说任何人都可以在自己的硬件上付出不太多的努力就可以运行它们。   下面就以下几个方面进行比较。   (1)CoreOS。两个项目都采取了CoreOS来驱动集群和分布式架构,Flynn使用CoreOS的etcd系统实现服务发现和集群级别的配置。Deis也是整个都采取CoreOS。CoreOS是一个自然基于集群分布式的OS(类似Riak),并且都是使用Docker作为首要选项。   (2)绝不是Heroku的克隆。任何人只要谈到构建PaaS,都认为只是重新发明轮子,将部署接口、负载平衡器、服务配置等捆绑在一起,但是Flynn和Deis不同,因为通常意义上人们看到的PaaS只是在用户接口和应用部署上更高的一种架构,而Flynn和Deis执着于提供一个健壮的可伸缩扩展的系统层,来驱动服务发现、任务调度和集群管理。 Flynn创建者将它们的架构划分为Layer 0(系统层)和Layer 1(部署维护层),很显然受Google Omega paper鼓舞,也可以和ZooKeeper和Mesos比较。   (3)在很多方面类似Heroku。一旦将它们的系统层直接带入开发者面向的层面,事情变得又非常类似Heroku。Flynn提供基于Procfile的部署规范,而Deis提供基于Dockerfile或Heroku Buildpack的规范。更有甚者,Flynn和Deis也可以提供基于git push的部署方式。   (4)面向服务的架构。Flynn和Deis是能够构造轻量服务和基于系统模块的应用,Docker是它们构建模块的必备基础,因为Docker容器可以帮助建立轻量可分布式的多租户系统。 4.6 容器云示例 4.6.1 Hadoop简介   Hadoop是一个由Apache基金会所开发的分布式系统基础架构,是一个能够对大量数据进行分布式处理的软件框架;Hadoop以一种可靠、高效、可伸缩的方式进行数据处理;用户可以在不了解分布式底层细节的情况下,开发分布式程序,如图4-19所示。   Hadoop采用了master/slave结构,一个HDFS架构包含一个master节点和若干个slave节点,为了应对节点故障,都会有数据备份,如图4-20所示。 图4-19 Hadoop 图4-20 master/slave结构   DataNode:存储数据,DataNode负责客户端请求的读写操作,在NameNode的调度下进行复制、创建和删除,DataNode以机架形式组织,机架通过交换机链接,DataNode内的数据按照block存储。   NameNode:一般单独运行一个实例机器上,主要存放两种数据:editlog和fsimage。NameNode对任何元数据产生的修改都会被记录在事务日志editlog上,而整个文件系统的元数据(命名空间、数据块到文件的映射、文件的属性等)都会存放在fsimage文件中。   Hadoop的安装模式如下。   (1)单机模式。   只在一台机器上运行,存储采用本地文件系统,没有采用分布式文件系统HDFS。   (2)伪分布式模式。   存储采用分布式文件系统HDFS,但是HDFS的节点和数据节点都在同一机器上。   (3)分布式模式。   存储采用分布式文件系统HDFS,而且HDFS的节点和数据节点位于不同的机器上。 4.6.2 基于Docker搭建Hadoop集群   搭建三节点Hadoop集群的操作步骤如下。   步骤1:使用Docker从镜像仓库下载Hadoop,代码如下。 sudo docker pull kiwenlau/hadoop:1.0   代码运行结果如图4-21所示。 图4-21 下载Hadoop   步骤2:下载git仓库,代码如下。 git clone https://github.com/kiwenlau/hadoop-cluster-docker   代码运行结果如图4-22所示。 图4-22 下载git仓库   步骤3:使用docker network命令创建一个名为Hadoop的网络模式,代码如下。 sudo docker network create --driver=bridge hadoop   代码运行结果如图4-23所示。 图4-23 创建Hadoop网络模式   如果不小心创建出错,可以通过以下命令进行纠正,代码如下。 docker network ls //查看已创建的网络列表 docker network rm [需要删除的网络] //删除网络   步骤4:进入已经下载好的hadoop-cluster-docker文件夹中,使用命令进入容器中,代码如下。 cd hadoop-cluster-docker ./start-container.sh   代码运行结果如图4-24所示。 图4-24 进入容器   从图4-24中可以看出,共启动了3个容器,分别是1个master容器和2个slave容器,运行命令后会自动进入hadoop-master容器的/root目录下。   步骤5:在容器内启动Hadoop,代码如下。 ./start-hadoop.sh   代码运行结果如图4-25所示。 图4-25 启动Hadoop   步骤6:在容器内运行wordcount,代码如下。 ./run-wordcount.sh   代码运行结果如图4-26所示。 图4-26 wordcount容器运行结果   以上步骤完成后,就搭建好了基于Docker的Hadoop集群(三节点),Hadoop网页管理地址如下。 NameNode: http://192.168.5.102:50070/ ResourceManager: http://192.168.5.102:8088/   192.168. 5.102为运行容器的主机的IP地址,如图4-27和图4-28所示。 图4-27 节点管理界面 图4-28 Hadoop资源管理界面 第5章 Docker与微服务       * 了解什么是微服务。 * 了解微服务的创建和部署,以及如何迁移微服务。   本章首先向读者介绍什么是微服务,再介绍如何创建和部署微服务,最后介绍迁移到微服务的操作步骤。 5.1 微服务概述 5.1.1 什么是微服务   1. 单体应用   早些年,各大互联网公司的应用技术栈大致可分为LAMP和MVC两大流派。无论是LAMP还是MVC,都是为单体应用架构设计的,其优点是学习成本低,开发上手快,测试、部署、运维也比较方便,甚至一个人就可以完成一个网站的开发与部署。然而随着业务规模的不断扩大,团队开发人员的不断扩张,单体应用架构开始出现下列问题。   (1)部署效率低下。当单体应用的代码越来越多,依赖的资源越来越多时,应用编译打包、部署测试一次,甚至需要10分钟以上。   (2)团队协作开发成本高。早期在团队开发人员只有2~3人时,协作修改代码,最后合并到同一个master分支,然后打包部署,尚且可控。但是一旦团队人员扩张,超过5人修改代码,然后一起打包部署,测试阶段只要有一块功能有问题,就需要重新编译打包部署,然后重新预览测试,所有相关的开发人员又都得参与其中,效率低下,开发成本极高。   (3)系统高可用性差。因为所有的功能开发最后都部署到同一个WAR包里,运行在同一个Tomcat进程中,一旦某一功能涉及的代码或者资源有问题,就会影响整个WAR包中部署的功能。   (4)线上发布变慢。特别是对于Java应用来说,一旦代码膨胀,服务启动的时间就会变长,有些甚至超过10分钟,如果机器规模超过100台,假设每次发布的步长为10%,单次发布就需要100分钟。因此,急需一种方法能够将应用的不同模块解耦,降低开发和部署成本。   2. 服务化   服务化是指把一个大型系统中的各个业务进行抽象后,以服务为单位进行开发和管理的方法。与之相关联就是面向服务架构(SOA)。   面向服务架构都是一种软件设计风格,其理念是通过服务组件来实现一个系统的需求。每个SOA服务都是一个独立的功能单元,可以独立执行。   SOA服务的4个属性如下。   (1)逻辑上代表了一种具有特定结果的商业活动。   (2)它是自成一体的。   (3)它对消费者来说是一个黑匣子,消费者不需要知道该服务的内部运作。   (4)可能由其他基础服务组成。   3. 微服务   那么微服务相比于服务化又有什么不同呢?可以总结为以下4点。   (1)服务拆分维度更细。微服务可以说是更细维度的服务化,小到一个子模块,只要该模块依赖的资源与其他模块都没有关系,就可以拆分为一个微服务。   (2)服务独立部署。每个微服务都严格遵循独立打包部署的准则,互不影响。比如一台物理机上可以部署多个Docker实例,每个Docker实例可以部署一个微服务的代码。   (3)服务独立维护。每个微服务都可以交由一个小团队甚至是个人来开发、测试、发布和运维,并对整个生命周期负责。   (4)服务治理能力要求高。因为拆分为微服务后,服务的数量变多,因此需要有统一的服务治理平台来对各个服务进行管理。   4. 服务化拆分   (1)纵向拆分,是从业务维度进行拆分。标准是按照业务的关联程度来决定,关联比较密切的业务适合拆分为一个微服务,而功能相对比较独立的业务适合单独拆分为一个微服务。   (2)横向拆分,是从公共且独立功能维度进行拆分。标准是按照是否有公共的被多个其他服务调用,且依赖的资源独立,不与其他业务耦合来决定。   以社交App举例,无论是首页信息流、评论、消息箱还是个人主页,都需要显示用户的昵称。假如用户的昵称功能有产品需求的变更,则需要上线几乎所有的服务,这样成本就会变高。显而易见,如果把用户的昵称功能单独部署成一个独立的服务,那么有什么变更只需要上线这个服务即可,其他服务不受影响,开发和上线成本就大大降低了。      5. 单体应用迁移到微服务架构遇到的问题   (1)服务如何定义?   对于单体应用来说,不同功能模块之间相互交互时,通常是以类库的方式来提供各个模块的功能。对于微服务来说,每个服务都运行在各自的进程中,应该以何种形式向外界传达自己的信息呢?答案就是接口,无论采用哪种通信协议(HTTP或者RPC),服务之间的调用都通过接口描述来约定,约定内容包括接口名、接口参数及接口返回值。   (2)服务如何发布和订阅?   单体应用由于部署在同一个WAR包里,接口之间的调用属于进程内的调用。而拆分为微服务独立部署后,服务提供者该如何对外暴露自己的地址,服务调用者该如何查询所需要调用的服务的地址呢?此时就需要一个类似登记处的地方,能够记录每个服务提供者的地址以供服务调用者查询,在微服务架构里,这个地方就是注册中心。   (3)服务如何监控?   通常对于一个服务,人们最关心的是QPS(调用量)、AvgTime(平均耗时)及P999(99.9%的请求响应时间在多少毫秒以内)等指标。此时就需要一种通用的监控方案,能够覆盖业务埋点、数据收集和数据处理,最后到数据展示的全链路功能。   (4)服务如何治理?   可以想象,拆分为微服务架构后,服务的数量变多了,依赖关系也变复杂了。比如一个服务的性能有问题时,依赖的服务都势必会受到影响。可以设定一个调用性能阈值,如果一段时间内一直超过这个值,那么依赖服务的调用可以直接返回,这就是熔断,也是服务治理最常用的手段之一。   (5)故障如何定位?   在单体应用拆分为微服务后,一次用户调用可能依赖多个服务,每个服务又部署在不同的节点上,如果用户调用出现问题,则需要有一种解决方案能够将一次用户请求进行标记,并在多个依赖的服务系统中继续传递,以便串联所有路径,从而进行故障定位。 5.1.2 微服务架构   服务提供者按照一定格式的服务描述,向注册中心注册服务,声明自己能够提供哪些服务及服务的地址是什么,完成服务发布。   服务消费者请求注册中心,查询所需要调用服务的地址,然后以约定的通信协议向服务提供者发起请求,得到请求结果后再按照约定的协议解析结果。   在服务的调用过程中,服务的请求耗时、调用量及成功率等指标都会被记录下来用作监控,调用经过的链路信息也会被记录下来,用于故障定位和问题追踪。在此期间,如果调用失败,可以通过重试等服务治理手段来保证成功率。   图5-1所示为微服务架构。 图5-1 微服务架构   微服务架构下,服务调用主要依赖以下几个基本组件,如图5-2所示。 图5-2 微服务基本组件   (1)服务描述。   (2)注册中心。   (3)服务框架。   (4)服务监控。   (5)服务追踪。   (6)服务治理。   1. 服务描述   (1)服务调用首先要解决的问题就是服务如何对外描述。比如,你对外提供了一个服务,那么这个服务的服务名是什么?调用这个服务需要提供哪些信息?调用这个服务返回的结果是什么格式的?该如何解析?这些就是服务描述需要解决的问题。   (2)常用的服务描述方式包括RESTful API、XML配置及IDL文件3种。其中,XML配置方式多用作RPC协议的服务描述,通过*.xml配置文件来定义接口名、参数及返回值类型等。IDL文件方式通常用作Thrift和gRPC这类跨语言服务调用框架中,如gRPC就是通过Protobuf文件来定义服务的接口名、参数及返回值的数据结构,如图5-3所示。 图5-3 服务的接口名、参数及返回值的数据结构   2. 注册中心   如果用户提供了一个服务,要想让外部想调用这项服务的人知道,就需要一个类似注册中心的角色,服务提供者将自己提供的服务及地址登记到注册中心,服务消费者则从注册中心查询所需要调用的服务的地址,然后发起请求。   一般来讲,注册中心的工作流程如下。   (1)服务提供者在启动时,根据服务发布文件中配置的发布信息向注册中心注册自己的服务。   (2)服务消费者在启动时,根据消费者配置文件中配置的服务信息向注册中心订阅自己所需要的服务。   (3)注册中心返回服务提供者地址列表给服务消费者。   (4)当服务提供者发生变化,如有节点新增或者销毁,注册中心将变更通知给服务消费者。   注册中心的工作流程如图5-4所示。 图5-4 注册中心的工作流程图   3. 服务框架   通过注册中心,服务消费者就可以获取到服务提供者的地址,有了地址后就可以发起调用。但在发起调用前还需要解决以下几个问题。   (1)服务通信采用什么协议?就是说服务提供者和服务消费者之间以什么样的协议进行网络通信,是采用四层TCP、UDP协议,还是采用七层HTTP协议,还是采用其他协议。   (2)数据传输采用什么方式?就是说服务提供者和服务消费者之间的数据传输采用哪种方式,是同步还是异步,是在单连接上传输,还是多路复用。   (3)数据压缩采用什么格式?通常数据传输都会对数据进行压缩,以减少网络传输的数据量,从而减少带宽消耗和网络传输时间,如常见的JSON序列化、Java对象序列化及Protobuf序列化等。   4. 服务监控   一旦服务消费者与服务提供者之间能够正常发起服务调用,就需要对调用情况进行监控,以了解服务是否正常。通常来讲,服务监控主要包括3个流程。   1)指标收集   就是把每一次服务调用的请求耗时及成功与否收集起来,并上传到集中的数据处理中心。   2)数据处理   有了每次调用的请求耗时及成功与否等信息,就可以计算每秒服务请求量、平均耗时及成功率等指标。   3)数据展示   数据收集起来,经过处理后,还需要以友好的方式对外展示,才能发挥价值。通常都是将数据展示在Dashboard面板上,并且每隔10秒等间隔自动刷新,用作业务监控和报警等。   5. 服务追踪   除了需要对服务调用情况进行监控,还需要记录服务调用经过的每一层链路,以便进行问题追踪和故障定位。服务追踪的工作原理大致如下。   (1)服务消费者发起调用前,会在本地按照一定的规则生成一个requestid,发起调用时,将requestid当作请求参数的一部分,传递给服务提供者。   (2)服务提供者接收到请求后,记录下这次请求的requestid,然后处理请求。如果服务提供者继续请求其他服务,会在本地再生成一个自己的requestid,然后把这两个requestid都当作请求参数继续往下传递。   以此类推,通过这种层层往下传递的方式,一次请求,无论最后依赖多少次服务调用、经过多少服务节点,都可以通过最开始生成的requestid串联所有节点,从而达到服务追踪的目的。   6. 服务治理   服务监控能够发现问题,服务追踪能够定位问题所在,而解决问题就得靠服务治理了。服务治理就是通过一系列的手段来保证在各种意外情况下,服务调用仍然能够正常进行。在生产环境中,经常会遇到下列几种状况。   (1)单机故障。通常遇到单机故障后,都是靠运维发现并重启服务或者从线上摘除故障节点。然而集群的规模越大,越容易遇到单机故障,在机器规模超过100台以上时,仅靠传统的人工运维显然难以应对。而服务治理可以通过一定的策略,自动摘除故障节点,不需要人为干预,就能保证单机故障不会影响业务。   (2)单IDC故障。大家或许经常听说某某App,因为施工挖断光缆导致大批量用户无法使用的严重故障。而服务治理可以通过自动切换故障IDC的流量到其他正常IDC,从而避免因为单IDC故障引起的大批量业务受到影响。   (3)依赖服务不可用。如果你的服务依赖于另一个服务,当另一个服务出现问题时,会拖慢甚至拖垮你的服务。而服务治理可以通过熔断,在依赖服务异常的情况下,一段时期内停止发起调用而直接返回。这样一方面保证了服务消费者能够不被拖垮,另一方面也给服务提供者减少了压力,使其能够尽快恢复。 5.1.3 微服务的优缺点   1. 微服务的优点   (1)微服务是松藕合的,无论是在开发阶段还是部署阶段都是独立的。   (2)能够快速响应,局部修改容易,一个服务出现问题不会影响整个应用。   (3)易于和第三方应用系统集成,支持使用不同的语言开发,允许融合最新技术。   (4)每个微服务都很小,足够内聚,足够小,代码容易理解。团队能够更关注自己的工作成果,聚焦指定的业务功能或业务需求。   (5)开发简单,开发效率高,一个服务可能就是专一地只干一件事,能够被小团队单独开发,这个小团队可以由2~5个开发人员组成。   2. 微服务的缺点   (1)微服务架构带来了过多的运维操作,可能需要团队具备一定的DevOps技巧。   (2)分布式系统可能复杂难以管理。因为分布部署跟踪问题难,当服务数量增加后,管理复杂性也会增加。 5.2 服务容器化   1. 微服务带来的问题   (1)测试和运维部署的成本提升。单体应用拆分成多个微服务后,能够实现快速开发迭代,但随之而来是测试和运维部署成本的提升,拆分成多个微服务后,有的业务需求需要同时修改多个微服务的代码,此时就有多个微服务都需要打包、测试和上线发布,一个业务需求就需要同时测试多个微服务接口的功能,上线发布多个系统,增加了很多测试和运维的工作量。   (2)微服务的软件配置依赖不同。拆分后的微服务相比原来大的单体应用更加灵活,经常要根据实际的访问量做在线扩缩容,而且通常会采用在公有云上创建的ECS来扩缩容。这又给微服务的运维带来另一个挑战,因为公有云上创建的ECS通常只包含基本的操作系统环境,微服务运行依赖的软件配置等需要运维再单独进行初始化工作,因为不同的微服务的软件配置依赖不同,比如Java服务依赖于JDK,就需要在ECS上安装JDK,而且可能不同的微服务依赖的JDK版本也不相同,一般情况下新的业务可能依赖的版本比较新,如JDK 8,而有些旧业务可能依赖的版本还是JDK 6,为此服务部署的初始化工作十分烦琐。   2. 容器化解决的问题   (1)环境一致问题。镜像解决了DevOps中微服务运行的环境难以在本地环境、测试环境及线上环境保持一致的难题。如此一来,开发就可以把在本地环境中运行测试通过的代码,以及依赖的软件和操作系统本身打包成一个镜像,然后自动部署在测试环境中进行测试,测试通过后再自动发布到线上环境中,整个开发、测试和发布的流程就打通了。   (2)Docker镜像运行环境封装。无论是使用内部物理机还是公有云的机器部署服务,都可以利用Docker镜像把微服务运行环境封装起来,从而屏蔽机器内部物理机和公有云机器运行环境的差异,实现同等对待,降低运维的复杂度。   (3)Docker能帮助解决服务运行环境可迁移问题的关键,就在于Docker镜像的使用上。实际在使用Docker镜像时往往并不是把业务代码、依赖的软件环境及操作系统本身直接都打包成一个镜像,而是利用Docker镜像的分层机制,在每一层通过编写Dockerfile文件来逐层打包镜像。这是因为虽然不同的微服务依赖的软件环境不同,但是还是存在大大小小的相同之处,因此在打包Docker镜像时,可以分层设计、逐层复用,这样可以减少每一层镜像文件的大小。   3. 微服务容器化运维   业务容器化后,运维面对的不再是一台台实实在在的物理机或者虚拟机,而是一个个Docker容器,它们可能都没有固定的IP,要想发布服务,需要用一个面向容器的新型运维平台。   一个容器运维平台通常包含以下几个组成部分:镜像仓库、资源调度、容器调度、调度策略、服务编排。   1)镜像仓库   (1)权限控制。   镜像仓库都设有两层权限控制:一是必须登录才可以访问,这是最外层的控制,它规定了哪些人可以访问镜像仓库;二是对镜像按照项目的方式进行划分,每个项目拥有自己的镜像仓库目录,并且给每个项目设置项目管理员、开发者及客人3个角色,只有项目管理员和开发者拥有自己镜像仓库目录下镜像的修改权限,而客人只拥有访问权限,项目管理员可以为这个项目设置哪些人是开发者。   (2)镜像同步。   在实际的生产环境中,往往需要把镜像同时发布到几十台或者上百台集群节点上,单个镜像仓库实例往往受带宽限制无法同时满足大量节点的下载需求,此时就需要配置多个镜像仓库实例来做负载均衡,同时也会产生镜像在多个镜像仓库实例之间同步的问题。一般来说,有两种解决方案,一种是一主多从,主从复制的方案,比如开源镜像仓库Harbor采用了这种方案;另一种是P2P的方案,如阿里的容器镜像分发系统蜻蜓就采用了P2P方案。   (3)高可用性。   一般而言,高可用性设计无非就是把服务部署在多个IDC,这样即使有IDC出现问题,也可以把服务迁移到其他正常的IDC中去。   2)资源调度   为了解决资源调度的问题,Docker官方提供了Docker Machine功能,通过Docker Machine可以在企业内部的物理机集群,或者虚拟机集群(如OpenStack集群),又或者公有云集群(如AWS集群)等上创建机器并且直接部署容器。Docker Machine的功能虽然很好,但是对于大部分已经发展了一段时间的业务团队来说,并不能直接拿来使用。   (1)物理机集群。   大部分中小团队应该都拥有自己的物理机集群,并且大多按照集群—服务池—服务器这种模式进行运维。   (2)虚拟机集群。   很多业务团队在使用物理机集群后,发现物理机集群存在使用率不高、业务迁移不灵活的问题,因此纷纷转向了虚拟化方向,构建自己的私有云,比如以OpenStack技术为主的私有云集群在国内外很多业务团队中都有大规模的应用。   (3)公有云集群。   现在越来越多的业务团队,尤其是初创公司,因为公有云快速灵活的特性,纷纷在公有云上搭建自己的业务。公有云最大的好处除了快速灵活、分钟级即可实现上百台机器的创建,还有一个优点就是配置统一、便于管理,不存在机器配置碎片化问题。   3)容器调度   容器调度是指,假如现在集群里有一批可用的物理机或者虚拟机,当服务需要发布时,该选择哪些机器部署容器。   比如集群里只有10台机器,并且已经有5台机器运行着其他容器,剩余5台机器空闲着,如果此时有一个服务要发布,但只需要3台机器即可。这时可以靠运维人为地从5台空闲的机器中选取3台机器,然后把服务的Docker镜像下载下来,再启动Docker容器服务即可完成发布。   这时如果集群中有上百台机器,就需要有专门的容器调度系统,为此也诞生了不少基于Docker的容器调度系统,比如Docker原生的调度系统Swarm、Mesosphere出品的Mesos,以及Google开源的Kubernetes。   4)调度策略   调度策略主要是为了解决容器创建时选择哪些主机最合适的问题,一般都是通过给主机打分来实现的。比如Swarm就包含了两种类似的策略:spread和binpack,它们都会根据每台主机的可用CPU、内存及正在运行的容器的数量来打分。spread策略会选择一个资源使用最少的节点,以使容器尽可能地分布在不同的主机上运行。它的好处是可以使每台主机的负载都比较平均,而且如果有一台主机有故障,受影响的容器也最少。而binpack策略恰恰相反,它会选择一个资源使用最多的节点,从而让容器尽可能地运行在少数机器上,节省资源的同时也避免了主机使用资源的碎片化。   具体选择哪种调度策略,还要综合实际的业务场景,通常的场景有以下几种。   (1)各主机的配置基本相同,并且使用也比较简单,一台主机上只创建一个容器。这样的话,每次创建容器时,直接从还没有创建过容器的主机中随机选择一台即可。   (2)在某些在线、离线业务混布的场景下,为了达到主机资源使用率最高的目标,需要综合考量容器中跑的任务的特点,比如在线业务主要使用CPU资源,而离线业务主要使用磁盘和I/O资源,这两种业务的容器大部分情况下适合混跑在一起。   (3)还有一种业务场景,主机上的资源都是充足的,每个容器只要划定了所用的资源限制,理论上跑在一起是没有问题的,但是有些时候会出现对某个资源的抢占,比如都是CPU密集型或者I/O密集型的业务,就不适合容器混跑在一台主机上。   5)服务编排   (1)服务依赖。   大部分情况下,微服务之间是相互独立的,在进行容器调度时不需要考虑彼此。但有时也会存在一些场景,比如服务A调度的前提必须是先有服务B,这就要求在进行容器调度时,还需要考虑服务之间的依赖关系。   Docker官方提供了Docker Compose的解决方案,它允许用户通过一个单独的docker-compose.yaml文件来定义一组相互关联的容器组成一个项目,从而以项目的形式来管理应用。比如要实现一个Web项目,不仅要创建Web容器(如Tomcat容器),还需要创建数据库容器(如MySQL容器)、负载均衡容器(如Nginx容器)等,此时就可以通过docker-compose.yaml来配置这个Web项目里包含的3个容器。   (2)服务发现。   容器调度完成以后,容器就可以启动了,但此时容器还不能对外提供服务,服务消费者并不知道这个新的节点,所以必须具备服务发现机制,使得新的容器节点能够加入到线上服务中去。   基于Nginx的服务发现主要是针对提供HTTP服务的,当有新的容器节点时,修改Nginx的节点列表配置,然后利用Nginx的重新加载机制,会重新读取配置,从而把新的节点加载进来。比如基于Consul-Template和Consul,把Consul作为DB存储容器的节点列表,Consul-Template部署在Nginx上,Consul-Template定期去请求Consul,如果Consul中存储的节点列表发生变化,就会更新Nginx的本地配置文件,然后Nginx就会重新加载配置。   基于注册中心的服务发现主要是针对提供RPC服务的,当有新的容器节点时,需要调用注册中心提供的服务注册接口。在使用这种方式时,如果服务部署在多个IDC,就要求容器节点分IDC进行注册,以便实现同IDC内就近访问。以微博的业务为例,微博服务除了部署在内部的两个IDC,还在阿里云上有部署,这样,内部机房上创建的容器节点应该加入到内部IDC分组,而云上的节点应该加入到阿里云的IDC。   (3)自动扩缩容。   容器完成调度后,仅仅做到有容器不可用时故障自愈还不够,有时还需要根据实际服务的运行状况,做到自动扩缩容。   一个很常见的场景就是,大部分互联网业务的访问呈现出访问时间的规律性。以微博业务为例,白天和晚上的使用人数远远大于凌晨的使用人数;而白天和晚上的使用人数也不是平均分布的,午高峰12点半和晚高峰10点半是使用人数最多的时刻。这时就需要根据实际使用需求,在午高峰和晚高峰时刻,增加容器的数量,确保服务的稳定性;在凌晨以后减少容器的数量,减少服务使用的资源成本。 5.3 微服务的创建与部署 5.3.1 DevOps   传统的业务上线流程是:开发人员开发完业务代码后,把自测通过的代码打包交给测试人员,然后测试人员把代码部署在测试环境中进行测试,如果测试不通过,就反馈问题给开发人员进行修复;如果通过,开发人员就把测试人员通过的代码交给运维人员打包,然后运维人员再发布到线上环境中去。   可见在传统的开发模式下,开发人员、测试人员和运维人员的职责划分十分明确,他们往往分属于不同的职能部门,一次业务上线流程需要三者之间进行多次沟通,整个周期基本上是以天为单位。假如能够把开发、测试和发布流程串联起来,就像生产流水线那样,每个步骤完成后,就自动执行下一个步骤,无须过多的人为干预,业务的迭代效率就会大大提升。   因此,DevOps应运而生,它是一种新型的业务研发流程,业务开发人员不仅需要负责业务代码的开发,还需要负责业务的测试及上线发布等全生命周期,真正做到掌控服务全流程。DevOps就是图5-5所示的中心部分,集开发、测试和运维三者角色于一体。 图5-5 DevOps示意图   而要实现DevOps,就必须开发人员完成代码开发后,能自动进行测试,测试通过后,能自动发布到线上。对应的这两个过程就是CI和CD,具体含义如下。   (1)CI(Continuous Integration),持续集成。开发人员完成代码开发后,能自动进行代码检查、单元测试、打包部署到测试环境,进行集成测试,跑自动化测试用例。   (2)CD(Continuous Deploy),持续部署。代码测试通过后,能自动部署到类生产环境中进行集成测试,测试通过后再进行小流量的灰度验证,验证通过后代码就达到线上发布的要求了,就可以把代码自动部署到线上。   其中,CD还有另外一个解释,就是持续交付(Continuous Delivery),它与持续部署不同的是,持续交付只需要做到代码达到线上发布要求的阶段即可,接下来的代码部署到线上既可以选择手动部署,也可以选择自动部署。   比较通用的实现DevOps的方案主要有两种,一种是使用Jenkins,一种是使用GitLab。如图5-6所示,一个服务的发布流程主要包含如下3个步骤。 图5-6 服务的发布流程   1)持续集成   这个步骤的主要作用是确保每一次代码的Merge Request都测试通过,可随时合并到代码的Develop分支,主要包括4个阶段:build阶段(开发分支代码的编译与单元测试)、package阶段(开发分支代码打包成Docker镜像)、deploy阶段(开发分支代码部署到测试环境)、test阶段(开发分支代码集成测试)。   2)持续交付   这个步骤的主要作用是确保所有代码合并Merge Request到Develop分支后,Develop分支的代码能够在生产环境中测试通过,并进行小流量灰度验证,可随时交付到线上。主要包括5个阶段:build阶段(Develop分支代码的编译与单元测试)、package阶段(Develop分支代码打包成Docker镜像)、deploy阶段(Develop分支代码部署到测试环境)、test阶段(Develop分支代码集成测试)、canary阶段(Develop分支代码的小流量灰度验证)。   3)持续部署   这个步骤的主要作用是合并Develop分支到Master主干,并打包成Docker镜像,可随时发布到线上。主要包括4个阶段:build阶段(Master主干代码的编译与单元测试)、package阶段(Master主干代码打包成Docker镜像)、clearup阶段(Master主干代码Merge回Develop分支)、production阶段(Master主干代码发布到线上)。   1. 持续集成阶段   1)代码检查   通过代码检查可以发现代码潜在的一些错误,如Java对象有可能是null空指针等,实际执行时可以在持续集成阶段集成类似Sonarqube之类的工具来实现代码检查。   2)单元测试   单元测试是保证代码运行质量的第二个关卡。单元测试是针对每个具体代码模块的,单元测试的覆盖度越高,各个代码模块出错的概率就越小。不过在实际业务开发过程中,为了追求开发速度,许多开发者并不在意单元测试的覆盖度,而是把大部分测试工作都留在了集成测试阶段,这样可能会造成集成测试阶段返工的次数太多,需要多次修复漏洞才能通过集成测试。尤其对于业务复杂度比较高的服务来说,在单元测试阶段多花费一些工夫,其实从整个代码开发周期角度来看,收益还是要远大于付出的。   3)集成测试   集成测试就是将各个代码的修改集成到一起,统一部署在测试环境中进行测试。为了实现整个流程的自动化,集成自测阶段的主要任务就是跑每个服务的自动化测试用例,所以自动化测试用例覆盖得越全,集成测试的可靠性就越高。这就要求开发和测试能及时沟通,在新的业务需求确定时,就开始编写测试用例,这样在跑自动化测试用例时,就不需要测试的介入了,节省了沟通成本。当然,业务开发人员也可以自己编写测试用例,这样就不需要专职的业务测试人员了。   2. 持续交付阶段   持续交付阶段的主要目的是保证最新的业务代码能够在类生产环境中正常运行。一般做法都是从线上生成环境中摘掉两个节点,然后在这两个节点上部署最新的业务代码,再进行集成测试,集成测试通过后再引入线上流量,来观察服务是否正常。通常需要解决以下两个问题。   如何从线上生产环境中摘除两个节点。这就需要接入线上容器管理平台,比如微博的容器管理平台DCP就提供了API,能够从线上生产环境中摘除某个节点,然后部署最新的业务代码。   如何观察服务是否正常。由于这两个节点上运行的代码是最新的代码,在引入线上流量后可能会出现内存泄露等在集成测试阶段无法发现的问题,所以在这个阶段,这两个节点上运行最新代码后的状态必须与线上其他节点一致。实际观察时,主要有两个手段,一个是观察节点本身的状态,如CPU、内存、I/O、网卡等;一个是观察业务运行产生的warn、error的日志量大小,尤其是当error日志量有异常时,往往说明最新的代码可能存在异常,需要处理后才能发布到线上。   3. 持续部署阶段   持续部署阶段的主要目的是把在类生产环境下运行通过的代码自动发布到线上所有节点中去。 5.3.2 Service Mesh   Service Mesh(服务网格)的概念最早是由Buoyant公司的CEO William Morgan在一篇文章中提出的,他给出的定义是:   Service Mesh是一种新型的用于处理服务与服务之间通信的技术,尤其适用于以云原生应用形式部署的服务,能够保证服务与服务之间调用的可靠性。在实际部署时,Service Mesh通常以轻量级的网络代理方式与应用的代码部署在一起,从而以应用无感知的方式实现服务治理。   1. 与传统的微服务架构的本质区别   Service Mesh以轻量级的网络代理方式与应用的代码部署在一起,用于保证服务与服务之间调用的可靠性,这与传统的微服务架构有着本质区别,具体体现在以下两点。   (1)跨语言服务调用的需要。大多数公司通常都存在多个业务团队,每个团队业务所采用的开发语言一般都不相同。以微博的业务为例,移动服务端的业务主要采用的是PHP语言开发,API平台的业务主要采用的是Java语言开发,移动服务端调用API平台使用的是HTTP请求。如果要进行服务化,改成RPC调用,就需要一种既支持PHP语言又支持Java语言的服务化框架。   (2)云原生应用服务治理的需要。现在微服务越来越多开始容器化,并使用类似Kubernetes的容器平台对服务进行管理,逐步向云原生应用的方向进化。而传统的服务治理要求在业务代码里集成服务框架的SDK,这显然与云原生应用的理念相悖,因此迫切需要一种对业务代码无侵入的适合云原生应用的服务治理方式。   2. Service Mesh的实现原理   Buoyant公司开发的第一代Service Mesh产品Linkerd如图5-7所示,服务A要调用服务B,经过Linkerd来代理转发,服务A和服务B的业务代码不需要关心服务框架功能的实现。为此Linkerd需要具备负载均衡、熔断、超时重试、监控统计及服务路由等功能。这样,对于跨语言服务调用来说,即使服务消费者和服务提供者采用的语言不同,也不需要集成各自语言的SDK。 图5-7 Linkerd服务产品   而对于云原生应用来说,可以在每个服务部署的实例上,都同等地部署一个Linkerd实例。如图5-8所示,服务A要想调用服务B,首先调用本地的Linkerd实例,经过本地的Linkerd实例转发给服务B所在节点上的Linkerd实例,最后再由服务B本地的Linkerd实例把请求转发给服务B。这样,所有的服务调用都得经过Linkerd进行代理转发,所有的Linkerd组合起来就像一个网格一样,这也是为什么把这项技术称为Service Mesh,也就是“服务网格”的原因。 图5-8 服务网格   可见Service Mesh的实现原理有以下两个。   (1)一个是轻量级的网络代理,也被称为SideCar,它的作用就是转发服务之间的调用。   (2)一个是基于SideCar的服务治理,也被称为Control Plane,它的作用是向SideCar发送各种指令,以完成各种服务治理功能。   3. SideCar   在传统的微服务架构下服务调用的原理如图5-9所示,服务消费者除了自身的业务逻辑实现,还需要集成部分服务框架的逻辑,如服务发现、负载均衡、熔断降级、封装调用等;而服务提供者除了实现服务的业务逻辑外,也要集成部分服务框架的逻辑,如线程池、限流降级、服务注册等。 图5-9 传统微服务框架   而在Service Mesh架构中,服务框架的功能都集中在SideCar中实现,并在每一个服务消费者和服务提供者的本地都部署一个SideCar,服务消费者和服务提供者只负责自己的业务实现,服务消费者向本地的SideCar发起请求,本地的SideCar根据请求的路径向注册中心查询,得到服务提供者的可用节点列表后,再根据负载均衡策略选择一个服务提供者节点,并向这个节点上的SideCar转发请求,服务提供者节点上的SideCar完成流量统计、限流等功能后,再把请求转发给本地部署的服务提供者进程,从而完成一次服务请求。整个流程可以参考图5-10。 图5-10 Service Mesh服务框架   服务消费者节点上的SideCar称为正向代理,服务提供者节点上的SideCar称为反向代理,那么Service Mesh架构的关键点就在于服务消费者发出的请求如何通过正向代理转发,以及服务提供者收到的请求如何通过反向代理转发。   基于iptables的网络拦截是一种解决方案。这种方案如图5-11所示,节点A上的服务消费者发出的TCP请求都会被拦截,然后发送给正向代理监听的端口15001,正向代理处理完成后再把请求转发到节点B的端口9080。节点B端口9080上的所有请求都会被拦截发送给反向代理监听的端口15001,反向代理处理完后再转发给本机上服务提供者监听的端口9080。 图5-11 基于iptables的网络拦截   既然SideCar能实现服务之间的调用拦截功能,那么服务之间的所有流量都可以通过SideCar来转发,这样所有的SideCar就组成了一个服务网格,再通过一个统一的地方与各个SideCar交互,就能控制网格中流量的运转了,这个统一的地方在Service Mesh中就被称为Control Plane,如图5-12所示。 图5-12 Control Plane的主要作用   4. Control Plane的主要作用   Control Plane包括以下几方面。   1)服务发现   服务提供者会通过SideCar注册到Control Plane的注册中心,这样服务消费者把请求发送给SideCar后,SideCar就会查询Control Plane的注册中心来获取服务提供者节点列表。   2)负载均衡   SideCar从Control Plane获取到服务提供者节点列表信息后,需要按照一定的负载均衡算法从可用的节点列表中选取一个节点发起调用,可以通过Control Plane动态修改SideCar中的负载均衡配置。   3)请求路由   SideCar从Control Plane获取的服务提供者节点列表,也可以通过Control Plane来动态改变,如需要进行A/B测试、灰度发布或者流量切换时,就可以动态地改变请求路由。   4)故障处理   服务之间的调用如果出现故障,就需要加以控制,常用的手段有超时重试、熔断等,这些都可以在SideCar转发请求时,通过Control Plane动态配置。   5)安全认证   可以通过Control Plane控制一个服务可以被谁访问,以及访问哪些信息。   6)监控上报   所有SideCar转发的请求信息都会发送到Control Plane,再由Control Plane发送给监控系统,如Prometheus等。   7)日志记录   所有SideCar转发的日志信息也会发送到Control Plane,再由Control Plane发送给日志系统,如Stackdriver等。      8)配额控制   可以在Control Plane中为服务的每个调用方配置最大调用次数,在SideCar转发请求给某个服务时,会审计调用是否超出服务对应的次数限制。 5.3.3 Istio   随着技术的发展,如今再谈到Service Mesh时,往往第一个想到的是Istio。之所以说Istio是Service Mesh的代表产品,主要有以下几个原因。   (1)相比Linkerd,Istio引入了Control Plane的理念,通过Control Plane能带来强大的服务治理能力,可以称得上是Linkerd的进化,算是第二代Service Mesh产品。   (2)Istio默认的SideCar采用了Envoy,它是用C++语言实现的,在性能和资源消耗上比采用Scala语言实现的Linkerd小,这点对于延迟敏感型和资源敏感型的服务来说尤其重要。   (3)有Google和IBM的背书,尤其是在微服务容器化的大趋势下,云原生应用越来越受欢迎,而Google开源的Kubernetes可以说已经成为云原生应用默认采用的容器平台。基于此,Google可以将Kubernetes与Istio很自然地整合,打造成云原生应用默认的服务治理方案。   如图5-13所示,Istio的架构由两部分组成,分别是Proxy和Control Plane。 图5-13 Istio的架构   Proxy就是前面提到的SideCar,与应用程序部署在同一个主机上,应用程序之间的调用都通过Proxy来转发,目前支持HTTP/1.1、HTTP/2、gRPC及TCP请求。   Control Plane与Proxy通信,来实现各种服务治理功能,包括3个基本组件:Pilot、Mixer及Citadel。   1. Proxy   Istio的Proxy采用的是Envoy,Envoy与前面提到的Linkerd是同一代产品,既要作为服务消费者端的正向代理,又要作为服务提供者端的反向代理,一般需要具备服务发现、服务注册、负载均衡、限流降级、超时熔断、动态路由、监控上报和日志推送等功能。   Envoy主要包含以下几个特性。   1)性能损耗低   因为采用了C++语言实现,Envoy能提供极高的吞吐量和极少的长尾延迟,而且对系统的CPU和内存资源占用也不大,所以跟业务进程部署在一起不会对业务进程造成影响。   2)可扩展性高   Envoy提供了可插拔过滤器的能力,用户可以开发定制过滤器以满足自己特定的需求。   3)动态可配置   Envoy对外提供了统一的API,包括CDS(集群发现服务)、RDS(路由发现服务)、LDS(监听器发现服务)、EDS(EndPoint发现服务)、HDS(健康检查服务)、ADS(聚合发现服务)等。通过调用这些API,可以实现相应配置的动态变更,而无须重启Envoy。   2. Pilot   Pilot的作用是实现流量控制,它通过向Envoy下发各种指令来实现流量控制,其架构如图5-14所示。从架构图中可以看出,Pilot主要包含以下几个部分。 图5-14 Envoy架构图   (1)Rules API。   对外封装统一的API,供服务的开发者或者运维人员调用,可以用于流量控制。   (2)Envoy API。   对内封装统一的API,供Envoy调用以获取注册信息、流量控制信息等。   (3)Abstract Model(抽象模型层)。   对服务的注册信息、流量控制规则等进行抽象,使其描述与平台无关。   (4)Platform Adapter(平台适配层)。   用于适配各个平台如Kubernetes、Mesos、Cloud Foundry等,把平台特定的注册信息、资源信息等转换成抽象模型层定义的与平台无关的描述。   Pilot是如何实现流量管理功能的呢?   (1)服务发现和负载均衡。如图5-15所描述的那样,服务B(也就是服务提供者)注册到对应平台的注册中心中去,如Kubernetes集群中的Pod,启动时会注册到注册中心etcd中。然后服务A(也就是服务消费者)在调用服务B时,请求会被Proxy拦截,然后Proxy会调用Pilot查询可用的服务提供者节点,再以某种负载均衡算法选择一个节点发起调用。此外,Proxy还会定期检查缓存的服务提供者节点的健康状况,当某个节点连续多次健康检查失败后,就会被Proxy从缓存的服务提供者节点列表中剔除。 图5-15 服务发现和负载均衡   (2)请求路由。Pilot可以对服务进行版本和环境的细分,服务B包含两个版本:v1.5和v2.0-alpha,其中,v1.5是生产环境运行的版本,而v2.0-alpha是灰度环境运行的版本。当需要做A/B测试时,希望灰度服务B的1%流量运行v2.0-alpha版本,就可以通过调用Pilot提供的Rules API,Pilot就会向Proxy下发路由规则,Proxy在转发请求时就按照给定的路由规则,把1%的流量转发给服务B的v2.0-alpha版本,99%的流量转发给服务B的v1.5版本,如图5-16所示。 图5-16 路由规则   (3)超时重试。默认状态下,Proxy转发HTTP请求时的超时是15秒,可以通过调用Pilot提供的Rules API来修改路由规则,覆盖这个限制。比如下面这段路由规则,表达的意思是ratings服务的超时时间是10秒。 apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: ratings spec: hosts: - ratings http: - route: - destination: host: ratings subset: v1 timeout: 10s   (4)故障注入。Istio还提供了故障注入的功能,能在不排除服务节点的情况下,通过修改路由规则,将特定的故障注入到网络中。它的原理是在TCP层制造数据包的延迟或者损坏,从而模拟服务超时和调用失败的场景,以此来观察应用是否健壮。比如下面这段路由规则的意思是,对v1版本的ratings服务流量中的10%注入5秒的延迟。 apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: ratings spec: hosts: - ratings http: - fault: delay: percent: 10 fixedDelay: 5s route: - destination: host: ratings subset: v1   3. Mixer   Mixer的作用是实现策略控制和监控日志收集等功能,实现方式是每一次Proxy转发的请求都要调用Mixer,它的架构如图5-17所示。而且Mixer的实现是可扩展的,通过适配层来适配不同的后端平台,这样Istio的其他部分就不需要关心各个基础设施,如日志系统、监控系统的实现细节等。 图5-17 Mixer架构图   理论上每一次的服务调用Proxy都需要调用Mixer,一方面检查调用的合法性,一方面要上报服务的监控信息和日志信息,所以这就要求Mixer必须是高可用和低延迟的,那么Mixer是如何做到的呢?图5-18是它的实现原理,从图中可以看到Mixer实现了两级的缓存结构。   (1)Proxy的本地缓存。为了减少Proxy对Mixer的调用,以尽量降低服务调用的延迟,在Proxy这一端会有一层本地缓存,但由于Proxy作为SideCar与每个服务实例部署在同一个节点上,所以不能对服务节点有太多的内存消耗,所以就限制了Proxy本地缓存的大小和命中率。   (2)Mixer的本地缓存。Mixer是独立运行的,所以可以在Mixer这一层使用大容量的本地缓存,从而减少对后端基础设施的调用,一方面可以减少延迟,另一方面也可以最大限度地减少后端基础设施故障给服务调用带来的影响。 图5-18 两级的缓存结构   Mixer是如何实现策略控制和监控日志收集功能呢?   (1)策略控制。Istio支持两类策略控制,一类是对服务的调用进行速率限制,一类是对服务的调用进行访问控制,它们都是通过在Mixer中配置规则来实现的。   (2)监控日志收集。跟策略控制的实现原理类似,Mixer的监控日志收集功能也是通过配置监控yaml文件来实现的,Proxy发起的每一次服务调用都会先调用Mixer,把监控信息发给Mixer,Mixer再根据配置的yaml文件来决定监控信息发送的目的地。   4. Citadel   Citadel的作用是保证服务之间访问的安全,它的工作原理如图5-19所示,可以看出实际的安全保障并不是Citadel独立完成的,而是需要Proxy、Pilot及Mixer的配合。具体来讲,Citadel里存储了密钥和证书,通过Pilot把授权策略和安全命名信息分发给Proxy。Proxy与Proxy之间的调用使用双向TLS认证来保证服务调用的安全。最后由Mixer来管理授权和审计。 图5-19 实现策略控制和监控日志收集功能 5.4 迁移到微服务   迁移到微服务的具体步骤如下。   (1)清理应用程序。确保应用程序具有良好的自动化测试套件,并使用了最新版本的软件包、框架和编程语言。   (2)重构应用程序,把它拆分成多个模块,为模块定义清晰的API。不要让外部代码直接触及模块内部,所有的交互应该通过模块提供的API来进行。   (3)从应用程序中选择一个模块,并把它拆分成独立的应用程序,部署在相同的主机上。可以从中获得一些好处,而不会带来太多的运维麻烦。不过,仍然需要解决这两个应用之间的交互问题,虽然它们都部署在同一个主机上。同时,可以忽略微服务架构里固有的网络分区问题和分布式系统的可用性问题。   (4)把独立出来的模块移动到不同的主机上。现在,需要处理跨网络交互问题,这样可以让这两个系统之间的耦合降得更低。   (5)如果可能,可以重构数据存储系统,让另一个主机上的模块负责自己的数据存储。    第6章 Kubernetes架构解析       * 了解Kubernetes架构解析。 * 列举一些Kubernetes的简单例子。 * 了解Kubernetes的核心概念。 * 从Kubernetes架构解析。   本章首先向读者介绍Kubernetes架构解析,通过列举一些Kubernetes的简单例子来更好地了解Kubernetes,再深入地挖掘Kubernetes核心概念,并从Kubernetes架构上来解析。 6.1 Kubernetes基础简介 6.1.1 什么是Kubernetes   首先,Kubernetes是一个全新的基于容器技术的分布式架构领先方案。Kubernetes是谷歌严格保密十几年的秘密武器——Borg的一个开源版本。Borg是谷歌内部使用的一个大规模集群管理系统,它基于容器技术,目的是实现资源管理的自动化,以及跨多个数据中心的资源利用率的最大化。   直到2015年4月,传闻许久的Borg论文伴随Kubernetes的高调宣传被谷歌首次公开,大家才得以了解它的更多内幕。   Kubernetes(简称k8s,因为第一个字母k和最后一个字母s中间有8个字母),其概念为:Kubernetes是一个完备的分布式系统支撑平台。Kubernetes具有完备的集群管理能力,包括多层次的安全防护和准入机制、多租户应用支撑能力、透明的服务注册和服务发现机制、内建的智能负载均衡器、强大的故障发现和自我修复能力、服务滚动升级和在线扩容能力、可扩展的资源自动调度机制,以及多粒度的资源配额管理能力。同时,Kubernetes提供了完善的管理工具,这些工具涵盖了包括开发、部署测试、运维监控在内的各个环节。因此,Kubernetes是一个全新的基于容器技术的分布式架构解决方案,并且是一个一站式的完备的分布式系统开发和支撑平台,如图6-1所示。 图6-1 Kubernetes 6.1.2 Kubernetes基础知识   在Kubernetes中,Service是分布式集群架构的核心,一个Service对象拥有以下4种关键特征。   (1)拥有唯一指定的名称(如mysql-server)。   (2)拥有一个虚拟IP(Cluster IP)和端口号。   (3)能够提供某种远程服务能力。   (4)能够将客户端对服务的访问请求转发到一组容器应用上。   Service的服务进程目前都基于Socket通信方式对外提供服务,比如Redis、Memcache、MySQL、Web Server,或者是实现了某个具体业务的特定TCP Server进程。虽然一个Service通常由多个相关的服务进程提供服务,每个服务进程都有一个独立的Endpoint(IP+Port)访问点,但Kubernetes能够让用户通过Service(虚拟Cluster IP +Service Port)连接到指定的Service。有了Kubernetes内建的透明负载均衡和故障恢复机制,不管后端有多少服务进程,也不管某个服务进程是否由于发生故障而被重新部署到其他机器,都不会影响对服务的正常调用。更重要的是,这个Service本身一旦创建就不再变化,这意味着用户再也不用为Kubernetes集群中服务的IP地址变来变去的问题而头疼了。   容器提供了强大的隔离功能,所以有必要把为Service提供服务的这组进程放入容器中进行隔离。为此,Kubernetes设计了Pod对象,将每个服务进程都包装到相应的Pod中,使其成为在Pod中运行的一个容器(Container)。为了建立Service和Pod间的关联关系,Kubernetes首先给每个Pod都贴上一个标签(Label),给运行MySQL的Pod贴上name=mysql标签,给运行PHP的Pod贴上name=php标签,然后给相应的Service定义标签选择器(Label Selector),比如MySQL Service的标签选择器的选择条件为name=mysql,意为该Service要作用于所有包含name=mysql Label的Pod。这样一来,就巧妙解决了Service与Pod的关联问题。   这里先简单介绍Pod的概念。首先,Pod运行在一个被称为节点(Node)的环境中,这个节点既可以是物理机,也可以是私有云或者公有云中的一个虚拟机,通常在一个节点上运行着几百个Pod;其次,在每个Pod中都运行着一个特殊的被称为Pause的容器,其他容器则为业务容器,这些业务容器共享Pause容器的网络栈和Volume挂载卷,因此它们之间的通信和数据交换更为高效,在设计时可以充分利用这一特性将一组密切相关的服务进程放入同一个Pod中;最后,需要注意的是,并不是每个Pod和它里面运行的容器都能被映射到一个Service上,只有提供服务(无论是对内还是对外)的那组Pod才会被映射为一个服务。   在集群管理方面,Kubernetes将集群中的机器划分为一个Master和一些Node。在Master上运行着集群管理相关的一组进程:kube-apiserver、kube-controller-manager和kubescheduler,这些进程实现了整个集群的资源管理、Pod调度、弹性伸缩、安全控制、系统监控和纠错等管理功能,并且都是自动完成的。Node作为集群中的工作节点,运行真正的应用程序,在Node上Kubernetes管理的最小运行单元是Pod。在Node上运行着Kubernetes的kubelet、kube-proxy服务进程,这些服务进程负责Pod的创建、启动、监控、重启、销毁,以及实现软件模式的负载均衡器。最后,来看一下传统的IT系统中服务扩容和服务升级这两个难题。   在Kubernetes集群中,只需为需要扩容的Service关联的Pod创建一个Deployment,服务扩容和服务升级等令人头疼的问题都迎刃而解。在一个Deployment定义文件中包括以下3个关键信息。   (1)目标Pod的定义。   (2)目标Pod需要运行的副本数量(Replicas)。   (3)要监控的目标Pod的标签。   在创建好Deployment(系统将自动创建好Pod)后,Kubernetes会通过在Deployment中定义的Label筛选出对应的Pod实例并实时监控其状态和数量,如果实例数量少于定义的副本数量,则会根据在Deployment中定义的Pod模板创建一个新的Pod,然后将此Pod调度到合适的Node上启动运行,直到Pod实例的数量达到预定目标。   为什么要用Kubernetes?原因有以下几个。   首先,可以“轻装上阵”地开发复杂系统。   其次,可以全面拥抱微服务架构。微服务架构的核心是将一个巨大的单体应用分解为很多小的互相连接的微服务,一个微服务可能由多个实例副本支撑,副本的数量可以随着系统的负荷变化进行调整。   再次,可以随时随地将系统整体“搬迁”到公有云上。Kubernetes最初的设计目标就是让用户的应用运行在谷歌自家的公有云GCE中,华为云(CCE)、阿里云(ACK)和腾讯云(TKE)先后宣布支持Kubernetes集群。   Kubernetes内在的服务弹性扩容机制可以让用户轻松应对突发流量,Kubernetes系统架构超强的横向扩容能力可以让用户的竞争力大大提升。 6.2 Kubernetes的核心概念   1. k8s的资源对象主要分为两种   (1)某种资源的对象,如节点(Node)、Pod、服务(Service)、存储卷(Volume)等。   (2)与资源对象相关的事物与动作,如标签(Label)、注解(Annotation)、命名空间(Namespace)、部署(Deployment)、HPA、PVC等。   k8s资源对象可以用yaml或者json格式声明。每个资源对象都有自己的特定结构定义,并统一保存在etcd这种非关系型数据库中。资源对象可以通过k8s提供的工具kubectl工具进行增删改,如以下内容说明。 # SOURCE: https://cloud.google.com/kubernetes-engine/docs/tutorials/guestbook apiVersion: apps/v1 #版本 kind: Deployment #类别 metadata: name: frontend #名称 spec: replicas: 3 selector: matchLabels: app: guestbook tier: frontend template: metadata: labels: #标签 ...   如上代码所示,每个资源对象都包含以下几个通用属性。   (1)版本:版本信息里面包括了对此对象所属的资源组,一些资源对象的属性会随着版本的升级而变化。   (2)类别:定义资源的类型。   (3)名称:名称在全局唯一。   (4)标签:k8s通过标签来表明资源对象的特征、类别,以及通过标签筛选不同的资源对象并实现对象之间的关联、控制或协作功能。   (5)注解:一种特殊的标签,通用于实现资源对象属性的自定义扩展。   2. 主要资源   (1)Pod:Pod是Kubernetes创建或部署的最小/最简单的基本单位,一个Pod代表集群上正在运行的一个进程。   (2)ReplicaSet:它的主要作用是确保Pod以用户指定的副本数运行,即如果有容器异常退出,会自动创建新的Pod来替代;而异常多出来的容器也会自动回收。   (3)Deployment:Deployment定义了一组Pod的信息。Deployment的主要职责与RC相似,同样是为了保证pod的数量和健康。此外还支持滚动升级、回滚等多种升级方案。   (4)DaemonSet:DaemonSet确保全部(或者某些)节点上运行一个Pod的副本。当有节点加入集群时,也会为他们新增一个Pod。当有节点从集群移除时,这些Pod也会被回收。   (5)Job:批处理任务通常并行(或串行)启动多个计算进程去处理一批工作项(work item),处理完成后,整个批处理任务结束。   (6)CronJob:CronJob即定时任务,类似于Linux系统的crontab,在指定的时间周期运行指定的任务。   (7)StatefulSet:StatefulSet里的每个Pod都有稳定、唯一的网络标识,可以用来发现集群内的其他成员。   3. Pod配置   (1)ConfigMap:ConfigMap存储Pod的配置文件。   (2)Secret:加密数据存储。   4. 存储类   (1)PersistentVolume:声明容器中可以访问的文件目录,被挂载到一个或多个Pod上。并且支持多样的存储类型。   (2)PersistentVolumeClaim:处理集群中的存储请求,绑定特定的pv,将请求进行存储。   (3)StorageClass:StorageClass对象会定义下面两部分内容:①PV的属性,如存储类型、Volume的大小等。②创建这种PV需要用到的存储插件。有了这两个信息后,Kubernetes就能够根据用户提交的PVC找到一个对应的StorageClass,之后Kubernetes就会调用该StorageClass声明的存储插件,进而创建出需要的PV。但是其实使用起来是一件很简单的事情,只需要根据自己的需求编写YAML文件,然后使用kubectl create命令执行即可。   5. 网络资源类   (1)Ingress:Ingress对象其实就是对“反向代理”的一种抽象,简单的说就是一个全局的负载均衡器,可以通过访问URL定位到后端的Service。   有了Ingress这个抽象,k8s就不需要关心Ingress的细节了,实际使用时,只需要选择一个具体的Ingress Controller部署即可,业界常用的反向代理项目有Nginx、HAProxy、Envoy和Traefik,都已经成为了k8s专门维护的Ingress Controller。   (2)Service:Service是一种抽象概念,它定义了一个Pod逻辑集合及访问它们的方式。支持ClusterIp、NodePort和LoadBalancer。   (3)Endpoint:Endpoint是k8s集群中的一个资源对象,存储在etcd中,用来记录一个Service对应的所有Pod的访问地址。   (4)NetworkPolicy:Network Policy提供了基于策略的网络控制,用于隔离应用并减少攻击面。它使用标签选择器模拟传统的分段网络,并通过策略控制它们之间的流量及来自外部的流量。   6. 集群   (1)Cluster:k8s集群。   (2)Master:k8s控制面板,主服务。   (3)Node:k8s集群里的worker节点。   (4)ETCD:k8s数据库。   (5)基于角色的访问控制权限RBAC model:Service Account:service account是k8s为Pod内部的进程访问apiserver创建的一种用户。其实在Pod外部也可以通过sa的token和证书访问apiserver,不过在Pod外部一般都是采用client证书的方式。   (6)User:k8s集群的用户。   (7)Group:用户组。   (8)Role:Role是一组权限的集合,如Role可以包含列出Pod权限及列出Deployment权限,Role用于给某个NameSpace中的资源进行鉴权。   (9)ClusterRole:ClusterRole是一组权限的集合,但与Role不同的是,ClusterRole可以在包括所有NameSpce和集群级别的资源或非资源类型进行鉴权。   (10)ClusterRoleBinding:可以使用ClusterRoleBinding在集群级别和所有命名空间中授予权限。   (11)RoleBinding:RoleBinding将Role中定义的权限分配给用户和用户组。RoleBinding包含主题(users、groups或service accounts)和授予角色的引用。对于namespace内的授权使用RoleBinding,集群范围内使用ClusterRoleBinding。   7. 集群配置   (1)LimitRange:前面已经讲解过资源配额,资源配额是对整个名称空间的资源的总限制,是从整体上来限制的,而LimitRange则是对Pod和Container级别来做限制的。   (2)Quota:其中ResourceQuota是针对namespace做的资源限制,而LimitRange是针对namespace中的每个组件做的资源限制。   (3)HorizontalPodAutoscaler:Horizontal Pod Autoscaling可以根据CPU使用率或应用自定义metrics自动扩展Pod数量(支持replication controller、deployment和replica set)。   8. 主节点控制面板组件   (1)k8s API Server:k8s API Server提供了k8s各类资源对象(如Pod、RC、Service等)的增删改查及watch等HTTP Rest接口,是整个系统的数据总线和数据中心。   (2)Controller Manager:Controller Manager作为集群内部的管理控制中心,负责集群内的Node、Pod副本、服务端点(Endpoint)、命名空间(Namespace)、服务账号(Service Account)、资源定额(ResourceQuota)的管理。   (3)Scheduler:管家的角色遵从一套机制为Pod提供调度服务,如基于资源的公平调度、调度Pod到指定节点,或者将通信频繁的Pod调度到同一节点等。   (4)Cloud Controller Manager:Cloud Controller Manager提供Kubernetes与阿里云基础产品的对接能力,如CLB、VPC等。目前,CCM的功能包括管理负载均衡、跨节点通信等。   (5)Kubelet:Kubelet组件运行在Node节点上,维持运行中的Pods提供kubernetes运行时环境。   (6)Kube-proxy:kube-proxy是Kubernetes的核心组件,部署在每个Node节点上,它是实现Kubernetes Service的通信与负载均衡机制的重要组件;kube-proxy负责为Pod创建代理服务,从apiserver获取所有Server信息,并根据Server信息创建代理服务,实现Server到Pod的请求路由和转发,从而实现k8s层级的虚拟转发网络。   9. 集群和链接   Namespace:Kubernetes支持多个虚拟集群,它们底层依赖于同一个物理集群。这些虚拟集群被称为命名空间,如图6-2所示,集群资源架构如图6-3所示。 图6-2 集群 图6-3 集群资源架构 6.3 Kubernetes配置文件解析   1. Kubernetes   Kubernetes用来管理容器集群的平台。既然是管理集群,那么就存在被管理节点,针对每个Kubernetes集群都由一个Master负责管理和控制集群节点。   通过Master对每个节点Node发送命令。简单来说,Master就是管理者,Node就是被管理者。   Node可以是一台机器或者一台虚拟机。在Node上面可以运行多个Pod,Pod是Kubernetes管理的最小单位,同时每个Pod可以包含多个容器(Docker),如图6-4所示。 图6-4 Kubernetes架构   2. APIServer   APIServer的核心功能是对核心对象(例如:Pod,Service,RC)的增删改查操作,同时也是集群内模块之间数据交换的枢纽。?   它包括常用的API、访问(权限)控制、注册、信息存储(etcd)等功能。在它的下面可以看到Scheduler,它将待调度的Pod绑定到Node上,并将绑定信息写入etcd中。   etcd包含在APIServer中,用来存储资源信息。   3. Controller Manager?   Kubernetes是一个自动化运行的系统,需要有一套管理规则来控制这套系统。   Controller Manager就是这个管理者,或者说是控制者。它包括8个Controller,分别对应着副本、节点、资源、命名空间、服务等。   Scheduler会把Pod调度到Node上,调度完以后就由kubelet来管理Node了。   kubelet用于处理Master下发到Node的任务(即Scheduler的调度任务),同时管理Pod及Pod中的容器。   在完成资源调度后,kubelet进程也会在APIServer上注册Node信息,定期向Master汇报Node信息,并通过cAdvisor监控容器和节点资源。   由于微服务的部署都是分布式的,所以对应的Pod及容器的部署也是。为了能够方便地找到这些Pod或者容器,引入了Service(kube-proxy)进程,由它来负责反向代理和负载均衡的实施。   以之前的小例子为例,分析说明k8s架构,该例子部署phpweb(前端)和redis到两个Node上面,中前端生成3个Pod,进行水平扩展,可以外网访问redis部署一个集群,提供内网访问,如图6-5所示。 图6-5 运行流程   既然要完成上面的例子,接下来就要部署两个应用。   首先,根据要部署的应用建立Replication Controller(RC)。RC是用来声明应用副本的个数,也就是Pod的个数。   按照上面例子,前端的RC有3个,Redis leader有1个,Redis follower有2个。   由于kubectl作为用户接口向Kubernetes下发指令,那么指令是通过“.yaml”的配置文件编写的。 # SOURCE: https://cloud.google.com/kubernetes-engine/docs/tutorials/guestbook apiVersion: apps/v1 #版本 kind: Deployment #类别 metadata: name: frontend #名称 spec: replicas: 3 #Pod副本数 selector: matchLabels: app: guestbook tier: frontend template: metadata: labels: #标签   从上面的配置文件可以看出,需要对这个RC定义一个名称,期望的副本数,以及容器中的镜像文件。然后通过kubectl作为客户端的cli工具,执行这个配置文件。   执行了上面命令后,Kubernetes会帮助用户部署副本前端的Pod到Node。 kubectl apply -f redis-follower-deployment.yaml   4. API Server的架构从上到下分为4层   (1)API层:主要以REST方式提供各种API接口,针对Kubernetes资源对象的CRUD和Watch等主要API,还有健康检查、UI、日志、性能指标等运维监控相关的API。   (2)访问控制层:负责身份鉴权,核准用户对资源的访问权限,设置访问逻辑(Admission Control)。   (3)注册表层:选择要访问的资源对象。注意,Kubernetes把所有资源对象都保存在注册表(Registry)中,如Pod、Service、Deployment等。   (4)etcd数据库:保存创建副本的信息。用来持久化Kubernetes资源对象的Key-Value数据库。   当kubectl用Create命令建立Pod时,是通过APIServer中的API层调用对应的RESTAPI方法。   之后会进入权限控制层,通过Authentication获取调用者身份,通过Authorization获取权限信息。   AdmissionControl中可配置权限认证插件,通过插件来检查请求约束。例如,启动容器之前需要下载镜像,或者检查具备某命名空间的资源。   还记得redis-leader-deployment.yaml中配置需要生成的Pod的个数为1。到了Registry层,会从CoreRegistry资源中取出1个Pod作为要创建的Kubernetes资源对象。   然后将Node、Pod和Container信息保存到etcd中去。这里的etcd可以是一个集群,由于里面保存集群中各个Node/Pod/Container的信息,所以必要时需要备份,或者保证其可靠性。   前面通过kubectl根据配置文件向APIServer发送命令,在Node上面建立Pod和Container。   在API Server,经过API调用、权限控制、调用资源和存储资源的过程,实际上还没有真正开始部署应用。   这里需要Controller Manager、Scheduler和kubelet的协助才能完成整个部署过程。   5. Controller Manager   Kubernetes需要管理集群中的不同资源,所以针对不同的资源要建立不同的Controller。   每个Controller通过监听机制获取API Server中的事件(消息),它们通过API Server提供的(List-Watch)接口监控集群中的资源,并且调整资源的状态。   可以把它想象成一个尽职的管理者,随时管理和调整资源。比如MySQL所在的Node意外宕机了,Controller Manager中的Node Controller会及时发现故障,并执行修复流程。   在部署了成百上千微服务的系统中,这个功能极大地协助了运维人员。由此可以看出,Controller Manager是Kubernetes资源的管理者,是运维自动化的核心。   它分为8个Controller,上面介绍了Replication Controller,这里把其他几个都列出来,就不展开描述了。   6. Scheduler与kubelet   Scheduler的作用是,将待调度的Pod按照算法和策略绑定到Node上,同时将信息保存在etcd中。   如果把Scheduler比作调度室,那么下面这3件事就是它需要关注的,待调度的Pod、可用的Node,以及调度算法和策略。   简单地说,就是通过调度算法/策略把Pod放到合适的Node中。此时Node上的kubelet通过API Server监听到Scheduler产生的Pod绑定事件,然后通过Pod的描述装载镜像文件,并且启动容器。   也就是说Scheduler负责思考Pod放在哪个Node中,然后将决策告诉kubelet,kubelet完成Pod在Node的加载工作。   也可以这样认为,Scheduler是“boss”,kubelet是干活的“工人”,它们都通过API Server进行信息交换。   部署在Kubernetes中,Pod如何访问其他的Pod呢?答案是通过Kubernetes的Service机制。   在Kubernetes中的Service定义了一个服务的访问入口地址(IP+Port)。Pod中的应用通过这个地址访问一个或者一组Pod副本。   Service与后端Pod副本集群之间是通过Label Selector来实现连接的。Service所访问的这一组Pod都会有同样的Label,通过这种方法知道这些Pod属于同一个组。   前端的Service代码如下, # SOURCE: https://cloud.google.com/kubernetes-engine/docs/tutorials/guestbook apiVersion: v1 kind: Service #说明创建资源对象的类型是Service metadata: name: frontend #Service全局唯一名称 labels: app: guestbook tier: frontend spec: ports: # the port that this service should serve on - port: 80 #Service的服务端口号 selector: #Service对应的Pod标签,用来给Pod分类 app: guestbook tier: frontend   前端的Service如图6-6所示。 图6-6 前端的Service   这里的Cluster-IP 10.102.157.141是由Kubernetes自动分配的。当一个Pod需要访问其他的Pod时,就需要通过Service的Cluster-IP和Port。   也就是说,Cluster-IP和Port是Kubernetes集群的内部地址,是提供给集群内的Pod之间访问使用的,外部系统无法通过这个Cluster-IP来访问Kubernetes中的应用。上面提到的Service只是一个概念,而真正将Service落实的是kube-proxy。   7. kube-proxy   只有理解了kube-proxy的原理和机制,才能真正理解Service背后的实现逻辑。在Kubernetes集群的每个Node上都会运行一个kube-proxy服务进程,可以把这个进程看作Service的负载均衡器,其核心功能是将发送到Service的请求转发到后端的多个Pod上。   此外,Service的Cluster-IP与NodePort是kube-proxy服务通过iptables的NAT转换实现的。kube-proxy在运行过程中动态创建与Service相关的iptables规则。   由于iptables机制针对的是本地的kube-proxy端口,所以在每个Node上都要运行kube- proxy组件。   因此在Kubernetes集群内部,可以在任意Node上发起对Service的访问请求。Pod在Kubernetes内互相访问,外网访问Pod。   另外,作为资源监控,Kubernetes在每个Node和容器上都运行了cAdvisor。它是用来分析资源使用率和性能的工具,支持Docker容器。   kubelet通过cAdvisor获取其所在Node及容器(Docker)的数据。cAdvisor自动采集CPU、内存、文件系统和网络使用的统计信息。   kubelet作为Node的管理者,把cAdvisor采集上来的数据通过RESTAPI的形式暴露给Kubernetes的其他资源,让他们知道Node/Pod中的资源使用情况。   下面部署一个Nginx服务来说明Kubernetes系统各个组件调用关系。   首先需要明确,一旦Kubernetes环境启动后,master和node都会将自身的信息存储到etcd数据库中。   一个Nginx服务的安装请求首先会被发送到master节点上的API Server组件。   API Server组件会调用Scheduler组件来决定到底应该把这个服务安装到哪个node节点上。此时,它会从etcd中读取各个Node节点的信息,然后按照一定的算法进行选择,并将结果告知API Server。   API Server调用Controller-Manager去调用Node节点安装Nginx服务。   Kubelet接收到指令后,会通知Docker,然后由Docker来启动一个Nginx的Pod。Pod是Kubernetes的最小操作单元,容器必须跑在Pod中。   这样,一个Nginx服务就开始运行了,如果需要访问Nginx,就需要通过kube-proxy来对Pod产生访问的代理,这样外界用户就可以访问集群中的Nginx服务了。    第7章 Kubernetes集群部署       * 了解Kubernetes安装与配置。 * 了解Kubernetes的命令行工具。 * 深入理解Pod。 * 深入理解Service。   本章首先向读者介绍Kubernetes安装与配置的要求,接着介绍Kubernetes的命令行,最后,了解Pod和Service的基础原理和概念。 7.1 Kubernetes的安装与配置 7.1.1 系统环境要求和先决条件   一个Kubernetes集群主要由控制节点(master)和工作节点(node)构成,每个节点上都会安装不同的组件。   (1)控制节点(master):集群的控制平面,负责集群的决策。   (2)API Server:集群操作的唯一入口,接收用户输入的命令,提供认证、授权、API注册和发现等机制。   (3)Scheduler:负责集群资源调度,按照预定的调度策略将Pod调度到相应的Node节点上。   (4)ControllerManager:负责维护集群的状态,如程序部署安排、故障检测、自动扩展和滚动更新等。   (5)Etcd:负责存储集群中各种资源对象的信息。   (6)工作节点(node):集群的数据平面,负责为容器提供运行环境。   (7)Kubelet:负责维护容器的生命周期,即通过控制Docker来创建、更新和销毁容器。   (8)KubeProxy:负责提供集群内部的服务发现和负载均衡。   (9)Docker:负责节点上容器的各种操作。   安装Kubernetes对软件和硬件的系统要求如表7-1所示。 表7-1 安装Kubernetes对软件和硬件的系统要求 软 硬 件 最 低 配 置 推 荐 配 置 主机资源 集群规模为1~5个节点时,要求如下: Master:至少1core CPU,2GB内存 Node:至少1core CPU,1GB内存 Master:至少1core CPU,2GB内存 Node:至少1core CPU,1GB内存 Linux操作系统 各种Linux发行版本,kernel在3.10及以上 CentOS 7.8 etcd v3版本及以上 v3 Docker 支持众多Docker版本 Docker CE 19.03      Kubernetes需要容器运行时(Container Runtime Interface,CRI)的支持,目前官方支持的容器运行时包括Docker、Containerd、CRI-O和frakti。本书以Docker作为容器运行环境,推荐版本为Docker CE 19.03。   宿主机操作系统以CentOS Linux 7为例,使用Systemd系统完成对Kubernetes服务的配置。其他Linux发行版的服务配置请参考相关的系统管理手册。为了便于管理,常见的做法是将Kubernetes服务程序配置为Linux系统开机自启动的服务。   在测试环境中直接关闭防火墙进行部署,代码如下。 systemctl stop firewalld.service systemctl disable firewalld.service   代码运行结果如图7-1所示。 图7-1 关闭防火墙   主机禁用SELinux,让容器可以读取主机文件系统,修改/etc/selinux/config,代码如下。 sudo setenforce 0 #临时关闭 sudo sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config #永久改为关闭模式   或者 sudo sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config #永久改为宽容模式   关闭Linux的swap系统交换区,代码如下。 swapoff –a #临时 sed -e '/swap/s/^/#/g' -i /etc/fstab #永久关闭   安装集群之前先安装容器运行时接口。   本质上,kubelet的主要功能就是启动和停止容器组件,即所说的CRI。   复习一下,当用户想要创建一个应用(deployment、statefulset)时,主要流程如下。   (1)通过kubectl命令向apiserver提交,apiserver将资源保存在etcd中。   (2)controllermanager通过控制循环,获取新创建的资源,并创建Pod信息。注意这里只创建Pod,并未调度和创建容器。   (3)kube-scheduler也会循环获取新创建但未调度的Pod,并在执行一系列调度算法后,将Pod绑定到一个Node上,并更新etcd中的信息。具体方式是在Pod的spec中加入nodeName字段。   (4)kubelet监视所有Pod对象的更改。当发现Pod已绑定Node,并且绑定的Node本身时,kubelet会接管所有后续任务,包括创建Pod网络、Container等。kubelet会通过CRI调用Container Runtime创建Pod中的Container。   (5)CRI在Kubernetes 1.5中引入,并充当kubelet和容器运行时之间的桥梁。期望与Kubernetes集成的高级容器运行时将实现CRI。预期runtimes将负责镜像的管理,并支持Kubernetes pods,以及管理各个容器。CRI仅具有一个功能:对于Kubernetes,它描述了容器应具有的操作及每个操作应具有的参数。   CRI是以容器为中心的API,设计CRI的初衷是不希望向容器(如Docker)暴露Pod信息或Pod的API,Pod始终是Kubernetes编排概念,与容器无关,因此这就是为什么必须使该API以容器为中心。   CRI工作在kubelet与Container Runtime之间,目前常见的runtime有以下几个。   (1)Docker:目前Docker已经将一部分功能移至Containerd中,CRI可以直接与Containerd交互。因此,Docker本身并不需要支持CRI(Containerd已经支持)。   (2)Containerd:Containerd可以通过shim对接不同low-level runtime。   (3)cri-o:一种轻量级的runtime,支持runc和Clear Containers作为low-level runtimes。   1. CRI是如何工作的   CRI大体包含3个接口:Sandbox、Container和Image,其中提供了一些操作容器的通用接口,包括Create Delete List等。   Sandbox为Container提供一定的运行环境,其中包括Pod的网络等。Container包括容器生命周期的具体操作,Image则提供对镜像的操作。   kubelet会通过grpc调用CRI接口,首先创建一个环境,也就是所谓的PodSandbox。当PodSandbox可用后,继续调用Image或Container接口去拉取镜像和创建容器。其中,shim会将这些请求翻译为具体的runtime API,并执行不同low-level runtime的具体操作。   2. PodSandbox   上文所说的Sandbox到底是什么东西呢?从虚拟机和容器化两方面来看,这两者都使用Cgroups做资源配额,而且概念上都抽离出一个隔离的运行时环境,只是区别在于资源隔离的实现。因此Sandbox是k8s为兼容不同运行时环境预留的空间,也就是说k8s允许low-level runtime依据不同的实现去创建不同的PodSandbox,对于kata来说PodSandbox就是虚拟机,对于docker来说就是Linux namespace。当Pod Sandbox建立起来后,kubelet就可以在其中创建用户容器。当删除Pod时,kubelet会先移除Pod Sandbox,然后再停止里面的所有容器,对Container来说,当Sandbox运行后,只需要将新的Container的namespace加入到已有的Sandbox的namespace中。   默认情况下,在CRI体系里,Pod Sandbox其实就是pause容器。kubelet代码引用的defaultSandboxImage其实就是官方提供的gcr.io/google_containers/pause-amd64镜像。   接着需要配置网桥工具,将桥接的IPv4流量传递到iptables的链,iptables是Linux内核集成的IP信息包过滤系统,这对后续集群搭建很重要,代码内容如下。 cat > /etc/sysctl.d/k8s.conf << EOF net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 EOF #生效 sysctl --system   代码运行结果如图7-2所示。 图7-2 配置网桥工具   因为配置kubernetes集群需要所有节点的时间一致,所以需要将所有主机的系统时间统一更新,代码如下。 yum install ntpdate -y ntpdate time.windows.com   代码运行结果如图7-3所示。 图7-3 更新系统时间   配置Docker的yum源(如果已经安装完Docker,则跳过),代码如下。 yum install wget -y #配置源 wget https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo -O /etc/ yum.repos.d/docker-ce.repo #安装指定版本的docker yum install docker-ce-19.03.13 -y   设定docker开机自启,代码如下。 #开机启动并运行docker systemctl start docker && systemctl enable docker   测试Docker是否安装完毕,并打开镜像仓库,代码如下。 docker pull hello-world docker images   代码运行结果如图7-4所示。 图7-4 检验Docker镜像仓库   接着需要修改Docker驱动的Cgroup Dirver,将Cgroupfs修改为systemd,因为驱动Cgroupfs与Kubernetes的内核不兼容,所以需要修改为systemd,查看代码如下。 docker info | grep Cgroup   代码运行结果如图7-5所示。 图7-5 Cgroup Dirver   通过两种方法来修改。   第一种:编辑Docker配置文件/etc/docker/daemon.json,代码如下。 vim /etc/docker/daemon.json #修改内容 "exec-opts": ["native.cgroupdriver=systemd"] #修改后重启 systemctl daemon-reload systemctl restart docker   第二种:编辑/usr/lib/systemd/system/docker.service,代码如下。 vim /usr/lib/systemd/system/docker.service #修改内容 ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd /containerd.sock --exec-opt native.cgroupdriver=systemd   文件内容如图7-6所示。 图7-6 编辑docker.service   选择两种方法中的一种进行修改后,重启Docker,代码如下。 #修改后重启 systemctl daemon-reload systemctl restart docker #设置完成后通过docker info命令可以看到Cgroup Driver为systemd docker info | grep Cgroup   代码运行结果如图7-7所示。 图7-7 Docker驱动模式 7.1.2 使用Kubeadm工具快速安装Kubernetes集群   为Kubernetes集群准备虚拟机主机,本书中kubeadm的安装使用3个VM虚拟机(华为云),配置如表7-2所示。 表7-2 配置主机需求 主 机 名 IP 配 置 节 点 S1 192.168.5.101 2核4GB内存 主节点master S2 192.168.5.102 2核4GB内存 工作节点node1 S3 192.168.5.103 2核4GB内存 工作节点node2   节点内网网络(ifconfig查看nodeip):192.168.0.0/16,Pod网络(必须和节点内网不同Podip):10.244.0.0/16,Service网络(cluseterip):10.96.0.0/12(主机范围10.96.0.1-10.111. 255.254)。   认识k8s的3个工具:kubeadm、kubectl和kubelet。   1. kubeadm   kubeadm是官方社区推出的一个用于快速部署Kubernetes集群的工具。这个工具能通过两条指令完成一个Kubernetes集群的部署,代码如下。 #创建一个 Master 节点 kubeadm init #将一个 Node 节点加入到当前集群中 kubeadm join   2. kubectl   kubectl是Kubernetes集群的命令行工具,通过kubectl能够对集群本身进行管理,并能够在集群上进行容器化应用的安装和部署。   3. kubelet   kubelet是Master派到Node节点的代表,管理本机容器一个集群中每个节点上运行的代理,它保证容器都运行在Pod中,负责维护容器的生命周期,同时也负责Volume(CSI)和网络(CNI)的管理。   步骤1:为每台主机分别设置主机名,代码如下。 hostnamectl set-hostname s1 hostnamectl set-hostname s2 hostnamectl set-hostname s3   步骤2:添加每个主机的vim /etc/hosts,红色表示对应主机要改为对应的,代码如下。 192.168.5.101 s1 192.168.5.102 s2 192.168.5.103 s3   内容修改结果如图7-8所示。 图7-8 hosts文件   步骤3:开始布置Kuberneter的集群的阿里云yum源,代码如下。 cat > /etc/yum.repos.d/kubernetes.repo << EOF [kubernetes] name=Kubernetes baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64 enabled=1 gpgcheck=0 repo_gpgcheck=0 gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors. aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg EOF   步骤4:3台虚拟机都需要安装指定的kubeadm、kubelet和kubectl版本,代码如下。 yum install kubelet-1.19.4 kubeadm-1.19.4 kubectl-1.19.4 -y   代码运行结果如图7-9所示。 图7-9 kubeadm、kubelet和kubectl运行结果   步骤5:设置为开机自启动,代码如下。 systemctl enable kubelet.service   代码运行结果如图7-10所示。 图7-10 开机自启动kubelet   检验kubeadm、kubelet和kubectl是否安装成功,代码如下。 yum list installed | grep kubelet yum list installed | grep kubeadm yum list installed | grep kubectl   代码运行结果如图7-11所示。 图7-11 k8s安装情况   步骤6:在虚拟机s1的主节点master,开始k8s的初始化安装,代码如下。 kubeadm init --apiserver-advertise-address=192.168.5.101 --image-repository registry.aliyuncs.com/google_containers --kubernetes-version v1.19.4 --service- cidr=10.96.0.0/12 --pod-network-cidr=10.244.0.0/16   参数解释如下。 --image-repository registry.aliyuncs.com/google_containers #使用阿里云源 --kubernetes-version= v1.19.4 #安装版本为v1.19.4 --pod-network-cidr=10.244.0.0/16 #设置Pod网段 --service-cidr=10.96.0.0/12 #设置Service网段   代码运行结果如图7-12所示。 图7-12 初始化安装   其中kubeadm join是加入主节点的命令,可以在其他节点上输入该命令,使其进入主节点中,而token的有效期为24小时,详细内容如下。 kubeadm join 192.168.5.101:6443 --token 65rrmt.ilwm00d9236k6r65 \ --discovery-token-ca-cert-hash sha256:b161ea069d3d31453d413c5a30158577b21 b07790ef3bb2f4487864723974188 #token失效生成新的token kubeadm token create --print-join-command --ttl 0   步骤7:在虚拟机S1的主节点Master上调试主节点的配置,代码如下。 mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config   步骤8:在虚拟机S1的主节点Master查看安装情况kubectl获取节点信息,代码如下。 kubectl get nodes   代码运行结果如图7-13所示。 图7-13 节点信息   获取虚拟机S1上主节点Master的集群信息,代码如下。 kubectl cluster-info   代码运行结果如图7-14所示。 图7-14 集群主节点信息   步骤9:在其他节点虚拟机上输入集群加入指令,进入集群节点,代码如下。 kubeadm join 192.168.5.101:6443 --token 65rrmt.ilwm00d9236k6r65 \ --discovery-token-ca-cert-hash sha256:b161ea069d3d31453d413c5a30158577b21b 07790ef3bb2f4487864723974188   步骤10:获取集群信息开启IP转发功能,为安装flannel网络组件做准备,代码如下。 cat > /etc/sysctl.d/kubernetes.conf << EOF net.ipv4.ip_forward = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 EOF #重新加载kernel参数 modprobe br_netfilter sysctl --system   步骤11:k8s用flannel作为网络规划服务,其功能是让集群中的不同节点主机创建的Docker容器都具有全集群唯一的虚拟IP地址,查看所有的Pod状况,代码如下。 kubectl get pods --all-namespaces   代码运行结果如图7-15所示。 图7-15 所有的Pod状况   步骤12:发现coredns状态为pending,Master需要安装flannel网络组件,代码如下。 kubectl apply -f ~/k8s/kube-flannel.yml   需要等待几分钟后,显示全部为1/1成功。   代码运行结果如图7-16所示。 图7-16 安装flannel网络组件   步骤13:加入子节点,先设置允许IP转发,代码如下。 cat > /etc/sysctl.d/kubernetes.conf << EOF net.ipv4.ip_forward = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 EOF #重新加载kernel参数 modprobe br_netfilter sysctl --system   步骤14:在S2和S3中按照上面的步骤安装3个工具,再加入节点,代码如下。 kubeadm join 192.168.5.101:6443 --token 65rrmt.ilwm00d9236k6r65 \ --discovery-token-ca-cert-hash sha256:b161ea069d3d31453d413c5a30158577b21b07790ef3bb2f4487864723974188   重新获取token,代码如下。 kubeadm token create --print-join-command --ttl 0   代码运行结果如图7-17所示。 图7-17 重新获取token   安装Pod网络插件(CNI)和Kubernetes网络插件,flannel:将真实IP写入Master的hosts文件,由于无法访问外网,所以需要通过在https://www.ipaddress.com/查询raw. githubusercontent.com的真实IP,并写入hosts文件,内容如下。 vim /etc/hosts 185.199.108.133 raw.githubusercontent.com 185.199.109.133 raw.githubusercontent.com 185.199.110.133 raw.githubusercontent.com 185.199.111.133 raw.githubusercontent.com   安装指令代码如下。 kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/ Documentation/kube-flannel.yml   步骤15:检查节点状态成功与否,代码如下。 kubectl get node   代码运行结果如图7-18所示。 图7-18 节点状态   检查所有Pod,代码如下。 kubectl get pods --all-namespaces   代码运行结果如图7-19所示。 图7-19 部分Pod没启动   可以发现还有个别的服务没有启动,这时可以输入以下命令启动,代码如下。 kubectl get pods --all-namespaces   代码运行结果如图7-20所示。 图7-20 所有Pod都启动   步骤16:安装dashboard,代码如下。 kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2. 1.0/aio/deploy/recommended.yaml   生成角色和权限,代码如下。 cd ~/k8s/ dashboard_token kubectl apply -f .   chrome访问https://外网:30043,如果出现不能访问提示不安全,请输入thisisunsafe,如图7-21所示。 图7-21 访问提示   登录dashboard获取token,代码如下。 kubectl -n kubernetes-dashboard get secret $(kubectl -n kubernetes-dashboard get sa/admin-user -o jsonpath="{.secrets[0].name}") -o go-template="{{.data.token | base64decode}}"   代码运行结果如图7-22所示。 图7-22 获取token   可以看到所有的资源和运行情况,如图7-23所示。 图7-23 获取所有的资源和运行情况 7.1.3 以二进制文件方式安装Kubernetes集群   不同于kubeadm容器化安装k8s,本节使用二进制文件进行安装。二进制安装k8s主要是Master节点需要安装kube-apiserver、kube-controller-manager、kube-scheduler和etcd,Node节点需要安装kubelet和kube-proxy,如图7-24所示。   在k8s中,Master节点扮演着总控中心的角色,通过不间断地与各个工作节点Node通信来维护整个集群的健康工作状态,集群中各资源对象的状态则被保存在etcd数据库中。如果Master不能正常工作,各Node就会处于不可管理状态,如果任何能够访问Master的客户端都可以通过API操作集群中的数据,可能导致对数据的非法访问或篡改,需要确保Master高可用,并启用安全访问机制,需要包含以下几个方面:Master的kube-apiserver、kube-controller-manager、kube-scheduler、etcd服务至少以3个节点的多实例方式部署,Master启用基于CA证书的HTTPS安全机制。   1. 主节点控制面板组件   1)k8s API Server   k8s API Server提供了k8s各类资源对象(Pod、RC、Service等)的增删改查及Watch等HTTP Rest接口,是整个系统的数据总线和数据中心。   2)Controller Manager   Controller Manager作为集群内部的管理控制中心,负责集群内的Node、Pod副本、服务端点(Endpoint)、命名空间(Namespace)、服务账号(Service Account)及资源定额(ResourceQuota)的管理。   3)Scheduler   管家的角色遵从一套机制为Pod提供调度服务,如基于资源的公平调度、调度Pod到指定节点,或者将通信频繁的Pod调度到同一节点等。   4)Cloud Controller Manager   Cloud Controller Manager提供Kubernetes与阿里云基础产品的对接能力,如CLB、VPC等。目前CCM的功能包括管理负载均衡、跨节点通信等。   5)kubelet   kubelet组件运行在Node节点上,维持运行中的Pods并提供Kubernetes运行时环境。   6)kube-proxy   kube-proxy是Kubernetes的核心组件,部署在每个Node节点上,它是实现Kubernetes Service的通信与负载均衡机制的重要组件;kube-proxy负责为Pod创建代理服务,从apiserver获取所有Server信息,并根据Server信息创建代理服务,实现Server到Pod的请求路由和转发,从而实现k8s层级的虚拟转发网络。   该实验案例需要6个主机,用VM虚拟6个主机,如图7-25所示。 图7-25 Linux界面虚拟机   该实验案例需要6个主机,用VM虚拟6个主机,如表7-3所示。 表7-3 配置主机 主 机 名 IP 配 置 角 色 安 装 软 件 master1 192.168.0.18 2核4GB内存 master kube-apiserver kube-controller-manager kube-scheduler etcd master2 192.168.0.19 2核4GB内存 master kube-apiserver kube-controller-manager kube-scheduler etcd master3 192.168.0.20 2核4GB内存 master kube-apiserver kube-controller-manager kube-scheduler etcd node1 192.168.0.14 2核4GB内存 worker kubelet、kube-proxy node2 192.168.0.21 2核4GB内存 worker kubelet、kube-proxy node3 192.168.0.22 2核4GB内存 worker kubelet、kube-proxy      2. 安装前的准备   步骤1:修改机器的/etc/hosts文件,代码如下。 cat >> /etc/hosts << EOF 192.168.0.27 master1 192.168.0.28 master2 192.168.0.29 master3 192.168.0.14 node1 192.168.0.21 node2 192.168.0.22 node3 EOF   步骤2:关闭防火墙和selinux,代码如下。 systemctl stop firewalld systemctl status firewalld setenforce 0 sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config swapoff -a   步骤3:时间同步,代码如下。 yum install -y chrony systemctl start chronyd systemctl enable chronyd chronyc sources   步骤4:安装ntp并同步,代码如下。 yum -y install ntp ntpdate ntpdate cn.pool.ntp.org   步骤5:同步时间并关闭防火墙,代码如下。 timedatectl set-timezone Asia/Shanghai systemctl stop firewalld.service systemctl disable firewalld.service systemctl status firewalld.service   步骤6:修改IP转发规则,代码如下。 cat > /etc/sysctl.d/k8s.conf << EOF net.ipv4.ip_forward = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 EOF sysctl --system   步骤7:加载ipvs模块,代码如下。 modprobe -- ip_vs modprobe -- ip_vs_rr modprobe -- ip_vs_wrr modprobe -- ip_vs_sh modprobe -- nf_conntrack_ipv4 lsmod | grep ip_vs lsmod | grep nf_conntrack_ipv4 yum install -y ipvsadm   步骤8:更改主机名,代码如下。 hostnamectl set-hostname master1 hostnamectl set-hostname master2 hostnamectl set-hostname master3 hostnamectl set-hostname node1 hostnamectl set-hostname node2 hostnamectl set-hostname node3   步骤9:每台机器都需要配置证书文件、组件的配置文件、组件的服务启动文件,现专门选择master1来统一生成这些文件,然后再分发到其他机器。   以下操作在master1上进行登录后复制。   注:该目录为配置文件和证书文件生成目录,后面的所有文件生成相关操作均在此目录下进行,代码如下。 ssh-keygen -t rsa -b 2048 cd /root/.ssh   步骤10:会生成一个公钥pub,一个私钥可以看到里面有id_rsa id_rsa.pub,将密钥分发到另外5台机器,让master1可以免密码登录其他机器,代码如下。 cat ./id_rsa.pub >> ./authorized_keys //加入授权,自己给自己认证 chmod 600 ./authorized_keys //为了其他用户都可以看到这个文件,需要添加可读权限 //最后验证ssh master是否不需密码就能登录其他机器,记得使用exit跳出   代码如下。 ssh-copy-id -i ./id_rsa.pub master2 #给节点认证,记得exit ssh-copy-id -i ./id_rsa.pub master3 ssh-copy-id -i ./id_rsa.pub node1 ssh-copy-id -i ./id_rsa.pub node2 ssh-copy-id -i ./id_rsa.pub node3   步骤11:搭载etcd集群,代码如下。 mkdir -p /etc/etcd #配置文件存放目录 mkdir -p /etc/etcd/ssl #证书文件存放目录 mkdir -p /etc/etcd/data #工作目录 mkdir -p /etc/kubernetes/pki #放CA证书,3个主机都要   步骤12:创建etcd的CA证书。   为etcd和k8s服务启用基于CA(Certificate Authority)认证的安全机制,需要CA证书配置。   CA证书的制作可以使用openssl、easyrsa、cfssl等工具完成,本书使用openssl。   CA证书是集群所有节点共享的,只需要创建一个CA证书,后续创建的所有证书都由它签名,代码如下。 cd /etc/kubernetes/pki   步骤13:为确保安全,Kubernetes系统各组件需要使用x509证书对通信进行加密和认证。CA是自签名的根证书,用来签名后续创建的其他证书,代码如下。 #生成CA根证书 openssl genrsa -out ca.key 2048 openssl req -x509 -new -nodes -key ca.key -subj "/CN=master1" -days 36500 -out ca.crt   CN:Common Name,kube-apiserver从证书中提取该字段作为请求的用户名(User Name);浏览器使用该字段验证网站是否合法,生成ssl证书请求文件,配置参数文件自签名,就可以不需要一步一步地写,代码如下。 cd /etc/etcd/ssl vim /etc/etcd/ssl/etcd_ssl.cnf [ req ] distinguished_name = req_distinguished_name req_extensions = v3_req [ req_distinguished_name ] countryName = FZ countryName_default = GB stateOrProvinceName = FJ stateOrProvinceName_default = China localityName = CN localityName_default = Fujian organizationName = Fujian organizationName_default = Home organizationalUnitName = Home organizationalUnitName_default = IT commonName = k8s commonName_max = 64 commonName_default = localhost [ v3_req ] basicConstraints = CA:FALSE keyUsage = nonRepudiation, digitalSignature, keyEncipherment subjectAltName = @alt_names [alt_names] IP.1 = 192.168.0.27 IP.2 = 192.168.0.28 IP.3 = 192.168.0.29   步骤14:生成etcd-ca证书,供服务器端使用,代码如下。 openssl genrsa -out etcd_server.key 2048 openssl req -new -key etcd_server.key -config etcd_ssl.cnf -subj "/CN=master1" -out etcd_server.csr openssl x509 -req -in etcd_server.csr -CA /etc/kubernetes/pki/ca.crt -CAkey /etc/kubernetes/pki/ca.key -CAcreateserial -days 36500 -extensions v3_req -extfile etcd_ssl.cnf -out etcd_server.crt   步骤15:生成etcd-ca证书,供客户端使用,代码如下。 openssl genrsa -out etcd_client.key 2048 openssl req -new -key etcd_client.key -config etcd_ssl.cnf -subj "/CN=master1" -out etcd_client.csr openssl x509 -req -in etcd_client.csr -CA /etc/kubernetes/pki/ca.crt -CAkey /etc/kubernetes/pki/ca.key -CAcreateserial -days 36500 -extensions v3_req -extfile etcd_ssl.cnf -out etcd_client.crt   步骤16:下载etcd,代码如下。 cd /etc/etcd/ wget https://github.com/etcd-io/etcd/releases/download/v3.5.0/etcd-v3.5.0- linux-amd64.tar.gz tar -xf etcd-v3.5.0-linux-amd64.tar.gz cp -p etcd-v3.5.0-linux-amd64/etcd* /usr/local/bin/   步骤17:同步到master2和master3。   rsync命令是一个远程数据同步工具,可通过LAN/WAN快速同步多台主机间的文件。rsync使用所谓的“rsync算法”来使本地和远程两个主机之间的文件达到同步,这个算法只传送两个文件的不同部分,而不是每次都整份传送,因此速度相当快。   yum install -y rsync #每台都要安装,代码如下。 rsync -vaz etcd-v3.5.0-linux-amd64/etcd* master2:/usr/local/bin/ rsync -vaz etcd-v3.5.0-linux-amd64/etcd* master3:/usr/local/bin/   步骤18:创建etcd的systemd启动文件,统一systemd管理。其中EnvironmentFile是环境变量,提供ExecStart配置里面调用${},代码如下。 vim /usr/lib/systemd/system/etcd.service [Unit] Description=Etcd Server After=network.target After=network-online.target Wants=network-online.target [Service] Type=notify EnvironmentFile=/etc/etcd/etcd.conf ExecStart=/usr/local/bin/etcd Restart=on-failure RestartSec=5 LimitNOFILE=65536 [Install] WantedBy=multi-user.target   步骤19:把文件复制到节点,代码如下。 for i in master2 master3;do rsync -vaz /etc/etcd/ $i:/etc/etcd/;done for i in master2 master3;do rsync -vaz /etc/kubernetes/pki/ $i:/etc/kubernetes/ pki/;done for i in master2 master3;do rsync -vaz /usr/lib/systemd/system/etcd.service $i:/usr/lib/systemd/system/;done   步骤20:这里注意不要把/etc/etcd/data里面的数据复制过去,代码如下。 chmod -R 777 /usr/local/bin/etcd chmod -R 777 /usr/local/bin/ chmod -R 777 /usr/lib/systemd/system/etcd.service   步骤21:创建环境变量文件配置参考官方文档。 https://insights-core.readthedocs.io/en/latest/shared_parsers_catalog/etcd_conf.html,代码如下。 vim /etc/etcd/etcd.conf   代码如下。 #etcd1 解释的 #[Member] ETCD_NAME="etcd1" #节点名称,集群中唯一 ETCD_DATA_DIR="/etc/etcd/data" #数据目录 ETCD_LISTEN_PEER_URLS="https://192.168.0.18:2380" #集群通信监听地址 ETCD_LISTEN_CLIENT_URLS="https://192.168.0.18:2379,http://127.0.0.1:2379" #客户端访问监听地址 ETCD_CERT_FILE=/etc/etcd/ssl/etcd_server.crt ETCD_KEY_FILE=/etc/etcd/ssl/etcd_server.key ETCD_TRUSTED_CA_FILE=/etc/kubernetes/pki/ca.crt ETCD_CLIENT_CERT_AUTH=true ETCD_PEER_CERT_FILE=/etc/etcd/ssl/etcd_server.crt ETCD_PEER_KEY_FILE=/etc/etcd/ssl/etcd_server.key ETCD_PEER_TRUSTED_CA_FILE=/etc/kubernetes/pki/ca.crt #[Clustering] ETCD_INITIAL_ADVERTISE_PEER_URLS="https://192.168.0.18:2380" #集群通告地址 ETCD_ADVERTISE_CLIENT_URLS="https://192.168.0.18:2379" #客户端通告地址 ETCD_INITIAL_CLUSTER="etcd1=https://192.168.0.18:2380,etcd2=https://192.168.0.19:2380,etcd3=https://192.168.0.20:2380" #集群节点地址 ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster #集群Token ETCD_INITIAL_CLUSTER_STATE=new #加入集群的当前状态,new是新集群,existing表示加入已 有集群   代码如下。 #etcd1 从以下开始复制 #[Member] ETCD_NAME=etcd1 ETCD_DATA_DIR=/etc/etcd/data ETCD_LISTEN_PEER_URLS=https://192.168.0.27:2380 ETCD_LISTEN_CLIENT_URLS=https://192.168.0.27:2379,http://127.0.0.1:2379 #[SECURITY] ETCD_CERT_FILE=/etc/etcd/ssl/etcd_server.crt #etcd2 #[Member] ETCD_NAME=etcd2 ETCD_DATA_DIR=/etc/etcd/data ETCD_LISTEN_PEER_URLS=https://192.168.0.28:2380 ETCD_LISTEN_CLIENT_URLS=https://192.168.0.28:2379,http://127.0.0.1:2379 #[SECURITY] ETCD_CERT_FILE=/etc/etcd/ssl/etcd_server.crt ETCD_KEY_FILE=/etc/etcd/ssl/etcd_server.key ETCD_TRUSTED_CA_FILE=/etc/kubernetes/pki/ca.crt ETCD_CLIENT_CERT_AUTH=true ETCD_PEER_CERT_FILE=/etc/etcd/ssl/etcd_server.crt ETCD_PEER_KEY_FILE=/etc/etcd/ssl/etcd_server.key ETCD_PEER_TRUSTED_CA_FILE=/etc/kubernetes/pki/ca.crt ETCD_PEER_CLIENT_CERT_AUTH=true #[Clustering] ETCD_INITIAL_ADVERTISE_PEER_URLS=https://192.168.0.28:2380 ETCD_ADVERTISE_CLIENT_URLS=https://192.168.0.28:2379 ETCD_INITIAL_CLUSTER=etcd1=https://192.168.0.27:2380,etcd2=https://192.168.0.28:2380,etcd3=https://192.168.0.29:2380 ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster ETCD_INITIAL_CLUSTER_STATE=new   代码如下。 #etcd3 #[Member] ETCD_NAME=etcd3 ETCD_DATA_DIR=/etc/etcd/data ETCD_LISTEN_PEER_URLS=https://192.168.0.29:2380 ETCD_LISTEN_CLIENT_URLS=https://192.168.0.29:2379,http://127.0.0.1:2379 #[SECURITY] ETCD_CERT_FILE=/etc/etcd/ssl/etcd_server.crt ETCD_KEY_FILE=/etc/etcd/ssl/etcd_server.key ETCD_TRUSTED_CA_FILE=/etc/kubernetes/pki/ca.crt ETCD_CLIENT_CERT_AUTH=true ETCD_PEER_CERT_FILE=/etc/etcd/ssl/etcd_server.crt ETCD_PEER_KEY_FILE=/etc/etcd/ssl/etcd_server.key ETCD_PEER_TRUSTED_CA_FILE=/etc/kubernetes/pki/ca.crt ETCD_PEER_CLIENT_CERT_AUTH=true #[Clustering] ETCD_INITIAL_ADVERTISE_PEER_URLS=https://192.168.0.29:2380 ETCD_ADVERTISE_CLIENT_URLS=https://192.168.0.29:2379 ETCD_INITIAL_CLUSTER=etcd1=https://192.168.0.27:2380,etcd2=https://192.168.0.28:2380,etcd3=https://192.168.0.29:2380 ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster ETCD_INITIAL_CLUSTER_STATE=new   步骤22:3台机子上都要打开etcd(现在启动第一个,等待好以后,可能会报错,没关系,再启动后面两个),代码如下。 systemctl daemon-reload systemctl enable etcd.service systemctl restart etcd.service systemctl status etcd systemctl daemon-reload systemctl restart etcd.service   代码如下。 ETCDCTL_API=3 /usr/local/bin/etcdctl --write-out=table --cacert=/etc/kubernetes/ pki/ca.crt --cert=/etc/etcd/ssl/etcd_client.crt --key=/etc/etcd/ssl/etcd_client.key --endpoints=https://192.168.0.27:2379,https://192.168.0.28:2379,https://192.168.0.29:2379 endpoint health   代码运行结果如图7-26所示。 图7-26 主机链接情况   步骤23:部署kube-apiserver。   下载k8s /v1.21.3,代码如下。 mkdir ~/k8s cd ~/k8s (见附件) wget https://storage.googleapis.com/kubernetes-release/release/v1.21.3/kubernetes- server-linux-amd64.tar.gz   代码如下。 tar -zxvf kubernetes-server-linux-amd64.tar.gz cd /root/k8s/kubernetes/server/bin   步骤24:复制k8s文件到usr/bin中,代码如下。 cp -r * /usr/bin/ chmod -R 777 /usr/bin/ cd /etc/kubernetes/   步骤25:生成token.csv,代码如下。 cat > token.csv << EOF $(head -c 16 /dev/urandom | od -An -t x | tr -d ' '),kubelet-bootstrap,10001, "system:kubelet-bootstrap" EOF   步骤26:生成证书请求文件apiserver_ssl.cnf,代码如下。   由于该证书后续被Kubernetes Master集群使用,需要将Master节点的IP都填上,同时还需要填写Service网络的首个IP DNS主机名Master Service虚拟服务名称,代码如下。 vim /etc/etcd/ssl/apiserver_ssl.cnf   代码如下。 [ req ] distinguished_name = req_distinguished_name req_extensions = v3_req [ req_distinguished_name ] countryName = FZ countryName_default = GB stateOrProvinceName = FJ stateOrProvinceName_default = China localityName = CN localityName_default = Fujian organizationName = Fujian organizationName_default = Home organizationalUnitName = Home organizationalUnitName_default = IT commonName = k8s commonName_max = 64 commonName_default = localhost [ v3_req ] basicConstraints = CA:FALSE keyUsage = nonRepudiation, digitalSignature, keyEncipherment subjectAltName = @alt_names [ alt_names ] IP.1 = 192.168.0.27 IP.2 = 192.168.0.28 IP.3 = 192.168.0.29 IP.4 = 192.168.0.14 IP.5 = 192.168.0.21 IP.6 = 192.168.0.22 DNS.1 = kubernetes DNS.2 = kubernetes.default DNS.3 = kubernetes.default.svc DNS.4 = kubernetes.default.svc.cluster DNS.5 = kubernetes.default.svc.cluster.local DNS.6 = master1 DNS.7 = master2 DNS.8 = master3 DNS.9 = node1 DNS.10 = node2 DNS.11 = node3   步骤27:生成apiserver.key和apiserver.crt,保存在/etc/kubernetes/pki中,代码如下。 cd /etc/kubernetes/pki   步骤28:生成apiserver-ca服务端证书,代码如下。 openssl genrsa -out apiserver.key 2048 openssl req -new -key apiserver.key -config /etc/etcd/ssl/apiserver_ssl.cnf -subj "/CN=master1" -out apiserver.csr openssl x509 -req -in apiserver.csr -CA /etc/kubernetes/pki/ca.crt -CAkey /etc/kubernetes/pki/ca.key -CAcreateserial -days 36500 -extensions v3_req -extfile /etc/etcd/ssl/apiserver_ssl.cnf -out apiserver.crt cd /etc/kubernetes/pki   步骤29:生成apiserver-ca客户端证书,代码如下。 openssl genrsa -out client.key 2048 openssl req -new -key client.key -subj "/CN=admin /O=system:masters" -out client.csr openssl x509 -req -in client.csr -CA /etc/kubernetes/pki/ca.crt -CAkey /etc/ kubernetes/pki/ca.key -CAcreateserial -days 36500 -extensions v3_req -out client.crt   步骤30:创建apiserver的systemd启动文件,统一systemd管理。其中EnvironmentFile是环境变量,提供ExecStart配置里面调用${},代码如下。 vim /usr/lib/systemd/system/apiserver.service   代码如下。 [Unit] Description=Kubernetes API Server Documentation=https://github.com/kubernetes/kubernetes After=etcd.service Wants=etcd.service [Service] EnvironmentFile=/etc/kubernetes/apiserver.conf ExecStart=/usr/bin/kube-apiserver $KUBE_APISERVER_OPTS Restart=on-failure RestartSec=5 Type=notify LimitNOFILE=65536 [Install] WantedBy=multi-user.target chmod -R 777 /usr/lib/systemd/system/apiserver.service   注:   –logtostderr:启用日志。   –v:日志等级。   –log-dir:日志目录。   –etcd-servers:etcd集群地址。   –bind-address:监听地址。   –secure-port:https安全端口。   –advertise-address:集群通告地址。   –allow-privileged:启用授权。   –service-cluster-ip-range:Service虚拟IP地址段。   –enable-admission-plugins:准入控制模块。   –authorization-mode:认证授权,启用RBAC授权和节点自管理。   –enable-bootstrap-token-auth:启用TLS bootstrap机制。   –token-auth-file:bootstrap token文件。   –service-node-port-range:Service nodeport类型默认分配端口范围。   –kubelet-client-xxx:apiserver访问kubelet客户端证书。   –tls-xxx-file:apiserver https证书。   –etcd-xxxfile:连接etcd集群证书。   –audit-log-xxx:审计日志,代码如下。 vim /etc/kubernetes/apiserver.conf   代码如下。 KUBE_APISERVER_OPTS="--enable-admission-plugins=NamespaceLifecycle,NodeRestriction,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota \ --anonymous-auth=false \ --bind-address=192.168.0.27 \ --secure-port=6443 \ --advertise-address=192.168.0.27 \ --insecure-port=0 \ --authorization-mode=Node,RBAC \ --token-auth-file=/etc/kubernetes/token.csv \ --runtime-config=api/all=true \ --enable-bootstrap-token-auth \ --service-cluster-ip-range=10.255.0.0/16 \ --service-node-port-range=30000-50000 \ --tls-cert-file=/etc/kubernetes/pki/apiserver.crt \ --tls-private-key-file=/etc/kubernetes/pki/apiserver.key \ --client-ca-file=/etc/kubernetes/pki/ca.crt \ KUBE_APISERVER_OPTS="--enable-admission-plugins=NamespaceLifecycle,NodeRestriction,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota \ --anonymous-auth=false \ --bind-address=192.168.0.27 \ --secure-port=6443 \ --advertise-address=192.168.0.27 \ --insecure-port=0 \ --authorization-mode=Node,RBAC \ --token-auth-file=/etc/kubernetes/token.csv \ --runtime-config=api/all=true \ --enable-bootstrap-token-auth \ --service-cluster-ip-range=10.255.0.0/16 \ --service-node-port-range=30000-50000 \ --tls-cert-file=/etc/kubernetes/pki/apiserver.crt \ --tls-private-key-file=/etc/kubernetes/pki/apiserver.key \ --client-ca-file=/etc/kubernetes/pki/ca.crt \   步骤31:将文件复制到master1和master2中,代码如下。 for i in master2 master3;do rsync -vaz /root/k8s/kubernetes/server/bin/ $i:/usr/bin/;done for i in master2 master3;do rsync -vaz /etc/kubernetes/ $i:/etc/kubernetes/;done for i in master2 master3;do rsync -vaz /usr/lib/systemd/system/apiserver.service $i:/usr/lib/systemd/system/apiserver.service;done   步骤32:修改master1和master2的IP,代码如下。 vim /etc/kubernetes/apiserver.conf chmod -R 777 /usr/bin/ chmod -R 777 /usr/lib/systemd/system/apiserver.service   步骤33:3台主机启动API,代码如下。 systemctl daemon-reload systemctl enable apiserver.service systemctl restart apiserver.service systemctl status apiserver.service journalctl -xe   步骤34:不要证书访问,代码如下。 curl --insecure https://192.168.0.27:6443/   这个访问应该是401错误,如图7-27所示。 图7-27 401错误   3. 创建客户端CA证书   kube-controller-manager、kube-scheduler、kubelet和kube-proxy作为客户端连接kube- apiserver服务,需要为它们创建客户端CA证书,后续kube-apiserver使用RBAC对客户端(如kubelet、kube-proxy和Pod)请求进行授权。   kube-apiserver预定义了一些RBAC使用的RoleBindings,如cluster-admin将Group system:masters与Role cluster-admin绑定,该Role授予了调用kube-apiserver的所有API的权限。   O指定该证书的Group为system:masters,kubelet使用该证书访问kube-apiserver时,由于证书被CA签名,所以认证通过,同时由于证书用户组为经过预授权的system:masters,所以被授予访问所有API的权限。   注:这个admin 证书是将来生成管理员用的kube config 配置文件用的,现在一般建议使用RBAC 来对Kubernetes 进行角色权限控制,Kubernetes 将证书中的CN 字段 作为User,O 字段作为 Group;“O”: “system:masters”,必须是system:masters,否则后面的kubectl create clusterrolebinding报错。   4. 部署kubeclt到apiserver   步骤1:kubectl默认从\~/.kube/config配置文件获取访问kube-apiserver地址、证书、用户名等信息,如果没有配置该文件,或者该文件个别参数配置出错,代码如下。 cd ~ mkdir /root/.kube/ kubectl config set-cluster kubernetes --certificate-authority=/etc/kubernetes/ pki/ca.crt --embed-certs=true --server=https://192.168.0.27:6443 --kubeconfig=kube.config kubectl config set-credentials admin --client-certificate=/etc/kubernetes/pki/ client.crt --client-key=/etc/kubernetes/pki/client.key --embed-certs=true --kubeconfig= kube.config kubectl config set-context kubernetes --cluster=kubernetes --user=admin --kubeconfig=kube.config kubectl config use-context kubernetes --kubeconfig=kube.config cp kube.config /root/.kube/config   以上会生成如下文件,代码如下。 vim /root/.kube/config apiVersion: v1 clusters: - cluster: certificate-authority-data: server: https://192.168.0.18:8443 name: kubernetes contexts: - context: cluster: kubernetes user: admin name: kubernetes current-context: kubernetes kind: Config preferences: {} users: - name: admin user: client-certificate-data: client-key-data:   步骤2:把文件移动到config,代码如下。 mkdir -p /root/.kube/ cp kube.config /root/.kube/config Kubectl接入apiserver kubectl create clusterrolebinding kube-apiserver:kubelet-apis --clusterrole= system:kubelet-api-admin --user kubernetes kubectl cluster-info kubectl get componentstatuses kubectl get all --all-namespaces      步骤3:同步kubectl配置文件到其他节点,代码如下。 rsync -vaz /root/.kube/config master2:/root/.kube/ rsync -vaz /root/.kube/config master3:/root/.kube/ kubectl 子命令补全 yum install -y bash-completion source /usr/share/bash-completion/bash_completion source <(kubectl completion bash) kubectl completion bash > ~/.kube/completion.bash.inc source '/root/.kube/completion.bash.inc' source $HOME/.bash_profile   以上会生成如下文件,代码如下。 vim /root/.kube/config apiVersion: v1 clusters: - cluster: certificate-authority-data: server: https://192.168.0.18:8443 name: kubernetes contexts: - context: cluster: kubernetes user: admin name: kubernetes current-context: kubernetes kind: Config preferences: {} users: - name: admin user: client-certificate-data: client-key-data: 7.1.4 Kubernetes集群的安全设置   1. 基于CA签名的双向数字证书认证方式   在一个安全的内网环境中,Kubernetes的各个组件与Master之间可以通过kube- apiserver的非安全端口http://kube-apiserver-ip:8080进行访问。   但如果API Server需要对外提供服务,或者集群中的某些容器也需要访问API Server以获取集群中的某些信息,则更安全的做法是启用HTTPS安全机制。Kubernetes提供了基于CA签名的双向数字证书认证方式和简单的基于HTTP Base或Token的认证方式,其中CA证书方式的安全性最高。本节先介绍如何以CA证书的方式配置Kubernetes集群。   要求Master上的kube-apiserver、kube-controller-manager、kube-scheduler进程及各Node上的kubelet、kube-proxy进程进行CA签名双向数字证书安全设置。   基于CA签名的双向数字证书的生成过程如下。   (1)为kube-apiserver生成一个数字证书,并用CA证书签名。   (2)为kube-apiserver进程配置证书相关的启动参数,包括CA证书(用于验证客户端证书的签名真伪)、自己的经过CA签名后的证书及私钥。   (3)为每个访问Kubernetes API Server的客户端(如kubecontroller-manager、kube- scheduler、kubelet、kube-proxy及调用API Server的客户端程序kubectl等)进程都生成自己的数字证书,也都用CA证书签名,在相关程序的启动参数里增加CA证书、自己的证书等相关参数。   2. 生成数字证书的6个步骤   (1)设置kube-apiserver的CA证书相关的文件和启动参数,使用OpenSSL工具在Master服务器上创建CA证书和私钥相关的文件。   (2)设置kube-controller-manager的客户端证书、私钥和启动参数。   (3)设置kube-scheduler启动参数。   (4)设置每个Node上kubelet的客户端证书、私钥和启动参数。   (5)设置kube-proxy的启动参数。   (6)设置kubectl客户端使用安全方式访问API Server。 7.1.5 Kubernetes集群的网络配置   在多个Node组成的Kubernetes集群内,跨主机的容器间网络互通是Kubernetes集群能够正常工作的前提条件。Kubernetes本身并不会对跨主机的容器网络进行设置,这需要额外的工具来实现。除了谷歌公有云GCE平台提供的网络设置,一些开源的工具包括Flannel、Open vSwitch、Weave、Calico等都能够实现跨主机的容器间网络互通。随着CNI网络模型的逐渐成熟,Kubernetes将优先使用CNI网络插件打通跨主机的容器网络。   (1)Flannel(覆盖网络)。   (2)Open vSwitch(虚拟交换机)。   (3)直接路由。 7.1.6 Kubernetes核心服务配置详解   Kubernetes的每个服务都提供了许多可配置的参数,这些参数涉及安全性、性能优化及功能扩展(Plugin)等方方面面。   每个服务的可用参数都可以通过运行“cmd --help”命令进行查看,其中cmd为具体的服务启动命令,如kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube- proxy等。另外,可以通过在命令的配置文件(如/etc/kubernetes/kubelet等)中添加“--参数名=参数取值”语句来完成对某个参数的配置。   1. 公共配置参数   公共配置参数适用于所有服务(kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy),如表7-4所示。 表7-4 公共配置参数表 参数名和取值示例 说 明 --log-backtrace-at traceLocation 记录日志每到“file:行号”时打印一次stack trace,默认值为0 --log-dir string 日志文件路径 --log-flush-frequency duration 设置flush日志文件的时间间隔,默认值为5s --logtostderr 设置为true则表示将日志输出到stderr,不输出到日志文件 --alsologtostderr 设置为true则表示将日志输出到文件的同时输出到stderr … …      2. kube-apiserver启动参数   kube-apiserver启动参数说明如表7-5所示。 表7-5 对kube-apiserver启动参数的说明 参数名和取值示例 说 明 --apiserver-count int 集群中运行的API Server数量,默认值为1 --audit-log-maxage int 审计日志文件保留最长天数 --audit-log-maxbackup int 审计日志文件个数 --audit-log-maxsize int 审计日志文件单个大小限制,单位为MB,默认为100MB --audit-log-path string 审计日志文件全路径 … …      3. kubelet启动参数   kubelet启动参数说明如表7-6所示。 表7-6 对kubelet启动参数的说明 参数名和取值示例 说 明 --address ip 绑定主机IP地址,默认值为0.0.0.0,表示使用全部网络接口 --allow-privileged 是否允许以特权模式启动容器,默认值为false 续表 参数名和取值示例 说 明 --api-servers API Server地址列表,以ip:port格式表示,以逗号分隔 --anonymous-auth 设置为true时表示Kubelet Server可以接收匿名请求。不会被任何authentication拒绝的请求将被标记为匿名请求。匿名请求的用户名为system:anonymous,用户组为system:unauthenticated。默认值为true --application-metrics-count-limit int 为每个容器保存的性能指标的最大数量,默认值为100 … … 7.2 Kubernetes命令行工具 7.2.1 kubectl用法介绍   Kubernetes集群中可以使用kubectl命令行工具进行管理。kubectl可在$HOME/.kube目录中查找一个名为config的配置文件。用户可以通过设置KUBECONFIG环境变量或设置--kubeconfig参数来指定其他kubeconfig文件。   使用以下语法?kubectl?从终端窗口运行命令,代码如下。 kubectl [command] [TYPE] [NAME] [flags]   (1)command:指定要对一个或多个资源执行的操作,如create、get、describe、delete等。   (2)TYPE:指定资源类型。资源类型不区分大小写,可以指定单数、复数或缩写形式,代码如下。 例如,以下命令输出相同的结果。 kubectl get pod pod1 kubectl get pods pod1 kubectl get po pod1   (3)NAME:指定资源的名称。名称区分大小写。如果省略名称,则显示所有资源的详细信息kubectl get pods。在对多个资源执行操作时,可以按类型和名称指定每个资源,或指定一个或多个文件。   要对所有类型相同的资源进行分组,请执行以下操作:TYPE1 name1 name2 name<#>,代码如下。 kubectl get pod example-pod1 example-pod2   分别指定多个资源类型:TYPE1/name1 TYPE1/name2 TYPE2/name3 TYPE<#>/name <#>,代码如下。 kubectl get pod/example-pod1 replicationcontroller/example-rc1   用一个或多个文件指定资源:-f file1 -f file2 -f file<#>。   flags:指定可选的参数。例如,可以使用-s或-server参数指定Kubernetes API服务器的地址和端口。   kubectl作为kubernetes的命令行工具,主要用于对集群中的资源的对象进行操作,包括对资源对象的创建、删除和查看等。在7.2.2节中详细介绍了kubectl支持的所有操作,以及这些操作的语法和描述信息。 7.2.2 kubectl子命令详解   kubectl支持的操作如表7-7~表7-9所示。 表7-7 Kubernetes命令1 操 作 语 法 描 述 delete kubectl delete(-f FILENAME \| TYPE [NAME \| /NAME \| -l label \| –all])[flags] 删除资源对象 describe kubectl describe(-f FILENAME \| TYPE [NAME_PREFIX \| /NAME \| -l label])[flags] 显示一个或者多个资源对象的详细信息 edit kubectl edit(-f FILENAME \| TYPE NAME \| TYPE/NAME)[flags] 通过默认编辑器编辑和更新服务器上的一个或多个资源对象 exec kubectl exec POD [-c CONTAINER] [-i] [-t] [flags] [– COMMAND [args…]] 在Pod的容器中执行一个命令 explain kubectl explain [–include-extended-apis=true] [–recursive=false] [flags] 获取Pod、Node和服务等资源对象的文档 expose kubectl expose(-f FILENAME \| TYPE NAME \| TYPE/NAME)[–port=port] [–protocol=TCP\|UDP] [–target-port=number-or-name] [–name=name] [—-external-ip=external-ip-of-service] [–type=type] [flags] 为副本控制器、服务或Pod等暴露一个新的服务 get kubectl get(-f FILENAME \| TYPE [NAME \| /NAME \| -l label])[–watch] [–sort-by=FIELD] [[-o \| –output]=OUTPUT_FORMAT] [flags] 列出一个或多个资源 label kubectl label(-f FILENAME \| TYPE NAME \| TYPE/NAME)KEY_1=VAL_1 … KEY_N=VAL_N [–overwrite] [–all] [–resource- version=version] [flags] 添加或更新一个或者多个资源对象的标签 表7-8 Kubernetes命令2 操  作 语  法 描  述 annotate kubectl annotate(-f FILENAME \| TYPE NAME \| TYPE/NAME)KEY_1=VAL_1 … KEY_N=VAL_N [–overwrite] [–all] [–resource- version=version] [flags] 添加或更新一个或多个资源的注释 续表 操  作 语  法 描  述 api-versions kubectl api-versions [flags] 列出可用的API版本 apply kubectl apply -f FILENAME [flags] 将来自于文件或stdin的配置变更应用到主要对象中 attach kubectl attach POD -c CONTAINER [-i] [-t] [flags] 连接到正在运行的容器上,以查看输出流或与容器交互(stdin) autoscale kubectl autoscale(-f FILENAME \| TYPE NAME \| TYPE/NAME)[–min=MINPODS] –max=MAXPODS [–cpu-percent=CPU] [flags] 自动扩容和缩容由副本控制器管理的Pod cluster-info kubectl cluster-info [flags] 显示群集中的主节点和服务的端点信息 config kubectl config SUBCOMMAND [flags] 修改kubeconfig文件 create kubectl create -f FILENAME [flags] 从文件或stdin中创建一个或多个资源对象    表7-9 Kubernetes命令3 操 作 语 法 描 述 logs kubectl logs POD [-c CONTAINER] [–follow] [flags] 显示Pod中一个容器的日志 patch kubectl patch(-f FILENAME \| TYPE NAME \| TYPE/NAME)–patch PATCH [flags] 使用策略合并补丁过程,更新资源对象中的一个或多个字段 port-forward kubectl port-forward POD [LOCAL_PORT:]REMOTE_PORT […[LOCAL_PORT_N:]REMOTE_PORT_N] [flags] 将一个或多个本地端口转发到Pod proxy kubectl proxy [–port=PORT] [–www=static-dir] [–www-prefix=prefix] [–api-prefix=prefix] [flags] 为kubernetes API服务器运行一个代理 replace kubectl replace -f FILENAME 从文件或stdin中替换资源对象 rolling-update kubectl rolling-update OLD_CONTROLLER_NAME([NEW_CONTROLLER_ NAME] –image=NEW_CONTAINER_IMAGE \| -f NEW_CONTROLLER_ SPEC)[flags] 通过逐步替换指定的副本控制器和Pod来执行滚动更新 run kubectl run NAME –image=image [–env=”key=value”] [–port=port] [–replicas=replicas] [–dry-run=bool] [–overrides=inline-json] [flags] 在集群上运行一个指定的镜像 scale kubectl scale(-f FILENAME \| TYPE NAME \| TYPE/NAME)–replicas=COUNT [–resource-version=version] [–current-replicas=count] [flags] 扩容和缩容副本集的数量 续表 操 作 语 法 描 述 version kubectl version [–client] [flags] 显示运行在客户端和服务器端的Kubernetes版本 7.2.3 kubectl输出格式   格式化输出所有kubectl命令的默认输出格式都是纯文本格式。要以特定格式向终端窗口输出详细信息,可以将-o或--output参数添加到受支持的kubectl命令中。kubectl [command] [TYPE] [NAME] -o=,如表7-10所示。 表7-10 kubectl输出格式 操 作 描 述 -o custom-columns= 使用逗号分隔的自定义列列表打印表 -o custom-columns-file= 使用??文件中的自定义列模板打印表 -o json 输出JSON格式的API对象 -o jsonpath=