实战篇 | 19 网络通信与序列化

进程间通信

通信与机器学习的关系

这些内容看起来好像和机器学习没有关系,但是对于一个完整的分布式实时处理系统来说,机器学习的框架是基于实时分布式处理系统的基础上去架构的,从而形成一个完整的Machine Learning Platform。如下图:

同一台机器内的进程间通信
  • 为什么需要进程间通信

思考这样一个背景问题
场景一:

我们为什么需要进程间通信,大家想想这样一个场景,我们有一个10*10的数据,我们有一个程序读入我们的数据,输出结果。这个时候呢,我们是不需要进程间通信的,在一个电脑程序端即可完成。但是假如我们的数据是100万,我们依次读进来计算是非常耗时的,我们在看一个算法的时候会计算算法复杂度,如果是n的线性增长还好,如果是n的平方或三次方的指数增长,这是时间增长就非常耗时了,这个时候紧紧靠优化算法n^2编程log(n)是不够的。也就说,随着数据量的增大,我们的计算时间会越来越长。

场景二:

Split程序负责读取数据,分给我们四个Sum任务,每个任务是一个单独的进程。

那我们看如果这样,假设我们的电脑有四个核,可以同时启用四个进程。那么我们只需要把数据分成4份,分别给四个进程,假设原来需要100,那么这次需要25s,而之后汇总s1 - s4的做一次求和,时间可以忽略不计,因为太短了。这样,我们加快了计算速度。

你可能有一个问题,比如做过全局提速的话,同时有多个任务执行,你可能最先想到的是用多线程的方式去计算。这边为什么用进程呢?

这里有几个因素:
其一,线程这个概念并不是很早就有的,也就是说并不是操作系统一出现,就有线程这个概念。在很早以前,只有进程这个概念,没有线程这个概念,如果你想让一个机器同时并行的去做计算的话,你就同时使用多个进程,而没有线程。

其二,假如我们是做分布式运算,这四个sum可能他是跑在不同的电脑上的,那大家想想看,线程有什么要求,它一定要在同一个进程里面。一个进程肯定只在一台电脑运行,但数据量特别大,你需要同时有几台机器去做运算的时候。线程就不顶用了,我肯定要考虑怎么用多个进程做运算。

其三,我们可能用一些特殊的技术,它本身是不支持多线程的。什么东西不支持多线程呢,像现在这个还是挺多的,最典型的就是node.js,它是作为高并发的Web服务器,它的问题就是它不支持多线程。除非说你用C++去实现它的一个native module。否则它本身是不支持多线程的。

其四,单个进程内存有很大的限制,导致我们可能会回到三四十面前那种模型,用多进程去解决问题。

基于种种限制,我们必须考虑我们的进程间通信,而不是使用线程类实现我们的类型计算。所以说我们需要进程间通信,它是非常重要的事情。

  • 如何进行进程间通信

大家知道,操作系统为了保护我们程序的安全,它给每个程序都设置了类似一个盒子的机制,什么意思呢?

假如我有两个程序A、B,A如果没有通过操作系统的某种权限的话,它是不可能访问A的内存的。

大家想想我们刚刚的Split程序,怎么把读取的数据发我四个Sum任务的?
如果不使用一些特殊手段的话,我们无法访问完全独立的Sum内存,这是做不到的。

那关于如何进行进程间通信又分为两个层面:
其一,同一台机器内的进程间通信,如果我们的进程都在一台机器上,我们怎么把数据传输,即一台机器同时开多个进程,怎么进行进程间的通信。
其二,不同机器之间的进程间通信,我们的进程分布在不同机器上的,我们不同机器上的进程怎么进行通信。

所以我们需要分两个层面来讲,先谈谈同一台机器内的进程之间的通信,后面在谈谈不同机器之间的进程间通信,即网络通信。

既然进程间的通信是操作系统做的,那么我们自然就只能求助于操作系统,如果你不求助于操作系统的话,你是不可能和另外一个进程产生任何关系的。那我们怎么样打破这个限制呢,我们就依赖于操作系统提供的一些机制。这些机制可以保证我们在不影响安全性的情况下可以实现进程间的通信,其中有一个东西叫做消息队列

1 - 消息队列

我们可以把消息队列看成是系统内核的数据结构。也就是说我们的消息队列在系统内核里面, 假如我想让进程1和进程2直接通信,我可以通过系统创建一个消息队列。

如何创建消息队列
比如说我们在Linux下可以通过一个消息队列的调用来创建消息队列。

进程1: 每个消息队列它都有一个唯一的标识符叫做Key。我们可以用进程1创建一个消息队列叫做Create Queue,给它一个唯一标识符叫做Key,然后我们可以往消息队列里塞入数据,那么一个消息队列它其实就是普通的队列。在数据结构中,队列的入队它遵循一个先入先出的原则。

进程2:需要获取这个信息队列。假设我们约定好了这个消息息队列叫做10001,Get Queue 10001,取数据我们称之为Dequeue 出队。取出第一个它就消失了,然后依次取消息2,消息3… ,直到把我们的信息队列里的消息取完为止。

那我只需要不断的加消息即可,加一下就出来一下。这样就实现了进程1和进程2之间的通信。

那么这边就有个问题了,它是单向通信

很多事情我们需要一个双向通信,比如进程1是任务的调度者,进程2是任务的执行者。我希望让进程2计算Sum和,计算完之后将结果返回给进程1。那我们怎么返回呢,其实还是通过消息队列机制,我们创建第二个消息队列。

在消息队列,所有的事情都是发消息和收消息,我们基于消息队列去开发出来的系统它就是一个基于消息的系统。我们可以发现以后很多系统都是基于这个模式去做的。

Best Practice:
比如说我们做一个Web服务,如果我们的计算量非常的简单,我们可能用户请求过来,我计算完就返回给客户端了,那问题就来了,如果我的计算非常非常耗时怎么办?

你可能第一个想到的是开一个线程去做运算。
这里面就有一个问题了,这个时候我们的程序就有一个健壮性问题,如果你的计算非常耗时,如果我把所有的运算全放在一个服务器程序里面,如果你一个线程出问题了,那你可能会扩散影响到整个服务。这件事在系统健壮性上是不可容忍的。所以我们为了提高系统的健壮性,我们往往把那些非常复杂、非常耗时的服务单独使用一些特殊的程序去做运算。那我们的Web服务器和我们的计算服务器怎么通信呢。就通过消息队列去通信,比如说你计算的服务垮掉了,那不会影响我Web服务的任何东西。可以非常好的提高我程序的健壮性。

另一方面我们可以通过消息队列实现多个进程的并行运算。这个后面再来讲这个问题。

这种进程间通信的模型是非常常用的模型。

还有什么其他进程间通信的手段呢, 这就是接下来要介绍的共享内存。

2 - 共享内存

刚刚讲,我们的操作系统会对我们的进程有保护,在没有授权的情况下,进程1的内存是没有办法访问进程2的内存的。进程2也没有办法访问进程1的内存。那他们是如何做这个保护的呢?

操作系统保护程序进程的思路
它的思路很简单,我们进程内部的模型它都是一个线性地址模型,进程内部的内存是虚拟内存,哪怕你用C++去做内存操作。你拿到那个地址也不是你的物理内存地址。假如你是32位的系统,你每个内存都可以看成0 - 4G的线性内存空间。你的地址永远是0 - 4G。每个进程都有一个0 - 4G的单独的地址空间。那这就出现问题了,我每个进程都要4G的地址空间,那它到底怎么样和我们的实际物理内存发生数据交换呢。怎么做呢?

虚拟内存如何与物理内存对应
它实际上就是把我们内存地址里面的虚拟内存和我们的物理内存做一个映射。比如我们创建一个进程1,我们会为他分配一个虚拟内存0 - 4G,然后我们在我们的物理内存分配出一块物理内存。实际内存我们拿去用,让我在物理内存和虚拟内存之间建立一个映射。怎么映射你不用管,交给系统和CPU去计算,它帮你完成。

所以说我们在进程1里面访问1000这个地址和进程2里面访问1000这个地址是不一样的。他们会访问不同的两个物理内存地址,这样我就可以做到 进程1和进程2是完全隔绝了。所以他们两个无论怎么计算,地址不会交叉到一起,系统会保证这点。正常情况是这个样子的,特殊情况我们会讲这个问题。

那我们就不能用非常简单的手段让一个进程去破坏另外一个进程的内存了。不然的话那就太恐怖了,假如你写一个程序,然后我写第二个程序,我可以无缘无故来拿你进程的内存。当然技术上有些手段,这些手段我们可以不考虑它。

那么在这种情况下,我们应该怎样让进程1去访问进程2的内存呢。

进程1如何访问进程2的内存 — 共享内存
简单说,我们之所以两个进程间没法交换数据,原因是操作系统帮我们把内存做了隔离。那我们如果能够把内存保护之间的隔离打破,我们是不是就可以互相访问对方的内存了。那我怎么打破它呢。既然这个保护是操作系统加上的。那么必须借助操作系统来打破内存保护的屏障。

我们看一下我们如何打破进程之间的内存保护。这个东西就叫做共享内存。

如果说操作系统可以把握两块虚拟内存空间都映射到同一块物理内存上。那我是不是就可以通过这片物理内存去交换数据了。如果我用的是同一块物理内存,那么我在进程1里面修改数据,我进程2就可以马上看到了。

如何创建共享内存
靠系统的一些API,比如说Linux它启动了创建共享内存的API,那我们怎么创建它呢,和原来的消息队列是一样的。
第一步我们要给共享内存一个key-编号,比如9980,那么我们进程1和进程2就约定了我们使用9980号共享内存来共享我们的数据。

然后,其中一个进程创建9980号共享内存。它做一个映射,它获取我们的9980号共享内存,并且呢使用系统调用把它映射到我们的某一个指针上面去,大家知道指针就可以指向一个地址。假设我们能让我们的指针指向共享内存的首地址。那我是不是就可以操纵这个内存了。

进程2就可以获取这个内存,因为我知道编号,然后我在把这个内存地址通过系统调用映射到我进程内部的一个指针上面去。这时候如果我使用进程2的指针去操纵这片内存,那么我就可以通过进程1的指针拿到在共享内存里由进程2产生的一些属性。

共享内存如何通信
那么这里面就有个问题了,在我们刚刚的消息队列里面,通信它有个顺序,我只有进程1向进程2发送消息的时候,进程2才能拿到这个消息并且去处理。进程1和进程2之间就有个顺序问题,进程1发送,进程2接收。那如果没有,就接收不到,什么都干不了。但如果我们使用共享内存,那就没有任何限制了。你拿到这块内存,你想干嘛就干嘛。线程之间是怎么互相协同的,拿到共享内存我们进程之间就怎么互相协同。如果你对多线程编程有概念,那你就知道线程自己同步。比如说要防止死锁,要防止竞争这些问题,那如果说我们用了共享内存。那我们就可以认为进程之间就很像两个线程。因为他们自己是使用一片内存的。那我们用在线程之间的同步方案就可以用在我们的进程里面去做进程之间的同步方案。但这不是我们的主题所以就不去细讲了。

我们之后不会用共享内存这种比较危险的方式来实现我们进程间的通信。但作为基础概念我们必须提出来我们有这种方式是可以直接用的。

Q&A:
是不是进程之间的通信用消息队列就可以了?

虽然我说消息队列是现在用的比较广泛的一种方式,每个东西它用的广泛是有原因的,一、它是完全隔离的模型,它不仅可以做单机之间的进程之间通信,多机之间也可以抽象出队列模型,所以说它是一个比较范用的通信模型,第二个原因是因为安全性,A和B自己假如你用消息队列,他们互相无法访问对方内存的。就可以保证我们系统对内存的一个保护,但是,它的害处是消息是顺序处理的。所以如果我们不希望顺序处理,这个模型就不是一个非常好的模型了。第二个问题是它是一个隔离性比较好的模型,整个通信效率就相对来说比较低。我们后面会讲为什么效率比较低,相对来说,共享内存它是一个效率比较高的方式,但它是一种非常危险的方式。说白了是一把双刃剑,所以我们为了日常提高我们的系统安全性和健壮性,我们一般会选择消息队列,但在一些特殊系统里面呢,我们还是会用一些共享内存的方式。

到现在为止,我们都假设说我们的进程是跑在同一台机器上的, 如果是不同的机器如何进行进程间通信呢

我原来的那些方案都是基于操作系统的,比如说我的消息队列它是创建在操作系统内核里面的。我的共享内存是由我的操作系统内核去统筹管理的。

那现在我的机器A和机器B之间,他们的操作系统是完全独立的,是没法直接去共享操作的。那么机器内部的进程间通信方案放在机器之间就完全没有任何用武之地。

那现在我们就来看一下网络通信。

不同机器之间的网络通信

网络通信概述

如果我们在A、B之间搭一根线,就是我们的网线,负责通过网线进行数据收发的硬件就是我们的网卡。就可以实现A和B两台机器的直接通信。

那么如果有三台机器,我们如何通信呢?

当我有三台机器,每个机器都要配两个网卡,每个网卡都需要网线连接,那么我们就需要有6个网卡,三条网线。当我们扩展出很多机器的时候,我们的网络可扩展性是非常非常差的。所以我们肯定不能用我们这种直接的方式来连接我们的机器。如果我的机器过多,我对我的网卡做一个硬编码,这又是一个很难扩展的行为。我们每加一台机器,我们都需要改一下配置或改一下代码才能做到更多机器去通信这肯定是无法容忍的。

这就涉及到一个问题:如何寻找机器

如何寻找机器:总线与物理地址

我们有一个比较特殊的方式叫做:总线

我们知道怎么做两台机器之间的通信,那么我们怎么解决多台机器之间的通信,我们搭一根总线,所有电脑都可以在总线上收发数据,假设说A想向B发消息,它就把一个数据放到我的总线上,假如这个数据叫做Hello,并且我告诉是由哪台机器来接收的,比如B来接收Hello(B),这条数据它就传遍整条总线。C和B都会来总线上获取数据,大家就看一看自己是不是这条数据发送的目标。数据过C的时候呢,假如C发现这个数据不是我的,他就不去拿它。当数据到B的时候,B发现这条数据是它的,B就可以把这条数据取过去。那我这条数据就送到了B这边。同样如果A想向C发一条数据呢,我只需要在它发的数据末尾标明它的接收端是

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值