paint-brush
Program Uygulamasında Aksiyomları Çökmekile@nekto0n
20,954 okumalar
20,954 okumalar

Program Uygulamasında Aksiyomları Çökmek

ile Nikita Vetoshkin9m2023/10/24
Read on Terminal Reader

Çok uzun; Okumak

Tecrübeli bir yazılım mühendisi olan yazar, sıralı koddan dağıtılmış sistemlere olan yolculuğuna dair içgörülerini paylaşıyor. Serileştirilmemiş yürütmeyi, çoklu iş parçacıklarını ve dağıtılmış bilgi işlemi benimsemenin performansın ve dayanıklılığın artmasını sağlayabileceğini vurguluyorlar. Karmaşıklık getirse de, yazılım geliştirmede bir keşif ve gelişmiş yetenek yolculuğudur.
featured image - Program Uygulamasında Aksiyomları Çökmek
Nikita Vetoshkin HackerNoon profile picture


Yeni hatalar yapmak

Yaklaşık 15 yıldır yazılım mühendisiyim. Kariyerim boyunca çok şey öğrendim ve bu öğrendiklerimi birçok dağıtılmış sistemi tasarlamak ve uygulamak (ve bazen aşamalı olarak kaldırmak veya olduğu gibi bırakmak) için uyguladım. Bu yolda birçok hata yaptım ve hâlâ yapmaya devam ediyorum. Ancak asıl odak noktam güvenilirlik olduğundan, hata sıklığını en aza indirmenin yollarını bulmak için deneyimlerime ve topluluğa bakıyorum. Benim sloganım şu: Kesinlikle yeni hatalar yapmayı denemeliyiz (daha az belirgin, daha karmaşık). Hata yapmak iyidir; bu şekilde öğreniriz, tekrarlamak üzücü ve cesaret kırıcıdır.


Muhtemelen beni matematik konusunda her zaman büyüleyen şey de budur. Sadece zarif ve özlü olduğu için değil, aynı zamanda mantıksal titizliği hataları önlediği için. Sizi mevcut bağlamınız, hangi varsayım ve teoremlere güvenebileceğiniz hakkında düşünmeye zorlar. Bu kurallara uymak verimli olur, doğru sonucu alırsınız. Bilgisayar biliminin matematiğin bir dalı olduğu doğrudur. Ama genelde uyguladığımız şey yazılım mühendisliği, çok farklı bir şey. Bilgisayar bilimindeki başarıları ve keşifleri pratiğe uyguluyor, zaman kısıtlamalarını ve iş ihtiyaçlarını hesaba katıyoruz. Bu blog, yarı matematiksel akıl yürütmeyi bilgisayar programlarının tasarımı ve uygulanmasına uygulama girişimidir. Birçok programlama hatasını önlemek için bir çerçeve sağlayan farklı yürütme rejimlerinden oluşan bir model ortaya koyacağız.


Mütevazı başlangıçlardan

Programlamayı öğrendiğimizde ve ilk geçici (veya cesur) adımlarımızı attığımızda genellikle basit bir şeyle başlarız:


  • döngüleri programlama, temel aritmetik işlemleri yapma ve sonuçları bir terminalde yazdırma
  • Matematik problemlerini muhtemelen MathCAD veya Mathematica gibi özel bir ortamda çözme


Kas hafızası kazanırız, dilin sözdizimini öğreniriz ve en önemlisi düşünme ve akıl yürütme şeklimizi değiştiririz. Kodu okumayı, nasıl yürütüldüğüne dair varsayımlarda bulunmayı öğreniyoruz. Neredeyse hiçbir zaman bir dil standardını okuyarak başlamıyoruz ve "Bellek modeli" bölümünü yakından okumuyoruz - çünkü henüz bunları tam olarak anlayacak ve kullanacak donanıma sahip değiliz. Deneme yanılma yapıyoruz: İlk programlarımızda mantıksal ve aritmetik hatalara yer veriyoruz. Bu hatalar bize varsayımlarımızı kontrol etmeyi öğretiyor: Bu döngü değişmezi doğru mu, dizi öğesinin indeksini ve uzunluğunu bu şekilde karşılaştırabilir miyiz (bunu -1'i nereye koyacağız)? Ancak bazı hataları görmezsek çoğu zaman örtülü olarak bazı hataları içselleştiririz. değişmezler sistem bizi zorluyor ve sağlıyor.


Yani bu:


Kod satırları her zaman aynı sırada (serileştirilmiş) değerlendirilir.

Bu varsayım, sonraki önermelerin doğru olduğunu varsaymamızı sağlar (onları kanıtlamayacağız):


  • değerlendirme sırası yürütmeler arasında değişmez
  • işlev çağrıları her zaman geri döner


Matematiksel aksiyomlar daha büyük yapıların sağlam bir temel üzerinde türetilmesine ve inşa edilmesine olanak sağlar. Matematikte 4+1 önermeli Öklid geometrisi vardır. Sonuncusu şöyle diyor:

Paralel doğrular paralel kalır, kesişmez veya ayrılmazlar


Binlerce yıldır matematikçiler bunu kanıtlamaya ve ilk dördünden çıkarmaya çalıştılar. Bunun mümkün olmadığı ortaya çıktı. Bu "paralel doğrular" varsayımını alternatiflerle değiştirebilir ve yeni perspektifler açan, uygulanabilir ve kullanışlı olduğu ortaya çıkan farklı türde geometriler (yani hiperbolik ve eliptik) elde edebiliriz. Sonuçta gezegenimizin yüzeyi düz değil ve bunu GPS yazılımı ve uçak rotaları gibi yöntemlerle hesaba katmamız gerekiyor.

Değişim ihtiyacı

Ama ondan önce duralım ve en mühendislik sorularını soralım: neden zahmet edesiniz ki? Program işini yapıyorsa, desteklenmesi, sürdürülmesi ve geliştirilmesi kolaysa, neden ilk etapta öngörülebilir sıralı yürütmenin bu rahat değişmezinden vazgeçelim?


İki cevap görüyorum. Birincisi performans . Programımızın iki kat daha hızlı veya benzer şekilde çalışmasını sağlayabilirsek (donanımın yarısı kadarını gerektirir) bu bir mühendislik başarısıdır. Aynı miktarda hesaplama kaynağı kullanırsak 2 kat (veya 3, 4, 5, 10 kat) veriyi işleyebiliriz; bu, aynı programın tamamen yeni uygulamalarını açabilir. Bir sunucu yerine cebinizdeki bir cep telefonunda çalışabilir. Bazen akıllı algoritmalar uygulayarak veya daha performanslı bir dilde yeniden yazarak hızlanmalar elde edebiliriz. Bunlar keşfedeceğimiz ilk seçenekler, evet. Ama bunların da bir sınırı var. Mimarlık neredeyse her zaman uygulamayı yener. Moor yasası son zamanlarda pek iyi durumda değil, tek bir CPU'nun performansı yavaş artıyor, RAM performansı (temel olarak gecikme) geride kalıyor. Doğal olarak mühendisler başka seçenekler aramaya başladı.


İkinci husus güvenilirliktir . Doğa kaotiktir; termodinamiğin ikinci yasası sürekli olarak kesin, sıralı ve tekrarlanabilir olan her şeye karşı çalışır. Parçalar ters dönüyor, malzemeler bozuluyor, elektrik kesiliyor, kablolar kesiliyor ve programlarımızın yürütülmesi engelleniyor. Sıralı ve tekrarlanabilir soyutlamayı sürdürmek zor bir iş haline gelir. Programlarımız yazılım ve donanım arızalarından daha uzun süre dayanabilseydi, rekabetçi iş avantajına sahip hizmetler sunabilirdik; bu, ele almaya başlayabileceğimiz başka bir mühendislik görevidir.


Hedefle donatılmış olarak serileştirilmemiş yaklaşımlarla deneylere başlayabiliriz.


Yürütme konuları

Bu sözde kod yığınına bakalım:


```

def fetch_coordinates(poi: str) -> Point:

def find_pois(center: Point, distance: int) -> List[str]:

def get_my_location() -> Point:


def fetch_coordinates(p) - Point:

def main():

me = get_my_location()

for point in find_pois(me, 500):
loc = fetch_coordinates(point)
sys.stdout.write(f“Name: {point} is at x={loc.x} y={loc.y}”)

Kodu yukarıdan aşağıya okuyabilir ve makul bir şekilde 'find_pois' fonksiyonunun 'get_my_location'dan sonra çağrılacağını varsayabiliriz. Ve bir sonrakini getirdikten sonra ilk İÇN'nin koordinatlarını alıp geri getireceğiz. Bu varsayımlar doğrudur ve program hakkında zihinsel bir model ve mantık oluşturmaya olanak sağlar.


Kodumuzu sıralı olmayan bir şekilde çalıştırabileceğimizi hayal edelim. Bunu sözdizimsel olarak yapmanın birçok yolu vardır. İfadelerin yeniden sıralanmasıyla ilgili deneyleri atlayacağız (modern derleyiciler ve CPU'lar bunu yapar) ve dilimizi, yeni bir işlev yürütme rejimini ifade edebilecek şekilde genişleteceğiz: eşzamanlı ya da paralel diğer işlevlerle ilgili olarak. Tekrar ifade edersek, birden fazla yürütme iş parçacığını tanıtmamız gerekiyor. Program fonksiyonlarımız belirli bir ortamda (işletim sistemi tarafından hazırlanmış ve bakımı yapılmış) yürütülür, şu anda adreslenebilir sanal bellek ve bir iş parçacığıyla (bir zamanlama birimi, bir CPU tarafından yürütülebilen bir şey) ilgileniyoruz.


İplikler farklı şekillerde gelir: POSIX ipliği, yeşil iplik, koroutin, gorutin. Ayrıntılar büyük ölçüde farklılık gösterir, ancak gerçekleştirilebilecek bir şeye indirgenir. Eğer birden fazla fonksiyon aynı anda çalışabiliyorsa, her birinin kendi planlama ünitesine ihtiyacı vardır. Yani çoklu iş parçacığı bir tane yerine birden fazla iş parçacığından geliyor. Bazı ortamlar (MPI) ve diller örtülü olarak iş parçacığı oluşturabilir, ancak genellikle bunu C'de 'pthread_create', Python'da 'threading' modül sınıflarını veya Go'da basit bir 'go' ifadesini kullanarak açıkça yapmamız gerekir. Bazı önlemlerle aynı kodun çoğunlukla paralel çalışmasını sağlayabiliriz:


 def fetch_coordinates(poi, results, idx) -> None: … results[idx] = poi def main(): me = get_my_location() points = find_pois(me, 500) results = [None] * len(points) # Reserve space for each result threads = [] for i, point in enumerate(find_pois(me, 500)): # i - index for result thr = threading.Thread(target=fetch_coordinates, args=(poi, results, i)) thr.start() threads.append(thr) for thr in threads: thr.wait() for point, result in zip(points, results): sys.stdout.write(f“Name: {poi} is at x={loc.x} y={loc.y}”)


Performans hedefimize ulaştık: Programımız birden fazla CPU üzerinde çalışabilir ve çekirdek sayısı arttıkça ve daha hızlı tamamlandıkça ölçeklenebilir. Sormamız gereken bir sonraki mühendislik sorusu: Ne pahasına?

Serileştirilmiş ve öngörülebilir yürütmeden kasıtlı olarak vazgeçtik. Orada itham yok bir fonksiyon + zamandaki bir nokta ile veriler arasında. Zamanın her noktasında, çalışan bir fonksiyon ile onun verileri arasında her zaman tek bir eşleme vardır:


Artık birden fazla işlev verilerle aynı anda çalışıyor:


Bir sonraki sonuç, bu kez bir işlevin diğerinden önce bitmesi, bir dahaki sefere ise tam tersi olabilmesidir. Bu yeni yürütme rejimi veri yarışlarına yol açmaktadır: eşzamanlı işlevler verilerle çalıştığında, bu, verilere uygulanan işlem sırasının tanımsız olduğu anlamına gelir. Veri yarışlarıyla karşılaşmaya başlıyoruz ve aşağıdakileri kullanarak bunlarla başa çıkmayı öğreniyoruz:

  • kritik bölümler: muteksler (ve spinlock'lar)
  • kilitsiz algoritmalar (en basit biçim yukarıdaki kod parçasındadır)
  • yarış tespit araçları
  • vesaire


Bu noktada en az iki şeyi keşfediyoruz. İlk olarak verilere erişmenin birden fazla yolu vardır. Bazı veriler yerel (örn. işlev kapsamındaki değişkenler) ve yalnızca biz görebiliriz (ve ona erişebiliriz) ve bu nedenle her zaman onu bıraktığımız durumdadır. Ancak bazı veriler paylaşılır veya uzak . Hala süreç belleğimizde bulunur, ancak ona erişmek için özel yollar kullanırız ve senkronizasyonu bozulabilir. Bazı durumlarda onunla çalışmak için veri yarışlarını önlemek amacıyla onu yerel hafızamıza kopyalarız - bu yüzden == .klon() ==Rust'ta popülerdir.


Bu mantık yürütmeye devam ettiğimizde, yerel iş parçacığı depolama gibi diğer teknikler de doğal olarak devreye giriyor. Programlama araçlarımıza yeni bir gadget ekledik ve yazılım geliştirerek yapabileceklerimizi genişlettik.


Ancak yine de güvenebileceğimiz bir değişmez var. Bir ileti dizisinden paylaşılan (uzak) verilere ulaştığımızda, her zaman onu alırız. Bazı bellek parçalarının mevcut olmadığı bir durum yoktur. Destekleyici fiziksel bellek bölgesinin arızalanması durumunda işletim sistemi, işlemi sonlandırarak tüm katılımcıları (iş parçacıklarını) sonlandıracaktır. Aynı şey "bizim" iş parçacığımız için de geçerlidir, eğer bir muteksi kilitlersek, kilidi kaybetmemiz mümkün değildir ve yaptığımız işi hemen durdurmamız gerekir. Tüm katılımcıların ya ölü ya da diri olduğu şeklindeki bu değişmeze (işletim sistemi ve modern donanım tarafından zorlanan) güvenebiliriz. Hepsi aynı kaderi paylaşıyor : eğer süreç (OOM), işletim sistemi (çekirdek hatası) veya donanım bir sorunla karşılaşırsa, tüm iş parçacıklarımız harici yan etkiler olmaksızın birlikte var olmaya son verecek.


Bir süreç icat etmek

Dikkat edilmesi gereken önemli bir nokta. Konuları tanıtarak bu ilk adımı nasıl attık? Ayrıldık, çatallaştık. Tek bir planlama birimi yerine birden fazla planlama birimini uygulamaya koyduk. Bu paylaşmama yaklaşımını uygulamaya devam edelim ve nasıl olacağını görelim. Bu sefer işlem sanal belleğini kopyalıyoruz. Buna bir sürecin ortaya çıkması denir. Programımızın başka bir örneğini çalıştırabilir veya mevcut başka bir yardımcı programı başlatabiliriz. Bu aşağıdakiler için harika bir yaklaşımdır:

  • diğer kodları katı sınırlarla yeniden kullanın
  • güvenilmeyen kodu kendi hafızamızdan izole ederek çalıştırırız


Hemen hemen hepsi == modern tarayıcılar ==bu şekilde çalışırlar, böylece İnternet'ten indirilen güvenilmeyen Javascript çalıştırılabilir kodunu çalıştırabilirler ve bir sekmeyi kapattığınızda tüm uygulamayı kapatmadan onu güvenilir bir şekilde sonlandırabilirler.

Bu, paylaşılan kader değişmezinden vazgeçerek ve sanal hafızayı paylaşmadan kaldırarak ve bir kopya oluşturarak keşfettiğimiz başka bir yürütme rejimidir. Kopyalar ücretsiz değildir:

  • İşletim sisteminin bellekle ilgili veri yapılarını yönetmesi gerekiyor (sanal -> fiziksel eşlemeyi korumak için)
  • Bazı bitler paylaşılmış olabilir ve bu nedenle işlemler ek bellek tüketir



Serbest kalmak

Neden burada duralım? Programımızı başka nelere kopyalayıp dağıtabileceğimizi keşfedelim. Ama neden ilk etapta dağıtılıyor? Çoğu durumda eldeki görevler tek bir makine kullanılarak çözülebilir.


Dağıtıma çıkmamız lazım ortak kaderden kaçmak Böylece yazılımımız, altta yatan katmanların karşılaştığı kaçınılmaz sorunlara bağlı olarak durur.


Birkaç isim:

  • İşletim sistemi yükseltmeleri: zaman zaman makinelerimizi yeniden başlatmamız gerekir

  • Donanım arızaları: istediğimizden daha sık oluyorlar

  • Dış arızalar: Güç ve ağ kesintileri bir şeydir.


Bir işletim sistemini kopyalarsak, buna sanal makine deriz ve müşterilerin programlarını fiziksel bir makinede çalıştırabilir ve üzerinde büyük bir bulut işi oluşturabiliriz. İki veya daha fazla bilgisayarı alıp programlarımızı her birinde çalıştırırsak, programımız bir donanım arızasına bile dayanabilir, 7/24 hizmet verebilir ve rekabet avantajı kazanabilir. Büyük şirketler uzun zaman önce daha da ileri gitti ve şimdi İnternet devleri kopyaları farklı veri merkezlerinde ve hatta kıtalarda çalıştırıyor, böylece bir programı bir tayfuna veya basit bir elektrik kesintisine karşı dayanıklı hale getiriyor.


Ancak bu bağımsızlığın bir bedeli var: Eski değişmezler uygulanmıyor, kendi başımızayız. Merak etmeyin ilk biz değiliz. Bize yardımcı olacak birçok teknik, araç ve hizmet var.


Paket servis

Az önce sistemler ve onların ilgili yürütme rejimleri hakkında akıl yürütme yeteneği kazandık. Her büyük ölçekli sistemin içinde çoğu parça tanıdık sıralı ve durumsuzdur; birçok bileşen, hepsi gerçekten dağıtılmış bazı parçaların bir karışımı tarafından bir arada tutulan bellek türleri ve hiyerarşilerle birlikte çok iş parçacıklıdır:


Amaç şu anda nerede olduğumuzu, değişmezlerin neleri tuttuğunu ayırt edebilmek ve buna göre hareket edebilmek (değiştirmek/tasarlayabilmektir). “Bilinmeyen bilinmeyenleri” “bilinen bilinmeyenlere” dönüştürerek temel mantığı vurguladık. Hafife almayın, bu önemli bir gelişme.