Dobrodošli u sedmi deo serijala na ITNetwork.rs! Do sada smo prošli kroz monolit i mikroservise, frontend arhitekturu, API-je, baze, mikroservise i serverless. Danas ulazimo u srž brzine i otpornosti – keširanje, optimizaciju performansi i skaliranje. Naučićemo kako smanjiti latenciju sa <50 ms, kako izbeći „thundering herd“ i kako skalirati sa milionima korisnika.
Zašto su performanse ključne?
| Metrika | Utjecaj |
|---|---|
| 1s kašnjenja | –16% konverzije (Amazon) |
| 100ms brže | +1% prihoda (Google) |
| LCP > 2.5s | 30% bounce rate |
Brzina nije luksuz – to je osnovna funkcionalnost.
1. Keširanje – više nivoa, više brzine
Keširanje na 4 nivoa
1.1 HTTP Keširanje (CDN + Browser)
Ključni zaglavlji:
Cache-Control: public, max-age=31536000, immutable
ETag: "abc123"
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT
Primer: Statički asseti (Next.js)
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/_next/static/:path*',
headers: [
{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }
]
}
];
}
};
Rezultat: CSS/JS se kešira godinu dana u browseru i CDN-u.
1.2 Aplikativno keširanje (Redis)
Kada keširati?
| Podaci | TTL |
|---|---|
| Lista proizvoda | 5–15 min |
| Korisnički profil | 1 min |
| Konfiguracija | 1h (sa invalidacijom) |
// lib/cache.ts
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
const CACHE_TTL = 300; // 5 min
export async function getExpensiveData(userId: string) {
const cacheKey = `user:${userId}:dashboard`;
// 1. Proveri keš
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// 2. Ako nema – DB
const data = await db.query('SELECT ... FROM analytics WHERE user_id = $1', [userId]);
// 3. Sačuvaj u keš
await redis.set(cacheKey, JSON.stringify(data), 'EX', CACHE_TTL);
return data;
}
PostgreSQL trikovi:
-- Materialized View (osveži svakih 5 min)
CREATE MATERIALIZED VIEW daily_sales AS
SELECT date, SUM(amount) FROM orders GROUP BY date;
-- Osveži asinhrono
REFRESH MATERIALIZED VIEW CONCURRENTLY daily_sales;
2. Rate Limiting & Throttling
Zaštita od:
- DDoS napada
- „Scraping“ botova
- „Thundering herd“ (1000 korisnika odjednom)
Alati:
| Alat | Gde |
|---|---|
| Redis + Lua | API Gateway |
| Cloudflare Rate Limiting | CDN nivo |
| NGINX limit_req | Reverse proxy |
-- rate-limiter.lua
local key = KEYS[1] -- IP adresa
local limit = tonumber(ARGV[1]) -- 10
local window = tonumber(ARGV[2]) -- 1s
local current = redis.call('INCR', key)
if current == 1 then
redis.call('EXPIRE', key, window)
end
if current > limit then
return 0
else
return 1
end
// middleware.ts
const script = redis.defineScript({ /* Lua script */ });
app.use(async (req, res, next) => {
const ip = req.ip;
const allowed = await redis.evalsha(script.sha, 1, `rate:${ip}`, 10, 1);
if (!allowed) {
return res.status(429).json({ error: 'Too many requests' });
}
next();
});
| Tip | Kada | Primer |
|---|---|---|
| Vertikalno | Mali tim, jednostavnost | Veći VM (16 → 32 GB RAM) |
| Horizontalno | Veliko opterećenje | Dodaj nodove iza Load Balancer-a |
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 3
maxReplicas: 50
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
Strategije:
| Strategija | Opis |
|---|---|
| Round Robin | Ravnomerno |
| Least Connections | Najmanje aktivnih |
| IP Hash | Isti korisnik → isti server |
// health.ts
app.get('/health', (req, res) => {
const checks = {
database: db.isConnected,
redis: redis.status === 'ready',
uptime: process.uptime()
};
const healthy = Object.values(checks).every(Boolean);
res.status(healthy ? 200 : 503).json(checks);
});
| Metrika | Alat | Cilj |
|---|---|---|
| Latency | Prometheus | P95 < 200ms |
| Error Rate | Grafana | < 0.1% |
| Throughput | Loki | req/s |
| Saturation | Node.js os.loadavg() | CPU < 80% |
import { Registry, Counter, Histogram } from 'prom-client';
const register = new Registry();
const httpRequests = new Counter({
name: 'http_requests_total',
help: 'Broj HTTP zahteva',
labelNames: ['method', 'route', 'status']
});
const requestDuration = new Histogram({
name: 'http_request_duration_ms',
help: 'Trajanje zahteva',
buckets: [5, 10, 25, 50, 100, 250, 500, 1000]
});
app.use((req, res, next) => {
const end = requestDuration.startTimer();
res.on('finish', () => {
httpRequests.inc({ method: req.method, route: req.route?.path || req.path, status: res.statusCode });
end({ route: req.route?.path });
});
next();
});
Arhitektura

Prvi poziv: 800ms Sledeći (5 min): <50ms
Poređenje: Bez vs Sa keširanjem
| Scenarij | Latencija | Trošak baze | Skalabilnost |
|---|---|---|---|
| Bez keša | 800ms | 1000 req/s | ★★ |
| Sa Redis + CDN | 50ms | 10 req/s | ★★★★★ |
| Nivo | Kada koristiti |
|---|---|
| CDN | Statički asseti, HTML (SSG) |
| Redis | Dinamički podaci, sesije |
| Aplikacija | Lokalni keš (LRU) |
| Baza | Materialized views, indeksiranje |
Zlatno pravilo: „Keširaj sve što možeš – ali uvek imaj plan za invalidaciju.“
Šta dalje?
U osmom delu ulazimo u bezbednost:
- JWT vs OAuth2
- RBAC/ABAC
- Passkeys, Magic Links
Tvoj izazov
- Dodaj Redis keš u svoj API
- Postavi Cloudflare sa Cache Everything za /blog/*
- Uradi load test sa k6:
bash
k6 run --vus 100 --duration 30s script.js - Komentariši ispod: „Koliko je tvoj P95 latencija trenutno?“
Autor: Dušan Antonijević – saradnik ITNetwork.rs
Hvala na čitanju! Podeli tekst sa kolegama.



