一切都在变化,同样软件的开发实践也在演变和进步, 一点一点改变着传统软件的开发方式,这些改变积累起来可以大大 影响软件的开发效率。 下面比较一下自己经历的软件开发方式的转变。
传统软件开发
传统典型的软件在我眼里是这个样子的。典型架构是分层,典型三层(展现层,逻辑层,数据层), 开发语言基本围绕主流开发语言再加几种脚本,所有的模块打包在一起,安装到用户的电脑里面,运行在一个进程里面。如果需要升级,用户需要重新下载一个大的安装包,几个G到十几个G。估计半年发布一次,如果激进也就是一个月发布一次。软件支持比较慢,如果有问题,会在下一次发布里面才能修掉。下面从几个不同视角看看这种传统的开发方式的特点。
1. 架构: monolithic。
其实monolithic 与微服务的比较已经被大家大家谈烂了. 简单回顾一下,传统架构 或许是分层的,分块的,插件模式的,六边形,但是这些架构都有有些限制,比如开发语言,数据库,技术栈的限制。 简单而言技术栈耦合,适合的事情没法用合适的工具开发。
代码耦合。所有的代码最后打包在一起,那么对与模块边界管理慢慢就不那么清晰。或许因为新人不留易就引入新的依赖。
再说发布耦合。不同的模块,对应的不同team,但是所有的模块需要一起发布。这个等待时间也是浪费。
运行耦合。模块最终运行在一个进程里面,可扩展性,容错,可用性都大大降低。
当然对于小的产品或者工具,这种架构经过时间检验,而且大家都熟悉,开发起来短平快。效率高。
2. 开发的协作方式。
开发分为若干个团队,每个团队有多个开发,但是大家都提交到一个codeline 里面。比较简单。 这就会产生一个问题,如果有人代码提交失败,那么今天可能就没有最终的build,测试也就没法进行。 想想如果上百人的团队,这个是多大的损失。 如果把build failure 作为kpi考虑,大家在这个层面上大家应该理解:)。
其次问题是,所有模块在一起,那么CI 运行起来就越来慢,反馈起来也就晚。如果4个小时的反馈速度,今天估计只能提交一次代码, 进一步或许防止出错下午大家都避免提交代码,放到第二天提交。
最重要的是这种开发方式,不支持小步快走的迭代反馈开发。理想的是做完独立的一件事,哪怕修改一行配置,独立提交一次。这样每次提交的代码少,不容易犯错误;同样如果出错,容易定位。但是这种情况下,反馈太慢,如果频繁提交的好处没有想象的那么大。 考虑到下面将会谈到的branch的爆炸,这种提交方式会给自己带来,天量的代码merge task,自己也会放弃这种方式。
3. 发布的流程:
软件的发布应该不要影响当前的新功能的开发,尤其是大规模开发情况下, 所以软件发布会创建新的branch。 一般会有四种不同类型的branch(dev, pre-release, release, hot-fix ). dev 做开发时提交; pre-release branch 作为release的做准备,只修bug,不增加新功能; 最终发布时,会从release branch拿代码。 如果发布有问题,需要补丁,还会有hotfix branch。为了方便hot-fix branch 可能和 pre-release 共享一个branch。 下图展现不同branch的代码的提交合并顺便。
最后修复的问题,最后都应该merge 回 dev branch( hot-fix 和 pre-release 上提交的代码需要merge回 dev branch)。 但是为了防止产生提交回路,那么提交都需要从dev上面提交。
pre-release 和hot -fix branch实际上是临时branch,最终发布后可以删掉的。在传统的集中式代码管理工具(perforce等),创建branch 是比较重的操作 , pre-release 会被重用。
其实这个发布流程很简单,但是如果考虑到传统软件维护的数量,这个问题就变的复杂起来。
4. 软件维护的版本数量, 组合爆炸。
典型传统企业软件的一个大版本支持3年,每一个版本有若干个patch。
Version1 v1.patch1 v1.patch2 v1.patch3 ...
Version2 v2.patch1 v2.patch2 v2.patch3 ...
Version3 v3.patch1 v3.patch2 v3.patch3 ...
如果每个patch作为一个单独的release维护,每一个release 有三个branch, 那基本就疯掉了,估计很少有公司能付出这样的维护成本。折中一下,对于每一个大版本只维护一个code line, 同一个大版本下的patch放在同一条code line上面,每一个code line 包含3个branch ( dev, cor , release), 也就是同一个时刻只维护一个大版本的最新patch。
Version1 -> v1.patch1 -> v1.patch2 -> v1.patch3 ...
Version2 -> v2.patch1 -> v2.patch2 -> v2.patch3 ...
Version3 -> v3.patch1 -> v3.patch2 -> v3.patch3 ...
那么现在就只有3个大版本(v1,v2, v3),每个大版本对应一条codeline,每条code line对应3个branch (dev, core , release). 一共9个branch。现在看一下发现一个bug,如何做?
如果在 v1.patch2里面发现一个bug, 也只能修在v1.patch4 里面( patch3 已经发布)。 在codeline V1 里面,补丁这样提交:由dev -> cor -> rel 的过程。那么这个补丁,同样需要提交到V2 的当前开发版本 (dev -> cor -> rel )。 当然也不要忘记 V3 (dev -> cor -> rel )。
项目上为了追踪这个bug,需要对于每一个code line 创建一个bug, 然后有不同的merge task。根据流程,往往不同版本还需要测试。如果考虑到下面即将说的环境和配置,需要多少测试环境和时间?想想维护的状态和测试case,已经够复杂。
项目到release的时候,代码的提交需要更加严格控制,那么对于每一次提交都需要审核流程。往往流程审核者,对你的代码没有甚至是功能不了解,但是还要显示走一个流程。
但是这个还不是最糟糕的,想想,如果有regression ,或许还需要revert, 然后从来一次。实际情况在不同的branch之间跳转,如果有冲突,需要解决。 防止编译失败,需要编译或者打开IDE验证。大脑负载不低,而且都是类似重复的事情,对于客户的而言基本没有价值。作为开发人员是一种什么样的感受?
5. 软件安装
软件最终发布一个安装包,最终产品安装到用户的环境。这里用户的环境(机器和网络),安装过程以及配置的差异,这些都不可控制,这样会增加出现问题的可能性。进一步,安装问题出问题,软件是完全不能用,肯定是第一优先级的bug。为了减少安装问题,需要强调测试覆盖率。安装测试不但需要覆盖各种环境,各种配置,此外还要考虑各种版本,以及升级路径和兼容问题。
然而安装测试会产生组合爆炸。 考虑维护的版本数,安装的环境和用户的配置,这些都会使的安装测试复杂度大大增加。比如, 支持版本的数量( 3, 不考虑patch) x 支持的环境(4 = (x32 +x64) x OS version)x 用户的配置 (比如关键4项配置) = 48. 如何每次安装半个小时,那么就是24 小时。 这个只是一次安装,如果有regression, 试试工作量又得增加多少?
实际情况更糟糕,安装测试往往到最后一个环节才能开始测,留的时间如果不够的充分或者不被重视。如果这个地方疏忽,会导致后期客户支持的压力。 而安装问题往往与环境有关联,常常症状是只有在客户的环境才能重现,这样需要对客户,技术支持,开发多方的远程支持的,可想而知效率很难高。
这个一个典型的软件变化成本图里面,bug发现的时间点越往后,其成本越高。这就是效率和成本杀手。
考虑到这些问题会导致复杂度变大,那么新的软件开发模式,就需要从根本上减少或者避免这些问题,从而提高软件开发效率。
新的软件开发方式
1. 软件的架构: 微服务。
微服务架构,支持动态扩容,高可用,以及容错性好。这些已经被讨论多次就不展开了。这里强调一下其他优势。
软件的边界更加清晰,对外API,其他都是黑盒; 对内,也不可能引用外面的依赖。
软件的复用,再也不是一个模块(dll, jar 包),语言相关, 而是随时可用的服务。
技术栈的选择,开发语言,数据库,web容器都是可以随时自己选择。
外部的变化,可以升级接口,独立部署。
考虑到云计算和容器,运行的环境,可以灵活选择。
这些意味着,软件可以独立演化。没有传统的那些语言耦合,技术栈耦合,部署耦合,运行耦合,升级耦合,运行环境耦合。 软件模块可以做到更高层次的低耦合高内聚合,降低耦合带来的复杂度。而复杂度是软件开发里面的头号敌人。当然这些好处不是免费,微服务的治理带来额外的复杂度。
2. 开发的协作:
其实这个集中的代码管理和上面一样。现在对于分布式的代码管理工具更加推荐,git,github。代码本地提交,速度杠杠的。创建branch,基本完全考虑代价。同时又供了新的协助方式。 基于 fork 和pull request的协作。
3. 发布流程 和版本维护:
One code line with two branch is enough 。
基于云上的软件,理想所有的客户用一个版本。如何这个假设为真,那么所有烦人merge task,烟消云散了。 Dev branch 做开发,master 用来做发布。 生活太美好;代价就是用如果有问题,所有的客户都有问题。升级尤其要注意,小范围试点然后扩大范围,准备好回滚机制等。
5. 安装到部署的改变 : continuous deliver
最终的运行环境在云上, 软件运行环境可以掌控;(所谓 Infrastructure as a code,cattle vs pet) 软件发布用容器,那么软件之间就隔离; 环境和配置的差异,完全可以掌控。更进一步可以做到自动化部署。 理论上距离按需发布进了一大步。
6. 新的挑战: 服务治理。
微服务带来这么多好处,需要付出一点代价。组织上的调整。 微服务将产品划分为不同模块,模块有各自不同团队负责。模块之间需要通讯,团队之间同样需要沟通。 简而言之,有什么样的架构就有什么样的团队,反之,团队的组织也会反过来影响软件的架构。 这个所谓Conway's Law. 软件架构与团队之间的组织架构的一致性。微服务需要团队负责服务的整个生命周期,包括部署和运维的。 强调团对的写作DevOps,需要组织架构文化的调整。
微服务带来的数据碎片化,如果出report,那么数据如何集成(replication)。微服务数据的服务独立,导致数据潜在的不一致性. CAP 原则,在可用性和一致性上只能有所取舍。 服务之间的通讯,如何 保证性功能,容错。在线如何服务流量监控,在线排错都是对软件质量提出新的需要。此外,服务发现,网管,安全,调度,都是一些问题都需要解决。
微服务概念很好,落地需要很多事情要做。 CNCF协会发展迅速,其包含的子项目 如docker负责软件隔离, k8s 负责管控环境和调度,service mash 负责模块之间通讯。 这个协会下的项目值的关注,让微服务真正的可以落地。
传统软件开发方式的迁移
如果软件越来越复杂,传统软件开发需要向新软件开发方式靠近。 如果直接切换也是不太现实的,那么就需要提供一个到云端过度的解决方案。 一部分服务搬到云上,或者新服务搬到云上,减少本地发布的频率。CI流程可以把大而慢的,改变为对于每一个模块做自己的CI Job,当然需要某种方式解决一些依赖。新服务迁移到云上,减少发布安装频率,可以大大减少维护成本。 甚至激进的开发方式如chrome,强制升级 (因为没有数据负担,兼容问题小)。 提高控制运行环境和配置方式,来较少测试成本。当然这些需要具体问题具体对待,没有一个好的方案。 或许时间到了,自然而言的就淘汰掉。
总结一下,软件是商品,是一种服务,用户按照使用的服务付费。从lean的角度看待,不能使用的服务都是浪费。比如架构导致的技术语言的绑定,开发过程中代码冲突合并(merge),编译的失败导致的等待,发布中多个版本之间相互等待,多个版本的维护,升级兼容的考量,不同配置和运行环境的差异导致的问题,以及本身bug, 这些都是浪费。 此外,这些会导致软件开发中不确定风险更高。 新软件开发,就是如何解决这些问题,很大成都上减少甚至避免这些问题。 比如基于微服务的架构,使得每一个模块可以自由选择合适的技术栈, 减少无用的耦合; 可以独立部署, 减少无关的等待; 容器使得,软件与运行环境隔离(软件发布包本身与外部隔离,以及运行环境隔离); 以及运行环境的标准化(infrastructure as a code), 对服务器的态度由pet 到 cattle的态度。 这样软件模块可以独立演化,采用合适的技术,架构,按需发布,不依赖运行环境, 这个大大降低软件复杂度,这就大大提升软件开发效率。
NOTE:
当然传统软件开发好的实践当然可以拿过来的, BDD, TDD, CI/CD, test automation,pair programming. 本文只是强调自己眼中的变化。
reference
1. https://www.atlassian.com/git/tutorials/comparing-workflows
2. https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow
网友评论