Repository navigation
Conversation
oowl
force-pushed
the
feat/balancer-keepalive-pool-size
branch
from
October 23, 2026 08:38
2eadc02 to
09e1df5
Compare
oowl
added a commit
to oowl/lua-resty-core
that referenced
this pull request
Oct 23, 2026
The C side of the pool_size argument lives in openresty/lua-nginx-module#2536 and is not on master yet, so CI must be built against that branch for the FFI signature to match. Drop this commit before merging, once #2536 is merged.
oowl
force-pushed
the
feat/balancer-keepalive-pool-size
branch
from
October 24, 2026 06:00
09e1df5 to
4546d94
Compare
oowl
force-pushed
the
feat/balancer-keepalive-pool-size
branch
from
October 25, 2026 16:26
a283b01 to
4169625
Compare
oowl
added a commit
to oowl/lua-resty-core
that referenced
this pull request
Oct 25, 2026
The C side of the pool_size argument lives in openresty/lua-nginx-module#2536 and is not on master yet, so CI must be built against that branch for the FFI signature to match. Drop this commit before merging, once #2536 is merged.
The balancer keepalive cache has a single capacity per upstream {} block,
set by balancer_keepalive (32 by default), and evicts the least recently
used idle connection regardless of which pool it belongs to. When many
pools share one upstream, as is typical when peers are picked dynamically
in balancer_by_lua*, a burst to one peer can push out the idle connections
of every other peer.
ngx_http_lua_ffi_balancer_enable_keepalive() now takes a pool_size
argument. A pool is the same key the cache already matches on for reuse:
the peer address, the host given to set_current_peer() and the local
address. When a connection is about to be cached and its pool already
holds pool_size idle connections, the least recently used idle connection
of that same pool is closed and its slot reused, instead of taking a slot
from the global LRU. pool_size 0 keeps the previous behaviour, and
pool_size never raises the balancer_keepalive total.
To keep this O(1), the hash buckets now hold one descriptor per pool
instead of the cached connections themselves. A descriptor keeps the
pool's idle connections (most recently used first) and their count, so
reusing a connection, checking whether a pool is full and evicting its
oldest connection no longer scan the other cached connections. Scanning
the bucket costs about 10ns per idle connection of the pool, i.e. ~5us
per released connection with a full pool of 512. Descriptors are
preallocated next to the cache items, one per item, as a pool only exists
while it holds idle connections.
The key comparison used by the reuse path is factored out into
ngx_http_lua_balancer_item_match().
This changes the FFI signature, lua-resty-core needs to be updated
accordingly.
oowl
force-pushed
the
feat/balancer-keepalive-pool-size
branch
from
October 25, 2026 16:43
4169625 to
d3fc3c1
Compare
oowl
added a commit
to oowl/lua-resty-core
that referenced
this pull request
Oct 25, 2026
The C side of the pool_size argument lives in openresty/lua-nginx-module#2536 and is not on master yet, so CI must be built against that branch for the FFI signature to match. Drop this commit before merging, once #2536 is merged.
oowl
force-pushed
the
feat/balancer-keepalive-pool-size
branch
from
October 27, 2026 08:27
d3fc3c1 to
d337206
Compare
This was referenced Oct 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related PRs
pool_sizein the balancer keepalive FFI API, per-pool descriptorsbalancer.enable_keepalive(idle_timeout?, max_requests?, pool_size?)ngx_http_lua_versionto 10033Suggested merge order:
bugfix: send the HUP mode signals from perl instead of running kill(1). test-nginx#190, then drop theDone: in #190 is merged and feature: added the pool_size argument to the balancer keepalive FFI API. #2536 no longer carries a CI commit.TEMPORARYCI commit from feature: added the pool_size argument to the balancer keepalive FFI API. #2536.TEMPORARYCI commits of change: bumped ngx_http_lua_version to 10033. #2540 and Modify response header when parsing response body #547 when merging. Drop theTEMPORARYCI commit from feature: ngx.balancer: added the pool_size argument to enable_keepalive(). lua-resty-core#544 once feature: added the pool_size argument to the balancer keepalive FFI API. #2536 is on master.Problem
The
balancer_by_lua*keepalive cache has a single capacity perupstream {}block (balancer_keepalive, 32 by default) and evicts the least recently used idle connection, no matter which pool it belongs to. You cannot put a limit on one pool. When many pools share an upstream, which is common when peers are picked dynamically inbalancer_by_lua*, a burst of traffic to one peer can push out the idle connections of every other peer.Change
ngx_http_lua_ffi_balancer_enable_keepalive()gets a newpool_sizeargument.hostgiven toset_current_peer(), and the local address (pc->local, so bothbind_to_local_addr()andproxy_bindare covered).pool_sizeidle connections, that pool's own least recently used idle connection is closed and its slot is reused. The global LRU slot is left alone.pool_size == 0keeps the current behaviour.pool_sizenever raises thebalancer_keepalivetotal. It only limits one pool inside that total.Per-pool descriptors (O(1))
A first version counted a pool's idle connections by scanning its hash bucket. I measured about 10ns per idle connection of the pool, so with a full pool of 512 (Kong's default size) each released connection cost about 5us. To avoid that, the hash buckets now hold one small descriptor per pool instead of the cached connections themselves:
ngx_http_lua_balancer_ka_pool_t) keeps the pool's idle connections, most recently used first, and their count.balancer_keepalivetotal, exact key matching throughngx_http_lua_balancer_item_match(), and reuse of the most recently used connection.Compatibility
The FFI signature changes, so this needs the matching lua-resty-core PR: openresty/lua-resty-core#544. The two PRs should be merged together.
Tests
t/195-balancer-keepalive-pool-size.t(new) calls the C function through an explicit 5-argument prototype (ffi.cast). The tests therefore run with either the current or the updated lua-resty-core. Assertions check which backend connection serves each request (ngx.var.connectionon the backend). They do not rely on debug logs.pool_size = 1. Of 3 connections released into one pool, only 1 stays idle.balancer_keepalive 2, a full pool evicts its own oldest connection. Another pool's idle connection is left alone.pool_size = 0shows the old behaviour, where the other pool's connection is evicted.errwith the new signature (no current peer set).balancer_keepalive 4, with sequential and concurrent requests. This stresses descriptor allocation and release. A counting bug trips an assertion, and breaking the count indetachwas verified to fail this test.pool_sizeconnections evicts nothing (boundary).pool_sizelarger thanbalancer_keepalivenever raises the total. The global LRU still applies.bind_to_local_addr) splits pools, and the reused connection really comes from the bound address.max_requestsis not cached, and the pool keeps working.Mutation check: I built nginx with one deliberate bug at a time. Each one makes at least one test fail: ignore the host in the pool key (TEST 2, 9), ignore the peer address (TEST 9), ignore the local address (TEST 10), off-by-one in the "pool full" check (TEST 7), and not decrementing the pool count (TEST 5, assertion).
TEST 1 and 2 fail if
pool_sizeis changed to 0. Note that the upstream blocks needkeepalive 0;, because otherwise nginx's own upstream keepalive module (enabled by default in recent nginx) takes the connection before the lua balancer sees it.The Lua API side and its tests are in openresty/lua-resty-core#544 (
t/balancer-keepalive.tTEST 29–33,t/balancer-keepalive-localaddr.tTEST 8).Ran locally against nginx 1.31.8 built with this branch, both as a normal
--with-debugbuild and as an ASan build (NGX_BUILD_ASAN=1):t/balancer*.t,t/stream/balancer*.t: all passt/195-balancer-keepalive-pool-size.t,t/138-balancer.t,t/138-balancer-upstream-bind.t,t/188-balancer_keepalive_pool_max_retry.t: all pass, with both lua-resty-core master and the optimize: reduce memory alloc when get peer from socket_pool #544 brancht/195-balancer-keepalive-pool-size.tandt/balancer-keepalive.t(lua-resty-core) each passed 10 runs in a row withTEST_NGINX_RANDOMIZE=1-DNGX_LUA_USE_ASSERT, as CI does, so the descriptor assertions are activengx-relengoutput is identical to masterCI
This PR no longer carries a temporary CI commit. The ASan job hang in the HUP tests was fixed in Test::Nginx by openresty/test-nginx#190, which is now merged, so CI uses openresty/test-nginx master again.