微服务理解
一、定义
或是将多个模块中重复的部分进行拆分,或是纯粹为了拆分膨胀的单体应用,这些拆分出来的部分独立成一个服务单独部署与维护,便是微服务了。
拆分后的需求:
- 从本地方法调用的关系衍变成远程过程调用的关系,那么可靠的通信功能是首要的。
- 随着拆分工作的推进,资源调度关系会变得错综复杂,这时候需要完善的服务治理。
- 随着拆分工作的推进,资源调度关系会变得错综复杂,这时候需要完善的服务治理。
- 服务化后,如果能结合容器化、Devops 技术实现服务运维一体化,将大大降低微服务维护的成本,不管是现在还是将来。
二、 长啥样子
红框圈出的部分都属于微服务的范畴。
包括最基础的 RPC 框架、注册中心、配置中心,以及更广义角度的监控追踪、治理中心、调度中心等。
大致会包含以下模块:
- 服务注册与发现。
- RPC 远程调用。
- 路由与负载均衡
- 服务监控
- 服务治理
广义的服务拆分即包含了应用拆分,也包含了数据拆分。应用拆分后需要引入微服务框架来进行服务通信与服务治理,这也就是传统定义上的微服务。
会引入一些手段来进行保障:
- 分布式ID。
- 新表优化。
- 数据迁移与数据同步。
- SQL调用方案改造。
- 切库方案。
- 数据一致性。
三、 微服务具备三个基本功能:
- 服务的发布与引用。
- 服务的注册与发现。
- 服务的远程通信。
3.1、服务的发布与引用
具体就是,这个服务的接口名是啥,有哪些参数,返回值是什么类型等等,通常也就是接口描述信息。
常用的发布和引用方式包括:
1、RESTful API/声明式 Restful API
2、XML
3、IDL
一般来讲,不管使用哪种方式,服务端定义接口与实现接口都是必要的。
【声明式 Restful API】:这种常使用 HTTP 或者 HTTPS 协议调用服务,相对来说,性能稍差。
首先服务端如上定义接口并实现接口,随后服务提供者可以使用类似 restEasy 这样的框架通过 Servlet 的方式发布服务,而服务消费者直接引用定义的接口调用。
除此之外还有一种类似 Feign 的方式,即服务端的发布依赖于 SpringMVC Controller,框架只基于客户端模板化 HTTP 请求调用。
这种情况下需接口定义与服务端 Controller 协商一致,这样客户端直接引用接口发起调用即可。
【XML】:使用私有 RPC 协议的都会选择 XML 配置的方式来描述接口,比较高效,例如 Dubbo、Motan 等。
同样服务端如上定义接口并实现接口,服务端通过 server.xml 将文件接口暴露出去。服务消费者则通过 client.xml 引用需要调用的接口。
但这种方式对业务代码入侵较高,XML 配置有变更时候,服务消费者和服务提供者都需要更新。
【IDL】:是接口描述语言,常用于跨语言之间的调用,最常用的 IDL 包括 Thrift 协议以及 gRpc 协议。
例如 gRpc 协议使用 Protobuf 来定义接口,写好一个 proto 文件后,利用语言对应的 protoc 插件生成对应 server 端与 client 端的代码,便可直接使用。
但是如果参数字段非常多,proto 文件会显得非常大难以维护。并且如果字段经常需要变更,例如删除字段,PB 就无法做到向前兼容。
Tips:不管哪种方式,在接口变更的时候都需要通知服务消费者。消费者对api的强依赖性是很难避免的,接口变更引起的各种调用失败也十分常见。所以如果有变更,尽量使用新增接口的方式,或者给每个接口定义好版本号吧。在使用上,大多数人的选择是对外 Restful,对内 XML,跨语言 IDL。
问题:在实际的服务发布与引用的落地上,还会存在很多问题,大多和配置信息相关。例如一个简单的接口调用超时时间配置,这个配置应该配在服务级别还是接口级别?是放在服务提供者这边还是服务消费者这边?
在实践中,大多数服务消费者会忽略这些配置,所以服务提供者自身提供默认的配置模板是有必要的,相当于一个预定义的过程。
每个服务消费者在继承服务提供者预定义好的配置后,还需要能够进行自定义的配置覆盖。
但是,比方说一个服务有 100 个接口,每个接口都有自身的超时配置,而这个服务又有 100 个消费者,当服务节点发生变更的时候,就会发生 100*100 次注册中心的消息通知,这是比较可怕的,就有可能引起网络风暴。
3.2、服务注册与发现
采用注册中心。
3.2.1、注册中心寻址并调用的过程:
- 服务启动时,向注册中心注册自身,并定期发送心跳汇报存活状态。
- 客户端调用服务时,向注册中心订阅服务,并将节点列表缓存至本地,再与服务端建立连接(当然这儿可以 lazy load)。发起调用时,在本地缓存节点列表中,基于负载均衡算法选取一台服务端发起调用。
- 当服务端节点发生变更,注册中心能感知到后通知到客户端。
3.2.2、注册中心的实现主要需要考虑一下问题:
- 自身的一致性与可用性。
- 注册方式。
- 存储结构。
- 服务健康检查。
- 状态变更通知。
【一致性与可用性】
分布式系统中的 CAP(一致性、可用性、分区容错性)。常见的注册中心大致分为CP注册中心与AP注册中心。
CP注册中心:Zookeeper,etcd,Consul。牺牲可用性保证一致性。通过Raft协议或者ZAB协议来保证一致性。
AP注册中心:Eureka。牺牲一致性保证可用性。Eureka每个服务单独保存节点列表,可能会出现不一致的情况。
理论而言,AP 型是远比 CP 型合适的。可用性的需求远远高于一致性,一致性只要保证最终一致即可,而不一致的时候还可以使用各种容错策略进行弥补。
保障高可用性其实还有很多办法,例如集群部署或者多 IDC 部署等。Consul 就是多 IDC 部署保障可用性的典型例子,它使用了 wan gossip 来保持跨机房状态同步。
【注册方式】
应用内:最常见的方式,客户端与服务端都集成相关sdk与注册中心进行交互。例如选择 Zookeeper 作为注册中心,那么就可以使用 Curator SDK 进行服务的注册与发现。
应用外:Consul 提供了应用外注册的解决方案,Consul Agent 或者第三方 Registrator 可以监听服务状态,从而负责服务提供者的注册或销毁。而 Consul Template 则可以做到定时从注册中心拉取节点列表,并刷新 LB 配置(例如通过 Nginx 的 upstream),这样就相当于完成了服务消费者端的负载均衡。
【存储结构】
注册中心存储相关信息一般采取目录化的层次结构,一般分为服务-接口-节点信息。
同时注册中心一般还会进行分组,分组的概念很广,可以是根据机房划分也可以根据环境划分。
节点信息主要会包括节点的地址(ip 和端口号),还有一些节点的其他信息,比如请求失败的重试次数、超时时间的设置等等。
当然很多时候,其实可能会把接口这一层给去掉,因为考虑到接口数量很多的情况下,过多的节点会造成很多问题,比如之前说的网络风暴。
【健康检查】
服务存活状态监测也是注册中心的一个必要功能。在 Zookeeper 中,每个客户端都会与服务端保持一个长连接,并生成一个 Session。
在 Session 过期周期内,通过客户端定时向服务端发送心跳包来检测链路是否正常,服务端则重置下次 Session 的过期时间。
如果 Session 过期周期内都没有检测到客户端的心跳包,那么就会认为它已经不可用了,将其从节点列表中移除。
【状态变更通知】
在注册中心具备服务健康检测能力后,还需要将状态变更通知到客户端。在 Zookeeper 中,可以通过监听器 Watcher 的 Process 方法来获取服务变更。
3.3、服务的远程通信
需要考虑的问题:
1、网络I/O的处理。
2、传输协议。
3、序列化方式。
【网络I/O的处理】
就是客户端是怎么处理请求?服务端又是怎么处理请求的?
先从客户端来说,我们创建连接的时机可以是从注册中心获取到节点信息的时候,但更多时候,我们会选择在第一次请求发起调用的时候去创建连接。此外,我们往往会为该节点维护一个连接池,进行连接复用。
如果是异步的情况下,我们还需要为每一个请求编号,并维护一个请求池,从而在响应返回时找到对应的请求。当然这并不是必须的,很多框架会帮我们干好这些事情,比如 rxNetty。
从服务端来说,处理请求的方式就可以追溯到 Unix 的 5 种 IO 模型了。我们可以直接使用 Netty、MINA 等网络框架来处理服务端请求,或者如果你有十分的兴趣,可以自己实现一个通信框架。
【传输协议】
最常见的当然是直接使用 HTTP 协议,使用双方无需关注和了解协议内容,方便直接,但自然性能上会有所折损。
还有就是目前比较火热的 HTTP2 协议,拥有二进制数据、头部压缩、多路复用等许多优良特性。
但从自身的实践上看,HTTP2 要走到生产仍有一段距离,一个最简单的例子,升级到 HTTP2 后所有的 header names 都变成小写,同时不是 case-insenstive 了,这时候就会有兼容性问题。
当然如果追求更高效与可控的传输,可以定制私有协议并基于 TCP 进行传输。私有协议的定制需要通信双方都了解其特性,设计上还需要注意预留好扩展字段,以及处理好粘包分包等问题。
【序列化方式】
在网络传输的前后,往往都需要在发送端进行编码,在服务端进行解码,这样主要是为了在网络传输时候减少数据传输量。
常用的序列化方式包括文本类的,例如 XML/JSON,还有二进制类型的,例如 Protobuf/Thrift 等。
序列化上需要考虑:
- 性能。 Protobuf 的压缩大小和压缩速度都会比 JSON 快很多,性能也更好。
- 兼容性。 相对来说,JSON 的前后兼容性会强一些,可以用于接口经常变化的场景。