From: Terry Burton Date: Mon, 18 Nov 2019 02:38:30 +0000 (+0000) Subject: PostgreSQL sqlippools: Find and allocate stored procedure X-Git-Tag: release_3_0_21~119 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=28ad0e18e84e2abc39468b8dad24a62691abd217;p=thirdparty%2Ffreeradius-server.git PostgreSQL sqlippools: Find and allocate stored procedure Replace existing stored procedure with one that combines the allocate_find and allocate_update queries. We can additionally avoid round trips by avoiding BEGIN and COMMIT since calling an SP provides a new transaction context [*]. Therefore the IP allocation process is reduced to running a single statement. Indicative performance testing showed increased IP allocation performance from 120 allocs/sec with separate queries to 1500 allocs/sec on a two-node master/slave cluster. [*] Verfied as follows which indicates that we are within a transaction rather than "autocommit"ing: > CREATE OR REPLACE FUNCTION sp_txid () RETURNS text LANGUAGE plpgsql AS $$ DECLARE BEGIN -- Write the current txid to a table INSERT INTO txids SELECT txid_current(); INSERT INTO txids SELECT txid_current(); RETURN txid_current(); END$$; > CREATE TEMPORARY TABLE txids (txid TEXT); > SELECT sp_txid(); txsp_txid = 651120 > SELECT sp_txid(); txsp_txid = 651121 > SELECT * FROM txids txids ------- 651120 651120 651121 651121 --- diff --git a/raddb/mods-config/sql/ippool/postgresql/procedure.sql b/raddb/mods-config/sql/ippool/postgresql/procedure.sql index 64b3f2e38b4..1e60746068f 100644 --- a/raddb/mods-config/sql/ippool/postgresql/procedure.sql +++ b/raddb/mods-config/sql/ippool/postgresql/procedure.sql @@ -1,20 +1,43 @@ -- --- Use the following indexes and function if using the stored procedure to --- find the previously used address. +-- A stored procedure to reallocate a user's previous address, otherwise +-- provide a free address. -- --- You may wish to set the ORDER BY expiry_time to DESC for the first two --- queries in order to assign the address that the user had most recently, --- instead of assigning the oldest address the user had used. +-- Using this SP reduces the usual set dialogue of queries to a single +-- query: +-- +-- START TRANSACTION; SELECT FOR UPDATE; UPDATE; COMMIT; -> SELECT sp() +-- +-- The stored procedure is executed on an database instance within a single +-- round trip which often leads to reduced deadlocking and significant +-- performance improvements especially on multi-master clusters, perhaps even +-- by an order of magnitude or more. +-- +-- To use this stored procedure the corresponding queries.conf statements must +-- be configured as follows: +-- +-- allocate_begin = "" +-- allocate_find = "\ +-- SELECT sp_allocate_previous_or_new_framedipaddress( \ +-- '%{control:${pool_name}}', \ +-- '%{User-Name}', \ +-- '%{Calling-Station-Id}', \ +-- '%{NAS-IP-Address}', \ +-- '${pool_key}', \ +-- ${lease_duration} \ +-- )" +-- allocate_update = "" +-- allocate_commit = "" -- -CREATE INDEX radippool_pool_name ON radippool USING btree (pool_name); -CREATE INDEX radippool_username ON radippool USING btree (username); -CREATE INDEX radippool_callingstationid ON radippool USING btree (callingstationid); +CREATE INDEX radippool_poolname_username_callingstationid ON radippool(pool_name,username,callingstationid); -CREATE OR REPLACE FUNCTION find_previous_or_new_framedipaddress ( +CREATE OR REPLACE FUNCTION sp_allocate_previous_or_new_framedipaddress ( v_pool_name VARCHAR(64), v_username VARCHAR(64), - v_callingstationid VARCHAR(64) + v_callingstationid VARCHAR(64), + v_nasipaddress VARCHAR(16), + v_pool_key VARCHAR(64), + v_lease_duration INT ) RETURNS inet LANGUAGE plpgsql @@ -22,36 +45,67 @@ AS $$ DECLARE r_address inet; BEGIN + + -- Reissue an existing IP address lease when re-authenticating a session + -- SELECT framedipaddress INTO r_address FROM radippool - WHERE radippool.pool_name = v_pool_name - AND radippool.expiry_time < 'now'::timestamp(0) - AND radippool.username = v_username - AND radippool.callingstationid = v_callingstationid - ORDER BY expiry_time + WHERE pool_name = v_pool_name + AND expiry_time > NOW() + AND username = v_username + AND callingstationid = v_callingstationid LIMIT 1 FOR UPDATE SKIP LOCKED; - IF r_address IS NOT NULL THEN - RETURN r_address; + + -- Reissue an user's previous IP address, provided that the lease is + -- available (i.e. enable sticky IPs) + -- + -- When using this SELECT you should delete the one above. You must also + -- set allocate_clear = "" in queries.conf to persist the associations + -- for expired leases. + -- + -- SELECT framedipaddress INTO r_address + -- FROM radippool + -- WHERE pool_name = v_pool_name + -- AND username = v_username + -- AND callingstationid = v_callingstationid + -- LIMIT 1 + -- FOR UPDATE SKIP LOCKED; + + -- If we didn't reallocate a previous address then pick the least + -- recently used address from the pool which maximises the likelihood + -- of re-assigning the other addresses to their recent user + -- + IF r_address IS NULL THEN + SELECT framedipaddress INTO r_address + FROM radippool + WHERE pool_name = v_pool_name + AND expiry_time < NOW() + ORDER BY + expiry_time + LIMIT 1 + FOR UPDATE SKIP LOCKED; END IF; - SELECT framedipaddress INTO r_address - FROM radippool - WHERE radippool.pool_name = v_pool_name - AND radippool.expiry_time < 'now'::timestamp(0) - AND radippool.username = v_username - ORDER BY expiry_time - LIMIT 1 - FOR UPDATE SKIP LOCKED; - IF r_address IS NOT NULL THEN + + -- Return nothing if we failed to allocated an address + -- + IF r_address IS NULL THEN RETURN r_address; END IF; - SELECT framedipaddress INTO r_address - FROM radippool - WHERE radippool.pool_name = v_pool_name - AND radippool.expiry_time < 'now'::timestamp(0) - ORDER BY expiry_time - LIMIT 1 - FOR UPDATE SKIP LOCKED; + + -- Update the pool having allocated an IP address + -- + UPDATE radippool + SET + nasipaddress = v_nasipaddress, + pool_key = v_pool_key, + callingstationid = v_callingstationid, + username = v_username, + expiry_time = NOW() + v_lease_duration * interval '1 sec' + WHERE framedipaddress = r_address; + + -- Return the address that we allocated RETURN r_address; + END $$; diff --git a/raddb/mods-config/sql/ippool/postgresql/queries.conf b/raddb/mods-config/sql/ippool/postgresql/queries.conf index fefdfef481c..03854cecc70 100644 --- a/raddb/mods-config/sql/ippool/postgresql/queries.conf +++ b/raddb/mods-config/sql/ippool/postgresql/queries.conf @@ -39,25 +39,6 @@ allocate_find = "\ # LIMIT 1 \ # FOR UPDATE SKIP LOCKED" -# -# Use a stored procedure to find the address. This requires PostgreSQL >= 9.5 as -# SKIP LOCKED is used. See `procedure.sql` in this directory for the -# indices and stored procedure used by this query. -# -# The "NO LOAD BALANCE" comment is included here to indicate to a PgPool -# system that this needs to be a write transaction. PgPool itself cannot -# detect this from the statement alone. If you are using PgPool and do not -# have this comment, the query may go to a read only server, and will fail. -# This has no negative effect if you are not using PgPool. -# -# allocate_find = "\ -# /*NO LOAD BALANCE*/ \ -# SELECT find_previous_or_new_framedipaddress( \ -# '%{control:${pool_name}}', \ -# '%{SQL-User-Name}', \ -# '%{Calling-Station-Id}' \ -# )" - # # If you prefer to allocate a random IP address every time, use this query instead # Note: This is very slow if you have a lot of free IPs. @@ -125,6 +106,32 @@ allocate_clear = "\ WHERE nasipaddress = '%{NAS-IP-Address}' \ AND pool_key = '${pool_key}'" +# +# Use a stored procedure to find AND allocate the address. Read and customise +# `procedure.sql` in this directory to determine the optimal configuration. +# +# This requires PostgreSQL >= 9.5 as SKIP LOCKED is used. +# +# The "NO LOAD BALANCE" comment is included here to indicate to a PgPool +# system that this needs to be a write transaction. PgPool itself cannot +# detect this from the statement alone. If you are using PgPool and do not +# have this comment, the query may go to a read only server, and will fail. +# This has no negative effect if you are not using PgPool. +# +#allocate_begin = "" +#allocate_find = "\ +# /*NO LOAD BALANCE*/ \ +# SELECT sp_allocate_previous_or_new_framedipaddress( \ +# '%{control:${pool_name}}', \ +# '%{SQL-User-Name}', \ +# '%{Calling-Station-Id}', \ +# '%{NAS-IP-Address}', \ +# '${pool_key}', \ +# '${lease_duration}' \ +# )" +#allocate_update = "" +#allocate_commit = "" + # # This query extends an IP address lease by "lease_duration" when an accounting # START record arrives