277 lines
8.9 KiB
Bash
277 lines
8.9 KiB
Bash
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
# Airsonic Advanced + PostgreSQL diagnostics
|
|
# Focus: slow media scans, index efficiency, write pressure, and cache tuning signals.
|
|
|
|
PGHOST="${PGHOST:-localhost}"
|
|
PGPORT="${PGPORT:-5432}"
|
|
PGUSER="${PGUSER:-postgres}"
|
|
PGDATABASE="${PGDATABASE:-airsonic}"
|
|
OUT_FILE="${1:-airsonic_pg_diagnostics_$(date +%Y%m%d_%H%M%S).txt}"
|
|
|
|
PSQL=(psql -X -v ON_ERROR_STOP=1 -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGDATABASE")
|
|
|
|
run_section() {
|
|
local title="$1"
|
|
local sql="$2"
|
|
|
|
{
|
|
echo
|
|
echo "================================================================"
|
|
echo "$title"
|
|
echo "================================================================"
|
|
} >> "$OUT_FILE"
|
|
|
|
if ! "${PSQL[@]}" -c "$sql" >> "$OUT_FILE" 2>&1; then
|
|
echo "Query failed for section: $title" >> "$OUT_FILE"
|
|
fi
|
|
}
|
|
|
|
query_single_value() {
|
|
local sql="$1"
|
|
"${PSQL[@]}" -Atc "$sql" 2>/dev/null || true
|
|
}
|
|
|
|
bytes_to_human() {
|
|
local bytes="$1"
|
|
if [[ -z "$bytes" || ! "$bytes" =~ ^[0-9]+$ ]]; then
|
|
echo "unknown"
|
|
return
|
|
fi
|
|
|
|
local kib=$((1024))
|
|
local mib=$((1024 * 1024))
|
|
local gib=$((1024 * 1024 * 1024))
|
|
|
|
if (( bytes >= gib )); then
|
|
awk -v b="$bytes" -v g="$gib" 'BEGIN { printf "%.2f GiB", b/g }'
|
|
elif (( bytes >= mib )); then
|
|
awk -v b="$bytes" -v m="$mib" 'BEGIN { printf "%.2f MiB", b/m }'
|
|
elif (( bytes >= kib )); then
|
|
awk -v b="$bytes" -v k="$kib" 'BEGIN { printf "%.2f KiB", b/k }'
|
|
else
|
|
echo "${bytes} B"
|
|
fi
|
|
}
|
|
|
|
{
|
|
echo "Airsonic PostgreSQL Diagnostics"
|
|
echo "Generated at: $(date -Iseconds)"
|
|
echo "Target: host=$PGHOST port=$PGPORT db=$PGDATABASE user=$PGUSER"
|
|
} > "$OUT_FILE"
|
|
|
|
run_section "1) Server identity and uptime" "
|
|
SELECT version();
|
|
SELECT now() AS current_time, pg_postmaster_start_time() AS postmaster_start_time;
|
|
"
|
|
|
|
run_section "2) Key PostgreSQL tuning parameters" "
|
|
SELECT name, setting, unit, short_desc
|
|
FROM pg_settings
|
|
WHERE name IN (
|
|
'shared_buffers',
|
|
'work_mem',
|
|
'maintenance_work_mem',
|
|
'effective_cache_size',
|
|
'max_connections',
|
|
'effective_io_concurrency',
|
|
'random_page_cost',
|
|
'seq_page_cost',
|
|
'checkpoint_timeout',
|
|
'checkpoint_completion_target',
|
|
'autovacuum',
|
|
'autovacuum_max_workers',
|
|
'autovacuum_naptime'
|
|
)
|
|
ORDER BY name;
|
|
"
|
|
|
|
run_section "3) Database cache hit ratio and temp usage" "
|
|
SELECT
|
|
datname,
|
|
blks_read,
|
|
blks_hit,
|
|
ROUND(100.0 * blks_hit / NULLIF(blks_hit + blks_read, 0), 2) AS cache_hit_pct,
|
|
temp_files,
|
|
pg_size_pretty(temp_bytes) AS temp_bytes,
|
|
deadlocks,
|
|
stats_reset
|
|
FROM pg_stat_database
|
|
WHERE datname = current_database();
|
|
"
|
|
|
|
run_section "4) Most read tables and their index-vs-seq scan profile" "
|
|
SELECT
|
|
relname,
|
|
seq_scan,
|
|
idx_scan,
|
|
n_live_tup,
|
|
n_dead_tup,
|
|
ROUND((100.0 * idx_scan / NULLIF(idx_scan + seq_scan, 0))::numeric, 2) AS index_scan_pct,
|
|
ROUND((100.0 * heap_blks_hit / NULLIF(heap_blks_hit + heap_blks_read, 0))::numeric, 2) AS table_cache_hit_pct
|
|
FROM pg_stat_user_tables t
|
|
JOIN pg_statio_user_tables io USING (relid)
|
|
ORDER BY (seq_scan + idx_scan) DESC NULLS LAST
|
|
LIMIT 25;
|
|
"
|
|
|
|
run_section "5) Potentially unused indexes (can slow writes during scans/indexing)" "
|
|
SELECT
|
|
s.schemaname,
|
|
s.relname AS table_name,
|
|
s.indexrelname AS index_name,
|
|
s.idx_scan,
|
|
pg_size_pretty(pg_relation_size(s.indexrelid)) AS index_size,
|
|
t.n_tup_ins + t.n_tup_upd + t.n_tup_del AS table_writes_since_reset
|
|
FROM pg_stat_user_indexes s
|
|
JOIN pg_index i ON i.indexrelid = s.indexrelid
|
|
JOIN pg_stat_user_tables t ON t.relid = s.relid
|
|
WHERE s.idx_scan = 0
|
|
AND NOT i.indisunique
|
|
AND NOT i.indisprimary
|
|
ORDER BY pg_relation_size(s.indexrelid) DESC
|
|
LIMIT 50;
|
|
"
|
|
|
|
run_section "6) Write-heavy tables (hot spot during media metadata indexing)" "
|
|
SELECT
|
|
relname,
|
|
n_tup_ins,
|
|
n_tup_upd,
|
|
n_tup_del,
|
|
n_tup_hot_upd,
|
|
n_dead_tup,
|
|
vacuum_count,
|
|
autovacuum_count,
|
|
analyze_count,
|
|
autoanalyze_count,
|
|
COALESCE(last_autovacuum::text, 'never') AS last_autovacuum,
|
|
COALESCE(last_autoanalyze::text, 'never') AS last_autoanalyze
|
|
FROM pg_stat_user_tables
|
|
ORDER BY (n_tup_ins + n_tup_upd + n_tup_del) DESC
|
|
LIMIT 25;
|
|
"
|
|
|
|
run_section "7) Largest relations (tables and indexes)" "
|
|
SELECT
|
|
n.nspname AS schema_name,
|
|
c.relname,
|
|
c.relkind,
|
|
pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size
|
|
FROM pg_class c
|
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
AND c.relkind IN ('r', 'i')
|
|
ORDER BY pg_total_relation_size(c.oid) DESC
|
|
LIMIT 30;
|
|
"
|
|
|
|
if [[ "$(query_single_value "SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements';")" == "1" ]]; then
|
|
run_section "8) Slowest normalized SQL from pg_stat_statements" "
|
|
SELECT
|
|
calls,
|
|
ROUND(total_exec_time::numeric, 2) AS total_exec_ms,
|
|
ROUND((total_exec_time / NULLIF(calls, 0))::numeric, 2) AS mean_exec_ms,
|
|
ROUND(rows::numeric / NULLIF(calls, 0), 2) AS avg_rows,
|
|
shared_blks_read,
|
|
shared_blks_hit,
|
|
temp_blks_read,
|
|
temp_blks_written,
|
|
LEFT(query, 300) AS query_sample
|
|
FROM pg_stat_statements
|
|
WHERE dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
|
|
ORDER BY mean_exec_ms DESC
|
|
LIMIT 20;
|
|
"
|
|
else
|
|
{
|
|
echo
|
|
echo "================================================================"
|
|
echo "8) Slowest normalized SQL from pg_stat_statements"
|
|
echo "================================================================"
|
|
echo "Extension pg_stat_statements is not installed."
|
|
echo "Install with: CREATE EXTENSION pg_stat_statements;"
|
|
} >> "$OUT_FILE"
|
|
fi
|
|
|
|
# Heuristic recommendation summary
|
|
CACHE_HIT_PCT="$(query_single_value "
|
|
SELECT ROUND(100.0 * blks_hit / NULLIF(blks_hit + blks_read, 0), 2)
|
|
FROM pg_stat_database
|
|
WHERE datname = current_database();")"
|
|
|
|
UNUSED_INDEX_COUNT="$(query_single_value "
|
|
SELECT COUNT(*)
|
|
FROM pg_stat_user_indexes s
|
|
JOIN pg_index i ON i.indexrelid = s.indexrelid
|
|
WHERE s.idx_scan = 0
|
|
AND NOT i.indisunique
|
|
AND NOT i.indisprimary;")"
|
|
|
|
SHARED_BUFFERS_BYTES="$(query_single_value "
|
|
SELECT pg_size_bytes(setting || unit)
|
|
FROM pg_settings
|
|
WHERE name = 'shared_buffers';")"
|
|
|
|
TOTAL_RAM_KB=""
|
|
if [[ -r /proc/meminfo ]]; then
|
|
TOTAL_RAM_KB="$(awk '/MemTotal:/ {print $2}' /proc/meminfo)"
|
|
fi
|
|
|
|
{
|
|
echo
|
|
echo "================================================================"
|
|
echo "9) Recommendations summary"
|
|
echo "================================================================"
|
|
|
|
if [[ -n "$CACHE_HIT_PCT" ]]; then
|
|
echo "Cache hit ratio: ${CACHE_HIT_PCT}%"
|
|
awk -v hit="$CACHE_HIT_PCT" 'BEGIN {
|
|
if (hit < 98.0) {
|
|
print "- Action: cache hit ratio is low for media workloads. Increase shared_buffers/effective_cache_size and review slow seq scans."
|
|
} else if (hit < 99.0) {
|
|
print "- Action: cache hit ratio is acceptable but can likely improve for large libraries."
|
|
} else {
|
|
print "- Action: cache hit ratio looks healthy."
|
|
}
|
|
}'
|
|
else
|
|
echo "Cache hit ratio: unavailable"
|
|
fi
|
|
|
|
if [[ -n "$UNUSED_INDEX_COUNT" ]]; then
|
|
echo "Potentially unused non-unique indexes: $UNUSED_INDEX_COUNT"
|
|
if [[ "$UNUSED_INDEX_COUNT" =~ ^[0-9]+$ ]] && (( UNUSED_INDEX_COUNT > 0 )); then
|
|
echo "- Action: validate and drop truly unused indexes to reduce write overhead during scans/indexing."
|
|
else
|
|
echo "- Action: no obvious unused non-unique indexes from current stats."
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$SHARED_BUFFERS_BYTES" ]]; then
|
|
echo "shared_buffers: $(bytes_to_human "$SHARED_BUFFERS_BYTES")"
|
|
if [[ -n "$TOTAL_RAM_KB" && "$TOTAL_RAM_KB" =~ ^[0-9]+$ ]]; then
|
|
TOTAL_RAM_BYTES=$((TOTAL_RAM_KB * 1024))
|
|
SHARED_BUFFER_PCT=$(awk -v s="$SHARED_BUFFERS_BYTES" -v t="$TOTAL_RAM_BYTES" 'BEGIN { printf "%.2f", (s/t)*100 }')
|
|
echo "Estimated shared_buffers/system RAM: ${SHARED_BUFFER_PCT}%"
|
|
awk -v p="$SHARED_BUFFER_PCT" 'BEGIN {
|
|
if (p < 10.0) {
|
|
print "- Action: shared_buffers appears low; typical starting point is ~15-25% of system RAM."
|
|
} else if (p > 40.0) {
|
|
print "- Action: shared_buffers appears high; ensure OS cache remains adequate."
|
|
} else {
|
|
print "- Action: shared_buffers proportion looks reasonable."
|
|
}
|
|
}'
|
|
else
|
|
echo "- Action: compare shared_buffers to system RAM manually (target often 15-25% for dedicated DB hosts)."
|
|
fi
|
|
fi
|
|
|
|
echo "- Action: for media scans, verify high-read tables use selective indexes and that autovacuum keeps dead tuples controlled."
|
|
echo "- Action: rerun this script before and during a scan to compare write pressure and cache behavior over time."
|
|
} >> "$OUT_FILE"
|
|
|
|
echo "Diagnostics complete. Report written to: $OUT_FILE"
|