入门微服务

微服务简介

我们暂时不去考虑复杂的架构,仅仅以讲清楚什么是微服务为目的,因为拆分成微服务最为敏感的就是数据库,但我们暂时不考虑这个问题。

下面给出微服务的定义:由单一应用程序构成的小服务,拥有自己的进程与轻量化处理,服务依业务功能设计,以全自动的方式部署,与其他服务使用 HTTP API 通讯。同时,服务会使用最小规模的集中管理 (例如 Docker)技术,服务可以用不同的编程语言与数据库等。

单体应用

早些年,各大互联网公司的应用技术栈大致可分为 LAMP和 MVC两大流派。无论是 LAMP 还是MVC,都是为单体应用架构设计的,其优点是学习成本低,开发上手快,测试、部署、运维也比较方便,甚至一个人就可以完成一个网站的开发与部署。

单体服务.png

服务化

互联网早期肯定没有问题,但随着网民增多,流量越来越大,这种架构方式肯定不可取,它的耦合度实在太高了。将功能模块进行拆分,独立成一个服务进行部署,这样可以让各团队只负责好自己的模块。

为此,首先可以把用户模块从单体应用中拆分出来,独立成一个服务部署,以 RPC 接口的形式对外提供服务。微博和消息模块调用用户接口,就从进程内的调用变成远程 RPC 调用。这样,用户模块就可以独立开发、测试、上线和运维,可以交由专门的团队来做,与主模块不耦合。进一步的可以再把消息模块也拆分出来作为独立的模块,交由专门的团队来开发和维护。

服务化.png

微服务

微服务就是把服务化继续进行更粒度的拆分,这得益于以 Docker 为代表的容器化技术的成熟以及 DevOps 文化的兴起。

进一步对内容模块的功能进行拆分,比如内容模块又包含了 Feed 模块、评论模块和个人页模块。通过微服务化,将这三个模块变成三个独立的服务,每个服务依赖各自的资源,并独立部署在不同的服务池中,可以由不同的开发人员进行维护。当评论服务需求变更时,只需要修改评论业务相关的代码,并独立上线发布;而 Feed 服务和个人页服务不需要变更,也不会受到发布可能带来的变更影响。

微服务化.png

从单体应用走向服务化

什么时候进行服务化拆分?

项目第一阶段的主要目标是快速开发和验证想法,证明产品思路是否可行。这个阶段功能设计一般不会太复杂,开发采取快速迭代的方式,架构也不适合过度设计。所以将所有功能打包部署在一起,集中地进行开发、测试和运维,对于项目起步阶段,是最高效也是最节省成本的方式。当可行性验证通过,功能进一步迭代,就可以加入越来越多的新特性。

根据实际项目经验,一旦单体应用同时进行开发的人员超过 10 人,就会遇到上面的问题,这个时候就该考虑进行服务化拆分了。

服务化拆分的两种姿势

实际生产中,下面两种划分方式是结合应用的。

纵向划分:从业务维度进行拆分。标准是按照业务的关联程度来决定,关联比较密切的业务适合拆分为一个微服务,而功能相对比较独立的业务适合单独拆分为一个微服务。

横向划分:从公共且独立功能维度拆分。标准是按照是否有公共的被多个其他服务调用,且依赖的资源独立不与其他业务耦合。

以社交 App 举例,无论是首页信息流、评论、消息箱还是个人主页,都需要显示用户的昵称。假如用户的昵称功能有产品需求的变更,你需要上线几乎所有的服务,这个成本就有点高了。显而易见,如果我把用户的昵称功能单独部署成一个独立的服务,那么有什么变更我只需要上线这个服务即可,其他服务不受影响,开发和上线成本就大大降低了。

初探微服务架构

微服务架构.png

首先服务提供者(就是提供服务的一方)按照一定格式的服务描述,向注册中心注册服务,声明自己能够提供哪些服务以及服务的地址是什么,完成服务发布。

接下来服务消费者(就是调用服务的一方)请求注册中心,查询所需要调用服务的地址,然后以约定的通信协议向服务提供者发起请求,得到请求结果后再按照约定的协议解析结果。

而且在服务的调用过程中,服务的请求耗时、调用量以及成功率等指标都会被记录下来用作监控,调用经过的链路信息会被记录下来,用于故障定位和问题追踪。在这期间,如果调用失败,可以通过重试等服务治理手段来保证成功率。

(一)服务描述

服务调用首先要解决的问题就是服务如何对外描述。比如,你对外提供了一个服务,那么这个服务的服务名叫什么?调用这个服务需要提供哪些信息?调用这个服务返回的结果是什么格式的?该如何解析?这些就是服务描述要解决的问题。如果你写过网络库和业务代码(比方说聊天服务器)就应该能明白这个含义。

常见的方式有:RESTful API、XML 配置以及 IDL 文件三种。

(二)注册中心

一般来讲,注册中心的工作流程是:

  1. 服务提供者在启动时,根据服务发布文件中配置的发布信息向注册中心注册自己的服务。
  2. 服务消费者在启动时,根据消费者配置文件中配置的服务信息向注册中心订阅自己所需要的服务。
  3. 注册中心返回服务提供者地址列表给服务消费者。
  4. 当服务提供者发生变化,比如有节点新增或者销毁,注册中心将变更通知给服务消费者。

(三)服务框架

通过注册中心,服务消费者就可以获取到服务提供者的地址,有了地址后就可以发起调用。但在发起调用之前你还需要解决以下几个问题:

  • 服务通信采用什么协议?
  • 数据传输采用什么方式?
  • 数据压缩采用什么格式?

这是一个网络库应该去考虑的问题,即主机间互相通信的问题。

(四)服务监控

一旦服务消费者与服务提供者之间能够正常发起服务调用,你就需要对调用情况进行监控,以了解服务是否正常。通常来讲,服务监控主要包括三个流程:

  1. 指标收集。就是要把每一次服务调用的请求耗时以及成功与否收集起来,并上传到集中的数据处理中心。
  2. 数据处理。有了每次调用的请求耗时以及成功与否等信息,就可以计算每秒服务请求量、平均耗时以及成功率等指标。
  3. 数据展示。数据收集起来,经过处理之后,还需要以友好的方式对外展示,才能发挥价值。通常都是将数据展示在 Dashboard 面板上,并且每隔 10s 等间隔自动刷新,用作业务监控和报警等。

这让我想到 Prometheus,它已经普遍被应用起来。

(五)服务追踪

除了需要对服务调用情况进行监控之外,你还需要记录服务调用经过的每一层链路,以便进行问题追踪和故障定位。服务追踪的工作原理大致如下:

  1. 服务消费者发起调用前,会在本地按照一定的规则生成一个 requestID,发起调用时,将 requestID 当作请求参数的一部分,传递给服务提供者。
  2. 服务提供者接收到请求后,记录下这次请求的 requestID,然后处理请求。如果服务提供者继续请求其他服务,会在本地再生成一个自己的 requestID,然后把这两个 requestID 都当作请求参数继续往下传递。

以此类推,通过这种层层往下传递的方式,一次请求,无论最后依赖多少次服务调用、经过多少服务节点,都可以通过最开始生成的 requestID 串联所有节点,从而达到服务追踪的目的。

(六)服务治理

服务监控能够发现问题,服务追踪能够定位问题所在,而解决问题就得靠服务治理了。服务治理就是通过一系列的手段来保证在各种意外情况下,服务调用仍然能够正常进行。

发布和引用服务

(一)RESTful API

主要被用作 HTTP 或者 HTTPS 协议的接口定义,即使在非微服务架构体系下,也被广泛采用。

因为 HTTP 协议本身是一个公开的协议,对于服务消费者来说几乎没有学习成本,所以比较适合用作跨业务平台之间的服务协议。比如你有一个服务,不仅需要在业务部门内部提供服务,还需要向其他业务部门提供服务,甚至开放给外网提供服务,这时候采用 HTTP 协议就比较合适,也省去了沟通服务协议的成本。

(二)XML 配置

这种方式的服务发布和引用主要分三个步骤:

  1. 服务提供者定义接口,并实现接口。
  2. 服务提供者进程启动时,通过加载 server.xml 配置文件将接口暴露出去。
  3. 服务消费者进程启动时,通过加载 client.xml 配置文件来引入要调用的接口。

一般是私有 RPC 框架会选择 XML 配置这种方式来描述接口,因为私有 RPC 协议的性能要比 HTTP 协议高,所以在对性能要求比较高的场景下,采用 XML 配置的方式比较合适。但这种方式对业务代码侵入性比较高,XML 配置有变更的时候,服务消费者和服务提供者都要更新,所以适合公司内部联系比较紧密的业务之间采用。如果要应用到跨部门之间的业务调用,一旦有 XML 配置变更,需要花费大量精力去协调不同部门做升级工作。

(三)IDL 配置

即 Grpc + Protobuf 常用组合方式。

gRPC 协议的服务描述是通过 proto 文件来定义接口的,然后再使用 protoc 来生成不同语言平台的客户端和服务端代码,从而具备跨语言服务调用能力。

有一点特别需要注意的是,在描述接口定义时,IDL 文件需要对接口返回值进行详细定义。如果接口返回值的字段比较多,并且经常变化时,采用 IDL 文件方式的接口定义就不太合适了。一方面可能会造成 IDL 文件过大难以维护,另一方面只要 IDL 文件中定义的接口返回值有变更,都需要同步所有的服务消费者都更新,管理成本就太高了。

注册和发现服务

注册中心可以说是实现服务化的关键,因为服务化之后,服务提供者和服务消费者不在同一个进程中运行,实现了解耦,这就需要一个纽带去连接服务提供者和服务消费者,而注册中心就正好承担了这一角色。此外,服务提供者可以任意伸缩即增加节点或者减少节点,通过服务健康状态检测,注册中心可以保持最新的服务节点信息,并将变化通知给订阅服务的服务消费者。

RPC远程服务调用

把服务消费者叫作客户端,服务提供者叫作服务端,两者通常位于网络上两个不同的地址,要完成一次 RPC 调用,就必须先建立网络连接。建立连接后,双方还必须按照某种约定的协议进行网络通信,这个协议就是通信协议。双方能够正常通信后,服务端接收到请求时,需要以某种方式进行处理,处理成功后,把请求结果返回给客户端。为了减少传输的数据大小,还要对数据进行压缩,也就是对数据进行序列化。

RPC 就是基于网络库规定通信的格式的协议,格式通常就是服务调用的规则。

监控微服务调用

搭建一个服务监控系统,涉及数据采集、数据传输、数据处理、数据展示等多个环节,Prometheus 已经包含这些内容。因此,后续会出一个这块专题的教程。

追踪微服务调用

服务追踪的作用

(一)优化系统瓶颈

通过记录调用经过的每一条链路上的耗时,我们能快速定位整个系统的瓶颈点在哪里。比如你访问微博首页发现很慢,肯定是由于某种原因造成的,有可能是运营商网络延迟,有可能是网关系统异常,有可能是某个服务异常,还有可能是缓存或者数据库异常。通过服务追踪,可以从全局视角上去观察,找出整个系统的瓶颈点所在,然后做出针对性的优化。

(二)优化链路调用

通过服务追踪可以分析调用所经过的路径,然后评估是否合理。比如一个服务调用下游依赖了多个服务,通过调用链分析,可以评估是否每个依赖都是必要的,是否可以通过业务优化来减少服务依赖。

还有就是,一般业务都会在多个数据中心都部署服务,以实现异地容灾,这个时候经常会出现一种状况就是服务 A 调用了另外一个数据中心的服务 B,而没有调用同处于一个数据中心的服务 B。

(三)生成网络拓扑

通过服务追踪系统中记录的链路信息,可以生成一张系统的网络调用拓扑图,它可以反映系统都依赖了哪些服务,以及服务之间的调用关系是什么样的,可以一目了然。除此之外,在网络拓扑图上还可以把服务调用的详细信息也标出来,也能起到服务监控的作用。

(四)透明传输数据

除了服务追踪,业务上经常有一种需求,期望能把一些用户数据,从调用的开始一直往下传递,以便系统中的各个服务都能获取到这个信息。比如业务想做一些 A/B 测试,这时候就想通过服务追踪系统,把 A/B 测试的开关逻辑一直往下传递,经过的每一层服务都能获取到这个开关值,就能够统一进行 A/B 测试。

服务追踪系统的实现原理

服务追踪原理.png

  • 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 这些基本信息之外,添加一些自己感兴趣的信息。

微服务常见的治理手段

节点管理

服务调用失败一般是由两类原因引起的,一类是服务提供者自身出现问题,如服务器宕机、进程意外退出等;一类是网络问题,如服务提供者、注册中心、服务消费者这三者任意两者之间的网络出现问题。

无论是服务提供者自身出现问题还是网络发生问题,都有两种节点管理手段:

(一)注册中心主动摘除机制

这种机制要求服务提供者定时的主动向注册中心汇报心跳,注册中心根据服务提供者节点最近一次汇报心跳的时间与上一次汇报心跳时间做比较,如果超出一定时间,就认为服务提供者出现问题,继而把节点从服务列表中摘除,并把最近的可用服务节点列表推送给服务消费者。

(二)服务消费者摘除机制

虽然注册中心主动摘除机制可以解决服务提供者节点异常的问题,但如果是因为注册中心与服务提供者之间的网络出现异常,最坏的情况是注册中心会把服务节点全部摘除,导致服务消费者没有可用的服务节点调用,但其实这时候服务提供者本身是正常的。所以,将存活探测机制用在服务消费者这一端更合理,如果服务消费者调用服务提供者节点失败,就将这个节点从内存中保存的可用服务提供者节点列表中移除。

负载均衡

一般情况下,服务提供者节点不是唯一的,多是以集群的方式存在,尤其是对于大规模的服务调用来说,服务提供者节点数目可能有上百上千个。由于机器采购批次的不同,不同服务节点本身的配置也可能存在很大差异,新采购的机器 CPU 和内存配置可能要高一些,同等请求量情况下,性能要好于旧的机器。对于服务消费者而言,在从服务列表中选取可用节点时,如果能让配置较高的新机器多承担一些流量的话,就能充分利用新机器的性能。这就需要对负载均衡算法做一些调整。

服务路由

对于服务消费者而言,在内存中的可用服务节点列表中选择哪个节点不仅由负载均衡算法决定,还由路由规则确定。所谓的路由规则,就是通过一定的规则如条件表达式或者正则表达式来限定服务节点的选择范围。

为什么要制定路由规则呢?主要有两个原因:

  • 业务存在灰度发布的需求。比如,服务提供者做了功能变更,但希望先只让部分人群使用,然后根据这部分人群的使用反馈,再来决定是否做全量发布。这个时候,就可以通过类似按尾号进行灰度的规则限定只有一定比例的人群才会访问新发布的服务节点。
  • 多机房就近访问需求。

服务容错

服务调用并不总是一定成功的,前面我讲过,可能因为服务提供者节点自身宕机、进程异常退出或者服务消费者与提供者之间的网络出现故障等原因。对于服务调用失败的情况,需要有手段自动恢复,来保证调用成功。常用的手段主要有以下几种:

  • FailOver:失败自动切换。就是服务消费者发现调用失败或者超时后,自动从可用的服务节点列表总选择下一个节点重新发起调用,也可以设置重试的次数。这种策略要求服务调用的操作必须是幂等的,也就是说无论调用多少次,只要是同一个调用,返回的结果都是相同的,一般适合服务调用是读请求的场景。
  • FailBack:失败通知。就是服务消费者调用失败或者超时后,不再重试,而是根据失败的详细信息,来决定后续的执行策略。比如对于非幂等的调用场景,如果调用失败后,不能简单地重试,而是应该查询服务端的状态,看调用到底是否实际生效,如果已经生效了就不能再重试了;如果没有生效可以再发起一次调用。
  • FailCache:失败缓存。就是服务消费者调用失败或者超时后,不立即发起重试,而是隔一段时间后再次尝试发起调用。比如后端服务可能一段时间内都有问题,如果立即发起重试,可能会加剧问题,反而不利于后端服务的恢复。如果隔一段时间待后端节点恢复后,再次发起调用效果会更好。
  • FailFast:快速失败。就是服务消费者调用一次失败后,不再重试。实际在业务执行时,一般非核心业务的调用,会采用快速失败策略,调用失败后一般就记录下失败日志就返回了。

⭐️内容取自极课时间《从0开始学微服务》,仅从中取出个人以为需要纪录的内容。不追求内容的完整性,却也不会丢失所记内容的逻辑性。如果需要了解细致,建议购买原课程。