1 进程间通信
和单体应用不同的是,单体应用部署在同一台机器,属于同一个进程,各个模块之间通过函数互相调用;但是微服务架构中的服务实例通常是部署在多个机器上的,所以必须使用进程间通信。
有很多进程间通信技术可供参考。服务可以使用基于同步/异步的通信机制,同时服务通信的消息格式也是不相同的,服务可以使用具有可读性的格式(JSON/XML),也可以使用更高效的二进制格式。
1.1 交互方式
有多种客户端和服务的交互方式,可以分两个维度。
1、一对一和一对多
- 一对一:每个客户端请求由一个服务实例来处理
- 一对多:每个客户端请求由多个服务实例来处理
2、同步和异步 - 同步:客户端调用服务端,必须等待服务端响应
-
异步:客户端的请求不会阻塞进程
各种交互方式可以用两个维度来表示:
image.png
一对一:
- 请求/响应:客户端向服务端发起请求,并等待响应,等待的过程可能造成线程阻塞,并且会造成服务的紧耦合
- 异步请求/响应:客户端发送请求到服务器,服务器异步响应请求,客户端不会阻塞
- 单向通知:客户端的请求发送到服务端,不期望服务端作出任何响应
一对多:
发布/订阅:客户端发出消息,发送给多个订阅者
发布/异步响应:客户端发布消息,并等待订阅者的响应。
1.2 API演化
API作为客户端和服务端的通信桥梁,不可避免的会随着功能的增加而发生变化。在单体应用中,变更API是相对简单并且不容易出错的事,因为更改之后编译器会对不兼容的调用提示编译错误;但是基于微服务的API更改起来就比较麻烦了,你并不能保证客户端和你的api保持一致,一般有两种处理方法
- 努力的进行向后兼容的更改,对API附加更强能功能,包括添加可选属性、向响应添加属性等,如果只是进行这些类型的更改,那么老版本的客户端依然可以继续使用更新后的服务,但是客户端和服务端必须同时支持健壮性原则的请求响应内容
- 另一种是无法对已有的api进行更改,因此服务必须在一段时间内同时支持新旧版本的API,如getV1和getV2,旧版本的API可以在下一个版本提出需求去掉
1.3 消息格式
如果使用了HTTP歇息,那么需要选择具体的消息格式(JSON/XML);有些进程间的通信机制已经指定了消息格式(gRPC/Thrift)。我们不应该使用类似Java序列化这类跟变成虚言强相关的消息格式,因为今后可能会扩展到其他的语言。
2 使用断路器保护系统
分布式系统中,当服务试图向另一个服务发送请求,随时都会面临着局部故障的风险,因为二者是独立的进程,如下图所示:
image.png
此时服务B和服务A将会无限期的阻塞,所以要通过合理的设计服务来防止整个应用程序的故障传导
- 网络超时:请求链接一定不要做成无限阻塞,而是要设定超时时间,指定时间内没有完成调用变返回超时错误
- 限流:服务接收请求是有能力上限的,当请求到达上线,让请求立即失败,可以保护服务端
- 断路器模式:服务端监控请求的成功和失败的数量,如果失败率超过一定比例就启动断路器,让后续的调用都失效。在经过一定的时间客户端应该继续尝试,如果调用成功就解除断路器。
- 弹性扩容/缩容:当访问量达到N时,增加一台机器,当访问量减少到M时,减少一台机器,可以提高系统处理请求的能力
3 服务发现机制
服务发现和服务注册是属于服务治理的概念(治理肯定是有问题了才需要的),你的客户端调用服务端,必须知道服务实例具体的ip地址和端口号,在单体应用中,这些都是固定的,但是在微服务中,因为一个服务可能通过X轴或者Z轴扩展为多个实例,并且服务实例的ip地址会变服务实例集群会动态更改因此微服务必须使用服务发现。
3.1 服务发现概念
其关键组件是服务注册表,它是包含服务实例网络位置信息的一个数据库。当服务实例启动或者停止时,服务发现机制会更新服务注册表。(采用服务发现需要给服务起一个为一个名字,以便于调用方可以调用)
3.2 应用层服务发现模式
3.2.1 概念
客户端会通过服务注册表,获取可用的服务列表,然后通过路由策略选择要调用的实例发送请求。这个过程包括两部分:
- 自注册:服务实例向服务注册表中注册自己的网络位置,并且提供运行状况检查URL以便于服务注册表定期检查服务是否正常(服务还可以通过“续命”的方式,定期调用心跳API防止注册过期)
-
客户端发现:客户端获取服务实例列表,并通过路由策略在它们之间进行负载均衡。
image.png
3.2.2 优点
可以处理多平台部署的问题
3.2.3 缺点
必须为每种编程语言提供API,并且必须由开发人员对服务发现进行开发。
3.3 平台层服务发现模式(服务治理平台)
3.3.1 概念
部署平台包括一个服务注册表,用于跟踪可用服务的IP地址,当由请求时,平台会根据指定的路由策略(同机房优先,同城市优先)在实例之间进行负载均衡,包括两部分:
- 第三方注册模式:注册统一由第三方负责(治理平台的一部分),而不是服务本身注册自己到服务注册表。
-
服务端发现模式:客户端不需要查询服务注册表,然后自己选择实例,而是直接向治理平台发送请求(带上服务的标识),平台查询服务注册表后根据指定路由返回可用实例,客户端再访问这个实例。
image.png
3.3.2 优点
客户端和服务端不需要包含服务发现相关的代码
3.3.3 缺点
对平台有限制
4 基于异步消息模式的通信
4.1 两种类型的消息通道
- 点对点:服务使用点对点通道来实现一对一交互方式
- 发布订阅:将一条消息发送给所有订阅的接收方,使用发布-订阅模式实现一对多的交互方式。
4.2 无代理消息和有代理消息
image.png如上图所示,无代理的架构中服务直接通信,有代理的架构中服务通过代理通信
4.2.1 无代理消息
服务可以直接通信,典型的是ZMQ,虽然操作起来很简单,但是客户端和服务端还是强耦合的方式,双方需要了解彼此的位置(也就是要使用服务发现机制),导致服务双方必须同时存在。
4.2.2 基于代理的消息
发送方将消息写入消息代理,消息代理将消息发送给接受方(发送方并不需要知道接收方的位置,并且可以使用消息队列进行削峰,例如Kafka,kafka使用topic和消费组的概念实现消息机制),可以理解发送方和接受方是松耦合,因为二者不需要知道彼此的位置,并且可以使用消息队列进行削峰;但是因为引入了消息代理,所以会引来消费队列造成的瓶颈问题。
4.2.3 kafka
4.2.3.1 名词解释
- Broker:Kafka节点,一个节点就是一个Broker,多个节点组成kafka集群
- topic:消息主题,供消费者或消费组订阅
- Partition:分片,一个topic可以分为多个分片,从而提高性能
- Producer:生产者,生产信息
- Consumer:消费者,订阅topic
- Consumer Group:消费组,可以包含多个消费者,一个消息只能被一个消费组消费一次。
4.2.3.2 消费逻辑
当启动一个消费组去消费一个topic的时候,无论有多少个consumer,这个consumer group一定回去把这个topic下所有的partition都消费了。当consumer group里面的consumer数量小于这个topic下的partition数量的时候,如下图groupA,groupB,就会出现一个conusmer thread消费多个partition的情况,总之是这个topic下的partition都会被消费。如果consumer group里面的consumer数量等于这个topic下的partition数量的时候,如下图groupC,此时效率是最高的,每个partition都有一个consumer thread去消费。当consumer group里面的consumer数量大于这个topic下的partition数量的时候,如下图GroupD,就会有一个consumer thread空闲。因此,我们在设定consumer group的时候,只需要指明里面有几个consumer数量即可,无需指定对应的消费partition序号,consumer会自动进行rebalance。
image.png
4.2.3.3 消息有序
有序从两方面保证:kafka节点不仅要保存数据的时候有序,并且消费者消费也要有序。kafka是怎么保证单个分片有序的?
- 生产者发送队列时,通过加锁保证发消息顺序(有两个问题,第一个是kafka节点因为网络原因没有及时ack,导致生产者重复发送;另一个是msg1发送失败,msg2发送成功;msg1重新发送导致乱序)。。。为实现Producer的幂等性,Kafka引入了Producer ID(即PID)和Sequence Number。对于每个PID,该Producer发送消息的每个<Topic, Partition>都对应一个单调递增的Sequence Number。同样,Broker端也会为每个<PID, Topic, Partition>维护一个序号,并且每Commit一条消息时将其对应序号递增。对于接收的每条消息,如果其序号比Broker维护的序号)大一,则Broker会接受它,否则将其丢弃:如果消息序号比Broker维护的序号差值比一大,说明中间有数据尚未写入,即乱序,此时Broker拒绝该消息,Producer抛出InvalidSequenceNumber如果消息序号小于等于Broker维护的序号,说明该消息已被保存,即为重复消息,Broker直接丢弃该消息,Producer抛出DuplicateSequenceNumberSender发送失败后会重试,这样可以保证每个消息都被发送到broker。
- Kafka Consumer端是需要维护这个partition当前消费到哪个message的offset信息的,这个offset信息,high level api是维护在Zookeeper上,low level api是自己的程序维护。(Kafka管理界面上只能显示high level api的consumer部分,因为low level api的partition offset信息是程序自己维护,kafka是不知道的,无法在管理界面上展示 )当使用high level api的时候,先拿message处理,再定时自动commit offset+1(也可以改成手动), 并且kakfa处理message是没有锁操作的。因此如果处理message失败,此时还没有commit offset+1,当consumer thread重启后会重复消费这个message。。。重复消费通常会使逻辑错误,需要在代码里进行幂等处理(通过加锁,然后检查数据库/redis的数据)
参考:https://www.zhihu.com/question/266390197/answer/772404605
网友评论