##这是我参与「第四届青训营 」笔记创作活动的第7天
整体项目架构图
ruyuan-dfs-datanode目录结构
.
└── main
├── java
│ └── com
│ └── ruyuan
│ └── dfs
│ └── datanode
│ ├── DataNode.java
│ ├── config
│ │ └── DataNodeConfig.java
│ ├── namenode
│ │ ├── HeartbeatTask.java
│ │ └── NameNodeClient.java
│ ├── replica
│ │ ├── PeerDataNodes.java
│ │ └── ReplicateManager.java
│ └── server
│ ├── DataNodeApis.java
│ ├── DataNodeServer.java
│ ├── DefaultFileTransportCallback.java
│ ├── MultiPortChannelInitializer.java
│ ├── StorageInfo.java
│ ├── StorageManager.java
│ ├── http
│ │ └── HttpFileServerHandler.java
│ └── locate
│ ├── AbstractFileLocator.java
│ ├── AesFileLocator.java
│ ├── FileLocator.java
│ ├── FileLocatorFactory.java
│ ├── Md5FileLocator.java
│ ├── Sha1FileLocator.java
│ └── SimpleFileLocator.java
└── resources
└── assembly.xml
主要模块:
- config: Datanode配置
- namenode: 主要负责与Namenode之间的通讯
- replica: 主要负责副本复制管理
- server: Datanode服务端实现,用于处理客户端的上传文件或下载文件的请求
- DataNode.java: DataNode启动类的实现
具体模块解读
1、config
主要功能:Datanode配置
包含文件:DataNodeConfig.java
源代码:
/**
* DataNode的配置
**/
// Lombok提供了一系列注解来简化Java代码
@Data
@Builder
public class DataNodeConfig {
private String baseDir;
private String nameNodeServers;
private String dataNodeTransportServer;
private String dataNodeHttpServer;
private int heartbeatInterval;
private int dataNodeId;
private String fileLocatorType;
private int dataNodeWorkerThreads;
// 读取conf/datanode.properties文件
public static DataNodeConfig parse(Properties properties) {
// 基础目录,可以理解为根目录
String baseDir = (String) properties.get("base.dir");
// namenode服务器地址
String nameNodeServers = (String) properties.get("namenode.servers");
// datanode监听的上传文件的服务器地址
String dataNodeTransportServer = (String) properties.get("datanode.transport.server");
// datanode监听的Http请求服务端口
String dataNodeHttpServer = (String) properties.get("datanode.http.server");
// 心跳间隔时间
int heartbeatInterval = Integer.parseInt((String) properties.get("datanode.heartbeat.interval"));
// 每一个datanode的id
int dataNodeId = Integer.parseInt((String) properties.get("datanode.id"));
// 根据文件名定位到本地磁盘目录文件位置的方式
String fileLocatorType = (String) properties.get("file.locator.type");
// 上传或下载文件的工作线程数
int dataNodeWorkerThreads = Integer.parseInt((String) properties.get("datanode.worker.threads"));
return DataNodeConfig.builder()
.baseDir(baseDir)
.nameNodeServers(nameNodeServers)
.dataNodeTransportServer(dataNodeTransportServer)
.dataNodeHttpServer(dataNodeHttpServer)
.heartbeatInterval(heartbeatInterval)
.dataNodeId(dataNodeId)
.fileLocatorType(fileLocatorType)
.dataNodeWorkerThreads(dataNodeWorkerThreads)
.build();
}
public int getNameNodePort() {
return Integer.parseInt(nameNodeServers.split(":")[1]);
}
public int getDataNodeHttpPort() {
return Integer.parseInt(dataNodeHttpServer.split(":")[1]);
}
public String getNameNodeAddr() {
return nameNodeServers.split(":")[0];
}
public int getDataNodeTransportPort() {
return Integer.parseInt(dataNodeTransportServer.split(":")[1]);
}
public String getDataNodeTransportAddr() {
return dataNodeTransportServer.split(":")[0];
}
}
Lombok相关介绍:blog.csdn.net/death05/art…
2、namenode
主要负责与Namenode之间的通讯
import com.google.common.collect.Lists;
import com.google.protobuf.InvalidProtocolBufferException;
import com.ruyuan.dfs.common.FileInfo;
import com.ruyuan.dfs.common.NettyPacket;
import com.ruyuan.dfs.common.enums.PacketType;
import com.ruyuan.dfs.common.ha.BackupNodeManager;
import com.ruyuan.dfs.common.network.NetClient;
import com.ruyuan.dfs.common.network.RequestWrapper;
import com.ruyuan.dfs.common.utils.DefaultScheduler;
import com.ruyuan.dfs.datanode.config.DataNodeConfig;
import com.ruyuan.dfs.datanode.replica.PeerDataNodes;
import com.ruyuan.dfs.datanode.replica.ReplicateManager;
import com.ruyuan.dfs.datanode.server.StorageInfo;
import com.ruyuan.dfs.datanode.server.StorageManager;
import com.ruyuan.dfs.model.backup.BackupNodeInfo;
import com.ruyuan.dfs.model.datanode.*;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.concurrent.ScheduledFuture;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 负责和NameNode通讯的组件
*
* @author Sun Dasheng
*/
@Slf4j
public class NameNodeClient {
private final ReplicateManager replicateManager;
private final DefaultScheduler defaultScheduler;
private final BackupNodeManager backupNodeManager;
private final StorageManager storageManager;
private NetClient netClient;
private final DataNodeConfig datanodeConfig;
private ScheduledFuture<?> scheduledFuture;
public NameNodeClient(StorageManager storageManager, DataNodeConfig datanodeConfig, DefaultScheduler defaultScheduler,
PeerDataNodes peerDataNodes) {
this.netClient = new NetClient("DataNode-NameNode-" + datanodeConfig.getNameNodeAddr(), defaultScheduler);
this.datanodeConfig = datanodeConfig;
this.storageManager = storageManager;
this.defaultScheduler = defaultScheduler;
this.backupNodeManager = new BackupNodeManager(defaultScheduler);
this.replicateManager = new ReplicateManager(defaultScheduler, peerDataNodes, storageManager, this);
peerDataNodes.setNameNodeClient(this);
}
/**
* 启动NameNode连接的客户端
*/
public void start() {
this.netClient.addNettyPackageListener(this::onReceiveMessage);//为客户端添加网络包监听器
this.netClient.addConnectListener(connected -> {//为客户端添加连接状态监听器
if (connected) {
fetchBackupInfo();//连接请求
register();//注册节点
} else {
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
scheduledFuture = null;
}
}
});
this.netClient.addNetClientFailListener(() -> {//添加连接失败标记
log.info("DataNode检测到NameNode挂了,标记NameNode已宕机");
backupNodeManager.markNameNodeDown();//标记宕机节点
});
this.netClient.connect(datanodeConfig.getNameNodeAddr(), datanodeConfig.getNameNodePort());//连接
}
/**
* DatNode节点注册
*/
private void register() throws InterruptedException {
StorageInfo storageInfo = storageManager.getStorageInfo();//报告存储信息
RegisterRequest request = RegisterRequest.newBuilder()
.setHostname(datanodeConfig.getDataNodeTransportAddr())//设置主机名称
.setNioPort(datanodeConfig.getDataNodeTransportPort())//设置NIO线程端口
.setHttpPort(datanodeConfig.getDataNodeHttpPort())//设置http请求端口
.setStoredDataSize(storageInfo.getStorageSize())//设置存储数据大小
.setFreeSpace(storageInfo.getFreeSpace())//可用空间
.setNodeId(datanodeConfig.getDataNodeId())//datanode的id
.build();
NettyPacket nettyPacket = NettyPacket.buildPacket(request.toByteArray(),//创建请求
PacketType.DATA_NODE_REGISTER);
log.info("DataNode发起注册请求: {}", request.getHostname());
netClient.send(nettyPacket);//发送注册请求
}
//接收信息
private void onReceiveMessage(RequestWrapper requestWrapper) throws Exception {
PacketType packetType = PacketType.getEnum(requestWrapper.getRequest().getPacketType());//请求包的类型
switch (packetType) {
case FETCH_BACKUP_NODE_INFO://客户端或者DataName往NameNode上发起请求获取备份信息
handleFetchBackupNodeInfoResponse(requestWrapper);
break;
case DATA_NODE_REGISTER://DataNode往NameNode注册请求
handleDataNodeRegisterResponse(requestWrapper);
break;
case HEART_BRET://DataNode往NameNode发送的心跳请求
handleDataNodeHeartbeatResponse(requestWrapper);
break;
default:
break;
}
}
//DataNode往NameNode注册请求
private void handleDataNodeHeartbeatResponse(RequestWrapper requestWrapper) throws Exception {
if (requestWrapper.getRequest().isError()) {
log.warn("心跳失败, 重新发起注册请求:[error={}]", requestWrapper.getRequest().getError());
register();
return;
}
HeartbeatResponse heartbeatResponse = HeartbeatResponse.parseFrom(requestWrapper.getRequest().getBody());//回复心跳
handleCommand(heartbeatResponse);
}
//DataNode往NameNode注册请求
private void handleDataNodeRegisterResponse(RequestWrapper requestWrapper) {
ChannelHandlerContext ctx = requestWrapper.getCtx();
if (scheduledFuture == null) {
log.info("开启定时任务发送心跳,心跳间隔为: [interval={}ms]", datanodeConfig.getHeartbeatInterval());//心跳间隔
scheduledFuture = ctx.executor().scheduleAtFixedRate(new HeartbeatTask(ctx, datanodeConfig),//心跳定时任务
0, datanodeConfig.getHeartbeatInterval(), TimeUnit.MILLISECONDS);//单位毫秒
}
if (requestWrapper.getRequest().isSuccess()) {//请求封装器
StorageInfo storageInfo = storageManager.getStorageInfo();
log.info("注册成功,发送请求到NameNode进行全量上报存储信息。[size={}]", storageInfo.getFiles().size());
reportStorageInfo(ctx, storageInfo);
} else {
log.info("DataNode重启,不需要全量上报存储信息。");
}
}
//回复心跳
private void handleCommand(HeartbeatResponse response) {
if (response.getCommandsList().isEmpty()) {
return;
}
List<ReplicaCommand> commands = response.getCommandsList();//获取列表指令
replicateManager.addReplicateTask(commands);//创建副本
}
//客户端或者DataName往NameNode上发起请求获取备份信息
private void handleFetchBackupNodeInfoResponse(RequestWrapper requestWrapper) throws InvalidProtocolBufferException {
if (requestWrapper.getRequest().getBody().length == 0) {
log.warn("拉取BackupNode信息为空,设置NetClient为无限重试.");
netClient.setRetryTime(-1);//超时设置
return;
}
netClient.setRetryTime(3);
BackupNodeInfo backupNodeInfo = BackupNodeInfo.parseFrom(requestWrapper.getRequest().getBody());//备份信息
backupNodeManager.maybeEstablishConnect(backupNodeInfo, hostname -> {//和BackupNode简历连接
datanodeConfig.setNameNodeServers(hostname + ":" + datanodeConfig.getNameNodePort());//配置服务器
netClient.shutdown();//释放连接
netClient = new NetClient("DataNode-NameNode-" + datanodeConfig.getNameNodeAddr(), defaultScheduler);//负责和namenode通信
log.info("检测到BackupNode升级为NameNode了,替换NameNode链接信息,并重新建立链接:[hostname={}, port={}]",
datanodeConfig.getNameNodeAddr(), datanodeConfig.getNameNodePort());//获取namenode地址
start();
});
}
private void fetchBackupInfo() throws InterruptedException {
//创建请求
NettyPacket nettyPacket = NettyPacket.buildPacket(new byte[0], PacketType.FETCH_BACKUP_NODE_INFO);
//发送请求
netClient.send(nettyPacket);
}
/**
* 上报文件副本信息
*
* @param fileName 文件名称
* @param fileSize 文件大小
*/
public void informReplicaReceived(String fileName, long fileSize) throws InterruptedException {
InformReplicaReceivedRequest replicaReceivedRequest = InformReplicaReceivedRequest.newBuilder()//接受备份请求
.setFilename(fileName)//设置文件名称
.setHostname(datanodeConfig.getDataNodeTransportAddr())//设置服务器名称为datenode上传文件地址
.setFileSize(fileSize)//设置文件大小
.build();
NettyPacket nettyPacket = NettyPacket.buildPacket(replicaReceivedRequest.toByteArray(), PacketType.REPLICA_RECEIVE);//创建请求
netClient.send(nettyPacket);
}
/**
* 上报文件副本信息
*
* @param fileName 文件名称
* @param fileSize 文件大小
*/
public void informReplicaRemoved(String fileName, long fileSize) throws InterruptedException {
InformReplicaReceivedRequest replicaReceivedRequest = InformReplicaReceivedRequest.newBuilder()
.setFilename(fileName)
.setHostname(datanodeConfig.getDataNodeTransportAddr())
.setFileSize(fileSize)
.build();
NettyPacket nettyPacket = NettyPacket.buildPacket(replicaReceivedRequest.toByteArray(), PacketType.REPLICA_REMOVE);
netClient.send(nettyPacket);
}
//存储信息
private void reportStorageInfo(ChannelHandlerContext ctx, StorageInfo storageInfo) {
List<FileInfo> files = storageInfo.getFiles();
// 每次最多上传100个文件信息
if (files.isEmpty()) {
ReportCompleteStorageInfoRequest request = ReportCompleteStorageInfoRequest.newBuilder()
.setHostname(datanodeConfig.getDataNodeTransportAddr())
.setFinished(true)//标记已完成
.build();
NettyPacket nettyPacket = NettyPacket.buildPacket(request.toByteArray(), PacketType.REPORT_STORAGE_INFO);//新建包
ctx.writeAndFlush(nettyPacket);//写队列并刷新
} else {
List<List<FileInfo>> partition = Lists.partition(files, 100);
for (int i = 0; i < partition.size(); i++) {
List<FileInfo> fileInfos = partition.get(i);
List<FileMetaInfo> fileMetaInfos = fileInfos.stream()
.map(e -> FileMetaInfo.newBuilder()
.setFilename(e.getFileName())
.setFileSize(e.getFileSize())
.build())
.collect(Collectors.toList());
boolean isFinish = i == partition.size() - 1;
ReportCompleteStorageInfoRequest.Builder builder = ReportCompleteStorageInfoRequest.newBuilder()
.setHostname(datanodeConfig.getDataNodeTransportAddr())
.addAllFileInfos(fileMetaInfos)
.setFinished(isFinish);
ReportCompleteStorageInfoRequest request = builder.build();
NettyPacket nettyPacket = NettyPacket.buildPacket(request.toByteArray(), PacketType.REPORT_STORAGE_INFO);
ctx.writeAndFlush(nettyPacket);
}
}
}
/**
* 停止服务
*/
public void shutdown() {
if (netClient != null) {
netClient.shutdown();
}
}
}
3、 replica
PeerDataNodes.java
虚线是重写方法
源代码解读:
package com.ruyuan.dfs.datanode.replica;
import com.ruyuan.dfs.common.NettyPacket;
import com.ruyuan.dfs.common.enums.PacketType;
import com.ruyuan.dfs.common.network.NetClient;
import com.ruyuan.dfs.common.utils.DefaultScheduler;
import com.ruyuan.dfs.datanode.config.DataNodeConfig;
import com.ruyuan.dfs.datanode.namenode.NameNodeClient;
import com.ruyuan.dfs.datanode.server.DataNodeApis;
import com.ruyuan.dfs.model.common.GetFileRequest;
import com.ruyuan.dfs.model.datanode.PeerNodeAwareRequest;
import io.netty.channel.socket.SocketChannel;
import lombok.extern.slf4j.Slf4j;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* DataNode相互感知的组件
*
* <pre>
* 在需要复制副本的时候,假设datanode01需要从datanode02复制副本
*
* 则会从datanode01发起一个和datanode02的网络连接: datanode01 -> datanode02
*
* 1. datanode01向datanode02发起连接,并保存这个链接
* 2. datanode02收到下发文件的请求,同时将datanod01的连接保存在 dataNodeChannelMap 中
* 3. 当datanode02需要从datanode01复制副本的时候,复用之前建立的链接
*
* </pre>
*
* @author Sun Dasheng
*/
@Slf4j
/*slf4j的全称是Simple Loging Facade For Java,
即它仅仅是一个为Java程序提供日志输出的统一接口,
并不是一个具体的日志实现方案,就比如JDBC一样,只是一种规则而已。*/
//PeerDataNode目标DataNode
public class PeerDataNodes {
private DataNodeApis dataNodeApis;
private DataNodeConfig dataNodeConfig;
private DefaultScheduler defaultScheduler;
private Map<String, PeerDataNode> dataNodeChannelMap = new ConcurrentHashMap<>();
public PeerDataNodes(DefaultScheduler defaultScheduler, DataNodeConfig dataNodeConfig, DataNodeApis dataNodeApis) {
this.defaultScheduler = defaultScheduler;
this.dataNodeConfig = dataNodeConfig;
this.dataNodeApis = dataNodeApis;
this.dataNodeApis.setPeerDataNodes(this);
}
//设置和NameNode通讯组件
public void setNameNodeClient(NameNodeClient nameNodeClient) {
this.dataNodeApis.setNameNodeClient(nameNodeClient);
}
/**
* <pre>
* 从目标DataNode获取文件
*
* 发送一个GET_FILE的网络包, 会被Peer DataNode收到, 从而把文件发送过来
*
* 这里有两种可能:
* 1. datanode01和datanode02没有建立连接:
*
* 1.1 如果当前实例是datanode01, 会主动发起一个连接:datanode01 -> datanode02
* 1.2 如果当前实例是datanode02, 会主动发起一个连接:datanode02 -> datanode01
*
* 2. 如果datanode02和datanode02已经建立连接,则会从连接池中获取到对应的连接,发送一个GET_FILE请求,从而获取文件
*
* </pre>
*
* @param hostname 主机名
* @param port 端口号
* @param filename 文件名
*/
public void getFileFromPeerDataNode(String hostname, int port, String filename) throws InterruptedException {
GetFileRequest request = GetFileRequest.newBuilder()
.setFilename(filename)
.build();//获取文件请求
NettyPacket nettyPacket = NettyPacket.buildPacket(request.toByteArray(), PacketType.GET_FILE);
PeerDataNode peerDataNode = maybeConnectPeerDataNode(hostname, port);//检查是否有连接,没有就新建,有就找到
peerDataNode.send(nettyPacket);//往PeerDataNode发送网络包
log.info("PeerDataNode发送GET_FILE请求,请求下载文件:[hostname={}, port={}, filename={}]", hostname, port, filename);
}
private PeerDataNode maybeConnectPeerDataNode(String hostname, int port) throws InterruptedException {
synchronized (this) {
String peerDataNode = hostname + ":" + port;
PeerDataNode peer = dataNodeChannelMap.get(peerDataNode);
if (peer == null) {//新建连接
NetClient netClient = new NetClient("DataNode-PeerNode-" + hostname, defaultScheduler);
netClient.addHandlers(Collections.singletonList(dataNodeApis));
netClient.addConnectListener(connected -> sendPeerNodeAwareRequest(netClient));//监听
netClient.connect(hostname, port);//启动连接
netClient.ensureConnected();//同步等待确保连接建立,堵塞了就等会儿,直到重新连接建立
peer = new PeerDataNodeClient(netClient, dataNodeConfig.getDataNodeId());
dataNodeChannelMap.put(peerDataNode, peer);
log.info("新建PeerDataNode的链接:{}", peerDataNode);
} else {
log.info("从保存的连接中获取PeerDataNode的链接:{}", peerDataNode);
}
return peer;//返回目标datanode
}
}
private void sendPeerNodeAwareRequest(NetClient netClient) throws InterruptedException {
PeerNodeAwareRequest request = PeerNodeAwareRequest.newBuilder()
.setPeerDataNode(dataNodeConfig.getDataNodeTransportServer())
.setDataNodeId(dataNodeConfig.getDataNodeId())
.build();//其他PeerDataNode
NettyPacket nettyPacket = NettyPacket.buildPacket(request.toByteArray(),
PacketType.DATA_NODE_PEER_AWARE);
log.info("连接上其他PeerDataNode了, 发送一个通知网络包:{}", request.getPeerDataNode());
netClient.send(nettyPacket);//发送网络包
}
/**
* DataNodeServer识别出是DataNode Peer后将连接保存起来
*
* <pre>
*
* 这里可能出现一种情况, 两个DataNode节点同时发起连接
* datanode01 -> datanode02
* datanode02 -> datanode01
*
* 所以每个DataNode定义一个ID,遇到这种情况的时候,DataNode ID更小的断开连接
*
* 考虑这样的场景:
* datanode01 : id = 1
* datanode02 : id = 2
*
* 两个DataNode同时发起连接:
*
* 1. datanode01往datanode02发起连接,带上自己的id:
* datanode01的连接集合: [datanodeId=2]
*
* 2. datanode02往datanode01发起连接,带上自己的id:
* datanode02的连接集合: [datanodeId=1]
*
* 3. datanode02收到datanode01的链接,发现新连接的dataNodeId=1, 维护的连接集合中已存在该连接:
* 3.1 如果自身的id比新的连接要大,则关闭新的连接
* 3.2 如果自身的id比新的连接要小,则关闭原本自身作为客户端的链接
* 结果是保留链接 : [datanode02 -> datanode01]
*
* 4. datanode01收到datanode02的链接,发现新连接的dataNodeId=2, 维护的连接集合中已存在该连接:
* 4.1 如果自身的id比新的连接要大,则关闭新的连接
* 4.2 如果自身的id比新的连接要小,则关闭原本自身作为客户端的链接
* 结果是保留链接 : [datanode02 -> datanode01]
*
* </pre>
*
* @param peerDataNode Peer Node
* @param channel 连接
* @param dataNodeId DataNode ID
*/
public void addPeerNode(String peerDataNode, int dataNodeId, SocketChannel channel, int selfDataNodeId) {
synchronized (this) {
PeerDataNode oldPeer = dataNodeChannelMap.get(peerDataNode);
PeerDataNode newPeer = new PeerDataNodeServer(channel, dataNodeId);
if (oldPeer == null) {
log.info("收到PeerDataNode的通知网络包, 保存连接以便下一次使用");
dataNodeChannelMap.put(peerDataNode, newPeer);
} else {
if (oldPeer instanceof PeerDataNodeServer && newPeer.getDataNodeId() == oldPeer.getDataNodeId()) {
// 此种情况为断线重连, 需要更新channel
PeerDataNodeServer peerDataNodeServer = (PeerDataNodeServer) oldPeer;
peerDataNodeServer.setSocketChannel(channel);
log.info("PeerDataNode断线重连,更新channel: {}", oldPeer.getDataNodeId());
} else {
// 此种情况为本身作为客户端以及发起连接,但是作为服务端同样收到了链接请求
// 首先判断是不是旧的连接已经不可用,如果已经不可用的话,直接替换新的
if (!oldPeer.isConnected()) {//旧的不能用了
oldPeer.close();//旧的关闭
dataNodeChannelMap.put(peerDataNode, newPeer);
log.info("旧的连接已经失效,则关闭旧的连接, 并替换链接: {}", oldPeer.getDataNodeId());
return;
}
if (selfDataNodeId > dataNodeId) {//新ID小于自己的
newPeer.close();//新的关闭
log.info("新的连接dataNodeId比较小,关闭新的连接: {}", newPeer.getDataNodeId());
} else {
oldPeer.close();//新ID大于等于自己的 就把旧的关了
dataNodeChannelMap.put(peerDataNode, newPeer);
log.info("新的连接dataNodeId比较大,则关闭旧的连接, 并替换链接: {}", oldPeer.getDataNodeId());
}
}
}
}
}
/**
* 优雅停止
*/
public void shutdown() {
log.info("Shutdown PeerDataNodes");
for (PeerDataNode peerDataNode : dataNodeChannelMap.values()) {
peerDataNode.close();
}
}
/**
* 表示一个DataNode节点的连接
*/
private interface PeerDataNode {//接口
/**
* 往 Peer DataNode 发送网络包, 如果连接断开了,会同步等待连接重新建立
*
* @param nettyPacket 网络包
* @throws InterruptedException 中断异常
*/
void send(NettyPacket nettyPacket) throws InterruptedException;//发送网络包
/**
* 关闭连接
*/
void close();
/**
* 获取DataNodeId
*
* @return DataNode ID
*/
int getDataNodeId();
/**
* 是否连接上
*
* @return 是否连接上
*/
boolean isConnected();
}
private static abstract class AbstractPeerDataNode implements PeerDataNode {
private int dataNodeId;
public AbstractPeerDataNode(int dataNodeId) {
this.dataNodeId = dataNodeId;
}
@Override
public int getDataNodeId() {//获取DataNode的ID
return dataNodeId;
}
}
/**
* 表示和PeerDataNode的连接,当前DataNode作为客户端
*/
private static class PeerDataNodeClient extends AbstractPeerDataNode {
private NetClient netClient;
public PeerDataNodeClient(NetClient netClient, int dataNodeId) {
super(dataNodeId);
this.netClient = netClient;
}
@Override
public void send(NettyPacket nettyPacket) throws InterruptedException {
netClient.send(nettyPacket);//发送网络包
}
@Override
public void close() {
netClient.shutdown();//关闭连接
}
@Override
public boolean isConnected() {
return netClient.isConnected();//检查是否连接上
}
}
/**
* 表示和PeerDataNode的连接,当前DataNode作为服务端
*/
private static class PeerDataNodeServer extends AbstractPeerDataNode {
private volatile SocketChannel socketChannel;//套连接字通道
public PeerDataNodeServer(SocketChannel socketChannel, int dataNodeId) {
super(dataNodeId);
this.socketChannel = socketChannel;
}
public void setSocketChannel(SocketChannel socketChannel) {//设置socketchannel
synchronized (this) {//加锁
this.socketChannel = socketChannel;
notifyAll();//释放锁,通知所有等待这个对象控制权的线程继续运行
}
}
@Override
public void send(NettyPacket nettyPacket) throws InterruptedException {
synchronized (this) {//加锁
// 如果这里断开连接了,会一直等待直到客户端会重新建立连接
while (!socketChannel.isActive()) {//当阻塞时
try {
wait(10);//等待
} catch (InterruptedException e) {//捕获异常
log.error("PeerDataNodeServer#send has Interrupted !!");
}
}
}
socketChannel.writeAndFlush(nettyPacket);//向客户端返回nettyPacket
}
@Override
public void close() {
socketChannel.close();//关闭连接
}
@Override
public boolean isConnected() {//检查是否连接上
return socketChannel != null && socketChannel.isActive();
}
}
}
RelicateManager.java
功能:复制管理组件
\
private class ReplicateWorker implements Runnable {
@Override
public void run() {
try {
ReplicaCommand command = replicaCommandQueue.poll();
if (command == null) {
return;
}
if (CommandType.REPLICA_COPY.getValue() == command.getCommand()) {
log.info("收到副本复制任务:[hostname={}, filename={}]", command.getHostname(), command.getFilename());
peerDataNodes.getFileFromPeerDataNode(command.getHostname(), command.getPort(), command.getFilename());
}
else if (CommandType.REPLICA_REMOVE.getValue() == command.getCommand()) {
//存储管理器中文件路径
String absolutePathByFileName = storageManager.getAbsolutePathByFileName(command.getFilename());
//在定义好的路径创建文件
File file = new File(absolutePathByFileName);
long length = 0;
if (file.exists()) {//file存在,得到文件长度
length = file.length();
}
FileUtil.delete(absolutePathByFileName);//删除文件
if (length > 0) {
nameNodeClient.informReplicaRemoved(command.getFilename(), length);//上报文件副本信息
}
log.info("收到副本删除任务:[filename={}, path={}]", command.getFilename(), absolutePathByFileName);
}
} catch (Exception e) {//捕获异常
log.info("ReplicateWorker处理副本任务异常:", e);
}
}
}
4、server
server分为http, locate文件夹和一系列服务提供文件,其目录结构如下:
server
│ DataNodeApis.java
│ DataNodeServer.java
│ DefaultFileTransportCallback.java
│ MultiPortChannelInitializer.java
│ StorageInfo.java
│ StorageManager.java
│
├───http
│ HttpFileServerHandler.java
│
└───locate
AbstractFileLocator.java
AesFileLocator.java
FileLocator.java
FileLocatorFactory.java
Md5FileLocator.java
Sha1FileLocator.java
SimpleFileLocator.java
1) http文件夹
这个文件夹下的HttpFileServerHandler 负责实现DataNode使用http协议与其他节点通信,主要是用于传输文件,它的构造函数需要一个StorageManager作为参数,继承了SimpleChannelInBoundHandler
HttpFileServerHandler 由重写的channelRead0()方法负责具体实现,其中会判断http请求下载文件是否合法,若否则由sendError()方法构造对应的HttpResponse返回错误信息
2) locate文件夹
这个文件夹中的类实现了根据文件的绝对路径查找文件位置(因为文件名经过加密所以与原来的不一样),接口,抽象类,与实现文件的关系如下,文件名分别可以用aes, sha1, md5加密,或者不加密,四个Locator实现不不同的加密方法
3) 其他server需要提供的服务
DataNodeApis
DataNodeApis继承了AbstractChannelHandler抽象类,重写了interestPackageTypes()和handlePackage()方法,可以根据不同NettyPackage的类型选择不同的处理方式
DataNodeServer
DataNodeServer类是DataNode提供服务的实体,它有私有的DataNodeApis、StorageManager、NetServer、DataNodeConfig、PeerDataNodes。start()和shutdown方法则是调用其私有属性的对应start()和shutdown(),代码如下
public class DataNodeServer {
private DataNodeApis dataNodeApis;
private StorageManager storageManager;
private NetServer netServer;
private DataNodeConfig dataNodeConfig;
private PeerDataNodes peerDataNodes;
public DataNodeServer(DataNodeConfig dataNodeConfig, DefaultScheduler defaultScheduler, StorageManager storageManager,
PeerDataNodes peerDataNodes, DataNodeApis dataNodeApis) {
this.dataNodeConfig = dataNodeConfig;
this.peerDataNodes = peerDataNodes;
this.storageManager = storageManager;
this.dataNodeApis = dataNodeApis;
this.netServer = new NetServer("DataNode-Server", defaultScheduler, dataNodeConfig.getDataNodeWorkerThreads());
}
/**
* 启动
*/
public void start() throws InterruptedException {
// 用于接收PeerDataNode发过来的通知信息
MultiPortChannelInitializer multiPortChannelInitializer = new MultiPortChannelInitializer(dataNodeConfig, storageManager);
multiPortChannelInitializer.addHandlers(Collections.singletonList(dataNodeApis));
this.netServer.setChannelInitializer(multiPortChannelInitializer);
this.netServer.bind(Arrays.asList(dataNodeConfig.getDataNodeTransportPort(), dataNodeConfig.getDataNodeHttpPort()));
}
/**
* 优雅停止
*/
public void shutdown() {
log.info("Shutdown DataNodeServer");
this.netServer.shutdown(); // 关闭网络服务器
this.peerDataNodes.shutdown(); // 关闭所有通信的DataNode
}
}
DefaultFileTransportCallback
DefaultFileTransportCallback类负责检测文件传输,可以获得文件路径,在文件传输过程中,或是传输完成时输出传输速率,记录收到的文件,它是接口FileTransportCallback的实现类
MultiPortChannelInitializer
MultiPortChannelInitializer继承自BaseChannelInitializer,用于建立连接时添加初始化channel,除了构造函数外,它只重写了initChannel()方法
@Override
protected void initChannel(SocketChannel ch) {
int localPort = ch.localAddress().getPort();
if (localPort == dataNodeConfig.getDataNodeTransportPort()) {
super.initChannel(ch);
} else if (localPort == dataNodeConfig.getDataNodeHttpPort()) {
ch.pipeline().addLast(new HttpServerCodec()); // 用于编码和解码
ch.pipeline().addLast(new HttpObjectAggregator(65536)); // 会将httpMessage 和 HttpContent聚合到一个FullHttpRequest中或者FullHttpResponse中。
/** ChunkedWriteHandler顾名思义:分块写入处理器,分批读取数据。比如我们通过ChunkedWriteHandler传递File文件,原理是
* - 1.将File分割成ChunkedFile,指定ChunkedSize和file的start offset以及 end offset
* - 2.每次最多生成chunkedSize大小的message,每次分配堆内存bytebuf,然后将message发送到bytebuf并且填满
* - 3.然后只要channel一直可写就一直发送
*/
ch.pipeline().addLast(new ChunkedWriteHandler());
ch.pipeline().addLast(new HttpFileServerHandler(storageManager));
}
}
StorageInfo
StorageInfo是一个记录存储信息的类,用于保存存储数据
@Data
public class StorageInfo {
// 文件信息
private List<FileInfo> files;
// 已用空间
private long storageSize;
// 可用空间
private long freeSpace;
public StorageInfo() {
this.storageSize = 0L;
}
}
StorageManager
StorageManager类用于管理一个DataNode上存储的文件
5、DataNode.java
功能:DataNode启动类
\
main函数源代码:
public static void main(String[] args) {
if (args == null || args.length == 0) {
throw new IllegalArgumentException("配置文件不能为空");
}
// 初始化配置文件
DataNodeConfig dataNodeConfig = null;
try {
Path path = Paths.get(args[0]);
try (InputStream inputStream = Files.newInputStream(path)) {
Properties properties = new Properties();
properties.load(inputStream);
dataNodeConfig = DataNodeConfig.parse(properties);
}
log.info("DataNode启动配置文件: {}", path.toAbsolutePath());
} catch (Exception e) {
log.error("无法加载配置文件 : ", e);
System.exit(1);
}
// 读取args中的自定义配置(比如basedir),并修改默认配置
parseOption(args, dataNodeConfig);
try {
DataNode datanode = new DataNode(dataNodeConfig);
// shutdown hook是一个已初始化但尚未启动的线程,当JVM开始关闭时,便会启动datanode的shutdown线程
Runtime.getRuntime().addShutdownHook(new Thread(datanode::shutdown));
datanode.start();
} catch (Exception e) {
log.error("启动DataNode失败:", e);
System.exit(1);
}
}
\
\