微服务
什么是微服务?
单体应用
- 整个应用程序作为一个单一的、可部署的单元进行开发、测试和部署。在单体应用中,所有的功能模块都被打包在一起。通俗来说,就是所有代码都在一个工程里。
- 随着业务规模的不断扩大,团队开发人员的不断扩张,单体应用架构就会开始出现问题:
- 部署效率低下。当单体应用的代码越来越多,依赖的资源越来越多时,应用编译打包、部署测试一次,甚至需要10分钟以上。
- 团队协作开发成本高。早期团队人数较少时,协作修改代码,最后合并到同一个master分支然后打包部署,尚且可控。一旦团队人员扩张,多人然后一起打包部署,测试阶段只要有一块功能有问题,就得重新编译打包部署,所有相关开发人员都得参与其中,效率低下,开发成本极高。
- 系统高可用性差。因为所有的功能开发最后都集中在同一个项目里,一旦某一功能涉及的代码或者资源有问题,那就会影响整体的功能。
服务化
- 将单体应用按照功能模块拆分成多个应用,每个应用是一个服务。
- 每个服务都是独立的开发、测试、部署的单元。
- 服务之间通过网络进行通信。
- 服务化可以提高应用程序的可扩展性、可维护性和可靠性,同时也可以提高开发效率和灵活性。
从单体服务走向服务化
- 纵向拆分:是从业务维度进行拆分。关联比较密切的业务适合拆分为一个服务。
- 横向拆分:是从公共且独立功能维度拆分。标准是按照是否有公共的被多个其他服务调用,且依赖的资源独立不与其他业务耦合。

微服务
- 微服务是更细维度的服务化,微服务架构则将应用程序拆分成多个小型、独立的服务单元,每个服务单元都有自己的数据存储、业务逻辑和接口,服务之间通过网络进行通信。
- 微服务架构中服务的角色分为服务提供者和服务消费者,一个服务可以同时是提供者和消费者。
- 微服务架构的优点:
- 可扩展性好:微服务架构可以根据业务需求进行水平扩展,每个服务单元都可以独立扩展,从而提高应用程序的可扩展性。
- 独立部署:每个服务单元都可以独立部署和更新,这可以降低部署的复杂度和风险。
- 可靠性高:微服务架构中的每个服务单元都是独立的,一个服务单元出现问题不会影响其他服务单元的稳定性和可靠性。
- 易于维护:微服务架构中的每个服务单元都是独立的,可以单独进行测试、更新和维护,从而降低维护成本。
实现微服务架构要解决的问题
服务如何定义?
- 对于单体应用来说,不同功能模块之前相互交互时,通常是以类库的方式来提供各个模块的功能。对于微服务来说,每个服务都运行在各自的进程之中,应该以何种形式向外界传达自己的信息呢?答案就是接口,无论采用哪种通讯协议,服务之间的调用都通过接口描述来约定,约定内容包括接口名、接口参数以及接口返回值。
服务如何发布和订阅?
- 单体应用由于部署在同一个WAR包里,接口之间的调用属于进程内的调用。而拆分为微服务独立部署后,服务之间通过网络进行通信,服务提供者该如何对外暴露自己的地址,服务调用者该如何查询所需要调用的服务的地址呢?这个时候你就需要一个类似登记处的地方,能够记录每个服务提供者的地址以供服务调用者查询,在微服务架构里,这个地方就是注册中心。
服务之间如何通信?
- 不同于项目内部调用,服务之间通过网络进行通信,一般采用 HTTP 或 RPC 的方式进行通信。
服务如何治理?
- 可以想象,拆分为微服务架构后,服务的数量变多了,依赖关系也变复杂了。我们需要知道监控各个服务的运行情况来掌握服务的健康状态。当发生故障时我们要能快速定位到是哪个服务的问题。
服务发布与服务引用
- 最常见的服务发布和引用的方式有三种:RESTful API、XML配置、IDL文件。
- RESTful API方式通常用于HTTP协议的服务描述,一般用于后端提供给前端调用的服务,常用文档或者Swagger来进行管理。
- XML配置方式多用作 RPC 协议的服务描述,通过*.xml配置文件来定义接口名、参数以及返回值类型等。这种方式的服务发布和引用主要分三个步骤:
- 服务提供者定义接口,并实现接口。
- 服务提供者进程启动时,通过加载 server.xml 配置文件将接口暴露出去。
- 服务消费者进程启动时,通过加载 client.xml 配置文件来引入要调用的接口。
- IDL 是接口描述语言(interface description language)的缩写,通过一种中立的方式来描述接口,使得在不同的平台上运行的对象和不同语言编写的程序可以相互通信交流。通常用作 Thrift 和 gRPC 这类跨语言服务调用框架中。
服务注册与服务发现
- 提供服务者,在启动时,根据服务发布文件server.xml中的配置的信息,向注册中心注册自身服务(报告IP地址、端口号),并向注册中心定期发送心跳汇报存活状态。
- 服务消费者,在启动时,根据服务引用文件client.xml中配置的信息,向注册中心订阅服务,把注册中心返回的服务节点列表(包含IP地址、端口号)缓存在本地内存中
- 当服务提供者节点发生变更时,注册中心会同步变更并通知服务消费者,服务消费者收到通知后,刷新本地内存中缓存的服务节点列表。
- 调用服务时,服务消费者从本地缓存的服务节点列表中,基于负载均衡算法选择一台服务提供者发起调用。

远程服务调用 RPC
- RPC,Remote Procedure Call,是一种远程调用协议。帮助我们屏蔽网络编程细节,实现调用远程方法就跟调用本地(同一个项目中的方法)一样的体验,我们不需要因为这个方法是远程调用就需要编写很多与业务无关的代码。
- 这就好比建在小河上的桥一样连接着河的两岸,如果没有小桥,我们需要通过划船、绕道等其他方式才能到达对面,但是有了小桥之后,我们就能像在路面上一样行走到达对面,并且跟在路面上行走的体验没有区别。
- 常见的 RPC 框架包括 gRPC、Apache Thrift、Apache Dubbo 等。


服务治理-服务监控
监控对象
- 用户端监控。通常是指业务直接对用户提供的功能的监控。以微博首页Feed为例,它向用户提供了聚合关注的所有人的微博并按照时间顺序浏览的功能,对首页Feed功能的监控就属于用户端的监控。
- 接口监控。通常是指业务提供的功能所依赖的具体RPC接口的监控。继续以微博首页Feed为例,这个功能依赖于用户关注了哪些人的关系服务,每个人发过哪些微博的微博列表服务,以及每条微博具体内容是什么的内容服务,对这几个服务的调用情况的监控就属于接口监控。
- 资源监控。通常是指某个接口依赖的资源的监控。比如用户关注了哪些人的关系服务使用的是Redis来存储关注列表,对Redis的监控就属于资源监控。
- 基础监控。通常是指对服务器本身的健康状况的监控。主要包括CPU利用率、内存使用量、I/O读写量、网卡带宽等。对服务器的基本监控也是必不可少的,因为服务器本身的健康状况也是影响服务本身的一个重要因素,比如服务器本身连接的网络交换机上联带宽被打满,会影响所有部署在这台服务器上的业务。
监控指标
- 请求量。请求量监控分为两个维度,一个是实时请求量,一个是统计请求量。实时请求量用QPS(Queries Per Second)即每秒查询次数来衡量,它反映了服务调用的实时变化情况。统计请求量一般用PV(Page View)即一段时间内用户的访问量来衡量,比如一天的PV代表了服务一天的请求量,通常用来统计报表。TPS(Transactions Per Second)是指每秒钟可以处理的事务数,通常用于衡量系统的性能和吞吐量。
- 响应时间。大多数情况下,可以用一段时间内所有调用的平均耗时来反映请求的响应时间。但它只代表了请求的平均快慢情况,有时候我们更关心慢请求的数量。为此需要把响应时间划分为多个区间,比如0~10ms、10ms~50ms、50ms~100ms、100ms~500ms、500ms以上这五个区间,其中500ms以上这个区间内的请求数就代表了慢请求量,正常情况下,这个区间内的请求数应该接近于0;在出现问题时,这个区间内的请求数会大幅增加,可能平均耗时并不能反映出这一变化。除此之外,还可以从TP90、TP95、TP99、TP999角度来监控请求的响应时间,比如TP99 = 500ms,意思是99%的请求响应时间在500ms以内,它代表了请求的服务质量,即SLA。
- 错误率。错误率的监控通常用一段时间内调用失败的次数占调用总次数的比率来衡量,比如对于接口的错误率一般用接口返回错误码为503的比率来表示。
监控维度
- 全局维度。从整体角度监控对象的的请求量、平均耗时以及错误率,全局维度的监控一般是为了让你对监控对象的调用情况有个整体了解。
- 分机房维度。一般为了业务的高可用性,服务通常部署在不止一个机房,因为不同机房地域的不同,同一个监控对象的各种指标可能会相差很大,所以需要深入到机房内部去了解。
- 单机维度。即便是在同一个机房内部,可能由于采购年份和批次的不同,位于不同机器上的同一个监控对象的各种指标也会有很大差异。一般来说,新采购的机器通常由于成本更低,配置也更高,在同等请求量的情况下,可能表现出较大的性能差异,因此也需要从单机维度去监控同一个对象。
- 时间维度。同一个监控对象,在每天的同一时刻各种指标通常也不会一样,这种差异要么是由业务变更导致,要么是运营活动导致。为了了解监控对象各种指标的变化,通常需要与一天前、一周前、一个月前,甚至三个月前做比较。
监控环节
- 监控系统主要包括四个环节:数据采集、数据传输、数据处理和数据展示。
数据采集
- 收集到每一次调用的详细信息,包括调用的响应时间、调用是否成功、调用的发起者和接收者分别是谁,这个过程叫作数据采集
- 通常有两种数据收集方式:
- 服务主动上报,这种处理方式通过在业务代码或者服务框架里加入数据收集代码逻辑,在每一次服务调用完成后,主动上报服务的调用信息。
- 代理收集,这种处理方式通过服务调用后把调用的详细信息记录到本地日志文件中,然后再通过代理去解析本地日志文件,然后再上报服务的调用信息。
数据传输
- 采集到数据之后,要把数据通过一定的方式传输给数据处理中心进行处理,这个过程叫作数据传输。
- 数据传输最常用的方式有两种:
- UDP传输,这种处理方式是数据处理单元提供服务器的请求地址,数据采集后通过UDP协议与服务器建立连接,然后把数据发送过去。
- Kafka传输,这种处理方式是数据采集后发送到指定的Topic,然后数据处理单元再订阅对应的Topic,就可以从Kafka消息队列中读取到对应的数据。
数据处理
- 数据处理是对收集来的原始数据进行聚合并存储。
- 数据聚合通常有两个维度:
- 接口维度聚合,这个维度是把实时收到的数据按照接口名维度实时聚合在一起,这样就可以得到每个接口的实时请求量、平均耗时等信息。
- 机器维度聚合,这个维度是把实时收到的数据按照调用的节点维度聚合在一起,这样就可以从单机维度去查看每个接口的实时请求量、平均耗时等信息。
- 聚合后的数据需要持久化到数据库中存储,所选用的数据库一般分为两种:
- 索引数据库,比如Elasticsearch,以倒排索引的数据结构存储,需要查询的时候,根据索引来查询。
- 时序数据库,比如OpenTSDB,以时序序列数据的方式存储,查询的时候按照时序如1min、5min等维度来查询。
链路追踪的作用
- 快速定位问题。如果可以跟踪记录一次用户请求都发起了哪些调用,经过哪些服务处理,并且记录每一次调用所涉及的服务的详细信息,这时候如果发生调用失败,你就可以通过这个日志快速定位是在哪个环节出了问题。
- 优化系统瓶颈。通过记录调用经过的每一条链路上的耗时,我们能快速定位整个系统的瓶颈点在哪里。通过服务追踪,可以从全局视角上去观察,找出整个系统的瓶颈点所在,然后做出针对性的优化。
- 优化链路调用。通过服务追踪可以分析调用所经过的路径,然后评估是否合理。比如一个服务调用下游依赖了多个服务,通过调用链分析,可以评估是否每个依赖都是必要的,是否可以通过业务优化来减少服务依赖。
- 生成网络拓扑。通过服务追踪系统中记录的链路信息,可以生成一张系统的网络调用拓扑图,它可以反映系统都依赖了哪些服务,以及服务之间的调用关系是什么样的,可以一目了然。
链路追踪的实现
- traceId,用于标识某一次具体的请求ID。当用户的请求进入系统后,会在RPC调用网络的第一层生成一个全局唯一的traceId,并且会随着每一层的RPC调用,不断往后传递,这样的话通过traceId就可以把一次用户请求在系统中调用的路径串联起来。
- spanId,用于标识一次RPC调用在分布式请求中的位置。以右图为例:
- 当用户的请求进入系统后,处在RPC调用网络的第一层时spanId初始值是 0
- 调用 A spanId = 0.1
- 调用 B spanId = 0.2
- 调用 C spanId = 0.3
- 在 C 中调用 D spanId = 0.3.1
- 在 D 中调用 E spanId = 0.3.1.1
- 调用 F spanId = 0.4
