HDFS的架构演变

HDFS(Hadoop Distributed File System)的架构演变其实就是Hadoop的更新迭代的过程,目前Hadoop有Hadoop1、Hadoop2、Hadoop3三个版本,对应的就有HDFS1,HDFS2,HDFS3。

HDFS1

HDFS1是一个主从架构,主节点只有一个叫NameNode,从节点可以由多个叫DataNode。

NameNode的职责:

  • 管理元数据信息(文件目录树):文件与Block块,Block块与DataNode主机的关系
  • NameNode为了快速响应用户的操作请求,所以把元数据加载到了内存里面

DataNode的职责:

  • 存储数据,把上传的数据划分成为固定大小的文件块(HDFS1默认为64M)
  • 为了保证数据安全,每个文件块默认都有三个副本

hadoop 副本 如何利用 hdfs有多个副本所以name_hdfs

HDFS1的架构缺陷

  • 单点故障问题(最主要的缺陷)
    因为只有单NameNode,如果NameNode挂了,系统瘫痪。
  • 内存受限问题
    NameNode为了快速响应用户的请求,会把元数据存放在内存中,压迫其他进程。

HDFS2

为了解决第一个问题,HDFS2提出了HA高可用搭建方式:

hadoop 副本 如何利用 hdfs有多个副本所以name_数据_02

如果使用多NameNode的话,有三个问题需要解决:

  • 元数据如何同步
  • 如何判断NameNode的主备
  • 如何切换主备

为了解决第一个问题,需要搭建一个JournalNode集群,他会从主NameNode中获取元数据,再同步给备NameNode。

为了解决第二个问题,需要搭建一个ZooKeePer集群,他会促使NameNode向ZK的锁中写信息,先写进去的为主NameNode。

为了解决第三个问题,ZooKeePer集群的ZKFC进程会实时监听NameNode的存活情况,如果主NameNode死亡,那么备NameNode会成为主NameNode。

虽然有了一个JournalNode集群,但是元数据受内存的限制并没有完美解决,所有又有了另一种搭建模式:联邦搭建模式

联邦模式简单来说就是把NameNode的内存拿出来组合成一个跨越物理限制的内存集合。

简单来说就是NameNode拿到元数据存放在所有NameNode的内存中,而所有的NameNode共享所有的元数据和所有的DataNode(本质上已经没有主备之分,所有的NameNode都是主节点也都是备节点)。

hadoop 副本 如何利用 hdfs有多个副本所以name_数据_03

联邦模式算是解决了内存受限问题,同时也间接解决了NameNode宕机问题,而且NameNode也可以构建HA高可用,将两个模式集成起来(联邦模式适合一千个节点以上的集群)。

HDFS3

HDFS3比较HDFS2做出的升级是:

  • HA高可用多个NameNode
  • 纠删码技术

HDFS支撑亿级流量的核心源码设计

分段加锁和双缓冲方案

HDFS写元数据的时候,最终元数据会写到磁盘上的EditLog中,但是直接和磁盘进行交互的话会导致性能降低,无法应对高并发场景。所以提出了双缓冲方案:

hadoop 副本 如何利用 hdfs有多个副本所以name_数据_04

用户向CurrentBuffer写入数据到一定地步,触发阈值后SyncBuffer内存和CurrentBuffer进行交换,CurrentBuffer内存清空,SyncBuffer开始向磁盘写入日志。

package com.zhenghe.zhenghetohive;

import java.util.LinkedList;

public class FSEditLog {
    private long txid=0L;
    private final DoubleBuffer editLogBuffer= new DoubleBuffer();
    //是否正在刷写磁盘
    private volatile Boolean isSyncRunning = false;
    private volatile Boolean isWaitSync = false;

    private volatile Long syncMaxTxid = 0L;

    //每个线程都对应自己的一个副本
    private final ThreadLocal<Long> localTxid= new ThreadLocal<>();

    // main方法,模拟一个高并发请求,调用logEdit方法
    public static void main(String[] args) {
        final FSEditLog fsEditLog = new FSEditLog();
        for (int i = 0; i < 50; i++) {
            new Thread(()-> {
                for (int j = 0; j < 1000; j++) {
                    fsEditLog.logEdit("日志信息");
                }
            }
            ).start();
        }
    }

    // 写日志,main方法进来一个高并发请求
    public void logEdit(String content){
        synchronized (this){//加锁的目的就是为了事务ID的唯一,而且是递增
            // 创建一个日志编号
            txid ++;
            // 线程1的第一条日志的编号就是1
            localTxid.set(txid);
            // 创建一个日志对象
            EditLog log = new EditLog(txid, content);
            // 将数据写入当前内存 editLogBuffer是双缓冲对象
            editLogBuffer.write(log);
        }
        // 释放锁, 其他线程上锁,开始循环
        logSync();
    }

    private  void logSync(){
        // 加锁
        synchronized(this){
            // 判断 SyncRunning 是否在刷写磁盘
            if(isSyncRunning){
                // 如果现在正在刷写磁盘,先查看当前线程的日志的编号
                long txid = localTxid.get();
                // 如果当前日志编号小于正在刷写磁盘的最大日志编号的话,退出
                if(txid <= syncMaxTxid){
                    return;
                }
                // 判断是否在等待刷写磁盘  加入已经有线程在等待刷写,其他线程就不必在等待
                if(isWaitSync){
                    return;
                }
                // 修改isWaitSync 状态
                isWaitSync = true;
                // 判断是否有人在刷写磁盘
                while(isSyncRunning){
                    try{
                        // 释放锁
                        // 1)被唤醒
                        // 2)到时间
                        wait(2000);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
                isWaitSync = false;
            }
            // 没有刷写磁盘的话,直接交换内存  (内存里面数据达到阈值,交换内存)
            editLogBuffer.setReadyToSync();

            // 内存交换
            if(editLogBuffer.syncBuffer.size() > 0) {
                syncMaxTxid = editLogBuffer.getSyncMaxTxid();
                System.out.println(syncMaxTxid);
            }
            // 修改状态 释放锁
            isSyncRunning = true;

        }
        // 内存中的数据写入磁盘  写磁盘操作没有加锁  分段加锁
        editLogBuffer.flush();
        synchronized (this) {
            // 修改状态
            isSyncRunning = false;
            // 唤醒wait
            notify();
        }
    }


    /**
     * 使用了面向对象的思想,把一条日志看成一个对象
     */
    static class EditLog{
        //顺序递增
        long txid;
        //操作内容  mkdir /a
        String content;

        //构造函数
        public EditLog(long txid,String content){
            this.txid = txid;
            this.content = content;
        }

        //为了测试方便
        @Override
        public String toString() {
            return "EditLog{" +
                "txid=" + txid +
                ", content='" + content + '\'' +
                '}';
        }
    }


    /**
     * 双缓存方案
     */
    static class DoubleBuffer{
        //内存1
        LinkedList<EditLog> currentBuffer = new LinkedList<>();
        //内存2
        LinkedList<EditLog> syncBuffer= new LinkedList<>();

        // 向内存1中写入日志
        public void write(EditLog log){
            currentBuffer.add(log);
        }

        // 内存1和内存2交换
        public void setReadyToSync(){
            LinkedList<EditLog> tmp= currentBuffer;
            currentBuffer = syncBuffer;
            syncBuffer = tmp;
        }

        // 获取当前刷磁盘的内存里的ID最大值
        public Long getSyncMaxTxid(){
            return syncBuffer.getLast().txid;
        }


        // 刷写磁盘
        public void flush(){
            for(EditLog log:syncBuffer){
                //把数据写到磁盘上
                System.out.println("存入磁盘日志信息:"+log);
            }

            //把内存2里面的数据要清空
            syncBuffer.clear();
        }
    }
}