龙空技术网

数据系统 - 数据流的模式

吃泡菜的鱼 292

前言:

眼前小伙伴们对“hornetq发送机制”大约比较重视,各位老铁们都需要学习一些“hornetq发送机制”的相关知识。那么小编也在网摘上汇集了一些对于“hornetq发送机制””的相关内容,希望看官们能喜欢,各位老铁们一起来学习一下吧!

之前的文章中我们说过当你不想共享内存但又想发送数据给另一个进程时,比方说,你想通过网络发送数据或者写一个文件,那么就需要把这些数据编码成一串字节。数据系统 - 数据的编码格式一文中我们就讨论了各种不同的编码方式。

我们谈到了向前和向后的兼容性,这对于演化是很重要的,这会允许你可以单独的升级你系统中的一部分,而不会影响其他部分,且这会变得很简单。兼容性就是一个对数据进行编码的进程与另一个对数据进行解码的进程之间的关系。

数据从一个进程流向另一个进程也存在很多的形式,这是一个相当抽象的概念。谁编码数据?谁又解码数据?接下来我们会讨论一些最常见的进程间数据流动的方式。

通过数据库的数据流

在一个数据库中,往数据库写入数据的进程编码数据,从数据库读出数据的进程解码数据。可能只有一个进程正在访问数据库,在这种情况下,读取数据的进程可能只是同一进程的更高版本-在这种情况下,您可以考虑将某些内容存储在数据库中作为向您自己的未来发送消息。

向后的兼容性在这里必然是需要的,否则未来的你将不能解码你自己写的数据。

一般来说,几个不同的进程同时访问数据库是很常见的。这些进程可能是几个不同的应用或服务,又或是同一个服务的好几个实例(并行运行以实现可扩展性或容错能力)。无论哪种方式,在应用程序不断更新的环境中,访问数据库的某些进程很可能正在运行较新的代码,而某些正在运行较旧的代码,例如,由于当前正在滚动升级部署新版本,因此有些实例已更新,而另一些尚未更新。

这也就意味着数据库的某一个数据有可能被新的新版本的代码写入的,并且随后被一个正在运行老版本代码进程读取。因此,对于数据来说,通常向后的兼容性也是需要的。

但是,有其他障碍。假设您将一个字段添加到记录模式,并且较新的代码将该新字段的值写入数据库。随后,旧版本的代码(尚不知道新字段)将读取记录,对其进行更新,然后将其写回。在这种情况下,通常的做法是使旧代码保持新字段的完整性,即使无法解释也是如此。

先前讨论的编码格式支持这种未知字段的保存,但是有时您需要在应用程序级别格外小心,如下图所示。例如,如果在应用程序中将数据库值解码为模型对象,然后再对这些模型对象进行重新编码,则在该转换过程中未知字段可能会丢失。解决这个问题并不困难。您只需要意识到这一点。

在不同时间写入的不同数值

一个数据库通常允许在任何时间更新任意的值。这也就是说在单个数据库,你会有一些5毫秒之前写入的值,并且你也会有一些5年前写入的值。

当你部署一个你的应用的新版本时,你可能会花一些时间完全取代老的版本。但数据库中的内容不是这样的:五年前的数据会仍然在那里,除非你已经明确重写了它。这种观察有时被总结为data outlives code。

重写(迁移)数据进一个新的模式当然是可行的,但对于一个庞大的数据集而言,开销是巨大的,所以大多数的数据库在可能的情况下都避免这种做法。大多数的关系型数据库允许简单的模式更改,比如在不重写现有值的情况下,增加一列默认值为null的新列。读取旧行时,数据库对磁盘上编码数据中缺少的任何列填充null。 LinkedIn的文档数据库Espresso使用Avro进行存储,从而使其可以使用Avro的模式演变规则。

因此,模式演化使整个数据库看起来好像是用单个模式编码的,即使基础存储可能包含使用模式的各种历史版本编码的记录。

归档存储Archival storage

也许你会对你的数据库时不时的进行一次快照,说这是为了备份或是加载其进入数据仓库。在这样的情况下,数据转储一般会使用最新的模式进行编码,即使在源数据库中存在着来自不同纪元的各种模式版本。由于仍然要复制数据,因此最好对数据副本进行一致的编码。

由于数据转储是一次性写入的,并且此后是不可变的,因此类似Avro对象容器文件的格式非常适合。这也是把数据编码成面向列的,对分析友好的格式的好机会。

通过服务的数据流:REST和RPC

当你有进行需要通过网络通信时,有几种不同的方式来安排这种沟通。最常见的安排就是定义两个角色:客户端和服务器。服务端在网络上暴露API,然后,客户端就可以连接服务器,对API发出请求。服务端暴露的API也被称为服务。

网站是按照如下这种方式工作的:客户端(网页浏览器)向网站服务器发出请求,通过GET请求下载HTML,CCS,JavaScript,图片等,并且通过POST请求向服务器提交数据。API由一组标准的协议和数据格式组成(HTTP,URLs,SSL/TLS,HTML等等)。因为网页浏览器、网站服务器和网站作者大多数都对这些标准达成了一致,因此你可以使用任意的浏览器访问任意的网址(至少理论上是这样的)。

网页浏览器不是唯一的客户端类型。比方说一个运行在移动设备或是台式电脑上的应用也可以向服务器发出网络请求,并且运行在网页浏览器中的客户端的JavaScript应用也可以使用XMLHttpRequest成为一个HTTP客户端(这种技术被称为Ajax)。在这样的情况下,服务器的响应一般不是展示给人的HTML,而是便于客户端应用代码后续处理的数据编码(比方说JSON)。虽然HTTP被用作传输协议,但顶部的API实现是根据应用特定的,客户端和服务器需要在API上细节上达成一致。

此外,服务器自己也可以是另一个服务的客户端(比方说一般网页应用服务器也是数据库的客户端)。这种方法通常用于按功能领域将大型应用程序分解为较小的服务,这样,当一个服务需要来自另一个服务的某些功能或数据时,就会向另一个服务发出请求。这种构建应用程序的方式传统上被称为面向服务的体系结构(service-oriented architecture,SOA),最近被完善并更名为微服务体系结构(microservices architecture)。

在某些方面,服务类似于数据库:它们通常允许客户提交和查询数据。但是,尽管数据库允许使用我们在之前讨论的查询语言进行任意查询,但是服务公开了特定于应用程序的API,该API仅允许输入和输出预先根据业务逻辑(应用程序代码)预先确定好的内容。这种限制提供了一定程度的封装:服务可以对客户端可以做什么和不能做什么进行严格的限制。

面向服务/微服务体系结构的关键设计目标是通过使服务能够独立部署和发展,使应用程序更易于更改和维护。例如,每个服务应由一个团队拥有,并且该团队应能够频繁发布服务的新版本,而不必与其他团队进行协调。换句话说,我们应该期望新旧版本的服务器和客户端能够同时运行,因此服务器和客户端使用的数据编码必须在服务API的各个版本之间兼容,这正是我们一直在谈论的内容。

Web Services

当HTTP用作与服务进行通信的基础协议时,它称为Web Services。这也许是一个轻微的误称,因为Web Services不仅在Web上使用,而且在几种不同的情形中也有被使用到。例如:

在用户设备上运行的客户端应用程序(例如,移动设备上的本机应用程序,或使用Ajax的JavaScript网络应用程序)通过HTTP向服务发出请求。这些请求通常通过公共互联网发送作为面向服务/微服务体系结构的一部分,一个服务向通常位于同一数据中心内的同一组织拥有的另一服务发出请求。 (支持这种用例的软件有时称为中间件。)一种通常通过Internet向其他组织拥有的服务发出请求的服务。这用于不同组织的后端系统之间的数据交换。此类别包括由在线服务(例如信用卡处理系统)提供的公共API,或用于共享访问用户数据的OAuth。

有两种流行的web services方式,REST和SOAP。他们几乎是截然相反的,而且常常是各自支持者之间激烈辩论的主题。

REST不是一个协议,而是基于HTTP上的设计哲学。它强调数据格式的简单化,使用URL标记资源并且使用HTTP特性用来控制缓存,身份验证和内容类型协商。相比于SOAP,REST更受大家欢迎。至少是在跨组织的不同的服务的整合上,并且通常与微服务相关联。一个根据REST原则设计的API被称为RESTful。

相对比,SOAP是一个基于XML的发送网络API请求的协议。虽然绝大多数也是基于HTTP,但它旨在从HTTP中独立出去并且避免使用绝大多数HTTP的特性。相反,它带有广泛而复杂的相关标准(web service framework,称为WS- *),这些标准添加了各种功能。

使用称为Web服务描述语言或WSDL的基于XML的语言来描述SOAP Web服务的API。 WSDL支持代码生成,以便客户端可以使用本地类和方法调用(它们被编码为XML消息并由框架再次解码)访问远程服务。这在静态类型的编程语言中很有用,而在动态类型的编程语言中则很少用到。

尽管表面上对SOAP及其各种扩展进行了标准化,但不同供应商的实现之间的互操作性经常会引起问题。由于所有这些原因,尽管SOAP仍在许多大型企业中使用,但是它已经下降了。在大多数较小的公司中都不受欢迎。

RESTful API倾向于使用更简单的方法,通常涉及更少的代码生成和自动化工具。诸如OpenAPI(也称为Swagger)之类的定义格式可用于描述RESTful API并生成文档。

Remode procedure call(RPC)的问题

Web服务只是用于通过网络发出API请求的众多技术的最新形式,其中许多技术受到了广泛宣传,但存在严重问题。企业JavaBean(EJB)和Java的远程方法调用(RMI)仅限Java。分布式组件对象模型(DCOM)仅限于Microsoft平台。通用对象请求代理体系结构(CORBA)过于复杂,并且不提供向后或向前兼容性。

所有这些都是基于Remode procedure call(RPC)的思想,该思想自1970年代就已经存在。 RPC模型试图在同一个进程内,使对远程网络服务的请求看起来与以编程语言调用函数或方法相同(这种抽象被称为location transparency)。尽管乍一看RPC似乎很方便,但是该方法从根本上来说是有缺陷的。网络请求与本地函数调用有很大不同:

局部函数调用是可预测的,成功或失败取决于仅在您控制下的参数。网络请求是不可预测的:由于网络问题,请求或响应可能会丢失,或者远程计算机可能运行缓慢或不可用,而这些问题完全不在您的控制范围之内。网络问题很常见,因此您必须预见它们,例如,通过重试失败的请求。局部函数调用要么返回结果,要么抛出异常,要么永不返回(因为它进入无限循环或进程崩溃)。网络请求还有另一个可能的结果:由于超时,它可能没有结果返回。在这种情况下,您根本不知道发生了什么:如果您没有从远程服务获得响应,那么您将无法知道请求是否通过。如果重试失败的网络请求,则可能是请求实际上已经通过,只有响应丢失了。在这种情况下,除非您在协议中建立重复数据删除(幂等性)的机制,否则重试将导致该操作多次执行。本地函数调用没有这个问题。每次调用局部函数时,通常需要大约相同的时间才能执行。网络请求比函数调用要慢得多,并且其延迟也很容易变化:在好时机,它可能会在不到一毫秒的时间内完成,但是当网络拥塞或远程服务超载时,它可能需要花费几秒钟的时间才能完成完全一样的东西。调用本地函数时,可以有效地将其引用(指针)传递给本地内存中的对象。发出网络请求时,所有这些参数都需要编码为可以通过网络发送的字节序列。如果参数是数字或字符串之类的原语,那没关系,但是对于较大的对象,很快就会出现问题。客户端和服务可能以不同的编程语言实现,因此RPC框架必须将数据类型从一种语言转换为另一种语言。由于并非所有语言都具有相同的类型,因此这可能很难实现-比如说JavaScript的数字大于2 ^ 53的问题。用单一语言编写的单个进程中不存在此问题。

所有这些因素都意味着,要使远程服务看起来像您的编程语言中的本地对象,没有任何意义,因为这是根本不同的事情。 REST的吸引力之一是它没有试图掩盖它是网络协议的事实(尽管这似乎并没有阻止人们在REST之上构建RPC库)。

RPC的当前方向

尽管存在所有这些问题,但RPC并没有消失。在之前提到的所有编码的基础上,已经构建了各种RPC框架:例如,Thrift和Avro附带了RPC支持,gRPC是使用protobuf实现的RPC,Finagle也使用Thrift,Rest.li则在HTTP上使用JSON 。

对于远程请求与本地函数调用不同的事实,新一代的RPC框架更加明确。例如,Finagle和Rest.li使用futures(promises)来封装可能失败的异步操作。Futures还简化了您需要并行请求多个服务并合并其结果的情况。 gRPC支持流,其中调用不仅包括一个请求和一个响应,还包括一段时间内的一系列请求和响应。

这些框架中的某些框架还提供service discovery-即,允许客户端可以在其中找到特定服务的IP地址和端口号。

具有二进制编码格式的自定义RPC协议可以实现比JSON over REST等通用功能更好的性能。但是,RESTful API具有其他显著优点:它适合进行实验和调试(您可以使用Web浏览器或命令行工具curl对其进行请求,而无需生成任何代码或安装软件),它受到以下方面的支持:所有主流编程语言和平台,以及广泛的可用工具生态系统(服务器,缓存,负载平衡器,代理,防火墙,监视,调试工具,测试工具等)。

由于这些原因,REST似乎是公共API的主要样式。 RPC框架的主要焦点是在同一组织(通常在同一数据中心内)拥有的服务之间的请求。

RPC的数据编码和演变

为了发展,重要的是可以独立地更改和部署RPC客户端和服务器。与通过数据库的数据流相比,对于通过服务的数据流,我们可以做一个简化的假设:合理的假设是首先更新所有服务器,然后更新所有客户端。因此,您只需要关注请求的向后兼容性,以及响应的向前兼容性。

RPC方案的向后和向前兼容性属性是从其使用的任何编码形式那继承的:

Thrift,gRPC(protobug)和Avro RPC可以根据各自编码格式的兼容性规则进行开发。在SOAP中,请求和响应是使用XML模式指定的。这些可以演化,但是有一些细微的小坑。RESTful API最通常使用JSON(没有正式指定的架构)进行响应,并使用JSON或URI编码/表单编码的请求参数。通常将添加可选的请求参数和向响应对象添加新字段视为保持兼容性的更改。

由于RPC通常用于跨组织边界的通信,因此使服务兼容性更加困难,因此,服务的提供者通常无法控制其客户端,因此无法强制其升级。因此,可能需要无限期地保持长时间的兼容性。如果需要破坏兼容性的更改,则服务提供商通常最终会并排维护多个版本的服务API。

对于API版本控制的工作方式(即客户端如何指示要使用的API版本),尚无共识。对于RESTful API,常见的方法是在URL或HTTP Accept标头中使用版本号。对于使用API​密钥标识特定客户端的服务,另一种选择是将客户端请求的API版本存储在服务器上,并允许通过单独的管理界面更新此版本选择。

传递消息的数据流

我们一直在研究编码数据从一个过程流到另一个过程的不同方式。到目前为止,我们已经讨论了REST和RPC(其中一个进程通过网络将请求发送到另一个进程并期望尽快响应)和数据库(其中一个进程写入编码的数据,而另一个进程会在未来的某个时间读取它)。

在最后一部分中,我们将简要介绍异步消息传递系统,该系统位于RPC和数据库之间。它们与RPC的相似之处在于,客户端的请求(通常称为消息)以低延迟传递到另一个进程。它们与数据库相似,因为消息不是通过直接的网络连接发送的,而是通过称为消息代理message broker(也称为消息队列message queue或面向消息的中间件message-oriented middleware)的中介发送的,该中介临时存储消息。

与直接RPC相比,使用消息代理有几个优点:

如果收消息的一方不可用或负载过重,它可以充当缓冲区,从而提高系统可靠性。它可以自动将消息重新传递到已崩溃的进程,从而防止消息丢失。它避免了发件人需要知道收件人的IP地址和端口号(这在虚拟机经常来去的云部署中特别有用)。它允许将一个消息发送给多个收件人。从逻辑上讲,它使发件人与收件人脱钩(发件人只是发布消息,而不管谁使用消息)。

但是,与RPC的区别在于,消息传递通信通常是单向的:发件人通常不希望收到对其消息的回复。进程可以发送响应,但这通常是在单独的通道上完成的。这种通信模式是异步的:发件人无需等待邮件的发送,而只是发送它,然后就忘记了。

消息代理message broker

过去,消息代理的格局主要由TIBCO,IBM WebSphere和webMethods等公司的商业企业软件主导。最近,诸如RabbitMQ,ActiveMQ,HornetQ,NATS和Apache Kafka之类的开源实现已变得很流行。

详细的传递语义因实现和配置而异,但是通常,消息代理的使用方式如下:一个进程将消息发送到命名队列或主题,并且代理确保将消息传递给这个队列或主题上的一个或多个使用者或订阅者。同一主题上可能有多个生产者和许多消费者。

一个主题提供了一个单向的数据流。然而,消费者自己也可以发布消息到另一个主题上(所以你可以把他们连接在一起),或是由原始消息的发送者使用的应答队列(允许请求/响应数据流,类似于RPC)。

消息代理一般不强制特定的数据模型 ,一条消息就是一串元数据的字节,所以你可以使用任何的编码。如果编码是向前并且向后兼容的,那么你就有了最大的灵活度,以任意的顺序独立的更改发布者和消费者并且部署。

如果消费者向另一个主题发送消息,那么你需要小心保存不知道的字段,防止之前数据库中描述的数据丢失的情况。

分布式参与者框架Distributed actor frameworks

参与者模型actor model是用于在单个进程中进行并发的编程模型。处理逻辑是封装在每个参与者中,而不是直接调用线程处理,每个参与者通常代表一个客户端或实体,它可能具有某种本地状态(不与任何其他参与者共享),并且它通过发送和接收异步消息与其他参与者进行通信。无法保证消息传递:在某些错误情况下,消息将丢失。由于每个参与者每次仅处理一条消息,因此无需担心线程问题,并且每个参与者都可以由框架独立安排。

在分布式参与者框架中,这个编程模型被用来扩展了访问多个节点的应用程序。同一个消息传递机制被使用,无论发送方或接受方是否在同一个节点上。如果他们在不同的节点上,消息会透明地编码成字节序列,通过网络发送,在另一边解码。

在参与者模型中,Location transparncy比在RPC中工作得更好,因为参与者模型已经假定即使在单个进程中,消息也可能丢失。尽管网络上的等待时间可能比同一过程中的等待时间要长,但是使用参与者模型时,本地和远程通信之间本质上的不匹配较少。

分布式参与者框架实质上将消息代理和参与者编程模型集成到单个框架中。但是,如果要对基于actor的应用程序执行滚动升级,则仍然需要担心向前和向后的兼容性,因为消息可能会从运行新版本的节点发送到运行旧版本的节点,反之亦然。

三种流行的分布式actor框架按以下方式处理消息编码:

默认情况下,Akka使用Java的内置序列化,不提供前向或后向兼容性。但是,您可以将其替换为“Protocol Buffer”之类的东西,从而获得滚动升级的功能。默认情况下,Orleans使用自定义数据编码格式,该格式不支持滚动升级部署。要部署应用程序的新版本,您需要设置一个新集群,将流量从旧集群移到新集群,然后关闭旧集群。与Akka一样,可以使用自定义序列化插件。在Erlang OTP中,很难更改记录模式(尽管系统具有为高可用性而设计的许多功能);可以进行滚动升级,但需要仔细计划。实验性的新的maps数据类型(2014年在Erlang R17中引入的类似JSON的结构)可能会在将来使此操作变得更容易。

标签: #hornetq发送机制