有赞支付微服务实践

有赞技术发展历程

2014年公司所有业务(交易,商品,ump,支付等等)都在一个单体应用中完成,使用php开发,满足了公司快速发展(我们姑且称为v1.0)。

2015年到2016期间,随着业务流量增长,现有架构模式遇到了挑战,公司开始朝着业务拆分和服务化方向迈进。开始采用java作为开发语言,服务化框架使用公司改进过的dubbox,支持跨语言服务调用的nova框架(v2.0)。

2017年在服务化的基础上我们更近一步,向微服务架构渐变。拥抱社区提供的丰富组件(v3.0)。

我们遇到的问题

随着业务的发展,团队规模也在增长,这时候v1.0单体架构遇到了挑战

  • 系统变的复杂和脆弱,架构需要升级
  • 开发成本高,学习成本高,加大了merge冲突,排队等待,故障发生率等
  • 测试成本高,修改一处,也需要回归测试
  • 应用难以做水平拓展,难以进行容量规则

第一个问题很棘手,我们先来说说架构升级会做些什么.

要明白做什么,首先需要考虑目标是什么?软件架构的目标是要设计软件系统来解决问题,所以架构要做的事从抽象的维度上看,就是:

  • 根据问题域,界定系统的边界(Eric Evans的领域驱动设计,划定bounded context)
  • 对系统进行切分,切分的目的是分工与协作(可以并行,以获得效率提升)
  • 被切分的各部分之间建立协作与沟通的原则和机制
  • 将各个部分连接合并成一个整体,完成系统的目标

上面是大的抽象原则,更具体一些来说,架构做得就是结构设计,在不同维度和层次上:

  • 高维度:是系统、子系统或服务的切分与交互结构
  • 中维度:是系统或服务内部的领域模块划分
  • 低纬度:是代码结构、数据结构、表结构,技术方案选择,开发用的爽不爽

架构执行过程中可能会出现一些新问题,是在当初的架构设计中未能考虑到的,需要对此做分析判断,并形成新的决策调整。而另一些问题,也许是执行过程中的走样,导致和当初的决策形成了偏差。架构师需要考虑所有这些关注点,并和开发工程师找到解决这些关注点的各种选项,在适当的时候根据真实环境的情景去采取合适的行动。有时,我们称这些行动叫作:重构或优化。当一个旧系统长期没有这样的行动,积累久了后,我们将迫不得已采取另外一种行动,我们称之为 —— 架构升级。

软件系统或架构,不像建筑物会因为时间的流逝而自然耗损腐坏,它只会因为变化而腐坏。一开始清晰整洁的架构与实现随着需求的变化而不断变得浑浊、混乱。计算机科学都爱借用一个物理学的术语「熵」,它表达体系的混乱程度,而软件系统的「熵」很容易不经意间随着需求的变化而变得更高。

软件系统「熵」有个临界值,当达到并超过临界值后,软件系统的生命也基本到头了。这时,我们就要采取那个迫不得已的行动了。图例展示了软件系统「熵」值的生命周期变化。

所以,不是所有的大型系统都是被很好的设计的,想要设计好一个巨型系统是非常困难的,而随着业务功能的叠加,原先的设计也会被堆砌的代码所淹没,以至打破原先的设计。我们所能掌控的是一个有着特定边界的系统,所以根据业务属性拆分系统,将其限定在一个有边界的上下文中(Bouded Context),是一个最直观也是最有效的方法。这也是领域驱动设计所追求的。在DDD欧洲大会上Eric也认可近年流行的微服务架构有个很大的优势,服务粒度合适,服务物理隔离,单个服务的「熵」增问题被局限在单个微服务内部。单个微服务的替换与重构成本十分有限,使得「熵」增问题局部化,不容易传染全局,以致失控。当然这有个前提,就是微服务的拆分和接口交互要合理,合理的检验标准就是随需求变化,总是实现变化或接口新增,而非总是调整接口交互。 架构始于系统生命之初,并伴随系统生命周期全程。每次需求变化带来的变动都应进行一次或大或小的重新架构过程。架构的关注点在于控制软件系统变动时「熵」值的变化。

按照业务领域拆分后,已经能很好地满足了业务发展。但是v2.0对开发人员负担过重,需要做一些方便架构执行的工作

  • 现有的java框架不能让开发人员只关注业务实现,框架本身没有提供一些开箱即用的三方的和公司的组件,需要大量地配置
  • 框架没有提供一些common patterns指导开发人员编写代码
  • 混乱的版本依赖
  • 项目结构不标准化
  • 应用健康检查不标准化
  • 编写测试复杂,难以持续集成

为什么选择spring boot

  • 开发体检极大地提升
  • 使应用配置变简单
  • 使编码变简单
  • 使编写测试代码更简单
  • 本地启动,方便开发调试
  • 使部署变简单,内嵌容器
  • 简单强大的spi机制,很容易拓展自定义的autoconfiguer
  • spring-boot-starter-actuator使监控更简单
  • 完善的生态圈,对主流框架无缝集成
  • 社区活跃,迭代迅速
  • 符合我们的微服务目标,方便未来容器化

解决问题

youzan pom and youzan-boot-parent

借鉴spring bom的做法,建立youzan bom,版本统一管理,彻底解决版本混乱问题。针对各个应用中重复配置问题,建立youzan-boot-parent,消除重复,无需各个应用间copy。另外还额外带来一个好处,方便统一升级。

另外,针对我们现有的运维环境,标准化了4套环境:开发,测试,预发,线上。

我们针对publish api jar deploy到maven仓库中做了严格的限制,api jar本应只包含一些DTO和一些接口,但由于开门人员经常是复制粘贴,也会把各种不需要的依赖(比如spring,各种log框架等) 加入到api中,导致使用该jar的应用方发生依赖冲突,通过会花一些不必要的时间来找到冲突并解决冲突,我们希望通过技术手段来做最后一道防线,从源头上解决因为依赖其他系统api jar而导致的依赖冲突。

youzan application

我们希望应用对一些开源组件和公司自己开发的组件使用起来更简单,降低接入成本。为此我们拓展了spring boot的autoconfiger,添加了各种starter。

  • youzan-boot-dependencies
  • youzan-boot-parent
  • youzan-boot
  • nova-spring-boot-starter
  • nsq-spring-boot-starter
  • druid-spring-boot-starter
  • mybatis-spring-boot-starter
  • chameleon-spring-boot-starter
  • youzan-boot-starter-test

举个例子,如果我们想使用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"
    }
}

面向失败设计(CircuitBreaker)

分布式系统设计中,有一条很重要的原则就是:为失败而设计,错误一定会发生。 为了防止系统出现级连失败,我们需要对依赖的服务所能够使用的资源做一定限制,保护应用本身。通常以下目标都是要考虑的:

  • 保护调用方,不因为依赖的服务(特别是网络服务)的问题(高延时和失败)而影响到调用方应用
  • 在复杂的分布式系统中阻止级联失败
  • 即时失败(fail fast)和快速恢复
  • fallback和优雅降级,如果可以的话
  • 能够实时的监控,动态调整参数和相关操作

下面简单介绍下hystrix实现原理,具体内容请参考官方文档

  • 使用HystrixCommand 或者 HystrixObservableCommand 来包装外部系统调用,通常是在单独的一个线程中执行
  • 每个dependency设置超时调用,可以自定义超时时间,也可以动态调整,通常超时时间设置的比测量的第99.5个百分位的值稍高一些
  • 为每个dependency维护一个小的线程池,当线程池满了,直接拒绝请求
  • 测量记录请求成功数,失败数,超时数和被拒绝数
  • 触发断路器,针对某个特定的服务阻止一切请求一段时间(可以手动触发也可以自动触发,自动触发规则是这个dependency的错误百分比超过阀值)
  • 当请求失败,超时,或者短路时执行fallback逻辑
  • 监控测量值和准实时配置变更

体现在代码上

@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文档的更新

作为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的场景通知给服务提供者,并落实在测试代码上,那是不是就可以让服务提供者感知到各个依赖方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  
  • 更方便地收集系统metrics数据,方便监控
  • spring cloud config/consul 配置中心
  • 服务发现/consul
  • spring cloud zuul api网关
  • spring cloud sluth & distributed tracing/zipkin 分布式tracing

参考

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

欢迎关注我们的公众号


Original url: Access
Created at: 2019-09-26 17:30:50
Category: default
Tags: none

请先后发表评论
  • 最新评论
  • 总共0条评论