Skip to content

Commit

Permalink
refine go consumer
Browse files Browse the repository at this point in the history
feat: update golang consumer group
  • Loading branch information
shabicheng authored May 29, 2023
2 parents 34c0951 + 2876f78 commit d767a4d
Show file tree
Hide file tree
Showing 13 changed files with 478 additions and 353 deletions.
83 changes: 59 additions & 24 deletions consumer/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# Aliyun LOG Go Consumer Library



Aliyun LOG Go Consumer Library 是一个易于使用且高度可配置的golang 类库,专门为大数据高并发场景下的多个消费者协同消费同一个logstore而编写的纯go语言的类库。

## 功能特点
Expand All @@ -19,9 +17,7 @@ Aliyun LOG Go Consumer Library 是一个易于使用且高度可配置的golang

- 用户可以创建多个消费者对同一Logstore中的数据进行消费,而且不用关心消费者之间的负载均衡,consumer library 会进行自动处理,并且保证数据不会被重复消费。在cpu等资源有限情况下可以尽最大能力去消费logstore中的数据,并且会自动为用户保存消费断点到服务端。
- 当网络不稳定出现网络震荡时,consumer library可以在网络恢复时继续消费并且保证数据不会丢失及重复消费。
- 提供了更多高阶用法,使用户可以通过多种方法去调控运行中的consumer library,具体事例请参考[aliyun log go consumer library 高阶用法](https://yq.aliyun.com/articles/693820)


- 提供了更多高阶用法,使用户可以通过多种方法去调控运行中的consumer library

## 安装

Expand All @@ -31,53 +27,93 @@ Aliyun LOG Go Consumer Library 是一个易于使用且高度可配置的golang
git clone git@github.com:aliyun/aliyun-log-go-sdk.git
```

## 原理剖析及快速入门


##原理剖析及快速入门

参考教程: [ALiyun LOG Go Consumer Library 快速入门及原理剖析](https://yq.aliyun.com/articles/693820)


参考教程: [ALiyun LOG Go Consumer Library 快速入门及原理剖析](https://developer.aliyun.com/article/693820)

## 使用步骤

1.**配置LogHubConfig**

LogHubConfig是提供给用户的配置类,用于配置消费策略,您可以根据不同的需求设定不同的值,各参数含义如其中所示
|参数|含义|详情|
| --- | --- | --- |
|Endpoint|sls的endpoint|必填,如cn-hangzhou.sls.aliyuncs.com|
|AccessKeyId|aliyun的AccessKeyId|必填|
|AccessKeySecret|aliyun的AccessKeySecret|必填|
|Project|sls的project信息|必填|
|Logstore|sls的logstore|必填|
|ConsumerGroupName|消费组名称|必填|
|Consumer|消费者名称|必填,sls的consumer需要自行指定,请注意不要重复|
|CursorPosition|消费的点位|必填,支持 1.BEGIN_CURSOR: logstore的开始点位 2. END_CURSOR: logstore的最新数据点位 3.SPECIAL_TIME_CURSOR: 自行设置的unix时间戳|
||sls的logstore|必填|
|HeartbeatIntervalInSecond|心跳的时间间隔|非必填,默认时间为20s, sdk会根据心跳时间与服务器确认alive|
|DataFetchIntervalInMs|数据默认拉取的间隔|非必填,默认为200ms|
|MaxFetchLogGroupCount|数据一次拉取的log group数量|非必填,默认为1000|
|CursorStartTime|数据点位的时间戳|非必填,CursorPosition为SPECIAL_TIME_CURSOR时需填写|
|InOrder|shard分裂后是否in order消费|非必填,默认为false,当为true时,分裂shard会在老的read only shard消费完后再继续消费|
|AllowLogLevel|允许的日志级别|非必填,默认为info,日志级别由低到高为debug, info, warn, error,仅高于此AllowLogLevel的才会被log出来|
|LogFileName|程序运行日志文件名称|非必填,默认为stdout|
|IsJsonType|是否为json类型|非必填,默认为logfmt格式,true时为json格式|
|LogMaxSize|日志文件最大size|非必填,默认为10|
|LogMaxBackups|最大保存的old日志文件|非必填,默认为10|
|LogCompass|日志是否压缩|非必填,默认不压缩,如果压缩为gzip压缩|
|HTTPClient|指定http client|非必填,可指定http client实现一些逻辑,sdk发送http请求会使用这个client|
|SecurityToken|aliyun SecurityToken|非必填,参考https://help.aliyun.com/document_detail/47277.html|
|AutoCommitDisabled|是否禁用sdk自动提交checkpoint|非必填,默认不会禁用|
|AutoCommitIntervalInMS|自动提交checkpoint的时间间隔|非必填,单位为MS,默认时间为60s|

2.**覆写消费逻辑**

```
func process(shardId int, logGroupList *sls.LogGroupList) string {
for _, logGroup := range logGroupList.LogGroups {
err := client.PutLogs(option.Project, "copy-logstore", logGroup)
if err != nil {
fmt.Println(err)
}
func process(shardId int, logGroupList *sls.LogGroupList, checkpointTracker CheckPointTracker) (string, error) {
err := dosomething()
if err != nil {
return "", nil
}
fmt.Println("shardId %v processing works sucess", shardId)
return "" // 不需要重置检查点情况下,请返回空字符串,如需要重置检查点,请返回需要重置的检查点游标。
fmt.Println("shardId %v processing works success", shardId)
// 标记给CheckPointTracker process已成功,保存存档点,
// false 标记process已成功,但并不直接写入服务器,等待一定的interval后sdk批量写入 (AutoCommitDisable为false情况SDK会批量写入)
// true 标记已成功, 且直接写入服务器
// 推荐大多数场景下使用false即可
checkpointTracker.SaveCheckPoint(false); // 代表process成功保存存档点,但并不直接写入服务器,等待一定的interval后写入
// 不需要重置检查点情况下,请返回空字符串,如需要重置检查点,请返回需要重置的检查点游标。
// 如果需要重置检查点的情况下,比如可以返回checkpointTracker.GetCurrentCursor, current checkpoint即尚未process的这批数据开始的检查点
// 如果已经返回error的话,无需重置到current checkpoint,代码会继续process这批数据,一般来说返回空即可
return "", nil
}
```

在实际消费当中,您只需要根据自己的需要重新覆写消费函数process 即可,上图只是一个简单的demo,将consumer获取到的日志进行了打印处理,注意,该函数参数和返回值不可改变,否则会导致消费失败。
在实际消费当中,您只需要根据自己的需要重新覆写消费函数process即可,上图只是一个简单的demo,将consumer获取到的日志进行了打印处理,注意,该函数参数和返回值不可改变,否则会导致消费失败。
另外的,如果你在process时有特别的需求,比如process暂存,实际异步操作,这里可以实现自己的Processor接口,除了Process函数,可以实现Shutdown函数对异步操作等进行优雅退出。
但是,请注意,checkpoint tracker是线程不安全的,它仅可负责本次process的checkpoint保存,请不要保存起来这个实例异步进行save!
```
type Processor interface {
Process(int, *sls.LogGroupList, CheckPointTracker) string
Shutdown(CheckPointTracker) error
}
```

3.**创建消费者并开始消费**

```
// option是LogHubConfig的实例
consumerWorker := consumerLibrary.InitConsumerWorker(option, process)
consumerWorker := consumerLibrary.InitConsumerWorkerWithCheckpointTracker(option, process)
// 如果实现了自己的processor,可以使用下面的语句
// consumerWroer := consumerLibrary.InitConsumerWorkerWithProcessor(option, myProcessor)
// 调用Start方法开始消费
consumerWorker.Start()
```
> 注意目前已废弃`InitConsumerWorker(option, process)`,其代表在process函数后,sdk会执行一次`checkpointTracker.SaveCheckPoint(false)`,但是无法手动强制写入服务器/获取上一个的checkpoint等功能
调用InitConsumerWorkwer方法,将配置实例对象和消费函数传递到参数中生成消费者实例对象,调用Start方法进行消费。

4.**关闭消费者**

```
ch:=make(chan os.Signal) //将os信号值作为信道
signal.Notify(ch, os.Kill, os.Interrupt)
ch := make(chan os.Signal, 1) //将os信号值作为信道
signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT)
consumerWorker.Start()
if _, ok := <-ch; ok { // 当获取到os停止信号以后,例如ctrl+c触发的os信号,会调用消费者退出方法进行退出。
consumerWorker.StopAndWait()
Expand All @@ -87,7 +123,6 @@ if _, ok := <-ch; ok { // 当获取到os停止信号以后,例如ctrl+c触发
上图中的例子通过go的信道做了os信号的监听,当监听到用户触发了os退出信号以后,调用StopAndWait()方法进行退出,用户可以根据自己的需要设计自己的退出逻辑,只需要调用StopAndWait()即可。



## 简单样例

为了方便用户可以更快速的上手consumer library 我们提供了两个简单的通过代码操作consumer library的简单样例,请参考[consumer library example](https://github.com/aliyun/aliyun-log-go-sdk/tree/master/example/consumer)
Expand Down
123 changes: 84 additions & 39 deletions consumer/checkpoint_tracker.go
Original file line number Diff line number Diff line change
@@ -1,61 +1,106 @@
package consumerLibrary

import (
"strings"
"time"

sls "github.com/aliyun/aliyun-log-go-sdk"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"time"
)

type ConsumerCheckPointTracker struct {
client *ConsumerClient
defaultFlushCheckPointIntervalSec int64
tempCheckPoint string
lastPersistentCheckPoint string
trackerShardId int
lastCheckTime int64
logger log.Logger
}

func initConsumerCheckpointTracker(shardId int, consumerClient *ConsumerClient, logger log.Logger) *ConsumerCheckPointTracker {
checkpointTracker := &ConsumerCheckPointTracker{
defaultFlushCheckPointIntervalSec: 60,
client: consumerClient,
trackerShardId: shardId,
logger: logger,
}
return checkpointTracker
type CheckPointTracker interface {
// GetCheckPoint get lastest saved check point
GetCheckPoint() string
// GetCurrentCursor get current fetched data cursor
GetCurrentCursor() string
// SaveCheckPoint, save checkpoint
SaveCheckPoint(force bool) error
}

func (checkPointTracker *ConsumerCheckPointTracker) setMemoryCheckPoint(cursor string) {
checkPointTracker.tempCheckPoint = cursor
type DefaultCheckPointTracker struct {
client *ConsumerClient
heartBeat *ConsumerHeartBeat
nextCursor string // cursor for already pulled data
currentCursor string // cursor for data processed, but may not be saved to server
pendingCheckPoint string // pending cursor to saved
savedCheckPoint string // already saved
shardId int
logger log.Logger
}

func (checkPointTracker *ConsumerCheckPointTracker) setPersistentCheckPoint(cursor string) {
checkPointTracker.lastPersistentCheckPoint = cursor
func initConsumerCheckpointTracker(shardId int, consumerClient *ConsumerClient, consumerHeatBeat *ConsumerHeartBeat, logger log.Logger) *DefaultCheckPointTracker {
checkpointTracker := &DefaultCheckPointTracker{
client: consumerClient,
heartBeat: consumerHeatBeat,
shardId: shardId,
logger: logger,
}
return checkpointTracker
}

func (checkPointTracker *ConsumerCheckPointTracker) flushCheckPoint() error {
if checkPointTracker.tempCheckPoint != "" && checkPointTracker.tempCheckPoint != checkPointTracker.lastPersistentCheckPoint {
if err := checkPointTracker.client.updateCheckPoint(checkPointTracker.trackerShardId, checkPointTracker.tempCheckPoint, true); err != nil {
return err
}
func (tracker *DefaultCheckPointTracker) initCheckPoint(cursor string) {
tracker.savedCheckPoint = cursor
}

checkPointTracker.lastPersistentCheckPoint = checkPointTracker.tempCheckPoint
func (tracker *DefaultCheckPointTracker) SaveCheckPoint(force bool) error {
tracker.pendingCheckPoint = tracker.nextCursor
if force {
return tracker.flushCheckPoint()
}

return nil
}

func (checkPointTracker *ConsumerCheckPointTracker) flushCheck() {
currentTime := time.Now().Unix()
if currentTime > checkPointTracker.lastCheckTime+checkPointTracker.defaultFlushCheckPointIntervalSec {
if err := checkPointTracker.flushCheckPoint(); err != nil {
level.Warn(checkPointTracker.logger).Log("msg", "update checkpoint get error", "error", err)
} else {
checkPointTracker.lastCheckTime = currentTime
func (tracker *DefaultCheckPointTracker) GetCheckPoint() string {
return tracker.savedCheckPoint
}

func (tracker *DefaultCheckPointTracker) GetCurrentCursor() string {
return tracker.currentCursor
}

func (tracker *DefaultCheckPointTracker) setCurrentCheckPoint(cursor string) {
tracker.currentCursor = cursor
}

func (tracker *DefaultCheckPointTracker) setNextCursor(cursor string) {
tracker.nextCursor = cursor
}

func (tracker *DefaultCheckPointTracker) flushCheckPoint() error {
if tracker.pendingCheckPoint == "" || tracker.pendingCheckPoint == tracker.savedCheckPoint {
return nil
}
for i := 0; ; i++ {
err := tracker.client.updateCheckPoint(tracker.shardId, tracker.pendingCheckPoint, true)
if err == nil {
break
}
slsErr, ok := err.(*sls.Error)
if ok {
if strings.EqualFold(slsErr.Code, "ConsumerNotExsit") || strings.EqualFold(slsErr.Code, "ConsumerNotMatch") {
tracker.heartBeat.removeHeartShard(tracker.shardId)
level.Warn(tracker.logger).Log("msg", "consumer has been removed or shard has been reassigned", "shard", tracker.shardId, "err", slsErr)
break
} else if strings.EqualFold(slsErr.Code, "ShardNotExsit") {
tracker.heartBeat.removeHeartShard(tracker.shardId)
level.Warn(tracker.logger).Log("msg", "shard does not exist", "shard", tracker.shardId)
break
}
}
if i >= 2 {
level.Error(tracker.logger).Log(
"msg", "failed to save checkpoint",
"consumer", tracker.client.option.ConsumerName,
"shard", tracker.shardId,
"checkpoint", tracker.pendingCheckPoint,
)
return err
}
time.Sleep(100 * time.Millisecond)
}
}

func (checkPointTracker *ConsumerCheckPointTracker) getCheckPoint() string {
return checkPointTracker.tempCheckPoint
tracker.savedCheckPoint = tracker.pendingCheckPoint
return nil
}
26 changes: 16 additions & 10 deletions consumer/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ type LogHubConfig struct {
// deleted.)
//:param LogCompass: Compress determines if the rotated log files should be compressed using gzip.
//:param HTTPClient: custom http client for sending data to sls
//:param AutoCommitDisabled: whether to disable commit checkpoint automatically, default is false, means auto commit checkpoint
// Note that if you set autocommit to false, you must use InitConsumerWorkerWithCheckpointTracker instead of InitConsumerWorker
//:param AutoCommitIntervalInSec: default auto commit interval, default is 30

Endpoint string
AccessKeyID string
Expand All @@ -56,17 +59,20 @@ type LogHubConfig struct {
LogCompass bool
HTTPClient *http.Client
SecurityToken string
AutoCommitDisabled bool
AutoCommitIntervalInMS int64
}

const (
BEGIN_CURSOR = "BEGIN_CURSOR"
END_CURSOR = "END_CURSOR"
SPECIAL_TIMER_CURSOR = "SPECIAL_TIMER_CURSOR"
INITIALIZING = "INITIALIZING"
INITIALIZING_DONE = "INITIALIZING_DONE"
PULL_PROCESSING = "PULL_PROCESSING"
PULL_PROCESSING_DONE = "PULL_PROCESSING_DONE"
CONSUME_PROCESSING = "CONSUME_PROCESSING"
CONSUME_PROCESSING_DONE = "CONSUME_PROCESSING_DONE"
SHUTDOWN_COMPLETE = "SHUTDOWN_COMPLETE"
BEGIN_CURSOR = "BEGIN_CURSOR"
END_CURSOR = "END_CURSOR"
SPECIAL_TIMER_CURSOR = "SPECIAL_TIMER_CURSOR"
)

const (
INITIALIZING = "INITIALIZING"
PULLING = "PULLING"
PROCESSING = "PROCESSING"
SHUTTING_DOWN = "SHUTTING_DOWN"
SHUTDOWN_COMPLETE = "SHUTDOWN_COMPLETE"
)
3 changes: 3 additions & 0 deletions consumer/consumer_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ func initConsumerClient(option LogHubConfig, logger log.Logger) *ConsumerClient
if option.MaxFetchLogGroupCount == 0 {
option.MaxFetchLogGroupCount = 1000
}
if option.AutoCommitIntervalInMS == 0 {
option.AutoCommitIntervalInMS = 60 * 1000
}
client := &sls.Client{
Endpoint: option.Endpoint,
AccessKeyID: option.AccessKeyID,
Expand Down
Loading

0 comments on commit d767a4d

Please sign in to comment.