mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-03 17:30:00 +00:00
Randomize apiserver watch timeouts
This commit is contained in:
parent
d9d12fd3f7
commit
8a5445d3db
@ -20,8 +20,10 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/cmd/kube-apiserver/app"
|
"github.com/GoogleCloudPlatform/kubernetes/cmd/kube-apiserver/app"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
@ -32,6 +34,8 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||||
|
rand.Seed(time.Now().UTC().UnixNano())
|
||||||
|
|
||||||
s := app.NewAPIServer()
|
s := app.NewAPIServer()
|
||||||
s.AddFlags(pflag.CommandLine)
|
s.AddFlags(pflag.CommandLine)
|
||||||
|
|
||||||
|
@ -46,6 +46,12 @@ import (
|
|||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Maximum duration before timing out read/write requests
|
||||||
|
// Set to a value larger than the timeouts in each watch server.
|
||||||
|
ReadWriteTimeout = time.Minute * 60
|
||||||
|
)
|
||||||
|
|
||||||
// APIServer runs a kubernetes api server.
|
// APIServer runs a kubernetes api server.
|
||||||
type APIServer struct {
|
type APIServer struct {
|
||||||
InsecureBindAddress util.IP
|
InsecureBindAddress util.IP
|
||||||
@ -393,8 +399,8 @@ func (s *APIServer) Run(_ []string) error {
|
|||||||
readOnlyServer := &http.Server{
|
readOnlyServer := &http.Server{
|
||||||
Addr: roLocation,
|
Addr: roLocation,
|
||||||
Handler: apiserver.MaxInFlightLimit(sem, longRunningRE, apiserver.RecoverPanics(apiserver.ReadOnly(apiserver.RateLimit(rl, m.InsecureHandler)))),
|
Handler: apiserver.MaxInFlightLimit(sem, longRunningRE, apiserver.RecoverPanics(apiserver.ReadOnly(apiserver.RateLimit(rl, m.InsecureHandler)))),
|
||||||
ReadTimeout: 5 * time.Minute,
|
ReadTimeout: ReadWriteTimeout,
|
||||||
WriteTimeout: 5 * time.Minute,
|
WriteTimeout: ReadWriteTimeout,
|
||||||
MaxHeaderBytes: 1 << 20,
|
MaxHeaderBytes: 1 << 20,
|
||||||
}
|
}
|
||||||
glog.Infof("Serving read-only insecurely on %s", roLocation)
|
glog.Infof("Serving read-only insecurely on %s", roLocation)
|
||||||
@ -413,8 +419,8 @@ func (s *APIServer) Run(_ []string) error {
|
|||||||
secureServer := &http.Server{
|
secureServer := &http.Server{
|
||||||
Addr: secureLocation,
|
Addr: secureLocation,
|
||||||
Handler: apiserver.MaxInFlightLimit(sem, longRunningRE, apiserver.RecoverPanics(m.Handler)),
|
Handler: apiserver.MaxInFlightLimit(sem, longRunningRE, apiserver.RecoverPanics(m.Handler)),
|
||||||
ReadTimeout: 5 * time.Minute,
|
ReadTimeout: ReadWriteTimeout,
|
||||||
WriteTimeout: 5 * time.Minute,
|
WriteTimeout: ReadWriteTimeout,
|
||||||
MaxHeaderBytes: 1 << 20,
|
MaxHeaderBytes: 1 << 20,
|
||||||
TLSConfig: &tls.Config{
|
TLSConfig: &tls.Config{
|
||||||
// Change default from SSLv3 to TLSv1.0 (because of POODLE vulnerability)
|
// Change default from SSLv3 to TLSv1.0 (because of POODLE vulnerability)
|
||||||
@ -454,12 +460,11 @@ func (s *APIServer) Run(_ []string) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
http := &http.Server{
|
http := &http.Server{
|
||||||
Addr: insecureLocation,
|
Addr: insecureLocation,
|
||||||
Handler: apiserver.RecoverPanics(m.InsecureHandler),
|
Handler: apiserver.RecoverPanics(m.InsecureHandler),
|
||||||
ReadTimeout: 5 * time.Minute,
|
ReadTimeout: ReadWriteTimeout,
|
||||||
WriteTimeout: 5 * time.Minute,
|
WriteTimeout: ReadWriteTimeout,
|
||||||
MaxHeaderBytes: 1 << 20,
|
MaxHeaderBytes: 1 << 20,
|
||||||
}
|
}
|
||||||
glog.Infof("Serving insecurely on %s", insecureLocation)
|
glog.Infof("Serving insecurely on %s", insecureLocation)
|
||||||
|
@ -132,6 +132,14 @@ type APIGroupVersion struct {
|
|||||||
Context api.RequestContextMapper
|
Context api.RequestContextMapper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Pipe these in through the apiserver cmd line
|
||||||
|
const (
|
||||||
|
// Minimum duration before timing out read/write requests
|
||||||
|
MinTimeoutSecs = 300
|
||||||
|
// Maximum duration before timing out read/write requests
|
||||||
|
MaxTimeoutSecs = 600
|
||||||
|
)
|
||||||
|
|
||||||
// InstallREST registers the REST handlers (storage, watch, proxy and redirect) into a restful Container.
|
// InstallREST registers the REST handlers (storage, watch, proxy and redirect) into a restful Container.
|
||||||
// It is expected that the provided path root prefix will serve all operations. Root MUST NOT end
|
// It is expected that the provided path root prefix will serve all operations. Root MUST NOT end
|
||||||
// in a slash. A restful WebService is created for the group and version.
|
// in a slash. A restful WebService is created for the group and version.
|
||||||
|
@ -17,10 +17,12 @@ limitations under the License.
|
|||||||
package apiserver
|
package apiserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/httplog"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/httplog"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||||
@ -32,19 +34,47 @@ import (
|
|||||||
"golang.org/x/net/websocket"
|
"golang.org/x/net/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
var connectionUpgradeRegex = regexp.MustCompile("(^|.*,\\s*)upgrade($|\\s*,)")
|
var (
|
||||||
|
connectionUpgradeRegex = regexp.MustCompile("(^|.*,\\s*)upgrade($|\\s*,)")
|
||||||
|
|
||||||
|
// nothing will ever be sent down this channel
|
||||||
|
neverExitWatch <-chan time.Time = make(chan time.Time)
|
||||||
|
)
|
||||||
|
|
||||||
func isWebsocketRequest(req *http.Request) bool {
|
func isWebsocketRequest(req *http.Request) bool {
|
||||||
return connectionUpgradeRegex.MatchString(strings.ToLower(req.Header.Get("Connection"))) && strings.ToLower(req.Header.Get("Upgrade")) == "websocket"
|
return connectionUpgradeRegex.MatchString(strings.ToLower(req.Header.Get("Connection"))) && strings.ToLower(req.Header.Get("Upgrade")) == "websocket"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// timeoutFactory abstracts watch timeout logic for testing
|
||||||
|
type timeoutFactory interface {
|
||||||
|
TimeoutCh() (<-chan time.Time, func() bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// realTimeoutFactory implements timeoutFactory
|
||||||
|
type realTimeoutFactory struct {
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimeoutChan returns a channel which will receive something when the watch times out,
|
||||||
|
// and a cleanup function to call when this happens.
|
||||||
|
func (w *realTimeoutFactory) TimeoutCh() (<-chan time.Time, func() bool) {
|
||||||
|
if w.timeout == 0 {
|
||||||
|
return neverExitWatch, func() bool { return false }
|
||||||
|
}
|
||||||
|
t := time.NewTimer(w.timeout)
|
||||||
|
return t.C, t.Stop
|
||||||
|
}
|
||||||
|
|
||||||
// serveWatch handles serving requests to the server
|
// serveWatch handles serving requests to the server
|
||||||
func serveWatch(watcher watch.Interface, scope RequestScope, w http.ResponseWriter, req *restful.Request) {
|
func serveWatch(watcher watch.Interface, scope RequestScope, w http.ResponseWriter, req *restful.Request) {
|
||||||
|
// Each watch gets a random timeout to avoid thundering herds. Rand is seeded once in the api installer.
|
||||||
|
timeout := time.Duration(MinTimeoutSecs+rand.Intn(MaxTimeoutSecs-MinTimeoutSecs)) * time.Second
|
||||||
|
|
||||||
watchServer := &WatchServer{watcher, scope.Codec, func(obj runtime.Object) {
|
watchServer := &WatchServer{watcher, scope.Codec, func(obj runtime.Object) {
|
||||||
if err := setSelfLink(obj, req, scope.Namer); err != nil {
|
if err := setSelfLink(obj, req, scope.Namer); err != nil {
|
||||||
glog.V(5).Infof("Failed to set self link for object %v: %v", reflect.TypeOf(obj), err)
|
glog.V(5).Infof("Failed to set self link for object %v: %v", reflect.TypeOf(obj), err)
|
||||||
}
|
}
|
||||||
}}
|
}, &realTimeoutFactory{timeout}}
|
||||||
if isWebsocketRequest(req.Request) {
|
if isWebsocketRequest(req.Request) {
|
||||||
websocket.Handler(watchServer.HandleWS).ServeHTTP(httplog.Unlogged(w), req.Request)
|
websocket.Handler(watchServer.HandleWS).ServeHTTP(httplog.Unlogged(w), req.Request)
|
||||||
} else {
|
} else {
|
||||||
@ -57,6 +87,7 @@ type WatchServer struct {
|
|||||||
watching watch.Interface
|
watching watch.Interface
|
||||||
codec runtime.Codec
|
codec runtime.Codec
|
||||||
fixup func(runtime.Object)
|
fixup func(runtime.Object)
|
||||||
|
t timeoutFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleWS implements a websocket handler.
|
// HandleWS implements a websocket handler.
|
||||||
@ -100,6 +131,9 @@ func (w *WatchServer) HandleWS(ws *websocket.Conn) {
|
|||||||
func (self *WatchServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
func (self *WatchServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
loggedW := httplog.LogOf(req, w)
|
loggedW := httplog.LogOf(req, w)
|
||||||
w = httplog.Unlogged(w)
|
w = httplog.Unlogged(w)
|
||||||
|
timeoutCh, cleanup := self.t.TimeoutCh()
|
||||||
|
defer cleanup()
|
||||||
|
defer self.watching.Stop()
|
||||||
|
|
||||||
cn, ok := w.(http.CloseNotifier)
|
cn, ok := w.(http.CloseNotifier)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -113,16 +147,15 @@ func (self *WatchServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
http.NotFound(w, req)
|
http.NotFound(w, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Transfer-Encoding", "chunked")
|
w.Header().Set("Transfer-Encoding", "chunked")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
flusher.Flush()
|
flusher.Flush()
|
||||||
|
|
||||||
encoder := watchjson.NewEncoder(w, self.codec)
|
encoder := watchjson.NewEncoder(w, self.codec)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-cn.CloseNotify():
|
case <-cn.CloseNotify():
|
||||||
self.watching.Stop()
|
return
|
||||||
|
case <-timeoutCh:
|
||||||
return
|
return
|
||||||
case event, ok := <-self.watching.ResultChan():
|
case event, ok := <-self.watching.ResultChan():
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -132,7 +165,6 @@ func (self *WatchServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
self.fixup(event.Object)
|
self.fixup(event.Object)
|
||||||
if err := encoder.Encode(&event); err != nil {
|
if err := encoder.Encode(&event); err != nil {
|
||||||
// Client disconnect.
|
// Client disconnect.
|
||||||
self.watching.Stop()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
flusher.Flush()
|
flusher.Flush()
|
||||||
|
@ -18,11 +18,13 @@ package apiserver
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest"
|
||||||
@ -278,4 +280,71 @@ func TestWatchProtocolSelection(t *testing.T) {
|
|||||||
t.Errorf("Unexpected response %#v", response)
|
t.Errorf("Unexpected response %#v", response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeTimeoutFactory struct {
|
||||||
|
timeoutCh chan time.Time
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *fakeTimeoutFactory) TimeoutCh() (<-chan time.Time, func() bool) {
|
||||||
|
return t.timeoutCh, func() bool {
|
||||||
|
defer close(t.done)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWatchHTTPTimeout(t *testing.T) {
|
||||||
|
watcher := watch.NewFake()
|
||||||
|
timeoutCh := make(chan time.Time)
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
// Setup a new watchserver
|
||||||
|
watchServer := &WatchServer{
|
||||||
|
watcher,
|
||||||
|
version2ServerCodec,
|
||||||
|
func(obj runtime.Object) {},
|
||||||
|
&fakeTimeoutFactory{timeoutCh, done},
|
||||||
|
}
|
||||||
|
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
watchServer.ServeHTTP(w, req)
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
// Setup a client
|
||||||
|
dest, _ := url.Parse(s.URL)
|
||||||
|
dest.Path = "/api/version/watch/resource"
|
||||||
|
dest.RawQuery = ""
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", dest.String(), nil)
|
||||||
|
client := http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
watcher.Add(&api.Pod{TypeMeta: api.TypeMeta{APIVersion: "v1beta3"}})
|
||||||
|
|
||||||
|
// Make sure we can actually watch an endpoint
|
||||||
|
decoder := json.NewDecoder(resp.Body)
|
||||||
|
var got watchJSON
|
||||||
|
err = decoder.Decode(&got)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeout and check for leaks
|
||||||
|
close(timeoutCh)
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
if !watcher.Stopped {
|
||||||
|
t.Errorf("Leaked watch on timeout")
|
||||||
|
}
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Errorf("Failed to stop watcher after 100ms of timeout signal")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we can't receive any more events through the timeout watch
|
||||||
|
err = decoder.Decode(&got)
|
||||||
|
if err != io.EOF {
|
||||||
|
t.Errorf("Unexpected non-error")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user