分布式系统本质论(3/3)

作者:韩伟 擅长领域: MMORPG后台 、 分布式系统设计、团队管理

小编推荐:作者深入浅出地介绍了分布式系统的产生背景、问题及基本解决方法,是对分布式系统方向一个很好的科普。本文分为 承载量是分布式系统存在的原因、分布式系统提高承载量的基本手段、分布式系统在可管理性上造成的问题、解决分布式系统可管理性的基本手段、分布式系统在开发效率上造成的问题和解决思路等五个部分。

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

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

微服务框架:EJB、WebService

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

tu21

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

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

tu22

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

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

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

tu23

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

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

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

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

tu24

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

tu25

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

tu26

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

云服务模型:IaaS/PaaS/SaaS

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

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

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

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

tu27

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

总结分布式系统问题的解决路径

问题层次 问题内容 解决方案
互联网海量承载问题 提高吞吐量 分层调用
异步并发
降低延迟 缓存
NoSQL
大量服务器管理问题 故障恢复和可扩展性 分布式目录服务
消息队列服务
分布式事务系统
运维便利性 自动部署工具
统一日志系统
开发效率问题 复杂的通信编程 微服务框架
异步编程工具
大量功能模块需要分工 搭建IaaS/PaaS/SaaS云服务

---分布式本质论系列完结

作者介绍:1999年大学实习期加入初创期的网易,成为第30号员工,一干就是8年,期间从程序员开始,历任项目经理、产品总监。2007年创业折腾了4年,做了几个游戏。2011年来腾讯,一直在IEG研发部,现在在公共技术研发中心,担任一些游戏公共技术产品的架构设计和开发。

geren

欢迎关注作者技术公众号和作者交流:handa1740168

Report Story
Tags :

留下你的评论