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 }