Skip to content

Commit a1ca6a0

Browse files
authored
Merge pull request #5819 from sysown/v3.0_partition-gate
perf: session-partition gate (supersedes #5776 and #5799)
2 parents 46ab989 + 9d9ede1 commit a1ca6a0

12 files changed

Lines changed: 169 additions & 46 deletions

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export SOURCE_DATE_EPOCH
132132
### rebuild SQLite with -USQLITE_ENABLE_MEMORY_MANAGEMENT in deps/Makefile
133133

134134
O0 := -O0
135-
O2 := -O2
135+
O2 := -O2 -fno-omit-frame-pointer
136136
O1 := -O1
137137
O3 := -O3 -mtune=native
138138

include/Base_Thread.h

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,39 @@ class Base_Thread {
4747

4848
private:
4949
bool maintenance_loop;
50-
public:
50+
51+
// Partition-gate state. note_pool_attempt() bumps the counters from the
52+
// get_MyConn_from_pool() call site; update_partition_gate() consumes them
53+
// once per outer process_all_sessions iteration. Single-threaded per worker.
54+
unsigned int partition_pool_attempts = 0;
55+
unsigned int partition_pool_nulls = 0;
56+
unsigned int partition_streak = 0;
57+
bool partition_active = false;
58+
59+
public:
60+
// Gate thresholds: NULL-ratio (NUM/DEN) classifies a tick as "stressed";
61+
// STREAK is the number of consecutive disagreeing ticks required to flip
62+
// the gate state (hysteresis filter against transient bursts).
63+
static constexpr unsigned int PARTITION_GATE_NULL_RATIO_NUM = 1;
64+
static constexpr unsigned int PARTITION_GATE_NULL_RATIO_DEN = 20; // 5%
65+
static constexpr unsigned int PARTITION_GATE_STREAK = 3;
66+
// Below this attempt count a tick carries no signal: gate state and
67+
// streak are left untouched. Avoids "2/2 NULL = 100% stressed" noise.
68+
static constexpr unsigned int PARTITION_GATE_MIN_ATTEMPTS = 4;
69+
70+
// Called by sessions inside this worker at the get_MyConn_from_pool()
71+
// call site to feed the gate.
72+
inline void note_pool_attempt(bool was_null) {
73+
++partition_pool_attempts;
74+
if (was_null) ++partition_pool_nulls;
75+
}
76+
77+
// Runs the hysteresis state machine from per-tick counters, resets them,
78+
// and returns whether the partition pass should run. MUST be called every
79+
// outer iteration of process_all_sessions (even when the partition path
80+
// is otherwise skipped) so the counters don't accumulate stale.
81+
bool update_partition_gate();
82+
5183
unsigned long long curtime;
5284
unsigned long long last_move_to_idle_thread_time;
5385
bool epoll_thread;
@@ -65,7 +97,7 @@ class Base_Thread {
6597
template<typename T>
6698
void check_for_invalid_fd(unsigned int n);
6799
template<typename S>
68-
void ProcessAllSessions_SortingSessions();
100+
void ProcessAllSessions_Partition();
69101
template<typename T>
70102
void ProcessAllMyDS_AfterPoll();
71103
template<typename T>

include/MySQL_Thread.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ class __attribute__((aligned(64))) MySQL_Thread : public Base_Thread
114114
bool retrieve_gtids_required; // if any of the servers has gtid_port enabled, this needs to be turned on too
115115

116116
PtrArray *cached_connections;
117+
unsigned int push_local_counter; // round-robin counter for bounded local caching: cache 1-in-N where N = mysql_threads
117118

118119
#ifdef IDLE_THREADS
119120
struct epoll_event events[MY_EPOLL_THREAD_MAXEVENTS];

include/PgSQL_Thread.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ class __attribute__((aligned(64))) PgSQL_Thread : public Base_Thread
160160
//bool maintenance_loop;
161161

162162
PtrArray* cached_connections;
163+
unsigned int push_local_counter; // round-robin counter for bounded local caching: cache 1-in-N where N = pgsql_threads
163164

164165
#ifdef IDLE_THREADS
165166
struct epoll_event events[MY_EPOLL_THREAD_MAXEVENTS];

lib/Base_Thread.cpp

Lines changed: 79 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
// Explicitly instantiate the required template class and member functions
1212
template MySQL_Session* Base_Thread::create_new_session_and_client_data_stream<MySQL_Thread, MySQL_Session*>(int);
1313
template PgSQL_Session* Base_Thread::create_new_session_and_client_data_stream<PgSQL_Thread, PgSQL_Session*>(int);
14-
template void Base_Thread::ProcessAllSessions_SortingSessions<MySQL_Session>();
15-
template void Base_Thread::ProcessAllSessions_SortingSessions<PgSQL_Session>();
14+
template void Base_Thread::ProcessAllSessions_Partition<MySQL_Session>();
15+
template void Base_Thread::ProcessAllSessions_Partition<PgSQL_Session>();
1616
template void Base_Thread::ProcessAllMyDS_AfterPoll<MySQL_Thread>();
1717
template void Base_Thread::ProcessAllMyDS_AfterPoll<PgSQL_Thread>();
1818
template void Base_Thread::ProcessAllMyDS_BeforePoll<MySQL_Thread>();
@@ -34,6 +34,29 @@ Base_Thread::Base_Thread() :
3434
Base_Thread::~Base_Thread() {
3535
};
3636

37+
bool Base_Thread::update_partition_gate() {
38+
// 64-bit so the multiplications below cannot overflow unsigned int.
39+
const uint64_t attempts = partition_pool_attempts;
40+
const uint64_t nulls = partition_pool_nulls;
41+
partition_pool_attempts = 0;
42+
partition_pool_nulls = 0;
43+
44+
// Low-volume ticks carry no signal; leave gate and streak unchanged.
45+
if (attempts < PARTITION_GATE_MIN_ATTEMPTS) {
46+
return partition_active;
47+
}
48+
49+
const bool stressed = (nulls * PARTITION_GATE_NULL_RATIO_DEN
50+
>= attempts * PARTITION_GATE_NULL_RATIO_NUM);
51+
if (stressed == partition_active) {
52+
partition_streak = 0;
53+
} else if (++partition_streak >= PARTITION_GATE_STREAK) {
54+
partition_active = stressed;
55+
partition_streak = 0;
56+
}
57+
return partition_active;
58+
}
59+
3760
template<typename T, typename S>
3861
void Base_Thread::register_session(T thr, S _sess, bool up_start) {
3962
if (mysql_sessions==NULL) {
@@ -230,34 +253,64 @@ void Base_Thread::check_for_invalid_fd(unsigned int n) {
230253
}
231254
}
232255

233-
// this function was inline in MySQL_Thread::process_all_sessions()
256+
234257
/**
235-
* @brief Sort all sessions based on maximum connection time.
236-
*
237-
* This function iterates through all MySQL sessions and sorts them based on their maximum connection time.
238-
* Sessions with a valid maximum connection time are compared, and if one session has a greater maximum connection
239-
* time than another, their positions in the session list are swapped. The sorting is performed in-place.
240-
*
241-
* @note This function assumes that MySQL sessions and their associated data structures have been initialized
242-
* and are accessible within the MySQL Thread.
258+
* @brief Partition all sessions into three blocks by backend state.
259+
*
260+
* Block layout produced in mysql_sessions->pdata:
261+
* [0, running_end) block A - running a query against the backend
262+
* (myconn != NULL, mct == 0, status != WAITING_CLIENT_DATA)
263+
* [running_end, idle_begin) block B - acquiring/awaiting a backend
264+
* (mct != 0)
265+
* [idle_begin, len) block C - idle, or holds-conn-but-WAITING_CLIENT_DATA
266+
*
267+
* Block A drives the backend and may release its conn at end-of-query, giving
268+
* block B sessions a fairness chance to acquire it. Sessions parked in
269+
* WAITING_CLIENT_DATA (idle in a transaction after BEGIN) hold the conn but
270+
* cannot release it until the client sends the next packet, so they live in C.
271+
*
272+
* Classification tests max_connect_time first: it must win over A even when
273+
* myconn != NULL, to catch CHANGING_USER_SERVER on pooled connections and the
274+
* post-error retry path where the old conn hasn't been destroyed yet.
275+
*
276+
* Single O(n) pass, in place. idx walks up, idle_begin walks down, they meet
277+
* and terminate. A previous Lomuto-style sort of the B band by max_connect_time
278+
* was removed: measurement showed it hurt throughput by ~12% at 500 clients /
279+
* 50-conn pool under SSL, without a corresponding tail-latency benefit. If
280+
* reintroduced, it should be gated on an explicit starvation-age signal rather
281+
* than run unconditionally on every iteration.
243282
*/
244283
template<typename S>
245-
void Base_Thread::ProcessAllSessions_SortingSessions() {
246-
unsigned int a=0;
247-
for (unsigned int n=0; n<mysql_sessions->len; n++) {
248-
S *sess=(S *)mysql_sessions->index(n);
249-
if (sess->mybe && sess->mybe->server_myds) {
250-
if (sess->mybe->server_myds->max_connect_time) {
251-
S *sess2=(S *)mysql_sessions->index(a);
252-
if (sess2->mybe && sess2->mybe->server_myds && sess2->mybe->server_myds->max_connect_time && sess2->mybe->server_myds->max_connect_time <= sess->mybe->server_myds->max_connect_time) {
253-
// do nothing
254-
} else {
255-
void *p=mysql_sessions->pdata[a];
256-
mysql_sessions->pdata[a]=mysql_sessions->pdata[n];
257-
mysql_sessions->pdata[n]=p;
258-
a++;
259-
}
284+
void Base_Thread::ProcessAllSessions_Partition() {
285+
size_t running_end = 0;
286+
size_t idle_begin = mysql_sessions->len;
287+
size_t idx = 0;
288+
289+
while (idx < idle_begin) {
290+
S* s = static_cast<S*>(mysql_sessions->index(idx));
291+
292+
const bool has_be = (s->mybe && s->mybe->server_myds);
293+
const bool is_B = has_be && (s->mybe->server_myds->max_connect_time != 0);
294+
const bool is_A = !is_B && has_be && (s->mybe->server_myds->myconn != nullptr) && (s->status != WAITING_CLIENT_DATA);
295+
296+
if (is_A) {
297+
if (idx != running_end) {
298+
void* p = mysql_sessions->pdata[idx];
299+
mysql_sessions->pdata[idx] = mysql_sessions->pdata[running_end];
300+
mysql_sessions->pdata[running_end] = p;
301+
}
302+
++running_end;
303+
++idx;
304+
} else if (is_B) {
305+
++idx;
306+
} else {
307+
--idle_begin;
308+
if (idx != idle_begin) {
309+
void* p = mysql_sessions->pdata[idx];
310+
mysql_sessions->pdata[idx] = mysql_sessions->pdata[idle_begin];
311+
mysql_sessions->pdata[idle_begin] = p;
260312
}
313+
// do NOT advance idx - re-examine the swapped-in element test
261314
}
262315
}
263316
}

lib/MySQL_Session.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7857,6 +7857,7 @@ void MySQL_Session::handler___client_DSS_QUERY_SENT___server_DSS_NOT_INITIALIZED
78577857
} else {
78587858
mc=MyHGM->get_MyConn_from_pool(mybe->hostgroup_id, this, (session_fast_forward || qpo->create_new_conn), NULL, 0, (int)qpo->max_lag_ms);
78597859
}
7860+
thread->note_pool_attempt(mc == NULL);
78607861
#ifdef STRESSTEST_POOL
78617862
if (mc && (loops < NUM_SLOW_LOOPS - 1)) {
78627863
if (mc->mysql) {

lib/MySQL_Thread.cpp

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4452,8 +4452,9 @@ void MySQL_Thread::process_all_sessions() {
44524452
sess_sort=false;
44534453
}
44544454
#endif // IDLE_THREADS
4455-
if (sess_sort && mysql_sessions->len > 3) {
4456-
ProcessAllSessions_SortingSessions<MySQL_Session>();
4455+
const bool partition_wanted = update_partition_gate();
4456+
if (sess_sort && mysql_sessions->len > 3 && partition_wanted) {
4457+
ProcessAllSessions_Partition<MySQL_Session>();
44574458
}
44584459
for (n=0; n<mysql_sessions->len; n++) {
44594460
MySQL_Session *sess=(MySQL_Session *)mysql_sessions->index(n);
@@ -4784,6 +4785,7 @@ MySQL_Thread::MySQL_Thread() {
47844785
pthread_mutex_init(&thread_mutex,NULL);
47854786
my_idle_conns=NULL;
47864787
cached_connections=NULL;
4788+
push_local_counter=0;
47874789
mysql_sessions=NULL;
47884790
mirror_queue_mysql_sessions=NULL;
47894791
mirror_queue_mysql_sessions_cache=NULL;
@@ -6466,14 +6468,22 @@ MySQL_Connection * MySQL_Thread::get_MyConn_local(unsigned int _hid, MySQL_Sessi
64666468
* @param c Pointer to the MySQL_Connection object to be pushed to the local connection pool.
64676469
*/
64686470
void MySQL_Thread::push_MyConn_local(MySQL_Connection *c) {
6469-
MySrvC *mysrvc=NULL;
6470-
mysrvc=(MySrvC *)c->parent;
6471+
// Bounded local cache: cache 1-in-N releases (N = mysql_threads), push the
6472+
// rest to the shared HGM pool so peer workers can pick them up.
6473+
// At N=1 always cache (no sibling to share with).
6474+
// Rationale: avoids the connection-hoarding behavior that starved sibling
6475+
// workers at high client count, while preserving most of the lock-amortization
6476+
// benefit at lower client counts.
6477+
MySrvC *mysrvc=(MySrvC *)c->parent;
64716478
// reset insert_id #1093
64726479
c->mysql->insert_id = 0;
64736480
if (mysrvc->get_status() == MYSQL_SERVER_STATUS_ONLINE) {
64746481
if (c->async_state_machine==ASYNC_IDLE) {
6475-
cached_connections->add(c);
6476-
return; // all went well
6482+
unsigned int n = (GloMTH && GloMTH->num_threads > 0) ? GloMTH->num_threads : 1;
6483+
if ((push_local_counter++ % n) == 0) {
6484+
cached_connections->add(c);
6485+
return;
6486+
}
64776487
}
64786488
}
64796489
MyHGM->push_MyConn_to_pool(c);

lib/PgSQL_Data_Stream.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,13 @@ static void __dump_pkt(const char* func, unsigned char* _ptr, unsigned int len)
108108

109109
static enum pgsql_sslstatus get_sslstatus(SSL* ssl, int n)
110110
{
111+
// See issue #5792.
112+
// SSL_get_error() classifies based on the return code + SSL_want() state, not on the
113+
// thread-local OpenSSL error queue. For SSL_ERROR_NONE / WANT_READ / WANT_WRITE no error is
114+
// pushed onto the queue, so there is nothing to clear. Only the actual error classifications
115+
// (ZERO_RETURN / SYSCALL / SSL / default) need the queue drained so the next SSL op on this
116+
// thread starts clean.
111117
int err = SSL_get_error(ssl, n);
112-
ERR_clear_error();
113118
switch (err) {
114119
case SSL_ERROR_NONE:
115120
return PGSQL_SSLSTATUS_OK;
@@ -119,6 +124,9 @@ static enum pgsql_sslstatus get_sslstatus(SSL* ssl, int n)
119124
case SSL_ERROR_ZERO_RETURN:
120125
case SSL_ERROR_SYSCALL:
121126
default:
127+
// drain the queue; any consumer that wanted the details should have read them
128+
// before returning to this point
129+
while (ERR_get_error()) { /* discard */ }
122130
return PGSQL_SSLSTATUS_FAIL;
123131
}
124132
}

lib/PgSQL_Session.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5420,6 +5420,7 @@ void PgSQL_Session::handler___client_DSS_QUERY_SENT___server_DSS_NOT_INITIALIZED
54205420
else {
54215421
mc = PgHGM->get_MyConn_from_pool(mybe->hostgroup_id, this, (session_fast_forward || qpo->create_new_conn), NULL, 0, (int)qpo->max_lag_ms);
54225422
}
5423+
thread->note_pool_attempt(mc == NULL);
54235424
#ifdef STRESSTEST_POOL
54245425
if (mc && (loops < NUM_SLOW_LOOPS - 1)) {
54255426
if (mc->pgsql) {

lib/PgSQL_Thread.cpp

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3832,8 +3832,9 @@ void PgSQL_Thread::process_all_sessions() {
38323832
sess_sort = false;
38333833
}
38343834
#endif // IDLE_THREADS
3835-
if (sess_sort && mysql_sessions->len > 3) {
3836-
ProcessAllSessions_SortingSessions<PgSQL_Session>();
3835+
const bool partition_wanted = update_partition_gate();
3836+
if (sess_sort && mysql_sessions->len > 3 && partition_wanted) {
3837+
ProcessAllSessions_Partition<PgSQL_Session>();
38373838
}
38383839
for (n = 0; n < mysql_sessions->len; n++) {
38393840
PgSQL_Session* sess = (PgSQL_Session*)mysql_sessions->index(n);
@@ -4209,6 +4210,7 @@ PgSQL_Thread::PgSQL_Thread() {
42094210
pthread_mutex_init(&thread_mutex, NULL);
42104211
my_idle_conns = NULL;
42114212
cached_connections = NULL;
4213+
push_local_counter = 0;
42124214
mysql_sessions = NULL;
42134215
mirror_queue_mysql_sessions = NULL;
42144216
mirror_queue_mysql_sessions_cache = NULL;
@@ -5840,14 +5842,20 @@ PgSQL_Connection* PgSQL_Thread::get_MyConn_local(unsigned int _hid, PgSQL_Sessio
58405842
}
58415843

58425844
void PgSQL_Thread::push_MyConn_local(PgSQL_Connection * c) {
5843-
PgSQL_SrvC* mysrvc = NULL;
5844-
mysrvc = (PgSQL_SrvC*)c->parent;
5845-
// reset insert_id #1093
5846-
//c->pgsql->insert_id = 0;
5845+
// Bounded local cache: cache 1-in-N releases (N = pgsql_threads), push the
5846+
// rest to the shared HGM pool so peer workers can pick them up.
5847+
// At N=1 always cache (no sibling to share with).
5848+
// Rationale: avoids the connection-hoarding behavior that starved sibling
5849+
// workers at high client count, while preserving most of the lock-amortization
5850+
// benefit at lower client counts.
5851+
PgSQL_SrvC* mysrvc = (PgSQL_SrvC*)c->parent;
58475852
if (mysrvc->status == MYSQL_SERVER_STATUS_ONLINE) {
58485853
if (c->async_state_machine == ASYNC_IDLE) {
5849-
cached_connections->add(c);
5850-
return; // all went well
5854+
unsigned int n = (GloPTH && GloPTH->num_threads > 0) ? GloPTH->num_threads : 1;
5855+
if ((push_local_counter++ % n) == 0) {
5856+
cached_connections->add(c);
5857+
return;
5858+
}
58515859
}
58525860
}
58535861
PgHGM->push_MyConn_to_pool(c);

0 commit comments

Comments
 (0)