Skip to content

Commit 28394a5

Browse files
authored
tiproxy: delete drained pods early when the connection drops to zero (#6936)
1 parent 706168b commit 28394a5

5 files changed

Lines changed: 309 additions & 26 deletions

File tree

pkg/controllers/tiproxy/tasks/finalizer.go

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,19 @@ func drainOrDeletePod(ctx context.Context, c client.Client, state State, pod *co
7979
}
8080

8181
remaining := time.Until(startAt.Add(time.Duration(seconds) * time.Second))
82+
if remaining <= 0 {
83+
return deleteTiProxyPod(ctx, c, pod)
84+
}
85+
86+
if tiProxyConnectionsDrained(ctx, state, c, logger) {
87+
return deleteTiProxyPod(ctx, c, pod)
88+
}
89+
8290
if remaining > task.DefaultRequeueAfter {
8391
remaining = task.DefaultRequeueAfter
8492
}
85-
if remaining > 0 {
86-
return remaining, nil
87-
}
8893

89-
return deleteTiProxyPod(ctx, c, pod)
94+
return remaining, nil
9095
}
9196

9297
func gracefulShutdownDeleteDelaySeconds(tiproxy *v1alpha1.TiProxy) (seconds int32, ok bool, err error) {
@@ -131,6 +136,48 @@ func deleteTiProxyPod(ctx context.Context, c client.Client, pod *corev1.Pod) (ti
131136
return task.DefaultRequeueAfter, nil
132137
}
133138

139+
func tiProxyConnectionsDrained(ctx context.Context, state State, c client.Client, logger logr.Logger) bool {
140+
tiproxy := state.Object()
141+
142+
tpClient, err := newTiProxyDeleteClient(ctx, state, c)
143+
if err != nil {
144+
logger.Info(
145+
"failed to build TiProxy API client before checking connections, continue waiting",
146+
"namespace", tiproxy.Namespace,
147+
"name", tiproxy.Name,
148+
"error", err,
149+
)
150+
return false
151+
}
152+
153+
connectionCount, err := tpClient.ConnectionCount(ctx)
154+
if err != nil {
155+
logger.Info(
156+
"failed to query TiProxy connections before graceful delete, continue waiting",
157+
"namespace", tiproxy.Namespace,
158+
"name", tiproxy.Name,
159+
"error", err,
160+
)
161+
return false
162+
}
163+
if connectionCount > 0 {
164+
logger.Info(
165+
"TiProxy still has active connections, continue waiting",
166+
"namespace", tiproxy.Namespace,
167+
"name", tiproxy.Name,
168+
"connectionCount", connectionCount,
169+
)
170+
return false
171+
}
172+
173+
logger.Info(
174+
"TiProxy has no active connections, delete pod without waiting for the remaining graceful delay",
175+
"namespace", tiproxy.Namespace,
176+
"name", tiproxy.Name,
177+
)
178+
return true
179+
}
180+
134181
func ensureTiProxyMarkedUnhealthy(ctx context.Context, state State, c client.Client, logger logr.Logger) bool {
135182
tiproxy := state.Object()
136183

pkg/controllers/tiproxy/tasks/finalizer_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,11 @@ type testTiProxyHealthServer struct {
5656
mu sync.Mutex
5757
healthStatus int
5858
markUnhealthyStatus int
59+
metricsStatus int
60+
connectionCount int
5961
healthCalls int
6062
markUnhealthyCalls int
63+
metricsCalls int
6164
}
6265

6366
func newTestTiProxyHealthServer(t *testing.T, healthStatus, markUnhealthyStatus int) *testTiProxyHealthServer {
@@ -66,6 +69,8 @@ func newTestTiProxyHealthServer(t *testing.T, healthStatus, markUnhealthyStatus
6669
s := &testTiProxyHealthServer{
6770
healthStatus: healthStatus,
6871
markUnhealthyStatus: markUnhealthyStatus,
72+
metricsStatus: http.StatusOK,
73+
connectionCount: 1,
6974
}
7075

7176
mux := http.NewServeMux()
@@ -84,13 +89,35 @@ func newTestTiProxyHealthServer(t *testing.T, healthStatus, markUnhealthyStatus
8489
w.WriteHeader(s.healthStatus)
8590
_, _ = w.Write([]byte(`{"config_checksum":1}`))
8691
})
92+
mux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
93+
s.mu.Lock()
94+
defer s.mu.Unlock()
95+
s.metricsCalls++
96+
w.WriteHeader(s.metricsStatus)
97+
_, _ = w.Write([]byte(`# HELP tiproxy_server_connections Number of connections.
98+
# TYPE tiproxy_server_connections gauge
99+
tiproxy_server_connections ` + strconv.Itoa(s.connectionCount) + `
100+
`))
101+
})
87102
server := httptest.NewServer(mux)
88103
t.Cleanup(server.Close)
89104

90105
s.server = server
91106
return s
92107
}
93108

109+
func (s *testTiProxyHealthServer) setConnectionCount(connectionCount int) {
110+
s.mu.Lock()
111+
defer s.mu.Unlock()
112+
s.connectionCount = connectionCount
113+
}
114+
115+
func (s *testTiProxyHealthServer) setMetricsStatus(status int) {
116+
s.mu.Lock()
117+
defer s.mu.Unlock()
118+
s.metricsStatus = status
119+
}
120+
94121
func (s *testTiProxyHealthServer) port(t *testing.T) int32 {
95122
t.Helper()
96123
u, err := url.Parse(s.server.URL)
@@ -107,6 +134,12 @@ func (s *testTiProxyHealthServer) counts() (health, markUnhealthy int) {
107134
return s.healthCalls, s.markUnhealthyCalls
108135
}
109136

137+
func (s *testTiProxyHealthServer) metricsCount() int {
138+
s.mu.Lock()
139+
defer s.mu.Unlock()
140+
return s.metricsCalls
141+
}
142+
110143
func runDrainPodForDeleteTask(t *testing.T, ctx context.Context, s State, c client.Client) (task.Result, bool) {
111144
t.Helper()
112145

@@ -261,6 +294,55 @@ func TestTaskDrainPodForDeleteWaitForDeleteDelayAfterGracefulShutdownStarted(t *
261294
assert.True(t, actual.GetDeletionTimestamp().IsZero())
262295
}
263296

297+
func TestTaskDrainPodForDeleteDeletePodWhenConnectionsDropToZero(t *testing.T) {
298+
t.Parallel()
299+
300+
ctx := context.Background()
301+
server := newTestTiProxyHealthServer(t, http.StatusBadGateway, http.StatusOK)
302+
server.setConnectionCount(0)
303+
cluster := localTestCluster()
304+
tiproxy := deletingTiProxyWithAPIAddress("3600", server.port(t))
305+
pod := fakePod(cluster, tiproxy)
306+
pod.Annotations = map[string]string{
307+
v1alpha1.AnnoKeyTiProxyGracefulShutdownBeginTime: time.Now().Add(-10 * time.Second).Format(time.RFC3339Nano),
308+
}
309+
310+
fc := client.NewFakeClient(cluster, tiproxy, pod)
311+
res, done := runDrainPodForDeleteTask(t, ctx, &state{cluster: cluster, tiproxy: tiproxy}, fc)
312+
313+
assert.Equal(t, task.SRetry.String(), res.Status().String())
314+
assert.False(t, done)
315+
assert.Equal(t, 1, server.metricsCount())
316+
317+
err := fc.Get(ctx, client.ObjectKeyFromObject(pod), &corev1.Pod{})
318+
assert.True(t, apierrors.IsNotFound(err))
319+
}
320+
321+
func TestTaskDrainPodForDeleteContinueDeleteDelayWhenConnectionMetricFails(t *testing.T) {
322+
t.Parallel()
323+
324+
ctx := context.Background()
325+
server := newTestTiProxyHealthServer(t, http.StatusBadGateway, http.StatusOK)
326+
server.setMetricsStatus(http.StatusInternalServerError)
327+
cluster := localTestCluster()
328+
tiproxy := deletingTiProxyWithAPIAddress("3600", server.port(t))
329+
pod := fakePod(cluster, tiproxy)
330+
pod.Annotations = map[string]string{
331+
v1alpha1.AnnoKeyTiProxyGracefulShutdownBeginTime: time.Now().Add(-10 * time.Second).Format(time.RFC3339Nano),
332+
}
333+
334+
fc := client.NewFakeClient(cluster, tiproxy, pod)
335+
res, done := runDrainPodForDeleteTask(t, ctx, &state{cluster: cluster, tiproxy: tiproxy}, fc)
336+
337+
assert.Equal(t, task.SRetry.String(), res.Status().String())
338+
assert.False(t, done)
339+
assert.Equal(t, 1, server.metricsCount())
340+
341+
actual := &corev1.Pod{}
342+
require.NoError(t, fc.Get(ctx, client.ObjectKeyFromObject(pod), actual))
343+
assert.True(t, actual.GetDeletionTimestamp().IsZero())
344+
}
345+
264346
func TestTaskDrainPodForDeleteDeletePodWithoutExtendingGracePeriodAfterDelay(t *testing.T) {
265347
t.Parallel()
266348

pkg/tiproxyapi/v1/client.go

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,22 @@ import (
2020
"crypto/tls"
2121
"encoding/json"
2222
"fmt"
23+
"io"
2324
"net"
2425
"net/http"
2526
"time"
2627

2728
"github.com/BurntSushi/toml"
29+
"github.com/prometheus/common/expfmt"
2830

2931
httputil "github.com/pingcap/tidb-operator/v2/pkg/utils/http"
3032
)
3133

3234
const (
33-
healthPath = "api/debug/health"
34-
configPath = "api/admin/config"
35+
healthPath = "api/debug/health"
36+
configPath = "api/admin/config"
37+
metricsPath = "metrics"
38+
tiproxyConnectionsMetricName = "tiproxy_server_connections"
3539
)
3640

3741
type healthOverrideRequest struct {
@@ -45,6 +49,8 @@ type TiProxyClient interface {
4549
IsHealthy(ctx context.Context) (bool, error)
4650
// MarkUnhealthy makes the TiProxy health endpoint report unhealthy.
4751
MarkUnhealthy(ctx context.Context) error
52+
// ConnectionCount returns the current number of TiProxy SQL client connections.
53+
ConnectionCount(ctx context.Context) (float64, error)
4854
// SetLabels sets the labels for TiProxy.
4955
SetLabels(ctx context.Context, labels map[string]string) error
5056
}
@@ -115,6 +121,55 @@ func (c *tiproxyClient) MarkUnhealthy(ctx context.Context) error {
115121
return err
116122
}
117123

124+
func (c *tiproxyClient) ConnectionCount(ctx context.Context) (float64, error) {
125+
apiURL := fmt.Sprintf("%s/%s", c.url, metricsPath)
126+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, http.NoBody)
127+
if err != nil {
128+
return 0, err
129+
}
130+
131+
//nolint:bodyclose,gosec // bodyclose: has been handled; gosec: URL is constructed from trusted internal config
132+
resp, err := c.httpClient.Do(req)
133+
if err != nil {
134+
return 0, err
135+
}
136+
defer httputil.DeferClose(resp.Body)
137+
138+
if resp.StatusCode >= http.StatusBadRequest {
139+
return 0, fmt.Errorf("get TiProxy metrics failed: status %d", resp.StatusCode)
140+
}
141+
142+
return parseConnectionCount(resp.Body)
143+
}
144+
145+
func parseConnectionCount(metrics io.Reader) (float64, error) {
146+
parser := expfmt.TextParser{}
147+
metricFamilies, err := parser.TextToMetricFamilies(metrics)
148+
if err != nil {
149+
return 0, fmt.Errorf("parse TiProxy metrics failed: %w", err)
150+
}
151+
152+
mf, ok := metricFamilies[tiproxyConnectionsMetricName]
153+
if !ok {
154+
return 0, fmt.Errorf("metric %s not found", tiproxyConnectionsMetricName)
155+
}
156+
157+
var count float64
158+
var found bool
159+
for _, metric := range mf.GetMetric() {
160+
if metric.Gauge == nil {
161+
continue
162+
}
163+
count += metric.Gauge.GetValue()
164+
found = true
165+
}
166+
if !found {
167+
return 0, fmt.Errorf("metric %s has no gauge value", tiproxyConnectionsMetricName)
168+
}
169+
170+
return count, nil
171+
}
172+
118173
func (c *tiproxyClient) SetLabels(ctx context.Context, labels map[string]string) error {
119174
type labelConfig struct {
120175
Labels map[string]string `toml:"labels"`

pkg/tiproxyapi/v1/client_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,22 @@ func TestTiProxyClient_MarkUnhealthy(t *testing.T) {
156156
err := client.MarkUnhealthy(context.Background())
157157
require.NoError(t, err)
158158
}
159+
160+
func TestTiProxyClient_ConnectionCount(t *testing.T) {
161+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
162+
assert.Equal(t, "/metrics", r.URL.Path)
163+
assert.Equal(t, http.MethodGet, r.Method)
164+
_, err := w.Write([]byte(`# HELP tiproxy_server_connections Number of connections.
165+
# TYPE tiproxy_server_connections gauge
166+
tiproxy_server_connections 7
167+
`))
168+
assert.NoError(t, err)
169+
}))
170+
defer server.Close()
171+
172+
addr := stringutil.RemoveHTTPPrefix(server.URL)
173+
client := NewTiProxyClient(addr, 5*time.Second, nil)
174+
count, err := client.ConnectionCount(context.Background())
175+
require.NoError(t, err)
176+
assert.InDelta(t, 7, count, 0)
177+
}

0 commit comments

Comments
 (0)