陈晓宇
云计算技术的发展历程,是一个逐步将资源虚拟、共享和服务化的过程,这带来两大收益:一方面,它最大限度地提高了资源利用率;另一方面,它更便于应用,应用开发者只需要管理好自己的应用和数据,把底层资源分配、调度及备份、安全等复杂的问题留给了云计算平台。
愿这本书成为运维工程师、应用开发者、系统架构师、技术管理者的良师益友。
DevOps与容器PaaS的天然集成,极大地提升了研发、测试、上线的整体效率。
作者是云计算科班出身,北京航空航天大学云计算专业2014年硕士毕业。毕业后一直致力于IaaS和PaaS平台的开发和推广,经验丰富,为行业用户IT容器虚拟化的推广做了不少铺垫性的基础工作。目前作者在宜信担任容器云架构师,快速推进了K8S引擎在宜信的落地。在快速迁移传统应用至容器化部署架构方面成果卓越,得到业务方的一致认可。
近日喜闻陈晓宇编著了本书,拿到样稿后,我一口气看完了本书的样稿,深深地为作者的知识全面、云平台技术功力深厚而折服。作者多年容器云平台的研发经验积累,使得整本书的逻辑性很强,从传统的IaaS到目前流行的以容器云K8S为核心的PaaS,从逐渐老去的OpenStack到迸发勃勃生机的K8S,一直到将会给应用体系架构带来革命性变革的Service Mesh
通过本书,读者可以快速了解云计算技术的前生后世,了解云计算技术发展的最新动向。
IaaS(Infrastructure as a Service),基础设施即服务,就是将基础设施当作服务对外输出,那么什么是基础设施呢?计算、存储、网络这些原始资源就是基础设施资源,通过互联网对外提供服务。典型的例子是Amazon的EC2服务,用户可以通过他们的管理页面或者API创建一台EC2实例(虚拟机),然后直接通过浏览器或者通过SSH客户端登录控制台,而不再需要考虑物理服务器购买、网络的布线、操作系统安装等烦琐的传统IT基础运维工作。
最近几年,容器和Kubernetes技术的不断成熟,为PaaS平台的实现提供了一种新的途径。通过将业务代码打包到容器的镜像内,再通过Kubernetes容器调度和运行管理对外提供服务,并且可以自动伸缩、滚动升级等。PaaS将管理的对象从资源升级到服务,面向接口编程和运维,PaaS平台的本质就是自动化编译构建及自动化服务运维。
我们使用的在线云编辑器就是一种SaaS服务,只需要通过浏览器就可以在线编辑Word或者PPT,并且可以云端保存,而且只要在能够连接到互联网的情况下,都可以编辑,用户不需要关心背后的实现细节。SaaS将会是未来应用交付的最优方式,浏览器在大部分应用场景中会逐渐取代桌面客户端程序,SaaS将应用的最终形态直接交付使用者,向用户暴露更少的技术细节。
云计算还可以根据行业划分为政务云(面向政府行业)、金融云(面向金融行业)、教育云(面向教育行业)等,根据每个行业的不同行业特征,提供定制的云服务。
在公有云中,资源管理按照层级划分,首先是区域(Region),每个区域都是独立的地理位置,并且完全隔离,可以实现一定程度的容错能力和稳定性,而且EC2实例支持跨区域的部署。我们以AWS的云部署为例,分为US-West(美国西部)、US-East(美国东部)、EU-West(欧盟伦敦)、EU-West(欧盟巴黎)等区域。
那么区域的下一层是可用区(AZ),可用区的设计是为了容灾备份的,每个区域都用独立的电源。通过部署独立可用区内的实例,可以避免单一位置故障的影响。可用区都是独立的,区域内的可用区通过低延迟链接相连,一般建议网络延迟在10ms内,AWS结构如图1-2所示。
第四是保证了服务的可靠性,在公有云环境中通过多区域、多机房部署,避免了服务单点部署故障,并且可以在服务负载压力高的情况下,完成资源的自动伸缩,保障服务稳定。
第五是可以缩短业务上线的周期,一方面通过“所得即所需”的资源提供方式,用户可以即刻获取所需要的资源,如需要数据库服务,只需要通过页面单击购买后,便可以通过分配的地址直接连接数据库服务,省去中间繁杂的服务部署等问题,另一方面,通过容器化结合DevOps业务流程,可以加快开发、测试,以及发布上线整个业务流程的进度,提高产品迭代速度,推动技术创新和业务发展。
③政策影响,如果将数据放到云上面,很多企业可能比较担忧数据的安全,即便企业能够放开,很多政府的条例也不允许,例如AWS进入中国,阿里云进入美国,都会面临很多政策风险。
2018年第二季度AWS占34%!,(MISSING)微软占14%!,(MISSING)IBM占8%!,(MISSING)谷歌占6%!,(MISSING)还有中国的阿里云占4%!。(MISSING)在短时间内,AWS的地位将很难撼动。下面我将给大家介绍AWS相关的一些服务。
1.8.1 IAM IAM(Identity and Access Management)是AWS(Amazon Web Services)的权限管理服务,负责AWS的认证和授权。可以通过IAM创建和管理AWS的用户和用户组,并设置各种权限来允许或拒绝用户对AWS资源的访问,从而保证资源的隔离和安全。
1.8.2 EC2 EC2(Amazon Elastic Compute Cloud)在AWS(Amazon Web Services)云中提供可扩展的计算服务。
虚拟机安全服务安全组(Security Group)起着虚拟防火墙的作用,可以控制一个或多个实例的流量。
计算虚拟化主要是针对CPU和内存的,提供虚拟机的交付方式。在物理服务器上面虚拟出多台虚拟服务器,每个服务器拥有独立的CPU、内存空间和系统盘。这些宿主机可以由不同的硬件厂商提供,并且,虚拟机可以运行在不同的操作系统。
1.节省资源 通过计算虚拟化,将数据中心计算资源细化,化大为小,从而可以更加精细地进行资源的划分和管理,节省资源。使用更少的物理机服务器,一台常规的x86服务器上可以运行多达30个虚拟机。
2.环境隔离 在传统的部署模式下,一台物理机上面可能部署多个服务,服务之间共享资源,互相影响。如果某个应用耗尽整台物理机资源,将导致物理机上所有应用崩溃。通过虚拟化,为每个应用生成一套独立的运行环境,从而达到运行环境隔离的目的,保障服务运行的稳定和安全。
通过虚拟化技术,屏蔽了物理设备的差异,将不同的硬件设备资源池化,最终形成标准化、多样化资源形态。
IaaS(基础设施即服务),它将物理服务器资源池化,并结合计算虚拟化、网络虚拟化,以及存储虚拟化技术,向最终用户交付计算、网络和存储服务。比如,我们可以在AWS或者阿里云上面购买虚拟机、VPC,以及块存储等服务,用户没有必要购买与运维任何物理设备。
存储管理负责维护存储的生命周期,从创建、使用到最终销毁。可以对外提供多种访问方式,包括对象存储、文件存储,以及块存储。
VMware的产品非常丰富,服务器虚拟化方面的产品主要是它的VMware vSphere系统。其中,计算虚拟化主要是它的ESXi操作系统,vCenter提供了ESXi服务器集中化管理
5.2 网络虚拟化的优势 网络虚拟化能够实现资源的最大化利用,将一个网卡虚拟成多个网卡,在一条网线上面传输多个链路数据,在一个虚拟的网桥上创建数百个网络端口,将机器的资源利用率最大化,从而降低成本,并且网络可以借助软件的方式实现网络虚拟化,从而节省昂贵的网络设备成本。
网络虚拟化能够更加高效地创建和变更网络,有效地解决了需要经常变动的网络拓扑问题,从而降低了管理难度。这点在虚拟化的场景中非常重要,虚拟机和容器的网络需要经常调整,人工管理几乎无法完成。网络虚拟化可以创建出多套租户网络,从而实现了网络的有效隔离,提升了网络安全性。
传统的数据中心网络架构主要分为三层:接入层、汇聚层和核心层,
VXLAN是由VMware、Cisco、RedHat等联合提出的一个解决方案,它本质就是MAC-in-UDP,将原始报文封装到UDP数据包中传输,负责处理(封包和解包)VXLAN报文的设备被称为VTEP(VXLAN Tunnel EndPoints)。
5.7 SDN 网络设备的配置和管理一直是一个被网络工程师“垄断”的工作,配置命令复杂,而且每个厂家设备的配置命令还各不相同。不仅如此,数据中心内繁多的设备需要逐一正确配置才能保证网络的通用性。一个网络的变更,网络工程师可能需要配置多台交换机、路由器、负载均衡器,以及防火墙等。有些设备已经基本具备了Web管理的功能,但操作起来仍然十分麻烦。
OpenStack是一个旨在为公有云及私有云的建设与管理提供软件的开源项目,构建一个云操作系统,管理数据中心计算、存储、网络等资源。管理员可以通过前端Web完成所有针对资源的操作,同时也提供了命令行管理工具。
OpenStack第一版是2010年7月发布的,命名为Austin,当时只有虚拟机管理Nova和对象存储Swift这两个项目
图6-1为OpenStack的整体功能图,可以看到主要功能还是集中在物理机、虚拟机、存储、网络的管理之上。
OpenStack部署主要可以通过Ansible、Puppet、Fuel、Rdo等自动化工具或者手动部署每个组件。对于开发者来说,使用Devstack则更加方便,下面将通过Devstack演示如何启动一个OpenStack集群。
回到1979年,有个叫作chroot的系统调用已经诞生,通过名字就可以知道通过它可以修改进程,以及其子进程的根目录,这样的程序活动范围就被设定到了指定的目录里面了,但这个隔离能力太弱,并没有太多应用。
直到2013年Docker诞生,才点燃了整个容器的生态圈,它倡导的“build ship run”概念和DevOps理念完美结合,打通了从开发测试到生产一整套流程,并且Docker定义了分层的容器镜像格式,这为容器跨主机部署定义了规范。2014年底,CoreOS正式发布了CoreOS的开源容器引擎Rocket(简称rkt)。Google坚定地站在了CoreOS一边,并将Kubernetes支持rkt作为一个重要里程碑
不过在2017年,Docker公司由于商业化的目的,已经将Docker项目改名为Moby。
但公司需要盈利,Docker公司将产品分为Docker CE和Docker EE,也就是社区版和企业版,不用说大家也知道,企业版是收费的,在功能上面也会更丰富。Docker的CE版本是通过Moby(https://github.com/moby/moby)项目编译生成的。
在版本的命名上面也做了调整,从之前的Docker 1.9.x、Docker 1.10.x、Docker 1.12.x的版本命名方式,修改成以发布年月命名的方式,后续就是Docker 17.x、Docker 18.x。
Docker只是容器的一种类型,其实在Docker之前,Linux原生就支持LXC。Docker出现之后,还有由CoreOS主导的rkt容器、国内阿里开源的Pouch等。
为啥Docker要设计成每个容器里面只运行一个进程,这样的设计主要是为了考虑软件的解耦和依赖的拆分,设想如果将两个应用部署在一个容器里,如果只是升级一个应用,必然会导致另一个应用的重新发布,这是不友好的。通过微服务架构,可以将单体服务拆分成多个微服务,部署到容器里行,而且可以随意扩缩单个服务。反过来,如果在容器内运行多个程序,既增加了维护的难度,还需要维护多个程序生命周期,以及需要在容器安装systemd程序管理工具。虽然rkt也这么尝试,但易用性很差。还有就是效率的提升,通过单一职责容器的划分,容器变得更加轻量化,部署更加方便。
Docker倡导的理念就是build、ship、run。一次打包,处处运行,这个要比Java的一次编译、处处运行又迈进了一步。
举个部署Java Web服务的例子。传统的部署方式是需要在服务器上先安装一套Java的运行环境,然后部署Tomcat服务,再把应用程序的war包放到tomcat目录下面,然后还需要配置一下war解压后的配置文件,例如,JDBC的配置文件,然后才能启动应用。这个过程不仅耗时、费力,而且还容易出现配置错误。
7.4.3 快速部署容器的镜像可以非常小,在生产环境中,一个Java的基础镜像一般是70MB左右,与虚拟机动辄几GB的镜像相比,在网络的传输上面耗时更短,并且启动也更加迅速,毕竟虚拟机需要先启动操作系统。在处理高并发的业务场景中,Docker能够实现秒级扩容,分担负载。
目前京东和阿里都已经部署超过十万台物理机规模的容器集群,其他很多公司也都在逐步尝试将应用迁移到容器里。
Docker不是强依赖Linux内核的吗?cgroup、namespace这些东西,无论是MAC还是Windows都没有啊,对,的确没有。其实Docker在Windows和MAC上面的运行都是在virtualbox微内核的支持之下,通过虚拟一个在他们之上运行的极简版的Linux内核,才能运行Docker容器。
下载的时候会先获取mainfest文件。这说明这个镜像所有的分层,和本地比较,如果发现部分层已经在本地存在,则跳过下载,只拉取本地没有的镜像层。新版的Docker还将之前单线程的拉取改成并发的拉取,提高了下载速度。
容器里面是一个新的进程空间,第一个进程的ID为1,它相当于操作系统的init进程。如果该进程运行结束,所有的子进程都会退出,整个容器也会退出。那么针对这个问题有办法解决吗?其实最简单的方式就是增加一个日志打印功能,不停地向控制台输出日志,这样这个“init”进程便可以一直运行不退出。
容器的IP默认是一个docker0网桥分配的内部IP,这个IP不能被外部网络访问,通过-p端口映射,格式为:主机端口:容器端口,可以将容器的端口映射到宿主机上,这样便可以通过主机的IP:PORT方式访问容器。这样同一个主机上面容器的端口可以相同,只需要保证宿主机映射的端口不冲突即可。如果容器之间需要共享信息,或者避免联合文件系统性能瓶颈,容器可以挂载外部存储,通过-v映射存储,格式为:主机路径:容器路径,可以将主机上面的目录或者文件直接挂载到容器里面。run命令末尾的COMMAND和ARG组合可以设定容器的启动命令。如果设置,会覆盖Dockerfile中的CMD命令。
stop命令是通过一种优雅关闭的方式停止容器,先向容器发送SIGTERM (kill-15)的信号,此时,如果容器内的程序截获到这个信号后,便可以拒绝新的请求,结束事务,回收自己所占用的资源,优雅退出。Docker默认等待10s后,如果容器仍然没有退出,则发送SIGKILL(kill-9)的信号,强制退出。这个10s可以通过docker stop后面追加-t的方式设定合理的停止时间,Kubernetes默认使用30s的停止时间。如果使用docker kill,则会直接发送SIGKILL信号。具体语法如下。
json-file是将日志输出到json文件中,默认情况就是/var/lib/docker/containers/容器ID/容器ID-json.log的文件中,这样就可以通过filebeat等日志采集插件,到改目录进行日志采集;syslog需要指定一个syslog的进程,将日志都输出到这个syslog程序中汇总,此时,docker logs将看不到容器的任何日志,在使用docker-compose启动一组容器的时候,经常会用syslog汇总;journald就是接着将日志输出到journald守护进程,那么就可以通过journalctl CONTAINER_NAME=容器名称的方式获取容器的日志了,并且可以在CentOS系统的/var/log/message里面看到日志;
第二,镜像构建默认会使用之前构建的缓存,这样不仅可以节省空间,还能加速镜像的构建,但这也会造成问题,例如通过“yum、apt-get”等命令安装软件包的时候,在不同的时间点安装的软件包版本是不相同的,但Docker在判断是否已存在的时候,通过比较父镜像ID、命令、添加文件的Hash值的方式判断Docker是否存在,这就导致软件包无法更新。解决的方法是可以删除本地缓存或者添加--no-cache,忽略缓存,每次都需要重新构建。
通过export设置的环境变量,只是在当前程序的上下文有效。当通过exec进入容器后,启动一个新进程,自然没法再保存到镜像中。
为容器内运行的程序指定用户和用户组,通常需要结合RUN adduser(添加用户)一起使用。可以通过docker run-u的方式覆盖USER的设定启动用户。之所以需要通过指定的用户启动程序,一方面是因为有些程序要以某些特定的用户才能启动的,另一方面是从安全角度考虑,避免所有程序都是root用户启动的。
在实际项目中,为了节省编译环境的安装和维护,经常使用Docker容器编译源代码,并打包编译过后的二进制到Docker镜像中。编译代码的镜像和程序运行的镜像通常是不同镜像,这样就需要通过两步才能完成。在Docker 17.05以后,引入了多阶段镜像构建功能,通过一个Dockerfile将镜像构建之前的准备工作和镜像打包合成一个Dockerfile,go程序的镜像打包代码如下所示。
那么这两步走是怎样关联的呢?构建任务通过as定义了本阶段名称为builder,然后通过--from指定文件来源于builder构建的产物,从而将多个阶段串联起来。
Docker容器中默认都是以root方式启动应用,很容易造成提权漏洞,为此建议在Dockerfile中通过USER命令指定一个普通用户启动。命令如下所示。[插图]第三是构建镜像,使用COPY而非ADD。由于ADD支持从公网链接中下载第三方包,而这个包很有可能被篡改而引发安全问题。
但从虚拟机直接迁移到Docker容器势必需要业务的改造,即便业务人员能够配合改造,也需要很长的周期才能完成。为此,阿里内部开始自主研发富容器Pouch,一个将SSH、Systemd打包到镜像中,并且使用固定IP的容器引擎就此诞生了。从而业务方可以无缝地从虚拟机迁移到容器中。
7.14 Go语言 Go语言又称为云语言,除了我们熟知的Docker和Kubernetes外,还有etcd、Prometheus、OpenFalcon、Harbor、Codies、beego、fabric等一大波采用Go语言编写的程序。
此外,Go语言最大的优势是用户态线程(协程),使用用户态线程最大的优势是创建和销毁协程的开销非常小,协程创建的主要开销只有为协程分配协程栈,并且多个协程之间不仅可以共享内存数据,还可以通过管道互相传输数据。运
Docker是基于LXC(Linux容器)实现的,但是它并不是要替代LXC。相反,Docker则是基于LXC提供一些高级功能,比如版本应用、跨主机部署可迁移应用等。而无论是Docker,还是LXC,都是基于内核的特性而开发的。Docker的本质是cgroup+namespace+union filesystem,其中,cgroup负责资源限制,namespace负责资源隔离,而union filesystem则是文件系统。
cgroup主要负责资源的限制和监控,如CPU和内存等。这里分为两个方面,一方面是资源限制,即对进程组使用的资源总额进行限制。如果程序超过了设定的cgroup内存的上限,便会触发OOM;另一方面是监控,可以统计资源的使用情况,如CPU使用时长,内存用量等。cgroup为了限制资源引入了以下概念。
Docker的旧版本存在一个严重的漏洞便是PID炸弹。在容器内部不停地创建新进程,导致耗尽整个宿主机的所有进程ID,进而导致主机宕机。在Docker之后的版本中添加“--pids-limit”参数,目的便是限制最大进程数。其实现原理也是依赖pid cgroup。
Docker最大的共享就是定义容器镜像的分层的存储格式,相比于cgroup和namespace的拿来主义,Docker将UnionFilesystem(联合文件系统)用于容器的镜像分层。这样既可以充分利用共享层,又可以减少存储占用。举例来说,一个是tomcat镜像,另一个是jetty镜像,它们底层都公用一个JDK镜像,只是在最上层有区别。
chroot,即change root directory(更改root目录)。在Linux系统中,系统默认的目录结构都是以“/”,即是以根(root)开始的。而在使用chroot之后,系统的目录结构将以指定的位置作为“/”位置。通过chroot,一方面可以增加系统的安全性,限制用户的权限;另一方面,在经过chroot之后,在新根下将访问不到旧系统的根目录结构和文件,从而增强了系统的安全性。其实是可以建立一个与原系统隔离的系统目录结构,方便用户开发。
当执行run命令后,程序调用自己(/proc/self/exe指向程序本身)child命令重新执行并设置各种namespace,切换root目录,从而创建一个新的容器。
Kubernetes起源于Google的Borg系统。Borg是在2003年开发的一个大规模集群管理系统。它支撑Google数千个应用程序(十万个应用程序)。
2016年3月16日,v1.2版本发布,支持1000+节点的集群,Pod启动时间小于3秒,并且通过读缓存(不是每次API的读请求都发送到ETCD中)做到了99%!的(MISSING)API操作延迟仅有几十毫秒,同时引入Pod生命周期事件生成器(PLEG),它取消了之前的并发(每个容器启动一个协程)周期获取容器状态的做法,改成缓存配合事件驱动的方式。
2018年6月27日,Kubernetes 2018年的第二个版本Kubernetes 1.11正式发布,继续推动Kubernetes走向成熟。ipvs负载均衡的引入解决了Kubernetes大规模部署时iptables条数过多导致性能下降的问题。
如果是多行字符串,可以通过“|”、“>”折叠起来,形式如下: 
9.2.4 声明式API 声明式API是相对于命令式API来说的。所谓的声明式API是指只需要告诉机器最终结果,不用关心实现过程和细节,而命令式API则是一种面向过程的操作,需要告诉机器每一步应如何执行,才能达到最终目标。可见,声明式API采用定义即所得的方式,更加便于用户理解和使用,相应的声明式在实现难度上面也更加复杂。
声明式API是Kubernetes最重要的设计思想,通过Yaml或者json文件定义最终的资源形态,剩下的交给Kubernetes去完成。Kubernetes获取用户提交的资源声明后,通过多个组件相互协作,最终满足用户定义的资源需求。
如果Deployment的选择器没有被设置(spec.selector.matchLabels为空),默认将会使用Pod标签(spec.template.metadata.labels)。
Kubernetes对于存储的挂载引入了两个资源管理对象PV和PVC,这是因为在实际生产环境中,存储维护和使用往往是由不同的部门负责维护的。为了充分解耦存储的提供者和存储使用者之间的关系,引入了PV和PVC。PV主要针对存储本身,是由存储的提供者创建的,它主要指定了存储的访问方式(ReadWriteOnce,只允许挂载一台主机;ReadOnlyMany,只读方法挂载多台主机;ReadWriteMany,读写挂载多台主机)、存储的容量,以及存储本身信息(譬如NFS会指定NFS服务地址和路径)
PVC是存储的使用者,使用申明的方式获取存储。PVC同样指定了存储访问方式和大小。那么,PVC如何绑定到PV呢?有两种方法,一种是静态指定,PVC创建时,指定pvc.Spec.VolumeName为PV名称,PVC会绑定到设定的PV上面,
还有一种方法属于Kubernetes自动关联,Kubernetes会根据存储的访问方式和申请存储的大小选择合适的PV绑定,这里要注意两个小细节,
Kubernetes提供了一种新的机制管理各种物理资源,即Device Plugins。GPU管理将在1.11版本后从主干代码中被移除。Device Plugins采用OutOfTree设计模式,提供通用设备插件机制和标准的设备API接口。这样设备厂商只需要实现相应的API接口,无须修改Kubelet主干代码,就可以实现支持GPU、FPGA、高性能NIC、InfiniBand等各种设备的扩展。
Device Plugins启动时会调用Kubelet的Register接口(GRPC),将自己注册到Kubelet中,如图9-2所示。之后,Device Plugins会启动一个GRPC服务,服务主要提供两个接口:ListAndWatch和Allocate。Kubelet会调用ListAndWatch接口获取Device Plugins的状态信息,通过Allocate完成资源初始化如GPU清理或者QRNG初始化。
Device Plugins需要在每台宿主机上面安装,所以在Kubernetes中通常采用DaemonSet的方式部署Device Plugins。在Device Plugins正常运行后,Device Plugins管理的设备便可以被Kubernetes识别,并当作扩展资源加入scheduler调度中。
由于kubeadm使用的是Google镜像,在国内无法直接使用,需要预先下载并修改镜像名称。
Minikube原理示意图,如图9-4所示,其本质是docker machine启动了一台虚拟机,然后通过kubeadm方式启动Kubernetes相关组件。
其中,grace-period指定优雅关闭时间,force表示是否强制删除。如果非强制删除,kubectl会一直同步等待Pod被删除后才结束,如果希望异步删除,可以添加--wait=false参数。
cp方法的本质是通过exec执行tar命令,所以容器必须内置tar,否则将无法完成拷贝。如果从容器内拷贝文件到本地,首先在容器内执行“tar cf-文件”,将文件归档后直接通过标准输出发送到本地。相反,如果是上传文件到容器,则是在容器内执行“tar-xf--C文件”解压归档文件。
(1)leader的切换频次。当Etcd通过raft协议选举出leader后,leader应该是固定不变的。如果leader一直发生切换,是非常不稳定的。Etcd提供了etcd_server_leader_changes_seen_total指标,表示Etcd的leader切换次数。生产环境如果在一个小时内发生超过三次的leader切换,需要发出告警。
(4)grpc指标。无论是Etcd节点之间的交互,还是客户端连接Etcd,v3版本的API已经全部切换到grpc。相比http方式,grpc有更高的性能。Etcd通过etcd_grpc_requests_failed_total指标表示grpc请求失败的次数,除以etcd_grpc_total(grpc请求总数)得出失败率。如果大于0.1可以认为请求的失败次数过多,应该提醒注意。
9.8.3.1 驱赶参数当计算节点(安装kubelet的节点)资源不足时,会触发驱赶行为。驱赶行为的触发条件主要包括memory.available可用内存、nodefs.available文件系统剩余空间、nodefs.inodesFree文件系统剩余Inode等、imagefs.available镜像文件系统剩余空间、imagefs.inodesFree镜像文件系统剩余Inode等。
kubelet可以针对这些指标设置软阈值和硬阈值。所谓的软阈值是指当达到阈值后触发关闭,逐步迁移,而硬阈值则是强制关闭容器,驱赶到其他节点运行。
当某个资源出现不足后,节点将会发出驱赶策略,并标记节点状态为MemoryPressure内存压力或者DiskPressure磁盘压力。当回收部分资源后(删除无用的镜像、回收死亡的容器、删除低优先级的Pod),节点将重新标记为正常运行,并重新接收新Pod创建,那么也可能会出现资源不足的情况。
Kubernetes API接口主要分为组、版本和资源三层,接口层次如图10-2所示。对于“/apis/batch/v1/jobs”接口,batch是组,v1是版本,jobs是资源。Kubernetes的核心资源都放在“/api”接口下,扩展的接口放在“/apis”下。
Apiserver启动后会将每个版本的接口都注册到一个核心的路由分发器(Mux)中。当客户端请求到达Apiserver后,首先经过Authentication认证和Authorization授权。认证支持Basic、Token及证书认证等。授权目前默认使用的是RBAC。
Scheduler的调度过程分为两个步骤。第一步是筛选(Predicate),筛选满足需要的节点。筛选的条件主要包括(1)Pod所需的资源(CPU、内存、GPU等);(2)端口是否冲突(主要是针对Pod HostPort端口和主机上面已有端口);(3)nodeSelector及亲和性(Pod亲和性和Node亲和性);(4)如果使用本地存储,那么Pod在调度时,将只会调度存储绑定的节点;
还有一些其他组件,如cloud-controller-manager,这个是在Kubernetes 1.6后面添加的,主要负责与IaaS云管理平台进行交互,主要是GCE和AWS。Kubernetes大部分部署目前都是在公有云环境中。Cloud-controller-manager通过调用云API获取计算节点状态,通过与云中负载均衡器交互,创建及销毁负载均衡,并且还可以支持云中存储的创建、挂载及销毁,主要是利用IaaS的能力扩展和增强Kubernetes的功能。
Kubernetes还支持Downward API动态注入。所谓动态注入是部分属性在Pod启动时才能确定的,譬如通过valueFrom方法,注入Pod所在宿主机名称(MY_NODE_NAME),以及Pod IP(MY_Pod_IP)。
如果是以目录方式挂载configmap,当configmap内容发生变化时,则会自动刷新(会有几秒的延迟)到容器里面。如果是以subpath挂载configmap中单个文件的时候,则需要重建Pod才能生效。虽然configmap能够自动刷新配置文件,但应用需要支持热加载功能,这里可以通过fsnotify等工具监测文件的变化。如果有变化,则调用自动刷新接口或者发送USR1信号刷新配置。
到容器时将数据反base64解密。Kubernetes默认为每一个命名空间都创建一个default的secret,并挂载到/var/run/secrets/Kubernetes.io/serviceaccount目录下,该secret主要是为容器内应用访问Kubernetes API提供安全凭证的,secret主要包含三个文件:ca证书、命名空间和访问token。
在业务容器启动之前,首先会运行Initcontainer容器,并在容器启动时执行PostStart Hook,在容器关闭时执行PreStop Hook。
面对各种容器运行时,Kubernetes希望提供一套统一的标准去兼容管理所有的容器运行时,这样就能够在无须重新编译Kubelet的前提下,管理新的容器运行时,CRI包含一组protocolbuffers、gRPC API、相关的库,以及在活跃开发下的额外规范和工具。
与Docker CRI方案(dockershim)的旧版方案相比,新的方案剔除了经过Docker dameon过长的调用链,并且跳过了Docker daemon本身的稳定性对容器的影响,一方面降低了容器启动的延迟时间,更重要的是大大降低了Kubelet和runtime的CPU和内存消耗。
首先,Docker Engine(dockerd)也是建立在containerd之上的,这样既可以增加Kubelet的性能和稳定性,也能继续保持Docker本身的特性。Containerd namespace可以将Kubelet和Docker Engine所建立的容器和镜像完全隔离,互相不影响,这样,如果想要查看Kubelet创建的容器,请使用crictl ps。对应的如果查看Docker命令行启动的容器,请使用docker ps。同
这样便可以通过kubectl命令像操作Kubernetes原生支持的资源一样,执行增、删、改、查等操作,
并没有控制器做任何操作(Kubernetes原生控制器肯定是无法识别Crontab类型的资源的)。如果还需要针对本资源操作,需要自己编写对应的控制。笔者根据官网提供的sample-controller案例简单改写了一个Crontab的控制器,并上传到GitHub(https://github.com/timchenxiaoyu/bookexample/sample-controller)上。首先,定义资源文件。
这里有一个细节是如果一个容器只设置了limit,而未设定request,则request的值等于limit值。
Kubelet驱赶的阈值分为软硬两个值,软阈值是指当节点满足该阈值后,Kubelet允许业务容器优雅停止,硬阈值则是会触发Kubelet强制杀死容器并且会标注节点处于不健康状态(内存不足MemoryPressure、磁盘不足DiskPressure),从而阻止新容器在调度时候分配到该节点。所以,软阈值设置比硬阈值要大,如内存的硬阈值设置成--eviction-hard=memory.available<200Mi,而软阈值设置为--eviction-softmemory.available<1Gi。
当主机资源不足时,会优先驱赶Pod QoS优先级较低的容器。为了保障高优先级任务的运行,建议将它们设置成Guaranteed。而优先级最低的Best-Effort,则是尽力保障容器的运行,需要谨慎使用。总体优先级如下:Best-Effort Pod < Burstable Pod < Guaranteed Pod.
Kubelet会根据容器使用资源超过容器申请资源(request)的程度进行排序,优先驱赶超过request较多的容器。由于Guaranteed Pod的limit和request相同,所以不会发生驱赶,而Best-Effort Pod由于request设置比较小,在驱赶时最容器被驱赶。Kubelet通过调用Apiserver将Pod状态设置成“Evicted”,此时controller manager紧接着将会创建一个新的Pod,并通过secheduler调度其他节点运行。
Kubernetes给我们带来的好处是毋庸置疑的,并且我们相信Kubernetes将会成为下一个Linux,所有应用都将慢慢符合云原生规范。
譬如,2019年2月25日,Kubernetes发布issue(#74534),公布了一个中等程度的安全漏洞CVE-2019-1002100。根据描述,具有patch权限的用户可以通过发送一个精心构造的patch请求来消耗Apiserver的资源,最终会导致Apiserver无法响应其他请求。企业需要有一定相关技术积累,以便关键问题能得到修复。
●学习曲线。Kubernetes采用Yaml和命令行的方式管理资源。这种偏运维的管理方式还存在一定的学习障碍,缺少一种简单操作的管理页面。
除了Kubernetes,还有其他一些容器管理平台,比如OpenShift、Rancher等管理平台。在之前的版本中,它们都独立开发了一套容器管理方案,但2018年后,无论是OpenShift,还是Rancher,都已经全面转向Kubernetes,把重点放到了Devops及Kubernetes集群管理等方面,容器的管理则全部交给Kubernetes。
10.10.1 RancherRancher是梁胜团队开源的一套容器管理系统,借助之前在CloudStack上面的积累,这套容器管理平台很多核心思想都是来自虚拟机管理方面的。
Kubernetes本身只提供了容器的资源管理和调度,但还缺少很多小伙伴才能构建一个完整容器平台。在CNCF的推动下,目前已经有很多项目已经从CNCF正式毕业,并且有更多的项目加入到CNCF下孵化。Kubernetes的生态圈正不断丰富和完善。
Prometheus是一套开源的系统监控报警框架。它受启发于Google的BorgMon监控系统,由工作在SoundCloud的Google前员工于2012年创建,作为社区开源项目进行开发,并于2015年正式发布。2016年,Prometheus正式加入CNCF,成为受欢迎度仅次于Kubernetes的项目。
Prometheus提供了多维度数据模型,每个数据指标都可以关联很多label,这个和Kubernetes的label思想一致,松耦合、高度自由定制数据维度。举例来说,每个容器的CPU指标都可以关联容器、Pod、容器所在主机、namespace等多个维度,那么在查询的时候可以根据这些label自由关联,并且支持promsql,允许用户以类sql的方式查询指标。
除了静态配置监控对象以外,为了适应监控对象不断变化的特点,Prometheus设计并开发了服务自动发现机制,能够支持Kubernetes、Etcd、DNS等多种方式的服务发现,Kubernetes的服务发现是通过watchKubernetes API动态发现容器的变化情况的。
Prometheus采用拉取(PULL)的方式采集数据,这与采用上报(PUSH)的采集方式不同,拉取方式有以下几个特点:第一是拉取方式对于客户端没有感知,客户端只需要不断采集数据,既不用关心数据后续的处理,也不需要维护数据状态(哪些数据已经上报,哪些数据上报失败需要重试,还有哪些还未上报),可以做到更加简单。第二是增强了数据汇聚服务的可控性,数据的汇聚节点可以根据当前系统情况,调整汇聚的周期和数据量,避免大量客户端一起上报数据,压垮汇聚节点。
为了扩展Prometheus数据采集的能力,可以建立Prometheus联邦,每个Prometheus负责采集一个区域内的监控对象,并在联邦的master上统一汇聚。
Prometheus Server默认将数据保存到本地时序数据库中,当前V3版本的TSDB,在性能上已经有了很大提升,可以支持每秒一千万个指标的存储。除此之外,Prometheus Server还提供了数据查询和告警的能力,数据查询也是采用HTTP+PromSQL的方式完成的,目前Grafana已经支持。告警也是借助PromSQL完成,Prometheus server会定时执行用户设定的PromSQL,如果满足告警条件则会向alertmanager发送告警通知。
CoreDNS是一种快速、灵活且现代的DNS服务器,可以在云原生部署中提供服务发现能力。
在CoreDNS之前,Kubernetes最早使用的DNS插件是KubeDNS。KubeDNS整体架构如图11-3所示,主要分为三个组件:kubedns、dnsmasq和exechealthz。
第二个作用是提供DNS查询服务。dnsmasq是一个开源的dns和dhcp服务,业务容器的DNS解析首先发送请求到dnsmasq。如果dnsmasq本地没有对应的解析记录,它将会向它的上游DNS服务(KubeDNS)查询,之后,dnsmasq充当DNS缓存,避免每次请求都通过KubeDNS解析。exechealthz是健康检查组件,通过定时发起DNS查询请求,检测KubeDNS和dnsmasq监控状态。
KubeDNS由于引入了dnsmasq导致整体比较复杂,并且dnsmasq是一个单线程的程序。性能比较一般,而且还有安全漏洞。为此,社区开发了第二版Kubernetes DNS方案CoreDNS。CoreDNS编译出来就是一个单独的二进制可执行文件,内置了缓存、后端存储、健康检查等功能,还可以支持各种插件。下面通过Corefile配置CoreDNS,Corefile是CoreDNS的配置文件。
CoreDNS除了支持DNS协议,还支持TLS和gRPC,即DNS-over-TLS和DNS-over-gRPC模式,从而可以更加灵活、安全地使用DNS解析。
Filebeat是一款由Go语言编写的具有高并发能力的日志采集插件。Filebeat的工作原理如图11-5所示:其中,Harvester负责逐行读取文件,每个文件都对应一个Harvester,然后Harvester将采集的数据发送到Spooler,经过Spooler整合后发送到不同的后端,如Elasticsearch或者Kafka等。
容器的日志采集主要分为两部分:一是标准日志输出(控制台日志),另一个是应用输出的日志文件,如tomcat的catalina.out,或者log4j生成的不同级别的日志。虽然容器的使用方式更建议使用第一种方式,但很多传统应用还是习惯于采用日志文件方式输出日志。第一种日志的采集相对简单,首先将Docker日志设置成json输出(/etc/docker/daemon.json)。将会在容器目录下生成json格式的日志文件,
/var/lib/docker/containers/容器ID/容器ID-json.log接下来,需要在Filebeat中设置“/var/lib/docker/containers”路径,以便采集容器的标准输出日志。此时,Filebeat为了将容器的日志和业务标签关联起来,会调用Kubernetes或者Docker API获取容器的标签信息,并将每一行日志打上对应的业务标签,从而可以很方便地在Elasticsearch里面通过Kubernetes定义的标签检索容器日志。
如果是容器内日志文件的采集,通常将日志文件映射到宿主机的某个指定的目录上,然后再配置Filebeat去宿主机指定的目录下面采集。这里还需要配合日志定期清理,避免日志不断累加,耗尽主机存储。
Yaml文件主要分为三个部分:第一是通过DaemonSet启动Filebeat的;第二是通过ConfigMap配置Filebeat的;第三是通过RBAC设置权限的。
Kubernetes启动容器之前需要先从镜像仓库拉取镜像。如果是企业私有化部署Kubernetes,出于安全和性能考虑,都会搭建私有镜像仓库。必须有支持多租户的镜像仓库,VMware开源的Harbor是最常用的镜像仓库。
Harbor还集成了原生的Docker镜像仓库Docker Registry,负责镜像的存储;集成了Clair,负责镜像扫描。Clair是CoreOS开源的镜像扫描组件,通过获取公网开放的CVE库,检测镜像中的安全漏洞。前端的API Routing路由分发是通过nginx实现的。Harbor中的每一个镜像仓库都属于一个项目,镜像的命名包含了项目名称和镜像仓库名称,格式如下:[插图]
在Harbor中,每个项目可以有三种角色:项目管理员(project admin)、开发者(developer)和访客(guest)。其中,访客只具有只读权限,开发者可以上传和下载镜像;项目管理员不仅具有该项目的读写权限,还可以管理项目,如在项目下添加用户。除
Harbor支持高可用部署,通过将多个Harbor放置到负载均衡器后端,每一个Harbor通过对接MySQL和Redis,共享集群的元数据,镜像仓库对接对象存储,从而将Harbor本身做成无状态应用。
Harbor镜像的删除只是删除镜像的元数据,真实的分层文件仍然存在。如果是Harbor 1.7之前的版本,需要启动官方推荐的GC镜像删除分层,如果是Harbor1.7之后的版本,则支持在线GC,通过Web管理页面,便可以直接触发GC回收无用的镜像分层。
图11-7 Harbor高可用部署架构图
镜像仓库保存了所有容器的启动镜像。当面对大规模容器集群(1000+节点)时,由于所有的镜像都需要从镜像仓库下载,镜像仓库往往会成为性能的瓶颈。在笔者之前的工作经历中,曾经遇到一次生产环境扩容2000个副本的场景,结果用了2个多小时才完成,等到扩容完成,业务的高峰期已经过去了。
那么有没有一种更加快速、高效的镜像分发技术呢?想必每个人都用过迅雷或者电驴之类的P2P下载技术,它的本质原理就是通过将每个下载节点作为数据的服务节点,提供下载文件的能力,从而快速地分发文件,避免单点瓶颈。在这个技术背景下,开源社区有两个相对成熟的项目,即阿里的Dragonfly(蜻蜓)和Uber的Kraken(海怪)。
Dragonfly是一款基于P2P的智能镜像和文件分发工具。借助P2P分发技术,提高文件传输的效率和速率,最大限度地利用网络带宽,尤其是在分发大量数据时,例如应用分发、缓存分发、日志分发和镜像分发。
Dragonfly是一种无侵入式的解决方案,并不需要修改Docker的源代码。图11-9所示为Dragonfly整体架构图,在每个节点上面会启动一个dfdaemon和dfget,dfdaemon是一个代理程序,它会截获dockerd上传或者下载镜像的请求,dfget是一个下载客户端工具。每个dfget启动后首先通过“/peer/registry”接口将自己注册到supernode上。supernode超级节点以被动CDN的方式产生种子数据块,并调度数据块分布。
当dockerd拉取镜像分层时,dfdaemon通过dfget请求supernode下载数据,supernode会从最终的镜像仓库拉取镜像,分割成多个数据块。dfdaemon下载数据块,并对外共享数据块,后续如果有其他节点也需要下载该镜像,会直接从之前的节点下载,避免将所有请求都转发到镜像仓库。
应用是将多个服务组合到一起维护的。一个应用包含了多个服务,譬如,一个网站是一个应用,这个应用是由MySQL、Spring Boot等多个服务组成。可以针对一个应用启动和关闭,以及编排。应用和服务逻辑关系图如图12-2所示。
●Fluentd主要负责日志采集,通常和应用服务安装在一起,读取日志文件或者从网络流中接收文本日志并发送到Kafka中。除了Fluentd以外,还有很多采集插件,如Fluent Bit、Filebeat、Flume Agent等。 ●Kafka主要负责汇聚数据,并提供数据整流,避免突发日志流量直接冲击后端系统。在实际生产环境中,Kafka是集群部署,通过将数据分区到不同节点实现数据的负载均衡。
●Logstash负责日志整理,可以过滤、修改日志内容,比如过滤日志中的敏感信息。 ●Elasticsearch负责日志的存储和检索。自带分布式存储,可以将采集的日志分片存储。为保证数据的高可用性,Elasticsearch引入多副本概念,并通过Lucene实现日志的索引和查询。 ●Kibana是一个日志查询组件,负责日志展现。
3)基于日志的告警,当多次出现某个写关键字后触发告警;(4)基于服务的日志查询,不仅可以查询单个容器的日志,还可以针对这一组容器进行检索。
镜像管理还需要做好镜像的存储容量监控和定期清理。随着业务被不断迭代,每次都会增加很多新的镜像。为了提供业务代码回滚能力,通常会选择删除创建时间比较早的镜像。
由于单个Kubernetes集群本身故障,如集群网络配置错误导致整个网络故障等,都将会影响业务的正常使用。因此,我们将Kubernetes部署在多个机房内,机房之间通过专线互连。那么多集群的管理将成为主要难点:第一是如何分配资源。分为两类资源,一类是配置型资源,如PV、Configmap之类的,它们可以在每个集群创建;另一类是运行类资源,譬如容器,当用户选择多集群部署后,系统根据每个集群的资源用量,决定每个集群分配的容器数量,并且保证每个集群至少有一个容器。
容器通过域名访问虚拟机和虚拟机通过域名访问容器都是普遍存在的。为了统一管理域名,我们没有采用Kubernetes自带的kube-dns(coreDNS),而是采用bind提供域名解析。通过Kubernetes支持的Default DNS策略将容器的域名指向公司的DNS服务器(参见图12-16),并配置域名管理的API动态配置。
因为机房并不允许跑BGP协议,且需要跨机房的容器互连,所以可采用Flannel的VXLAN方案。为了实现跨机房的互通,两个集群的Flannel连接同一个Etcd集群,这样保障网络配置的一致性。旧版本的Flannel存在很多问题,比如路由条数过多,ARP表缓存失效等问题。建议修改成网段路由的形式,并且设置ARP规则永久有效,避免因为Etcd等故障导致集群网络瘫痪。Flannel网络转发原理如图12-17所示。
另一部分是容器内业务的监控。通过集成公司开源的APM监控系统UAV (https://github.com/uavorg/uavstack),进行应用的性能监控。UAV的链路跟踪基于Java Agent字节码修改技术。修改Tomcat等其他核心处理类,达到截获请求的目的,从而获取每次请求的执行耗时及调用层次。如果用户部署应用勾选了使用UAV监控,系统在构建镜像时,将在容器启动前通过Initcontainer将UAV Agent植入镜像内,并修改启动参数。UAV应用监控功能模块图如图12-22所示。
关闭计算节点的Swap分区。如果Swap分区开启,在系统内存不足时,将会使用Swap分区,导致性能下降,出现服务不稳定的情况。
注意关注证书的有效期。在部署Kubernetes集群时,很多都是自签的证书。在不指定的情况下,openssl默认一年的有效期。更新证书需要非常谨慎,因为整个Kubernetes的API都是基于证书构建的,所有关联的服务都需要修改。
13.1 CNCF 13.1.1 简介 2015年,谷歌与Linux基金会及众多行业合作伙伴一起建立了一个云原生计算基金会(CNCF,Cloud Native Computing Foundation)。CNCF旨在创建并推动一个新的计算范式,这个范式的目的是增强现代分布式系统,使其能够扩展到数千个且具备故障自愈的多租户节点。
如果企业想对外提供Kubernetes支持,CNCF提供了一个针对企业的认证KCSP (Kubernetes Certified ServiceProvider)。需要满足以下条件:(1)三名以上工程师通过认证Kubernetes管理员(CKA)考试;(2)将Kubernetes以一定的商业模式提供给客户,包括驻场办公;(3)成为CNCF会员。
如何成为CNCF会员呢?这个相对简单,就是“充值”。CNCF官方明码标价,分为:●Silver Member(银牌会员);●Gold Member(金牌会员);●Platinum Member(铂金会员);●Academic/Nonprofit Member(学术非盈利会员);●End User Member(终端用户会员)。
当然,成为会员也有一定福利,主要是有CNCF大会门票,在CNCF和Kubernetes官网发博客,以及社区里面拥有更多的话语权。目前国内包括阿里云、华为云等多家公司获得KSCP认证。
截止2019年2月份,CNCF已经有四个项目顺利毕业,分别是容器管理系统Kubernetes、监控系统Prometheus、路由转发组件Envoy和域名解析系统CoreDNS。
还有一些正在孵化的项目,包括链路跟踪组件OpenTracing、日志采集组件Fluentd、远程方法调用gRPC、容器运行时rkt和容器网络CNI等,其中值得一提的是容器镜像仓库Harbor,它是在2018年加入CNCF孵化项目的,它是由VMware中国团队开发的。CNCF所有项目如图13-1所示。
13.2 云原生应用规范 云原生应用其实就是需要严格的分离架构(程序)和数据,包含三个核心概念:微服务、Dev0ps和容器化
在介绍微服务之前,我们先了解一下微服务出现的背景。传统的单体应用架构都是三层模式:表示层(用户可见的交互页面,如Web页面)、业务层(核心业务逻辑处理)和数据访问层(将应用数据保存到后端存储,如数据库、磁盘等)。然后将它们打包编译后放到一个Web容器(如Tomcat、Jetty)里面运行,如图13-3所示。这种单体架构在面对小规模、简单的业务场景应用时得心应手,易于开发、测试和部署。
随着业务越来越复杂,用户数(并发数)不断增多,可维护性和可扩展性越来越低。惯用的解决方案是前端通过负载均衡分流,后端分库分表,增加缓存等。这些调整可以在一定范围内增加并发,但系统仍然是单体,仍然存在很多问题。
(2)扩展性差,单体的应用始终无法避免的问题是数据库的性能瓶颈,并且在资源扩容的时候很难做到精准控制,比如将一个计算密集型的业务和一个I/O密集型的业务放到一个单体服务中,那么部署该服务的机器就必须同时满足这两点需求,这会造成资源浪费。
个微服务由一个小团队独立完成,这也更符合康威定律。比如可以将产品、合同、订单拆分成三个微服务。一个微服务就是一个SRP(Single Responsibility Principle,单一职责)的独立个体。根据业务边界来确定服务边界,避免与其他服务共用资源。
●独立部署 每一个服务都可以独立开发、构建、测试和部署。每一个服务只需要连接自己数据库,而不用担心与别的服务之间的耦合,部署更加简单。
●可复用可组合 在微服务的架构下,服务不再属于某一应用,而是可以为不同的应用提供相应的能力,这体现了微服务的最大价值,重用微服务避免了重新编写代码,大大降低了开发成本。在一个企业中,随着公共的微服务,如邮件、短信、OA等不断增多,每个新的项目只需要编写自己的业务逻辑,通过服务调用的方式接入各种外部系统。
微服务将应用拆分后势必导致服务个数的暴增,传统的“写死”服务调用地址的方式已经不适用了,必须有一套服务自动发现与注册机制。
微服务中有很多服务需要对外提供,每个服务都需要编写认证鉴权、流量控制、访问日志等,这不仅增加了代码量,维护性也极差。为此,微服务中还需要一个公共的API网关服务。网关服务主要提供了路由转发功能,将请求转发到对应的后端,不仅如此,还可以将一次对网关的服务调用转化成多个微服务调用,比如购物App的个人信息页面,不仅有历史订单、物流情况、红包卡券、收藏夹等多个功能模块,还可以通过请求一次网关服务,网关分布调用这些后端服务,并将结果合并到一起发送客户端。
除此之外,网关可以完成以下几个功能:(1)协议转化,将SOAP转化为Rest,将xml转化为json等;(2)认证鉴权,统一拦截请求,校验权限;(3)日志审计,记录每个请求的访问日志;(4)流量管控,控制请求的并发数,防止恶意攻击。
应用从单体到微服务,对底层框架的技术要求更高,分布式系统排查问题的难度也会加大,所以,应用是否需要微服务还得根据实际的生产场景判断,如果只是几个人维护的一个简单应用,是没有必要折腾微服务的,毕竟“适合自己的才是最好的”。
分布式系统中,每个组件都需要配置文件,众多配置文件分散到每台机器上面,维护难度很大。为了解决这个问题,Spring Cloud引入了配置中心Config Server。Config Server从本地文件目录或者Git(笔者更推荐)中读取配置文件,并通过HTTP接口提供配置文件读取。由于Config Server本身是无状态的,所以可以部署多套保障高可用性,Config Server部署如图13-9所示。
13.2.2 DevOps 随着软件发布迭代频次越来越高,传统“瀑布式”(开发—测试—发布)软件开发流程已经不能满足需求,即传统的开发人员只负责代码开发,而运维人员只负责运行环境的维护,并且研发通常关注功能开发,总是想尽快上线新业务,不断满足新的客户需求,而运维总是想使环境稳定,少出故障,“稳定压倒一切”。
开发人员不清楚代码在生产环境如何运行,而运维人员也不清楚代码是如何构建出来的。线上环境出了问题只能反馈研发,然后运维人员还得给研发人员讲解一下生产环境如何部署,沟通成本非常高,部门之间合作解决问题导致故障的响应时间较长。
2009年,DevOps应运而生。简单来说,就是更好地优化开发(DEV)、测试(QA)、运维(OPS)的流程,开发运维一体化。通过高度自动化的工具与流程使软件构建、测试、发布更加快捷、频繁和可靠。
DevOps本质上是一个开发人员干完所有的活,从代码开发到服务上线,DevOps最先都是一些硅谷的初创公司的大牛们为了节省成本,艺高人胆大,自己干完了所有事,后来发现这样办事的效率很高,然后各个互联网公司也都争相效仿,才慢慢普及的。
这种DevOps的开发方式带来了很多好处,首先,开发流程高度自动化,每次的代码修改都可以直接交付,缩短了软件交付周期,提高了开发效率。其次,持续的自动化回归测试,提高了产品的质量和稳定性,最后,共享的基础设施,提高了资源的利用率。
DevOps是一种方法论,或者是一种软件开发文化。它的具体实现方式和实现工具有很多
代码库:Gitlab、GitHub、gogs、svn、BitBucket; 自动构建:Ant、Maven、Gradle; CI/CD:Jenkins、Travis CI、Fabric、Gitlab CI、buildbot; 配置管理:puppet、chef、saltstack、ansible; 部署平台:Kubernetes、OpenShift、Mesos。
AWS更是DevOps的先驱,AWS工程师需要具备开发和运维的能力。
譬如容器内proc文件系统是没有隔离的,在容器内看到的都是宿主机proc信息,这给很多应用程序带来了困扰,JVM初始的堆大小为内存总量的1/4,如果容器被限制在2GB的内存上限,而宿主机通常都是200G+内存,很容易导致JVM的OOM。
将应用迁移到容器最主要的改造是将应用变成“无状态”,那么,什么是“状态”?状态指的是应用里面的数据状态,具体来说,就是应用的会话、用户数据、中间变量、文件等。“无状态”就是将应用的状态信息从应用中剥离出去,保存到对应的存储中间件中,如通过Redis保存会话和Token,通过MySQL保存关系型数据,通过对象存储保存图片
.2.4 云原生项目概览云原生技术借助于容器、服务网格(Service Mesh)、微服务、不可变基础设施及申明式API等技术,不断增强企业在公有云、私有云及混合云上构建并运行可伸缩应用的能力。
图13-12 CNCF推荐云原生项目的缩略图
编排管理(Orchestration&Management)主要包括了Kubernetes、Swarm等调度编排,CoreDNS、Etcd等服务发现组件,envoy、HAProxy服务调用代理,linkd、istio等service mesh。应用开发(App Definition Development)主要包括了MariaDB、MongoDB、cassandra数据库服务,spark、flink等流处理,helm等应用定义以及Jenkins。drone等CICD组件。
了微服务框架Spring Cloud,但这种微服务框架也存在一些不足。首先框架代码和业务代码的耦合,尽管这些框架已经进行了很好封装但无可避免地仍需要代码的植入,框架API的调用以及编译时的依赖;其次每种微服务框架只能对特定语言的支持,譬如Sping Cloud只能用在Java技术栈,并且传统应用迁移到微服务框架上面也需要很多的代码改造。微服务框架缺乏整个集群的链路跟踪、全局限流、流量调度等高级特性。
在2017年,一个新的微服务治理理念Service Mesh(服务网格)诞生了。Service Mesh本质上讲微服务的客户端负载均衡以sidercar的模式绑定业务容器,并且配合Service Mesh控制平面,控制流量的转发。
在数据平面主要是负责数据流量的转发而在控制平面主要负责生成并下发转发规则。数据平台由一系列代理网关组成,与传统的nginx之类的代理相比,这些数据平台代理网关最大的特点是可编程性,可以通过接口动态接收配置并生效。控制平面除了规则配置以外,还包括监控、限流、日志收集等。
技术的发展总是朝着简化开发、便于使用的方式演进的。在蛮荒计算机时代(20世纪六七十年代),任何一个模块都需要开发人员自己完成的,如何解决多核并发、如何解决数据存储等一系列复杂问题需要开发人员考虑,为此诞生了操作系统。开发人员没有必要面对丑陋的硬件接口。后来大家觉得网络很复杂,如何建立连接,如何高效传输数据及如何断开连接等问题需要开发人员控制,因此诞生了协议栈,将复杂的网络连接转化为简单的接口调用。
13.3.1 Envoy之所以先介绍Envoy是因为Istio本身依赖于Envoy的。Envoy是一个由C++开发的高性能代理,用于调解服务网格中服务的入站和出站流量。2017年9月14日,Envoy加入CNCF,成为CNCF的第二个Service Mesh项目。2018年11月28日,CNCF宣布Envoy毕业,成为继Kubernetes和Prometheus后,第三个孵化成熟的CNCF项目。目前Envoy已经成为数据转发平面的标准。
Envoy的功能非常完善,包括了服务动态发现、负载均衡、TLS终止、HTTP2&gRPC代理、熔断器、健康检查、基于百分比流量拆分的灰度发布、故障注入及非常丰富的监控指标。如图13-14所示,Envoy通过配置的路由规则将99%!的(MISSING)流量打到version:v1.5的后端服务,1%!的(MISSING)流量打到version:v2.0-alpha中。
Envoy定义了几个核心概念。Cluster(集群):这里的集群指的是应用集群,代表一组运行相同业务的主机。每个业务可以有多个集群代表业务的不同版本。
Route(路由):设置Envoy流量转发规则。Envoy根据路由规则将流量转发到不同的集群。
如果是在有控制器的环境中,上面的这些配置便可以通过xDS方式动态更新。为了便于集成,Envoy社区提供了实现xDS数据平台接口的Go语言项目envoyproxy/go-control-plane,Istio也是使用这个SDK和Envoy交互。
Envoy是作为一个sidecar的方式和业务绑定到一起运行的,那么如何将流量导入到容器里面呢?Istio的实现是通过iptables将出去和进入的流量都导入Envoy中,关键规则如下:
除了Istio使用Envoy以外,还有包括AWS的App Mesh、微软的Service Frabric Mesh、腾讯的Tecent Service Mesh、阿里的Dubbo Mesh等网格平台都采用Envoy作为数据转发,Envoy目前已经成为了Service Mesh在数据平面上面的标准。
Istio功能非常强大,不仅支持丰富的路由规则、重试、故障转移和故障注入,还可以对流量行为进行细粒度控制。支持访问控制、速率限制和配额、流量监控、链路追踪等。配合身份的验证和授权,可以在集群中实现安全的服务间通信。
Istio流量分发中定义了两个重要概念:DestinationRule(目标规则)和VirtualService (虚拟服务)。
一个好汉三个帮,Istio生态中也借助了很多其他的开源组件。比如,(1)Jaeger是Uber开源的分布式调用链跟踪组件,它不仅支持多语言,如Java、Go、Scala等,而且无须埋点,对业务没有侵入。Jaeger在Istio中非常重要,在ServiceMesh环境中服务的调用错综复杂,需要一套链路追踪系统获取服务组件调用关系,并协助排查和诊断故障。(2)Prometheus在前面Kubernetes容器监控时候已经介绍过,在Istio环境中,除了使用Prometheus监控容器指标外,还从Mixer获取数据平面转发指标,如QPS等。
可以说Istio是一套升级版本的Spring Cloud,增加了多语言和非侵入等优点。