DigitalOcean'dan Hetzner'e migrasyon
(isayeter.com)- Aylık 1.432 $ tutarındaki production altyapısı, aylık 233 $'lık dedicated server'a taşınırken işletim sistemi de değiştirildi ve buna rağmen kesinti olmadan hizmet sürekliliği korundu
- 30 adet MySQL veritabanı ve 34 adet Nginx sanal host, GitLab EE, Neo4J, Supervisor, Gearman yeni sunucuda birebir kuruldu; gerçek zamanlı replikasyon ve son artımlı senkronizasyonla taşıma tamamlandı
- Veritabanı taşımasının çekirdeğinde mydumper·myloader paralel işleme ile MySQL replication kombinasyonu vardı; MySQL 5.7'den 8.0'a yükseltirken ortaya çıkan sys şeması ve yetki sorunları da düzeltildi
- Cutover süreci DNS TTL düşürme, eski sunucuda Nginx reverse proxy'ye geçiş, toplu A kaydı değişikliği sırasıyla yürütüldü; böylece DNS yayılımı sırasında eski IP'ye gelen istekler de yeni sunucuya iletildi
- Sonuç olarak aylık 1.199 $ tasarruf, yıllık 14.388 $ tasarruf, daha yüksek CPU·bellek·depolama ve 0 dakika kesinti birlikte elde edildi
Migrasyonun arka planı
- Türkiye'de bir yazılım şirketi işletilen ortamda hızlı enflasyon ve Türk lirasındaki değer kaybı nedeniyle dolar bazlı altyapı maliyetlerinin yükü ciddi biçimde artmış durumdaydı
- Mevcut DigitalOcean sunucusunun maliyeti her ay 1.432 $ idi; yapılandırma 192GB RAM, 32 vCPU, 600GB SSD, 2 adet 1TB block volume ve yedeklemeleri içeriyordu
- Yeni hedef Hetzner AX162-R dedicated server idi; AMD EPYC 9454P 48 çekirdek 96 thread, 256GB DDR5, 1.92TB NVMe Gen4 RAID1 yapılandırmasına sahipti
- Aylık maliyet 233 $'a düştü; aylık tasarruf 1.199 $, yıllık tasarruf ise 14.388 $ seviyesine ulaştı
- Mevcut sunucunun güvenilirliği ya da geliştirici deneyimiyle ilgili bir memnuniyetsizlik yoktu; ancak steady-state iş yüklerinde fiyat/performans artık makul değildi
Mevcut operasyon ortamı
- Operasyon stack'i basit bir test ortamı değil, gerçek bir production ortamıydı
- Toplam 248GB veri içeren 30 MySQL veritabanı
- Birden fazla domain üzerinde çalışan 34 Nginx sanal host
- 42GB GitLab EE yedeği
- 30GB Neo4J Graph DB
- Supervisor ile yönetilen onlarca arka plan worker'ı
- Gearman iş kuyruğu
- Yüz binlerce kullanıcıya hizmet veren canlı mobil uygulamalar
- Mevcut sunucunun işletim sistemi CentOS 7 idi ve destek süresi sona ermişti
- Yeni sunucunun işletim sistemi AlmaLinux 9.7; RHEL 9 uyumlu bir dağıtım ve CentOS sonrası için doğal bir tercih
- Bu taşıma yalnızca maliyet düşürmek için değil, yıllardır güvenlik güncellemesi alamayan işletim sisteminden çıkmak için de bir fırsat oldu
Kesintisiz strateji
- Basit DNS değişikliği ve servis yeniden başlatma yaklaşımı kabul edilmedi; 6 aşamalı migrasyon prosedürü ile kesintisiz taşıma yapıldı
-
1. aşama: Yeni sunucuya tüm stack'in kurulması
- Nginx, eski sunucudakiyle aynı flag'lerle source üzerinden derlenip kuruldu
- PHP, Remi repo üzerinden kuruldu ve eski sunucudaki aynı
.iniayar dosyaları uygulandı - MySQL 8.0, Neo4J Graph DB, GitLab EE, Node.js, Supervisor, Gearman kuruldu ve mevcut davranışla aynı olacak şekilde yapılandırıldı
- DNS kayıtlarına dokunmadan önce tüm servisler eski sunucudakiyle aynı şekilde çalışacak hale getirildi
- SSL sertifikaları, eski sunucudaki
/etc/letsencrypt/dizininin tamamı rsync ile kopyalanarak taşındı - Tüm trafik yeni sunucuya geçtikten sonra
certbot renew --force-renewalile sertifikalar topluca zorla yenilendi
-
2. aşama: Web dosyalarının rsync ile kopyalanması
/var/www/htmldizininin tamamı, yaklaşık 65GB ve 1,5 milyon dosya, SSH tabanlırsyncile kopyalandı- Bütünlük doğrulaması için
--checksumseçeneği kullanıldı - Cutover'dan hemen önce değişen dosyaları yansıtmak için son bir artımlı senkronizasyon daha yapıldı
-
3. aşama: MySQL master-slave replikasyonu
- Dump alıp restore ederek veritabanını durdurmak yerine gerçek zamanlı replikasyon kuruldu
- Eski sunucu master, yeni sunucu read-only slave olarak yapılandırıldı
- İlk büyük veri yüklemesinde
mydumperkullanıldı; sonrasında dump metadata'sında kaydedilen kesin binlog konumundan itibaren replikasyon başlatıldı - Cutover anına kadar iki taraftaki veritabanı gerçek zamanlı senkron halde tutuldu
-
4. aşama: DNS TTL düşürme
- Tüm A/AAAA kayıtlarının TTL değeri, DigitalOcean DNS API'sine script ile çağrı yapılarak 3600 saniyeden 300 saniyeye düşürüldü
- MX ve TXT kayıtları değiştirilmedi
- Mail kayıtlarının TTL değerini değiştirmenin iletim sorunlarına yol açabileceği düşünülerek hariç bırakıldı
- Eski TTL'nin dünya genelinde süresinin dolması için 1 saat beklendi ve ardından 5 dakika içinde cutover'a hazır hale gelindi
-
5. aşama: Eski sunucudaki Nginx'in reverse proxy'ye çevrilmesi
- Python script'i, 34 Nginx site yapılandırmasının tamamındaki
server {}bloklarını parse etti - Mevcut ayarlar yedeklendi ve yerlerine yeni sunucuya yönlendiren proxy ayarları yazıldı
- Böylece DNS yayılımı sırasında bile eski IP'ye gelen istekler sessizce yeni sunucuya aktarıldı
- Kullanıcı tarafında görünür bir kesinti oluşmadı
- Python script'i, 34 Nginx site yapılandırmasının tamamındaki
-
6. aşama: DNS cutover ve eski sunucunun kapatılması
- Python script'i ile DigitalOcean API çağrılarak tüm A kayıtları birkaç saniye içinde yeni sunucunun IP'siyle değiştirildi
- Eski sunucu 1 hafta boyunca cold standby olarak tutulduktan sonra kapatıldı
- Tüm süreç boyunca servis ya doğrudan yanıt verdi ya da proxy üzerinden yanıt verdi; böylece erişilebilirlikte boşluk oluşmadı
MySQL migrasyonu
- Tüm işin en karmaşık kısmı MySQL taşımasıydı
-
Veri dump'ı
- Standart
mysqldumpyerine mydumper kullanıldı - Yeni sunucunun 48 CPU çekirdeği sayesinde paralel export/import yapılarak, tek thread'li
mysqldumpile günler sürecek iş birkaç saate indirildi - Kullanılan başlıca seçenekler arasında
--threads 32,--compress,--trx-consistency-only,--skip-definer,--chunk-filesize 256vardı - Ana dump içindeki
metadatadosyasına snapshot anındaki binlog konumu yazıldıFile: mysql-bin.000004Position: 21834307
- Bu değerler daha sonra replikasyonun başlangıç noktası olarak kullanıldı
- Standart
-
Dump aktarımı
- Dump tamamlandıktan sonra SSH tabanlı rsync ile yeni sunucuya aktarıldı
- Toplam 248GB sıkıştırılmış chunk taşındı
mydumperiçindeki--compressseçeneği, sıkıştırılmış chunk'lar sayesinde ağ aktarım hızını artırdı
-
Veri yükleme
myloaderkullanıldı- Başlıca seçenekler
--threads 32,--overwrite-tables,--ignore-errors 1062,--skip-defineridi
-
MySQL 5.7'den 8.0'a geçişte yaşanan sorunlar
- CentOS 7 ortamı nedeniyle eski sunucu MySQL 5.7'de kalmıştı
- Taşıma öncesinde
mysqlcheck --check-upgradeile verinin MySQL 8.0 ile uyumlu olup olmadığı kontrol edildi ve sonuç sorunsuzdu - Yeni sunucuya güncel MySQL 8.0 Community kuruldu
- Tüm projelerde query çalışma süreleri anlamlı biçimde azaldı; orijinal yazıda bunun nedeni olarak MySQL 8.0'ın geliştirilmiş optimizer'ı ve InnoDB iyileştirmeleri gösterildi
- Ancak sürüm sıçraması nedeniyle bazı sorunlar da yaşandı
- Import sonrasında
mysql.usertablosundaki kolon yapısı beklenen 51 değil, 45 kolon durumundaydı - Bunun sonucunda
mysql.infoschemaeksik kaldı ve kullanıcı doğrulama sorunları ortaya çıktı
- Import sonrasında
- İlk düzeltme denemesinde aşağıdaki komutlar kullanıldı
systemctl stop mysqldmysqld --upgrade=FORCE --user=mysql &
- İlk deneme
ERROR: 'sys.innodb_buffer_stats_by_schema' is not VIEWhatasıyla başarısız oldu - Bunun nedeni, sys şemasının view yerine normal tablo olarak import edilmiş olmasıydı
- Çözüm olarak
DROP DATABASE sys;çalıştırıldı ve upgrade yeniden denendi - Sonrasında işlem normal şekilde tamamlandı
MySQL replikasyon yapılandırması
- Her iki sunucuda da dump yüklemesi bittikten sonra, yeni sunucu eski sunucunun replica'sı olarak yapılandırıldı
CHANGE MASTER TOifadesinde eski sunucunun IP'si, replikasyon kullanıcısı, port 3306,MASTER_LOG_FILE='mysql-bin.000004',MASTER_LOG_POS=21834307belirtildi- Ardından
START SLAVE;çalıştırıldı - Neredeyse hemen ardından error 1062 Duplicate Key nedeniyle replikasyon durdu
- Bunun nedeni, dump işleminin iki parçaya bölünmesi ve bu arada bazı tablolara yazma yapılması; böylece import edilen dump ile binlog replay aynı satırı iki kez eklemeye çalıştı
- Bunu çözmek için aşağıdaki ayar uygulandı
SET GLOBAL slave_exec_mode = 'IDEMPOTENT';START SLAVE;
- IDEMPOTENT modu, duplicate key ve missing row hatalarını sessizce atlayarak ilerliyor
- Tüm kritik veritabanları hatasız senkronize oldu ve birkaç dakika içinde
Seconds_Behind_Masterdeğeri 0'a düştü
Cutover öncesi doğrulama
- DNS kayıtlarına dokunmadan önce tüm servislerin yeni sunucuda doğru çalıştığının doğrulanması gerekiyordu
- Doğrulama yöntemi, yerel makinedeki
/etc/hostsdosyasını geçici olarak düzenleyip domain'i yeni sunucunun IP'sine eşlemekti - Tarayıcı ve Postman yeni sunucuya istek gönderirken, dış kullanıcılar hâlâ eski sunucuya erişmeye devam etti
- API endpoint'leri, yönetim paneli ve her servisin yanıt durumu kontrol edildi
- Her şey doğrulandıktan sonra gerçek cutover yapıldı
SUPER yetkisi sorunu
- Master-slave replikasyonu tamamen senkron hale geldikten sonra, yeni sunucuda
read_only = 1olmasına rağmen INSERT sorgularının başarılı olduğu görüldü - Bunun nedeni, tüm PHP uygulama kullanıcılarına SUPER yetkisi verilmiş olmasıydı
- MySQL'de SUPER yetkisi
read_onlyayarını bypass eder SHOW GRANTS FOR 'some_db_user'@'localhost';çıktısındaSUPERyetkisinin bulunduğu doğrulandı- Toplam 24 uygulama kullanıcısı için
REVOKE SUPER ON *.* FROM 'some_db_user'@'localhost';komutu tekrar tekrar çalıştırıldı - Ardından
FLUSH PRIVILEGES;uygulandı - Sonrasında
read_only = 1, uygulama kullanıcılarının yazmasını doğru şekilde engellerken replikasyonun devam etmesine izin verdi
DNS hazırlığı
- Tüm domain'ler DigitalOcean DNS ile yönetiliyordu ve nameserver bağlantısı GoDaddy üzerinden yapılıyordu
- TTL düşürme işi, DigitalOcean API hedeflenerek script'leştirildi
- Değişiklik yalnızca A ve AAAA kayıtları ile sınırlandı
- MX ve TXT kayıtlarına dokunulmadı
- Google Workspace iletim sorunları yaşanma ihtimali nedeniyle mail ile ilgili kayıtların TTL değeri değiştirilmedi
- Eski TTL'nin süresinin dolması için 1 saat beklendi ve ardından cutover'a hazır hale gelindi
Eski sunucudaki Nginx'in reverse proxy'ye çevrilmesi
- 34 yapılandırma dosyasını elle düzenlemek yerine, Python script'iyle otomatik dönüşüm yapıldı
- Script tüm yapılandırma dosyalarındaki
server {}bloklarını parse etti, ana content block'u tespit etti ve onu proxy ayarlarıyla değiştirdi - Orijinal ayarlar
.backupdosyaları olarak yedeklendi - Örnek yapılandırmada
proxy_pass https://NEW_SERVER_IP;,proxy_set_header Host $host;,proxy_set_header X-Real-IP $remote_addr;,proxy_read_timeout 150;kullanıldı - Kritik seçenek
proxy_ssl_verify offidi- Çünkü yeni sunucudaki SSL sertifikaları domain için geçerliydi, IP adresi için geçerli değildi
- İki ucu da kontrol eden bir ortam olduğu için burada doğrulamanın kapatılması kabul edilebilir görüldü
Cutover prosedürü
- Cutover öncesindeki koşullar, replikasyon gecikmesinin
Seconds_Behind_Master: 0olması ve reverse proxy'nin hazır olmasıydı - Uygulama sırası şöyleydi
- Yeni sunucuda
STOP SLAVE; - Yeni sunucuda
SET GLOBAL read_only = 0; - Yeni sunucuda
RESET SLAVE ALL; - Yeni sunucuda
supervisorctl start all - Eski sunucuda
nginx -t && systemctl reload nginxçalıştırılarak proxy etkinleştirildi - Eski sunucuda
supervisorctl stop all - Yerel Mac üzerinde
python3 do_cutover.pyçalıştırılarak DNS'teki tüm A kayıtları yeni sunucu IP'siyle değiştirildi - Yaklaşık 5 dakika yayılım beklendi
- Eski sunucudaki tüm crontab girdileri yorum satırına alındı
- Yeni sunucuda
- DNS cutover script'i, DigitalOcean API'yi çağırarak tüm A kayıtlarını yaklaşık 10 saniye içinde değiştirdi
Cutover sonrası ek işler
- Taşıma tamamlandıktan sonra çok sayıda GitLab proje webhook'unun hâlâ eski sunucu IP'sini işaret ettiği görüldü
- GitLab API üzerinden tüm projeleri tarayıp webhook'ları topluca güncelleyen bir script yazıldı ve uygulandı
Nihai sonuç
- Aylık maliyet 1.432 $'dan 233 $'a düştü
- Yıllık tasarruf 14.388 $ oldu
- Performans tarafında da daha güçlü bir sunucu elde edildi
- CPU, 32 vCPU'dan 96 logical CPU'ya çıktı
- RAM, 192GB'dan 256GB DDR5'e yükseldi
- Depolama, yaklaşık 2,6TB karma yapıdan 2TB NVMe RAID1'a geçti
- Kesinti süresi 0 dakika oldu
- Tüm migrasyon yaklaşık 24 saat sürdü
- Kullanıcı tarafında hiçbir etki yaşanmadı
Temel dersler
- MySQL replication, kesintisiz migrasyonun temel aracı
- Erken kurulup yeterince catch-up yapması beklendikten sonra cutover yapılmalı
- MySQL kullanıcı yetkileri taşımadan önce mutlaka gözden geçirilmeli
- SUPER yetkisi varsa
read_onlybypass edilerek slave ortamının gerçekte salt okunur olmaması gibi bir sorun çıkabiliyor
- SUPER yetkisi varsa
- DNS güncellemeleri, Nginx yapılandırma değişiklikleri ve webhook düzeltmeleri için script'leştirme önemli
- 34'ten fazla siteyi elle yönetmek hem zaman kaybettirir hem hata riskini artırır
- mydumper + myloader kombinasyonu, büyük veri setlerinde
mysqldump'tan çok daha hızlı- 32 thread'li paralel dump/restore ile günler sürecek iş birkaç saate indirilebiliyor
- Steady-state iş yüklerinde cloud sağlayıcıları pahalı kalabilir; dedicated server daha düşük maliyetle daha yüksek performans sunabilir
GitHub script'leri
- Migrasyonda kullanılan tüm Python script'leri GitHub üzerinde açıklandı
- Dahil olan script listesi
do_list_domains_ttl.py- Tüm DigitalOcean domain'lerinin A kayıtlarını, IP'lerini ve TTL değerlerini listeler
do_ttl_update.py- Tüm A/AAAA kayıtlarının TTL değerini topluca 300 saniyeye düşürür
do_to_hetzner_bulk_dns_records_import.py- Tüm DNS zone'larını DigitalOcean'dan Hetzner DNS'e taşır
do_cutover_to_new_ip.py- Tüm A kayıtlarını eski sunucu IP'sinden yeni sunucu IP'sine geçirir
nginx_reverse_proxy_update.py- Tüm nginx site ayarlarını reverse proxy yapılandırmasına dönüştürür
mysql_compare.py- İki MySQL sunucusundaki tüm tabloların row count değerlerini karşılaştırır
final_gitlab_webhook_update.py- Tüm GitLab proje webhook'larını yeni sunucu IP'sine günceller
mydumper- mydumper kütüphanesi
- Tüm script'ler, gerçek uygulama öncesinde güvenli önizleme sağlayan
DRY_RUN = Truemodunu destekliyor
1 yorum
Hacker News yorumları
Birkaç ay önce iki sunucuyu Linode ve DO’dan Hetzner’e taşıdım ve maliyeti benzer ölçüde büyük oranda düşürdüm. Daha da etkileyici olan, onlarca sitenin farklı diller, eski kütüphaneler, MySQL ve Redis’le birbirine dolaştığı tam bir karmaşa yığını olmasıydı. Ama Claude Code bunların hepsini taşıdı, eksik kütüphaneler için de bazı kodları yeniden yazarak halletti. Artık bu tür karmaşık geçişler çok daha kolay, bu yüzden gelecekte sağlayıcılar arası taşınabilirliğin daha da artacağını düşünüyorum
AWS’den Hetzner’e geçiş planlıyorum. Amazon’un rakiplerinden bazen 20 kat daha pahalı fiyat çekmesi, biraz makul fiyat almak için uzun vadeli taahhüt dayatması ve veri çıkışını da aşırı pahalı hâle getirmesi bana çok müşteri düşmanı geliyor. İnsanları egress ücretleriyle kilitlediklerini sanırsın ama aslında bir parçayı bile rakibe taşısan seni tümünü taşımaya zorlayan bir baskı unsuru gibi çalışıyor. Yine de platformumu Amazon’a özgü servislerin üstüne kurmadığım için geçiş biraz daha kolay olacak
Böyle yazıları her gördüğümde, kimsenin pek yedeklilik ya da load balancer gibi şeylerden söz etmemesine şaşırıyorum. Tek bir sunucu ölürse birden çok servis birlikte düşebilir; insanların gerçekten bunu sorun etmediğini merak ediyorum. Belki paradan tasarruf etmişlerdir ama bakım süresi ve gelecekteki baş ağrıları daha pahalıya mal olabilir
Biz lithus.eu olarak müşterileri farklı cloud’lardan Hetzner’e sık sık taşıdık. Genelde çok sunuculu, bazen de çoklu AZ kurulum yapıyor ve Kubernetes ile iş yüklerini dağıtarak HA sağlıyoruz. Tek düğümde Kubernetes fazla olabilir ama birden çok düğüm varsa çok daha mantıklı. Yedeklerde Velero ile uygulama seviyesinde yedeklemeyi birlikte kullanıyoruz; örneğin Postgres için WAL yedekleriyle PITR da sağlıyoruz. State veriyi en az iki düğümde tutarak HA garanti ediyoruz. Performans açısından da bare metal çoğunlukla daha iyi oluyor; AWS’ye kıyasla yanıt süresinin yarıya indiği çok oldu. Bunun sebebinin sanallaştırmanın kendisinden çok NVMe, düşük ağ gecikmesi ve daha az cache contention gibi çevresel etkenler olduğunu düşünüyorum. Bununla ilgili daha fazlasını daha önce yazdığım HN gönderisinde de anlattım
Bu yazıyı okumak epey zordu. Sanki Claude migration’ı yapmış, sonra da Claude’un yazdığı raporu okuyormuşsun gibi. LLM sayesinde bu kadar tasarruf ettilerse harika, ama yayımlayacaksan en azından üzerinden geçip tekrarları ve LLM tarzı anlatımı temizlemek gerekirdi
Hetzner konusunda dikkatli olunması gerektiğini düşünüyorum. Eskiden gerçekten seviyordum ama yakın zamanda ayrıldım. CI/CD pipeline’ımızda kullandığımız yaklaşık 30 VM’in tamamını, tek bir 36 dolarlık fatura anlaşmazlığı yüzünden kapattılar. Banka kayıtları dâhil ödemenin tamamlandığını kanıtladık ama bakmayı bile reddettiler; acil şekilde iletişime geçmeye çalışırken sonunda tüm erişimi kestiler. Şimdi Scaleway’e taşındık
Birkaç ay önce küçük bir SaaS yan projesi için AWS alternatifi ararken, maliyet azaltma ve AB bulutlarını destekleme açısından önce Hetzner’i ciddi ciddi değerlendirdim. Daha çok şeyi kendim yapmam gerekse bile razıydım ama asıl engel IP itibarı oldu. Şirketimizdeki managed AWS firewall kurallarından biri Hetzner IP’lerinin çoğunu, belki tamamını engelliyordu; iş dizüstü bilgisayarımdan da Hetzner IP’lerinde barındırılan siteler IT politikası nedeniyle açılmıyordu. Cloudflare gibi bir şey kullanmak bunu azaltabilir ama DDoS korumasının zayıf olduğuna dair yorumlar da gördüm. Sonunda AB bölgesinde DO App Platform’u seçtim; managed veritabanı seçenekleri de büyük artıydı
Böyle migration deneyimlerini paylaşmaları gerçekten faydalı, o yüzden teşekkür ederim. Ben DO ile Hetzner karşılaştırmasını, DoorDash ya da UberEats açmakla akşam yemeğini kendin yapmak arasındaki trade-off gibi görüyorum. Maliyet oranı da aşağı yukarı benzer hissettiriyor. Üç büyük cloud’u da on-prem’i de yönetiyorum ama ufak işler ya da PoC testleri için hâlâ DigitalOcean konsoluna gidiyorum. Birkaç tıklamayla sunucu ya da bucket hazırlamak, mantıklı varsayılanlar ve tek bir onay kutusuyla yedekleme eklemek gibi kolaylıkların zaman değeri düşünülünce kesinlikle bir karşılığı var
DB yedeklerini nasıl yaptıklarını merak ettim. Replica ya da standby var mıydı, yoksa sadece saatlik yedekler mi alınıyordu öğrenmek istedim. Böyle tek sunuculu bir kurulumda SSD gibi donanım arızaları olursa uygulama doğrudan durabilir; özellikle SSD ölürse yeniden kurulum sırasında saatlerce hatta günlerce kesinti olabilir diye düşündüm
Başlıktaki meme görseli benim yaptığım bir şeydi. Şu yazıya koymuştum; böyle iki kez kullanıldığını görmek hoşuma gitti