【分布式系统遨游】分布式通讯

今天咱们来讨论分布式通讯技术。java

为何须要分布式通讯

咱们以前在讲分布式资源调度的时候,把分布式系统中的各个节点与操做系统的进程作了类比。咱们知道,操做系统的进程之间因为须要数据的交换,是须要进程通讯机制的。那么同理,分布式系统之间一样须要通讯。在业务层面,每一个分布式系统通常都承载着一个微服务,因此,微服务之间也必定是须要通讯的。好比,咱们各条业务线均须要查询用户中心微服务的数据等等。咱们经常使用的通讯方式有三种:RPC、发布-订阅、消息队列。算法

RPC

在传统的B/S模式中,服务端会对外暴露接口,而后客户端经过调用这个接口来完成两者之间的通讯。那么在分布式系统中,咱们一样也能够采用这种模式。可是,B/S 架构是基于 HTTP 协议实现的,每次调用接口时,都须要先进行 HTTP 请求。这样既繁琐又浪费时间,不适用于有低时延要求的大规模分布式系统,因此远程调用的实现大多采用更底层的网络通讯协议。咱们先用一张图俯瞰一下RPC的架构:

在这里,订单系统进程并不须要知道底层是如何传输的,在用户眼里,远程过程调用和调用一次本地服务没什么不一样。这,就是 RPC 的核心。即图中的第3步和第8步,对咱们调用方是透明的。与咱们常用的接口调用不一样,图中的网络通讯基本是基于TCP协议本身封装的一些协议。这样作能够约定通讯双方的数据格式,从而让客户端封包和服务端解包更加快速,更加适用于分布式系统。这里的通讯协议封装可参考Redis的RESP协议与FastCGI协议。spring

RPC的典型实现 - Dubbo

假设咱们要本身去实现一个RPC通讯框架,咱们应该如何实现呢?假如咱们用4个调用方与4个服务提供方,咱们该如何管理他们呢?

首先,咱们最容易想到的,就是服务提供方为服务调用方,提供相关的SDK,服务调用方直接引入SDK便可发起RPC调用请求,而SDK内部具体是利用什么协议,调用方并不关心。这是一种方案。可是,随着服务提供方和服务调用方愈来愈多,服务调用关系会越发复杂。假设服务提供方有 n个, 服务调用方有 m 个,则调用关系可达 n*m,这会致使系统的通讯量很大,SDK就显得力不从心了。此时,你可能会想到,在计算机领域,全部的问题均可以经过增长一个中间层来解决。那么,咱们为何不使用一个服务注册中心来进行统一管理呢,这样调用方只须要到服务注册中心去查找相应的地址便可,并不关心有多少个服务提供方,从而实现了服务调用方与服务提供方的解耦:

Dubbo 在引入服务注册中心的基础上,又加入了监控中心组件(用来监控服务的调用状况,以方便进行服务治理),实现了一个 RPC 框架。以下图所示,Dubbo 的架构主要包括 4 部分:数据库

  • 服务提供方。服务提供方会向服务注册中心注册本身提供的服务。
  • 服务注册中心。服务注册与发现中心,负责存储和管理服务提供方注册的服务信息和服务调用方订阅的服务类型等。
  • 服务调用方。根据服务注册中心返回的服务所在的地址列表,经过远程调用访问远程服务。
  • 监控中心。统计服务的调用次数和调用时间等信息的监控中心,以方便进行服务管理或服务失败分析等。

下面是Dubbo官网给出的一个调用方的Demo。首先是对须要调用的服务在服务注册中心的地址进行配置:apache

<?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了:segmentfault

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 会负责消费哪些分区等。架构

消息队列

消息队列与发布-订阅模型比较类似,可是也有一些不一样之处。接着咱们以前吃鸡的例子来讲,消息队列并不关心谁须要什么子弹,只把本身多的资源放到某个位置,让队友来拿就行了。若是队友有须要,自取便可。消息队列并不直接把资源分配到某个具体消费者,只负责发布到消息队列中,而后消费者各取所需。最典型的一个场景就是异步通讯。
举个例子,用户注册须要写数据库、发送邮件,按照最简单的同步通讯方式,那么从用户提交注册到收到响应,须要等系统完成这两个步骤,才会给用户返回注册成功。若是发送邮件耗时很是之长,那么用户就得一直等下去:

以下图所示,若是引入消息队列,做为注册消息写入数据库和发送邮件、短信这三个组件间的中间通讯者,那么这三个组件就能够实现异步通讯、异步执行:

即用户只须要在写入数据库以后,写入发送邮件的消息队列便可返回注册成功,而并不须要等待真正的去发送邮件以后才会返回。因此,咱们解除了注册与发送邮件两种操做之间的耦合,大大提升了注册的响应速度。那你可能会问,若是发送邮件失败了怎么办?咱们通常会在业务层写一些重试逻辑,确保邮件发送成功以后,才算成功消费。而队列通常也会有持久化机制,确保消息不会丢失。
除了将同步转化为异步,消息队列在高并发系统中也承担着流量削峰的做用。对于流量控制,还有漏桶和令牌桶算法,感兴趣的读者能够进一步去了解。并发

下期预告

【分布式系统遨游】分布式计算app

关注咱们

欢迎对本系列文章感兴趣的读者订阅咱们的公众号,关注博主下次不迷路~

Nosay

相关文章
相关标签/搜索