paint-brush
프로그램 실행에서 원칙 깨기~에 의해@nekto0n
20,889 판독값
20,889 판독값

프로그램 실행에서 원칙 깨기

~에 의해 Nikita Vetoshkin9m2023/10/24
Read on Terminal Reader

너무 오래; 읽다

숙련된 소프트웨어 엔지니어인 저자는 순차 코드에서 분산 시스템으로의 여정에 대한 통찰력을 공유합니다. 그들은 비직렬화된 실행, 멀티스레딩, 분산 컴퓨팅을 수용하면 성능과 탄력성이 향상될 수 있다고 강조합니다. 이는 복잡성을 가져오지만 소프트웨어 개발에서 발견과 향상된 기능을 찾는 여정입니다.
featured image - 프로그램 실행에서 원칙 깨기
Nikita Vetoshkin HackerNoon profile picture


새로운 실수를 저지르다

저는 소프트웨어 엔지니어로 일한 지 약 15년이 되었습니다. 저는 경력 전반에 걸쳐 많은 것을 배웠고 이러한 학습 내용을 적용하여 많은 분산 시스템을 설계 및 구현(때로는 단계적으로 폐지하거나 그대로 두는 경우도 있음)했습니다. 그 과정에서 나는 수많은 실수를 저질렀고 지금도 계속해서 실수를 저지르고 있습니다. 하지만 내 주된 초점은 신뢰성이었기 때문에 오류 빈도를 최소화하는 방법을 찾기 위해 내 경험과 커뮤니티를 되돌아보았습니다. 내 좌우명은: 우리는 반드시 새로운 실수를 저지르도록 노력해야 한다는 것입니다(덜 명백하고, 더 정교합니다). 실수하는 것은 괜찮습니다. 그것이 우리가 배우고 반복하는 방법입니다. 이는 슬프고 실망스럽습니다.


아마도 그것이 수학에 관해 나를 항상 매료시켰던 이유일 것입니다. 우아하고 간결할 뿐만 아니라 논리적 엄격함으로 인해 실수를 방지할 수 있기 때문입니다. 이는 현재 상황, 신뢰할 수 있는 가정 및 정리에 대해 생각하게 만듭니다. 이러한 규칙을 따르면 유익한 결과를 얻을 수 있으며 올바른 결과를 얻을 수 있습니다. 컴퓨터 과학이 수학의 한 분야라는 것은 사실입니다. 하지만 우리가 일반적으로 실천하는 것은 소프트웨어 엔지니어링으로, 매우 별개의 분야입니다. 우리는 시간 제약과 비즈니스 요구 사항을 고려하여 컴퓨터 과학 성과와 발견을 실습에 적용합니다. 이 블로그는 컴퓨터 프로그램의 설계 및 구현에 반수학적 추론을 적용하려는 시도입니다. 우리는 많은 프로그래밍 오류를 피할 수 있는 프레임워크를 제공하는 다양한 실행 방식 의 모델을 제시할 것입니다.


겸손한 시작부터

우리가 프로그래밍하는 법을 배우고 첫 번째 잠정적(또는 대담한) 단계를 수행할 때 일반적으로 다음과 같은 간단한 것부터 시작합니다.


  • 루프 프로그래밍, 기본 산술 수행 및 결과를 터미널에 인쇄
  • MathCAD 또는 Mathematica와 같은 특수한 환경에서 수학 문제 해결


우리는 근육 기억을 습득하고 언어 구문을 배우며 가장 중요한 것은 우리가 생각하고 추론하는 방식을 바꿉니다. 우리는 코드를 읽고 코드가 어떻게 실행되는지 가정하는 방법을 배웁니다. 우리는 언어 표준을 읽는 것부터 시작하여 "기억 모델" 섹션을 자세히 읽는 일이 거의 없습니다. 왜냐하면 우리는 아직 표준을 완전히 이해하고 활용할 준비가 되어 있지 않기 때문입니다. 우리는 시행착오를 연습합니다. 첫 번째 프로그램에는 논리 및 산술 버그가 도입됩니다. 이러한 실수는 가정을 확인하도록 가르쳐줍니다. 이 루프 불변이 올바른지, 배열 요소의 인덱스와 길이를 이런 식으로 비교할 수 있습니까(-1을 어디에 입력합니까)? 그러나 어떤 종류의 오류가 표시되지 않으면 종종 암묵적으로 일부 오류를 내면화합니다. 불변성 시스템은 우리에게 시행하고 제공합니다.


즉 이것은:


코드 줄은 항상 동일한 순서(직렬화)로 평가됩니다.

이 가정을 통해 우리는 다음 명제가 참이라고 가정할 수 있습니다. (우리는 이를 증명하지는 않을 것입니다.)


  • 평가 순서는 실행 간에 변경되지 않습니다.
  • 함수 호출은 항상 반환됩니다.


수학적 공리를 사용하면 탄탄한 기반에서 더 큰 구조를 파생하고 구축할 수 있습니다. 수학에는 4+1 가정을 갖는 유클리드 기하학이 있습니다. 마지막 내용은 다음과 같습니다.

평행선은 평행을 유지하며 교차하거나 갈라지지 않습니다.


수천년 동안 수학자들은 그것을 증명하려고 노력했고 처음 4가지로부터 그것을 도출하려고 노력했습니다. 그것은 불가능하다는 것이 밝혀졌습니다. 우리는 이 "평행선" 가정을 대안으로 대체하고 새로운 전망을 열어주고 적용 가능하고 유용한 것으로 판명되는 다양한 종류의 기하학(즉, 쌍곡선 및 타원)을 얻을 수 있습니다. 결국 우리 행성의 표면은 평평하지 않으며 GPS 소프트웨어나 비행기 노선 등에서 이를 고려해야 합니다.

변화의 필요성

하지만 그 전에 멈춰서 가장 공학적인 질문을 던져봅시다. 왜 귀찮게 할까요? 프로그램이 해당 작업을 수행하고 지원, 유지 관리 및 진화가 쉽다면 애초에 예측 가능한 순차 실행의 아늑한 불변성을 포기해야 하는 이유는 무엇입니까?


두 가지 대답이 보입니다. 첫 번째는 성능 입니다. 프로그램을 두 배 빠르게 또는 유사하게 실행할 수 있다면(하드웨어의 절반이 필요함) 이는 엔지니어링 성과입니다. 동일한 양의 계산 리소스를 사용하면 2배(또는 3, 4, 5, 10배)의 데이터를 처리할 수 있습니다. 이는 동일한 프로그램의 완전히 새로운 응용 프로그램을 열 수 있습니다. 서버 대신 주머니에 있는 휴대폰에서 실행될 수도 있습니다. 때로는 영리한 알고리즘을 적용하거나 보다 성능이 좋은 언어로 다시 작성하여 속도를 높일 수 있습니다. 이것이 우리가 탐색할 첫 번째 옵션입니다. 그렇습니다. 하지만 한계가 있습니다. 아키텍처는 거의 항상 구현을 능가합니다. 최근에는 무어의 법칙이 잘 이루어지지 않고 있으며, 단일 CPU의 성능은 느리게 성장하고 있으며, RAM 성능(주로 대기 시간)은 뒤쳐져 있습니다. 그래서 자연스럽게 엔지니어들은 다른 옵션을 찾기 시작했습니다.


두 번째 고려 사항은 신뢰성 입니다. 자연은 혼란스럽습니다. 열역학 제2법칙은 정확하고 순차적이며 반복 가능한 모든 것에 끊임없이 작용합니다. 비트가 뒤집히고, 재료가 저하되고, 전원이 꺼지고, 전선이 끊어져 프로그램이 실행되지 않습니다. 순차적이고 반복 가능한 추상화를 유지하는 것은 어려운 일이 됩니다. 우리 프로그램이 소프트웨어 및 하드웨어 오류보다 오래 지속될 수 있다면 경쟁적 비즈니스 이점이 있는 서비스를 제공할 수 있습니다. 이는 우리가 해결할 수 있는 또 다른 엔지니어링 작업입니다.


목표를 달성하면 직렬화되지 않은 접근 방식으로 실험을 시작할 수 있습니다.


실행 스레드

이 의사 코드 덩어리를 살펴보겠습니다.


```

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}”)

우리는 코드를 위에서 아래로 읽을 수 있고 `find_pois` 함수가 `get_my_location` 다음에 호출될 것이라고 합리적으로 가정할 수 있습니다. 그리고 다음 POI를 가져온 후 첫 번째 POI의 좌표를 가져오고 반환합니다. 이러한 가정은 정확하며 정신적 모델을 구축하고 프로그램에 대한 추론을 가능하게 합니다.


코드를 비순차적으로 실행하도록 만들 수 있다고 상상해 봅시다. 이를 구문적으로 수행할 수 있는 방법은 여러 가지가 있습니다. 명령문 재정렬(현대 컴파일러와 CPU가 수행하는 작업)에 대한 실험을 건너뛰고 새로운 함수 실행 체계를 표현할 수 있도록 언어를 확장하겠습니다. 동시에 또는 병행하여 다른 기능에 관해서. 바꿔 말하면, 여러 실행 스레드를 도입해야 합니다. 우리의 프로그램 기능은 특정 환경(OS에 의해 제작 및 유지 관리됨)에서 실행됩니다. 현재 우리는 주소 지정 가능한 가상 메모리와 스레드(CPU에서 실행할 수 있는 스케줄링 단위)에 관심이 있습니다.


스레드는 POSIX 스레드, 녹색 스레드, 코루틴, 고루틴 등 다양한 형태로 제공됩니다. 세부 사항은 크게 다르지만 실행 가능한 것으로 요약됩니다. 여러 기능이 동시에 실행될 수 있는 경우 각 기능에는 자체 예약 단위가 필요합니다. 즉, 멀티스레딩은 하나가 아닌 여러 개의 실행 스레드를 사용하는 것에서 비롯되었습니다. 일부 환경(MPI)과 언어는 암시적으로 스레드를 생성할 수 있지만 일반적으로 C의 `pthread_create`, Python의 `threading` 모듈 클래스 또는 Go의 간단한 `go` 문을 사용하여 이를 명시적으로 수행해야 합니다. 몇 가지 예방 조치를 취하면 동일한 코드를 대부분 병렬로 실행할 수 있습니다.


 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}”)


우리는 성능 목표를 달성했습니다. 우리 프로그램은 여러 CPU에서 실행될 수 있으며 코어 수가 증가하고 더 빠르게 완료됨에 따라 확장될 수 있습니다. 우리가 물어봐야 할 다음 엔지니어링 질문: 비용은 얼마입니까?

우리는 의도적으로 직렬화되고 예측 가능한 실행을 포기했습니다. 있다 편견 없음 함수 + 시점과 데이터 사이. 각 시점에서 실행 중인 함수와 해당 데이터 사이에는 항상 단일 매핑이 있습니다.


이제 여러 기능이 데이터를 동시에 사용합니다.


다음 결과는 이번에는 한 기능이 다른 기능보다 먼저 완료될 수 있고, 다음번에는 다른 방식이 될 수 있다는 것입니다. 이 새로운 실행 방식은 데이터 경합으로 이어집니다. 동시 함수가 데이터와 함께 작동할 때 이는 데이터에 적용되는 작업 순서가 정의되지 않음을 의미합니다. 우리는 데이터 경쟁에 직면하기 시작하고 다음을 사용하여 이를 처리하는 방법을 배웁니다.

  • 중요 섹션: 뮤텍스(및 스핀록)
  • 잠금 없는 알고리즘(가장 간단한 형태는 위의 스니펫에 있음)
  • 인종 감지 도구


이 시점에서 우리는 적어도 두 가지 사실을 발견하게 됩니다. 첫째, 데이터에 접근하는 방법에는 여러 가지가 있습니다. 일부 데이터는 현지의 (예: 함수 범위 변수) 오직 우리만이 볼 수 있고 접근할 수 있으므로 항상 우리가 놔두었던 상태에 있습니다. 그러나 일부 데이터는 공유되거나 원격 . 이는 여전히 프로세스 메모리에 있지만 특별한 방법을 사용하여 액세스하면 동기화되지 않을 수 있습니다. 어떤 경우에는 데이터 경합을 피하기 위해 이를 로컬 메모리에 복사합니다. 이것이 == 이유입니다. .클론() ==Rust에서는 인기가 있습니다.


이러한 추론을 계속하면 스레드 로컬 저장소와 같은 다른 기술이 자연스럽게 등장합니다. 우리는 프로그래밍 도구 벨트에서 새로운 장치를 획득하여 소프트웨어 구축을 통해 달성할 수 있는 것을 확장했습니다.


그러나 우리가 여전히 의지할 수 있는 불변성이 있습니다. 스레드에서 공유(원격) 데이터를 얻으려면 항상 데이터를 얻습니다. 일부 메모리 청크를 사용할 수 없는 상황은 없습니다. OS는 지원하는 물리적 메모리 영역이 오작동하는 경우 프로세스를 종료하여 모든 참가자 (스레드)를 종료합니다. 뮤텍스를 잠근 경우 "우리" 스레드에도 동일하게 적용됩니다. 잠금을 잃을 수 있는 방법은 없으며 수행 중인 작업을 즉시 중지해야 합니다. 우리는 모든 참가자가 죽었거나 살아 있다는 이 불변성(OS 및 최신 하드웨어에 의해 시행됨)을 신뢰할 수 있습니다. 모두 운명을 공유합니다 . 프로세스(OOM), OS(커널 버그) 또는 하드웨어에 문제가 발생하면 모든 스레드는 외부 부작용 없이 함께 존재하지 않게 됩니다.


프로세스 발명

주목해야 할 중요한 사항 중 하나입니다. 스레드를 도입하여 어떻게 첫 번째 단계를 만들 수 있었나요? 우리는 헤어지고 갈라졌습니다. 하나의 일정 단위를 사용하는 대신 여러 개의 일정 단위를 도입했습니다. 이 비공유 접근 방식을 계속 적용하고 어떻게 진행되는지 살펴보겠습니다. 이번에는 프로세스 가상 메모리를 복사합니다. 이를 프로세스 생성 이라고 합니다. 프로그램의 다른 인스턴스를 실행하거나 다른 기존 유틸리티를 시작할 수 있습니다. 이는 다음에 대한 훌륭한 접근 방식입니다.

  • 엄격한 경계로 다른 코드 재사용
  • 신뢰할 수 없는 코드를 실행하여 자체 메모리에서 격리


거의 다 == 최신 브라우저 ==이 방법으로 작동하면 인터넷에서 다운로드한 신뢰할 수 없는 Javascript 실행 코드를 실행할 수 있으며 전체 응용 프로그램을 중단하지 않고 탭을 닫을 때 안정적으로 종료할 수 있습니다.

이것은 불변의 공유된 운명을 포기하고 가상 메모리를 공유 해제 하고 복사본을 만들어 우리가 발견한 또 다른 실행 체제입니다. 사본은 무료가 아닙니다.

  • OS는 메모리 관련 데이터 구조를 관리해야 합니다(가상 -> 물리적 매핑을 유지하기 위해).
  • 일부 비트가 공유될 수 있으므로 프로세스는 추가 메모리를 소비합니다.



속보

여기서 멈춰야 하는 이유는 무엇입니까? 프로그램을 복사하고 배포 할 수 있는 다른 항목을 살펴보겠습니다. 그런데 애초에 왜 배포해야 할까요? 많은 경우 단일 기계를 사용하여 당면한 작업을 해결할 수 있습니다.


우리는 분산시켜야 해 같은 운명에서 벗어나기 위해 기본 레이어에서 발생하는 피할 수 없는 문제에 따라 소프트웨어가 중단되도록 하는 것입니다.


몇 가지 예를 들면 다음과 같습니다.

  • OS 업그레이드: 때때로 컴퓨터를 재부팅해야 합니다.

  • 하드웨어 오류: 우리가 원하는 것보다 더 자주 발생합니다.

  • 외부 오류: 정전 및 네트워크 중단이 문제입니다.


OS를 복사하면 이를 가상 머신 이라고 부르며 물리적 머신에서 고객의 프로그램을 실행하고 그 위에 거대한 클라우드 비즈니스를 구축할 수 있습니다. 두 대 이상의 컴퓨터를 사용하여 각각에서 프로그램을 실행하면 프로그램은 하드웨어 오류에도 불구하고 연중무휴 서비스를 제공하고 경쟁 우위를 확보할 수 있습니다. 오래 전에 대기업은 훨씬 더 나아갔고 이제 인터넷 거대 기업은 다양한 데이터 센터, 심지어 대륙에서 복사본을 실행하여 태풍이나 단순한 정전에도 프로그램을 복원할 수 있게 만듭니다.


그러나 이러한 독립에는 대가가 따릅니다. 기존의 불변성은 적용되지 않으며 우리는 스스로 책임을 져야 합니다. 걱정하지 마세요. 우리가 첫 번째는 아닙니다. 우리를 도와줄 기술, 도구, 서비스가 많이 있습니다.


테이크아웃

우리는 시스템과 각각의 실행 체제에 대해 추론할 수 있는 능력을 얻었습니다. 모든 대규모 확장 시스템 내에서 대부분의 부분은 친숙한 순차 및 상태 비저장이며, 많은 구성 요소는 실제로 분산된 일부 부분의 혼합으로 함께 유지되는 메모리 유형 및 계층 구조로 멀티 스레드로 구성됩니다.


목표는 우리가 현재 어디에 있는지, 무엇이 불변성을 보유하고 있는지 구별하고 그에 따라 작동(수정/설계)할 수 있는 것입니다. 우리는 "알려지지 않은 미지"를 "알려진 미지"로 변환하는 기본적인 추론을 강조했습니다. 가볍게 여기지 마십시오. 이는 상당한 진전입니다.