相对NN,DN主要就是对数据块的副本进行操作,如增删改等操作,管理DN中的这些副本,另外提供对副本的接口给client,NN,其他的DN。
startDataNode() 方法:
首先从配置文件中读取与DN 相关的配置参数。
与NN 进行握手。
根据参数配置好的数据块存放的文件目录,为每个目录建立起 DataStorage ,然后调用该类的 recoverTransitionRead 方法去读取存储元信息,锁住目录,然后转变文件状态。做一些格式化,恢复,回滚,升级操作。
然后调用 FSDataset 构造函数,读取上一步的文件目录下的副本文件,将副本状态信息保存到内存列队中,同时创建线程池用于执行对副本的异步删除操作。
实始化数据块扫描类 DataBlockScanner 。
最后,启动一系列服务端口,如接收数据的端口,web server 访问端口等。
run() 方法:
首先启动 DN 的数据接收服务守护线程 DataXceiverServer 。
然后循环做以下操作:
判断是否需要更新,如参数发生变化了,则需要重新初始化 DN 。
发送心跳,发送最近接收的 block ,报告 DN 当前的 block 列表给 NN 。
接收 NN 发送给 DN 的相关命令,并且执行命令。
报告 DN 当前的所有 block 列表的时间间隔相对要长很多,默认是 1 个小时报告一次。
启动 DataBlockScanner 线程。
由 DataNode 类中的 processCommand() 方法完成。
副本复制:
由NN 告诉DN 将DN 中存在的block 传送给其他DN, 主要是由于NN 检测到该数据块的副本数少于3 ,因此需要将副本复制给其他DN ,于是通知副本所在的DN ,将副本数据推送给其他的DN 。其中NN 将数据块的信息和目标的DN 信息存放在类 BlockCommand 中,DN 收到这些信息后,解析出数据块和目标DN 信息,分别将相应的数据块传送给相应的目标DN 。
private void transferBlocks( Block blocks[],
DatanodeInfo xferTargets[][]
) {
for ( int i = 0; i < blocks. length ; i++) {
try {
transferBlock(blocks[i], xferTargets[i]);
} catch (IOException ie) {
LOG .warn( "Failed to transfer block " + blocks[i], ie);
}
}
}
然后判断该副本的在DN 磁盘上的大小小于NN 中存放的大小时,则该副本是不可用的,然后调用 namenode .reportBadBlocks ()告诉NN ,这副本是坏的。
否则启动线程去进行副本传送工作。
new Daemon( new DataTransfer (xferTargets, block, this )).start();
DataTransfer 线程里面将会调用BlockSender. sendBlock() 将副本数据分成多个packs 传送给相应的目标DN 。
标识无效的数据块:
同样从NN 中拿到无效的数据块,因为客户已经将数据块删除了。
一方面是告诉DataBlockScanner 不再去对该数据块进行扫描检查工作。
另一方面,告诉FSDataset ,从内存的副本集合中删除掉该副本,同时调用FSDatasetAsyncDiskService 类deleteAsync ()异步的删除该副本在本地磁盘上的数据文件和元信息文件。
if ( blockScanner != null ) {
blockScanner . deleteBlocks (toDelete);
}
data .invalidate(toDelete);
数据块恢复:
将为每个要恢复的数据块建立一个线程来完成恢复工作。
public Daemon recoverBlocks( final Collection
Daemon d = new Daemon( threadGroup , new Runnable() {
/** Recover a list of blocks. It is run by the primary datanode. */
public void run() {
for (RecoveringBlock b : blocks) {
try {
logRecoverBlock ( "NameNode" , b.getBlock(), b.getLocations());
recoverBlock(b);
} catch (IOException e) {
LOG .warn( "recoverBlocks FAILED: " + b, e);
}
}
}
});
d.start();
return d;
}
然后分别去该数据块的其他DN 上查询到在这些DN 上副本记录。
然后进行数据块恢复工作。
如果所有的DN 上该数据块都没有保存数据,则告诉调用
namenode .commitBlockSynchronization(block, recoveryId, 0,
true , true , DatanodeID. EMPTY_ARRAY );
告诉NN ,这是一个空的数据块。
不然的话,计算该数据块最佳可用的副本状态。即取所有的DN 中收到数据值最小作为可用的数据。
最后,在将该数据块的状态告诉其他的DN 和NN 。
Block reply = r. datanode .updateReplicaUnderRecovery(
r. rInfo , recoveryId, newBlock.getNumBytes());
namenode .commitBlockSynchronization(block,
newBlock.getGenerationStamp(), newBlock.getNumBytes(), true , false ,
nlist);
关闭DN :
将DN 中的守护线程都停止掉。如:DataXceiverServer ,DataBlockScanner ,DataNode 的心跳线程,以及与NN 的RPC 通信线程等。
重新注册:
调用RPC ,告之NN ,DN 当前的通信信息。
dnRegistration = namenode .registerDatanode( dnRegistration );
另外,设置BR 的周期。
scheduleBlockReport( initialBlockReportDelay );
完成升级:
由于升级完成,因为在升级之前,为了保证数据不会丢失,将当前的文件目录都从.cuttent 移到了. Previous ,升级成功后,将.pervious 目录移到.tmp 目录下,然后删除.tmp 临时目录下的所有数据。
由DataXceiver 类中的run() 方法完成。
读数据块:
建立起向客户端的输出流,然后调用BlockSender 类将数据块副本的数据从磁盘读取后发送给客户端。
对应于客户端来DN 中读取相应的数据块。
写数据块:
收到一个数据块的数据,将其写到磁盘上,同时要向下一个DN 传送该数据块的数据。
对应于客户端或者上游DN 向本地DN 写数据。
替换数据块
向相应的DN 发送复制操作,然后接收该DN 发送来的数据块数据,并且将数据保存到磁盘中。
复制数据块
从磁盘中读取到数据块的数据,然后将数据发送给发送复制操作给自已的目标DN 。
获得数据块的校验数据:
从元数据文件里获得校验数据,然后发送给客户端。
存储信息的管理类。
protected List
变量保存着文件目录列表。
DN 构造时会调用该函数,在读文件目录前做回滚和保存现场,用于防止启动失败,数据丢失。
void recoverTransitionRead(NamespaceInfo nsInfo,
Collection
StartupOption startOpt
)
为每个数据目录计算状态,确定一致性。根据当前的不同状态进行格式化或者恢复操作。
参数dfs.datanode.data.dir 中由管理员配置副本存放的文件目录,可以有多个目录,用, 分开。
switch (curState) {
case NORMAL :
break ;
case NON_EXISTENT :
// ignore this storage
LOG .info( "Storage directory " + dataDir + " does not exist." );
it.remove();
continue ;
case NOT_FORMATTED : // format
LOG .info( "Storage directory " + dataDir + " is not formatted." );
LOG .info( "Formatting ..." );
format(sd, nsInfo);
break ;
default : // recovery part is common
sd.doRecover(curState);
}
然后再进行事务处理。
for ( int idx = 0; idx < getNumStorageDirs(); idx++) {
doTransition(getStorageDir(idx), nsInfo, startOpt);
assert this .getLayoutVersion() == nsInfo.getLayoutVersion() :
"Data-node and name-node layout versions must be the same." ;
assert this .getCTime() == nsInfo.getCTime() :
"Data-node and name-node CTimes must be the same." ;
}
回滚操作: 将current 命名为tmp ,previous to current ,再删除tmp 目录。
升级操作: 清空 detach 目录,删除previous 目录,将current 命名为tmp 目录,
将finalized 和 RBW 状态的数据在current 目录里创建硬链接指向tmp 目录中的数据块文件。
最后在将tmp 目录命名为previous 目录。
再将本次启动的事务信息写入到文件中,如版本号之类的。
this .writeAll();
副本集合的管理类。
副本列表(数据块列表)。
ReplicasMap volumeMap = new ReplicasMap();
FSDataset ()
由DN 构造函数时调用该类构造函数,用于实始化DN 中的内部结构。
调用FSVolumeSet 类中的方法getVolumeMap (),将相应的副本信息读入到内存中,即副本队列中。扫描多个数据目录,以及目录下的各个子目录,子文件。
void getVolumeMap(ReplicasMap volumeMap) throws IOException {
// add finalized replicas
dataDir .getVolumeMap(volumeMap, this );
// add rbw replicas
addToReplicasMap(volumeMap, rbwDir , false );
}
最后创建一个磁盘异步服务类 FSDatasetAsyncDiskService 。
File[] roots = new File[storage.getNumStorageDirs()];
for ( int idx = 0; idx < storage.getNumStorageDirs(); idx++) {
roots[idx] = storage.getStorageDir(idx).getCurrentDir();
}
asyncDiskService = new FSDatasetAsyncDiskService(roots);
为每个数据块目录创建一个线程池,并用作为一个线程组,池的最小值为1 ,最大值为4 ,当前版本,只有delete 操作会将任务经过线程池调度来进行异步处理,读写操作都是调文件操作同步去执行的。
FSDatasetAsyncDiskService(File[] volumes) {
threadFactory = new ThreadFactory() {
public Thread newThread(Runnable r) {
return new Thread( threadGroup , r);
}
};
// Create one ThreadPool per volume
for ( int v = 0 ; v < volumes. length ; v++) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_THREADS_PER_VOLUME , MAXIMUM_THREADS_PER_VOLUME ,
THREADS_KEEP_ALIVE_SECONDS , TimeUnit. SECONDS ,
new LinkedBlockingQueue
// This can reduce the number of running threads
executor.allowCoreThreadTimeOut( true );
executors .put(volumes[v], executor);
}
最后来一张副本的状态变化图
想要知道更多的细节,看源代码是最清楚的,同时自已可以调试运行代码。