github.com/machinefi/w3bstream@v1.6.5-rc9.0.20240426031326-b8c7c4876e72/pkg/modules/metrics/clickhouse.go (about)

     1  package metrics
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"log"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/ClickHouse/clickhouse-go/v2"
    11  	"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
    12  
    13  	"github.com/machinefi/w3bstream/pkg/depends/conf/logger"
    14  	"github.com/machinefi/w3bstream/pkg/types"
    15  )
    16  
    17  const (
    18  	queueLength      = 5000
    19  	popThreshold     = 3
    20  	concurrentWorker = 10
    21  )
    22  
    23  type (
    24  	ClickhouseClient struct {
    25  		workerPool []*connWorker
    26  		sqLQueue   chan *queueElement
    27  	}
    28  
    29  	queueElement struct {
    30  		query string
    31  		count int
    32  	}
    33  )
    34  
    35  var (
    36  	clickhouseCLI *ClickhouseClient
    37  	sleepTime     = 10 * time.Second
    38  )
    39  
    40  func Init(ctx context.Context) {
    41  	cfg, existed := types.MetricsCenterConfigFromContext(ctx)
    42  	if !existed || len(cfg.ClickHouseDSN) == 0 {
    43  		log.Println("fail to get the config of metrics center")
    44  		return
    45  	}
    46  	opts, err := clickhouse.ParseDSN(cfg.ClickHouseDSN)
    47  	if err != nil {
    48  		panic(err)
    49  	}
    50  	{
    51  		opts.Settings["async_insert"] = 1
    52  		opts.Settings["wait_for_async_insert"] = 0
    53  		opts.Settings["async_insert_busy_timeout_ms"] = 100
    54  	}
    55  	clickhouseCLI = newClickhouseClient(opts)
    56  	log.Println("clickhouse client is initialized")
    57  }
    58  
    59  func newClickhouseClient(cfg *clickhouse.Options) *ClickhouseClient {
    60  	cc := &ClickhouseClient{
    61  		sqLQueue: make(chan *queueElement, queueLength),
    62  	}
    63  	for i := 0; i < concurrentWorker; i++ {
    64  		cc.workerPool = append(cc.workerPool, &connWorker{
    65  			sqLQueue: cc.sqLQueue,
    66  			cfg:      cfg,
    67  		})
    68  		go cc.workerPool[i].run()
    69  	}
    70  	return cc
    71  }
    72  
    73  func (c *ClickhouseClient) Insert(query string) error {
    74  	select {
    75  	case c.sqLQueue <- &queueElement{
    76  		query: query,
    77  		count: 0,
    78  	}:
    79  	default:
    80  		return errors.New("the queue of client is full")
    81  	}
    82  	return nil
    83  }
    84  
    85  type SQLBatcher struct {
    86  	signal   chan string
    87  	preStatm string
    88  	buf      []string
    89  }
    90  
    91  const (
    92  	batchSize      = 50000
    93  	tickerInterval = 200 * time.Millisecond
    94  )
    95  
    96  func NewSQLBatcher(preStatm string) *SQLBatcher {
    97  	bw := &SQLBatcher{
    98  		signal:   make(chan string, queueLength),
    99  		preStatm: preStatm,
   100  		buf:      make([]string, 0, batchSize),
   101  	}
   102  	go bw.run()
   103  	return bw
   104  }
   105  
   106  func (b *SQLBatcher) Insert(query string) error {
   107  	_, l := logger.NewSpanContext(context.Background(), "metrics.SQLBatcher.Insert")
   108  	defer l.End()
   109  
   110  	if clickhouseCLI == nil {
   111  		return errors.New("clickhouse client is not initialized")
   112  	}
   113  	select {
   114  	case b.signal <- query:
   115  		return nil
   116  	default:
   117  		return errors.New("the queue of SQLBatcher is full")
   118  	}
   119  }
   120  
   121  func (b *SQLBatcher) run() {
   122  	ticker := time.NewTicker(tickerInterval)
   123  	for {
   124  		select {
   125  		case <-ticker.C:
   126  			if len(b.buf) == 0 {
   127  				continue
   128  			}
   129  			if clickhouseCLI == nil {
   130  				log.Println("clickhouse client is not initialized")
   131  				continue
   132  			}
   133  			_ = b.insert()
   134  		case str, ok := <-b.signal:
   135  			if !ok {
   136  				return
   137  			}
   138  			if clickhouseCLI == nil {
   139  				log.Println("clickhouse client is not initialized")
   140  				continue
   141  			}
   142  			b.buf = append(b.buf, str)
   143  			if len(b.buf) >= batchSize {
   144  				if b.insert() != nil {
   145  					continue
   146  				}
   147  				ticker.Reset(tickerInterval)
   148  			}
   149  		}
   150  	}
   151  }
   152  
   153  func (b *SQLBatcher) insert() error {
   154  	_, l := logger.NewSpanContext(context.Background(), "metrics.SQLBatcher.insert")
   155  	defer l.End()
   156  
   157  	err := clickhouseCLI.Insert(b.preStatm + "(" + strings.Join(b.buf, "),(") + ")")
   158  	if err != nil {
   159  		l.Error(err)
   160  		return err
   161  	}
   162  	b.buf = b.buf[0:0]
   163  	return nil
   164  }
   165  
   166  type connWorker struct {
   167  	sqLQueue chan *queueElement
   168  	conn     driver.Conn
   169  	cfg      *clickhouse.Options
   170  }
   171  
   172  func (c *connWorker) run() {
   173  	for {
   174  		if err := c.connect(); err != nil {
   175  			log.Println("ClickhouseClient failed to connect: ", err)
   176  			time.Sleep(sleepTime)
   177  			continue
   178  		}
   179  		ele := <-c.sqLQueue
   180  		if err := c.conn.Exec(context.Background(), ele.query); err != nil {
   181  			if !c.liveness() {
   182  				c.conn = nil
   183  				log.Printf("ClickhouseClient failed to connect the server: error: %s, query %s\n", err, ele.query)
   184  			} else {
   185  				log.Printf("ClickhouseClient failed to insert data: error %s, query %s\n ", err, ele.query)
   186  			}
   187  			if ele.count > popThreshold {
   188  				log.Printf("the query %s in ClickhouseClient is poped due to %d times failure.", ele.query, ele.count)
   189  				continue
   190  			}
   191  			ele.count++
   192  			// TODO: Double linked list should be used to append the element to the head
   193  			// when the order of the queue is important
   194  			c.sqLQueue <- ele
   195  		}
   196  	}
   197  }
   198  
   199  func (c *connWorker) connect() error {
   200  	if c.conn != nil {
   201  		return nil
   202  	}
   203  	conn, err := clickhouse.Open(c.cfg)
   204  	if err != nil {
   205  		return err
   206  	}
   207  	c.conn = conn
   208  	if !c.liveness() {
   209  		c.conn = nil
   210  		return errors.New("failed to ping clickhouse server")
   211  	}
   212  	log.Println("clickhouse server login successfully")
   213  	return nil
   214  }
   215  
   216  func (c *connWorker) liveness() bool {
   217  	if err := c.conn.Ping(context.Background()); err != nil {
   218  		log.Println("failed to ping clickhouse server: ", err)
   219  		return false
   220  	}
   221  	return true
   222  }