Bulut Depolama ile Media İşleme Pipeline’ı: Video ve Görsel Optimizasyonu
Video ve görsel içeriklerin boyutu, modern web uygulamalarının en büyük performans düşmanlarından biri. Bir kullanıcı sayfanıza girdiğinde 4MB’lık bir JPEG yüklüyorsa ya da optimize edilmemiş bir MP4 akıyor ise, o kullanıcıyı kaybetmeniz an meselesi. Bulut depolama ve medya işleme pipeline’larını bir araya getirerek bu sorunu sistematik olarak çözmek mümkün. Bu yazıda S3 uyumlu depolama, FFmpeg tabanlı video işleme ve görsel optimizasyon araçlarını kullanarak production ortamına uygun, otomatik bir medya pipeline’ı nasıl kurulur adım adım anlatacağım.
Mimariyi Kafaya Oturtmak
Önce ne inşa edeceğimizi netleştirelim. Temel fikir şu: Bir kullanıcı ham videoyu veya görseli yüklüyor, bu dosya bir tetikleyici oluşturuyor, arka planda işleme başlıyor, optimize edilmiş çıktılar CDN üzerinden servis ediliyor. Kulağa basit geliyor ama detaylar can sıkıcı olabiliyor.
Pipeline bileşenleri şöyle sıralanıyor:
- Kaynak depolama: Ham dosyaların tutulduğu S3 bucket’ı (ya da MinIO, DigitalOcean Spaces)
- İşleme katmanı: FFmpeg ve ImageMagick/Sharp çalıştıran worker servisler
- Çıktı depolama: Optimize edilmiş dosyaların tutulduğu ayrı bucket
- Mesaj kuyruğu: RabbitMQ veya AWS SQS ile işleme görevleri
- CDN katmanı: CloudFront veya Cloudflare üzerinden servis
Bu mimaride kaynak bucket’ına hiçbir zaman son kullanıcı erişmemeli. Sadece CDN’den servis etmelisiniz.
Ortam Kurulumu
Önce işleme sunucusunu hazırlayalım. Ubuntu 22.04 baz alıyoruz:
# FFmpeg ve gerekli codec'leri kur
apt update && apt install -y
ffmpeg
imagemagick
libvips-tools
python3-pip
awscli
nodejs
npm
# Sharp için Node.js bağımlılıkları
npm install -g sharp-cli
# Python bağımlılıkları
pip3 install boto3 pillow celery redis
# FFmpeg versiyonunu doğrula
ffmpeg -version | head -n 1
# libvips versiyonunu kontrol et
vips --version
AWS CLI veya S3 uyumlu depolama için kimlik bilgilerini yapılandıralım:
# MinIO veya S3 uyumlu servis için
export AWS_ACCESS_KEY_ID="your_access_key"
export AWS_SECRET_ACCESS_KEY="your_secret_key"
export AWS_DEFAULT_REGION="eu-central-1"
export S3_ENDPOINT_URL="https://s3.your-provider.com" # MinIO için
# Bucket yapısını oluştur
aws s3 mb s3://media-raw --endpoint-url $S3_ENDPOINT_URL
aws s3 mb s3://media-processed --endpoint-url $S3_ENDPOINT_URL
aws s3 mb s3://media-thumbnails --endpoint-url $S3_ENDPOINT_URL
# Bucket politikalarını ayarla
aws s3api put-bucket-acl
--bucket media-processed
--acl public-read
--endpoint-url $S3_ENDPOINT_URL
Video İşleme Pipeline’ı
Video optimizasyonu en kaynak yoğun kısım. FFmpeg ile birden fazla çözünürlük ve format çıktısı üretmek gerekiyor. Modern tarayıcılar için H.264, VP9 ve AV1 formatlarını desteklemeniz ideal ancak AV1 encoding süresi çok uzun, production’da H.264 ve VP9 ile başlayın.
#!/bin/bash
# video_process.sh - Ham videoyu optimize et
INPUT_FILE="$1"
OUTPUT_DIR="$2"
BASENAME=$(basename "$INPUT_FILE" | sed 's/.[^.]*$//')
# Giriş video bilgilerini çek
VIDEO_WIDTH=$(ffprobe -v error -select_streams v:0
-show_entries stream=width
-of default=noprint_wrappers=1:nokey=1 "$INPUT_FILE")
VIDEO_HEIGHT=$(ffprobe -v error -select_streams v:0
-show_entries stream=height
-of default=noprint_wrappers=1:nokey=1 "$INPUT_FILE")
VIDEO_DURATION=$(ffprobe -v error
-show_entries format=duration
-of default=noprint_wrappers=1:nokey=1 "$INPUT_FILE")
echo "Kaynak: ${VIDEO_WIDTH}x${VIDEO_HEIGHT}, Süre: ${VIDEO_DURATION}s"
# 1080p H.264 (yüksek kalite)
ffmpeg -i "$INPUT_FILE"
-c:v libx264
-preset slow
-crf 22
-c:a aac
-b:a 128k
-movflags +faststart
-vf "scale='min(1920,iw)':'min(1080,ih)':force_original_aspect_ratio=decrease"
-y "${OUTPUT_DIR}/${BASENAME}_1080p.mp4"
# 720p H.264 (orta kalite)
ffmpeg -i "$INPUT_FILE"
-c:v libx264
-preset medium
-crf 24
-c:a aac
-b:a 96k
-movflags +faststart
-vf "scale='min(1280,iw)':'min(720,ih)':force_original_aspect_ratio=decrease"
-y "${OUTPUT_DIR}/${BASENAME}_720p.mp4"
# 480p H.264 (düşük bant genişliği)
ffmpeg -i "$INPUT_FILE"
-c:v libx264
-preset fast
-crf 28
-c:a aac
-b:a 64k
-movflags +faststart
-vf "scale='min(854,iw)':'min(480,ih)':force_original_aspect_ratio=decrease"
-y "${OUTPUT_DIR}/${BASENAME}_480p.mp4"
# Thumbnail oluştur (her 10 saniyede bir kare)
ffmpeg -i "$INPUT_FILE"
-vf "fps=1/10,scale=320:-1"
-q:v 3
"${OUTPUT_DIR}/${BASENAME}_thumb_%03d.jpg"
# Video poster görseli (ilk kare)
ffmpeg -i "$INPUT_FILE"
-ss 00:00:01
-vframes 1
-q:v 2
"${OUTPUT_DIR}/${BASENAME}_poster.jpg"
echo "Video işleme tamamlandı: $OUTPUT_DIR"
-movflags +faststart parametresine dikkat edin. Bu flag, MP4 dosyasının metadata’sını başa alıyor, böylece video tam indirilmeden oynatılabiliyor. Bu olmadan kullanıcılar dosya tamamen inene kadar beklemek zorunda kalıyor.
HLS ile Adaptive Bitrate Streaming
Büyük videolar için HLS formatına dönüştürmek şart. Bu sayede kullanıcının bant genişliğine göre otomatik kalite seçimi yapılıyor:
#!/bin/bash
# hls_package.sh - HLS paketleme
INPUT_FILE="$1"
OUTPUT_DIR="$2"
BASENAME=$(basename "$INPUT_FILE" | sed 's/.[^.]*$//')
HLS_DIR="${OUTPUT_DIR}/${BASENAME}_hls"
mkdir -p "$HLS_DIR"
# Çoklu bitrate HLS oluştur
ffmpeg -i "$INPUT_FILE"
-filter_complex
"[v:0]split=3[v1][v2][v3];
[v1]scale=1920:1080[v1out];
[v2]scale=1280:720[v2out];
[v3]scale=854:480[v3out]"
-map "[v1out]" -c:v:0 libx264 -b:v:0 5000k -maxrate:v:0 5350k -bufsize:v:0 7500k
-map "[v2out]" -c:v:1 libx264 -b:v:1 2800k -maxrate:v:1 2996k -bufsize:v:1 4200k
-map "[v3out]" -c:v:2 libx264 -b:v:2 1400k -maxrate:v:2 1498k -bufsize:v:2 2100k
-map a:0 -map a:0 -map a:0
-c:a aac -b:a 128k
-f hls
-hls_time 6
-hls_playlist_type vod
-hls_flags independent_segments
-hls_segment_type mpegts
-hls_segment_filename "${HLS_DIR}/stream_%v/segment_%03d.ts"
-master_pl_name "master.m3u8"
-var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2"
"${HLS_DIR}/stream_%v/playlist.m3u8"
echo "HLS paketleme tamamlandı: $HLS_DIR"
# Tüm HLS segmentlerini S3'e yükle
aws s3 sync "$HLS_DIR"
"s3://media-processed/${BASENAME}/hls/"
--cache-control "max-age=31536000"
--content-type "application/x-mpegURL"
--exclude "*"
--include "*.m3u8"
--endpoint-url $S3_ENDPOINT_URL
aws s3 sync "$HLS_DIR"
"s3://media-processed/${BASENAME}/hls/"
--cache-control "max-age=31536000"
--content-type "video/MP2T"
--exclude "*"
--include "*.ts"
--endpoint-url $S3_ENDPOINT_URL
Görsel Optimizasyon Pipeline’ı
Görsel optimizasyonu için ImageMagick ve libvips kullanacağız. libvips, büyük görselleri işlerken çok daha az bellek tüketiyor ve ImageMagick’ten yaklaşık 4-8 kat daha hızlı çalışıyor. E-ticaret sitelerinde ürün görselleri için bu fark çok kritik oluyor.
#!/bin/bash
# image_optimize.sh - Görsel optimizasyon pipeline'ı
INPUT_FILE="$1"
OUTPUT_DIR="$2"
BASENAME=$(basename "$INPUT_FILE" | sed 's/.[^.]*$//')
EXTENSION="${INPUT_FILE##*.}"
# WebP dönüşümü (modern tarayıcılar için)
vips webpsave "$INPUT_FILE"
"${OUTPUT_DIR}/${BASENAME}.webp"
--Q 82
--strip
--lossless FALSE
# AVIF dönüşümü (daha iyi sıkıştırma, daha yavaş)
# Sadece boyutu 500KB üzerindeyse AVIF üret
FILE_SIZE=$(stat -f%z "$INPUT_FILE" 2>/dev/null || stat -c%s "$INPUT_FILE")
if [ "$FILE_SIZE" -gt 512000 ]; then
vips heifsave "$INPUT_FILE"
"${OUTPUT_DIR}/${BASENAME}.avif"
--Q 70
--compression av1
--strip TRUE
fi
# Responsive görseller için farklı boyutlar
SIZES=(320 640 960 1280 1920)
for SIZE in "${SIZES[@]}"; do
# WebP versiyonu
vips thumbnail "$INPUT_FILE"
"${OUTPUT_DIR}/${BASENAME}_${SIZE}w.webp"
"$SIZE"
--height $((SIZE * 2))
--size down
# JPEG fallback
vips thumbnail "$INPUT_FILE"
"${OUTPUT_DIR}/${BASENAME}_${SIZE}w.jpg"
"$SIZE"
--height $((SIZE * 2))
--size down
# JPEG'i optimize et
mogrify -strip -quality 85
-sampling-factor 4:2:0
"${OUTPUT_DIR}/${BASENAME}_${SIZE}w.jpg"
done
echo "Görsel optimizasyon tamamlandı: $OUTPUT_DIR"
Python ile Otomatik İşleme Orchestration
Tüm bu işlemleri Celery ve Redis ile otomatize edelim. Gerçek dünyada S3 event notification veya SQS tetikleyicisi kullanıyorsunuz, burada basit bir Celery worker gösteriyorum:
#!/usr/bin/env python3
# tasks.py - Celery medya işleme görevleri
import os
import subprocess
import boto3
from celery import Celery
from pathlib import Path
import tempfile
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = Celery('media_pipeline',
broker='redis://localhost:6379/0',
backend='redis://localhost:6379/1')
app.conf.update(
task_serializer='json',
result_expires=3600,
worker_concurrency=4,
task_acks_late=True,
worker_prefetch_multiplier=1
)
s3_client = boto3.client(
's3',
endpoint_url=os.getenv('S3_ENDPOINT_URL'),
aws_access_key_id=os.getenv('AWS_ACCESS_KEY_ID'),
aws_secret_access_key=os.getenv('AWS_SECRET_ACCESS_KEY')
)
@app.task(bind=True, max_retries=3, default_retry_delay=60)
def process_video(self, bucket_name, object_key):
"""Ham videoyu işle ve optimize et"""
try:
with tempfile.TemporaryDirectory() as tmpdir:
input_path = os.path.join(tmpdir, 'input_video')
output_dir = os.path.join(tmpdir, 'output')
os.makedirs(output_dir)
logger.info(f"Video indiriliyor: s3://{bucket_name}/{object_key}")
s3_client.download_file(bucket_name, object_key, input_path)
# Video işleme script'ini çalıştır
result = subprocess.run(
['/opt/scripts/video_process.sh', input_path, output_dir],
capture_output=True,
text=True,
timeout=3600
)
if result.returncode != 0:
raise Exception(f"Video işleme hatası: {result.stderr}")
# Çıktıları S3'e yükle
basename = Path(object_key).stem
upload_count = 0
for output_file in Path(output_dir).rglob('*'):
if output_file.is_file():
relative_path = output_file.relative_to(output_dir)
s3_key = f"processed/{basename}/{relative_path}"
content_type = 'video/mp4'
if str(output_file).endswith('.jpg'):
content_type = 'image/jpeg'
elif str(output_file).endswith('.m3u8'):
content_type = 'application/x-mpegURL'
elif str(output_file).endswith('.ts'):
content_type = 'video/MP2T'
s3_client.upload_file(
str(output_file),
'media-processed',
s3_key,
ExtraArgs={
'ContentType': content_type,
'CacheControl': 'max-age=31536000'
}
)
upload_count += 1
logger.info(f"Video işleme tamamlandı: {upload_count} dosya yüklendi")
return {'status': 'success', 'files_uploaded': upload_count}
except Exception as exc:
logger.error(f"Hata: {exc}")
raise self.retry(exc=exc)
@app.task(bind=True, max_retries=3)
def process_image(self, bucket_name, object_key):
"""Ham görseli optimize et"""
try:
with tempfile.TemporaryDirectory() as tmpdir:
input_path = os.path.join(tmpdir, 'input_image')
output_dir = os.path.join(tmpdir, 'output')
os.makedirs(output_dir)
s3_client.download_file(bucket_name, object_key, input_path)
result = subprocess.run(
['/opt/scripts/image_optimize.sh', input_path, output_dir],
capture_output=True,
text=True,
timeout=300
)
if result.returncode != 0:
raise Exception(f"Görsel işleme hatası: {result.stderr}")
basename = Path(object_key).stem
for output_file in Path(output_dir).glob('*'):
if output_file.is_file():
ext = output_file.suffix.lower()
content_types = {
'.webp': 'image/webp',
'.avif': 'image/avif',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg'
}
content_type = content_types.get(ext, 'application/octet-stream')
s3_key = f"images/{basename}/{output_file.name}"
s3_client.upload_file(
str(output_file),
'media-processed',
s3_key,
ExtraArgs={
'ContentType': content_type,
'CacheControl': 'max-age=2592000'
}
)
return {'status': 'success', 'key': object_key}
except Exception as exc:
raise self.retry(exc=exc)
CDN Cache ve Invalidation Stratejisi
İşlenen dosyaları CDN üzerinden serve etmek yetmiyor, cache stratejisini de doğru kurgulamak gerekiyor:
#!/bin/bash
# cdn_invalidate.sh - CloudFront cache temizleme
DISTRIBUTION_ID="E1EXAMPLE123"
PATHS_FILE="$1"
# Toplu invalidation oluştur (AWS başına 1000 path'e kadar ücretsiz)
aws cloudfront create-invalidation
--distribution-id "$DISTRIBUTION_ID"
--paths file://"$PATHS_FILE"
# Invalidation durumunu takip et
INVALIDATION_ID=$(aws cloudfront list-invalidations
--distribution-id "$DISTRIBUTION_ID"
--query 'InvalidationList.Items[0].Id'
--output text)
echo "Invalidation başlatıldı: $INVALIDATION_ID"
# Tamamlanana kadar bekle
aws cloudfront wait invalidation-completed
--distribution-id "$DISTRIBUTION_ID"
--id "$INVALIDATION_ID"
echo "Cache temizleme tamamlandı"
# Cloudflare için alternatif (Zone ID gerekli)
# curl -X POST "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/purge_cache"
# -H "Authorization: Bearer ${CF_API_TOKEN}"
# -H "Content-Type: application/json"
# --data '{"prefixes":["https://cdn.example.com/processed/"]}'
İzleme ve Hata Yönetimi
Production ortamında pipeline sağlığını izlemek kritik. Basit bir monitoring scripti:
#!/bin/bash
# pipeline_monitor.sh - Pipeline sağlık kontrolü
REDIS_HOST="localhost"
QUEUE_NAME="celery"
ALERT_THRESHOLD=50 # Kuyrukta bu kadar görev birikirse uyar
# Celery kuyruk uzunluğunu kontrol et
QUEUE_LENGTH=$(redis-cli -h "$REDIS_HOST" llen "$QUEUE_NAME" 2>/dev/null)
if [ -z "$QUEUE_LENGTH" ]; then
echo "CRITICAL: Redis bağlantısı kurulamadı"
exit 2
fi
echo "Kuyruk uzunluğu: $QUEUE_LENGTH"
if [ "$QUEUE_LENGTH" -gt "$ALERT_THRESHOLD" ]; then
echo "WARNING: Kuyruk dolmaya başlıyor ($QUEUE_LENGTH görev bekliyor)"
# Slack veya PagerDuty webhook
curl -s -X POST "$SLACK_WEBHOOK_URL"
-H "Content-Type: application/json"
-d "{"text": "Media pipeline uyarı: $QUEUE_LENGTH görev kuyrukta bekliyor"}"
fi
# Worker süreçlerini kontrol et
WORKER_COUNT=$(celery -A tasks inspect active 2>/dev/null | grep -c "worker@" || echo 0)
echo "Aktif worker sayısı: $WORKER_COUNT"
if [ "$WORKER_COUNT" -eq 0 ]; then
echo "CRITICAL: Hiç aktif worker yok, yeniden başlatılıyor..."
systemctl restart celery-media-worker
fi
# S3 bucket boyutlarını raporla
for BUCKET in media-raw media-processed; do
BUCKET_SIZE=$(aws s3 ls "s3://${BUCKET}" --recursive --human-readable
--summarize 2>/dev/null | grep "Total Size" | awk '{print $3,$4}')
echo "Bucket ${BUCKET}: $BUCKET_SIZE"
done
Gerçek Dünya Senaryosu: E-Ticaret Platformu
Bir e-ticaret projesinde bu pipeline’ı hayata geçirdiğimizde karşılaştığımız bazı durumları paylaşayım. 500.000 ürün görseli olan bir mağazada görsellerin ortalama boyutu 2.3MB’tı. Pipeline sonrasında WebP çıktıları ortalama 280KB’a düştü, yani yaklaşık yüzde seksen yedi küçülme. Sayfa yükleme süresi 4.2 saniyeden 1.1 saniyeye indi.
Dikkat etmeniz gereken bazı noktalar var:
- JPEG kalite dengesi: 85 kalite değeri genellikle ideal başlangıç noktası, ancak fotoğraf içeriğine göre 75-90 arası test edin
- WebP desteği: Internet Explorer desteklemiyor ama artık 2024’te bu çok küçük bir kullanıcı kitlesi, JPEG fallback bulundurun
- Video timeout ayarları: 4K videolar için Celery task timeout’unu en az 2 saate ayarlayın
- Disk alanı: Worker sunucularında geçici dosyalar için yeterli disk alanı bırakın, 100GB’lık ham video işliyorsanız tmpdir için 300GB’a ihtiyacınız olabilir
- Concurrent işleme sınırı: CPU başına maksimum 2 FFmpeg süreci çalıştırın, aksi takdirde sistem yanıt vermez hale geliyor
ImageMagick kullanıyorsanız /etc/ImageMagick-6/policy.xml dosyasındaki bellek ve disk limitleri üretim ortamınıza göre mutlaka ayarlanmalı. Varsayılan değerler çok kısıtlayıcı:
# ImageMagick policy ayarlarını güncelle
sed -i 's/rights="none" pattern="PS"/rights="read|write" pattern="PS"/'
/etc/ImageMagick-6/policy.xml
# Bellek limitini artır
sed -i 's/<policy domain="resource" name="memory" value="256MiB"/>/<policy domain="resource" name="memory" value="2GiB"/>/'
/etc/ImageMagick-6/policy.xml
Sonuç
Bu pipeline kurulumu ilk bakışta karmaşık görünebiliyor ama parçalara ayırdığınızda her bileşen kendi içinde anlaşılır. Önemli olan tasarım kararları: Ham dosyaları asla CDN üzerinden serve etmemek, her format için doğru cache header’larını set etmek, worker’ları iş yüküne göre horizontal scale edebilmek ve monitoring olmadan üretime almamak.
S3 event notification veya SQS ile tetikleyici ekleyerek bu sistemi tam otomatik hale getirmek mümkün. Büyük hacimli işlemler için AWS Batch veya Kubernetes Job’ları da tercih edilebilir. Ancak başlangıç için Celery ile Redis yeterince sağlam ve yönetimi kolay bir altyapı sunuyor.
Boyut küçültmeden elde ettiğiniz CDN bant genişliği tasarrufu hem maliyetleri düşürüyor hem de kullanıcı deneyimini ciddi ölçüde iyileştiriyor. Bu iki şeyin aynı anda gerçekleştiği durumlar sysadmin hayatında pek fazla olmuyor, bu yüzden iyi bir medya pipeline’ı kurmak her zaman değer.
