]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
PostgreSQL sqlippools: Find and allocate stored procedure
authorTerry Burton <tez@terryburton.co.uk>
Mon, 18 Nov 2019 02:38:30 +0000 (02:38 +0000)
committerAlan DeKok <aland@freeradius.org>
Tue, 19 Nov 2019 19:56:48 +0000 (14:56 -0500)
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

raddb/mods-config/sql/ippool/postgresql/procedure.sql
raddb/mods-config/sql/ippool/postgresql/queries.conf

index 64b3f2e38b4bef4db93e292d5051947745974738..1e60746068f8997f164a609d979df50d75bcf602 100644 (file)
@@ -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
 $$;
index fefdfef481cdd8af43d83ddda60aa3abcbe10482..03854cecc70e7a8a905ccbcaf3cc3a6db8b46308 100644 (file)
@@ -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