当一份悲伤升起的时候

能否把他捧在手心里

就像捧着宝石一样

全然专注的和他在一起~

服务化拆分

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

服务化拆分的两种姿势:

  • 纵向拆分:按业务维度拆分,关联比较密切的几个业务业务适合拆成微服务;功能相对独立的业务拆成微服务;
  • 横向拆分:从公共且独立功能维度拆分。标准是是否有公共的服务被多个其它服务调用,且依赖的资源独立不与其他业务耦合。

在微服务架构中,服务的拆分是非常重要的一步,通常有纵向拆分和横向拆分两种方式。【自己想纵向切和横向切】

  • 纵向拆分是按业务维度进行的拆分。这意味着关联密切的业务会被一起拆分成一个微服务,而功能相对独立的业务则会被拆分成另一个微服务。比如一个电商网站,用户服务(如注册、登录、个人信息管理等)和商品服务(如商品展示、商品查询等)就可以分别拆分成两个微服务,因为它们各自的业务功能相对独立。
  • 横向拆分则是从公共且独立的功能维度进行拆分。如果多个服务都需要调用某个公共服务,且这个公共服务依赖的资源独立,不与其他业务耦合,那么这个公共服务就适合被横向拆分出来。比如在电商网站中,支付服务就可以被横向拆分出来,因为无论是购买商品还是购买会员,都需要调用支付服务,而支付服务本身与商品服务和用户服务没有直接的耦合关系。

通过纵向拆分和横向拆分,可以使得服务更加清晰,降低服务间的耦合度,提高系统的可维护性和可扩展性。

初探微服务架构

微服务架构下,服务调用主要依赖下面几个基本组件:

  • 服务描述
  • 注册中心
  • 服务框架
  • 服务监控
  • 服务追踪
  • 服务治理

服务描述—如何发布和引用微服务

常用的服务描述方式包括 RESTful API、XML 配置以及 IDL 文件三种。

RESTful API 基于 HTTP 协议,因此对调用方(服务消费者)来说几乎不需要任何学习成本即可调用,因此比较适合用作跨平台之间的服务协议。

补充

RESTful API是一种用于Web数据接口设计的规范,它的核心思想是将服务器的资源(比如数据对象)映射到URL上,然后通过HTTP的不同方法来操作这些资源。

下面我会通过一个例子来详细解释下RESTful API的设计方法。Note:Swagger生成接口在线文档

假设我们有一个线上图书商店,我们需要设计一套API来管理书籍资源,那么我们可以这样设计:

  1. GET /books:列出所有的书籍。
  2. POST /books:添加一本新书。
  3. GET /books/{id}:获取指定ID的书籍的详情。
  4. PUT /books/{id}:更新指定ID的书籍的信息。
  5. DELETE /books/{id}:删除指定ID的书籍。

上面的例子中,URL(统一资源定位符)是/books/books/{id},它们代表了书籍这个资源。HTTP方法(GET, POST, PUT, DELETE)则代表了对这个资源的不同操作:获取、添加、更新和删除。

这就是一个简单的RESTful API的设计例子。当然,实际的API设计会根据具体的业务需求有所不同,可能会更复杂一些。总的来说,设计RESTful API的原则是尽量保持简洁和一致性,使得API易于理解和使用。

学成在线的例子分析:

首先,先会去访问网关,在从网关走到具体的每个模块,因此,是基于 HTTP 协议的调用。

image-20240529171608769

每个模块将这些信息统一交给nacos管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
servlet:
context-path: /content
port: 63040
#微服务配置
spring:
application:
name: content-api
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.101.65:3306/xcplus_content148?serverTimezone=UTC&userUnicode=true&useSSL=false&
username: root
password: mysql
# 日志文件配置路径
logging:
config: classpath:log4j2-dev.xml

需要就直接从上面拉取信息即可。


XML 配置方式的服务发布和引用分三个步骤:

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

XML 配置方式的接口变更需要同时更改服务方和调用方的接口文件,在跨部门调用时非常麻烦,所以一般用于私有 RPC 服务。

如果要做变更,尽量新加接口,而不是在现有接口上进行更改。

IDL 是接口描述语言(Interface Description Language)的缩写。将一个使用独立语言(如 protobuf)编写的定义文件编译为其他语言的模块,来实现跨语言的服务通信交流。
常用的 IDL 有两种:Thrift 和 gRPC.
IDL 的优势在于跨平台,但劣势在于它需要对请求和响应格式进行详细定义,如果响应字段很多或格式频繁变化时,服务迭代将会变得很麻烦。

具体采用哪种服务描述方式是根据实际情况决定的,通常情况下,如果只是企业内部之间的服务调用,并且都是 Java 语言的话,选择 XML 配置方式是最简单的。如果企业内部存在多个服务,并且服务采用的是不同语言平台,建议使用 IDL 文件方式进行描述服务。如果还存在对外开放服务调用的情形的话,使用 RESTful API 方式则更加通用。

服务描述方式 使用场景 缺点
RESTful API 跨语言平台,组织内外皆可 使用 HTTP 作为通信协议,相比 TCP 协议,性能较差
XML 配置 Java 平台,一般用于组织内部 不支持跨语言平台
IDL 文件 跨语言平台,组织内外皆可 修改或删除 Protobuf 字段不能向前兼容【gRPC 就是通过 Protobuf 文件定义服务的接口名】可以思考在HTTP使用protobuf协议的例子

注册中心—如何注册和发现微服务

在微服务架构中,注册中心扮演着至关重要的角色,它负责服务的注册与发现、服务状态的检查等任务。目前,常见的注册中心包括Zookeeper、Eureka、Nacos、Consul和ETCD。这些注册中心各有其特点和优势,适用于不同的应用场景和需求。

  • Zookeeper:Zookeeper是一个分布式协调服务,它提供了包括服务注册与发现、配置管理、分布式锁等功能。Zookeeper以其高可靠性和高可用性著称,适用于需要强一致性的场景。
  • Eureka:Eureka是Netflix开源的服务注册中心,主要用于服务发现和故障转移。Eureka的设计目标是高可用性和低延迟,适用于需要快速发现服务的场景。
  • Nacos:Nacos是阿里巴巴开源的服务注册中心,提供了服务发现、配置管理和动态更新等功能。Nacos支持多种协议和配置管理,适用于需要动态配置和服务的场景。
  • Consul:Consul是HashiCorp开发的服务发现和配置管理工具,它提供了服务注册与发现、健康检查、Key/Value存储等功能。Consul的特点是简单易用,适用于需要灵活配置的场景。
  • ETCD:ETCD是一个分布式键值存储系统,也常被用作注册中心。它提供了高可用性的键值存储,适用于需要存储和检索配置信息的场景。

注册中心常用API

  • 服务注册接口:服务提供者通过调用服务注册接口来完成服务注册。
  • 服务反注册接口:服务提供者通过调用服务反注册接口来完成服务注销。
  • 心跳汇报接口:服务提供者通过调用心跳汇报接口完成节点存活状态上报。
  • 服务订阅接口:服务消费者通过调用服务订阅接口完成服务订阅,获取可用的服务提供者节点列表。
  • 服务变更查询接口:服务消费者通过调用服务变更查询接口,获取最新的可用服务节点列表。
  • 服务查询接口:查询注册中心当前注册了哪些服务信息。
  • 服务修改接口:修改注册中心中某一服务的信息。

微服务架构下主要有三种角色:服务提供者、服务消费者和服务注册中心。

注册中心负责存储所有可用服务的信息,并将这些信息提供给服务消费者,可以认为是提供者和消费者之间的纽带。

image-20240529163919567

注册中心为了及时更新服务状态,需要定期对服务进行健康状态监测,以免将不可用的服务提供给服务消费者。

为例保证高可用性,注册中心一般采用分布式集群存储

使用注册中心的方式,客户端可以与所有可用的 server 建立连接池,从而在调用端实现请求的负载均衡。

白名单机制

在微服务架构中,白名单机制是一种安全策略,用于控制哪些客户端可以访问你的服务。

具体来说,白名单是一个列表,里面包含了被允许访问服务的客户端的标识,这些标识可以是IP地址、用户名、设备ID等。当一个客户端试图访问服务时,服务会检查这个客户端的标识是否在白名单中。只有在白名单中的客户端才被允许访问,不在白名单中的客户端将被拒绝。

这种机制可以保护你的服务不被未经授权的客户端访问,从而提高服务的安全性。但是,白名单机制也有一些缺点,比如管理白名单可能会比较麻烦,特别是当白名单中的客户端数量非常大时。此外,如果一个客户端的标识被盗取,那么攻击者也可以通过这个标识来访问服务。

总的来说,白名单机制是一种简单有效的安全策略,但是在使用时也需要注意一些潜在的问题。

【简单说,白名单机制是一种简单有效的安全策略,在实际的微服务测试和部署时,通常包含多套环境,比如生产环境一套、测试环境一套】

如何实现 RPC 远程服务调用

RPC 调用方与服务提供方建立连接后,双方需要按照某种约定的协议进行网络通信。为了减少数据传输,通常还会对数据进行压缩(序列化)。

服务端处理请求的方式:

  • 同步阻塞(BIO):双方均阻塞
  • 同步非阻塞(NIO):客户端同步调用,服务端通过多路复用进行异步处理
  • 异步非阻塞(AIO):客户端发起调用后返回,服务端处理结束后,客户端会收到结果

序列化方式的选用主要会从三个角度来考虑:支持数据结构类型的复杂度;跨语言支持程度;序列化性能(消息压缩比和序列化速度)

如何监控微服务调用

监控对象可以从上到下分为四个层次:

  • 用户端监控:对功能的直接监控
  • 接口监控:对接口本身的监控
  • 资源监控:对功能依赖资源的监控(如 Redis)
  • 基础监控:对服务器本身的健康状况的监控(CPU,内存,IO 等)

监控指标:

  • 请求量。请求量监控分为两个维度,一个是实时请求量,一个是统计请求量。实时请求量用 QPS(Queries Per Second)即每秒查询次数来衡量,它反映了服务调用的实时变化情况。统计请求量一般用 PV(Page View)即一段时间内用户的访问量来衡量,比如一天的 PV 代表了服务一天的请求量,通常用来统计报表。
  • 响应时间。大多数情况下,可以用一段时间内所有调用的平均耗时来反映请求的响应时间。但它只代表了请求的平均快慢情况,有时候我们更关心慢请求的数量。为此需要把响应时间划分为多个区间,比如 0~10ms、10ms~50ms、50ms~100ms、100ms~500ms、500ms 以上这五个区间,其中 500ms 以上这个区间内的请求数就代表了慢请求量,正常情况下,这个区间内的请求数应该接近于 0;在出现问题时,这个区间内的请求数会大幅增加,可能平均耗时并不能反映出这一变化。除此之外,还可以从 P90、P95、P99、P999 角度来监控请求的响应时间,比如 P99 = 500ms,意思是 99% 的请求响应时间在 500ms 以内,它代表了请求的服务质量,即 SLA。
  • 错误率。错误率的监控通常用一段时间内调用失败的次数占调用总次数的比率来衡量,比如对于接口的错误率一般用接口返回错误码为 503 的比率来表示。

监控维度:

  • 全局维度
  • 分机房维度
  • 单机维度
  • 时间维度
  • 核心维度(根据业务是否为核心业务来进行部署和监控上的隔离)

监控系统主要包括四个环节:数据采集、数据传输、数据处理和数据展示。

如何追踪微服务调用

基本概念

常常一个请求包含多个服务,服务出错了应该怎么debug呢?举例如下

image-20240529164404445

服务追踪的作用:

  • 优化系统瓶颈【找出每一条链路上的耗时】
  • 优化链路调用【通过服务追踪可以分析调用所经过的路径,然后评估是否合理】
  • 生成网络拓扑
  • 透明传输数据

Google 发布的一篇的论文Dapper, a Large-Scale Distributed Systems Tracing Infrastructure,详细讲解了服务追踪系统的实现原理。

服务追踪的核心原理就是调用链:通过一个全局唯一的 ID 将分布在各个服务节点上的同一次请求串联起来,从而还原原有的调用关系。
比较有名的追踪框架有 Twitter 的 Zipkin,阿里的鹰眼,美团的 MTrace ,Opentracing 等。

先举个通俗易懂的例子,以Zipkin为例,它的工作原理是这样的:

  1. 当一个请求进入系统时,比如用户点击了一个"购买"按钮,前端会向后端发送一个请求。这个请求首先到达的可能是一个网关服务,或者叫做API网关。
  2. 这个API网关在接收到请求后,会生成一个全局唯一的Trace ID,然后添加到请求的Header里。这个Trace ID就像是这次请求的身份证,无论它走到哪里,都会被带上。
  3. 然后,API网关可能需要调用其他的服务,比如用户服务。在调用用户服务的时候,它会把包含Trace ID的请求发送过去。用户服务在处理这个请求的时候,会先从请求的Header里取出这个Trace ID。
  4. 用户服务在处理完请求后,可能还需要调用其他的服务,比如库存服务。同样,它也会把这个Trace ID添加到请求的Header里,然后再发起调用。
  5. 这样一来,无论这个请求经过了多少个服务,只要我们根据这个Trace ID,就可以将这个请求在各个服务之间的调用路径串联起来,形成一条完整的调用链。
  6. 每个服务在处理完请求后,都会把自己的处理时间、状态等信息,以及这个Trace ID,发送到Zipkin。然后,Zipkin就可以根据这些信息,以及这个Trace ID,把这个请求的完整调用链还原出来。

这就是如何通过Trace ID将一个请求的完整调用链串联起来的基本原理。

具体介绍下美团的 MTrace 的原理

搞懂基本概念:traceId、spanId、annonation

image-20240529201824459

  • 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。

    • 理解:在分布式追踪系统中,Annotation(注解)是一种常见的数据收集方式,用于收集和记录一些关键的事件或者信息。

      以一个购物网站为例,当一个用户点击"购买"按钮进行购买操作时,可能会涉及到很多步骤,比如检查库存、确认价格、进行支付等等。在这个过程中,我们可能会对一些关键的步骤进行注解。

      比如,当用户点击"购买"按钮时,我们可以添加一个"用户点击购买按钮"的注解,当检查库存成功时,我们可以添加一个"检查库存成功"的注解,当支付成功时,我们可以添加一个"支付成功"的注解。这些注解可以帮助我们更好地理解一次请求的完整处理过程。

      此外,注解还可以包含一些自定义的数据。比如,我们可能会把用户的UID添加到注解中,这样就可以知道是哪个用户发起的这次请求。或者,我们可能会把商品的ID添加到注解中,这样就可以知道用户购买的是哪个商品。

      这些自定义的数据可以帮助我们更好地理解和分析业务,比如分析哪个用户最活跃,哪个商品最受欢迎等等。

      总的来说,注解就是在处理请求的过程中,记录一些关键事件和自定义数据的一种方式,可以帮助我们更好地理解和分析业务。

traceId 是用于串联某一次请求在系统中经过的所有路径,spanId 是用于区分系统不同服务之间调用的先后关系,而 annotation 是用于业务自定义一些自己感兴趣的数据,在上传 traceId 和 spanId 这些基本信息之外,添加一些自己感兴趣的信息。

服务追踪系统实现

image-20240529203101539

服务追踪系统架构图,你可以看到一个服务追踪系统可以分为三层。

  • 数据采集层,负责数据埋点并上报。
  • 数据处理层,负责数据的存储与计算。
  • 数据展示层,负责数据的图形化展示。

1. 数据采集层

数据采集层的作用就是在系统的各个不同模块中进行埋点,采集数据并上报给数据处理层进行处理。

image-20240529203132048

以红色方框里圈出的 A 调用 B 的过程为例,一次 RPC 请求可以分为四个阶段。

  • CS(Client Send)阶段 : 客户端发起请求,并生成调用的上下文。
  • SR(Server Recieve)阶段 : 服务端接收请求,并生成上下文。
  • SS(Server Send)阶段 : 服务端返回请求,这个阶段会将服务端上下文数据上报,下面这张图可以说明上报的数据有:traceId=123456,spanId=0.1,appKey=B,method=B.method,start=103,duration=38。
  • CR(Client Recieve)阶段 : 客户端接收返回结果,这个阶段会将客户端上下文数据上报,上报的数据有:traceid=123456,spanId=0.1,appKey=A,method=B.method,start=103,duration=38。

image-20240529203147859

2. 数据处理层

数据处理的需求一般分为两类,一类是实时计算需求,一类是离线计算需求。

  • 实时数据处理

针对实时数据处理,一般采用 Storm 或者 Spark Streaming 来对链路数据进行实时聚合加工,存储一般使用 OLTP 数据仓库,比如 HBase,使用 traceId 作为 RowKey,能天然地把一整条调用链聚合在一起,提高查询效率。

  • 离线数据处理

针对离线数据处理,一般通过运行 MapReduce 或者 Spark 批处理程序来对链路数据进行离线计算,存储一般使用 Hive。

3. 数据展示层

  • 调用链路图

下面以一张 Zipkin 的调用链路图为例,通过这张图可以看出下面几个信息。

服务整体情况:服务总耗时、服务调用的网络深度、每一层经过的系统,以及多少次调用。下图展示的一次调用,总共耗时 209.323ms,经过了 5 个不同的系统模块,调用深度为 7 层,共发生了 24 次系统调用。

每一层的情况:每一层发生了几次调用,以及每一层调用的耗时。

image-20240529203225959

  • 调用拓扑图

下面是一张 Pinpoint 的调用拓扑图,通过这张图可以看出系统内都包含哪些应用,它们之间是什么关系,以及依赖调用的 QPS、平均耗时情况。

image-20240529203240870

微服务治理的手段

节点管理

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

  1. 注册中心主动摘除机制
  2. 服务消费者摘除机制(注册中心照常提供注册信息,服务消费者发现服务提供方不可用时,将它从本地的提供方列表中摘除)

负载均衡算法:

  1. 随机算法:均匀随机;
  2. 轮询算法:按照固定的权重,对可用节点进行轮询。权重可以静态配置,也可以通过某些指标来设置;
  3. 最少活跃调用算法:统计当前消费者和每个节点之间建立的连接数,向连接最少的一方发送请求
  4. 一致性 Hash 算法

服务路由:可用节点的选择除了由负载均衡算法决定,还会由路由规则来决定。

  • 为什么要制定路由规则呢?主要有两个原因
    • 业务存在灰度发布的需求:服务提供者做了功能变更,但希望先只让部分人群使用,然后根据这部分人群的使用反馈,再来决定是否做全量发布。
    • 多机房就近访问的需求

服务容错:对于调用失败的请求,要通过一些手段自动恢复,保证调用成功。

总结:在实际的微服务架构实践中,上面这些服务治理手段一般都会在服务框架中默认集成了,比如阿里开源的服务框架 Dubbo、微博开源的服务框架 Motan 等,不需要业务代码去实现。