@@ -24,6 +24,7 @@ use std::net::{IpAddr, SocketAddr};
2424use std:: path:: PathBuf ;
2525use std:: pin:: Pin ;
2626use std:: sync:: Arc ;
27+ use std:: sync:: atomic:: { AtomicBool , AtomicU32 , Ordering } ;
2728use std:: time:: { Duration , Instant } ;
2829
2930use anyhow:: Context ;
@@ -46,7 +47,7 @@ use mz_dyncfg::ConfigSet;
4647use mz_frontegg_auth:: Authenticator as FronteggAuthentication ;
4748use mz_ore:: cast:: CastFrom ;
4849use mz_ore:: id_gen:: conn_id_org_uuid;
49- use mz_ore:: metrics:: { ComputedGauge , IntCounter , IntGauge , MetricsRegistry } ;
50+ use mz_ore:: metrics:: { ComputedGauge , ComputedUIntGauge , IntCounter , IntGauge , MetricsRegistry } ;
5051use mz_ore:: netio:: AsyncReady ;
5152use mz_ore:: now:: { NowFn , SYSTEM_TIME , epoch_to_uuid_v7} ;
5253use mz_ore:: task:: { JoinSetExt , spawn} ;
@@ -72,13 +73,13 @@ use tokio_metrics::TaskMetrics;
7273use tokio_openssl:: SslStream ;
7374use tokio_postgres:: error:: SqlState ;
7475use tower:: Service ;
75- use tracing:: { debug, error, warn} ;
76+ use tracing:: { debug, error, info , warn} ;
7677use uuid:: Uuid ;
7778
7879use crate :: codec:: { BackendMessage , FramedConn } ;
7980use crate :: dyncfgs:: {
80- INJECT_PROXY_PROTOCOL_HEADER_HTTP , SIGTERM_CONNECTION_WAIT , SIGTERM_LISTEN_WAIT ,
81- has_tracing_config_update, tracing_config,
81+ INJECT_PROXY_PROTOCOL_HEADER_HTTP , MAX_CONNECTIONS , SIGTERM_CONNECTION_WAIT ,
82+ SIGTERM_LISTEN_WAIT , has_tracing_config_update, tracing_config,
8283} ;
8384
8485/// Balancer build information.
@@ -315,6 +316,7 @@ impl BalancerService {
315316 } ;
316317
317318 let metrics = ServerMetricsConfig :: register_into ( & self . cfg . metrics_registry ) ;
319+ let limiter = ConnectionLimiter :: new ( & self . cfg . metrics_registry , self . configs . clone ( ) ) ;
318320
319321 let mut set = JoinSet :: new ( ) ;
320322 let mut server_handles = Vec :: new ( ) ;
@@ -338,6 +340,7 @@ impl BalancerService {
338340 tls : pgwire_tls,
339341 internal_tls : self . cfg . internal_tls ,
340342 metrics : ServerMetrics :: new ( metrics. clone ( ) , "pgwire" ) ,
343+ limiter : Arc :: clone ( & limiter) ,
341344 now : SYSTEM_TIME . clone ( ) ,
342345 } ;
343346 let ( handle, stream) = self . pgwire ;
@@ -370,6 +373,7 @@ impl BalancerService {
370373 resolve_template : Arc :: from ( addr) ,
371374 port,
372375 metrics : Arc :: from ( ServerMetrics :: new ( metrics, "https" ) ) ,
376+ limiter,
373377 configs : self . configs . clone ( ) ,
374378 internal_tls : self . cfg . internal_tls ,
375379 } ;
@@ -613,6 +617,91 @@ impl ServerMetrics {
613617 }
614618}
615619
620+ /// Ceiling on the number of client connections proxied at once, shared by the pgwire and HTTPS
621+ /// listeners.
622+ ///
623+ /// balancerd's memory use scales with the number of connections it proxies. Without a ceiling it
624+ /// keeps accepting until the container is OOM killed, which drops every established connection.
625+ /// Refusing new connections instead sheds only the load we cannot serve.
626+ #[ derive( Debug ) ]
627+ struct ConnectionLimiter {
628+ configs : ConfigSet ,
629+ active : AtomicU32 ,
630+ /// Whether connections are currently being refused, so that reaching and clearing the limit
631+ /// are logged once each rather than once per connection.
632+ limited : AtomicBool ,
633+ rejected : IntCounter ,
634+ _limit : ComputedUIntGauge ,
635+ }
636+
637+ impl ConnectionLimiter {
638+ fn new ( registry : & MetricsRegistry , configs : ConfigSet ) -> Arc < Self > {
639+ let rejected = registry. register ( metric ! (
640+ name: "mz_balancer_connection_rejected_total" ,
641+ help: "Count of connections refused because the connection limit was reached." ,
642+ ) ) ;
643+ let limit = registry. register_computed_gauge (
644+ metric ! (
645+ name: "mz_balancer_connection_limit" ,
646+ help: "Maximum number of connections proxied at once, 0 if unlimited." ,
647+ ) ,
648+ {
649+ let configs = configs. clone ( ) ;
650+ move || u64:: from ( MAX_CONNECTIONS . get ( & configs) )
651+ } ,
652+ ) ;
653+ Arc :: new ( ConnectionLimiter {
654+ configs,
655+ active : AtomicU32 :: new ( 0 ) ,
656+ limited : AtomicBool :: new ( false ) ,
657+ rejected,
658+ _limit : limit,
659+ } )
660+ }
661+
662+ /// Reserves capacity for one connection, or returns `None` if the limit has been reached, in
663+ /// which case the caller must refuse the connection.
664+ fn acquire ( self : & Arc < Self > ) -> Option < ConnectionGuard > {
665+ let limit = MAX_CONNECTIONS . get ( & self . configs ) ;
666+ let unlimited = limit == 0 ;
667+ match self
668+ . active
669+ . fetch_update ( Ordering :: SeqCst , Ordering :: SeqCst , |active| {
670+ ( unlimited || active < limit) . then ( || active + 1 )
671+ } ) {
672+ Ok ( prev) => {
673+ // Only clear the limited state once comfortably below the limit, so that
674+ // connections churning at the ceiling do not flap the log lines.
675+ if unlimited || prev + 1 <= limit - limit / 10 {
676+ if self . limited . swap ( false , Ordering :: Relaxed ) {
677+ info ! ( "accepting new connections again, limit is {limit}" ) ;
678+ }
679+ }
680+ Some ( ConnectionGuard ( Arc :: clone ( self ) ) )
681+ }
682+ Err ( active) => {
683+ self . rejected . inc ( ) ;
684+ if !self . limited . swap ( true , Ordering :: Relaxed ) {
685+ warn ! (
686+ "refusing new connections: at the limit of {limit} connections \
687+ ({active} active)"
688+ ) ;
689+ }
690+ None
691+ }
692+ }
693+ }
694+ }
695+
696+ /// Releases the connection reserved by [`ConnectionLimiter::acquire`] when dropped.
697+ struct ConnectionGuard ( Arc < ConnectionLimiter > ) ;
698+
699+ impl Drop for ConnectionGuard {
700+ fn drop ( & mut self ) {
701+ self . 0 . active . fetch_sub ( 1 , Ordering :: SeqCst ) ;
702+ }
703+ }
704+
616705pub enum CancellationResolver {
617706 Directory ( PathBuf ) ,
618707 Static ( String ) ,
@@ -624,6 +713,7 @@ struct PgwireBalancer {
624713 cancellation_resolver : Arc < CancellationResolver > ,
625714 resolver : Arc < BalancerResolver > ,
626715 metrics : ServerMetrics ,
716+ limiter : Arc < ConnectionLimiter > ,
627717 now : NowFn ,
628718}
629719
@@ -637,6 +727,7 @@ impl PgwireBalancer {
637727 tls_mode : Option < TlsMode > ,
638728 internal_tls : bool ,
639729 metrics : & ServerMetrics ,
730+ limiter : & Arc < ConnectionLimiter > ,
640731 ) -> Result < ( ) , io:: Error >
641732 where
642733 A : AsyncRead + AsyncWrite + AsyncReady + Send + Sync + Unpin ,
@@ -663,6 +754,15 @@ impl PgwireBalancer {
663754 return conn. send ( err) . await ;
664755 }
665756
757+ let Some ( _conn_guard) = limiter. acquire ( ) else {
758+ return conn
759+ . send ( ErrorResponse :: fatal (
760+ SqlState :: TOO_MANY_CONNECTIONS ,
761+ "balancer is at its connection limit" ,
762+ ) )
763+ . await ;
764+ } ;
765+
666766 let resolved = match resolver. resolve ( conn, user, metrics) . await {
667767 Ok ( v) => v,
668768 Err ( err) => {
@@ -837,6 +937,7 @@ impl mz_server_core::Server for PgwireBalancer {
837937 let resolver = Arc :: clone ( & self . resolver ) ;
838938 let inner_metrics = self . metrics . clone ( ) ;
839939 let outer_metrics = self . metrics . clone ( ) ;
940+ let limiter = Arc :: clone ( & self . limiter ) ;
840941 let cancellation_resolver = Arc :: clone ( & self . cancellation_resolver ) ;
841942 let conn_uuid = epoch_to_uuid_v7 ( & ( self . now ) ( ) ) ;
842943 let peer_addr = conn. peer_addr ( ) ;
@@ -910,6 +1011,7 @@ impl mz_server_core::Server for PgwireBalancer {
9101011 tls. map ( |tls| tls. mode ) ,
9111012 internal_tls,
9121013 & inner_metrics,
1014+ & limiter,
9131015 )
9141016 . await ?;
9151017 conn. flush ( ) . await ?;
@@ -1107,12 +1209,31 @@ async fn cancel_request(
11071209 }
11081210}
11091211
1212+ /// Writes an HTTP error response to a client and closes the connection.
1213+ ///
1214+ /// The proxied connections are raw TCP streams, so the HTTP framing has to be written by hand.
1215+ /// Errors are ignored: the connection is going away regardless.
1216+ async fn send_http_error ( client_stream : & mut Box < dyn ClientStream > , status : & str , body : & str ) {
1217+ let response = format ! (
1218+ "HTTP/1.1 {status}\r \n \
1219+ Content-Type: text/plain\r \n \
1220+ Content-Length: {}\r \n \
1221+ Connection: close\r \n \
1222+ \r \n \
1223+ {body}",
1224+ body. len( ) ,
1225+ ) ;
1226+ let _ = client_stream. write_all ( response. as_bytes ( ) ) . await ;
1227+ let _ = client_stream. shutdown ( ) . await ;
1228+ }
1229+
11101230struct HttpsBalancer {
11111231 resolver : Arc < TenantDnsResolver > ,
11121232 tls : Option < ReloadingSslContext > ,
11131233 resolve_template : Arc < str > ,
11141234 port : u16 ,
11151235 metrics : Arc < ServerMetrics > ,
1236+ limiter : Arc < ConnectionLimiter > ,
11161237 configs : ConfigSet ,
11171238 internal_tls : bool ,
11181239}
@@ -1203,6 +1324,7 @@ impl mz_server_core::Server for HttpsBalancer {
12031324 let port = self . port ;
12041325 let inner_metrics = Arc :: clone ( & self . metrics ) ;
12051326 let outer_metrics = Arc :: clone ( & self . metrics ) ;
1327+ let limiter = Arc :: clone ( & self . limiter ) ;
12061328 let peer_addr = conn. peer_addr ( ) ;
12071329 let inject_proxy_headers = INJECT_PROXY_PROTOCOL_HEADER_HTTP . get ( & self . configs ) ;
12081330 Box :: pin ( async move {
@@ -1231,6 +1353,16 @@ impl mz_server_core::Server for HttpsBalancer {
12311353 }
12321354 _ => ( Box :: new ( conn) , None ) ,
12331355 } ;
1356+ let Some ( _conn_guard) = limiter. acquire ( ) else {
1357+ send_http_error (
1358+ & mut client_stream,
1359+ "503 Service Unavailable" ,
1360+ "balancer is at its connection limit" ,
1361+ )
1362+ . await ;
1363+ return Ok ( ( ) ) ;
1364+ } ;
1365+
12341366 let resolved =
12351367 Self :: resolve ( & resolver, & resolve_template, port, servername. as_deref ( ) )
12361368 . await ?;
@@ -1242,24 +1374,12 @@ impl mz_server_core::Server for HttpsBalancer {
12421374 Ok ( stream) => stream,
12431375 Err ( e) => {
12441376 error ! ( "failed to connect to upstream server: {e}" ) ;
1245- let body = "upstream server not available" ;
1246- // We know this is an HTTPs stream (see name
1247- // HttpsBalancer), but we actually don't care what type
1248- // of traffic it is and we only use raw tcp streams.In
1249- // order to respond with HTTP we have to write this as a
1250- // raw http message.
1251- let response = format ! (
1252- "HTTP/1.1 502 Bad Gateway\r \n \
1253- Content-Type: text/plain\r \n \
1254- Content-Length: {}\r \n \
1255- Connection: close\r \n \
1256- \r \n \
1257- {}",
1258- body. len( ) ,
1259- body
1260- ) ;
1261- let _ = client_stream. write_all ( response. as_bytes ( ) ) . await ;
1262- let _ = client_stream. shutdown ( ) . await ;
1377+ send_http_error (
1378+ & mut client_stream,
1379+ "502 Bad Gateway" ,
1380+ "upstream server not available" ,
1381+ )
1382+ . await ;
12631383 return Ok ( ( ) ) ;
12641384 }
12651385 } ;
@@ -1654,6 +1774,8 @@ struct ResolvedAddr {
16541774
16551775#[ cfg( test) ]
16561776mod tests {
1777+ use mz_dyncfg:: ConfigUpdates ;
1778+
16571779 use super :: * ;
16581780
16591781 #[ mz_ore:: test]
@@ -1726,4 +1848,35 @@ mod tests {
17261848 assert_eq ! ( strip_ipv6_brackets( "[unclosed" ) , "[unclosed" ) ;
17271849 assert_eq ! ( strip_ipv6_brackets( "unopened]" ) , "unopened]" ) ;
17281850 }
1851+
1852+ #[ mz_ore:: test]
1853+ fn test_connection_limiter ( ) {
1854+ let configs = dyncfgs:: all_dyncfgs ( ConfigSet :: default ( ) ) ;
1855+ let set_max = |max : u32 | {
1856+ let mut updates = ConfigUpdates :: default ( ) ;
1857+ updates. add ( & MAX_CONNECTIONS , max) ;
1858+ updates. apply ( & configs) ;
1859+ } ;
1860+
1861+ set_max ( 2 ) ;
1862+ let limiter = ConnectionLimiter :: new ( & MetricsRegistry :: new ( ) , configs. clone ( ) ) ;
1863+ let first = limiter. acquire ( ) . expect ( "under the limit" ) ;
1864+ let second = limiter. acquire ( ) . expect ( "at the limit" ) ;
1865+ assert ! ( limiter. acquire( ) . is_none( ) ) ;
1866+ assert_eq ! ( limiter. rejected. get( ) , 1 ) ;
1867+
1868+ // A connection closing frees capacity for a new one.
1869+ drop ( second) ;
1870+ let _third = limiter. acquire ( ) . expect ( "capacity freed" ) ;
1871+ drop ( first) ;
1872+
1873+ // Lowering the limit below the current count refuses new connections but leaves the
1874+ // established ones alone.
1875+ set_max ( 1 ) ;
1876+ assert ! ( limiter. acquire( ) . is_none( ) ) ;
1877+
1878+ // Zero disables the limit.
1879+ set_max ( 0 ) ;
1880+ assert ! ( limiter. acquire( ) . is_some( ) ) ;
1881+ }
17291882}
0 commit comments