通过前边多篇文章的学习,我们已经可以将应用程序部署到Kubernetes平台上,并且也能给容器挂载外部的存储资源,我们已经基本实现了应用的容器化部署。容器化部署其实是云原生设计理念的体现,在云原生模式下应用架构和传统应用的架构有很大的不同。虽然笔者在前边的文章中多次强调过云原生和云计算:云计算主要是解决我们的应用在哪里部署运行,而云原生解决的是我们能如何设计应用程序,来充分利用云平台提供的扩展性,可靠性,灵活性,安全性等红利。
读者可能会问,内容都能看懂,但是云原生应用到底长啥样呢?为了说清楚这个事情,我们可以从深度学习的角度来分析一下。现在只要和数据相关的概念都很热,大数据分析,深度学习,人工智能等,本质上说来说去,这几个概念想表达的是从无序到有序,从简单到复杂,从数据到智能的一个过程。人工智能简单来说就是通过获取和加工信息,从而获取智能。而深度学习的过程中,如果想让计算机能够认识到某个物体,我们需要提取物体的特征,而计算机要做的就是对下一个物体,基于输入的数据来计算特征值是否在区间内,比如判定图片上的动物是不是猫。
对于云原生的概念其实也一样,我们来看看云原生模式下,应用程序具体有哪些特征。虽然说我们也可以定义一套自己的云原生应该具备的特征列表,还是咱这次就不重复发明轮子了,直接借用Heroku公司抽象的云原生应用15因素,来介绍云原生应用的架构模式。
注:The Twelve-Factor methodology来自Heroku公司创始人Adam Wiggings,这位老哥基于Heroku多年的Pass和SaSS运营和运维经验,总结抽象出了云原生应用应该具备的12个特征,或者说云原生应用设计的12原则,这些原则都是经过实践检验,对于我们设计系统来说,有很好的借鉴意义。12因素不仅仅包含云原生应用架构设计的原则,还包含云原生应用构建的管理规范,是设计和开发云原生应用最好的最佳实践。
接下来,我们就先来认识一下这些云原生应用设计的规范,试图回答我们在开篇提到的问题,云原生应用到底长啥样?希望读者能认真阅读一下这里的每一条原则,会对我们无论是从系统分析还是架构设计,都产生巨大的影响:
- 源代码仓库原则。任何应用程序都应该通过单独的代码仓库来进行源代码管理,对于需要在多个模块共享的代码功能,要么通过单独的common库来统一管理,要么将共享的代码能力封装成公共服务,所有依赖于这些公共代码的服务可以直接调用共享服务。应用程序能够在不修改代码的情况下,迅速部署到多个环境中。
- API原则。云原生应用程序,或者说微服务架构下的应用程序大多由粒度适中的多个服务组成,服务之间通过定义良好的API通信。
- 依赖管理原则。所有的应用程序都应该通过显示的声明来定义自己的外部依赖,比如在SpringBoot应用中,我们通过Maven仓库来管理依赖,因此应用无论是通过哪种方式构建,只要配置文件中有完整的依赖声明,都可以成功打包。
- 软件的生命周期管理原则。云原生应用有定义明确的生命周期管理,比如设计开发,构建,版本发布和运行状态。
- 配置管理原则。配置管理原则要求我们对配置和代码的修改做到完全隔离,配置的修改不应该涉及源代码的修改。笔者建议大家在项目中使用不同的代码仓库来管理源代码和配置。举个例子,笔者之前负责的项目中,库存中心有这么几个repo:stock-center,stock-center-configuration,stock-center-job,来通过不同的仓库管理源代码,配置和定时任务。
- 日志统一处理原则。云原生架构下的应用程序不应该重新发明轮子,自己处理日志的收集和分析。应用程序把日志输出的标准输出,外部的日志统一收集中间件对日志进行收集和分析,提供给运维可开发人员使用。因此日志处理不再是云原生应用程序的职责,而应该是如阿里云上诸如日志服务这样的云原生中间件负责,提供日志的统一收集接入,分析处理,查询,告警等功能。
- 服务实例可快速重建原则。对于传统应用程序,我们需要耗费大量的精力和资源来保证应用程序平稳运行,比如监控,日志,告警,人肉盯着等,当系统出现故障的时候,第一时间响应故障,降低对业务的影响。而对于部署在云平台上的云原生应用来说,本质上我们不会对运行在POD中的每个应用实例状态,因为应用的运行实例可以认为都是“临时的",我们随时可以在业务流量增加创建出更多的应用实例,来支撑峰值流量。当应用程序出现错误的时候,我们的运维很简单,结束有出故障的应用实例,然后重新启动一个新的应用实例。
- 后台服务可替换原则。云原生应用的后台服务指依赖的外部资源,包括但不限于数据库,MQ,缓存,邮件服务器,FTP服务器,三方HTTP服务等。这些后台服务可随时进行替换,并且替换后不需要修改源代码应用就能继续运行。举个例子,为了节省成本,在软件生命周期里不同阶段,可以使用不同的数据库。比如开发阶段用H5内存数据库;在测试和生产环境使用关系型数据库。这里的核心是,当我们把应用从开发推到测试甚至生产环境的时候,不需要修改任何代码来适配不同的数据库类型,数据库资源可以通过资源绑定来实现,具体的实例绑定很简单,我们只需要提供不同的URL,用户名和密码配置就行。
- 环境对等原则。环境对等性要求我们必须保持不同目的的运行环境一致。造成环境不一致的主要因素有两个:1,时间因素造成的环境不一致,不同环境代码包有版本差异,本质是因为代码提交的时间点不同造成,我们要尽最大的努力力让不同环境的应用程序版本保持一致,或者兼容;2,组织结构因素造成的环境差异。开发和运维在大部分企业是两个团队,开发人员完成代码编写和自测后,就扔给”墙“的另外一边;由于不同团队一般有不同的任务和优先级,因此运维团队并不一定会马上部署到对应的环境,因此会造成差异。因此笔者建议大家在项目上尽量实践Devops理念,开发运维一家人,消除这种因为组织结构造成的应用程序运行环境差异。
- 服务端口绑定原则。云原生应用对外提供服务的端口应该通过在运行期间绑定来实现。在云原生应用的部署架构下,我们通过网关将应用提供的服务能力暴露出去,因此外部服务请求需要映射为内部的服务实例,才能访问到。传统的应用程序需要部署到Web容器中才能运行起来,而云原生应用一版不需要外部的Web容器依赖。比如说SpringBoot在打包的时候,直接内嵌了Tomcat服务器,在任何安装了Java的机器上都可以直接启动运行。
- 无状态原则,让应用具备可扩展性是很多企业愿意按照云原生模式构建软件的主要动机。可扩展性的实现需要应用程序无状态,并且遵守Share Nothing的架构原则。Share Nothing架构原则要求同一个应用程序不同实例之间不共享任何状态信息。而判断应用是否遵守这个原则很简单,当应用因为故障被重启后,判断是否会丢数据即可。当然应用程序完全没有状态,那就不具备价值了,我们只是将应用程序的状态管理,转移到后台数据库服务而已。
- 并发处理原则。无状态对于可扩展性来说是必要条件,因为我们云原生应用一般需要支持高并发的访问流量,因此大多都会使用并发处理模型来提升应用程序的吞吐量。
- 可观测性原则。可观测要求云原生应用程序能够通过给外部透传出来的数据指标,日志和链路追踪数据,确定系统当前运行的健康状况。分布式系统很复杂,特别是部署在云基础设施上的分布式系统,更加复杂。这种动态的应用部署模式,让运维团队苦不堪言。可观测性是降低分布式系统复杂性的唯一可行的方法。可观测性要求应用提供完整和准确的监控和追踪数据,运维人员可以从透传出的指标准确了解和评估系统的当前运行状态,在发生异常的时候,能够快速介入,降低对业务造成的影响。我们对待可观测的态度就应该如同我们作为总设计师,发射火星探测器祝融号一样,你需要祝融号探测器提供什么样的监控数据,来确保航空器运行健康,整个探索过程顺利?
- 认证和授权原则,在云原生模式下,安全的经典AA原则被扩充为AAA原则,包括:认证,授权和审计。安全无小事,特别是涉及到公司核心业务资产数据的,个人敏感信息的系统,一次系统被攻破,造成的损失无可估量,可能直接造成公司倒闭。认证和授权解决的问题不太一样,认证是解决你是谁的问题,而授权则回答你能做什么的问题。审计的目的是记录谁在什么时候做了什么事情,以方便回溯。这三个原则并不是割裂开的,需要在系统设计的时候,通盘考虑。当然并不是说我们实现AAA后系统就100%安全了.安全是个很宽广的概念,我们还需要考虑数据传输安全,数据存储安全,服务器安全,防范DDOS攻击(影响可用性)等。
以上就是经典的云原生12范式,这些都是指导性的原则,大家需要结合自己项目的实际情况来采用和落地实施。特别是基于Kubernetes平台容器化部署的应用程序,和传统的应用程序类似,都需要进行配置管理。比如启动时候需要配置的参数,环境变量,以及后台服务的URL,地址等信息。
我们在前边的文章中介绍过,我们可以在构建容器镜像的时候,需要制定容器启动时执行的命名,比如通过ENTRYPOINT语句。我们也可以在Dockerfile中使用ENV命令来设置环境变量,如果应用程序需要从配置中读取配置项信息(对于SpringBOOT应用来说,这是推荐做法),如果配置信息放在properties或者yml这样的文件中,我们可以通过COPY命令将配置文件拷贝到镜像的文件系统中。
接下来我们来通过一个实际的例子,来重温一下如何配置应用程序的启动。为了文章读起来不那么单调,我们正式放弃继续使用简单粗暴的k8ssample这个SpringCloud应用程序了,取而代之的是新建的node js应用程序。读者可以使用docker pull qigaopan/yunpan-node:v1.0来拉取镜像。node应用的Dockerfile配置如下图所示:
如上图所示,有我们使用CMD指令,因此我们可以在启动的时候,通过--listen-port命令行参数在应用启动的时候进行修改。另外,我们的NODEJS应用代码中,会从环境变量读取信息并返回给HTTP请求。坦白讲将参数写到镜像的Dockerfile中和硬编码到代码中没有本质的区别,因为如果我们要修改配置参数,必须重新build镜像。读者需要注意的是,务必不要将敏感信息放到容器镜像中,因为有访问权限的用户都可以访问到这些如用户密码,数据库密码等等敏感信息。
那么这些配置类的信息应该放到哪里合适呢?读者如果看过前边的一系列文章,一定会记得我们强调过初始化容器的职责中,就包括加载敏感信息来初始应用的执行环境,但是其实我们有更好的做法。在介绍详细信息之前,我们先来看看,如何在不重新构建容器镜像的情况下,更新应用运行需要的配置信息。
如图1.1所示,我们在构建容器镜像的时候,可以使用ENTRYPOINT和CMD命令来执行应用启动时需要的参数。当容器启动的时候,会基于配置的命令和参数来启动进程。Kubernetes提供了类似的配置选项:command和args,我们可以在POD的YAML文件中进行设置,当POD中的容器开始运行的时候,POD中设置的command和args字段会组合起来,作为容器的启动进程和输入参数,如下图所示:
如上图所示,我们在POD的YAML文件中,可以使用command和args字段来重写容器镜像中设置的启动命令和参数,Kubernetes提供的命令和Dockerfile提供的指令对比映射如下:
- ENTRYPOINT(Dockerfile) vs command (Kubernetes),指定容器启动时要运行的命令。
- CMD(Dockerfile)vs args (Kubernetes),如果命令启动的时候,需要指定参数,通过CMD或者args来指定。
接下来我们通过两个实际的例子来看看如何在POD的YAML文件中使用command和args。假设我们的nodejs应用程序需要把CPU profiling和heap profiling功能打开,那么我们就可以通过给node命令传入选项--cpu-prof和--heap-prof来实现。
在不修改Dockerfile的情况下,我们可以通过在POD的YAML文件定义中,设置command命令来实现,如下图所示:
如上图所示,当我们在Kubernetes集群中部署和启动这个应用的时候,命令node --cpu-prof --heap-prof app.js会取代容器镜像在打包时从Dockerfile中读取的命令和参数来执行。
最后,我们在POD的YAML文件也可以覆盖命令执行需要输入的参数,如下图所示:
如上图所示,镜像中设置的启用启动监听端口号8080会被POD的YAML文件中的参数。笔者强烈建议读者在自己的集群上验证一下如上展示的POD定义,体会一下Kuberntes提供的这种灵活性。
好了,今天这篇文章的内容就这么多了,下一篇我们继续介绍通过其他方式给应用提供配置信息,敬请期待!
-