• 导读
  • 高吞吐、高并发、低延迟和负载均衡(大量用户访问同一个互联网业务)
  • 分布式系统提高承载量的基本手段(分层模型、并发模型)
  • 硬件故障率
  • 资源利用率(集群系统的扩充和缩容)
  • 消息队列服务(两个进程间的通信摒弃TCP和UDP,而使用消息队列机制)


搞懂分布式技术开篇:浅析分布式系统的架构及常用方案

导读

我们常常会听说,某个互联网应用的服务器端系统多么牛逼,比如QQ、微信、淘宝。那么,一个互联网应用的服务器端系统,到底牛逼在什么地方?为什么海量的用户访问,会让一个服务器端系统变得更复杂?本文就是想从最基本的地方开始,探寻服务器端系统技术的基础概念。

高吞吐、高并发、低延迟和负载均衡(大量用户访问同一个互联网业务)

高吞吐(多台服务器实现)

高吞吐,意味着你的系统,可以同时承载大量的用户使用。这里关注的整个系统能同时服务的用户数。这个吞吐量肯定是不可能用单台服务器解决的,因此需要多台服务器协作,才能达到所需要的吞吐量。而在多台服务器的协作中,如何才能有效的利用这些服务器,不致于其中某一部分服务器成为瓶颈,从而影响整个系统的处理能力,这就是一个分布式系统,在架构上需要仔细权衡的问题。

高并发

高并发是高吞吐的一个延伸需求。当我们在承载海量用户的时候,我们当然希望每个服务器都能尽其所能的工作,而不要出现无谓的消耗和等待的情况。然而,软件系统并不是简单的设计,就能对同时处理多个任务,做到“尽量多”的处理。很多时候,我们的程序会因为要选择处理哪个任务,而导致额外的消耗。这也是分布式系统解决的问题

低延迟

低延迟对于人数稀少的服务来说不算什么问题。然而,如果我们需要在大量用户访问的时候,也能很快的返回计算结果,这就要困难的多。因为除了大量用户访问可能造成请求在排队外,还有可能因为排队的长度太长,导致内存耗尽、带宽占满等空间性的问题。如果因为排队失败而采取重试的策略,则整个延迟会变的更高。所以分布式系统会采用很多请求分拣和分发的做法,尽快的让更多的服务器来出来用户的请求。但是,由于一个数量庞大的分布式系统,必然需要把用户的请求经过多次的分发,整个延迟可能会因为这些分发和转交的操作,变得更高,所以分布式系统除了分发请求外,还要尽量想办法减少分发的层次数,以便让请求能尽快的得到处理。

负载均衡(服务器部署的地方不同—通信方式,同时来的数据—负载均衡)

由于互联网业务的用户来自全世界,因此在物理空间上可能来自各种不同延迟的网络和线路,
所以要有效的应对这种用户来源的复杂性,就需要把多个服务器部署在不同的空间来提供服务
同时,我们也需要让同时发生的请求,有效的让多个不同服务器承载。所谓的负载均衡,就是分布式系统与生俱来需要完成的功课。

分布式系统提高承载量的基本手段(分层模型、并发模型)

分层模型(路由、代理)

服务器如何部署多个docker环境 多服务器架构_分布式系统


使用多态服务器来协同完成计算任务,最简单的思路就是,让每个服务器都能完成全部的请求,然后把请求随机的发给任何一个服务器处理。

最早期的互联网应用中,DNS轮询就是这样的做法:当用户输入一个域名试图访问某个网站,这个域名会被解释成多个IP地址中的一个,随后这个网站的访问请求,就被发往对应IP的服务器了,这样多个服务器(多个IP地址)就能一起解决处理大量的用户请求。

(登录信息)
然而,单纯的请求随机转发,并不能解决一切问题。比如我们很多互联网业务,都是需要用户登录的。在登录某一个服务器后,用户会发起多个请求,如果我们把这些请求随机的转发到不同的服务器上,那么用户登录的状态就会丢失,造成一些请求处理失败。简单的依靠一层服务转发是不够的,所以我们会增加一批服务器,这些服务器会根据用户的Cookie,或者用户的登录凭据,来再次转发给后面具体处理业务的服务器。

除了登录的需求外,我们还发现,很多数据是需要数据库来处理的,而我们的这些数据往往都只能集中到一个数据库中,否则在查询的时候就会丢失其他服务器上存放的数据结果。所以往往我们还会把数据库单独出来成为一批专用的服务器。

至此,我们就会发现,一个典型的三层结构出现了:接入、逻辑、存储。然而,这种三层结果,并不就能包医百病。例如,当我们需要让用户在线互动(网游就是典型)
,那么分割在不同逻辑服务器上的在线状态数据,是无法知道对方的,这样我们就需要专门做一个类似互动服务器的专门系统,让用户登录的时候,也同时记录一份数据到它那里,表明某个用户登录在某个服务器上,而所有的互动操作,要先经过这个互动服务器,才能正确的把消息转发到目标用户的服务器上。

并发模型(多线程、异步)

当我们在编写服务器端程序是,我们会明确的知道,大部分的程序,都是会处理同时到达的多个请求的。因此我们不能好像HelloWorld那么简单的,从一个简单的输入计算出输出来。因为我们会同时获得很多个输入,需要返回很多个输出。在这些处理的过程中,往往我们还会碰到需要“等待”或“阻塞”的情况,比如我们的程序要等待数据库处理结果,等待向另外一个进程请求结果等等……如果我们把请求一个挨着一个的处理,那么这些空闲的等待时间将白白浪费,造成用户的响应延时增加,以及整体系统的吞吐量极度下降。

所以在如何同时处理多个请求的问题上,业界有2个典型的方案。一种是多线程,一种是异步。在早期的系统中,多线程或多进程是最常用的技术。这种技术的代码编写起来比较简单,因为每个线程中的代码都肯定是按先后顺序执行的。但是由于同时运行着多个线程,所以你无法保障多个线程之间的代码的先后顺序。这对于需要处理同一个数据的逻辑来说,是一个非常严重的问题,最简单的例子就是显示某个新闻的阅读量。两个++操作同时运行,有可能结果只加了1,而不是2。所以多线程下,我们常常要加很多数据的锁,而这些锁又反过来可能导致线程的死锁。

因此异步回调模型在随后比多线程更加流行,除了多线程的死锁问题外,异步还能解决多线程下,线程反复切换导致不必要的开销的问题:每个线程都需要一个独立的栈空间,在多线程并行运行的时候,这些栈的数据可能需要来回的拷贝,这额外消耗了CPU。同时由于每个线程都需要占用栈空间,所以在大量线程存在的时候,内存的消耗也是巨大的。而异步回调模型则能很好的解决这些问题,不过异步回调更像是“手工版”的并行处理,需要开发者自己去实现如何“并行”的问题。

硬件故障率

除了服务器自己的内存、硬盘等故障,服务器之间的网络线路故障更加常见。而且这种故障还有可能是偶发的,或者是会自动恢复的。面对这种问题,如果只是简单的把“出现故障”的机器剔除出去,那还是不够的。因为网络可能过一会儿就又恢复了,而你的集群可能因为这一下的临时故障,丢失了过半的处理能力。

如何让分布式系统,在各种可能随时出现故障的情况下,尽量的自动维护和维持对外服务,成为了编写程序就要考虑的问题。由于要考虑到这种故障的情况,所以我们在设计架构的时候,
也要有意识的预设一些冗余、自我维护的功能。这些都不是产品上的业务需求
完全就是技术上的功能需求。能否在这方面提出对的需求,然后正确的实现,是服务器端程序员最重要的职责之一。

资源利用率(集群系统的扩充和缩容)

扩容

在对一个集群扩容的时候,我们往往会要停掉整个集群的服务,然后修改各种配置,最后才能重新启动一个加入了新的服务器的集群。 由于在每个服务器的内存里,都可能会有一些用户使用的数据,所以如果冒然在运行的时候,就试图修改集群中提供服务的配置,很可能会造成内存数据的丢失和错误。因此,运行时扩容在对无状态的服务上,是比较容易的,比如增加一些Web服务器。但如果是在有状态的服务上,比如网络游戏,几乎是不可能进行简单的运行时扩容的。

缩容

分布式集群除了扩容,还有缩容的需求。当用户人数下降,服务器硬件资源出现空闲的时候,我们往往需要这些空闲的资源能利用起来,放到另外一些新的服务集群里去。缩容和集群中有故障需要容灾有一定类似之处,区别是缩容的时间点和目标是可预期的。

消息队列服务(两个进程间的通信摒弃TCP和UDP,而使用消息队列机制)

在一个基于分布式的游戏服务器系统中,不同的服务器之间,哪种通信方式是不可行的

两个进程间如果要跨机器通讯,我们几乎都会用TCP/UDP这些协议。但是直接使用网络API去编写跨进程通讯,是一件非常麻烦的事情。除了要编写大量的底层socket代码外,我们还要处理诸如:如何找到要交互数据的进程,如何保障数据包的完整性不至于丢失,如果通讯的对方进程挂掉了,或者进程需要重启应该怎样等等这一系列问题。这些问题包含了容灾扩容、负载均衡等一系列的需求。

为了解决分布式系统进程间通讯的问题,人们总结出了一个有效的模型,就是“消息队列”模型。消息队列模型,就是把进程间的交互,抽象成对一个个消息的处理,而对于这些消息,我们都有一些“队列”,也就是管道,来对消息进行暂存。每个进程都可以访问一个或者多个队列,从里面读取消息(消费)或写入消息(生产)。由于有一个缓存的管道,我们可以放心的对进程状态进行变化。当进程起来的时候,它会自动去消费消息就可以了。而消息本身的路由,也是由存放的队列决定的,这样就把复杂的路由问题,变成了如何管理静态的队列的问题。

一般的消息队列服务,都是提供简单的“投递”和“收取”两个接口,但是消息队列本身的管理方式却比较复杂,一般来说有两种。一部分的消息队列服务,提倡点对点的队列管理方式:每对通信节点之间,都有一个单独的消息队列。这种做法的好处是不同来源的消息,可以互不影响,不会因为某个队列的消息过多,挤占了其他队列的消息缓存空间。而且处理消息的程序也可以自己来定义处理的优先级——先收取、多处理某个队列,而少处理另外一些队列。

但是这种点对点的消息队列,会随着集群的增长而增加大量的队列,这对于内存占用和运维管理都是一个复杂的事情。因此更高级的消息队列服务,开始可以让不同的队列共享内存空间,而消息队列的地址信息、建立和删除,都采用自动化的手段。——这些自动化往往需要依赖上文所述的“目录服务”,来登记队列的ID对应的物理IP和端口等信息。比如很多开发者使用ZooKeeper来充当消息队列服务的中央节点;而类似Jgropus这类软件,则自己维护一个集群状态来存放各节点今昔。

另外一种消息队列,则类似一个公共的邮箱。一个消息队列服务就是一个进程,任何使用者都可以投递或收取这个进程中的消息。这样对于消息队列的使用更简便,运维管理也比较方便。不过这种用法下,任何一个消息从发出到处理,最少进过两次进程间通信,其延迟是相对比较高的。并且由于没有预定的投递、收取约束,所以也比较容易出BUG。

不管使用那种消息队列服务,在一个分布式服务器端系统中,进程间通讯都是必须要解决的问题,所以作为服务器端程序员,在编写分布式系统代码的时候,使用的最多的就是基于消息队列驱动的代码,这也直接导致了EJB3.0把“消息驱动的Bean”加入到规范之中。