github.com/sequix/cortex@v1.1.6/pkg/chunk/gcp/bigtable_index_client.go (about)

     1  package gcp
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/binary"
     7  	"encoding/hex"
     8  	"flag"
     9  	"fmt"
    10  	"strings"
    11  
    12  	"cloud.google.com/go/bigtable"
    13  	ot "github.com/opentracing/opentracing-go"
    14  	otlog "github.com/opentracing/opentracing-go/log"
    15  
    16  	"github.com/sequix/cortex/pkg/chunk"
    17  	chunk_util "github.com/sequix/cortex/pkg/chunk/util"
    18  	"github.com/sequix/cortex/pkg/util"
    19  	"github.com/sequix/cortex/pkg/util/grpcclient"
    20  	"github.com/pkg/errors"
    21  )
    22  
    23  const (
    24  	columnFamily = "f"
    25  	columnPrefix = columnFamily + ":"
    26  	column       = "c"
    27  	separator    = "\000"
    28  	maxRowReads  = 100
    29  	null         = string('\xff')
    30  )
    31  
    32  // Config for a StorageClient
    33  type Config struct {
    34  	Project  string `yaml:"project"`
    35  	Instance string `yaml:"instance"`
    36  
    37  	GRPCClientConfig grpcclient.Config `yaml:"grpc_client_config"`
    38  
    39  	ColumnKey      bool
    40  	DistributeKeys bool
    41  }
    42  
    43  // RegisterFlags adds the flags required to config this to the given FlagSet
    44  func (cfg *Config) RegisterFlags(f *flag.FlagSet) {
    45  	f.StringVar(&cfg.Project, "bigtable.project", "", "Bigtable project ID.")
    46  	f.StringVar(&cfg.Instance, "bigtable.instance", "", "Bigtable instance ID.")
    47  
    48  	cfg.GRPCClientConfig.RegisterFlags("bigtable", f)
    49  }
    50  
    51  // storageClientColumnKey implements chunk.storageClient for GCP.
    52  type storageClientColumnKey struct {
    53  	cfg       Config
    54  	schemaCfg chunk.SchemaConfig
    55  	client    *bigtable.Client
    56  	keysFn    keysFn
    57  
    58  	distributeKeys bool
    59  }
    60  
    61  // storageClientV1 implements chunk.storageClient for GCP.
    62  type storageClientV1 struct {
    63  	storageClientColumnKey
    64  }
    65  
    66  // NewStorageClientV1 returns a new v1 StorageClient.
    67  func NewStorageClientV1(ctx context.Context, cfg Config, schemaCfg chunk.SchemaConfig) (chunk.IndexClient, error) {
    68  	opts := toOptions(cfg.GRPCClientConfig.DialOption(bigtableInstrumentation()))
    69  	client, err := bigtable.NewClient(ctx, cfg.Project, cfg.Instance, opts...)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  	return newStorageClientV1(cfg, schemaCfg, client), nil
    74  }
    75  
    76  func newStorageClientV1(cfg Config, schemaCfg chunk.SchemaConfig, client *bigtable.Client) *storageClientV1 {
    77  	return &storageClientV1{
    78  		storageClientColumnKey{
    79  			cfg:       cfg,
    80  			schemaCfg: schemaCfg,
    81  			client:    client,
    82  			keysFn: func(hashValue string, rangeValue []byte) (string, string) {
    83  				rowKey := hashValue + separator + string(rangeValue)
    84  				return rowKey, column
    85  			},
    86  		},
    87  	}
    88  }
    89  
    90  // NewStorageClientColumnKey returns a new v2 StorageClient.
    91  func NewStorageClientColumnKey(ctx context.Context, cfg Config, schemaCfg chunk.SchemaConfig) (chunk.IndexClient, error) {
    92  	opts := toOptions(cfg.GRPCClientConfig.DialOption(bigtableInstrumentation()))
    93  	client, err := bigtable.NewClient(ctx, cfg.Project, cfg.Instance, opts...)
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  	return newStorageClientColumnKey(cfg, schemaCfg, client), nil
    98  }
    99  
   100  func newStorageClientColumnKey(cfg Config, schemaCfg chunk.SchemaConfig, client *bigtable.Client) *storageClientColumnKey {
   101  
   102  	return &storageClientColumnKey{
   103  		cfg:       cfg,
   104  		schemaCfg: schemaCfg,
   105  		client:    client,
   106  		keysFn: func(hashValue string, rangeValue []byte) (string, string) {
   107  
   108  			// We hash the row key and prepend it back to the key for better distribution.
   109  			// We preserve the existing key to make migrations and o11y easier.
   110  			if cfg.DistributeKeys {
   111  				hashValue = hashPrefix(hashValue) + "-" + hashValue
   112  			}
   113  
   114  			return hashValue, string(rangeValue)
   115  		},
   116  	}
   117  }
   118  
   119  // hashPrefix calculates a 64bit hash of the input string and hex-encodes
   120  // the result, taking care to zero pad etc.
   121  func hashPrefix(input string) string {
   122  	prefix := hashAdd(hashNew(), input)
   123  	var encodedUint64 [8]byte
   124  	binary.LittleEndian.PutUint64(encodedUint64[:], prefix)
   125  	var hexEncoded [16]byte
   126  	hex.Encode(hexEncoded[:], encodedUint64[:])
   127  	return string(hexEncoded[:])
   128  }
   129  
   130  func (s *storageClientColumnKey) Stop() {
   131  	s.client.Close()
   132  }
   133  
   134  func (s *storageClientColumnKey) NewWriteBatch() chunk.WriteBatch {
   135  	return bigtableWriteBatch{
   136  		tables: map[string]map[string]*bigtable.Mutation{},
   137  		keysFn: s.keysFn,
   138  	}
   139  }
   140  
   141  // keysFn returns the row and column keys for the given hash and range keys.
   142  type keysFn func(hashValue string, rangeValue []byte) (rowKey, columnKey string)
   143  
   144  type bigtableWriteBatch struct {
   145  	tables map[string]map[string]*bigtable.Mutation
   146  	keysFn keysFn
   147  }
   148  
   149  func (b bigtableWriteBatch) Add(tableName, hashValue string, rangeValue []byte, value []byte) {
   150  	rows, ok := b.tables[tableName]
   151  	if !ok {
   152  		rows = map[string]*bigtable.Mutation{}
   153  		b.tables[tableName] = rows
   154  	}
   155  
   156  	rowKey, columnKey := b.keysFn(hashValue, rangeValue)
   157  	mutation, ok := rows[rowKey]
   158  	if !ok {
   159  		mutation = bigtable.NewMutation()
   160  		rows[rowKey] = mutation
   161  	}
   162  
   163  	mutation.Set(columnFamily, columnKey, 0, value)
   164  }
   165  
   166  func (s *storageClientColumnKey) BatchWrite(ctx context.Context, batch chunk.WriteBatch) error {
   167  	bigtableBatch := batch.(bigtableWriteBatch)
   168  
   169  	for tableName, rows := range bigtableBatch.tables {
   170  		table := s.client.Open(tableName)
   171  		rowKeys := make([]string, 0, len(rows))
   172  		muts := make([]*bigtable.Mutation, 0, len(rows))
   173  		for rowKey, mut := range rows {
   174  			rowKeys = append(rowKeys, rowKey)
   175  			muts = append(muts, mut)
   176  		}
   177  
   178  		errs, err := table.ApplyBulk(ctx, rowKeys, muts)
   179  		if err != nil {
   180  			return err
   181  		}
   182  		for _, err := range errs {
   183  			if err != nil {
   184  				return err
   185  			}
   186  		}
   187  	}
   188  
   189  	return nil
   190  }
   191  
   192  func (s *storageClientColumnKey) QueryPages(ctx context.Context, queries []chunk.IndexQuery, callback func(chunk.IndexQuery, chunk.ReadBatch) bool) error {
   193  	sp, ctx := ot.StartSpanFromContext(ctx, "QueryPages")
   194  	defer sp.Finish()
   195  
   196  	// A limitation of this approach is that this only fetches whole rows; but
   197  	// whatever, we filter them in the cache on the client.  But for unit tests to
   198  	// pass, we must do this.
   199  	callback = chunk_util.QueryFilter(callback)
   200  
   201  	type tableQuery struct {
   202  		name    string
   203  		queries map[string]chunk.IndexQuery
   204  		rows    bigtable.RowList
   205  	}
   206  
   207  	tableQueries := map[string]tableQuery{}
   208  	for _, query := range queries {
   209  		tq, ok := tableQueries[query.TableName]
   210  		if !ok {
   211  			tq = tableQuery{
   212  				name:    query.TableName,
   213  				queries: map[string]chunk.IndexQuery{},
   214  			}
   215  		}
   216  		hashKey, _ := s.keysFn(query.HashValue, nil)
   217  		tq.queries[hashKey] = query
   218  		tq.rows = append(tq.rows, hashKey)
   219  		tableQueries[query.TableName] = tq
   220  	}
   221  
   222  	errs := make(chan error)
   223  	for _, tq := range tableQueries {
   224  		table := s.client.Open(tq.name)
   225  
   226  		for i := 0; i < len(tq.rows); i += maxRowReads {
   227  			page := tq.rows[i:util.Min(i+maxRowReads, len(tq.rows))]
   228  			go func(page bigtable.RowList, tq tableQuery) {
   229  				var processingErr error
   230  				// rows are returned in key order, not order in row list
   231  				err := table.ReadRows(ctx, page, func(row bigtable.Row) bool {
   232  					query, ok := tq.queries[row.Key()]
   233  					if !ok {
   234  						processingErr = errors.WithStack(fmt.Errorf("Got row for unknown chunk: %s", row.Key()))
   235  						return false
   236  					}
   237  
   238  					val, ok := row[columnFamily]
   239  					if !ok {
   240  						// There are no matching rows.
   241  						return true
   242  					}
   243  
   244  					return callback(query, &columnKeyBatch{
   245  						items: val,
   246  					})
   247  				})
   248  
   249  				if processingErr != nil {
   250  					errs <- processingErr
   251  				} else {
   252  					errs <- err
   253  				}
   254  			}(page, tq)
   255  		}
   256  	}
   257  
   258  	var lastErr error
   259  	for _, tq := range tableQueries {
   260  		for i := 0; i < len(tq.rows); i += maxRowReads {
   261  			err := <-errs
   262  			if err != nil {
   263  				lastErr = err
   264  			}
   265  		}
   266  	}
   267  	return lastErr
   268  }
   269  
   270  // columnKeyBatch represents a batch of values read from Bigtable.
   271  type columnKeyBatch struct {
   272  	items []bigtable.ReadItem
   273  }
   274  
   275  func (c *columnKeyBatch) Iterator() chunk.ReadBatchIterator {
   276  	return &columnKeyIterator{
   277  		i:              -1,
   278  		columnKeyBatch: c,
   279  	}
   280  }
   281  
   282  type columnKeyIterator struct {
   283  	i int
   284  	*columnKeyBatch
   285  }
   286  
   287  func (c *columnKeyIterator) Next() bool {
   288  	c.i++
   289  	return c.i < len(c.items)
   290  }
   291  
   292  func (c *columnKeyIterator) RangeValue() []byte {
   293  	return []byte(strings.TrimPrefix(c.items[c.i].Column, columnPrefix))
   294  }
   295  
   296  func (c *columnKeyIterator) Value() []byte {
   297  	return c.items[c.i].Value
   298  }
   299  
   300  func (s *storageClientV1) QueryPages(ctx context.Context, queries []chunk.IndexQuery, callback func(chunk.IndexQuery, chunk.ReadBatch) bool) error {
   301  	return chunk_util.DoParallelQueries(ctx, s.query, queries, callback)
   302  }
   303  
   304  func (s *storageClientV1) query(ctx context.Context, query chunk.IndexQuery, callback func(result chunk.ReadBatch) (shouldContinue bool)) error {
   305  	const null = string('\xff')
   306  
   307  	sp, ctx := ot.StartSpanFromContext(ctx, "QueryPages", ot.Tag{Key: "tableName", Value: query.TableName}, ot.Tag{Key: "hashValue", Value: query.HashValue})
   308  	defer sp.Finish()
   309  
   310  	table := s.client.Open(query.TableName)
   311  
   312  	var rowRange bigtable.RowRange
   313  
   314  	/* Bigtable only seems to support regex match on cell values, so doing it
   315  	   client side for now
   316  	readOpts := []bigtable.ReadOption{
   317  		bigtable.RowFilter(bigtable.FamilyFilter(columnFamily)),
   318  	}
   319  	if query.ValueEqual != nil {
   320  		readOpts = append(readOpts, bigtable.RowFilter(bigtable.ValueFilter(string(query.ValueEqual))))
   321  	}
   322  	*/
   323  	if len(query.RangeValuePrefix) > 0 {
   324  		rowRange = bigtable.PrefixRange(query.HashValue + separator + string(query.RangeValuePrefix))
   325  	} else if len(query.RangeValueStart) > 0 {
   326  		rowRange = bigtable.NewRange(query.HashValue+separator+string(query.RangeValueStart), query.HashValue+separator+null)
   327  	} else {
   328  		rowRange = bigtable.PrefixRange(query.HashValue + separator)
   329  	}
   330  
   331  	err := table.ReadRows(ctx, rowRange, func(r bigtable.Row) bool {
   332  		if query.ValueEqual == nil || bytes.Equal(r[columnFamily][0].Value, query.ValueEqual) {
   333  			return callback(&rowBatch{
   334  				row: r,
   335  			})
   336  		}
   337  
   338  		return true
   339  	})
   340  	if err != nil {
   341  		sp.LogFields(otlog.String("error", err.Error()))
   342  		return errors.WithStack(err)
   343  	}
   344  	return nil
   345  }
   346  
   347  // rowBatch represents a batch of rows read from Bigtable.  As the
   348  // bigtable interface gives us rows one-by-one, a batch always only contains
   349  // a single row.
   350  type rowBatch struct {
   351  	row bigtable.Row
   352  }
   353  
   354  func (b *rowBatch) Iterator() chunk.ReadBatchIterator {
   355  	return &rowBatchIterator{
   356  		rowBatch: b,
   357  	}
   358  }
   359  
   360  type rowBatchIterator struct {
   361  	consumed bool
   362  	*rowBatch
   363  }
   364  
   365  func (b *rowBatchIterator) Next() bool {
   366  	if b.consumed {
   367  		return false
   368  	}
   369  	b.consumed = true
   370  	return true
   371  }
   372  
   373  func (b *rowBatchIterator) RangeValue() []byte {
   374  	// String before the first separator is the hashkey
   375  	parts := strings.SplitN(b.row.Key(), separator, 2)
   376  	return []byte(parts[1])
   377  }
   378  
   379  func (b *rowBatchIterator) Value() []byte {
   380  	cf, ok := b.row[columnFamily]
   381  	if !ok || len(cf) != 1 {
   382  		panic("bad response from bigtable")
   383  	}
   384  	return cf[0].Value
   385  }