1. 背景
在Hadoop Yarn中,App、AppAttempt、Container、Node都有自己的生命周期,因此Yarn实现了一套状态机进行管理。通过状态机的管理后,用户可以直观看到App、AppAttempt、Container、Node的状态,其状态切换也更规范。但是状态机也导致Yarn的代码可能性很差,无法很好调试。
在HDFS中就不需要维护状态机,对于HDFS的操作,只有成功和失败。因此,在代码分析上,更容易对代码进行阅读。
通过https://blog.51cto.com/u_15327484/7995505文章了解到HDFS是GFS之上进行了一些简化,本文在此基础上,分析客户端向DataNode写数据的详细过程,文章最后会总结客户端发送数据的流程。
同时,本文通过代码分析,会解释以下难点:
- Datanode响应客户端的数据粒度。是按Block响应?还是按packet响应?还是按chunk响应?
- 客户端如何切分block?block在哪里切分成packet?packet哪里切分成chunk?
- 一个packet64KB,实际每个chunk写入packet的大小为516Byte,它们不是倍数关系,是不是一个packet没办法装满了?(来自强迫症患者的疑问)
2. HDFS客户端写操作流程概览
- 客户端创建DistributedFileSystem,请求Namenode创建文件。
- 向FSDataOutputStream写入要发送给DataNode的文件数据。
- FSDataOutputStream中,DataStream线程先向Namenode申请创建Block,并根据返回的block位置信息,与datanode建立连接。
- DataStream向DataNode发送packet。如果Block写入数据量达到128MB,就跳转到步骤3。
3. 写流程源码分析
3.1 写操作典型业务代码
进行写操作时,一般业务代码包含以下几步骤:
- 设置配置文件。
- 根据配置文件创建HDFS客户端对象。
- 基于HDFS客户端对象执行write写操作。
如下所示:
public void create() throws URISyntaxException, IOException, InterruptedException {
// 配置文件
Configuration conf = new Configuration();
// 获取文件系统
FileSystem fs = FileSystem.get(new URI("hdfs://{namenode IP}:9000"), conf, 访问的用户);
// 创建文件并写入数据
FSDataOutputStream out = fs.create(new Path("/root/test3.txt"));
out.write("Hello, HDFS".getBytes());
out.flush();
// 关闭流
fs.close();
}
3.2 根据URL创建HDFS客户端
FileSystem是一个抽象类,它提供get方法获取文件系统客户端实现类对象。之所以没有直接new一个HDFS客户端,是因为Hadoop提供多种文件系统的访问,例如alluxio、S3a。FileSystem读取url和Configuration即可创建对应的文件系统客户端。Hadoop自带的FileSystem实现类有如下:
本文文档只用于研究HDFS,因此这里FileSystem.get方法访问的是HDFS文件系统的客户端:
public abstract class FileSystem extends Configured
implements Closeable, DelegationTokenIssuer {
public static FileSystem get(final URI uri, final Configuration conf,
final String user) throws IOException, InterruptedException {
String ticketCachePath =
conf.get(CommonConfigurationKeys.KERBEROS_TICKET_CACHE_PATH);
UserGroupInformation ugi =
UserGroupInformation.getBestUGI(ticketCachePath, user);
return ugi.doAs(new PrivilegedExceptionAction<FileSystem>() {
@Override
public FileSystem run() throws IOException {
return get(uri, conf);
}
});
}
}
由于FileSystem是一个抽象类,返回的对象必须是其子类。get方法负责创建FileSystem子类,如下,调用getFileSystemClass方法,它会获取core-site.xml中fs.hdfs.impl的配置,默认为org.apache.hadoop.hdfs.DistributedFileSystem,后续通过反射创建DistributedFileSystem对象:
private static FileSystem createFileSystem(URI uri, Configuration conf)
throws IOException {
Tracer tracer = FsTracer.get(conf);
try(TraceScope scope = tracer.newScope("FileSystem#createFileSystem")) {
scope.addKVAnnotation("scheme", uri.getScheme());
//查询core-site.xml中fs.hdfs.impl的配置,默认是org.apache.hadoop.hdfs.DistributedFileSystem
Class<?> clazz = getFileSystemClass(uri.getScheme(), conf);
//通过反射创建FileSystem子类DistributedFileSystem对象
FileSystem fs = (FileSystem)ReflectionUtils.newInstance(clazz, conf);
//DistributedFileSystem初始化操作
fs.initialize(uri, conf);
return fs;
}
}
查看DistributedFileSystem类,可以发现,它包含最核心的成员是DFSClient,DFSClient负责执行真正的请求namenode和datanode读写操作:
public class DistributedFileSystem extends FileSystem
implements KeyProviderTokenIssuer {
//工作目录,一般为/user/{USER},类似linux中,hadoop用户的工作目录是/home/hadoop。默认情况下,使用hdfs dfs -ls . 即可访问/user/{User}家目录
private Path workingDir;
//集群地址,一般为hdfs://{集群名}
private URI uri;
//用来连接namenode和datanode最核心的客户端
DFSClient dfs;
//检验数据是否正确写入或读取
private boolean verifyChecksum = true;
//客户端审计信息
private DFSOpsCountStatistics storageStatistics;
}
3.3 客户端请求namenode创建文件
创建好HDFS客户端对象后,业务方此时会调用.create方法新建一个文件以为后续写数据作准备。
在执行create方法创建文件时,有几个非常重要的参数可以留意。
- permission:默认情况下,为默认权限与默认umask异或运算。即666-022=644
- overwrite:是否覆盖原有有文件,默认为true,即进行覆盖。
- bufferSize:写入数据时使用的缓冲区大小。默认情况下读取io.file.buffer.size配置,单位byte,默认4KB。
- replication:默认情况下,读取dfs.replication获取文件副本数,默认3。获取默认值的方法由DistributedFileSystem#getDefaultReplication提供。
- blockSize:默认情况下:读取file.blocksize获取每个block大小,默认为64MB,可以设置为128MB。获取默认值的方法由DistributedFileSystem#getDefaultBlockSize提供。
- favoredNodes:数据块首选节点,默认为空。即不指定首选节点。
public HdfsDataOutputStream create(final Path f,
final FsPermission permission, final boolean overwrite,
final int bufferSize, final short replication, final long blockSize,
final Progressable progress, final InetSocketAddress[] favoredNodes)
throws IOException {
statistics.incrementWriteOps(1);
storageStatistics.incrementOpCounter(OpType.CREATE);
Path absF = fixRelativePart(f);
return new FileSystemLinkResolver<HdfsDataOutputStream>() {
@Override
public HdfsDataOutputStream doCall(final Path p) throws IOException {
//创建文件
final DFSOutputStream out = dfs.create(getPathName(f), permission,
overwrite ? EnumSet.of(CreateFlag.CREATE, CreateFlag.OVERWRITE)
: EnumSet.of(CreateFlag.CREATE),
true, replication, blockSize, progress, bufferSize, null,
favoredNodes);
//准备开始写数据
return dfs.createWrappedOutputStream(out, statistics);
}
//省略
}.resolve(this, absF);
}
create方法最终调用DFSOutputStream#newStreamForCreate方法,创建文件并启动写入流程:
- 通过执行dfsClient.namenode.create()方法向namenode服务端发起请求创建文件。
- 创建DFSOutputStream对象,该对象内部有一个DataStreamer线程。向DFSOutputStream写入的数据会发送给DataStreamer线程。
static DFSOutputStream newStreamForCreate(DFSClient dfsClient, String src,
FsPermission masked, EnumSet<CreateFlag> flag, boolean createParent,
short replication, long blockSize, Progressable progress,
DataChecksum checksum, String[] favoredNodes, String ecPolicyName)
throws IOException {
try (TraceScope ignored =
dfsClient.newPathTraceScope("newStreamForCreate", src)) {
HdfsFileStatus stat = null;
// Retry the create if we get a RetryStartFileException up to a maximum
// number of times
boolean shouldRetry = true;
int retryCount = CREATE_RETRY_COUNT;
while (shouldRetry) {
shouldRetry = false;
try {
//创建文件
stat = dfsClient.namenode.create(src, masked, dfsClient.clientName,
new EnumSetWritable<>(flag), createParent, replication,
blockSize, SUPPORTED_CRYPTO_VERSIONS, ecPolicyName);
break;
//省略
}
final DFSOutputStream out;
if(stat.getErasureCodingPolicy() != null) {
out = new DFSStripedOutputStream(dfsClient, src, stat,
flag, progress, checksum, favoredNodes);
} else {
out = new DFSOutputStream(dfsClient, src, stat,
flag, progress, checksum, favoredNodes, true);
}
//启动发送数据的线程
out.start();
return out;
}
}
首先,查看创建文件的请求流程。dfsClient.namenode成员类型实际上是客户端与Namenode的协议ClientProtocol,通过Haddoop RPC可以知道,要想访问namenode,必须创建ClientProtocol的代理对象。
在DFSClient的对象创建过程汇中,可以看到,代理对象的创建是在NameNodeProxiesClient#createProxyWithLossyRetryHandler方法中,它指定创建ClientProtocol.class的代理对象:
public class DFSClient implements java.io.Closeable, RemotePeerFactory,
DataEncryptionKeyFactory, KeyProviderTokenIssuer {
final ClientProtocol namenode;
ProxyAndInfo<ClientProtocol> proxyInfo = null;
proxyInfo = NameNodeProxiesClient.createProxyWithLossyRetryHandler(conf,
nameNodeUri, ClientProtocol.class, numResponseToDrop,
nnFallbackToSimpleAuth);
this.namenode = proxyInfo.getProxy();
}
进入NameNodeProxiesClient,它先创建ProxyProvider,再由ProxyProvider创建代理对象:
AbstractNNFailoverProxyProvider<T> failoverProxyProvider =
createFailoverProxyProvider(config, nameNodeUri, xface, true,
fallbackToSimpleAuth);
T proxy = (T) Proxy.newProxyInstance(
failoverProxyProvider.getInterface().getClassLoader(),
new Class[]{xface}, dummyHandler);
ProxyProvider的实现类由hdfs-site.xml中的dfs.client.failover.proxy.provider.{集群名}配置指定,默认是org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider。
在ConfiguredFailoverProxyProvider中,创建了active namenode和standby namenode两个proxy对象。当通过RetryProxy创建的代理对象执行rpc失败后,会通过performFailover方法切换到另外一个proxy对象,这涉及到Java Proxy机制,不详细研究。
protected final List<NNProxyInfo<T>> proxies;
ConfiguredFailoverProxyProvider最终调用NameNodeProxiesClient#createProxyWithAlignmentContext创建proxy对象,如下,最终的基于ClientNamenodeProtocolPB创建proxy,后续就是protobuf生成代码,不详细研究:
public static ClientProtocol createProxyWithAlignmentContext(
InetSocketAddress address, Configuration conf, UserGroupInformation ugi,
boolean withRetries, AtomicBoolean fallbackToSimpleAuth,
AlignmentContext alignmentContext)
throws IOException {
RPC.setProtocolEngine(conf, ClientNamenodeProtocolPB.class,
ProtobufRpcEngine.class);
final RetryPolicy defaultPolicy =
RetryUtils.getDefaultRetryPolicy(
conf,
HdfsClientConfigKeys.Retry.POLICY_ENABLED_KEY,
HdfsClientConfigKeys.Retry.POLICY_ENABLED_DEFAULT,
HdfsClientConfigKeys.Retry.POLICY_SPEC_KEY,
HdfsClientConfigKeys.Retry.POLICY_SPEC_DEFAULT,
SafeModeException.class.getName());
final long version = RPC.getProtocolVersion(ClientNamenodeProtocolPB.class);
ClientNamenodeProtocolPB proxy = RPC.getProtocolProxy(
ClientNamenodeProtocolPB.class, version, address, ugi, conf,
NetUtils.getDefaultSocketFactory(conf),
org.apache.hadoop.ipc.Client.getTimeout(conf), defaultPolicy,
fallbackToSimpleAuth, alignmentContext).getProxy();
if (withRetries) { // create the proxy with retries
Map<String, RetryPolicy> methodNameToPolicyMap = new HashMap<>();
ClientProtocol translatorProxy =
new ClientNamenodeProtocolTranslatorPB(proxy);
return (ClientProtocol) RetryProxy.create(
ClientProtocol.class,
new DefaultFailoverProxyProvider<>(ClientProtocol.class,
translatorProxy),
methodNameToPolicyMap,
defaultPolicy);
} else {
return new ClientNamenodeProtocolTranslatorPB(proxy);
}
}
基于协议,namenode会返回文件的元数据给client。比如:路径、权限、拥有者、组、大小、修改时间,block位置信息。详细元数据如下所示:
private long length = 0L;
private boolean isdir = false;
private int replication = 0;
private long blocksize = 0L;
private long mtime = 0L;
private long atime = 0L;
private FsPermission permission = null;
private EnumSet<Flags> flags = EnumSet.noneOf(Flags.class);
private String owner = null;
private String group = null;
private byte[] symlink = null;
private byte[] path = EMPTY_NAME;
private long fileId = -1L;
private int childrenNum = 0;
private FileEncryptionInfo feInfo = null;
private byte storagePolicy =
HdfsConstants.BLOCK_STORAGE_POLICY_ID_UNSPECIFIED;
private ErasureCodingPolicy ecPolicy = null;
private LocatedBlocks locations = null;
在客户端创建完Namenode代理对象后,会启动DFSOutputStream中DataStreamer线程。客户端会将数据通过该线程向DataNode中进行写入。将在下一节中详细介绍DataStreamer线程:
out = new DFSOutputStream(dfsClient, src, stat,
flag, progress, checksum, favoredNodes, true);
//启动发送数据的线程
out.start();
3.4 客户端向datanode发送数据
💡 在了解写入流程前,需要知道数据的三个单位:
- block:对于一个逻辑上的大文件首先划分为block,大文件按block为单位存储到datanode中。block大小在客户端创建文件时指定,它读取dfs.block.size配置,默认为64MB,建议128MB。
- packet:对于一个block的读写,会讲block划分成为packet进行传输,它读取dfs.client-write-packet-size配置,默认64KB。划分packet好处是,当传输异常时,重新传输只需要重传64KB的packet,而不是128MB的block,效率更高。
- chunk:对于64KB的packet,为了保证传输过程中不出现数据传输错误的问题,每512字节的业务数据会生成4字节的checkSum校验数据,它们一起传输到DataNode中。数据与检验值的比值为128:1,所以对于一个128M的block会有一个1M的校验文件与之对应。备注:在datanode中,checkSum校验数据会保存在*.meta文件中。
创建好客户端代理对象后,业务代码就开始向datanode发送数据了。如下所示:
FSDataOutputStream out = fs.create(new Path("/root/test3.txt"));
//开始写入数据
out.write("Hello, HDFS".getBytes());
out.flush();
FSDataOutputStream.write最终会调用其内部对象DFSOutputStream.write写入数据,它会调用FSOutputSummer#write1方法开始写入业务数据到输出流。其中:
- 输入参数byte b[]是要写到datanode中的业务数据。
- 业务数据通过write1方法是要写到chunk中的,chunk实际上就是一个byte类型的数组的成员buf,它的大小设置为512*9。即一次写入9个chunk,避免多次系统调用。
- 每个chunk有一个4字节的checksum,由成员变量checksum表示,每次写入5129字节的业务数据,都会生成49字节的checksum写入到packet中。
- 写完9个chunk后如果业务数据还没写完,继续调用write1方法写入,知道业务数据全部写完。
public synchronized void write(byte b[], int off, int len)
throws IOException {
checkClosed();
if (off < 0 || len < 0 || off > b.length - len) {
throw new ArrayIndexOutOfBoundsException();
}
//如果write1还没有写完业务数据,则继续写
for (int n=0;n<len;n+=write1(b, off+n, len-n)) {
}
}
this.buf = new byte[sum.getBytesPerChecksum() * BUFFER_NUM_CHUNKS];
this.checksum = new byte[getChecksumSize() * BUFFER_NUM_CHUNKS];
private int write1(byte b[], int off, int len) throws IOException {
//如果业务数据大于512 * 9,就直接写到输出流程
if(count==0 && len>=buf.length) {
// local buffer is empty and user buffer size >= local buffer size, so
// simply checksum the user buffer and send it directly to the underlying
// stream
final int length = buf.length;
writeChecksumChunks(b, off, length);
return length;
}
//如果业务数据小于512 * 9,将业务数据拷贝到buffer中,再flushBuffer,避免数组越界
// copy user data to local buffer
int bytesToCopy = buf.length-count;
bytesToCopy = (len<bytesToCopy) ? len : bytesToCopy;
System.arraycopy(b, off, buf, count, bytesToCopy);
count += bytesToCopy;
if (count == buf.length) {
// local buffer is full
flushBuffer();
}
return bytesToCopy;
}
如下,开始调用writeChunk方法循环写入chunk。注意,此时指定的len参数为buf的大小,即512*9,后续的业务数据本次调用不会处理。
- 它先生成9个chunk对应的9个checkSum,写入到checksum数组中。
- 将9个chunk和9个checksum作为参数,准备向packet中写数据。
private void writeChecksumChunks(byte b[], int off, int len)
throws IOException {
//生成9个chunk对应的9个checkSum,写入到checksum数组中
sum.calculateChunkedSums(b, off, len, checksum, 0);
TraceScope scope = createWriteTraceScope();
try {
for (int i = 0; i < len; i += sum.getBytesPerChecksum()) {
int chunkLen = Math.min(sum.getBytesPerChecksum(), len - i);
int ckOffset = i / sum.getBytesPerChecksum() * getChecksumSize();
//将9个chunk和9个checksum作为参数,准备向packet中写数据
writeChunk(b, off + i, chunkLen, checksum, ckOffset,
getChecksumSize());
}
} finally {
if (scope != null) {
scope.close();
}
}
}
调用FSDataOutputStream#writeChunk向packet中写chunk。首先会通过createPacket方法创建一个packect。同时,指定packet参数,有几个非常重要:
- packetSize:读dfs.client-write-packet-size配置,默认64KB。
- chunksPerPacket:每个packet能够写入的chunk数量。通过computePacketChunkSize方法计算。首先packet要给header预留PKT_MAX_HEADER_LEN大小,其次,每个chunk实际上是chunksize+checksum=516。因此每个packet能够写入的chunk数为
(64KB-PKT_MAX_HEADER_LEN)/516
。 - BytesCurBlock:block大小,配置中设置为128MB。
- Seqno:当前packet编号。
currentPacket = createPacket(packetSize, chunksPerPacket, getStreamer()
.getBytesCurBlock(), getStreamer().getAndIncCurrentSeqno(), false);
protected void computePacketChunkSize(int psize, int csize) {
final int bodySize = psize - PacketHeader.PKT_MAX_HEADER_LEN;
final int chunkSize = csize + getChecksumSize();
chunksPerPacket = Math.max(bodySize/chunkSize, 1);
packetSize = chunkSize*chunksPerPacket;
DFSClient.LOG.debug("computePacketChunkSize: src={}, chunkSize={}, "
+ "chunksPerPacket={}, packetSize={}",
src, chunkSize, chunksPerPacket, packetSize);
}
writeChunk写入chunk到packet中。注意,每次写完checksum和data后,incBytesCurBlock只在DataStream中记录增加512字节而不是516字节。这说明checksum不会和业务数据保存在datanode的同一个文件中:
protected synchronized void writeChunk(byte[] b, int offset, int len,
byte[] checksum, int ckoff, int cklen) throws IOException {
//创建packet
writeChunkPrepare(len, ckoff, cklen);
//packet写入4字节checksum
currentPacket.writeChecksum(checksum, ckoff, cklen);
//packet写入512字节业务数据
currentPacket.writeData(b, offset, len);
//packet中的chunk数+1
currentPacket.incNumChunks();
//当前block写入的大小增加512
getStreamer().incBytesCurBlock(len);
//如果写入的chunk数量达到每个packet的上限,将packet加入到DataStreamer队列dataQueue中
// If packet is full, enqueue it for transmission
if (currentPacket.getNumChunks() == currentPacket.getMaxChunks() ||
getStreamer().getBytesCurBlock() == blockSize) {
enqueueCurrentPacketFull();
}
}
在创建HDFS客户端代理对象后,我们看到了它启动了DFSOutputStream中的一个线程:
out = new DFSOutputStream(dfsClient, src, stat,
flag, progress, checksum, favoredNodes, true);
//启动发送数据的线程
out.start();
该线程其实就是DataStreamer,它有以下几个重要步骤:
- 向Namenode申请创建HDFS文件的block。
- 构建DataNode数据管道。
- 从DFSOutputStream的dataQueue中获取一个packet。
- 将packet发送给DataNode。
- 将packet加入到ackQueue队列中。
public void run() {
while (!closed && clientRunning) {
Packet one = null;
//省略
// get new block from namenode.
// 从namenode处申请获得新的数据块
if (stage == BlockConstructionStage.PIPELINE_SETUP_CREATE) { // create新建文件写入模式
setPipeline(nextBlockOutputStream());
initDataStreaming();
} else if (stage == BlockConstructionStage.PIPELINE_SETUP_APPEND) { // append追加写模式
setupPipelineForAppendOrRecovery();
initDataStreaming();
}
// send the packet
// 将packet从dataQueue移至ackQueue,等待确认;数据包发送前准备
synchronized (dataQueue) {
// move packet from dataQueue to ackQueue
if (!one.isHeartbeatPacket()) {
dataQueue.removeFirst();
//将packet加入到ackQueue中
ackQueue.addLast(one);
dataQueue.notifyAll();
}
}
// write out data to remote datanode
try {
// 利用生成的写入流将数据写入DataNode中的block
one.writeTo(blockStream);
blockStream.flush();
} catch (IOException e) {
}
//省略
closeInternal();
}
上述流程中,可以仔细看一下申请block并构建数据管道的流程,写入流程直接调用write方法,比较简单。在DataStreamer#nextBlockOutputStream中申请block并建立datanode的连接:
protected LocatedBlock nextBlockOutputStream() throws IOException {
LocatedBlock lb;
//省略
lb = locateFollowingBlock(
excluded.length > 0 ? excluded : null, oldBlock);
//省略
success = createBlockOutputStream(nodes, nextStorageTypes, nextStorageIDs,
0L, false);
//省略
return lb;
}
通过方法,最终调用DFSOutputStream#addBlock请求创建block:
return dfsClient.namenode.addBlock(src, dfsClient.clientName, prevBlock,
excludedNodes, fileId, favoredNodes, allocFlags);
在createBlockOutputStream方法中,根据block位置,向datanode建立连接,发送socket请求:
boolean createBlockOutputStream(DatanodeInfo[] nodes,
StorageType[] nodeStorageTypes, String[] nodeStorageIDs,
long newGS, boolean recoveryFlag) {
//省略
//与datanode创建socket连接
s = createSocketForPipeline(nodes[0], nodes.length, dfsClient);
//省略
// send the request
//向datanode发送writeBlock的请求
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 && targetPinnings[0]), targetPinnings,
nodeStorageIDs[0], nodeStorageIDs);
//
// receive ack for connect
BlockOpResponseProto resp = BlockOpResponseProto.parseFrom(
PBHelperClient.vintPrefixed(blockReplyStream));
//省略
}
创建完连接后,启动ResponseProcessor线程用于处理datanode对于packet的响应:
private void initDataStreaming() {
response = new ResponseProcessor(nodes);
response.start();
stage = BlockConstructionStage.DATA_STREAMING;
}
最后,ResponseProcessor线程获取获取Datanode响应,如果Datanode返回失败信息,直接在catch中修改errorState状态,会导致DataStreamer直接线程终止;否则DataStreamer正常执行:
public void run() {
...
PipelineAck ack = new PipelineAck();
while (!closed && clientRunning && !lastPacketInBlock) {
try {
// read an ack from the pipeline
ack.readFields(blockReplyStream);
...
// 处理所有DataNode响应的状态
for (int i = ack.getNumOfReplies()-1; i >=0 && clientRunning; i--) {
short reply = ack.getReply(i);
// ack验证,如果DataNode写入packet失败,则出错
if (reply != DataTransferProtocol.OP_STATUS_SUCCESS) {
// 记录损坏的DataNode,会在processDatanodeError方法移除该失败的DataNode
errorIndex = i;
throw new IOException("Bad response " + reply + " for block " + block + " from datanode " + targets[i].getName());
}
}
//省略
one = ackQueue.getFirst();
//省略
synchronized (ackQueue) {
// 验证ack
assert ack.getSeqno() == lastAckedSeqno + 1;
lastAckedSeqno = ack.getSeqno();
// 移除确认写入成功的packet
ackQueue.removeFirst();
ackQueue.notifyAll();
}
} catch (Exception e) {
lastException.set(e);
errorState.setInternalError();
errorState.markFirstNodeIfNotMarked();
synchronized (dataQueue) {
dataQueue.notifyAll();
}
}
}
}
DataStreamer在循环时,会判断errorState状态,如果有一场,直接终止DataStreamer线程:
private boolean shouldStop() {
return streamerClosed || errorState.hasError() || !dfsClient.clientRunning;
}
packet写入DataNode线程模型如下所示:
4. 写流程总结
- 业务方调用FileSystem.get方法获取URL对应的文件系统客户端对象。它获取hdfs://{集群名}的URL前缀hdfs,拼接成配置项fs.hdfs.impl,从core-site.xml中获取该配置,默认是org.apache.hadoop.hdfs.DistributedFileSystem,通过反射创建DistributedFileSystem对象并返回。
- 业务方调用DistributedFileSystem.create方法创建HDFS文件。它携带HDFS的副本数、权限、block大小等参数对文件规格进行设置。实际上,它是读取hdfs-site.xml中的dfs.client.failover.proxy.provider.{集群名}配置,默认为org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider。由ConfiguredFailoverProxyProvider创建每个Namenode客户端代理对象,这样一个Namenode挂掉,还可以访问另外一个。通过该代理对象访问Namenode,并创建文件。创建完HDFS文件后,启动DataStreamer线程,准备接受数据并发送给DataNode。
- 业务方调用FSDataOutputStream.write开始写数据。FSDataOutputStream创建一个5129大小的数组和49大小的数组,前者表示有9个chunk,每个chunk大小为512字节,每个chunk会生成4字节的校验数据checkSum,后者数组中存储的是每个chunk对应的checkSum。FSDataOutputStream循环读取512*9大小的业务数组,并声成对应的checkSum数据,分别写入到上述两个数组中。
- FSDataOutputStream创建一个packet,默认packet大小是64KB。指定单个packet可以容纳chunk数量上限是(64KB-PKT_MAX_HEADER_LEN)/516,持续写入chunk和checkSum到packet中,知道达到可容纳chunk数量上限。然后将packet加入到DataStreamer的dataQueue队列中。
- DataStreamer线程持续获取dataQueue队列中的packet,准备发送给datanode。在发送datanode之前,按需向Namenode申请创建block,成功后与datanode建立socket连接并发送writeBlock请求,后续就写入block了。同时,启动ResponseProcessor建成检查datanode是否正确存储packet,如果失败,就会导致datastreamr写入流程终止,即客户端写数据流程终止。
- 当一个packet处理完后,跳转到第三步继续写入chunk数据。
5. 疑问解答
- Datanode响应客户端的数据粒度。是按Block响应?还是按packet响应?还是按chunk响应?
- 答:DataNode按packet向客户端进行响应。
- 客户端如何切分block?block在哪里切分成packet?packet哪里切分成chunk?
- 答:对于业务写入的数据,每次FSDataOutputStream只会获取512*9的数据,复制到空字节数组中,这个过程可以看作是切分chunk过程。将chunk字节数组内容复制到packet的字节数组中,这个过程可以看作是切分packet过程。当客户端传输的packet大小总共超过block大小,就会新建一个block,这个过程可以看作是切分block过程。
- 一个packet64KB,实际每个chunk写入packet的大小为516Byte,它们不是倍数关系,是不是一个packet没办法装满了?(来自强迫症患者的疑问)
- 答:packet按照chunk数量来接收chunk数据,chunk上限为
(64KB-PKT_MAX_HEADER_LEN)/516
。
- 答:packet按照chunk数量来接收chunk数据,chunk上限为