Amazon’da nasıl çözüm ürettiğimize yönelik rehber ilkelerimizden birisi şudur: tek yönlü kapılardan geçmekten kaçınma. Bu, geri dönmesi veya genişletmesi zor kararlardan uzak durduğumuz anlamına gelir. Bu ilkeyi, ürün, özellik, API, arka uç sistemleri geliştirmeden dağıtıma kadar yazılım geliştirmenin tüm adımlarında uyguluyoruz. Bu makalede, bu ilkeyi yazılım dağıtımına nasıl uyguladığımızı anlatacağım.

Dağıtım, bir yazılım ortamını bir durumdan (sürüm) başka bir duruma taşır. Yazılım bu durumların herhangi birinde gayet iyi çalışabilir. Ancak, yazılım ileriye geçiş (yükseltme veya ileri alma) veya geriye geçiş (eski sürümü yükleme veya geri alma)sırasında veya sonrasında düzgün çalışmayabilir. Bir yazılım düzgün çalışmadığı zaman onu müşteriler için güvenilmez yapan hizmet kesintisine neden olur. Bu makalede, yazılımın iki sürümünün de beklendiği gibi çalıştığını varsayıyorum. Dağıtım sırasında ileri veya geri almanın herhangi bir hataya sebep olmamasından nasıl emin olunacağına odaklanıyorum.

Bir yazılımın yeni sürümünü de yayınlamadan önce, yazılımı beta veya gama test ortamında işlevsellik, eş zamanlılık, performans, ölçek ve aşağı akış hatası ile başa çıkma gibi çoklu boyutlarda test ederiz. Bu test, yeni sürümdeki sorunları keşfetmemizi ve onları çözmemizi sağlar. Ancak, bu her zaman başarılı bir dağıtımı sağlamaya yeterli olmayabilir. Üretim ortamlarında beklenmedik durumlar veya yetersiz yazılım davranışı ile karşılaşabiliriz. Amazon’da dağıtımı geriye almanın müşterilerimiz için hataya sebep olabileceği durumlardan kaçınmak isteriz. Bu durumdan kaçınmak için her dağıtımdan önce kendimizi geri almaya tam olarak hazırlarız. Hata veya kesinti olmadan önceki sürümde mevcut olan işlevselliğine geri alınabilen yazılım geriye dönük uyumlu olarak adlandırılır. Her düzenlemeden sonra plan yapar ve yazılımımızın geriye dönük uyumlu olduğundan emin oluruz.

Amazon’un yazılım güncellemelerine yaklaşımı hakkında detaylara girmeden önce, tek başına ve dağıtılmış yazılım dağıtımları arasındaki bazı farklardan bahsedelim.

Tek başına ve dağıtılmış yazılım dağıtımları

Dağıtımlar, bir cihazda bir işlem olarak çalışan tek başına yazılımlar için atomiktir. Yazılımın iki sürümü asla aynı anda çalışmaz. Tek başına yazılımın durumunu koruması halinde yeni sürüm eski sürüm tarafından ve aynı şekilde eski sürüm ise yeni sürüm tarafından yazılan (yani serileştirilen) verileri okumak (yani seriden çıkarmak) zorunda kalır. Bu durumu sağlamak dağıtımın ileri ve geri alınması için güvenli hale getirilmesini sağlar.
 
Dağıtılmış bir sistemde, dağıtım daha karmaşık hale gelir. Dağıtımlar, erişilebilirlik etkilenmesin diye yuvarlama güncellemeleri ile gerçekleştirilir. Yeni sürüm tek seferde bir konak alt kümesinin kullanımına sunulur ki diğer konaklar isteklere yanıt vermeye devam edebilsin. Genelde, bu konaklar uzaktan işlem çağrısı (RPC) veya paylaşılan kalıcı durum (örneğin, meta veriler veya denetim noktaları) aracılığıyla birbirleri ile iletişim kurarlar. Bu iletişim veya paylaşılan durum ek zorluklar çıkarabilir. Yazıcı ve okuyucu yazılımın farklı sürümlerini çalıştırıyor olabilir. Sonuç olarak veriyi farklı yorumlayabilirler. Hatta okuyucu tamamen veriyi okumakta bile başarısız olarak kesintiye yol açabilir.

Protokol değişiklikleri ile ilgili sorunlar

Geri alma sorununun en yaygın sebebinin protokol değişikliği olduğunu bulduk. Örneğin, veriyi diske kalıcı kılarken aynı zamanda sıkıştıran bir kod değişikliğini düşünün. Yeni sürüm, biraz sıkıştırılmış veri yazdıktan sonra geri almak bir seçenek olmaktan çıkar. Eski sürüm, veriyi diskten okuduktan sonra sıkıştırılmış veriyi açması gerektiğini bilmez. Eğer veri, bir blobda veya belge deposunda depolanıyorsa diğer sunucular dağıtım sürecinde dahi veriyi okumakta başarısız olacaktır. Eğer veri iki işlem veya sunucu arasında iletildiyse alıcı veriyi okumakta başarısız olacaktır.

Bazen protokol değişikliklerinin algılanması zor olabilir. Örneğin, iki sunucunun eş zamanlı olmayan bir şekilde bir bağlantı üzerinden iletişime geçtiğini düşünün. Birbirlerini yaşadıklarından haberdar etmek için her beş saniyede bir birbirlerine sinyal gönderme konusunda anlaşırlar. Eğer bir sunucu öngörülen zamanda sinyal almaz ise diğer sunucunun devre dışı kaldığını varsayarak bağlantıyı kapatır.

Sinyal süresini 10 saniyeye çıkaran bir dağıtım düşünün. Kod yürütmesi sadece küçük bir numara değişikliği olarak gözüküyor. Ancak, artık hem ileri hem de geri alma güvenli değildir. Dağıtım sırasında yeni sürümü çalıştıran sürücü her 10 saniyede bir sinyal gönderir. Bu nedenle eski sürümü çalıştıran sunucu beş saniyeden fazla sinyal görmez ve yeni sürümü çalıştıran sunucu ile bağlantısını keser. Geniş bir filoda bu durum birkaç bağlantı arasında yaşanabilir ve erişilebilirlik düşüşüne neden olur.

Bu gibi algılanması zor değişiklikleri kod okuyarak veya belge tasarlayarak analiz etmesi zordur. Bu yüzden, açıkça her bir dağıtımın güvenle ileri veya geri alınabileceğini onaylıyoruz.

İki aşamalı dağıtım tekniği

Güvenle geri alabileceğimizden emin olmanın yollarından biri genellikle iki aşamalı dağıtım tekniği olarak adlandırılan tekniği kullanmaktır. Şu olası senaryoyu, Amazon Simple Storage Service (Amazon S3) üzerindeki verileri yöneten (bu verilere yazan ve bunlardan okuyan) bir hizmet ile düşünün. Hizmet, ölçeklendirme ve erişilebilirlik için çoklu Erişilebilirlik Alanı boyunca bulunan sunucu filosunu yönetiyor.

Şu anda hizmet, verileri kalıcı kılmak için XML biçimini kullanıyor. Sürüm V1’de yer alan aşağıdaki diyagramda gösterildiği gibi tüm sunucular XML biçiminde yazar ve okur. İşe yönelik sebeplerden dolayı verileri JSON biçiminde kalıcı kılmak istiyoruz. Bu değişikliğin bir dağıtımda yapıldığı durumlarda değişikliği alan sunucular JSON biçiminde yazacaklardır. Fakat, diğer sunucular şu an JSON okumayı bilmiyorlar. Bu durum hatalara sebep olur. Bu yüzden, böyle bir değişikliği iki parçaya böleriz ve iki aşamalı dağıtım uygularız.

Önceki diyagramda gösterildiği gibi ilk aşamaya Hazırlık diyoruz. Bu aşamada, tüm sunucuları (XML’ye ek olarak) JSON okumaya hazırlarız. Fakat sürüm V2’yi dağıtarak XML biçiminde yazmaya devam ederler. Bu değişiklik operasyonel açıdan hiçbir şeyi değiştirmez. Tüm sunucular hâlâ XML okuyabilir ve tüm veriler hâlâ XML biçiminde yazılır. Eğer bu değişikliği geri almaya karar verirsek sunucular JSON okuyamadıkları duruma geri dönerler. Hiçbir veri JSON biçiminde yazılmadığı için bu durum sorun teşkil etmez.

Önceki diyagramda gösterildiği gibi ikinci aşamaya Etkinleştirme diyoruz. Bu aşamada, sürüm V3’ü dağıtarak yazma için JSON biçimini kullanmalarını sağlamak amacıyla sunucuları etkinleştiriyoruz. Her sunucu bu değişikliği aldığında JSON biçiminde yazmaya başlar. Bu değişikliği henüz almayan sunucular da ilk aşamada hazırlandıkları için hâlâ JSON okuyabilirler. Eğer bu değişikliği geri almaya karar verirsek geçici olarak Etkinleştirme aşamasında olan sunucular tarafından yazılan tüm veriler JSON biçiminde olur. Etkinleştirme aşamasında olmayan sunucular tarafından yazılan veriler XML biçimindedir. Bu iyi bir durum çünkü V2’de gösterildiği üzere sunucular geri alınmadan sonra hâlâ hem XML hem JSON biçimini okuyabilirler.

Önceki diyagram XML’den JSON’a serileştirme biçim değişikliğini gösterse de, genel teknik daha önce Protokol değişiklikleri bölümünde tanımlanan tüm durumlar için geçerlidir. Örneğin, sunucular arasındaki sinyal süresinin beş saniyeden 10 saniyeye yükseltilmesi gereken önceki senaryoyu düşünün. Hazırlık aşamasında tüm sunucuların beş saniyede bir sinyal göndermesine rağmen tüm sunucuların beklenen sinyal süresini 10 saniyeye uzatabiliriz. Etkinleştirme aşamasında, sıklığı her 10 saniyede bir olarak değiştirebiliriz.

İki aşamalı dağıtımlarda alınan önlemler

Şimdi, iki aşamalı dağıtım tekniğini izlerken aldığımız önlemleri anlatacağım. Önceki bölümde tanımlanan örnek senaryoya değinmeme rağmen bu önlemler çoğu iki aşamalı dağıtım için geçerlidir.

Birçok dağıtım aracı, eğer minimum sayıda konak değişikliği alır ve iyi durumda olduklarını bildirirlerse kullanıcıların dağıtımı başarılı kabul etmelerini sağlar. Örneğin, AWS CodeDeploy’da minimumHealthyHosts (minimum iyi durumda konak) adlı bir dağıtım yapılandırması vardır.

İki aşamalı dağıtım örneğinde olan kritik varsayımlardan birisi ilk aşamanın sonunda tüm sunucuların XML ve JSON biçimini okuyabilecek şekilde yükseltildiğidir. Eğer bir veya daha fazla sunucunun yükseltilmesi ilk aşamada başarısız olursa bu sunucular ikinci aşama sırasında ve sonrasında veriyi okumakta başarısız olurlar. Bu yüzden Hazırlık aşamasında tüm sunucuların değişikliği aldığını açıkça doğruluyoruz.

Ben Amazon DynamoDB üzerinde çalışırken birçok mikro hizmeti kapsayan çok sayıda sunucu arasındaki iletişim protokolünü değiştirmeye karar verdik. Tüm mikro hizmetler arasındaki dağıtımları ben koordine ettim. Böylece, tüm sunucular öncelikle Hazırlık aşamasına ulaşacak ve sonra Etkinleştirme aşamasına geçecekti. Her bir aşama sonunda önlem olarak dağıtımın tüm sunucularda başarılı olduğunu açık bir şekilde doğruladım.

Her iki aşamanın da geri alınabilme için güvenli olmasına rağmen her iki değişikliği geri alamayız. Önceki örnekte, Etkinleştirme aşamasının sonunda sunucular veriyi JSON biçiminde yazıyordu. Hazırlık ve Etkinleştirme değişiklikleri öncesinde kullanımda olan yazılım sürümü JSON okumayı bilmiyor. Bu yüzden önlem olarak Hazırlık ve Etkinleştirme aşamaları arasında hatırı sayılır bir süre geçmesine izin veririz. Buna pişirme süreci diyoruz ve genellikle birkaç gün sürer. Eski bir sürüme geri almamız gerekmediğinden emin olmak için bekleriz.

Etkinleştirme aşamasından sonra yazılımın XML okuma becerisini güvenle kaldıramayız. Kaldırmak güvenli değildir çünkü Hazırlık aşamasından önce yazılan tüm veriler XML biçimindedir. XML okuma becerisini ancak her bir nesnenin JSON biçiminde tekrardan yazıldığından emin olduktan sonra kaldırabiliriz. Bu sürece geri doldurma diyoruz. Hizmet, verileri yazarken ve okurken eş zamanlı olarak çalışabilen ek araçlar gerekebilir.

Serileştirme için en iyi uygulamalar

Çoğu yazılım kalıcı kılmak veya bir ağ üzerinden aktarım için veri serileştirmeyi içerir. Serileştirme mantığının geliştikçe değişmesi yaygındır. Değişiklikler yeni bir alan ekleme ile tamamen biçimi değiştirme arasında dağılım gösterebilir. Yıllar içerisinde serileştirme için takip ettiğimiz bazı en iyi uygulamalara ulaştık:

• Genellikle özel serileştirme biçimlerini geliştirmekten kaçınırız.

Özel serileştirme için ilk mantık önemsiz gibi görünebilir ve hatta daha iyi performans sağlayabilir. Ancak, biçimin sonraki yinelemeleri JSON, Protocol Buffers, Cap’n Proto ve FlatBuffers gibi iyi yapılandırılmış çerçeveler tarafından çözülmüş zorluklar sunar. Uygun bir şekilde kullanıldığında bu çerçeveler kaçış, geriye dönük uyumluluk ve öznitelik varlık takibi (bir alanın doğrudan kurulup kurulmadığı veya dolaylı olarak varsayılan değer atanıp atanmadığı) gibi güvenlik özellikleri sağlar.

• Her değişiklikle beraber serileştiricilere doğrudan ayrı bir sürüm atarız.

Bunu kaynak kodu veya sürüm oluşturmadan bağımsız olarak yaparız. Serileştirici sürümünü serileşmiş veri ile veya meta veri içerisinde depolarız. Eski serileştirici sürümler yeni yazılımda işlev görmeye devam eder. Yazılmış veya okunmuş verinin sürümü için bir ölçüm yayınlamanın genellikle faydalı olduğunu düşünürüz. Bu, hatların olması durumunda operatörlere görünürlük ve sorun giderme bilgisi sunar. Tüm bunlar RPC ve API sürümleri için de geçerlidir.

• Kontrol edemediğimiz veri yapılarını serileştirmekten kaçınırız.

Örneğin, yansıtma ile Java’nın koleksiyon nesnelerini serileştirebilirdik. Ancak, JDK’yı güncelleştirmeyi denediğimizde böyle sınıfların temel uygulamalarının değişebileceğini ve serinin kaldırılmamasına sebep olabileceğini gördük. Bu risk ekipler arasında paylaşılan kitaplıklardan sınıflar için de geçerlidir.

•Genel olarak serileştiricileri bilinmeyen özniteliklerin varlığına izin vermeleri için tasarlarız.
 
Makul olduğu yerlerde serileştiricilerimiz, veriyi geri yazarken bilinmeyen öznitelikleri saklar. Bu konaklama ile, yazılımın yeni sürümünü çalıştıran sunucu bile serileştirilirken veride yeni öznitelikler içerir ve eski sürümü çalıştıran sürücüler aynı veriyi güncellerken bu öznitelikleri silmeyecektir. Bu yüzden, iki aşamalı dağıtım gerekli değildir.

En iyi uygulamalarımızda da olduğu gibi, yönergelerimiz tüm uygulama ve senaryolar için geçerli olmadığından bunları dikkatle paylaşırız.

Bir değişikliğin geri alma için güvenli olduğunu doğrulamak

Genel olarak, bir yazılım değişikliğinin bizim yükseltme-eski sürümü yükleme dediğimiz test ile ileri ve geri almanın güvenli olduğunu açıkça doğrularız. Bu süreç için üretim ortamlarının bir temsilcisi olan bir test ortamı kurarız. Yıllardır test ortamlarını kurarken kaçındığımız birkaç modeli tanımladık.

Test ortamındaki tüm testleri geçmesine rağmen bir değişikliğin üründeki dağıtımın hatalara sebep olduğu durumlar gördüm. Durumlardan birinde test ortamındaki hizmetlerin her birinde sadece birer tane sunucu vardı. Bu yüzden tüm dağıtımlar atomikti ve bu da yazılımın farklı sürümlerini eş zamanlı çalıştırma ihtimalini olanaksızlaştırdı. Test ortamlarında üretim ortamları kadar trafik olmasa da üretim ortamlarındaki gibi her bir hizmet arkasında farklı Erişilebilirlik Alanlarından birden çok sunucu kullanırız. Amazon’da sadeliği severiz fakat kalite sağlamak söz konusu olduğunda değil.

Başka bir olayda ise test ortamında birden fazla sunucu vardı. Ancak, testi hızlandırmak için dağıtım tüm sunuculara aynı anda uygulandı. Bu yaklaşım aynı zamanda yazılımın eski ve yeni sürümlerinin aynı anda çalışmasını önledi. İleri alma ile ilgili sorun tespit edilmedi. Şimdi tüm test ve üretim ortamlarında aynı dağıtım yapılandırmasını kullanıyoruz.

Mikro hizmetler arası koordinasyon içeren değişiklikler için test ve üretim ortamlarında mikro hizmetler boyunca aynı dağıtım sırasını sürdürüyoruz. Ancak, ileri ve geri alma sırası farklı olabilir. Örneğin, genellikle serileştirme bağlamında özel bir sıra takip ediyoruz. Yani ileri alınırken okuyucular yazarlardan önce gelirken geri alınırken ise yazarlar okuyuculardan önce gelir. Uygun düzen genellikle test ve üretim ortamlarında takip edilir.

Bir test ortamı kurulumu üretim ortamına benzer olduğunda mümkün olduğunca üretim trafiğini simüle ederiz. Örneğin, hızlıca art arda birkaç kayıt (veya ileti) oluşturur ve okuruz. Tüm API’ler sürekli olarak uygulanır. Sonra, ortamı üç aşama ile alırız. Her biri potansiyel hataları tespit edecek kadar makul bir sürede tamamlanır. Bu, tüm API’lerin, arka uç iş akışlarının ve iş grubunun en azından bir kez çalışması için yeterince uzun bir süredir.

Öncelikle, yazılım sürümünün bir arada olmasını sağlamak için filonun yaklaşık yarısına değişikliği dağıtırız. İkinci olarak, dağıtımı tamamlarız. Daha sonra, geri almayı başlatır ve tüm sunucular eski yazılımı çalıştırana kadar aynı adımları takip ederiz. Bu aşamalar sırasında hata veya beklenmeyen davranışlar yoksa testi başarılı sayarız.

Sonuç

Müşterilerimiz için hiçbir kesinti olmadan bir dağıtımı geri alabilmemizi sağlamamız bir hizmeti güvenilir yapmak için önemlidir. Geri alma güvenliğini açıkça test etmek hata eğilimi olabilen manuel analizlere güven ihtiyacını ortadan kaldırır. Bir değişikliğin geri alma için güvenli olmadığını keşfettiğimizde bunu genellikle her biri geri ve ileri alma için güvenli olan iki değişikliğe bölebiliriz.

Daha fazla kaynak

Amazon’un müşteri memnuniyeti ve geliştirici üretkenliğini artırırken aynı zamanda hizmetlerinin güvenliği ve erişilebilirliğini nasıl iyileştirdiği hakkında daha fazla bilgi edinmek için Sürekli teslim ile daha hızlı ilerleme bölümüne göz atın.


Yazar hakkında

Sandeep Pokkunuri AWS’de baş mühendistir. 2011 yılında Amazon’a katıldığından beri Amazon Dynamodb ve Amazon Simple Queue Service (SQS) dahil olmak üzere birçok hizmette çalışmıştır. Şu an insan dillerini (örneğin, ASR, NLP, NLU ve Makine Çevirisi) içeren ML teknolojileri üzerine odaklanan Sandeep Pokkunuri Amazon Lex’te baş mühendistir. AWS’ye katılmadan önce Google’da sosyal medyada spam ve rahatsız edici içerik tespiti ile ağ erişim günlüklerinde anormallik tespiti gibi makine öğrenimi sorunları üzerinde çalışmıştır.

Sürekli teslim ile daha hızlı ilerleme