Microservices Without the Migraine: Production Patterns for 2019
Everyone wants to talk about breaking apart the monolith until they see the latency graph of their first distributed stack. I’ve seen it happen too many times: a team in Oslo takes a perfectly functional PHP application, chops it into twelve Go services, and suddenly their page load time jumps from 200ms to 1.5 seconds. Why? Because they treated network calls like local function calls.
Microservices aren't a silver bullet; they are a trade-off. You trade code complexity for operational complexity. If your infrastructure isn't rock solid—if you're dealing with CPU steal or slow disk I/O—your distributed architecture will collapse under its own weight. Here is how we build this correctly, using patterns that actually work in production environments right now.
1. The API Gateway: Stop Exposing Your Internals
The biggest mistake I see in 2019 is frontend applications calling microservices directly. Do not do this. It creates a tightly coupled mess where a change in your User Service API breaks the mobile app.
You need an API Gateway. It handles SSL termination, rate limiting, and request routing. While Kong and Traefik are gaining traction, good old Nginx is still the king of performance per watt. Here is a battle-tested configuration for routing traffic to upstream services while handling failures gracefully.
http {
upstream user_service {
server 10.10.0.5:8080 max_fails=3 fail_timeout=30s;
server 10.10.0.6:8080 max_fails=3 fail_timeout=30s;
keepalive 64;
}
upstream inventory_service {
server 10.10.0.10:5000;
keepalive 64;
}
server {
listen 443 ssl http2;
server_name api.yourservice.no;
# SSL config omitted for brevity
location /v1/users/ {
proxy_pass http://user_service/;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
# Aggressive timeouts are better than hanging connections
proxy_connect_timeout 5s;
proxy_read_timeout 10s;
}
}
}
2. The "Database per Service" Reality Check
Sharing a single MySQL instance across ten services is not microservices architecture; it's a distributed monolith with a single point of failure. Each service needs its own datastore. This ensures that a complex join in your Analytics service doesn't lock the table for your Checkout service.
However, this multiplies your connection overhead. If you are running PostgreSQL, you simply cannot survive without connection pooling. PgBouncer is mandatory.
Here is a snippet for a robust pgbouncer.ini setup that we use for high-throughput services:
[databases]
user_db = host=127.0.0.1 port=5432 dbname=user_db
[pgbouncer]
listen_port = 6432
listen_addr = *
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
Pro Tip: When splitting databases, you lose ACID transactions across services. You must implement eventual consistency. Don't try to implement Two-Phase Commit (2PC) in 2019; it's a performance killer. Use the Saga pattern instead.
3. Infrastructure Matters: The "Noisy Neighbor" Problem
This is where the hardware hits the road. In a monolithic app, a 50ms disk spike slows down one request. In a microservices chain of 5 services, that latency compounds. If Service A waits for Service B, which waits for Service C, and Service C is on a VPS with a slow HDD and a neighbor mining crypto, your request times out.
For microservices, consistent low latency is more important than raw throughput. This is why we built CoolVDS on pure NVMe storage with KVM virtualization. Unlike OpenVZ (which relies on a shared kernel), KVM ensures that your memory and CPU registers are yours alone.
Kernel Tuning for Microservices
Default Linux kernel settings are tuned for general usage, not for handling thousands of ephemeral connections between containers. You need to tune sysctl.conf to avoid running out of TCP sockets.
# /etc/sysctl.conf
# Allow reuse of sockets in TIME_WAIT state for new connections
net.ipv4.tcp_tw_reuse = 1
# Increase range of local ports to allow more outgoing connections
net.ipv4.ip_local_port_range = 1024 65535
# Increase max open files (essential for heavy I/O)
fs.file-max = 2097152
# Protect against SYN flood attacks
net.ipv4.tcp_syncookies = 1
Apply these with sysctl -p. If you are on a managed host that blocks sysctl access, migrate immediately.
4. Observability: If You Can't See It, It's Down
In a monolith, you tail a log file. In microservices, you have 20 log files scattered across different containers. You need centralized logging and metrics. The ELK stack (Elasticsearch, Logstash, Kibana) is the industry standard for logs, but it is heavy on resources (Java heap size is no joke).
For metrics, Prometheus combined with Grafana is the superior choice in 2019. It pulls metrics rather than waiting for them to be pushed. Here is a basic Prometheus scrape config for a Dockerized environment:
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'node_exporter'
static_configs:
- targets: ['node-exporter:9100']
- job_name: 'docker_containers'
static_configs:
- targets: ['cadvisor:8080']
5. The Norwegian Context: Latency and Compliance
If your target audience is in Norway, hosting your microservices in a US-East data center is architectural suicide. The round-trip time (RTT) alone will kill the snappiness of your application. With the GDPR in full force and the Datatilsynet keeping a close watch on data sovereignty, keeping data within the EEA (European Economic Area) is not just a technical preference; it's a legal safeguard.
Peering matters. CoolVDS routes traffic through optimized paths to NIX (Norwegian Internet Exchange), ensuring that a request from a user in Bergen doesn't route through Stockholm to get to Oslo.
Comparison: Monolith vs. Microservices on CoolVDS
| Feature | Monolith | Microservices |
|---|---|---|
| Deployment | Simple (Copy file) | Complex (CI/CD pipelines, Docker) |
| Scaling | Vertical (Bigger Server) | Horizontal (More Nodes) |
| Storage Req. | Standard SSD | NVMe (High IOPS required) |
| Fault Tolerance | Low (Process crash kills all) | High (Circuit Breakers isolate failures) |
Final Thoughts
Microservices solve the problem of organizational scaling, but they introduce the problem of distributed latency. You cannot fix bad architecture with good code, and you cannot fix bad infrastructure with good architecture.
You need a foundation that supports high concurrency and low I/O wait times. That is why we designed our KVM slices the way we did. We don't oversell, and we don't throttle your I/O when you need it most.
Ready to decouple your architecture? Deploy a KVM NVMe instance on CoolVDS today and see what 0% I/O wait feels like.