为什么走上了拆分微服务的道路?

1,069 阅读16分钟

1. 我们遇到的问题

前边章节介绍了系统的容量,在这里我想重申一下,以便于大家更好理解本章节内容。

系统目标

抽奖接口要求10000 QPS,TP95时延=100ms

兑奖接口 20000QPS ,TP95时延=50ms。

这两个接口写在了同一个服务中,意味着这两个接口将共用CPU,内存、进程、JVM等。

在压测时,我们遇到了问题

  1. 抽奖接口在QPS=10000,lantency TP95=100ms时CPU利用率>60%,内存使用率>70%,很显然系统容量是无法满足兑奖 QPS=20000。

  2. 由于两个接口不是在同一时间达到最大峰值,比如在抽奖QPS=10000,兑奖QPS=100,此时CPU利用率<30%,内存利用率<20%,会造成巨大的资源浪费。

那该如何解决这样的难题呢?

物理隔离:如果想做到资源互不影响最好的方式是资源隔离,每个接口都使用自己的CPU,内存,进程等。

一个应用里只有一个接口???感觉有点违反常识呀?

但仔细想想哪些地方又违反了常理了呢?有时候直觉或者过去的经验不一定是行的通的,还是要具体事情具体分析,最终抽奖接口写到了一个服务里,服务命名assignAwardServive,兑奖写到了一个服务里,命名receiveAwardService

这么拆分服务符合什么规则吗? 总不能一言不合就拆吧,也不能随意拍脑袋就决定吧,还是需要有些理论性的支撑,我们来看拆成微服务需要什么样的理论。

2. 微服务

来自亚马逊的定义

微服务是一种开发软件的架构和组织方法,其中软件由通过明确定义的 API 进行通信的小型独立服务组成。这些服务由各个小型独立团队负责。微服务架构使应用程序更易于扩展和更快地开发,从而加速创新并缩短新功能的上市时间

Red Hat

微服务指的是一种应用架构,其中的一系列独立服务通过轻量级 API 来进行通信。

Martin Fowler AND James Lewis

the microservice architectural style [1] is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies.

微服务的概念是在2014年的时候,Martin Flower和James Lewis提出来的,关于微服务我个人总结是以下几点:

独立进程、独立数据、轻量级API、业务自洽、团队自治

独立进程:每个服务拥有自己的进程,不和其他的服务共享。

独立数据:每个服务有自己的数据库,别的服务不允许直接操作数据库,如果需要相关数据,通过API接口访问

轻量级API:每个服务都以API的方式提供服务,通常以Restful接口语言描述。

业务自洽:相关性强的业务放到一起,形成边界,形成高内聚低耦合。

团队自治 : 每个微服务只归属同一个团队,不同团队维护不同微服务。

微服务更像是为符合组织架构应运而生的技术架构,为不同团队之间的交互限定了边界。

2.1 微服务的前世今生

在微服务盛行之前,常常以单体提供服务,在系统容量不够的情况下,大不了在负载均衡上游多部署几台,过去单体是非常成功的软件开发方式。随着业务的发展单体系统也迎来了一些挑战。

2.1.1 单体系统的挑战

1. 牵一发而动全身:只要有一个地方进行变更,整个系统都要重新构建,重新部署。

2. 协作繁琐:不同的研发人员在同一个工程进行各自的功能迭代,除了要沟通上线发版之外,还要合并代码,共同维护公共类,一旦公共类被一方修改就可能导致问题,在过去因为不小心修改公共类导致的问题屡见不鲜。

3. 工程越来越臃肿:所有的功能都放在一个模块导致代码结构越来越臃肿了,难以维护,最后不得不走上重构的血泪之路。

我曾经见到过把各种功能堆放到一个模块里,经手的人都离职了,最后导致一行代码都不敢改,生怕改一行代码就无法work。

4.昂贵的费用:单体系统可以通过增加机器的配置来抗流量,但人是贵的,机器是贵的,这将带来巨大的成本。

2.1.2 微服务的特点

1. 组件即服务:组件作为软件单元,可以独立部署,独立升级,也就是我们现在所说的saas(software as a service)

2. 组织即业务:不同的微服务相互协作、组织在一起提供服务能力,比如UI团队+业务开发团队+DBA。

3. 产品而不是项目:微服务是以产品方式运营,像亚马逊的理念一样 you build ,you run it,对整个软件的生命周期负责。

4. 智能端和哑管道:有两种方式,第一种通过http restful方式,收request,处理logic,产生response,另外一个是通过消息中间件产生消息和消费消息,把消息的接受,转换,处理都统一了。

5. 去中心化治理:如果是单体很难在一个工程中使用异构的方式,但微服务可以使用不同的语言进行构建,比如可以使用c++构建订单服务,java构建物流服务,每个服务按照接口契约提供服务。

6. 去中心化数据管理:每个微服务都可以有自己的数据库,不像单体所有的数据都放在一起。

7. 自动化部署:通过CI和CD实现持续集成持续部署是实现微服务的工具,只有自动化部署才能解决上百个乃至上千个微服务部署问题。

8. 为失败而设计:多个微服务的调用,必然会存在各种问题,在出现问题的时候,如何优雅的降级是非常重要,比如在调用第三方服务超时时,可以提示用户请重试一次。

9. 持续设计:随着业务的发展,会不断地打破原来的微服务边界,这就需要不断地调整和优化微服务的设计,从而符合业务的高速发展。

2.2 什么时候我们应该用微服务

这是martinfowler 写的另一篇文章 Microservices Guide martinfowler.com/microservic…

所有的架构都是一种取舍,没有绝对的好和坏。

我曾经参与过从一个单体服务拆成几十个微服务的事情,刚开始选择单体主要是因为简单而且迭代速度快,因为我们要求一周迭代一次。

当时我们做的是一个在线教育的项目,包含3个模块,一个是增长和拉新,第二个是上课管理,第三个是辅导老师的运营管理。这3个模块放在同一个工程中,刚开始是有6个人来维护,随着业务的发展,目前单体系统无论从迭代效率还是扩展性上已经不能满足要求,于是我们通过几天的讨论把单体按照模块拆成了15个左右微服务。

在微服务的日子里,我们的人员从6个人也上升到了30左右,每个人在自己的领域里快乐的开发。 但同时也带来了运维上的难度和沟通协调上的问题,这也许不一定是最好的方案,这是trade off的结果。

2.3 微服务常用的技术手段

2.3.1 UML建模

很多公司是不要求UML建模的,在互联网高速发展阶段,公司以快速验证市场为主,没有原型,没有设计甚至直接就进入了开发阶段,这也是常见的事情。但这绝对不是一种良好的开发习惯。

我有个反思是,高速的互联网发展有时候伤害了程序员的成长,该按部就班的事情被略过了,这部分的能力被抹杀了。

我介绍几种常见的UML图,时序图,流程图,类图,尤其是类图对开发人员是非常重要,在做开发之前如果能把类图画好将事半功倍。

介绍几种画UML图的工具吧

  1. www.processon.com/ 基础功能免费,收费版也不算贵。

  2. www.draw.io/ 这个很好用,conflunce上可以集成。

建模是一种循序渐进的过程,我们在做设计的时候大概经历了4~5版才最终定版,在设计中有欣喜有痛苦,值得欣喜的是总是有新的Idea出现,根据新的Iead不断地修正方案,方案一版比一版好,但痛苦的是改版是一个辛苦活,非常消耗能量,但最终我们是开心的。

2.3.2远程通信

2.3.2.1 为什么需要远程通信

从单体拆分成微服务之后,每个微服务都有自己的进程,函数调用不再是在同一个物理空间和地址,所以需要通过远程调用的方式完成请求。

2.3.2.2远程通信的方式有哪些

1. 同步方式

HTTP 最常用的通信方式,目前最常用的是HTTP1.1,该协议配合RESTFUL架构风格完成通信,下图是HTTP的版本历史, 目前HTTP3的传输层协议已经从TCP替换成了QUIC + UDP

image.png

RPC:一台机器调用另外一台机器的地址空间就像在本地调用一样。

image.png

GRPC:由google推出的,基于HTTP2.0的协议,接口描述语言使用PB,性能比RPC更强一些。

2. 异步方式

消息:通过消息中间件连接两个服务,一个作为生产者,一个作为消费者,常见的消息中间有Kafka,RocketMQ,Redis的pubsub,生产者和消费者不需要关心数据库结构,只关心消息。

事件驱动:通过发布事件和订阅事件的方式完成通信,是消息方式的一种变种。

2.3.3负载均衡

2.3.3.1 为什么需要负载均衡

  1. 防止把请求分发到不健康的服务器上。
  2. 阻止资源过载,LB可以做到限流,防止大量请求直接打到服务器导致无法work。
  3. 帮助消除单点故障,通过探活机制把请求分发到健康服务器上
  4. 卸载SSL:解密请求,加密响应
  5. 把同一个session路由到同一台实例上,保持session的一致,做有状态的管理。

2.3.3.2 负载均衡都有哪些

硬件:H5

软件: nginx、HAProxy

2.3.3.3 负载均衡的算法都有哪些?

  • Random:随机把请求打到服务上

  • Least loaded: 把请求打到最少负载的机器上

  • Session/cookies:请求打到同一个seesion上

  • Round robin :轮询去请求可用的servers,不会严格完美的分配请求数量

  • weighted round robin:按照权重轮巡。

    比如有三台机器,在池子里的每个机器都被分配不同的值
    server1: 4
    server2:12
    server3:1
    server1 处理4个请求,server2处理12个请求,server3处理1个请求
    本质就是按照能够承受的量来分配请求

2.3.4服务注册与发现

2.3.4.1 为什么需要服务发现

你在北上广深租过房吗?你一般会怎么找房子呢?有的人直接找房东,有的直接找中介。

在我的租房经历中,这两种方式我都用过,但如果要找到心仪的房子,第一种方式基本上是很难的,因为房东并不好找,而且现在很多房东都把房子委托给了中介,所以我基本上通过中介的方式找到心仪的房子。

中介在这里起到了作用呢? 中介掌握了房源和客源,只有房源和客时匹配上了,房东和租房者见面,签订合同,甚至不见面,直接和中介签订合同,这样房东顺利的把房子租出去了,租房者也顺利租到了心仪的房子。

中介就是服务时注册中间件,房源和客源就是服务请求者和服务的提供者。

服务的请求者是如何调用服务的提供者的呢?

  1. 服务A和服务B去中介注册自己的信息,简称服务A和服务B
  2. 服务A跟中介说我想要找到服务B,中介提供了服务B的IP地址和端口
  3. 服务A 通过IP和端口调用服务B,完成请求

2.3.4.2 常见的组件有哪些

zookeeper,consul,dns,eureka等

2.3.5 熔断降级

2.3.5.1 为什么需要熔断降级

熔断是为了保护自己从而切断和其他服务之间的调用关系。

比如在一分钟内请求超时率超过50%,就切断调用第三方的服务。 降级是在系统发生故障时,为了保护核心功能而牺牲其他的功能的行为,比如在流量过大时,关闭注册功能,通知功能而保护下单功能。

熔断降级是在发生故障之后应该采取的是什么措施,比如超时率超过50%以后,给客户端返回请稍后再试,而不是直接报异常信息。

总之熔断是一种自我保护的行为,防止被别人拉下水。

2.3.5.2 常见的熔断降级组件有哪些

常见的熔断组件有 resilience4j 和hystrix。

hystrix是springCloud全家桶的熔断组件,有两种隔离策略,一种是线程池策略,一种是信号量策略

image.png How it Works · Netflix/Hystrix Wiki · GitHub

线程池隔离策略是每个依赖的服务都有自己的线程池,当DependencyA client异常不影响DependencyI client。 信号量是所有请求共用一个池子,池子满了就拒绝服务。

2.3.6 限流

2.3.6.1 为什么需要限流

本来想明天去故宫转转,在小程序上约明天的票,小程序提示明天已约满,提示你可以约后天的票。

不知道你有遇到过这种情况吗?为什么故宫会这么做呢?

故宫是著名的旅游景点,去的人非常多,倘若全国的人在同一天都约着去故宫,可以想象故宫会成什么样,基本上就是和全国同胞交流感情去了,别提什么观赏了。

你坐过北京早高峰的地铁吗?

你听说过天通苑北地铁站吗,也就是5号线的北段终点站,为什么进站口设计成蛇形的方式,倘若所有的人都一股脑进到地铁里了,地铁基本上就瘫痪了。

这两个事情都很好说明了为什么要进行限流,本质就是为了保护故宫或者地铁系统能更好的运行。

2.3.6.2 常用的限流组件

alibaba的Sentinel组件功能非常强大,支持热点参数,系统保护、流控等策略。

说到这里我讲个真实故事,在我们这次引入限流组件的时候,为了能够让系统的流控规则更灵活,我把所有的流控规则外放到了配置中,也就是可以通过配置的方式完成对规则的定制,由于流控规则过于复杂导致QA根本无法理解,在我讲解了3~4遍之后,依然无法理解,最终要求简化规则。

这里边有个问题,是不是所有的流控规则我们都需要?对于不需要流控规则能不能对非研发人员隐藏,这就涉及到软件设计的取舍。后续我会有专门的章节讲解限流的故事。

2.3.7 配置中心

2.3.7.1 为什么需要配置中心

在没有配置中心之前,我们是怎么做配置的?

我们通常的做法是在工程中通过环境变量来区分环境配置

dev:appplication-dev.yaml
test:appplication-test.yaml
pro:appplication-pro.yaml

这么做有什么问题吗?

假设线上突然出现了个问题,需要把logger的级别从INFO调成DEBUG,方便看到更多的日志,这时候只能修改配置文件,然后重新打包,重新发布。

这就是问题所在,使用本地配置在修改某些配置时非常的不方便

如果使用了配置中心会怎样呢?

可以直接在配置中心修改logger的级别,不需要重启服务,即时生效,岂不是美滋滋。

在我的工作经历中,曾经接手了一个团队,十几个服务都是本地化配置,这样的事情会持续的让我们阵痛,所以我们第一优先级就改成了配置中心,这种小改动大收益的事情让我们身心愉悦。

2.3.7.1 配置中心都有哪些

我们常用的配置中心,一个是springCloud全家桶的 global config 另外一个是阿里的nacos 这两个组件我都用过,基本上都差不多,都具备refresh的功能。

多说一句,nacos也可以注册中心用。

总结

本章节介绍了构建微服务常用的技术手段

  1. 好用的UML设计工具,draw.io
  2. 负载均衡的用处以及算法,另外负载均衡有4层和7层的
  3. 常用的通信协议有RPC和HTTP,语言接口有PB和RestFul
  4. 服务注册与发现的重要,可以称为联络服务的大脑
  5. 介绍了熔断限流降级的方法和组件,是保护系统不被打垮的技术手段
  6. 介绍了配置中心的灵活性和一些场景

现在最流行的微服务构建体系当属springCloud全家桶,提供了全面的微服务构建组件。

微服务之所以能够盛行离不开容器化技术,容器化技术使部署上百个乃至上千个服务成了可能,同时服务的编排和伸缩也同样重要。