How to Optimize MySQL Memory Usage on a Low-RAM VPS (256MB)


2 views

When MySQLTuner reports your maximum possible memory usage exceeds physical RAM (153% in your case), it's calculating:

Global buffers + (Per-thread buffers × max_connections)

In your 256MB VPS, the current configuration could theoretically consume 392.9MB - a guaranteed path to thrashing.

Your current global buffers (338M) are shockingly high for a 256MB system. The main offenders:

query_cache_size = 64M  # Overkill for small VPS
key_buffer = 8M         # Too small for your 9.4M indexes
table_cache = 128       # Each consumes memory

Each connection consumes:

thread_stack (128K) 
+ sort_buffer (default 2M)
+ join_buffer (256K)
+ other per-connection buffers

With max_connections=20, this becomes dangerous quickly.

Immediate changes for your my.cnf:

[mysqld]
key_buffer_size = 12M            # Cover all MyISAM indexes
query_cache_size = 8M            # Drastic reduction
table_open_cache = 64            # Halve the cache
thread_cache_size = 4            # Reduce thread overhead
max_connections = 10             # Critical reduction
tmp_table_size = 16M             # From insane 256M
max_heap_table_size = 16M        # Match tmp_table_size
join_buffer_size = 128K          # Minimum effective size

Beyond configuration, execute these commands:

# Defragment tables (from MySQLTuner warning):
mysql -e "SELECT CONCAT('OPTIMIZE TABLE ', table_schema, '.', table_name, ';') 
FROM information_schema.tables 
WHERE data_free > 0 AND engine='MyISAM'" | mysql

# Monitor real memory usage:
watch -n 5 "ps -eo pmem,pcpu,rss,vsize,args | grep mysqld"

For applications you control, implement connection pooling. Example PHP snippet:

$mysqli = new mysqli('p:localhost', 'user', 'pass', 'db');
// 'p:' enables persistent connections

Consider installing ProxySQL to manage connection spikes:

# Installation (Ubuntu):
sudo apt-get install proxysql
# Configure in /etc/proxysql.cnf:
max_connections=100              # Proxy handles spikes
mysql-max_connections=5          # Real MySQL connections

Create a monitoring script (save as mysql_mem.sh):

#!/bin/bash
while true; do
  mysql -e "SHOW STATUS LIKE 'Threads_connected';"
  free -m
  sleep 5
done

After changes, your target metrics should show:

Maximum possible memory usage ≤ 200M (78% of RAM)
Threads_connected averaging ≤ 5
Key buffer hit rate ≥ 95%

Your MySQLTuner report shows alarming memory consumption (392.9M) exceeding your 256M VPS capacity. Let's decode how MySQL calculates this:

Maximum Memory Usage = 
  Global Buffers + 
  (Per-thread Buffers * max_connections) +
  (OS/Application Overhead)

Examining your my.cnf reveals several optimizations needed:

# Current problematic settings
key_buffer = 8M               # Too small for 9.4M indexes
query_cache_size = 64M        # Excessive for read-heavy workload
tmp_table_size = 256M         # Dangerous for 256M VPS
max_heap_table_size = 256M    # Matches tmp_table_size (good) but too large
max_connections = 20          # High for low-memory system

Immediate Memory Reductions

Add these to your my.cnf under [mysqld] section:

key_buffer_size = 10M         # Slightly larger than total index size
query_cache_size = 16M        # Reduced from 64M
tmp_table_size = 16M          # Drastically reduced
max_heap_table_size = 16M     # Must match tmp_table_size
table_open_cache = 64         # Reduced from 128
thread_cache_size = 4         # Reduced from 8
max_connections = 10          # Halved from 20

Query Cache Optimization

For your read-heavy workload (100% reads), monitor cache efficiency:

SHOW STATUS LIKE 'Qcache%';
SHOW VARIABLES LIKE 'query_cache%';

Consider disabling completely if efficiency remains low:

query_cache_type = 0
query_cache_size = 0

Address the 34 fragmented tables reported by MySQLTuner:

# Generate optimization commands for all MyISAM tables
SELECT CONCAT('OPTIMIZE TABLE ', table_schema, '.', table_name, ';') 
FROM information_schema.tables 
WHERE engine = 'MyISAM' AND table_schema NOT IN ('information_schema','mysql');

Instead of high max_connections, implement connection pooling:

# Example PHP PDO connection pooling
$options = [
    PDO::ATTR_PERSISTENT => true,
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
];
$dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass, $options);

Use this query to track real memory usage:

SELECT 
    @@key_buffer_size +
    @@query_cache_size +
    @@tmp_table_size +
    (@@read_buffer_size + @@sort_buffer_size + 
     @@join_buffer_size + @@thread_stack) * @@max_connections 
AS estimated_max_memory;

For pure MyISAM environments (no InnoDB), add these specialized settings:

myisam_sort_buffer_size = 2M
concurrent_insert = 2
bulk_insert_buffer_size = 1M