5 Ocak 2025 Pazar

Amazon S3 Objelerinde ETag Hesplaması


Merhaba. Bu yazıda S3'e yüklenen dosyaların ETag adı verilen hash'lerinin nasıl hesaplandıklarından bahsedeceğim. ETag genel olarak, bir S3 bucket'ına yüklenen her dosya için hesaplanan MD5 hash'ten başka birşey değil. S3'teki dosyaların gerçekte bizim anladığımız anlamda dosya olmadıklarını biliyoruz. S3, "Object Storage" olarak alınıyor ve dosyalar da doğru terminolojiyle obje olarak saklanıyor. 

Belli bir dosya büyüklüğünden sonra, aws s3 cp veya aws s3 sync ile yapılan yüklemeler, (muhtemelen) daha kolay saklanabilmesi için otomatik olarak eş büyüklükte parçalara bölünür. Buna multipart objeler denir. Peki ne kadar bir büyüklük? Genel geçer bir ölçü olmasa da benim dosyalarım şu anda 8 ila 16M arası parçalar olarak saklanıyor. Yazıyı hazırlarken kullandığım kaynakta, 5G'ye kadar olan dosyaların parçalanmadıklarından bahsedilmiş [1][2], ancak benim gözlemime göre bu artık doğru değil ama bu değer çok da önemli değil.

Eğer bir dosya bu eşikten küçükse, tek parça olarak saklanır ve objenin ETag'i MD5 hash'ine eşit. Buraya kadar bir sorun yok. Eğer dosya bu eşikten büyükse multipart obje olarak saklanınca işler biraz karışıyor. Bir objenin multipart olup olmadığı ETag'ına bakarak kolayca anlaşılabilir. Normal bir MD5 hash yalnızca hexadecimal basamaklardan oluşur. Dolayısıyla tire işareti ( - ) MD5 hash'e ait değildir. Eğer S3'teki bir dosyanın ETag'ında tire işareti varsa, bu multipart bir objedir ve dosyanın kaç parçaya bölündüğü tireden sonra gelen kısımdadır. Bunların hepsine ait somut örnekleri yazının ilerleyen kısmında vereceğim. 

Multipart objelerde ETag hesaplaması şöyle işliyor: Her bir parça ayrı ayrı MD5'le hash'leniyor, çıkan hash'ler uç uca eklenip tekrar hash'leniyor. Bu ETag'ın tireden önceki kısmı. Parça sayısı basitçe tireden sonra en sonra ekleniyor [3].

Ben bilgisayarlarımın disklerini düzenli olarak Clonezilla ile yedekliyorum. Yedekleri önce harici diske alıp, bunları S3'e kopyalıyorum. Harici diskte en yeni kopya duruyor, son üç kopya S3'te. Geriye doğru (FAT32) uyumluluk nedeniyle, yedekleri 4G'lik parçalara bölüyorum (her ne kadar yedeği FAT32 ortama almasam da). Zaten ETag karşılaştırma ihtiyacı S3'teki kopyaları doğrulamak istememden çıktı.

Bu noktada aws komut satırı arabiriminin yüklü ve ayarlı olduğunu varsayıyorum. Ayarlar .aws/config dosyasından yapılıyor ancak yazıyı uzatmamak için buna değinmeyeceğim. Önce küçük dosya örneğini ele alalım:

$ aws s3api head-object --bucket mybucket --key image_backup/2023-10-15-10-img/Info-lshw.txt
{
    "AcceptRanges": "bytes",
    "LastModified": "2023-10-15T18:28:31+00:00",
    "ContentLength": 40960,
    "ETag": "\"fe78f69cb9d41a23ba23b4783e542a7b\"",
    "ContentType": "text/plain",
    "ServerSideEncryption": "AES256",
    "Metadata": {}
}

Önceden belirttiğim gibi, bu multipart obje değil. Haliyle MD5 hash'i yani ETag'ı basitçe bulunabilir. Aşağıda büyük dosya örneği var: 

$ aws s3api head-object --bucket mybucket --key image_backup/2024-12-01-13-img/sda5.ntfs-ptcl-img.xz.ac
{
    "AcceptRanges": "bytes",
    "LastModified": "2024-12-03T17:00:58+00:00",
    "ContentLength": 4096008192,
    "ETag": "\"360f5e8babf8cd28673eaafd32eb405f-489\"",
    "ContentType": "application/vnd.nokia.n-gage.ac+xml",
    "ServerSideEncryption": "AES256",
    "Metadata": {}
}

Bu 4096 MB'lık bir dosya ve ETag'dan görüleceği gibi 489 parçadan oluşuyor. Burada önemli olan parçaların büyüklüklerini bulmak. ContentLength, 489'a bölününce 8M'ye çok yakın bir değer bulunuyor. Buradan aslında dosyanın 8M'lik parçalara bölündüğünü varsayabilirim ama bir programda kullanmak için bunun kesin değerini bulmak gerek. Bunun için aynı komuta --part-number parametresini ekleyip tek bir parçayı inceleyeceğim. Dosyalar sabit büyüklükte parçalandıklarından yalnızca en son parçanın boyutu farklı, ancak her parça için ETag değeri aynı. Başka bir deyişle --part-number her parçanın ayrı ayrı MD5 hash'ini vermiyor.

$ aws s3api head-object --bucket mybucket --key image_backup/2023-10-15-10-img/sda5.ntfs-ptcl-img.gz.aac --part-number 1
{
    "AcceptRanges": "bytes",
    "LastModified": "2023-10-15T18:28:31+00:00",
    "ContentLength": 16777216,
    "ETag": "\"aba379cb0d00f21f53da5136fc5b0366-299\"",
    "ContentType": "audio/aac",
    "ServerSideEncryption": "AES256",
    "Metadata": {},
    "PartsCount": 299
}

$ aws s3api head-object --bucket mybucket --key image_backup/2023-10-15-10-img/sda5.ntfs-ptcl-img.gz.aac --part-number 299
{
    "AcceptRanges": "bytes",
    "LastModified": "2023-10-15T18:28:31+00:00",
    "ContentLength": 401408,
    "ETag": "\"aba379cb0d00f21f53da5136fc5b0366-299\"",
    "ContentType": "audio/aac",
    "ServerSideEncryption": "AES256",
    "Metadata": {},
    "PartsCount": 299
}

Bu arada resmi AWS dökümantasyonuna göre (Aralık 2024 itibariyle) [4] default parça büyüklüğü 8 MB ancak yukarıda görüldüğü üzere Ekim 2023'te bir dosya 16 MB'lik parçalarla yüklenmiş. Dolayısıyla bu değeri sabit kabul etmek yerine, ContentLength alanından almak daha mantıklı. Görünüşe göre Amazon'dakiler canları sıkıldıkça default'u değiştiriyorlar. Bu arada aws komutu json çıktı üretiyor. bash script'le çalışırken, çıktıyı grep yerine jq ile parse etmek daha şık sonuç veriyor:

$ aws s3api head-object --bucket mybucket --key image_backup/2023-10-15-10-img/sda5.ntfs-ptcl-img.gz.aac --part-number 1 | jq -r '.ETag'
"aba379cb0d00f21f53da5136fc5b0366-299"

$ aws s3api head-object --bucket mybucket --key image_backup/2023-10-15-10-img/sda5.ntfs-ptcl-img.gz.aac --part-number 1 | jq -r '.ContentLength'
16777216

Ben, aldığım yedekteki tüm dosyaları tek tek karşılaştırmak için bir script hazırladım. Biraz uzun olduğu için buradan paylaşmayacağım, repo linkiyle ulaşılabilir. Script, kullanıcıdan basitçe bucket adını ve yedeklerin olduğu dizinin adını alıyor. Ben yedekleri image_backup adında bir dizinde, <YYYY-MM-DD-HH-img> formatlı alt dizinlerde tutuyorum, bu kısım (satır 12) ihtiyaca göre değiştirilebilir. Parça sayısı birse, doğrudan md5 alınıyor (satır 26). Birden fazla parça varsa, bu parçalar dd ile bölünüyor (satır 36), hepsinin ayrı ayrı hash'leri geçici bir dosyaya yazılıyor. Parçalar bittiği zaman oluşan dosyanın tekrar hash'i alınıp dosya siliniyor (satır 41-42). Dosyanın geri kalan kısmı bash string işlemleriyle hash'ler karşılaştırılıp aynı ise OK farklı ise FAIL yazdırılıyor.


[1]: https://stackoverflow.com/questions/45421156
[2]: https://stackoverflow.com/questions/6591047
[3]: https://stackoverflow.com/questions/12186993
[4]: https://docs.aws.amazon.com/cli/latest/topic/s3-config.html#multipart-chunksize

Hiç yorum yok:

Yorum Gönder