说起微服务,大家应该并不陌生,不只是一线大厂,很多中小规模团队也已经将这项技术引入并在实际业务中落地。
那作为一名开发人员,应该如何学习微服务呢?
虽然现在开源的微服务框架有很多,各种编程语言的都有,花上几个小时搭建一套可运行的开发环境也并不是一件难事。但毕竟微服务涉及的组件还是挺多的,相比于单体架构来说,复杂度提升了不少。
不知道你有没有和我一样的困扰,有时候想要深入学习一下,但却不知道从什么地方入手,结果就是对其了解只是浮于表面。
所以我最近看了极客时间的专栏《从 0 开始学微服务》,觉得作为入门还是不错的。
同时,我总结了专栏中的部分内容,并加入一些自己的思考,形成了这篇文章。算是学习微服务的一个大纲,然后再按照这个大纲去深入学习,不断充实这套技术体系。
好了,下面正文开始。
什么是微服务
微服务是指开发应用所用的一种架构形式。通过微服务,可将大型应用分解成多个独立的组件,其中每个组件都有各自的责任领域。在处理一个用户请求时,基于微服务的应用可能会调用许多内部微服务来共同生成其响应。
微服务的特点:
- 服务拆分粒度: 小到一个子模块,只要该模块依赖的资源与其他模块都没有关系,那么就可以拆分为一个微服务。
- 服务独立部署: 每个微服务都严格遵循独立打包部署的准则,互不影响。比如一台物理机上可以部署多个 Docker 实例,每个 Docker 实例可以部署一个微服务的代码。
- 服务独立维护: 每个微服务都可以交由一个小团队甚至个人来开发、测试、发布和运维,并对整个生命周期负责。
- 服务治理能力要求高: 因为拆分为微服务之后,服务的数量变多,因此需要有统一的服务治理平台,来对各个服务进行管理。
服务发布和引用
想要构建微服务,首先要解决的问题是,服务提供者如何发布一个服务,服务消费者如何引用这个服务。具体来说,就是这个服务的接口名是什么?调用这个服务需要传递哪些参数?接口的返回值是什么类型?以及一些其他接口描述信息。
最常见的服务发布和引用的方式有三种:
- RESTful API
- XML 配置
- IDL 文件
RESTful API
这种方式就比较常见了,主要被用作 HTTP 或者 HTTPS 协议的接口定义,即使在非微服务架构体系下,也被广泛采用。而且学习成本低,比较适合用作跨业务平台之间的服务协议。
XML 配置
这种方式下需要在服务提供者和服务消费者之间维持一份对等的 XML 配置文件,来保证服务调用。比如提供者维护 server.xml,消费者维护 client.xml。
在接口变更时,需要同时修改这两个接口描述文件。
IDL 文件
IDL 就是接口描述语言(interface description language)的缩写,通过一种中立的方式来描述接口,使得在不同的平台上运行的对象和不同语言编写的程序可以相互通信交流。
也就是说 IDL 主要是用作跨语言平台的服务之间的调用,有两种最常用的 IDL:一个是 Facebook 开源的 Thrift 协议,另一个是 Google 开源的 gRPC 协议。
小结
每种方式都有各自的优缺点,具体应该如何选择,需要根据自身的业务特点。
描述方式 |
使用场景 |
缺点 |
RESTful API |
跨语言平台,组织内外皆可 |
使用 HTTP 作为通信协议,相比于 TCP,性能较差 |
XML 配置 |
Java 平台,一般用于组织内部 |
不支持跨语言平台 |
IDL 文件 |
跨语言平台,组织内外皆可 |
修改和删除字段不支持向前兼容 |
注册中心
服务拆分之后,服务的提供者和消费者分别运行在不同的机器上,而且服务众多,有几十个,甚至上百个。这就涉及到一个问题,消费者如何才能找到服务提供者,这也是注册中心需要解决的问题。
注册中心需要实现哪些 API?
- 服务注册接口: 服务提供者通过调用服务注册接口来完成服务注册。
- 服务反注册接口: 服务提供者通过调用服务反注册接口来完成服务注销。
- 心跳汇报接口: 服务提供者通过调用心跳汇报接口完成节点存活状态上报。
- 服务订阅接口: 服务消费者通过调用服务订阅接口完成服务订阅,获取可用的服务提供者节点列表。
- 服务变更查询接口: 服务消费者通过调用服务变更查询接口,获取最新的可用服务节点列表。
除此之外,为了便于管理,注册中心还必须提供一些后台管理的 API,例如:
- 服务查询接口: 查询注册中心当前注册了哪些服务信息。
- 服务修改接口: 修改注册中心中某一服务的信息。
配置中心
在拆分为微服务架构前,单体应用只需要管理一套配置;而拆分为微服务后,每一个系统都有自己的配置,并且都各不相同,而且因为服务治理的需要,有些配置还需要能够动态改变,以达到动态降级、切流量、扩缩容等目的,所以如何管理配置十分重要。
配置中心一般包含下面几个功能:
- 配置注册
- 配置反注册
- 配置查看
- 配置变更订阅
API 网关
API 网关可以被视为一种充当应用程序服务和不同客户端之间的中间件,可以管理许多事情,例如:
- 路由: 网关接收所有 API 请求并将它们转发到目标服务。
- 记录日志: 能够在一处记录所有请求。
- 权限认证: 检查用户是否有资格访问该服务,如果没有,可以拒绝该请求。
- 性能分析: 估计每个请求的执行时间并检查性能瓶颈。
- 缓存: 通过在网关级别处理缓存,可以降低服务的流量压力。
事实上,它是作为一个反向代理工作的,客户端只需要知道系统的网关,应用服务就可以隐藏起来,不直接向其他系统暴露。
如果没有 API 网关,可能需要在每个服务中做一些横切关注点,比如想记录服务的请求和响应。此外,如果应用程序由多个服务组成,客户端需要知道每个服务地址,并且在更改服务地址的情况下,需要更新多个地方。
负载均衡
微服务架构拥有很好的可扩展性,我们能够通过运行更多服务实例来处理更多请求,但问题是,哪个实例应该接收请求,或客户端如何知道哪个服务实例应该处理请求?
这些问题的答案是负载均衡。负载均衡是高可用网络基础架构的关键组件,通常用于将工作负载分布到多个服务器来提高网站、应用、数据库或其他服务的性能和可靠性。
常用的负载均衡算法有一下五种:
随机算法
顾名思义就是从可用的服务节点中,随机挑选一个节点来访问。
实现比较简单,在请求量远超可用服务节点数量的情况下,各个服务节点被访问的概率基本相同,主要应用在各个服务节点的性能差异不大的情况下。
轮询算法
跟随机算法类似,各个服务节点被访问的概率也基本相同,也主要应用在各个服务节点性能差异不大的情况下。
加权轮询算法
在轮询算法基础上的改进,可以通过给每个节点设置不同的权重来控制访问的概率,因此主要被用在服务节点性能差异比较大的情况。
比如经常会出现一种情况,因为采购时间的不同,新的服务节点的性能往往要高于旧的节点,这个时候可以给新的节点设置更高的权重,让它承担更多的请求,充分发挥新节点的性能优势。
最少活跃连接算法
与加权轮询算法预先定义好每个节点的访问权重不同,采用最少活跃连接算法,客户端同服务端节点的连接数是在时刻变化的,理论上连接数越少代表此时服务端节点越空闲,选择最空闲的节点发起请求,能获取更快的响应速度。
尤其在服务端节点性能差异较大,而又不好做到预先定义权重时,采用最少活跃连接算法是比较好的选择。
一致性 hash 算法
因为它能够保证同一个客户端的请求始终访问同一个服务节点,所以适合服务端节点处理不同客户端请求差异较大的场景。
比如服务端缓存里保存着客户端的请求结果,如果同一客户端一直访问一个服务节点,那么就可以一直从缓存中获取数据。
服务监控
不管是单体架构,还是微服务架构,监控都是必不可少的。只不过微服务架构更加复杂,监控起来也就更加不容易。
对于一个微服务来说,必须明确要监控哪些对象、哪些指标,并且还要从不同的维度进行监控,才能掌握微服务的调用情况。
监控对象
监控对象可以分为四个层次,由上到下可归纳为:
- 用户端监控: 通常是指业务直接对用户提供的功能的监控。
- 接口监控: 通常是指业务提供的功能所依赖的具体 RPC 接口的监控。
- 资源监控: 通常是指某个接口依赖的资源的监控。
- 基础监控: 通常是指对服务器本身的健康状况的监控。主要包括 CPU 利用率、内存使用量、I/O 读写量、网卡带宽等。
监控指标
搞清楚要监控的对象之后,需要监控具体哪些指标呢?
- 请求量: 请求量监控分为两个维度,一个是实时请求量,一个是统计请求量。实时请求量用 QPS(Queries Per Second)即每秒查询次数来衡量,它反映了服务调用的实时变化情况。统计请求量一般用 PV(Page View)即一段时间内用户的访问量来衡量,比如一天的 PV 代表了服务一天的请求量,通常用来统计报表。
- 响应时间: 大多数情况下,可以用一段时间内所有调用的平均耗时来反映请求的响应时间。但它只代表了请求的平均快慢情况,有时候我们更关心慢请求的数量。为此需要把响应时间划分为多个区间,比如 0~10ms、10ms~50ms、50ms~100ms、100ms~500ms、500ms 以上这五个区间,其中 500ms 以上这个区间内的请求数就代表了慢请求量,正常情况下,这个区间内的请求数应该接近于 0;在出现问题时,这个区间内的请求数会大幅增加,可能平均耗时并不能反映出这一变化。
- 错误率: 错误率的监控通常用一段时间内调用失败的次数占调用总次数的比率来衡量,比如对于接口的错误率一般用接口返回错误码为 503 的比率来表示。
监控维度
一般来说,要从多个维度来对业务进行监控,包括下面几个维度:
- 全局维度: 从整体角度监控对象的的请求量、平均耗时以及错误率,全局维度的监控一般是为了让你对监控对象的调用情况有个整体了解。
- 分机房维度: 一般为了业务的高可用性,服务通常部署在不止一个机房,因为不同机房地域的不同,同一个监控对象的各种指标可能会相差很大,所以需要深入到机房内部去了解。
- 单机维度: 即便是在同一个机房内部,可能由于采购年份和批次的不同,位于不同机器上的同一个监控对象的各种指标也会有很大差异。一般来说,新采购的机器通常由于成本更低,配置也更高,在同等请求量的情况下,可能表现出较大的性能差异,因此也需要从单机维度去监控同一个对象。
- 时间维度: 同一个监控对象,在每天的同一时刻各种指标通常也不会一样,这种差异要么是由业务变更导致,要么是运营活动导致。为了了解监控对象各种指标的变化,通常需要与一天前、一周前、一个月前,甚至三个月前做比较。
- 核心维度: 业务上一般会依据重要性程度对监控对象进行分级,最简单的是分成核心业务和非核心业务。核心业务和非核心业务在部署上必须隔离,分开监控,这样才能对核心业务做重点保障。
服务追踪
在调试单体应用时,非常直观容易。但是在微服务架构上,因为一个请求可能会通过不同的服务,而不同的服务又不在一个地方,这使得调试和跟踪变得困难。
所以服务追踪是分布式系统中必不可少的功能,它能够帮助我们查询一次用户请求在系统中的具体执行路径,以及每一条路径的上下游的详细情况,对于追查问题十分有用。
它的核心理念就是调用链:通过一个全局唯一的 ID 将分布在各个服务节点上的同一次请求串联起来,从而还原原有的调用关系,可以追踪系统问题、分析调用数据并统计各种系统指标。
- traceId: 用于标识某一次具体的请求 ID。当用户的请求进入系统后,会在 RPC 调用网络的第一层生成一个全局唯一的 traceId,并且会随着每一层的 RPC 调用,不断往后传递,这样的话通过 traceId 就可以把一次用户请求在系统中调用的路径串联起来。
- spanId: 用于标识一次 RPC 调用在分布式请求中的位置。当用户的请求进入系统后,处在 RPC 调用网络的第一层 A 时 spanId 初始值是 0,进入下一层 RPC 调用 B 的时候 spanId 是 0.1,继续进入下一层 RPC 调用 C 时 spanId 是 0.1.1,而与 B 处在同一层的 RPC 调用 E 的 spanId 是 0.2,这样的话通过 spanId 就可以定位某一次 RPC 请求在系统调用中所处的位置,以及它的上下游依赖分别是谁。
- annotation: 用于业务自定义埋点数据,可以是业务感兴趣的想上传到后端的数据,比如一次请求的用户 UID。
小结一下,traceId 是用于串联某一次请求在系统中经过的所有路径,spanId 是用于区分系统不同服务之间调用的先后关系,而 annotation 是用于业务自定义一些自己感兴趣的数据,在上传 traceId 和 spanId 这些基本信息之外,添加一些自己感兴趣的信息。
故障处理
系统故障是避免不了的,虽然微服务架构做了服务拆分,不至于像单体架构那样整体崩溃,但由于其整体复杂度也大大提升,故障处理也更加困难。
限流
顾名思义,限流就是限制流量。通常情况下,系统能够承载的流量根据集群规模的大小是固定的,可以称之为系统的最大容量。
当真实流量超过了系统的最大容量后,就会导致系统响应变慢,服务调用出现大量超时,反映给用户的感觉就是卡顿、无响应。
所以,应该根据系统的最大容量,给系统设置一个阈值,超过这个阈值的请求会被自动抛弃,这样的话可以最大限度地保证系统提供的服务正常。
熔断
熔断和限流还不太一样,上面我们可以看到限流是控制请求速率,只要还能承受,那么都会处理,但熔断不是。
在一条调用链上,如果发现某个服务异常,比如响应超时。那么调用者为了避免过多请求导致资源消耗过大,最终引发系统雪崩,会直接返回错误,而不是疯狂调用这个服务。
降级
什么是降级呢?降级就是通过停止系统中的某些功能,来保证系统整体的可用性。
降级可以说是一种被动防御的措施,为什么这么说呢?因为它一般是系统已经出现故障后所采取的一种止损措施。
容器化
单体应用拆分成多个微服务后,能够实现快速开发迭代,但随之带来的问题是测试和运维部署成本的提升。
而容器技术正好可以很好的解决这些问题,目前最流行的当属 Docker 莫属。
微服务容器化运维主要涉及到以下几点:
- 镜像仓库
- 容器调度
- 服务编排
镜像仓库
镜像仓库的概念其实跟 Git 代码仓库类似,就是有一个集中存储的地方,把镜像存储在这里,在服务发布的时候,各个服务器都访问这个集中存储来拉取镜像,然后启动容器。
容器调度
这个阶段主要是解决在哪些机器上启动容器的问题,特别是规模比较大的公司,一般有物理机集群,虚拟机集群,私有云和公有云。对接这么多不同的平台,难度还是不小的。
需要统一管理来自不同集群的机器权限管理、成本核算以及环境初始化等操作,这个时候就需要有一个统一的层来完成这个操作。
很显然,靠人工是肯定不行的,需要搭建统一的部署运维平台。
服务编排
大部分情况下,微服务之间是相互独立的,在进行容器调度的时候不需要考虑彼此。
但有时候也会存在一些场景,比如服务 A 调度的前提必须是先有服务 B,这样的话就要求在进行容器调度的时候,还需要考虑服务之间的依赖关系。
Service Mesh
Service Mesh 的概念最早是由 Buoyant 公司的 CEO William Morgan 提出,他给出的服务网格的定义是:
A service mesh is a dedicated infrastructure layer for handling service-to-service communication. It’s responsible for the reliable delivery of requests through the complex topology of services that comprise a modern, cloud native application. In practice, the service mesh is typically implemented as an array of lightweight network proxies that are deployed alongside application code, without the application needing to be aware.
被很多人定义为下一代的微服务架构。
目前,Service Mesh 的代表产品当属 Google 和 IBM 的 lstio。
Istio 的架构可以说由两部分组成,分别是 Proxy 和 Control Plane。
- Proxy: 与应用程序部署在同一个主机上,应用程序之间的调用都通过 Proxy 来转发,目前支持 HTTP/1.1、HTTP/2、gRPC 以及 TCP 请求。
- Control Plane: 与 Proxy 通信,来实现各种服务治理功能,包括三个基本组件:Pilot、Mixer 以及 Citadel。