微服务架构最佳实践

6,167 阅读16分钟
  • 注:文章来源:极客时间的专栏《从0开始学架构》

方法篇

服务粒度

  • 三个火枪手原则,即一个微服务三个人负责开发
  • 从系统规模来讲,3个人负责开发一个系统,系统的复杂度刚好达到每个人都能全面理解整个系统,又能够进行分工的粒度;2个人,系统的复杂度不够,开发人员可能觉得无法体现自己的技术实力;4个及以上,系统复杂度又无法让开发人员对系统的细节都了解很深
  • 从团队管理来说,3个人可以形成一个稳定的备份,即使一个人休假或者调配到其他系统,剩余2个人还可以支撑;2个人压力太大;一个人就是单点啦

拆分方法

  • 基于“三个火枪手”的理论,可以计算出拆分后合适的服务数量
1. 基于业务逻辑拆分
  • 将系统中的业务模块按照职责范围识别出来,每个单独的业务模块拆分为一个独立的服务
  • 难点问题在于,对“职责范围”的理解差异很大。例如,一个电商系统,第一种方式是将服务划分为“商品”“交易”“用户”3个服务,第二种方式是划分为“商品”“订单”“支付”“发货”“卖家”“买家”6个服务,哪种方式更合理?
  • 困惑在于从业务的角度来拆分,规模粗和细都没有问题,因为拆分基础都是业务逻辑,要判断拆分粒度,不能从业务逻辑角度,根据“三个火枪手”原则,计算一下大概的服务范围
  • 例如,有10个人,按以上原则,大约需要划分4个服务,那么“登录、注册、用户信息管理”都可以划到“用户服务”职责范围内;如果团队规模是100人支撑服务,服务数量可以达到40个,那么“用户登录”就是一个服务了;如果团队规模达到1000人支撑业务,那“用户连接管理”可能就是一个独立的服务了
2. 基于可扩展拆分
  • 将系统中的业务模块按照稳定性排序,将已经成熟和改动不大的服务拆分为稳定服务,将经常变化和迭代的服务拆分为变动服务
  • 稳定的服务粒度可以粗一些,即使逻辑上没有强关联的服务,也可以放在同一个子系统中,例如将“日志服务”和“升级服务”放在同一个子系统中;不稳定的服务粒度可以细一些,但不要太细,始终记住要控制服务的总数量
  • 这样的拆分主要是为了提升项目快速迭代的效率,避免在开发的时候,不小心影响了已有的成熟功能导致线上问题
3. 基于可靠性拆分
  • 将系统中的业务模块按照优先级排序,将可靠性要求高的核心服务和要求低的非核心服务拆分开来,然后重点保证核心服务的高可用。
好处
  • 避免非核心服务故障影响核心服务

  例如,日志上报一般都属于非核心服务,但是在某些场景下可能有大量的日志上报,如果系统没有拆分,那么日志上报可能导致核心服务故障;拆分后即使日志上报有问题,也不会影响核心服务

  • 核心服务高可用方案可以更加单

  核心服务的功能逻辑更加简单,存储的数据可能更少,用到的组件也会更少,设计高可用方案部分情况下要比不拆分简单很多

  • 能够降低高可用成本

  将核心服务拆分出来后,核心服务占用的机器、带宽等资源比不拆分要少很多。因此,只针对核心服务做高可用方案,机器、带宽等成本比不拆分要节省较多

4. 基于性能拆分
  • 将性能要求高或者性能压力大的模块拆分出来,避免性能压力大的服务影响其他服务
  • 常见的拆分方式和具体的性能瓶颈有关,可以拆分Web服务、数据库、缓存等
  • 例如,电商的抢购,性能压力最大的是入口的排队功能,可以将排队功能独立为一个服务
以上拆分,可以根据实际情况自由排列组合

基础设施

  • “automated”是重要一环,如果其相关的基础设施不健全,那微服务就是焦油坑,让研发,测试,运维陷入各种陷阱中
  • 微服务基础设施如下图所示

实施微服务
  • 有开源的微服务基础设施全家桶,例如,Spring Cloud项目,涵盖了服务发现、服务路由、网关、配置中心等功能
  • 如果微服务的数量并不是很多的话,并不是每个基础设施都是必须的
按优先级来搭建基础设施
    1. 服务发现、服务路由、服务容错:这是最基本的微服务基础设施
    1. 接口框架、API网关:主要是为了提升开发效率,接口框架是提升内部服务的开发效率,API网关是为了提升与外部服务对接的效率
    1. 自动化部署、自动化测试、配置中心:主要是为了提升测试和运维效率
    1. 服务监控、服务跟踪、服务安全:主要是为了进一步提升运维效率
  • 以上3和4两类基础设施,其重要性会随着微服务节点数量增加而越来越重要,但在微服务节点数量较少的时候,可以通过人工的方式支撑,虽然效率不高,但也基本能够顶住

基础设施

自动化测试

  • 微服务将原本大一统的系统拆分为多个独立运行的“微”服务,微服务之间的接口数量大大增加,并且微服务提倡快速交付,版本周期短,版本更新频繁
  • 如果每次更新都靠人工回归整个系统,则工作量大,效率低下,达不到“快速交付”的目的,因此必须通过自动化测试系统来完成绝大部分测试回归的工作中
  • 自动化测试涵盖的范围包括代码级的单元测试、单个系统级的集成测试、系统间的接口测试,理想情况是每类测试都是自动化
  • 因为团队规模和人力的原因无法全面覆盖,至少要做到接口测试自动化

自动化部署

  • 相比大一统的系统,微服务需要部署的节点增加了几倍甚至十几倍,微服务部署的频率也会大幅提升(例如,我们的业务系统70%的工作日都部署操作),综合计算下来,微服务部署的次数是大一统系统部署次数的几十倍
  • 这么大的部署操作,如果继续采用人工手工处理,需要投入大量的人力,且容易出错,因此需要自动化部署的系统来完成部署操作
  • 自动化部署系统包括版本管理、资源管理(例如,机器管理、虚拟机管理)、部署操作、回退操作等功能

配置中心

  • 微服务的节点数量非常多,通过人工登录每台机器手工修改,效率低,容易出错
  • 特别是部署或者排障时,需要快速增删改查配置,人工操作的方式显然是不行的
  • 有的运行期配置需要动态修改并且所有节点即时生效,人工操作是无法做到的
  • 综上,微服务需要一个统一的配置中心来管理所有微服务节点的配置
  • 配置中心包括配置版本管理(例如,同样的微服务,有10个节点是给移动用户服务的,有20个节点给联通用户服务的,配置项都一样,配置值不一样)、增删改查配置、节点配置、配置同步、配置推送等功能

接口框架

  • 微服务提倡轻量级的通信方式,一般采用HTTP/REST或者RPC方式统一接口协议
  • 但在实践过程中,光统一接口协议还不够,还需要统一接口传递的数据格式
  • 例如,我们需要指定接口协议为HTTP/REST,但这还不够,还需要指定HTTP/REST的数据格式采用JSON,并且JSON的数据都遵循如下规范。

  • 如果我们只是简单指定了HTTP/REST协议,而不指定JSON和JSON的数据规范,那么就会出现这样混乱的情况:有的微服务采用XML,有的采用JSON,有的采用键值对;即使同样都是JSON,JSON数据格式也不一样。这样每个微服务都要适配几套甚至几十套接口协议,相当于把曾经由ESB做的事情转交给微服务自己做了,这样做的效率显然是无法接受的,因此需要统一接口框架
  • 接口框架不是一个可运行的系统,一般以库或者包的形式提供给所有微服务调用。例如,针对上面的JSON样例,可以由某个基础技术团队提供多种不同语言的解析包(Java包、Python包 、C库等)

API网关

  • 系统拆分为微服务后,内部的微服务之间是互联互通的,相互之间的访问都是点对点的
  • 如果外部系统想调用系统的某个功能,也采取点对点的方式,则外部系统会非常“头大”
  • 因为在外部系统看来,它不需要也没办法理解这么多微服务的职责分工和边界,它只会关注它需要的能力,而不会关注这个能力应该由哪个微服务提供
  • 外部系统访问系统还涉及安全和权限相关的限制,如果外部系统直接访问某个微服务,则意味着每个微服务都要自己实现安全和权限的功能,这样做不但工作量大,而且都是重复工作
  • 综合上面的分析,微服务需要一个统一的API网关,负责外部系统的访问操作
  • API网关是外部系统访问的接口,所有的外部系统接入系统都需要通过API网关,主要包括接入鉴权(是否允许接入)、权限控制(可以访问哪些功能)、传输加密、请求路由、流量控制等功能

服务发现

  • 微服务种类和数量很多,如果这些信息全部通过手工配置的方式写入各个微服务节点,首先配置工作量大,配置文件可能要配几百上千行,几十个节点加起来后配置项就是几万几十万行了,人工维护这么大数量的配置项是一项灾难
  • 其次是微服务节点经常变化,可能是由于扩容导致节点增加,也可能是故障处理时隔离掉一部分节点,还可能是采用灰度升级,先将一部分节点升级到新版本,然后让新老版本同时运行
  • 不管哪种情况,我们都希望节点的变化能够及时同步到所有其他依赖的微服务。如果采用手工配置,是不可能做到实时更改生效的
  • 因此,需要一套服务发现的系统来支撑微服务的自动注册和发现
服务发现主要有两种实现方式:自理式和代理式
1. 自理式
  • 自理式结构如下:

  • 自理式结构就是指每个微服务自己完成服务发现。例如,图中SERVICE INSTANCE A访问SERVICE REGISTRY获取服务注册信息,然后直接访问SERVICE INSTANCE B
  • 自理式服务发现实现比较简单,因为这部分的功能一般通过统一的程序库或者程序包提供给各个微服务调用,而不会每个微服务都自己来重复实现一遍;并且由于每个微服务都承担了服务发现的功能,访问压力分散到了各个微服务节点,性能和可用性上不存在明显的压力和风险
2. 代理式
  • 代理式结构如下:

  • 代理式结构就是指微服务之间有一个负载均衡系统,由负载均衡系统来完成微服务之间的服务发现
  • 代理式的方式看起来更加清晰,微服务本身的实现也简单了很多,但实际上这个方案风险较大
  • 第一个风险是可用性风险,一旦LOAD BALANCER系统故障,就会影响所有微服务之间的调用
  • 第二个风险是性能风险,所有的微服务之间的调用流量都要经过LOAD BALANCER系统,性能压力会随着微服务数量和流量增加而不断增加,最后成为性能瓶颈
  • 因此LOAD BALANCER系统需要设计成集群的模式,但LOAD BALANCER集群的实现本身又增加了复杂性
  • 不管是自理式还是代理式,服务发现的核心功能就是服务注册表,注册表记录了所有的服务节点的配置和状态,每个微服务启动后都需要将自己的信息注册到服务注册表,然后由微服务或者LOAD BALANCER系统到服务注册表查询可用服务

服务路由

  • 有了服务发现之后,微服务之间能够方便地获取相关配置信息,但具体进行某次调用请求时,我们还需要从所有符合条件的可用微服务节点中挑选出一个具体的节点发起请求,这就是服务路由需要完成的功能
  • 服务路由和服务发现紧密相关,服务路由一般不会设计成一个独立运行的系统,通常情况下是和服务发现放在一起实现的
  • 对于自理式服务发现,服务路由是微服务内部实现的;对于代理式服务发现,服务路由是由LOAD BALANCER系统实现的
  • 无论放在哪里实现,服务路由核心的功能就是路由算法。常见的路由算法有:随机路由、轮询路由、最小压力路由、最小连接数路由等

服务容错

  • 系统拆分为微服务后,单个微服务故障的概率变小,故障影响的范围也减少,但是微服务的节点数量大大增加
  • 从整体上来看,系统中某个微服务出故障的概率会大大增加
  • 微服务具有故障扩散的特点,如果不及时处理故障,故障扩散开来就会导致看起来系统中很多服务节点都故障了, 因此需要微服务能够自动应对这种出错场景,及时进行处理。否则,如果节点一故障就需要人工处理,投入人力大,处理速度慢;而一旦处理速度慢,则故障就很快扩散,所以我们需要服务容错的能力
  • 常见的服务容错包括请求重试、流控和服务隔离
  • 通常情况下,服务容错会集成在服务发现和服务路由系统中

服务监控

  • 系统拆分微服务后,节点数量大大增加,导致需要监控的机器、网络、进程、接口调用数等控制对象的数量大大增加;同时,一旦发生故障,我们需要快速根据各类信息来定位故障。靠人力去完成是不现实的
  • 例如,我们收到用户投诉说业务有问题,如果此时采用人工的方式去搜集、分析信息,可能把几十个节点的日志打开一遍就需要十几分钟了,因此需要服务监控系统来完成微服务节点的监控
作用
  • 实施搜集信息并进行分析,避免故障后再来分析,减少了处理时间
  • 服务监控可以在实时分析的基础上进行预警,在问题萌芽的阶段发觉并预警,降低了问题影响的范围和时间

  通常情况下,服务监控需要搜集分析大量的数据,因此建议做成独立的系统,而不要集成到服务发现、API网关等系统中

服务跟踪

  • 服务监控可以做到微服务节点级的监控和信息收集,但如果我们需要跟踪某一个请求在微服务中的完整路径,服务监控是难以实现的。如果每个服务的完整请求链信息都实时发送给服务监控系统中,数据量会大到无法处理
  • 服务监控和服务跟踪的区别可以简单概括为宏观和微观的区别。例如,A服务通过HTTP协议请求B服务10次,B通过HTTP返回JSON对象,服务监控会记录请求次数、响应时间、响应错误码、请求参数、返回的JOSN对象等信息
  • 目前无论是分布式跟踪还是微服务的服务跟踪,绝大部分请求跟踪的实现技术都基于Google的Dapper论文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》

服务安全

  • 系统拆分为微服务后,数据分散在各个微服务节点上
  • 从系统连接的角度来说,任意微服务都可以访问所有其他微服务节点
  • 从业务的角度来说,部分敏感数据或者操作,只能部分微服务可以访问,而不是所有的微服务都可以访问,因此需要设计服务安全机制来保证业务和数据的安全性
  • 服务安全主要分为三部分:接入安全、数据安全、传输安全
  • 通常情况下,服务安全可以集成到配置中心系统中进行实现,即配置中心配置微服务的接入安全策略和数据安全策略,微服务节点从配置中心获取这些配置信息,然后在处理具体的微服务调用请求时根据安全策略进行处理。
  • 由于这些策略是通用的,一般会把策略封装通用的库提供给各个微服务调用。
  • 基本架构如下:

注:有兴趣了解极客时间专栏的同学,可以查看极客时间专栏—可提供返现服务