集中式与分布式
文章目录
[toc]
集中式与分布式
集中式系统架构
集中式是指由一台或多台主计算机组成的中心节点,数据集中存储于这个中心节点中,并且整个系统的所有业务单元都集中部署在这个中心节点上,系统的所有功能均由其集中处理。在集中式系统中,每个终端或客户端机器仅仅负责数据的录入和输出,而数据的存储与控制处理完全交由主机来完成。
集中式系统架构在服务器的部署上有两种方式,一种是单机式,一种是集群式。
单体架构
跟着学习的大家现在都有单机应用开发的经验了,有些同学在寒假作业中采用了MVC(Model, View, Controller)模式开发,这种属于单体应用开发模式,其特点如下:
-
所有的代码统一打包成一个部署文件
-
代码调用的api都是本地库或者是代码中的,基本上不存在远程服务调用
-
当需要发布新版本时,必须停下所有业务进行升级
在业务发展的初期,大家使用的初始架构都是单体应用,应用的服务体量小,也不需要考虑高并发、扩展性的问题,仅仅只需要解决业务需求。但是这种单体应用架构随着公司体量的增大和使用人群的扩展,会出现许多问题:
-
随着业务的增加,代码量也随之增加,代码之间相互调用,对于新人的学习成本增加
-
出现bug时,问题可能出现在代码的每一个角落,排查问题成本增加
-
单体应用的可维护性差,扩展性差,当有新业务接入时,基本上需要修改所有的业务逻辑,牵一发而动全身
单机式系统
一个系统业务量很小的时候,把所有的代码都放在一个项目中(也就是说,这个项目采用的是单体架构),而且这个项目只部署在一台服务器上,整个项目所有的服务都由这台服务器提供,并且整个系统中只配置一台计算机和相应的外部设备,这就是单机系统。
单机系统的缺点是显而易见的:单机的处理能力是有限的。遇到处理上限问题,我们的第一反应是加钱,上最好的。但就算把硬件资源更换成当前情况下的最好的,当你的业务增长到一定程度的时候,单机的硬件资源也无法满足你的业务需求。
Linux内核发送TCP的极限包频 ≈ 100万/秒。假设我们有一个300万在线人数的直播间,每个人每秒发一条信息,高负载下服务端发包频率将跟不上用户群产出消息频率。
所以这种系统只适合对性能,并发等指标要求不高的系统。
集群
加钱的想法是对的,关键在与钱加在哪里。既然一台机子不够,那我们就多加几台机子。
单机处理到达瓶颈的时候,我们把单机复制几份,这样就构成了一个"集群"。集群中每台服务器就叫做这个集群的一个"“节点”,所有节点构成了一个集群。每个节点都提供相同的服务,那么这样系统的处理能力就相当于提升了好几倍。集群系统相当于单机的多实例。
那么新的问题来了,用户的请求究竟由哪个节点来处理呢?最好能够让此时此刻负载较小的节点来处理,这样使得每个节点的压力比较平均。要实现这个功能,就需要在所有节点之前增加一个"调度者"的角色,用户的所有请求都先交给它,然后它根据当前所有节点的负载情况,决定将这个请求交给哪个节点处理。这个"调度者"最后被叫做负载均衡服务器。
集群系统的好处就是系统扩展非常容易。如果随着你们系统业务的发展,当前的系统又支撑不住了,那么给这个集群再增加节点就行了。我们无需改动任何的项目代码,只需要新增服务器部署相同的应用并配置好负载均衡,就可以很好的减轻随着业务增量带来的系统压力,并且可以直接在单机架构上直接进行调整。
但是,当你的业务发展到一定程度的时候,你会发现一个问题——无论怎么增加节点,貌似整个集群性能的提升效果并不明显了。这时候,你就需要使用微服务结构了。
总结
集中式系统架构最大的特点是部署结构简单,由于集中式系统往往基于底层性能卓越的大型主机,因此,无需考虑如何对服务进行多个节点的部署,也就不用考虑负载均衡问题。但缺点也明显,比如集中式架构在设计上是一个单点,且单服务器的造价昂贵,所以系统横向扩展性差。如果发生单点故障(单机不可用即全部不可用)会导致系统停机,且维护时要暂停全部业务,影响严重。
分布式系统架构
为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务,分布式系统应运而生。分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。其目的是利用更多的机器,处理更多的数据。
分布式结构就是将一个完整的系统,按照业务功能,拆分成一个个独立的子系统,在分布式结构中,每个子系统就被称为“服务”。这些子系统能够独立运行在web容器中,它们之间通过RPC方式通信。从进程角度看,两个程序分别运行在两个台主机的进程上,它们相互协作最终完成同一个服务(或者功能),那么理论上这两个程序所组成的系统,也可以称作是"分布式系统"。
假设开发一个在线商城。按照微服务的思想,我们需要按照功能模块拆分成多个独立的服务,如:用户服务、产品服务、订单服务、后台管理服务、数据分析服务等等。这一个个服务都是一个个独立的项目,可以独立运行。如果服务之间有依赖关系,那么通过RPC方式调用。
这样做有很多好处:
- **系统之间的耦合度大大降低:**可以独立开发、独立部署、独立测试,系统与系统之间的边界非常明确,排错也变得相当容易,开发效率大大提升。
- **系统更易于扩展:**我们可以针对性地扩展某些服务。假设这个商城要搞一次大促,下单量可能会大大提升,因此我们可以针对性地提升订单系统、产品系统的节点数量,而对于后台管理系统、数据分析系统而言,节点数量维持原有水平即可。
- **服务的复用性更高:**比如,当我们将用户系统作为单独的服务后,该公司所有的产品都可以使用该系统作为用户系统,无需重复开发。
分布式与集群
咋一看,是不是觉得分布式和集群很相似:分布式使用多个计算机作为节点,集群也是。
分布式和集群都是用来提高系统效率的,只是方式不同。
-
分布式:一个业务拆成多个子业务,部署在不同的服务器上,以缩短单个任务的执行时间来提升效率
-
集群:同一个业务,部署在多个服务器上,通过提高单位时间内执行的任务数来提升效率
例如,如果一个任务由10个子任务组成,每个子任务单独执行需1小时,则在一台服务器上执行改任务需10小时。
采用分布式方案,提供10台服务器,每台服务器只负责处理一个子任务,不考虑子任务间的依赖关系,执行完这个任务只需一个小时。采用集群方案,同样提供10台服务器,每台服务器都能独立处理这个任务。假设有10个任务同时到达,10个服务器将同时工作,10小时后,10个任务同时完成,这样,整体来看,还是1小时内完成一个任务。
集群是个物理形态,分布式是个工作方式。只要是一堆机器,就可以叫集群,他们是不是一起协作着干活,这个谁也不知道;一个程序或系统,只要运行在不同的机器上,就可以叫分布式。集群一般是物理集中、统一管理的,而分布式系统则不强调这一点。所以,集群可能运行着一个或多个分布式系统,也可能根本没有运行分布式系统;分布式系统可能运行在一个集群上,也可能运行在不属于一个集群的多台机器上。这两者是不冲突的。
分布式和集群通常结合起来使用,分布式提供去中心化的能力,可以把系统的不同业务拆分出来,不同的服务器提供不同的业务服务,解决了之前单一入口压力过大问题,但当某个服务器出现问题,此服务器中的业务就失效了,集群提供了高可用性能力,就可以对每个业务构建集群,这样就保证了业务稳定性,集群同时还有很好的扩展性,当某个业务压力过大时,可以对此业务所在集群动态添加服务器,增强此业务的性能。
微服务
微服务架构是采用一组服务的方式来构建一个应用,服务独立部署在不同的服务器或者相同服务器的不同进程中。服务之间使用数据进行通信,比如RPC或者HTTP等。不同的服务之间相互不影响,甚至可以使用不同的编程语言进行开发。
官方给微服务的定义为:
- 一些独立的服务共同组成系统
- 每个服务单独部署,跑在自己的进程中
- 各个服务为独立的业务开发
- 分布式管理
- 强隔离性。
微服务相比分布式服务来说,它的粒度更小,服务之间耦合度更低,由于每个微服务都由独立的小团队负责,因此它敏捷性更高,分布式服务最后都会向微服务架构演化,这是一种趋势, 不过服务微服务化后带来的挑战也是显而易见的,例如服务粒度小,数量大,后期运维将会很难。
一般而可以按两种方式拆分微服务:
-
**按照不同的业务域进行拆分:**通过对业务进行梳理,根据业务的特性把应用拆开,不同的业务模块独立部署。例如订单、营销、风控、积分资源等。形成独立的业务领域微服务集群
-
**按照一个业务功能里的不同模块或者组件进行拆分:**例如把公共组件拆分成独立的原子服务,下沉到底层,形成相对独立的原子服务层
RPC
我们之前讲了将服务拆分,但是通常来讲服务直接一般会有依赖,这代表两个服务之间需要通信以进行数据交换,这个过程称为远程调用。
在分布式计算中,远程过程调用(英语:Remote Procedure Call
,RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互作用编程(无需关注细节)。RPC是一种服务器-客户端(Client/Server
)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用。
RPC是一种进程间通信的模式,程序分布在不同的地址空间里。如果在同一主机里,RPC可以通过不同的虚拟地址空间(即便使用相同的物理地址)进行通讯,而在不同的主机间,则通过不同的物理地址进行交互。许多技术(通常是不兼容)都是基于这种概念而实现的。
简单的来说是一个节点请求另一个节点提供的服务,并且不需要知道底层的网络技术
本地过程调用
RPC就是要像调用本地的函数一样去调远程函数。在研究RPC前,我们先看看本地调用是怎么调的。假设我们要调用函数Multiply来计算lvalue * rvalue的结果:
|
|
那么在第8行时,我们实际上执行了以下操作:
- 将
lvalue
和rvalue
的值压栈 - 进入
Multiply
函数,取出栈中的值10 和 20,将其赋予 l 和 r - 执行第2行代码,计算
l * r
,并将结果存在 y - 将 y 的值压栈,然后从
Multiply
返回 - 第8行,从栈中取出返回值 200 ,并赋值给
l_times_r
以上5步就是执行本地调用的过程。(注:以上步骤只是为了说明原理。事实上编译器经常会做优化,对于参数和返回值少的情况会直接将其存放在寄存器,而不需要压栈弹栈的过程,甚至都不需要调用call,而直接做inline操作。仅就原理来说,这5步是没有问题的。)
远程过程调用带来的新问题
在远程调用时,我们需要执行的函数体是在远程的机器上的,也就是说,Multiply是在另一个进程中执行的。这就带来了几个新问题:
- Call ID映射。我们怎么告诉远程机器我们要调用
Multiply
,而不是Add
或者FooBar
呢?在本地调用中,函数体是直接通过函数指针来指定的,我们调用Multiply
,编译器就自动帮我们调用它相应的函数指针。但是在远程调用中,函数指针是不行的,因为两个进程的地址空间是完全不一样的。所以,在RPC
中,所有的函数都必须有自己的一个ID。这个ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,必须附上这个ID。然后我们还需要在客户端和服务端分别维护一个 (函数 <=>Call ID
) 的对应表。两者的表不一定需要完全相同,但相同的函数对应的Call ID
必须相同。当客户端需要进行远程调用时,它就查一下这个表,找出相应的Call ID
,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。 - 序列化和反序列化。客户端怎么把参数值传给远程的函数呢?在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种语言(比如服务端用
C++
,客户端用Java
或者Python
)。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。 - 网络传输。远程调用往往用在网络上,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把
Call ID
和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。只要能完成这两者的,都可以作为传输层使用。因此,它所使用的协议其实是不限的,能完成传输就行。尽管大部分RPC
框架都使用TCP
协议,但其实UDP
也可以,而gRPC
干脆就用了HTTP2
。Java
的Netty
也属于这层的东西。
有了这三个机制,就能实现RPC了,具体过程如下:
Client端
-
将这个调用映射为
Call ID
。这里假设用最简单的字符串当Call ID
的方法 -
将
Call ID
,lvalue
和rvalue
序列化 -
把上一步中得到的数据包发送给Server端对应服务地址+端口
-
等待服务器返回结果
-
如果服务器调用成功,那么就将结果反序列化,并赋给
l_times_r
Server端
-
在本地维护一个
Call ID
到函数指针的映射call_id_map
-
等待请求
-
得到一个请求后,将其数据包反序列化,获取调用参数
-
根据
Call ID
在call_id_map
中查找,得到相应的函数指针 -
将
value
和rvalue
反序列化后,在本地调用Multiply
函数,得到结果 -
将结果序列化后通过网络返回给Client
所以要实现一个RPC框架,其实只需要按以上流程实现就基本完成了。
RPC
与HTTP
HTTP 和RPC 是两个维度的东西。HTTP 指的是通信协议, 而RPC 则是远程调用。 RPC 的通信可以用HTTP协议,也可以自定义协议,是不做约束的,其从传输层横跨到应用层。
构建于HTTP 之上的远程调用解决方案会有更好的通用性,如WebServices 或REST 架构,使用HTTP + JSON 可以说是一个无脑的标准解决方案。选择构建在 HTTP 之上,有两个最大的优势:
-
HTTP 的语义和可扩展性能很好的满足 RPC 调用需求
-
HTTP 协议几乎被网络上的所有设备所支持,具有很好的协议穿透性
但也存在比较明显的问题:
-
有用信息占比少
-
效率低
-
使用HTTP协议调用远程方法比较复杂,要封装各种参数名和参数值
通用定义的http1.1协议的tcp报文包含太多废信息,一个POST协议的格式大致如下:
|
|
如上图所使用的报文中有效字节数仅仅占约 30%,也就是70%的时间用于传输元数据废编码。当然实际情况下报文内容可能会比这个长,但是报头所占的比例也是非常可观的。而假如我们使用自定义的tcp协议的报文,除去上面无用的字段便可以极大地精简传输内容,这也就是为什么后端进程间通常会采用自定义tcp协议的rpc来进行通信的原因。
公司内部的服务之间调用,一般采用 rpc
的方式
http 的特点是比较通用,像对外的 openAPI
,第三方的接口,一般都是http
的格式的
Protobuf
由于在系统底层,数据的传输形式是简单的字节序列形式传递,即在底层,系统不认识对象,只认识字节序列,而为了达到进程通讯的目的,需要先将数据序列化,而序列化就是将对象转化字节序列的过程。相反地,当字节序列被运到相应的进程的时候,进程为了识别这些数据,就要将其反序列化,即把字节序列转化为对象
Protobuf 是 Protocol Buffers 的简称, 它是Google 开发的一种跨语言、跨平台、可扩展的用于序列化数据协议, Protobuf 可以用于结构化数据序列化,它序列化出来的数据量少,再加上以K-V 的方式来存储数据,非常适用于在网络通讯中的数据载体。它很适合做数据存储或RPC数据交换格式。也是目前最流行的rpc通信协议。
相比于 json 和 XML,ProtoBuf 的优势比较明显。例如 json 虽然表达方便,语法清晰,但是,有一个硬伤就是没有 schema,对于 Client-Server 的应用/服务来说,这就意味着双方需要使用其他方式进行沟通 schema,否则将无法正确的交流;XML 确实提供了强大的 Schema 支持,但是,可能因为年纪更大的缘故,XML 自身的语法啰嗦,更别说定义它的 Schema 了,一句话概括,那就是非常得不现代。
语法
Protobuf协议规定:使用该协议进行数据序列化和反序列化操作时,首先定义传输数据的格式,并命名为以.proto
为扩展名的消息定义文件,下面是一个proto文件示例:
|
|
一般禁止将proto文件分开放。若分开放就必须为其编写Makefile,定义输出路径
message
proto文件就是围绕着消息体进行通信的,与Go语言中的struct类似,不同的协议规则对应不同的描述,我们只介绍"proto3"的规则协议。
消息体内容组成:
|
|
message与message之间可以嵌套定义,相当于go里结构体嵌套匿名结构体
你也可以使用其它message类型作为字段的类型值,如果要使用的类型在其它proto文件中定义,你需要使用import
引入对应的文件
标识号:
在消息的定义中,每个字段等号后面都有唯一的标识号,用于在反序列化过程中识别各个字段的,一旦开始使用就不能改变。标识号可以是乱序的,但是我们规定项目中的标识号一定是从1开始且连续,标识号的范围为[1,2^29 – 1]。
其中[19000-19999]为Protobuf协议预留字段,开发者不建议使用该范围的标识号;一旦使用,在编译时Protoc编译器会报出警告
常见数据类型
常见的数据类型与protoc协议中的数据类型映射见官网
字段修饰
protoc3只有一种字段修饰词repeated
,它表示允许字段重复,对于Go语言来说,它会编译成切片类型。其中类型可以是以下几种类型:
-
数字类型:
double
、float
、int32
、int64
、uint32
、uint64
、sint32
、sint64
-
存储固定大小的数字类型:
fixed32
、fixed64
、sfixed32
、sfixed64
-
布尔类型:
bool
-
字符串:
string
-
字节数组:
bytes
-
消息类型:
message
-
枚举类型:
enum
-
oneof
枚举类型
proto协议支持使用枚举类型,和正常的编程语言一样,枚举类型可以使用enum关键字定义在.proto文件中:
|
|
Oneof
类型
如果你有一组字段,同时最多允许这一组中的一个字段出现,就可以使用Oneof
定义这一组字段,这有像C语言的Union。
|
|
map类型
map类型需要设置键和值的类型,格式是如下:
|
|
gRPC
ProtoBuf 除了经常被用于数据保存交换之外,还被用于定义 gRPC 服务,gRPC 也是 Google 公开的高性能 RPC 调用框架,号称高效,支持广。
安装grpc和protobuf
用go mod一键sync一下两个库
|
|
protocol buffer编译器
这个编译器可以单独下载,但我们也可以使用Goland里面的protocol buffer
的编译器插件
|
|
安装protoc
到protobuf release,选择适合自己操作系统的压缩包文件
将解压后得到的protoc
二进制文件移动到$GOPATH/bin里
不会有人不知道$GOPATH/bin,也没有将这个文件夹加到path环境变量里吧
go的protoc编辑器插件
具体可以看其他网上教程
|
|
简单的demo
我们把讲protobuf语法时的示例文件作为例子,将其改名为login.proto
并执行以下命令:
|
|
这两条命令会生成两个文件login.pb.go
和login_grpc.pb.go
(放在项目的proto文件夹里),服务端和客户端都需要这两个文件。
如果已经在服务端部署好了某项服务,在远程或者另外一台服务器上需要调用这个服务器上部署的这个微服务,则需要:首先,将服务端部署好的最新自动生成的那几个文件拉到现在这个服务器,这样服务端和客户端都持有同一份‘’服务协议‘’,根据这个协议就可以使用远程的微服务了
值得一提的是,protoc支持很多种语言代码生成,也就是说,客户端和服务端可以是不同语言开发的。服务端go语言开发,但是可以通过protoc生成对应java、c++等代码,这样客户端是哪种语言写的只需要拉取相应的protoc代码生成的文件,如果没有,可以拉取proto语法源码,自己本地生成自己需要的代码
login.pb.go
放置了将login.proto
里的结构翻译成go语言结构体和相关东西。
|
|
login_grpc.pb.go
放置了gRPC框架封装好的逻辑
|
|
接下来使用下面的server代码起一个rpc服务端:
|
|
接下来使用下面的client代码起一个rpc客户端:
|
|
protobuf 3中还有一种数据类型——steam(流),其用于传输流式数据,感兴趣可以参考这篇文章(这篇文章是我随便找的用于简要了解)
Begonia
可以看到,上面讲到的rpc通信协议——Protobuf和rpc框架——gRPC都避免不了编写proto文件+代码生成的操作,而且一旦更改rpc接口,客户端和服务端都要更新相应的文件。
为了更加方便、快速的起rpc服务,红岩巨佬仓仓子开发了一个轻量级、API友好的RPC框架Begonia,详细文档可见Begonia README。
依旧以上面讲的login为例子,如果采用begonia,你只需要以下步骤(前提是你已经安装了Begonia):
- 启动服务中心
|
|
- 编写service代码
|
|
- 编写client代码
|
|
Reference(partial)
[什么是分布式系统,如何学习分布式系统](https://www.cnblogs.com/xybaby/p/7787034.html)
文章作者 cold-bin
上次更新 2022-07-07