今天我们来讨论分布式通信技术。
为什么需要分布式通信
我们之前在讲分布式资源调度的时候,把分布式系统中的各个节点与操作系统的进程做了类比。我们知道,操作系统的进程之间由于需要数据的交换,是需要进程通信机制的。那么同理,分布式系统之间同样需要通信。在业务层面,每个分布式系统一般都承载着一个微服务,所以,微服务之间也一定是需要通信的。比如,我们各条业务线均需要查询用户中心微服务的数据等等。我们常用的通信方式有三种:RPC、发布-订阅、消息队列。
RPC
在传统的B/S模式中,服务端会对外暴露接口,然后客户端通过调用这个接口来完成二者之间的通信。那么在分布式系统中,我们同样也可以采用这种模式。但是,B/S 架构是基于 HTTP 协议实现的,每次调用接口时,都需要先进行 HTTP 请求。这样既繁琐又浪费时间,不适用于有低时延要求的大规模分布式系统,所以远程调用的实现大多采用更底层的网络通信协议。我们先用一张图俯瞰一下RPC的架构:
在这里,订单系统进程并不需要知道底层是如何传输的,在用户眼里,远程过程调用和调用一次本地服务没什么不同。这,就是 RPC 的核心。即图中的第3步和第8步,对我们调用方是透明的。与我们经常使用的接口调用不同,图中的网络通信基本是基于TCP协议自己封装的一些协议。这样做可以约定通信双方的数据格式,从而让客户端封包和服务端解包更加快速,更加适用于分布式系统。这里的通信协议封装可参考Redis的RESP协议与FastCGI协议。
RPC的典型实现 - Dubbo
假设我们要自己去实现一个RPC通信框架,我们应该如何实现呢?假如我们用4个调用方与4个服务提供方,我们该如何管理他们呢?
首先,我们最容易想到的,就是服务提供方为服务调用方,提供相关的SDK,服务调用方直接引入SDK即可发起RPC调用请求,而SDK内部具体是利用什么协议,调用方并不关心。这是一种方案。但是,随着服务提供方和服务调用方越来越多,服务调用关系会愈加复杂。假设服务提供方有 n个, 服务调用方有 m 个,则调用关系可达 n*m,这会导致系统的通信量很大,SDK就显得力不从心了。此时,你可能会想到,在计算机领域,所有的问题都可以通过增加一个中间层来解决。那么,我们为什么不使用一个服务注册中心来进行统一管理呢,这样调用方只需要到服务注册中心去查找相应的地址即可,并不关心有多少个服务提供方,从而实现了服务调用方与服务提供方的解耦:
Dubbo 在引入服务注册中心的基础上,又加入了监控中心组件(用来监控服务的调用情况,以方便进行服务治理),实现了一个 RPC 框架。如下图所示,Dubbo 的架构主要包括 4 部分:
- 服务提供方。服务提供方会向服务注册中心注册自己提供的服务。
- 服务注册中心。服务注册与发现中心,负责存储和管理服务提供方注册的服务信息和服务调用方订阅的服务类型等。
- 服务调用方。根据服务注册中心返回的服务所在的地址列表,通过远程调用访问远程服务。
- 监控中心。统计服务的调用次数和调用时间等信息的监控中心,以方便进行服务管理或服务失败分析等。
下面是Dubbo官网给出的一个调用方的Demo。首先是对需要调用的服务在服务注册中心的地址进行配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<!-- consumer's application name, used for tracing dependency relationship (not a matching criterion),
don't set it same as provider -->
<dubbo:application name="demo-consumer"/>
<!-- use multicast registry center to discover service -->
<dubbo:registry address="multicast://224.5.6.7:1234"/>
<!-- generate proxy for the remote service, then demoService can be used in the same way as the
local regular interface -->
<dubbo:reference id="demoService" check="false" interface="org.apache.dubbo.demo.DemoService"/>
</beans>
然后,在业务代码中调用刚刚配置好的服务提供方地址即可。我们不再需要SDK了:
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.apache.dubbo.demo.DemoService;
public class Consumer {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"META-INF/spring/dubbo-demo-consumer.xml"});
context.start();
// Obtaining a remote service proxy
DemoService demoService = (DemoService)context.getBean("demoService");
// Executing remote methods
String hello = demoService.sayHello("world");
// Display the call result
System.out.println(hello);
}
}
发布-订阅
发布-订阅的思想在生活中随处可见。比如我们吃鸡的时候,一般是4个人组队开黑,我们在分布式系统中可以比作4个节点。举一个相当经典的场景,比如我跳了机场,资源很多,有5.56的子弹、7.62的子弹等。于是我就和队友说,我多5.56和7.62子弹,谁需要的话和我说一下。但是有些队友去打野了,就会比较穷,他们就会和我说,我要5.56子弹或者我要7.62子弹。然后,我就会找到这个穷队友,然后把相应的5.56和7.62子弹分给他们,这样就完成了一次发布-订阅的流程。其中,”我就和队友说,我多5.56和7.62子弹,谁需要的话和我说一下“,这个就是将”我多子弹“这个消息事件发布出去,然后很穷的队友说”我需要xxx子弹“,就相当于订阅我发布的这个消息事件,然后我就会把子弹给到他们,这个子弹就相当于我们的消息,这样就完成了一次发布-订阅模型的通信:
其中,生产者可以发送消息到中心,而消息中心通常以主题(Topic)进行划分,每条消息都会有相应的主题,它代表该条消息的类型。订阅该主题的所有消费者均可获得该消息进行消费。这里我们的5.56子弹与7.62子弹,就相当于两个topic,我们可以订阅其中一个topic,来获得我们需要的子弹类型。
发布-订阅的典型实现 - Kafka
Kafka是一个典型的发布订阅消息系统,其系统架构也是包括生产者、消费者和消息中心三部分:
在Kafka中,为了解决消息存储的负载均衡和系统可靠性问题,所以引入了主题(topic)和分区(partition)的概念。topic的概念我们刚才讲过了,它是一个逻辑概念,指的是消息类型或数据类型。那么分区是基于topic而言的。一个topic的内容可以被划分成多个分区,而这些分区又分布在不同的集群节点上,每个分区的数据内容依赖数据同步机制,来确保每个分区内部存储数据的一致性:
每个broker就代表了一个集群中的物理节点。通过分区机制,我们避免了“将数据都放在一个篮子里”,将数据分散在不同的broker机器上,提高了系统的数据可靠性,且实现了负载均衡。
在图中,还有一点不一样的地方就是,有两个消费者组成了一个消费组。那么为什么要引入消费组呢?我们知道,在消息过多的情况下,单个消费者消费能力有限时,会导致消费效率过低,从而导致 Broker 存储溢出,从而不得不丢弃一部分消息。Kafka为了解决这个问题,所以引入了消费组,提高了消费的速度。
在Kafka中,除了基本的三要素之外,还使用了Zookeeper。ZooKeeper是一个提供了分布式服务协同能力的第三方组件,用来协调和管理整个集群中的Broker和Consumer,实现了Broker 和Consumer的解耦,并为系统提供可靠性保证。Consumer 和 Broker 启动时均会向 ZooKeeper 进行注册,由 ZooKeeper 进行统一管理和协调。
ZooKeeper 中会存储一些元数据信息,比如对于 Broker,会存储主题对应哪些分区(Partition),每个分区的存储位置等;对于 Consumer,会存储消费组(Consumer Group)中包含哪些 Consumer,每个 Consumer 会负责消费哪些分区等。
消息队列
消息队列与发布-订阅模型比较相似,但是也有一些不同之处。接着我们之前吃鸡的例子来说,消息队列并不关心谁需要什么子弹,只把自己多的资源放到某个位置,让队友来拿就好了。如果队友有需要,自取即可。消息队列并不直接把资源分配到某个具体消费者,只负责发布到消息队列中,然后消费者各取所需。最典型的一个场景就是异步通信。
举个例子,用户注册需要写数据库、发送邮件,按照最简单的同步通信方式,那么从用户提交注册到收到响应,需要等系统完成这两个步骤,才会给用户返回注册成功。如果发送邮件耗时非常之长,那么用户就得一直等下去:
如下图所示,如果引入消息队列,作为注册消息写入数据库和发送邮件、短信这三个组件间的中间通信者,那么这三个组件就可以实现异步通信、异步执行:
即用户只需要在写入数据库之后,写入发送邮件的消息队列即可返回注册成功,而并不需要等待真正的去发送邮件之后才会返回。所以,我们解除了注册与发送邮件两种操作之间的耦合,大大提高了注册的响应速度。那你可能会问,如果发送邮件失败了怎么办?我们一般会在业务层写一些重试逻辑,确保邮件发送成功之后,才算成功消费。而队列一般也会有持久化机制,确保消息不会丢失。
除了将同步转化为异步,消息队列在高并发系统中也承担着流量削峰的作用。对于流量控制,还有漏桶和令牌桶算法,感兴趣的读者可以进一步去了解。
下期预告
【分布式系统遨游】分布式计算
关注我们
欢迎对本系列文章感兴趣的读者订阅我们的公众号,关注博主下次不迷路~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。