Skip to content

Commit 14bc5df

Browse files
authored
Adds a metric for force drained pods (#221)
I did add pod name and UID to the metric. This could in theory add quite some churn. But there's probably very few pods force drained, and it allows us to see if pods get stuck and non-deletable or if they are recreated.
1 parent 6eb395b commit 14bc5df

2 files changed

Lines changed: 46 additions & 4 deletions

File tree

controllers/node_force_drain_controller.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"time"
2525

2626
"github.com/itchyny/gojq"
27+
"github.com/prometheus/client_golang/prometheus"
2728
"go.uber.org/multierr"
2829
appsv1 "k8s.io/api/apps/v1"
2930
corev1 "k8s.io/api/core/v1"
@@ -37,12 +38,31 @@ import (
3738
"sigs.k8s.io/controller-runtime/pkg/client"
3839
"sigs.k8s.io/controller-runtime/pkg/handler"
3940
"sigs.k8s.io/controller-runtime/pkg/log"
41+
"sigs.k8s.io/controller-runtime/pkg/metrics"
4042
"sigs.k8s.io/controller-runtime/pkg/reconcile"
4143
"sigs.k8s.io/controller-runtime/pkg/source"
4244

4345
managedupgradev1beta1 "github.com/appuio/openshift-upgrade-controller/api/v1beta1"
4446
)
4547

48+
var forceDrainPodDeletions = prometheus.NewCounterVec(prometheus.CounterOpts{
49+
Namespace: MetricsNamespace,
50+
Subsystem: "node_force_drain",
51+
Name: "pod_deletions_total",
52+
Help: "Counts the number of deletion attempts for pods on nodes that are being force drained. A pod can be deleted multiple times if it is stuck and can't get deleted on the first attempt.",
53+
}, []string{"node_force_drain", "node", "namespace", "pod_name", "pod_uid"})
54+
55+
var forceDrainPodTermination = prometheus.NewCounterVec(prometheus.CounterOpts{
56+
Namespace: MetricsNamespace,
57+
Subsystem: "node_force_drain",
58+
Name: "pod_terminations_total",
59+
Help: "Counts the number of termination attempts for pods on nodes that are being force drained. This metric counts pods with long grace periods. The controller deletes them with a grace period of 1. A pod can be terminated multiple times if it is stuck and can't get terminated on the first attempt.",
60+
}, []string{"node_force_drain", "node", "namespace", "pod_name", "pod_uid"})
61+
62+
func init() {
63+
metrics.Registry.MustRegister(forceDrainPodDeletions, forceDrainPodTermination)
64+
}
65+
4666
// NodeForceDrainReconciler reconciles the NodeForceDrain object
4767
// It should be called on node change events.
4868
// It force drains nodes after a certain period of time.
@@ -169,7 +189,7 @@ func (r *NodeForceDrainReconciler) Reconcile(ctx context.Context, req ctrl.Reque
169189
errors := make([]error, 0, len(nodesOutOfGracePeriod))
170190
for _, node := range nodesOutOfGracePeriod {
171191
l.Info("Force draining node", "node", node.Name)
172-
deleted, err := r.forceDrainNode(ctx, node, podNSSelector, podJQQuery)
192+
deleted, err := r.forceDrainNode(ctx, fd.Name, node, podNSSelector, podJQQuery)
173193
if deleted {
174194
didDeletePod = true
175195
}
@@ -187,7 +207,7 @@ func (r *NodeForceDrainReconciler) Reconcile(ctx context.Context, req ctrl.Reque
187207
errors := make([]error, 0, len(nodesOutOfGracePeriod))
188208
for _, node := range nodesOutOfGracePeriod {
189209
l.Info("Force deleting pods on node", "node", node.Name)
190-
if t, err := r.forceDeletePodsOnNode(ctx, node, pfd, podNSSelector, podJQQuery); err != nil {
210+
if t, err := r.forceDeletePodsOnNode(ctx, fd.Name, node, pfd, podNSSelector, podJQQuery); err != nil {
191211
errors = append(errors, fmt.Errorf("failed to force delete pods on node %s: %w", node.Name, err))
192212
} else if t > 0 && (timeUntilNextPodForceDelete == 0 || t < timeUntilNextPodForceDelete) {
193213
timeUntilNextPodForceDelete = t
@@ -252,7 +272,7 @@ func (r *NodeForceDrainReconciler) setLastObservedNodeDrain(fd *managedupgradev1
252272

253273
// forceDrainNode deletes all pods on the node.
254274
// Returns true if there was an attempt to delete a pod and any errors that occurred.
255-
func (r *NodeForceDrainReconciler) forceDrainNode(ctx context.Context, node corev1.Node, podNSSelector labels.Selector, podJQQuery *gojq.Query) (bool, error) {
275+
func (r *NodeForceDrainReconciler) forceDrainNode(ctx context.Context, forceDrainName string, node corev1.Node, podNSSelector labels.Selector, podJQQuery *gojq.Query) (bool, error) {
256276
l := log.FromContext(ctx).WithName("NodeForceDrainReconciler.forceDrainNode").WithValues("node", node.Name)
257277

258278
pods, err := r.getDeletionCandidatePodsForNode(ctx, node, podNSSelector, podJQQuery)
@@ -269,6 +289,7 @@ func (r *NodeForceDrainReconciler) forceDrainNode(ctx context.Context, node core
269289
l.Info("Deleting pod", "pod", pod.Name, "podNamespace", pod.Namespace)
270290
attemptedDeletion = true
271291
r.Recorder.Eventf(&node, corev1.EventTypeWarning, "NodeDrainDeletePod", "Deleting pod %q in namespace %q", pod.Name, pod.Namespace)
292+
forceDrainPodDeletions.WithLabelValues(forceDrainName, node.Name, pod.Namespace, pod.Name, string(pod.UID)).Inc()
272293
if err := r.Delete(ctx, &pod); err != nil {
273294
deletionErrs = append(deletionErrs, err)
274295
}
@@ -280,7 +301,7 @@ func (r *NodeForceDrainReconciler) forceDrainNode(ctx context.Context, node core
280301
// forceDeletePodsOnNode deletes all pods on the node.
281302
// Returns the time until the next force delete and any errors that occurred.
282303
// A time of 0 means that there are no pods to force delete.
283-
func (r *NodeForceDrainReconciler) forceDeletePodsOnNode(ctx context.Context, node corev1.Node, gracePeriod time.Duration, podNSSelector labels.Selector, podJQQuery *gojq.Query) (time.Duration, error) {
304+
func (r *NodeForceDrainReconciler) forceDeletePodsOnNode(ctx context.Context, forceDrainName string, node corev1.Node, gracePeriod time.Duration, podNSSelector labels.Selector, podJQQuery *gojq.Query) (time.Duration, error) {
284305
l := log.FromContext(ctx).WithName("NodeForceDrainReconciler.forceDeletePodsOnNode").WithValues("node", node.Name)
285306

286307
pods, err := r.getDeletionCandidatePodsForNode(ctx, node, podNSSelector, podJQQuery)
@@ -308,6 +329,7 @@ func (r *NodeForceDrainReconciler) forceDeletePodsOnNode(ctx context.Context, no
308329

309330
l.Info("Forcing pod termination")
310331
r.Recorder.Eventf(&node, corev1.EventTypeWarning, "NodeDrainForcingPodTermination", "Forcing pod termination: Deleting pod %q in namespace %q with grace period of 1.", pod.Name, pod.Namespace)
332+
forceDrainPodTermination.WithLabelValues(forceDrainName, node.Name, pod.Namespace, pod.Name, string(pod.UID)).Inc()
311333
if err := r.Delete(ctx, &pod, &client.DeleteOptions{
312334
// As far is I was able to find a grace period of 0 will leave the hanging pod on the node and block the reboot of the node.
313335
// Therefore we set a grace period of 1 second for the quickest possible deletion.

controllers/node_force_drain_controller_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
package controllers
22

33
import (
4+
"strings"
45
"testing"
56
"time"
67

78
"github.com/go-logr/logr/testr"
9+
"github.com/prometheus/client_golang/prometheus/testutil"
810
"github.com/stretchr/testify/require"
911
appsv1 "k8s.io/api/apps/v1"
1012
corev1 "k8s.io/api/core/v1"
1113
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/apimachinery/pkg/types"
1215
"k8s.io/utils/ptr"
1316
"sigs.k8s.io/controller-runtime/pkg/client"
1417
"sigs.k8s.io/controller-runtime/pkg/log"
18+
"sigs.k8s.io/controller-runtime/pkg/metrics"
1519
"sigs.k8s.io/controller-runtime/pkg/reconcile"
1620

1721
managedupgradev1beta1 "github.com/appuio/openshift-upgrade-controller/api/v1beta1"
@@ -39,6 +43,7 @@ func Test_NodeForceDrainReconciler_Reconcile_E2E(t *testing.T) {
3943
ObjectMeta: metav1.ObjectMeta{
4044
Name: "pod-on-node1",
4145
Namespace: "default",
46+
UID: types.UID("pod-on-node1-uid"),
4247
},
4348
Spec: corev1.PodSpec{
4449
NodeName: node1.Name,
@@ -48,6 +53,7 @@ func Test_NodeForceDrainReconciler_Reconcile_E2E(t *testing.T) {
4853
ObjectMeta: metav1.ObjectMeta{
4954
Name: "pod-on-node1-force-delete",
5055
Namespace: "default",
56+
UID: types.UID("pod-on-node1-force-delete-uid"),
5157
Finalizers: []string{StuckPodSimulationFinalizer},
5258
},
5359
Spec: corev1.PodSpec{
@@ -229,6 +235,20 @@ func Test_NodeForceDrainReconciler_Reconcile_E2E(t *testing.T) {
229235
var storageNodePod corev1.Pod
230236
require.NoError(t, cli.Get(ctx, client.ObjectKeyFromObject(stuckPodOnDrainingStorageNode), &storageNodePod), "pod should still be there")
231237
})
238+
239+
step(t, "check metrics", func(t *testing.T) {
240+
if err := testutil.GatherAndCompare(metrics.Registry, strings.NewReader(`
241+
# HELP openshift_upgrade_controller_node_force_drain_pod_deletions_total Counts the number of deletion attempts for pods on nodes that are being force drained. A pod can be deleted multiple times if it is stuck and can't get deleted on the first attempt.
242+
# TYPE openshift_upgrade_controller_node_force_drain_pod_deletions_total counter
243+
openshift_upgrade_controller_node_force_drain_pod_deletions_total{namespace="default",node="node1",node_force_drain="node-force-drain",pod_name="pod-on-node1",pod_uid="pod-on-node1-uid"} 1
244+
openshift_upgrade_controller_node_force_drain_pod_deletions_total{namespace="default",node="node1",node_force_drain="node-force-drain",pod_name="pod-on-node1-force-delete",pod_uid="pod-on-node1-force-delete-uid"} 1
245+
# HELP openshift_upgrade_controller_node_force_drain_pod_terminations_total Counts the number of termination attempts for pods on nodes that are being force drained. This metric counts pods with long grace periods. The controller deletes them with a grace period of 1. A pod can be terminated multiple times if it is stuck and can't get terminated on the first attempt.
246+
# TYPE openshift_upgrade_controller_node_force_drain_pod_terminations_total counter
247+
openshift_upgrade_controller_node_force_drain_pod_terminations_total{namespace="default",node="node1",node_force_drain="node-force-drain",pod_name="pod-on-node1-force-delete",pod_uid="pod-on-node1-force-delete-uid"} 1
248+
`), "openshift_upgrade_controller_node_force_drain_pod_deletions_total", "openshift_upgrade_controller_node_force_drain_pod_terminations_total"); err != nil {
249+
t.Fatal(err)
250+
}
251+
})
232252
}
233253

234254
func Test_NodeForceDrainReconciler_Reconcile_DrainIgnoreActiveDaemonsSetsStaticPods(t *testing.T) {

0 commit comments

Comments
 (0)