github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/pkg/storage/stores/tsdb/head_manager_test.go (about)

     1  package tsdb
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math"
     7  	"sync"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/go-kit/log"
    12  	"github.com/prometheus/common/model"
    13  	"github.com/prometheus/prometheus/model/labels"
    14  	"github.com/prometheus/prometheus/tsdb/chunks"
    15  	"github.com/prometheus/prometheus/tsdb/record"
    16  	"github.com/stretchr/testify/require"
    17  
    18  	"github.com/grafana/loki/pkg/storage/chunk/client/util"
    19  	"github.com/grafana/loki/pkg/storage/stores/tsdb/index"
    20  )
    21  
    22  type noopTSDBManager struct {
    23  	dir string
    24  	*tenantHeads
    25  }
    26  
    27  func newNoopTSDBManager(dir string) noopTSDBManager {
    28  	return noopTSDBManager{
    29  		dir:         dir,
    30  		tenantHeads: newTenantHeads(time.Now(), defaultHeadManagerStripeSize, NewMetrics(nil), log.NewNopLogger()),
    31  	}
    32  }
    33  
    34  func (m noopTSDBManager) BuildFromWALs(_ time.Time, wals []WALIdentifier) error {
    35  	return recoverHead(m.dir, m.tenantHeads, wals)
    36  }
    37  func (m noopTSDBManager) Start() error { return nil }
    38  
    39  func chunkMetasToChunkRefs(user string, fp uint64, xs index.ChunkMetas) (res []ChunkRef) {
    40  	for _, x := range xs {
    41  		res = append(res, ChunkRef{
    42  			User:        user,
    43  			Fingerprint: model.Fingerprint(fp),
    44  			Start:       x.From(),
    45  			End:         x.Through(),
    46  			Checksum:    x.Checksum,
    47  		})
    48  	}
    49  	return
    50  }
    51  
    52  // Test append
    53  func Test_TenantHeads_Append(t *testing.T) {
    54  	h := newTenantHeads(time.Now(), defaultHeadManagerStripeSize, NewMetrics(nil), log.NewNopLogger())
    55  	ls := mustParseLabels(`{foo="bar"}`)
    56  	chks := []index.ChunkMeta{
    57  		{
    58  			Checksum: 0,
    59  			MinTime:  1,
    60  			MaxTime:  10,
    61  			KB:       2,
    62  			Entries:  30,
    63  		},
    64  	}
    65  	_ = h.Append("fake", ls, chks)
    66  
    67  	found, err := h.GetChunkRefs(
    68  		context.Background(),
    69  		"fake",
    70  		0,
    71  		100,
    72  		nil, nil,
    73  		labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"),
    74  	)
    75  	require.Nil(t, err)
    76  	require.Equal(t, chunkMetasToChunkRefs("fake", ls.Hash(), chks), found)
    77  
    78  }
    79  
    80  // Test multitenant reads
    81  func Test_TenantHeads_MultiRead(t *testing.T) {
    82  	h := newTenantHeads(time.Now(), defaultHeadManagerStripeSize, NewMetrics(nil), log.NewNopLogger())
    83  	ls := mustParseLabels(`{foo="bar"}`)
    84  	chks := []index.ChunkMeta{
    85  		{
    86  			Checksum: 0,
    87  			MinTime:  1,
    88  			MaxTime:  10,
    89  			KB:       2,
    90  			Entries:  30,
    91  		},
    92  	}
    93  
    94  	tenants := []struct {
    95  		user string
    96  		ls   labels.Labels
    97  	}{
    98  		{
    99  			user: "tenant1",
   100  			ls: append(ls.Copy(), labels.Label{
   101  				Name:  "tenant",
   102  				Value: "tenant1",
   103  			}),
   104  		},
   105  		{
   106  			user: "tenant2",
   107  			ls: append(ls.Copy(), labels.Label{
   108  				Name:  "tenant",
   109  				Value: "tenant2",
   110  			}),
   111  		},
   112  	}
   113  
   114  	// add data for both tenants
   115  	for _, tenant := range tenants {
   116  		_ = h.Append(tenant.user, tenant.ls, chks)
   117  
   118  	}
   119  
   120  	// ensure we're only returned the data from the correct tenant
   121  	for _, tenant := range tenants {
   122  		found, err := h.GetChunkRefs(
   123  			context.Background(),
   124  			tenant.user,
   125  			0,
   126  			100,
   127  			nil, nil,
   128  			labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"),
   129  		)
   130  		require.Nil(t, err)
   131  		require.Equal(t, chunkMetasToChunkRefs(tenant.user, tenant.ls.Hash(), chks), found)
   132  	}
   133  
   134  }
   135  
   136  // test head recover from wal
   137  func Test_HeadManager_RecoverHead(t *testing.T) {
   138  	now := time.Now()
   139  	dir := t.TempDir()
   140  	cases := []struct {
   141  		Labels labels.Labels
   142  		Chunks []index.ChunkMeta
   143  		User   string
   144  	}{
   145  		{
   146  			User:   "tenant1",
   147  			Labels: mustParseLabels(`{foo="bar", bazz="buzz"}`),
   148  			Chunks: []index.ChunkMeta{
   149  				{
   150  					MinTime:  1,
   151  					MaxTime:  10,
   152  					Checksum: 3,
   153  				},
   154  			},
   155  		},
   156  		{
   157  			User:   "tenant2",
   158  			Labels: mustParseLabels(`{foo="bard", bazz="bozz", bonk="borb"}`),
   159  			Chunks: []index.ChunkMeta{
   160  				{
   161  					MinTime:  1,
   162  					MaxTime:  7,
   163  					Checksum: 4,
   164  				},
   165  			},
   166  		},
   167  	}
   168  
   169  	mgr := NewHeadManager(log.NewNopLogger(), dir, nil, newNoopTSDBManager(dir))
   170  	// This bit is normally handled by the Start() fn, but we're testing a smaller surface area
   171  	// so ensure our dirs exist
   172  	for _, d := range managerRequiredDirs(dir) {
   173  		require.Nil(t, util.EnsureDirectory(d))
   174  	}
   175  
   176  	// Call Rotate() to ensure the new head tenant heads exist, etc
   177  	require.Nil(t, mgr.Rotate(now))
   178  
   179  	// now build a WAL independently to test recovery
   180  	w, err := newHeadWAL(log.NewNopLogger(), walPath(mgr.dir, now), now)
   181  	require.Nil(t, err)
   182  
   183  	for i, c := range cases {
   184  		require.Nil(t, w.Log(&WALRecord{
   185  			UserID: c.User,
   186  			Series: record.RefSeries{
   187  				Ref:    chunks.HeadSeriesRef(i),
   188  				Labels: c.Labels,
   189  			},
   190  			Chks: ChunkMetasRecord{
   191  				Chks: c.Chunks,
   192  				Ref:  uint64(i),
   193  			},
   194  		}))
   195  	}
   196  
   197  	require.Nil(t, w.Stop())
   198  
   199  	grp, ok, err := walsForPeriod(mgr.dir, mgr.period, mgr.period.PeriodFor(now))
   200  	require.Nil(t, err)
   201  	require.True(t, ok)
   202  	require.Equal(t, 1, len(grp.wals))
   203  	require.Nil(t, recoverHead(mgr.dir, mgr.activeHeads, grp.wals))
   204  
   205  	for _, c := range cases {
   206  		refs, err := mgr.GetChunkRefs(
   207  			context.Background(),
   208  			c.User,
   209  			0, math.MaxInt64,
   210  			nil, nil,
   211  			labels.MustNewMatcher(labels.MatchRegexp, "foo", ".+"),
   212  		)
   213  		require.Nil(t, err)
   214  		require.Equal(t, chunkMetasToChunkRefs(c.User, c.Labels.Hash(), c.Chunks), refs)
   215  	}
   216  
   217  }
   218  
   219  // test mgr recover from multiple wals across multiple periods
   220  func Test_HeadManager_Lifecycle(t *testing.T) {
   221  	dir := t.TempDir()
   222  	curPeriod := time.Now()
   223  	cases := []struct {
   224  		Labels labels.Labels
   225  		Chunks []index.ChunkMeta
   226  		User   string
   227  	}{
   228  		{
   229  			User:   "tenant1",
   230  			Labels: mustParseLabels(`{foo="bar", bazz="buzz"}`),
   231  			Chunks: []index.ChunkMeta{
   232  				{
   233  					MinTime:  1,
   234  					MaxTime:  10,
   235  					Checksum: 3,
   236  				},
   237  			},
   238  		},
   239  		{
   240  			User:   "tenant2",
   241  			Labels: mustParseLabels(`{foo="bard", bazz="bozz", bonk="borb"}`),
   242  			Chunks: []index.ChunkMeta{
   243  				{
   244  					MinTime:  1,
   245  					MaxTime:  7,
   246  					Checksum: 4,
   247  				},
   248  			},
   249  		},
   250  	}
   251  
   252  	mgr := NewHeadManager(log.NewNopLogger(), dir, nil, newNoopTSDBManager(dir))
   253  	w, err := newHeadWAL(log.NewNopLogger(), walPath(mgr.dir, curPeriod), curPeriod)
   254  	require.Nil(t, err)
   255  
   256  	// Write old WALs
   257  	for i, c := range cases {
   258  		require.Nil(t, w.Log(&WALRecord{
   259  			UserID: c.User,
   260  			Series: record.RefSeries{
   261  				Ref:    chunks.HeadSeriesRef(i),
   262  				Labels: c.Labels,
   263  			},
   264  			Chks: ChunkMetasRecord{
   265  				Chks: c.Chunks,
   266  				Ref:  uint64(i),
   267  			},
   268  		}))
   269  	}
   270  
   271  	require.Nil(t, w.Stop())
   272  
   273  	// Start, ensuring recovery from old WALs
   274  	require.Nil(t, mgr.Start())
   275  
   276  	// Ensure old WAL data is queryable
   277  	multiIndex, err := NewMultiIndex(mgr, mgr.tsdbManager.(noopTSDBManager).tenantHeads)
   278  	require.Nil(t, err)
   279  
   280  	for _, c := range cases {
   281  		refs, err := multiIndex.GetChunkRefs(
   282  			context.Background(),
   283  			c.User,
   284  			0, math.MaxInt64,
   285  			nil, nil,
   286  			labels.MustNewMatcher(labels.MatchRegexp, "foo", ".+"),
   287  		)
   288  		require.Nil(t, err)
   289  
   290  		lbls := labels.NewBuilder(c.Labels)
   291  		lbls.Set(TenantLabel, c.User)
   292  		require.Equal(t, chunkMetasToChunkRefs(c.User, c.Labels.Hash(), c.Chunks), refs)
   293  	}
   294  
   295  	// Add data
   296  	newCase := struct {
   297  		Labels labels.Labels
   298  		Chunks []index.ChunkMeta
   299  		User   string
   300  	}{
   301  		User:   "tenant3",
   302  		Labels: mustParseLabels(`{foo="bard", other="hi"}`),
   303  		Chunks: []index.ChunkMeta{
   304  			{
   305  				MinTime:  1,
   306  				MaxTime:  7,
   307  				Checksum: 4,
   308  			},
   309  		},
   310  	}
   311  
   312  	require.Nil(t, mgr.Append(newCase.User, newCase.Labels, newCase.Chunks))
   313  
   314  	// Ensure old + new data is queryable
   315  	for _, c := range append(cases, newCase) {
   316  		refs, err := multiIndex.GetChunkRefs(
   317  			context.Background(),
   318  			c.User,
   319  			0, math.MaxInt64,
   320  			nil, nil,
   321  			labels.MustNewMatcher(labels.MatchRegexp, "foo", ".+"),
   322  		)
   323  		require.Nil(t, err)
   324  
   325  		lbls := labels.NewBuilder(c.Labels)
   326  		lbls.Set(TenantLabel, c.User)
   327  		require.Equal(t, chunkMetasToChunkRefs(c.User, c.Labels.Hash(), c.Chunks), refs)
   328  	}
   329  }
   330  
   331  func BenchmarkTenantHeads(b *testing.B) {
   332  	for _, tc := range []struct {
   333  		readers, writers int
   334  	}{
   335  		{
   336  			readers: 10,
   337  		},
   338  		{
   339  			readers: 100,
   340  		},
   341  		{
   342  			readers: 1000,
   343  		},
   344  	} {
   345  		b.Run(fmt.Sprintf("%d", tc.readers), func(b *testing.B) {
   346  			heads := newTenantHeads(time.Now(), defaultHeadManagerStripeSize, NewMetrics(nil), log.NewNopLogger())
   347  			// 1000 series across 100 tenants
   348  			nTenants := 10
   349  			for i := 0; i < 1000; i++ {
   350  				tenant := i % nTenants
   351  				ls := mustParseLabels(fmt.Sprintf(`{foo="bar", i="%d"}`, i))
   352  				heads.Append(fmt.Sprint(tenant), ls, index.ChunkMetas{
   353  					{},
   354  				})
   355  			}
   356  
   357  			for n := 0; n < b.N; n++ {
   358  				var wg sync.WaitGroup
   359  				for r := 0; r < tc.readers; r++ {
   360  					wg.Add(1)
   361  					go func(r int) {
   362  						defer wg.Done()
   363  						var res []ChunkRef
   364  						tenant := r % nTenants
   365  
   366  						// nolint:ineffassign,staticcheck
   367  						res, _ = heads.GetChunkRefs(
   368  							context.Background(),
   369  							fmt.Sprint(tenant),
   370  							0, math.MaxInt64,
   371  							res,
   372  							nil,
   373  							labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"),
   374  						)
   375  					}(r)
   376  				}
   377  
   378  				wg.Wait()
   379  			}
   380  
   381  		})
   382  	}
   383  }