Resolving PostgreSQL VACUUM FULL Table Locks Blocking Queries on CentOS Stream / Rocky Linux
Diagnose and safely resolve critical table locks caused by VACUUM FULL operations in PostgreSQL, preventing query blocks on CentOS Stream and Rocky Linux.
Diagnose and safely resolve critical table locks caused by VACUUM FULL operations in PostgreSQL, preventing query blocks on CentOS Stream and Rocky Linux.
Introduction
As a seasoned Systems Administrator managing production PostgreSQL databases, encountering prolonged table locks that bring your application to a crawl is a critical incident. One of the most common culprits for severe, application-wide database freezes is an ill-timed or improperly executed VACUUM FULL command. Unlike a regular VACUUM, VACUUM FULL acquires an ACCESS EXCLUSIVE lock on the target table, preventing all other operations, including simple SELECT queries, until it completes. This guide will walk you through diagnosing, safely resolving, and preventing such occurrences on CentOS Stream and Rocky Linux systems.
Symptom & Error Signature
Users will experience slow or unresponsive applications, eventually leading to timeouts. On the application side, you might see errors indicating a cancelled query or an inability to acquire a lock. From the database perspective, pg_stat_activity will reveal an active VACUUM FULL operation holding an ACCESS EXCLUSIVE lock, with many other queries in a waiting state.
Typical Application Error (e.g., Python psycopg2):
psycopg2.errors.QueryCanceled: canceling statement due to statement timeout
PostgreSQL Log Entries (often accompanied by context from the application):
LOG: duration: 120000.000 ms statement: SELECT * FROM my_large_table WHERE id = 123;
ERROR: canceling statement due to statement timeout
DETAIL: Statement has been running for too long.
Identifying the Blocking Process using ps and pg_stat_activity:
You might first notice a long-running postgres process consuming resources, but pg_stat_activity is the definitive tool.
# From the OS, you might see a process, but it's not specific enough
$ ps aux | grep postgres | grep "vacuum full"
postgres 12345 99.9 0.5 1234567 87654 ? Rs 10:00 1:34:56 vacuum full public.my_large_table
-- Inside psql, this is the primary diagnostic query
SELECT
pid,
usename,
application_name,
client_addr,
backend_start,
state,
wait_event_type,
wait_event,
query_start,
query
FROM pg_stat_activity
WHERE state = 'active'
AND query ILIKE '%VACUUM FULL%'
ORDER BY query_start;
Expected output showing the VACUUM FULL and other queries waiting:
pid | usename | application_name | client_addr | backend_start | state | wait_event_type | wait_event | query_start | query
-------+------------+------------------+----------------+-------------------------+---------+-----------------+---------------+-------------------------------+-------------------------------------------
12345 | postgres | psql | | 2023-10-26 10:00:05.123 | active | Client | ClientRead | 2023-10-26 10:00:10.456 | VACUUM FULL public.my_large_table;
12346 | app_user | MyWebApp | 192.168.1.100 | 2023-10-26 10:01:00.789 | waiting | Lock | relation | 2023-10-26 10:01:05.111 | SELECT id, name FROM public.my_large_table WHERE status = 'active';
12347 | app_user | MyWebApp | 192.168.1.101 | 2023-10-26 10:01:10.123 | waiting | Lock | relation | 2023-10-26 10:01:15.444 | UPDATE public.my_large_table SET last_seen = NOW() WHERE id = 123;
(3 rows)
In this output, pid 12345 is running the VACUUM FULL, and pid 12346 and 12347 are waiting for a Lock on the relation (table).
Root Cause Analysis
The root cause of this issue lies in the locking behavior of the VACUUM FULL command in PostgreSQL:
ACCESS EXCLUSIVELock Acquisition: WhenVACUUM FULLruns on a table, it requests and holds anACCESS EXCLUSIVElock on that table. This is the strictest lock level in PostgreSQL.- Complete Blockage: An
ACCESS EXCLUSIVElock prevents any other access to the table. This includes allSELECT,INSERT,UPDATE,DELETE,ALTER TABLE, and otherVACUUMoperations. - Purpose of
VACUUM FULL:VACUUM FULLis designed to aggressively reclaim disk space by rewriting the entire table and associated indexes to a new file, freeing up space at the operating system level. It's often misunderstood as a routine maintenance task. - Misapplication: It is rarely needed for routine maintenance. Regular
VACUUM(often managed byautovacuum) handles tuple visibility and allows space reuse within the table files, but it does not shrink the file size on disk.VACUUM FULLis only justified in specific scenarios, such as:- After a massive deletion of rows from a table where significant disk space needs to be returned to the OS, and other methods (like
pg_repack) are not feasible. - In a maintenance window where application downtime is acceptable.
- After a massive deletion of rows from a table where significant disk space needs to be returned to the OS, and other methods (like
- Long-Running Operations: On large tables,
VACUUM FULLcan take hours or even days to complete, leading to prolonged application outages if run during peak hours. - Concurrency Issues: Even if
VACUUM FULLis initiated, if there are any existingACCESS SHARElocks (e.g., from an activeSELECTquery) on the table, theVACUUM FULLwill wait to acquire itsACCESS EXCLUSIVElock. This means a long-runningSELECTquery could itself block theVACUUM FULLfrom even starting, only for theVACUUM FULLto block everything else once it does start.
Step-by-Step Resolution
Addressing a blocking VACUUM FULL requires careful action to minimize further disruption.
1. Safely Identify the Blocking Process and Blocked Queries
First, connect to your PostgreSQL database using psql. Ensure you connect as a superuser (e.g., postgres user).
sudo -u postgres psql
Then, execute the following query to identify all active and waiting queries, focusing on the VACUUM FULL operation and those waiting on it.
SELECT
pid,
usename,
application_name,
client_addr,
backend_start,
state,
wait_event_type,
wait_event,
query_start,
query,
age(now(), query_start) AS query_duration
FROM pg_stat_activity
WHERE state IN ('active', 'waiting')
ORDER BY wait_event IS NOT NULL DESC, query_start;
Note down the
pidof theVACUUM FULLprocess. This is the process you will need to target for termination. Also, observe thequery_durationto gauge how long it has been running.
2. Assess the Impact and Decision to Terminate
Before terminating, consider:
- How long has it been running? If it's only been a few minutes on a small table, it might finish soon.
- How critical is current application uptime? If the application is completely down, termination is likely the best course.
- What table is it running on? Terminating a
VACUUM FULLwill roll back its transaction, which might take some time itself, proportional to the work done. The table will remain in its original state.
Terminating a
VACUUM FULLwill roll back the operation. This means any disk space it would have reclaimed will not be reclaimed. The table will remain in its pre-VACUUM FULLstate.
3. Gracefully Cancel the Blocking VACUUM FULL
The preferred method to stop a long-running query is pg_cancel_backend(). This sends a SIGINT to the backend process, requesting it to terminate gracefully. The backend will attempt to stop its current operation and roll back its transaction.
-- Replace <PID_OF_VACUUM_FULL> with the actual PID identified in step 1.
SELECT pg_cancel_backend(<PID_OF_VACUUM_FULL>);
After executing, wait a few moments and re-run the pg_stat_activity query from Step 1.
- If the
VACUUM FULLprocess disappears, it was successfully cancelled. - If it's still there but
statehas changed (e.g., toidle in transactionoraborting), it's in the process of rolling back. - If it remains
activewith the same query, it might be stuck or ignoring the cancel request.
4. Forcefully Terminate the Blocking VACUUM FULL (If Cancellation Fails)
If pg_cancel_backend() does not work after a reasonable wait time (e.g., 1-5 minutes, depending on the workload), you may need to forcefully terminate the backend using pg_terminate_backend(). This sends a SIGTERM to the backend, which is a stronger signal. The backend will immediately disconnect, and its transaction will be aborted and rolled back.
Use
pg_terminate_backend()with caution. While generally safe for the database's integrity (PostgreSQL's crash recovery ensures consistency), it immediately kills the backend process and can take longer for rollback on very large transactions. Ensure you target the correct PID.
-- Replace <PID_OF_VACUUM_FULL> with the actual PID identified in step 1.
SELECT pg_terminate_backend(<PID_OF_VACUUM_FULL>);
After termination, monitor your database logs and pg_stat_activity to ensure the process is gone and other queries resume normal operation.
5. Prevent Future VACUUM FULL Lockdowns
Prevention is key. VACUUM FULL should almost never be run in a production environment without a dedicated maintenance window.
a. Understand Autovacuum
PostgreSQL's autovacuum daemon handles routine VACUUM and ANALYZE operations automatically. Ensure it's enabled and correctly configured. This daemon performs regular VACUUM operations that do not acquire ACCESS EXCLUSIVE locks and are non-blocking.
Check postgresql.conf:
autovacuum = on
Also, review autovacuum_vacuum_cost_delay and related settings. A common tuning point is reducing the delay for busier tables to allow autovacuum to run more frequently and efficiently.
b. Use pg_repack for Online Table Rewrites
For reclaiming disk space without downtime, consider pg_repack. pg_repack is an external tool that reorganizes PostgreSQL tables and indexes with minimal locking, similar to VACUUM FULL but online.
# On CentOS Stream / Rocky Linux, install pg_repack:
sudo dnf install pg_repack<version>_14 # For PostgreSQL 14, adjust version as needed
# Or, if using EPEL:
sudo dnf install pg_repack
Then, you can use it like this (example for database mydb, table my_large_table):
pg_repack -d mydb -t my_large_table
pg_repackstill requires certain locks for brief periods, but it is far less disruptive thanVACUUM FULL. Always test on a staging environment first.
c. Schedule VACUUM FULL During Maintenance Windows Only
If VACUUM FULL is absolutely necessary, schedule it during known downtime or low-activity periods. This might involve:
- Stopping application servers.
- Notifying users of planned maintenance.
- Performing the operation.
- Restarting applications.
d. Review and Audit Scheduled Jobs
Check any cron jobs, scripts, or database maintenance tools that might be inadvertently running VACUUM FULL. Remove or modify these to use VACUUM (without FULL) or pg_repack instead.
# Check system-wide cron jobs
sudo grep -r "VACUUM FULL" /etc/cron.* /var/spool/cron/
# Check for scripts in common locations
sudo find /opt /usr/local -name "*.sh" -exec grep -l "VACUUM FULL" {} +
By understanding the nature of VACUUM FULL and leveraging appropriate tools and practices, you can prevent these critical table lock situations and maintain high availability for your PostgreSQL databases on CentOS Stream or Rocky Linux.
Our Production Verification Guarantee
Encountering a bug not covered here or running a non-standard kernel configuration? Our solutions are continually refined against real production incidents. Submit an environment trace for our editorial team to replicate.