Fill partial pages on the server rather than forcing client to

The etcd3 storage now attempts to fill partial pages to prevent clients having to make more round trips (latency from server to etcd is lower than client to server). The server makes repeated requests to etcd of the current page size, then uses the filter function to eliminate any matches. After this change the apiserver will always return full pages, but we leave the language in place that clients must tolerate it. Reduces tail latency of large filtered lists, such as viewing pods assigned to a node.
parent 113889e7
...@@ -396,11 +396,11 @@ func (s *store) GetToList(ctx context.Context, key string, resourceVersion strin ...@@ -396,11 +396,11 @@ func (s *store) GetToList(ctx context.Context, key string, resourceVersion strin
if err != nil { if err != nil {
return storage.NewInternalError(err.Error()) return storage.NewInternalError(err.Error())
} }
elems := []*elemForDecode{{ v, err := conversion.EnforcePtr(listPtr)
data: data, if err != nil || v.Kind() != reflect.Slice {
rev: uint64(getResp.Kvs[0].ModRevision), panic("need ptr to slice")
}} }
if err := decodeList(elems, storage.SimpleFilter(pred), listPtr, s.codec, s.versioner); err != nil { if err := appendListItem(v, data, uint64(getResp.Kvs[0].ModRevision), storage.SimpleFilter(pred), s.codec, s.versioner); err != nil {
return err return err
} }
// update version with cluster level revision // update version with cluster level revision
...@@ -472,7 +472,14 @@ func (s *store) List(ctx context.Context, key, resourceVersion string, pred stor ...@@ -472,7 +472,14 @@ func (s *store) List(ctx context.Context, key, resourceVersion string, pred stor
if err != nil { if err != nil {
return err return err
} }
key = path.Join(s.pathPrefix, key) v, err := conversion.EnforcePtr(listPtr)
if err != nil || v.Kind() != reflect.Slice {
panic("need ptr to slice")
}
if s.pathPrefix != "" {
key = path.Join(s.pathPrefix, key)
}
// We need to make sure the key ended with "/" so that we only get children "directories". // We need to make sure the key ended with "/" so that we only get children "directories".
// e.g. if we have key "/a", "/a/b", "/ab", getting keys with prefix "/a" will return all three, // e.g. if we have key "/a", "/a/b", "/ab", getting keys with prefix "/a" will return all three,
// while with prefix "/a/" will return only "/a/b" which is the correct answer. // while with prefix "/a/" will return only "/a/b" which is the correct answer.
...@@ -481,81 +488,127 @@ func (s *store) List(ctx context.Context, key, resourceVersion string, pred stor ...@@ -481,81 +488,127 @@ func (s *store) List(ctx context.Context, key, resourceVersion string, pred stor
} }
keyPrefix := key keyPrefix := key
filter := storage.SimpleFilter(pred)
// set the appropriate clientv3 options to filter the returned data set // set the appropriate clientv3 options to filter the returned data set
var paging bool
options := make([]clientv3.OpOption, 0, 4) options := make([]clientv3.OpOption, 0, 4)
if s.pagingEnabled && pred.Limit > 0 { if s.pagingEnabled && pred.Limit > 0 {
paging = true
options = append(options, clientv3.WithLimit(pred.Limit)) options = append(options, clientv3.WithLimit(pred.Limit))
} }
var returnedRV int64 var returnedRV int64
switch { switch {
case s.pagingEnabled && len(pred.Continue) > 0: case s.pagingEnabled && len(pred.Continue) > 0:
continueKey, continueRV, err := decodeContinue(pred.Continue, keyPrefix) continueKey, continueRV, err := decodeContinue(pred.Continue, keyPrefix)
if err != nil { if err != nil {
return err return apierrors.NewBadRequest(fmt.Sprintf("invalid continue token: %v", err))
}
if len(resourceVersion) > 0 && resourceVersion != "0" {
return apierrors.NewBadRequest("specifying resource version is not allowed when using continue")
} }
options = append(options, clientv3.WithRange(clientv3.GetPrefixRangeEnd(key))) rangeEnd := clientv3.GetPrefixRangeEnd(keyPrefix)
options = append(options, clientv3.WithRange(rangeEnd))
key = continueKey key = continueKey
options = append(options, clientv3.WithRev(continueRV)) options = append(options, clientv3.WithRev(continueRV))
returnedRV = continueRV returnedRV = continueRV
case len(resourceVersion) > 0: case s.pagingEnabled && pred.Limit > 0:
fromRV, err := strconv.ParseInt(resourceVersion, 10, 64) if len(resourceVersion) > 0 {
if err != nil { fromRV, err := strconv.ParseInt(resourceVersion, 10, 64)
return fmt.Errorf("invalid resource version: %v", err) if err != nil {
return apierrors.NewBadRequest(fmt.Sprintf("invalid resource version: %v", err))
}
if fromRV > 0 {
options = append(options, clientv3.WithRev(fromRV))
}
returnedRV = fromRV
} }
options = append(options, clientv3.WithPrefix(), clientv3.WithRev(fromRV)) rangeEnd := clientv3.GetPrefixRangeEnd(keyPrefix)
returnedRV = fromRV options = append(options, clientv3.WithRange(rangeEnd))
default: default:
options = append(options, clientv3.WithPrefix()) if len(resourceVersion) > 0 {
} fromRV, err := strconv.ParseInt(resourceVersion, 10, 64)
if err != nil {
return apierrors.NewBadRequest(fmt.Sprintf("invalid resource version: %v", err))
}
if fromRV > 0 {
options = append(options, clientv3.WithRev(fromRV))
}
returnedRV = fromRV
}
getResp, err := s.client.KV.Get(ctx, key, options...) options = append(options, clientv3.WithPrefix())
if err != nil {
return interpretListError(err, len(pred.Continue) > 0)
} }
elems := make([]*elemForDecode, 0, len(getResp.Kvs)) // loop until we have filled the requested limit from etcd or there are no more results
for _, kv := range getResp.Kvs { var lastKey []byte
data, _, err := s.transformer.TransformFromStorage(kv.Value, authenticatedDataString(kv.Key)) var hasMore bool
for {
getResp, err := s.client.KV.Get(ctx, key, options...)
if err != nil { if err != nil {
utilruntime.HandleError(fmt.Errorf("unable to transform key %q: %v", kv.Key, err)) return interpretListError(err, len(pred.Continue) > 0)
continue
} }
hasMore = getResp.More
elems = append(elems, &elemForDecode{ if len(getResp.Kvs) == 0 && getResp.More {
data: data, return fmt.Errorf("no results were found, but etcd indicated there were more values remaining")
rev: uint64(kv.ModRevision), }
})
}
if err := decodeList(elems, storage.SimpleFilter(pred), listPtr, s.codec, s.versioner); err != nil { // take items from the response until the bucket is full, filtering as we go
return err for _, kv := range getResp.Kvs {
} if paging && int64(v.Len()) >= pred.Limit {
hasMore = true
break
}
lastKey = kv.Key
data, _, err := s.transformer.TransformFromStorage(kv.Value, authenticatedDataString(kv.Key))
if err != nil {
utilruntime.HandleError(fmt.Errorf("unable to transform key %q: %v", kv.Key, err))
continue
}
// indicate to the client which resource version was returned if err := appendListItem(v, data, uint64(kv.ModRevision), filter, s.codec, s.versioner); err != nil {
if returnedRV == 0 { return err
returnedRV = getResp.Header.Revision }
}
// indicate to the client which resource version was returned
if returnedRV == 0 {
returnedRV = getResp.Header.Revision
}
// no more results remain or we didn't request paging
if !hasMore || !paging {
break
}
// we're paging but we have filled our bucket
if int64(v.Len()) >= pred.Limit {
break
}
key = string(lastKey) + "\x00"
} }
switch {
case !getResp.More: // instruct the client to begin querying from immediately after the last key we returned
// no continuation // we never return a key that the client wouldn't be allowed to see
return s.versioner.UpdateList(listObj, uint64(returnedRV), "") if hasMore {
case len(getResp.Kvs) == 0:
return fmt.Errorf("no results were found, but etcd indicated there were more values")
default:
// we want to start immediately after the last key // we want to start immediately after the last key
// TODO: this reveals info about certain keys next, err := encodeContinue(string(lastKey)+"\x00", keyPrefix, returnedRV)
key := string(getResp.Kvs[len(getResp.Kvs)-1].Key)
next, err := encodeContinue(key+"\x00", keyPrefix, returnedRV)
if err != nil { if err != nil {
return err return err
} }
return s.versioner.UpdateList(listObj, uint64(returnedRV), next) return s.versioner.UpdateList(listObj, uint64(returnedRV), next)
} }
// no continuation
return s.versioner.UpdateList(listObj, uint64(returnedRV), "")
} }
// Watch implements storage.Interface.Watch. // Watch implements storage.Interface.Watch.
...@@ -677,23 +730,16 @@ func decode(codec runtime.Codec, versioner storage.Versioner, value []byte, objP ...@@ -677,23 +730,16 @@ func decode(codec runtime.Codec, versioner storage.Versioner, value []byte, objP
return nil return nil
} }
// decodeList decodes a list of values into a list of objects, with resource version set to corresponding rev. // appendListItem decodes and appends the object (if it passes filter) to v, which must be a slice.
// On success, ListPtr would be set to the list of objects. func appendListItem(v reflect.Value, data []byte, rev uint64, filter storage.FilterFunc, codec runtime.Codec, versioner storage.Versioner) error {
func decodeList(elems []*elemForDecode, filter storage.FilterFunc, listPtr interface{}, codec runtime.Codec, versioner storage.Versioner) error { obj, _, err := codec.Decode(data, nil, reflect.New(v.Type().Elem()).Interface().(runtime.Object))
v, err := conversion.EnforcePtr(listPtr) if err != nil {
if err != nil || v.Kind() != reflect.Slice { return err
panic("need ptr to slice")
} }
for _, elem := range elems { // being unable to set the version does not prevent the object from being extracted
obj, _, err := codec.Decode(elem.data, nil, reflect.New(v.Type().Elem()).Interface().(runtime.Object)) versioner.UpdateObject(obj, rev)
if err != nil { if filter(obj) {
return err v.Set(reflect.Append(v, reflect.ValueOf(obj).Elem()))
}
// being unable to set the version does not prevent the object from being extracted
versioner.UpdateObject(obj, elem.rev)
if filter(obj) {
v.Set(reflect.Append(v, reflect.ValueOf(obj).Elem()))
}
} }
return nil return nil
} }
......
...@@ -9,6 +9,7 @@ go_library( ...@@ -9,6 +9,7 @@ go_library(
name = "go_default_library", name = "go_default_library",
srcs = [ srcs = [
"aggregator.go", "aggregator.go",
"chunking.go",
"custom_resource_definition.go", "custom_resource_definition.go",
"etcd_failure.go", "etcd_failure.go",
"framework.go", "framework.go",
...@@ -58,6 +59,7 @@ go_library( ...@@ -58,6 +59,7 @@ go_library(
"//vendor/k8s.io/client-go/kubernetes:go_default_library", "//vendor/k8s.io/client-go/kubernetes:go_default_library",
"//vendor/k8s.io/client-go/util/cert:go_default_library", "//vendor/k8s.io/client-go/util/cert:go_default_library",
"//vendor/k8s.io/client-go/util/retry:go_default_library", "//vendor/k8s.io/client-go/util/retry:go_default_library",
"//vendor/k8s.io/client-go/util/workqueue:go_default_library",
"//vendor/k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1:go_default_library", "//vendor/k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1:go_default_library",
"//vendor/k8s.io/sample-apiserver/pkg/apis/wardle/v1alpha1:go_default_library", "//vendor/k8s.io/sample-apiserver/pkg/apis/wardle/v1alpha1:go_default_library",
], ],
......
/*
Copyright 2017 The Kubernetes Authors.
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 apimachinery
import (
"fmt"
"math/rand"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/util/workqueue"
"k8s.io/kubernetes/test/e2e/framework"
)
const numberOfTotalResources = 400
var _ = SIGDescribe("Servers with support for API chunking", func() {
f := framework.NewDefaultFramework("chunking")
It("should return chunks of results for list calls", func() {
ns := f.Namespace.Name
c := f.ClientSet
client := c.Core().PodTemplates(ns)
By("creating a large number of resources")
workqueue.Parallelize(20, numberOfTotalResources, func(i int) {
for tries := 3; tries >= 0; tries-- {
_, err := client.Create(&v1.PodTemplate{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("template-%04d", i),
},
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
{Name: "test", Image: "test2"},
},
},
},
})
if err == nil {
return
}
framework.Logf("Got an error creating template %d: %v", i, err)
}
Fail("Unable to create template %d, exiting", i)
})
By("retrieving those results in paged fashion several times")
for i := 0; i < 3; i++ {
opts := metav1.ListOptions{}
found := 0
var lastRV string
for {
opts.Limit = int64(rand.Int31n(numberOfTotalResources/10) + 1)
list, err := client.List(opts)
Expect(err).ToNot(HaveOccurred())
framework.Logf("Retrieved %d/%d results with rv %s and continue %s", len(list.Items), opts.Limit, list.ResourceVersion, list.Continue)
if len(lastRV) == 0 {
lastRV = list.ResourceVersion
}
if lastRV != list.ResourceVersion {
Expect(list.ResourceVersion).To(Equal(lastRV))
}
for _, item := range list.Items {
Expect(item.Name).To(Equal(fmt.Sprintf("template-%04d", found)))
found++
}
if len(list.Continue) == 0 {
break
}
opts.Continue = list.Continue
}
Expect(found).To(BeNumerically("==", numberOfTotalResources))
}
By("retrieving those results all at once")
list, err := client.List(metav1.ListOptions{Limit: numberOfTotalResources + 1})
Expect(err).ToNot(HaveOccurred())
Expect(list.Items).To(HaveLen(numberOfTotalResources))
})
})
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