云原生技术实践-关键要素和原则
今天谈下云原生技术实践的一些关键原则和要素。对于云原生实践,在很早就有云原生12要素,本文则结合云原生12要素的一些内容,重新思考和整理企业上云或在进行云原生技术实践的时候的一些关键原则。
因此本文不会去详细描述云原生12要素的内容,对于该内容仅仅做一个简单的总体说明,具体的一些思考总结在我自己参考12要素并进行扩展后的原则中体现。同时里面有些原则内容也是对我前面云原生和微服务架构实践,DevOps持续集成和交付关键点的进一步总结思考。
云原生12要素概述
对于云原生12要素The Twelve-Factor App是Heroku创始人 Adam Wiggins 在2012年提出的。相比于不符合这些特征的传统应用服务,具有这些特征的应用更合适云化。而实际上云原生概念的提出是在13年,因此当时提出这12个要素的时候更多的是面向IT应用如何能够快速的向公有云环境交付为目标提出。
这里面的12个要素具体为:
Codebase:基线代码
Dependencies:依赖
Configuration:配置分离
Backing services:后端服务分离
Build, release, run:构建、发布、运行
Processes:无状态的服务进程
Port binding:端口绑定
Concurrency:大并发能力
Disposability:易处置
Dev/prod parity:环境对等
Log:日志作为事件流
Admin processes:管理进程
从这个内容也可以看到,整体属于技术要素的范畴,很多都是我们在软件开发,持续集成,或者想公有云交付和部署中实际会遇到的问题点。
举个简单的例子,你刚开始设计和开发了一个IT系统,但是最终向公有云交付或部署的时候才发现很多东西无法适配,或者很多原来设计的内容都要修改或调整后才能够部署下去,那么整体就不符合最基本的云原生要素和原则。
更加详细的12要素描述,请参考:
https://12factor.net/
从以上的内容也可以看到,大部分的内容都属于持续集成和持续交付中容易出的问题点,因为当时本身也还没提出类似微服务,容器云,服务网格等概念。因此本文重点会参考上述内容,并结合我前面云原生和微服务架构实践相关文章,对云原生技术实践关键要素做进一步总结。
一次构建,多处运行
这个实际是我每次谈DevOps和持续集成的时候都会谈到的概念,即一次构建,多处运行。即代码通过编译构建后形成完整的部署包,这个呈现的是二进制资源文件。
这个最终的部署包应该配合持续集成的过程在开发环境,测试环境,生产环境多处运行。但是每个环境都存在不一样的环境变量和配置信息。那么这些信息应该体现在操作系统环境变量或独立的配置文件中,而不能打包到具体的二进制部署包中。
在没有容器化时候我们交付的是二进制部署包+配置文件。
在容器化后部署包已经通过打包步骤形成了完整的容器镜像,那么在进行环境迁移时候就涉及到对镜像内的配置文件信息进行修改。
因此更好的做法是配置文件信息从镜像中独立出来,将配置文件信息作为操作系统的环境变量信息。其次就是形成公共的配置中心提供服务能力,应用从配置中心获取全局配置信息。
配置和分支策略
在前面文章就谈到,在云原生技术实践中,配置和分支管理策略会和原来有比较大的变化,没有开发分支,测试分支,生产分支点的概念。
即只要一套完整的正式基线代码,然后两个独立的分支,一个开发分支,一个Bug分支。开发分支是为了确保进行每日构建和持续集成需要,而Bug分支是为了生成环境上线版本的Bug修复需要。Master分支应该根据版本发布情况做好相关的基线管理工作。
从资源依赖到服务依赖
这个实际也是在谈云原生的时候,经常会谈到的一个内容点。不管是云计算本身的发展演进还是云原生的技术发展趋势,从IaaS服务到PaaS服务,都是从对底层物理资源或逻辑资源的依赖,不断的进行抽象,转变为对服务能力的依赖。
在云原生架构实践中,应用的开发部署涉及到前端的应用程序和部署包,也涉及到后端的资源。典型的后端资源就是数据库。
当我们在观察一个应用的时候你会发现,对于后端资源往往是一种强依赖关系,即不是使用的数据库服务能力,而仍然是使用的数据库资源,典型的就是我们自己购买虚拟机,然后按照数据库。而不是直接订购的数据库即服务能力。
数据库,消息中间件,缓存,文件等后端资源,本身也是可以进行抽象,应该考虑将对资源的依赖转变为对服务能力的依赖。
我们需要的一个典型转变就是你对类似数据库等后端操作都是通过服务或API进行的,而不是直接连接到后端,通过命令行等工具进行的随意操作。
也就是说我们需要在前端应用和后端资源之间通过服务化方式解耦。
这个实际和12要素里面提到的后端服务分离思路差不多。
前端应用和后端资源之间应该是一种松耦合的关系,他们之间通过一个标准连接或Http地址进行交互和协同。当后端资源出现问题的时候,可以进行快速的切换操作。当从本地私有环境朝云端切换的时候,你也可以快速的基于统一标准进行地址或配置切换。
后端资源作为服务
即使你是自己管理的后端资源,我们也建议尽量减少直接对底层后端资源的操作,而是通过标准的服务接口进行后端资源操作。要明白当迁移到云环境后,很多底层管理端的操作能力由于安全性要求往往是关闭的,你必须通过接口服务来管理后端资源,而不是通过后台命令。
如果一开始无法选择公有云技术服务,那么在后端资源选择上也应该为服务化扩展而设计。
在进行应用开发的时候,建议选择主流的后端资源能力,方便后续和公有云环境的迁移和适配。比如我们在选择消息中间件的时候,应该优选主流的各大公有云都提供共性能力支撑的产品服务,这样即使前期我们进行的私有搭建和部署,在后期也可以快速的迁移和切换到公有云的技术服务。
单体应用到微服务
对于传统应用的开发,应该从单体应用转变到微服务。对于具体为何要采用微服务架构,或者传统单体应用为何要转变为微服务架构,可能不同的开发团队有不同的原因。
常见的原因包括了原有的单体应用太庞大耦合紧密,难以维护,难以进行变更。或者是传统架构很难进行扩展,性能无法满足。当然也有的技术团队仅仅是从技术发展趋势角度来采用微服务进行新应用的开发和交付。
从云原生的角度来说,为何要采用微服务,简单来说可以总结为:
微服务化后,单个微服务粒度更小,更加容易快速集成和交付
微服务更加容易实现资源层的动态弹性扩展
微服务方便团队拆分,更加敏捷
微服务架构更加易于系统后续变更和运维
即微服务架构开发的应用更加容易向公有云环境持续集成和交付,也方便公有云环境在后续对资源进行动态弹性扩展调度。特别是微服务拆分后,单个微服务颗粒度更小,更加容易托管到轻量的容器中运行和管理。
在IT应用设计和开发实施微服务架构后,我们还可以看到以下关键原则和要素。
基于业务分域和上下文边界划分
对于微服务的划分,应该基于业务驱动而非技术驱动,是业务本身分域并松耦合,其次才是技术实现上能够松耦合。因此微服务的划分应该业务驱动进行,可以是基于传统企业架构业务流程梳理分析到CRUD分析再进行聚合,也可以是基于领域建模思路基于事件风暴思路进行业务分域和上下文边界的划分。
不论是哪种方法,企业本身的业务组织单位或中心,企业核心价值链流程的大阶段都应该是微服务划分重要的边界识别点。微服务如果拆分不好,比如拆分的粒度太细导致两个微服务之间大量接口交互而紧耦合,这样反而导致了后续微服务持续集成和交互的难度。
因此微服务拆分应该作为微服务架构设计中的一个关键点,需要熟悉业务的架构师来主导完成拆分,技术类架构师辅助进行拆分后的模拟验证工作。
面向API接口进行设计,开发和集成
在微服务开发过程中,整个微服务划分和微服务间的接口设计仍然需要保持高度的架构完整性和概念一致性。即首先通过架构人员进行微服务拆分,关键接口设计,其次才是进行各个微服务模块的开发,在开发完成后进行集成工作。
如上图,大家遵循同样的接口契约,那么后端开发,前端开发和测试人员可以并行开始各自的工作。对于前端优先进行接口开发和实现,前端则通过接口契约产生Mock模拟,通过接口模拟实现来进行前端功能的开发。在前后端开发过程中,测试人员也可以根据接口定义进行测试设计工作,同时进行相关的测试脚本设计或录制工作。
接口开发完成后,前端和后端首先各自进行单元测试,在单元测试完成后进行前后端的集成测试和验证。同时测试人员可以启动相应的接口自动化测试工作。
横向分层和前后端分离
前后端分离虽然没有出现在标准的微服务架构定义中,但是却应该作为微服务设计开发的一个最佳实践。前后端分离实际和SOA思想里面的服务分层思路相对一致,至少SOA跨系统间的服务分层,服务组织思想进入到单个微服务内部的功能实现机制上。
前后端分离实际上当前是包括了技术和团队组织两个方面,在技术上前端和后端完全拆分,都可以独立编译打包,在团队上形成前端和后端角色,不再是一个人前后贯通实现。
为何前后端分离设计如此重要,总结如下:
更加容易贯彻SOA服务重用和领域建模思想,后端提供能力,前端组装能力
对需求变化更加敏捷和优化,后端相对稳定,前端灵活调整
除了这两点,可以看到前后端分离方便进行可复用的领域服务和API接口积累,这个能力可以作为企业核心的中台服务层能力。
当在谈Serverless无服务器化架构的时候,觉得不可能依赖于公有云的技术服务能力就能完成一个业务场景和流程,更多的还需要依赖企业长久积累形成的中台共享业务服务能力。这个服务能力才是后续能否Serverless化的前提。
区分微服务和应用边界
我们理想的目标是逐步没有应用的概念,完全是一个个独立的微服务。但是从企业信息化建设的角度,只要组织业务域的边界在,那么传统应用的边界仍然在。
比如我们常见的企业有CRM和SRM两个业务系统,两个传统的单体业务系统都可以采用微服务架构和微服务化开发,但是两个传统应用的边界仍然还在,最终仍然交付的是CRM应用和SRM应用。同时一般情况下,两个应用本身也是属于不同的开发团队,不同的业务团队在负责管理和使用。
因此在这种情况下必须区分微服务边界和应用边界的概念
简单来讲应用和应用之间应该有明确的边界划分,每个应用内部都可以有自己的注册中心和微服务网关,在应用内部来说集成和交互的粒度是微服务的粒度,而应用和应用之间的粒度则应该是控制到细粒度的API接口的粒度。
举例如上图来说,CRM中的客户微服务实际上暴露了20个接口,在进行微服务注册后,CRM应用内的模块可以不受约束的使用这20个接口。但是客户微服务只注册和接入了3个API接口到外部的API网关,那么对于SRM应用来说只能够消费和使用这3个授权后的接口。
面向开发和治理分离而设计
在微服务架构设计开发下,由于进行了单体微服务拆分,实际上整体应用的集成复杂度和管理难度反而更大,特别是后期的微服务治理复杂度剧增。
很多企业往往就出现前期进行了微服务架构开发和上线,但是在后期微服务治理管控能力,监控运维能力无法跟上,导致系统上线问题无法第一时间诊断和解决,也无法对整体应用的性能提前监控和预警,对服务变更无法进行完整影响分析导致上线后故障。
微服务治理确实复杂,那么是否就导致了基于微服务架构进行的设计开发也复杂?
包括当前主流的微服务开发框架,实际除了基本的微服务功能开发外,都额外提供了类似注册中心,网关,限流熔断,链路监控,日志,安全等一系列的配合组件来完成整体微服务架构的治理。而这也势必引入了微服务开发过程的复杂度,比如在开发过程中经常出现大量的为了治理需求进行的规则类编写,配置文件编写等。
在这种背景下,更好的思路即开发和治理分离。
即不应该将微服务治理的复杂度过多的引入到微服务模块功能开发中,微服务功能开发只要遵循标准的开发框架和开发规范进行即可,微服务治理作为一个扩展插件能力可以动态植入。
以上思路正是当前主流的ServiceMesh服务网格的思路。
即在ServiceMesh思路下,通过在微服务模块中下发Sidecar的方式来实现控制流和数据流的分离。同时实现了我们需要的服务注册发现,安全,日志,流控等各种微服务治理能力。
对于Sidecar代理模块的下发,当微服务和DevOps,容器云集成的时候可以看到,整个代理的下发完全可以作为自动化下发并完成注入和配置工作,这些内容对于微服务模块开发人员在开发态都不需要去关心。
虽然类似Istio等服务网格化方案在企业内还没有得到广泛应用,但是这一定是云原生架构下微服务治理的一个最终发展趋势。同样,对于服务链监控,APM监控等也是同样的道理,在微服务架构开发中应该确保能够满足服务链监控,APM性能监控的需求,而无须对代码进行任何修改和变更处理。
面向无状态而设计
在前面谈到了一个完整的应用本身包括了后端资源和前端应用。对于后端资源类似数据库,缓存,文件等都属于后端资源能力。后端资源一般是有状态的,涉及到状态保持和数据的持久化等操作,因此这块本身应该按照有状态进行设计。
将后端资源分离后,我们其他的是应用开发的其他部分能够完全按无状态思路进行设计。类似Http API接口服务就是一种典型的无状态设计。
前后端分离
在进行前后端分离开发后,可以看到后端可以提供API接口服务能力,那么后端模块无须保持状态。而对于前端模块往往存在用户会话状态保持。
当存在会话状态保持情况时候,也可以考虑将会话保持到数据库或Redis缓存库中,而服务在中间件服务器上进行会话状态保持,以使应用中间件集群节点转变为一种完整的无状态模式。
配置文件
如果中间件服务器存在配置文件,那么配置文件本身有状态。那么在中间件节点扩展的时候如何确保配置文件也能够同步或变更。
因此在这种情况下往往需要考虑采用类似ZooKeeper或Etcd等分布式协调来实现中间件节点需要的配置文件信息的实时分发,变化后同步等。即通过管理节点来实现关键的配置文件等状态信息在各个节点之间的分发和实时刷新操作。
日志文件
对于中间件集群中各个节点的运行往往会产生各类日志信息,这些日志本身有状态,有些日志会直接存在在中间件服务器节点上。那么如果节点出现扩展,变更或销毁,将导致日志信息本身丢失。因此对于日志信息必须有一套方式来实现统一的管理。
当前主流的做法是通过定时或准实时的分布式日志采集系统,对日志进行进行采集再统一进存储分析,类似主流的ELK日志采集和分析方案等。当然我们也可以考虑一个更加通用的思路,即在微服务实现过程中形成一个日志标准输出接口,只要订阅了这个接口就可以实施的采集或获取日志信息。
面向解耦而设计
前面已经谈到,不是你用了Http API接口就是松耦合,如果两个微服务模块之间有大量的API接口交互,那么仍然是一种紧耦合的关系。
谈微服务的时候你会发现,一个微服务要成功正常运行,有大量的底层技术组件或微服务依赖,也有大量的同层的其它微服务模块API接口依赖。如果任何一个依赖的微服务出现问题,或者数据库出现问题都会导致微服务无法正常运行。
不论现在谈缓存,还是谈消息中间件和事件驱动架构,你可以看到都是希望对微服务间进行解耦,对微服务和数据库之间进行解耦。
对于核心的解耦思路实际在前面已经谈到过,即:
对于查询,采用缓存方式进行解耦
对于导入或CUD接口,采用消息中间件解耦
实际上面的思路和经常谈到的CQRS命令查询职责分离思路类似,通过CQRS一开始是为了更好的配合读写分离的数据库使用。但是真正CQRS实现解耦的重点仍然是两个。
其一是将命令作为事件推送到消息中间件处理,以避免出现长周期分布式事务。其次就是启用单独的R读库,可以是数据库,也可以是缓存库,来实现查询功能独立解耦和性能提升。
在实际的实践中,不同开发团队之间交互接口最好能够通过消息中间件或缓存进行彻底解耦,以降低相互之间的依赖和影响。
比如对于微服务A需要推送数据到微服务B,同时需要从微服务C查询数据。那么推送数据库到B的接口可以实现为消息接口,先推送数据到消息中间件;而对于数据的查询则可以在获取数据后进行缓存等。
面向持续集成和持续交付而设计
对于持续集成和持续交付,本身也是云原生里面的关键技术要素。
在业务需求不断变化的情况下,如何快速的相应业务需求变化并快速交付可用的版本,这里面本身有大量的实践,包括敏捷开发,微服务拆分,持续集成,可复用的服务库积累等都是为了满足应用的集成和交付效率。
到了云原生阶段出现一个新的问题点,即应用设计开发如何快速的交付到生产环境?
如果开发,测试和生产全部在公有云环境,那么我们希望整个持续集成和交付过程能够完全自动化,即对于我们UAT测试和验证通过的应用能够一键交付到生产环境。如果开发测试在启用内部,仅仅是生产环境需要交付到公有云环境,那么我们希望有一个实现DevOps功能的协同平台,来完成这个衔接操作,可以实现私有云到公有云环境的持续交付。
持续集成和持续交付
简单来说持续集成针对内部团队,而持续交付针对的是客户。软件持续集成的过程是对确保内部需求,开发和测试人员协同,提供一个完整的可用版本。持续交付的过程是将可用版本最终发布给客户的过程。
动态部署和灰度发布
持续交付本身需要做到的就是对用户的影响最小,即不能因为交付的动作而导致系统不可用。为了实现这点,往往需要考虑系统本身具备热部署能力,其次是系统支持灰度发布,支持灵活的灰度发布策略的制定。
构建私有云和公有云间多云适配
一个传统企业在进行数字化转型,进行传统IT架构微服务架构改造和上云迁移时候,本身是一个逐步迁移演进的过程。即企业内部的IT应用常时间会处于一个企业内部私有云和外部公有云同时使用,同时提供服务能力的状态。
那么多云适配平台就可以起到公有云和私有云间的适配桥梁。
对于多云适配平台核心仍然是基于DevOps持续集成和交付的思路,通过调研公有云开发的API接口服务能力来实现应用向公有云环境的部署和交付。
面向可运维而设计
一个云原生应用,最终托管到公有云环境运行,面对的是资源或服务层能力。那么这个应用本身的运维和管理方式都出现巨大的变化。
举例来说传统方式你可能还登陆中间件服务器后台,查看具体的错误日志,但是新方式下中间件集群节点都是容器实例,而是本身动态变化,并不允许你直接去操作容器底层。
因此云原生应用的运维不是传统的运维方式,而应该是基于API服务能力进行整合后形成的一个统一运维,监控,治理能力平台。那么对于应用开发来讲,需要考虑的就是面向运维而设计,开发完成的应用本身要具备可运维,可监控,可运营分析等关键特征。
一个完整的运维,包括了资源监控,服务和服务链监控,应用监控,日志监控和分析多个方面的内容,即最终上线的云原生应用要确保能够纳入以上监控体系,满足监控的准入条件。
当然一个应用本身可运维,还需要表现在对变更友好,当出现需求变更的时候能够快速的分析微服务或API接口的相互依赖关系和影响,避免变更导致的其它系统故障或问题。
一个好的可运维系统应该具备两个方面的特征:
其一是整个应用本身应该是面向风险而设计的,而不是面向问题而设计。即能够提前预估风险并进行告警,并基于风险采用有针对性的处理措施,避免风险转变为具体的故障或问题。
其次是如果出现系统故障或性能问题,当前的异常提示,日志记录,链路追踪等能够准确的定位到具体的边界和问题点,减少问题诊断和排查的时间。
面向变更而设计
在微服务架构实践过程中,由于很多接口是采用Http API接口方式进行调用,很多接口修改实际并不会引起编译构建期的错误。因此导致某个微服务接口修改后导致其它微服务模块功能出现异常的情况。当出现问题后,我们才在事后进行修复。
对于服务链监控和链路跟踪是一个事后的行为,重点是发现性能问题而不是帮你去分析服务之间的依赖关系。
因此提前梳理清楚微服务间的接口交互和依赖关系是必须的,如上图。
通过上图的接口交互矩阵,可以很清楚的看到当某个接口出现变化的时候,究竟会对哪些微服务模块,哪些功能造成影响,那这些影响点就必须考虑配套的变更或者说在提交测试的时候,这些影响到的微服务模块或功能也需要进行测试。
简单来说,面向运维而设计需要具备如下能力
服务器资源和负荷情况可监控
中间件资源和负荷情况可监控
操作系统,中间件,应用日志可监控
应用性能和健康状态可监控
服务和服务链可监控
微服务依赖关系可视
微服务API接口依赖关系可视
应用-微服务-容器资源-物理资源关系可视化
当具备以上能力的时候,一个云原生应用基本具备完整的可运维,可管控能力。