前言nginx
新技术层出不穷。过去十年时间里,咱们经历了许多激动人心的新技术,包括那些新的框架、语言、平台、编程模型等等。这些新技术极大地改善了开发人员的工做环境,缩短了产品和项目的面世时间。然而做为在软件行业第一线工做多年的从业者,咱们却不得不面对一个现实,那就是当初采用新技术的乐趣随着项目周期的增加而迅速减小。不管当初的选择多么光鲜,半年、一年以后,只要这个项目依然活跃,业务在扩张——愈来愈多的功能须要加入,一些公共的问题就会逐渐显露出来。构建过慢,完成新功能让你痛不欲生,团队成员没法很快融入,文档没法及时更新等等。git
在长期运转的项目中,架构的腐化是怎么产生的?为何常见的面向对象技术没法解决这类问题?如何延缓架构的腐化?程序员
本文将尝试解释这一切,并提出相应的解决方案。读者须要具有至关的开发经验——至少在同一个项目的开发上一年以上;公司负责架构演进、产品演进的角色会从本文找到灵感。数据库
架构编程
架构这个词在各类场合不断地以各类面目表现出来。从维基百科的词条看来,咱们常常听到的有插件架构(Plugin),以数据库为中心的架构(Database Centric),模型-视图-控制器架构(MVC),面向服务的架构(SOA),三层模型(Three-Tier model),模型驱动架构(MDA)等等等等。奇妙的是,这些词越大,实际的开发者就越痛苦。SOA很好——但在它提出的那个年代,带给开发者的只是面向厂商虚无缥缈的“公共数据类型”;MDA甚至都没有机会沦为新一轮使人笑话的CASE工具。浏览器
在继续阅读以前,读者不妨问本身一个问题:在长期的项目中,这些大词是否真的切实给你带来过好处?更为功利的问题是:你,做为战斗在一线的开发者,在长期项目中可曾有过美好的体验?缓存
技术的演变与挥之不去的痛服务器
企业应用的发展彷佛从十年前开始腾飞。从Microsoft ASP/LAMP(Linux、Apache、MySQL、PHP)年代开始,各类企业应用纷纷向浏览器迁移。通过十年的发展,目前阵营已经百花齐放。与过去不一样,如今的技术不只仅在编程语言方面,常见的编程套路、最佳实践、方法学、社区,都是各类技术独特拥有的。目前占据主流的阵营有:架构
Rails框架
Java EE平台。值得一提的是Java VM已经成为一种新的宿主平台,Scala、JRuby更为活跃并引人瞩目
LAMP平台。Linux/MySQL/Apache并无多少变化,PHP社区从Rails社区得到了很多营养,出现了许多更加优秀的开发框架
Microsoft .NET平台
Django
没有理由对这些新技术不感到振奋。它们解决了许多它们出现以前的问题。在它们的网站上都宣称各类生产效率如何之高的广告语,相似于15分钟建立一个博客应用;2分钟快速教程等等。比起过去21天才能学会XXX,如今它们在上手难度上早已大幅度下降。
须要泼冷水的是,本文开篇提出的问题,在上述任何一种技术下,都如幽灵般挥之不去。采用Ruby on Rails的某高效团队在10人团队工做半年以后,构建时间从当初的2分钟变成2小时;咱们以前采用Microsoft .NET 3.5 (C# 3.0)的一个项目,在产生2万行代码的时候,构建时间已经超过半小时;咱们的一些客户,工做在10年的Java代码库上——他们不遗余力,保持技术栈与时俱进:Spring、Hibernate、Struts等,面对的困境是他们须要同时打开72个项目才能在Eclipse中得到编译;因为编译打包时间过长,他们去掉了大部分的单元测试——带来巨大的质量风险。
若是你真的在一个长期的项目工做过,你应该清楚地了解到,这种痛苦,彷佛不是任何一种框架可以根本性解决的。这些新时代的框架解决了大部分显而易见的问题,然而在一个长期项目中所面对的问题,它们无能为力。
一步一步:架构是如何腐化的
不管架构师在任什么时候代以何种绚丽的方式描述架构,开发中的项目不会超出下图所示:
基本架构示意
针对架构的技术我建立一个Java架构学习群:650385180,里面会分享录制微服务,分布式,源码分析,JVM,Java工程化这些专题的视频,感兴趣的朋友能够加一下。
一些基本的准则:
为了下降耦合,系统应当以恰当的方式进行分层。目前最经考验的分层是MVC+Service。
为了提供基础的访问,一些基本的、平台级别的API应该被引入。用Spring之类的框架来作这件事情。
用AOP进行横向切分业务层面共性的操做,例如日志、权限等。
为了保证项目正常构建,你还须要数据库、持续集成服务器,以及对应的与环境无关的构建脚本和数据库迁移脚本。
阶段1
知足这个条件的架构在初期是很是使人愉悦的。上一部分咱们描述的框架都符合这种架构。这个阶段开发很是快:IDE打开很快,开发功能完成很快,团队这个时候每每规模较小,交流也没有问题。全部人都很高兴——由于用了新技术,由于这个架构是如此的简单、清晰、有效。
阶段2
好日子不算太长。
很快你的老板(或者客户,随便什么)有一揽子的想法要在这个团队实现。工做有条不紊的展开。更多的功能加入进来,更多的团队成员也加入了进来。新加入的功能也按照以前的架构方式开发着;新加入的团队成员也对清晰的架构表示欣喜,也一丝不苟的遵循着。用不了多久——也许是三个月,或者更短,你会发现代码库变成下面的样子:
正常开发以后
你也许很快会意识到这其中有什么问题。但你很难意识到这到底意味着什么。常见的动做每每围绕着重构——将纵向相关的抽取出来,造成一个新的项目;横向相关的抽取出来,造成一个名叫common或者base的项目。
不管你作什么类型的重构,一些变化在悄悄产生(也许只是快慢的不一样)。构建过程不可避免的变长。从刚开始的一两分钟变成好几分钟,到十几分钟。经过重构构建脚本,去掉那些不须要的部分,构建时间会降到几分钟,你满意了,因而继续。
阶段3
更多的功能、更多的成员加入了。构建时间又变长了。随着加载代码的增多,IDE也慢了下来;交流也多了起来——不是全部人可以了解全部代码了。在某些时候,一个颇有道德的程序员尝试重构一部分重复逻辑,发现牵涉的代码太多了,好多都是他看不懂的业务,因而他放弃了。更多的人这么作了,代码库愈来愈臃肿,最终没有一我的可以搞清楚系统具体是怎么工做的了。
系统在混乱的状态下继续缓慢地混乱——这个过程远比本文写做的时间要长不少,之间会有反复,但据我观察,在不超过1年的时间内,不管采用何种技术框架,应用何种架构,这个过程彷佛是不可抗拒的宿命。
常见的解决方案
咱们并不是是坐以待毙的。身边优秀的同事们在问题发现以前采起了各类解决方案。常见的解决方案以下:
升级工做环境
没有什么比一台与时俱进的电脑更能激励开发人员了。最多每隔三年,升级一次开发人员的电脑——升级到当时最好的配置,可以大幅度的提高生产效率,激励开发人员。反过来,利用过期的电脑,在慢速的机器上进行开发,带来的不只仅是客观上开发效率的下降,更大程度上带来的是开发人员心理上的懈怠。
升级的工做环境不只仅是电脑,还包括工做的空间。良好的,促进沟通的空间(以及工做方式)可以促进问题的发现从而减小问题的产生。隔断不适合开发。
分阶段的构建
通常而言,构建的顺序是:本地构建确保全部的功能运行正常,而后提交等待持续集成工做正常。本地构建超过5分钟的时候就变得难以忍受;大多数状况下你但愿这个反馈时间越短越好。项目的初期每每会运行全部的步骤:编译全部代码,运行全部测试。随着项目周期的变长,代码的增多,时间会愈来愈长。在尝试若干次重构构建脚本再也没办法优化以后,“分阶段构建”成为绝大多数的选择。经过合理的拆分、分层,每次运行特定的步骤,例如只运行特定的测试、只构建必要的部分;而后提交,让持续集成服务器运行全部的步骤。这样开发者可以继续进行后续的工做。
分布式构建
即使本地快了起来,采用分阶段构建的团队很快发现,CI服务器的构建时间也愈来愈让人不满意。每次提交半小时以后才能获得构建结果太不可接受了。各类各样的分布式技术被建立出来。除了常见的CI服务器自己提供的能力,许多团队也发明了本身的分布式技术,他们每每可以将代码分布到多台机器进行编译和运行测试。这种解决方案可以在比较长的一段时间内生效——当构建变慢的时候,只须要调整分布策略,让构建过程运行在更多的集群机器上,就能够显著的减小构建时间。
采用JRebel或者Spork
一些新的工具可以显著地提速开发人员的工做。JRebel可以将须要编译的Java语言变成修改、保存当即生效,减小了大量的修改、保存、从新编译、部署的时间;Spork可以启动一个Server,将RSpec测试相关的代码缓存于其中,这样在运行RSpec测试的时候就不用从新进行加载,极大提高了效率。
究竟是什么问题?
上述的解决方案在特定的时间域内很好地解决了一部分问题。然而,在项目运转一年,两年或者更久,它们最终依然没法避免构建时间变长、开发变慢、代码变得混乱、架构晦涩难懂、新人难以上手等问题。到底问题的症结是什么?
人们喜欢简洁。但这更多的看起来是一个谎话——没有多少团队可以自始至终保持简洁。人们喜欢简洁只是由于这个难以作到。并非说人们不肯意如此。不少人都知道软件开发不比其余的劳动力密集型的行业——人越多,产量越大。《人月神话》中已经提到,项目增长更多的人,在提高工做产出的同时,也产生了混乱。短时间内,这些混乱可以被团队经过各类形式消化;但从长期看来,随着团队人员的变更(新人加入,老人离开),以及人正常天然的遗忘曲线,代码库会逐渐失控,混乱没法被消化,而项目并不会中止,新功能不断的加入,架构就在一每天的过程当中被腐蚀。
人的理解总有一个边界,而需求和功能不会——今天的功能总比昨天的多;这个版本的功能总比上个版本的多。而在长时间的开发中,忘记以前的代码是正常的;忘记某些约定也是正常的。造成某些小而不经意的错误是正常的,在巨大的代码库中,这些小错误被忽视也是正常的。这些不断积攒的小小的不一致、错误,随着时间的积累,最终变得难以控制。
不多有人注意到,规模的变大才是致使架构腐化的根源——因果关系在时空上的不连续,使得人们并不能从其中得到经验,只是一再重复这个悲剧的循环。
针对架构的技术我建立一个Java架构学习群:650385180,里面会分享录制微服务,分布式,源码分析,JVM,Java工程化这些专题的视频,感兴趣的朋友能够加一下。
解决方案
解决方案的终极目标是:在混乱发生以前,在咱们的认知出现障碍以前,就将项目的规模控制在必定范围以内。这并不容易。大多数团队都有至关的交付压力。大多数的业务用户并无意识到,往一个项目/产品毫无节制地增长需求只会致使产品的崩溃。看看Lotus Notes,你就知道产品最终会多么使人费解、难以使用。咱们这里主要讨论的是技术方案。业务上你也须要始终对需求的增加保持警戒。
这多是最廉价的、最容易采用的方案。新技术的产生每每为了解决某些特定的问题,它们每每是经验的集合。学习,理解这些新技术可以极大程度减小过去为了完成某些技术目标而进行的必要的经验积累过程。就像武侠小说中常常有离奇遭遇的主人公忽然得到某个世外高人多年的内力同样,这些新技术可以迅速帮助团队从某些特定的痛点中解脱出来。
已经有足够多的例子来证实这一观点。在Spring出现以前,开发者的基本上只能遵循J2EE模式文档中的各类实践,来构建本身的系统。有一些简单的框架可以帮助这一过程,但整体来讲,在处理今天看起来很基础的如数据库链接,异常管理,系统分层等等方面,还有不少手工的工做要作。Spring出现以后,你不须要花费不少精力,很快就能获得一个系统分层良好、大部分设施已经准备就绪的基础。这为减小代码库容量以及解决可能出现的低级Bug提供了帮助。
Rails则是另一个极端的例子。Rails带来的不只仅是开发的便利,还带来了人们在Linux世界多年的部署经验。数据库Migration, Apache + FastCGI或者nginx+passenger,这些过去看起来复杂异常的技术在Rails中变得无足轻重——稍懂命令行的人便可进行部署。
任何一个组织都没法所有拥有这些新技术。所以做为软件从业者,须要不断地保持对技术社区的关注。闭门造车只能加速架构的腐化——特别是这些本身的发明在开源社区早已有成熟的方案的时候。在那些貌似光鲜的产品背后,实际上有着无数的失败的案例成功的经验在支撑。
咱们曾经有一个项目。在乎识到需求可能转向相似于key-value的文档数据库以后,团队大胆的尝试采用SQLServer 2008的XML能力,在SQL Server内部实现了相似于No-SQL的数据库。这是一个新的发明,创造者初期很兴奋,终于有机会作不一样的事情了。然而随着项目的进行,愈来愈多的需求出现了:Migration的支持、监控、管理工具的支持、文档、性能等等。随着项目的进展,最终发现这些能力与时下流行的MongoDB是如此的类似 ——MongoDB已经解决了大多数的问题。这个时候,代码库已经有至关的规模了——而这部分的代码,让许多团队成员费解;在一年以后,大约只有2我的可以了解其实现过程。若是在早期采用MongoDB,团队本有机会摒弃大部分相关的工做。
值得一提的是,高傲的开发者每每对新技术不够耐心;或者说对新技术的能力或局限缺少足够耐心去了解。每个产品都有其针对的问题域,对于问题域以外,新技术每每没有成熟到可以应对的地步。开发者须要不断地阅读、思考、参与,来验证本身的问题域是否与其匹配。浅尝辄止不是好的态度,也阻碍了新技术在团队内的推广。
新技术的选型每每发生在项目/产品特定的时期,如开始阶段,某个特定的痛点时期。平常阶段,开发者仍然须要保持对代码库的关注。下一条,重构到物理隔离的组件则是对不断增大的代码库另外一种解决方案。
显而易见的趋势是,对于同一个产品而言,需求老是不断增多的。去年有100个功能,今年就有200个。去年有10万行代码,今年也许就有20万行。去年2G 内存的机器可以正常开发,今年彷佛得加倍才行。去年有15个开发人员,今年就到30个了。去年构建一次最多15–20分钟,今年就得1个小时了,还得整个分布式的。
有人会注意到代码的设计问题,孜孜不倦地进行着重构;有人会注意到构建变慢的问题,不懈地改进着构建时间。然而不多有人注意到代码库的变大才是问题的根源。不少常规的策略每每是针对组织的:例如将代码库按照功能模块划分(例如ABC功能之类)或者按层次划分(例如持久层、表现层),但这些拆分以后的项目依然存在于开发人员的工做空间中。不管项目如何组织,开发者都须要打开全部的项目才能完成编译和运行过程。我曾经见到一个团队须要在Visual Studio中打开120个项目;我本身也经历过须要在Eclipse中打开72个项目才能完成编译。
解决方案是物理隔离这些组件。就像团队在使用Spring/Hibernate/Asp.NET MVC/ActiveRecord这些库的时候,不用将它们对应的源代码放到工做空间进行编译同样,团队也能够将稳定工做的代码单元整理出来造成对应的库,标记版本而后直接引用二进制文件。
在不一样的技术平台上有着不一样的方案。Java世界有历史悠久的Maven库,可以良好的将不一样版本的 JAR以及他们的以来进行管理;.NET比较遗憾,这方面真正成熟的什么也没有——但参考Maven的实现,团队本身造一个也不是难事(可能比较困难的是与MSBuild的集成);Ruby/Rails世界则有著名的gem/bundler系统。将本身整理出来的比较独立的模块不要放到rails/lib /中,整理出来,造成一个新的gem,对其进行依赖引用(团队内须要搭建本身的gems库)。
同时,代码库也须要进行大刀阔斧的整改。以前的代码结构可能以下,(这里以SVN为例,由于SVN有明确的trunk/branches/tags目录结构。git/hg相似)
原来的库结构
改进以后,将会以下图所示:
改进的库结构
每一个模块都有属于本身的代码库,拥有本身的独立的升级和发布周期,甚至有本身的文档。
这一方案看起来很容易理解,但在实际操做过程当中则困难重重。团队运转很长一段时间以后,不多有人去关心模块之间的依赖。一旦要拆分出来,去分析几十上百个现存项目之间的依赖至关费劲。最简单的处理办法是,检查代码库的提交记录,例如最近3个月以内某个模块就没有人提交过,那么这个模块基本上就能够拿出来造成二进制依赖了。
不少开源产品都是经过这个过程造成的,例如Spring(请参考阅读《J2EE设计开发编程指南》,Rod Johnson基本上阐述了整个Spring的设计思路来源)。一旦团队开始这样去思考,每隔一段时间从新审视代码库,你会发现核心代码库不可能失控,同时也得到了一组设计良好、工做稳定的组件。
上面的解决方案核心原则只有一条:始终将核心代码库控制在团队能够理解的范围内。若是运转良好,可以很大程度上解决架构由于代码规模变大而腐化的问题。然而该解决方案只解决了在系统在静态层面的隔离。当隔离出的模块愈来愈多,系统也所以也须要愈来愈多的依赖来运行。这部分依赖在运行期分为两类:一类是相似于 Spring/Hibernate/Apache Commons之类的,系统运行的基础,运行期这些必须存在;另一类是相对独立的业务功能,例如缓存的读取,电子商城的支付模块等。
第二类依赖则能够更进一步:将其放到独立的进程中。如今稍具规模的系统,登陆、注销功能已经从应用中脱离而出,要么采用SSO的方案来进行登录,要么则干脆代理给别的登录系统。LiveJournal团队在开发过程当中,发现缓存的读写实际上能够放到独立的进程中进行(而不是相似EhCache的方案,直接运行于所在的运行环境中),因而发明了如今鼎鼎有名的memcached. 咱们以前进行的一个项目中,发现支付模块彻底可以独立出来,因而将其进行隔离,造成了一个新的、没有界面的、永远在运行的系统,经过REST处理支付请求。在另一个出版项目中,咱们发现编辑编写报告的过程实际上与报告发行过程虽然存在类级别的重用,但在业务层面是独立的。最终咱们将报告发行过程作成了一个常驻服务,系统其余的模块经过MQ消息与其进行交互。
这一解决方案应该不难理解。与解决方案1不一样的是,这一方案更多的是要对系统进行面向业务层面的思考。因为系统将会以独立的进程来运行这一模块,在不一样的进程中可能存在必定的代码重复。例如Spring同时存在两个不相关的项目中你们以为没什么大不了的;但若是是本身的某个业务组件同时在同一个项目的两个进程中重复,许多人就有些洁癖不可接受了。(题外话:这种洁癖在OSGi环境中也存在)这里须要提醒的是:当处于不一样的进程时,它们在物理上、运行时上已经完全隔离了。必须以进程的观点去思考整个架构,而不是简单的物理结构。
从单进程模型到多进程模型的架构思惟转变也不太容易——须要架构师有意识的增强这方面的练习。流行的.NET和Java世界倾向于把什么都放到一块儿。而 Linux世界Rails/Django则能更好的平衡优秀产品之间的进程协调。例如memcached的使用。另外,如今多核环境愈来愈多,与其费尽心思在编程语言层面上不如享受多核的好处,多进程可以简单而且显著地利用多核能力。
如今将眼光看更远一些。想象一下咱们在作一个相似于开心网、Facebook、人人网的系统。它们的共同特色是可以接入几乎无限的第三方应用,不管是买卖朋友这类简单的应用,仍是绚丽无比的各类社交游戏。神奇的是,实现这一点并不须要第三方应用的开发者采用跟它们同样的技术平台,也不须要服务端提供无限的运算能力——大部分的架构由开发方来控制。
在企业应用中实现这个并不难。这其中的秘诀在于:当用户经过Facebook访问某个第三方应用的时候,Facebook实际上经过后台去访问了第三方应用,将当前用户的信息(以及好友信息)经过HTTP POST送到第三方应用指定的服务网址,而后将应用的HTML结果渲染到当前页面中。某种意义上说,这种技术本质上是一种服务器端的mashup. (详情参考InfoQ 文章)
Facebook App架构
这种架构的优势在于极度的分布式。从外观上看起来一致的系统,实际由若干个耦合极低、技术架构彻底不一样的小应用组成。它们不须要被部署在同一台机器上,能够单独地开发、升级、优化。一个应用的瘫痪不影响整个系统的运行;每一个应用的自行升级对整个系统也彻底没有影响。
这并不是是终极的解决方案,只在某些特定的条件下有效。当系统规模上很是庞大,例如由若干个子系统组成;界面基本一致;子系统之间关联较少。针对这个前提,能够考虑采用这种架构。抽象出极少的、真正有效公用的信息,在系统之间经过HTTP POST.。其余的系统彻底能够独立开发、部署,甚至针对应用访问的状况进行特定的部署优化。若是不这么作,动辄上百万千万行的代码堆在一个系统中,随着时间的推移,开发者逐渐对代码失控,架构的腐化是早晚的事情。
例如,银行的财务系统,包括了十多个个子系统,包括薪资、资产、报表等等模块,每一部分功能都相对独立而且复杂。整个系统若是按照这种方式拆分,就可以实现单点优化而无需从新启动整个应用。针对每一个应用,开发者可以在更小的代码内采用本身熟悉的技术方案,从而减小架构腐化的可能。
结语
没有糟糕的架构,变化使之
我访问过不少团队。在不少项目开始的时候,他们花不少时间在选择用何种技术体系,何种架构,乃至何种IDE。就像小孩子选择本身钟爱的玩具,我相信不管过程如何,团队最终都会欣然选择他们所选择的,而且坚信他们的选择没有错误。事实也确实如此。在项目的开始阶段很难有真正的架构挑战。困难的地方在于,随着时间的增加,人们会忘记;有不少的人加入,他们须要理解旧代码的同时完成新功能;每一次代码量的突破,都会引发架构的不适应;这些不适应包括:新功能引入变得困难,新人难以迅速上手;构建时间变长等等。这些可否引发团队的警觉,而且采起结构性的解决方案而不是临时性的。
关于文档
不少人说敏捷不提倡文档。他们说文档很难写。他们说开发人员写不了文档。因而就没有文档。
奇怪的是我看到的状况却不是这样。程序写得优秀的人,写起文字来也很不错。ThoughtBlogs上绝大多数都是程序员,不少人的文字写得都很赞。
而项目中的文档每每少得可怜。新人来了老是一头雾水。使人奇怪的是,新人可以一天或者两天以内经过阅读RSpec或者JBehave迅速了解这些工具的使用,到了团队里面却没有了文档。
抛开项目持续运转并交付的特性不谈,我认为巨大的、不稳定的代码库是文档迅速失效的根源。若是咱们可以按照上述的解决方案,将代码库缩小,那么独立出来的模块或者应用就有机会在更小的范围内具有更独特的价值。想象一下如今的Rails3/Spring框架,他们每每有超过20个第三方依赖,咱们却没有以为理解困难,最重要的缘由是依赖隔离以后,这些模块有了独立的文档能够学习。
企业级项目也能够如此。
建立应用程序的生态环境,而非单一的项目
功能老是不断的、不断的加到同一个产品中。这绝不奇怪。然而经过咱们前面的分析,咱们应当从新思考这个常识。是建立一个日益庞大的、缓慢的、毫无生机的产品,仍是将其有机分解,成为一个生机勃勃的具备不一样依赖的生态系统?项目的各方人员(包括业务用户、架构师、开发者)应当从短视的眼光中走出来,着眼于建立可持续的应用程序生态系统。
若是想学习Java工程化、高性能及分布式、深刻浅出。性能调优、Spring,MyBatis,Netty源码分析的朋友能够加个人Java高级架构进阶群:180705916,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给你们