Skip to content

Commit a625a04

Browse files
authored
Merge pull request kubernetes#114051 from chrishenzie/rwop-preemption
[scheduler] Support preemption of pods using ReadWriteOncePod PVCs
2 parents 710ab59 + d2af38a commit a625a04

4 files changed

Lines changed: 598 additions & 52 deletions

File tree

pkg/scheduler/framework/plugins/volumerestrictions/volume_restrictions.go

Lines changed: 165 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package volumerestrictions
1818

1919
import (
2020
"context"
21+
"fmt"
2122

2223
v1 "k8s.io/api/core/v1"
2324
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -40,17 +41,57 @@ type VolumeRestrictions struct {
4041
var _ framework.PreFilterPlugin = &VolumeRestrictions{}
4142
var _ framework.FilterPlugin = &VolumeRestrictions{}
4243
var _ framework.EnqueueExtensions = &VolumeRestrictions{}
43-
44-
// Name is the name of the plugin used in the plugin registry and configurations.
45-
const Name = names.VolumeRestrictions
44+
var _ framework.StateData = &preFilterState{}
4645

4746
const (
47+
// Name is the name of the plugin used in the plugin registry and configurations.
48+
Name = names.VolumeRestrictions
49+
// preFilterStateKey is the key in CycleState to VolumeRestrictions pre-computed data for Filtering.
50+
// Using the name of the plugin will likely help us avoid collisions with other plugins.
51+
preFilterStateKey = "PreFilter" + Name
52+
4853
// ErrReasonDiskConflict is used for NoDiskConflict predicate error.
4954
ErrReasonDiskConflict = "node(s) had no available disk"
5055
// ErrReasonReadWriteOncePodConflict is used when a pod is found using the same PVC with the ReadWriteOncePod access mode.
5156
ErrReasonReadWriteOncePodConflict = "node has pod using PersistentVolumeClaim with the same name and ReadWriteOncePod access mode"
5257
)
5358

59+
// preFilterState computed at PreFilter and used at Filter.
60+
type preFilterState struct {
61+
// Names of the pod's volumes using the ReadWriteOncePod access mode.
62+
readWriteOncePodPVCs sets.Set[string]
63+
// The number of references to these ReadWriteOncePod volumes by scheduled pods.
64+
conflictingPVCRefCount int
65+
}
66+
67+
func (s *preFilterState) updateWithPod(podInfo *framework.PodInfo, multiplier int) {
68+
s.conflictingPVCRefCount += multiplier * s.conflictingPVCRefCountForPod(podInfo)
69+
}
70+
71+
func (s *preFilterState) conflictingPVCRefCountForPod(podInfo *framework.PodInfo) int {
72+
conflicts := 0
73+
for _, volume := range podInfo.Pod.Spec.Volumes {
74+
if volume.PersistentVolumeClaim == nil {
75+
continue
76+
}
77+
if s.readWriteOncePodPVCs.Has(volume.PersistentVolumeClaim.ClaimName) {
78+
conflicts += 1
79+
}
80+
}
81+
return conflicts
82+
}
83+
84+
// Clone the prefilter state.
85+
func (s *preFilterState) Clone() framework.StateData {
86+
if s == nil {
87+
return nil
88+
}
89+
return &preFilterState{
90+
readWriteOncePodPVCs: s.readWriteOncePodPVCs,
91+
conflictingPVCRefCount: s.conflictingPVCRefCount,
92+
}
93+
}
94+
5495
// Name returns name of the plugin. It is used in logs, etc.
5596
func (pl *VolumeRestrictions) Name() string {
5697
return Name
@@ -117,46 +158,138 @@ func haveOverlap(a1, a2 []string) bool {
117158
return false
118159
}
119160

161+
// PreFilter computes and stores cycleState containing details for enforcing ReadWriteOncePod.
120162
func (pl *VolumeRestrictions) PreFilter(ctx context.Context, cycleState *framework.CycleState, pod *v1.Pod) (*framework.PreFilterResult, *framework.Status) {
121-
if pl.enableReadWriteOncePod {
122-
return nil, pl.isReadWriteOncePodAccessModeConflict(ctx, pod)
163+
if !pl.enableReadWriteOncePod {
164+
return nil, nil
165+
}
166+
167+
pvcs, err := pl.readWriteOncePodPVCsForPod(ctx, pod)
168+
if err != nil {
169+
if apierrors.IsNotFound(err) {
170+
return nil, framework.NewStatus(framework.UnschedulableAndUnresolvable, err.Error())
171+
}
172+
return nil, framework.AsStatus(err)
173+
}
174+
175+
s, err := pl.calPreFilterState(ctx, pod, pvcs)
176+
if err != nil {
177+
return nil, framework.AsStatus(err)
178+
}
179+
cycleState.Write(preFilterStateKey, s)
180+
return nil, nil
181+
}
182+
183+
// AddPod from pre-computed data in cycleState.
184+
func (pl *VolumeRestrictions) AddPod(ctx context.Context, cycleState *framework.CycleState, podToSchedule *v1.Pod, podInfoToAdd *framework.PodInfo, nodeInfo *framework.NodeInfo) *framework.Status {
185+
if !pl.enableReadWriteOncePod {
186+
return nil
187+
}
188+
state, err := getPreFilterState(cycleState)
189+
if err != nil {
190+
return framework.AsStatus(err)
123191
}
124-
return nil, framework.NewStatus(framework.Success)
192+
state.updateWithPod(podInfoToAdd, 1)
193+
return nil
125194
}
126195

127-
// isReadWriteOncePodAccessModeConflict checks if a pod uses a PVC with the ReadWriteOncePod access mode.
128-
// This access mode restricts volume access to a single pod on a single node. Since only a single pod can
129-
// use a ReadWriteOncePod PVC, mark any other pods attempting to use this PVC as UnschedulableAndUnresolvable.
130-
// TODO(#103132): Mark pod as Unschedulable and add preemption logic.
131-
func (pl *VolumeRestrictions) isReadWriteOncePodAccessModeConflict(ctx context.Context, pod *v1.Pod) *framework.Status {
196+
// RemovePod from pre-computed data in cycleState.
197+
func (pl *VolumeRestrictions) RemovePod(ctx context.Context, cycleState *framework.CycleState, podToSchedule *v1.Pod, podInfoToRemove *framework.PodInfo, nodeInfo *framework.NodeInfo) *framework.Status {
198+
if !pl.enableReadWriteOncePod {
199+
return nil
200+
}
201+
state, err := getPreFilterState(cycleState)
202+
if err != nil {
203+
return framework.AsStatus(err)
204+
}
205+
state.updateWithPod(podInfoToRemove, -1)
206+
return nil
207+
}
208+
209+
func getPreFilterState(cycleState *framework.CycleState) (*preFilterState, error) {
210+
c, err := cycleState.Read(preFilterStateKey)
211+
if err != nil {
212+
// preFilterState doesn't exist, likely PreFilter wasn't invoked.
213+
return nil, fmt.Errorf("cannot read %q from cycleState", preFilterStateKey)
214+
}
215+
216+
s, ok := c.(*preFilterState)
217+
if !ok {
218+
return nil, fmt.Errorf("%+v convert to volumerestrictions.state error", c)
219+
}
220+
return s, nil
221+
}
222+
223+
// calPreFilterState computes preFilterState describing which PVCs use ReadWriteOncePod
224+
// and which pods in the cluster are in conflict.
225+
func (pl *VolumeRestrictions) calPreFilterState(ctx context.Context, pod *v1.Pod, pvcs sets.Set[string]) (*preFilterState, error) {
226+
conflictingPVCRefCount := 0
227+
for pvc := range pvcs {
228+
key := framework.GetNamespacedName(pod.Namespace, pvc)
229+
if pl.sharedLister.StorageInfos().IsPVCUsedByPods(key) {
230+
// There can only be at most one pod using the ReadWriteOncePod PVC.
231+
conflictingPVCRefCount += 1
232+
}
233+
}
234+
return &preFilterState{
235+
readWriteOncePodPVCs: pvcs,
236+
conflictingPVCRefCount: conflictingPVCRefCount,
237+
}, nil
238+
}
239+
240+
func (pl *VolumeRestrictions) readWriteOncePodPVCsForPod(ctx context.Context, pod *v1.Pod) (sets.Set[string], error) {
241+
pvcs := sets.New[string]()
132242
for _, volume := range pod.Spec.Volumes {
133243
if volume.PersistentVolumeClaim == nil {
134244
continue
135245
}
136246

137247
pvc, err := pl.pvcLister.PersistentVolumeClaims(pod.Namespace).Get(volume.PersistentVolumeClaim.ClaimName)
138248
if err != nil {
139-
if apierrors.IsNotFound(err) {
140-
return framework.NewStatus(framework.UnschedulableAndUnresolvable, err.Error())
141-
}
142-
return framework.AsStatus(err)
249+
return nil, err
143250
}
144251

145252
if !v1helper.ContainsAccessMode(pvc.Spec.AccessModes, v1.ReadWriteOncePod) {
146253
continue
147254
}
255+
pvcs.Insert(pvc.Name)
256+
}
257+
return pvcs, nil
258+
}
148259

149-
key := framework.GetNamespacedName(pod.Namespace, volume.PersistentVolumeClaim.ClaimName)
150-
if pl.sharedLister.StorageInfos().IsPVCUsedByPods(key) {
151-
return framework.NewStatus(framework.UnschedulableAndUnresolvable, ErrReasonReadWriteOncePodConflict)
260+
// Checks if scheduling the pod onto this node would cause any conflicts with
261+
// existing volumes.
262+
func satisfyVolumeConflicts(pod *v1.Pod, nodeInfo *framework.NodeInfo) bool {
263+
for i := range pod.Spec.Volumes {
264+
v := &pod.Spec.Volumes[i]
265+
// fast path if there is no conflict checking targets.
266+
if v.GCEPersistentDisk == nil && v.AWSElasticBlockStore == nil && v.RBD == nil && v.ISCSI == nil {
267+
continue
268+
}
269+
270+
for _, ev := range nodeInfo.Pods {
271+
if isVolumeConflict(v, ev.Pod) {
272+
return false
273+
}
152274
}
153275
}
276+
return true
277+
}
154278

279+
// Checks if scheduling the pod would cause any ReadWriteOncePod PVC access mode conflicts.
280+
func satisfyReadWriteOncePod(ctx context.Context, state *preFilterState) *framework.Status {
281+
if state == nil {
282+
return nil
283+
}
284+
if state.conflictingPVCRefCount > 0 {
285+
return framework.NewStatus(framework.Unschedulable, ErrReasonReadWriteOncePodConflict)
286+
}
155287
return nil
156288
}
157289

290+
// PreFilterExtensions returns prefilter extensions, pod add and remove.
158291
func (pl *VolumeRestrictions) PreFilterExtensions() framework.PreFilterExtensions {
159-
return nil
292+
return pl
160293
}
161294

162295
// Filter invoked at the filter extension point.
@@ -168,21 +301,20 @@ func (pl *VolumeRestrictions) PreFilterExtensions() framework.PreFilterExtension
168301
// - AWS EBS forbids any two pods mounting the same volume ID
169302
// - Ceph RBD forbids if any two pods share at least same monitor, and match pool and image, and the image is read-only
170303
// - ISCSI forbids if any two pods share at least same IQN and ISCSI volume is read-only
171-
func (pl *VolumeRestrictions) Filter(ctx context.Context, _ *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
172-
for i := range pod.Spec.Volumes {
173-
v := &pod.Spec.Volumes[i]
174-
// fast path if there is no conflict checking targets.
175-
if v.GCEPersistentDisk == nil && v.AWSElasticBlockStore == nil && v.RBD == nil && v.ISCSI == nil {
176-
continue
177-
}
178-
179-
for _, ev := range nodeInfo.Pods {
180-
if isVolumeConflict(v, ev.Pod) {
181-
return framework.NewStatus(framework.Unschedulable, ErrReasonDiskConflict)
182-
}
183-
}
304+
// If the pod uses PVCs with the ReadWriteOncePod access mode, it evaluates if
305+
// these PVCs are already in-use and if preemption will help.
306+
func (pl *VolumeRestrictions) Filter(ctx context.Context, cycleState *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
307+
if !satisfyVolumeConflicts(pod, nodeInfo) {
308+
return framework.NewStatus(framework.Unschedulable, ErrReasonDiskConflict)
184309
}
185-
return nil
310+
if !pl.enableReadWriteOncePod {
311+
return nil
312+
}
313+
state, err := getPreFilterState(cycleState)
314+
if err != nil {
315+
return framework.AsStatus(err)
316+
}
317+
return satisfyReadWriteOncePod(ctx, state)
186318
}
187319

188320
// EventsToRegister returns the possible events that may make a Pod

0 commit comments

Comments
 (0)