Skip to content

Commit a0d4fcc

Browse files
authored
Parquet add no convert marker (#7625)
* parquetconverter: skip blocks with too many label names Changes: - Add parquet no-convert marker and read/write logic - Add max-block-label-names limit, blocks exceeding it get a no-convert marker instead of being converted. - Add parquet_converter_max_block_label_names to exporter test - Add integration test for parquet no-convert marker Signed-off-by: Siddarth Gundu <siddarthg0910@gmail.com> * Update config docs for max-block-label-names Signed-off-by: Siddarth Gundu <siddarthg0910@gmail.com> * parquetconverter: skip manually marked no-convert blocks The converter only read no-convert markers when the label-name limit was enabled, so manually marked blocks were still converted when the limit was 0. Read the marker unconditionally before conversion so these blocks stay skipped. Signed-off-by: Siddarth Gundu <siddarthg0910@gmail.com> * parquetconverter: track limits in no-convert Retry conversion if the current limit has increased beyond the label count stored in the old no-convert mark. Update converter tests for the new marker fields and retry behavior Signed-off-by: Siddarth Gundu <siddarthg0910@gmail.com> * parquetconverter: split no-convert skip reasons - Update tests to check skip with lower current limit Signed-off-by: Siddarth Gundu <siddarthg0910@gmail.com> * parquetconverter: use requires_docker build tag so integration test runs in CI Signed-off-by: Siddarth Gundu <siddarthg0910@gmail.com> --------- Signed-off-by: Siddarth Gundu <siddarthg0910@gmail.com>
1 parent 485a32d commit a0d4fcc

11 files changed

Lines changed: 502 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
* [FEATURE] StoreGateway: Add experimental optional limit `blocks-storage.bucket-store.max-concurrent-data-bytes` on the data bytes (postings, series and chunks) fetched via the Series() API call and processed concurrently across all queries per store gateway to protect from oomkill. This returns an error that is retryable at querier level. #7271
3636
* [ENHANCEMENT] Upgrade prometheus alertmanager version to v0.32.1. #7462
3737
* [ENHANCEMENT] Tenant Federation: Avoid purging the regex resolver LRU cache on user-sync ticks when the set of known users has not changed. #7489
38+
* [ENHANCEMENT] Parquet Converter: Add `parquet-converter.max-block-label-names` limit to skip conversion of TSDB blocks with too many label names. #7625
3839
* [ENHANCEMENT] Parquet Converter: Add a ring status page to expose the ring status. #7455
3940
* [ENHANCEMENT] Parquet: Add `-blocks-storage.bucket-store.parquet-query-concurrency` flag to configure the maximum number of concurrent goroutines applied at each level of parquet query processing in store-gateway: shard querying, row group processing, and column materialization. #7613
4041
* [ENHANCEMENT] Parquet: Add a row ranges cache for parquet query filtering in querier and store-gateway. #7478

docs/configuration/config-file-reference.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4914,6 +4914,12 @@ query_rejection:
49144914
# CLI flag: -parquet-converter.sort-columns
49154915
[parquet_converter_sort_columns: <list of string> | default = []]
49164916

4917+
# [Experimental] Maximum number of distinct label names allowed in a TSDB block
4918+
# for parquet conversion. If exceeded, the converter writes a no-convert marker.
4919+
# 0 to disable.
4920+
# CLI flag: -parquet-converter.max-block-label-names
4921+
[parquet_converter_max_block_label_names: <int> | default = 0]
4922+
49174923
# S3 server-side encryption type. Required to enable server-side encryption
49184924
# overrides for a specific tenant. If not set, the default S3 client settings
49194925
# are used.

docs/configuration/v1-guarantees.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,9 @@ Currently experimental features are:
158158
- Parquet storage
159159
- Parquet Converter: the `-parquet-converter.*` CLI flags, including `-parquet-converter.enabled`,
160160
`-parquet-converter.max-num-columns` (automatically shards parquet files when the number of columns
161-
exceeds the configured limit) and the `-parquet-converter.ring.*` ring configuration
161+
exceeds the configured limit), `-parquet-converter.max-block-label-names` (if enabled, adds a
162+
no-convert mark and skips blocks with too many label names) and the `-parquet-converter.ring.*`
163+
ring configuration
162164
- Querier: `-querier.parquet-queryable-default-block-store`, `-querier.parquet-queryable-fallback-disabled`,
163165
the `-querier.parquet-queryable.max-fetched-*` limits and `-querier.parquet-shard-cache-*`
164166
- Store Gateway: `-blocks-storage.bucket-store.parquet-query-concurrency`,
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
//go:build requires_docker
2+
3+
package integration
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"math/rand"
9+
"path/filepath"
10+
"testing"
11+
"time"
12+
13+
"github.com/prometheus/prometheus/model/labels"
14+
"github.com/stretchr/testify/require"
15+
"github.com/thanos-io/objstore"
16+
"github.com/thanos-io/thanos/pkg/block"
17+
"github.com/thanos-io/thanos/pkg/block/metadata"
18+
19+
"github.com/cortexproject/cortex/integration/e2e"
20+
e2ecache "github.com/cortexproject/cortex/integration/e2e/cache"
21+
e2edb "github.com/cortexproject/cortex/integration/e2e/db"
22+
"github.com/cortexproject/cortex/integration/e2ecortex"
23+
"github.com/cortexproject/cortex/pkg/storage/bucket"
24+
"github.com/cortexproject/cortex/pkg/storage/tsdb"
25+
"github.com/cortexproject/cortex/pkg/util/log"
26+
cortex_testutil "github.com/cortexproject/cortex/pkg/util/test"
27+
)
28+
29+
func TestParquetConverter_NoConvertMarkWithTooManyLabels(t *testing.T) {
30+
s, err := e2e.NewScenario(networkName)
31+
require.NoError(t, err)
32+
defer s.Close()
33+
34+
consul := e2edb.NewConsulWithName("consul")
35+
memcached := e2ecache.NewMemcached()
36+
require.NoError(t, s.StartAndWaitReady(consul, memcached))
37+
38+
baseFlags := mergeFlags(AlertmanagerLocalFlags(), BlocksStorageFlags())
39+
flags := mergeFlags(
40+
baseFlags,
41+
map[string]string{
42+
"-target": "all,parquet-converter",
43+
"-blocks-storage.tsdb.block-ranges-period": "1m,24h",
44+
"-blocks-storage.tsdb.ship-interval": "1s",
45+
"-blocks-storage.bucket-store.sync-interval": "1s",
46+
"-blocks-storage.bucket-store.metadata-cache.bucket-index-content-ttl": "1s",
47+
"-blocks-storage.bucket-store.bucket-index.idle-timeout": "1s",
48+
"-blocks-storage.bucket-store.bucket-index.enabled": "true",
49+
"-blocks-storage.bucket-store.index-cache.backend": tsdb.IndexCacheBackendInMemory,
50+
// compactor
51+
"-compactor.cleanup-interval": "1s",
52+
// Ingester.
53+
"-ring.store": "consul",
54+
"-consul.hostname": consul.NetworkHTTPEndpoint(),
55+
// Distributor.
56+
"-distributor.replication-factor": "1",
57+
// Store-gateway.
58+
"-store-gateway.sharding-enabled": "false",
59+
"--querier.store-gateway-addresses": "nonExistent", // Make sure we do not call Store gateways
60+
// alert manager
61+
"-alertmanager.web.external-url": "http://localhost/alertmanager",
62+
// Enable vertical sharding.
63+
"-frontend.query-vertical-shard-size": "3",
64+
"-frontend.max-cache-freshness": "1m",
65+
// enable experimental promQL funcs
66+
"-querier.enable-promql-experimental-functions": "true",
67+
// parquet-converter
68+
"-parquet-converter.ring.consul.hostname": consul.NetworkHTTPEndpoint(),
69+
"-parquet-converter.conversion-interval": "1s",
70+
"-parquet-converter.enabled": "true",
71+
"-parquet-converter.max-block-label-names": "1",
72+
// Querier
73+
"-querier.enable-parquet-queryable": "true",
74+
// Enable cache for parquet labels and chunks
75+
"-blocks-storage.bucket-store.parquet-labels-cache.backend": "inmemory,memcached",
76+
"-blocks-storage.bucket-store.parquet-labels-cache.memcached.addresses": "dns+" + memcached.NetworkEndpoint(e2ecache.MemcachedPort),
77+
"-blocks-storage.bucket-store.chunks-cache.backend": "inmemory,memcached",
78+
"-blocks-storage.bucket-store.chunks-cache.memcached.addresses": "dns+" + memcached.NetworkEndpoint(e2ecache.MemcachedPort),
79+
},
80+
)
81+
82+
// make alert manager config dir
83+
require.NoError(t, writeFileToSharedDir(s, "alertmanager_configs", []byte{}))
84+
85+
ctx := context.Background()
86+
rnd := rand.New(rand.NewSource(time.Now().Unix()))
87+
dir := filepath.Join(s.SharedDir(), "data")
88+
lbls := []labels.Labels{
89+
labels.FromStrings("__name__", "test_series_a", "job", "test"),
90+
}
91+
92+
numSamples := 60
93+
scrapeInterval := time.Minute
94+
now := time.Now()
95+
start := now.Add(-time.Hour * 24)
96+
end := now.Add(-time.Hour)
97+
98+
minio := e2edb.NewMinio(9000, flags["-blocks-storage.s3.bucket-name"])
99+
require.NoError(t, s.StartAndWaitReady(minio))
100+
101+
cortex := e2ecortex.NewSingleBinary("cortex", flags, "")
102+
require.NoError(t, s.StartAndWaitReady(cortex))
103+
storage, err := e2ecortex.NewS3ClientForMinio(minio, flags["-blocks-storage.s3.bucket-name"])
104+
require.NoError(t, err)
105+
bkt := bucket.NewUserBucketClient("user-1", storage.GetBucket(), nil)
106+
107+
id, err := e2e.CreateBlock(ctx, rnd, dir, lbls, numSamples,
108+
start.UnixMilli(),
109+
end.UnixMilli(),
110+
scrapeInterval.Milliseconds(), 10,
111+
)
112+
require.NoError(t, err)
113+
114+
err = block.Upload(ctx, log.Logger, bkt, filepath.Join(dir, id.String()), metadata.NoneFunc)
115+
require.NoError(t, err)
116+
117+
// Wait for the converter to write the no-convert marker
118+
cortex_testutil.Poll(t, 30*time.Second, true, func() interface{} {
119+
noConvertMarkerPath := fmt.Sprintf("%s/parquet-no-convert-mark.json", id.String())
120+
found := false
121+
err := bkt.Iter(ctx, "", func(name string) error {
122+
if name == noConvertMarkerPath {
123+
found = true
124+
}
125+
return nil
126+
}, objstore.WithRecursiveIter())
127+
require.NoError(t, err)
128+
return found
129+
})
130+
131+
// confirm the conversion did not happen (check both paths)
132+
blockID := id.String()
133+
markerPaths := []string{
134+
fmt.Sprintf("%s/parquet-converter-mark.json", blockID),
135+
fmt.Sprintf("parquet-markers/%s-parquet-converter-mark.json", blockID),
136+
}
137+
for _, markerPath := range markerPaths {
138+
exists, err := bkt.Exists(ctx, markerPath)
139+
require.NoError(t, err)
140+
require.False(t, exists, "converter mark should not exist at %s", markerPath)
141+
}
142+
}

pkg/parquetconverter/converter.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ const (
4949
ringKey = "parquet-converter"
5050

5151
converterMetaPrefix = "converter-meta-"
52+
53+
parquetConverterDataColumnDuration = time.Hour * 8
54+
parquetConverterSystemColumnCount = 2 // s_col_indexes and s_series_hash.
5255
)
5356

5457
var RingOp = ring.NewOp([]ring.InstanceState{ring.ACTIVE}, nil)
@@ -424,6 +427,29 @@ func (c *Converter) convertUser(ctx context.Context, logger log.Logger, ring rin
424427
continue
425428
}
426429

430+
configuredMaxBlockLabelNames := c.limits.ParquetConverterMaxBlockLabelNames(userID)
431+
maxBlockLabelNames := effectiveMaxBlockLabelNames(configuredMaxBlockLabelNames, b.MinTime, b.MaxTime)
432+
433+
noConvertMark, err := cortex_parquet.ReadNoConvertMark(ctx, b.ULID, uBucket, logger)
434+
if err != nil {
435+
level.Error(logger).Log("msg", "failed to read parquet no-convert marker", "block", b.ULID.String(), "err", err)
436+
continue
437+
}
438+
439+
if cortex_parquet.ValidNoConvertMarkVersion(noConvertMark.Version) {
440+
if noConvertMark.Reason != cortex_parquet.NoConvertReasonTooManyLabels {
441+
level.Debug(logger).Log("msg", "skipping block, no-convert marker already exists", "block", b.ULID.String())
442+
c.metrics.skippedBlocks.WithLabelValues(userID, cortex_parquet.NoConvertReasonMarkerExists).Inc()
443+
continue
444+
}
445+
446+
if noConvertMark.ShouldSkipBlock(maxBlockLabelNames) {
447+
level.Debug(logger).Log("msg", "skipping block because label count still exceeds current limit", "block", b.ULID.String(), "label_names_count", noConvertMark.LabelNamesCount, "current_limit", maxBlockLabelNames)
448+
c.metrics.skippedBlocks.WithLabelValues(userID, cortex_parquet.NoConvertReasonTooManyLabels).Inc()
449+
continue
450+
}
451+
}
452+
427453
if err := os.RemoveAll(c.compactRootDir()); err != nil {
428454
level.Error(logger).Log("msg", "failed to remove work directory", "path", c.compactRootDir(), "err", err)
429455
if c.checkConvertError(userID, err) {
@@ -453,6 +479,33 @@ func (c *Converter) convertUser(ctx context.Context, logger log.Logger, ring rin
453479
continue
454480
}
455481

482+
if configuredMaxBlockLabelNames > 0 {
483+
labelNames, err := tsdbBlock.LabelNames(ctx)
484+
if err != nil {
485+
_ = tsdbBlock.Close()
486+
level.Error(logger).Log("msg", "failed to get label names", "block", b.ULID.String(), "err", err)
487+
if c.checkConvertError(userID, err) {
488+
return err
489+
}
490+
continue
491+
}
492+
labelNamesCount := len(labelNames)
493+
if labelNamesCount > maxBlockLabelNames {
494+
if err := cortex_parquet.WriteNoConvertMark(ctx, b.ULID, uBucket, labelNamesCount, maxBlockLabelNames); err != nil {
495+
_ = tsdbBlock.Close()
496+
level.Error(logger).Log("msg", "failed to write parquet no-convert marker", "block", b.ULID.String(), "err", err)
497+
if c.checkConvertError(userID, err) {
498+
return err
499+
}
500+
continue
501+
}
502+
level.Debug(logger).Log("msg", "skipping parquet conversion for block with too many label names", "block", b.ULID.String(), "label_names", labelNamesCount, "limit", maxBlockLabelNames)
503+
c.metrics.skippedBlocks.WithLabelValues(userID, cortex_parquet.NoConvertReasonTooManyLabels).Inc()
504+
_ = tsdbBlock.Close()
505+
continue
506+
}
507+
}
508+
456509
level.Info(logger).Log("msg", "converting block", "block", b.ULID.String(), "dir", bdir)
457510
start := time.Now()
458511

@@ -514,6 +567,25 @@ func (c *Converter) convertUser(ctx context.Context, logger log.Logger, ring rin
514567
return nil
515568
}
516569

570+
func effectiveMaxBlockLabelNames(configuredMaxBlockLabelNames int, mint, maxt int64) int {
571+
if configuredMaxBlockLabelNames <= 0 {
572+
return configuredMaxBlockLabelNames
573+
}
574+
575+
dataColumnCount := 0
576+
if maxt >= mint {
577+
dataColumnCount = int((maxt-mint)/parquetConverterDataColumnDuration.Milliseconds()) + 1
578+
}
579+
580+
// Reserve for s_col_indexes, s_series_hash, and generated s_data_* columns.
581+
maxBlockLabelNames := max(parquet.MaxColumnIndex-parquetConverterSystemColumnCount-dataColumnCount, 0)
582+
583+
if configuredMaxBlockLabelNames > maxBlockLabelNames {
584+
return maxBlockLabelNames
585+
}
586+
return configuredMaxBlockLabelNames
587+
}
588+
517589
func (c *Converter) checkConvertError(userID string, err error) (terminate bool) {
518590
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || c.isCausedByPermissionDenied(err) {
519591
terminate = true

0 commit comments

Comments
 (0)