github.com/muhammadn/cortex@v1.9.1-0.20220510110439-46bb7000d03d/tools/blocksconvert/builder/tsdb.go (about)

     1  package builder
     2  
     3  import (
     4  	"context"
     5  	"crypto/rand"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/go-kit/log"
    13  	"github.com/go-kit/log/level"
    14  	"github.com/oklog/ulid"
    15  	"github.com/pkg/errors"
    16  	"github.com/prometheus/client_golang/prometheus"
    17  	"github.com/prometheus/common/model"
    18  	"github.com/prometheus/prometheus/pkg/labels"
    19  	"github.com/prometheus/prometheus/tsdb"
    20  	"github.com/prometheus/prometheus/tsdb/chunkenc"
    21  	"github.com/prometheus/prometheus/tsdb/chunks"
    22  	tsdb_errors "github.com/prometheus/prometheus/tsdb/errors"
    23  	"github.com/prometheus/prometheus/tsdb/index"
    24  	"github.com/thanos-io/thanos/pkg/block"
    25  	"github.com/thanos-io/thanos/pkg/block/metadata"
    26  
    27  	"github.com/cortexproject/cortex/pkg/chunk"
    28  	"github.com/cortexproject/cortex/pkg/querier/iterators"
    29  )
    30  
    31  const (
    32  	unsortedChunksDir     = "unsorted_chunks"
    33  	readChunksConcurrency = 16
    34  )
    35  
    36  // This builder uses TSDB's chunk and index writer directly, without
    37  // using TSDB Head.
    38  type tsdbBuilder struct {
    39  	log log.Logger
    40  
    41  	ulid        ulid.ULID
    42  	outDir      string
    43  	tmpBlockDir string
    44  
    45  	unsortedChunksWriterMu sync.Mutex
    46  	unsortedChunksWriter   tsdb.ChunkWriter
    47  
    48  	startTime          model.Time
    49  	endTime            model.Time
    50  	timestampTolerance int // in milliseconds
    51  
    52  	series    *seriesList
    53  	seriesDir string
    54  
    55  	writtenSamples  prometheus.Counter
    56  	processedSeries prometheus.Counter
    57  	seriesInMemory  prometheus.Gauge
    58  }
    59  
    60  func newTsdbBuilder(outDir string, start, end time.Time, timestampTolerance time.Duration, seriesBatchLimit int, log log.Logger, processedSeries, writtenSamples prometheus.Counter, seriesInMemory prometheus.Gauge) (*tsdbBuilder, error) {
    61  	id, err := ulid.New(ulid.Now(), rand.Reader)
    62  	if err != nil {
    63  		return nil, errors.Wrap(err, "create ULID")
    64  	}
    65  
    66  	blockDir := filepath.Join(outDir, id.String()+".tmp")
    67  	seriesDir := filepath.Join(blockDir, "series")
    68  
    69  	err = os.RemoveAll(blockDir)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  
    74  	// Also makes blockDir, if missing
    75  	err = os.MkdirAll(seriesDir, 0777)
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  
    80  	unsortedChunksWriter, err := chunks.NewWriter(filepath.Join(blockDir, unsortedChunksDir))
    81  	if err != nil {
    82  		return nil, errors.Wrap(err, "chunks writer")
    83  	}
    84  
    85  	return &tsdbBuilder{
    86  		log:                  log,
    87  		ulid:                 id,
    88  		outDir:               outDir,
    89  		tmpBlockDir:          blockDir,
    90  		unsortedChunksWriter: unsortedChunksWriter,
    91  		startTime:            model.TimeFromUnixNano(start.UnixNano()),
    92  		endTime:              model.TimeFromUnixNano(end.UnixNano()),
    93  		timestampTolerance:   int(timestampTolerance.Milliseconds()),
    94  		series:               newSeriesList(seriesBatchLimit, seriesDir),
    95  		seriesDir:            seriesDir,
    96  
    97  		processedSeries: processedSeries,
    98  		writtenSamples:  writtenSamples,
    99  		seriesInMemory:  seriesInMemory,
   100  	}, err
   101  }
   102  
   103  // Called concurrently with all chunks required to build a single series.
   104  func (d *tsdbBuilder) buildSingleSeries(metric labels.Labels, cs []chunk.Chunk) error {
   105  	defer d.processedSeries.Inc()
   106  
   107  	// Used by Prometheus, in head.go (with a reference to Gorilla paper).
   108  	const samplesPerChunk = 120
   109  
   110  	chs := make([]chunks.Meta, 0, 25) // On average, single series seem to have around 25 chunks.
   111  	seriesSamples := uint64(0)
   112  
   113  	// current chunk and appender. If nil, new chunk will be created.
   114  	var (
   115  		ch  *chunks.Meta
   116  		app chunkenc.Appender
   117  		err error
   118  	)
   119  
   120  	// This will merge and deduplicate samples from chunks.
   121  	it := iterators.NewChunkMergeIterator(cs, d.startTime, d.endTime)
   122  	for it.Next() && it.Err() == nil {
   123  		t, v := it.At()
   124  
   125  		mt := model.Time(t)
   126  
   127  		if mt < d.startTime {
   128  			continue
   129  		}
   130  		if mt >= d.endTime {
   131  			break
   132  		}
   133  
   134  		if ch == nil {
   135  			chs = append(chs, chunks.Meta{})
   136  			ch = &chs[len(chs)-1]
   137  			ch.MinTime = t
   138  
   139  			ch.Chunk = chunkenc.NewXORChunk()
   140  			app, err = ch.Chunk.Appender()
   141  			if err != nil {
   142  				return err
   143  			}
   144  		}
   145  
   146  		// If gap since last scrape is very close to an exact number of seconds, tighten it up
   147  		if d.timestampTolerance != 0 && ch.MaxTime != 0 {
   148  			gap := t - ch.MaxTime
   149  			seconds := ((gap + 500) / 1000)
   150  			diff := int(gap - seconds*1000)
   151  			// Don't go past endTime.
   152  			if diff != 0 && diff >= -d.timestampTolerance && diff <= d.timestampTolerance && ch.MaxTime+seconds*1000 <= int64(d.endTime) {
   153  				t = ch.MaxTime + seconds*1000
   154  			}
   155  		}
   156  
   157  		ch.MaxTime = t
   158  		app.Append(t, v)
   159  		seriesSamples++
   160  
   161  		if ch.Chunk.NumSamples() == samplesPerChunk {
   162  			ch.Chunk.Compact()
   163  			ch = nil
   164  		}
   165  	}
   166  
   167  	if ch != nil {
   168  		ch.Chunk.Compact()
   169  		ch = nil
   170  	}
   171  
   172  	d.unsortedChunksWriterMu.Lock()
   173  	err = d.unsortedChunksWriter.WriteChunks(chs...)
   174  	d.unsortedChunksWriterMu.Unlock()
   175  
   176  	if err != nil {
   177  		return err
   178  	}
   179  
   180  	// Remove chunks data from memory, but keep reference.
   181  	for ix := range chs {
   182  		if chs[ix].Ref == 0 {
   183  			return errors.Errorf("chunk ref not set")
   184  		}
   185  		chs[ix].Chunk = nil
   186  	}
   187  
   188  	// No samples, ignore.
   189  	if len(chs) == 0 {
   190  		return nil
   191  	}
   192  
   193  	minTime := chs[0].MinTime
   194  	maxTime := chs[len(chs)-1].MaxTime
   195  
   196  	err = d.series.addSeries(metric, chs, seriesSamples, minTime, maxTime)
   197  
   198  	d.seriesInMemory.Set(float64(d.series.unflushedSeries()))
   199  	d.writtenSamples.Add(float64(seriesSamples))
   200  	return err
   201  }
   202  
   203  func (d *tsdbBuilder) finishBlock(source string, labels map[string]string) (ulid.ULID, error) {
   204  	if err := d.unsortedChunksWriter.Close(); err != nil {
   205  		return ulid.ULID{}, errors.Wrap(err, "closing chunks writer")
   206  	}
   207  
   208  	if err := d.series.flushSeries(); err != nil {
   209  		return ulid.ULID{}, errors.Wrap(err, "flushing series")
   210  	}
   211  	d.seriesInMemory.Set(0)
   212  
   213  	level.Info(d.log).Log("msg", "all chunks fetched, building block index")
   214  
   215  	meta := &metadata.Meta{
   216  		BlockMeta: tsdb.BlockMeta{
   217  			ULID:    d.ulid,
   218  			Version: 1,
   219  			MinTime: int64(d.startTime),
   220  			MaxTime: int64(d.endTime),
   221  			Compaction: tsdb.BlockMetaCompaction{
   222  				Level:   1,
   223  				Sources: []ulid.ULID{d.ulid},
   224  			},
   225  		},
   226  
   227  		// We populate SegmentFiles (which is deprecated, but still used). The new Files property
   228  		// will be populated by Thanos block.Upload().
   229  		Thanos: metadata.Thanos{
   230  			Labels:       labels,
   231  			Source:       metadata.SourceType(source),
   232  			SegmentFiles: block.GetSegmentFiles(d.tmpBlockDir),
   233  		},
   234  	}
   235  
   236  	toClose := map[string]io.Closer{}
   237  	defer func() {
   238  		for k, c := range toClose {
   239  			err := c.Close()
   240  			if err != nil {
   241  				level.Error(d.log).Log("msg", "close failed", "name", k, "err", err)
   242  			}
   243  		}
   244  	}()
   245  
   246  	const (
   247  		indexWriterName          = "index writer"
   248  		unsortedChunksReaderName = "unsorted chunks reader"
   249  		chunksWriterName         = "chunks writer"
   250  	)
   251  
   252  	indexWriter, err := index.NewWriter(context.Background(), filepath.Join(d.tmpBlockDir, "index"))
   253  	if err != nil {
   254  		return ulid.ULID{}, errors.Wrap(err, indexWriterName)
   255  	}
   256  	toClose[indexWriterName] = indexWriter
   257  
   258  	symbols, err := addSymbolsToIndex(indexWriter, d.series)
   259  	if err != nil {
   260  		return ulid.ULID{}, errors.Wrap(err, "adding symbols")
   261  	}
   262  
   263  	level.Info(d.log).Log("msg", "added symbols to index", "count", symbols)
   264  
   265  	unsortedChunksReader, err := chunks.NewDirReader(filepath.Join(d.tmpBlockDir, unsortedChunksDir), nil)
   266  	if err != nil {
   267  		return ulid.ULID{}, errors.Wrap(err, unsortedChunksReaderName)
   268  	}
   269  	toClose[unsortedChunksReaderName] = unsortedChunksReader
   270  
   271  	chunksWriter, err := chunks.NewWriter(filepath.Join(d.tmpBlockDir, "chunks"))
   272  	if err != nil {
   273  		return ulid.ULID{}, errors.Wrap(err, chunksWriterName)
   274  	}
   275  	toClose[chunksWriterName] = chunksWriter
   276  
   277  	stats, err := addSeriesToIndex(indexWriter, d.series, unsortedChunksReader, chunksWriter)
   278  	if err != nil {
   279  		return ulid.ULID{}, errors.Wrap(err, "adding series")
   280  	}
   281  	meta.Stats = stats
   282  
   283  	level.Info(d.log).Log("msg", "added series to index", "series", stats.NumSeries, "chunks", stats.NumChunks, "samples", stats.NumSamples)
   284  
   285  	// Close index writer, unsorted chunks reader and chunks writer.
   286  	for k, c := range toClose {
   287  		delete(toClose, k)
   288  
   289  		err := c.Close()
   290  		if err != nil {
   291  			return ulid.ULID{}, errors.Wrapf(err, "closing %s", k)
   292  		}
   293  	}
   294  
   295  	// Delete unsorted chunks, they are no longer needed.
   296  	if err := os.RemoveAll(filepath.Join(d.tmpBlockDir, unsortedChunksDir)); err != nil {
   297  		return ulid.ULID{}, errors.Wrap(err, "deleting unsorted chunks")
   298  	}
   299  
   300  	if err := meta.WriteToDir(d.log, d.tmpBlockDir); err != nil {
   301  		return ulid.ULID{}, errors.Wrap(err, "writing meta.json")
   302  	}
   303  
   304  	if err := os.Rename(d.tmpBlockDir, filepath.Join(d.outDir, d.ulid.String())); err != nil {
   305  		return ulid.ULID{}, errors.Wrap(err, "rename to final directory")
   306  	}
   307  
   308  	return d.ulid, nil
   309  }
   310  
   311  func addSeriesToIndex(indexWriter *index.Writer, sl *seriesList, unsortedChunksReader *chunks.Reader, chunksWriter *chunks.Writer) (tsdb.BlockStats, error) {
   312  	var stats tsdb.BlockStats
   313  
   314  	it, err := sl.seriesIterator()
   315  	if err != nil {
   316  		return stats, errors.Wrap(err, "reading series")
   317  	}
   318  
   319  	type chunkToRead struct {
   320  		ref   uint64
   321  		chunk *chunkenc.Chunk
   322  		err   *error
   323  	}
   324  
   325  	ch := make(chan chunkToRead)
   326  	defer close(ch) // To make sure that goroutines stop.
   327  
   328  	// Number of chunks that should be loaded.
   329  	var pendingChunks sync.WaitGroup
   330  
   331  	// These goroutines read chunks into memory.
   332  	for n := 0; n < readChunksConcurrency; n++ {
   333  		go func() {
   334  			for ctr := range ch {
   335  				c, e := unsortedChunksReader.Chunk(ctr.ref)
   336  				*ctr.chunk = c
   337  				*ctr.err = e
   338  				pendingChunks.Done()
   339  			}
   340  		}()
   341  	}
   342  
   343  	seriesRef := 0
   344  	for s, ok := it.Next(); ok; s, ok = it.Next() {
   345  		l := s.Metric
   346  		cs := s.Chunks
   347  
   348  		readErrors := make([]error, len(cs))
   349  
   350  		// Read chunks into memory by asking goroutines to load them.
   351  		for ix := range cs {
   352  			pendingChunks.Add(1)
   353  			ch <- chunkToRead{
   354  				ref:   cs[ix].Ref,
   355  				chunk: &cs[ix].Chunk,
   356  				err:   &readErrors[ix],
   357  			}
   358  			cs[ix].Ref = 0
   359  		}
   360  
   361  		// Wait for all chunks to be fetched.
   362  		pendingChunks.Wait()
   363  
   364  		multi := tsdb_errors.NewMulti()
   365  		for _, e := range readErrors {
   366  			if e != nil {
   367  				multi.Add(e)
   368  			}
   369  		}
   370  		if err := multi.Err(); err != nil {
   371  			return stats, errors.Wrap(err, "failed to read chunks")
   372  		}
   373  
   374  		// Write chunks again. This time they will be written in the same order as series.
   375  		err = chunksWriter.WriteChunks(cs...)
   376  		if err != nil {
   377  			return stats, errors.Wrap(err, "failed to write sorted chunks")
   378  		}
   379  
   380  		// Remove chunks data from memory, but keep reference for writing to index.
   381  		for ix := range cs {
   382  			if cs[ix].Ref == 0 {
   383  				return stats, errors.Errorf("chunk ref not set after writing sorted chunks")
   384  			}
   385  			cs[ix].Chunk = nil
   386  		}
   387  
   388  		if err := indexWriter.AddSeries(uint64(seriesRef), l, cs...); err != nil {
   389  			return stats, errors.Wrapf(err, "adding series %v", l)
   390  		}
   391  
   392  		seriesRef++
   393  
   394  		stats.NumSamples += s.Samples
   395  		stats.NumSeries++
   396  		stats.NumChunks += uint64(len(cs))
   397  	}
   398  
   399  	return stats, nil
   400  }
   401  
   402  func addSymbolsToIndex(indexWriter *index.Writer, sl *seriesList) (int, error) {
   403  	symbols := 0
   404  	it, err := sl.symbolsIterator()
   405  	if err != nil {
   406  		return 0, errors.Wrap(err, "reading symbols")
   407  	}
   408  
   409  	for s, ok := it.Next(); ok; s, ok = it.Next() {
   410  		symbols++
   411  		if err := indexWriter.AddSymbol(s); err != nil {
   412  			_ = it.Close() // Make sure to close any open files.
   413  			return 0, errors.Wrapf(err, "adding symbol %v", s)
   414  		}
   415  	}
   416  	if err := it.Error(); err != nil {
   417  		_ = it.Close() // Make sure to close any open files.
   418  		return 0, err
   419  	}
   420  
   421  	if err := it.Close(); err != nil {
   422  		return 0, err
   423  	}
   424  
   425  	return symbols, nil
   426  }