浅析海量用户的分布式系统设计(2)

接上篇《浅析海量用户的分布式系统设计(1)

解决分布式系统可管理性的基本手段

1.目录服务(ZooKeeper)

分布式系统是一个由很多进程组成的整体,这个整体中每个成员部分,都会具备一些状态,比如自己的负责模块,自己的负载情况,对某些数据的掌握等等。而这些和其他进程相关的数据,在故障恢复、扩容缩容的时候变得非常重要。

简单的分布式系统,可以通过静态的配置文件,来记录这些数据:进程之间的连接对应关系,他们的IP地址和端口,等等。然而一个自动化程度高的分布式系统,必然要求这些状态数据都是动态保存的。这样才能让程序自己去做容灾和负载均衡的工作。

一些程序员会专门自己编写一个DIR服务(目录服务),来记录集群中进程的运行状态。集群中进程会和这个DIR服务产生自动关联,这样在容灾、扩容、负载均衡的时候,就可以自动根据这些DIR服务里的数据,来调整请求的发送目地,从而达到绕开故障机器、或连接到新的服务器的操作。

然而,如果我们只是用一个进程来充当这个工作。那么这个进程就成为了这个集群的“单点”——意思就是,如果这个进程故障了,那么整个集群可能都无法运行的。所以存放集群状态的目录服务,也需要是分布式的。幸好我们有ZooKeeper这个优秀的开源软件,它正是一个分布式的目录服务区。

ZooKeeper可以简单启动奇数个进程,来形成一个小的目录服务集群。这个集群会提供给所有其他进程,进行读写其巨大的“配置树”的能力。这些数据不仅仅会存放在一个ZooKeeper进程中,而是会根据一套非常安全的算法,让多个进程来承载。这让ZooKeeper成为一个优秀的分布式数据保存系统。

由于ZooKeeper的数据存储结构,是一个类似文件目录的树状系统,所以我们常常会利用它的功能,把每个进程都绑定到其中一个“分枝”上,然后通过检查这些“分支”,来进行服务器请求的转发,就能简单的解决请求路由(由谁去做)的问题。另外还可以在这些“分支”上标记进程的负载的状态,这样负载均衡也很容易做了。

目录服务是分布式系统中最关键的组件之一。而ZooKeeper是一个很好的开源软件,正好是用来完成这个任务。

2.[消息队列服务[(https://www.qcloud.com/product/cmq?fromSource=gwzcw.59502.59502.59502)(ActiveMQ、ZeroMQ、Jgroups)

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

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

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

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

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

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

3.事务系统

在分布式的系统中,事务是最难解决的技术问题之一。由于一个处理可能分布在不同的处理进程上,任何一个进程都可能出现故障,而这个故障问题则需要导致一次回滚。这种回滚大部分又涉及多个其他的进程。这是一个扩散性的多进程通讯问题。要在分布式系统上解决事务问题,必须具备两个核心工具:一个是稳定的状态存储系统;另外一个是方便可靠的广播系统。

事务中任何一步的状态,都必须在整个集群中可见,并且还要有容灾的能力。这个需求,一般还是由集群的“目录服务”来承担。如果我们的目录服务足够健壮,那么我们可以把每步事务的处理状态,都同步写到目录服务上去。ZooKeeper再次在这个地方能发挥重要的作用。

如果事务发生了中断,需要回滚,那么这个过程会涉及到多个已经执行过的步骤。也许这个回滚只需要在入口处回滚即可(加入那里有保存回滚所需的数据),也可能需要在各个处理节点上回滚。如果是后者,那么就需要集群中出现异常的节点,向其他所有相关的节点广播一个“回滚!事务ID是XXXX”这样的消息。这个广播的底层一般会由消息队列服务来承载,而类似Jgroups这样的软件,直接提供了广播服务。

虽然现在我们在讨论事务系统,但实际上分布式系统经常所需的“分布式锁”功能,也是这个系统可以同时完成的。所谓的“分布式锁”,也就是一种能让各个节点先检查后执行的限制条件。如果我们有高效而单子操作的目录服务,那么这个锁状态实际上就是一种“单步事务”的状态记录,而回滚操作则默认是“暂停操作,稍后再试”。这种“锁”的方式,比事务的处理更简单,因此可靠性更高,所以现在越来越多的开发人员,愿意使用这种“锁”服务,而不是去实现一个“事务系统”。

4.自动部署工具(Docker

由于分布式系统最大的需求,是在运行时(有可能需要中断服务)来进行服务容量的变更:扩容或者缩容。而在分布式系统中某些节点故障的时候,也需要新的节点来恢复工作。这些如果还是像老式的服务器管理方式,通过填表、申报、进机房、装服务器、部署软件……这一套做法,那效率肯定是不行。

在分布式系统的环境下,我们一般都是采用“池”的方式来管理服务。我们预先会申请一批机器,然后在某些机器上运行服务软件,另外一些则作为备份。显然我们这一批服务器不可能只为某一个业务服务,而是会提供多个不同的业务承载。那些备份的服务器,则会成为多个业务的通用备份“池”。随着业务需求的变化,一些服务器可能“退出”A服务而“加入”B服务。

这种频繁的服务变化,依赖高度自动的软件部署工具。我们的运维人员,应该掌握这开发人员提供的部署工具,而不是厚厚的手册,来进行这类运维操作。一些比较有经验的开发团队,会统一所有的业务底层框架,以期大部分的部署、配置工具,都能用一套通用的系统来进行管理。而开源界,也有类似的尝试,最广为人知的莫过于RPM安装包格式,然而RPM的打包方式还是太复杂,不太符合服务器端程序的部署需求。所以后来又出现了Chef为代表的,可编程的通用部署系统。

在虚拟机技术出现之后,PaaS平台为自动部署提供了强大的支持:如果我们是按某个PaaS平台的规范来编写的应用,可以完全把程序丢给平台去部署,其承载量计算、部署规划,都自动完成了。这方面的佼佼者是Google的AppEngine:我们可以直接用Eclipse开发一个本地的Web应用,然后上传到AppEngine里面,所有的部署就完成了!AppEngine会自动的根据对这个Web应用的访问量,来进行扩容、缩容、故障恢复。

然而,真正有革命性的工具,是Docker的出现。虽然虚拟机、沙箱技术早就不是什么新技术,但是真正使用这些技术来作为部署工具的时间却不长。Linux高效的轻量级容器技术,提供了部署上巨大的便利性——我们可以在各种库、各种协作软件的环境下打包我们的应用程序,然后随意的部署在任何一个Linux系统上。

为了管理大量的分布式服务器端进程,我们确实需要花很多功夫,其优化其部署管理的工作。统一服务器端进程的运行规范,是实现自动化部署管理的基本条件。我们可以根据“操作系统”作为规范,采用Docker技术;也可以根据“Web应用”作为规范,采用某些PaaS平台技术;或者自己定义一些更具体的规范,自己开发完整的分布式计算平台。

5.日志服务(log4j)

服务器端的日志,一直是一个既重要又容易被忽视的问题。很多团队在刚开始的时候,仅仅把日志视为开发调试、排除BUG的辅助工具。但是很快会发现,在服务运营起来之后,日志几乎是服务器端系统,在运行时可以用来了解程序情况的唯一有效手段。

尽管我们有各种profile工具,但是这些工具大部分都不适合在正式运营的服务上开启,因为会严重降低其运行性能。所以我们更多的时候需要根据日志来分析。尽管日志从本质上,就是一行行的文本信息,但是由于其具有很大的灵活性,所以会很受开发和运维人员的重视。

日志本身从概念上,是一个很模糊的东西。你可以随便打开一个文件,然后写入一些信息。但是现代的服务器系统,一般都会对日志做一些标准化的需求规范:日志必须是一行一行的,这样比较方便日后的统计分析;每行日志文本,都应该有一些统一的头部,比如日期时间就是基本的需求;日志的输出应该是分等级的,比如fatal/error/warning/info/debug/trace等等,程序可以在运行时调整输出的等级,以便可以节省日志打印的消耗;日志的头部一般还需要一些类似用户ID或者IP地址之类的头信息,用于快速查找定位过滤某一批日志记录,或者有一些其他的用于过滤缩小日志查看范围的字段,这叫做染色功能;日志文件还需要有“回滚”功能,也就是保持固定大小的多个文件,避免长期运行后,把硬盘写满。

由于有上述的各种需求,所以开源界提供了很多游戏的日志组件库,比如大名鼎鼎的log4j,以及成员众多的log4X家族库,这些都是应用广泛而饱受好评的工具。

不过对比日志的打印功能,日志的搜集和统计功能却往往比较容易被忽视。作为分布式系统的程序员,肯定是希望能从一个集中节点,能搜集统计到整个集群日志情况。而有一些日志的统计结果,甚至希望能在很短时间内反复获取,用来监控整个集群的健康情况。要做到这一点,就必须有一个分布式的文件系统,用来存放源源不断到达的日志(这些日志往往通过UDP协议发送过来)。而在这个文件系统上,则需要有一个类似Map Reduce架构的统计系统,这样才能对海量的日志信息,进行快速的统计以及报警。有一些开发者会直接使用Hadoop系统,有一些则用Kafka来作为日志存储系统,上面再搭建自己的统计程序。

日志服务是分布式运维的仪表盘、潜望镜。如果没有一个可靠的日志服务,整个系统的运行状况可能会是失控的。所以无论你的分布式系统节点是多还是少,必须花费重要的精力和专门的开发时间,去建立一个对日志进行自动化统计分析的系统。

分布式系统在开发效率上造成的问题和解决思路

根据上文所述,分布式系统在业务需求的功能以为,还需要增加额外很多非功能的需求。这些非功能需求,往往都是为了一个多进程系统能稳定可靠运行而去设计和实现的。这些“额外”的工作,一般都会让你的代码更加复杂,如果没有很好的工具,就会让你的开发效率严重下降。

1.微服务框架:EJB、WebService

当我们在讨论服务器端软件分布的时候,服务进程之间的通信就难免了。然而服务进程间的通讯,并不是简单的收发消息就能完成的。这里还涉及了消息的路由、编码解码、服务状态的读写等等。如果整个流程都由自己开发,那就太累人了。

所以业界很早就推出了各种分布式的服务器端开发框架,最著名的就是“EJB”——企业JavaBean。但凡冠以“企业”的技术,往往都是分布式下所需的部分,而EJB这种技术,也是一种分布式对象调用的技术。我们如果需要让多个进程合作完成任务,则需要把任务分解到多个“类”上,然后这些“类”的对象就会在各个进程容器中存活,从而协作提供服务。这个过程很“面向对象”。每个对象都是一个“微服务”,可以提供某些分布式的功能。

而另外一些系统,则走向学习互联网的基本模型:HTTP。所以就有了各种的WebService框架,从开源的到商业软件,都有各自的WebService实现。这种模型,把复杂的路由、编解码等操作,简化成常见的一次HTTP操作,是一种非常有效的抽象。开发人员开发和部署多个WebService到Web服务器上,就完成了分布式系统的搭建。

不管我们是学习EJB还是WebService,实际上我们都需要简化分布式调用的复杂程度。而分布式调用的复杂之处,就是因为需要把容灾、扩容、负载均衡等功能,融合到跨进程调用里。所以使用一套通用的代码,来为所有的跨进程通讯(调用),统一的实现容灾、扩容、负载均衡、过载保护、状态缓存命中等等非功能性需求,能大大简化整个分布式系统的复杂性。

一般我们的微服务框架,都会在路由阶段,对整个集群所有节点的状态进行观察,如哪些地址上运行了哪些服务的进程,这些服务进程的负载状况如何,是否可用,然后对于有状态的服务,还会使用类似一致性哈希的算法,去尽量试图提高缓存的命中率。当集群中的节点状态发生变化的时候,微服务框架下的所有节点,都能尽快的获得这个变化的情况,从新根据当前状态,重新规划以后的服务路由方向,从而实现自动化的路由选择,避开那些负载过高或者失效的节点。

有一些微服务框架,还提供了类似IDL转换成“骨架”、“桩”代码的工具,这样在编写远程调用程序的时候,完全无需编写那些复杂的网络相关的代码,所有的传输层、编码层代码都自动的编写好了。这方面EJB、Facebook的Thrift,Google gRPC都具备这种能力。在具备代码生成能力的框架下,我们编写一个分布式下可用的功能模块(可能是一个函数或者是一个类),就好像编写一个本地的函数那样简单。这绝对是分布式系统下非常重要的效率提升。

2.异步编程工具:协程、Futrue、Lamda

在分布式系统中编程,你不可避免的会碰到大量的“回调”型API。因为分布式系统涉及非常多的网络通信。任何一个业务命令,都可能被分解到多个进程,通过多次网络通信来组合完成。由于异步非阻塞的编程模型大行其道,所以我们的代码也往往动不动就要碰到“回调函数”。然而,回调这种异步编程模型,是一种非常不利于代码阅读的编程方法。因为你无法从头到尾的阅读代码,去了解一个业务任务,是怎样被逐步的完成的。属于一个业务任务的代码,由于多次的非阻塞回调,从而被分割成很多个回调函数,在代码的各处被串接起来。

更有甚者,我们有时候会选择使用“观察者模式”,我们会在一个地方注册大量的“事件-响应函数”,然后在所有需要回调的地方,都发出一个事件。——这样的代码,比单纯的注册回调函数更难理解。因为事件对应的响应函数,通常在发出事件处是无法找到的。这些函数永远都会放在另外的一些文件里,而且有时候这些函数还会在运行时改变。而事件名字本身,也往往是匪夷所思难以理解的,因为当你的程序需要成千上百的事件的时候,起一个容易理解名符其实的名字,几乎是不可能的。

为了解决回调函数这种对于代码可读性的破坏作用,人们发明了很多不同的改进方法。其中最著名的是“协程”。我们以前常常习惯于用多线程来解决问题,所以非常熟悉以同步的方式去写代码。协程正是延续了我们的这一习惯,但不同于多线程的是,协程并不会“同时”运行,它只是在需要阻塞的地方,用Yield()切换出去执行其他协程,然后当阻塞结束后,用Resume()回到刚刚切换的位置继续往下执行。这相当于我们可以把回调函数的内容,接到Yield()调用的后面。这种编写代码的方法,非常类似于同步的写法,让代码变得非常易读。但是唯一的缺点是,Resume()的代码还是需要在所谓“主线程”中运行。用户必须自己从阻塞恢复的时候,去调用Resume()。协程另外一个缺点,是需要做栈保存,在切换到其他协程之后,栈上的临时变量,也都需要额外占用空间,这限制了协程代码的写法,让开发者不能用太大的临时变量。

而另外一种改善回调函数的写法,往往叫做Future/Promise模型。这种写法的基本思路,就是“一次性把所有回调写到一起”。这是一个非常实用的编程模型,它没有让你去彻底干掉回调,而是让你可以把回调从分散各处,集中到一个地方。在同一段代码中,你可以清晰的看到各个异步的步骤是如何串接、或者并行执行的。

最后说一下lamda模型,这种写法流行于js语言的广泛应用。由于在其他语言中,定一个回调函数是非常费事的:Java语言要设计一个接口然后做一个实现,简直是五星级的费事程度;C/C++支持函数指针,算是比较简单,但是也很容易导致代码看不懂;脚本语言相对好一些,也要定义个函数。而直接在调用回调的地方,写回调函数的内容,是最方便开发,也比较利于阅读的。更重要的,lamda一般意味着闭包,也就是说,这种回调函数的调用栈,是被分别保存的,很多需要在异步操作中,需要建立一个类似“会话池”的状态保存变量,在这里都是不需要的,而是可以自然生效的。这一点和协程有异曲同工之妙。

不管使用哪一种异步编程方式,其编码的复杂度,都是一定比同步调用的代码高的。所以我们在编写分布式服务器代码的时候,一定要仔细规划代码结构,避免出现随意添加功能代码,导致代码的可读性被破坏的情况。不可读的代码,就是不可维护的代码,而大量异步回调的服务器端代码,是更容易出现这种情况的。

云服务模型:IaaS/PaaS/SaaS

在复杂的分布式系统开发和使用过程中,如何对大量服务器和进程的运维,一直是一个贯穿其中的问题。不管是使用微服务框架、还是统一的部署工具、日志监控服务,都是因为大量的服务器,要集中的管理,是非常不容易的。这里背后的原因,主要是大量的硬件和网络,把逻辑上的计算能力,切割成很多小块。

随着计算机运算能力的提升,出现的虚拟化技术,却能把被分割的计算单元,更智能的统一起来。其中最常见的就是IaaS技术:当我们可以用一个服务器硬件,运行多个虚拟的服务器操作系统的时候,我们需要维护的硬件数量就会成倍的下降。

而PaaS技术的流行,让我们可以为某一种特定的编程模型,统一的进行系统运行环境的部署维护。而不需要再一台台服务器的去装操作系统、配置运行容器、上传运行代码和数据。在没有统一的PaaS之前,安装大量的MySQL数据库,曾经是消耗大量时间和精力的工作。

当我们的业务模型,成熟到可以抽象为一些固定的软件时,我们的分布式系统就会变得更加易用。我们的计算能力不再是代码和库,而是一个个通过网络提供服务的云——SaaS,这样使用者根本来维护、部署的工作都不需要,只要申请一个接口,填上预期的容量额度,就能直接使用了。这不仅节省了大量开发对应功能的事件,还等于把大量的运维工作,都交出去给SaaS的维护者——而他们做这样的维护会更加专业。

在运维模型的进化上,从IaaS到PaaS到SaaS,其应用范围也许是越来越窄,但使用的便利性却成倍的提高。这也证明了,软件劳动的工作,也是可以通过分工,向更专业化、更细分的方向去提高效率。

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏JAVA高级架构

9种高性能可用高并发的技术架构

1、分层   分层是企业应用系统中最常见的一种架构模式,将系统在横向维度上切分成几个部分,每个部分负责一部分相对简单并比较单一的职责,然后通过上层对下层的依赖和...

35611
来自专栏EAWorld

微服务编排之道

目录: 一、微服务需要编排吗? 二、微服务编排的流程 三、微服务编排的一致性 四、微服务编排的监控工具支撑 一、微服务需要编排吗? 微服务是一种新的软件架构风格...

4886
来自专栏平凡文摘

高可用高并发的 9 种技术架构!

1695
来自专栏CSDN技术头条

Autodesk基于Mesos的通用事件系统架构

【编者按】本文由Autodesk Cloud软件架构师Olivier Paugam撰写,解释了如何集合Mesos、Kafka、RabbitMQ、Akka、Spl...

1915

32位 or 64位:Apache CloudStack系统VM架构选择

最近我和一些朋友讨论了一个关于CloudStack的问题:为什么现在CloudStack 4.3同时提供32位或64位系统虚拟机的选项。我提出了一个观点,并将其...

1996
来自专栏IT技术精选文摘

如何实现系统的可扩展性和高可用性

概述 可扩展性,高可用性和性能 可扩展性,高可用性,性能和关键任务这些术语对不同组织或组织内的不同部门来说意味着不同的事情。它们经常被互换,造成混乱,导致管理...

31610
来自专栏Java编程技术

利用MongoDB 分片集群(Sharded Cluster)实现高并发大数据处理

考虑这样一个场景,有个数据量有10多亿数据的设备库,里面存放了注册的设备的信息,并且设备数据还可能会递增,然后业务集群需要对指定条件的设备群发信息,那么如何才能...

1002
来自专栏张善友的专栏

从APM角度上看:NoSQL和关系数据库并无不同

Michael Kopp拥有十年以上C++、Java/JEE的架构及开发经验,现Compuware技术策略师,专攻大规模产品部署的架构和性能。 以下为译文: 传...

1908
来自专栏腾讯移动品质中心TMQ的专栏

后台性能测试不可不知的二三事

某月黑风高之夜,某打车平台上线了一大波(G+)优惠活动,众人纷纷下单。于是乎,该打车平台使用的智能提示服务扛不住直接趴窝了(如下图)。事后,负责智能提示服务开发...

2727
来自专栏企鹅号快讯

9种高性能可用高并发的技术架构

1、分层 分层是企业应用系统中最常见的一种架构模式,将系统在横向维度上切分成几个部分,每个部分负责一部分相对简单并比较单一的职责,然后通过上层对下层的依赖和调度...

2208

扫码关注云+社区