all: kubernetes builder autoscaling
Improvements to support rapid scheduling of many build jobs:
- Retry logic in Kubernetes client to handle sporadic connection
closes from their API server under heavy load
- Cluster autoscaler scales on default CPU utilization metric
- Debug mode allows scheduling multiple builds to test scaling
- Account for scheduled vs. provisioned resources in a cluster
and use that information to estimate when a build's pod
will be scheduled and in running state
- Use estimated scheduled time to set context timeout
- Track pod lifecycle (requested time, estimated available time,
actual available time, terminate time, etc)
Change-Id: I14d6c5e01af0970dbb3390a29d1ee5c43049fff8
Reviewed-on: https://go-review.googlesource.com/19524
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/kubernetes/client.go b/kubernetes/client.go
index 5767465..16c2c1f 100644
--- a/kubernetes/client.go
+++ b/kubernetes/client.go
@@ -53,8 +53,7 @@
// Run creates a new pod resource in the default pod namespace with
// the given pod API specification.
// It returns the pod status once it has entered the Running phase.
-// An error is returned if the pod can not be created, if it does
-// does not enter the running phase within 2 minutes, or if ctx.Done
+// An error is returned if the pod can not be created, or if ctx.Done
// is closed.
func (c *Client) RunPod(ctx context.Context, pod *api.Pod) (*api.Pod, error) {
var podJSON bytes.Buffer
@@ -82,17 +81,27 @@
if err := json.Unmarshal(body, &podResult); err != nil {
return nil, fmt.Errorf("failed to decode pod resources: %v", err)
}
- ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
- defer cancel()
- createdPod, err := c.AwaitPodNotPending(ctx, podResult.Name, podResult.ObjectMeta.ResourceVersion)
- if err != nil {
- // The pod did not leave the pending state. We should try to manually delete it before
- // returning an error.
- c.DeletePod(context.Background(), podResult.Name)
- return nil, fmt.Errorf("timed out waiting for pod %q to leave pending state: %v", pod.Name, err)
+ retryWait := 1
+ retryMax := retryWait << 3 // retry 3 times
+ for {
+ createdPod, err := c.AwaitPodNotPending(ctx, podResult.Name, podResult.ObjectMeta.ResourceVersion)
+ if err != nil {
+ if err == context.Canceled {
+ return nil, err
+ }
+ if retryWait < retryMax { // retry
+ time.Sleep(time.Duration(retryWait) * time.Second)
+ retryWait = retryWait << 1
+ continue
+ }
+ // The pod did not leave the pending state. We should try to manually delete it before
+ // returning an error.
+ c.DeletePod(context.Background(), podResult.Name)
+ return nil, fmt.Errorf("waiting for pod %q to leave pending state: %v", pod.Name, err)
+ }
+ return createdPod, nil
}
- return createdPod, nil
}
// GetPods returns all pods in the cluster, regardless of status.
@@ -219,11 +228,14 @@
return
}
res, err := ctxhttp.Do(ctx, c.httpClient, req)
- defer res.Body.Close()
if err != nil {
- statusChan <- PodStatusResult{Err: fmt.Errorf("failed to make request: GET %q: %v", getURL, err)}
+ if err != context.Canceled {
+ statusChan <- PodStatusResult{Err: fmt.Errorf("failed to make request: GET %q: %v", getURL, err)}
+ }
+ statusChan <- PodStatusResult{Err: err} // context.Canceled
return
}
+ defer res.Body.Close()
var wps watchPodStatus
reader := bufio.NewReader(res.Body)