今天继续谈企业在进行微服务架构转型和DevOps过程实施中,对若干痛点问题的解决。对于DevOps相关实践的文章,在我头条历史文章中都有过描述,在这里不再展开。
对于软件研发人员,一定要形成一个意识,即对于重复的内容一定要自动化,对于共性内容则通过抽象可变参数后也自动化。软件系统开发应用本身就是为了实现业务工作的自动化,但是很多开发人员反而没有这种自动化和可配置化的意识。
对于DevOps的驱动力,可以看到往往来源于高层领导和下层技术团队两个方向。
对于技术团队来说,DevOps的实施或者说狭义的持续集成和交付过程的实施,很大的驱动来源于问题驱动,而不是风险驱动。比如在实施微服务架构化后,原来一个单体已经拆分为20个微服务,整个编译构建,打包,部署的工作如果还需要靠人工来完成已经巨大的工作量,这个时候不得不寻求变化,将人工的工作自动化掉提升效率。
对于高层管理来说,推进DevOps实施和文化建设,很大的一个驱动力来源于一个意识,即任何IT系统的建设最终都需要形成对企业发展有用的资产,这个资产不是服务器,而是可管理可运维的软件,可分析可决策的数据。而DevOps刚好是积累这个资产最好的一个过程支撑,简单来说就是良好过程支撑最终形成的资产才是可见可控的。
其次,对于整个IT团队来说,最大的价值是IT系统能够高效的支撑业务,这个支撑即包括了IT敏捷的响应和快速上线交付能力,也包括产品的高质量交付能力。这些虽然在DevOps出现前,已经有类似敏捷开发,自动化测算,代码检查,灰度发布等大量的过程实践。但是可以看到通过DevOps过程实施可以将这些实践更好的进行融合,形成一个完整的整体。
那么DevOps过程实施真正的难点在哪里?
在我前面文章也谈到了,即使你自己采用类似Jekins工具来打通一个完整的持续集成和部署的过程链也不是太难得事情,因此整个DevOps实施难点不在基础的支撑工具层面。
真正的难点实际体现在三个方面。
团队技能储备:面对太多新技术,团队技术本身的储备问题
遗留系统迁移:不涉及微服务化还好,如果涉及微服务化往往改造迁移难度巨大
研发敏捷文化:不要期望工具能够驱动文化改进,而是文化改进推动工具应用
当面临以上问题点的时候,可以看到DevOps推进相对困难,这个还没说高层领导本身是否支持,即使高层支持研发团队仍然面临上面几个关键问题需要解决。
比如一个研发团队,原来就没有实践过敏捷开发,或连Jekins持续集成也没有实施过程,更没有接触过Docker容器方面的内容。那么要实践这个过程实际仍然有很大的技术难度。当然DevOps实施比较好的点在于整个底层技术链支撑平台构建不需要每个人都参与,只需要1到2个核心人员来构建即可,但是仍然需要规范所有研发人员的研发流程,沟通协同机制等。
从最早谈每日构建和持续集成的时候,就在谈一个关键的概念,即整个持续集成的过程应该是基于打包完成后的二进制部署包文件的,而不是在每个环境再重新打包构建。
这个点为何如此重要?
简单来说就是测试人员在测试环境测试验证通过的版本是基于二进制部署包的,因此只应该对二进制部署包质量负责,如果在生产发布的时候重新在构建,那么对于代码一致性,构建过程等引入的其它质量隐患将无法预知。这也是经常出现测试和开发人员扯皮的一个原因,即本来测试的版本没有问题,但是一发布到生产环境去功能就出现问题。
但是每个环境总会有一些和环境相关的全局参数或配置文件信息,因此这些信息不能打包到部署包里面,而是应该单独拿出来进行管理。
如果是简单的全局参数,可以直接启用类似环境变量进行设置。但是如果涉及到比较复杂的配置文件,那么就需要考虑单独进行配置文件的管理和分发,类似当前微服务架构下,可以采用全局的配置中心来管理和分发配置文件。而这个功能在进行DevOps持续集成和交付的时候仍然需要。
在基于二进制包进行持续集成后,可以看到实际不会再存在类似测试环境进行独立的编译构建的说法,因此对于传统的分支管理策略也需要调整。
在最早的软件开发和分支管理里面可以看到,如果有测试环境会单独存在一个测试分支,也有独立的生产分支,其次才是开发分支和Bug分支。
先分析下传统开发下为何需要独立的测试分支?
看下测试分支的使用场景,启用测试分支的一个重要目的就是要将测试环境和测试的源代码版本和开发环境版本完全区分开。保证测试环境版本的稳定性。我们可以试想一个最简单的环境,同样的一个软件模块或源代码文件,涉及到两次需求变更的处理,分别对应到两次的版本规划和发布。而第一次的版本变更修改完后就需要提交测试,如果没有测试分支,直接导致的后果就是开发人员对第二次需要变更的内容和测试人员对第一次变更的测试两个工作无法并行。而传统的做法往往是开发人员将二次变更暂时不 check in 到开发分支上,这直接导致的问题仍然是最新的源代码无法在配置服务器环境上准确受控。同时开发分支,往往每天都涉及到程序员代码的检入和检出,直接导致开发环境分支本身并不是一个稳定的,随时都可以提交给测试的版本。这也是启用测试分支的一个重要意义。
而在持续集成模式下,开发需要确保每天checkin的代码都是能够编译通过,构建成功的。同时在敏捷短周期迭代模式下,也尽量减少了同时需要应对上面谈到的多个版本需求变更的情况。
因此在这种场景下测试分支没有存在的意义。在DevOps和持续集成下,实际基于Master主分支拆分开发分支和Bug分支两个分支即可以满足日常配置管理需求。
对于日常作为版本规划后的功能开发在开发分支进行,在版本开发完成并测试通过后进行版本基线,同时将内容Deliver到Master主分支。同时基于Master主干可以拉出一个Bug修改分支,专门负责Bug的修改,当Bug修改完成测试通过后Deliver到主干分支。同时开发在拉取代码或交付下个版本到主干的时候,如果存在代码冲突再启动自动或人工的Merge操作。
在实施DevOps的时候往往重点都在思考研发和运维的协同和一体化。但是实际上可以看到研发过程管理和持续集成的协同才是一个需要关键解决的问题点。
在我前面就提到一个观点,即如果在实施完成DevOps后,你发现研发和测试,研发和运维之间还存在大量需要线下去人工沟通的协同点。那么即使DevOps工具链让持续集成过程自动化了,但是对于整个研发过程仍然没有实现理想的自动化和流水线作业。
首先基于上图再对整个协同过程做下说明。
1.开发人员开发完成代码,在本机进行单元测试或自测通过。
2.开发人员将对应的需求或缺陷状态修改为待部署。
2.开发人员检入代码到Git代码库,这里可以实时触发或定时触发流水线。
3.流水线触发,开始执行拉取代码,编译构建,打包,镜像制作和部署流水线作业过程
4.流水线执行成功,自动将对应的需求或缺陷状态变更到待测试
5.测试人员进入研发平台在待办中可以看到待测试的内容,同时当前测试环境已经是最新版本
6.测试人员进行测试,如果有缺陷则提交缺陷
7.在有缺陷情况下,测试人员将流水线处理未不通过退回
8.开发基于测试提交的缺陷进一步修改,同时整个流水线持续迭代。
9.在测试验证通过所有缺陷后,测试通过,流水线执行环境迁移动作。
以上过程希望达到的一个最简单目标就是开发和测试之间并不需要复杂的沟通,类似哪个功能开发完了,你可以到测试环境测试了。这些减肥瘦身都是不必要沟通,这些信息完全可以通过研发任务管理平台和持续集成的协同进行推送,各方只需要关注待办即可。同时对于环境的迁移也应该是自动化的,只要当前版本测试问题全部关闭,则可以进行环境迁移动作。
流水线设计不是一个开发环境的编译构建自动化,还涉及到整个环境迁移。比如当前有开发,SIT测试,UAT测试三个环境,实际的流水线应该为:
代码拉取->编译构建->打包->开发部署->测试验证->UAT迁移部署
特别是当你面对外部的用户进行UAT测试或需要提供给外部人员一个演示环境的时候,整个流水线过程的价值就进一步体现。比如内部测试人员最终测试通过的版本,可以完全自动化快速的在5到10分钟就交付一个UAT环境给外部用户进行测试。
关于代码检入是否实时触发构建的问题?
实际上可以看到,没有必要一有代码检入就实时触发构建,每天定时2到3次左右的构建频度基本完全可以满足日常敏捷性的需求。
在定时进行流水线执行的时候,在我前面也提到一个关键点,即如果定时触发执行的时候,发现代码分支没有任何修改或检入,这个时候应该直接跳过执行,不再进行构建操作。
对于一个软件产品在进行微服务架构改造后,就形成了产品-项目的两级管理体系,项目这级对应到具体的微服务模块。比如我们自己的DevOps支撑平台产品,已经拆分为门户,研发过程管理,持续集成,容器管理和调度,资源管理,制品库,测试管理,度量管理等多个微服务模块。
但是最终向生产环境交付的是一个完整的产品,产品中包含了多个微服务,每个微服务本身又是以容器化部署的方式进行独立交付。那么在进行持续集成和持续交付的时候,就一定存在产品-项目的两级流水线,两级流水线如何协同就是必须要考虑的一个关键问题。
基于两级流水线设计,我们希望达到的效果就是:
我们不用去关心产品变更的时候究竟变更了哪些微服务模块,即一次变更我们直接启动产品流水线。产品流水线启动后自动检查哪些微服务发生变更,如果发生变更则出现持续集成操作,如果没有变更直接调整到End完成节点。
在所有微服务单个流水线执行完成后,我们聚合到产品流水线进行人工测试和验证,没有问题后我们可以进一步进行打标签操作,或触发环境迁移操作。
注意环境迁移我们可以根据本次变更版本号或根据我们手工打的标签号进行,同时环境迁移操作本身不再涉及到编译构建和打包操作,因此环境迁移应该配置在产品级流水线上进行。
如果从SIT环境迁移到UAT环境后测试不通过,测试出了相应的Bug,这个时候产品流水线退回到开始节点,同时开发人员在修改完成Bug后,同样系统自动判断哪些微服务模块代码出现变更并自动触发构建打包操作,直至所有的Bug修改完成并验证通过。
当然,更好的方式是在规划了新的迭代版本的时候,首先分析会影响到哪些微服务模块,然后对这些微服务模块进行开发版本升级,并启动迭代开发任务,检入检出代码。但是运行整个产品流水线的时候,只对本次版本升级到的微服务进行编译构建和打包操作,而对未影响的微服务不启动相应的重复编译构建操作。
同时在最终的产品向生产环境的交付环境,同样需要做到只对变更过的微服务进行版本部署和交付,而对没有变更的模块不再进行重复的部署和交付。
通过产品和项目微服务两级的流水线管理方式,实际的产品迭代版本是管理到产品这个粒度,但是最终的持续集成和交付又是管控到微服务这个粒度。产品层面可以人工或自动的分析究竟哪些微服务需要变更和受影响,仅交付和部署变更的内容,而不是整个产品重新部署。
基础依赖Jar包版本变化
假设上面的研发管理,持续集成,制品库三个微服务,都同时依赖一个common.jar包。里面为公共可复用的方法函数。
这时候研发管理微服务对common包提出需求,新增加了一个共性接口,这个common包需要重新构建并进行了版本升级。那么这个时候实际上持续集成和制品库两个微服务是没有必要进行重新编译的,即使发布了新的jar包版本,仍然不需要重新编译这两个微服务。
不能因为是共性依赖包,只要版本变化就对所有上层的微服务模块全部重新编译构建,这个和我们前面谈到的通过微服务进行解耦思路是相违背的。
同时更好的方法应该是将common包本身提升为一个独立的微服务,同时提供API接口给上层的微服务模块使用,通过API接口来实现彻底的解耦。
先说下整体微服务架构下的内部协同。
对应内部协同仍然可以使用Eureka注册中心,即Eureka本身也做为一个独立的微服务组件通过容器化的方式托管部署到整个容器资源池中。同时在微服务模块自动部署或自动进行弹性伸缩扩展后,注册中心仍然能够自动发现对应的微服务,仍然能对扩展的微服务模块进行心跳监控。也就是说传统微服务下的内部基于注册中心的服务注册发现,服务调用模式本身并不需要大变化。
其次是对外进行的API接口暴露,这块在前面文章给出了一个说明如上图。
当微服务和容器云集成后,在微服务部署完成或扩展完成后,会形成一个ClusterIP虚拟的集群节点供内部模块访问。如果需要对外也可以走Ingress或Loadbancer模式进行。
如果仅仅是BS端内部应用模式,走K8s集群IP完全可以满足需要。
但是如果涉及到外部APP或其它第三方应用访问,仍然需要将相关的API接口注册到API网关再统一对外暴露。而API网关本身又存在两种方法。
其一是直接对接到Kurbernetes集群的集群IP上
其二是对接底层的NodePort各个节点,然后进行负载均衡
不论是哪种方式,实际期望的是在微服务自动部署或弹性扩展完成后,能够自动的完成相关的API接口服务在API网关上的自动注册操作。
比如可以在微服务开发中新增加一个配置文件,确定哪些微服务接口需要进行自动化注册,然后再部署完成后,系统自动读取这个配置文件,调用API网关的接口完成自动化注册操作。
对于单元测试和自动化测试,实际问题不在于技术和方法,而在于工作量。在10多年前团队进行的单元测试实践就看到,要做全部覆盖所有业务场景的单元测试,那么写Junit单元测试用例的时间往往比代码开发的时间还长。这也是导致全面的单元测试很难推行的一个原因。
而到了当前敏捷研发过程中,要花费如此长的时间去写单元测试用例并执行,显然也是很难落地执行,这个事情虽然是长期有效,但是短期来看往往是降低了整体交付效率。
那么单元测试究竟做不做?
如上图,可以看到微服务B存在后端和前端两个微服务模块,而微服务B提供的80%以上接口都是直接给前端模块使用。如图仅仅只有03,04两个接口需要提供给微服务C使用,而05接口需要提供给微服务D使用。当然微服务B本身也依赖微服务A提供的两个API接口服务。
在这个分析清楚后,为了减少微服务模块之间本身的相互依赖影响。对于微服务B暴露给C和D使用的接口我们建议是必须进行单元测试,并确保验证通过。如果这个单元测试没有通过那么微服务B部署失败,需要进行回退。
其核心原因就是一个微服务的部署不能影响到其它微服务模块的使用,这个是必须进行严格的边界控制的地方。而这种对外接口本身量不大,完全可以启用单元测试并进行自动化执行。
这部分内容应该由开发人员编写类似Junit代码来完成单元测试和验证,而不是由测试人员进行自动化测试,这样才能够确保交付到测试时候的基本功能完整性。
上图是一个典型的微服务部署逻辑架构,实际上可以看到在后期的持续集成和开发中,只有红色部分会不断的进行版本变更和迭代升级。对于蓝色和绿色部分属于基础框架和技术服务内容,往往只需一次部署即可。
在当前云原生架构趋势下可以看到,PaaS云平台逐渐这部分能力,当前比较主流的就是类似图中涉及到的数据库,消息中间件,缓存,监控,日志等能力全部由PaaS云平台提供共性技术服务支撑。而对于绿色部分注册中心,内部网关,限流熔断等往往可能暂时还由应用私有,需要自己进行部署和管理。
在这种情况下就需要提供对这些基础框架和技术服务的容器化部署和托管支撑。
这块的能力实际上并不存在微服务模块一样的基于代码的编译构建操作,而是直接基于已有的安装包进行安装部署。因此对于这块的能力提供主要有两种方式:
其一是自己基于Dockerfile制作容器,并托管部署到云平台
其二是提供标准的技术服务申请能力,直接申请你需要的技术服务,由平台完成部署
对于类似微服务网关,限流熔断本身是否可以自动的完成高可用性架构托管部署,实际需要进一步验证,包括后期如果出现性能问题,能否类似微服务模块一样进行自动化扩展等。
在微服务开发过程中,整个微服务划分和微服务间的接口设计仍然需要保持高度的架构完整性和概念一致性。即首先通过架构人员进行微服务拆分,关键接口设计,其次才是进行各个微服务模块的开发,在开发完成后进行集成工作。
如上图,大家遵循同样的接口契约,那么后端开发,前端开发和测试人员可以并行开始各自的工作。对于前端优先进行接口开发和实现,前端则通过接口契约产生Mock模拟,通过接口模拟实现来进行前端功能的开发。在前后端开发过程中,测试人员也可以根据接口定义进行测试设计工作,同时进行相关的测试脚本设计或录制工作。
接口开发完成后,前端和后端首先各自进行单元测试,在单元测试完成后进行前后端的集成测试和验证。同时测试人员可以启动相应的接口自动化测试工作。
接口暴露和开放范围
在确定了接口驱动开发的思路后,还是要重新回顾接口开放的范围问题。比如上面测试部分谈到的接口集成图。微服务B开放了很多Rest API接口,但是大接口都是给自己的前端模块使用,只有03-05三个接口是给其它微服务使用的。
在这种场景下我们并不希望微服务C和D能够随意访问微服务B提供的所有接口。
因此就需要进一步对接口访问权限进行控制,约束到外部微服务仅仅能够访问的接口,至少首先要做到对于需要对外跨模块暴露的接口单独编写独立的接口文件并发布。