公司决定将支付业务从原来所在部门剥离出来,成为一个独立的团队,以应付迅速发展的业务需求。原团队负责支付系统开发的几位同学转到现团队,形成开发班底。此后开始招聘,三个月团队扩充到10多个人。与此同时,公司业务也在快速发展,6月份宣布会员突破2千万。一些热片上映往往也会引发会员注册缴费的小高峰。其他业务,包括直播,阅读,动漫等,也都进入了发展的快车道。每天订单量早已经超过百万,比去年某片上映时把系统打垮时还早高。移动端每个月发布一个版本,桌面则是半个月。产品经理们夜以继日地规划各种功能,待开发功能都排到好几个月之后。而随着项目团队的日益扩大,却出现一些奇怪的事情:
-
开发效率和以前没有太大区别,尽管队伍扩大了4倍多,人员素质则有所提升。
-
大部分开发工作还是几位老员工在忙,新员工还比较难介入核心开发工作。
除了管理因素,作为工程师,我们还是期待从技术上找到根源所在,解决问题,提高效率。最终的决策,是使用微服务架构来重构现有系统。这一系列博文,描述在这过程中我们做的选择、取得的成果、走过的弯路,以及经验教训。
一、原有架构
从技术角度看,原有系统是一个基于SSH架构的传统实现,软件架构整体上是大家所熟知的多层Java软件架构:
代码让人看的非常怀旧,虽然开发人员和我说是4年前开发的,但这熟悉的SSH架构,可是妥妥10年前的东西。使用Apache Struts做展示层,对数据访问层做个简单封装实现业务逻辑层,基于Spring 的AOP以及Hibernate实现数据访问。 数据保存在MySQL中,单库多表的结构。
二、架构问题
-
DAO层
使用Hibernate来封装数据库访问操作。其优势是在面向对象领域,通过系统自动生成数据库访问语句的方式,使得开发者无需考虑数据库的实现细节,专注于对象的设计和使用,简化了开发工作。另外,使用Hibernate还支持系统可以在不同类型的对象数据库间无缝迁移。在业务对象关系复杂的管理系统开发中广泛使用。存在的问题是,它隐藏了数据库的实现细节,这导致在大数据场景下开发人员很难对数据库访问进行优化,而这却是互联网应用开发的重点。
-
Service层
业务逻辑层为Controller层提供具体业务的实现。但在实现上,问题还不少。如果是严格按照分层架构来实现,对业务逻辑层进行拆分,将本地调用变成远程调用,即可比较容易实现拆分。但实际中往往会碰到如下问题:
这个层往往并未实现单向依赖,部分业务逻辑层实现被注入了接口层的参数(request,response),使其耦合到接口层。
为了应对不断变更的需求,不少接口会使用map作为输入输出参数,此类接口在维护时无法约束其参数。
Service层绝大部分实现是使用工厂模式来管理数据对象。仅对工厂类建模,未对业务实体建模。这个层的实现是不完整的。 这使得对业务实体的操作需要推迟到Controller层来实现,导致Controller臃肿。
当服务之间存在大量依赖关系时,开发人员往往会直接将Spring BeanFactory注入到各个服务中,或者简单封装一个FacadeService,通过这个Service可以访问到所有的业务逻辑对象。这个类的使用导致无法评估Controller层对Service业务对象的具体依赖。
-
Controller层
基于Apache Struts来实现, Apache Struts 漏洞频繁爆发,修复慢。当前已经很少在对外的应用中使用了。由于Service层实现上的问题,Controller层承担了部分业务逻辑实现,使其臃肿,难以测试。
三、功能问题
从功能模块上来看,并没有区分对端的服务以及对运营管理系统的服务,仅实现了支付系统的基本功能:
四、实施问题
-
可扩展性差,性能提升困难
web应用性能瓶颈基本都在数据库上。这个系统使用mysql作为数据库。三个应用对应三个数据库。没有读写分离。读写都在一个库上操作。数据量最大的表当时在5000万条数据。高峰期数据库操作的QPS在1000左右,压测结果是可以支撑2000的QPS。这个指标令我诧异。为什么能有这么好的性能?首先是,没有复杂的查询逻辑,所有查询都在一个表里操作,没有跨表事务处理,复杂的处理,分解为多个语句来执行。最复杂的一个action中,执行了将近20次数据查询。其次,也是最重要的因素,这里用的是SSD磁盘。从目前情况看,撑到年底应该是可以的,这也为我们技改争取了足够的时间。尽管这样,对mysql还是没有把握。每次运营部门搞活动,我们都玩胆战心惊地盯着,祈祷活动不要太有效果。
从应用层来看,目前读写比在10:1,接口日访问量10亿。高峰期访问量在300QPS。公司业务增长迅猛,数据量半年翻一番,访问量预估10倍增长。还有一个严峻的挑战,产品同学扬言要搞秒杀,秒杀…每秒十万的量必须支持到。这就超过MYSQL能承受的压力范围,需要把读操作切到内存数据库上,但是在SSH架构下,读写分离实现就得伤筋动骨了。另外由于Hibernate封装了对数据库的操作,不用写SQL了,精细优化也搞不定了。每次系统变慢,就得求DBA,帮看看有那些SQL被卡住了。每隔一段时间,还得请DBA导出SQL语句,研究怎么建索引。
-
系统臃肿,学习周期长
100多个接口,分为三个大项目。最大项目有1300多个类,其次是600多个和300多个类。SSH架构,SVN版本控制,resin作为容器,Nginx前置路由。路由这个让人欣慰,它是整个重构工作的有力支撑。纯后端的项目,为移动端app,PCWEB应用提供接口。这也使得重构工作难度大大降低。如果把前端也耦合进来,那就更酸爽了。
庞大的系统规模为团队成员接手带来困难。 支付业务独立出来后,开发人员从原来的5人,在2个月内扩充到10人。与此同时,兴奋的产品同学也都跟打鸡血一样,各种想法纷纷变为产品,开发压力骤增。但是新增的同学,看着几百个类,往往一片茫然,无法下手。不知道哪些功能实现了,哪些功能是待改进的。一直到3个月后,新员工才逐步进入角色。尽管如此,还是有不少恐龙级代码,无人敢挑战。最大的一个类的规模是2000多行, 核心方法超过500行,大量重复代码, 每次调整都以失败告终。
-
合作成本高
随着项目组人员增加,每次新版本开发都需要多人一起合作,修改同一个项目代码。 虽然使用版本控制工具来对分支进行管理,但是不可避免的,大量的时间花费在代码冲突处理上。新增功能,增强功能,bug修复,支持各种客户端,都在一个项目上进行,需要建立不同的分支,高峰期五六个分支同时进行都是常见的。这种情况下,代码冲突的频率非常高。一个周的小版本开发,1天时间在解决冲突都是很正常的。
-
测试难度大
测试工作也逐步的恶化了。
测试环境构建难度高。随着分支的增加,每个进入测试的分支,都需要准备独立的测试环境。环境构建成本高。
刚测试完的功能,由于分支合并冲突处理,又得重新跑一遍。严重影响项目进度。
-
上线风险高
随着系统复杂度的增加,上线风险也越来也大。一个小功能的修改,打印一个日志,修复一个bug,都需要整体上线。一旦有一个地方修改错了,这个系统就崩溃了。上线时间长,一次上线,半个小时是必须的。
-
引入新技术困难
互联网公司对新技术的追求和使用显得特别饥渴,SSH框架降低开发难度和成本同时,也屏蔽了其他技术的导入。缓存机制,数据库优化,读写分离等,SSH有自己的一套逻辑体系,要调整姿势,成本相对高,技术难度也大,需要对实现底层有深入了解。
CONWAY’S LAW
很长一段时间,这个系统是2-3位开发人员在维护,对外接口、运营系统,都混杂在一起实现,访问量也不算大。3个独立系统,对应3个版本库,每个人负责1-2个系统。当有新功能添加到系统中的时候,大家优先考虑的是如何对现有系统进行改进,而不会考虑是否需要建立新系统。而当公司做业务调整,人员迅速增加后,原有的合作方式,就需要变更了。这就应了所谓的康威定律:
Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization’s communication structure.
我们需要一套新的机制来应对新形式下的系统演化的需求。
五、分层与共享库
避免对原有系统作大规模调整,我们首先考虑的是利用原系统分层实现的特点,实现基于层的分工。在实践方面,以前负责的管理系统的开发项目,使用SSH架构的,大部分是采用基于分层的分工:
将业务逻辑层、数据访问层、数据表示层封装为可以独立维护的库;
将接口层按照业务来拆分,将代码依赖调整为库依赖。
为各个独立的库和项目建立各自的代码库。
层之间通过接口来交互,基础层通过单元测试来保证质量,
这种分层的优势在于能够很好地解决学习周期的问题。每个层的技术相对独立,开发人员可以快速上手。
DAO层相对简单,因而对于团队中的新手,可以从这个层入手,熟悉系统架构和软件过程;
业务逻辑层是整个系统的核心,由老员工来负责,对上可以协助显示层的开发,对下可以指导DAO层的新同学。
显示层需要对HTML,CSS等技术要有所了解。
这种分工,适合10人以内的、一同办公的团队。团队之间是紧耦合的合作关系。对于大型项目,首先需要对项目按照业务进行切分,每个子项目分配到10人以内的团队来完成,之后对每个团队,采用分层的分工。但采用这种合作方式,存在的问题是:
要求有很好的系统架构设计。需要在编码启动前,将各层的接口、数据库结构确定下来。而这对轻架构的互联网应用开发来说几乎是不现实的。不少互联网公司甚至都没有架构师的角色,有架构师的公司,还有不少是形同虚设的。
团队内部沟通成本高。层与层之间是紧耦合的关系,对接口的修改必须通知到所有使用方。这要求开发人员之间建立稳定的合作关系,通过约定俗成的规则,降低沟通成本。
上述各种问题仍然存在。基础库的变更,都需要对线上的系统更新库之后重新上线。
六、微服务
在开始支付项目改造之前,我们刚刚完成了公司数据仓库项目的微服务架构改进。这个项目实施详细过程,在dockone社区做了分享,详情参见这里。 我们认为调整为微服务架构可以解决上述问题。
-
性能问题
对于性能要求高的接口,可以通过建立数据缓存的方式进行优化。
-
学习周期
一个项目仅包含少数紧耦合的接口,接口的业务逻辑单一,开发人员1-2小时通读代码,即可快速上手。
-
合作成本
每个项目相对独立,项目之间仅通过接口来交互。确定完接口后,开发、测试、上线,都是独立进行的,从而降低了沟通成本。
-
版本控制
由于项目之间是接口依赖而不是代码依赖,每个项目都可以建立独立的代码库。同时项目切分的比较细,每个项目开发时,仅会有一个开发人员对其做修改。这基本就不存在代码合并工作,也避免了代码合并过程中的各种问题。实际上,基于微服务架构的开发,我们并没有采用分支策略,而是直接用主干开发。
-
测试难度
每个项目独立部署、独立测试。由于消除了代码分支,没有代码合并的隐患,重复测试的工作量减少了。
-
上线风险
每个项目独立上线,就算出现问题了,也仅影响到少数接口。
-
新技术
在微服务改造进行一个季度后,各种新技术被引入到系统中,开发不再局限于SSH架构。Spark, Hadoop, Hbase等大数据处理相关的技术,Couchbase, Redis等缓存系统,都开始在项目中使用,并有效地解决的业务上存在的问题。
当然,有利必有弊,微服务带来的问题,也不少,包括项目多、出问题时排查难等,在实施过程中,也积累了不少的经验。这些问题,将在后续的分享中逐步做介绍。