上篇文章讲了数据传输的格式,本文就来说说hdfs中写文件的流程。
hdfs客户端写文件的流程,大体可以分为两个步骤:第一步是创建或打开文件,第二步是进行block的写操作。
block的写操作具体又包括向NN请求添加一个新的block,然后根据NN的返回结果,与对应的DN建立连接,并进行数据的发送。
当一个block写完时,再次向NN请求添加新的block,同样根据返回结果与对应的DN建立连接并发送数据,之后不断重复这个过程,直到文件内容全部写完。
下图以创建文件为例,展开描述详细流程:
- 向NN发送创建文件请求 首先,客户端向NN发送创建文件的请求,在请求中指明文件的位置,文件的权限,文件的block副本数,block大小等。 NN收到请求后,进行有效性检查,以及相应的鉴权动作,然后将操作写入editlog,最后给客户端应答。
- 向NN发送添加block请求 文件创建成功后,对于业务层的代码来说就是直接进行write写数据了,但在客户端的底层实现中,会先向NN发送一个新增block的请求。 NN收到请求后,主要执行一系列的分配算法(涉及就近分配,按存储策略分配,按机架感知策略分配,DN的数据均衡等因素,这里不展开说明),按照block所属文件的副本数,为block分配一组DN,并将结果返回给客户端。
- 向DN建立连接并发送写block请求 客户端从新增block的请求结果中拿到DN节点列表后,向列表的第一个DN建立tcp连接,并发送block写操作请求(OpWriteBlock)。在请求中,包含了block的checksum信息,DN列表,block描述等信息。 DN感知有新的tcp连接建立后,会创建线程(DataXceiver)用于接收连接上的数据。当接收到客户端block写操作请求后,从请求中拿到DN列表,并向列表中的第二个DN建立tcp连接,同时转发block写操作请求(请求中的DN列表剔除本节点),后续的DN接收到新连接后,进行同样的操作,直到DN列表中的最后一个节点。 当DN发现接收到的block写操作请求中DN列表为空时,会标记本节点为最后一个节点。此时,该节点不需要再下下游DN节点转发请求,从而创建用于发送packet数据包请求响应的packet responder线程,然后向上游DN节点回复block写操作请求的ack响应。 上游的DN收到block写操作请求的响应后,继续向该节点的上游DN节点转发请求响应,同样,内部也会创建用于发送packet数据包请求响应的packet responder线程。 这样,DN与DN之间形成一个流水线,后续的packet包数据会在DN之间依次传递。 这里一点需要注意:对于一个block的写操作,DN内部实际上是创建了两个线程,这两个线程属于同一个线程组。线程组中的线程总数是可配置的,当线程数到达配置的上限后,不会再创建新的线程,此时与该节点数据传输的tcp连接是无法成功建立的。
- packet数据的发送 当客户端接收到block写操作请求的应答后,意味着所有的DN均已准备就绪。此后,客户端按packet格式组织数据,并依次发送给DN。 DN接收到一个完整packet后,先转发给下游DN,然后将packet中的数据按chunk大小进行checksum校验,校验无误后将packet中的数据写入本地文件,最后通过内部的队列向packet responder线程发送该packet的ack应答。 packet responder线程从队列中取出packet的应答消息后,阻塞等待下游DN的packet的应答消息,当接收到下游DN的packet应答消息后,才真正向客户端回复packet的应答消息。 由此可见,只要客户端收到了packet的应答消息,就意味着该packet一定是在所有副本节点上都成功接收了,这样可以保证数据的一致性。
- 结束block 当客户端发送的数据达到block的大小时,客户端底层实现会自动结束该block。 具体是向NN请求更新block的信息,NN收到请求后,会检查该block的状态,为该block更新时间戳,最后将操作记录写入editlog,并给客户端应答。 客户端收到应答后,继续向NN发送complete请求,到此,一个block的写流程全部结束。
- 重复步骤2-5写新的block直到文件写完 如果此时,文件内容还没有写完,客户端会继续重复步骤2到5,继续一个新的block写流程,直到文件写完。 注意:这里没有文件的关闭动作,当一个block写完,不再申请新的block,逻辑上就意味着该文件已经完成写流程。
总的流程捋清楚了后,我们来推敲一些细节。
- packet是同步发送还是异步发送?
从上面的流程描述,并不能直观的说明客户端packet发送是采用同步还是异步方式。
也就是说客户端发送完一个packet后,是否必须等接收到该packet的应答包后才发送下一个packet?
答案显然是否定的,因为这样会极大的降低吞吐量,客户端发送完一个packet后,不需要等待DN的应答,就可以继续发送下一个packet。packet发送后,将该packet放到等待应答的队列中,等待DN的应答。
注:每个packet都会有对应的应答包。并且由于每个packet都有各自的包序号,packet的发送也是按序号递增的,因此如果接收到的应答包不是按递增序号来的,客户端通常会报错。
- 客户端侧的packet缓存问题
虽然客户端可以不用等DN的应答就可以继续发送packet,但由于发送的包没有被确认,因此,客户端通常都会在内部将这些packet进行缓存,以便异常时可以告知上层哪些packet被应答了,哪些没有被应答,方便业务进行重试。
那么问题来了,这个缓存大小是多少呢?缓存满了又会这样呢?
这个问题,对于不同的客户端实现有不同的处理。
例如原生java客户端内部,将待发送的packet会放到一个队列中,发送线程从队列中取出packet进行发送,发送成功后将packet放到待确认队列中。另外一个线程接收DN的响应后,从待确认队列中将packet取出并删除。
两个队列的长度累计达到一定数量后,write操作将被阻塞。
而对于go客户端,逻辑上大同小异,以发送的packet写入管道,管道的最大长度为5,即当有5个packet未被DN确认时,成功发送的packet写管道将被阻塞,进而阻塞业务的write接口。
- packet包何时刷到磁盘中
DN接收到完整的packet包后,先转发到下游DN,然后写入本地文件。这个写文件本质上只是写到了文件系统的缓存中,并没有执行sync/flush将数据刷到磁盘上。
那么什么时候,会将数据刷到磁盘中呢?
DN默认的策略是依赖操作系统自身的机制,而不去主动触发调用进行刷盘的动作。这么做的原因是刷盘耗时相对比较长,从而会影响性能,而数据通常都是有3副本的,即便是该节点突然出现了断电,数据在其他节点上还有副本。
因此,综合性能和数据可靠性的考虑,完全依赖操作系统自身的机制还是可以保证数据不会丢失的。
当然,我们可以通过配置项dfs.datanode.synconclose设置为true,这样,当DN接收到block的最后一个packet后,会触发进行一次刷盘的动作。
除此之外,客户端在打开文件时,也可以设置SYNC_BLOCK标识,可以达到同样的效果。
【总结】
本文先讲述了hdfs的写文件流程,以及流程中的一些细节。当然,整个写流程中,可挖掘的细节还有很多,这里不逐一展开说明,后续如有遇到问题,再进行对应的总结说明。
好了,本文就介绍到这里,原创不易,点赞,在看,分享是最好的支持, 谢谢~