I was met with the challenge of running Zimbra on a server with only 6 GB of RAM, which is less than the recommended minimum of 8GB. This server had additional memory hungry processes running on it as well, which further constrained the available usable RAM for Zimbra to around 3GB. However, the server was only expected to handle about 30 accounts across 5 domains.

The default install of Zimbra sucked up almost all available RAM, and yet was frustratingly unresponsive. A lot of performance tuning guides out there were for outdated versions of Zimbra, so I set out to do some research for the latest version (8.7 at the time of writing).

Memory Optimizations

The two configuration changes that made the most difference were innodb_buffer_pool_size and mailboxd_java_heap_size. By default, these are both set to about 40% each of available memory, which leads to a lot of trashing to swap if you have other services running.

mailboxd_java_heap_size is set in /opt/zimbra/conf/my.cnf. I tweaked this down to a more reasonable 1GB:

1 innodb_buffer_pool_size = 1073741824

mailboxd_java_heap_size is set using zmlocalconfig, and I tuned this down to 1GB as well:

1 zmlocalconfig -e mailboxd_java_heap_size=1024
2 zmlocalconfig -e mailboxd_java_heap_new_size_percent=15

I also tuned down MySQL’s memory usage down to 15% of system memory:

1 zmlocalconfig -e mysql_memory_percent=15

Next up was the number of Amavis servers and ClamAV threads that are spun up (10 by default). I tuned this down to 1 which was more reasonable for this environment. One thing to note is the number of Amavis and ClamAV threads should match, i.e., if you set Amavis threads to 2, ClamAV threads must be at least 2 (they aparently can be more, but not less, so we set Amavis values first).

These values can be adjusted in zmprov:

1 zmprov ms `zmhostname` zimbraAmavisMaxServers 1
2 zmprov ms `zmhostname` zimbraClamAVMaxThreads 1

The next thing to adjust down is the number of LMTP threads from the default value of 20. I set this down to 4:

1 zmprov ms `zmhostname` zimbraLmtpNumThreads 4

I then adjusted the number of threads for http. This should roughly be the number of users who concurrently log in to the web client, * 1.5. In newer versions of Zimbra, zimbraHttpNumThreads controls both HTTP and HTTPS threads.

1 zmprov ms `zmhostname` zimbraHttpNumThreads 30

I tweaked the number of threads and open connections for POP and IMAP. I turned POP off as it wasn’t needed, and adjusted IMAP threads down as needed. Thanks to NIO, a single IMAP thread can handle about 200 to 500 connections. The rough calculation for the number of connections needed is num users * 4 * num devices per user. IMAP threads are pooled and unused threads don’t take up resources, so this value can be set higher than required.

1 zmprov ms `zmhostname` zimbraPop3ServerEnabled FALSE
2 zmprov ms `zmhostname` zimbraImapNumThreads 20
3 zmprov ms `zmhostname` zimbraImapMaxConnections 200

Finally, tweak down the number of message headers held in the server cache from 10,000 to a more reasonable value.

1 zmprov ms `zmhostname` zimbraMessageCacheSize 2000

CPU Optimizations

The zmconfigd process monitors Zimbra configuration keys, and restarts the appropriate Zimbra services when their values change. It also restarts failed processes ensuring HA. By default, this process checks the config values every 60 seconds and consumes a lot of CPU. I adjusted this down to run once every hour, which significantly reduced CPU usage.

1 zmlocalconfig -e zmconfigd_interval=3600

By default, Zimbra does a lot of logging and retains log files for up to two years. I adjusted this down to 1 month to conserver disk space.

1 zmprov mcf zimbraLogRawLifetime 7d
2 zmprov mcf zimbraLogSummaryLifetime 30d

The zimbra status log process runs every two minutes. I adjusted this down to a more reasonable 60 minutes. This can be adjusted in the zimbra cron file, by typing crontab -e when logged in as the zimbra user. Find the line that reads /opt/zimbra/libexec/zmstatuslog and change the cron timings from */2 to */60

1 */60 * * * * /opt/zimbra/libexec/zmstatuslog > /dev/null 2>&1

All said and done, a zimbra restart was in order:

1 zmcontrol restart

After restarting and allowing the server run for a day or two, I came back to check on memory usage and was pleased with the results:

1 ps aux | grep zimbra | awk '{memory +=$4}; END {print memory }'

showed 38.1, which meant Zimbra was using less than 50% of available memory, which corresponded with my stated goal of getting Zimbra to use 3GB or less of the available 6GB of memory. Web client responsiveness improved considerably, and swap usage came down close to the ideal value of ~0.

There are other settings that can be tweaked, including variables for the LDAP server, but a quick survey of Zimbra processes showed these were already using minuscule amounts of RAM, and tweaking them seemed unnecessary. Further information on these performance parameters can be found here:
https://wiki.zimbra.com/wiki/Performance_Tuning_Guidelines_for_Large_Deployments

Performance tune Zimbra 8.7 to run on 3GB of RAM or less