ES-Fastloader

82 阅读4分钟

github.com/didi/ES-Fas…

ES-Fastloader工作流程

1. 扫描Hive表数据,计算路由

  • MapReduce作业读取Hive表数据
  • 根据分片数和扩展因子(EXPANFACTOR)计算数据路由
  • 将数据分配到不同的Reducer任务

2. 生成Lucene文件

  • 每个Reducer启动一个嵌入式ES节点
  • 通过Bulk API将数据写入这个本地ES节点
  • ES自动在本地生成Lucene索引文件
  • 这些文件存储在ariusFastIndex/data目录下

3. 复制Lucene文件到HDFS

  • MapReduce作业完成后,将生成的Lucene文件复制到HDFS
  • 记录这些文件的路径,供后续加载使用

4. 通过插件加载Lucene文件到ES

  • Server组件为每个ES分片创建加载任务
  • 这些任务将HDFS上的Lucene文件复制到目标ES节点
  • 通过Plugin插件的API触发加载操作
  • Plugin使用indexWriter.addIndexes(indexes)将Lucene文件直接加载到目标分片

核心优势

  • 绕过了ES的标准索引API,直接在Lucene层面操作
  • 利用MapReduce的并行处理能力,加速索引构建
  • 通过直接加载Lucene文件,大大提高了数据导入效率
  • 处理主键冲突,确保数据一致性

这种方法比传统的文档索引方式快几个数量级,特别适合大规模数据导入场景。整个过程是高度并行的,多个分片可以同时加载数据,进一步提高效率。


ES-Fastloader工作流程举例

场景

假设我们有一个Hive表order_table,包含以下字段:

  • order_id:订单ID(主键)
  • customer_name:客户名称
  • product_id:产品ID
  • amount:订单金额
  • order_time:订单时间

我们想将这个表的数据导入到Elasticsearch索引orders_20230101中。

1. 启动MapReduce作业

hadoop jar ES-Fastloader.jar com.didichuxing.datachannel.arius.fastindex.FastIndex \
  --template orders \
  --time 20230101 \
  --hiveDB sales \
  --hiveTable order_table \
  --key order_id \
  --server http://es-server:8080

2. Mapper阶段(FastIndexMapper)

假设表中有这些数据:

order_id=1001, customer_name="张三", product_id=101, amount=100, order_time="2023-01-01 10:00:00"
order_id=1002, customer_name="李四", product_id=102, amount=200, order_time="2023-01-01 11:00:00"
order_id=1003, customer_name="王五", product_id=103, amount=300, order_time="2023-01-01 12:00:00"

Mapper会读取这些记录,并根据order_id计算分片号:

String keyStr = getKeyValue(keyList, hCatRecord);  // 提取"1001"
shardNo = CommonUtils.getShardId(keyStr, templateConfig.getReducerNum());  // 计算分片号,如0
context.write(new IntWritable(0), hCatRecord);  // 写入Reducer 0

/* 基于es murmur3hash计算docId所属分片 */
public static int getShardId(String docId, int shardsNum) {
    int hash = murmur3hash(docId);
    return mod(hash, shardsNum);
}

假设我们有2个Reducer,计算结果可能是:

  • order_id=1001 → Reducer 0
  • order_id=1002 → Reducer 1
  • order_id=1003 → Reducer 0

3. Reducer阶段(FastIndexReducer)

Reducer 0处理:

  1. 启动嵌入式ES节点:

    esNode = new ESNode(taskConfig, indexInfo);
    esNode.init(context);
    
  2. 创建索引并写入数据:

    // 对于order_id=1001的记录
    JSONObject jsonObject = transformer.tranform(record.getAll());
    // jsonObject = {"order_id":"1001","customer_name":"张三","product_id":101,"amount":100,"order_time":"2023-01-01 10:00:00"}
    esWriter.bulk("1001", jsonObject);
    
    // 对于order_id=1003的记录
    jsonObject = {"order_id":"1003","customer_name":"王五","product_id":103,"amount":300,"order_time":"2023-01-01 12:00:00"}
    esWriter.bulk("1003", jsonObject);
    
  3. 生成Lucene文件:

    • ES将数据写入内存并生成Lucene索引文件
    • 文件存储在ariusFastIndex/data目录
  4. 复制Lucene文件到HDFS:

    • ariusFastIndex/data目录下的文件复制到HDFS路径,如/user/hadoop/es-fastloader/orders_20230101/reducer0/

Reducer 1执行类似操作,处理order_id=1002的记录,并将Lucene文件复制到/user/hadoop/es-fastloader/orders_20230101/reducer1/

4. 服务端调度(FastIndexService)

  1. 创建加载任务:

    // 为每个ES分片创建任务
    FastIndexLoadDataPo fastIndexLoadDataPo = new FastIndexLoadDataPo();
    fastIndexLoadDataPo.setIndexName("orders_20230101");
    fastIndexLoadDataPo.setShardNum(0);  // 分片0
    fastIndexLoadDataPo.setRedcueIds("0,2");  // 使用Reducer 0和2的输出
    fastIndexLoadDataPo.setHdfsSrcDir("/user/hadoop/es-fastloader/orders_20230101");
    // ...设置其他参数
    fastIndexLoadDataPos.add(fastIndexLoadDataPo);
    
  2. 执行加载任务:

    • 调度器定期执行任务
    • 将HDFS上的Lucene文件复制到目标ES节点
    • 通过HTTP调用Plugin API:
      curl "es-node1:9200/lucene/append?indexName=orders_20230101&uuid=abc123&shardId=0&append=/tmp/lucene/reducer0,/tmp/lucene/reducer2&primeKey=order_id"
      

5. 插件处理(AppendLuceneTransportAction)

  1. 处理主键冲突:

    • 假设ES中已有order_id=1001的文档
    • 插件检测到冲突,删除现有文档:
      dstIndexWriter.deleteDocuments(srcTerm);  // 删除order_id=1001的文档
      
  2. 加载Lucene文件:

    Directory[] indexes = new Directory[appendDirs.size()];
    for (int i = 0; i < appendDirs.size(); i++) {
        indexes[i] = FSDirectory.open(Paths.get(appendDirs.get(i)));  // 打开/tmp/lucene/reducer0目录
    }
    indexWriter.addIndexes(indexes);  // 将Lucene文件添加到分片
    indexWriter.commit();  // 提交更改
    

6. 完成索引设置

  1. 更新索引设置:

    indexOpDao.updateSetting("orders_20230101", "routing.rebalance.enable", "all");
    indexOpDao.updateSetting("orders_20230101", "number_of_replicas", "1");
    
  2. 标记任务完成:

    po.setFinish(true);
    po.setFinishTime(System.currentTimeMillis());
    

结果

最终,Hive表中的数据被成功导入到Elasticsearch索引orders_20230101中,整个过程通过MapReduce并行处理和直接操作Lucene文件,比传统的文档索引方式快几个数量级。