2019独角兽企业重金招聘Python工程师标准>>>
文章来自 在外企和互联网碰撞的猴子 的微信公共账号。是我们跨境电商项目的架构师写的。我们项目的每个点读提到了,方便记录查找,我转载一下。
背景
针对如火如荼的跨境电商行业,催生了提供一个SaaS电商平台为一些传统企业和机构开展跨境电商业务的市场机会,所以我们有机会来做这件事情。今天重点讨论销售和运营系统,不涉及跨境贸易价值链中的采购、通关、购汇结汇以及支撑电商业务的CRM,OMS,ERP等系统。
团队成员-最初由3名具有比较丰富企业级电商平台开发经验(>6年)的资深工程师组成,经过1年发展,团队规模成长到30+工程师。
架构准则
1. 领域驱动 – 基于自己对企业级电商平台设计和开发的丰富经验,进行领域建模,并将领域映射到业务子系统,划分高阶系统边界
2. 分布式、服务化 - 吸取企业级电商平台的经验教训,各业务子系统独立开发、测试和部署,子系统以服务暴露自己的能力,子系统与子系统之间以服务进行通信和交互。这一点可能会有些争议,在业务发展初期,整个系统的容量不需要那么大,分布式是不是设计过度,特别是在部署上对一个小团队来讲也很难驾驭。在项目中途我也曾经深深的质疑过这一点,但是慢慢也体会到它带来的好处,放开分布式架构带来的未来系统更容易更灵活的优势暂且不谈,至少每个业务子系统能比较独立高效的开发,测试和部署,在一个高速发展的团队,不可避免的沟通会成为一个巨大的挑战,这种独立性可以很大程度减少一些艰难和低效的沟通。
3. 持续集成 – 从第一天开始建立持续集成的体系和流程,保持开发、测试、部署的高效性
第一阶段(2个月)
在人员有限的情况下,以一个业务领域切入,构建业务子系统,确定基础技术选型,提供业务子系统的样本项目工程和框架供后续业务子系统开发参考。因为交易是电商平台最重要的一个业务领域,并且涉及到跟几乎所有其他业务领域的交互,我们选择交易子系统为切入点。
基础技术选型:Java+Spring作为服务开发的基础框架,MySQL存储交易数据,RestEasy作为REST服务框架,子系统间的服务调用使用Hessian。因为团队成员曾经在淘宝交易平台的工作经历,整个服务框架基础技术栈的的选型深受阿里的影响。
1. 一个业务子系统由-app, -client, -server三个项目组成。其中app是controller层,通过RestEasy把服务以REST接口发布出去;client暴露了业务服务接口及Domain对象;server提供业务服务的实现。这里app不会直接调用server,app需要调用业务服务也是以client为入口。这里主要的考虑是基础服务和应用服务这两级服务的概念。这个概念相信了解淘系系统的人不会陌生,基础服务只提供对核心领域对象的基本操作(典型的增删改查及基于此的一些基本操作),应用服务一般对应一个业务场景,需要调用一个或多个基础服务,进行串联、聚合或其它计算处理。例如:购物车的增删改查是典型的基础服务,而计算购物车时一个典型的应用服务。这样设计,随着业务量的增长,系统未来更容易向微服务架构演进。
2. 业务子系统打包成WAR部署到Tomcat需要一个web项目,-web,Web项目里,包含datasource,hessian服务发布,声明包含哪些业务子系统,以及配置文件。因为在项目初期我们需要支撑的业务量还没达到一定的规模,出于成本控制的考虑我们需要将多个业务子系统打包发布到一个WAR中,所以这里出现了需要在Web项目中声明包含哪些业务子系统。未来如果需要从一个WAR中将一些业务子系统拆出来单独发布,也比较灵活。
3. 服务调用框架,说到这里,大家第一反应肯定是Dubbo,在项目初期,我们并未使用Dubbo的方案,主要是考虑到团队成员并没有Dubbo使用的经验,而且在我们并未产生大规模分布式服务和服务治理的需求时,Dubbo的使用在项目初期反而可能成为我们的不可承受之重,把Dubbo用起来并不难,难的是如何驾驭它。所以我们选择使用DNS+Load Balance来做服务发现,基于动态代理实现了Hessian/Local/Mock的服务调用机制(Local主要用于不同业务子系统部署到一个WAR中,Mock主要用于UT),每个Web项目提供一个服务调用的配置文件,配置这个Web项目要调用的服务的调用机制和服务endpoint,因为我们使用DNS+Load Balance来做服务发现,所以服务endpoint都是Load Balance的地址。
4. 分布式日志,分布式服务调用如何跟踪一个请求的整个服务调用链条,是我们必须要解决的问题,基于Google的Dapper论文,淘宝实现了EagleEye。其实Dapper的原理并不难理解,我们选择自己实现请求的global ID并在分布式服务调用间进行透传,与ELK(ElasticSearch+Logstash+Kibana)相结合,可以比较清晰的呈现一个外部API请求跨系统服务调用的所有日志。
5. 缓存的使用,对于内容型数据(例如:价格,商品税率),使用Redis缓存,这里特别要注意防缓存穿透(当数据库记录本身不存在每次都穿透缓存去读数据库);对于配置型数据,使用Local JVM缓存(未使用EhCahce,自己实现MRU和缓存空间设定,主要原因是以前项目有相应的积累,可重用);对于操作型数据(例如:购物车,库存),使用Redis缓存,操作型数据里,特别是购物车读写同样频繁,在高并发环境下容易因为缓存更新失败而导致缓存和数据库数据不一致,需要使用MQ记录不一致情形并通过Listener应用执行缓存失效。
6. 图片服务器的使用,团队小伙伴以前有大规模应用TFS的经验,给我反馈是TFS算是淘宝开源作品里为数不多的精品,刹那间我是有点心动的。现实马上打醒我,小伙伴需要去做其它更重要的事情,TFS这玩意,等我们图片规模大到一定程度必须自己来做图片服务器和存储系统再考虑吧。于是我们选择了七牛,很完整的图片管理和CDN服务,算下来一年的价格也不贵。当然第三方服务始终是不可靠的,我们也专门引入了assets的业务子系统管理图片及图片在服务器上的元信息,这样在将来比较容易的迁移到其它图片云服务或者自己使用Nginx+TFS。
第二个阶段(4个月)
随着人员的逐步到位,各个业务领域的业务子系统开始铺开进行开发,这个时候如何协调开发人员以统一的风格来实现各个业务子系统就显得格外重要了。而且,在基础框架和工具层面,也要尽量统一,减少维护的成本,所以,我们在开发团队里一直有一个由精兵强将组成的小组负责这一部分。
1. 数据库规范:包括每个业务对象对应的数据库表如何设定主键,唯一ID,唯一性索引,每张数据库的预留字段(创建时间,更新时间,创建人,更新人,乐观锁计数等)。
2. REST API规范:增删改查的URI pattern,特殊POST操作的URI pattern,API版本。
3. 通用参数规范:对语言,货币,请求ID,调用者等的统一命名和处理。
4. 领域边界划分:在划分基础领域边界的基础上,为了提高性能,减少不必要的远程服务调用,在业务允许的情况下,进行一定的冗余。例如:在购物车中不仅仅存储商品SKU ID,还包括商品的名称,商品缩略图链接,店铺名称等信息,避免在查询购物车时再去调用商品API获取这些信息。
5. 异常处理:提供异常基础类,基于异常基础类提供异常处理框架在业务服务层和REST服务层对异常进行统一处理。各业务子系统仅需扩展异常基础类,在业务代码中抛出这些异常即可。异常处理框架帮助处理剩下的事情。这样对于API调用者来说也是极其友好的。
6. 配置管理:在系统还没有发展到一定规模时,可能还不需要一个集中的配置中心。但是,每个业务子系统切忌分散管理配置信息,因为开发,测试,预发布,线上各种环境需要的配置很可能是不相同的,分散管理的配置信息在每次部署一个新的环境简直是噩梦。目前,因为我们已经的系统规模已经发展到10+个业务子系统,即使每个子系统都集中管理配置信息,如何高效的管理各个不同环境的配置信息已经成为一个非常突出的问题,我们基于disconf已经做了POC,计划在下一个阶段把disconf用起来。我本人是Netflix OSS的粉丝,本来Netflix Archiaus是我的首选,但是考虑最近接触的国内开源的工具越来越好,社区也越来越活跃,我们果断的选择了disconf。
7. Session:在这个方面我们是走了一些弯路的,已开始我们有规划专门的session service,并且与shiro做整合,后面的session信息存到Redis中,这个已经基本开发完毕。但是在项目实际演进的过程中,这个阶段我们的前端应用里PC端和移动端商城都是基于PHP开发(基于Yii Framework),在PHP里已经有比较完整的session处理,我们后端的业务服务完全是无状态的,在这里再去使用这个统一的session service反到在项目时间特别紧张的情况下给PHP开发造成很多困扰,所以,我们决定短期内放弃session service,全权交由PHP来处理。在未来系统演进到前段多应用,甚至不同技术栈时在重新把它找回来。
8. Scheduler和任务处理:对淘宝了解的同学一定听说过他们早期开源的TBScheduler和现在正在使用的TTD,还有最近当当开源的Elastic-Job,可是,对于一个刚刚起步的业务系统,对于这种任务处理还没到需要分布式的地步,所以,我们选择了从Quartz和Spring Batch切入,但是Quartz本身只负责调度本身,我们还是需要存储一些任务的详细信息和状态,要支持异步任务的回调通知状态,排他性任务,已经简单的任务流程编排,我们按照自己的需求,基于Quartz做了扩展支持这些功能。未来如果业务发展需要我们引入分布式任务处理的机制,我想Elastic-Job会是我的第一选择。
9. 统一登录服务:今天我相信在Java的世界里大家要实现统一登录服务大部分人会第一选择会是CAS。CAS对各种认证协议的支持未尝完备,后面也很容易挂自己实现的credential数据库。不过比较讨厌的是,CAS自带前台登录页面,这部分是基于JSP的,而我们的前端工程师全是PHP的,改造起来那叫一个费劲。虽然我们可以放弃它自带的JSP页面直接调用CAS的API,但是处理起来过于复杂,对于我们这个规模有限的初创团队有点不可承受之重。另外,目前我们团队在注册这个环节是直接绕过了CAS调用用户子系统的注册API,对于PHP同学们来讲也是很高兴的。
10. 非Java业务子系统:在电商平台中,从业务需求来讲,促销是非常复杂的一个业务子系统,各种不同的促销条件,促销次数限制,促销之间的互斥和组合,促销金额计算,和优惠券或优惠码结合使用,其实这需要一个高效的规则引擎,一方面能够比较清晰的定义这些规则,另一方面在执行期也能够在毫秒级能够完成对一个订单的促销计算,传统的Java+关系数据库建模是很难搞定的。已开始我们想基于一个开源的规则引擎来做,所以我们队Drools做了一定的研究,作为一个通用的规则引擎,Drools功能比较强大,但是也比较复杂,在我们只有1个开发工程师能铺到这个领域的情况下,也很难搞定;另外,Drools在实时规则计算的效率上也是一个很大的问号,而且促销管理业务工具对我们是非常重要的,基于Drools的规则定义语言来开发这个工具难度太高。我们最终选择了DSL,定义一套促销这个领域的专有语言,用JRuby来实现促销DSL的语义模型和语法解析器,一来Ruby的一些声明式语法特性是非常适合来定义DSL的语义模型和进行语法解析,另一方面JRuby运行在JVM中可以和各种Java库无缝整合。促销引擎分为4个子系统:DSL引擎-负责管理促销DSL定义,解析DSL转化为语义模式;预计算引擎-负责对促销规则进行预计算,将商品和其可能应用的促销进行关联(提高runtime计算的效率);runtime计算引擎-在线为一个订单计算促销结果。优惠券引擎 – 处理优惠券和优惠码的生成,管理和使用。目前,我们的促销规则DSL定义,预计算结果和优惠券信息均使用MongoDB进行存储。其实即使这样,业务管理工具来管理DSL来表达的促销规则还是一个挑战,我们在两者直接加入了一层meta data层来做映射缓解这个问题,这就意味着业务管理工具只是DSL表达能力的一个子集,对于一些高级的促销,我们直接在工具上开放了DSL编辑器直接对DSL进行编辑。另外,促销引擎的4个子系统都已经利用Docker进行部署,也为我们系统向容器化部署演进打下了基础。
11. 前台商城系统和CMS:本身我自己在CMS上有一些研究,对WordPress, Joomla和Drupal都有一定的了解,加上电商运营的特点,对CMS的要求特别强,我一开始是打算在Drupal上往前走的。而且我们在Drupal上也做了投资,研究学习,POC,前前后后也有小三个月,但是慢慢我也发现一个问题:Drupal本身也是很完整的一个系统,加上生态系统,和电商的领域已经有很多交集,这给我的小伙伴造成了很多困扰,他一直在质疑为什么产品目录树,购物车这些Drupal都有现成的插件我们还要再去实现一套后端服务。虽然从架构上很容易解释,但是现实是当你每天面对这样一个系统时是难免会很纠结的。后来,我们找到了一个既有丰富的Drupal经验,也经历了去Drupal,自主开发CMS的小伙伴,一番沟通和探讨,我们决定放弃Druple,自己干。
我这里贴一下我的小伙伴给我的建议吧:drupal善于做cms,模块固然成熟,但是牺牲了性能为代价。我们的业务场景更加灵活丰富,结合了传统pc,线下收银,以及移动,o2o等各种场景。单纯的依靠定制模块开发将会是事情变得更加复杂且不可控。 采用一个流行且易用的开源框架,使得我们在迅速迭代与稳定架构之间获得了更好的平衡,同时也能更好的结合中国特色的市场需求。通过框架的crud功能,我们迅速完成了传统的cms模块。而通过mvc最流行的业务分层模式,我们更好的通过m层同后台进行了松耦合式的开发体验。使得整个系统既可以脱离应用层纵向拓展,也可以通过其他各种应用层架构(node.js agular.js)实现横向的拓展。drupal的缺点还有一些,比如社区陈旧,模块数量虽多但是优质模块很少。性能上因为其自身机制,当系统复杂之后容易产生性能瓶颈,且不利于开发人员trace。 如果采用drupal,我们将被迫用5-10年的senior phper去学习一个在中国并不流行的技术架构。基本是很难找到人的。换开源php框架之后,那么,只需要有几个senior的开发人员进行组织整体架构并review,就能利用中国大量的码农(coder)。
12. 业务管理工具:AngularJS+Bootstrap,这个应该没有太多争议,虽然现如今React.js如火如荼,可是在我们那个时候Angular还是更靠谱的选择,另外我们的工具应用选用了Spring的AppFuse框架,让他们能够更快速的开发前台工具应用,并方便的处理访问控制,多语言,前后台交互等问题。
单独的话题
1. 单元测试 – 节奏再快,资源再短缺,我还是坚守底线,大家做好UT,而且是基于mock的UT,前期在UT的投资虽然对于快速迭代的节奏来讲往往都会有冲突,可是它带来的后期维护的便捷和高效往往是值得我们付出的。
2. 持续集成 – 从第一天,我们就投入一个专职工程师进行来负责持续集成,Jenkins, Maven repostiory, Git的搭建,持续集成脚本的开发,持续集成流程的建立。这些在很多小团队看来不必要的投资其实随着项目的执行其带来的好处是会不断被放大的。我们采用chef来生成操作系统和中间件级别的标准image(例如:Tomcat, Nginx, MySQL, Redis, MQ, ElasticSearch等等的标准image),但是我们的持续发布脚本并未使用chef来编写,而是直接用ruby来实现的,这个主要原因是直接用ruby提供了更大的灵活性,我们在以前的项目中也有一定的积累。
3. 测试 – 很遗憾,我们的测试资源是在有限,在保系统功能的基本准则下,我们牺牲了API级别的功能测试,前期只能靠UT来保障了。这也有很多无奈,如果有一天测试资源跟上,需要第一时间把API自动化测试,和基于场景的自动化测试不上。
在路上
1. 配置中心,目前很痛的一个环节,前文已经提到,我们已完成disconf的POC,接下来就会使用。
2. 服务发现和调用框架:用不用dubbo?其实有其它更轻量的选择(例如:consul做服务注册和发现,hystrix/turbine做故障隔离和服务metris),我们现在的实现已经很小清新。另一个角度dubbo在国内的生态的确不错,基本上主流电商公司都在使用。这个留着慢慢纠结吧。
3. 错误数据校正平台:在高并发的场景总是不可避免出现一些异常情况造成错误数据,建立一套错误数据校正平台是很重要的,要知道客户投诉起来,运营抱怨起来,往往技术团队是最惨的。
4. 全docker化:目前除了促销引擎的4个业务子系统,其他还是使用的青云上的linux虚机,考虑到初期业务不大要节省服务器成本,我们不得不将一些业务子系统合并到一个WAR里面发布,如果每个业务子系统都能够docker化,就不需要这么费劲了。
5. API网关:目前我们的业务服务已经有需求要被外部系统调用(例如:联盟营销系统),如何将内部的业务服务暴露出去变成一个课题,我们需要引入API网关来解决这个问题,目前我们已经开始调研zuul。
6. 多租户模式下不同客户的个性化定制:目前我们只能做到前端商城的个性化定制和外部系统集成的个性化定制,核心业务层如何做个性化定制是需要解决的,我们以前有做传统企业级电商平台的定制框架的经验,但是在SaaS多租户的场景下这个将会是个巨大的挑战。
7. 压测体系:SaaS和传统企业电商平台的压测是很不一样的,如果评估线上环境的容量,如何持续的对线上环境做压测,如何模拟真实流量,现在这些一二线互联网电商平台已经积累了足够的经验,我们要做的,就是一步一步,吸取人家的精华,结合自身的情况,执行起来。
8. 运维体系:这是一个比较大的话题,我们可以单独再讨论。