大数据-228 离线数仓 Flume Taildir + 自定义 Interceptor:从 JSON 提取时间戳写入 HDFS 分区

38 阅读9分钟

TL;DR

  • 场景:多目录日志采集,按日志类型打 Header,并按事件时间落 HDFS 分区
  • 结论:Taildir Source + 自定义 Interceptor 抽取 JSON 时间戳写 Header,HDFS Sink 依据 Header 路由与滚动
  • 产出:一套可复用的“日志采集→Header 打标→按天分区落盘→生产 nohup 启动”落地模板

大数据-228 离线数仓 Flume Taildir + 自定义 Interceptor:从 JSON 提取时间戳写入 HDFS 分区

离线数仓架构图

日志数据采集小结

(总结前置,下面是正文)

在 Apache Flume 中,拦截器(Interceptor)是数据流管道的一个关键组件,它允许在事件(Event)进入 Flume Channel 之前对其进行修改或过滤。通过自定义拦截器,你可以实现特定的业务逻辑,如数据过滤、字段添加或修改、格式转换等。 自定义拦截器 是指用户根据需求自行编写 Java 代码来扩展 Flume 的功能,而不是使用默认的拦截器。

  • 使用 taildir source监控指定多个目录,可以给不同目录的日志加上不同Header
  • 在每个目录上可以使用正则匹配多个文件
  • 使用自定义拦截器,主要功能是从JSON串种获取时间戳,加到event的header中
  • hdfs sink使用event header中的信息写数据(控制写文件的位置)
  • hdfs文件的滚动方式(基于文件大小、基于event数量、基于时间)
  • 调节Flume JVM内存的分配

工作原理

  • 事件生成(Source): Flume 通过 Source 组件从各种外部系统采集数据。常见的 Source 类型包括:

    • Exec Source:通过执行指定命令(如 tail -F 日志文件)获取数据
    • Spooling Directory Source:监控指定目录中的新文件
    • NetCat Source:通过 TCP/UDP 端口接收数据
    • Kafka Source:从 Kafka 主题消费消息 Source 会将采集到的原始数据封装为 Flume Event 对象,包含事件头和字节数组形式的负载数据。
  • 拦截器(Interceptor): 在事件进入 Channel 前,可以通过链式拦截器进行预处理:

    1. Timestamp Interceptor:自动添加事件时间戳
    2. Host Interceptor:添加主机名或IP信息
    3. Regex Filtering Interceptor:基于正则表达式过滤事件
    4. Search-and-Replace Interceptor:修改事件内容
    5. 自定义拦截器:实现特定业务逻辑处理 拦截器可以单个或多个串联使用,按配置顺序依次执行。
  • 传输(Channel): 作为事件缓冲区,保证可靠传输。主要类型:

    • Memory Channel:基于内存队列,性能高但可能丢失数据
    • File Channel:持久化到本地文件系统,保证数据可靠性
    • JDBC Channel:使用数据库存储事件
    • Kafka Channel:集成 Kafka 作为存储后端 Channel 提供事务支持,确保事件在 Source 和 Sink 间可靠传递。
  • 消费(Sink): 负责将事件写入目标系统,常见实现:

    • HDFS Sink:写入 Hadoop 分布式文件系统
    • HBase Sink:存入 HBase 数据库
    • Kafka Sink:发布到 Kafka 主题
    • File Roll Sink:写入本地文件系统
    • Avro Sink:转发到其他 Flume Agent Sink 支持批量写入和故障重试机制,可根据目标系统特性配置相应参数。

开发和部署注意事项

  • 依赖管理: 开发自定义拦截器需要依赖 Flume 的核心库,如 flume-ng-core 和 flume-ng-sdk。
  • 测试: 在本地测试拦截器逻辑,确保其功能正确,性能符合预期。
  • 部署: 将 JAR 文件上传至 Flume Agent 的 lib 目录并重启 Flume 服务。
  • 性能监控: 自定义拦截器可能会影响 Flume 的性能,尤其是在拦截逻辑复杂的情况下。建议在生产环境中监控资源使用情况。

采集启动日志和事件日志

上节我们完成了Agent 的配置,接来我们继续。

自定义拦截器

编码完成后打包上传到服务器,放置在 $FLUME_HOME/lib

编写代码

package icu.wzk;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.apache.flume.Context;
import org.apache.flume.Event;
import org.apache.flume.interceptor.Interceptor;

import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class LogTypeInterceptor implements Interceptor {

    @Override
    public void initialize() {

    }

    @Override
    public Event intercept(Event event) {
        // 获取Event的body
        String eventBody = new String(event.getBody(), StandardCharsets.UTF_8);
        // 获取Event的Header
        Map<String, String> headerMap = event.getHeaders();
        // 解析body获取JSON串
        String[] bodyArr = eventBody.split("\\s+");
        try {
            String jsonStr = bodyArr[6];
            String timestampStr = "";
            JSONObject jsonObject = JSON.parseObject(jsonStr);
            if (headerMap.getOrDefault("logtype", "").equals("start")) {
                // 取启动时间戳
                jsonObject.getJSONObject("app_active").getString("time");
            } else if (headerMap.getOrDefault("logtype", "").equals("event")) {
                // 取事件日志第一条记录的时间戳
                JSONArray jsonArray = jsonObject.getJSONArray("wzk_event");
                if (jsonArray.size() > 0) {
                    timestampStr = jsonArray.getJSONObject(0).getString("time");
                }
            }
            // 将时间戳转换为 yyyy-MM-dd
            long timestamp = Long.parseLong(timestampStr);
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
            Instant instant = Instant.ofEpochMilli(timestamp);
            LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
            String date = formatter.format(localDateTime);
            // 转换后将字符串放置到Header中
            headerMap.put("logtime", date);
            event.setHeaders(headerMap);
        } catch (Exception e) {
            headerMap.put("logtime", "Unknown");
            event.setHeaders(headerMap);
        }
        return event;
    }

    @Override
    public List<Event> intercept(List<Event> events) {
        List<Event> lstEvent = new ArrayList<>();
        for (Event event: events){
            Event outEvent = intercept(event);
            if (outEvent != null) {
                lstEvent.add(outEvent);
            }
        }
        return lstEvent;
    }

    @Override
    public void close() {

    }

    public static class Builder implements
            Interceptor.Builder {
        @Override
        public Interceptor build() {
            return new LogTypeInterceptor();
        }
        @Override
        public void configure(Context context) {
        }
    }
    
}

打包项目

mvn clean package

打包结果如下,我们需要将“”上传到服务器中: 离线数仓代码测试

启动测试

flume-ng agent --conf-file /opt/wzk/flume-conf/flume-log2hdfs3.conf -name a1 -Dflume.roog.logger=INFO,console

启动的结果如下图所示,如果你缺什么文件夹之类的,自己创建出来: 离线数仓 flume agent

测试结果

写入log文件:

vim /opt/wzk/logs/start/test.log

写入的内容如下所示:

2020-08-02 18:19:32.959 [main] INFO icu.wzk.ecommerce.AppStart - {"app_active":{"name":"app_active","json":{"entry":"1","action":"0","error_code":"0"},"time":1596342840284},"attr":{"area":"大庆","uid":"2F10092A2","app_v":"1.1.15","event_type":"common","device_id":"1FB872-9A1002","os_type":"2.8","channel":"TB","language":"chinese","brand":"iphone-8"}}

写入的结果如下图所示: 离线数仓 Flume 数据测试 写入log文件:

vim /opt/wzk/logs/event/test.log

写入的内容如下所示:

2020-08-02 18:20:11.877 [main] INFO icu.wzk.ecommerce.AppEvent - {"wzk_event":[{"name":"goods_detail_loading","json":{"entry":"1","goodsid":"0","loading_time":"93","action":"3","staytime":"56","showtype":"2"},"time":1596343881690},{"name":"loading","json":{"loading_time":"15","action":"3","loading_type":"3","type":"1"},"time":1596356988428},{"name":"notification","json":{"action":"1","type":"2"},"time":1596374167278},{"name":"favorites","json":{"course_id":1,"id":0,"userid":0},"time":1596350933962}],"attr":{"area":"长治","uid":"2F10092A4","app_v":"1.1.14","event_type":"common","device_id":"1FB872-9A1004","os_type":"0.5.0","channel":"QL","language":"chinese","brand":"xiaomi-0"}}

写入的结果如下图所示: 离线数仓 Flume 测试

查看结果

控制台已经输出了结果: 离线数仓 Flume taildir 测试

我们查看HDFS,也输出了对应的内容出来: 离线数仓 HDFS 数据查看

生产环境

生产环节中,推荐使用:

nohup flume-ng agent --conf-file /opt/wzk/flume-conf/flume-log2hdfs3.conf -name a1 -Dflume.roog.logger=INFO,console > dev/null 2>&1 &
  • nohup 该命令允许用户退出账户、关闭终端之后还继续运行相应的进程
  • /dev/null 代表Linux的空设备文件,所有往这个文件里面写入的内容都会丢失,也称黑洞
  • 标准输入0,从键盘获得输入 /proc/self/fd/0
  • 标准输出1,输出到屏幕(控制台)/proc/self/fd/1
  • 错误输出2,输出到屏幕(控制台)/proc/self/fd/2
  • /dev/null 标准输出1重定向到 /dev/null 中,此时标准输出不存在,没有任何地方能够找到输出的内容
  • 2>&1 错误输出将会和标准输出输出到同一个地方
  • /dev/null 2>&1 不会输出任何信息到控制台,也不会有任何信息输出到文件中

错误速查

症状根因定位修复
HDFS路径未按日期分区,logtime为空或UnknownInterceptor未正确写入timestampStr/异常被catch查看Flume控制台、在Interceptor打日志、检查Header是否含logtime;修正对timestampStr的赋值逻辑;异常时不要吞掉关键信息,至少输出body/索引信息
启动日志分支始终取不到时间戳start分支只调用getString未赋值给timestampStr复查intercept中start分支代码路径;将启动时间戳写入timestampStr(保持与event分支一致的赋值方式)
ArrayIndexOutOfBounds/JSON解析失败eventBody.split后固定取bodyArr[6],对日志格式强耦合抓取一条原始日志body,打印split后数组长度与内容;用更稳健的提取方式:定位JSON起始“{”并截取;或用正则提取JSON段
NumberFormatException(timestamp解析失败)timestampStr为空/非纯数字/字段路径不一致打印timestampStr、JSON结构;确认time字段类型;增加空值判断与类型兼容(字符串/数字);字段缺失时走降级策略
日期与预期不一致(跨时区/跨天)ZoneId.systemDefault()受服务器时区影响对比服务器时区与业务时区;抽样对照原始timestamp;固定业务时区(如ZoneId.of("Asia/Shanghai")),或在配置中可切换
启动命令参数不生效/日志级别不对JVM参数拼写错误(如root/logger关键词写错)对照Flume官方参数名;观察是否仍输出默认级别;更正JVM参数键名;确认控制台实际输出的logger生效
nohup后仍有输出或命令报找不到文件重定向路径写错(dev/null少“/”)直接执行nohup命令观察报错;改为>/dev/null 2>&1;必要时输出到指定日志文件便于排障
HDFS小文件过多/滚动频繁rollSize/rollCount/rollInterval配置不合理统计HDFS目录文件数量与平均大小;结合吞吐设置合理滚动阈值;按天/小时分区同时控制滚动策略
内存飙升/吞吐下降Memory Channel或拦截器解析开销过大观察Flume JVM、GC、Channel backoff;调整Channel类型与容量;优化JSON解析与字符串处理,减少对象创建

其他系列

🚀 AI篇持续更新中(长期更新)

AI炼丹日志-29 - 字节跳动 DeerFlow 深度研究框斜体样式架 私有部署 测试上手 架构研究,持续打造实用AI工具指南! AI研究-132 Java 生态前沿 2025:Spring、Quarkus、GraalVM、CRaC 与云原生落地 🔗 AI模块直达链接

💻 Java篇持续更新中(长期更新)

Java-218 RocketMQ Java API 实战:同步/异步 Producer 与 Pull/Push Consumer MyBatis 已完结,Spring 已完结,Nginx已完结,Tomcat已完结,分布式服务已完结,Dubbo已完结,MySQL已完结,MongoDB已完结,Neo4j已完结,FastDFS 已完结,OSS已完结,GuavaCache已完结,EVCache已完结,RabbitMQ已完结,RocketMQ正在更新... 深入浅出助你打牢基础! 🔗 Java模块直达链接

📊 大数据板块已完成多项干货更新(300篇):

包括 Hadoop、Hive、Kafka、Flink、ClickHouse、Elasticsearch 等二十余项核心组件,覆盖离线+实时数仓全栈! 大数据-278 Spark MLib - 基础介绍 机器学习算法 梯度提升树 GBDT案例 详解 🔗 大数据模块直达链接