필자는 고양이를 키우는 사람으로서, 멋있게 보이기 위한 핵심은 일부러 실수를 하는 척 하는 것이라는 점을 배웠다. 또는 흔히 교사가 학생들을 상대로 "자, 내가 일부러 실수한 부분이 어떤 것인지 찾았니?"라고 말하는 것이다.
Pseudo 프로젝트 내내, 흥미를 끄는 잘못된 출발과 이상한 버그, 그 밖에 다른 학습 경험이 많이 있었다. 그 중 어떤 것은 기묘한 코너 케이스(정상 조작 요인들의 범위 밖에서만 발생하는 문제나 상황)이고, 어떤 것은 도저히 올바로 작동할 것이라 믿을 수 없는 것이다. 필자가 결국 컴파일하지도 않을 코드를 수백 개나 열거하지는 않을 것이므로 너무 걱정하진 말자. 필자는 간단한 오자를 하나의 예술 형식으로 격상시켰다.
Pseudo의 최초 작업 릴리스에서는 이름 바꾸기 조작이 실제로 작동하지 않았다. Pseudo 디먼이 추가 액세스에서 해당 데이터베이스 항목을 자동으로 수정했기 때문에 실제로는 아무런 결과로 나오지 않았다.
SQLite SQL 엔진의 한계 중 하나는 LIKE 비교를 위해 인덱스를 사용할 수 없다는 점이다. 디렉토리의 이름을 바꿀 때는
디렉토리 내에 있는 파일의 이름도 바꾸고 싶다는 것이다.
그래서 /foo 디렉토리의 이름을 /bar로 바꿀 경우 /foo/로
시작하는 모든 경로에서 /foo라는 문자열을 /bar라는 문자열로 대체하려고 한다.
하지만, SQL 절 (path LIKE ? || '/')를 사용하여 이 작업을 수행하는 경우 인덱스를 사용할 수 없으므로 끔찍할
정도로 속도가 느리다. 그래서 이리저리 찾아본 결과, 필자는 기쁘게도 (path > (? || '/') AND path
< (? || '0')이라는 임시 해결책을 찾았지만, 그건 성마른 결정이었다.
ASCII 시스템으로 가정할 때, 이것은 정확히 path/ 뒤에 무엇이든 붙은 것과 똑같았지만, 그건 단지 관계형 연산자에
불과했으므로 인덱스를 사용했다. 그래서 작은 파일 시스템에서조차 대략 20,000배에 달하는 속도 향상 효과를 보았다.
하지만, 이렇게 변환하는 동안 아주 작은 실수를 하나 저질렀다. 그 결과, 매개변수 바인딩의 순서를 바꾸고 말았고, 그에 따라
/foo를 /bar로 이름을 바꾼다는 게 원래 목적이었지만 결국 /foo/로
시작하는 모든 경로에서 /bar를 /foo로 이름을 바꾸는 결과가 되고 말았다. 결국 아무 것도 한 게 없지만,
최소한 빠르게 아무 것도 하지 않은 셈이 되었다.
Pseudo의 편집증 및 온전성 검사 덕분에, 이런 실수로 인해 실제로 믿을 수 없는 결과가 나오지는 않았고 다만 로그 파일에 수많은 경고가 표시되었다.
Pseudo에 관한 초기의 가정은 모든 조작이 서버에서 직렬화되었기 때문에 직렬화 문제점이 없고 주어진 클라이언트에서 두 개의 조작이 연속적으로 손상될 수는 없을 것이라는 점이었다. "640K면 누구에게든 충분할 것"이라는 레벨에서 볼 때 이는 전혀 그렇지 않지만, 확실히 심각한 실수였다.
원래 설계에서는 기본 조작이 시도된 다음, 성공한 경우 서버에 기록되었다. 단일 프로그램에 대해서는 항상 올바로 작동했다. 하지만, 여러 프로그램에서는 경합 상태가 발생할 가능성이 있었다.
프로세스 A에서는 inode 번호가 12345인 임시 파일을 작성한다. 그런 다음, 프로세스 A는 이 임시 파일을 제거한다. 임시 파일이 제거된 후, 프로세스 B에서는 inode 번호 12345를 다시 사용하는 파일을 새로 작성한다. 하지만, 새로 파일이 작성되면 pseudo 디먼은 프로세스 A에서의 링크 끊기 메시지를 보기 전에 프로세스 B에서의 작성 메시지를 본다. 자, 어떤 일이 일어날까?
B로부터 작성 메시지를 수신할 때, pseudo 디먼은 inode 번호가 같은 데이터베이스(A의 임시 파일)에 오래된 항목이 있음을 알리고, 종속성을 로그에 기록하고 해당 항목을 제거한다. 그런 다음, 데이터베이스 항목을 새로 작성한다. 하지만, 상황은 더 나빠진다. 삭제 메시지가 수신될 때, 디먼은 inode 번호가 같은 데이터베이스(B의 파일)에 오래된 항목이 있음을 알린다. 디먼은 의사 항목을 제거한 다음, 계속 진행하여 데이터베이스에서 A의 임시 파일도 제거하려 한다. 이 작업이 끝나면 B의 파일이 데이터베이스에 더 이상 기록되지 않는다.
이 문제를 수정하기 위한 필자의 첫 시도는 참담한 실패로 돌아갔다. 파일에 대한 데이터베이스 항목을 리턴하도록 UNLINK
조작을 수정하고 클라이언트가 UNLINK 메시지를 보내도록 한 다음, 기본 시스템 호출에 실패한 경우 파일을 다시 연결하도록 했다. 이
방법으로 경쟁 조건은 제거되었다.
하지만, 이로 인해 훨씬 더 나쁜 오류 모드가 발생했다. 즉, 파일이 있는 디렉토리에서 rmdir(2)이 실행되어
모든 파일에 대한 데이터베이스 항목이 삭제되었다(디렉토리를 제거한다는 것은 그 안의 모든 컨텐츠를 제거한다는 의미이므로).
파일에 "deleting(삭제 중)" 플래그를 추가하고
MAY_UNLINK,
DID_UNLINK 및
CANCEL_UNLINK 메시지를 추가하여 이 문제를 마침내 수정했다.
데이터베이스는 이런 메시지를 통해 어떤 파일이 곧 삭제될 것으로 판단된다는 점을 기록할 수 있으므로, 그에 대한 작성 메시지에서 오류가
생성되지 않는다.
그러면 파일에 deleting 플래그가 설정된 경우에만 DID_UNLINK 메시지가 파일을 삭제한다. 따라서 이 파일이
최종적으로 삭제된 것으로 생각한다.
디렉토리의 이름을 바꾸어 그 디렉토리에 있는 파일이 잊혀지도록 할 때 불가사의한 문제점이 발생했다. 이 문제점은 뚜렷이 구별되는 세 가지 버그의 결과였다. 이 세 가지 버그 중 어느 것이든 수정해도 문제가 되는 작동이 정정되었다.
디렉토리의 이름을 바꿀 때, pseudo는 해당 디렉토리가 pseudo 데이터베이스에 이미 알려진 디렉토리인지 확인하고, 만약 알려지지 않은 디렉토리라면 이름 바꾸기 조작이 정상적으로 이루어질 수 있도록 그 이름의 디렉토리를 작성한다(따라서 그 디렉토리에 포함되었고 pseudo에 이미 알려진 파일의 이름을 전부 바꿈). 예를 들어, pseudo 환경 외부에서 디렉토리를 작성한 다음 pseudo 환경에서 실행하는 동안 그 디렉토리 내에서 파일을 작성한 경우 이런 일이 발생할 수 있다.
문제점은 세 가지 선택 사항의 조합에서 생겼다. 첫 번째는 파일을 연결할 때 pseudo가 같은 이름을 가진 기존 파일의 링크를 끊는 데 도움이 되었다는 점이었다. 두 번째는 디렉토리의 링크를 끊을 때 pseudo가 그 디렉토리의 컨텐츠에 대한 링크를 끊는 데 도움이 된다는 점이었다. 이들을 이름 바꾸기에서의 암시적 링크와 결합한다는 것은 데이터베이스에 이전에 기록되지 않은 디렉토리의 이름을 바꿀 때 pseudo가 데이터베이스에 기록되었던 해당 디렉토리 내부의 파일에 대한 모든 항목을 손실하게 됨을 의미한다.
이것만으로는 우리의 빌드 시스템에서 논제가 되지 않았을 것이다. 문제를 촉발한 것은 rename(3)이 파일 시스템
전체에 걸쳐 어떤 파일의 이름을 바꾸는 경우에 처리 방법을 개선하려는 필자의 노력에서 완전히 불가해한 의사결정이었다. 사실 이런 일은 일어날 수 없지만,
어떤 이유로 필자는 지원 기능을 구현하려 했을 뿐 아니라, 이름 바꾸기 랩퍼가 결국은 항상 이름을 바꾸기 전에 데이터베이스에서 이전 이름을 연결하도록 하는
방식으로 매우 나쁜 방법을 택했다. 그에 따라, 파일이 있는 디렉토리를 이동할 때 해당 파일이 항상 데이터베이스에서 제거되는 결과를 초래했다.
우리는 이런 중대한 오류를 수정했다. LINK 조작에 의해 완료되는 암시적 링크 끊기로 그 안에 있는 것처럼 보이는
파일이 아니라, 이름이 지정된 파일만 제거된다. 이름 바꾸기 조작에서는 더 이상 허위로 링크를 작성하지 않는다. 그 결과, 디렉토리의 이름을 바꾸어도 더 이상
아무 것도 망치지 않는다.
에지 케이스(극한의 조작 매개변수(최대 또는 최소)에서만 발생하는 문제점이나 상황)와 코너 케이스에 대해 들어본 적이 있을 것이다. 필자의 소프트웨어 개발 경력을 통틀어, 이번이 직접 경험한 유일한 5차원 버텍스(vertex) 케이스였다.
"deleting" 플래그가 추가되었을 때, 이는 IPC를 위해 pseudo에서 사용하는 데이터 구조에 변화가 있다는 의미였다. IPC 메시지를 한 번도 버전 관리를 하지 않았기 때문에, 클라이언트와 서버가 자신이 사용 중인 IPC 메시지의 버전에 동의하지 않는 것이 이론적으로는 가능하다. 하지만, 그런 일은 결코 일어나지 않는다. 우리의 빌드 시스템은 항상 그와 동시에 컴포넌트를 다시 빌드하도록 하기 때문이다.
그래도 빌드의 특정 지점에서 단일 프로그램이 때때로 실패하는 매우 이상한 문제점이 있었다. 여기서 말하는 "실패"란 "디먼으로부터의 응답을 기다리며 무기한 정지"되는 것을 뜻한다. 그 동안에 디먼은 소켓으로부터의 입력을 기다리고 있었다.
Pseudo 프로토콜에 대해 좀 더 자세히 알아보는 것이 좋을 것이다. 클라이언트가 시작될 때 클라이언트가 가장 먼저 하는 일은 PSEUDO_MSG_PING
메시지를 서버로 보내는 것이다. 그 메시지에 있는 정보로는 클라이언트의 PID, 클라이언트 2진 이름, 그 클라이언트에서 발생하는 이벤트 로깅에 사용하기 위한
"tag" 메시지(선택사항)가 포함된다. 태그 메시지가 없다면 그냥 생략된 것이다. (이름과 태그는 "path"로 전송되며, pathlen 필드에 길이가 표시된다.)
Ping 중에 정지가 발생 중이었다. 한 개발자의 머신에서 일시적으로만 정지가 발생했다. 하지만, 우리는 결국 이 이벤트를 추적해 들어갔다.
이때의 변경사항으로 인해 pseudo 메시지 구조의 길이가 4바이트 증가했다. 서버는 기본 구조 크기를 초과하는 부분을 읽는 데는 민첩하게 대응하지만, 처음에는 항상 전체를 읽는 것으로 가정할 뿐이다. (필자는 아직 이 버그를 수정하지 않았다.)
어떤 식으로든 기존 pseudo 클라이언트를 이용하여 길이가 4바이트 더 길어진 새로운 구조의 pseudo 디먼을 실행하도록 꾸밀 수 있다면, 예상했던 것처럼 많은 데이터를 가져오지 않을 것이다. 클라이언트는 경로 이름과 태그도 전송할 것이다. 따라서 실행 중인 실행 파일의 이름이 4자 미만(이 경우에는 sed였음)이고 설정된 태그가 없는 경우에만 문제가 된 실패가 발생할 수 있었다. 그래도 이전 pseudo 클라이언트와 새 pseudo 디먼은 어떻게 얻는 걸까?
이 빌드 시스템에서는 (Indir을 사용하여) 빌드 디렉토리에 symlink의 트리로 미러되어 사전 빌드되는 호스트 도구를 가질 수 있고, 그 다음에는 새로운 버전을 가져오도록 다시 빌드해야 하는 도구가 다시 빌드된다. 문제의 개발자에게는 미러된 pseudo 디먼과 클라이언트 라이브러리를 포함한 기존의 호스트 도구가 있었고, 프로젝트 디렉토리에 새 디먼과 새 클라이언트 라이브러리를 포함한 새로운 도구를 빌드했다.
해당 프로젝트 디렉토리를 가리키도록 LD_LIBRARY_PATH를 설정한 이후로, 우리는 새로운 라이브러리를 일관되게
선택했고 모두 제대로 작동했다. 그래도 작은 결함이 한 가지 있었다. 실행 파일에서 두 가지 방법으로 링커 검색 경로를 설정할 수 있다. 예상하는 대로
세련되고 익숙한 RUNPATH 설정이 사용된다. 하지만, 오래 전에 사용된 낯선 RPATH 설정은
LD_LIBRARY_PATH 앞에서 처리되는 이상한 특성이 있다. 문제의 2진 코드는 RPATH가
$ORIGIN/../lib:$ORIGIN/../lib64로 설정된 상태로 빌드되었다. $ORIGIN 매직 쿠키는 이 2진 코드가
있는 디렉토리로 확장된다.
도구가 symlink로 미러되었다고 말한 것을 기억하는가? $ORIGIN 쿠키의 처리는 symlink를 따른다. 따라서 이 특정
실행 파일을 실행할 때, 동적 링커는 결국 LD_LIBRARY_PATH가 아니라 라이브러리 디렉토리에서 사전 빌드된 코드가 있는지 찾으므로,
이전 pseudo 클라이언트 라이브러리를 가져오게 된다. 실행 파일 이름이 3자 미만이었기 때문에 크래시나 진단이 아니라 정지되는 결과를 낳았다.
이 버그를 재현하기 위해 필요한 것은 다음과 같다.
- 최소한 1주일 이상 지난 pseudo의 사전 빌드 버전
- 새 버전을 다시 빌드하는 소스 트리
- 다시 빌드할 필요가 없었던 사전 빌드된 트리에 있는 실행 파일
- ... 이름의 길이는 3자 이하여야 함
- ...
RPATH를 사용하여,$ORIGIN을 사용하는 라이브러리 검색 경로 지정
이를 추적하는 데는 시간이 걸렸다. 장시간의 수정에는 메시지의 버전 관리(현재 메시지에서는 결코 발생할 수 없는 표시기를 사용하는 것이
이상적임)와 다수의 다른 개선사항이 포함된다. 또한, 링크 경로를 나타내기 위해 RPATH 사용을 중지해야 하고, 이런 경로를
symlink로 연결하지 않고 2진 코드를 복사해야 할 수도 있다.
일부 최신 Linux 머신에서는 기존의 일반적인 /bin/cp로 복사된 파일이 잘못된 권한 비트로 끝나고 있었다. getxattr()/setxattr()
함수 패밀리를 사용하면 확장된 속성뿐 아니라 POSIX 모드를 쿼리하거나 설정할 수 있는 것으로 드러난다. 한 특정 시스템에서는 일반 chmod()를
사용하는 대신 이 조작이 완료된다. 편리하게도, 스펙의 요구사항에 따라 *xattr() 함수가 실패하는 경우
chmod()로 다시 돌아가야 하고, 지금으로서는 pseudo가 이런 함수를 가로채고 실패하여 errno를
ENOTSUP로 설정하게 된다. 이후에 이를 수정해야 할 수도 있다.
이와 유사하게, 주요 리팩토링 단계 중에 pseudo의 랩퍼 중 다수는 방금 다른 함수를 호출한 간단한 함수로 다시 구현되었다. 예를 들어,
O_CREAT와 함께 open()을 사용하여
creat()를 구현했다. 특히, *at() 변형이 있었던 다수의 함수는 AT_FDCWD를
dirfd 매개변수로 사용하여 상응하는 *at() 함수를 호출하여 구현되었다. openat()를
지원하지 않는 머신에서 이 작업을 수행할 때까지는 아무 문제 없이 작동했다.
시간의 경과에 따라, 다른 종류의 API 지원을 제공하는 시스템에 맞춰 더욱 완전한 처리 방법을 개발해야 할 가능성이 크다.
Pseudo의 최초 개발 및 지속적인 유지보수 중에 맞닥뜨린 문제점 중 다수는 추적 및 진단이 비교적 쉬웠다. 처음부터 견고성과 착실한 로깅에 초점을 맞추기로 한 의사결정이 명확하게 성과를 안겨 주었다. 이에 반해, "곧" 작성하기로 계속 계획만 하고 있는 테스트 스위트는 중대하고도 현저한 차이를 보였다. 좀 더 일찍 더 많은 테스트 지원 기능을 빌드하고 이를 더 많이 사용했더라면 많은 시간을 절약할 수 있었을 것이다.
수행하려는 업무와 일치한다면 기존의 코드와 프로젝트를 사용하는 것이 당연히 좋겠지만, 해결하려고 노력 중인 문제점이 실제로는 새로운 문제점이라는 결론을 내리는 데 주저하지는 말자. 그럴 수 있기 때문이다. 이런 일이 그리 자주 일어나지는 않지만 분명히 발생하게 마련이며(필자에게 이런 일이 생긴 건 처음인 것 같음), 이에 대비하면 되므로 걱정하지 말자.
향후의 작업에 대해 말하자면, 견고성과 진단 기능에 아직 개선해야 할 점이 많지만 다음에 살펴봐야 할 주요 영역은 틀림없이 성능일 것이다. Pseudo는 상당히 효과적으로 수행할 것으로 인정받는 역할을 제대로 수행하겠지만 fakeroot보다는 상당히 느리다는 건 부인할 수 없는 사실이며, pseudo의 이런 약점을 아마 상당히 개선할 수 있을 것이다. 디스크에 안정적인 데이터베이스 형식으로 어떤 데이터를 저장하는 것이 그냥 메모리에만 보관하는 것만큼 빨라진다는 것은 결코 있을 수 없는 일이겠지만, 속도를 높일 여지는 아직도 많이 있다.
교육
- The pseudo project는 전적으로 내부적 요구를 충족시킬
목적으로 개발되었지만, 오픈 소스로 릴리스되었다.
- developerWorks 팟캐스트: 소프트웨어 개발자의 흥미로운 인터뷰와 토론을 확인할 수 있다.
- 기술 행사 및 웹 캐스트: developerWorks Live! briefings에서 최신 정보를 얻을 수 있다.
- Twitter의 developerWorks 페이지: developerWorks의 트윗을 팔로우하여 최신 뉴스를 확인하자.
- 관심 이벤트: IBM 오픈 소스 개발자에게 유익한 컨퍼런스, 기술 박람회, 웹 캐스트 및 기타 행사를 확인하고 참여하자.
- developerWorks 오픈 소스 영역: 오픈 소스 기술을 활용하여 개발 작업을 수행하고 이러한 기술을 IBM 제품과 함께 사용하는 데
도움이 되는 사용법 정보, 도구 및 프로젝트 업데이트와 가장 인기 있는 기사 및 튜토리얼을 확인할 수 있다.
- developerWorks On Demand 데모: 무료 데모를 보면서 IBM의 기술 및 오픈 소스 기술을 배우고 제품의 기능을 익히자.
제품 및 기술
- IBM 시험판 소프트웨어: 다운로드하거나 DVD로 이용할 수 있는 IBM 시험판 소프트웨어를 사용하여 차기 오픈 소스 개발 프로젝트를 강화하자.
토론
- developerWorks 커뮤니티: 개발자가 운영하고 있는 블로그, 포럼, 그룹 및 위키를 살펴보면서 다른 developerWorks 사용자와 의견을 나눌 수 있다.
