The Erlang VM (BEAM) can process millions of messages per second, but tuning it for CouchDB requires understanding how its concurrency model interacts with I/O and memory.
Let’s see BEAM handle a high-volume request. Imagine a single CouchDB node receiving 10,000 requests per second, each involving a read from disk and a small write back.
% Simulate a single request handler process
-module(couchdb_handler).
-export([start_link/1]).
start_link(NumRequests) ->
Pid = spawn(?MODULE, loop, [NumRequests]),
{ok, Pid}.
loop(0) ->
ok;
loop(N) ->
% Simulate fetching data from CouchDB (blocking I/O)
% In reality, this would involve network calls and disk reads
receive
{request, From} ->
% Simulate processing the request
% In CouchDB, this might involve document lookups, index queries, etc.
Result = {processed, self()},
From ! Result,
loop(N - 1)
end.
% In a real scenario, a supervisor would manage these handlers
% and a dispatcher would route incoming requests.
When this loop process receives a request, it simulates work. The key here is that BEAM’s scheduler is designed to preempt processes very quickly. If this simulated work took too long (e.g., a long-running CPU computation or a synchronous I/O call that blocks the OS thread), it would starve other processes. CouchDB, being an I/O-bound application, relies on BEAM’s ability to switch between many lightweight processes rapidly, allowing many requests to be "in flight" concurrently.
To tune for high throughput, we focus on how the BEAM handles processes, memory, and I/O. The default settings are often too conservative for aggressive workloads.
1. Scheduler Threads (+S)
- What broke: By default,
+Sis set totrue, which means the number of scheduler threads is automatically determined by the number of CPU cores. For high-throughput I/O-bound workloads, you often want more scheduler threads than cores to ensure that when one scheduler thread is waiting for I/O, another can immediately pick up a ready process. - Diagnosis: Check the current scheduler configuration:
erl -eval 'io:format("~p~n", [erlang:system_info(schedulers_online)])' -noshell - Fix: Start your CouchDB node with
+Sset to a value higher than your core count. A common starting point iscores * 2orcores * 3. For an 8-core machine, you might use:./bin/couchdb -eval 'erlang:display(erlang:system_info(schedulers_online)), erlang:halt()' -noshell # Check current ./bin/couchdb -eval 'erlang:system_info(schedulers_online).' # Check after starting with options # To start with 16 schedulers: ./bin/couchdb -s 16 - Why it works: More schedulers mean more OS threads running the BEAM scheduler loop. When one scheduler thread is blocked waiting for an OS-level I/O operation to complete (like a disk read), other scheduler threads can continue executing other ready Erlang processes. This keeps the CPU busy with actual work rather than waiting.
2. Max Processes (+P)
- What broke: Each Erlang process consumes a small amount of memory for its stack and heap. If CouchDB is handling thousands of concurrent requests, it might spawn thousands of processes. The default
+Pvalue (often 256,000) might be too low, causing the VM to refuse new processes. - Diagnosis: Monitor the number of processes:
If you see./bin/couchdb -eval 'erlang:display(erlang:system_info(process_count)), erlang:halt()' -noshellmaxapproaching the limit or errors about process creation failing, you need to increase+P. - Fix: Increase the
+Plimit. A value of 500,000 or 1,000,000 is common for high-load servers.# To set max processes to 1,000,000: ./bin/couchdb -p 1000000 - Why it works: This directly raises the ceiling on how many Erlang processes can exist concurrently within the VM. It’s a hard limit that prevents the system from running out of memory for process metadata, even if individual processes are small.
3. Stack Size (+Sst)
- What broke: Each Erlang process has a stack for function calls. If you have very deep call stacks (common in complex logic or recursive functions without tail-call optimization), you might hit the default stack limit.
- Diagnosis: Look for errors like
badargorstack overflowin your CouchDB logs, particularly during periods of high load. - Fix: Increase the stack size. The default is 128 words. Increase it to 256 or 512 words.
# To set stack size to 256 words: ./bin/couchdb -sst 256 - Why it works: This provides more space on the call stack for each process, allowing for deeper function call chains before a stack overflow occurs.
4. Max Heap Size (+HM) and Garbage Collection (+A)
- What broke: CouchDB processes create and discard many small data structures (e.g., document fragments, request contexts). If garbage collection (GC) is too aggressive or not frequent enough, it can lead to increased latency and memory pressure. The default GC might not be optimal for a workload with many short-lived objects.
- Diagnosis: Monitor memory usage and GC activity. Tools like
etoporobservercan show process memory and GC stats. Long GC pauses are the primary symptom. - Fix: While
+HM(max heap size) can be tuned, more impactful is often adjusting the garbage collection strategy. For many short-lived objects, a "trivial GC" strategy can be more efficient.# This is often configured via erlang:apply(erlang, system_info, [runtime_options]) # but for CouchDB, it's typically set in the vm.args file. # Example: in vm.args, add: # +A 1 # +pc unicode # +sbwt none # The +A option controls the garbage collector. +A 1 enables trivial GC. # +pc unicode is good for UTF-8, which CouchDB uses extensively. # +sbwt none disables the scheduler blocking warning, which can be noisy. - Why it works: Trivial GC (
+A 1) is optimized for heaps where most objects are short-lived. It performs GC more frequently on smaller heaps, reducing the chance of long pauses and keeping memory usage lower per process.+pc unicodeensures efficient handling of UTF-8 characters.
5. Open Files Limit (ulimit -n)
- What broke: CouchDB, like any network service, opens many files: sockets for connections, files for data storage (e.g.,
.couchfiles, index files). If the OS limit on open files (ulimit -n) is too low, CouchDB will fail to accept new connections or open new data files. - Diagnosis: Check the open file limit for the user running CouchDB:
Look for errors like "too many open files" in CouchDB logs.ulimit -n - Fix: Increase the open files limit for the CouchDB user. This is usually done in
/etc/security/limits.confor systemd service files. A common recommendation is 65536 or higher.
Then restart CouchDB.# Example for /etc/security/limits.conf: couchdb soft nofile 65536 couchdb hard nofile 65536 - Why it works: This allows the operating system to allocate more file descriptors to the CouchDB process, enabling it to manage more concurrent network connections and data file handles.
Tuning the Erlang VM for high-throughput CouchDB workloads is about giving the scheduler enough threads to keep busy, allowing enough processes to exist, and ensuring the garbage collector doesn’t become a bottleneck for short-lived data.
After these changes, the next common bottleneck you’ll encounter is disk I/O saturation.