Merge pull request #5556 from travisn/node-failure-cleanup

Handle node failure more gracefully
This commit is contained in:
Travis Nielsen authored and GitHub committed 2020-05-28 15:44:48 -06:00
commit 2a6d4d4cef
7 files changed
+172 -67

No files matched your search

+51 -27
View File
@@ -17,6 +17,7 @@ limitations under the License.
package mon
import (
"fmt"
"strings"
"time"
@@ -27,6 +28,7 @@ import (
cephconfig "github.com/rook/rook/pkg/daemon/ceph/config"
cephutil "github.com/rook/rook/pkg/daemon/ceph/util"
"github.com/rook/rook/pkg/operator/ceph/controller"
"github.com/rook/rook/pkg/operator/k8sutil"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -133,13 +135,13 @@ func (c *Cluster) checkHealth() error {
// when the mon isn't in the clusterInfo, but is in quorum and there are
// enough mons, remove it else remove it on the next run
if inQuorum && len(quorumStatus.MonMap.Mons) > desiredMonCount {
logger.Warningf("mon %s not in source of truth but in quorum, removing", mon.Name)
logger.Warningf("mon %q not in source of truth but in quorum, removing", mon.Name)
if err := c.removeMon(mon.Name); err != nil {
logger.Warningf("failed to remove mon %q. %v", mon.Name, err)
}
} else {
logger.Warningf(
"mon %s not in source of truth and not in quorum, not enough mons to remove now (wanted: %d, current: %d)",
"mon %q not in source of truth and not in quorum, not enough mons to remove now (wanted: %d, current: %d)",
mon.Name,
desiredMonCount,
len(quorumStatus.MonMap.Mons),
@@ -148,38 +150,42 @@ func (c *Cluster) checkHealth() error {
}
if inQuorum {
logger.Debugf("mon %s found in quorum", mon.Name)
logger.Debugf("mon %q found in quorum", mon.Name)
// delete the "timeout" for a mon if the pod is in quorum again
if _, ok := c.monTimeoutList[mon.Name]; ok {
delete(c.monTimeoutList, mon.Name)
logger.Infof("mon %s is back in quorum, removed from mon out timeout list", mon.Name)
logger.Infof("mon %q is back in quorum, removed from mon out timeout list", mon.Name)
}
} else {
logger.Debugf("mon %s NOT found in quorum. Mon quorum status: %+v", mon.Name, quorumStatus)
allMonsInQuorum = false
// If not yet set, add the current time, for the timeout
// calculation, to the list
if _, ok := c.monTimeoutList[mon.Name]; !ok {
c.monTimeoutList[mon.Name] = time.Now()
}
// when the timeout for the mon has been reached, continue to the
// normal failover/delete mon pod part of the code
if time.Since(c.monTimeoutList[mon.Name]) <= MonOutTimeout {
timeToFailover := int(MonOutTimeout.Seconds() - time.Since(c.monTimeoutList[mon.Name]).Seconds())
logger.Warningf("mon %s not found in quorum, waiting for timeout (%d seconds left) before failover", mon.Name, timeToFailover)
continue
}
logger.Warningf("mon %s NOT found in quorum and timeout exceeded, mon will be failed over", mon.Name)
c.failMon(len(quorumStatus.MonMap.Mons), desiredMonCount, mon.Name)
// only deal with one unhealthy mon per health check
return nil
continue
}
logger.Debugf("mon %q NOT found in quorum. Mon quorum status: %+v", mon.Name, quorumStatus)
allMonsInQuorum = false
// If not yet set, add the current time, for the timeout
// calculation, to the list
if _, ok := c.monTimeoutList[mon.Name]; !ok {
c.monTimeoutList[mon.Name] = time.Now()
}
// when the timeout for the mon has been reached, continue to the
// normal failover/delete mon pod part of the code
if time.Since(c.monTimeoutList[mon.Name]) <= MonOutTimeout {
timeToFailover := int(MonOutTimeout.Seconds() - time.Since(c.monTimeoutList[mon.Name]).Seconds())
logger.Warningf("mon %q not found in quorum, waiting for timeout (%d seconds left) before failover", mon.Name, timeToFailover)
// Restart the mon if it is stuck on a failed node
c.restartMonIfStuckTerminating(mon.Name)
continue
}
logger.Warningf("mon %q NOT found in quorum and timeout exceeded, mon will be failed over", mon.Name)
c.failMon(len(quorumStatus.MonMap.Mons), desiredMonCount, mon.Name)
// only deal with one unhealthy mon per health check
return nil
}
// after all unhealthy mons have been removed/failovered
// after all unhealthy mons have been removed or failed over
// handle all mons that haven't been in the Ceph mon map
for mon := range monsNotFound {
logger.Warningf("mon %s NOT found in ceph mon map, failover", mon)
@@ -422,3 +428,21 @@ func (c *Cluster) addOrRemoveExternalMonitor(status client.MonStatusResponse) (b
logger.Debugf("ClusterInfo.Monitors is %+v", c.ClusterInfo.Monitors)
return changed, nil
}
// restartMonIfStuckTerminating will check if a mon is on a node that is not ready.
// If the pod is stuck in terminating state, go ahead and force delete the pod so K8s
// will allow the mon to be restarted on another node.
func (c *Cluster) restartMonIfStuckTerminating(monName string) error {
logger.Debugf("Checking for a stuck mon %q pod", monName)
labels := fmt.Sprintf("app=%s,mon=%s", AppName, monName)
pods, err := c.context.Clientset.CoreV1().Pods(c.Namespace).List(metav1.ListOptions{LabelSelector: labels})
if err != nil {
return errors.Wrapf(err, "failed to get pod for mon %q", monName)
}
for _, pod := range pods.Items {
if err := k8sutil.ForceDeletePodIfStuck(c.context, pod); err != nil {
logger.Warningf("skipping forced restart of mon %q. %v", monName, err)
}
}
return nil
}
@@ -22,18 +22,23 @@ import (
"os"
"sync"
"testing"
"time"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"github.com/rook/rook/pkg/clusterd"
"github.com/rook/rook/pkg/daemon/ceph/client"
clienttest "github.com/rook/rook/pkg/daemon/ceph/client/test"
cephconfig "github.com/rook/rook/pkg/daemon/ceph/config"
"github.com/rook/rook/pkg/operator/k8sutil"
testopk8s "github.com/rook/rook/pkg/operator/k8sutil/test"
"github.com/rook/rook/pkg/operator/test"
exectest "github.com/rook/rook/pkg/util/exec/test"
"github.com/stretchr/testify/assert"
apps "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
func TestCheckHealth(t *testing.T) {
@@ -284,3 +289,77 @@ func TestAddOrRemoveExternalMonitor(t *testing.T) {
// ClusterInfo should now have 2 monitors
assert.Equal(t, 2, len(c.ClusterInfo.Monitors))
}
func TestForceDeleteFailedMon(t *testing.T) {
clientset := test.New(t, 1)
context := &clusterd.Context{
Clientset: clientset,
}
c := &Cluster{
context: context,
Namespace: "ns",
}
failedMonName := "a"
monToFail := createTestMonPod(t, clientset, c.Namespace, failedMonName)
createTestMonPod(t, clientset, c.Namespace, "b")
createTestMonPod(t, clientset, c.Namespace, "c")
assert.NoError(t, c.restartMonIfStuckTerminating(failedMonName))
// The mon should still exist since it wasn't in a deleted state
p, err := context.Clientset.CoreV1().Pods(c.Namespace).Get(monToFail.Name, metav1.GetOptions{})
assert.NoError(t, err)
assert.NotNil(t, p)
// Add a deletion timestamp to the pod
monToFail.DeletionTimestamp = &metav1.Time{Time: time.Now()}
_, err = context.Clientset.CoreV1().Pods(c.Namespace).Update(&monToFail)
assert.NoError(t, err)
assert.NoError(t, c.restartMonIfStuckTerminating(failedMonName))
// The pod should still exist since the node is ready
p, err = context.Clientset.CoreV1().Pods(c.Namespace).Get(monToFail.Name, metav1.GetOptions{})
assert.NoError(t, err)
assert.NotNil(t, p)
// Set the node to a not ready state
nodes, err := context.Clientset.CoreV1().Nodes().List(metav1.ListOptions{})
assert.NoError(t, err)
for _, node := range nodes.Items {
node.Status.Conditions[0].Status = v1.ConditionFalse
_, err := context.Clientset.CoreV1().Nodes().Update(&node)
assert.NoError(t, err)
}
assert.NoError(t, c.restartMonIfStuckTerminating(failedMonName))
// The pod should be deleted since the pod is marked as deleted and the node is not ready
_, err = context.Clientset.CoreV1().Pods(c.Namespace).Get(monToFail.Name, metav1.GetOptions{})
assert.Error(t, err)
assert.True(t, errors.IsNotFound(err))
// mons b and c should still exist
_, err = context.Clientset.CoreV1().Pods(c.Namespace).Get("b-test", metav1.GetOptions{})
assert.NoError(t, err)
_, err = context.Clientset.CoreV1().Pods(c.Namespace).Get("c-test", metav1.GetOptions{})
assert.NoError(t, err)
}
func createTestMonPod(t *testing.T, clientset kubernetes.Interface, namespace, name string) v1.Pod {
pod := v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: name + "-test",
Namespace: namespace,
Labels: map[string]string{
k8sutil.AppAttr: AppName,
"mon": name,
},
},
}
pod.Spec.NodeName = "node0"
_, err := clientset.CoreV1().Pods(namespace).Create(&pod)
assert.NoError(t, err)
return pod
}
+3 -29
View File
@@ -24,7 +24,6 @@ import (
"github.com/rook/rook/pkg/clusterd"
"github.com/rook/rook/pkg/daemon/ceph/client"
"github.com/rook/rook/pkg/operator/k8sutil"
v1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -36,7 +35,7 @@ const (
)
var (
healthCheckInterval = 300 * time.Second
healthCheckInterval = 60 * time.Second
)
// OSDHealthMonitor defines OSD process monitoring
@@ -160,35 +159,10 @@ func (m *OSDHealthMonitor) restartOSDIfStuck(osdID int) error {
if err != nil {
return errors.Wrapf(err, "failed to get OSD with ID %d", osdID)
}
return m.restartOSDPodsIfStuck(osdID, pods)
}
func (m *OSDHealthMonitor) restartOSDPodsIfStuck(osdID int, pods *v1.PodList) error {
for _, pod := range pods.Items {
logger.Debugf("checking if osd %d pod is stuck and should be force deleted", osdID)
if pod.DeletionTimestamp.IsZero() {
logger.Debugf("skipping restart of OSD %d since the pod is not deleted", osdID)
continue
if err := k8sutil.ForceDeletePodIfStuck(m.context, pod); err != nil {
logger.Warningf("skipping restart of OSD %d. %v", osdID, err)
}
node, err := m.context.Clientset.CoreV1().Nodes().Get(pod.Spec.NodeName, metav1.GetOptions{})
if err != nil {
logger.Warningf("skipping restart of OSD %d since the node status is not available. %v", osdID, err)
continue
}
if k8sutil.NodeIsReady(*node) {
logger.Debugf("skipping restart of OSD %d since the node status is ready", osdID)
continue
}
logger.Infof("force deleting pod %q that appears to be stuck terminating", pod.Name)
var gracePeriod int64
deleteOpts := &metav1.DeleteOptions{GracePeriodSeconds: &gracePeriod}
if err := m.context.Clientset.CoreV1().Pods(m.namespace).Delete(pod.Name, deleteOpts); err != nil {
logger.Warningf("pod %q deletion failed. %v", pod.Name, err)
continue
}
logger.Infof("pod %q deletion succeeded", pod.Name)
}
return nil
}
+6 -8
View File
@@ -117,7 +117,7 @@ func TestOSDRestartIfStuck(t *testing.T) {
Clientset: clientset,
}
pod := &v1.Pod{
pod := v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "osd0",
Namespace: namespace,
@@ -128,13 +128,12 @@ func TestOSDRestartIfStuck(t *testing.T) {
},
}
pod.Spec.NodeName = "node0"
_, err := context.Clientset.CoreV1().Pods(namespace).Create(pod)
_, err := context.Clientset.CoreV1().Pods(namespace).Create(&pod)
assert.NoError(t, err)
m := NewOSDHealthMonitor(context, namespace, false)
podList := &v1.PodList{Items: []v1.Pod{*pod}}
assert.NoError(t, m.restartOSDPodsIfStuck(0, podList))
assert.NoError(t, k8sutil.ForceDeletePodIfStuck(m.context, pod))
// The pod should still exist since it wasn't in a deleted state
p, err := context.Clientset.CoreV1().Pods(namespace).Get(pod.Name, metav1.GetOptions{})
@@ -143,11 +142,10 @@ func TestOSDRestartIfStuck(t *testing.T) {
// Add a deletion timestamp to the pod
pod.DeletionTimestamp = &metav1.Time{Time: time.Now()}
_, err = context.Clientset.CoreV1().Pods(namespace).Update(pod)
_, err = context.Clientset.CoreV1().Pods(namespace).Update(&pod)
assert.NoError(t, err)
podList = &v1.PodList{Items: []v1.Pod{*pod}}
assert.NoError(t, m.restartOSDPodsIfStuck(0, podList))
assert.NoError(t, k8sutil.ForceDeletePodIfStuck(m.context, pod))
// The pod should still exist since the node is ready
p, err = context.Clientset.CoreV1().Pods(namespace).Get(pod.Name, metav1.GetOptions{})
@@ -163,7 +161,7 @@ func TestOSDRestartIfStuck(t *testing.T) {
assert.NoError(t, err)
}
assert.NoError(t, m.restartOSDPodsIfStuck(0, podList))
assert.NoError(t, k8sutil.ForceDeletePodIfStuck(m.context, pod))
// The pod should be deleted since the pod is marked as deleted and the node is not ready
_, err = context.Clientset.CoreV1().Pods(namespace).Get(pod.Name, metav1.GetOptions{})
+3 -2
View File
@@ -556,7 +556,7 @@ func (c *Cluster) startOSDDaemonsOnNode(nodeName string, config *provisionConfig
_, createErr := c.context.Clientset.AppsV1().Deployments(c.Namespace).Create(dp)
if createErr != nil {
if kerrors.IsAlreadyExists(createErr) {
logger.Infof("deployment for osd %d already exists. updating if needed", osd.ID)
logger.Debugf("deployment for osd %d already exists. updating if needed", osd.ID)
if err = updateDeploymentAndWait(c.context, dp, c.Namespace, opconfig.OsdType, strconv.Itoa(osd.ID), c.skipUpgradeChecks, c.continueUpgradeAfterChecksEvenIfNotHealthy); err != nil {
logger.Errorf("failed to update osd deployment %d. %v", osd.ID, err)
}
@@ -565,8 +565,9 @@ func (c *Cluster) startOSDDaemonsOnNode(nodeName string, config *provisionConfig
logger.Warningf("failed to create osd deployment for node %q, osd %+v. %v", n.Name, osd, createErr)
continue
}
} else {
logger.Infof("created deployment for osd %d", osd.ID)
}
logger.Infof("started deployment for osd %d", osd.ID)
}
}
+2 -1
View File
@@ -74,6 +74,8 @@ func UpdateDeploymentAndWait(context *clusterd.Context, modifiedDeployment *apps
// If deployments are different, let's update!
if !patchResult.IsEmpty() {
logger.Infof("updating deployment %q after verifying it is safe to stop", modifiedDeployment.Name)
// Let's verify the deployment can be stopped
// retry for 5 times, every minute
err = util.Retry(5, 60*time.Second, func() error {
@@ -89,7 +91,6 @@ func UpdateDeploymentAndWait(context *clusterd.Context, modifiedDeployment *apps
return nil, fmt.Errorf("failed to set hash annotation on deployment %q. %v", modifiedDeployment.Name, err)
}
logger.Infof("updating deployment %q", modifiedDeployment.Name)
if _, err := context.Clientset.AppsV1().Deployments(namespace).Update(modifiedDeployment); err != nil {
return nil, fmt.Errorf("failed to update deployment %q. %v", modifiedDeployment.Name, err)
}
+28
View File
@@ -25,7 +25,9 @@ import (
"strconv"
"strings"
"github.com/pkg/errors"
rookv1 "github.com/rook/rook/pkg/apis/rook.io/v1"
"github.com/rook/rook/pkg/clusterd"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -331,3 +333,29 @@ func SetNodeAntiAffinityForPod(pod *v1.PodSpec, p rookv1.Placement, requiredDuri
})
}
}
func ForceDeletePodIfStuck(context *clusterd.Context, pod v1.Pod) error {
logger.Debugf("checking if pod %q is stuck and should be force deleted", pod.Name)
if pod.DeletionTimestamp.IsZero() {
logger.Debugf("skipping pod %q restart since the pod is not deleted", pod.Name)
return nil
}
node, err := context.Clientset.CoreV1().Nodes().Get(pod.Spec.NodeName, metav1.GetOptions{})
if err != nil {
return errors.Wrap(err, "node status is not available")
}
if NodeIsReady(*node) {
logger.Debugf("skipping restart of pod %q since the node status is ready", pod.Name)
return nil
}
logger.Infof("force deleting pod %q that appears to be stuck terminating", pod.Name)
var gracePeriod int64
deleteOpts := &metav1.DeleteOptions{GracePeriodSeconds: &gracePeriod}
if err := context.Clientset.CoreV1().Pods(pod.Namespace).Delete(pod.Name, deleteOpts); err != nil {
logger.Warningf("pod %q deletion failed. %v", pod.Name, err)
return nil
}
logger.Infof("pod %q deletion succeeded", pod.Name)
return nil
}