从集中式到分布式
首先我们来思考一下,什么是 “分布式系统”?
要了解分布式系统,必须先了解集中式系统。在20世纪60年代,计算机科学家们为了解决 "大规模计算"问题,大型主机(由上万个cpu构成一个超级大的计算机)被研发出来,由于其卓越的性能和良好的稳定性,集中式计算系统成为了那个时代的骄子。
但是随着计算机网络的发展,集中式计算系统越来越不能适应人们的需求。
首先大型主机、大型主机人才的培养成本是非常之高的;且集中式有明显的单点问题,一旦大型机出现问题,那么构建在其上的所有软件系统都将不可用。
基于上面的原因和网络的发展,分布式系统的概念被提出和使用。
分布式计算系统: 由很多通用计算机(廉价),通过IP以太网链接,构成一个庞大的计算集群,然后按需分配资源。这些机器可以分布在不同的机架、不同的机房和不同的城市之中。
分布式系统,必然会面临部分失效的问题,这就需要依靠软件系统来提供容错机制。换句话说,我们需要在不可靠的组件之上构建可靠的系统。
下面我们将针对在构建分布式系统的过程中,可能会出现那些问题来进行探讨。
故障和部分失效
单机系统上的程序要么正常工作,要么彻底出错。
但是在分布式系统中,系统可能会有一部分节点正常工作,而另外一部分节点停止运行。
难点在于部分失效是不确定的。因为你不知道是网络问题,还是另外节点上面的服务已经挂掉。所以分布式系统不好做出对应的响应措施。
我们将这种现象称作 “部分失效”。
不可靠的网络
大多数时候我们讨论的分布式系统都是无共享的分布式系统,即通过网络连接多个节点,而不是一台机器直接访问另一台机器的内存和磁盘,除非通过网络向对方发出请求。
我们通过网络构建了分布式系统,那么在网络传输过程中会出现什么问题了?
1、请求可能已经丢失(比如有人拔掉了网线)。
2、请求正在某个队列中等待,无法马上发送。
3、远程接收节点已经挂掉。
4、远程接收节点完成了请求处理,但回复确在网络中丢失(如网络交换机配置错误)。
网络问题多种多样,多以我们必须学会基于不可靠的组件构建可靠的系统。
在计算机技术中,基于不可靠的组件构建可靠的系统的案例,不胜枚举。
1、纠错码的使用:通过纠错码在各种通信链路上传输数据。
2、TCP协议:在IP之上提供了更加可靠的传输层,保证丢失的数据被重传,消除重复包...
因此,在分布式系统中网络虽然偶尔不可靠,但是我们要尽可能在软件层面消除这种不可靠的因素。
正是因为网络的不可靠,故障检测就显得尤为重要。或者可以说故障检测是分布式系统必须要实现的功能。大多数的分布式系统都是通过超时机制来实现故障检测的。
所谓超时机制,即在等待一段时间之后,如果仍然没有收到回复信息,则认为检测节点出现故障。
超时时间的设置并不是一个不变的常量,而是要根据测量选择合适的超时时间。因为不同环境的响应时间是不相同的。影响网络请求时长的原因多种多样,如当数据包到达目标节点之后,如果CPU所有的核都处于繁忙状态,则该网络请求会被操作系统排队,直到应用程序能够处理。
不可靠的时钟
在程序设计中,有很多功能都需要依赖时钟的。如:
1、某个请求是否超时了?
2、缓存调目多长时间过时?
3、日志文件中错误消息的事件戳。
等等…
好了,首先我们来了解一下时钟。
在现代计算机系统内部至少有两种不同的时钟,一个是墙上时钟,一个是单调时钟。
时钟的分类
墙上时钟: 根据日历返回当前的时间,如Linux的clock_gettime(CLOCK_REALTIME) 和 JAVA的System.currentTimeMillis(),他们都返回的是墙上时钟,表示自1970年1月1号以来的秒数和毫秒数。
墙上时钟可以和NTP(时间服务器)进行同步,强行同步之后会跳回到先前的某个时间点。因此墙上时间不太适合来测量时间间隔。
单调时钟: 顾名思义单调时钟总是向前的(不会出现墙上时钟回调的情况),如Linux的clock_gettime(CLOCK_MONOTONIC) 和 JAVA的System.nonoTime()。返回的时钟即为单调时钟。
单调时钟的绝对值是没有意义的,他可以是电脑启动之后的纳秒数或者其它的东西。所以比较不同节点上的单调时钟值是毫无意义的。
单调时钟适合用来测量时间间隔。
分布式系统中的时钟
时钟同步精准吗?
在分布式系统中,一般通过从时间服务器(NTP)同步时间来保证时间的精度。要知道的是,即使通过NTP服务器来同步时间,也无法做到100%精准。因为NTP同步受限于网络环境的延迟、网络阻塞等客观情况。
分布式系统避免高度依赖时钟。
对于一个常见的功能:跨节点的事件顺序,如果它高度依赖时钟计时,就存在一定的技术风险,例如两个客户端同时写入分布式数据库,谁先到达?
分布式系统和锁
在分布式系统构建过程中会有各种各样的挑战。这里我们来探讨一下在分布式系统中错误使用锁。有可能会出现什么问题?
下面是一个经典的hbase早期的bug。
如下图:其设计目标是确保存储系统的文件一次只能由一个客户端写入。如果多个客户端尝试写入该文件,文件就会被破坏。因此,在访问文件之前客户端需要从锁服务获取访问租约。
客户端1的租约其实已经过期,但是他自认为有效,最终导致客户端2的文件被破坏。
那么要怎么解决上面这种设计的缺陷了。一个行之有效的办法是。每次操作文件的时候除了需要获取访问租约之外,还需要携带令牌(版本号)。如果发现旧的令牌,则拒绝当前请求。
这种机制要求在服务端检查令牌的合法性(而不是客户端)。
当使用zookeeper 作为锁服务的时候,可以使用事务标识zxid或者节点版本cversion来充当令牌。
这个分布式锁的案例是为了说明,一些在集中式环境下面看起来很简单的问题,切换到分布式环境中将会变得非常之复杂。