Skip to content

Commit e4a9dab

Browse files
nolouchblacktear23
authored andcommitted
placement: supports survival preferences (pingcap#40613)
close pingcap#38605
1 parent a281757 commit e4a9dab

15 files changed

+9993
-9869
lines changed

ddl/ddl_api.go

+2
Original file line numberDiff line numberDiff line change
@@ -3026,6 +3026,8 @@ func SetDirectPlacementOpt(placementSettings *model.PlacementSettings, placement
30263026
placementSettings.FollowerConstraints = stringVal
30273027
case ast.PlacementOptionVoterConstraints:
30283028
placementSettings.VoterConstraints = stringVal
3029+
case ast.PlacementOptionSurvivalPreferences:
3030+
placementSettings.SurvivalPreferences = stringVal
30293031
default:
30303032
return errors.Trace(errors.New("unknown placement policy option"))
30313033
}

ddl/placement/bundle.go

+45-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/pingcap/tidb/tablecodec"
3030
"github.com/pingcap/tidb/util/codec"
3131
"golang.org/x/exp/slices"
32+
"gopkg.in/yaml.v2"
3233
)
3334

3435
// Refer to https://github.com/tikv/pd/issues/2701 .
@@ -123,7 +124,13 @@ func NewBundleFromConstraintsOptions(options *model.PlacementSettings) (*Bundle,
123124
rules = append(rules, rule)
124125
}
125126
}
126-
127+
labels, err := newLocationLabelsFromSurvivalPreferences(options.SurvivalPreferences)
128+
if err != nil {
129+
return nil, err
130+
}
131+
for _, rule := range rules {
132+
rule.LocationLabels = labels
133+
}
127134
return &Bundle{Rules: rules}, nil
128135
}
129136

@@ -155,9 +162,17 @@ func NewBundleFromSugarOptions(options *model.PlacementSettings) (*Bundle, error
155162

156163
var rules []*Rule
157164

165+
locationLabels, err := newLocationLabelsFromSurvivalPreferences(options.SurvivalPreferences)
166+
if err != nil {
167+
return nil, err
168+
}
169+
158170
// in case empty primaryRegion and regions, just return an empty bundle
159171
if primaryRegion == "" && len(regions) == 0 {
160172
rules = append(rules, NewRule(Voter, followers+1, NewConstraintsDirect()))
173+
for _, rule := range rules {
174+
rule.LocationLabels = locationLabels
175+
}
161176
return &Bundle{Rules: rules}, nil
162177
}
163178

@@ -195,6 +210,11 @@ func NewBundleFromSugarOptions(options *model.PlacementSettings) (*Bundle, error
195210
}
196211
}
197212

213+
// set location labels
214+
for _, rule := range rules {
215+
rule.LocationLabels = locationLabels
216+
}
217+
198218
return &Bundle{Rules: rules}, nil
199219
}
200220

@@ -223,6 +243,19 @@ func newBundleFromOptions(options *model.PlacementSettings) (bundle *Bundle, err
223243
return bundle, err
224244
}
225245

246+
// newLocationLabelsFromSurvivalPreferences will parse the survival preferences into location labels.
247+
func newLocationLabelsFromSurvivalPreferences(survivalPreferenceStr string) ([]string, error) {
248+
if len(survivalPreferenceStr) > 0 {
249+
labels := []string{}
250+
err := yaml.UnmarshalStrict([]byte(survivalPreferenceStr), &labels)
251+
if err != nil {
252+
return nil, ErrInvalidSurvivalPreferenceFormat
253+
}
254+
return labels, nil
255+
}
256+
return nil, nil
257+
}
258+
226259
// NewBundleFromOptions will transform options into the bundle.
227260
func NewBundleFromOptions(options *model.PlacementSettings) (bundle *Bundle, err error) {
228261
bundle, err = newBundleFromOptions(options)
@@ -257,6 +290,15 @@ func (b *Bundle) String() string {
257290
func (b *Bundle) Tidy() error {
258291
extraCnt := map[PeerRoleType]int{}
259292
newRules := b.Rules[:0]
293+
294+
// One Bundle is from one PlacementSettings, rule share same location labels, so we can use the first rule's location labels.
295+
var locationLabels []string
296+
for _, rule := range b.Rules {
297+
if len(rule.LocationLabels) > 0 {
298+
locationLabels = rule.LocationLabels
299+
break
300+
}
301+
}
260302
for i, rule := range b.Rules {
261303
// useless Rule
262304
if rule.Count <= 0 {
@@ -300,6 +342,8 @@ func (b *Bundle) Tidy() error {
300342
Key: EngineLabelKey,
301343
Values: []string{EngineLabelTiFlash},
302344
}},
345+
// the merged rule should have the same location labels with the original rules.
346+
LocationLabels: locationLabels,
303347
})
304348
}
305349
b.Rules = newRules

ddl/placement/bundle_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,9 @@ func TestTidy(t *testing.T) {
887887
bundle.Rules = append(bundle.Rules, rules3...)
888888
bundle.Rules = append(bundle.Rules, rules4...)
889889

890+
for _, r := range bundle.Rules {
891+
r.LocationLabels = []string{"zone", "host"}
892+
}
890893
chkfunc := func() {
891894
require.NoError(t, err)
892895
require.Len(t, bundle.Rules, 3)
@@ -901,6 +904,7 @@ func TestTidy(t *testing.T) {
901904
Values: []string{EngineLabelTiFlash},
902905
},
903906
}, bundle.Rules[2].Constraints)
907+
require.Equal(t, []string{"zone", "host"}, bundle.Rules[2].LocationLabels)
904908
}
905909
err = bundle.Tidy()
906910
chkfunc()

ddl/placement/errors.go

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ var (
2929
ErrInvalidConstraintsMapcnt = errors.New("label constraints in map syntax have invalid replicas")
3030
// ErrInvalidConstraintsFormat is from rule.go.
3131
ErrInvalidConstraintsFormat = errors.New("invalid label constraints format")
32+
// ErrInvalidSurvivalPreferenceFormat is from rule.go.
33+
ErrInvalidSurvivalPreferenceFormat = errors.New("survival preference format should be in format [xxx=yyy, ...]")
3234
// ErrInvalidConstraintsRelicas is from rule.go.
3335
ErrInvalidConstraintsRelicas = errors.New("label constraints with invalid REPLICAS")
3436
// ErrInvalidBundleID is from bundle.go.

ddl/placement/rule.go

+10-9
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,16 @@ type RuleGroupConfig struct {
4545

4646
// Rule is the core placement rule struct. Check https://github.com/tikv/pd/blob/master/server/schedule/placement/rule.go.
4747
type Rule struct {
48-
GroupID string `json:"group_id"`
49-
ID string `json:"id"`
50-
Index int `json:"index,omitempty"`
51-
Override bool `json:"override,omitempty"`
52-
StartKeyHex string `json:"start_key"`
53-
EndKeyHex string `json:"end_key"`
54-
Role PeerRoleType `json:"role"`
55-
Count int `json:"count"`
56-
Constraints Constraints `json:"label_constraints,omitempty"`
48+
GroupID string `json:"group_id"`
49+
ID string `json:"id"`
50+
Index int `json:"index,omitempty"`
51+
Override bool `json:"override,omitempty"`
52+
StartKeyHex string `json:"start_key"`
53+
EndKeyHex string `json:"end_key"`
54+
Role PeerRoleType `json:"role"`
55+
Count int `json:"count"`
56+
Constraints Constraints `json:"label_constraints,omitempty"`
57+
LocationLabels []string `json:"location_labels,omitempty"`
5758
}
5859

5960
// TiFlashRule extends Rule with other necessary fields.

ddl/placement_policy_test.go

+20-2
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ func TestPlacementPolicy(t *testing.T) {
155155
"LEARNERS=1 " +
156156
"LEARNER_CONSTRAINTS=\"[+region=cn-west-1]\" " +
157157
"FOLLOWERS=3 " +
158-
"FOLLOWER_CONSTRAINTS=\"[+disk=ssd]\"")
158+
"FOLLOWER_CONSTRAINTS=\"[+disk=ssd]\"" +
159+
"SURVIVAL_PREFERENCES=\"[region, zone]\"")
159160

160161
checkFunc := func(policyInfo *model.PolicyInfo) {
161162
require.Equal(t, true, policyInfo.ID != 0)
@@ -168,6 +169,7 @@ func TestPlacementPolicy(t *testing.T) {
168169
require.Equal(t, "[+region=cn-west-1]", policyInfo.LearnerConstraints)
169170
require.Equal(t, model.StatePublic, policyInfo.State)
170171
require.Equal(t, "", policyInfo.Schedule)
172+
require.Equal(t, "[region, zone]", policyInfo.SurvivalPreferences)
171173
}
172174

173175
// Check the policy is correctly reloaded in the information schema.
@@ -590,11 +592,16 @@ func TestCreateTableWithPlacementPolicy(t *testing.T) {
590592
tk.MustExec("create placement policy x " +
591593
"FOLLOWERS=2 " +
592594
"CONSTRAINTS=\"[+disk=ssd]\" ")
595+
tk.MustExec("create placement policy z " +
596+
"FOLLOWERS=1 " +
597+
"SURVIVAL_PREFERENCES=\"[region, zone]\"")
593598
tk.MustExec("create placement policy y " +
594599
"FOLLOWERS=3 " +
595600
"CONSTRAINTS=\"[+region=bj]\" ")
596601
tk.MustExec("create table t(a int)" +
597602
"PLACEMENT POLICY=\"x\"")
603+
tk.MustExec("create table tt(a int)" +
604+
"PLACEMENT POLICY=\"z\"")
598605
tk.MustQuery("SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, TIDB_PLACEMENT_POLICY_NAME FROM information_schema.Tables WHERE TABLE_SCHEMA='test' AND TABLE_NAME = 't'").Check(testkit.Rows(`def test t x`))
599606
tk.MustExec("create table t_range_p(id int) placement policy x partition by range(id) (" +
600607
"PARTITION p0 VALUES LESS THAN (100)," +
@@ -617,7 +624,18 @@ func TestCreateTableWithPlacementPolicy(t *testing.T) {
617624
require.Equal(t, "y", policyY.Name.L)
618625
require.Equal(t, true, policyY.ID != 0)
619626

620-
tbl := external.GetTableByName(t, tk, "test", "t")
627+
policyZ := testGetPolicyByName(t, tk.Session(), "z", true)
628+
require.Equal(t, "z", policyZ.Name.L)
629+
require.Equal(t, true, policyZ.ID != 0)
630+
require.Equal(t, "[region, zone]", policyZ.SurvivalPreferences)
631+
632+
tbl := external.GetTableByName(t, tk, "test", "tt")
633+
require.NotNil(t, tbl)
634+
require.NotNil(t, tbl.Meta().PlacementPolicyRef)
635+
require.Equal(t, "z", tbl.Meta().PlacementPolicyRef.Name.L)
636+
require.Equal(t, policyZ.ID, tbl.Meta().PlacementPolicyRef.ID)
637+
638+
tbl = external.GetTableByName(t, tk, "test", "t")
621639
require.NotNil(t, tbl)
622640
require.NotNil(t, tbl.Meta().PlacementPolicyRef)
623641
require.Equal(t, "x", tbl.Meta().PlacementPolicyRef.Name.L)

docs/design/2020-06-24-placement-rules-in-sql.md

+21
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,26 @@ If a table is imported when `tidb_placement_mode='IGNORE'`, and the placement po
413413

414414
The default value for `tidb_placement_mode` is `STRICT`. The option is an enum, and in future we may add support for a `WARN` mode.
415415

416+
417+
#### Survival preference
418+
419+
Some important data may need to store multiple copies across availability zones, so as to have high disaster recovery survivability, such as region-level survivability, `SURVIVAL_PREFERENCES` can provide survivability preference settings.
420+
421+
The following example sets a constraint that the data try to satisfy the Survival Preferences setting:
422+
423+
``` sql
424+
CREATE PLACEMENT POLICY multiregion
425+
follower=4
426+
PRIMARY_REGION="region1"
427+
SURVIVAL_PREFERENCES="[region, zone]";
428+
```
429+
430+
For tables with this policy set, the data will first meet the survival goal of cross-region data isolation, and then ensure the survival goal of cross-zone data isolation.
431+
432+
> **Note:**
433+
>
434+
> `SURVIVAL_PREFERENCES` is equivalent to `LOCATION_LABELS` in PD. For more information, please refer to [Replica scheduling by topology label](https://docs.pingcap.com/tidb/dev/schedule-replicas-by-topology-labels#schedule-replicas-by-topology-labels).
435+
416436
#### Ambiguous and edge cases
417437

418438
The following two policies are not identical:
@@ -491,6 +511,7 @@ In this case the default rules will apply to placement, and the output from `SHO
491511
- `FOLLOWER_CONSTRAINTS`
492512
- `LEARNER_CONSTRAINTS`
493513
- `PLACEMENT POLICY`
514+
- `SURVIVAL_PREFERENCE`
494515

495516
For a more complex rule using partitions, consider the following example:
496517

domain/infosync/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ go_library(
4343
"@com_github_pingcap_failpoint//:failpoint",
4444
"@com_github_pingcap_kvproto//pkg/metapb",
4545
"@com_github_pingcap_kvproto//pkg/resource_manager",
46+
"@com_github_pingcap_log//:log",
4647
"@com_github_tikv_client_go_v2//oracle",
4748
"@com_github_tikv_pd_client//:client",
4849
"@io_etcd_go_etcd_client_v3//:client",

domain/infosync/placement_manager.go

+3
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ import (
2121
"path"
2222
"sync"
2323

24+
"github.com/pingcap/log"
2425
"github.com/pingcap/tidb/ddl/placement"
2526
"github.com/pingcap/tidb/util/pdapi"
2627
clientv3 "go.etcd.io/etcd/client/v3"
28+
"go.uber.org/zap"
2729
)
2830

2931
// PlacementManager manages placement settings
@@ -72,6 +74,7 @@ func (m *PDPlacementManager) PutRuleBundles(ctx context.Context, bundles []*plac
7274
return err
7375
}
7476

77+
log.Debug("Put placement rule bundles", zap.String("rules", string(b)))
7578
_, err = doRequest(ctx, "PutPlacementRules", m.etcdCli.Endpoints(), path.Join(pdapi.Config, "placement-rule")+"?partial=true", "POST", bytes.NewReader(b))
7679
return err
7780
}

parser/ast/ddl.go

+5
Original file line numberDiff line numberDiff line change
@@ -2025,6 +2025,7 @@ const (
20252025
PlacementOptionLearnerConstraints
20262026
PlacementOptionFollowerConstraints
20272027
PlacementOptionVoterConstraints
2028+
PlacementOptionSurvivalPreferences
20282029
PlacementOptionPolicy
20292030
)
20302031

@@ -2089,6 +2090,10 @@ func (n *PlacementOption) Restore(ctx *format.RestoreCtx) error {
20892090
ctx.WriteKeyWord("PLACEMENT POLICY ")
20902091
ctx.WritePlain("= ")
20912092
ctx.WriteName(n.StrValue)
2093+
case PlacementOptionSurvivalPreferences:
2094+
ctx.WriteKeyWord("SURVIVAL_PREFERENCES ")
2095+
ctx.WritePlain("= ")
2096+
ctx.WriteString(n.StrValue)
20922097
default:
20932098
return errors.Errorf("invalid PlacementOption: %d", n.Tp)
20942099
}

parser/misc.go

+1
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,7 @@ var tokenMap = map[string]int{
715715
"SUBSTRING": substring,
716716
"SUM": sum,
717717
"SUPER": super,
718+
"SURVIVAL_PREFERENCES": survivalPreferences,
718719
"SWAPS": swaps,
719720
"SWITCHES": switchesSym,
720721
"SYSTEM": system,

parser/model/model.go

+1
Original file line numberDiff line numberDiff line change
@@ -1747,6 +1747,7 @@ type PlacementSettings struct {
17471747
LearnerConstraints string `json:"learner_constraints"`
17481748
FollowerConstraints string `json:"follower_constraints"`
17491749
VoterConstraints string `json:"voter_constraints"`
1750+
SurvivalPreferences string `json:"survival_preferences"`
17501751
}
17511752

17521753
// PolicyInfo is the struct to store the placement policy.

0 commit comments

Comments
 (0)