효율성은 회복 탄력성과 상충한다
원문은 Nelson Elhage님이 에 게재했습니다. 이 블로그 구독하기
서버의 CPU 사용률에서 “적정한” 수준이란 어느 정도일까? 잘 설계되고 잘 운영되는 서비스의 모니터링 대시보드를 본다면, 하루나 이틀 평균으로 어느 정도의 CPU 사용률을 기대해야 할까?
매우 포괄적인 질문이고, 하나의 정답이 있어야 하는지도 분명치 않다. 그럼에도 오랫동안 나는 높을수록 무조건 좋다고 믿어 왔다. 가능한 한 100%에 가깝게 끌어올려야 한다는 것이다. 왜 그럴까? 100%에 못 미친다는 건 하드웨어 용량이 놀고 있다는 뜻이고, 곧 자원을 낭비하고 있다는 의미이기 때문이다. 서비스가 CPU를 한계까지 쓰지 않고 있다면 더 작은 인스턴스로 옮기거나, 그 노드에서 다른 작업을 함께 돌릴 수도 있을 것이다.
그런데 이런 단순한 직관은 대개 그리 들어맞지 않는다.
이상적인 상태를 달성해 서비스가 100%에 가까운 사용률로 돌아간다고 가정해 보자. 그런데 갑자기 서비스가 입소문을 타며 예상치 못한 트래픽 급증이 발생하면 어떻게 될까? 혹은 요청마다 CPU를 조금 더 쓰는 새 기능을 배포하고 싶다면 어떻게 될까?
이미 사용률이 100%인 상태에서 부하를 키우는 일이 생기면 곤란해진다. 100%로 돌리고 있다는 것은 추가 부하를 흡수할 여지가 전혀 없다는 뜻이다. 결국 어떤 형태로든 성능이 저하되거나, 허겁지겁 긴급 용량을 추가해야 하거나, 혹은 둘 다 겪게 될 것이다.
이 단순한 예시는 매우 일반적인 현상의 한 사례다. 효율성을 높이려는 개선은 종종 회복 탄력성과 상충하며, 시스템을 더 최적화할수록 이런 상충 관계는 대체로 더 심해진다.
어느 지점을 넘어서면 시스템을 더 효율적으로 만든다는 것은 곧 회복 탄력성을 떨어뜨린다는 뜻이 되고, 반대로 견고함을 더하면 효율성이 떨어지는 경향이 있다(적어도 단기적으로는). 그렇다고 모든 경우에 윈윈이 없다는 얘기는 아니다. 때로는 파레토 프런티어 자체를 바깥쪽으로 밀어낼 수 있다. “어리석은” 성능 버그를 고치는 것이 그런 효과를 내기도 한다. 하지만 일정 수준 이상의 노력을 기울이고 나면 결국 트레이드오프를 감수해야 한다.
여기서 말하는 “회복 탄력성”이 단순히 “안정성”이나 “서비스를 계속 유지하는 능력”보다 훨씬 넓은 개념이라는 점을 짚고 넘어가야 한다. 내가 말하는 것은 “변화를 흡수하거나 대응하는 능력”이라는 보다 일반적인 개념이다. 버그나 장애 같은 변화는 물론, 제품 요구사항의 변화, 시장의 변화, 조직이나 팀 구성의 변화 등 온갖 종류의 변화를 포함한다.
이 트레이드오프의 사례들
이 트레이드오프는 용량 계획 beyond 영역을 넘어선다. 거의 모든 기술 시스템이나 조직의 거의 모든 수준에 적용된다. 내가 관찰한 다른 사례들은 다음과 같다.
이중화
하나의 서비스를 여러 인스턴스로 실행하고, 특정 인스턴스에 장애가 생기면 로드 밸런서가 트래픽을 다른 인스턴스로 투명하게 옮기도록 구성하는 것은 매우 흔한 방식이다. 수준 높은 조직에서는 이 패턴을 데이터센터 전체 단위로 적용해, 데이터센터 하나가 통째로 장애가 나도 그 부하를 다른 데이터센터로 옮길 수 있는 아키텍처를 갖추기도 한다.
이 구조가 동작하려면 각 인스턴스가 장애로 넘어온 추가 부하를 흡수할 만큼의 여유 용량을 가지고 있어야 한다. 장애가 없는 정상 상태에서는 그 용량이 그대로 유휴 상태로 남아 있어야 하고, 아무리 잘 활용해도 언제든 중단할 수 있는 낮은 우선순위 작업을 처리하는 정도에 그친다. 이중화를 더 많이 원할수록 정상 상태에서 유휴로 보유해야 하는 용량도 더 커진다.
최적화
정교한 성능 최적화는 대개 문제 영역의 특정한 속성이나 구조를 활용하는 방식으로 동작한다. 불변 조건을 자료구조와 코드 구조에 녹여 넣음으로써 큰 성능 향상을 얻을 수 있다. 하지만 특정 가정에 깊게 의존할수록 그 가정을 바꾸기가 어려워지고, 그 결과 고도로 최적화된 코드는 기능을 발전시키거나 추가하기가 훨씬 더 어려워지는 경우가 많다.
구체적인 예로, Sorbet에 대한 회고에서 나는 타입 추론을 로컬 전용이자 단일 패스로 하기로 결정하고 그 가정을 코드와 자료구조에 녹여 넣은 과정을 이야기한 적이 있다. 이 선택은 상당한 효율성 향상을 가져왔지만, 어떤 의미에서는 시스템을 더 취약하게 만들었다. 경쟁 타입 시스템들이 제공하는 많은 기능이 그 가정 때문에 Sorbet 코드베이스에서는 구현이 불가능하거나 지나치게 어려워졌기 때문이다. 나는 그 프로젝트에서는 여전히 올바른 선택이었다고 확신하지만, 그 트레이드오프는 인정할 가치가 있다.
Hillel Wayne은 이와 비슷한 성질을 “똑똑한 코드(clever code)”라고 부르며 다음과 같이 정의한다.
문제에 대한 지식을 활용하는 코드
그 역시 (이런 의미의) 똑똑한 코드는 효율적인 경향이 있지만 때로는 취약하다고 말한다.
직렬화 포맷
“메모리에 있는 structs를 그대로 디스크에 복사하는” 것보다 더 효율적인 직렬화 포맷을 찾기는 어렵다. 사실상 코드가 거의 필요 없고 데이터를 저장하거나 불러오는 데 드는 직렬화 비용도 거의 0에 가깝기 때문이다.
하지만 이는 시스템을 취약하게 만든다. 필드 하나를 추가하는 데도 데이터를 다시 쓰거나 별도의 특수 처리가 필요해진다. 엔디안이나 워드 크기가 다른 머신 간에 데이터를 공유하는 것도 난제가 된다.
반대로 모든 데이터를 JSON이나 이와 유사한 범용 컨테이너로 작성하면 새로운 필드를 추가하거나 새로운 모듈끼리 기존 코드에 영향을 주지 않고 소통하는 데 끝없는 유연성을 얻을 수 있지만, 네트워크 전송과 데이터 스트림을 직렬화·역직렬화하는 CPU 작업에서 상당한 오버헤드를 치러야 한다.
분산 시스템
내가 가장 좋아하는 시스템 논문 중 하나는 COST 논문이다. 이 논문은 여러 빅데이터 플랫폼을 검토하면서, 그중 다수가 가용 하드웨어에 대해 (거의) 선형으로 확장되는 바람직한 특성을 갖지만, 그 대가로 튜닝된 단일 스레드 구현보다 터무니없이 비효율적이라는 점을 지적한다.
나는 이것이 흔한 트레이드오프라는 것을 알게 됐다. 분산 컴퓨팅 프레임워크는 규모를 키워 거의 모든 워크로드에 대응할 수 있다는 점에서 유연하고 회복 탄력성이 있다. 누군가 비효율적인 코드를 배포해도 스케일 아웃으로 감당할 수 있고, 하드웨어 장애도 투명하게 처리한다. 더 많은 데이터를 처리해야 하는가? 그냥 하드웨어를 추가하면 된다(대개는 일종의 오토스케일링을 이용해 투명하게).
반면 정성껏 작성된 단일 노드 솔루션은 더 빠른 경향이 있지만(때로는 10~100배 더 빠르다!), 훨씬 더 취약하다. 데이터셋이 더 이상 단일 노드에 들어가지 않거나, 10배 더 비용이 많이 드는 분석을 수행해야 하거나, 팀의 새로운 엔지니어가 무심코 촘촘한 내부 루프 안에 느린 코드를 커밋하면 전체 시스템이 무너지거나 제 역할을 하지 못할 수 있다.
작은 팀과 큰 조직
작은 팀 — 심지어 1인 “팀”을 포함해 — 은 엄청나게 생산적이고 효율적일 수 있다. 팀이 작을수록 커뮤니케이션 오버헤드가 적고, 풍부한 공유 컨텍스트를 모든 엔지니어의 머릿속에 유지하기도 더 쉽다. 문서를 작성할 필요도, 변경 사항을 두고 소통할 필요도, 새로운 멤버를 온보딩하고 교육하는 데 들이는 시간도 더 적다. 작은 팀은 큰 팀보다 “신중하게 생각하고 열심히 시도한다” 같은 전략만으로도 훨씬 더 멀리 나아갈 수 있으며, 린터나 방어적인 추상화 설계 같은 도구에 의존할 필요도 대개 더 적다.
조건이 잘 맞으면, 세심한 설계와 경험 많은 엔지니어를 갖춘 작은 팀이 자신보다 10배 큰 팀의 생산량을 대략 따라잡는 것도 때로는 가능하다. 효율성 면에서 엄청난 향상이다!
하지만 작은 팀은 훨씬 더 취약하고, 조직이나 기술 환경, 프로젝트의 비즈니스 요구사항 변화에 대한 회복 탄력성이 떨어진다. 4인 팀에서 한 명이 떠나면 대역폭이 25% 줄어든다. 더 나쁜 것은, 그 팀은 새로운 멤버를 채용하고 온보딩하는 경험이 거의 없고, 방대한 지식과 문서가 남은 멤버들의 머릿속에만 존재한다는 점이다.
마찬가지로 사업 방향이 바뀌거나 신제품을 출시하면서 팀의 시스템에 훨씬 더 많은 기능이나 다른 개발이 요구되면, 팀의 지원 역량을 비교적 쉽게 초과하게 되고, 같은 이유로 팀을 빠르게 키우는 것도 난제가 된다.
자동화 대 인간 프로세스
일반적으로 어떤 작업을 인간이 수동으로 수행하는 것보다 기계가 수행하는 것이 더 효율적이다. 더 저렴하고, 더 빠르며, 종종 더 안정적이다.
하지만 인간은 끝없이 적응력이 뛰어난 반면, 기계(물리적인 기계와 소프트웨어 시스템 모두)는 훨씬 더 취약하고 정해진 방식에 고착되어 있다. 사람이 개입된 시스템은 상황 변화나 예기치 않은 사건에 즉석에서 대응할 수 있는 선택지가 훨씬 더 많다.
같은 인력을 그대로 유지하면서 작업 일부를 가속하기 위해 자동화를 보강하더라도, 우리는 자동화 의존성이라는 위험을 감수하게 된다. 인간이 자동화에 지나치게 의존하고 부적절하게 신뢰하게 되거나, 자동화 없이는 기능하는 능력이 퇴화되어 필요할 때 더 이상 적절히 “수동”으로 개입할 수 없게 되는 상황이다.
슬랙
이 구체적인 관찰 중 많은 부분은 사실 슬랙(여유)(소프트웨어 제품이 아니다)에 대한 관찰이다.
건강한 수준의 여유를 가진 시스템은 — 적어도 단기적으로 단순하게 분석하면 — 정의상 비효율적일 수밖에 없다. 그 여유는 “유휴” 상태로 남아 있는 시간이나 자원이며, 달리 보면 생산적인 output을 내는 데 쓸 수도 있는 것이기 때문이다.
하지만 더 넓게 보면 그 여유가 바로 회복 탄력성의 핵심이다. 시스템에 “완충”이 있으면 작은 장애나 사고를 감당할 수 있다. 개발자나 운영자는 그 여유를 활용해 예기치 않은 부하를 처리하거나, 문제가 치명적이 되거나 외부에 드러나기 전에 근본적인 이슈를 해결하기 위해 개입할 수 있다.
여유가 전혀 없는 시스템은 동작하는 동안에는 효율적이지만 취약하며, 평소 운영 방식에 조금이라도 변화가 생기면 빠르게 무너진다.
결론
나는 효율성과 신뢰성이 서로 충돌하고, 트레이드오프를 이루거나 적어도 서로 반대 방향으로 압력을 가하는 구체적인 사례들을 여럿 짚어 보려 했다. 부디 이것이 광범위한 현상이며, 엄밀한 트레이드오프가 아직 존재하지 않는 경우에도 두 가치가 서로 대립하며 각기 다른 결정을 시사하는 경향이 있다는 점을 설득할 수 있었기를 바란다.
안타깝게도 이 관찰만으로는 특정 시스템에 대해 무엇을 해야 할지 좀처럼 알 수 없다. 어떤 시스템을 보고 1차 분석에서 입력 자원을 비효율적으로 쓰고 있다는 결론이 나왔다 해도, 더 자세히 들여다보지 않고서는 그것이 기능 부전과 잘못된 설계 결정의 소굴인지, 아니면 그 피상적인 비효율성이 다가올 어떤 변화든 견뎌낼 수 있게 해주는 방대한 이중화와 유연성, 여유의 비축을 뒷받침하고 있는 것인지 알 수 없다. 더 면밀히 살펴봐야 하며, 거의 항상 해당 팀과 해당 문제에 대한 도메인 전문성이 필요하다.
게다가 때로는 공짜 점심이 존재하기도 한다. 일부 설계 선택이나 결정은 우리를 파레토 프런티어를 따라 이동시키는 것이 아니라 프런티어 자체를 바깥쪽으로 밀어내기도 한다. 잘 최적화된 시스템에서는 드물 수 있지만, 그 가능성을 무시할 수는 없다. 그리고 많은 시스템은 아직 그 정도로 잘 최적화되지도 않았다!
게다가 설계 공간에서 최적의 지점은 시스템에 따라 다르다. 때로는 안정성이나 회복 탄력성이 결정적으로 중요해서, 1차원적인 큰 비효율을 감수하는 것이 옳다. 하지만 때로는 극단적인 효율성이 올바른 목표가 되기도 한다. 마진이 너무 얇아 그것만이 유일한 선택지인 경우도 있고, 우리 도메인과 시스템에 대한 요구사항이 충분히 안정적이어서 어떤 종류의 급격한 변화도 겪지 않을 것이라고 확신하는 경우도 있기 때문이다.
그래서 결국 내가 할 수 있는 일은 대부분, 엔지니어이자 설계자이며 시스템을 관찰하는 사람으로서 우리가 이 트레이드오프와 그 함의를 인지한 채 일하도록 촉구하는 것뿐이다. 어떤 시스템이 낭비적이고 비효율적이라고 비난할 때, 그 “낭비”가 무엇을 사고 있는 것인지 잠시 멈춰 물어볼 가치가 있다. 시스템을 최적화하려 할 때, 현재 시스템에서 관절과 유연성이 어디에 있는지, 그중 무엇이 핵심적인지 이해하기 위해 잠시 멈추고, 그것을 최대한 보존하려 노력해야 한다. 시스템이나 팀, 조직에 효율성을 요구하는 지표나 목표를 설정할 때, 상쇄하는 압력이 없다면 우리는 아마도 시스템이 더 취약하고 부서지기 쉬워지도록 요구하고 있다는 점도 인지해야 한다.
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기