1、在上一篇文章中,我们已经初步为大家讲解了HadoopHDFS的整体架构。相信大家都有一定的了解和认识。如果你还没有看过上一篇文章,可以阅读:《干掉几百行的大SQL,我用Hadoop》这篇文章。在本文中,我们来看看如果大量客户端发起高并发(比如每秒上千次)访问NameNode修改元数据,NameNode是如何抵抗的。2.问题的根源我们先来分析NameNode在高并发请求下会遇到什么样的问题。现在大家都知道,NameNode每次请求修改一个元数据(比如申请上传一个文件,需要在内存目录树中添加一个文件),都要写一个editslog,包括两个步骤:写入本地磁盘。通过网络传输到JournalNodes集群。但是如果对Java有一定了解的同学应该知道多线程并发的安全问题吧?NameNode写入editslog的第一个原则:必须保证每条editslog都有一个全局顺序递增的transactionId(简称txid),这样才能识别每条editslog的顺序。那么如果要保证每次editslog的txid都递增,就必须加锁。每个线程修改元数据,当要写入editslog时,必须排队获取锁,然后生成增量txid,代表本次要写入的editslog的序号。好了,那么问题来了,我们来看下图。如果每次都在一个锁定的代码块中生成txid,然后将editslog写入磁盘文件,网络请求写入journalnodes的一个editslog怎么办?不用说,这个绝对搞砸了!NameNode本身使用多个线程来接收来自多个客户端的并发请求。结果,修改了内存中的元数据后,多个线程排队写入editslog!而且你要知道写本地磁盘+网络传输到journalnodes是非常耗时的!两大性能杀手:磁盘写入+网络写入!HDFS的架构如果真这么设计的话,基本上NameNode每秒能承载的并发数是很少的,可能每秒能处理几十个并发请求。3、HDFS的优雅解决方案那么,针对这个问题,HDFS做了很多的优化!首先想一想,既然我们不想让每个线程都写editslog,序列化排队生成txid+写磁盘+写JournalNode,那我们能不能有个内存缓冲区?也就是说,多个线程可以快速获取锁,生成txid,然后快速将editslog写入内存缓冲区。然后快速释放锁,让下一个线程继续获取锁,生成id+writeeditslog到内存缓冲区。然后有一个线程可以将内存中的editslog刷新到磁盘,但是在这个过程中,它仍然允许其他线程将editslog写入内存缓冲区。但是这里还有一个问题。如果有人同时写同一个内存缓冲区,有人同时读和写磁盘,那么也有问题,因为一块共享内存数据不能并发读写!所以HDFS这里采用double-buffer双缓冲机制来处理!将内存缓冲区分为两部分:一部分可以写入另一部分用于读写磁盘和JournalNodes。大家可能会觉得文字描述不够直观,老规矩,先上图,按顺序给大家讲解一下。(1)分段锁机制+内存双缓冲机制首先,每个线程轮流第一次获取锁,产生顺序递增的txid,然后将editslog写入内存双缓冲区域1,然后立即释放第一次上锁。利用这个间隙,后续线程可以立即再次第一次获取锁,然后立即将自己的editslog写入内存缓冲区。写内存这么快,可能只需要几十微秒,然后第一时间就立马释放锁。所以这个并发优化肯定是有效的。你感受到了吗?然后各个线程第二次竞争获取锁。一个线程获取到锁后,我们看看有没有人在写磁盘和网络?如果没有,好吧,那么这个线程是幸运的!直接交换双缓冲区域1和2,然后第二次释放锁。这个过程相当快,不到几微秒就可以判断出内存中的几个条件。好了,至此,内存缓冲区已经交换完毕,后面的线程可以立即快速的一个一个获取锁,然后将editslog写入内存缓冲区的区域2。区域1中的数据被锁定,无法写入。怎么样,是不是又有点多线程并发优化的感觉了?(2)多线程并发吞吐百倍优化接下来,之前的幸运线程读取内存缓冲区1区的数据(此时没有人在写1区,都在写2区),并将edtis日志写入磁盘文件,通过网络写入JournalNodes集群。这个过程很费时间!不过没关系,人家已经优化过了,在写磁盘和网络的过程中不持有锁!所以后面的线程可以快速快速的第一次获取到锁,立即写入内存缓冲区的区域2,然后释放锁。这时候大量线程可以快速写入内存,不会阻塞,不会滞后!这个怎么样?感受并发优化的感觉吗?(3)批量数据刷盘+网络优化那么幸运线程在向磁盘和网络写入数据的过程中,后排大量线程快速获取锁并写入内存缓冲区区域2,释放锁之后,这些线程在第二次获取到锁后会做什么呢?他们会发现有人正在写入磁盘,伙计们!所以它会立即休眠1秒并释放锁。这时候如果有大量线程并发过来,就会快速在这里第二次获取锁,然后发现有人在写磁盘和网络,快速释放锁,休眠。这个过程怎么样,谁也挡不了别人半天!因为锁会很快释放,所以后续线程在第一次获取到锁后仍然可以快速写入内存缓冲区!再次!感受并发优化的感觉吗?而这个时候肯定有很多线程发现之前那个lucky线程的txid好像排在了自己的后面,所以必须把它的editslog从buffer写到磁盘和网络中。这些线程甚至不休眠和等待,它们只是回去做其他事情,它们根本不会卡在这里。在这里感受到并发的优化了吗?然后当幸运线程写完磁盘和网络后,它会唤醒那些之前处于休眠状态的线程。那些线程会在第二次获取到锁后依次排队进入判断,哎!发现没人再写磁盘和网络了!然后会判断后面有没有线程把自己的edtislog写到磁盘和网络上了。如果有,则直接返回。如果没有,那就成为第二个幸运线程,交换两个缓冲区,交换区域1和区域2。然后释放锁,开始将区域2的数据写入磁盘和网络。不过这时候无所谓了。如果后续线程要写editslog,仍然可以在第一次获取锁后立即写内存缓冲区,然后释放锁。等等。4.总结其实这个机制还是比较复杂的,涉及到段锁和内存双缓冲两种机制。NameNode通过这种机制保证了在高并发下多线程修改元数据后写入editslog时,不会说一个线程一次一个线程写磁盘和网络,这样性能太差,并发能力太弱!因此,通过上述一套复杂的机制,我们将尽可能保证一个线程能够将缓冲区中的多个编辑日志批量刷写到磁盘和网络中。在这个漫长的过程中,其他线程可以高并发的快速将editslog写入内存缓冲区,而不会阻塞其他线程写入editslog。因此,依靠上述机制,NameNode处理修改元数据的高并发访问的能力得到了最大化!
