go自定义time支持rfc3339_基于Go数据框架设计与实现-Csink

由于最近工作比较忙,文章停更了很久,非常抱歉。今天给大家分享最近开发项目:基于Go数据框架Csink

在讲Csink设计与实现前先介绍相关背景,为什么要自研一套数据清洗聚合框架。

背景

最近在搞实时数据采集分析,需要对数据进行ETL,最原始需求:(Kafka -> 简单字符串处理 -> Kafka)

e79d7c064c90bdc90c50508163797e69.png

需求不复杂,在做选型时果断放弃Flink这样开源流处理框架,放弃原因如下

go自定义time支持rfc3339_基于Go数据框架设计与实现-Csink_第1张图片

既然目标有了,下一步进行开源产品调研,在摸索中找到了Flume,Flume自定义拦截器可实现对实时流数据的简单处理,也可以结合Flume强大的配置管理进行很多数据收集及Sink操作,目前看Flume很适合我的要求,说干就干,花了些时间写了两个Flume插件Jar包,自测没问题,数据也可以进行ETL操作。

正式数据测试。。。敌军30秒到达战场

刚开始对配置有一些懵懵懂懂,测试使用Topics有9个分区,Flume NG启动一个进程进行消费,数据跑了几小时发现数据消费超级慢,具体分析如下

  1. Topics 9个分区, Flume NG单进程消费,多对一的情况下消费会下降(这里原本期望Flume会根据分区数,启动相应线程来监听数据,然后消费),测试结果大失所望,他是单分区消费。
  2. 既然单进程->单分区,那么我就启动9个Flume NG,问题是解决了,但资源占用相当可怕。。。一台机器32G服务器,只能跑2个Topics。。。
  3. 自定义拦截器清洗数据时,有重型的数据转换操作,也有影响数据清洗和积压

Csink

Csink = collect sink

通过层层磨练找到相关问题点,剩下的是如何攻破难关。

从节省资源,性能的前提入手第一想到的Golang来做这件事情。(golang生态数据分析产品不太多,也没找到称心的产品,只有自己动手解决,毕竟目测代码量不会太大)

通过调研及最终整合需求如下

1. 基础ETL操作

2. 高性能聚合查询

3. 可配置Srouce - Sink吞吐量

4. 插件化(针对不同服务配置不同的插件,实现插件编排)

5. Kafka & tailf数据收集 <->发送到不同的目的地(kafka/mysql/es/hdfs)

设计 & 实现

go自定义time支持rfc3339_基于Go数据框架设计与实现-Csink_第2张图片

架构图

1. Agent

2. Etcd

3. Web配置管理

4. 插件服务(任务)

在4大块中"插件服务"处于架构中最底层,要想快速完成一期交付,先把基础脚手架搞定,并提供基础配置操作

Kafka Topic多分区(草稿规划)

go自定义time支持rfc3339_基于Go数据框架设计与实现-Csink_第3张图片
  1. Kafka Topic A (Topics分区列表)
  2. 分区-协程池(为每个分区分配协程,并消费多分区数据)
  3. Channel
  4. 工作区-协程池(为每行数据分配协程,增加并行处理聚合数据吞吐量)

草稿期间未实现模块化处理,仅为测试想法是否可行的小脚本。

采集聚合服务(一期)

go自定义time支持rfc3339_基于Go数据框架设计与实现-Csink_第4张图片
  1. 单独配置某插件基础项
  2. 某插件吞吐量配置
  3. 插件服务编排
  4. 合理化目录结构分层
  5. Tailf采集文本日志
  6. Kafka生产、消费

数据框架重点采用Golang channel select goroutine实现。

目录结构

├── apps               #业务逻辑
│   ├── live
├── comment
├── conf               #配置文件
│   ├── csink.ini
│   ├── testplugin.ini
├── csink.log
├── go.mod
├── go.sum
├── internal           #内部核心代码
│   ├── csink          #核心
│   ├── logger         #日志
│   ├── utils          #公用方法
│   └── plugins.go     #插件入口
├── plugins            #插件服务入口
│   └── streamPlugin

核心配置 csink.ini

[csink]
;消息行 - 并发数量[消费端-source],例:从topic分区取出数据,需要多少个协程来处理,可以增加聚合运算处理速度,建议数量 < 20000
message.pool.max=20000

;sink生产任务数量,例:清洗聚合数据都已完成,需要多少消费->生产任务去处理,取决于你的分区数量,建议数量 < 20
sink.data.pool.max = 20

; 处理完成数据处理类型,可以采用CustomSinkStream自定义,也可以由系统完成, 1=系统执行   2=插件自定义CustomSinkStream
sink.data.handler.type = 1

插件配置 testplugin.ini

案例1:消费kafka <- ->生产kafka
[test-source]
source.type=kafka
brokers = *.*.*.*:9002,*.*.*.*::9003
topics = xiaoliang1
groupid = test-consumer-group


[test-sink]
sink.type=kafka
brokers = *.*.*.*::9002,*.*.*.*::9003
topics = xiaoliang1-sink

案例2:tail -f方法实际文本日志 -> kafka topics
[test1-source]
source.type = tailf
source.filepath = nums20000.txt


[test1-sink]
sink.type=kafka
brokers = *.*.*.*::9002,*.*.*.*::9003
topics = xiaoliang1-sink

看到这个配置有木有跟Flume很像,没错,思路也是借鉴了Flume。

编写插件 - 场景1(普通)

type ViewPlugin struct {
}

// source stream handler
func (p *ViewPlugin) SourceStream(msgCtx *csink.MsgContext) string {

    //time.Sleep(3*time.Second)
    return fmt.Sprintf("xiaoliang-view清洗后数据 - %s", msgCtx.MessageText)
}

// sink 需要集合[sink.data.handler.type]使用
// sink.data.handler.type=1 不会请求到该函数,默认执行系统通道
// sink.data.handler.type=2 请求CustomSinkStream,自行处理
func (p *ViewPlugin) CustomSinkStream(str string) string {
    str1 := fmt.Sprintf("View-sink:=%s", str)
    fmt.Println(str1)
    return str1
}

func main() {
    go func() {
        runtime.SetMutexProfileFraction(1)  // 开启对锁调用的跟踪
        runtime.SetBlockProfileRate(1)      // 开启对阻塞操作的跟踪
        http.ListenAndServe("0.0.0.0:6060", nil)
    }()
    internal.RegisterTopics("views",&ViewPlugin{})
    internal.StartTopics()

}

编写插件 - 场景2(收集每行数据,进行耗时多数据源请求)并行执行 业务应用 文件路径:apps/live/comment.go

package live

import (
    "fmt"
    "sync"
    "time"
)

type Comment struct {
    uid int
}

var cwg sync.WaitGroup

func (c *Comment) Start() {
    cwg.Add(2)
}

func (c *Comment) Setuid(_uid int) {
    c.uid = _uid
}

func (c *Comment) Go1(cc chan <- string) {
    defer cwg.Done()
    time.Sleep(5*time.Second)
    cc <- fmt.Sprintf("%d %s", c.uid, "获取数据源[1]")
}

func (c *Comment) Go2(cc chan <- string) {
    defer cwg.Done()
    time.Sleep(5*time.Second)
    cc <- fmt.Sprintf("%d %s", c.uid, "获取数据源[2]")
}

func (c *Comment) End() {
    cwg.Wait()
}

采集插件 文件路径:plugins/streamPlugin/comment.go

var liveComment live.Comment

// source stream handler
func (p *CommentPlugin) SourceStream(msgCtx *csink.MsgContext) string {

    uid := 9981

    liveComment.Setuid(uid)
    dataSource1 := make(chan string, 1)
    dataSource2 := make(chan string, 1)

    liveComment.Start()

    go liveComment.Go1(dataSource1)
    go liveComment.Go2(dataSource2)

    views := <- dataSource1
    zan := <- dataSource2
    liveComment.End()

    str := fmt.Sprintf("tianye-comment清洗后数据 - %s", msgCtx.MessageText+"==" + views + zan)

    fmt.Println(str)
    return str
}

// sink 需要集合[sink.data.handler.type]使用
// sink.data.handler.type=1 不会请求到该函数,默认执行系统通道
// sink.data.handler.type=2 请求CustomSinkStream,自行处理
func (p *CommentPlugin) CustomSinkStream(str string) string {
    str1 := fmt.Sprintf("comment-sink:=%s", str)
    fmt.Println(str1)
    return str1
}

func main() {
    go func() {
        runtime.SetMutexProfileFraction(1)  // 开启对锁调用的跟踪
        runtime.SetBlockProfileRate(1)      // 开启对阻塞操作的跟踪
        http.ListenAndServe("0.0.0.0:6060", nil)
    }()
    internal.RegisterTopics("comment",&CommentPlugin{})
    internal.RegisterTopics("views",&ViewPlugin{})
    internal.StartTopics()
}

每行数据为一个协程(轻量、快速),我尝试过开4W协程,普通电脑完全能承受资源占用,真正的实现高性能聚合处理。在开发过程中让我越来越喜欢golang,真心希望golang可以越来越普及。

截止目前暂时满足基础需求,下一步要考虑如何更好的结合业务场景。

结束

学习最好方式:多看、多练、多思考

不知道在什么地方看到这样一句话:『眼是懒蛋,手是好汉』,想要在互联网这行走的远、稳,一定要怀着对新技术的好奇心,多动手写写码。

你可能感兴趣的:(go自定义time支持rfc3339_基于Go数据框架设计与实现-Csink)