平滑迁移 HDFS 到 JuiceFS

大数据平台在不同存储系统之间进行数据迁移通常都是一个大工程,要怎么做到尽量不影响上层业务是一个问题。借助 JuiceFS 特有的符号链接功能,可以实现将 HDFS 中的数据平滑地迁移到 JuiceFS,并且对业务透明。

下面将会分别介绍两种方案,这两种方案本质上都是基于 JuiceFS 的符号链接来实现,只是一种需要修改 Hive metadata 中的 LOCATION,另一种不需要。可以根据实际情况选择其中一种方案。

方案一:修改 LOCATION

这个方案需要修改 Hive 库、表或者分区的 LOCATION 信息,将 hdfs://{ns} 改为 jfs://{name}。迁移的时候可以做到表级别迁移。

准备阶段

在准备阶段,主要包含部署 JuiceFS 基础环境,在 JuiceFS 的根路径下建立对应 HDFS 的符号链接,以及迁移回收站目录到 JuiceFS 上。

注意此阶段所有命令都需要使用 HDFS 的 superuser 来运行,一般集群默认为 hdfs 用户。在此阶段,数据读写仍然是在 HDFS 上,所以不会出现任何的数据不一致问题。

  1. 部署 JuiceFS,详见 「自动安装 JuiceFS Java 客户端」。注意这一步可以暂时不用重启相关服务,后面会统一操作。

  2. 增加新的 JuiceFS Java 客户端配置:

    1. <property>
    2. <name>juicefs.migrating</name>
    3. <value>true</value>
    4. </property>
  3. 重启相关服务,具体需要重启哪些服务可以参考「使用步骤」文档。

  4. 初始化 JuiceFS:

    1. java -cp `hadoop classpath`:${HIVE_HOME}/lib/* juicefs-hadoop.jar com.juicefs.Main hive \
    2. --src-fs hdfs://{ns} \
    3. --dst-fs jfs://{name} \
    4. --init

    此操作会在 JuiceFS 的根路径 “/“ 下面创建到 HDFS 根路径 “/“ 的所有符号链接,如下图:

    image

    这些符号链接可以通过挂载 JuiceFS 以后看到。

  5. 初始化 Trash 目录:

    1. java -cp `hadoop classpath`:${HIVE_HOME}/lib/* juicefs-hadoop.jar com.juicefs.Main hive \
    2. --src-fs hdfs://{ns} \
    3. --dst-fs jfs://{name} \
    4. --init-trash

    由于 Hive 或者 Spark 等任务运行的时候,会将部分删除的数据 rename 到 /user/{username}/.Trash 目录下。由于不支持跨文件系统 rename,所以需要先将 Trash 目录迁移过来。

准备阶段完成以后可以查看 juicefs.access-log 配置的文件(默认为 /tmp/juicefs.access.log),看是否包含 cmd: Lookup (/.MIGRATING,1) 来验证配置是否成功。

数据迁移阶段

当准备阶段完成后,就可以开始数据迁移了。

在此阶段,将会包含几个步骤:修改 Hive 表或者分区的 LOCATION 信息、同步目录结构以及数据迁移。

在修改 LOCATION 步骤,数据读写仍然在 HDFS 上。在同步目录结构步骤完成以后,新增数据将会写入 JuiceFS。在数据迁移步骤,历史数据也将拷贝到 JuiceFS 上来。

  1. 修改 Hive 表的 LOCATION 信息:

    1. java -cp `hadoop classpath`:${HIVE_HOME}/lib/* juicefs-hadoop.jar com.juicefs.Main hive \
    2. --src-fs hdfs://{ns} \
    3. --dst-fs jfs://{name} \
    4. --hive-meta thrift://{host:port} \
    5. --tables {db_name}[:{table_name} [, {table_name}] ...]

    此命令会将 Hive 表的 LOCATION 改为 JuiceFS 的地址,即将 --src-fs 替换为 --dst-fs 设置的值。对于分区表,还会同步修改所有分区的 LOCATION。由于准备阶段已经在 JuiceFS 的根路径下面建立了指向 HDFS 的符号链接,因此这个时候 Hive 仍然是通过 HDFS 访问数据,只是地址改为了 JuiceFS 的地址。上面命令中的 --tables 选项支持几种格式:

    • 只指定数据库名称:--tables {db_name}。这会修改对应数据库中的所有表。
    • 指定一张表:--table {db_name}:{table_name}。这会修改对应数据库中的某一张表。
    • 指定多张表:--table {db_name}:{table_name},{table_name}。通过逗号分隔表名,可以修改对应数据库中的多张表。
    • 你也可以重复输入多个 --tables 选项,来实现同样的效果。
  2. 同步目录结构:

    1. java -cp `hadoop classpath`:${HIVE_HOME}/lib/* juicefs-hadoop.jar com.juicefs.Main hive \
    2. --src-fs hdfs://{ns} \
    3. --dst-fs jfs://{name} \
    4. --hive-meta thrift://{host:port} \
    5. --tables {db_name}[:{table_name} [, {table_name}] ...] \
    6. --link

    相比上一步的命令行多了 --link 选项,此选项会将 Hive 表 LOCATION 下面的所有目录结构从 HDFS 迁移过来,目录内的文件则以符号链接的方式链接到 HDFS 上。因为这一步仅涉及元数据操作,没有数据拷贝,因此可以以极快的速度将历史数据的目录结构从 HDFS 迁移到 JuiceFS 上。

    这个过程需要尽量快,当部分目录在 HDFS,而另外一部分在 JuiceFS 时,跨越文件系统的 rename 操作会失败。JuiceFS SDK 会感知到当前的迁移,从而等待最多 10 分钟(可以调整 juicefs.migrate.wait 配置,默认 10,单位 min),需要保证可能有跨文件系统 rename 操作的子树要在 10 分钟内完成。

    当目录结构已经存在于 JuiceFS 上后,后续新写入的数据将直接写在 JuiceFS 上(如下图所示)。

    image

    红框圈出的文件即为新增数据,直接存储于 JuiceFS 上。历史数据依旧以符号链接的方式指向 HDFS 中的地址。

  3. 迁移数据:

    1. java -cp `hadoop classpath`:${HIVE_HOME}/lib/* juicefs-hadoop.jar com.juicefs.Main hive \
    2. --src-fs hdfs://{ns} \
    3. --dst-fs jfs://{name} \
    4. --hive-meta thrift://{host:port} \
    5. --tables {db_name}[:{table_name} [, {table_name}] ...] \
    6. --copy

    当指定 --copy 选项后,历史数据将会从 HDFS 逐步迁移到 JuiceFS 上,替换掉符号链接。默认会启动 10 个线程并发拷贝,你也可以通过 --threads 选项调整。或者在多个机器分别迁移不同的子目录来实现并行,加速数据拷贝。

    迁移数据完成后的示例如下图:

    image

    此目录下面的所有数据均已经拷贝到 JuiceFS。可以使用下面的命令确认此目录是否已经迁移完成:

    1. find /path/to/table -type l

    如果没有任何内容(即不存在符号链接),则此目录已经迁移完成,后续针对于此目录的所有操作均是在 JuiceFS 上面进行。

反向迁移

在数据迁移过程中也可以通过反向迁移随时回滚,来撤销迁移操作。不同阶段会对应不同的回滚操作,请确保你完全理解回滚可能产生的影响。

  1. 在「准备阶段」回滚

    在准备阶段由于所有的读写仍然在 HDFS 上,所以只需要修改 Hive 表或者分区的 LOCATION 即可,然后滚动重启 Hive。

    1. java -cp `hadoop classpath`:${HIVE_HOME}/lib/* juicefs-hadoop.jar com.juicefs.Main hive \
    2. --src-fs hdfs://{ns} \
    3. --dst-fs jfs://{name} \
    4. --hive-meta thrift://{host:port} \
    5. --tables {db_name}[:{table_name} [, {table_name}] ...] \
    6. --reverse
  2. 在「数据迁移阶段」回滚

    1. java -cp `hadoop classpath`:${HIVE_HOME}/lib/* juicefs-hadoop.jar com.juicefs.Main hive \
    2. --src-fs hdfs://{ns} \
    3. --dst-fs jfs://{name} \
    4. --hive-meta thrift://{host:port} \
    5. --tables {db_name}[:{table_name} [, {table_name}] ...] \
    6. --reverse

    命令和准备阶段一样,不过此阶段可能已经有新增数据写入到 JuiceFS 里面了,所以需要将新增数据拷贝回 HDFS。

特别注意:为了避免并发导致的数据不一致,因此需要完全停止服务(如 Hive)才能进行此步骤。

方案二:不修改 LOCATION

这个方案不需要修改 Hive 库、表或者分区的 LOCATION 信息,只需要通过 core-site.xml 修改原有 HDFS 的实现,因此改动更少。

准备阶段

在准备阶段,主要包含部署 JuiceFS 基础环境,在 JuiceFS 的根路径下建立对应 HDFS 的符号链接,迁移回收站目录到 JuiceFS 上,以及修改 core-site.xml

注意此阶段所有命令都需要使用 HDFS 的 superuser 来运行,一般集群默认为 hdfs 用户。在此阶段,数据读写仍然是在 HDFS 上,所以不会出现任何的数据不一致问题。

  1. 部署 JuiceFS,详见 「自动安装 JuiceFS Java 客户端」。注意这一步可以暂时不用重启相关服务,后面会统一操作。

  2. 增加新的 JuiceFS Java 客户端配置:

    1. <property>
    2. <name>juicefs.migrating</name>
    3. <value>true</value>
    4. </property>
  3. 初始化 JuiceFS:

    1. java -cp $(hadoop classpath) com.juicefs.tools.Mover \
    2. --src-fs hdfs://{ns} \
    3. --dst-fs jfs://{name} \
    4. --init

    此操作会在 JuiceFS 的根路径 “/” 下面创建到 HDFS 根路径 “/” 的所有符号链接,如下图:

    image

    这些符号链接可以通过挂载 JuiceFS 以后看到。

  4. 初始化 Trash 目录:

    1. java -cp $(hadoop classpath) com.juicefs.tools.Mover \
    2. --src-fs hdfs://{ns} \
    3. --dst-fs jfs://{name} \
    4. --init-trash

    由于 Hive 或者 Spark 等任务运行的时候,会将部分删除的数据 rename 到 /user/{username}/.Trash 目录下。由于不支持跨文件系统 rename,所以需要先将 Trash 目录迁移过来。

  5. 修改 core-site.xml,增加以下配置:

    1. <property>
    2. <name>fs.hdfs.impl</name>
    3. <value>com.juicefs.MigratingFileSystem</value>
    4. </property>
    5. <property>
    6. <name>juicefs.migrate.src-fs</name>
    7. <value>hdfs://{ns}/</value>
    8. </property>
    9. <property>
    10. <name>juicefs.migrate.dst-fs</name>
    11. <value>jfs://{name}/</value>
    12. </property>
    13. <property>
    14. <name>juicefs.caller-context</name>
    15. <value>jfs</value>
    16. </property>

    此步骤可以在任意一台机器上执行,如果使用 Ambari 也可以通过机器分组来使得配置只在部分机器上面生效。

    juicefs.caller-context 是一个可选配置,用来在 HDFS audit log 中标识数据是通过 JuiceFS 访问的,在 audit log 里面会显示类似 callerContext=*_jfs 这样的字段。这个功能需要 Hadoop 2.8.0 及以上版本(HDFS-9184),并且开启 hadoop.caller.context.enabled 配置。

  6. 重启

    只需要重启修改过配置的机器上面的服务,常见的使用 JuiceFS 的服务有:YARN、HBase、Hive、Spark、Flume。建议直接重启机器上面的所有服务。

Hive 或者 Spark 应用使用的是任务提交节点的配置,所以一旦此节点被重启,配置生效后,后续通过该节点提交的所有任务都将使用 JuiceFS 的实现去访问 HDFS。

在所有需要访问 HDFS 的节点重复执行前面的步骤,并重启使得配置生效。

准备阶段完成以后可以查看 juicefs.access-log 配置的文件(默认为 /tmp/juicefs.access.log),看是否包含 cmd: Lookup (/.MIGRATING,1) 来验证配置是否成功。

数据迁移阶段

当准备阶段完成后,就可以开始数据迁移了。

在此阶段,将会包含两个步骤:同步目录结构以及数据迁移。

  1. 同步目录结构:

    1. java -cp $(hadoop classpath) com.juicefs.tools.Mover \
    2. --src-fs hdfs://{ns} \
    3. --dst-fs jfs://{name} \
    4. --dir /path/to/table

    以上命令会将历史数据用符号链接的方式导入到 JuiceFS,最终效果如下(以迁移 /tpcds-generate/10/store_sales 目录为例):

    image

    此操作将顶层的符号链接逐级替换为真正的目录和指向文件的符号链接。

    这个过程需要尽量快,当部分目录在 HDFS,而另外一部分在 JuiceFS 时,跨越文件系统的 rename 操作会失败。JuiceFS SDK 会感知到当前的迁移,从而等待最多 10 分钟(可以调整 juicefs.migrate.wait 配置,默认 10,单位 min),需要保证可能有跨文件系统 rename 操作的子树要在 10 分钟内完成。

    当目录结构已经存在于 JuiceFS 上后,后续新写入的数据将直接写在 JuiceFS 上(如下图所示)。

    image

    红框圈出的文件即为新增数据,直接存储于 JuiceFS 上。历史数据依旧以符号链接的方式指向 HDFS 中的地址。

  2. 迁移数据:

    1. java -cp $(hadoop classpath) com.juicefs.tools.Mover \
    2. --src-fs hdfs://{ns} \
    3. --dst-fs jfs://{name} \
    4. --dir /path/to/table \
    5. --copy

    当指定 --copy 选项后,历史数据将会从 HDFS 逐步迁移到 JuiceFS 上,替换掉符号链接。默认会启动 10 个线程并发拷贝,你也可以通过 --threads 选项调整。或者在多个机器分别迁移不同的子目录来实现并行,加速数据拷贝。

    迁移数据完成后的示例如下图:

    image

    此目录下面的所有数据均已经拷贝到 JuiceFS。可以使用下面的命令确认此目录是否已经迁移完成:

    1. find /path/to/table -type l

    如果没有任何内容(即不存在符号链接),则此目录已经迁移完成,后续针对于此目录的所有操作均是在 JuiceFS 上面进行。

反向迁移

在数据迁移过程中也可以通过反向迁移随时回滚,来撤销迁移操作。不同阶段会对应不同的回滚操作,请确保你完全理解回滚可能产生的影响。

  1. 在「准备阶段」回滚

    在准备阶段由于所有的读写仍然在 HDFS 上,所以只需要在 core-site.xml 里面将 fs.hdfs.impl 配置项删除即可,然后分批滚动重启机器。

  2. 在「数据迁移阶段」回滚

    1. java -cp $(hadoop classpath) com.juicefs.tools.Mover \
    2. --src-fs hdfs://{ns} \
    3. --dst-fs jfs://{name} \
    4. --dir /path/to/table \
    5. --reverse

    此阶段可能已经有新增数据写入到 JuiceFS 里面了,所以需要将新增数据拷贝回 HDFS。

特别注意:为了避免并发导致的数据不一致,因此需要完全停止服务(如 Hive)才能进行此步骤。