如何设计一个云原生应用?应用的架构应该是什么样的?有哪些特性、模式和最佳实践可以参考和遵循?哪些基础架构和运营问题是最为重要的?
十二原则
是用来衡量一个服务是否适合迁移到云上的原则,换言之,相比于不符合这些特征的传统应用服务,具有这些特征的应用更合适云化。
为了理解这些原则,我们需要搞清楚服务是怎样在运行平台上运行的。
以标准的K8s(Kubernetes)
平台为例,一个典型的容器化后端服务,从开发到上线需要经历一系列复杂的步骤,初次上线、后续迭代的流程已经比较复杂,如果单靠手工处理,单体系统还勉强可以应付,毕竟单体系统即使变成“大泥球”,也大多还处于人力可控的范围内。但随着系统复杂度的进一步提升,整个系统演化成微服务系统之后,随之而来的技术挑战是显而易见的。
为提高开发和运营效率、减少错误,在设计和开发阶段就需要考虑借助云平台以及整个生态的能力,从一开始就要做一个适合在云上运行的服务。十二原则给了我们一把衡量是否适合上云的标尺,如果不遵循这些原则,不借助于云平台提供的能力、不剥离业务无关的部分,随着服务规模不断增大、业务复杂度进一步提升,就容易引发各种问题。在理解十二原则存在的价值后,我们分析一下每个原则的深刻含义。
云原生应用必须有单一的代码库,并在版本管理系统中进行追踪,且能够支持多个环境中进行不同的部署。
单一代码库是一个源代码仓库或一组存储库,它们共享一个公共源,用于生成任意数量的不可变发行版。这个唯一的代码库有助于支持开发团队之间的协作,并有助于实现应用程序的正确版本控制。例如Git是目前最常用的代码版本控制工具之一。
所有与应用程序相关的资产,包括源代码、配置脚本和配置设置,都存储在一个可供开发人员、测试人员和系统管理人员访问的源代码仓库中。所有自动化脚本都可以访问源代码仓库,这是持续集成/持续交付(Continuous Integration/Continuous Delivery, CI/CD)过程的一部分,这些过程是企业软件开发生命周期的一部分。
项目使用的依赖项以及版本必须显式声明并与源代码隔离。
对于像Node. js的package、Java的Jar包、.NET的NuGet包这些外部构件,在开发、测试和生产运行时,都应该引用自依赖关系清单,需要避免将构件和源代码一起存储在源代码仓库中。
云原生应用程序永远不能存在隐式依赖于系统级别的包,因此该要素鼓励显式声明和隔离应用程序的依赖关系。这有助于提高开发和生产环境之间的一致性,简化应用程序新手开发人员的设置,并支持云平台之间的可移植性。
配置信息以环境变量或独立配置文件中定义的方式配置,并注入各种运行环境中。
在云原生应用中存在三种不同类型的实体:
● 代码: 包括源代码和相关资源文件。
● 配置: 与部署环境相关的配置信息,通常以XML、YAML、JSON等文件的形式出现,包含应用自身配置属性、第三方服务的连接方式等信息。
● 凭据: 密码、密钥等敏感信息。
代码与配置的区别在于:代码不会随部署环境而变化,配置则会随着部署环境变化而变化。因此在云原生应用的实践中,应该尽可能把配置从应用中拆离出来,通过外部化进行管理,构建出来的二进制程序中不包含任何配置信息,实际的配置值在部署时根据环境来确定。
需要注意的是,在源代码仓库中不应该显式的出现凭据信息。
将应用程序与其所依赖的支撑服务进行解耦。
应用所依赖的外部服务:例如数据库、消息队列、邮件服务器、缓存系统、文件存储系统等都是应用的支撑服务,在云原生应用中将这些支撑服务统一称作资源。
资源的实现需要支持动态附加与动态分离,通过云环境低耦合的方式与应用相结合,配合前面提到的配置原则,实现资源可以动态替换为不同的实例,替换后不会对应用程序产生任何影响,不需要重新编译、部署应用,这让云原生应用程序拥有强大的灵活性与弹性。
严格对应用程序的构建、发布、运行阶段进行分离。
将应用程序的部署过程分解为以下三个可复制的阶段,可以在任何时候进行实例化。
● 构建阶段: 是从源代码管理系统检出代码并构建/编译成存储在构件仓库中的构件的阶段。
● 发布阶段: 在编译代码之后应用配置设置。
● 运行阶段: 使用Ansible之类的工具通过脚本提供一个执行运行环境,应用程序及其依赖关系被部署到新配置的运行环境中。
构建、发布和运行的关键是该过程的瞬时性,如果流水线上的任何东西被破坏,所有的构件和环境都可以使用存储在源代码仓库中的资产从零再造。
云原生应用程序的每个部署阶段都是独立的,并且是单独发生的。一旦运行,云运行时将负责其维护、健康和动态扩展。
应用程序的所有进程与组件都必须是无状态且独立的。
应用程序将作为无状态进程的集合运行。无状态的云原生进程使扩展更加容易。当流程是无状态的时候,可以动态增加和删除实例,以应对特定时间点的特定负载负担。由于每个进程都是独立运行的,无状态可以防止意想不到的副作用。
应用程序是完全独立的,它应该不依赖于任何特定网络服务器就可以创建一个面向网络的服务。应用通过端口绑定来提供服务,并监听发送至该端口的请求。
例如,基于.NET的Web应用不应该依赖于IIS,它应该可以独立提供服务。云原生应用在运行时并不负责管理实际的端口绑定,而是由云平台统一管理。例如,ASP.NET Core的Web应用通常使用端口5001,当应用运行在云平台上时,这个端口只是容器内的端口,并不是外部用户或服务访问时的实际端口。云平台对网络进行统一管理,负责分配实际的服务端口,云平台同时提供了相应的机制来发现访问服务的实际地址和端口。
进程是“一等公民”,根据进程的用途来组织进程,然后将其拆分,以便根据需要扩展和回收这些进程。
支持并发意味着应用程序的不同部分可以扩展以满足当前的性能需求,否则当不支持并发时,架构除了垂直扩展整个应用程序之外将别无选择。支持云原生应用程序弹性扩展的理想方法是水平扩展。与其让单个庞大的进程变得更大,不如创建多个进程,并将应用程序的负载拆分后分配给这些进程。
应用程序是一次性的,即:随起随停。
应用进程应该根据需要不断的创建、停止,因此云原生应用程序一定要能够快速启动和正常退出,如果不能实现将会导致应用无法快速扩展、部署、发布、恢复。
在当今高网络流量的网络世界中,如果一个应用程序启动需要几分钟才能进入稳定服务状态,这一段时间内将会出现成百上千的请求被拒绝的情况。
同样,如果不能快速且正确的退出应用,也会带来资源无法回收、数据产生破坏等情况。
应用程序旨在通过缩小开发与生产环境之间的差距来实现持续部署。
该原则确保了生产部署过程与开发部署过程的一致性,这样就可以确保所有潜在的错误/故障都可以在开发和测试中识别出来,而不是在应用程序投入生产后才暴露出来。
像Docker这样的工具可以帮助实现这种开发/测试/生产环境的等价性,它为运行代码创建和使用同样的镜像,提供了绝对统一的环境,它还有助于确保在每个环境中使用相同的后端服务。
应用程序从不关心日志输出流的路由与存储。
将日志数据发送到流中,各种感兴趣的消费者可以访问这个日志流,即使某个应用程序退出运行,日志数据在之后仍然存在。
例如,某个日志的消费者可能只对Error数据感兴趣,而另一个消费者可能对Request/Response数据感兴趣,另一个消费者可能对存储用于事件归档的所有日志数据感兴趣。
云原生应用程序的日志聚合、处理和存储是云提供商或其他工具套件(例如,ELK技术栈、Splunk等)的职责,这些工具套件与正在使用的云平台一起运行。通过简化应用程序在日志聚合和分析中的部分,可以简化应用程序的代码库,并更多地关注业务逻辑。
将管理任务作为一次性流程运行。
云原生应用运行中可能会需要执行一些管理任务,比如生成报表或者执行一次性的数据查询等,这些任务通常并不属于业务流程的一部分,更多的是为了管理和运维的需要。这些任务在执行中会使用到云原生应用所依赖的支撑服务,对于这些任务,应该创建独立的应用,并在同样的云平台上运行。对于定期执行的任务,可以充分利用云平台的支持,比如,Kubernetes提供了对定时任务(CronJob)的支持。
以上是对云原生应用设计的十二原则的详述,十二原则的总结如下。
1.单一代码: 每个微服务都有单个代码库,存储在其自身的存储库中。它通过版本控制进行跟踪,可以部署到多个环境(QA、暂存、生产)。
2.依赖管理: 每个微服务都隔离并打包其自身的依赖项,以在不影响整个系统的情况下进行更改。
3.配置: 配置信息通过代码之外的配置管理工具移出微服务之外,并实现外部化。在应用了正确配置的情况下,相同部署可以在环境间传播。
4.支撑服务: 辅助资源(数据存储、缓存、消息中间件等)应通过可寻址的URL进行公开。这样做可使资源与应用程序分离,使其可以动态替换。
5.构建、发布、运行: 每个版本都必须在构建、发布和运行阶段执行严格的分离。各自都应使用唯一ID进行标记,并支持回滚功能。新式的持续集成和持续部署CI/CD系统有助于实现此原则。
6.进程: 每个微服务应在其自身的进程中执行,与正在运行的服务隔离。将所需状态外部化到支持服务,如分布式缓存或数据存储。
7.端口绑定: 每个微服务都应是独立的,其接口和功能在自己的端口上公开。这样做可与微服务隔离。
8.并发: 当需要增加服务能力的时候,通过扩展多个相同进程(副本)横向扩展服务,而不是在功能强大的计算机上纵向扩展单个大型实例。将应用程序开发为并发应用程序,从而顺滑地在云环境中横向扩展。
9.易回收: 服务实例应是易回收的。支持快速启动以增加可伸缩性机会,以及支持正常关闭以使系统保持正确状态。Docker容器以及业务流程协调程序本质上满足此要求。
10.环境对等: 使整个应用程序生命周期中的各个环境尽可能相似,避免使用成本高昂的方式。在这里,通过促进使用相同的执行环境,容器技术可以提供重要帮助。
11.日志流: 将微服务生成的日志视为事件流。使用事件聚合器处理它们。将日志数据传播到数据挖掘/日志管理工具(如Azure Monitor或Splunk)并最终长期存档。
12.管理进程: 以一次性进程形式运行管理性/管理任务,例如数据清理或计算分析。使用独立工具从生产环境调用这些任务,但独立于应用程序。