tulunta 2017-07-31
2014年公司所有业务(交易,商品,ump,支付等等)都在一个单体应用中完成,使用php开发,满足了公司快速发展(我们姑且称为v1.0)。
2015年到2016期间,随着业务流量增长,现有架构模式遇到了挑战,公司开始朝着业务拆分和服务化方向迈进。开始采用java作为开发语言,服务化框架使用公司改进过的dubbox,支持跨语言服务调用的nova框架(v2.0)。
2017年在服务化的基础上我们更近一步,向微服务架构渐变。拥抱社区提供的丰富组件(v3.0)。
第一个问题很棘手,我们先来说说 架构升级会做些什么 .
要明白做什么,首先需要考虑目标是什么?软件架构的目标是要设计软件系统来解决问题,所以架构要做的事从抽象的维度上看,就是:
上面是大的抽象原则,更具体一些来说,架构做得就是结构设计,在不同维度和层次上:
架构执行过程中可能会出现一些新问题,是在当初的架构设计中未能考虑到的,需要对此做分析判断,并形成新的决策调整。而另一些问题,也许是执行过程中的走样,导致和当初的决策形成了偏差。架构师需要考虑所有这些关注点,并和开发工程师找到解决这些关注点的各种选项,在适当的时候根据真实环境的情景去采取合适的行动。有时,我们称这些行动叫作:重构或优化。当一个旧系统长期没有这样的行动,积累久了后,我们将迫不得已采取另外一种行动,我们称之为 —— 架构升级。
软件系统或架构,不像建筑物会因为时间的流逝而自然耗损腐坏,它只会因为变化而腐坏。一开始清晰整洁的架构与实现随着需求的变化而不断变得浑浊、混乱。计算机科学都爱借用一个物理学的术语「熵」,它表达体系的混乱程度,而软件系统的「熵」很容易不经意间随着需求的变化而变得更高。
软件系统「熵」有个临界值,当达到并超过临界值后,软件系统的生命也基本到头了。这时,我们就要采取那个迫不得已的行动了。图例展示了软件系统「熵」值的生命周期变化。
所以,不是所有的大型系统都是被很好的设计的,想要设计好一个巨型系统是非常困难的,而随着业务功能的叠加,原先的设计也会被堆砌的代码所淹没,以至打破原先的设计。我们所能掌控的是一个有着特定边界的系统,所以根据业务属性拆分系统,将其限定在一个有边界的上下文中(Bouded Context),是一个最直观也是最有效的方法。这也是领域驱动设计所追求的。在DDD欧洲大会上Eric也认可近年流行的微服务架构有个很大的优势, 服务粒度合适,服务物理隔离 ,单个服务的「熵」增问题被局限在单个微服务内部。单个微服务的替换与重构成本十分有限,使得「熵」增问题局部化,不容易传染全局,以致失控。当然这有个前提,就是微服务的拆分和接口交互要合理,合理的检验标准就是随需求变化,总是实现变化或接口新增,而非总是调整接口交互。 架构始于系统生命之初,并伴随系统生命周期全程。每次需求变化带来的变动都应进行一次或大或小的重新架构过程。架构的关注点在于控制软件系统变动时「熵」值的变化。
借鉴spring bom的做法,建立youzan bom,版本统一管理,彻底解决版本混乱问题。针对各个应用中重复配置问题,建立youzan-boot-parent,消除重复,无需各个应用间copy。另外还额外带来一个好处,方便统一升级。
另外,针对我们现有的运维环境,标准化了4套环境:开发,测试,预发,线上。
我们针对publish api jar deploy到maven仓库中做了严格的限制,api jar本应只包含一些DTO和一些接口,但由于开门人员经常是复制粘贴,也会把各种不需要的依赖(比如spring,各种log框架等) 加入到api中,导致使用该jar的应用方发生依赖冲突,通过会花一些不必要的时间来找到冲突并解决冲突,我们希望通过技术手段来做最后一道防线,从源头上解决因为依赖其他系统api jar而导致的依赖冲突。
我们希望应用对一些开源组件和公司自己开发的组件使用起来更简单,降低接入成本。为此我们拓展了spring boot的autoconfiger,添加了各种starter。
举个例子,如果我们想使用nova框架,只需一个注解@EnableNova即可
curl http://127.0.0.1:8080/health
{ status: "UP", diskSpace: { status: "UP", total: 249779191808, free: 61591195648, threshold: 10485760 }, redis: { status: "UP", version: "3.0.3" }, db: { status: "UP", database: "MySQL", hello: 1 }, refreshScope: { status: "UP" }, hystrix: { status: "UP" } }
分布式系统设计中,有一条很重要的原则就是:为失败而设计,错误一定会发生。 为了防止系统出现级连失败,我们需要对依赖的服务所能够使用的资源做一定限制,保护应用本身。通常以下目标都是要考虑的:
下面简单介绍下hystrix实现原理,具体内容请参考 官方文档
体现在代码上
@HystrixCommand(groupKey = "RiskGroup", commandKey = "RiskClient-containsSensitiveWord", fallbackMethod = "fallback", commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000") }) public boolean containsSensitiveWord(List<String> words) { if(skipSensitiveWordValidation()){ return false; } PlainResult<Boolean> result = sensitiveWordFilter.containSensitiveWord(words, RECEIPT_SCENE); LazyLogs.info(logger, "RiskClient.containsSensitiveWord({}), result={}", () -> words, () -> JSON.toJSONString(result)); return result.getData(); } /** * 风控接口调用出现异常,则降级,是弱依赖 */ public boolean fallback(List<String> words, Throwable e) { logger.warn("RiskClient.containsSensitiveWord({}) fallback", words, e); return false; }
通过配置中心可以动态控制hystrix的参数
作为api的使用者的开发经常会发现文档是过期的,甚至是错误的,据说程序员都不喜欢写文档,因为他们喜欢写代码,所以最好通过代码来自动生成文档。利用spring restdocs可以通过测试代码自动生成文档,还有一个好处时,如果接口中增加或减少字段时,如果不同步更新测试的话,测试就不会通过,这样就可以保证文档始终是最新的。
测试代码
@Test public void withdrawSummary() throws Exception { given(withdrawQueryService.queryWithdrawStatus()) .willReturn(PlainResults.success(WithdrawStatus.getStatusMap())); this.mockMvc.perform(get("/withdraw/queryWithdrawStatus")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("success").value(true)) .andExpect(jsonPath("code").value(0)) .andExpect(jsonPath("message").value("")) .andDo(document("withdraw-status", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), responseFields( subsectionWithPath("requestId").ignored().optional(), subsectionWithPath("success").description("请求结果"), subsectionWithPath("code").description("错误码,0表示无错误"), subsectionWithPath("message").description("错误提示消息,如果有错误的话"), subsectionWithPath("data").description("提现状态列表") ))); }
生成的api文档
在公司或组织内部,api提供者最大的烦恼莫过于找不到消费者到底有哪些,以及消费者是如何使用他们api的。更不要说经过一些公司人员变动之后的情况。有个真实的案例,开发人员将某个字段单词拼写错误修正回来,结果发布上线后,有个依赖方因为使用到该字段,而导致依赖方服务不可用。其实这种场景和经历发生多次后,开发人员就会畏手畏脚,对原先一些不合理的设计和错误就会不去改进它,听之任之。
其实这种问题根本原因是服务提供者与消费者协作模式的问题,我们希望有某种机制来减轻这种问题。如果服务消费方能够把使用api的场景通知给服务提供者,并落实在测试代码上,那是不是就可以让服务提供者感知到各个依赖方api使用场景。其实这是一种契约精神,服务提供方与依赖方要多多沟通交流,并将沟通交流的成果落实到测试代码上。当然了得有些得力的工具和框架来支持我们这种设想,契约测试(Contract Testing)就是来达成这些目标的。具体使用文档请参考 Spring Cloud Contract
持续集成的好处不用多说,关键在于执行下去。
curl -i -X POST -H 'Content-Type: application/json' -d '{"configuredLevel": "DEBUG"}' http://localhost:8080/loggers/com.youzan.pay
Eric Evans — Tackling Complexity in the Heart of Software
spring boot
spring cloud
https://martinfowler.com/microservices
microXchg 2017 – Juven Xu: AliExpress’ Way to Microservices
Microservices at Netflix Scale
https://jenkins.io/
BoundedContext