Unverified Commit 795b7da8 authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #65714 from resouer/fix-63784

Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. Re-design equivalence class cache to two level cache **What this PR does / why we need it**: The current ecache introduced a global lock across all the nodes, and this patch tried to assign ecache per node to eliminate that global lock. The improvement of scheduling performance and throughput are both significant. **CPU Profile Result** Machine: 32-core 60GB GCE VM 1k nodes 10k pods bench test (we've highlighted the critical function): 1. Current default scheduler with ecache enabled: ![equivlance class cache bench test 001](https://user-images.githubusercontent.com/1701782/42196992-51b0a32a-7eb3-11e8-89ee-f13383091a00.jpeg) 2. Current default scheduler with ecache disabled: ![equivlance class cache bench test 002](https://user-images.githubusercontent.com/1701782/42196993-51eb0c68-7eb3-11e8-9326-1a7762072863.jpeg) 3. Current default scheduler with this patch and ecache enabled: ![equivlance class cache bench test 003](https://user-images.githubusercontent.com/1701782/42196994-52280ed8-7eb3-11e8-8100-690e2af2cf2f.jpeg) **Throughput Test Result** 1k nodes 3k pods `scheduler_perf` test: Current default scheduler, ecache is disabled: ```bash Minimal observed throughput for 3k pod test: 200 PASS ok k8s.io/kubernetes/test/integration/scheduler_perf 30.091s ``` With this patch, ecache is enabled: ```bash Minimal observed throughput for 3k pod test: 556 PASS ok k8s.io/kubernetes/test/integration/scheduler_perf 11.119s ``` **Design and implementation:** The idea is: we re-designed ecache into a "two level cache". The first level cache holds the global lock across nodes and sync is needed only when node is added or deleted, which is of much lower frequency. The second level cache is assigned per node and its lock is restricted to per node level, thus there's no need to bother the global lock during whole predicate process cycle. For more detail, please check [the original discussion](https://github.com/kubernetes/kubernetes/issues/63784#issuecomment-399848349). **Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*: Fixes #63784 **Special notes for your reviewer**: ~~Tagged as WIP to make sure this does not break existing code and tests, we can start review after CI is happy.~~ **Release note**: ```release-note Re-design equivalence class cache to two level cache ```
parents 24fc97a8 e5a7a4ca
...@@ -52,7 +52,7 @@ type schedulerCache struct { ...@@ -52,7 +52,7 @@ type schedulerCache struct {
period time.Duration period time.Duration
// This mutex guards all fields within this cache struct. // This mutex guards all fields within this cache struct.
mu sync.Mutex mu sync.RWMutex
// a set of assumed pod keys. // a set of assumed pod keys.
// The key could further be used to get an entry in podStates. // The key could further be used to get an entry in podStates.
assumedPods map[string]bool assumedPods map[string]bool
...@@ -112,8 +112,8 @@ func newSchedulerCache(ttl, period time.Duration, stop <-chan struct{}) *schedul ...@@ -112,8 +112,8 @@ func newSchedulerCache(ttl, period time.Duration, stop <-chan struct{}) *schedul
// Snapshot takes a snapshot of the current schedulerCache. The method has performance impact, // Snapshot takes a snapshot of the current schedulerCache. The method has performance impact,
// and should be only used in non-critical path. // and should be only used in non-critical path.
func (cache *schedulerCache) Snapshot() *Snapshot { func (cache *schedulerCache) Snapshot() *Snapshot {
cache.mu.Lock() cache.mu.RLock()
defer cache.mu.Unlock() defer cache.mu.RUnlock()
nodes := make(map[string]*NodeInfo) nodes := make(map[string]*NodeInfo)
for k, v := range cache.nodes { for k, v := range cache.nodes {
...@@ -164,8 +164,8 @@ func (cache *schedulerCache) List(selector labels.Selector) ([]*v1.Pod, error) { ...@@ -164,8 +164,8 @@ func (cache *schedulerCache) List(selector labels.Selector) ([]*v1.Pod, error) {
} }
func (cache *schedulerCache) FilteredList(podFilter PodFilter, selector labels.Selector) ([]*v1.Pod, error) { func (cache *schedulerCache) FilteredList(podFilter PodFilter, selector labels.Selector) ([]*v1.Pod, error) {
cache.mu.Lock() cache.mu.RLock()
defer cache.mu.Unlock() defer cache.mu.RUnlock()
// podFilter is expected to return true for most or all of the pods. We // podFilter is expected to return true for most or all of the pods. We
// can avoid expensive array growth without wasting too much memory by // can avoid expensive array growth without wasting too much memory by
// pre-allocating capacity. // pre-allocating capacity.
...@@ -216,8 +216,8 @@ func (cache *schedulerCache) finishBinding(pod *v1.Pod, now time.Time) error { ...@@ -216,8 +216,8 @@ func (cache *schedulerCache) finishBinding(pod *v1.Pod, now time.Time) error {
return err return err
} }
cache.mu.Lock() cache.mu.RLock()
defer cache.mu.Unlock() defer cache.mu.RUnlock()
glog.V(5).Infof("Finished binding for pod %v. Can be expired.", key) glog.V(5).Infof("Finished binding for pod %v. Can be expired.", key)
currState, ok := cache.podStates[key] currState, ok := cache.podStates[key]
...@@ -387,8 +387,8 @@ func (cache *schedulerCache) IsAssumedPod(pod *v1.Pod) (bool, error) { ...@@ -387,8 +387,8 @@ func (cache *schedulerCache) IsAssumedPod(pod *v1.Pod) (bool, error) {
return false, err return false, err
} }
cache.mu.Lock() cache.mu.RLock()
defer cache.mu.Unlock() defer cache.mu.RUnlock()
b, found := cache.assumedPods[key] b, found := cache.assumedPods[key]
if !found { if !found {
...@@ -403,8 +403,8 @@ func (cache *schedulerCache) GetPod(pod *v1.Pod) (*v1.Pod, error) { ...@@ -403,8 +403,8 @@ func (cache *schedulerCache) GetPod(pod *v1.Pod) (*v1.Pod, error) {
return nil, err return nil, err
} }
cache.mu.Lock() cache.mu.RLock()
defer cache.mu.Unlock() defer cache.mu.RUnlock()
podState, ok := cache.podStates[key] podState, ok := cache.podStates[key]
if !ok { if !ok {
...@@ -539,8 +539,8 @@ func (cache *schedulerCache) RemovePDB(pdb *policy.PodDisruptionBudget) error { ...@@ -539,8 +539,8 @@ func (cache *schedulerCache) RemovePDB(pdb *policy.PodDisruptionBudget) error {
} }
func (cache *schedulerCache) ListPDBs(selector labels.Selector) ([]*policy.PodDisruptionBudget, error) { func (cache *schedulerCache) ListPDBs(selector labels.Selector) ([]*policy.PodDisruptionBudget, error) {
cache.mu.Lock() cache.mu.RLock()
defer cache.mu.Unlock() defer cache.mu.RUnlock()
var pdbs []*policy.PodDisruptionBudget var pdbs []*policy.PodDisruptionBudget
for _, pdb := range cache.pdbs { for _, pdb := range cache.pdbs {
if selector.Matches(labels.Set(pdb.Labels)) { if selector.Matches(labels.Set(pdb.Labels)) {
...@@ -551,8 +551,8 @@ func (cache *schedulerCache) ListPDBs(selector labels.Selector) ([]*policy.PodDi ...@@ -551,8 +551,8 @@ func (cache *schedulerCache) ListPDBs(selector labels.Selector) ([]*policy.PodDi
} }
func (cache *schedulerCache) IsUpToDate(n *NodeInfo) bool { func (cache *schedulerCache) IsUpToDate(n *NodeInfo) bool {
cache.mu.Lock() cache.mu.RLock()
defer cache.mu.Unlock() defer cache.mu.RUnlock()
node, ok := cache.nodes[n.Node().Name] node, ok := cache.nodes[n.Node().Name]
return ok && n.generation == node.generation return ok && n.generation == node.generation
} }
......
...@@ -243,17 +243,21 @@ func TestRunPredicate(t *testing.T) { ...@@ -243,17 +243,21 @@ func TestRunPredicate(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
node := schedulercache.NewNodeInfo() node := schedulercache.NewNodeInfo()
node.SetNode(&v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "n1"}}) testNode := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "n1"}}
node.SetNode(testNode)
pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "p1"}} pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "p1"}}
meta := algorithm.EmptyPredicateMetadataProducer(nil, nil) meta := algorithm.EmptyPredicateMetadataProducer(nil, nil)
// Initialize and populate equivalence class cache.
ecache := NewCache() ecache := NewCache()
nodeCache, _ := ecache.GetNodeCache(testNode.Name)
equivClass := NewClass(pod) equivClass := NewClass(pod)
if test.expectCacheHit { if test.expectCacheHit {
ecache.updateResult(pod.Name, "testPredicate", test.expectFit, test.expectedReasons, equivClass.hash, test.cache, node) nodeCache.updateResult(pod.Name, "testPredicate", test.expectFit, test.expectedReasons, equivClass.hash, test.cache, node)
} }
fit, reasons, err := ecache.RunPredicate(test.pred.predicate, "testPredicate", pod, meta, node, equivClass, test.cache) fit, reasons, err := nodeCache.RunPredicate(test.pred.predicate, "testPredicate", pod, meta, node, equivClass, test.cache)
if err != nil { if err != nil {
if err.Error() != test.expectedError { if err.Error() != test.expectedError {
...@@ -284,7 +288,7 @@ func TestRunPredicate(t *testing.T) { ...@@ -284,7 +288,7 @@ func TestRunPredicate(t *testing.T) {
if !test.expectCacheHit && test.pred.callCount == 0 { if !test.expectCacheHit && test.pred.callCount == 0 {
t.Errorf("Predicate should be called") t.Errorf("Predicate should be called")
} }
_, ok := ecache.lookupResult(pod.Name, node.Node().Name, "testPredicate", equivClass.hash) _, ok := nodeCache.lookupResult(pod.Name, node.Node().Name, "testPredicate", equivClass.hash)
if !ok && test.expectCacheWrite { if !ok && test.expectCacheWrite {
t.Errorf("Cache write should happen") t.Errorf("Cache write should happen")
} }
...@@ -339,21 +343,25 @@ func TestUpdateResult(t *testing.T) { ...@@ -339,21 +343,25 @@ func TestUpdateResult(t *testing.T) {
}, },
} }
for _, test := range tests { for _, test := range tests {
node := schedulercache.NewNodeInfo()
testNode := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: test.nodeName}}
node.SetNode(testNode)
// Initialize and populate equivalence class cache.
ecache := NewCache() ecache := NewCache()
nodeCache, _ := ecache.GetNodeCache(testNode.Name)
if test.expectPredicateMap { if test.expectPredicateMap {
ecache.cache[test.nodeName] = make(predicateMap)
predicateItem := predicateResult{ predicateItem := predicateResult{
Fit: true, Fit: true,
} }
ecache.cache[test.nodeName][test.predicateKey] = nodeCache.cache[test.predicateKey] =
resultMap{ resultMap{
test.equivalenceHash: predicateItem, test.equivalenceHash: predicateItem,
} }
} }
node := schedulercache.NewNodeInfo() nodeCache.updateResult(
node.SetNode(&v1.Node{ObjectMeta: metav1.ObjectMeta{Name: test.nodeName}})
ecache.updateResult(
test.pod, test.pod,
test.predicateKey, test.predicateKey,
test.fit, test.fit,
...@@ -363,7 +371,7 @@ func TestUpdateResult(t *testing.T) { ...@@ -363,7 +371,7 @@ func TestUpdateResult(t *testing.T) {
node, node,
) )
cachedMapItem, ok := ecache.cache[test.nodeName][test.predicateKey] cachedMapItem, ok := nodeCache.cache[test.predicateKey]
if !ok { if !ok {
t.Errorf("Failed: %s, can't find expected cache item: %v", t.Errorf("Failed: %s, can't find expected cache item: %v",
test.name, test.expectCacheItem) test.name, test.expectCacheItem)
...@@ -473,11 +481,16 @@ func TestLookupResult(t *testing.T) { ...@@ -473,11 +481,16 @@ func TestLookupResult(t *testing.T) {
} }
for _, test := range tests { for _, test := range tests {
testNode := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: test.nodeName}}
// Initialize and populate equivalence class cache.
ecache := NewCache() ecache := NewCache()
nodeCache, _ := ecache.GetNodeCache(testNode.Name)
node := schedulercache.NewNodeInfo() node := schedulercache.NewNodeInfo()
node.SetNode(&v1.Node{ObjectMeta: metav1.ObjectMeta{Name: test.nodeName}}) node.SetNode(testNode)
// set cached item to equivalence cache // set cached item to equivalence cache
ecache.updateResult( nodeCache.updateResult(
test.podName, test.podName,
test.predicateKey, test.predicateKey,
test.cachedItem.fit, test.cachedItem.fit,
...@@ -493,7 +506,7 @@ func TestLookupResult(t *testing.T) { ...@@ -493,7 +506,7 @@ func TestLookupResult(t *testing.T) {
ecache.InvalidatePredicatesOnNode(test.nodeName, predicateKeys) ecache.InvalidatePredicatesOnNode(test.nodeName, predicateKeys)
} }
// calculate predicate with equivalence cache // calculate predicate with equivalence cache
result, ok := ecache.lookupResult(test.podName, result, ok := nodeCache.lookupResult(test.podName,
test.nodeName, test.nodeName,
test.predicateKey, test.predicateKey,
test.equivalenceHashForCalPredicate, test.equivalenceHashForCalPredicate,
...@@ -689,9 +702,12 @@ func TestInvalidateCachedPredicateItemOfAllNodes(t *testing.T) { ...@@ -689,9 +702,12 @@ func TestInvalidateCachedPredicateItemOfAllNodes(t *testing.T) {
for _, test := range tests { for _, test := range tests {
node := schedulercache.NewNodeInfo() node := schedulercache.NewNodeInfo()
node.SetNode(&v1.Node{ObjectMeta: metav1.ObjectMeta{Name: test.nodeName}}) testNode := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: test.nodeName}}
node.SetNode(testNode)
nodeCache, _ := ecache.GetNodeCache(testNode.Name)
// set cached item to equivalence cache // set cached item to equivalence cache
ecache.updateResult( nodeCache.updateResult(
test.podName, test.podName,
testPredicate, testPredicate,
test.cachedItem.fit, test.cachedItem.fit,
...@@ -707,8 +723,8 @@ func TestInvalidateCachedPredicateItemOfAllNodes(t *testing.T) { ...@@ -707,8 +723,8 @@ func TestInvalidateCachedPredicateItemOfAllNodes(t *testing.T) {
// there should be no cached predicate any more // there should be no cached predicate any more
for _, test := range tests { for _, test := range tests {
if algorithmCache, exist := ecache.cache[test.nodeName]; exist { if nodeCache, exist := ecache.nodeToCache[test.nodeName]; exist {
if _, exist := algorithmCache[testPredicate]; exist { if _, exist := nodeCache.cache[testPredicate]; exist {
t.Errorf("Failed: cached item for predicate key: %v on node: %v should be invalidated", t.Errorf("Failed: cached item for predicate key: %v on node: %v should be invalidated",
testPredicate, test.nodeName) testPredicate, test.nodeName)
break break
...@@ -761,9 +777,12 @@ func TestInvalidateAllCachedPredicateItemOfNode(t *testing.T) { ...@@ -761,9 +777,12 @@ func TestInvalidateAllCachedPredicateItemOfNode(t *testing.T) {
for _, test := range tests { for _, test := range tests {
node := schedulercache.NewNodeInfo() node := schedulercache.NewNodeInfo()
node.SetNode(&v1.Node{ObjectMeta: metav1.ObjectMeta{Name: test.nodeName}}) testNode := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: test.nodeName}}
node.SetNode(testNode)
nodeCache, _ := ecache.GetNodeCache(testNode.Name)
// set cached item to equivalence cache // set cached item to equivalence cache
ecache.updateResult( nodeCache.updateResult(
test.podName, test.podName,
testPredicate, testPredicate,
test.cachedItem.fit, test.cachedItem.fit,
...@@ -775,10 +794,10 @@ func TestInvalidateAllCachedPredicateItemOfNode(t *testing.T) { ...@@ -775,10 +794,10 @@ func TestInvalidateAllCachedPredicateItemOfNode(t *testing.T) {
} }
for _, test := range tests { for _, test := range tests {
// invalidate cached predicate for all nodes // invalidate all cached predicate for node
ecache.InvalidateAllPredicatesOnNode(test.nodeName) ecache.InvalidateAllPredicatesOnNode(test.nodeName)
if _, exist := ecache.cache[test.nodeName]; exist { if _, ok := ecache.GetNodeCache(test.nodeName); ok {
t.Errorf("Failed: cached item for node: %v should be invalidated", test.nodeName) t.Errorf("Failed: node: %v should not be found in internal cache", test.nodeName)
break break
} }
} }
......
...@@ -356,20 +356,25 @@ func (g *genericScheduler) findNodesThatFit(pod *v1.Pod, nodes []*v1.Node) ([]*v ...@@ -356,20 +356,25 @@ func (g *genericScheduler) findNodesThatFit(pod *v1.Pod, nodes []*v1.Node) ([]*v
meta := g.predicateMetaProducer(pod, g.cachedNodeInfoMap) meta := g.predicateMetaProducer(pod, g.cachedNodeInfoMap)
var equivClass *equivalence.Class var equivClass *equivalence.Class
if g.equivalenceCache != nil { if g.equivalenceCache != nil {
// getEquivalenceClassInfo will return immediately if no equivalence pod found // getEquivalenceClassInfo will return immediately if no equivalence pod found
equivClass = equivalence.NewClass(pod) equivClass = equivalence.NewClass(pod)
} }
checkNode := func(i int) { checkNode := func(i int) {
var nodeCache *equivalence.NodeCache
nodeName := nodes[i].Name nodeName := nodes[i].Name
if g.equivalenceCache != nil {
nodeCache, _ = g.equivalenceCache.GetNodeCache(nodeName)
}
fits, failedPredicates, err := podFitsOnNode( fits, failedPredicates, err := podFitsOnNode(
pod, pod,
meta, meta,
g.cachedNodeInfoMap[nodeName], g.cachedNodeInfoMap[nodeName],
g.predicates, g.predicates,
g.cache, g.cache,
g.equivalenceCache, nodeCache,
g.schedulingQueue, g.schedulingQueue,
g.alwaysCheckAllPredicates, g.alwaysCheckAllPredicates,
equivClass, equivClass,
...@@ -472,7 +477,7 @@ func podFitsOnNode( ...@@ -472,7 +477,7 @@ func podFitsOnNode(
info *schedulercache.NodeInfo, info *schedulercache.NodeInfo,
predicateFuncs map[string]algorithm.FitPredicate, predicateFuncs map[string]algorithm.FitPredicate,
cache schedulercache.Cache, cache schedulercache.Cache,
ecache *equivalence.Cache, nodeCache *equivalence.NodeCache,
queue SchedulingQueue, queue SchedulingQueue,
alwaysCheckAllPredicates bool, alwaysCheckAllPredicates bool,
equivClass *equivalence.Class, equivClass *equivalence.Class,
...@@ -512,7 +517,7 @@ func podFitsOnNode( ...@@ -512,7 +517,7 @@ func podFitsOnNode(
// Bypass eCache if node has any nominated pods. // Bypass eCache if node has any nominated pods.
// TODO(bsalamat): consider using eCache and adding proper eCache invalidations // TODO(bsalamat): consider using eCache and adding proper eCache invalidations
// when pods are nominated or their nominations change. // when pods are nominated or their nominations change.
eCacheAvailable = equivClass != nil && !podsAdded eCacheAvailable = equivClass != nil && nodeCache != nil && !podsAdded
for _, predicateKey := range predicates.Ordering() { for _, predicateKey := range predicates.Ordering() {
var ( var (
fit bool fit bool
...@@ -522,7 +527,7 @@ func podFitsOnNode( ...@@ -522,7 +527,7 @@ func podFitsOnNode(
//TODO (yastij) : compute average predicate restrictiveness to export it as Prometheus metric //TODO (yastij) : compute average predicate restrictiveness to export it as Prometheus metric
if predicate, exist := predicateFuncs[predicateKey]; exist { if predicate, exist := predicateFuncs[predicateKey]; exist {
if eCacheAvailable { if eCacheAvailable {
fit, reasons, err = ecache.RunPredicate(predicate, predicateKey, pod, metaToUse, nodeInfoToUse, equivClass, cache) fit, reasons, err = nodeCache.RunPredicate(predicate, predicateKey, pod, metaToUse, nodeInfoToUse, equivClass, cache)
} else { } else {
fit, reasons, err = predicate(pod, metaToUse, nodeInfoToUse) fit, reasons, err = predicate(pod, metaToUse, nodeInfoToUse)
} }
......
...@@ -1405,7 +1405,8 @@ func TestCacheInvalidationRace(t *testing.T) { ...@@ -1405,7 +1405,8 @@ func TestCacheInvalidationRace(t *testing.T) {
// Set up the mock cache. // Set up the mock cache.
cache := schedulercache.New(time.Duration(0), wait.NeverStop) cache := schedulercache.New(time.Duration(0), wait.NeverStop)
cache.AddNode(&v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "machine1"}}) testNode := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "machine1"}}
cache.AddNode(testNode)
mockCache := &syncingMockCache{ mockCache := &syncingMockCache{
Cache: cache, Cache: cache,
cycleStart: make(chan struct{}), cycleStart: make(chan struct{}),
......
...@@ -772,6 +772,11 @@ func (c *configFactory) addNodeToCache(obj interface{}) { ...@@ -772,6 +772,11 @@ func (c *configFactory) addNodeToCache(obj interface{}) {
glog.Errorf("scheduler cache AddNode failed: %v", err) glog.Errorf("scheduler cache AddNode failed: %v", err)
} }
if c.enableEquivalenceClassCache {
// GetNodeCache() will lazily create NodeCache for given node if it does not exist.
c.equivalencePodCache.GetNodeCache(node.GetName())
}
c.podQueue.MoveAllToActiveQueue() c.podQueue.MoveAllToActiveQueue()
// NOTE: add a new node does not affect existing predicates in equivalence cache // NOTE: add a new node does not affect existing predicates in equivalence cache
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment