Compare commits

...

3 Commits

Author SHA1 Message Date
Michael Hoennig
897ab191c7 better formatted SQL and documentation of performance analysis for SELECT 2024-08-22 17:39:54 +02:00
Michael Hoennig
6dd20db238 improved formatting 2024-08-22 14:31:15 +02:00
Michael Hoennig
aede5bc8d6 baseline document potential rbac optimizations 2024-08-22 12:40:41 +02:00
2 changed files with 179 additions and 135 deletions

View File

@ -15,7 +15,7 @@ We could not find a pattern, why that was the case. The impression that it had t
## Preparation ## Preparation
### Configuring PostgreSQL ### Configuring PostgreSQL
The pg_stat_statements PostgreSQL-Extension can be used to measure how long queries take and how often they are called. The pg_stat_statements PostgreSQL-Extension can be used to measure how long queries take and how often they are called.
@ -355,6 +355,68 @@ In production, the `SELECT ... FROM hs_office_relation_rv` (No. 2) with about 0.
3. For the production code, we could use raw-entities for referenced entities, here usually RBAC SELECT permission is given anyway. 3. For the production code, we could use raw-entities for referenced entities, here usually RBAC SELECT permission is given anyway.
## The Problematically Huge Join
The origin problem was the expensive RBAC check for many SELECT queries.
This consists of two parts:
1. The recursive CTE query to determine which object's UUIDs are visible for the current subject.
This query itself takes currently about 250ms thus is no problem by itself as long as we only need it once per request.
2. Joining the result from 1. with the result if a business query.
The performance of the business query itself is no problem, for the join see the following explanations.
Superusers can see all objects (currently already over 90.000)
and even high level roles of customers with many hosting assets can see several thousand objects.
This is the one side of that problematic join.
The other side of that problematic is the result of the business query.
For example if a user wants to select all of their e-mail-addresses, that might easily half of the visible objects.
Thus, we would have a join of for example 5.000 x 2.500 rows, which is going to be slow.
As there are currently about 84.000 objects are hosting assets and 33.000 e-mail-addresses in our system,
for a superuser we would even run into an 84.0000 x 33.0000 join.
We found some solution approaches:
1. Getting rid of the `rbacrole` and `rbacpermission` table and only having implicit roles with implicit grants (OWNER->ADMIN->AGENT->TENENT->REFERRER) by comparison of ordered enum values and fixed permission assignments (e.g. OWENER->DELETE, ADMIN->UPDATE etc.). We could also get rid of the table `rbacreferece` if we enter users as business objects.
This should dramatically reduce the size of the table `rbackgrant` as well as the recusion levels.
But since we only apply this query once for each business query, that would only improve performance once we have way more objects in our system, but does not help our current problem.
It's quite some effort to implement even just a prototype, so we did not further explore this idea.
2. Adding the object type to the table `rbacObject` to reduce the size of the result of the recursive CTE query.
See chapter below.
### Adding The Object Type To The Table `rbacObject`
This optimization idea came from Michael Hierweck and was promising.
The idea is to reduce the size of the result of the recursive CTE query and maybe even speed up that query itself.
To evaluate this, I added a type column to the `rbacObject` table, initially as an enum hsHostingAssetType. Then I entered the type there for all rows from hs_hosting_asset. This means that 83,886 of 92,545 rows in `rbacobject` have a type set, leaving 8,659 without.
If we do this for other types (we currently have 1,271 relations and 927 booking items), it gets more complicated because they are different enum types. As varchar(16), we could lose performance again due to the higher storage space requirements.
But the performance gained is not particularly high anyway.
See the average seconds per recursive CTE select as role 'hs_hosting_asset:<DEBITOR>defaultproject:ADMIN',
joined with business query for all `'EMAIL_ADDRESSES'`:
| | D-1000000-hsh | D-1000300-mih |
|-----------------------------------------------------|------------------|---------------|
| currently (without type comparision in rbacobject): | ~3.30 - ~3.49 | ~0.23 |
| optimized (with type comparision in rbacobject): | ~2.99 - ~3.08 | ~0.21 |
As you can see, the query is no problem at all for normal customers (in the example, yours truly). With Hostsharing (D-1000000-hsh) it is quite slow.
Luckily this experiment also shows that it's not a big problem, having all hosting assets in the same database table.
Implementing this approach would be a bit difficult anyway, because we would need to transfer the type query parameter into the definition of the restricted view. We have not even the slightest idea how this could be done.
See the related queries in [recursive-cte-experiments-for-accessible-uuids.sql](../sql/recursive-cte-experiments-for-accessible-uuids.sql). They might have changed independently since this document was written, but you can still check out the old version from git.
## Summary ## Summary
### What we did Achieve? ### What we did Achieve?

View File

@ -1,142 +1,124 @@
-- just a permanent playground to explore optimization of the central recursive CTE query for RBAC -- just a permanent playground to explore optimization of the central recursive CTE query for RBAC
rollback transaction; select * from hs_statistics_view;
begin transaction;
SET TRANSACTION READ ONLY;
call defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
'hs_booking_project#D-1000000-hshdefaultproject:ADMIN');
-- 'hs_booking_project#D-1000300-mihdefaultproject:ADMIN');
select count(type) as counter, type from hs_hosting_asset_rv
group by type
order by counter desc;
commit transaction;
-- ========================================================
-- An example for a restricted view (_rv) as generated by our RBAC system:
drop view if exists hs_hosting_asset_example_rv;
create view hs_hosting_asset_example_rv as
with accessible_hs_hosting_asset_uuids as (
with recursive
rollback transaction; recursive_grants as (
begin transaction; select distinct rbacgrants.descendantuuid,
SET TRANSACTION READ ONLY; rbacgrants.ascendantuuid,
call defineContext('performance testing', null, 'superuser-alex@hostsharing.net', 1 as level,
'hs_booking_project#D-1000000-hshdefaultproject:ADMIN'); true
-- 'hs_booking_project#D-1000300-mihdefaultproject:ADMIN'); from rbacgrants
where (rbacgrants.ascendantuuid = any (currentsubjectsuuids()))
with accessible_hs_hosting_asset_uuids as --and rbacgrants.assumed
(with recursive union all
recursive_grants as select distinct g.descendantuuid,
(select distinct rbacgrants.descendantuuid, g.ascendantuuid,
rbacgrants.ascendantuuid, grants.level + 1 as level,
1 as level, assertTrue(grants.level < 22, 'too many grant-levels: ' || grants.level)
true from rbacgrants g
from rbacgrants join recursive_grants grants on grants.descendantuuid = g.ascendantuuid
where rbacgrants.assumed where g.assumed
and (rbacgrants.ascendantuuid = any (currentsubjectsuuids())) ),
union all grant_count as (
select distinct g.descendantuuid, select count(*) as grant_count from recursive_grants
g.ascendantuuid, ),
grants.level + 1 as level, count_check as (
assertTrue(grants.level < 22, 'too many grant-levels: ' || grants.level) select assertTrue((select grant_count from grant_count) < 600000,
from rbacgrants g 'too many grants for current subjects: ' || (select grant_count from grant_count)) as valid
join recursive_grants grants on grants.descendantuuid = g.ascendantuuid )
where g.assumed), select distinct perm.objectuuid
grant_count AS ( from recursive_grants
SELECT COUNT(*) AS grant_count FROM recursive_grants join rbacpermission perm on recursive_grants.descendantuuid = perm.uuid
), join rbacobject obj on obj.uuid = perm.objectuuid
count_check as (select assertTrue((select count(*) as grant_count from recursive_grants) < 300000, join count_check cc on cc.valid
'too many grants for current subjects: ' || (select count(*) as grant_count from recursive_grants)) where obj.objecttable::text = 'hs_hosting_asset'::text
as valid) and obj.type = 'EMAIL_ADDRESS'::hshostingassettype -- with/without this type condition
select distinct perm.objectuuid
from recursive_grants
join rbacpermission perm on recursive_grants.descendantuuid = perm.uuid
join rbacobject obj on obj.uuid = perm.objectuuid
join count_check cc on cc.valid
where obj.objecttable::text = 'hs_hosting_asset'::text)
select type,
-- count(*) as counter
target.uuid,
-- target.version,
-- target.bookingitemuuid,
-- target.type,
-- target.parentassetuuid,
-- target.assignedtoassetuuid,
target.identifier,
target.caption
-- target.config,
-- target.alarmcontactuuid
from hs_hosting_asset target
where (target.uuid in (select accessible_hs_hosting_asset_uuids.objectuuid
from accessible_hs_hosting_asset_uuids))
and target.type in ('EMAIL_ADDRESS', 'CLOUD_SERVER', 'MANAGED_SERVER', 'MANAGED_WEBSPACE')
-- and target.type = 'EMAIL_ADDRESS'
-- order by target.identifier;
-- group by type
-- order by counter desc
;
commit transaction;
rollback transaction;
begin transaction;
SET TRANSACTION READ ONLY;
call defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
'hs_booking_project#D-1000000-hshdefaultproject:ADMIN');
-- 'hs_booking_project#D-1000300-mihdefaultproject:ADMIN');
with one_path as (with recursive path as (
-- Base case: Start with the row where ascending equals the starting UUID
select ascendantuuid,
descendantuuid,
array [ascendantuuid] as path_so_far
from rbacgrants
where ascendantuuid = any (currentsubjectsuuids())
union all
-- Recursive case: Find the next step in the path
select c.ascendantuuid,
c.descendantuuid,
p.path_so_far || c.ascendantuuid
from rbacgrants c
inner join
path p on c.ascendantuuid = p.descendantuuid
where c.ascendantuuid != all (p.path_so_far) -- Prevent cycles
) )
-- Final selection: Output all paths that reach the target UUID select target.*
select distinct array_length(path_so_far, 1), from hs_hosting_asset target
path_so_far || descendantuuid as full_path where (target.uuid in (select accessible_hs_hosting_asset_uuids.objectuuid
from path from accessible_hs_hosting_asset_uuids));
join rbacpermission perm on perm.uuid = path.descendantuuid -- end of the example view.
join hs_hosting_asset ha on ha.uuid = perm.objectuuid
-- JOIN rbacrole_ev re on re.uuid = any(path_so_far) -- -------------------------------------------------------------------------------
where ha.identifier = 'vm1068'
order by array_length(path_so_far, 1) rollback transaction;
limit 1 DO language plpgsql $$
) DECLARE
start_time timestamp;
end_time timestamp;
total_time interval;
letter char(1);
BEGIN
start_time := clock_timestamp();
CALL defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
'hs_booking_project#D-1000000-hshdefaultproject:ADMIN');
-- 'hs_booking_project#D-1000300-mihdefaultproject:ADMIN');
SET TRANSACTION READ ONLY;
FOR i IN 0..25 LOOP
letter := chr(i+ascii('a'));
PERFORM count(*) from (
-- An example for a business query based on the view:
select type, uuid, identifier, caption
from hs_hosting_asset_example_rv
where type = 'EMAIL_ADDRESS'
and identifier like letter || '%'
-- end of the business query example.
) AS timed;
END LOOP;
end_time := clock_timestamp();
total_time := end_time - start_time;
RAISE NOTICE 'average execution time: %', total_time/26;
END;
$$;
-- average seconds per recursive CTE select as role 'hs_hosting_asset:<DEBITOR>defaultproject:ADMIN'
-- joined with business query for all 'EMAIL_ADDRESSES':
-- D-1000000-hsh D-1000300-mih
-- - without type comparision in rbacobject: ~3.30 - ~3.49 ~0.23
-- - with type comparision in rbacobject: ~2.99 - ~3.08 ~0.21
-- =============================================================================
-- extending the rbacobject table:
alter table rbacobject
-- just for performance testing, we would need a joined enum or a varchar(16) which would make it slow
add column type hshostingassettype;
-- and fill the type column with hs_hosting_asset types:
rollback transaction;
begin transaction;
call defineContext('setting rbacobject.type from hs_hosting_asset.type', null, 'superuser-alex@hostsharing.net');
UPDATE rbacobject
SET type = hs.type
FROM hs_hosting_asset hs
WHERE rbacobject.uuid = hs.uuid;
end transaction;
-- check the result:
select select
( (select count(*) as "total" from rbacobject),
SELECT ARRAY_AGG(re.roleidname ORDER BY ord.idx) (select count(*) as "not null" from rbacobject where type is not null),
FROM UNNEST(one_path.full_path) WITH ORDINALITY AS ord(uuid, idx) (select count(*) as "null" from rbacobject where type is null);
JOIN rbacrole_ev re ON ord.uuid = re.uuid
) AS name_array
from one_path;
commit transaction;
with grants as (
select uuid
from rbacgrants
where descendantuuid in (
select uuid
from rbacrole
where objectuuid in (
select uuid
from hs_hosting_asset
-- where type = 'DOMAIN_MBOX_SETUP'
-- and identifier = 'example.org|MBOX'
where type = 'EMAIL_ADDRESS'
and identifier='test@example.org'
))
)
select * from rbacgrants_ev gev where exists ( select uuid from grants where gev.uuid = grants.uuid );