Skip to content

Commit 5825ca6

Browse files
committed
placement: supports survival preferences (pingcap#40613) (pingcap#44701)
close pingcap#38605
1 parent cd4df44 commit 5825ca6

15 files changed

+9829
-9706
lines changed

ddl/ddl_api.go

+2
Original file line numberDiff line numberDiff line change
@@ -3022,6 +3022,8 @@ func SetDirectPlacementOpt(placementSettings *model.PlacementSettings, placement
30223022
placementSettings.FollowerConstraints = stringVal
30233023
case ast.PlacementOptionVoterConstraints:
30243024
placementSettings.VoterConstraints = stringVal
3025+
case ast.PlacementOptionSurvivalPreferences:
3026+
placementSettings.SurvivalPreferences = stringVal
30253027
default:
30263028
return errors.Trace(errors.New("unknown placement policy option"))
30273029
}

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
@@ -193,7 +193,8 @@ func TestPlacementPolicy(t *testing.T) {
193193
"LEARNERS=1 " +
194194
"LEARNER_CONSTRAINTS=\"[+region=cn-west-1]\" " +
195195
"FOLLOWERS=3 " +
196-
"FOLLOWER_CONSTRAINTS=\"[+disk=ssd]\"")
196+
"FOLLOWER_CONSTRAINTS=\"[+disk=ssd]\"" +
197+
"SURVIVAL_PREFERENCES=\"[region, zone]\"")
197198

198199
checkFunc := func(policyInfo *model.PolicyInfo) {
199200
require.Equal(t, true, policyInfo.ID != 0)
@@ -206,6 +207,7 @@ func TestPlacementPolicy(t *testing.T) {
206207
require.Equal(t, "[+region=cn-west-1]", policyInfo.LearnerConstraints)
207208
require.Equal(t, model.StatePublic, policyInfo.State)
208209
require.Equal(t, "", policyInfo.Schedule)
210+
require.Equal(t, "[region, zone]", policyInfo.SurvivalPreferences)
209211
}
210212

211213
// Check the policy is correctly reloaded in the information schema.
@@ -628,11 +630,16 @@ func TestCreateTableWithPlacementPolicy(t *testing.T) {
628630
tk.MustExec("create placement policy x " +
629631
"FOLLOWERS=2 " +
630632
"CONSTRAINTS=\"[+disk=ssd]\" ")
633+
tk.MustExec("create placement policy z " +
634+
"FOLLOWERS=1 " +
635+
"SURVIVAL_PREFERENCES=\"[region, zone]\"")
631636
tk.MustExec("create placement policy y " +
632637
"FOLLOWERS=3 " +
633638
"CONSTRAINTS=\"[+region=bj]\" ")
634639
tk.MustExec("create table t(a int)" +
635640
"PLACEMENT POLICY=\"x\"")
641+
tk.MustExec("create table tt(a int)" +
642+
"PLACEMENT POLICY=\"z\"")
636643
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`))
637644
tk.MustExec("create table t_range_p(id int) placement policy x partition by range(id) (" +
638645
"PARTITION p0 VALUES LESS THAN (100)," +
@@ -655,7 +662,18 @@ func TestCreateTableWithPlacementPolicy(t *testing.T) {
655662
require.Equal(t, "y", policyY.Name.L)
656663
require.Equal(t, true, policyY.ID != 0)
657664

658-
tbl := external.GetTableByName(t, tk, "test", "t")
665+
policyZ := testGetPolicyByName(t, tk.Session(), "z", true)
666+
require.Equal(t, "z", policyZ.Name.L)
667+
require.Equal(t, true, policyZ.ID != 0)
668+
require.Equal(t, "[region, zone]", policyZ.SurvivalPreferences)
669+
670+
tbl := external.GetTableByName(t, tk, "test", "tt")
671+
require.NotNil(t, tbl)
672+
require.NotNil(t, tbl.Meta().PlacementPolicyRef)
673+
require.Equal(t, "z", tbl.Meta().PlacementPolicyRef.Name.L)
674+
require.Equal(t, policyZ.ID, tbl.Meta().PlacementPolicyRef.ID)
675+
676+
tbl = external.GetTableByName(t, tk, "test", "t")
659677
require.NotNil(t, tbl)
660678
require.NotNil(t, tbl.Meta().PlacementPolicyRef)
661679
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
@@ -41,6 +41,7 @@ go_library(
4141
"@com_github_pingcap_errors//:errors",
4242
"@com_github_pingcap_failpoint//:failpoint",
4343
"@com_github_pingcap_kvproto//pkg/metapb",
44+
"@com_github_pingcap_log//:log",
4445
"@com_github_tikv_client_go_v2//oracle",
4546
"@com_github_tikv_pd_client//:client",
4647
"@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
@@ -1958,6 +1958,7 @@ const (
19581958
PlacementOptionLearnerConstraints
19591959
PlacementOptionFollowerConstraints
19601960
PlacementOptionVoterConstraints
1961+
PlacementOptionSurvivalPreferences
19611962
PlacementOptionPolicy
19621963
)
19631964

@@ -2022,6 +2023,10 @@ func (n *PlacementOption) Restore(ctx *format.RestoreCtx) error {
20222023
ctx.WriteKeyWord("PLACEMENT POLICY ")
20232024
ctx.WritePlain("= ")
20242025
ctx.WriteName(n.StrValue)
2026+
case PlacementOptionSurvivalPreferences:
2027+
ctx.WriteKeyWord("SURVIVAL_PREFERENCES ")
2028+
ctx.WritePlain("= ")
2029+
ctx.WriteString(n.StrValue)
20252030
default:
20262031
return errors.Errorf("invalid PlacementOption: %d", n.Tp)
20272032
}

parser/misc.go

+1
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,7 @@ var tokenMap = map[string]int{
707707
"SUBSTRING": substring,
708708
"SUM": sum,
709709
"SUPER": super,
710+
"SURVIVAL_PREFERENCES": survivalPreferences,
710711
"SWAPS": swaps,
711712
"SWITCHES": switchesSym,
712713
"SYSTEM": system,

parser/model/model.go

+1
Original file line numberDiff line numberDiff line change
@@ -1735,6 +1735,7 @@ type PlacementSettings struct {
17351735
LearnerConstraints string `json:"learner_constraints"`
17361736
FollowerConstraints string `json:"follower_constraints"`
17371737
VoterConstraints string `json:"voter_constraints"`
1738+
SurvivalPreferences string `json:"survival_preferences"`
17381739
}
17391740

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

0 commit comments

Comments
 (0)