Multipart Upload ile Büyük Dosya Yükleme Optimizasyonu

Büyük dosya transferleri her sysadmin’in kabusudur. 50 GB’lık bir veritabanı yedeğini S3’e yüklerken bağlantı 3 saat sonra koptuğunda, baştan başlamak zorunda kalmak hem sinir bozucu hem de zaman kaybıdır. İşte tam burada multipart upload devreye girer. Bu yazıda gerçek dünya senaryoları üzerinden büyük dosya yükleme optimizasyonunu ele alacağız.

Multipart Upload Nedir ve Neden Önemlidir?

Multipart upload, büyük bir dosyayı parçalara bölerek her parçayı ayrı ayrı yükleyen ve sonunda bunları birleştiren bir transfer yöntemidir. AWS S3, Google Cloud Storage, Azure Blob Storage ve benzeri platformların tamamı bu yöntemi destekler.

Tek parça yüklemenin sorunlarını düşünelim. 10 GB’lık bir dosyayı upload ederken 9. GB’da bağlantı koparsa sıfırdan başlarsın. Ağ bantgenişliğini tek bir akış üzerinden kullanırsın, paralel yükleme yapamazsın. Bellek tüketimi artar çünkü bazı istemciler tüm dosyayı tampona alır.

Multipart upload ile bunların hepsini aşabilirsin:

  • Hata toleransı: Sadece başarısız olan parçayı tekrar yüklersin
  • Paralel yükleme: Birden fazla parçayı aynı anda göndererek bant genişliğini verimli kullanırsın
  • Bellek optimizasyonu: Her seferinde sadece bir parça bellekte tutulur
  • Büyük dosya desteği: S3’te tekli upload limiti 5 GB iken multipart ile 5 TB’a kadar çıkabilirsin

AWS S3 için resmi tavsiye şudur: 100 MB’ın üzerindeki dosyalar için multipart upload kullan. Bence bu sınırı 50 MB’a çekebilirsin, özellikle kararsız ağ bağlantılarında.

AWS S3 ile Multipart Upload

AWS CLI ile Temel Kullanım

En basit yöntemden başlayalım. AWS CLI otomatik olarak multipart upload kullanır, ancak eşikleri ve parça boyutlarını ayarlayabilirsin.

# Varsayılan ayarlarla yükleme (8 MB parça boyutu, 10 paralel işlem)
aws s3 cp /data/backup.tar.gz s3://my-bucket/backups/

# Parça boyutu ve eşzamanlı işlem sayısını ayarlama
aws s3 cp /data/backup.tar.gz s3://my-bucket/backups/ 
  --storage-class STANDARD_IA 
  --multipart-chunksize 64MB 
  --multipart-threshold 64MB

# Daha agresif paralel yükleme (yüksek bant genişliği için)
aws s3 cp /data/bigfile.tar.gz s3://my-bucket/archives/ 
  --multipart-chunksize 128MB 
  --expected-size 10737418240

# Transfer hızını izlemek için
aws s3 cp /data/backup.tar.gz s3://my-bucket/backups/ 
  --no-progress false 
  2>&1 | tee /var/log/upload.log

AWS CLI config dosyasında global ayarları kalıcı yapabilirsin:

# ~/.aws/config dosyasına ekle
cat >> ~/.aws/config << 'EOF'
[default]
s3 =
    multipart_threshold = 64MB
    multipart_chunksize = 64MB
    max_concurrent_requests = 20
    max_queue_size = 1000
    use_accelerate_endpoint = false
EOF

Python boto3 ile Gelişmiş Multipart Upload

AWS CLI yeterli değilse boto3 ile tam kontrol sağlayabilirsin. Gerçek senaryolarda bu genellikle bir backup scriptine entegre edilir.

#!/usr/bin/env python3
import boto3
import os
import math
import threading
from botocore.exceptions import ClientError

def multipart_upload(file_path, bucket, key, chunk_size_mb=64, max_workers=10):
    """
    Büyük dosyaları multipart upload ile S3'e yükler.
    Hata durumunda sadece başarısız parçaları tekrar gönderir.
    """
    s3_client = boto3.client('s3')
    file_size = os.path.getsize(file_path)
    chunk_size = chunk_size_mb * 1024 * 1024
    num_parts = math.ceil(file_size / chunk_size)
    
    print(f"Dosya boyutu: {file_size / (1024**3):.2f} GB")
    print(f"Parça sayısı: {num_parts} ({chunk_size_mb} MB/parça)")
    
    # Multipart upload başlat
    mpu = s3_client.create_multipart_upload(
        Bucket=bucket,
        Key=key,
        StorageClass='STANDARD_IA',
        ServerSideEncryption='AES256'
    )
    upload_id = mpu['UploadId']
    print(f"Upload ID: {upload_id}")
    
    parts = []
    failed_parts = []
    lock = threading.Lock()
    
    def upload_part(part_num, offset):
        try:
            with open(file_path, 'rb') as f:
                f.seek(offset)
                data = f.read(chunk_size)
            
            response = s3_client.upload_part(
                Bucket=bucket,
                Key=key,
                UploadId=upload_id,
                PartNumber=part_num,
                Body=data
            )
            
            with lock:
                parts.append({
                    'PartNumber': part_num,
                    'ETag': response['ETag']
                })
            print(f"Parça {part_num}/{num_parts} tamamlandı")
            
        except Exception as e:
            with lock:
                failed_parts.append(part_num)
            print(f"Parça {part_num} başarısız: {e}")
    
    # Thread pool ile paralel yükleme
    threads = []
    for i in range(num_parts):
        part_number = i + 1
        offset = i * chunk_size
        t = threading.Thread(target=upload_part, args=(part_number, offset))
        threads.append(t)
        t.start()
        
        # Max worker sayısını aşma
        if len([t for t in threads if t.is_alive()]) >= max_workers:
            threads[0].join()
    
    # Tüm thread'lerin bitmesini bekle
    for t in threads:
        t.join()
    
    if failed_parts:
        print(f"Başarısız parçalar: {failed_parts}")
        s3_client.abort_multipart_upload(Bucket=bucket, Key=key, UploadId=upload_id)
        return False
    
    # Parçaları part numarasına göre sırala
    parts.sort(key=lambda x: x['PartNumber'])
    
    # Upload'u tamamla
    s3_client.complete_multipart_upload(
        Bucket=bucket,
        Key=key,
        UploadId=upload_id,
        MultipartUpload={'Parts': parts}
    )
    
    print(f"Upload tamamlandı: s3://{bucket}/{key}")
    return True

if __name__ == "__main__":
    multipart_upload(
        file_path="/data/database_backup_20240115.tar.gz",
        bucket="company-backups",
        key="databases/prod/database_backup_20240115.tar.gz",
        chunk_size_mb=64,
        max_workers=10
    )

Azure Blob Storage ile Multipart Upload

Azure’da kavram biraz farklı çalışır. Block Blob kullanarak her bloğu ayrı yükler ve sonunda taahhüt edersin (commit).

#!/usr/bin/env python3
from azure.storage.blob import BlobServiceClient, BlobBlock
import os
import base64
import hashlib
from concurrent.futures import ThreadPoolExecutor, as_completed

def azure_block_upload(file_path, container_name, blob_name, 
                        block_size_mb=100, max_workers=8):
    """
    Azure Blob Storage'a parallel block upload.
    100 MB parçalar halinde yükler.
    """
    connection_string = os.environ.get('AZURE_STORAGE_CONNECTION_STRING')
    blob_service = BlobServiceClient.from_connection_string(connection_string)
    blob_client = blob_service.get_blob_client(
        container=container_name, 
        blob=blob_name
    )
    
    block_size = block_size_mb * 1024 * 1024
    file_size = os.path.getsize(file_path)
    blocks = []
    
    def upload_block(block_index, offset):
        with open(file_path, 'rb') as f:
            f.seek(offset)
            data = f.read(block_size)
        
        # Block ID oluştur (base64 encoded)
        block_id = base64.b64encode(
            f"block-{block_index:08d}".encode()
        ).decode()
        
        blob_client.stage_block(block_id=block_id, data=data)
        print(f"Block {block_index} yüklendi ({len(data) / (1024**2):.1f} MB)")
        return BlobBlock(block_id=block_id)
    
    # Paralel block yükleme
    num_blocks = (file_size + block_size - 1) // block_size
    futures_map = {}
    
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        for i in range(num_blocks):
            offset = i * block_size
            future = executor.submit(upload_block, i, offset)
            futures_map[future] = i
        
        results = [None] * num_blocks
        for future in as_completed(futures_map):
            idx = futures_map[future]
            results[idx] = future.result()
    
    # Tüm blokları commit et
    blob_client.commit_block_list(results)
    print(f"Azure upload tamamlandı: {container_name}/{blob_name}")

# Kullanım
azure_block_upload(
    file_path="/var/backups/vm_snapshot.vhd",
    container_name="vm-backups",
    blob_name="snapshots/prod-web-01/vm_snapshot_20240115.vhd",
    block_size_mb=100,
    max_workers=8
)

Google Cloud Storage ile Resumable Upload

GCS’de büyük dosyalar için resumable upload kullanmak en doğru yaklaşım. Ağ kesilse bile kaldığı yerden devam edebilir.

#!/bin/bash
# GCS'e büyük dosya yükleme scripti
# gsutil kullanır, resumable upload otomatik aktiftir

BUCKET="gs://company-backups"
SOURCE_DIR="/data/backups"
LOG_FILE="/var/log/gcs_upload.log"

# gsutil paralel composite upload ayarları
configure_gsutil() {
    cat > ~/.boto << 'EOF'
[GSUtil]
parallel_composite_upload_threshold = 150M
parallel_composite_upload_component_size = 50M
max_upload_retries = 5
parallel_thread_count = 8
parallel_process_count = 4
EOF
    echo "gsutil yapılandırması tamamlandı"
}

# Büyük dosyaları paralel yükle
upload_large_files() {
    local source="$1"
    local dest="$2"
    
    echo "$(date): Yükleme başlıyor: $source -> $dest" | tee -a "$LOG_FILE"
    
    gsutil -m -o "GSUtil:parallel_composite_upload_threshold=150M" 
           -o "GSUtil:parallel_composite_upload_component_size=50M" 
           cp -r "$source" "$dest" 2>&1 | tee -a "$LOG_FILE"
    
    if [ $? -eq 0 ]; then
        echo "$(date): Yükleme başarılı" | tee -a "$LOG_FILE"
    else
        echo "$(date): HATA: Yükleme başarısız" | tee -a "$LOG_FILE"
        exit 1
    fi
}

configure_gsutil
upload_large_files "$SOURCE_DIR" "$BUCKET/$(date +%Y%m%d)"

Eksik Upload’ları Temizleme

Multipart upload başlar ama tamamlanmazsa (script crash, ağ kopması, vb.) S3 veya diğer platformlarda yarım kalan parçalar birikir. Bu parçalar depolama alanı ve para harcar. Bunu düzenli olarak temizlemek gerekir.

#!/bin/bash
# S3'teki tamamlanmamış multipart upload'ları temizle
# Cron job olarak günlük çalıştırılabilir

BUCKET="my-bucket"
MAX_AGE_DAYS=7

echo "Eski incomplete multipart upload'lar temizleniyor..."
echo "Bucket: $BUCKET, Maksimum yaş: $MAX_AGE_DAYS gün"

# Mevcut incomplete upload'ları listele
aws s3api list-multipart-uploads 
    --bucket "$BUCKET" 
    --query 'Uploads[?Initiated<`'"$(date -d "$MAX_AGE_DAYS days ago" --utc +%Y-%m-%dT%H:%M:%SZ)"'`].[UploadId,Key]' 
    --output text | while read upload_id key; do
    
    if [ -n "$upload_id" ] && [ -n "$key" ]; then
        echo "Siliniyor: Key=$key, UploadId=$upload_id"
        aws s3api abort-multipart-upload 
            --bucket "$BUCKET" 
            --key "$key" 
            --upload-id "$upload_id"
        
        if [ $? -eq 0 ]; then
            echo "  -> Silindi"
        else
            echo "  -> HATA: Silinemedi"
        fi
    fi
done

echo "Temizlik tamamlandı"

# Bucket lifecycle policy ile otomatik temizlik
cat > /tmp/lifecycle.json << 'EOF'
{
    "Rules": [
        {
            "ID": "abort-incomplete-multipart",
            "Status": "Enabled",
            "Filter": {"Prefix": ""},
            "AbortIncompleteMultipartUpload": {
                "DaysAfterInitiation": 7
            }
        }
    ]
}
EOF

aws s3api put-bucket-lifecycle-configuration 
    --bucket "$BUCKET" 
    --lifecycle-configuration file:///tmp/lifecycle.json

echo "Lifecycle policy uygulandı: 7 günden eski incomplete upload'lar otomatik silinecek"

Transfer Hızı ve Optimizasyon Metrikleri

Gerçek bir senaryoda yükleme performansını izlemek kritik öneme sahip. Aşağıdaki script hem yükleme yapar hem de metrikleri kaydeder.

#!/bin/bash
# Büyük dosya yükleme performans takibi
# S3 Transfer Acceleration aktifse --endpoint-url ile değiştirebilirsin

FILE="$1"
BUCKET="$2"
KEY="$3"

if [ -z "$FILE" ] || [ -z "$BUCKET" ] || [ -z "$KEY" ]; then
    echo "Kullanım: $0 <dosya> <bucket> <key>"
    exit 1
fi

FILE_SIZE=$(stat -c%s "$FILE")
START_TIME=$(date +%s)

echo "=== Multipart Upload Başlıyor ==="
echo "Dosya: $FILE ($(du -sh $FILE | cut -f1))"
echo "Hedef: s3://$BUCKET/$KEY"
echo "Başlangıç: $(date)"

# pv ile gerçek zamanlı hız takibi + aws s3 cp
pv -p -t -e -r -b "$FILE" | 
    aws s3 cp - "s3://$BUCKET/$KEY" 
        --expected-size "$FILE_SIZE" 
        --multipart-chunksize 64MB 
        --storage-class INTELLIGENT_TIERING

EXIT_CODE=$?
END_TIME=$(date +%s)
ELAPSED=$((END_TIME - START_TIME))

if [ $EXIT_CODE -eq 0 ]; then
    SPEED=$(echo "scale=2; $FILE_SIZE / $ELAPSED / 1048576" | bc)
    echo ""
    echo "=== Upload Tamamlandı ==="
    echo "Süre: $((ELAPSED / 60)) dakika $((ELAPSED % 60)) saniye"
    echo "Ortalama hız: ${SPEED} MB/s"
    
    # Yüklenen dosyanın checksum kontrolü
    echo "Checksum doğrulama..."
    LOCAL_MD5=$(md5sum "$FILE" | cut -d' ' -f1)
    echo "Yerel MD5: $LOCAL_MD5"
    echo "S3 ETag doğrulama için aws s3api head-object kullanın"
else
    echo "HATA: Upload başarısız (exit code: $EXIT_CODE)"
    exit 1
fi

Gerçek Dünya Senaryosu: Veritabanı Backup Otomasyonu

Bütün öğrendiklerimizi bir araya getirelim. Üretim ortamında PostgreSQL dump’larını S3’e güvenli şekilde yükleyen bir sistem kuralım.

#!/bin/bash
# PostgreSQL backup ve S3 multipart upload
# /etc/cron.d/pg-backup dosyasına ekle:
# 0 2 * * * root /usr/local/sbin/pg_backup_s3.sh >> /var/log/pg_backup.log 2>&1

set -euo pipefail

# Yapılandırma
DB_HOST="localhost"
DB_NAME="production"
DB_USER="postgres"
BACKUP_DIR="/var/backups/postgres"
S3_BUCKET="company-db-backups"
RETENTION_DAYS=30
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/${DB_NAME}_${DATE}.sql.gz"
S3_KEY="postgres/${DB_NAME}/${DATE:0:6}/${DB_NAME}_${DATE}.sql.gz"

mkdir -p "$BACKUP_DIR"

log() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') [$1] $2"
}

# Disk alanı kontrolü (en az 20 GB boş alan olsun)
AVAILABLE_GB=$(df -BG "$BACKUP_DIR" | awk 'NR==2 {print $4}' | tr -d 'G')
if [ "$AVAILABLE_GB" -lt 20 ]; then
    log "ERROR" "Yetersiz disk alanı: ${AVAILABLE_GB}GB. En az 20GB gerekli."
    exit 1
fi

log "INFO" "Backup başlıyor: $DB_NAME"

# pg_dump ile sıkıştırmalı backup al
pg_dump -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME" 
    --format=custom 
    --compress=9 
    --no-password 
    --verbose 2>/tmp/pg_dump.log | 
    gzip -9 > "$BACKUP_FILE"

BACKUP_SIZE=$(du -sh "$BACKUP_FILE" | cut -f1)
log "INFO" "Backup tamamlandı. Boyut: $BACKUP_SIZE"

# S3'e multipart upload (paralel yükleme ile hızlandırılmış)
log "INFO" "S3 yükleme başlıyor: s3://$S3_BUCKET/$S3_KEY"

aws s3 cp "$BACKUP_FILE" "s3://$S3_BUCKET/$S3_KEY" 
    --storage-class STANDARD_IA 
    --server-side-encryption AES256 
    --multipart-chunksize 64MB 
    --metadata "db_name=$DB_NAME,backup_date=$DATE,hostname=$(hostname)"

log "INFO" "S3 yükleme tamamlandı"

# Yerel eski backup'ları temizle
find "$BACKUP_DIR" -name "*.sql.gz" -mtime "+$RETENTION_DAYS" -delete
log "INFO" "$RETENTION_DAYS günden eski yerel backup'lar temizlendi"

# S3'teki eski backup'ları temizle (lifecycle policy tercih edilir ama manuel de yapılabilir)
CUTOFF_DATE=$(date -d "$RETENTION_DAYS days ago" +%Y-%m-%d)
aws s3 ls "s3://$S3_BUCKET/postgres/$DB_NAME/" --recursive | 
    awk -v cutoff="$CUTOFF_DATE" '$1 < cutoff {print $4}' | 
    while read old_key; do
        aws s3 rm "s3://$S3_BUCKET/$old_key"
        log "INFO" "Eski S3 backup silindi: $old_key"
    done

log "INFO" "Tüm işlemler tamamlandı"

Sık Karşılaşılan Sorunlar ve Çözümleri

Multipart upload kullanırken bazı tipik problemlerle karşılaşırsın.

Part boyutu çok küçük seçmek: S3’te minimum parça boyutu 5 MB, maksimum 5 GB. Eğer çok küçük parçalar seçersen API çağrı sayısı artar ve maliyet yükselir. Optimum değer genellikle 64 MB ile 128 MB arasıdır.

Upload ID yönetimi: Bir upload başlatıp ID’yi kaybedersen tamamlayamazsın. ID’yi bir dosyaya veya veritabanına kaydet. Uzun sürecek uploadlarda bu kritik.

Eşzamanlı yazma sorunları: Aynı dosyayı birden fazla process yüklemeye çalışırsa çakışma olabilir. Lock dosyası kullanmayı unutma.

IAM izinleri eksik: Multipart upload için s3:CreateMultipartUpload, s3:UploadPart, s3:CompleteMultipartUpload ve s3:AbortMultipartUpload izinleri gerekir. Sadece s3:PutObject yeterli değil.

Ağ zaman aşımı: Uzun transfer sürelerinde TCP keepalive ayarlarını kontrol et. AWS CLI için --cli-connect-timeout ve --cli-read-timeout parametrelerini artırabilirsin.

  • –cli-connect-timeout: Bağlantı zaman aşımı süresi (saniye)
  • –cli-read-timeout: Okuma zaman aşımı süresi (saniye)
  • –multipart-threshold: Multipart upload başlatılacak minimum dosya boyutu
  • –multipart-chunksize: Her parçanın boyutu
  • –max-concurrent-requests: Eş zamanlı istek sayısı

Maliyet Optimizasyonu

Multipart upload kullanırken maliyet tarafını da göz ardı etme.

Storage class seçimi: Backup dosyaları için STANDARD_IA veya GLACIER kullanmak maliyeti ciddi düşürür. Aktif erişim olmayan veriler için INTELLIGENT_TIERING iyi bir seçenek.

Transfer maliyeti: Aynı AWS bölgesi içinde transfer ücretsiz. Bölgeler arası transferde veri boyutu kritik. Mümkünse sıkıştırma uygulayarak veri boyutunu küçült.

API çağrı maliyeti: Her parça için bir PUT isteği gönderilir. Çok küçük parçalar API maliyetini artırır. Parça boyutunu optimize ederek bu dengeyi kur.

Tamamlanmamış uploadlar: Lifecycle policy olmadan biriken incomplete upload’lar yer kaplar ve para harcar. Her bucket için lifecycle rule mutlaka kur.

Sonuç

Multipart upload, büyük dosya transferlerinde güvenilirlik ve performans için temel bir araç. Özellikle üretim ortamında otomatik backup sistemleri kuruyorsan, bu yöntemi doğru yapılandırmak hem zaman hem de para tasarrufu sağlar.

Pratik olarak takip etmem gereken birkaç kural var: 50 MB üzerindeki tüm dosyalar için multipart upload kullan, parça boyutunu 64-128 MB arasında tut, her bucket’a lifecycle policy ekle ve tamamlanmamış uploadları takip et. Paralel thread sayısını ağ bant genişliğine göre ayarla, fazla thread açmak bazen ters etki yaratabilir.

Monitoring tarafını da ihmal etme. Yükleme sürelerini, hata oranlarını ve maliyet trendlerini düzenli olarak izle. CloudWatch veya benzeri bir monitoring çözümüyle bu metrikleri dashboard’a eklemek, olası sorunları erken tespit etmeni sağlar.

Sonunda temel prensip şu: büyük dosya transferi sadece teknik bir iş değil, operasyonel bir güvenilirlik meselesi. Multipart upload bu güvenilirliği sistematik olarak sağlamanın en pratik yolu.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir