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 }