一:什么是微服务?
微服务就是将庞杂臃肿的单体应用拆分成细粒度的服务,独立部署,并交给各个中小团队来负责开发、测试、上线和运维整个生命周期。
微服务拆分需要考虑的点?
什么时候进行拆分?
初期不管如何,都肯定是以先跑起来为依据,可以暂且不管架构的好坏,这也符合UNIX编程艺术的优化原则。一般而言,一旦单体应用同时进行开发的人员超过 10 人,就会遇到问题,这个时候就该考虑进行服务化拆分了。
如何拆分?
- 纵向拆分:按业务的关联程度
- 横向拆分:按公共且独立功能维度拆分。标准是按照是否有公共的被多个其他服务调用,且依赖的资源独立不与其他业务耦合。
以社交 App 举例,无论是首页信息流、评论、消息箱还是个人主页,都需要显示用户的昵称。假如用户的昵称功能有产品需求的变更,你需要上线几乎所有的服务,这个成本就有点高了。显而易见,如果我把用户的昵称功能单独部署成一个独立的服务,那么有什么变更我只需要上线这个服务即可,其他服务不受影响,开发和上线成本就大大降低了。
在想如果两个结合起来会不会更好?就是需要注意会有业务重复。昵称你一套,我再搞一套。也会使得服务过多。
不过实际工作中都是两个结合进行拆分的:
- 首先收集整理公用的模块,将其进行服务化处理,这是横向拆分。
- 其次根据不同业务之间的耦合程度,将相对独立的服务拆分到不同的服务中,这属于纵向拆分。
最后的微服务列表中,既有被其他微服务使用的公共服务,也有彼此独立运行的业务服务
服务化拆分最好不要涉及跨数据库表交互!原因如下:
服务化最难的是在数据库层,因为数据库中很多数据都是相互关联的,比如用户跟订单,订单和商品等等这些数据之间都是有关联的,服务拆分之后会面临以下问题:
- 当需要读取关联数据时,如果采用表连接的方式查询数据会出现跨数据库查询的可能
- 如果是通过RPC的方式多次调用(比如要查订单,就需要查询商品及订单详细信息),也会出现多次调用导致的频繁多次的数据库连接,而如果使用缓存,也会面临数据库与缓存之间的数据同步问题,特别是数据库与缓存服务器都存在集群的情况下,会更难处理
拆分服务面临的问题:
- 服务如何定义。对于单体应用来说,不同功能模块之前相互交互时,通常是以类库的方式来提供各个模块的功能。对于微服务来说,每个服务都运行在各自的进程之中,应该以何种形式向外界传达自己的信息呢?答案就是接口,无论采用哪种通讯协议,是 HTTP 还是 RPC,服务之间的调用都通过接口描述来约定,约定内容包括接口名、接口参数以及接口返回值。(在这里就需要考虑模块原则,组合原则)
- 服务如何发布和订阅。单体应用由于部署在同一个 WAR 包里,接口之间的调用属于进程内的调用。而拆分为微服务独立部署后,服务提供者该如何对外暴露自己的地址,服务调用者该如何查询所需要调用的服务的地址呢?这个时候你就需要一个类似登记处的地方,能够记录每个服务提供者的地址以供服务调用者查询,在微服务架构里,这个地方就是注册中心
- 服务如何监控。通常对于一个服务,我们最关心的是 QPS(调用量)、AvgTime(平均耗时)以及 P999(99.9% 的请求性能在多少毫秒以内)这些指标。这时候你就需要一种通用的监控方案,能够覆盖业务埋点、数据收集、数据处理,最后到数据展示的全链路功能。
- 服务如何治理。可以想象,拆分为微服务架构后,服务的数量变多了,依赖关系也变复杂了。比如一个服务的性能有问题时,依赖的服务都势必会受到影响。可以设定一个调用性能阈值,如果一段时间内一直超过这个值,那么依赖服务的调用可以直接返回,这就是熔断,也是服务治理最常用的手段之一。
- 故障如何定位
二:微服务的架构
具体调用流程就是:
- 服务提供者按照一定格式的服务描述,向注册中心注册服务,声明自己能够提供哪些服务以及服务的地址是什么,完成服务发布。
- 接下来服务消费者(就是调用服务的一方)请求注册中心,查询所需要调用服务的地址,然后以约定的通信协议向服务提供者发起请求,得到请求结果后再按照约定的协议解析结果。
- 而且在服务的调用过程中,服务的请求耗时、调用量以及成功率等指标都会被记录下来用作监控,调用经过的链路信息会被记录下来,用于故障定位和问题追踪。在这期间,如果调用失败,可以通过重试等服务治理手段来保证成功率。
可以看出,微服务架构主要有上面描述的六大组件。接下来,一个一个介绍一下:
服务描述
服务名是什么?调用服务需要提供哪些信息?服务的返回结果是什么格式的?如何解析?
常用的服务描述方式包括 RESTful API、XML 配置以及 IDL 文件三种。
- RESTful API:一般用于 Http 协议的服务描述
- XML 配置:多用作 RPC 协议的服务描述。通过XML来定义接口名,参数,返回值等。在对业务性能要求比较高的场景下使用。
- IDL :通常用作 Thrift 和 gRPC 这类跨语言服务调用框架中。比如 gRPC 就是通过 Protobuf 文件来定义服务的接口名、参数以及返回值的数据结构
注册中心
一般来讲,注册中心的工作流程是:
- 服务提供者在启动时,根据服务发布文件中配置的发布信息向注册中心注册自己的服务。
- 服务消费者在启动时,根据消费者配置文件中配置的服务信息向注册中心订阅自己所需要的服务。
- 注册中心返回服务提供者地址列表给服务消费者。
- 当服务提供者发生变化,比如有节点新增或者销毁,注册中心将变更通知给服务消费者。
monitor: 监控
服务框架(其实就是如何定义协议)
- 采用什么协议
- 数据如何传输
- 数据形式是什么?如何压缩? PB/JSON
服务监控
- 指标收:耗时之类的东西
- 数据处理:计算成功率之类的一些东西
- 数据展示:
服务追踪
可查看 Google 的发表的一篇论文:Dapper
https://storage.googleapis.com/pub-tools-public-publication-data/pdf/36356.pdf
翻译版:
http://bigbully.github.io/Dapper-translation/
服务治理:
服务治理就是通过一系列的手段来保证在各种意外情况下,服务调用仍然能够正常进行。
那么都有什么意外情况呐?
- 单机故障。通常遇到单机故障,都是靠运维发现并重启服务或者从线上摘除故障节点。然而集群的规模越大,越是容易遇到单机故障,在机器规模超过一百台以上时,靠传统的人肉运维显然难以应对。而服务治理可以通过一定的策略,自动摘除故障节点,不需要人为干预,就能保证单机故障不会影响业务。
- 单 IDC 故障。你应该经常听说某某 App,因为施工挖断光缆导致大批量用户无法使用的严重故障。而服务治理可以通过自动切换故障 IDC 的流量到其他正常 IDC,可以避免因为单 IDC 故障引起的大批量业务受影响。
- 依赖服务不可用。比如你的服务依赖了另一个服务,当另一个服务出现问题时,会拖慢甚至拖垮你的服务。而服务治理可以通过熔断,在依赖服务异常的情况下,一段时期内停止发起调用而直接返回。这样一方面保证了服务消费者能够不被拖垮,另一方面也给服务提供者减少压力,使其能够尽快恢复。
- 等等
三:如何注册和发现服务?(ZooKeeper)
注册中心的原理
- RPC Server 提供服务,在启动时,根据服务发布文件 server.xml 中的配置的信息,向 Registry 注册自身服务,并向 Registry 定期发送心跳汇报存活状态。
- RPC Client 调用服务,在启动时,根据服务引用文件 client.xml 中配置的信息,向 Registry 订阅服务,把 Registry 返回的服务节点列表缓存在本地内存中,并与 RPC Sever 建立连接。
- 当 RPC Server 节点发生变更时,Registry 会同步变更,RPC Client 感知后会刷新本地内存中缓存的服务节点列表。
- RPC Client 从本地缓存的服务节点列表中,基于负载均衡算法选择一台 RPC Sever 发起调用。
一般采用分布式集群部署,注意数据一致性的要求。
RPC远程调用
跨进程:共享内存
跨机器:RPC
序列化的考虑(一定就是PB这些好吗?):
主要有两类:
- XML/JSON
- PB/Thrift
具体采用哪种方式,主要取决于三个方面的因素:
- 支持数据结构类型的丰富度。数据结构种类支持的越多越好,这样的话对于使用者来说在编程时更加友好,有些序列化框架如 Hessian 2.0 还支持复杂的数据结构比如 Map、List 等。
- 跨语言
- 性能。主要看两点,一个是序列化后的压缩比,一个是序列化的速度。以常用的 PB 序列化和 JSON 序列化协议为例来对比分析,PB 序列化的压缩比和速度都要比 JSON 序列化高很多,所以对性能和存储空间要求比较高的系统选用 PB 序列化更合适;而 JSON 序列化虽然性能要差一些,但可读性更好,更适合对外部提供服务。
四:如何治理微服务?
将单体应用改造为微服务架构后,服务调用由本地调用变成远程调用,服务消费者 A 需要通过注册中心去查询服务提供者 B 的地址,然后发起调用,这个看似简单的过程就可能会遇到下面几种情况,比如:
- 注册中心宕机;
- 服务提供者 B 有节点宕机;
- 服务消费者 A 和注册中心之间的网络不通;
- 服务提供者 B 和注册中心之间的网络不通;
- 服务提供者 B 有些节点性能变慢;
- 服务提供者 B 短时间内出现问题。
可见,一次服务调用,服务提供者、注册中心、网络这三者都可能会有问题,此时服务消费者应该如何处理才能确保调用成功呢?这就是服务治理要解决的问题。
节点管理
1. 注册中心摘除机制
这种机制要求服务提供者定时的主动向注册中心汇报心跳,注册中心根据服务提供者节点最近一次汇报心跳的时间与上一次汇报心跳时间做比较,如果超出一定时间,就认为服务提供者出现问题,继而把节点从服务列表中摘除,并把最近的可用服务节点列表推送给服务消费者。
2. 服务消费者摘除机制
一般情况下,服务提供者节点不是唯一的,多是以集群的方式存在,尤其是对于大规模的服务调用来说,服务提供者节点数目可能有上百上千个。由于机器采购批次的不同,不同服务节点本身的配置也可能存在很大差异,新采购的机器 CPU 和内存配置可能要高一些,同等请求量情况下,性能要好于旧的机器。对于服务消费者而言,在从服务列表中选取可用节点时,如果能让配置较高的新机器多承担一些流量的话,就能充分利用新机器的性能。这就需要对负载均衡算法做一些调整。
负载均衡
一般情况下,服务提供者节点不是唯一的,多是以集群的方式存在,尤其是对于大规模的服务调用来说,服务提供者节点数目可能有上百上千个。由于机器采购批次的不同,不同服务节点本身的配置也可能存在很大差异,新采购的机器 CPU 和内存配置可能要高一些,同等请求量情况下,性能要好于旧的机器。对于服务消费者而言,在从服务列表中选取可用节点时,如果能让配置较高的新机器多承担一些流量的话,就能充分利用新机器的性能。这就需要对负载均衡算法做一些调整。
主要算法有:
- 随机
- 轮询:可以给配置高的机器加权,权重一点。
- Least Connections(最小连接):优先选择连接数最少的服务器,在普遍会话较长的情况下推荐使用。
- Source:根据请求源的 IP 的散列(hash)来选择要转发的服务器。这种方式可以一定程度上保证特定用户能连接到相同的服务器。
服务路由
对于服务消费者而言,在内存中的可用服务节点列表中选择哪个节点不仅由负载均衡算法决定,还由路由规则确定。
所谓的路由规则,就是通过一定的规则如条件表达式或者正则表达式来限定服务节点的选择范围。
为什么要制定路由规则呢?主要有两个原因。
- 业务存在灰度发布的需求
就是提供的功能变更,只希望让少部分人使用。用来限定服务范围。
- 多机房就近访问(优先选择同一 IP 段的节点)
主要有两种配置方式。
- 静态配置
就是在服务消费者本地存放服务调用的路由规则。
- 动态配置
这种方式下,路由规则是存在注册中心的,服务消费者定期去请求注册中心来保持同步,要想改变服务消费者的路由配置,可以通过修改注册中心的配置,服务消费者在下一个同步周期之后,就会请求注册中心来更新配置,从而实现动态更新。
服务容错
常用的服务出错后的解决手段有:
- FailOver:失败自动切换。就是服务消费者发现调用失败或者超时后,自动从可用的服务节点列表总选择下一个节点重新发起调用,也可以设置重试的次数。这种策略要求服务调用的操作必须是幂等的,也就是说无论调用多少次,只要是同一个调用,返回的结果都是相同的,一般适合服务调用是读请求的场景。
- FailBack:失败通知。就是服务消费者调用失败或者超时后,不再重试,而是根据失败的详细信息,来决定后续的执行策略。比如对于非幂等的调用场景,如果调用失败后,不能简单地重试,而是应该查询服务端的状态,看调用到底是否实际生效,如果已经生效了就不能再重试了;如果没有生效可以再发起一次调用。(类似于信号的 restart )
- FailCache:失败缓存。就是服务消费者调用失败或者超时后,不立即发起重试,而是隔一段时间后再次尝试发起调用。比如后端服务可能一段时间内都有问题,如果立即发起重试,可能会加剧问题,反而不利于后端服务的恢复。如果隔一段时间待后端节点恢复后,再次发起调用效果会更好。
- FailFast:快速失败。就是服务消费者调用一次失败后,不再重试。实际在业务执行时,一般非核心业务的调用,会采用快速失败策略,调用失败后一般就记录下失败日志就返回了。
一般情况下对于幂等的调用,可以选择 FailOver 或者 FailCache,非幂等的调用可以选择 FailBack 或者 FailFast。
参考:极客时间之从0开始学微服务