《HDFS源码分析之DataXceiverServer》一文中,我们了解到在DataNode中,有一个后台工作的线程DataXceiverServer。它被用于接收来自客户端或其他数据节点的数据读写请求,为每个数据读写请求创建一个单独的线程去处理。而处理每次读写请求时所创建的线程,就是本文要讲的DataXceiver。本文,我们来看下DataXceiver的具体实现,着重讲解下它得到数据读写请求后的整体处理流程。

首先,我们先看下DataXceiver的成员变量,具体如下:

  1. // 封装了Socket、输入流、输出流的Peer,是DataXceiver线程工作的主要依托者
  2. private Peer peer;
  3. // 通讯两端地址:远端地址remoteAddress、本地端地址localAddress,均是从peer(即socket)中获得的
  4. private final String remoteAddress; // address of remote side
  5. private final String localAddress;  // local address of this daemon
  6. // DataNode节点进程实例datanode
  7. private final DataNode datanode;
  8. // DataNode节点配置信息dnConf
  9. private final DNConf dnConf;
  10. // DataXceiverServer线程实例dataXceiverServer
  11. private final DataXceiverServer dataXceiverServer;
  12. // 连接DataNode是否使用主机名,取参数dfs.datanode.use.datanode.hostname,参数未配置的话默认为false,不使用
  13. private final boolean connectToDnViaHostname;
  14. // 接收到一个操作op的开始时间
  15. private long opStartTime; //the start time of receiving an Op
  16. // InputStream输入流socketIn
  17. private final InputStream socketIn;
  18. // OutputStream输出流socketOut
  19. private OutputStream socketOut;
  20. // 数据块接收器BlockReceiver对象blockReceiver
  21. private BlockReceiver blockReceiver = null;
  22. /**
  23. * Client Name used in previous operation. Not available on first request
  24. * on the socket.
  25. * previousOpClientName为之前操作的客户端名字,它对于socket上的第一个请求不可用
  26. */
  27. private String previousOpClientName;

既然DataXceiver是为处理数据读写请求而创建的线程,那么Socket、输入流、输出流就是必不可少的成员。而首当其冲的Peer,便封装了Socket、输入流、输出流的Peer,是DataXceiver线程工作的主要依托者,而接下来的输入流socketIn、输出流socketOut都是来自peer的socket。另外,DataXceiver还提供了通讯两端地址:远端地址remoteAddress、本地端地址localAddress,均是从peer(即socket)中获得的。

既然是由DataNode上的DataXceiverServer线程创建的,那么自然少不了datanode、dataXceiverServer、dnConf等变量,并且,它是专门用来处理数据读写请求的,自然也需要像数据块接收器BlockReceiver对象blockReceiver这种成员变量。dnConf是DNConf类型的数据节点DataNode上的配置信息。

剩下的几个,便是在处理具体的数据读写请求时用到的connectToDnViaHostname、opStartTime、previousOpClientName等变量。其中,connectToDnViaHostname标识连接DataNode是否使用主机名,取参数dfs.datanode.use.datanode.hostname,参数未配置的话默认为false,不使用,opStartTime为接收到一个操作op的开始时间,最后的previousOpClientName为之前操作的客户端名字,它对于socket上的第一个请求不可用。

下面我们再看下它的构造方法,只有一个private的,如下:

  1. /**
  2. * 私有构造函数,需要Peer、DataNode、DataXceiverServer三个参数
  3. */
  4. private DataXceiver(Peer peer, DataNode datanode,
  5. DataXceiverServer dataXceiverServer) throws IOException {
  6. / peer、datanode、dataXceiverServer等成员变量直接赋值
  7. this.peer = peer;
  8. this.dnConf = datanode.getDnConf();
  9. // 输入流socketIn、输出流socketOut来自peer的socket
  10. this.socketIn = peer.getInputStream();
  11. this.socketOut = peer.getOutputStream();
  12. this.datanode = datanode;
  13. this.dataXceiverServer = dataXceiverServer;
  14. // connectToDnViaHostname取自数据节点配置信息dnConf
  15. this.connectToDnViaHostname = datanode.getDnConf().connectToDnViaHostname;
  16. // 远端remoteAddress和本地localAddress地址取自Peer
  17. remoteAddress = peer.getRemoteAddressString();
  18. localAddress = peer.getLocalAddressString();
  19. if (LOG.isDebugEnabled()) {
  20. LOG.debug("Number of active connections is: "
  21. + datanode.getXceiverCount());
  22. }
  23. }

但是,它提供了一个类的静态create()方法,用于DataXceiver对象的构造,代码如下:

  1. /**
  2. * 提供了一个静态方法create(),调用私有构造函数构造DataXceiver对象
  3. */
  4. public static DataXceiver create(Peer peer, DataNode dn,
  5. DataXceiverServer dataXceiverServer) throws IOException {
  6. return new DataXceiver(peer, dn, dataXceiverServer);
  7. }

上述构造方法及静态create()方法都很简单,不再赘述。

接下来,我们再着重分析下,DataXceiver线程在启动后,是如何处理来自客户端或者其他数据节点发送的数据读写请求的。既然是线程,那么就不得不看看它的run()方法,代码如下:

  1. /**
  2. * Read/write data from/to the DataXceiverServer.
  3. * 从DataXceiverServer中读取或者往DataXceiverServer中写入数据
  4. */
  5. @Override
  6. public void run() {
  7. int opsProcessed = 0;
  8. Op op = null;
  9. try {
  10. // 在dataXceiverServer中增加peer与该DataXceiver实例所在线程和DataXceiver实例的映射关系
  11. dataXceiverServer.addPeer(peer, Thread.currentThread(), this);
  12. // peer中设置socket写入超时时间
  13. peer.setWriteTimeout(datanode.getDnConf().socketWriteTimeout);
  14. InputStream input = socketIn;
  15. try {
  16. // IOStreamPair为一个输入输出流对,既包含输入流,也包含输出流
  17. IOStreamPair saslStreams = datanode.saslServer.receive(peer, socketOut,
  18. socketIn, datanode.getXferAddress().getPort(),
  19. datanode.getDatanodeId());
  20. // 包装saslStreams的输入流in为BufferedInputStream,得到输入流input,其缓冲区大小取参数io.file.buffer.size的一半,
  21. // 参数未配置的话默认为512,且最大也不能超过512
  22. input = new BufferedInputStream(saslStreams.in,
  23. HdfsConstants.SMALL_BUFFER_SIZE);
  24. // 从saslStreams中获取输出流socketOut
  25. socketOut = saslStreams.out;
  26. } catch (InvalidMagicNumberException imne) {
  27. LOG.info("Failed to read expected encryption handshake from client " +
  28. "at " + peer.getRemoteAddressString() + ". Perhaps the client " +
  29. "is running an older version of Hadoop which does not support " +
  30. "encryption");
  31. return;
  32. }
  33. // 调用父类initialize()方法,完成初始化,实际上就是设置父类的输入流in
  34. super.initialize(new DataInputStream(input));
  35. // We process requests in a loop, and stay around for a short timeout.
  36. // This optimistic behaviour allows the other end to reuse connections.
  37. // Setting keepalive timeout to 0 disable this behavior.
  38. // 在一个do...while循环内完成请求的处理。
  39. do {
  40. // 更新当前线程名称,通过线程名标识进度的一种手段,不错
  41. updateCurrentThreadName("Waiting for operation #" + (opsProcessed + 1));
  42. try {
  43. // 由于第一次是创建一个新的socket使用,连接的时间可能会很长,所以连接超时时间设置的比较大,
  44. // 而后续使用的话,是复用socket,连接的超时时间限制就没必要设置那么大了
  45. if (opsProcessed != 0) {
  46. // 如果不是第一次出来请求,确保dnConf的socketKeepaliveTimeout大于0,
  47. // 将其设置为设置peer(即socket)的读超时时间,
  48. // 取参数dfs.datanode.socket.reuse.keepalive,参数为配置的话,默认为4s
  49. assert dnConf.socketKeepaliveTimeout > 0;
  50. peer.setReadTimeout(dnConf.socketKeepaliveTimeout);
  51. } else {
  52. // 最开始第一次处理请求时,设置peer(即socket)读超时时间为dnConf的socketTimeout
  53. // 即取参数dfs.client.socket-timeout,参数未配置的话默认为60s
  54. peer.setReadTimeout(dnConf.socketTimeout);
  55. }
  56. // 通过readOp()方法读取操作符op
  57. op = readOp();
  58. } catch (InterruptedIOException ignored) {
  59. // Time out while we wait for client rpc
  60. // 如果是InterruptedIOException异常,跳出循环
  61. break;
  62. } catch (IOException err) {
  63. // Since we optimistically expect the next op, it's quite normal to get EOF here.
  64. if (opsProcessed > 0 &&
  65. (err instanceof EOFException || err instanceof ClosedChannelException)) {
  66. if (LOG.isDebugEnabled()) {
  67. LOG.debug("Cached " + peer + " closing after " + opsProcessed + " ops");
  68. }
  69. } else {
  70. throw err;
  71. }
  72. break;
  73. }
  74. // restore normal timeout
  75. // 重新存储正常的超时时间,即dnConf的socketTimeout
  76. if (opsProcessed != 0) {
  77. peer.setReadTimeout(dnConf.socketTimeout);
  78. }
  79. // 设置操作的起始时间opStartTime
  80. opStartTime = now();
  81. // 通过processOp()方法根据操作符op调用相应的方法处理操作符op
  82. processOp(op);
  83. // 累加操作数
  84. ++opsProcessed;
  85. } while ((peer != null) &&
  86. (!peer.isClosed() && dnConf.socketKeepaliveTimeout > 0));
  87. // 循环的条件便是:peer未关闭且复用超时时间socketKeepaliveTimeout大于0
  88. } catch (Throwable t) {
  89. String s = datanode.getDisplayName() + ":DataXceiver error processing "
  90. + ((op == null) ? "unknown" : op.name()) + " operation "
  91. + " src: " + remoteAddress + " dst: " + localAddress;
  92. if (op == Op.WRITE_BLOCK && t instanceof ReplicaAlreadyExistsException) {
  93. // For WRITE_BLOCK, it is okay if the replica already exists since
  94. // client and replication may write the same block to the same datanode
  95. // at the same time.
  96. if (LOG.isTraceEnabled()) {
  97. LOG.trace(s, t);
  98. } else {
  99. LOG.info(s + "; " + t);
  100. }
  101. } else {
  102. LOG.error(s, t);
  103. }
  104. } finally {
  105. if (LOG.isDebugEnabled()) {
  106. LOG.debug(datanode.getDisplayName() + ":Number of active connections is: "
  107. + datanode.getXceiverCount());
  108. }
  109. // 更新当前线程名称
  110. updateCurrentThreadName("Cleaning up");
  111. // 关闭peer(socket)、输入流等资源
  112. if (peer != null) {
  113. dataXceiverServer.closePeer(peer);
  114. IOUtils.closeStream(in);
  115. }
  116. }
  117. }

run()方法的处理流程逻辑十分清晰,概括如下:

1、在dataXceiverServer中增加peer与该DataXceiver实例所在线程和DataXceiver实例的映射关系;

2、peer中设置socket写入超时时间,取参数dfs.datanode.socket.write.timeout,参数未配置的话默认为8分钟;

3、获取IOStreamPair类型的saslStreams,其为一个输入输出流对,既包含输入流,也包含输出流;

4、包装saslStreams的输入流in为BufferedInputStream,得到输入流input,其缓冲区大小取参数io.file.buffer.size的一半,参数未配置的话默认为512,且最大也不能超过512;

5、从saslStreams中获取输出流socketOut;

6、调用父类initialize()方法,完成初始化,实际上就是设置父类的输入流in;

7、在一个do...while循环内完成请求的处理,循环的条件便是--peer未关闭且复用超时时间socketKeepaliveTimeout大于0:

7.1、更新当前线程名称,通过线程名标识进度的一种手段,不错,线程名此时为Waiting for operation #100(100为操作处理次数累加器的下一个值);

7.2、处理读超时时间设置:由于第一次是创建一个新的socket使用,连接的时间可能会很长,所以连接超时时间设置的比较大,而后续使用的话,是复用socket,连接的超时时间限制就没必要设置那么大了。所以,最开始第一次处理请求时,设置peer(即socket)读超时时间为dnConf的socketTimeout,即取参数dfs.client.socket-timeout,参数未配置的话默认为60s;如果不是第一次出来请求,确保dnConf的socketKeepaliveTimeout大于0,将其设置为设置peer(即socket)的读超时时间,取参数dfs.datanode.socket.reuse.keepalive,参数为配置的话,默认为4s;

7.3、通过readOp()方法读取操作符op;

7.4、重新存储正常的超时时间,即dnConf的socketTimeout;

7.5、设置操作的起始时间opStartTime,为当前时间;

7.6、通过processOp()方法根据操作符op调用相应的方法处理操作符op;

7.7、累加操作数opsProcessed;

8、更新当前线程名称:Cleaning up;

9、关闭peer(socket)、输入流等资源。

实际上,对于读写请求的处理的一个主线,便是在socket未关闭的情况下,不停的读取操作符,然后调用相应的方法处理,也就是do...while循环内的op = readOp()-----processOp(op)这一处理主线。

下面,我们来看下读取操作符的readOp()方法,它位于DataXceiver的父类Receiver中。代码如下:

  1. /** Read an Op.  It also checks protocol version. */
  2. protected final Op readOp() throws IOException {
  3. // 首先从输入流in中读入版本号version,short类型,占2个字节
  4. final short version = in.readShort();
  5. // 校验版本号version是否与DataTransferProtocol中的DATA_TRANSFER_VERSION相等,该版本中为28
  6. if (version != DataTransferProtocol.DATA_TRANSFER_VERSION) {
  7. throw new IOException( "Version Mismatch (Expected: " +
  8. DataTransferProtocol.DATA_TRANSFER_VERSION  +
  9. ", Received: " +  version + " )");
  10. }
  11. // 调用Op的read()方法,从输入流in中获取操作符op
  12. return Op.read(in);
  13. }

代码中有详细注释,不再解释。继续追踪Op的read()方法,代码如下:

  1. private static final int FIRST_CODE = values()[0].code;
  2. /** Return the object represented by the code. */
  3. private static Op valueOf(byte code) {
  4. final int i = (code & 0xff) - FIRST_CODE;
  5. return i < 0 || i >= values().length? null: values()[i];
  6. }
  7. /** Read from in */
  8. public static Op read(DataInput in) throws IOException {
  9. return valueOf(in.readByte());
  10. }

很简单,通过read()方法从输入流读取byte,并通过valueOf()方法,首先将byte转化为int,然后减去Op操作符枚举类型的第一个值:WRITE_BLOCK,即80,得到i。如果i小于0或者大于枚举中操作符的个数,说明输入流中传入的操作符不在枚举范围内,否则利用i作为索引取出相应的操作符。枚举类型如下:

  1. WRITE_BLOCK((byte)80),
  2. READ_BLOCK((byte)81),
  3. READ_METADATA((byte)82),
  4. REPLACE_BLOCK((byte)83),
  5. COPY_BLOCK((byte)84),
  6. BLOCK_CHECKSUM((byte)85),
  7. TRANSFER_BLOCK((byte)86),
  8. REQUEST_SHORT_CIRCUIT_FDS((byte)87),
  9. RELEASE_SHORT_CIRCUIT_FDS((byte)88),
  10. REQUEST_SHORT_CIRCUIT_SHM((byte)89);

比较简单,写数据块为80,读数据块为81等,不再一一介绍。操作符为int类型,也就意味着它占4个字节。

接下来,我们再看下处理操作符的processOp()方法,同样在DataXceiver的父类Receiver中。代码如下:

  1. /** Process op by the corresponding method. */
  2. protected final void processOp(Op op) throws IOException {
  3. // 通过调用相应的方法处理操作符
  4. switch(op) {
  5. case READ_BLOCK:// 读数据块调用opReadBlock()方法
  6. opReadBlock();
  7. break;
  8. case WRITE_BLOCK:// 写数据块调用opWriteBlock()方法
  9. opWriteBlock(in);
  10. break;
  11. case REPLACE_BLOCK:// 替换数据块调用opReplaceBlock()方法
  12. opReplaceBlock(in);
  13. break;
  14. case COPY_BLOCK:// 复制数据块调用REPLACE()方法
  15. opCopyBlock(in);
  16. break;
  17. case BLOCK_CHECKSUM:// 数据块检验调用opBlockChecksum()方法
  18. opBlockChecksum(in);
  19. break;
  20. case TRANSFER_BLOCK:// 移动数据块调用opTransferBlock()方法
  21. opTransferBlock(in);
  22. break;
  23. case REQUEST_SHORT_CIRCUIT_FDS:
  24. opRequestShortCircuitFds(in);
  25. break;
  26. case RELEASE_SHORT_CIRCUIT_FDS:
  27. opReleaseShortCircuitFds(in);
  28. break;
  29. case REQUEST_SHORT_CIRCUIT_SHM:
  30. opRequestShortCircuitShm(in);
  31. break;
  32. default:
  33. throw new IOException("Unknown op " + op + " in data stream");
  34. }
  35. }

一目了然,根据操作符的不同,调用不同的方法去处理。比如读数据块调用opReadBlock()方法,写数据块调用opWriteBlock()方法,替换数据块调用opReplaceBlock()方法等等,读者可自行阅读。
        至此,HDFS源码分析DataXceiver之整体流程全部叙述完毕。后续文章会陆续推出对于写数据块、读数数据块、替换数据块、移动数据块等的详细操作,以及DataXceiver线程中用到的数据块发送器BlockSender、数据块接收器BlockReceiver的详细分析,敬请期待!

HDFS源码分析DataXceiver之整体流程的更多相关文章

  1. (转)linux内存源码分析 - 内存回收(整体流程)

    http://www.cnblogs.com/tolimit/p/5435068.html------------linux内存源码分析 - 内存回收(整体流程) 概述 当linux系统内存压力就大时 ...

  2. linux内存源码分析 - 内存回收(整体流程)

    本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 概述 当linux系统内存压力就大时,就会对系统的每个压力大的zone进程内存回收,内存回收主要是针对匿名页和文 ...

  3. HDFS源码分析心跳汇报之BPServiceActor工作线程运行流程

    在<HDFS源码分析心跳汇报之数据结构初始化>一文中,我们了解到HDFS心跳相关的BlockPoolManager.BPOfferService.BPServiceActor三者之间的关系 ...

  4. HDFS源码分析数据块校验之DataBlockScanner

    DataBlockScanner是运行在数据节点DataNode上的一个后台线程.它为所有的块池管理块扫描.针对每个块池,一个BlockPoolSliceScanner对象将会被创建,其运行在一个单独 ...

  5. HDFS源码分析EditLog之获取编辑日志输入流

    在<HDFS源码分析之EditLogTailer>一文中,我们详细了解了编辑日志跟踪器EditLogTailer的实现,介绍了其内部编辑日志追踪线程EditLogTailerThread的 ...

  6. HDFS源码分析EditLog之读取操作符

    在<HDFS源码分析EditLog之获取编辑日志输入流>一文中,我们详细了解了如何获取编辑日志输入流EditLogInputStream.在我们得到编辑日志输入流后,是不是就该从输入流中获 ...

  7. HDFS源码分析心跳汇报之数据块汇报

    在<HDFS源码分析心跳汇报之数据块增量汇报>一文中,我们详细介绍了数据块增量汇报的内容,了解到它是时间间隔更长的正常数据块汇报周期内一个smaller的数据块汇报,它负责将DataNod ...

  8. HDFS源码分析心跳汇报之数据块增量汇报

    在<HDFS源码分析心跳汇报之BPServiceActor工作线程运行流程>一文中,我们详细了解了数据节点DataNode周期性发送心跳给名字节点NameNode的BPServiceAct ...

  9. Solr4.8.0源码分析(5)之查询流程分析总述

    Solr4.8.0源码分析(5)之查询流程分析总述 前面已经写到,solr查询是通过http发送命令,solr servlet接受并进行处理.所以solr的查询流程从SolrDispatchsFilt ...

随机推荐

  1. html执行.NET函数 html操作数据库 html与ashx结合

    原文发布时间为:2009-09-30 -- 来源于本人的百度文章 [由搬家工具导入] html页面执行.NET函数 html与ashx的结合 1、添加一般应用程序Handler.ashx <%@ ...

  2. 调用Outlook发送邮件

    #region 查找与指定文件关联在一起的程序的文件名 /// <summary> /// 查找与指定文件关联在一起的程序的文件名 /// </summary> /// < ...

  3. js 数组知识复习

    2.Array类型 2.1 创建数组 两种方式: 1.new Array(); //创建一个空数组 var arr1 = new Array(); //创建一个长度为10的空数组, var arr2 ...

  4. maven更换阿里云仓库

    本来不想写,网上到处都是,不过好多到我这不行,自己记录下,省的到处找 D:\apache-maven-3.6.1\conf目录下setting.xml文件(这是我的解压的位置) <mirrors ...

  5. jenkins 中 violation使用pylint

    在jenkins中无法打开源码问题: 1. 在 Report Violations的 Source encoding 设置为 项目文件的编码, 如: utf-8.  缺省是 default. 2. 在 ...

  6. LeetCode OJ-- Distinct Subsequences ** 递推

    https://oj.leetcode.com/problems/distinct-subsequences/ 对于string S 和 T求,T 是 S的几种子串. 首先想到了递归方法,列出递归公式 ...

  7. HDU 1024 Max Sum Plus Plus [动态规划+m子段和的最大值]

    Max Sum Plus Plus Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) Tot ...

  8. Codeforces Gym 101471D Money for Nothing(2017 ACM-ICPC World Finals D题,决策单调性)

    题目链接  2017 ACM-ICPC World Finals Problem D (这题细节真的很多) 把所有的(pi,di)按横坐标升序排序. 对于某个点,若存在一个点在他左下角,那么这个点就是 ...

  9. Linux内核网络栈源代码分析

    http://blog.csdn.net/column/details/linux-kernel-net.html

  10. Qt编程简介与基本知识

    1. 什么是Qt? Qt是一个基于C++的跨平台应用程序和UI开发框架.它包含一个类库,和用于跨平台开发及国际化的工具. 由挪威Trolltech公司开发,后被Nokia收购,目前被Digia公司收购 ...