1 . 创建流指向指定路径下文件
FSDataOutputStream fsDataOutputStream = fileSystem.create(new Path("/user.txt"));
最后调用到DistributeFileSystem实现类内的create方法
创建一个DFSOutputStream,进行初始化
final DFSOutputStream dfsos = dfs.create(getPathName(p), permission
(1)创建流系列操作
final DFSOutputStream result = DFSOutputStream.newStreamForCreate(this,
1)向目录树添加InodeFile,记录元数据日志并添加契约【需要跟namenode服务端进行交互(NamenodeRPCServer)
【此处是重试的代码结构【while (shouldRetry){ 】,因为这一段代码涉及到网络相关的RPC调用,所以多执行几次确保其不会因为突发网络问题而失败
stat = dfsClient.namenode.create(src, masked, dfsClient.clientName,
检查namenode的启动状态
checkNNStartup();
创建文件的核心代码
status = namesystem.startFile(src, perm, clientName, clientMachine, -> startFileInt
等待加载元数据
waitForLoadingFSImage();
重要
toRemoveBlocks = startFileInternal(
1( 向文件目录树内添加INodeFile节点【dir就是FSDirectory
iip = dir.addFile(parent.getKey(), parent.getValue(), permissions,
创建一个INodeFile节点
INodeFile newNode = newINodeFile(allocateNewInodeId(), permissions, modTime,
添加一个文件
newiip = addINode(existing, newNode);
后续即查询到父目录,添加节点,如果多级目录就继续添加等等步骤
2( 添加契约(lease翻译为契约)
leaseManager.addLease(newNode.getFileUnderConstructionFeature()
.getClientName(), src);
此处涉及添加契约以及续约,没有契约则添加契约,存在契约则修改心跳时间并保存而达成续约
2)初始化DataStreamer,是写数据流程的重要对象
final DFSOutputStream out = new DFSOutputStream(dfsClient, src, stat,
flag, progress, checksum, favoredNodes);
计算packet大小
computePacketChunkSize(dfsClient.getConf().writePacketSize, bytesPerChecksum);
创建DataStreamer
streamer = new DataStreamer(stat, null);
3)启动DataStreamer【run方法执行
out.start();
刚刚启动的DataStreamer线程没有数据,所以进入下面的while
(此时dataQueue.size() == 0)
while ((!streamerClosed && !hasError && dfsClient.clientRunning
&& dataQueue.size() == 0 &&
(stage != BlockConstructionStage.DATA_STREAMING ||
stage == BlockConstructionStage.DATA_STREAMING &&
now - lastPacket < dfsClient.getConf().socketTimeout/2)) || doSleep )
dataqueue没有数据,就会阻塞在此处等待向队列添加数据
dataQueue.wait(timeout);
(2)开启续约
beginFileLease(result.getFileId(), result); -> put
创建了一个后台线程(客户端针对每一个文件都会创建一个后台线程)
daemon = new Daemon(new Runnable() {
进行契约的续约
LeaseRenewer.this.run(id);
间隔一定周期(一秒)进行一次检查如果当前时间减去上次续约的时间大于30秒,即有30秒未进行续约了,即执行续约操作
renew();
1( 获取namenode代理(跳转查看)
namenode.renewLease(clientName);
调用leaseManager里的续约方法【后续删除旧契约,保存添加新契约并修改最后一次契约更新的时间值
leaseManager.renewLease(holder);
执行到此处,我们猜想一下可能存在的契约扫描机制,LeaseManager内肯定还有一个线程在周期性的检查契约的情况
于是查看LeaseManager的run
run内的检查契约代码
needSync = checkLeases();
从存储契约的数据结构里拿出第一个(注:这个数据结构内重写了compareable方法,会将契约从老到新排序,第一个就是最老的)
leaseToCheck = sortedLeases.first();
对这个契约进行判断,如果未超时,则后续的系列契约均不可能超时,直接return
if (!leaseToCheck.expiredHardLimit()) {
break;
}
最老契约过期,则移除该契约
removeLease(leaseToCheck, p);
2( 修改上一次的续约时间
updateLastLeaseRenewal();
2 . 使用流向指定文件写入
fsDataOutputStream.write("fsfsfsfsfs".getBytes());
fsDataOutputStream对象其实是实现类HdfsDataOutputStream对象,该实现类没有重写write方法,故查看FSDataOutputStream父类内对write方法的实现
out.write(b);
【
对于out变量,经过对create方法的研究,我们发现out是被封装进入HdfsDataOutputStream里的DFSOutputStream对象,于是进入该对象进行查看
结果DFSOutputStream里面又没有write方法,于是进入其父类FSOutputSummer类内,找到对应的write方法
】
方法内的写文件操作
flushBuffer();
核心代码(此处是一个chunk一个chunk的去写的) 注:HDFS文件 -》 Dlock文件块128MB -》 packet64K == 127chunk -》 chunk 512 + chucksum 4 = 516
writeChecksumChunks(buf, 0, lenToFlush);
1 . 计算出chunk校验和(即上文4kb大小的chucksum)
sum.calculateChunkedSums(b, off, len, checksum, 0);
2 . 按照chunk的大小遍历数据
for (int i = 0; i < len; i += sum.getBytesPerChecksum()) {
3 . 逐个chunk的写数据(此方法重写于DFSOutputStream子类)
writeChunk(b, off + i, chunkLen, checksum, ckOffset, getChecksumSize() );
写chunk【DFSOutputStream类内
writeChunkImpl(b, offset, len, checksum, ckoff, cklen);
创建packet
currentPacket = createPacket(packetSize, chunksPerPacket,
bytesCurBlock, currentSeqno++, false);
向packet里写 chunk校验和 4 byte
currentPacket.writeChecksum(checksum, ckoff, cklen);
向packet里写 一个chunk 512 byte
currentPacket.writeData(b, offset, len);
累计的chunk数量 -> packet 写满127个chunk就是一个 packet
currentPacket.incNumChunks();
Block -> packet Block —> 128M 就写满了一个文件块
bytesCurBlock += len;
两个条件
1) 写满了一个packet(127 chunk)
2) 写满了一个文件块Block(128M) (2048 packet)
if (currentPacket.getNumChunks() == currentPacket.getMaxChunks() ||
bytesCurBlock == blockSize) {
写满一个packet,将packet放入dataqueue队列
waitAndQueueCurrentPacket();
如果队列满了,就等待
dataQueue.wait();
把当前packet写入队列dataqueue
queueCurrentPacket();
向dataQueue内添加一个packet
dataQueue.addLast(currentPacket);
在上文我们创建流的过程中启动了一个DataStreamer线程,DataStreamer监视dataQueue,在没有数据写入 的情况下一直wait,所以此处向DataQueue添加过packet后,就通过notifyAll唤醒了正在等待的线程。下一步要继续去查看DataStreamer类的run方法
dataQueue.notifyAll();
DataStreamer的run内:
从队伍里取出packet
one = createHeartbeatPacket();
1) 建立数据管道
/**
* nextBlockOutputStream完成了两个事
* 1 向namenode申请block
* 2 建立数据管道
* 3 管道的容错
*/
// 进入nextBlockOutputStream()
setPipeline(nextBlockOutputStream());
(1 存储有问题的主机的集合
DatanodeInfo[] excluded =
excludedNodes.getAllPresent(excludedNodes.asMap().keySet())
.keySet()
.toArray(new DatanodeInfo[0]);
(2 向namenode申请block
/**
* 服务端此处的操作:
* 1): 创建一个block
* 2): 在磁盘上记录元数据信息
* 3): 在BlockManager内记录block元数据信息
*/ // 将存储有问题主机的集合excluded一并传去,再次申请时将不再从他们里面申请block了
lb = locateFollowingBlock(excluded.length > 0 ? excluded : null);
RPC调用NameNode服务端代码
return dfsClient.namenode.addBlock(src, dfsClient.clientName,
block, excludedNodes, fileId, favoredNodes);
添加一个block
/**
* 1) 选择三台DataNode副本机器
* 2) 修改目录树
* 3) 储存数据信息【磁盘
*/
LocatedBlock locatedBlock = namesystem.getAdditionalBlock(src, fileId,
clientName, previous, excludedNodesSet, favoredNodesList);
1 . 选择存放block的datanode主机(负载均衡): chooseTarget4NewBlock【根据HDFS 机架感知
final DatanodeStorageInfo targets[] = getBlockManager().chooseTarget4NewBlock(
src, replication, clientNode, excludedNodes, blockSize, favoredNodes,
storagePolicyID);
根据block放置的策略
final DatanodeStorageInfo[] targets = blockplacement.chooseTarget(src,
numOfReplicas, client, excludedNodes, blocksize,
favoredDatanodeDescriptors, storagePolicy);
2 .修改内存里的目录树(修改内存里的元数据)
saveAllocatedBlock(src, inodesInPath, newBlock, targets);
添加block
BlockInfoContiguous b = dir.addBlock(src, inodesInPath, newBlock, targets);
BlockManager里记录了这个block的信息
getBlockManager().addBlockCollection(blockInfo, fileINode);
在内存里记录新的block信息
return blocksMap.addBlockCollection(block, bc);
新产生的block,放到文件节点下
fileINode.addBlock(blockInfo);
3 .把元数据写入磁盘
persistNewBlock(src, pendingFile);
(3 建立HDFS数据管道(建立的过程中传输的是一个空文件)
success = createBlockOutputStream(nodes, storageTypes, 0L, false);
创建一个socket
s = createSocketForPipeline(nodes[0], nodes.length, dfsClient);
此处创建socket,最后return
final Socket sock = client.socketFactory.createSocket();
创建输出流(把数据写到DataNode上)
OutputStream unbufOut = NetUtils.getOutputStream(s, writeTimeout);
创建输入流(读取响应结果)
InputStream unbufIn = NetUtils.getInputStream(s);
注意此处的一个socket
IOStreamPair saslStreams = dfsClient.saslClient.socketSend(s,
unbufOut, unbufIn, dfsClient, accessToken, nodes[0]);
这个输出流把客户端数据写到DataNode内【包装流1】
out = new DataOutputStream(new BufferedOutputStream(unbufOut,
HdfsConstants.SMALL_BUFFER_SIZE));
通过输入流读取DataNode返回的信息【包装流2】
blockReplyStream = new DataInputStream(unbufIn);
发送数据请求【接收到socket请求后,datanode创建DataXceiver来接收socket请求
new Sender(out).writeBlock(blockCopy, nodeStorageTypes[0], accessToken,
dfsClient.clientName, nodes, nodeStorageTypes, null, bcs,
nodes.length, block.getNumBytes(), bytesSent, newGS,
checksum4WriteBlock, cachingStrategy.get(), isLazyPersistFile,
(targetPinnings == null ? false : targetPinnings[0]), targetPinnings);
写数据,此处的写操作都是write_block
send(out, Op.WRITE_BLOCK, proto.build());
将socket的数据写到datanode,查看DataXceiver的run方法
out.flush();
读取此次数据的请求类型
op = readOp();
根据操作类型处理我们的数据
processOp(op);
case WRITE_BLOCK:
opWriteBlock(in);
写数据(查看实现类的方法)
writeBlock
创建BlockReceiver(查看run)【对ackQueue进行管控
blockReceiver = new BlockReceiver(block, storageType, in,
如果不是数据管道内的最后一个节点
if (type != PacketResponderType.LAST_IN_PIPELINE && !mirrorError) {
读取下游数据的处理结果
ack.readFields(downstreamIn);
向上游节点发送处理结果
sendAckUpstream(ack, expected, totalAckTimeNanos,
(pkt != null ? pkt.offsetInBlock : 0),
PipelineAck.combineHeader(datanode.getECN(), myStatus));
如果下游数据处理成功,当前datanode就会从ackQueue内移除packet
removeAckHead();
下游还存在服务器的话,继续连接下游服务器
if (targets.length > 0) {
mirror镜像,其实就是副本,就是datanode
mirrorSock = datanode.newSocket();
向下游发送socket链接(自上文writeBlock继续向下执行,形成循环)
new Sender(mirrorOut).writeBlock(originalBlock, targetStorageTypes[0],
接收block【内部启动PacketResponder
blockReceiver.receiveBlock(mirrorOut, mirrorIn, replyOut,
mirrorAddr, null, targets, false);
返回响应
writeResponse(SUCCESS, null, replyOut);
(4 数据管道建立不成功,就放弃这个block
dfsClient.namenode.abandonBlock(block, fileId, src,
dfsClient.clientName);
(5 获取有问题的这个机器存入excludedNodes
excludedNodes.put(nodes[errorIndex], nodes[errorIndex]);
注意此处的wrile,上述代码仍然使用重试的理念
2) 启动ResponceProcessor线程监听package是否发送成功
initDataStreaming();
由此处start可知ResponseProcessor为线程,查看其中run方法
response.start();
读取下游的处理结果
ack.readFields(blockReplyStream);
如果发送成功就把ackQueue内的packet备份删除
ackQueue.removeFirst();
3) 从dataQueue内把要发送的这个packet移除
dataQueue.removeFirst();
4) 向ackQueue内添加packet
ackQueue.addLast(one);
5) 写数据的代码(查看DataXceiver的run)
blockStream.flush();
针对步骤 5 的异常处理机制:
上述写数据的代码受下列catch内的代码进行异常捕获
【
进一步查看捕捉到这个异常的位置的代码 // 该方法将errorIndex赋值为0
tryMarkPrimaryDatanodeFailed();
向外抛异常,查看外层的catch
throw e;
】
其外层catch:
【
捕获到了异常,将hasError标识修改后,再进行while循环时就可以进入上文的if语句
hasError = true;
】
两个将会执行的if子句:
关闭对象
response.close();
打断当前线程,主要目的是让线程快速退出
this,interrupt();
让线程暂停
doSleep = processDatanodeError();
关闭流(此方法内关闭了所有相关的流)
closeStream();
将数据从ackQueue内重新加入DataQueue(恢复到出问题前的数据状态)
dataQueue.addAll(0, ackQueue);
清空ackQueue
ackQueue.clear();
重新建立管道
boolean doSleep = setupPipelineForAppendOrRecovery();
记录下刚刚出现问题的那个dataNode服务器(such hadoop1)
failed.add(nodes[errorIndex]);
当我们有一半以上的服务器出问题了,那么我们不能用剩下的节点直接建立管道,需要构建新的数据管道
if (dfsClient.dtpReplaceDatanodeOnFailure.satisfy(blockReplication,
nodes, isAppend, isHflushed)) {
addDatanode2ExistingPipeline();
申请新的datanode节点
final LocatedBlock lb = dfsClient.namenode.getAdditionalDatanode(
重新保存数据管道的信息
setPipeline(lb);
建立新的数据管道
transfer(src, targets, targetStorageTypes, lb.getBlockToken());
当我们有一半以内的服务器出问题时,则修改原有的数据管道
LocatedBlock lb = dfsClient.namenode.updateBlockForPipeline(block, dfsClient.clientName);