diff --git a/br/pkg/conn/util/util.go b/br/pkg/conn/util/util.go new file mode 100644 index 0000000000000..f569edd03ed2b --- /dev/null +++ b/br/pkg/conn/util/util.go @@ -0,0 +1,63 @@ +package util + +import ( + "context" + + "github.com/pingcap/errors" + "github.com/pingcap/kvproto/pkg/metapb" + pd "github.com/tikv/pd/client" + + errors2 "github.com/pingcap/tidb/br/pkg/errors" + "github.com/pingcap/tidb/br/pkg/version" +) + +// GetAllTiKVStores returns all TiKV stores registered to the PD client. The +// stores must not be a tombstone and must never contain a label `engine=tiflash`. +func GetAllTiKVStores( + ctx context.Context, + pdClient pd.Client, + storeBehavior StoreBehavior, +) ([]*metapb.Store, error) { + // get all live stores. + stores, err := pdClient.GetAllStores(ctx, pd.WithExcludeTombstone()) + if err != nil { + return nil, errors.Trace(err) + } + + // filter out all stores which are TiFlash. + j := 0 + for _, store := range stores { + isTiFlash := false + if version.IsTiFlash(store) { + if storeBehavior == SkipTiFlash { + continue + } else if storeBehavior == ErrorOnTiFlash { + return nil, errors.Annotatef(errors2.ErrPDInvalidResponse, + "cannot restore to a cluster with active TiFlash stores (store %d at %s)", store.Id, store.Address) + } + isTiFlash = true + } + if !isTiFlash && storeBehavior == TiFlashOnly { + continue + } + stores[j] = store + j++ + } + return stores[:j], nil +} + +// StoreBehavior is the action to do in GetAllTiKVStores when a non-TiKV +// store (e.g. TiFlash store) is found. +type StoreBehavior uint8 + +const ( + // ErrorOnTiFlash causes GetAllTiKVStores to return error when the store is + // found to be a TiFlash node. + ErrorOnTiFlash StoreBehavior = 0 + // SkipTiFlash causes GetAllTiKVStores to skip the store when it is found to + // be a TiFlash node. + SkipTiFlash StoreBehavior = 1 + // TiFlashOnly caused GetAllTiKVStores to skip the store which is not a + // TiFlash node. + TiFlashOnly StoreBehavior = 2 +) diff --git a/br/pkg/lightning/backend/backend.go b/br/pkg/lightning/backend/backend.go index e090cc053dc37..791f7b1bb6b8a 100644 --- a/br/pkg/lightning/backend/backend.go +++ b/br/pkg/lightning/backend/backend.go @@ -209,6 +209,9 @@ type AbstractBackend interface { // ResolveDuplicateRows resolves duplicated rows by deleting/inserting data // according to the required algorithm. ResolveDuplicateRows(ctx context.Context, tbl table.Table, tableName string, algorithm config.DuplicateResolutionAlgorithm) error + + // Total Memory usage. This is only used for local backend + TotalMemoryConsume() int64 } // Backend is the delivery target for Lightning @@ -280,6 +283,10 @@ func (be Backend) FlushAll(ctx context.Context) error { return be.abstract.FlushAllEngines(ctx) } +func (be Backend) TotalMemoryConsume() int64 { + return be.abstract.TotalMemoryConsume() +} + // CheckDiskQuota verifies if the total engine file size is below the given // quota. If the quota is exceeded, this method returns an array of engines, // which after importing can decrease the total size below quota. @@ -398,11 +405,20 @@ func (engine *OpenedEngine) LocalWriter(ctx context.Context, cfg *LocalWriterCon return &LocalEngineWriter{writer: w, tableName: engine.tableName}, nil } +func (engine *OpenedEngine) TotalMemoryConsume() int64 { + return engine.engine.backend.TotalMemoryConsume() +} + // WriteRows writes a collection of encoded rows into the engine. func (w *LocalEngineWriter) WriteRows(ctx context.Context, columnNames []string, rows kv.Rows) error { return w.writer.AppendRows(ctx, w.tableName, columnNames, rows) } +// WriteRows writes a collection of encoded rows into the engine. +func (w *LocalEngineWriter) WriteRow(ctx context.Context, columnNames []string, kvs []common.KvPair) error { + return w.writer.AppendRow(ctx, w.tableName, columnNames, kvs) +} + func (w *LocalEngineWriter) Close(ctx context.Context) (ChunkFlushStatus, error) { return w.writer.Close(ctx) } @@ -485,6 +501,16 @@ type EngineWriter interface { columnNames []string, rows kv.Rows, ) error + AppendRow( + ctx context.Context, + tableName string, + columnNames []string, + kvs []common.KvPair, + ) error IsSynced() bool Close(ctx context.Context) (ChunkFlushStatus, error) } + +func (oe *OpenedEngine) GetEngineUuid() uuid.UUID { + return oe.uuid +} \ No newline at end of file diff --git a/br/pkg/lightning/backend/kv/session_test.go b/br/pkg/lightning/backend/kv/kvtest/session_test.go similarity index 82% rename from br/pkg/lightning/backend/kv/session_test.go rename to br/pkg/lightning/backend/kv/kvtest/session_test.go index 9703390afb2ec..b6c6d801ed87f 100644 --- a/br/pkg/lightning/backend/kv/session_test.go +++ b/br/pkg/lightning/backend/kv/kvtest/session_test.go @@ -12,17 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -package kv +package kvtest import ( "testing" + "github.com/pingcap/tidb/br/pkg/lightning/backend/kv" "github.com/pingcap/tidb/parser/mysql" "github.com/stretchr/testify/require" ) func TestSession(t *testing.T) { - session := newSession(&SessionOptions{SQLMode: mysql.ModeNone, Timestamp: 1234567890}) + session := kv.NewSession(&kv.SessionOptions{SQLMode: mysql.ModeNone, Timestamp: 1234567890}) _, err := session.Txn(true) require.NoError(t, err) } diff --git a/br/pkg/lightning/backend/kv/sql2kv_test.go b/br/pkg/lightning/backend/kv/kvtest/sql2kv_test.go similarity index 83% rename from br/pkg/lightning/backend/kv/sql2kv_test.go rename to br/pkg/lightning/backend/kv/kvtest/sql2kv_test.go index a202b39bf56fb..ab0ab4ab1df29 100644 --- a/br/pkg/lightning/backend/kv/sql2kv_test.go +++ b/br/pkg/lightning/backend/kv/kvtest/sql2kv_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package kv +package kvtest import ( "errors" @@ -20,6 +20,7 @@ import ( "reflect" "testing" + lkv "github.com/pingcap/tidb/br/pkg/lightning/backend/kv" "github.com/pingcap/tidb/br/pkg/lightning/common" "github.com/pingcap/tidb/br/pkg/lightning/log" "github.com/pingcap/tidb/br/pkg/lightning/verification" @@ -47,7 +48,7 @@ func TestMarshal(t *testing.T) { minNotNull := types.Datum{} minNotNull.SetMinNotNull() encoder := zapcore.NewMapObjectEncoder() - err := encoder.AddArray("test", RowArrayMarshaler{types.NewStringDatum("1"), nullDatum, minNotNull, types.MaxValueDatum()}) + err := encoder.AddArray("test", lkv.RowArrayMarshaler{types.NewStringDatum("1"), nullDatum, minNotNull, types.MaxValueDatum()}) require.NoError(t, err) require.Equal(t, encoder.Fields["test"], []interface{}{ map[string]interface{}{"kind": "string", "val": "1"}, @@ -58,7 +59,7 @@ func TestMarshal(t *testing.T) { invalid := types.Datum{} invalid.SetInterface(1) - err = encoder.AddArray("bad-test", RowArrayMarshaler{minNotNull, invalid}) + err = encoder.AddArray("bad-test", lkv.RowArrayMarshaler{minNotNull, invalid}) require.Regexp(t, "cannot convert.*", err) require.Equal(t, encoder.Fields["bad-test"], []interface{}{ map[string]interface{}{"kind": "min", "val": "-inf"}, @@ -77,7 +78,7 @@ func TestEncode(t *testing.T) { c1 := &model.ColumnInfo{ID: 1, Name: model.NewCIStr("c1"), State: model.StatePublic, Offset: 0, FieldType: *types.NewFieldType(mysql.TypeTiny)} cols := []*model.ColumnInfo{c1} tblInfo := &model.TableInfo{ID: 1, Columns: cols, PKIsHandle: false, State: model.StatePublic} - tbl, err := tables.TableFromMeta(NewPanickingAllocators(0), tblInfo) + tbl, err := tables.TableFromMeta(lkv.NewPanickingAllocators(0), tblInfo) require.NoError(t, err) logger := log.Logger{Logger: zap.NewNop()} @@ -86,7 +87,7 @@ func TestEncode(t *testing.T) { } // Strict mode - strictMode, err := NewTableKVEncoder(tbl, &SessionOptions{ + strictMode, err := lkv.NewTableKVEncoder(tbl, &lkv.SessionOptions{ SQLMode: mysql.ModeStrictAllTables, Timestamp: 1234567890, }) @@ -108,17 +109,17 @@ func TestEncode(t *testing.T) { } pairs, err = strictMode.Encode(logger, rowsWithPk2, 2, []int{0, 1}, "1.csv", 1234) require.NoError(t, err) - require.Equal(t, pairs, &KvPairs{pairs: []common.KvPair{ + require.Equal(t, pairs, lkv.MakeRowFromKvPairs([]common.KvPair{ { Key: []uint8{0x74, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x5f, 0x72, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, Val: []uint8{0x8, 0x2, 0x8, 0x2}, RowID: 2, }, - }}) + })) // Mock add record error mockTbl := &mockTable{Table: tbl} - mockMode, err := NewTableKVEncoder(mockTbl, &SessionOptions{ + mockMode, err := lkv.NewTableKVEncoder(mockTbl, &lkv.SessionOptions{ SQLMode: mysql.ModeStrictAllTables, Timestamp: 1234567891, }) @@ -127,7 +128,7 @@ func TestEncode(t *testing.T) { require.EqualError(t, err, "mock error") // Non-strict mode - noneMode, err := NewTableKVEncoder(tbl, &SessionOptions{ + noneMode, err := lkv.NewTableKVEncoder(tbl, &lkv.SessionOptions{ SQLMode: mysql.ModeNone, Timestamp: 1234567892, SysVars: map[string]string{"tidb_row_format_version": "1"}, @@ -135,22 +136,22 @@ func TestEncode(t *testing.T) { require.NoError(t, err) pairs, err = noneMode.Encode(logger, rows, 1, []int{0, 1}, "1.csv", 1234) require.NoError(t, err) - require.Equal(t, pairs, &KvPairs{pairs: []common.KvPair{ + require.Equal(t, pairs, lkv.MakeRowFromKvPairs([]common.KvPair{ { Key: []uint8{0x74, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x5f, 0x72, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, Val: []uint8{0x8, 0x2, 0x8, 0xfe, 0x1}, RowID: 1, }, - }}) + })) } func TestDecode(t *testing.T) { c1 := &model.ColumnInfo{ID: 1, Name: model.NewCIStr("c1"), State: model.StatePublic, Offset: 0, FieldType: *types.NewFieldType(mysql.TypeTiny)} cols := []*model.ColumnInfo{c1} tblInfo := &model.TableInfo{ID: 1, Columns: cols, PKIsHandle: false, State: model.StatePublic} - tbl, err := tables.TableFromMeta(NewPanickingAllocators(0), tblInfo) + tbl, err := tables.TableFromMeta(lkv.NewPanickingAllocators(0), tblInfo) require.NoError(t, err) - decoder, err := NewTableKVDecoder(tbl, "`test`.`c1`", &SessionOptions{ + decoder, err := lkv.NewTableKVDecoder(tbl, "`test`.`c1`", &lkv.SessionOptions{ SQLMode: mysql.ModeStrictAllTables, Timestamp: 1234567890, }) @@ -171,6 +172,15 @@ func TestDecode(t *testing.T) { }) } +type LocalKvPairs struct { + pairs []common.KvPair +} + +func fromRow(r lkv.Row) (l LocalKvPairs) { + l.pairs = lkv.KvPairsFromRow(r) + return l +} + func TestDecodeIndex(t *testing.T) { logger := log.Logger{Logger: zap.NewNop()} tblInfo := &model.TableInfo{ @@ -194,7 +204,7 @@ func TestDecodeIndex(t *testing.T) { State: model.StatePublic, PKIsHandle: false, } - tbl, err := tables.TableFromMeta(NewPanickingAllocators(0), tblInfo) + tbl, err := tables.TableFromMeta(lkv.NewPanickingAllocators(0), tblInfo) if err != nil { fmt.Printf("error: %v", err.Error()) } @@ -205,16 +215,16 @@ func TestDecodeIndex(t *testing.T) { } // Strict mode - strictMode, err := NewTableKVEncoder(tbl, &SessionOptions{ + strictMode, err := lkv.NewTableKVEncoder(tbl, &lkv.SessionOptions{ SQLMode: mysql.ModeStrictAllTables, Timestamp: 1234567890, }) require.NoError(t, err) pairs, err := strictMode.Encode(logger, rows, 1, []int{0, 1, -1}, "1.csv", 123) - data := pairs.(*KvPairs) + data := fromRow(pairs) require.Len(t, data.pairs, 2) - decoder, err := NewTableKVDecoder(tbl, "`test`.``", &SessionOptions{ + decoder, err := lkv.NewTableKVDecoder(tbl, "`test`.``", &lkv.SessionOptions{ SQLMode: mysql.ModeStrictAllTables, Timestamp: 1234567890, }) @@ -235,7 +245,7 @@ func TestEncodeRowFormatV2(t *testing.T) { c1 := &model.ColumnInfo{ID: 1, Name: model.NewCIStr("c1"), State: model.StatePublic, Offset: 0, FieldType: *types.NewFieldType(mysql.TypeTiny)} cols := []*model.ColumnInfo{c1} tblInfo := &model.TableInfo{ID: 1, Columns: cols, PKIsHandle: false, State: model.StatePublic} - tbl, err := tables.TableFromMeta(NewPanickingAllocators(0), tblInfo) + tbl, err := tables.TableFromMeta(lkv.NewPanickingAllocators(0), tblInfo) require.NoError(t, err) logger := log.Logger{Logger: zap.NewNop()} @@ -243,7 +253,7 @@ func TestEncodeRowFormatV2(t *testing.T) { types.NewIntDatum(10000000), } - noneMode, err := NewTableKVEncoder(tbl, &SessionOptions{ + noneMode, err := lkv.NewTableKVEncoder(tbl, &lkv.SessionOptions{ SQLMode: mysql.ModeNone, Timestamp: 1234567892, SysVars: map[string]string{"tidb_row_format_version": "2"}, @@ -251,7 +261,7 @@ func TestEncodeRowFormatV2(t *testing.T) { require.NoError(t, err) pairs, err := noneMode.Encode(logger, rows, 1, []int{0, 1}, "1.csv", 1234) require.NoError(t, err) - require.Equal(t, pairs, &KvPairs{pairs: []common.KvPair{ + require.Equal(t, pairs, lkv.MakeRowFromKvPairs([]common.KvPair{ { // the key should be the same as TestEncode() Key: []uint8{0x74, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x5f, 0x72, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, @@ -266,7 +276,7 @@ func TestEncodeRowFormatV2(t *testing.T) { }, RowID: 1, }, - }}) + })) } func TestEncodeTimestamp(t *testing.T) { @@ -283,12 +293,12 @@ func TestEncodeTimestamp(t *testing.T) { } cols := []*model.ColumnInfo{c1} tblInfo := &model.TableInfo{ID: 1, Columns: cols, PKIsHandle: false, State: model.StatePublic} - tbl, err := tables.TableFromMeta(NewPanickingAllocators(0), tblInfo) + tbl, err := tables.TableFromMeta(lkv.NewPanickingAllocators(0), tblInfo) require.NoError(t, err) logger := log.Logger{Logger: zap.NewNop()} - encoder, err := NewTableKVEncoder(tbl, &SessionOptions{ + encoder, err := lkv.NewTableKVEncoder(tbl, &lkv.SessionOptions{ SQLMode: mysql.ModeStrictAllTables, Timestamp: 1234567893, SysVars: map[string]string{ @@ -299,23 +309,23 @@ func TestEncodeTimestamp(t *testing.T) { require.NoError(t, err) pairs, err := encoder.Encode(logger, nil, 70, []int{-1, 1}, "1.csv", 1234) require.NoError(t, err) - require.Equal(t, pairs, &KvPairs{pairs: []common.KvPair{ + require.Equal(t, pairs, lkv.MakeRowFromKvPairs([]common.KvPair{ { Key: []uint8{0x74, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x5f, 0x72, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46}, Val: []uint8{0x8, 0x2, 0x9, 0x80, 0x80, 0x80, 0xf0, 0xfd, 0x8e, 0xf7, 0xc0, 0x19}, RowID: 70, }, - }}) + })) } func TestEncodeDoubleAutoIncrement(t *testing.T) { tblInfo := mockTableInfo(t, "create table t (id double not null auto_increment, unique key `u_id` (`id`));") - tbl, err := tables.TableFromMeta(NewPanickingAllocators(0), tblInfo) + tbl, err := tables.TableFromMeta(lkv.NewPanickingAllocators(0), tblInfo) require.NoError(t, err) logger := log.Logger{Logger: zap.NewNop()} - encoder, err := NewTableKVEncoder(tbl, &SessionOptions{ + encoder, err := lkv.NewTableKVEncoder(tbl, &lkv.SessionOptions{ SQLMode: mysql.ModeStrictAllTables, SysVars: map[string]string{ "tidb_row_format_version": "2", @@ -332,7 +342,7 @@ func TestEncodeDoubleAutoIncrement(t *testing.T) { types.NewFloat64Datum(1.0), }, 70, []int{0, -1}, "1.csv", 1234) require.NoError(t, err) - require.Equal(t, &KvPairs{pairs: []common.KvPair{ + require.Equal(t, lkv.MakeRowFromKvPairs([]common.KvPair{ { Key: []uint8{0x74, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x5f, 0x72, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46}, Val: []uint8{0x80, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x8, 0x0, 0xbf, 0xf0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, @@ -343,7 +353,7 @@ func TestEncodeDoubleAutoIncrement(t *testing.T) { Val: []uint8{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46}, RowID: 70, }, - }}, pairsExpect) + }), pairsExpect) pairs, err := encoder.Encode(logger, []types.Datum{ types.NewStringDatum("1"), @@ -351,7 +361,7 @@ func TestEncodeDoubleAutoIncrement(t *testing.T) { require.NoError(t, err) require.Equal(t, pairsExpect, pairs) - require.Equal(t, tbl.Allocators(encoder.(*tableKVEncoder).se).Get(autoid.AutoIncrementType).Base(), int64(70)) + require.Equal(t, tbl.Allocators(lkv.GetSession4test(encoder)).Get(autoid.AutoIncrementType).Base(), int64(70)) } func TestEncodeMissingAutoValue(t *testing.T) { @@ -440,9 +450,9 @@ func TestDefaultAutoRandoms(t *testing.T) { tblInfo := mockTableInfo(t, "create table t (id bigint unsigned NOT NULL auto_random primary key clustered, a varchar(100));") // seems parser can't parse auto_random properly. tblInfo.AutoRandomBits = 5 - tbl, err := tables.TableFromMeta(NewPanickingAllocators(0), tblInfo) + tbl, err := tables.TableFromMeta(lkv.NewPanickingAllocators(0), tblInfo) require.NoError(t, err) - encoder, err := NewTableKVEncoder(tbl, &SessionOptions{ + encoder, err := lkv.NewTableKVEncoder(tbl, &lkv.SessionOptions{ SQLMode: mysql.ModeStrictAllTables, Timestamp: 1234567893, SysVars: map[string]string{"tidb_row_format_version": "2"}, @@ -452,32 +462,32 @@ func TestDefaultAutoRandoms(t *testing.T) { logger := log.Logger{Logger: zap.NewNop()} pairs, err := encoder.Encode(logger, []types.Datum{types.NewStringDatum("")}, 70, []int{-1, 0}, "1.csv", 1234) require.NoError(t, err) - require.Equal(t, pairs, &KvPairs{pairs: []common.KvPair{ + require.Equal(t, pairs, lkv.MakeRowFromKvPairs([]common.KvPair{ { Key: []uint8{0x74, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x5f, 0x72, 0xf0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46}, Val: []uint8{0x80, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0}, RowID: 70, }, - }}) - require.Equal(t, tbl.Allocators(encoder.(*tableKVEncoder).se).Get(autoid.AutoRandomType).Base(), int64(70)) + })) + require.Equal(t, tbl.Allocators(lkv.GetSession4test(encoder)).Get(autoid.AutoRandomType).Base(), int64(70)) pairs, err = encoder.Encode(logger, []types.Datum{types.NewStringDatum("")}, 71, []int{-1, 0}, "1.csv", 1234) require.NoError(t, err) - require.Equal(t, pairs, &KvPairs{pairs: []common.KvPair{ + require.Equal(t, pairs, lkv.MakeRowFromKvPairs([]common.KvPair{ { Key: []uint8{0x74, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x5f, 0x72, 0xf0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x47}, Val: []uint8{0x80, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0}, RowID: 71, }, - }}) - require.Equal(t, tbl.Allocators(encoder.(*tableKVEncoder).se).Get(autoid.AutoRandomType).Base(), int64(71)) + })) + require.Equal(t, tbl.Allocators(lkv.GetSession4test(encoder)).Get(autoid.AutoRandomType).Base(), int64(71)) } func TestShardRowId(t *testing.T) { tblInfo := mockTableInfo(t, "create table t (s varchar(16)) shard_row_id_bits = 3;") - tbl, err := tables.TableFromMeta(NewPanickingAllocators(0), tblInfo) + tbl, err := tables.TableFromMeta(lkv.NewPanickingAllocators(0), tblInfo) require.NoError(t, err) - encoder, err := NewTableKVEncoder(tbl, &SessionOptions{ + encoder, err := lkv.NewTableKVEncoder(tbl, &lkv.SessionOptions{ SQLMode: mysql.ModeStrictAllTables, Timestamp: 1234567893, SysVars: map[string]string{"tidb_row_format_version": "2"}, @@ -489,7 +499,7 @@ func TestShardRowId(t *testing.T) { for i := int64(1); i <= 32; i++ { pairs, err := encoder.Encode(logger, []types.Datum{types.NewStringDatum(fmt.Sprintf("%d", i))}, i, []int{0, -1}, "1.csv", i*32) require.NoError(t, err) - kvs := pairs.(*KvPairs) + kvs := fromRow(pairs) require.Len(t, kvs.pairs, 1) _, h, err := tablecodec.DecodeRecordKey(kvs.pairs[0].Key) require.NoError(t, err) @@ -498,7 +508,7 @@ func TestShardRowId(t *testing.T) { keyMap[rowID>>60] = struct{}{} } require.Len(t, keyMap, 8) - require.Equal(t, tbl.Allocators(encoder.(*tableKVEncoder).se).Get(autoid.RowIDAllocType).Base(), int64(32)) + require.Equal(t, tbl.Allocators(lkv.GetSession4test(encoder)).Get(autoid.RowIDAllocType).Base(), int64(32)) } func TestSplitIntoChunks(t *testing.T) { @@ -521,35 +531,35 @@ func TestSplitIntoChunks(t *testing.T) { }, } - splitBy10 := MakeRowsFromKvPairs(pairs).SplitIntoChunks(10) - require.Equal(t, splitBy10, []Rows{ - MakeRowsFromKvPairs(pairs[0:2]), - MakeRowsFromKvPairs(pairs[2:3]), - MakeRowsFromKvPairs(pairs[3:4]), + splitBy10 := lkv.MakeRowsFromKvPairs(pairs).SplitIntoChunks(10) + require.Equal(t, splitBy10, []lkv.Rows{ + lkv.MakeRowsFromKvPairs(pairs[0:2]), + lkv.MakeRowsFromKvPairs(pairs[2:3]), + lkv.MakeRowsFromKvPairs(pairs[3:4]), }) - splitBy12 := MakeRowsFromKvPairs(pairs).SplitIntoChunks(12) - require.Equal(t, splitBy12, []Rows{ - MakeRowsFromKvPairs(pairs[0:2]), - MakeRowsFromKvPairs(pairs[2:4]), + splitBy12 := lkv.MakeRowsFromKvPairs(pairs).SplitIntoChunks(12) + require.Equal(t, splitBy12, []lkv.Rows{ + lkv.MakeRowsFromKvPairs(pairs[0:2]), + lkv.MakeRowsFromKvPairs(pairs[2:4]), }) - splitBy1000 := MakeRowsFromKvPairs(pairs).SplitIntoChunks(1000) - require.Equal(t, splitBy1000, []Rows{ - MakeRowsFromKvPairs(pairs[0:4]), + splitBy1000 := lkv.MakeRowsFromKvPairs(pairs).SplitIntoChunks(1000) + require.Equal(t, splitBy1000, []lkv.Rows{ + lkv.MakeRowsFromKvPairs(pairs[0:4]), }) - splitBy1 := MakeRowsFromKvPairs(pairs).SplitIntoChunks(1) - require.Equal(t, splitBy1, []Rows{ - MakeRowsFromKvPairs(pairs[0:1]), - MakeRowsFromKvPairs(pairs[1:2]), - MakeRowsFromKvPairs(pairs[2:3]), - MakeRowsFromKvPairs(pairs[3:4]), + splitBy1 := lkv.MakeRowsFromKvPairs(pairs).SplitIntoChunks(1) + require.Equal(t, splitBy1, []lkv.Rows{ + lkv.MakeRowsFromKvPairs(pairs[0:1]), + lkv.MakeRowsFromKvPairs(pairs[1:2]), + lkv.MakeRowsFromKvPairs(pairs[2:3]), + lkv.MakeRowsFromKvPairs(pairs[3:4]), }) } func TestClassifyAndAppend(t *testing.T) { - kvs := MakeRowFromKvPairs([]common.KvPair{ + kvs := lkv.MakeRowFromKvPairs([]common.KvPair{ { Key: []byte("txxxxxxxx_ryyyyyyyy"), Val: []byte("value1"), @@ -564,14 +574,14 @@ func TestClassifyAndAppend(t *testing.T) { }, }) - data := MakeRowsFromKvPairs(nil) - indices := MakeRowsFromKvPairs(nil) + data := lkv.MakeRowsFromKvPairs(nil) + indices := lkv.MakeRowsFromKvPairs(nil) dataChecksum := verification.MakeKVChecksum(0, 0, 0) indexChecksum := verification.MakeKVChecksum(0, 0, 0) kvs.ClassifyAndAppend(&data, &dataChecksum, &indices, &indexChecksum) - require.Equal(t, data, MakeRowsFromKvPairs([]common.KvPair{ + require.Equal(t, data, lkv.MakeRowsFromKvPairs([]common.KvPair{ { Key: []byte("txxxxxxxx_ryyyyyyyy"), Val: []byte("value1"), @@ -581,7 +591,7 @@ func TestClassifyAndAppend(t *testing.T) { Val: []byte("value2"), }, })) - require.Equal(t, indices, MakeRowsFromKvPairs([]common.KvPair{ + require.Equal(t, indices, lkv.MakeRowsFromKvPairs([]common.KvPair{ { Key: []byte("txxxxxxxx_izzzzzzzz"), Val: []byte("index1"), @@ -594,7 +604,7 @@ func TestClassifyAndAppend(t *testing.T) { type benchSQL2KVSuite struct { row []types.Datum colPerm []int - encoder Encoder + encoder lkv.Encoder logger log.Logger } @@ -634,9 +644,9 @@ func SetUpTest(b *testing.B) *benchSQL2KVSuite { tableInfo.State = model.StatePublic // Construct the corresponding KV encoder. - tbl, err := tables.TableFromMeta(NewPanickingAllocators(0), tableInfo) + tbl, err := tables.TableFromMeta(lkv.NewPanickingAllocators(0), tableInfo) require.NoError(b, err) - encoder, err := NewTableKVEncoder(tbl, &SessionOptions{SysVars: map[string]string{"tidb_row_format_version": "2"}}) + encoder, err := lkv.NewTableKVEncoder(tbl, &lkv.SessionOptions{SysVars: map[string]string{"tidb_row_format_version": "2"}}) require.NoError(b, err) logger := log.Logger{Logger: zap.NewNop()} diff --git a/br/pkg/lightning/backend/kv/sql2kv.go b/br/pkg/lightning/backend/kv/sql2kv.go index 4dc80e0c17ce2..a1a278c2900ce 100644 --- a/br/pkg/lightning/backend/kv/sql2kv.go +++ b/br/pkg/lightning/backend/kv/sql2kv.go @@ -19,6 +19,7 @@ package kv import ( "context" "fmt" + "github.com/pingcap/tidb/sessionctx" "math" "math/rand" "sort" @@ -42,9 +43,6 @@ import ( "github.com/pingcap/tidb/util/chunk" "go.uber.org/zap" "go.uber.org/zap/zapcore" - - // Import tidb/planner/core to initialize expression.RewriteAstExpr - _ "github.com/pingcap/tidb/planner/core" ) var ExtraHandleColumnInfo = model.NewExtraHandleColInfo() @@ -65,6 +63,10 @@ type tableKVEncoder struct { autoIDFn autoIDConverter } +func GetSession4test(encoder Encoder) sessionctx.Context { + return encoder.(*tableKVEncoder).se +} + func NewTableKVEncoder(tbl table.Table, options *SessionOptions) (Encoder, error) { metric.KvEncoderCounter.WithLabelValues("open").Inc() meta := tbl.Meta() @@ -312,6 +314,14 @@ func KvPairsFromRows(rows Rows) []common.KvPair { return rows.(*KvPairs).pairs } +// KvPairsFromRow converts a Rows instance constructed from MakeRowsFromKvPairs +// back into a slice of KvPair. This method panics if the Rows is not +// constructed in such way. +// nolint:golint // kv.KvPairsFromRows sounds good. +func KvPairsFromRow(rows Row) []common.KvPair { + return rows.(*KvPairs).pairs +} + func evaluateGeneratedColumns(se *session, record []types.Datum, cols []*table.Column, genCols []genCol) (err error, errCol *model.ColumnInfo) { mutRow := chunk.MutRowFromDatums(record) for _, gc := range genCols { diff --git a/br/pkg/lightning/backend/local/duplicate.go b/br/pkg/lightning/backend/local/duplicate.go index 983ae33fcfd68..01510ed4a125c 100644 --- a/br/pkg/lightning/backend/local/duplicate.go +++ b/br/pkg/lightning/backend/local/duplicate.go @@ -33,7 +33,7 @@ import ( "github.com/pingcap/tidb/br/pkg/lightning/errormanager" "github.com/pingcap/tidb/br/pkg/lightning/log" "github.com/pingcap/tidb/br/pkg/logutil" - "github.com/pingcap/tidb/br/pkg/restore" + "github.com/pingcap/tidb/br/pkg/restore/split" "github.com/pingcap/tidb/br/pkg/utils" "github.com/pingcap/tidb/distsql" tidbkv "github.com/pingcap/tidb/kv" @@ -298,7 +298,7 @@ type RemoteDupKVStream struct { func getDupDetectClient( ctx context.Context, - region *restore.RegionInfo, + region *split.RegionInfo, keyRange tidbkv.KeyRange, importClientFactory ImportClientFactory, ) (import_sstpb.ImportSST_DuplicateDetectClient, error) { @@ -330,7 +330,7 @@ func getDupDetectClient( // NewRemoteDupKVStream creates a new RemoteDupKVStream. func NewRemoteDupKVStream( ctx context.Context, - region *restore.RegionInfo, + region *split.RegionInfo, keyRange tidbkv.KeyRange, importClientFactory ImportClientFactory, ) (*RemoteDupKVStream, error) { @@ -392,7 +392,7 @@ func (s *RemoteDupKVStream) Close() error { type DuplicateManager struct { tbl table.Table tableName string - splitCli restore.SplitClient + splitCli split.SplitClient tikvCli *tikv.KVStore errorMgr *errormanager.ErrorManager decoder *kv.TableKVDecoder @@ -405,7 +405,7 @@ type DuplicateManager struct { func NewDuplicateManager( tbl table.Table, tableName string, - splitCli restore.SplitClient, + splitCli split.SplitClient, tikvCli *tikv.KVStore, errMgr *errormanager.ErrorManager, sessOpts *kv.SessionOptions, @@ -656,14 +656,14 @@ func (m *DuplicateManager) CollectDuplicateRowsFromDupDB(ctx context.Context, du func (m *DuplicateManager) splitKeyRangeByRegions( ctx context.Context, keyRange tidbkv.KeyRange, -) ([]*restore.RegionInfo, []tidbkv.KeyRange, error) { +) ([]*split.RegionInfo, []tidbkv.KeyRange, error) { rawStartKey := codec.EncodeBytes(nil, keyRange.StartKey) rawEndKey := codec.EncodeBytes(nil, keyRange.EndKey) - allRegions, err := restore.PaginateScanRegion(ctx, m.splitCli, rawStartKey, rawEndKey, 1024) + allRegions, err := split.PaginateScanRegion(ctx, m.splitCli, rawStartKey, rawEndKey, 1024) if err != nil { return nil, nil, errors.Trace(err) } - regions := make([]*restore.RegionInfo, 0, len(allRegions)) + regions := make([]*split.RegionInfo, 0, len(allRegions)) keyRanges := make([]tidbkv.KeyRange, 0, len(allRegions)) for _, region := range allRegions { startKey := keyRange.StartKey @@ -706,7 +706,7 @@ func (m *DuplicateManager) processRemoteDupTaskOnce( remainKeyRanges *pendingKeyRanges, ) (madeProgress bool, err error) { var ( - regions []*restore.RegionInfo + regions []*split.RegionInfo keyRanges []tidbkv.KeyRange ) for _, kr := range remainKeyRanges.list() { diff --git a/br/pkg/lightning/backend/local/engine.go b/br/pkg/lightning/backend/local/engine.go index 82ebc4c4c3e65..5f7db70d842e3 100644 --- a/br/pkg/lightning/backend/local/engine.go +++ b/br/pkg/lightning/backend/local/engine.go @@ -233,6 +233,18 @@ func (e *Engine) unlock() { e.mutex.Unlock() } +func (e *Engine) TotalMemorySize() int64 { + var memSize int64 = 0 + e.localWriters.Range(func(k, v interface{}) bool { + w := k.(*Writer) + if w.kvBuffer != nil { + memSize += w.kvBuffer.TotalSize() + } + return true + }) + return memSize +} + type rangeOffsets struct { Size uint64 Keys uint64 @@ -1100,6 +1112,35 @@ func (w *Writer) AppendRows(ctx context.Context, tableName string, columnNames [ return w.appendRowsUnsorted(ctx, kvs) } +// Used to write one key:value pair into local writer. +func (w *Writer) AppendRow(ctx context.Context, tableName string, columnNames []string, kvs []common.KvPair) error { + // Now, only one index pairs was writen into local write buffer. + if len(kvs) != 1 { + return nil + } + + if w.engine.closed.Load() { + return errorEngineClosed + } + + w.Lock() + defer w.Unlock() + + // if chunk has _tidb_rowid field, we can't ensure that the rows are sorted. + if w.isKVSorted && w.writer == nil { + for _, c := range columnNames { + if c == model.ExtraHandleName.L { + w.isKVSorted = false + } + } + } + + if w.isKVSorted { + return w.appendRowsSorted(kvs) + } + return w.appendRowsUnsorted(ctx, kvs) +} + func (w *Writer) flush(ctx context.Context) error { w.Lock() defer w.Unlock() diff --git a/br/pkg/lightning/backend/local/local.go b/br/pkg/lightning/backend/local/local.go index 0d19c6887c4f8..d0e258962039a 100644 --- a/br/pkg/lightning/backend/local/local.go +++ b/br/pkg/lightning/backend/local/local.go @@ -49,7 +49,7 @@ import ( "github.com/pingcap/tidb/br/pkg/logutil" "github.com/pingcap/tidb/br/pkg/membuf" "github.com/pingcap/tidb/br/pkg/pdutil" - split "github.com/pingcap/tidb/br/pkg/restore" + "github.com/pingcap/tidb/br/pkg/restore/split" "github.com/pingcap/tidb/br/pkg/utils" "github.com/pingcap/tidb/br/pkg/version" "github.com/pingcap/tidb/infoschema" @@ -341,6 +341,18 @@ func NewLocalBackend( return backend.MakeBackend(local), nil } +func (local *local) TotalMemoryConsume() int64 { + var memConsume int64 = 0 + local.engines.Range(func(k, v interface{}) bool { + e := v.(*Engine) + if e != nil { + memConsume += e.TotalMemorySize() + } + return true + }) + return memConsume +} + func (local *local) checkMultiIngestSupport(ctx context.Context) error { stores, err := local.pdCtl.GetPDClient().GetAllStores(ctx, pd.WithExcludeTombstone()) if err != nil { @@ -1638,6 +1650,10 @@ func (t tblNames) String() string { return b.String() } +func CheckTiFlashVersion4test(ctx context.Context, g glue.Glue, checkCtx *backend.CheckCtx, tidbVersion semver.Version) error { + return checkTiFlashVersion(ctx, g, checkCtx, tidbVersion) +} + // check TiFlash replicas. // local backend doesn't support TiFlash before tidb v4.0.5 func checkTiFlashVersion(ctx context.Context, g glue.Glue, checkCtx *backend.CheckCtx, tidbVersion semver.Version) error { diff --git a/br/pkg/lightning/backend/local/local_test.go b/br/pkg/lightning/backend/local/local_test.go index 0711bfb1fc463..f097ee1b36ee5 100644 --- a/br/pkg/lightning/backend/local/local_test.go +++ b/br/pkg/lightning/backend/local/local_test.go @@ -29,9 +29,7 @@ import ( "testing" "github.com/cockroachdb/pebble" - "github.com/coreos/go-semver/semver" "github.com/docker/go-units" - "github.com/golang/mock/gomock" "github.com/google/uuid" "github.com/pingcap/errors" "github.com/pingcap/failpoint" @@ -41,12 +39,10 @@ import ( "github.com/pingcap/tidb/br/pkg/lightning/backend" "github.com/pingcap/tidb/br/pkg/lightning/backend/kv" "github.com/pingcap/tidb/br/pkg/lightning/common" - "github.com/pingcap/tidb/br/pkg/lightning/mydump" "github.com/pingcap/tidb/br/pkg/membuf" - "github.com/pingcap/tidb/br/pkg/mock" "github.com/pingcap/tidb/br/pkg/pdutil" - "github.com/pingcap/tidb/br/pkg/restore" "github.com/pingcap/tidb/br/pkg/utils" + "github.com/pingcap/tidb/br/pkg/restore/split" "github.com/pingcap/tidb/br/pkg/version" tidbkv "github.com/pingcap/tidb/kv" "github.com/pingcap/tidb/sessionctx/stmtctx" @@ -422,11 +418,11 @@ func TestLocalWriterWithIngestUnsort(t *testing.T) { } type mockSplitClient struct { - restore.SplitClient + split.SplitClient } -func (c *mockSplitClient) GetRegion(ctx context.Context, key []byte) (*restore.RegionInfo, error) { - return &restore.RegionInfo{ +func (c *mockSplitClient) GetRegion(ctx context.Context, key []byte) (*split.RegionInfo, error) { + return &split.RegionInfo{ Leader: &metapb.Peer{Id: 1}, Region: &metapb.Region{ Id: 1, @@ -448,7 +444,7 @@ func TestIsIngestRetryable(t *testing.T) { }, } ctx := context.Background() - region := &restore.RegionInfo{ + region := &split.RegionInfo{ Leader: &metapb.Peer{Id: 1}, Region: &metapb.Region{ Id: 1, @@ -622,54 +618,54 @@ func TestLocalIngestLoop(t *testing.T) { require.Equal(t, atomic.LoadInt32(&maxMetaSeq), f.finishedMetaSeq.Load()) } -func TestCheckRequirementsTiFlash(t *testing.T) { - controller := gomock.NewController(t) - defer controller.Finish() - glue := mock.NewMockGlue(controller) - exec := mock.NewMockSQLExecutor(controller) - ctx := context.Background() - - dbMetas := []*mydump.MDDatabaseMeta{ - { - Name: "test", - Tables: []*mydump.MDTableMeta{ - { - DB: "test", - Name: "t1", - DataFiles: []mydump.FileInfo{{}}, - }, - { - DB: "test", - Name: "tbl", - DataFiles: []mydump.FileInfo{{}}, - }, - }, - }, - { - Name: "test1", - Tables: []*mydump.MDTableMeta{ - { - DB: "test1", - Name: "t", - DataFiles: []mydump.FileInfo{{}}, - }, - { - DB: "test1", - Name: "tbl", - DataFiles: []mydump.FileInfo{{}}, - }, - }, - }, - } - checkCtx := &backend.CheckCtx{DBMetas: dbMetas} - - glue.EXPECT().GetSQLExecutor().Return(exec) - exec.EXPECT().QueryStringsWithLog(ctx, tiFlashReplicaQuery, gomock.Any(), gomock.Any()). - Return([][]string{{"db", "tbl"}, {"test", "t1"}, {"test1", "tbl"}}, nil) - - err := checkTiFlashVersion(ctx, glue, checkCtx, *semver.New("4.0.2")) - require.Regexp(t, "^lightning local backend doesn't support TiFlash in this TiDB version. conflict tables: \\[`test`.`t1`, `test1`.`tbl`\\]", err.Error()) -} +// func TestCheckRequirementsTiFlash(t *testing.T) { +// controller := gomock.NewController(t) +// defer controller.Finish() +// glue := mock.NewMockGlue(controller) +// exec := mock.NewMockSQLExecutor(controller) +// ctx := context.Background() +// +// dbMetas := []*mydump.MDDatabaseMeta{ +// { +// Name: "test", +// Tables: []*mydump.MDTableMeta{ +// { +// DB: "test", +// Name: "t1", +// DataFiles: []mydump.FileInfo{{}}, +// }, +// { +// DB: "test", +// Name: "tbl", +// DataFiles: []mydump.FileInfo{{}}, +// }, +// }, +// }, +// { +// Name: "test1", +// Tables: []*mydump.MDTableMeta{ +// { +// DB: "test1", +// Name: "t", +// DataFiles: []mydump.FileInfo{{}}, +// }, +// { +// DB: "test1", +// Name: "tbl", +// DataFiles: []mydump.FileInfo{{}}, +// }, +// }, +// }, +// } +// checkCtx := &backend.CheckCtx{DBMetas: dbMetas} +// +// glue.EXPECT().GetSQLExecutor().Return(exec) +// exec.EXPECT().QueryStringsWithLog(ctx, tiFlashReplicaQuery, gomock.Any(), gomock.Any()). +// Return([][]string{{"db", "tbl"}, {"test", "t1"}, {"test1", "tbl"}}, nil) +// +// err := checkTiFlashVersion(ctx, glue, checkCtx, *semver.New("4.0.2")) +// require.Regexp(t, "^lightning local backend doesn't support TiFlash in this TiDB version. conflict tables: \\[`test`.`t1`, `test1`.`tbl`\\]", err.Error()) +// } func makeRanges(input []string) []Range { ranges := make([]Range, 0, len(input)/2) diff --git a/br/pkg/lightning/backend/local/localhelper.go b/br/pkg/lightning/backend/local/localhelper.go index 98413b20e71e0..f5d044fb4f5f7 100644 --- a/br/pkg/lightning/backend/local/localhelper.go +++ b/br/pkg/lightning/backend/local/localhelper.go @@ -34,7 +34,7 @@ import ( "github.com/pingcap/tidb/br/pkg/lightning/common" "github.com/pingcap/tidb/br/pkg/lightning/log" "github.com/pingcap/tidb/br/pkg/logutil" - split "github.com/pingcap/tidb/br/pkg/restore" + "github.com/pingcap/tidb/br/pkg/restore/split" "github.com/pingcap/tidb/util/codec" "github.com/pingcap/tidb/util/mathutil" "go.uber.org/multierr" diff --git a/br/pkg/lightning/backend/local/localhelper_test.go b/br/pkg/lightning/backend/local/localhelper_test.go index 48ce64da5e3b6..4f2a3009dd79f 100644 --- a/br/pkg/lightning/backend/local/localhelper_test.go +++ b/br/pkg/lightning/backend/local/localhelper_test.go @@ -29,7 +29,7 @@ import ( "github.com/pingcap/kvproto/pkg/metapb" "github.com/pingcap/kvproto/pkg/pdpb" "github.com/pingcap/tidb/br/pkg/lightning/glue" - "github.com/pingcap/tidb/br/pkg/restore" + "github.com/pingcap/tidb/br/pkg/restore/split" "github.com/pingcap/tidb/kv" "github.com/pingcap/tidb/parser/mysql" "github.com/pingcap/tidb/sessionctx/stmtctx" @@ -43,14 +43,13 @@ import ( func init() { // Reduce the time cost for test cases. - restore.ScanRegionAttemptTimes = 2 splitRetryTimes = 2 } type testClient struct { mu sync.RWMutex stores map[uint64]*metapb.Store - regions map[uint64]*restore.RegionInfo + regions map[uint64]*split.RegionInfo regionsInfo *pdtypes.RegionTree // For now it's only used in ScanRegions nextRegionID uint64 splitCount atomic.Int32 @@ -59,7 +58,7 @@ type testClient struct { func newTestClient( stores map[uint64]*metapb.Store, - regions map[uint64]*restore.RegionInfo, + regions map[uint64]*split.RegionInfo, nextRegionID uint64, hook clientHook, ) *testClient { @@ -77,11 +76,11 @@ func newTestClient( } // ScatterRegions scatters regions in a batch. -func (c *testClient) ScatterRegions(ctx context.Context, regionInfo []*restore.RegionInfo) error { +func (c *testClient) ScatterRegions(ctx context.Context, regionInfo []*split.RegionInfo) error { return nil } -func (c *testClient) GetAllRegions() map[uint64]*restore.RegionInfo { +func (c *testClient) GetAllRegions() map[uint64]*split.RegionInfo { c.mu.RLock() defer c.mu.RUnlock() return c.regions @@ -97,7 +96,7 @@ func (c *testClient) GetStore(ctx context.Context, storeID uint64) (*metapb.Stor return store, nil } -func (c *testClient) GetRegion(ctx context.Context, key []byte) (*restore.RegionInfo, error) { +func (c *testClient) GetRegion(ctx context.Context, key []byte) (*split.RegionInfo, error) { c.mu.RLock() defer c.mu.RUnlock() for _, region := range c.regions { @@ -109,7 +108,7 @@ func (c *testClient) GetRegion(ctx context.Context, key []byte) (*restore.Region return nil, errors.Errorf("region not found: key=%s", string(key)) } -func (c *testClient) GetRegionByID(ctx context.Context, regionID uint64) (*restore.RegionInfo, error) { +func (c *testClient) GetRegionByID(ctx context.Context, regionID uint64) (*split.RegionInfo, error) { c.mu.RLock() defer c.mu.RUnlock() region, ok := c.regions[regionID] @@ -121,12 +120,12 @@ func (c *testClient) GetRegionByID(ctx context.Context, regionID uint64) (*resto func (c *testClient) SplitRegion( ctx context.Context, - regionInfo *restore.RegionInfo, + regionInfo *split.RegionInfo, key []byte, -) (*restore.RegionInfo, error) { +) (*split.RegionInfo, error) { c.mu.Lock() defer c.mu.Unlock() - var target *restore.RegionInfo + var target *split.RegionInfo splitKey := codec.EncodeBytes([]byte{}, key) for _, region := range c.regions { if bytes.Compare(splitKey, region.Region.StartKey) >= 0 && @@ -137,7 +136,7 @@ func (c *testClient) SplitRegion( if target == nil { return nil, errors.Errorf("region not found: key=%s", string(key)) } - newRegion := &restore.RegionInfo{ + newRegion := &split.RegionInfo{ Region: &metapb.Region{ Peers: target.Region.Peers, Id: c.nextRegionID, @@ -160,8 +159,8 @@ func (c *testClient) SplitRegion( } func (c *testClient) BatchSplitRegionsWithOrigin( - ctx context.Context, regionInfo *restore.RegionInfo, keys [][]byte, -) (*restore.RegionInfo, []*restore.RegionInfo, error) { + ctx context.Context, regionInfo *split.RegionInfo, keys [][]byte, +) (*split.RegionInfo, []*split.RegionInfo, error) { c.mu.Lock() defer c.mu.Unlock() c.splitCount.Inc() @@ -179,7 +178,7 @@ func (c *testClient) BatchSplitRegionsWithOrigin( default: } - newRegions := make([]*restore.RegionInfo, 0) + newRegions := make([]*split.RegionInfo, 0) target, ok := c.regions[regionInfo.Region.Id] if !ok { return nil, nil, errors.New("region not found") @@ -202,7 +201,7 @@ func (c *testClient) BatchSplitRegionsWithOrigin( if bytes.Compare(key, startKey) <= 0 || bytes.Compare(key, target.Region.EndKey) >= 0 { continue } - newRegion := &restore.RegionInfo{ + newRegion := &split.RegionInfo{ Region: &metapb.Region{ Peers: target.Region.Peers, Id: c.nextRegionID, @@ -235,13 +234,13 @@ func (c *testClient) BatchSplitRegionsWithOrigin( } func (c *testClient) BatchSplitRegions( - ctx context.Context, regionInfo *restore.RegionInfo, keys [][]byte, -) ([]*restore.RegionInfo, error) { + ctx context.Context, regionInfo *split.RegionInfo, keys [][]byte, +) ([]*split.RegionInfo, error) { _, newRegions, err := c.BatchSplitRegionsWithOrigin(ctx, regionInfo, keys) return newRegions, err } -func (c *testClient) ScatterRegion(ctx context.Context, regionInfo *restore.RegionInfo) error { +func (c *testClient) ScatterRegion(ctx context.Context, regionInfo *split.RegionInfo) error { return nil } @@ -251,15 +250,15 @@ func (c *testClient) GetOperator(ctx context.Context, regionID uint64) (*pdpb.Ge }, nil } -func (c *testClient) ScanRegions(ctx context.Context, key, endKey []byte, limit int) ([]*restore.RegionInfo, error) { +func (c *testClient) ScanRegions(ctx context.Context, key, endKey []byte, limit int) ([]*split.RegionInfo, error) { if c.hook != nil { key, endKey, limit = c.hook.BeforeScanRegions(ctx, key, endKey, limit) } infos := c.regionsInfo.ScanRange(key, endKey, limit) - regions := make([]*restore.RegionInfo, 0, len(infos)) + regions := make([]*split.RegionInfo, 0, len(infos)) for _, info := range infos { - regions = append(regions, &restore.RegionInfo{ + regions = append(regions, &split.RegionInfo{ Region: info.Meta, Leader: info.Leader, }) @@ -288,7 +287,7 @@ func (c *testClient) SetStoresLabel(ctx context.Context, stores []uint64, labelK return nil } -func cloneRegion(region *restore.RegionInfo) *restore.RegionInfo { +func cloneRegion(region *split.RegionInfo) *split.RegionInfo { r := &metapb.Region{} if region.Region != nil { b, _ := region.Region.Marshal() @@ -300,7 +299,7 @@ func cloneRegion(region *restore.RegionInfo) *restore.RegionInfo { b, _ := region.Region.Marshal() _ = l.Unmarshal(b) } - return &restore.RegionInfo{Region: r, Leader: l} + return &split.RegionInfo{Region: r, Leader: l} } // For keys ["", "aay", "bba", "bbh", "cca", ""], the key ranges of @@ -311,7 +310,7 @@ func initTestClient(keys [][]byte, hook clientHook) *testClient { Id: 1, StoreId: 1, } - regions := make(map[uint64]*restore.RegionInfo) + regions := make(map[uint64]*split.RegionInfo) for i := uint64(1); i < uint64(len(keys)); i++ { startKey := keys[i-1] if len(startKey) != 0 { @@ -321,7 +320,7 @@ func initTestClient(keys [][]byte, hook clientHook) *testClient { if len(endKey) != 0 { endKey = codec.EncodeBytes([]byte{}, endKey) } - regions[i] = &restore.RegionInfo{ + regions[i] = &split.RegionInfo{ Region: &metapb.Region{ Id: i, Peers: peers, @@ -338,7 +337,7 @@ func initTestClient(keys [][]byte, hook clientHook) *testClient { return newTestClient(stores, regions, uint64(len(keys)), hook) } -func checkRegionRanges(t *testing.T, regions []*restore.RegionInfo, keys [][]byte) { +func checkRegionRanges(t *testing.T, regions []*split.RegionInfo, keys [][]byte) { for i, r := range regions { _, regionStart, _ := codec.DecodeBytes(r.Region.StartKey, []byte{}) _, regionEnd, _ := codec.DecodeBytes(r.Region.EndKey, []byte{}) @@ -348,21 +347,21 @@ func checkRegionRanges(t *testing.T, regions []*restore.RegionInfo, keys [][]byt } type clientHook interface { - BeforeSplitRegion(ctx context.Context, regionInfo *restore.RegionInfo, keys [][]byte) (*restore.RegionInfo, [][]byte) - AfterSplitRegion(context.Context, *restore.RegionInfo, [][]byte, []*restore.RegionInfo, error) ([]*restore.RegionInfo, error) + BeforeSplitRegion(ctx context.Context, regionInfo *split.RegionInfo, keys [][]byte) (*split.RegionInfo, [][]byte) + AfterSplitRegion(context.Context, *split.RegionInfo, [][]byte, []*split.RegionInfo, error) ([]*split.RegionInfo, error) BeforeScanRegions(ctx context.Context, key, endKey []byte, limit int) ([]byte, []byte, int) - AfterScanRegions([]*restore.RegionInfo, error) ([]*restore.RegionInfo, error) + AfterScanRegions([]*split.RegionInfo, error) ([]*split.RegionInfo, error) } type noopHook struct{} -func (h *noopHook) BeforeSplitRegion(ctx context.Context, regionInfo *restore.RegionInfo, keys [][]byte) (*restore.RegionInfo, [][]byte) { +func (h *noopHook) BeforeSplitRegion(ctx context.Context, regionInfo *split.RegionInfo, keys [][]byte) (*split.RegionInfo, [][]byte) { delayTime := rand.Int31n(10) + 1 time.Sleep(time.Duration(delayTime) * time.Millisecond) return regionInfo, keys } -func (h *noopHook) AfterSplitRegion(c context.Context, r *restore.RegionInfo, keys [][]byte, res []*restore.RegionInfo, err error) ([]*restore.RegionInfo, error) { +func (h *noopHook) AfterSplitRegion(c context.Context, r *split.RegionInfo, keys [][]byte, res []*split.RegionInfo, err error) ([]*split.RegionInfo, error) { return res, err } @@ -370,7 +369,7 @@ func (h *noopHook) BeforeScanRegions(ctx context.Context, key, endKey []byte, li return key, endKey, limit } -func (h *noopHook) AfterScanRegions(res []*restore.RegionInfo, err error) ([]*restore.RegionInfo, error) { +func (h *noopHook) AfterScanRegions(res []*split.RegionInfo, err error) ([]*split.RegionInfo, error) { return res, err } @@ -423,7 +422,7 @@ func doTestBatchSplitRegionByRanges(ctx context.Context, t *testing.T, hook clie // current region ranges: [, aay), [aay, bba), [bba, bbh), [bbh, cca), [cca, ) rangeStart := codec.EncodeBytes([]byte{}, []byte("b")) rangeEnd := codec.EncodeBytes([]byte{}, []byte("c")) - regions, err := restore.PaginateScanRegion(ctx, client, rangeStart, rangeEnd, 5) + regions, err := split.PaginateScanRegion(ctx, client, rangeStart, rangeEnd, 5) require.NoError(t, err) // regions is: [aay, bba), [bba, bbh), [bbh, cca) checkRegionRanges(t, regions, [][]byte{[]byte("aay"), []byte("bba"), []byte("bbh"), []byte("cca")}) @@ -449,7 +448,7 @@ func doTestBatchSplitRegionByRanges(ctx context.Context, t *testing.T, hook clie splitHook.check(t, client) // check split ranges - regions, err = restore.PaginateScanRegion(ctx, client, rangeStart, rangeEnd, 5) + regions, err = split.PaginateScanRegion(ctx, client, rangeStart, rangeEnd, 5) require.NoError(t, err) result := [][]byte{ []byte("b"), []byte("ba"), []byte("bb"), []byte("bba"), []byte("bbh"), []byte("bc"), @@ -503,7 +502,7 @@ type scanRegionEmptyHook struct { cnt int } -func (h *scanRegionEmptyHook) AfterScanRegions(res []*restore.RegionInfo, err error) ([]*restore.RegionInfo, error) { +func (h *scanRegionEmptyHook) AfterScanRegions(res []*split.RegionInfo, err error) ([]*split.RegionInfo, error) { h.cnt++ // skip the first call if h.cnt == 1 { @@ -520,7 +519,7 @@ type splitRegionEpochNotMatchHook struct { noopHook } -func (h *splitRegionEpochNotMatchHook) BeforeSplitRegion(ctx context.Context, regionInfo *restore.RegionInfo, keys [][]byte) (*restore.RegionInfo, [][]byte) { +func (h *splitRegionEpochNotMatchHook) BeforeSplitRegion(ctx context.Context, regionInfo *split.RegionInfo, keys [][]byte) (*split.RegionInfo, [][]byte) { regionInfo, keys = h.noopHook.BeforeSplitRegion(ctx, regionInfo, keys) regionInfo = cloneRegion(regionInfo) // decrease the region epoch, so split region will fail @@ -538,7 +537,7 @@ type splitRegionEpochNotMatchHookRandom struct { cnt atomic.Int32 } -func (h *splitRegionEpochNotMatchHookRandom) BeforeSplitRegion(ctx context.Context, regionInfo *restore.RegionInfo, keys [][]byte) (*restore.RegionInfo, [][]byte) { +func (h *splitRegionEpochNotMatchHookRandom) BeforeSplitRegion(ctx context.Context, regionInfo *split.RegionInfo, keys [][]byte) (*split.RegionInfo, [][]byte) { regionInfo, keys = h.noopHook.BeforeSplitRegion(ctx, regionInfo, keys) if h.cnt.Inc() != 0 { return regionInfo, keys @@ -559,7 +558,7 @@ type splitRegionNoValidKeyHook struct { errorCnt atomic.Int32 } -func (h *splitRegionNoValidKeyHook) BeforeSplitRegion(ctx context.Context, regionInfo *restore.RegionInfo, keys [][]byte) (*restore.RegionInfo, [][]byte) { +func (h *splitRegionNoValidKeyHook) BeforeSplitRegion(ctx context.Context, regionInfo *split.RegionInfo, keys [][]byte) (*split.RegionInfo, [][]byte) { regionInfo, keys = h.noopHook.BeforeSplitRegion(ctx, regionInfo, keys) if h.errorCnt.Inc() <= h.returnErrTimes { // clean keys to trigger "no valid keys" error @@ -619,7 +618,7 @@ type reportAfterSplitHook struct { ch chan<- struct{} } -func (h *reportAfterSplitHook) AfterSplitRegion(ctx context.Context, region *restore.RegionInfo, keys [][]byte, resultRegions []*restore.RegionInfo, err error) ([]*restore.RegionInfo, error) { +func (h *reportAfterSplitHook) AfterSplitRegion(ctx context.Context, region *split.RegionInfo, keys [][]byte, resultRegions []*split.RegionInfo, err error) ([]*split.RegionInfo, error) { h.ch <- struct{}{} return resultRegions, err } @@ -701,7 +700,7 @@ func doTestBatchSplitByRangesWithClusteredIndex(t *testing.T, hook clientHook) { startKey := codec.EncodeBytes([]byte{}, rangeKeys[0]) endKey := codec.EncodeBytes([]byte{}, rangeKeys[len(rangeKeys)-1]) // check split ranges - regions, err := restore.PaginateScanRegion(ctx, client, startKey, endKey, 5) + regions, err := split.PaginateScanRegion(ctx, client, startKey, endKey, 5) require.NoError(t, err) require.Equal(t, len(ranges)+1, len(regions)) @@ -729,14 +728,14 @@ func TestNeedSplit(t *testing.T) { keys := []int64{10, 100, 500, 1000, 999999, -1} start := tablecodec.EncodeRowKeyWithHandle(tableID, kv.IntHandle(0)) regionStart := codec.EncodeBytes([]byte{}, start) - regions := make([]*restore.RegionInfo, 0) + regions := make([]*split.RegionInfo, 0) for _, end := range keys { var regionEndKey []byte if end >= 0 { endKey := tablecodec.EncodeRowKeyWithHandle(tableID, kv.IntHandle(end)) regionEndKey = codec.EncodeBytes([]byte{}, endKey) } - region := &restore.RegionInfo{ + region := &split.RegionInfo{ Region: &metapb.Region{ Id: 1, Peers: peers, diff --git a/br/pkg/lightning/backend/local/mock/local_test.go b/br/pkg/lightning/backend/local/mock/local_test.go new file mode 100644 index 0000000000000..e824e1b97fa93 --- /dev/null +++ b/br/pkg/lightning/backend/local/mock/local_test.go @@ -0,0 +1,65 @@ +package mock + +import ( + "context" + "github.com/pingcap/tidb/br/pkg/lightning/backend/local" + "testing" + + "github.com/coreos/go-semver/semver" + "github.com/golang/mock/gomock" + "github.com/pingcap/tidb/br/pkg/lightning/backend" + "github.com/pingcap/tidb/br/pkg/lightning/mydump" + "github.com/pingcap/tidb/br/pkg/mock" + "github.com/stretchr/testify/require" +) + +var tiFlashReplicaQuery = "SELECT TABLE_SCHEMA, TABLE_NAME FROM information_schema.TIFLASH_REPLICA WHERE REPLICA_COUNT > 0;" + +func TestCheckRequirementsTiFlash(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + glue := mock.NewMockGlue(controller) + exec := mock.NewMockSQLExecutor(controller) + ctx := context.Background() + + dbMetas := []*mydump.MDDatabaseMeta{ + { + Name: "test", + Tables: []*mydump.MDTableMeta{ + { + DB: "test", + Name: "t1", + DataFiles: []mydump.FileInfo{{}}, + }, + { + DB: "test", + Name: "tbl", + DataFiles: []mydump.FileInfo{{}}, + }, + }, + }, + { + Name: "test1", + Tables: []*mydump.MDTableMeta{ + { + DB: "test1", + Name: "t", + DataFiles: []mydump.FileInfo{{}}, + }, + { + DB: "test1", + Name: "tbl", + DataFiles: []mydump.FileInfo{{}}, + }, + }, + }, + } + checkCtx := &backend.CheckCtx{DBMetas: dbMetas} + + glue.EXPECT().GetSQLExecutor().Return(exec) + exec.EXPECT().QueryStringsWithLog(ctx, tiFlashReplicaQuery, gomock.Any(), gomock.Any()). + Return([][]string{{"db", "tbl"}, {"test", "t1"}, {"test1", "tbl"}}, nil) + + err := local.CheckTiFlashVersion4test(ctx, glue, checkCtx, *semver.New("4.0.2")) + require.Regexp(t, "^lightning local backend doesn't support TiFlash in this TiDB version. conflict tables: \\[`test`.`t1`, `test1`.`tbl`\\]", err.Error()) +} diff --git a/br/pkg/lightning/backend/noop/noop.go b/br/pkg/lightning/backend/noop/noop.go index 2ac3e2b346dbb..8d2f850d9a352 100644 --- a/br/pkg/lightning/backend/noop/noop.go +++ b/br/pkg/lightning/backend/noop/noop.go @@ -21,6 +21,7 @@ import ( "github.com/google/uuid" "github.com/pingcap/tidb/br/pkg/lightning/backend" "github.com/pingcap/tidb/br/pkg/lightning/backend/kv" + "github.com/pingcap/tidb/br/pkg/lightning/common" "github.com/pingcap/tidb/br/pkg/lightning/config" "github.com/pingcap/tidb/br/pkg/lightning/log" "github.com/pingcap/tidb/br/pkg/lightning/verification" @@ -155,6 +156,10 @@ func (b noopBackend) ResolveDuplicateRows(ctx context.Context, tbl table.Table, return nil } +func (b noopBackend) TotalMemoryConsume() int64 { + return 0 +} + type noopEncoder struct{} // Close the encoder. @@ -181,6 +186,10 @@ func (w Writer) AppendRows(context.Context, string, []string, kv.Rows) error { return nil } +func (w Writer) AppendRow(context.Context, string, []string, []common.KvPair) error { + return nil +} + func (w Writer) IsSynced() bool { return true } diff --git a/br/pkg/lightning/backend/tidb/tidb.go b/br/pkg/lightning/backend/tidb/tidb.go index 9ae70e7afef1d..11ff8ad1fc232 100644 --- a/br/pkg/lightning/backend/tidb/tidb.go +++ b/br/pkg/lightning/backend/tidb/tidb.go @@ -466,6 +466,9 @@ rowLoop: return nil } +func (be *tidbBackend) TotalMemoryConsume() int64 { + return 0 +} type stmtTask struct { rows tidbRows stmt string @@ -723,6 +726,9 @@ func (w *Writer) AppendRows(ctx context.Context, tableName string, columnNames [ return w.be.WriteRows(ctx, tableName, columnNames, rows) } +func (w *Writer) AppendRow(ctx context.Context, tableName string, columnNames []string, kvs []common.KvPair) error { + return nil +} func (w *Writer) IsSynced() bool { return true } diff --git a/br/pkg/lightning/config/config.go b/br/pkg/lightning/config/config.go index fee2aaf29deb2..a3bc9891b489e 100644 --- a/br/pkg/lightning/config/config.go +++ b/br/pkg/lightning/config/config.go @@ -860,8 +860,39 @@ func (cfg *Config) Adjust(ctx context.Context) error { zap.ByteString("invalid-char-replacement", []byte(cfg.Mydumper.DataInvalidCharReplace))) } + mustHaveInternalConnections, err := cfg.AdjustCommon() + if err != nil { + return err + } + + // mydumper.filter and black-white-list cannot co-exist. + if cfg.HasLegacyBlackWhiteList() { + log.L().Warn("the config `black-white-list` has been deprecated, please replace with `mydumper.filter`") + if !common.StringSliceEqual(cfg.Mydumper.Filter, DefaultFilter) { + return common.ErrInvalidConfig.GenWithStack("`mydumper.filter` and `black-white-list` cannot be simultaneously defined") + } + } + + for _, rule := range cfg.Routes { + if !cfg.Mydumper.CaseSensitive { + rule.ToLower() + } + if err := rule.Valid(); err != nil { + return common.ErrInvalidConfig.Wrap(err).GenWithStack("file route rule is invalid") + } + } + + if err := cfg.CheckAndAdjustTiDBPort(ctx, mustHaveInternalConnections); err != nil { + return err + } + cfg.AdjustMydumper() + cfg.AdjustCheckPoint() + return cfg.CheckAndAdjustFilePath() +} + +func (cfg *Config) AdjustCommon() (bool, error) { if cfg.TikvImporter.Backend == "" { - return common.ErrInvalidConfig.GenWithStack("tikv-importer.backend must not be empty!") + return false, common.ErrInvalidConfig.GenWithStack("tikv-importer.backend must not be empty!") } cfg.TikvImporter.Backend = strings.ToLower(cfg.TikvImporter.Backend) mustHaveInternalConnections := true @@ -880,7 +911,7 @@ func (cfg *Config) Adjust(ctx context.Context) error { } cfg.DefaultVarsForImporterAndLocalBackend() default: - return common.ErrInvalidConfig.GenWithStack("unsupported `tikv-importer.backend` (%s)", cfg.TikvImporter.Backend) + return mustHaveInternalConnections, common.ErrInvalidConfig.GenWithStack("unsupported `tikv-importer.backend` (%s)", cfg.TikvImporter.Backend) } // TODO calculate these from the machine's free memory. @@ -893,7 +924,7 @@ func (cfg *Config) Adjust(ctx context.Context) error { if cfg.TikvImporter.Backend == BackendLocal { if err := cfg.CheckAndAdjustForLocalBackend(); err != nil { - return err + return mustHaveInternalConnections, err } } else { cfg.TikvImporter.DuplicateResolution = DupeResAlgNone @@ -904,7 +935,7 @@ func (cfg *Config) Adjust(ctx context.Context) error { switch cfg.TikvImporter.OnDuplicate { case ReplaceOnDup, IgnoreOnDup, ErrorOnDup: default: - return common.ErrInvalidConfig.GenWithStack( + return mustHaveInternalConnections, common.ErrInvalidConfig.GenWithStack( "unsupported `tikv-importer.on-duplicate` (%s)", cfg.TikvImporter.OnDuplicate) } } @@ -912,36 +943,13 @@ func (cfg *Config) Adjust(ctx context.Context) error { var err error cfg.TiDB.SQLMode, err = mysql.GetSQLMode(cfg.TiDB.StrSQLMode) if err != nil { - return common.ErrInvalidConfig.Wrap(err).GenWithStack("`mydumper.tidb.sql_mode` must be a valid SQL_MODE") + return mustHaveInternalConnections, common.ErrInvalidConfig.Wrap(err).GenWithStack("`mydumper.tidb.sql_mode` must be a valid SQL_MODE") } if err := cfg.CheckAndAdjustSecurity(); err != nil { - return err - } - - // mydumper.filter and black-white-list cannot co-exist. - if cfg.HasLegacyBlackWhiteList() { - log.L().Warn("the config `black-white-list` has been deprecated, please replace with `mydumper.filter`") - if !common.StringSliceEqual(cfg.Mydumper.Filter, DefaultFilter) { - return common.ErrInvalidConfig.GenWithStack("`mydumper.filter` and `black-white-list` cannot be simultaneously defined") - } + return mustHaveInternalConnections, err } - - for _, rule := range cfg.Routes { - if !cfg.Mydumper.CaseSensitive { - rule.ToLower() - } - if err := rule.Valid(); err != nil { - return common.ErrInvalidConfig.Wrap(err).GenWithStack("file route rule is invalid") - } - } - - if err := cfg.CheckAndAdjustTiDBPort(ctx, mustHaveInternalConnections); err != nil { - return err - } - cfg.AdjustMydumper() - cfg.AdjustCheckPoint() - return cfg.CheckAndAdjustFilePath() + return mustHaveInternalConnections, err } func (cfg *Config) CheckAndAdjustForLocalBackend() error { diff --git a/br/pkg/lightning/lightning.go b/br/pkg/lightning/lightning.go index 3c6f0256a740c..7b8339e3f4627 100644 --- a/br/pkg/lightning/lightning.go +++ b/br/pkg/lightning/lightning.go @@ -51,10 +51,13 @@ import ( "github.com/pingcap/tidb/br/pkg/storage" "github.com/pingcap/tidb/br/pkg/utils" "github.com/pingcap/tidb/br/pkg/version/build" + lit "github.com/pingcap/tidb/ddl/lightning" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/shurcooL/httpgzip" "go.uber.org/zap" "go.uber.org/zap/zapcore" + // get rid of `import cycle`: just init expression.RewriteAstExpr,and called at package `backend.kv` + _ "github.com/pingcap/tidb/planner/core" ) type Lightning struct { @@ -388,7 +391,7 @@ func (l *Lightning) run(taskCtx context.Context, taskCfg *config.Config, o *opti // and also put it here could avoid injecting another two SkipRunTask failpoint to caller g := o.glue if g == nil { - db, err := restore.DBFromConfig(ctx, taskCfg.TiDB) + db, err := lit.DBFromConfig(ctx, taskCfg.TiDB) if err != nil { return common.ErrDBConnect.Wrap(err) } @@ -884,7 +887,7 @@ func CleanupMetas(ctx context.Context, cfg *config.Config, tableName string) err tableName = "" } // try to clean up table metas if exists - db, err := restore.DBFromConfig(ctx, cfg.TiDB) + db, err := lit.DBFromConfig(ctx, cfg.TiDB) if err != nil { return errors.Trace(err) } diff --git a/br/pkg/lightning/restore/restore.go b/br/pkg/lightning/restore/restore.go index c776510ae3c9d..5ceff8cf8abe2 100644 --- a/br/pkg/lightning/restore/restore.go +++ b/br/pkg/lightning/restore/restore.go @@ -53,6 +53,7 @@ import ( "github.com/pingcap/tidb/br/pkg/utils" "github.com/pingcap/tidb/br/pkg/version" "github.com/pingcap/tidb/br/pkg/version/build" + lit "github.com/pingcap/tidb/ddl/lightning" "github.com/pingcap/tidb/meta/autoid" "github.com/pingcap/tidb/parser/model" "github.com/pingcap/tidb/util/collate" @@ -374,7 +375,7 @@ func NewRestoreControllerWithPauser( pauser: p.Pauser, backend: backend, tidbGlue: p.Glue, - sysVars: defaultImportantVariables, + sysVars: lit.DefaultImportantVariables, tls: tls, checkTemplate: NewSimpleTemplate(), diff --git a/br/pkg/lightning/restore/tidb.go b/br/pkg/lightning/restore/tidb.go index 0d09b8bd3f576..d7e35b6772dd3 100644 --- a/br/pkg/lightning/restore/tidb.go +++ b/br/pkg/lightning/restore/tidb.go @@ -19,7 +19,6 @@ import ( "database/sql" "fmt" "math" - "strconv" "strings" "github.com/pingcap/errors" @@ -36,13 +35,39 @@ import ( "github.com/pingcap/tidb/parser/model" "github.com/pingcap/tidb/parser/mysql" "go.uber.org/zap" - "golang.org/x/exp/maps" ) +type TiDBManager struct { + db *sql.DB + parser *parser.Parser +} + +func NewTiDBManager(ctx context.Context, dsn config.DBStore, tls *common.TLS) (*TiDBManager, error) { + db, err := DBFromConfig(ctx, dsn) + if err != nil { + return nil, errors.Trace(err) + } + + return NewTiDBManagerWithDB(db, dsn.SQLMode), nil +} + +// NewTiDBManagerWithDB creates a new TiDB manager with an existing database +// connection. +func NewTiDBManagerWithDB(db *sql.DB, sqlMode mysql.SQLMode) *TiDBManager { + parser := parser.New() + parser.SetSQLMode(sqlMode) -// defaultImportantVariables is used in ObtainImportantVariables to retrieve the system + return &TiDBManager{ + db: db, + parser: parser, + } +} +func (timgr *TiDBManager) Close() { + timgr.db.Close() +} +// DefaultImportantVariables is used in ObtainImportantVariables to retrieve the system // variables from downstream which may affect KV encode result. The values record the default // values if missing. -var defaultImportantVariables = map[string]string{ +var DefaultImportantVariables = map[string]string{ "max_allowed_packet": "67108864", "div_precision_increment": "4", "time_zone": "SYSTEM", @@ -52,16 +77,57 @@ var defaultImportantVariables = map[string]string{ "group_concat_max_len": "1024", } -// defaultImportVariablesTiDB is used in ObtainImportantVariables to retrieve the system +// DefaultImportVariablesTiDB is used in ObtainImportantVariables to retrieve the system // variables from downstream in local/importer backend. The values record the default // values if missing. -var defaultImportVariablesTiDB = map[string]string{ +var DefaultImportVariablesTiDB = map[string]string{ "tidb_row_format_version": "1", } -type TiDBManager struct { - db *sql.DB - parser *parser.Parser +func ObtainImportantVariables(ctx context.Context, g glue.SQLExecutor, needTiDBVars bool) map[string]string { + var query strings.Builder + query.WriteString("SHOW VARIABLES WHERE Variable_name IN ('") + first := true + for k := range DefaultImportantVariables { + if first { + first = false + } else { + query.WriteString("','") + } + query.WriteString(k) + } + if needTiDBVars { + for k := range DefaultImportVariablesTiDB { + query.WriteString("','") + query.WriteString(k) + } + } + query.WriteString("')") + kvs, err := g.QueryStringsWithLog(ctx, query.String(), "obtain system variables", log.L()) + if err != nil { + // error is not fatal + log.L().Warn("obtain system variables failed, use default variables instead", log.ShortError(err)) + } + + // convert result into a map. fill in any missing variables with default values. + result := make(map[string]string, len(DefaultImportantVariables)+len(DefaultImportVariablesTiDB)) + for _, kv := range kvs { + result[kv[0]] = kv[1] + } + + setDefaultValue := func(res map[string]string, vars map[string]string) { + for k, defV := range vars { + if _, ok := res[k]; !ok { + res[k] = defV + } + } + } + setDefaultValue(result, DefaultImportantVariables) + if needTiDBVars { + setDefaultValue(result, DefaultImportVariablesTiDB) + } + + return result } func DBFromConfig(ctx context.Context, dsn config.DBStore) (*sql.DB, error) { @@ -114,32 +180,7 @@ func DBFromConfig(ctx context.Context, dsn config.DBStore) (*sql.DB, error) { param.Vars = vars db, err = param.Connect() return db, errors.Trace(err) -} - -func NewTiDBManager(ctx context.Context, dsn config.DBStore, tls *common.TLS) (*TiDBManager, error) { - db, err := DBFromConfig(ctx, dsn) - if err != nil { - return nil, errors.Trace(err) - } - - return NewTiDBManagerWithDB(db, dsn.SQLMode), nil -} - -// NewTiDBManagerWithDB creates a new TiDB manager with an existing database -// connection. -func NewTiDBManagerWithDB(db *sql.DB, sqlMode mysql.SQLMode) *TiDBManager { - parser := parser.New() - parser.SetSQLMode(sqlMode) - - return &TiDBManager{ - db: db, - parser: parser, - } -} - -func (timgr *TiDBManager) Close() { - timgr.db.Close() -} +} func InitSchema(ctx context.Context, g glue.Glue, database string, tablesSchema map[string]string) error { logger := log.With(zap.String("db", database)) @@ -303,52 +344,6 @@ func UpdateGCLifeTime(ctx context.Context, db *sql.DB, gcLifeTime string) error ) } -func ObtainImportantVariables(ctx context.Context, g glue.SQLExecutor, needTiDBVars bool) map[string]string { - var query strings.Builder - query.WriteString("SHOW VARIABLES WHERE Variable_name IN ('") - first := true - for k := range defaultImportantVariables { - if first { - first = false - } else { - query.WriteString("','") - } - query.WriteString(k) - } - if needTiDBVars { - for k := range defaultImportVariablesTiDB { - query.WriteString("','") - query.WriteString(k) - } - } - query.WriteString("')") - kvs, err := g.QueryStringsWithLog(ctx, query.String(), "obtain system variables", log.L()) - if err != nil { - // error is not fatal - log.L().Warn("obtain system variables failed, use default variables instead", log.ShortError(err)) - } - - // convert result into a map. fill in any missing variables with default values. - result := make(map[string]string, len(defaultImportantVariables)+len(defaultImportVariablesTiDB)) - for _, kv := range kvs { - result[kv[0]] = kv[1] - } - - setDefaultValue := func(res map[string]string, vars map[string]string) { - for k, defV := range vars { - if _, ok := res[k]; !ok { - res[k] = defV - } - } - } - setDefaultValue(result, defaultImportantVariables) - if needTiDBVars { - setDefaultValue(result, defaultImportVariablesTiDB) - } - - return result -} - func ObtainNewCollationEnabled(ctx context.Context, g glue.SQLExecutor) (bool, error) { newCollationEnabled := false newCollationVal, err := g.ObtainStringWithLog( diff --git a/br/pkg/lightning/restore/tidb_test.go b/br/pkg/lightning/restore/tidb_test.go index 5d05b041e6fdb..3c5fe7dd916b6 100644 --- a/br/pkg/lightning/restore/tidb_test.go +++ b/br/pkg/lightning/restore/tidb_test.go @@ -35,14 +35,13 @@ import ( "github.com/pingcap/tidb/util/mock" "github.com/stretchr/testify/require" ) - type tidbSuite struct { mockDB sqlmock.Sqlmock timgr *TiDBManager tiGlue glue.Glue } -func newTiDBSuite(t *testing.T) (*tidbSuite, func()) { +func NewTiDBSuite(t *testing.T) (*tidbSuite, func()) { var s tidbSuite db, mock, err := sqlmock.New() require.NoError(t, err) @@ -60,7 +59,7 @@ func newTiDBSuite(t *testing.T) (*tidbSuite, func()) { } func TestCreateTableIfNotExistsStmt(t *testing.T) { - s, clean := newTiDBSuite(t) + s, clean := NewTiDBSuite(t) defer clean() dbName := "testdb" @@ -165,7 +164,7 @@ func TestCreateTableIfNotExistsStmt(t *testing.T) { } func TestInitSchema(t *testing.T) { - s, clean := newTiDBSuite(t) + s, clean := NewTiDBSuite(t) defer clean() ctx := context.Background() @@ -194,7 +193,7 @@ func TestInitSchema(t *testing.T) { } func TestInitSchemaSyntaxError(t *testing.T) { - s, clean := newTiDBSuite(t) + s, clean := NewTiDBSuite(t) defer clean() ctx := context.Background() @@ -211,7 +210,7 @@ func TestInitSchemaSyntaxError(t *testing.T) { } func TestInitSchemaErrorLost(t *testing.T) { - s, clean := newTiDBSuite(t) + s, clean := NewTiDBSuite(t) defer clean() ctx := context.Background() @@ -237,7 +236,7 @@ func TestInitSchemaErrorLost(t *testing.T) { } func TestInitSchemaUnsupportedSchemaError(t *testing.T) { - s, clean := newTiDBSuite(t) + s, clean := NewTiDBSuite(t) defer clean() ctx := context.Background() @@ -260,7 +259,7 @@ func TestInitSchemaUnsupportedSchemaError(t *testing.T) { } func TestDropTable(t *testing.T) { - s, clean := newTiDBSuite(t) + s, clean := NewTiDBSuite(t) defer clean() ctx := context.Background() @@ -275,7 +274,7 @@ func TestDropTable(t *testing.T) { } func TestLoadSchemaInfo(t *testing.T) { - s, clean := newTiDBSuite(t) + s, clean := NewTiDBSuite(t) defer clean() ctx := context.Background() @@ -366,7 +365,7 @@ func TestLoadSchemaInfoMissing(t *testing.T) { } func TestGetGCLifetime(t *testing.T) { - s, clean := newTiDBSuite(t) + s, clean := NewTiDBSuite(t) defer clean() ctx := context.Background() @@ -382,7 +381,7 @@ func TestGetGCLifetime(t *testing.T) { } func TestSetGCLifetime(t *testing.T) { - s, clean := newTiDBSuite(t) + s, clean := NewTiDBSuite(t) defer clean() ctx := context.Background() @@ -398,7 +397,7 @@ func TestSetGCLifetime(t *testing.T) { } func TestAlterAutoInc(t *testing.T) { - s, clean := newTiDBSuite(t) + s, clean := NewTiDBSuite(t) defer clean() ctx := context.Background() @@ -419,7 +418,7 @@ func TestAlterAutoInc(t *testing.T) { } func TestAlterAutoRandom(t *testing.T) { - s, clean := newTiDBSuite(t) + s, clean := NewTiDBSuite(t) defer clean() ctx := context.Background() @@ -444,7 +443,7 @@ func TestAlterAutoRandom(t *testing.T) { } func TestObtainRowFormatVersionSucceed(t *testing.T) { - s, clean := newTiDBSuite(t) + s, clean := NewTiDBSuite(t) defer clean() ctx := context.Background() @@ -480,7 +479,7 @@ func TestObtainRowFormatVersionSucceed(t *testing.T) { } func TestObtainRowFormatVersionFailure(t *testing.T) { - s, clean := newTiDBSuite(t) + s, clean := NewTiDBSuite(t) defer clean() ctx := context.Background() @@ -508,7 +507,7 @@ func TestObtainRowFormatVersionFailure(t *testing.T) { } func TestObtainNewCollationEnabled(t *testing.T) { - s, clean := newTiDBSuite(t) + s, clean := NewTiDBSuite(t) defer clean() ctx := context.Background() diff --git a/br/pkg/restore/split/region.go b/br/pkg/restore/split/region.go new file mode 100644 index 0000000000000..2d8a72f75e072 --- /dev/null +++ b/br/pkg/restore/split/region.go @@ -0,0 +1,21 @@ +package split + +import ( + "bytes" + + "github.com/pingcap/kvproto/pkg/metapb" +) + +// RegionInfo includes a region and the leader of the region. +type RegionInfo struct { + Region *metapb.Region + Leader *metapb.Peer +} + +// ContainsInterior returns whether the region contains the given key, and also +// that the key does not fall on the boundary (start key) of the region. +func (region *RegionInfo) ContainsInterior(key []byte) bool { + return bytes.Compare(key, region.Region.GetStartKey()) > 0 && + (len(region.Region.GetEndKey()) == 0 || + bytes.Compare(key, region.Region.GetEndKey()) < 0) +} diff --git a/br/pkg/restore/split/split.go b/br/pkg/restore/split/split.go new file mode 100644 index 0000000000000..487f72c52c611 --- /dev/null +++ b/br/pkg/restore/split/split.go @@ -0,0 +1,25 @@ +package split + +import "time" + +// Constants for split retry machinery. +const ( + SplitRetryTimes = 32 + SplitRetryInterval = 50 * time.Millisecond + SplitMaxRetryInterval = time.Second + + SplitCheckMaxRetryTimes = 64 + SplitCheckInterval = 8 * time.Millisecond + SplitMaxCheckInterval = time.Second + + ScatterWaitMaxRetryTimes = 64 + ScatterWaitInterval = 50 * time.Millisecond + ScatterMaxWaitInterval = time.Second + ScatterWaitUpperInterval = 180 * time.Second + + ScanRegionPaginationLimit = 128 + + RejectStoreCheckRetryTimes = 64 + RejectStoreCheckInterval = 100 * time.Millisecond + RejectStoreMaxCheckInterval = 2 * time.Second +) diff --git a/br/pkg/restore/split/split_client.go b/br/pkg/restore/split/split_client.go new file mode 100644 index 0000000000000..19e8ea1756066 --- /dev/null +++ b/br/pkg/restore/split/split_client.go @@ -0,0 +1,668 @@ +package split + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "path" + "strconv" + "strings" + "sync" + "time" + + "github.com/pingcap/errors" + "github.com/pingcap/failpoint" + "github.com/pingcap/kvproto/pkg/errorpb" + "github.com/pingcap/kvproto/pkg/kvrpcpb" + "github.com/pingcap/kvproto/pkg/metapb" + "github.com/pingcap/kvproto/pkg/pdpb" + "github.com/pingcap/kvproto/pkg/tikvpb" + "github.com/pingcap/log" + "github.com/pingcap/tidb/store/pdtypes" + pd "github.com/tikv/pd/client" + "go.uber.org/multierr" + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + + "github.com/pingcap/tidb/br/pkg/conn/util" + errors2 "github.com/pingcap/tidb/br/pkg/errors" + "github.com/pingcap/tidb/br/pkg/httputil" + "github.com/pingcap/tidb/br/pkg/logutil" + "github.com/pingcap/tidb/br/pkg/redact" + "github.com/pingcap/tidb/br/pkg/utils/utildb" +) + +// SplitClient is an external client used by RegionSplitter. +type SplitClient interface { + // GetStore gets a store by a store id. + GetStore(ctx context.Context, storeID uint64) (*metapb.Store, error) + // GetRegion gets a region which includes a specified key. + GetRegion(ctx context.Context, key []byte) (*RegionInfo, error) + // GetRegionByID gets a region by a region id. + GetRegionByID(ctx context.Context, regionID uint64) (*RegionInfo, error) + // SplitRegion splits a region from a key, if key is not included in the region, it will return nil. + // note: the key should not be encoded + SplitRegion(ctx context.Context, regionInfo *RegionInfo, key []byte) (*RegionInfo, error) + // BatchSplitRegions splits a region from a batch of keys. + // note: the keys should not be encoded + BatchSplitRegions(ctx context.Context, regionInfo *RegionInfo, keys [][]byte) ([]*RegionInfo, error) + // BatchSplitRegionsWithOrigin splits a region from a batch of keys and return the original region and split new regions + BatchSplitRegionsWithOrigin(ctx context.Context, regionInfo *RegionInfo, keys [][]byte) (*RegionInfo, []*RegionInfo, error) + // ScatterRegion scatters a specified region. + ScatterRegion(ctx context.Context, regionInfo *RegionInfo) error + // GetOperator gets the status of operator of the specified region. + GetOperator(ctx context.Context, regionID uint64) (*pdpb.GetOperatorResponse, error) + // ScanRegion gets a list of regions, starts from the region that contains key. + // Limit limits the maximum number of regions returned. + ScanRegions(ctx context.Context, key, endKey []byte, limit int) ([]*RegionInfo, error) + // GetPlacementRule loads a placement rule from PD. + GetPlacementRule(ctx context.Context, groupID, ruleID string) (pdtypes.Rule, error) + // SetPlacementRule insert or update a placement rule to PD. + SetPlacementRule(ctx context.Context, rule pdtypes.Rule) error + // DeletePlacementRule removes a placement rule from PD. + DeletePlacementRule(ctx context.Context, groupID, ruleID string) error + // SetStoreLabel add or update specified label of stores. If labelValue + // is empty, it clears the label. + SetStoresLabel(ctx context.Context, stores []uint64, labelKey, labelValue string) error +} + +func checkRegionConsistency(startKey, endKey []byte, regions []*RegionInfo) error { + // current pd can't guarantee the consistency of returned regions + if len(regions) == 0 { + return errors.Annotatef(errors2.ErrPDBatchScanRegion, "scan region return empty result, startKey: %s, endkey: %s", + redact.Key(startKey), redact.Key(endKey)) + } + + if bytes.Compare(regions[0].Region.StartKey, startKey) > 0 { + return errors.Annotatef(errors2.ErrPDBatchScanRegion, "first region's startKey > startKey, startKey: %s, regionStartKey: %s", + redact.Key(startKey), redact.Key(regions[0].Region.StartKey)) + } else if len(regions[len(regions)-1].Region.EndKey) != 0 && bytes.Compare(regions[len(regions)-1].Region.EndKey, endKey) < 0 { + return errors.Annotatef(errors2.ErrPDBatchScanRegion, "last region's endKey < startKey, startKey: %s, regionStartKey: %s", + redact.Key(endKey), redact.Key(regions[len(regions)-1].Region.EndKey)) + } + + cur := regions[0] + for _, r := range regions[1:] { + if !bytes.Equal(cur.Region.EndKey, r.Region.StartKey) { + return errors.Annotatef(errors2.ErrPDBatchScanRegion, "region endKey not equal to next region startKey, endKey: %s, startKey: %s", + redact.Key(cur.Region.EndKey), redact.Key(r.Region.StartKey)) + } + cur = r + } + + return nil +} + +// PaginateScanRegion scan regions with a limit pagination and +// return all regions at once. +// It reduces max gRPC message size. +func PaginateScanRegion( + ctx context.Context, client SplitClient, startKey, endKey []byte, limit int, +) ([]*RegionInfo, error) { + if len(endKey) != 0 && bytes.Compare(startKey, endKey) >= 0 { + return nil, errors.Annotatef(errors2.ErrRestoreInvalidRange, "startKey >= endKey, startKey: %s, endkey: %s", + hex.EncodeToString(startKey), hex.EncodeToString(endKey)) + } + + var regions []*RegionInfo + err := utildb.WithRetry(ctx, func() error { + regions = []*RegionInfo{} + scanStartKey := startKey + for { + batch, err := client.ScanRegions(ctx, scanStartKey, endKey, limit) + if err != nil { + return errors.Trace(err) + } + regions = append(regions, batch...) + if len(batch) < limit { + // No more region + break + } + scanStartKey = batch[len(batch)-1].Region.GetEndKey() + if len(scanStartKey) == 0 || + (len(endKey) > 0 && bytes.Compare(scanStartKey, endKey) >= 0) { + // All key space have scanned + break + } + } + if err := checkRegionConsistency(startKey, endKey, regions); err != nil { + log.Warn("failed to scan region, retrying", logutil.ShortError(err)) + return err + } + return nil + }, newScanRegionBackoffer()) + + return regions, err +} + +type scanRegionBackoffer struct { + attempt int +} + +func newScanRegionBackoffer() utildb.Backoffer { + return &scanRegionBackoffer{ + attempt: 3, + } +} + +// NextBackoff returns a duration to wait before retrying again +func (b *scanRegionBackoffer) NextBackoff(err error) time.Duration { + if errors2.ErrPDBatchScanRegion.Equal(err) { + // 500ms * 3 could be enough for splitting remain regions in the hole. + b.attempt-- + return 500 * time.Millisecond + } + b.attempt = 0 + return 0 +} + +// Attempt returns the remain attempt times +func (b *scanRegionBackoffer) Attempt() int { + return b.attempt +} + +const ( + splitRegionMaxRetryTime = 4 +) + +// pdClient is a wrapper of pd client, can be used by RegionSplitter. +type pdClient struct { + mu sync.Mutex + client pd.Client + tlsConf *tls.Config + storeCache map[uint64]*metapb.Store + isRawKv bool + // FIXME when config changed during the lifetime of pdClient, + // this may mislead the scatter. + needScatterVal bool + needScatterInit sync.Once +} + +// NewSplitClient returns a client used by RegionSplitter. +func NewSplitClient(client pd.Client, tlsConf *tls.Config, isRawKv bool) SplitClient { + cli := &pdClient{ + client: client, + tlsConf: tlsConf, + storeCache: make(map[uint64]*metapb.Store), + isRawKv: isRawKv, + } + return cli +} + +func (c *pdClient) needScatter(ctx context.Context) bool { + c.needScatterInit.Do(func() { + var err error + c.needScatterVal, err = c.checkNeedScatter(ctx) + if err != nil { + log.Warn("failed to check whether need to scatter, use permissive strategy: always scatter", logutil.ShortError(err)) + c.needScatterVal = true + } + if !c.needScatterVal { + log.Info("skipping scatter because the replica number isn't less than store count.") + } + }) + return c.needScatterVal +} + +func (c *pdClient) GetStore(ctx context.Context, storeID uint64) (*metapb.Store, error) { + c.mu.Lock() + defer c.mu.Unlock() + store, ok := c.storeCache[storeID] + if ok { + return store, nil + } + store, err := c.client.GetStore(ctx, storeID) + if err != nil { + return nil, errors.Trace(err) + } + c.storeCache[storeID] = store + return store, nil +} + +func (c *pdClient) GetRegion(ctx context.Context, key []byte) (*RegionInfo, error) { + region, err := c.client.GetRegion(ctx, key) + if err != nil { + return nil, errors.Trace(err) + } + if region == nil { + return nil, nil + } + return &RegionInfo{ + Region: region.Meta, + Leader: region.Leader, + }, nil +} + +func (c *pdClient) GetRegionByID(ctx context.Context, regionID uint64) (*RegionInfo, error) { + region, err := c.client.GetRegionByID(ctx, regionID) + if err != nil { + return nil, errors.Trace(err) + } + if region == nil { + return nil, nil + } + return &RegionInfo{ + Region: region.Meta, + Leader: region.Leader, + }, nil +} + +func (c *pdClient) SplitRegion(ctx context.Context, regionInfo *RegionInfo, key []byte) (*RegionInfo, error) { + var peer *metapb.Peer + if regionInfo.Leader != nil { + peer = regionInfo.Leader + } else { + if len(regionInfo.Region.Peers) == 0 { + return nil, errors.Annotate(errors2.ErrRestoreNoPeer, "region does not have peer") + } + peer = regionInfo.Region.Peers[0] + } + storeID := peer.GetStoreId() + store, err := c.GetStore(ctx, storeID) + if err != nil { + return nil, errors.Trace(err) + } + conn, err := grpc.Dial(store.GetAddress(), grpc.WithInsecure()) + if err != nil { + return nil, errors.Trace(err) + } + defer conn.Close() + + client := tikvpb.NewTikvClient(conn) + resp, err := client.SplitRegion(ctx, &kvrpcpb.SplitRegionRequest{ + Context: &kvrpcpb.Context{ + RegionId: regionInfo.Region.Id, + RegionEpoch: regionInfo.Region.RegionEpoch, + Peer: peer, + }, + SplitKey: key, + }) + if err != nil { + return nil, errors.Trace(err) + } + if resp.RegionError != nil { + log.Error("fail to split region", + logutil.Region(regionInfo.Region), + logutil.Key("key", key), + zap.Stringer("regionErr", resp.RegionError)) + return nil, errors.Annotatef(errors2.ErrRestoreSplitFailed, "err=%v", resp.RegionError) + } + + // BUG: Left is deprecated, it may be nil even if split is succeed! + // Assume the new region is the left one. + newRegion := resp.GetLeft() + if newRegion == nil { + regions := resp.GetRegions() + for _, r := range regions { + if bytes.Equal(r.GetStartKey(), regionInfo.Region.GetStartKey()) { + newRegion = r + break + } + } + } + if newRegion == nil { + return nil, errors.Annotate(errors2.ErrRestoreSplitFailed, "new region is nil") + } + var leader *metapb.Peer + // Assume the leaders will be at the same store. + if regionInfo.Leader != nil { + for _, p := range newRegion.GetPeers() { + if p.GetStoreId() == regionInfo.Leader.GetStoreId() { + leader = p + break + } + } + } + return &RegionInfo{ + Region: newRegion, + Leader: leader, + }, nil +} + +func splitRegionWithFailpoint( + ctx context.Context, + regionInfo *RegionInfo, + peer *metapb.Peer, + client tikvpb.TikvClient, + keys [][]byte, +) (*kvrpcpb.SplitRegionResponse, error) { + failpoint.Inject("not-leader-error", func(injectNewLeader failpoint.Value) { + log.Debug("failpoint not-leader-error injected.") + resp := &kvrpcpb.SplitRegionResponse{ + RegionError: &errorpb.Error{ + NotLeader: &errorpb.NotLeader{ + RegionId: regionInfo.Region.Id, + }, + }, + } + if injectNewLeader.(bool) { + resp.RegionError.NotLeader.Leader = regionInfo.Leader + } + failpoint.Return(resp, nil) + }) + failpoint.Inject("somewhat-retryable-error", func() { + log.Debug("failpoint somewhat-retryable-error injected.") + failpoint.Return(&kvrpcpb.SplitRegionResponse{ + RegionError: &errorpb.Error{ + ServerIsBusy: &errorpb.ServerIsBusy{}, + }, + }, nil) + }) + return client.SplitRegion(ctx, &kvrpcpb.SplitRegionRequest{ + Context: &kvrpcpb.Context{ + RegionId: regionInfo.Region.Id, + RegionEpoch: regionInfo.Region.RegionEpoch, + Peer: peer, + }, + SplitKeys: keys, + }) +} + +func (c *pdClient) sendSplitRegionRequest( + ctx context.Context, regionInfo *RegionInfo, keys [][]byte, +) (*kvrpcpb.SplitRegionResponse, error) { + var splitErrors error + for i := 0; i < splitRegionMaxRetryTime; i++ { + var peer *metapb.Peer + // scanRegions may return empty Leader in https://github.com/tikv/pd/blob/v4.0.8/server/grpc_service.go#L524 + // so wee also need check Leader.Id != 0 + if regionInfo.Leader != nil && regionInfo.Leader.Id != 0 { + peer = regionInfo.Leader + } else { + if len(regionInfo.Region.Peers) == 0 { + return nil, multierr.Append(splitErrors, + errors.Annotatef(errors2.ErrRestoreNoPeer, "region[%d] doesn't have any peer", regionInfo.Region.GetId())) + } + peer = regionInfo.Region.Peers[0] + } + storeID := peer.GetStoreId() + store, err := c.GetStore(ctx, storeID) + if err != nil { + return nil, multierr.Append(splitErrors, err) + } + opt := grpc.WithInsecure() + if c.tlsConf != nil { + opt = grpc.WithTransportCredentials(credentials.NewTLS(c.tlsConf)) + } + conn, err := grpc.Dial(store.GetAddress(), opt) + if err != nil { + return nil, multierr.Append(splitErrors, err) + } + defer conn.Close() + client := tikvpb.NewTikvClient(conn) + resp, err := splitRegionWithFailpoint(ctx, regionInfo, peer, client, keys) + if err != nil { + return nil, multierr.Append(splitErrors, err) + } + if resp.RegionError != nil { + log.Warn("fail to split region", + logutil.Region(regionInfo.Region), + zap.Stringer("regionErr", resp.RegionError)) + splitErrors = multierr.Append(splitErrors, + errors.Annotatef(errors2.ErrRestoreSplitFailed, "split region failed: err=%v", resp.RegionError)) + if nl := resp.RegionError.NotLeader; nl != nil { + if leader := nl.GetLeader(); leader != nil { + regionInfo.Leader = leader + } else { + newRegionInfo, findLeaderErr := c.GetRegionByID(ctx, nl.RegionId) + if findLeaderErr != nil { + return nil, multierr.Append(splitErrors, findLeaderErr) + } + if !CheckRegionEpoch(newRegionInfo, regionInfo) { + return nil, multierr.Append(splitErrors, errors2.ErrKVEpochNotMatch) + } + log.Info("find new leader", zap.Uint64("new leader", newRegionInfo.Leader.Id)) + regionInfo = newRegionInfo + } + log.Info("split region meet not leader error, retrying", + zap.Int("retry times", i), + zap.Uint64("regionID", regionInfo.Region.Id), + zap.Any("new leader", regionInfo.Leader), + ) + continue + } + // TODO: we don't handle RegionNotMatch and RegionNotFound here, + // because I think we don't have enough information to retry. + // But maybe we can handle them here by some information the error itself provides. + if resp.RegionError.ServerIsBusy != nil || + resp.RegionError.StaleCommand != nil { + log.Warn("a error occurs on split region", + zap.Int("retry times", i), + zap.Uint64("regionID", regionInfo.Region.Id), + zap.String("error", resp.RegionError.Message), + zap.Any("error verbose", resp.RegionError), + ) + continue + } + return nil, errors.Trace(splitErrors) + } + return resp, nil + } + return nil, errors.Trace(splitErrors) +} + +func (c *pdClient) BatchSplitRegionsWithOrigin( + ctx context.Context, regionInfo *RegionInfo, keys [][]byte, +) (*RegionInfo, []*RegionInfo, error) { + resp, err := c.sendSplitRegionRequest(ctx, regionInfo, keys) + if err != nil { + return nil, nil, errors.Trace(err) + } + + regions := resp.GetRegions() + newRegionInfos := make([]*RegionInfo, 0, len(regions)) + var originRegion *RegionInfo + for _, region := range regions { + var leader *metapb.Peer + + // Assume the leaders will be at the same store. + if regionInfo.Leader != nil { + for _, p := range region.GetPeers() { + if p.GetStoreId() == regionInfo.Leader.GetStoreId() { + leader = p + break + } + } + } + // original region + if region.GetId() == regionInfo.Region.GetId() { + originRegion = &RegionInfo{ + Region: region, + Leader: leader, + } + continue + } + newRegionInfos = append(newRegionInfos, &RegionInfo{ + Region: region, + Leader: leader, + }) + } + return originRegion, newRegionInfos, nil +} + +func (c *pdClient) BatchSplitRegions( + ctx context.Context, regionInfo *RegionInfo, keys [][]byte, +) ([]*RegionInfo, error) { + _, newRegions, err := c.BatchSplitRegionsWithOrigin(ctx, regionInfo, keys) + return newRegions, err +} + +func (c *pdClient) getStoreCount(ctx context.Context) (int, error) { + stores, err := util.GetAllTiKVStores(ctx, c.client, util.SkipTiFlash) + if err != nil { + return 0, err + } + return len(stores), err +} + +func (c *pdClient) getMaxReplica(ctx context.Context) (int, error) { + api := c.getPDAPIAddr() + configAPI := api + "/pd/api/v1/config" + req, err := http.NewRequestWithContext(ctx, "GET", configAPI, nil) + if err != nil { + return 0, errors.Trace(err) + } + res, err := httputil.NewClient(c.tlsConf).Do(req) + if err != nil { + return 0, errors.Trace(err) + } + var conf pdtypes.ReplicationConfig + if err := json.NewDecoder(res.Body).Decode(&conf); err != nil { + return 0, errors.Trace(err) + } + return int(conf.MaxReplicas), nil +} + +func (c *pdClient) checkNeedScatter(ctx context.Context) (bool, error) { + storeCount, err := c.getStoreCount(ctx) + if err != nil { + return false, err + } + maxReplica, err := c.getMaxReplica(ctx) + if err != nil { + return false, err + } + log.Info("checking whether need to scatter", zap.Int("store", storeCount), zap.Int("max-replica", maxReplica)) + // Skipping scatter may lead to leader unbalanced, + // currently, we skip scatter only when: + // 1. max-replica > store-count (Probably a misconfigured or playground cluster.) + // 2. store-count == 1 (No meaning for scattering.) + // We can still omit scatter when `max-replica == store-count`, if we create a BalanceLeader operator here, + // however, there isn't evidence for transform leader is much faster than scattering empty regions. + return storeCount >= maxReplica && storeCount > 1, nil +} + +func (c *pdClient) ScatterRegion(ctx context.Context, regionInfo *RegionInfo) error { + if !c.needScatter(ctx) { + return nil + } + return c.client.ScatterRegion(ctx, regionInfo.Region.GetId()) +} + +func (c *pdClient) GetOperator(ctx context.Context, regionID uint64) (*pdpb.GetOperatorResponse, error) { + return c.client.GetOperator(ctx, regionID) +} + +func (c *pdClient) ScanRegions(ctx context.Context, key, endKey []byte, limit int) ([]*RegionInfo, error) { + regions, err := c.client.ScanRegions(ctx, key, endKey, limit) + if err != nil { + return nil, errors.Trace(err) + } + regionInfos := make([]*RegionInfo, 0, len(regions)) + for _, region := range regions { + regionInfos = append(regionInfos, &RegionInfo{ + Region: region.Meta, + Leader: region.Leader, + }) + } + return regionInfos, nil +} + +func (c *pdClient) GetPlacementRule(ctx context.Context, groupID, ruleID string) (pdtypes.Rule, error) { + var rule pdtypes.Rule + addr := c.getPDAPIAddr() + if addr == "" { + return rule, errors.Annotate(errors2.ErrRestoreSplitFailed, "failed to add stores labels: no leader") + } + req, err := http.NewRequestWithContext(ctx, "GET", addr+path.Join("/pd/api/v1/config/rule", groupID, ruleID), nil) + if err != nil { + return rule, errors.Trace(err) + } + res, err := httputil.NewClient(c.tlsConf).Do(req) + if err != nil { + return rule, errors.Trace(err) + } + b, err := io.ReadAll(res.Body) + if err != nil { + return rule, errors.Trace(err) + } + res.Body.Close() + err = json.Unmarshal(b, &rule) + if err != nil { + return rule, errors.Trace(err) + } + return rule, nil +} + +func (c *pdClient) SetPlacementRule(ctx context.Context, rule pdtypes.Rule) error { + addr := c.getPDAPIAddr() + if addr == "" { + return errors.Annotate(errors2.ErrPDLeaderNotFound, "failed to add stores labels") + } + m, _ := json.Marshal(rule) + req, err := http.NewRequestWithContext(ctx, "POST", addr+path.Join("/pd/api/v1/config/rule"), bytes.NewReader(m)) + if err != nil { + return errors.Trace(err) + } + res, err := httputil.NewClient(c.tlsConf).Do(req) + if err != nil { + return errors.Trace(err) + } + return errors.Trace(res.Body.Close()) +} + +func (c *pdClient) DeletePlacementRule(ctx context.Context, groupID, ruleID string) error { + addr := c.getPDAPIAddr() + if addr == "" { + return errors.Annotate(errors2.ErrPDLeaderNotFound, "failed to add stores labels") + } + req, err := http.NewRequestWithContext(ctx, "DELETE", addr+path.Join("/pd/api/v1/config/rule", groupID, ruleID), nil) + if err != nil { + return errors.Trace(err) + } + res, err := httputil.NewClient(c.tlsConf).Do(req) + if err != nil { + return errors.Trace(err) + } + return errors.Trace(res.Body.Close()) +} + +func (c *pdClient) SetStoresLabel( + ctx context.Context, stores []uint64, labelKey, labelValue string, +) error { + b := []byte(fmt.Sprintf(`{"%s": "%s"}`, labelKey, labelValue)) + addr := c.getPDAPIAddr() + if addr == "" { + return errors.Annotate(errors2.ErrPDLeaderNotFound, "failed to add stores labels") + } + httpCli := httputil.NewClient(c.tlsConf) + for _, id := range stores { + req, err := http.NewRequestWithContext( + ctx, "POST", + addr+path.Join("/pd/api/v1/store", strconv.FormatUint(id, 10), "label"), + bytes.NewReader(b), + ) + if err != nil { + return errors.Trace(err) + } + res, err := httpCli.Do(req) + if err != nil { + return errors.Trace(err) + } + err = res.Body.Close() + if err != nil { + return errors.Trace(err) + } + } + return nil +} + +func (c *pdClient) getPDAPIAddr() string { + addr := c.client.GetLeaderAddr() + if addr != "" && !strings.HasPrefix(addr, "http") { + addr = "http://" + addr + } + return strings.TrimRight(addr, "/") +} + +func CheckRegionEpoch(new, old *RegionInfo) bool { + return new.Region.GetId() == old.Region.GetId() && + new.Region.GetRegionEpoch().GetVersion() == old.Region.GetRegionEpoch().GetVersion() && + new.Region.GetRegionEpoch().GetConfVer() == old.Region.GetRegionEpoch().GetConfVer() +} diff --git a/br/pkg/utils/utildb/db.go b/br/pkg/utils/utildb/db.go new file mode 100644 index 0000000000000..537cda6b04dbf --- /dev/null +++ b/br/pkg/utils/utildb/db.go @@ -0,0 +1,32 @@ +// Copyright 2021 PingCAP, Inc. Licensed under Apache-2.0. + +package utildb + +import ( + "context" + "database/sql" +) + +var ( + // check sql.DB and sql.Conn implement QueryExecutor and DBExecutor + _ DBExecutor = &sql.DB{} + _ DBExecutor = &sql.Conn{} +) + +// QueryExecutor is a interface for exec query +type QueryExecutor interface { + QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) + QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row +} + +// StmtExecutor define both query and exec methods +type StmtExecutor interface { + QueryExecutor + ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) +} + +// DBExecutor is a interface for statements and txn +type DBExecutor interface { + StmtExecutor + BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) +} diff --git a/br/pkg/utils/utildb/retry.go b/br/pkg/utils/utildb/retry.go new file mode 100644 index 0000000000000..09a9031acab18 --- /dev/null +++ b/br/pkg/utils/utildb/retry.go @@ -0,0 +1,143 @@ +// Copyright 2020 PingCAP, Inc. Licensed under Apache-2.0. + +package utildb + +import ( + "context" + "database/sql" + stderrors "errors" + "io" + "net" + "reflect" + "regexp" + "strings" + "time" + + "github.com/go-sql-driver/mysql" + "github.com/pingcap/errors" + "go.uber.org/multierr" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + tmysql "github.com/pingcap/tidb/errno" + tidbkv "github.com/pingcap/tidb/kv" +) + +var retryableServerError = []string{ + "server closed", + "connection refused", + "connection reset by peer", + "channel closed", + "error trying to connect", + "connection closed before message completed", + "body write aborted", + "error during dispatch", + "put object timeout", +} + +// RetryableFunc presents a retryable operation. +type RetryableFunc func() error + +// Backoffer implements a backoff policy for retrying operations. +type Backoffer interface { + // NextBackoff returns a duration to wait before retrying again + NextBackoff(err error) time.Duration + // Attempt returns the remain attempt times + Attempt() int +} + +// WithRetry retries a given operation with a backoff policy. +// +// Returns nil if `retryableFunc` succeeded at least once. Otherwise, returns a +// multierr containing all errors encountered. +func WithRetry( + ctx context.Context, + retryableFunc RetryableFunc, + backoffer Backoffer, +) error { + var allErrors error + for backoffer.Attempt() > 0 { + err := retryableFunc() + if err != nil { + allErrors = multierr.Append(allErrors, err) + select { + case <-ctx.Done(): + return allErrors // nolint:wrapcheck + case <-time.After(backoffer.NextBackoff(err)): + } + } else { + return nil + } + } + return allErrors // nolint:wrapcheck +} + +// MessageIsRetryableStorageError checks whether the message returning from TiKV is retryable ExternalStorageError. +func MessageIsRetryableStorageError(msg string) bool { + msgLower := strings.ToLower(msg) + // UNSAFE! TODO: Add a error type for retryable connection error. + for _, errStr := range retryableServerError { + if strings.Contains(msgLower, errStr) { + return true + } + } + return false +} + +// sqlmock uses fmt.Errorf to produce expectation failures, which will cause +// unnecessary retry if not specially handled >:( +var stdFatalErrorsRegexp = regexp.MustCompile( + `^call to (?s:.*) was not expected|arguments do not match:|could not match actual sql|mock non-retryable error`, +) +var stdErrorType = reflect.TypeOf(stderrors.New("")) + +// IsRetryableError returns whether the error is transient (e.g. network +// connection dropped) or irrecoverable (e.g. user pressing Ctrl+C). This +// function returns `false` (irrecoverable) if `err == nil`. +// +// If the error is a multierr, returns true only if all suberrors are retryable. +func IsRetryableError(err error) bool { + for _, singleError := range errors.Errors(err) { + if !isSingleRetryableError(singleError) { + return false + } + } + return true +} + +func isSingleRetryableError(err error) bool { + err = errors.Cause(err) + + switch err { + case nil, context.Canceled, context.DeadlineExceeded, io.EOF, sql.ErrNoRows: + return false + } + if tidbkv.ErrKeyExists.Equal(err) || strings.Contains(err.Error(), "1062") { + return false + } + + switch nerr := err.(type) { + case net.Error: + return nerr.Timeout() + case *mysql.MySQLError: + switch nerr.Number { + // ErrLockDeadlock can retry to commit while meet deadlock + case tmysql.ErrUnknown, tmysql.ErrLockDeadlock, tmysql.ErrWriteConflictInTiDB, tmysql.ErrPDServerTimeout, tmysql.ErrTiKVServerTimeout, tmysql.ErrTiKVServerBusy, tmysql.ErrResolveLockTimeout, tmysql.ErrRegionUnavailable: + return true + default: + return false + } + default: + switch status.Code(err) { + case codes.DeadlineExceeded, codes.NotFound, codes.AlreadyExists, codes.PermissionDenied, codes.ResourceExhausted, codes.Aborted, codes.OutOfRange, codes.Unavailable, codes.DataLoss: + return true + case codes.Unknown: + if reflect.TypeOf(err) == stdErrorType { + return !stdFatalErrorsRegexp.MatchString(err.Error()) + } + return true + default: + return false + } + } +} diff --git a/br/pkg/utils/utildb/retry_test.go b/br/pkg/utils/utildb/retry_test.go new file mode 100644 index 0000000000000..2523d804d5f26 --- /dev/null +++ b/br/pkg/utils/utildb/retry_test.go @@ -0,0 +1,63 @@ +package utildb + +import ( + "context" + "fmt" + "io" + "net" + + "github.com/go-sql-driver/mysql" + . "github.com/pingcap/check" + "github.com/pingcap/errors" + "go.uber.org/multierr" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + tmysql "github.com/pingcap/tidb/errno" +) + +type utilSuite struct{} + +var _ = Suite(&utilSuite{}) + +func (s *utilSuite) TestIsRetryableError(c *C) { + c.Assert(IsRetryableError(context.Canceled), IsFalse) + c.Assert(IsRetryableError(context.DeadlineExceeded), IsFalse) + c.Assert(IsRetryableError(io.EOF), IsFalse) + c.Assert(IsRetryableError(&net.AddrError{}), IsFalse) + c.Assert(IsRetryableError(&net.DNSError{}), IsFalse) + c.Assert(IsRetryableError(&net.DNSError{IsTimeout: true}), IsTrue) + + // MySQL Errors + c.Assert(IsRetryableError(&mysql.MySQLError{}), IsFalse) + c.Assert(IsRetryableError(&mysql.MySQLError{Number: tmysql.ErrUnknown}), IsTrue) + c.Assert(IsRetryableError(&mysql.MySQLError{Number: tmysql.ErrLockDeadlock}), IsTrue) + c.Assert(IsRetryableError(&mysql.MySQLError{Number: tmysql.ErrPDServerTimeout}), IsTrue) + c.Assert(IsRetryableError(&mysql.MySQLError{Number: tmysql.ErrTiKVServerTimeout}), IsTrue) + c.Assert(IsRetryableError(&mysql.MySQLError{Number: tmysql.ErrTiKVServerBusy}), IsTrue) + c.Assert(IsRetryableError(&mysql.MySQLError{Number: tmysql.ErrResolveLockTimeout}), IsTrue) + c.Assert(IsRetryableError(&mysql.MySQLError{Number: tmysql.ErrRegionUnavailable}), IsTrue) + c.Assert(IsRetryableError(&mysql.MySQLError{Number: tmysql.ErrWriteConflictInTiDB}), IsTrue) + + // gRPC Errors + c.Assert(IsRetryableError(status.Error(codes.Canceled, "")), IsFalse) + c.Assert(IsRetryableError(status.Error(codes.Unknown, "")), IsTrue) + c.Assert(IsRetryableError(status.Error(codes.DeadlineExceeded, "")), IsTrue) + c.Assert(IsRetryableError(status.Error(codes.NotFound, "")), IsTrue) + c.Assert(IsRetryableError(status.Error(codes.AlreadyExists, "")), IsTrue) + c.Assert(IsRetryableError(status.Error(codes.PermissionDenied, "")), IsTrue) + c.Assert(IsRetryableError(status.Error(codes.ResourceExhausted, "")), IsTrue) + c.Assert(IsRetryableError(status.Error(codes.Aborted, "")), IsTrue) + c.Assert(IsRetryableError(status.Error(codes.OutOfRange, "")), IsTrue) + c.Assert(IsRetryableError(status.Error(codes.Unavailable, "")), IsTrue) + c.Assert(IsRetryableError(status.Error(codes.DataLoss, "")), IsTrue) + + // sqlmock errors + c.Assert(IsRetryableError(fmt.Errorf("call to database Close was not expected")), IsFalse) + c.Assert(IsRetryableError(errors.New("call to database Close was not expected")), IsTrue) + + // multierr + c.Assert(IsRetryableError(multierr.Combine(context.Canceled, context.Canceled)), IsFalse) + c.Assert(IsRetryableError(multierr.Combine(&net.DNSError{IsTimeout: true}, &net.DNSError{IsTimeout: true})), IsTrue) + c.Assert(IsRetryableError(multierr.Combine(context.Canceled, &net.DNSError{IsTimeout: true})), IsFalse) +} diff --git a/config/config.go b/config/config.go index fc335c3f75850..d55fc4aa9e325 100644 --- a/config/config.go +++ b/config/config.go @@ -84,6 +84,8 @@ const ( DefExpensiveQueryTimeThreshold = 60 // DefMemoryUsageAlarmRatio is the threshold triggering an alarm which the memory usage of tidb-server instance exceeds. DefMemoryUsageAlarmRatio = 0.8 + // DefLightningSortPath is the default sort dir for add index lightning solution + DefLightningSortPath = "" ) // Valid config maps @@ -269,6 +271,8 @@ type Config struct { CheckMb4ValueInUTF8 AtomicBool `toml:"check-mb4-value-in-utf8" json:"check-mb4-value-in-utf8"` EnableCollectExecutionInfo bool `toml:"enable-collect-execution-info" json:"enable-collect-execution-info"` Plugin Plugin `toml:"plugin" json:"plugin"` + // LightningSortPath used to specific the lighting DDL local sort path. + LightningSortPath string `toml:"lightning-sort-path" json:"lightning-sort-path"` } // UpdateTempStoragePath is to update the `TempStoragePath` if port/statusPort was changed diff --git a/config/const.go b/config/const.go index 42c314cd64cef..9196e1b9929d8 100644 --- a/config/const.go +++ b/config/const.go @@ -16,6 +16,3 @@ package config // DefRowsForSampleRate is default sample rows used to calculate samplerate. const DefRowsForSampleRate = 110000 - -// TrackMemWhenExceeds is the threshold when memory usage needs to be tracked. -const TrackMemWhenExceeds = 104857600 // 100MB diff --git a/ddl/backfilling.go b/ddl/backfilling.go index ab39a1cff1b65..4158f9963a588 100644 --- a/ddl/backfilling.go +++ b/ddl/backfilling.go @@ -36,6 +36,7 @@ import ( "github.com/pingcap/tidb/store/copr" "github.com/pingcap/tidb/store/driver/backoff" "github.com/pingcap/tidb/table" + "github.com/pingcap/tidb/table/tables" "github.com/pingcap/tidb/tablecodec" "github.com/pingcap/tidb/util" "github.com/pingcap/tidb/util/dbterror" @@ -533,6 +534,39 @@ func loadDDLReorgVars(w *worker) error { return ddlutil.LoadDDLReorgVars(w.ctx, ctx) } +func pruneDecodeColMap(colMap map[int64]decoder.Column, t table.Table, indexInfo *model.IndexInfo) map[int64]decoder.Column { + resultMap := make(map[int64]decoder.Column) + virtualGeneratedColumnStack := make([]*model.ColumnInfo, 0) + for _, idxCol := range indexInfo.Columns { + if isVirtualGeneratedColumn(t.Meta().Columns[idxCol.Offset]) { + virtualGeneratedColumnStack = append(virtualGeneratedColumnStack, t.Meta().Columns[idxCol.Offset]) + } + resultMap[t.Meta().Columns[idxCol.Offset].ID] = colMap[t.Meta().Columns[idxCol.Offset].ID] + } + if t.Meta().IsCommonHandle { + for _, pkCol := range tables.TryGetCommonPkColumns(t) { + resultMap[pkCol.ID] = colMap[pkCol.ID] + } + } else if t.Meta().PKIsHandle { + pkCol := t.Meta().GetPkColInfo() + resultMap[pkCol.ID] = colMap[pkCol.ID] + } + + for len(virtualGeneratedColumnStack) > 0 { + checkCol := virtualGeneratedColumnStack[0] + for dColName := range checkCol.Dependences { + col := model.FindColumnInfo(t.Meta().Columns, dColName) + if isVirtualGeneratedColumn(col) { + virtualGeneratedColumnStack = append(virtualGeneratedColumnStack, col) + } + resultMap[col.ID] = colMap[col.ID] + } + virtualGeneratedColumnStack = virtualGeneratedColumnStack[1:] + } + + return resultMap +} + func makeupDecodeColMap(sessCtx sessionctx.Context, t table.Table) (map[int64]decoder.Column, error) { dbName := model.NewCIStr(sessCtx.GetSessionVars().CurrentDB) writableColInfos := make([]*model.ColumnInfo, 0, len(t.WritableCols())) @@ -588,6 +622,10 @@ func (w *worker) writePhysicalTableRecord(t table.PhysicalTable, bfWorkerType ba return errors.Trace(err) } + if indexInfo != nil { + decodeColMap = pruneDecodeColMap(decodeColMap, t, indexInfo) + } + if err := w.isReorgRunnable(reorgInfo.Job); err != nil { return errors.Trace(err) } @@ -603,6 +641,20 @@ func (w *worker) writePhysicalTableRecord(t table.PhysicalTable, bfWorkerType ba // variable.ddlReorgWorkerCounter can be modified by system variable "tidb_ddl_reorg_worker_cnt". workerCnt := variable.GetDDLReorgWorkerCounter() + // Caculate worker count for lightnint. + // if litWorkerCnt is 0 or err exist, means not good for lightning execution, + // then go back use kernel way to reorg index. + var litWorkerCnt int + if reorgInfo.Meta.IsLightningEnabled { + litWorkerCnt, err = prepareLightningEngine(job, indexInfo.ID, int(workerCnt)) + if err != nil || litWorkerCnt == 0 { + reorgInfo.Meta.IsLightningEnabled = false + } else { + if workerCnt > int32(litWorkerCnt) { + workerCnt = int32(litWorkerCnt) + } + } + } backfillWorkers := make([]*backfillWorker, 0, workerCnt) defer func() { closeBackfillWorkers(backfillWorkers) @@ -624,6 +676,16 @@ func (w *worker) writePhysicalTableRecord(t table.PhysicalTable, bfWorkerType ba if len(kvRanges) < int(workerCnt) { workerCnt = int32(len(kvRanges)) } + + if reorgInfo.Meta.IsLightningEnabled && workerCnt > int32(litWorkerCnt) { + count, err := prepareLightningEngine(job, indexInfo.ID, int(workerCnt - int32(litWorkerCnt))) + if err != nil || count == 0 { + workerCnt = int32(litWorkerCnt) + } else { + workerCnt = int32(litWorkerCnt + count) + } + } + // Enlarge the worker size. for i := len(backfillWorkers); i < int(workerCnt); i++ { sessCtx := newContext(reorgInfo.d.store) @@ -647,10 +709,20 @@ func (w *worker) writePhysicalTableRecord(t table.PhysicalTable, bfWorkerType ba switch bfWorkerType { case typeAddIndexWorker: - idxWorker := newAddIndexWorker(sessCtx, w, i, t, indexInfo, decodeColMap, reorgInfo) - idxWorker.priority = job.Priority - backfillWorkers = append(backfillWorkers, idxWorker.backfillWorker) - go idxWorker.backfillWorker.run(reorgInfo.d, idxWorker, job) + // Firstly, check and try lightning path + if reorgInfo.Meta.IsLightningEnabled { + idxWorker, err := newAddIndexWorkerLit(sessCtx, w, i, t, indexInfo, decodeColMap, reorgInfo, job.ID) + if err == nil { + idxWorker.priority = job.Priority + backfillWorkers = append(backfillWorkers, idxWorker.backfillWorker) + go idxWorker.backfillWorker.run(reorgInfo.d, idxWorker, job) + } + } else { + idxWorker := newAddIndexWorker(sessCtx, w, i, t, indexInfo, decodeColMap, reorgInfo) + idxWorker.priority = job.Priority + backfillWorkers = append(backfillWorkers, idxWorker.backfillWorker) + go idxWorker.backfillWorker.run(reorgInfo.d, idxWorker, job) + } case typeUpdateColumnWorker: // Setting InCreateOrAlterStmt tells the difference between SELECT casting and ALTER COLUMN casting. sessCtx.GetSessionVars().StmtCtx.InCreateOrAlterStmt = true @@ -698,15 +770,25 @@ func (w *worker) writePhysicalTableRecord(t table.PhysicalTable, bfWorkerType ba zap.Int("regionCnt", len(kvRanges)), zap.String("startHandle", tryDecodeToHandleString(startKey)), zap.String("endHandle", tryDecodeToHandleString(endKey))) + + // Disk quota checking and import data into TiKV if needed. + // Do lightning flush data to make checkpoint. + if reorgInfo.Meta.IsLightningEnabled { + if importPartialDataToTiKV(job.ID, indexInfo.ID) != nil { + return errors.Trace(err) + } + } remains, err := w.sendRangeTaskToWorkers(t, backfillWorkers, reorgInfo, &totalAddedCount, kvRanges) if err != nil { return errors.Trace(err) } - if len(remains) == 0 { break } startKey = remains[0].StartKey + if err != nil { + return errors.Trace(err) + } } return nil } diff --git a/ddl/ddl.go b/ddl/ddl.go index cb6674691f9fa..9ac107dd04888 100644 --- a/ddl/ddl.go +++ b/ddl/ddl.go @@ -32,6 +32,7 @@ import ( "github.com/pingcap/errors" "github.com/pingcap/failpoint" "github.com/pingcap/tidb/config" + lit "github.com/pingcap/tidb/ddl/lightning" "github.com/pingcap/tidb/ddl/util" "github.com/pingcap/tidb/infoschema" "github.com/pingcap/tidb/kv" @@ -516,6 +517,9 @@ func (d *ddl) Start(ctxPool *pools.ResourcePool) error { // Start some background routine to manage TiFlash replica. d.wg.Run(d.PollTiFlashRoutine) + + // Init Lighting Global environment. Once met error then the + lit.InitGolbalLightningBackendEnv() return nil } diff --git a/ddl/index.go b/ddl/index.go index a94bc04669d09..14cf5274d23f9 100644 --- a/ddl/index.go +++ b/ddl/index.go @@ -595,7 +595,32 @@ func doReorgWorkForCreateIndex(w *worker, d *ddlCtx, t *meta.Meta, job *model.Jo return false, ver, errors.Trace(err) } + // If reorg task started already, now be here for restore previous execution. + if reorgInfo.Meta.IsLightningEnabled { + // If reorg task can not be restore with lightning execution, should restart reorg task to keep data consist. + if !canRestoreReorgTask(reorgInfo, indexInfo.ID) { + reorgInfo, err = getReorgInfo(d.jobContext(job), d, rh, job, tbl, elements) + if err != nil || reorgInfo.first { + return false, ver, errors.Trace(err) + } + } + } + + // Check and set up lightning Backend. Whether use lightning add index will depends on + // TiDBFastDDL sysvars is true or false at this time. Also if IsLightningEnabled mean + // restore lightning reorg task, no need to init the lightning environment another time. + if IsAllowFastDDL() && !reorgInfo.Meta.IsLightningEnabled { + // Check if the reorg task is re-entry task, If TiDB is restarted, then currently + // reorg task should be restart. + err = prepareBackend(w.ctx, indexInfo.Unique, job, reorgInfo.ReorgMeta.SQLMode) + // Once Env is created well, set IsLightningOk to true. + if err == nil { + reorgInfo.Meta.IsLightningEnabled = true + } + } + err = w.runReorgJob(rh, reorgInfo, tbl.Meta(), d.lease, func() (addIndexErr error) { + defer util.Recover(metrics.LabelDDL, "onCreateIndex", func() { addIndexErr = dbterror.ErrCancelledDDLJob.GenWithStack("add table `%v` index `%v` panic", tbl.Meta().Name, indexInfo.Name) @@ -614,8 +639,19 @@ func doReorgWorkForCreateIndex(w *worker, d *ddlCtx, t *meta.Meta, job *model.Jo logutil.BgLogger().Warn("[ddl] run add index job failed, convert job to rollback, RemoveDDLReorgHandle failed", zap.String("job", job.String()), zap.Error(err1)) } } + // Clean job related lightning backend data, will handle both user cancel ddl job and + // others errors that occurs in reorg processing. + // For error that will rollback the add index statement, here only remove locale lightning + // files, other rollback process will follow add index roll back flow. + cleanUpLightningEnv(reorgInfo, true, indexInfo.ID) + return false, ver, errors.Trace(err) } + // Ingest data to TiKV + importIndexDataToStore(w.ctx, reorgInfo, indexInfo.ID, indexInfo.Unique, tbl) + + // Cleanup lightning environment + cleanUpLightningEnv(reorgInfo, false) return true, ver, errors.Trace(err) } diff --git a/ddl/index_lightning.go b/ddl/index_lightning.go new file mode 100644 index 0000000000000..c6a66313a5919 --- /dev/null +++ b/ddl/index_lightning.go @@ -0,0 +1,210 @@ +// Copyright 2022 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package ddl + +import ( + "context" + "time" + + "github.com/pingcap/errors" + "github.com/pingcap/failpoint" + lit "github.com/pingcap/tidb/ddl/lightning" + "github.com/pingcap/tidb/kv" + "github.com/pingcap/tidb/metrics" + "github.com/pingcap/tidb/parser/model" + "github.com/pingcap/tidb/parser/mysql" + "github.com/pingcap/tidb/sessionctx" + "github.com/pingcap/tidb/sessionctx/variable" + "github.com/pingcap/tidb/table" + "github.com/pingcap/tidb/table/tables" + "github.com/pingcap/tidb/types" + decoder "github.com/pingcap/tidb/util/rowDecoder" +) + +const ( + BackfillProgressPercent float64 = 0.6 +) + +// Whether Fast DDl is allowed. +func IsAllowFastDDL() bool { + // Only when both TiDBFastDDL is set to on and Lightning env is inited successful, + // the add index could choose lightning path to do backfill procedure. + if variable.FastDDL.Load() && lit.GlobalLightningEnv.IsInited { + return true + } else { + return false + } +} + +func prepareBackend(ctx context.Context, unique bool, job *model.Job, sqlMode mysql.SQLMode) (err error) { + bcKey := lit.GenBackendContextKey(job.ID) + // Create and regist backend of lightning + err = lit.GlobalLightningEnv.LitMemRoot.RegistBackendContext(ctx, unique, bcKey, sqlMode) + if err != nil { + lit.GlobalLightningEnv.LitMemRoot.DeleteBackendContext(bcKey) + return err + } + + return err +} + +func prepareLightningEngine(job *model.Job, indexId int64, workerCnt int) (wCnt int, err error) { + bcKey := lit.GenBackendContextKey(job.ID) + enginKey := lit.GenEngineInfoKey(job.ID, indexId) + wCnt, err = lit.GlobalLightningEnv.LitMemRoot.RegistEngineInfo(job, bcKey, enginKey, int32(indexId), workerCnt) + if err != nil { + lit.GlobalLightningEnv.LitMemRoot.DeleteBackendContext(bcKey) + } + return wCnt, err +} + +// Import local index sst file into TiKV. +func importIndexDataToStore(ctx context.Context, reorg *reorgInfo, indexId int64, unique bool, tbl table.Table) error { + if reorg.Meta.IsLightningEnabled { + engineInfoKey := lit.GenEngineInfoKey(reorg.ID, indexId) + // just log info. + err := lit.FinishIndexOp(ctx, engineInfoKey, tbl, unique) + if err != nil { + err = errors.Trace(err) + } + } + // After import local data into TiKV, then the progress set to 100. + metrics.GetBackfillProgressByLabel(metrics.LblAddIndex).Set(100) + return nil +} + +// Used to clean backend, +func cleanUpLightningEnv(reorg *reorgInfo, isCanceled bool, indexIds ...int64) { + if reorg.Meta.IsLightningEnabled { + bcKey := lit.GenBackendContextKey(reorg.ID) + // If reorg is cancled, need do clean up engine. + if isCanceled { + lit.GlobalLightningEnv.LitMemRoot.ClearEngines(reorg.ID, indexIds...) + } + lit.GlobalLightningEnv.LitMemRoot.DeleteBackendContext(bcKey) + } +} + +// Disk quota checking and ingest data. +func importPartialDataToTiKV(jobId int64, indexIds int64) error { + return lit.UnsafeImportEngineData(jobId, indexIds) +} + +// Check if this reorg is a restore reorg task +// Check if current lightning reorg task can be executed continuely. +// Otherwise, restart the reorg task. +func canRestoreReorgTask(reorg *reorgInfo, indexId int64) bool { + // The reorg just start, do nothing + if reorg.SnapshotVer == 0 { + return false + } + + // Check if backend and engine are cached. + if !lit.CanRestoreReorgTask(reorg.ID, indexId) { + reorg.SnapshotVer = 0 + return false + } + return true +} + +// Below is lightning worker logic +type addIndexWorkerLit struct { + addIndexWorker + + // Lightning related variable. + writerContex *lit.WorkerContext +} + +func newAddIndexWorkerLit(sessCtx sessionctx.Context, worker *worker, id int, t table.PhysicalTable, indexInfo *model.IndexInfo, decodeColMap map[int64]decoder.Column, reorgInfo *reorgInfo, jobId int64) (*addIndexWorkerLit, error) { + index := tables.NewIndex(t.GetPhysicalID(), t.Meta(), indexInfo) + rowDecoder := decoder.NewRowDecoder(t, t.WritableCols(), decodeColMap) + // ToDo: Bear Currently, all the lightning worker share one openengine. + engineInfoKey := lit.GenEngineInfoKey(jobId, indexInfo.ID) + + lwCtx, err := lit.GlobalLightningEnv.LitMemRoot.RegistWorkerContext(engineInfoKey, id) + if err != nil { + return nil, err + } + // Add build openengine process. + return &addIndexWorkerLit{ + addIndexWorker: addIndexWorker{ + baseIndexWorker: baseIndexWorker{ + backfillWorker: newBackfillWorker(sessCtx, id, t, reorgInfo), + indexes: []table.Index{index}, + rowDecoder: rowDecoder, + defaultVals: make([]types.Datum, len(t.WritableCols())), + rowMap: make(map[int64]types.Datum, len(decodeColMap)), + metricCounter: metrics.BackfillTotalCounter.WithLabelValues("add_idx_rate"), + sqlMode: reorgInfo.ReorgMeta.SQLMode, + }, + index: index, + }, + writerContex: lwCtx, + }, err +} + +// BackfillDataInTxn will backfill table index in a transaction. A lock corresponds to a rowKey if the value of rowKey is changed, +// Note that index columns values may change, and an index is not allowed to be added, so the txn will rollback and retry. +// BackfillDataInTxn will add w.batchCnt indices once, default value of w.batchCnt is 128. +func (w *addIndexWorkerLit) BackfillDataInTxn(handleRange reorgBackfillTask) (taskCtx backfillTaskContext, errInTxn error) { + failpoint.Inject("errorMockPanic", func(val failpoint.Value) { + if val.(bool) { + panic("panic test") + } + }) + + oprStartTime := time.Now() + errInTxn = kv.RunInNewTxn(context.Background(), w.sessCtx.GetStore(), true, func(ctx context.Context, txn kv.Transaction) error { + taskCtx.addedCount = 0 + taskCtx.scanCount = 0 + txn.SetOption(kv.Priority, w.priority) + if tagger := w.reorgInfo.d.getResourceGroupTaggerForTopSQL(w.reorgInfo.Job); tagger != nil { + txn.SetOption(kv.ResourceGroupTagger, tagger) + } + + idxRecords, nextKey, taskDone, err := w.fetchRowColVals(txn, handleRange) + if err != nil { + return errors.Trace(err) + } + taskCtx.nextKey = nextKey + taskCtx.done = taskDone + + err = w.batchCheckUniqueKey(txn, idxRecords) + if err != nil { + return errors.Trace(err) + } + + for _, idxRecord := range idxRecords { + taskCtx.scanCount++ + // The index is already exists, we skip it, no needs to backfill it. + // The following update, delete, insert on these rows, TiDB can handle it correctly. + if idxRecord.skip { + continue + } + + // Create the index. + key, idxVal, _, err := w.index.Create4SST(w.sessCtx, txn, idxRecord.vals, idxRecord.handle, idxRecord.rsData, table.WithIgnoreAssertion) + if err != nil { + return errors.Trace(err) + } + // Currently, only use one kVCache, later may use multi kvCache to parallel compute/io performance. + w.writerContex.WriteRow(key, idxVal, idxRecord.handle) + + taskCtx.addedCount++ + } + return nil + }) + logSlowOperations(time.Since(oprStartTime), "AddIndexLightningBackfillDataInTxn", 3000) + return +} diff --git a/ddl/index_lightning_test.go b/ddl/index_lightning_test.go new file mode 100644 index 0000000000000..6a9c5fdc5428c --- /dev/null +++ b/ddl/index_lightning_test.go @@ -0,0 +1,80 @@ +package ddl_test + +import ( + "fmt" + "strconv" + "testing" + "time" + + "github.com/pingcap/tidb/ddl" + "github.com/pingcap/tidb/domain" + "github.com/pingcap/tidb/sessionctx" + "github.com/pingcap/tidb/testkit" + "github.com/stretchr/testify/require" +) + +func testLitAddIndex(tk *testkit.TestKit, t *testing.T, ctx sessionctx.Context, tblID int64, unique bool, indexName string, colName string, dom *domain.Domain) int64 { + un := "" + if unique { + un = "unique" + } + sql := fmt.Sprintf("alter table t add %s index %s(%s)", un, indexName, colName) + tk.MustExec(sql) + + idi, _ := strconv.Atoi(tk.MustQuery("admin show ddl jobs 1;").Rows()[0][0].(string)) + id := int64(idi) + v := getSchemaVer(t, ctx) + require.NoError(t, dom.Reload()) + tblInfo, exist := dom.InfoSchema().TableByID(tblID) + require.True(t, exist) + checkHistoryJobArgs(t, ctx, id, &historyJobArgs{ver: v, tbl: tblInfo.Meta()}) + return id +} + +func TestEnableLightning(t *testing.T) { + store, _, clean := testkit.CreateMockStoreAndDomain(t) + defer clean() + tk := testkit.NewTestKit(t, store) + tk.MustExec("use test") + // Check default value, Current is off + allow := ddl.IsAllowFastDDL() + require.Equal(t, false, allow) + // Set illedge value + err := tk.ExecToErr("set @@global.tidb_fast_ddl = abc") + require.Error(t, err) + allow = ddl.IsAllowFastDDL() + require.Equal(t, false, allow) + + // set to on + tk.MustExec("set @@global.tidb_fast_ddl = on") + allow = ddl.IsAllowFastDDL() + require.Equal(t, true, allow) +} + +func TestAddIndexLit(t *testing.T) { + store, dom, clean := testkit.CreateMockStoreAndDomain(t) + defer clean() + ddl.SetWaitTimeWhenErrorOccurred(1 * time.Microsecond) + tk := testkit.NewTestKit(t, store) + tk.MustExec("use test") + tk.MustExec("create table t (c1 int primary key, c2 int, c3 int)") + tk.MustExec("insert t values (1, 1, 1), (2, 2, 2), (3, 3, 1);") + tk.MustExec("set @@global.tidb_fast_ddl = on") + + var tableID int64 + rs := tk.MustQuery("select TIDB_TABLE_ID from information_schema.tables where table_name='t' and table_schema='test';") + tableIDi, _ := strconv.Atoi(rs.Rows()[0][0].(string)) + tableID = int64(tableIDi) + + // Non-unique secondary index + jobID := testLitAddIndex(tk, t, testNewContext(store), tableID, false, "idx1", "c2", dom) + testCheckJobDone(t, store, jobID, true) + + // Unique secondary index + jobID = testLitAddIndex(tk, t, testNewContext(store), tableID, true, "idx2", "c2", dom) + testCheckJobDone(t, store, jobID, true) + + // Unique duplicate key + err := tk.ExecToErr("alter table t1 add index unique idx3(c3)") + require.Error(t, err) +} \ No newline at end of file diff --git a/ddl/lightning/backend.go b/ddl/lightning/backend.go new file mode 100644 index 0000000000000..0746ce3ae111f --- /dev/null +++ b/ddl/lightning/backend.go @@ -0,0 +1,154 @@ +// Copyright 2022 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package lightning + +import ( + "context" + "database/sql" + "path/filepath" + "strconv" + + "github.com/pingcap/tidb/br/pkg/lightning/backend" + "github.com/pingcap/tidb/br/pkg/lightning/backend/local" + "github.com/pingcap/tidb/br/pkg/lightning/checkpoints" + "github.com/pingcap/tidb/br/pkg/lightning/config" + "github.com/pingcap/tidb/br/pkg/lightning/glue" + "github.com/pingcap/tidb/br/pkg/lightning/log" + "github.com/pingcap/tidb/parser" + "github.com/pingcap/tidb/parser/model" + "go.uber.org/zap" +) + +type BackendContext struct { + Key string // Currently, backend key used ddl job id string + Backend *backend.Backend + Ctx context.Context + cfg *config.Config + EngineCache map[string]*engineInfo + sysVars map[string]string +} + +func newBackendContext(key string, be *backend.Backend, ctx context.Context, cfg *config.Config, vars map[string]string) *BackendContext { + return &BackendContext{ + Key: key, + Backend: be, + Ctx: ctx, + cfg: cfg, + EngineCache: make(map[string]*engineInfo, 10), + sysVars: vars, + } +} + +func generateLightningConfig(ctx context.Context, unique bool, bcKey string) (*config.Config, error) { + cfg := config.NewConfig() + cfg.TikvImporter.Backend = config.BackendLocal + // Each backend will build an single dir in linghtning dir. + cfg.TikvImporter.SortedKVDir = filepath.Join(GlobalLightningEnv.SortPath, bcKey) + // Should not output err, after go through cfg.adjust function. + _, err := cfg.AdjustCommon() + if err != nil { + log.L().Warn(LWAR_CONFIG_ERROR, zap.Error(err)) + return nil, err + } + adjustImportMemory(cfg) + cfg.Checkpoint.Enable = true + if unique { + cfg.TikvImporter.DuplicateResolution = config.DupeResAlgRecord + } else { + cfg.TikvImporter.DuplicateResolution = config.DupeResAlgNone + } + cfg.TiDB.PdAddr = GlobalLightningEnv.PdAddr + cfg.TiDB.Host = "127.0.0.1" + cfg.TiDB.StatusPort = int(GlobalLightningEnv.Status) + return cfg, err +} + +func createLocalBackend(ctx context.Context, cfg *config.Config, glue glue.Glue) (backend.Backend, error) { + tls, err := cfg.ToTLS() + if err != nil { + log.L().Error(LERR_CREATE_BACKEND_FAILED, zap.Error(err)) + return backend.Backend{}, err + } + + return local.NewLocalBackend(ctx, tls, cfg, glue, int(GlobalLightningEnv.limit), nil) +} + +func CloseBackend(bcKey string) { + log.L().Info(LINFO_CLOSE_BACKEND, zap.String("backend key", bcKey)) + GlobalLightningEnv.LitMemRoot.DeleteBackendContext(bcKey) + return +} + +func GenBackendContextKey(jobId int64) string { + return strconv.FormatInt(jobId, 10) +} + +// Adjust lightning memory parameters according memory root's max limitation +func adjustImportMemory(cfg *config.Config) { + var scale int64 + defaultMemSize := int64(cfg.TikvImporter.LocalWriterMemCacheSize) * int64(cfg.TikvImporter.RangeConcurrency) + defaultMemSize += 4 * int64(cfg.TikvImporter.EngineMemCacheSize) + log.L().Info(LINFO_INIT_MEM_SETTING, + zap.String("LocalWriterMemCacheSize:", strconv.FormatInt(int64(cfg.TikvImporter.LocalWriterMemCacheSize), 10)), + zap.String("EngineMemCacheSize:", strconv.FormatInt(int64(cfg.TikvImporter.LocalWriterMemCacheSize), 10)), + zap.String("rangecounrrency:", strconv.Itoa(cfg.TikvImporter.RangeConcurrency))) + + if defaultMemSize > GlobalLightningEnv.LitMemRoot.maxLimit { + scale = defaultMemSize / GlobalLightningEnv.LitMemRoot.maxLimit + } + + // scale equal to 1 means there is no need to adjust memory settings for lightning. + // 0 means defaultMemSize is less than memory maxLimit for Lightning, no need to adjust. + if scale == 1 || scale == 0 { + return + } + + cfg.TikvImporter.LocalWriterMemCacheSize /= config.ByteSize(scale) + cfg.TikvImporter.EngineMemCacheSize /= config.ByteSize(scale) + // ToDo adjust rangecourrency nubmer to control total concurrency in future. + log.L().Info(LINFO_CHG_MEM_SETTING, + zap.String("LocalWriterMemCacheSize:", strconv.FormatInt(int64(cfg.TikvImporter.LocalWriterMemCacheSize), 10)), + zap.String("EngineMemCacheSize:", strconv.FormatInt(int64(cfg.TikvImporter.LocalWriterMemCacheSize), 10)), + zap.String("rangecounrrency:", strconv.Itoa(cfg.TikvImporter.RangeConcurrency))) + return +} + +type glue_lit struct{} + +func (_ glue_lit) OwnsSQLExecutor() bool { + return false +} +func (_ glue_lit) GetSQLExecutor() glue.SQLExecutor { + return nil +} +func (_ glue_lit) GetDB() (*sql.DB, error) { + return nil, nil +} +func (_ glue_lit) GetParser() *parser.Parser { + return nil +} +func (_ glue_lit) GetTables(context.Context, string) ([]*model.TableInfo, error) { + return nil, nil +} +func (_ glue_lit) GetSession(context.Context) (checkpoints.Session, error) { + return nil, nil +} +func (_ glue_lit) OpenCheckpointsDB(context.Context, *config.Config) (checkpoints.DB, error) { + return nil, nil +} + +// Record is used to report some information (key, value) to host TiDB, including progress, stage currently +func (_ glue_lit) Record(string, uint64) { + +} diff --git a/ddl/lightning/backend_test.go b/ddl/lightning/backend_test.go new file mode 100644 index 0000000000000..455d61d029adf --- /dev/null +++ b/ddl/lightning/backend_test.go @@ -0,0 +1,45 @@ +// Copyright 2021 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lightning + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAdjustMemory(t *testing.T) { + type TestCase struct { + name string + quota int64 + lsize int64 + ensize int64 + } + tests := []TestCase{ + {"Mem1", 4 * _kb, 32 * _kb, 128 * _kb}, + {"Mem2", 4 * _mb, 128 * _kb, 512 * _kb}, + {"Mem3", 1 * _gb, 32 * _mb, 128 * _mb}, + {"Mem4", 4 * _gb, 128 * _mb, 512 * _mb}, + } + InitGolbalLightningBackendEnv() + for _, test := range tests { + GlobalLightningEnv.LitMemRoot.Reset(test.quota) + cfg, err := generateLightningConfig(context.TODO(), false, "bckey") + require.NoError(t, err) + require.Equal(t, test.lsize, int64(cfg.TikvImporter.LocalWriterMemCacheSize)) + require.Equal(t, test.ensize, int64(cfg.TikvImporter.EngineMemCacheSize)) + } +} \ No newline at end of file diff --git a/ddl/lightning/engine.go b/ddl/lightning/engine.go new file mode 100644 index 0000000000000..582037af9d8fa --- /dev/null +++ b/ddl/lightning/engine.go @@ -0,0 +1,272 @@ +// Copyright 2022 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package lightning + +import ( + "context" + "strconv" + + "github.com/google/uuid" + "github.com/pingcap/tidb/br/pkg/lightning/backend" + "github.com/pingcap/tidb/br/pkg/lightning/backend/kv" + "github.com/pingcap/tidb/br/pkg/lightning/common" + "github.com/pingcap/tidb/br/pkg/lightning/config" + "github.com/pingcap/tidb/br/pkg/lightning/log" + tidbkv "github.com/pingcap/tidb/kv" + "github.com/pingcap/tidb/parser/model" + "github.com/pingcap/tidb/parser/mysql" + "github.com/pingcap/tidb/table" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +var ( + compactMem int64 = 1 * _gb + compactConcurr int = 4 +) + +// One engine for one index reorg task, each task will create new writer under the +// OpenEngine. Note engineInfo is not thread safe. +type engineInfo struct { + id int32 + key string + + backCtx *BackendContext + openedEngine *backend.OpenedEngine + uuid uuid.UUID + cfg *backend.EngineConfig + tableName string + WriterCount int + writerCache map[string]*backend.LocalEngineWriter +} + +func NewEngineInfo( + id int32, key string, cfg *backend.EngineConfig, bCtx *BackendContext, + en *backend.OpenedEngine, tblName string, uuid uuid.UUID, wCnt int) *engineInfo { + ei := engineInfo{ + id: id, + key: key, + cfg: cfg, + backCtx: bCtx, + openedEngine: en, + uuid: uuid, + tableName: tblName, + WriterCount: wCnt, + writerCache: make(map[string]*backend.LocalEngineWriter, wCnt), + } + return &ei +} + +func GenEngineInfoKey(jobid int64, indexId int64) string { + return strconv.FormatInt(jobid, 10) + strconv.FormatInt(indexId, 10) +} + +func CreateEngine( + ctx context.Context, + job *model.Job, + backendKey string, + engineKey string, + indexId int32, + wCnt int) (err error) { + var cfg backend.EngineConfig + cfg.Local = &backend.LocalEngineConfig{ + Compact: true, + CompactThreshold: compactMem, + CompactConcurrency: compactConcurr, + } + // Open lightning engine + bc := GlobalLightningEnv.LitMemRoot.backendCache[backendKey] + be := bc.Backend + + en, err := be.OpenEngine(ctx, &cfg, job.TableName, indexId) + if err != nil { + errMsg := LERR_CREATE_ENGINE_FAILED + err.Error() + log.L().Error(errMsg) + return errors.New(errMsg) + } + uuid := en.GetEngineUuid() + ei := NewEngineInfo(indexId, engineKey, &cfg, bc, en, job.TableName, uuid, wCnt) + GlobalLightningEnv.LitMemRoot.EngineMgr.StoreEngineInfo(engineKey, ei) + bc.EngineCache[engineKey] = ei + log.L().Info(LINFO_OPEN_ENGINE, + zap.String("backend key", ei.backCtx.Key), + zap.String("Engine key", ei.key)) + return nil +} + +func FinishIndexOp(ctx context.Context, engineInfoKey string, tbl table.Table, unique bool) (err error) { + var errMsg string + var keyMsg string + ei, exist := GlobalLightningEnv.LitMemRoot.EngineMgr.LoadEngineInfo(engineInfoKey) + if !exist { + return errors.New(LERR_GET_ENGINE_FAILED) + } + defer func() { + GlobalLightningEnv.LitMemRoot.EngineMgr.ReleaseEngine(engineInfoKey) + }() + + keyMsg = "backend key:" + ei.backCtx.Key + "Engine key:" + ei.key + // Close engine + log.L().Info(LINFO_CLOSE_ENGINE, zap.String("backend key", ei.backCtx.Key), zap.String("Engine key", ei.key)) + indexEngine := ei.openedEngine + closeEngine, err1 := indexEngine.Close(ei.backCtx.Ctx, ei.cfg) + if err1 != nil { + errMsg = LERR_CLOSE_ENGINE_ERR + keyMsg + log.L().Error(errMsg) + return errors.New(errMsg) + } + + // Local dupl check + if unique { + hasDupe, err := ei.backCtx.Backend.CollectLocalDuplicateRows(ctx, tbl, ei.tableName, &kv.SessionOptions{ + SQLMode: mysql.ModeStrictAllTables, + SysVars: ei.backCtx.sysVars, + }) + if hasDupe { + errMsg = LERR_LOCAL_DUP_EXIST_ERR + keyMsg + log.L().Error(errMsg) + return errors.New(errMsg) + } else if err != nil { + errMsg = LERR_LOCAL_DUP_CHECK_ERR + keyMsg + log.L().Error(errMsg) + return errors.New(errMsg) + } + } + + // Ingest data to TiKV + log.L().Info(LINFO_START_TO_IMPORT, zap.String("backend key", ei.backCtx.Key), + zap.String("Engine key", ei.key), + zap.String("Split Region Size", strconv.FormatInt(int64(config.SplitRegionSize), 10))) + err = closeEngine.Import(ctx, int64(config.SplitRegionSize), int64(config.SplitRegionKeys)) + if err != nil { + errMsg = LERR_INGEST_DATA_ERR + keyMsg + log.L().Error(errMsg) + return errors.New(errMsg) + } + + // Clean up the engine + err = closeEngine.Cleanup(ctx) + if err != nil { + errMsg = LERR_CLOSE_ENGINE_ERR + keyMsg + log.L().Error(errMsg) + return errors.New(errMsg) + } + + // Check Remote duplicate value for index + if unique { + hasDupe, err := ei.backCtx.Backend.CollectRemoteDuplicateRows(ctx, tbl, ei.tableName, &kv.SessionOptions{ + + SQLMode: mysql.ModeStrictAllTables, + SysVars: ei.backCtx.sysVars, + }) + if hasDupe { + errMsg = LERR_REMOTE_DUP_EXIST_ERR + keyMsg + log.L().Error(errMsg) + return errors.New(errMsg) + } else if err != nil { + errMsg = LERR_REMOTE_DUP_CHECK_ERR + keyMsg + log.L().Error(errMsg) + return errors.New(errMsg) + } + } + return nil +} + +func FlushEngine(engineKey string, ei *engineInfo) error { + err := ei.openedEngine.Flush(ei.backCtx.Ctx) + if err != nil { + log.L().Error(LERR_FLUSH_ENGINE_ERR, zap.String("Engine key:", engineKey)) + return err + } + return nil +} + +// Check if the disk quota arrived, if yes then ingest temp file into TiKV +func UnsafeImportEngineData(jobId int64, indexId int64) error { + engineKey := GenEngineInfoKey(jobId, indexId) + ei, exist := GlobalLightningEnv.LitMemRoot.EngineMgr.LoadEngineInfo(engineKey) + if !exist { + log.L().Error(LERR_GET_ENGINE_FAILED, zap.String("Engine key:", engineKey)) + return errors.New(LERR_GET_ENGINE_FAILED) + } + // Flush wirter cached data into local disk for engine first. + err := FlushEngine(engineKey, ei) + if err != nil { + return err + } + totalDiskSize := GlobalLightningEnv.LitMemRoot.TotalDiskUsage() + if GlobalLightningEnv.NeedImportEngineData(totalDiskSize) { + log.L().Info(LINFO_UNSAFE_IMPORT, zap.String("Engine key:", engineKey), zap.String("Current total used disk:", strconv.FormatInt(totalDiskSize, 10))) + err = ei.backCtx.Backend.UnsafeImportAndReset(ei.backCtx.Ctx, ei.uuid, int64(config.SplitRegionSize) * int64(config.MaxSplitRegionSizeRatio), int64(config.SplitRegionKeys)) + if err != nil { + log.L().Error(LERR_FLUSH_ENGINE_ERR, zap.String("Engine key:", engineKey), + zap.String("import partial file failed, current disk storage consume", strconv.FormatInt(totalDiskSize, 10))) + return err + } + } + return nil +} + +type WorkerContext struct { + eInfo *engineInfo + lWrite *backend.LocalEngineWriter +} + +// Init Worker Context will get worker local writer from engine info writer cache first, if exist. +// If local wirter not exist, then create new one and store it into engine info writer cache. +// note operate ei.writeCache map is not thread safe please make sure there is sync mechaism to +// make sure the safe. +func (wCtx *WorkerContext) InitWorkerContext(engineKey string, workerid int) (err error) { + wCtxKey := engineKey + strconv.Itoa(workerid) + ei, exist := GlobalLightningEnv.LitMemRoot.EngineMgr.enginePool[engineKey] + if !exist { + return errors.New(LERR_GET_ENGINE_FAILED) + } + wCtx.eInfo = ei + + // Fisrt get local writer from engine cache. + wCtx.lWrite, exist = ei.writerCache[wCtxKey] + // If not exist then build one + if !exist { + wCtx.lWrite, err = ei.openedEngine.LocalWriter(ei.backCtx.Ctx, &backend.LocalWriterConfig{}) + if err != nil { + return err + } + // Cache the lwriter, here we do not lock, because this is called in mem root alloc + // process it will lock while alloc object. + ei.writerCache[wCtxKey] = wCtx.lWrite + } + return nil +} + +func (wCtx *WorkerContext) WriteRow(key, idxVal []byte, h tidbkv.Handle) { + var kvs []common.KvPair = make([]common.KvPair, 1, 1) + kvs[0].Key = key + kvs[0].Val = idxVal + kvs[0].RowID = h.IntValue() + wCtx.lWrite.WriteRow(wCtx.eInfo.backCtx.Ctx, nil, kvs) +} + +// Only when backend and Engine still be cached, then the task could be restore, +// otherwise return false to let reorg task restart. +func CanRestoreReorgTask(jobId int64, indexId int64) bool { + engineInfoKey := GenEngineInfoKey(jobId, indexId) + bcKey := GenBackendContextKey(jobId) + _, enExist := GlobalLightningEnv.LitMemRoot.EngineMgr.LoadEngineInfo(engineInfoKey) + _, bcExist := GlobalLightningEnv.LitMemRoot.getBackendContext(bcKey) + if enExist && bcExist { + return true + } + return false +} diff --git a/ddl/lightning/engine_mgr.go b/ddl/lightning/engine_mgr.go new file mode 100644 index 0000000000000..df3813a4e4e38 --- /dev/null +++ b/ddl/lightning/engine_mgr.go @@ -0,0 +1,55 @@ +// Copyright 2022 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package lightning + +import ( + "github.com/pingcap/tidb/br/pkg/lightning/log" + "go.uber.org/zap" +) + +type EngineManager struct { + enginePool map[string]*engineInfo +} + +func (em *EngineManager) init() { + em.enginePool = make(map[string]*engineInfo, 10) +} + +func (em *EngineManager) StoreEngineInfo(key string, ei *engineInfo) { + em.enginePool[key] = ei +} + +func (em *EngineManager) LoadEngineInfo(key string) (*engineInfo, bool) { + ei, exist := em.enginePool[key] + if !exist { + log.L().Error(LERR_GET_ENGINE_FAILED, zap.String("Engine_Manager:", "Not found")) + return nil, exist + } + return ei, exist +} + +func (em *EngineManager) ReleaseEngine(key string) { + log.L().Info(LINFO_ENGINE_DELETE, zap.String("Engine info key:", key)) + delete(em.enginePool, key) + return +} + +// Caculate all memory used by all active engine. +func (em *EngineManager) totalSize() int64 { + var memUsed int64 + for _, en := range em.enginePool { + memUsed += en.openedEngine.TotalMemoryConsume() + } + return memUsed +} diff --git a/ddl/lightning/env.go b/ddl/lightning/env.go new file mode 100644 index 0000000000000..a2d0f80f68345 --- /dev/null +++ b/ddl/lightning/env.go @@ -0,0 +1,165 @@ +// Copyright 2022 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package lightning + +import ( + "os" + "path/filepath" + "strconv" + "syscall" + + lcom "github.com/pingcap/tidb/br/pkg/lightning/common" + "github.com/pingcap/tidb/br/pkg/lightning/log" + "github.com/pingcap/tidb/config" + "github.com/pingcap/tidb/sessionctx/variable" + "github.com/pingcap/tidb/util/logutil" + "go.uber.org/zap" +) + +const ( + prefix_str = "------->" + _kb = 1024 + _mb = 1024 * _kb + _gb = 1024 * _mb + _tb = 1024 * _gb + _pb = 1024 * _tb + flush_size = 1 * _mb + diskQuota = 512 * _mb + importThreadhold float32 = 0.85 +) + +type ClusterInfo struct { + PdAddr string + // TidbHost string - 127.0.0.1 + Port uint + Status uint +} +type LightningEnv struct { + limit int64 + ClusterInfo + SortPath string + LitMemRoot LightningMemoryRoot + diskQuota int64 + IsInited bool +} + +var ( + GlobalLightningEnv LightningEnv + maxMemLimit uint64 = 128 * _mb +) + +func init() { + GlobalLightningEnv.limit = 1024 // Init a default value 1024 for limit. + GlobalLightningEnv.diskQuota = 10 * _gb // default disk quota set to 10 GB + var rLimit syscall.Rlimit + err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) + if err != nil { + logutil.BgLogger().Warn(LERR_GET_SYS_LIMIT_ERR, zap.String("OS error:", err.Error()), zap.String("Default: ", "1024.")) + } else { + GlobalLightningEnv.limit = int64(rLimit.Cur) + } + GlobalLightningEnv.IsInited = false + GlobalLightningEnv.diskQuota = diskQuota + +} + +func InitGolbalLightningBackendEnv() { + var ( + bufferSize uint64 + err error + ) + log.SetAppLogger(logutil.BgLogger()) + + cfg := config.GetGlobalConfig() + GlobalLightningEnv.Port = cfg.Port + GlobalLightningEnv.Status = cfg.Status.StatusPort + GlobalLightningEnv.PdAddr = cfg.Path + + GlobalLightningEnv.SortPath, err = genLightningDataDir(cfg.LightningSortPath) + err = GlobalLightningEnv.parseDiskQuota(int(variable.DiskQuota.Load())) + // Set Memory usage limitation to 1 GB + sbz := variable.GetSysVar("sort_buffer_size") + bufferSize, err = strconv.ParseUint(sbz.Value, 10, 64) + // If get bufferSize err, then maxMemLimtation is 128 MB + // Otherwise, the ddl maxMemLimitation is 1 GB + if err == nil { + maxMemLimit = bufferSize * 4 * _kb + log.L().Info(LINFO_SET_MEM_LIMIT, + zap.String("Memory limitation set to:", strconv.FormatUint(maxMemLimit, 10))) + } else { + log.L().Info(LWAR_GEN_MEM_LIMIT, + zap.Error(err), + zap.String("will use default memory limitation:", strconv.FormatUint(maxMemLimit, 10))) + } + GlobalLightningEnv.LitMemRoot.init(int64(maxMemLimit)) + log.L().Info(LINFO_ENV_INIT_SUCC, + zap.String("Memory limitation set to:", strconv.FormatUint(maxMemLimit, 10)), + zap.String("Sort Path disk quota:", strconv.FormatUint(uint64(GlobalLightningEnv.diskQuota), 10)), + zap.String("Max open file number:", strconv.FormatInt(GlobalLightningEnv.limit, 10))) + GlobalLightningEnv.IsInited = true + return +} + +func (l *LightningEnv) parseDiskQuota(val int) error { + sz, err := lcom.GetStorageSize(l.SortPath) + if err != nil { + log.L().Error(LERR_GET_STORAGE_QUOTA, + zap.String("Os error:", err.Error()), + zap.String("default disk quota", strconv.FormatInt(l.diskQuota, 10))) + return err + } + + setDiskValue := int64(val * _gb) + // The Dist quota should be 100 GB to 1 PB + if setDiskValue > int64(sz.Available) { + l.diskQuota = int64(sz.Available) + } else { + l.diskQuota = setDiskValue + } + + return err +} + +// Generate lightning local store dir in TiDB datadir. +func genLightningDataDir(sortPath string) (string, error) { + sortPath = filepath.Join(sortPath, "/lightning") + shouldCreate := true + if info, err := os.Stat(sortPath); err != nil { + if !os.IsNotExist(err) { + log.L().Error(LERR_CREATE_DIR_FAILED, zap.String("Sort path:", sortPath), + zap.String("Error:", err.Error())) + return "/tmp/lightning", err + } + } else if info.IsDir() { + shouldCreate = false + } + + if shouldCreate { + err := os.Mkdir(sortPath, 0o700) + if err != nil { + log.L().Error(LERR_CREATE_DIR_FAILED, zap.String("Sort path:", sortPath), + zap.String("Error:", err.Error())) + return "/tmp/lightning", err + } + } + log.L().Info(LINFO_SORTED_DIR, zap.String("data path:", sortPath)) + return sortPath, nil +} + +func (g *LightningEnv) NeedImportEngineData(UsedDisk int64) bool { + if UsedDisk > int64(importThreadhold * float32(g.diskQuota)) { + return true + } + return false +} diff --git a/ddl/lightning/env_test.go b/ddl/lightning/env_test.go new file mode 100644 index 0000000000000..b6fc0a57b03da --- /dev/null +++ b/ddl/lightning/env_test.go @@ -0,0 +1,74 @@ +// Copyright 2021 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lightning + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGenSortPath(t *testing.T) { + type TestCase struct { + name string + inputPath string + outputPath string + } + tests := []TestCase{ + {"path1", "/tmp/", "/tmp/lightning"}, + {"path2", "/data/tidb/data", "/data/tidb/data/lightning"}, + {"path3", "127.0.0.1", "/tmp/lightning"}, + {"path4", "~/data1/", "/tmp/lightning"}, + {"path5", "../data1/", "/tmp/lightning"}, + {"path6", "/data/tidb/data/", "/data/tidb/data/lightning"}, + {"path7", "", "/data/tidb/data/lightning"}, + {"path8", "/lightning", "/lightning"}, + } + for _, test := range tests { + result, err := genLightningDataDir(test.inputPath) + if err == nil { + require.Equal(t, test.outputPath, result) + } else { + require.Error(t, err) + } + } +} + +func TestSetDiskQuota(t *testing.T) { + type TestCase struct { + name string + sortPath string + inputQuota int + outputQuota int + } + tests := []TestCase{ + {"quota1", "/tmp/", 10, 10 * _gb}, + {"quota2", "/data/tidb/data", 100, 100 * _gb}, + {"quota3", "127.0.0.1", 1000, 1000 * _gb}, + {"quota4", "~/data1/", 512, 512 * _gb}, + {"quota5", "../data1/", 10000, 10000 * _gb}, + {"quota6", "/data/tidb/data/", 100000, 100000 * _gb}, + {"quota7", "", 10000, 10000 * _gb}, + {"quota8", "/lightning", 10000, 10000 * _gb}, + } + for _, test := range tests { + result, _ := genLightningDataDir(test.sortPath) + GlobalLightningEnv.SortPath = result + GlobalLightningEnv.parseDiskQuota(test.inputQuota) + if GlobalLightningEnv.diskQuota > int64(test.inputQuota * _gb) { + require.Equal(t, test.outputQuota, GlobalLightningEnv.diskQuota) + } + } +} diff --git a/ddl/lightning/lightning_error.go b/ddl/lightning/lightning_error.go new file mode 100644 index 0000000000000..43a62f7b9334c --- /dev/null +++ b/ddl/lightning/lightning_error.go @@ -0,0 +1,61 @@ +// Copyright 2022 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package lightning + +const ( + // Error messages + LERR_ALLOC_MEM_FAILED string = "Lightning: Allocate memory failed" + LERR_OUT_OF_MAX_MEM string = "Lightning: Memory used up for Lightning add index" + LERR_UNKNOW_MEM_TYPE string = "Lightning: Unknown struct mem required for Lightning add index" + LERR_CREATE_DIR_FAILED string = "Lightning: Create lightning sort path error" + LERR_CREATE_BACKEND_FAILED string = "Lightning: Build lightning backend failed, will use kernel index reorg method to backfill the index" + LERR_GET_BACKEND_FAILED string = "Lightning: Can not get cached backend" + LERR_CREATE_ENGINE_FAILED string = "Lightning: Build lightning engine failed, will use kernel index reorg method to backfill the index" + LERR_CREATE_CONTEX_FAILED string = "Lightning: Build lightning worker context failed, will use kernel index reorg method to backfill the index" + LERR_GET_ENGINE_FAILED string = "Lightning: Can not get catched engininfo" + LERR_GET_STORAGE_QUOTA string = "Lightning: Get storage quota error" + LERR_GET_SYS_LIMIT_ERR string = "Lightning: Get system open file limit error" + LERR_CLOSE_ENGINE_ERR string = "Lightning: Close engine error" + LERR_FLUSH_ENGINE_ERR string = "Lightning: Flush engine data err" + LERR_INGEST_DATA_ERR string = "Lightning: Ingest data into TiKV error" + LERR_LOCAL_DUP_CHECK_ERR string = "Lightning: Locale duplicate check error" + LERR_LOCAL_DUP_EXIST_ERR string = "Lightning: Locale duplicate index key exist" + LERR_REMOTE_DUP_CHECK_ERR string = "Lightning: Remote duplicate check error" + LERR_REMOTE_DUP_EXIST_ERR string = "Lightning: Remote duplicate index key exist" + // Warning messages + LWAR_ENV_INIT_FAILD string = "Lightning: Initialize environment failed" + LWAR_BACKEND_NOT_EXIST string = "Lightning: Backend not exist" + LWAR_CONFIG_ERROR string = "Lightning: Build config for backend failed" + LWAR_GEN_MEM_LIMIT string = "Lightning: Generate memory max limitation" + LWAR_EXTENT_WORKER string = "Lightning: Extend worker failed will use worker count number worker to keep doing backfill task " + // Infomation messages + LINFO_ENV_INIT_SUCC string = "Lightning: Init global lightning backend environment finished" + LINFO_SORTED_DIR string = "Lightning: The lightning sorted dir" + LINFO_CREATE_BACKEND string = "Lightning: Create one backend for an DDL job" + LINFO_CLOSE_BACKEND string = "Lightning: Close one backend for DDL job" + LINFO_OPEN_ENGINE string = "Lightning: Open an engine for index reorg task" + LINFO_CLEANUP_ENGINE string = "Lightning: CleanUp one engine for index reorg task" + LINFO_CREATE_WRITER string = "Lightning: Create one local Writer for Index reorg task" + LINFO_CLOSE_ENGINE string = "Lightning: Flush all writer and get closed engine" + LINFO_LOCAL_DUPL_CHECK string = "Lightning: Start Local duplicate checking" + LINFO_REMOTE_DUPL_CHECK string = "Lightning: Start remote duplicate checking" + LINFO_START_TO_IMPORT string = "Lightning: Start to import data" + LINFO_SET_MEM_LIMIT string = "Lightning: Set max memory limitation" + LINFO_CHG_MEM_SETTING string = "Lightning: Change memory setting for lightning" + + LINFO_INIT_MEM_SETTING string = "Lightning: Initial memory setting for lightning," + LINFO_ENGINE_DELETE string = "Lightning: Delete one engine from engine manager cache," + LINFO_UNSAFE_IMPORT string = "Lightning: Do a partial import data into TiKV," + +) diff --git a/ddl/lightning/res_mgr.go b/ddl/lightning/res_mgr.go new file mode 100644 index 0000000000000..e3d2f5d612fa4 --- /dev/null +++ b/ddl/lightning/res_mgr.go @@ -0,0 +1,454 @@ +// Copyright 2022 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package lightning + +import ( + "context" + "errors" + "strconv" + "sync" + "unsafe" + + "github.com/docker/go-units" + "github.com/pingcap/tidb/br/pkg/lightning/backend" + "github.com/pingcap/tidb/br/pkg/lightning/common" + "github.com/pingcap/tidb/br/pkg/lightning/config" + "github.com/pingcap/tidb/br/pkg/lightning/log" + "github.com/pingcap/tidb/parser/model" + "github.com/pingcap/tidb/parser/mysql" + "github.com/pingcap/tidb/sessionctx/variable" + "go.uber.org/zap" +) + +type defaultType string + +const ( + // Default struct need to be count. + ALLOC_BACKEND_CONTEXT defaultType = "AllocBackendContext" + ALLOC_ENGINE_INFO defaultType = "AllocEngineInfo" + ALLOC_WORKER_CONTEXT defaultType = "AllocWorkerCONTEXT" + + // Used to mark the object size did not stored in map + firstAlloc int64 = -1 + allocFailed int64 = 0 +) + +// MemoryRoot is used to trace the memory usage of all light DDL environment. +type LightningMemoryRoot struct { + maxLimit int64 + currUsage int64 + engineUsage int64 + writeBuffer int64 + backendCache map[string]*BackendContext + EngineMgr EngineManager + // This map is use to store all object memory allocated size. + structSize map[string]int64 + mLock sync.Mutex +} + +func (m *LightningMemoryRoot) init(maxMemUsage int64) { + // Set lightning memory quota to 2 times flush_size + if maxMemUsage < flush_size { + m.maxLimit = flush_size + } else { + m.maxLimit = maxMemUsage + } + + m.currUsage = 0 + m.engineUsage = 0 + m.writeBuffer = 0 + + m.backendCache = make(map[string]*BackendContext, 10) + m.EngineMgr.init() + m.structSize = make(map[string]int64, 10) + m.initDefaultStruceMemSize() +} + +// Caculate memory struct size and save it into map. +func (m *LightningMemoryRoot) initDefaultStruceMemSize() { + var ( + bc BackendContext + ei engineInfo + wCtx WorkerContext + ) + + m.structSize[string(ALLOC_BACKEND_CONTEXT)] = int64(unsafe.Sizeof(bc)) + m.structSize[string(ALLOC_ENGINE_INFO)] = int64(unsafe.Sizeof(ei)) + m.structSize[string(ALLOC_WORKER_CONTEXT)] = int64(unsafe.Sizeof(wCtx)) +} + +// Reset memory quota. but not less than flush_size(1 MB) +func (m *LightningMemoryRoot) Reset(maxMemUsage int64) { + m.mLock.Lock() + defer func() { + m.mLock.Unlock() + }() + // Set lightning memory quota to flush_size + if maxMemUsage < flush_size { + m.maxLimit = flush_size + } else { + m.maxLimit = maxMemUsage + } +} + +// Check Memory allocated for lightning. +func (m *LightningMemoryRoot) checkMemoryUsage(t defaultType) error { + var ( + requiredMem int64 = 0 + ) + + switch t { + case ALLOC_BACKEND_CONTEXT: + requiredMem, _ = m.structSize[string(ALLOC_BACKEND_CONTEXT)] + case ALLOC_ENGINE_INFO: + requiredMem, _ = m.structSize[string(ALLOC_ENGINE_INFO)] + case ALLOC_WORKER_CONTEXT: + requiredMem, _ = m.structSize[string(ALLOC_WORKER_CONTEXT)] + default: + return errors.New(LERR_UNKNOW_MEM_TYPE) + } + + if m.currUsage + requiredMem > m.maxLimit { + return errors.New(LERR_OUT_OF_MAX_MEM) + } + return nil +} + +// check and create one backend +func (m *LightningMemoryRoot) RegistBackendContext(ctx context.Context, unique bool, key string, sqlMode mysql.SQLMode) error { + var ( + err error = nil + bd backend.Backend + exist bool = false + cfg *config.Config + ) + m.mLock.Lock() + defer func() { + m.mLock.Unlock() + }() + // Firstly, get backend Context from backend cache. + _, exist = m.backendCache[key] + // If bc not exist, build new backend for reorg task, otherwise reuse exist backend + // to continue the task. + if exist == false { + // First to check the memory usage + m.totalMemoryConsume() + err = m.checkMemoryUsage(ALLOC_BACKEND_CONTEXT) + if err != nil { + log.L().Warn(LERR_ALLOC_MEM_FAILED, zap.String("backend key", key), + zap.String("Current Memory Usage:", strconv.FormatInt(m.currUsage, 10)), + zap.String("Memory limitation:", strconv.FormatInt(m.maxLimit, 10))) + return err + } + cfg, err = generateLightningConfig(ctx, unique, key) + if err != nil { + log.L().Warn(LERR_ALLOC_MEM_FAILED, zap.String("backend key", key), + zap.String("Generate config for lightning error:", err.Error())) + return err + } + glue := glue_lit{} + bd, err = createLocalBackend(ctx, cfg, glue) + if err != nil { + log.L().Error(LERR_CREATE_BACKEND_FAILED, zap.String("backend key", key)) + return err + } + + // Init important variables + sysVars := obtainImportantVariables() + + m.backendCache[key] = newBackendContext(key, &bd, ctx, cfg, sysVars) + + // Count memory usage. + m.currUsage += m.structSize[string(ALLOC_BACKEND_CONTEXT)] + log.L().Info(LINFO_CREATE_BACKEND, zap.String("backend key", key), + zap.String("Current Memory Usage:", strconv.FormatInt(m.currUsage, 10)), + zap.String("Memory limitation:", strconv.FormatInt(m.maxLimit, 10)), + zap.String("Unique Index:", strconv.FormatBool(unique))) + } + return err +} + +// Uniform entry to close backend and release related memory allocated +func (m *LightningMemoryRoot) DeleteBackendContext(bcKey string) { + // Only acquire/release lock here. + m.mLock.Lock() + defer func() { + delete(m.backendCache, bcKey) + m.mLock.Unlock() + }() + // Close key specific backend + bc, exist := m.backendCache[bcKey] + if !exist { + log.L().Error(LERR_GET_BACKEND_FAILED, zap.String("backend key", bcKey)) + return + } + + // Close and delete backend by key + m.deleteBackendEngines(bcKey) + bc.Backend.Close() + + m.currUsage -= m.structSize[bc.Key] + delete(m.structSize, bcKey) + m.currUsage -= m.structSize[string(ALLOC_BACKEND_CONTEXT)] + log.L().Info(LINFO_CLOSE_BACKEND, zap.String("backend key", bcKey), + zap.String("Current Memory Usage:", strconv.FormatInt(m.currUsage, 10)), + zap.String("Memory limitation:", strconv.FormatInt(m.maxLimit, 10))) + return +} + +// In exception case, clear intermediate files that lightning engine generated for index. +func (m *LightningMemoryRoot) ClearEngines(jobId int64, indexIds ...int64) { + for _, indexId := range indexIds { + eiKey := GenEngineInfoKey(jobId, indexId) + ei, exist := m.EngineMgr.enginePool[eiKey] + if exist { + indexEngine := ei.openedEngine + closedEngine, err := indexEngine.Close(ei.backCtx.Ctx, ei.cfg) + if err != nil { + log.L().Error(LERR_CLOSE_ENGINE_ERR, zap.String("Engine key", eiKey)) + return + } + // Here the local intermediate file will be removed. + closedEngine.Cleanup(ei.backCtx.Ctx) + } + } + return +} + +// Check and allocate one EngineInfo, delete engineInfo are put into delete backend +// The worker count means this time the engine need pre-check memory for workers +func (m *LightningMemoryRoot) RegistEngineInfo(job *model.Job, bcKey string, engineKey string, indexId int32, workerCount int) (int, error) { + var err error = nil + m.mLock.Lock() + defer func() { + m.mLock.Unlock() + }() + bc, exist := m.backendCache[bcKey] + if !exist { + log.L().Warn(LWAR_BACKEND_NOT_EXIST, zap.String("Backend key", bcKey)) + return 0, err + } + + // Caculate lightning concurrecy degree and set memory usage. + // and pre-allocate memory usage for worker + newWorkerCount := m.workerDegree(workerCount, engineKey) + // When return workerCount is 0, means there is no memory available for lightning worker. + if workerCount == int(allocFailed) { + log.L().Warn(LERR_ALLOC_MEM_FAILED, zap.String("Backend key", bcKey), + zap.String("Engine key", engineKey), + zap.String("Expected worker count:", strconv.Itoa(workerCount)), + zap.String("Currnt alloc wroker count:", strconv.Itoa(newWorkerCount))) + return 0, errors.New(LERR_CREATE_ENGINE_FAILED) + } + _, exist1 := bc.EngineCache[engineKey] + if !exist1 { + // Firstly, update and check the memory usage + m.totalMemoryConsume() + err = m.checkMemoryUsage(ALLOC_ENGINE_INFO) + if err != nil { + log.L().Warn(LERR_ALLOC_MEM_FAILED, zap.String("Backend key", bcKey), + zap.String("Engine key", engineKey), + zap.String("Current Memory Usage:", strconv.FormatInt(m.currUsage, 10)), + zap.String("Memory limitation:", strconv.FormatInt(m.maxLimit, 10))) + return 0, err + } + // Create one slice for one backend on one stmt, current we share one engine + err = CreateEngine(bc.Ctx, job, bcKey, engineKey, indexId, workerCount) + if err != nil { + return 0, errors.New(LERR_CREATE_ENGINE_FAILED) + } + + // Count memory usage. + m.currUsage += m.structSize[string(ALLOC_ENGINE_INFO)] + m.engineUsage += m.structSize[string(ALLOC_ENGINE_INFO)] + } + log.L().Info(LINFO_OPEN_ENGINE, zap.String("backend key", bcKey), + zap.String("Engine key", engineKey), + zap.String("Current Memory Usage:", strconv.FormatInt(m.currUsage, 10)), + zap.String("Memory limitation:", strconv.FormatInt(m.maxLimit, 10)), + zap.String("Expected Worker Count", strconv.Itoa(workerCount)), + zap.String("Allocated worker count", strconv.Itoa(newWorkerCount))) + return newWorkerCount, nil +} + +// Create one +func (m *LightningMemoryRoot) RegistWorkerContext(engineInfoKey string, id int) (*WorkerContext, error) { + var ( + err error = nil + wCtx *WorkerContext + memRequire int64 = m.structSize[string(ALLOC_WORKER_CONTEXT)] + ) + m.mLock.Lock() + defer func() { + m.mLock.Unlock() + }() + // First to check the memory usage + m.totalMemoryConsume() + err = m.checkMemoryUsage(ALLOC_WORKER_CONTEXT) + if err != nil { + log.L().Error(LERR_ALLOC_MEM_FAILED, zap.String("Engine key", engineInfoKey), + zap.String("worer Id:", strconv.Itoa(id)), + zap.String("Memory allocate:", strconv.FormatInt(memRequire, 10)), + zap.String("Current Memory Usage:", strconv.FormatInt(m.currUsage, 10)), + zap.String("Memory limitation:", strconv.FormatInt(m.maxLimit, 10))) + return nil, err + } + + wCtx = &WorkerContext{} + err = wCtx.InitWorkerContext(engineInfoKey, id) + if err != nil { + log.L().Error(LERR_CREATE_CONTEX_FAILED, zap.String("Engine key", engineInfoKey), + zap.String("worer Id:", strconv.Itoa(id)), + zap.String("Memory allocate:", strconv.FormatInt(memRequire, 10)), + zap.String("Current Memory Usage:", strconv.FormatInt(m.currUsage, 10)), + zap.String("Memory limitation:", strconv.FormatInt(m.maxLimit, 10))) + return nil, err + } + + // Count memory usage. + m.currUsage += memRequire + log.L().Info(LINFO_CREATE_WRITER, zap.String("Engine key", engineInfoKey), + zap.String("worer Id:", strconv.Itoa(id)), + zap.String("Memory allocate:", strconv.FormatInt(memRequire, 10)), + zap.String("Current Memory Usage:", strconv.FormatInt(m.currUsage, 10)), + zap.String("Memory limitation:", strconv.FormatInt(m.maxLimit, 10))) + return wCtx, err +} + +// Uniform entry to release Engine info. +func (m *LightningMemoryRoot) deleteBackendEngines(bcKey string) error { + var err error = nil + var count int = 0 + bc, exist := m.getBackendContext(bcKey) + if !exist { + log.L().Error(LERR_GET_BACKEND_FAILED, zap.String("backend key", bcKey)) + return err + } + count = 0 + // Delete EngienInfo registed in m.engineManager.engineCache + for _, ei := range bc.EngineCache { + eiKey := ei.key + m.currUsage -= m.structSize[ei.key] + delete(m.structSize, eiKey) + delete(m.EngineMgr.enginePool, eiKey) + count++ + } + + bc.EngineCache = make(map[string]*engineInfo, 10) + m.currUsage -= m.structSize[string(ALLOC_ENGINE_INFO)] * int64(count) + m.engineUsage -= m.structSize[string(ALLOC_ENGINE_INFO)] * int64(count) + log.L().Info(LINFO_CLOSE_BACKEND, zap.String("backend key", bcKey), + zap.String("Current Memory Usage:", strconv.FormatInt(m.currUsage, 10)), + zap.String("Memory limitation:", strconv.FormatInt(m.maxLimit, 10))) + return err +} + +func (m *LightningMemoryRoot) getBackendContext(bcKey string) (*BackendContext, bool) { + bc, exist := m.backendCache[bcKey] + if !exist { + log.L().Warn(LWAR_BACKEND_NOT_EXIST, zap.String("backend key:", bcKey)) + return nil, false + } + return bc, exist +} + +func (m *LightningMemoryRoot) totalMemoryConsume() { + var diffSize int64 = 0 + for _, bc := range m.backendCache { + curSize := bc.Backend.TotalMemoryConsume() + bcSize, exist := m.structSize[bc.Key] + if !exist { + diffSize += curSize + m.structSize[bc.Key] = curSize + } else { + diffSize += curSize - bcSize + m.structSize[bc.Key] += curSize - bcSize + } + m.structSize[bc.Key] = curSize + } + m.currUsage += diffSize + return +} + +func (m *LightningMemoryRoot) workerDegree(workerCnt int, engineKey string) int { + var kvp common.KvPair + size := unsafe.Sizeof(kvp) + // If only one worker's memory init requirement still bigger than mem limitation. + if int64(size*units.MiB)+m.currUsage > m.maxLimit { + return int(allocFailed) + } + + for int64(size*units.MiB*uintptr(workerCnt))+m.currUsage > m.maxLimit && workerCnt > 1 { + workerCnt /= 2 + } + + m.currUsage += int64(size * units.MiB * uintptr(workerCnt)) + _, exist := m.structSize[engineKey] + if !exist { + m.structSize[engineKey] = int64(size * units.MiB * uintptr(workerCnt)) + } else { + m.structSize[engineKey] += int64(size * units.MiB * uintptr(workerCnt)) + } + return workerCnt +} + +func (m *LightningMemoryRoot) TotalDiskUsage() int64 { + var totalDiskUsed int64 + for _, bc := range m.backendCache { + _, _, bcDiskUsed, _ := bc.Backend.CheckDiskQuota(GlobalLightningEnv.diskQuota) + totalDiskUsed += bcDiskUsed + } + return totalDiskUsed +} + +// defaultImportantVariables is used in ObtainImportantVariables to retrieve the system +// variables from downstream which may affect KV encode result. The values record the default +// values if missing. +var defaultImportantVariables = map[string]string{ + "max_allowed_packet": "67108864", + "div_precision_increment": "4", + "time_zone": "SYSTEM", + "lc_time_names": "en_US", + "default_week_format": "0", + "block_encryption_mode": "aes-128-ecb", + "group_concat_max_len": "1024", +} + +// defaultImportVariablesTiDB is used in ObtainImportantVariables to retrieve the system +// variables from downstream in local/importer backend. The values record the default +// values if missing. +var defaultImportVariablesTiDB = map[string]string{ + "tidb_row_format_version": "1", +} + +func obtainImportantVariables() map[string]string { + // convert result into a map. fill in any missing variables with default values. + result := make(map[string]string, len(defaultImportantVariables)+len(defaultImportVariablesTiDB)) + for key, value := range defaultImportantVariables { + result[key] = value + v := variable.GetSysVar(key) + if v.Value != value { + result[key] = value + } + } + + for key, value := range defaultImportVariablesTiDB { + result[key] = value + v := variable.GetSysVar(key) + if v.Value != value { + result[key] = value + } + } + return result +} diff --git a/ddl/reorg.go b/ddl/reorg.go index 1ff7af457cedb..eb44d6ba53c97 100644 --- a/ddl/reorg.go +++ b/ddl/reorg.go @@ -231,6 +231,9 @@ func (w *worker) runReorgJob(rh *reorgHandler, reorgInfo *reorgInfo, tblInfo *mo waitTimeout = ReorgWaitTimeout } + // ToDo: Bear init Lightning openengine if there is need to open multi openengine for index backfill. + // Init lightning job meta. + // wait reorganization job done or timeout select { case err := <-rc.doneCh: @@ -256,7 +259,12 @@ func (w *worker) runReorgJob(rh *reorgHandler, reorgInfo *reorgInfo, tblInfo *mo switch reorgInfo.Type { case model.ActionAddIndex, model.ActionAddPrimaryKey: + // For lightning there is a part import should be counted. + if (reorgInfo.Meta.IsLightningEnabled) { + metrics.GetBackfillProgressByLabel(metrics.LblAddIndex).Set(BackfillProgressPercent * 100) + } else { metrics.GetBackfillProgressByLabel(metrics.LblAddIndex).Set(100) + } case model.ActionModifyColumn: metrics.GetBackfillProgressByLabel(metrics.LblModifyColumn).Set(100) } @@ -283,6 +291,7 @@ func (w *worker) runReorgJob(rh *reorgHandler, reorgInfo *reorgInfo, tblInfo *mo // Update a reorgInfo's handle. // Since daemon-worker is triggered by timer to store the info half-way. // you should keep these infos is read-only (like job) / atomic (like doneKey & element) / concurrent safe. + // Todo: Bear, need update and store lightning related runtime status into meta. err := rh.UpdateDDLReorgStartHandle(job, currentElement, doneKey) logutil.BgLogger().Info("[ddl] run reorg job wait timeout", @@ -324,7 +333,13 @@ func updateBackfillProgress(w *worker, reorgInfo *reorgInfo, tblInfo *model.Tabl } switch reorgInfo.Type { case model.ActionAddIndex, model.ActionAddPrimaryKey: - metrics.GetBackfillProgressByLabel(metrics.LblAddIndex).Set(progress * 100) + // For lightning there is a part import should be counted. + if (reorgInfo.Meta.IsLightningEnabled) { + metrics.GetBackfillProgressByLabel(metrics.LblAddIndex).Set(BackfillProgressPercent * progress * 100) + } else { + metrics.GetBackfillProgressByLabel(metrics.LblAddIndex).Set(progress * 100) + } + case model.ActionModifyColumn: metrics.GetBackfillProgressByLabel(metrics.LblModifyColumn).Set(progress * 100) } @@ -387,6 +402,14 @@ type reorgInfo struct { PhysicalTableID int64 elements []*meta.Element currElement *meta.Element + + // Extend meta information for reorg task. + Meta reorgMeta +} + +type reorgMeta struct { + // Mark whether the lightning execution environment is built or not + IsLightningEnabled bool } func (r *reorgInfo) String() string { @@ -395,7 +418,8 @@ func (r *reorgInfo) String() string { "StartHandle:" + tryDecodeToHandleString(r.StartKey) + "," + "EndHandle:" + tryDecodeToHandleString(r.EndKey) + "," + "First:" + strconv.FormatBool(r.first) + "," + - "PhysicalTableID:" + strconv.FormatInt(r.PhysicalTableID, 10) + "PhysicalTableID:" + strconv.FormatInt(r.PhysicalTableID, 10) + "," + + "Lightning execution:" + strconv.FormatBool(r.Meta.IsLightningEnabled) } func constructDescTableScanPB(physicalTableID int64, tblInfo *model.TableInfo, handleCols []*model.ColumnInfo) *tipb.Executor { @@ -634,6 +658,8 @@ func getReorgInfo(ctx *JobContext, d *ddlCtx, rh *reorgHandler, job *model.Job, // Update info should after data persistent. job.SnapshotVer = ver.Ver element = elements[0] + // Init reorgInfo meta. + info.Meta.IsLightningEnabled = false } else { failpoint.Inject("MockGetIndexRecordErr", func(val failpoint.Value) { // For the case of the old TiDB version(do not exist the element information) is upgraded to the new TiDB version. diff --git a/go.sum b/go.sum index 14022ed2f4e17..3251964aacdb0 100644 --- a/go.sum +++ b/go.sum @@ -504,6 +504,8 @@ github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiD github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/godepgraph v0.0.0-20190626013829-57a7e4a651a9 h1:ZkWH0x1yafBo+Y2WdGGdszlJrMreMXWl7/dqpEkwsIk= +github.com/kisielk/godepgraph v0.0.0-20190626013829-57a7e4a651a9/go.mod h1:Gb5YEgxqiSSVrXKWQxDcKoCM94NO5QAwOwTaVmIUAMI= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= diff --git a/sessionctx/variable/sysvar.go b/sessionctx/variable/sysvar.go index 9ee43476be4d2..7d568b76576e0 100644 --- a/sessionctx/variable/sysvar.go +++ b/sessionctx/variable/sysvar.go @@ -1578,6 +1578,25 @@ var defaultSysVars = []*SysVar{ return nil }, }, + {Scope: ScopeGlobal, Name: TiDBFastDDL, Value: BoolToOnOff(DefTiDBFastDDL), Type: TypeBool, GetGlobal: func(sv *SessionVars) (string, error) { + return BoolToOnOff(FastDDL.Load()), nil + }, SetGlobal: func(s *SessionVars, val string) error { + FastDDL.Store(TiDBOptOn(val)) + return nil + }}, + // This system var is set disk quota for lightning sort dir, from 100 GB to 1PB. + {Scope: ScopeGlobal, Name: TiDBDiskQuota, Value: strconv.Itoa(DefTiDBDiskQuota), Type: TypeInt, MinValue: 100, MaxValue: 1048576, GetGlobal: func(sv *SessionVars) (string, error) { + return strconv.Itoa(int(DiskQuota.Load())), nil + }, SetGlobal: func(s *SessionVars, val string) error { + value, err := strconv.Atoi(val) + if err != nil { + DiskQuota.Store(int32(value)) + } else { + // Set to default value. + DiskQuota.Store(DefTiDBDiskQuota) + } + return nil + }}, } // FeedbackProbability points to the FeedbackProbability in statistics package. diff --git a/sessionctx/variable/sysvar_test.go b/sessionctx/variable/sysvar_test.go index 6c62a697e6d5b..06e59d54bb67a 100644 --- a/sessionctx/variable/sysvar_test.go +++ b/sessionctx/variable/sysvar_test.go @@ -986,3 +986,28 @@ func TestTiDBCommitterConcurrency(t *testing.T) { require.Equal(t, val, fmt.Sprintf("%d", expected)) require.NoError(t, err) } + +func TestSetTIDBFastDDL(t *testing.T) { + vars := NewSessionVars() + mock := NewMockGlobalAccessor4Tests() + mock.SessionVars = vars + vars.GlobalVarsAccessor = mock + fastDDL := GetSysVar(TiDBFastDDL) + + // Default off + require.Equal(t, fastDDL.Value, Off) + + // Set to On + err := mock.SetGlobalSysVar(TiDBFastDDL, On) + require.NoError(t, err) + val, err1 := mock.GetGlobalSysVar(TiDBFastDDL) + require.NoError(t, err1) + require.Equal(t, On, val) + + // Set to off + err = mock.SetGlobalSysVar(TiDBFastDDL, Off) + require.NoError(t, err) + val, err1 = mock.GetGlobalSysVar(TiDBFastDDL) + require.NoError(t, err1) + require.Equal(t, Off, val) +} diff --git a/sessionctx/variable/tidb_vars.go b/sessionctx/variable/tidb_vars.go index 12916ed13e874..0bd0c6e1c96ca 100644 --- a/sessionctx/variable/tidb_vars.go +++ b/sessionctx/variable/tidb_vars.go @@ -704,6 +704,9 @@ const ( // TiDBMaxAutoAnalyzeTime is the max time that auto analyze can run. If auto analyze runs longer than the value, it // will be killed. 0 indicates that there is no time limit. TiDBMaxAutoAnalyzeTime = "tidb_max_auto_analyze_time" + // TiDBFastDDL indicates whether use lighting to help acceleate adding index stmt. + TiDBFastDDL = "tidb_fast_ddl" + TiDBDiskQuota = "tidb_disk_quota" ) // TiDB intentional limits @@ -892,6 +895,8 @@ const ( DefTiDBEnablePrepPlanCache = true DefTiDBPrepPlanCacheSize = 100 DefTiDBPrepPlanCacheMemoryGuardRatio = 0.1 + DefTiDBFastDDL = false + DefTiDBDiskQuota = 100 // 100GB ) // Process global variables. @@ -937,6 +942,10 @@ var ( EnablePreparedPlanCache = atomic.NewBool(DefTiDBEnablePrepPlanCache) PreparedPlanCacheSize = atomic.NewUint64(DefTiDBPrepPlanCacheSize) PreparedPlanCacheMemoryGuardRatio = atomic.NewFloat64(DefTiDBPrepPlanCacheMemoryGuardRatio) + // TiDBFastDDL indicates whether to use lightning to enhance DDL reorg performance. + FastDDL = atomic.NewBool(false) + // Temporary Variable for set dist quota for lightning add index, int type, GB as unit + DiskQuota = atomic.NewInt32(100) ) var ( @@ -947,3 +956,4 @@ var ( // SetStatsCacheCapacity is the func registered by domain to set statsCache memory quota. SetStatsCacheCapacity atomic.Value ) + diff --git a/sessionctx/variable/variable.go b/sessionctx/variable/variable.go index ad91470802795..6f87765ca62d4 100644 --- a/sessionctx/variable/variable.go +++ b/sessionctx/variable/variable.go @@ -616,4 +616,4 @@ type GlobalVarAccessor interface { GetTiDBTableValue(name string) (string, error) // SetTiDBTableValue sets a value+comment for the mysql.tidb key 'name' SetTiDBTableValue(name, value, comment string) error -} +} \ No newline at end of file diff --git a/table/index.go b/table/index.go index 62892c135b7c3..45f9c5527cd7e 100644 --- a/table/index.go +++ b/table/index.go @@ -16,7 +16,6 @@ package table import ( "context" - "github.com/pingcap/tidb/kv" "github.com/pingcap/tidb/parser/model" "github.com/pingcap/tidb/sessionctx" @@ -76,4 +75,11 @@ type Index interface { // Param columns is a reused buffer, if it is not nil, FetchValues will fill the index values in it, // and return the buffer, if it is nil, FetchValues will allocate the buffer instead. FetchValues(row []types.Datum, columns []types.Datum) ([]types.Datum, error) + // Add one method for lightning create index + Create4SST(ctx sessionctx.Context, txn kv.Transaction, indexedValues []types.Datum, h kv.Handle, handleRestoreData []types.Datum, opts ...CreateIdxOptFunc) ([]byte, []byte, bool, error) +} + +// TODO: delete later. just for cycle import test. +func ref() { + // addindex.IndexCycleReference() } diff --git a/table/tables/index.go b/table/tables/index.go index d7e88a824c7ab..bedea4e7aed74 100644 --- a/table/tables/index.go +++ b/table/tables/index.go @@ -214,6 +214,66 @@ func (c *index) Create(sctx sessionctx.Context, txn kv.Transaction, indexedValue return handle, kv.ErrKeyExists } +// Create creates a new entry in the kvIndex data. +// If the index is unique and there is an existing entry with the same key, +// Create will return the existing entry's handle as the first return value, ErrKeyExists as the second return value. +func (c *index) Create4SST(sctx sessionctx.Context, txn kv.Transaction, indexedValues []types.Datum, h kv.Handle, handleRestoreData []types.Datum, opts ...table.CreateIdxOptFunc) ([]byte, []byte, bool, error) { + if c.Meta().Unique { + txn.CacheTableInfo(c.phyTblID, c.tblInfo) + } + var opt table.CreateIdxOpt + for _, fn := range opts { + fn(&opt) + } + vars := sctx.GetSessionVars() + writeBufs := vars.GetWriteStmtBufs() + key, distinct, err := c.GenIndexKey(vars.StmtCtx, indexedValues, h, writeBufs.IndexKeyBuf) + if err != nil { + return key, nil, distinct, err + } + + ctx := opt.Ctx + if opt.Untouched { + txn, err1 := sctx.Txn(true) + if err1 != nil { + return key, nil, distinct, err + } + // If the index kv was untouched(unchanged), and the key/value already exists in mem-buffer, + // should not overwrite the key with un-commit flag. + // So if the key exists, just do nothing and return. + v, err := txn.GetMemBuffer().Get(ctx, key) + if err == nil { + if len(v) != 0 { + return key, nil, distinct, nil + } + // The key is marked as deleted in the memory buffer, as the existence check is done lazily + // for optimistic transactions by default. The "untouched" key could still exist in the store, + // it's needed to commit this key to do the existence check so unset the untouched flag. + if !txn.IsPessimistic() { + keyFlags, err := txn.GetMemBuffer().GetFlags(key) + if err != nil { + return key, nil, distinct, err + } + if keyFlags.HasPresumeKeyNotExists() { + opt.Untouched = false + } + } + } + } + + // save the key buffer to reuse. + writeBufs.IndexKeyBuf = key + c.initNeedRestoreData.Do(func() { + c.needRestoredData = NeedRestoredData(c.idxInfo.Columns, c.tblInfo.Columns) + }) + idxVal, err := tablecodec.GenIndexValuePortal(sctx.GetSessionVars().StmtCtx, c.tblInfo, c.idxInfo, c.needRestoredData, distinct, opt.Untouched, indexedValues, h, c.phyTblID, handleRestoreData) + if err != nil { + return key, nil, distinct, err + } + + return key, idxVal, distinct, err +} + // Delete removes the entry for handle h and indexedValues from KV index. func (c *index) Delete(sc *stmtctx.StatementContext, txn kv.Transaction, indexedValues []types.Datum, h kv.Handle) error { key, distinct, err := c.GenIndexKey(sc, indexedValues, h, nil) diff --git a/tidb-server/main.go b/tidb-server/main.go index 06e26eb2341c6..208519c9d2a39 100644 --- a/tidb-server/main.go +++ b/tidb-server/main.go @@ -33,6 +33,7 @@ import ( "github.com/pingcap/tidb/bindinfo" "github.com/pingcap/tidb/config" "github.com/pingcap/tidb/ddl" + lit "github.com/pingcap/tidb/ddl/lightning" "github.com/pingcap/tidb/domain" "github.com/pingcap/tidb/executor" "github.com/pingcap/tidb/kv" @@ -198,6 +199,8 @@ func main() { printInfo() setupBinlogClient() setupMetrics() + // Init Lighting Global environment. Once met error then the + lit.InitGolbalLightningBackendEnv() storage, dom := createStoreAndDomain() svr := createServer(storage, dom) diff --git a/types/json/path_expr.go b/types/json/path_expr.go index 5d23c1a16ed3a..b954852e237da 100644 --- a/types/json/path_expr.go +++ b/types/json/path_expr.go @@ -15,11 +15,15 @@ package json import ( + "math" "regexp" "strconv" "strings" + "sync" "github.com/pingcap/errors" + "github.com/pingcap/tidb/util/hack" + "github.com/pingcap/tidb/util/kvcache" ) /* @@ -86,6 +90,19 @@ func (pef pathExpressionFlag) containsAnyAsterisk() bool { return byte(pef) != 0 } +var peCache PathExpressionCache + +type pathExpressionKey string + +func (key pathExpressionKey) Hash() []byte { + return hack.Slice(string(key)) +} + +type PathExpressionCache struct { + mu sync.Mutex + cache *kvcache.SimpleLRUCache +} + // PathExpression is for JSON path expression. type PathExpression struct { legs []pathLeg @@ -150,6 +167,22 @@ func (pe PathExpression) ContainsAnyAsterisk() bool { // ParseJSONPathExpr parses a JSON path expression. Returns a PathExpression // object which can be used in JSON_EXTRACT, JSON_SET and so on. func ParseJSONPathExpr(pathExpr string) (pe PathExpression, err error) { + peCache.mu.Lock() + val, ok := peCache.cache.Get(pathExpressionKey(pathExpr)) + if ok { + peCache.mu.Unlock() + return val.(PathExpression), nil + } + peCache.mu.Unlock() + + defer func() { + if err == nil { + peCache.mu.Lock() + peCache.cache.Put(pathExpressionKey(pathExpr), kvcache.Value(pe)) + peCache.mu.Unlock() + } + }() + // Find the position of first '$'. If any no-blank characters in // pathExpr[0: dollarIndex), return an ErrInvalidJSONPath error. dollarIndex := strings.Index(pathExpr, "$") @@ -261,3 +294,7 @@ func (pe PathExpression) String() string { } return s.String() } + +func init() { + peCache.cache = kvcache.NewSimpleLRUCache(1000, 0.1, math.MaxUint64) +} \ No newline at end of file diff --git a/util/memory/tracker.go b/util/memory/tracker.go index 106ff210e83ed..806fec30eaef4 100644 --- a/util/memory/tracker.go +++ b/util/memory/tracker.go @@ -22,7 +22,6 @@ import ( "sync" "sync/atomic" - "github.com/pingcap/tidb/config" "github.com/pingcap/tidb/metrics" atomicutil "go.uber.org/atomic" ) @@ -83,6 +82,9 @@ type actionMu struct { // softScale means the scale of the soft limit to the hard limit. const softScale = 0.8 +// TrackMemWhenExceeds is the threshold when memory usage needs to be tracked. +const TrackMemWhenExceeds = 104857600 // 100MB + // bytesLimits holds limit config atomically. type bytesLimits struct { bytesHardLimit int64 // bytesHardLimit <= 0 means no limit, used for actionMuForHardLimit. @@ -388,7 +390,7 @@ func (t *Tracker) Consume(bytes int64) { // BufferedConsume is used to buffer memory usage and do late consume func (t *Tracker) BufferedConsume(bufferedMemSize *int64, bytes int64) { *bufferedMemSize += bytes - if *bufferedMemSize > int64(config.TrackMemWhenExceeds) { + if *bufferedMemSize > int64(TrackMemWhenExceeds) { t.Consume(*bufferedMemSize) *bufferedMemSize = int64(0) }