Friday, August 30, 2013

[Translation] Dynamic Instrumentation of Production Systems (DTrace)



[Translation] Dynamic Instrumentation of Production Systems
[번역] 상용 시스템에서의 동적 인스트루먼트

Bryan M . Cantrill, Michael W. Shapiro and Adam H. Leventhal


1. Introduction

시스템이 커지고 복잡해짐에 따라 개발 중에 개발자에 의해서가 아니라 제품에서 시스템 integrator에 의해 성능 분석을 수행하는 경우가 늘어나고 있다. 컴포넌트화하고 응용프로그램을 통합하는 경향으로 인해 이런 변화를 가속화하고 있다. 시스템 integrator는 off-the-shelf 컴포넌트를 가져다 원래 개발자가 생각지 못한 방법으로 조합해 사용한다. 성능 분석 인프라스트럭처는 제품 내에서 성능 분석을 요구하는 흐름을 일반적으로 따라가지 못하고 있다. 이 분석 인프라스트럭처는 여전히 개발자를 위해, 개발 중인 시스템 상에서 사용하도록 되어 있다.성능 분석 인프라스트럭처를 제품에서 사용할 수 있도록 설계하였다면 항상 특정 프로세스에 국한된 것이다(?). 따라서 시스템 전반적인 문제를 이해하는 도움 되지 않는다.

성능 분석 인프라스트럭처를 제품 수준에서 사용하려면 disable시 성능 분석을 위한 probe로 인한 영향이 없어야 한다. enable시 절대로 안전하게 분석을 수행해야 한다. 즉 성능 분석 인프라스트럭처 자체가 있다고 해서 시스템이 느려지면 않되고, 이를 잘못 사용함으로 해서 시스템이 fail되는 경우가 없어야 한다. 시스템 전체를 분석하기 위해 전체 시스템에 instrument할 수 있어야 하고 시스템 전반에 관한 현상을 하일라이트할 수 있도록 데이터를 쉽게 압축할 수 있어야 한다. 제품화 수준의 시스템에서 데이터를 모으고 압축할 수 있는 시스템 전반에 걸처 동적으로 instrumentation할 수 있는 도구를 개발했다. 이 도구를 DTrace라 부르는데 Solaris에 탑재해서 사용 가능하다. DTrace의 특징은 다음과 같다.

  • 동적 instrumentation. 정적 instrumentation은 disable시에도 probe로 인한 영향이 항상 있다. 제품에서 이 도구를 사용하기 위해 이런 영향을 없애야 하는데 DTrace는 동적 instrumentation만을 사용한다. DTrace를 사용하지 않을 때 시스템은 DTrace가 전혀 없는 것과 같다.
  • 통합 instrumentation. DTrace는 사용자 소프트웨어와 커널 소프트웨어 모두 동적으로 instrument할 수 있고, 또한 통합된 방법으로 할 수 있다. 즉, 사용자와 커널 경계를 넘나들며 데이터와 제어 흐름을 따라갈 수 있다.
  • 커널의 임의의 context에 대한 instrumentation. DTrace는 거의 모든 커널을 instrument할 수 있다. 예를 들어 scheduler와 synchronization과 같이 민감한 서브 시스템도 instrument할 수 있다.
  • Data integrity. DTrace는 데이터를 기록할 수 없는 어떤 에러도 항상 리포트한다. 이런 에러가 없다면 DTrace는 data integrity를 보장한다. 즉, 기록된 데이터가 그냥 깨지거나 잃는 경우는 없다.
  • 임의의 액션. 주어진 instrumentation 포인트에서 어떤 액션을 취할지를 미리 정의하거나 제한하지 않는다. 사용자는 임의의 probe를 임의의 액션을 가지고 enable할 수 있다. 더우기 DTrace는 사용자가 정의한 action에 대해서 안정성을 보장한다. illegal memory access와 같은 실행 시간에 발생하는 에러도 잡아 리포트 한다.
  • Predicates. 논리 술어 메커니즘을 통해 사용자가 지정한 조건이 충족될 때만 액션을 실행한다. 발생 시점에 원하지 않는 데이터를 걸러낼 수 있다. 이렇게 DTrace는 궁극적으로 버릴 데이터를 유지하고 복사하고 저장하지 않는다.
  • 고급 제어 언어. Predicates과 액션을 "D"라 부르는 C와 유사한 언어로 기술한다. ANSI C 연산자를 모두 지원하고 커널 변수와 타입을 사용할 수 있다. D를 통해 사용자가 직접 변수를 정의할 수 있다. 전역 변수, 쓰레드 지연 변수, associative 배열도 정의할 수 있다. D에서 포이터 참조도 가능하다. Predicate이나 액션에서 구조체 체인을 따라갈 수 있는데 이때 DTrace에서 실행 시간 안전 장치가 제공된다.
  • 데이터를 수집하는 scalable한 방법. DTrace에서는 D 식들로 이뤄진 튜플 형태로 데이터를 모은다. 이 방법은 데이터가 만들어질 때 압축해서 프레임워크를 통해 이동할 데이터 양을 데이터 포인트 수 만큼 줄인다. D식들로 aggregration하는 방법을 통해 사용자는 거의 모든 것을 모을 수 있다.
  • Speculative tracing. DTrace는 데이터를 트레이싱할 때 일단 시도하고 나중에 그 데이터를 commit할지 버릴 결정하는 방법을 제공한다. Speculative 트레이싱으로 이따금 나타나는 이상 동작을 검사할 때 맨 마지막 단계에서 후 처리가 불필요 할 수 있다.
  • Heterogeneous instrumentation. 트레이싱 프레임워크는 단일 instrumentation 방법으로 설계해왔었다. DTrace에서는 instrumentation provider는 probe 처리 프레임워크와 잘 정의된 API를 통해 공식적으로 분리되어 있어서 다양한 기법의 동적 instrumentation 기술들을 공통 프레임워크에 꽂아 사용할 수 있다.
  • Scalable 구조. DTrace는 수십만개의 instrumentation 포인트를 허용하고, (아무리 작은 시스템에서도 대개 3만개 수준의 포인트를 포함한다.) 이중 일부를 효율적으로 선택하고 enable할 수 있도록 한다.
  • Virtualized consumers. DTrace에 대한 모든 것은 consumer 별로 가상화되어 제공된다. 여러 consumer가 동일한 probe를 다른 방법으로 enable할 수 있고 한 consumer가 어떤 probe를 여러가지 방법으로 enable할 수 있다. 동시에 동작하는 DTrace consumer 수에 제한이 없다.

논문의 나머지 장에서 DTrace에 대해 자세히 기술한다. 2절은 동적 instrumentation 분야에 대한 연관된 기술을 논의한다. 3절은 DTrace 구조를 소개한다. 4절은 DTrace에 구현한 instrumentation provider에 대해 설명한다. 5절은 D언어를 설명한다. 6절은 데이터를 모으기 위한 방법을 설명한다. 7절은 사용자 instrumentation 방법을 설명한다. 8절은 speculative 트레이싱 방법을 설명한다. 9절은 DTrace를 사용하여 실제 제품에서 겪은 성능 문제를 해결한 사례를 설명한다. 마지막으로 10절은 future work을 논의하고, 11절에서 결론을 맺는다.

2. Related Work

운영체제에 사용자가 지정한 코드로 확장해서 안전하게 실행하는 개념은 VINO와 SPIN과 같은 확장성 있는 시스템에서 사용되었다. 더 일반적으로 AspectJ와 같은 aspect-oriented 프로그래밍 시스템에서도 사용되었다. 그러나 이들 시스템들에서는 사용자가 시스템이나 응용 프로그램을 확장하도록 설계되었지만 DTrace에서는 사용자가 시스템이나 응용 프로그램을 단지 이해하도록 설계되었다. 일반적인 목적의 확장성 있는 시스템에서는 시스템의 동작을 이해하는데 훨씬 더 적은 방법들을 가지고 있다.

ATOM이나 Purify와 같은 시스템들은 프로그램을 이해할 목적으로 프로그램을 instrument한다. 하지만 이 시스템들은 정적으로 instrument한다는 점에서 근본적으로 DTrace와 다른다. 오프라인에서 바이너리를 instrument하고 원래 바이너리 대신 instrument된 바이너리를 실행한다. 더우기 이들 정적 instrumentation 시스템들은 시스템 전반에 관한 통찰을 제공하지 않는다. 별개의 응용 프로그램들에 적용한 instrumentation을 통합할 수 없고, 일반적으로 운영체제를 instrument할 수 없다. 단일 응용프로그램 instrumentation 내에서도 이들 시스템들은 제품 환경에는 부적합하다. 왜냐하면 제품 환경에서 응용프로그램을 재실행하는 것을 받아들일 수 없기 때문이다.

시스템 전체에 instrumentation하는 것과 동적으로 instrumentation하는 것에 대한 많은 이전 아이디어가 있다. DTrace의 Predicate과 같은 특징은 [8]에서 직접 가져왔다. 시스템 모니터링을 위해 고급 언어를 사용하는 아이디어는 [1,4,9]에 있다. 그러나 DTrace는 쓰레드 지연 변수나 associative 배열과 같은 중요하고 새로운 특징을 제공한다. aggregations같은 특징은 [1,4]에서 기본적인 형태로만 존재하는데 DTrace는 이 아이디어를 발전시켰다. speculative 트레이싱은 이전에 사용되지 않은 새로운 아이디어다.


2.1 Linux Trace Toolkit

Linux Trace Toolki (LTT)는 전통적인 정적 instrumenation 방법으로 설계되었다. 각 instrumentation 포인트에서 probe를 사용하지 않더라도 작더라도 성능의 변화를 가져온다[16]. disable된 probe 영향을 줄이기 위해 LTT는 제한된 숫자의 instrumentation 포인트만을 정의한다. 대략 45개 이벤트로 구성된다. LTT는 임의의 액션을 취할 수 없다. 각각의 정적으로 정의된 이벤트는 이벤트에 특정적인 "detail"을 정의한다. LTT는 액션을 기술한 고급 언어를 가지고 있지 않다. LTT는 어떤 데이터를 버릴지 선택하는 정밀하지 못한 방법을 제공한다. 즉 트레이싱된 이벤트들은 주어진 PID, 프로세스 그룹, GID, UID에 속하는 이벤트들로 한정될 수 있다. LTT는 데이터를 버리거나 압축해서 데이터 흐름을 줄이는 방법이 적기 때문에 커널에서 사용자 공간으로의 데이터를 트레이싱할 때 가능한 패스를 최적화하는데 많이 노력해야 한다[17].


2.2 DProbes

DProbes는 OS/2를 위해 설계되었는데 나중에 Linux로 포팅되어 확장되었다[9]. 피상적으로 DProbes와 DTrace는 약간 유사한 속성들을 갖는다. 둘다 동적 instrumentation을 사용하고 따라서 enable되지 않았을 때 시스템에 영향을 주지 않는다. 그리고 둘 다 임의의 액션을 정의하기 위한 언어를 제공하고 이를 구현하기 위한 간단한 가상 머신을 제공한다. 그러나 이 둘은 매우 다르다. DProbes는 동적 instrumentation을 사용하지만 다른 CPU들에서 동시에 한 probe가 hit되었을 때 정보를 잃을 수 있는 기법을 사용한다. DProbes는 사용자 정의 변수를 제공하지만 쓰레드 로컬 변수와 associative 배열을 제공하지 않는다. 더우기 data 모음을 위한 방법을 제공하지 않고 predicate을 지원하지 않는다. DProbes는 안정성에 대한 고려를 했지만 (예를 들어 이상한 주소에서 load할 때 예외 메커니즘을 통해 처리한다.) 절대 안정성을 설계 조건으로 두지 않았다. DProbes를 잘못 사용하면 시스템이 다운될 수 있다[1].

2.3 K42

K42는 연구 목적의 커널이다. 그 자체로 정적인 instrumentation 프레임워크를 가지고 있다[14]. K42의 instrumentation은 LTT의 제약 사항들을 많이 가지고 있다. 예를 들어 정적으로 정의된 액션, 데이터를 줄이기 위한 방법이 제공되지 않음 등. 하지만 DTrace에서 처럼 instrumentation scalability를 고려해서 설계되었다. DTrace 처럼 K42도 lock-free, per-CPU 버퍼링을 제공한다. 하지만 K42는 트레이싱하는 데이터의 integrity를 희생하는 방법으로 그러한 특징들을 제공한다[2]. 최근에 K42에서의 scalalbe한 트레이싱 기법은 LTT에도 적용되었는데 LTT의 scalability 문제를 추측컨데 해결하는데 도움을 주었을 것이다. (하지만 데이터 integrity를 보장하지 못할 것이다.)

2.4 Kerninst

Kerninst는 동적 instrumenation 프레임워크이다. 상용 운영체제 커널에서 사용하려고 설계되었다[13]. Kerninst는 diable시 시스템에 영향을 미치지 않는다. 커널에 있는 거의 모든 코드를 instrumentation할 수 있다. 하지만 Kerninst는 instrumentation에서 매우 공격적인데, 사용자는 안전하게 instrument할 수 없는 루틴을 우연히 instrumenting함으로 해서 실수로 fatal 에러를 만들어 낼 수 있다. Kerninst는 데이터를 압축하는 방법을 제공하지만 데이터는 임의의 튜플로 aggregate될 수 없다. Kerninst는 predicate를 지원하지만 임의의 predicate를 사용할 수 없고 임의의 액션을 지원하지 않는다.


3. DTrace 아키텍쳐

3.1 Providers & Probes

DTrace 프레임워크에서 시스템을 instrumentation 하는 대신 Provider들이 실제 instrumentation를 맡는다. Provider는 로딩 가능한 커널 모듈이다. 잘 정의된 API를 통해서 DTrace 커널 모듈과 커뮤니케이션한다. DTrace 프레임워크이 Provider에게 instrumentation하도록 요청할 때 해당 Provider는 instrumentation 할 수 있는 위치를 결정한다. 각 Instrumentation 포인트에 대해 Provider는 DTrace 프레임워크을 부르고 Proble를 만든다. Provider는 모듈 이름, 함수 이름, 함수 내 위치를 뜻하는 이름을 지정해서 Probe를 만든다. 각 Probe는 <provider, module, function, name>라는 이름을 붙인다.

Probe를 만드는 것과 실제 시스템을 instrumentation하는 것과 별개다. Probe는 DTrace 프레임워크가 instrumentation할 위치만을 정한다. Provider가 Probe를 새로 만들면 DTrace는 Provider에 probe 고유 아이디를 리턴한다. Consumer가 Probe를 사용할 수 있도록 제공된다. Probe를 활성화 시키면 ECB (Enabling Control Block)를 새로 만들고 이 활성화된 Proble와 연결시킨다. Probe와 연관된 ECB가 없다면 DTrace 프레임워크는 Probe의 Provider를 호출하여 그 Probe를 활성화시킨다. Probe가 fire되면 (Probe에 지정된 Predicate이 참이 되면) DTrace 프레임워크에서 이 Probe 식별자에 해당하는 엔트리 포인트로 제어를 넘긴다. 이런 방식으로 Provider는 동적으로 시스템을 instrumentation한다.

Probe를 fire하는 문맥에 관한 특별한 제약 사항은 없다. DTrace 프레임워크 자체는 non-blocking이고 Kernel을 직간접적으로 호출하지 않는다.

Probe가 fire되어 제어를 DTrace 프레임워크로 이동하면 현재 CPU 상에서 인터럽트를 Disable하고 DTrace 프레임워크는 해당 Probe의 ECB 체인 상의 각 ECB에 지정된 activity를 수행한다. 그 다음, 인터럽트는 다시 enable되고 제어를 Provider로 옮긴다. Provider의 각 Probe들에 대한 여러 Consumer들에 대해 자체적으로 멀티플렉싱 처리를 고려할 필요는 없다. DTrace 프레임워크의 ECB abstraction이 모든 멀티플렉싱을 처리한다.


[의문점]
  • Dynamic instrumentation => DTrace 프로그램을 실행할 때 Firing할 지 결정하기 위해 로딩된 Probe의 Predicates에 대해서만 계산함 (Pay as you go)
  • Probe의 실제 의미를 구현하려면 Probing하려는 실제 위치에 Hook을 걸어야 하지 않은가? 즉 ECB를 어떻게 만들지 궁금.


3.2 Actions and Predicates

ECB는 각각 Predicate와 연관되어 있다. 만일 ECB에 연관된 Predicate이 참이 아닌 경우 다음 ECB로 넘어간다. 모든 ECB는 Action 리스트를 가지고 있다. 따라서 ECB의 Predicate이 참이면 그 ECB에 포함된 Action들을 수행한다. Action 수행 중 데이터를 기록하는 경우 이 ECB를 생성한 Consumer와 연관된 버퍼에 저장한다. 이 버퍼는 CPU별로 별도로 할당된다. (See Section 3.3) Action 수행 중 D 변수를 변경할 수 있다. 사용자 변수에 관해서는 Section 5에서 설명한다.  Actions들을 수행하면서 커널 메모리에 값을 저장하거나 레지스터 값을 변경하거나, 시스템 상태를 변경하는 것을 금지한다.

3.3 Buffers

각 DTrace Consumer는 커널 내의 CPU별로 할당된 버퍼들을 할당하고 Consumer 상태로 참조된다. Consumer 상태는 그 Consumer의 ECB들 각각에 의해 차례로 참조된다. ECB Action 수행 중 데이터를 기록할 때 ECB Consumer의 버퍼에 기록한다. 각 ECB에 허용된 기록 가능 데이터 크기는 항상 일정하다. 서로 다른 ECB들은 서로 다른 크기의 데이터를 기록할 수 있지만 주어진 ECB는 항상 동일한 크기의 데이터를 기록한다. ECB를 처리하기 전에 충분한 공간이 있는지 관련 버퍼를 미리 확인한다. ECB에서 데이터를 기록하는 Action 수행을 위해 필요한 공간이 부족하다면 해당 버퍼의 drop count를 증가시키고 다음 ECB로 넘어간다.

Consumer는 주기적으로 버퍼 내용을 읽어 drop count를 감소시켜야 한다. 커널에서 버퍼 내용을 읽을 때 data integrity를 유지하고 probe 처리가 wait-free이도록 처리한다. Active 버퍼와 Inactive 버퍼, 두 개의 버퍼를 유지한다. DTrace Consumer가 지정된 CPU의 버퍼를 읽을 때 그 CPU에 대한 cross-call을 한다. Cross-call은 지정된 CPU에서 실행되는데 그 CPU 인터럽트를 disable시키고 Active 버퍼를 Inactive 버퍼로 스위치하고 인터럽트를 enable하고 리턴한다. Prove를 처리하고 버퍼를 스위치할 때 인터럽트를 disable하기 때문에 그리고 버퍼 스위칭은 스위치될 CPU상에서 항상 일어나기 때문에 지켜야할 순서가 있다. 버퍼 스위칭과 Probe 처리들은 동일한 CPU 상에서 상호 atomi하게 처리되어야 한다. (서로 interleave되지 않도록) Active 버퍼와 Inactive 버퍼가 일단 스위치되면 그 Inactive 버퍼는 Consumer에 복사된다.

버퍼의 데이터 레코드 레이아웃은 먼저 EPID (Enabled Probe Identifier)가 있고 그 다음에 지정된 데이터가 놓이는 형태다. EPID는 ECB와 일대일 대응 관계이다. 해당 ECB에 의해 저장된 데이터의 크기와 레이아웃을 커널에 쿼리할 때 EPID를 사용할 수 있다. ECB의 데이터 레이아웃은 항상 동일하기 때문에 ECB 메타 데이터는 사용자 공간에 캐쉬될 수 있다. 이 디자인은 데이터 스트림과 메타데이터 스트림을 분리해서 런타임 분석 툴을 상당히 단순화 시킨다.


3.4 DIF

Action과 Predicate은 가상 머신 명령어로 표현하고 Probe를 Firing할 시점에 커널에서 에뮬레이션된다. 가상 머신 명령어 (D Intermediate Format, DIF)는 일종의 RISI 명령어 셋으로 간단한 에뮬레이션과 on-the-fly 코드 생성을 목적으로 설계되었다. 64비트 레지스터, 64비트 산술 연산과 논리 연산, 비교 연산과 분기 연산, 1바이트, 2바이트 4바이트, 8바이트 커널과 사용자 공간 메모리 로딩, 변수와 문자열을 다루는 특별한 명령어들을 포함하고 있다. DIF는 에뮬레이션을 간단하게 할 목적으로 설계되었다. 예를 들어 Addressin 모드는 한가지 이고 대부분의 명령어들은 레지스터들을 가지고 연산한다.

3.5 DIF Safety

Probe를 Firing할 때 DIF를 에뮬레이션하므로 DIF 에뮬레이션은 절대적으로 안전해야 한다. 기본적으로 DIF를 커널에 로딩할 때 opcode, reserved bit, register, string reference, variable reference가 valid한지 검사한다. 무한 루프의 DIF를 막기 위해 오직 Forward 분기만을 허용한다. 다소 심한 제약으로 생각될 수 있지만 루프를 모두 제거하지만 이로 인한 제약 사항은 실제로 없다.

불법 로딩이나 0으로 나누는 연산과 같은 런타임 에러는 프로그램 텍스트를 보고 검출할 수 없다. 이 런타임 에러들은 DIF 가상 머신에 의해 처리된다. Align되지 않는 로딩이나 division by zero는 쉽게 처리 가능하다. 에뮬레이터는 그러한 연산을 수행하지 않는다. 즉 현재 ECB 처리를 abort하고 DTrace Consumer에 런타임 에러를 알린다. Memory-mapped I/O 디바이스에서 로딩하는 것은 커널이 memory-mapped 디바이스 레지스터를 위해 확보한 가상 주소 범위에 로딩하려는 주소가 포함되지 않음을 확인함으로써 막는다. Unmapped memory로 부터 로딩을 막는 것은 더 복잡하다. Probe firing 문맥에서 VM 자료를 probe하는 것은 불가능하기 때문이다. 에뮬레이션 엔진이 그런 로딩을 수행하려 할 때 H/W fault가 발생한다. 커널의 page fault 핸들러를 수정해서 그 로딩이 DIF-directed인지를 검사한다. (DIF-directed???) 만일 그렇다면 그 fault handler는 특정 비트를 셋 함으로써 fault가 일어났음을 표시한다. 그런 다음 fault가 난 로딩 다음 명령어로 이동한다. 각 로딩을 에뮬레이션한 다음 DIF 에뮬레이션 엔진은 faulted 비트가 있는지를 확인한다. 만약 그런 비트가 셋되었다면 현재 ECB를 처리하는 것은 abort하고 사용자에 에러를 리포트한다. 이 방법은 커널의 페이지 fault path에 약간의 오버헤드를 수반할 뿐 전체 시스템 성능에는 적은 영향을 미친다.


4. Providers

Instrumentation Prover와 Core 프레임워크를 분리하여 DTrace는 다양한 instrumentation 방법을 허용할 수 있다. 미래의 instrumentation 방법들이 개발되면 이들 또한 DTrace 프레임워크에 쉽게 플러그인 될 수 있다. 12개의 Provider를 구현해서 시스템의 많은 면들을 관찰할 수 있는 여지를 제공한다. 이 Provider들은 다른 Instrumentation 방법들을 사용하지만 모든 DTrace Provider들은 disable되면 DTrace가 없는 시스템과 차이가 없다. Provider들 중 일부를 아래에 소개하지만 어떻게 그 것들을 구현했는지 자세한 사항은 다루지 않는다.

4.1 Function Boundary Tracing

함수 경계 트레이싱 (FBT) Provider는 커널에 있는 거의 모든 함수의 엔트리와 리턴에 대해 probe를 가능하게 한다. 커널에 수많은 함수가 있으므로 FBT는 많은 probe를 제공한다. 아무리 가장 작은 시스템에서도 FBT는 25,000개 이상의 probe를 제공할 것이다. 다른 DTrace provider와 마찬가지로 FBT Provider는 enable되지 않으면 probe가 없는 상황과 차이가 없다. (zero probe effect). Enable되면 probe된 함수만 probe로 인한 영향을 만든다. FBT 구현을 위해 사용한 방법은 명령어 집합 아키텍쳐에 매우 의존적이다. SPARC과 x86을 위한 FTB가 구현되었다.

SPARC 상에서 FBT를 위해 무조건 annulled banch-always (ba,a) 명령어를 어떤 명령어로 대체한다. 그 분기는 제어 흐름을 FBT-controlled trampoline으로 바꾸어 적적한 인자를 준비해서 DTrace로 제어를 보낸다. DTrace로 부터 제어가 돌아오면  대체한 명령어를 tranpoline에서 실행하여 제어 흐름을 instrumented code path로 옮긴다. 이 방법은 Kerninst가 사용한 방법과 유사한 방법이다. Kerninst 방법과 달리 오직 함수 엔트리와 리턴 만을 instrument하므로 다소 일반적인 방법은 아니지만 TL>0에서 실행한 코드를 instrument할 때 결과 오류가 있을 수 없으므로 확실히 안전하다.

x86 상에서 FBT는 스택 프레임을 만들거나 분해하는 코드에 포함된 명령어를 IDT (interrupt description table)로 제어를 옮기는 명령어로 대체하는 trap 기반 방법을 사용한다. IDT 처리기는 trap된 명령어 포인터를 통해 FBT probe를 찾아 제어를 DTrace에 전달한다. DTrace로 부터 제어가 돌아오면 trap 처리기를 통해 trap 스택을 다룸으로 해서 대체한 명령어를 에뮬레이션한다. 명령어를 rewriting하거나 reexecution하는 대신 에뮬레이션함으로써 FBT는 DProbe 방법들의 잠재적인 lossiness로 인한 문제점을 방지한다. 

4.2 Statically-defined Tracing

FBT는 probe 넓은 가능 범위를 허용하지만 효과적으로 사용하려면 그 커널 구현에 익숙해져야 한다. Probe에 의미를 부여하려면 커널 구현 안에 정적으로 (프로그램 텍스트 상에서?) probe를 선언해야 한다. 이를 구현하는 방법으로 매크로를 사용할 수 있다. 트레이싱이 Enable되면 이 매크로는 트레이싱 프레임 워크를 호출하는 조건부 함수 호출을 매크로로 정의한다. 이 방법의 probe 영향은 작지만 있긴하다. Disable되었을 때도 매크로로 부터 나온 load, compare, branch 명령어를 실행하기 때문이다. Disable되었을 때 probe 영향이 없어야 하는 설계 원칙을 지키기 위해 statically-defined tracing (SDT) provider를 구현하기를  __dtrace_probe_를 prefix로 갖는 존재하지 않는 함수를 호출하는 코드로 바뀌는 C 매크로를 정의한다. 커널 링커가 이 prefix를 갖는 함수를 relocation할 때
이 함수 호출 명령어를 no-operation으로 대체하고 가짜 함수의 이름을 그 함수 호출 명령어 위치와 함께 기록한다. SDT가 enable되면 그 no-operation을 SDT-controlled trampoline으로의 함수 호출로 바뀐다. SDT-controlled trampoline은 제어를 DTrace로 옮긴다.

이론상으로 이 Provider는 disable되었더라도 probe 영향이 있다. 즉 call site가 no-operation을 대체되었지만 컴파일러는 그 call site를 존재하지 않는 함수로 제어를 옮기도록 다루어야 하기 때문이다. 결과적으로 이 방법은 레지스터 사용에 대한 불필요한 pressure를 만들어 낼 수 있다. 그러나 SDT probe를 주의깊게 함수에 대한 extant calls(?) 가까이 위치하게 하면 이러한 disable시 발생할 수 있는 probe 영향을 최소화할 수 있다. 실제로 Solaris 커널애 150군데 넘게 포함시켰고 어떤 성능 차이를 측정할 수 없었다. 이러한 disabled probe effect를 측정하기 위해 설계된 마이크로 벤치마크에서 조차도 성능 차이가 없었다.

4.3 Lock Tracing

Lockstat Provider는 커널 동기화에 관한 거의 모든 점을 이해하는데 사용할 수 있는 probe를 제공한다. Lockstat Provider는 동기화 연산을 다루는 커널 함수들을 동적으로 rewriting하며 동작한다. 모든 다른 DTrace provider와 마찬가지로 enable되었을 때 만 해당 instrumentation을 수행한다. Enable되지 않으면 probe efffect는 없다. Lockstat Provider로 인한 instrumentation은 Solaris에서 오랬동안 제공되어 왔다. lockstat 명령어의 기본이 되어 왔다. Lockstat provider는 커널 리소스 contention을 이해하는데 특히 유용하다. 커널 내의 Lockstat 관련 컴포넌트를 확장해 lockstat provider를 만들었고, lockstat 커맨드를 DTrace Consumer로 다시 구현했다. 기존의 custom-built, single-purpose data processing 프레임워크는 버렸다.

4.4 System Call Tracing


Syscall Provider는 시스템의 각 시스템 콜의 엔트리와 리턴 지점에서 probe를 제공한다. 시스템 콜은 사용자 응용 프로그램과 OS 커널간의 주요 인터페이스 이므로 응용 프로그램이 시스템에 상대적으로 어떻게 동작하는지에 대한 거대한 통찰력을 Syscall Provider을 통해 제공할 수 있다. Probe를 enable할 때 Syscall Provider는 시스템 콜 테이블에 있는 엔트리를 동적으로 rewriting함으로써 동작한다.


4.5 Profiling

위에서 기술한 Provider들은 텍스트 상에서 지정된 위치에 놓인(anchored) probe들을 제공한다. DTrace는 또한 unanchored probe들을 허용한다. Unanchored probe란 특정 실행 지점과 연관지어 있지 않고 비동기적인 이벤트 소스와 연관되어 있는 probe이다. Profile Provider는 이러한 probe들을 제공하는 Provider인데, 지정된 간격의 타임 인터럽트를 이벤트 소스로 한다. 이 Probe들은 매 지정된 단위 시간에 시스템 상태를 샘플링하는데 사용할 수 있다. 이 샘플들을 통해 시스템이 어떻게 동작하는지를 유추하는데 사용할 수 있다. DTrace를 지원하는 임의의 Actions에 대해 Profile Provider는 시스템에서 사실상 어떤 데이터도 샘플 할 수 있다. 예를 들어 현재 쓰레드의 상태를 샘플하거나, CPU 상태를 샘플하거나, 현재 스택 트레이스를 샘플하거나, 현재 머신 명령어를 샘플할 수 있다.



5. D Language

DTrace 사용자는 고급언어 D 프로그래밍 언어로 Predicates과 Actions을 작성할 수 있다. D는 C와 유사한 언어로 모든 ANSI C 연산자들을 지원하고 커널의 native 타입과 global 변수들을 사용할 수 있도록 한다. D는 global, clause-local, thread-local 변수와 같은 사용자 변수들을 지원하고 associative 배열을 지원한다. D 프로그램은 DTrace 라이브러리 안에 구현된 컴파일러에 의해 DIF로 컴파일된다. DIF는 메모리 상의 객체 파일로 표현되어 커널 내 DTrace 프레임워크로 보내져 validation 검사하고 probe를 enabling한다. dtarce 명령은 D 컴파일러와 DTrace에 대한 일반적인 front-end를 제공한다. 컴파일러 라이브러리를 가지고 다른 layered tools들을 만들 수도 있다. (예를 들어 앞에서 설명한 lockstat의 새로운 구현)


5.1 Program Structure

D 프로그램은 하나 혹은 그 이상의 clauses로 구성되어 있다. 각 clause는 DTrace에 의해 enable된 instrumentation을 묘사한다. 각 probe clause는 다음의 형태를 갖는다.

probe-descriptions
/predicate/
{
 action-statements
}

probe description은 provider:module:function:name 형태이다. 생략된 필드들은 어떤 값하고도 매치된다. sh(1) globbing 구문을 지원한다. (?) Predicate과 Action은 생략 가능하다.

D는 awk과 유사한 프로그램 구조를 사용한다. 왜냐하면 실행 순서가 기존의 함수 oriented 프로그램 구조를 따르지 않는 다는 점에서 트레이싱 프로그램들은 패턴 매칭 프로그램을 닮았기 때문이다. 내부 테스팅에 의하면 UNIX 개발자들은 이 프로그램 형태의 의미를 즉시 이해하였고 신속하게 이 언어에 적응했다.

5.2. Types, Operators and Expressions 

C가 UNIX의 언어인것 처럼 동적 트레이싱에 사용하기 위해 C와 유사하게 D를 설계했다. D Predicates과 Actions을 C 언어 구문과 동일하게 작성한다. 모든 ANSI C 연산자들을 사용할 수 있고 동일한 우선순위 규칙을 따른다. D는 모든 C 기본 데이터 타입, typedef, struct, union, enum type을 지원한다. 변수를 선언하고 사용할 수 있다. DTrace에 의해 정의된 함수와 변수들을 사용할 수 있다.

D 컴파일러는 또한 커널 서비스를 통해 C 소스 타입과 심볼 정보를 이용한다. D 프로그래머는 커널 소스 코드에 정의된 C 타입과 global 변수를 특별한 선언 없이 사용할 수 있다. FBT provider는 커널 함수들과 관련된 probes를 fire할 때 이 함수들의 입력 인자와 리턴 값을 DTrace에 export한다. D 컴파일러는 C 타입 서비스를 통해 이들 인자들을, FBT probe를 매치하는 D 프로그램 clause에서 사용한 C 데이터 타입과 연관짓는다. (Oh my god!) 기존 C 소스 파일과 달리 D 소스 파일은 다양한 스코프의 타입과 심볼을 사용할 수 있다. 예를 들어 커널 core, 커널 모듈들, D 프로그램에서 선언된 타입과 변수를 사용 가능하다. 외부 이름 공간을 사용할 때 역따옴포 (`) 문자를 심볼과 타입 식별자에 넣어서 역따옴표 앞에 놓은 식별자에 의해 뜻하는 이름 공간을 참조한다. 예를 들어 foo`bar는 커널 모듈 foo에 있는 C 타입 struct bar를 지칭한다. `rootvp는 커널의 전역 변수 rootvp를 지칭하고 D 컴파일러에 의해 자동으로 그 변수의 타입은 vnode_t*이 된다.

5.3 User Variables

D 변수는 C 선언 구문을 사용해서 프로그램의 외부 scope(the outer scope)에서 선언한다. 또는 assignment 문장으로 묵시적으로 정의하기도 한다. assignment로 변수들을 정의하면 왼쪽 identifier을 오른쪽 식의 타입으로 선언하는 의미를 갖는다. 경험상 D 프로그램을 빠르게 개발하고 편집하고 종종 dtrace 커맨드 상에서 직접 쓰기도 한다. 따라서 간단한 프로그램의 경우 선언문을 생략하는 장점을 이용할 수 있다.

5.4 Variable Scopes

D 프로그램에서 global 변수 뿐만 아니라 임의의 타입을 가진 clause-local과 thread-local 변수를 선언할 수 있다. 이 두가지 scope의 변수들은 키워드 this->와 self->를 prefix로 해서 사용한다. 이 prefix들은 변수들 이름 공간을 분리하는데 사용하기도 하고 assignment 문에서 변수를 선언하지 않고 사용하도록 유도한다. (facilitate) Clause-local 변수는 D program clauses들이 실행할 때 마다 새로 할당된다(re-used?). C의 automatic 변수와 유사하다. Thread-local 변수는 각 OS의 쓰레드를 위한 별도 storage에 하나의 변수 이름을 부여한다. 인터럽트로 인해 발생하는 쓰레드의 경우도 동일하다.

syscall::read:entry
{
        self->t = timestamp;
}

syscall::read:return
/self->t/
{
        printf("%d/%d spent %d nsecs in read\n",
            pid, tid, timestamp - self->t);
}

Figure 1: 이 스크립트는 Thread-local 변수를 사용해서 쓰레드가 read() 시스템 콜을 호출할 때 걸린 시간을 출력한다. 쓰레드 로컬 self->t는 필요에 따라 설정한다. 쓰레드가 syscall::read:entry probe를 fire하고 이 쓰레드에 timestamp 값을 할당한다. 그 다음 프로그램은 시스템 콜이 리턴할 때 시간 차이를 계산한다. 동적 변수를 할당하다 실패하면 DTrace는 기록된 데이터와 함께 failure를 리포트한다. 따라서 묵시적으로 데이터를 잃는 일이 없도록 한다.

D 프로그램에서 Thread-local 변수를 사용해서 어떤 관찰하고자 하는 activity를 수행하는 쓰레드에 데이터를 관련시킨다. 예를 들어 Figure 1의 스크립트는 thread-local 변수들을 사용해서 read 시스템콜을 호출할 때 걸린 시간을 출력한다.

5.5 Associative Arrays

D 프로그램은 associate array를 생성해서 각 배열 원소를 식 값으로 이뤄진 튜플로 인덱싱하고 배열 자료 원소들은 필요에 따라 생성한다. 예를 들어 D 프로그램 문장 a[123,"hello"]=456은 [int,string] 튜플을 인덱스로 하는 associative array를 정의하고 원소는 int타입이다. 그리고 [123,"hello"] 튜플에 인덱스된 원소에 456을 assign한다. D는 global 혹은 thread-local associative arrays를 지원한다. Perl과 같은 다른 언어에서 처럼 복잡한 딕셔너리 데이터 구조를 쉽게 만들고 다룰 수 있다. 메모리 관리나 lookup 함수를 만들 필요가 없다.


5.6 Strings

D언어는 string 타입을 지원해서 C char* 타입으로 주소를 표현하는지, 한 문자가 저장된 주소를 표현하는지, NUL로 끝나는 문자열의 주소를 표현하는지의 모호성을 해결한다. D string 타입은 C 타입의 char [n]과 같다. n은 컴파일 시점에 정해질 수 있는 고정된 문자열 최대 길이다. 이 문자열 최대 길이는 DTrace in-kernl 컴포넌트에 의해 이 최대 길이를 넘지 않게 한다. strlen() 함수를 제공하며 불법적인 문자열 주소가 주어지더라도 유한한 실행 시간을 보장한다. D언어는 "the operator"(?)를 사용해서 문자열을 복사하고 관계 연산자를 사용해서 문자열들을 비교한다. D언어는 char*와 char[]을 string으로 묵시적으로 변환한다. (promote)

6. Aggregating Data

시스템을 instrument하여 성능을 점검할 때 각 probe에 의해 모은 데이터에 관해서 생각하기 보다 이 데이터들을 어떻게 조합해서 시스템에 관한 의문을 답할지 생각한다. 예를 들어 사용자 ID에 의한 시스템 call의 수를 알고 싶다면 각 시스템 call에서 모은 데이터를 고려할 필요가 없다. 사용자 ID와 시스템 call에 대한 테이블을 보려 할 것이다. 이전 도구에서는 각 시스템 call에서 데이터를 모으고 awk나 perl과 같은 툴로 데이터를 포스트 프로세싱했다. DTrace에서는 데이터를 모으는 (aggregating) 연산을 first-class로 표현할 수 있도록 하여 프로그램 소스에서 이 연산을 수행한다.


6.1 Aggregating Functions

aggregating 함수는 다음과 같은 속성을 갖는다.

f(f(x0) ∪ f(x1) ∪ ... f(xn)) = f(x0 ∪ x1 ∪ ... ∪ xn)

xn 임의의 데이터 집합이다. 전체의 부분 집합들에 aggregating 함수를 적용한 다음 그 결과에 이 함수를 다시 적용하면 전체 집합에 이 함수를 적용한 것과 동일한 결과를 낸다. 데이터 집합을 이해하는데 흔히 많이 사용하는 함수들은 aggregating 함수이다. 예를 들어 함수에 있는 원소들 수를 세거나 최대 값을 계산하거나 모든 원소들을 합하는 함수가 있다. 모든 함수가 aggregating 함수는 아니다. mode(?)나 median을 계산하는 함수가 aggregating 함수가 아닌 두가지 예다.

aggregating 함수를 사용하는 것은 많은 장점이 있다.
  • 전체 데이터 집합을 저장할 필요가 없다. 새 원소를 이 집합에 포함시킬 때 현재 중간 결과와 새 원소를 구성하는 집합에 대해 aggregating 함수를 계산한다. 새 결과를 계산하면 새 원소를 버린다. 이런 방식은 많은 데이터를 저장하기 위해 필요한 저장 장소 양을 줄인다.
  • scalable한 구현이 가능하다. 데이터를 수집할 때 불필요한 scalability 문제가 발생하지 않기를 바란다. aggregating 함수는 중간 결과들을 CPU들 간의 공유된 자료 구조에 저장하는 것이 아니라 각 CPU 마다 따로 저장할 수 있다. 시스템 전체적인 결과를 원할때 각 CPU 중간 결과들을 구성하는 집합에  aggregating 함수는 적용한다.

6.2 Aggregations

DTrace는 aggregating 함수를 aggregations으로 구현한다. aggregation은 n개의 튜플을 인덱스로 하는 이름을 붙인 구조로 aggregaing 함수의 결과를 저장한다. D 언어에서 aggregation은 다음의 구문을 갖는다.

@identifer [keys] = aggfunc (args)

identifier는 aggregation의 이름이고, keys는 D 언어 식으로 콤마로 분리된 리스트이고, aggfunc는 DTrace aggregatin 함수이고 args는 이 함수에 대한 인자 리스트이다. 대부분 aggregating 함수는 오직 하나의 인자를 받는다. 새로운 데이터를 뜻한다. )

예를 들어, 다음 DTrace 스크립트는  write () 시스템 call을 부르는 application 이름을 센다. 

syscall::write:entry
{
       @counts[execname] = count();
}
DTrace가 종료하면 보통 aggregation 결과를 보여준다. (printa() 함수로 aggregation 출력을 명시적으로 조정할 수 있다.) 위 스크립트 이름이 write.d라 하자. 이 스크립트를 다음과 같이 실행해서 얻은 출력 결과이다.

# dtrace -s write.d
dtrace: script 'write.d' matched 1 probe
^C
 dtrace                                    1
 cat                                       4
 sed                                       9
 head                                      9
 grep                                     14
 find                                     15
 tail                                     25
 mountd                                   28
 expr                                     72
 sh                                      291
 tee                                     814
 sshd                                   1996
 make.bin                               2010
위 출력에서 sshd라는 이름의 프로세스로 부터 호출된 write 시스템 call에 대해 더 이해하고 싶을 수 있다. 예를 들어 file descriptor당 write한 데이터 크기의 분포를 알기 위해서 arg2에 관해 quantize() aggregating 함수를 사용해서 arg0에 관해 데이터를 모을 수 있다. arg0는 write 시스템 call의 file descriptor 인자이고, arg2는 write 시스템 call에 대한 size 인자이다.

syscall::write:entry
/execname == "sshd"/
{
       @[arg0] = quantize(arg2);
}
위 스크립트를 실행하면 각 file descriptor에 대한 빈도 분포를 만든다. 예를 들어

    5
value  --------- Distribution --------- count    
   16 |                                 0        
   32 |                                 1        
   64 |                                 0        
  128 |                                 0        
  256 |@@                               13       
  512 |@@                               13       
 1024 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@     199      
 2048 |                                 0        


위 출력을 보면 file descriptor 5에 대한 1024와 2047 바이트 사이를 199번 write했다. 이 파일 descriptor를 쓰는 origin을 이해하려면 arg0가 5인지 확인하는
predicate를 추가하고 ustack 함수를 사용해서 application의 stack 트레이스를 모을 수 있다.

syscall::write:entry
/execname == "sshd" && arg0 == 5/
{
       @[ustack()] = quantize(arg2);
}


7. User-level Instrumentation


DTrace의 pid Provider를 통해 사용자 프로그램을 instrumentation할 수 있다. 지정된 프로세스에서 임의의 명령어들을 instrument할 수 있다. Pid Provider는 다른 Provider들과 약간 다른 점은 실제로 Provider의 클래스를 정의한다는 점이다. 각 프로세스는 잠재적으로 관련된 Pid Provider를 갖을 수 있다. 프로세스 식별자는 각 pid provider 이름에 붙인다. 예를 들어 Probe pid1203:libc.so.1:malloc:entry는 프로세스 1203의 malloc(3C)의 함수 엔트리에 해당한다.

동적 instrumentation이라는 DTrace의 철학에 따라 타겟 프로세스는 instrument를 하기 위해 다시 시작할 필요가 없다. 그리고 다른 Provider와 마찬가지로 probe들을 enable하지 않으면 Probe effect는 없다.

Pid Provider에 의해 사용된 기법들은 ISA(?)에 국한된 것이다. 그러나 이 기법들은 모두 instrumented 명령어를 rewrite해서 OS로의 trap을 만드는 방법을 사용한다. Trap기반 방법들은 Branch기반 방법 보다 enabled probe effect가 크지만 이 방법은 커널 공간과 사용자 공간에서의 instrumentation을 완전히 통일할 수 있다. 즉 커널에서 사용한 probe와 함께 사용되는 DTrace 방법은 사용자 공간 probe와 함께 사용될 수 있다. 예를 들어 Figure 2는 thread local D 변수들을 사용하는 스크립트로 사용자 공간과 커널 공간에서의 모든 activity를 지정한 사용자 공간의 함수에서 부터 추적한다. Figure 3은 이 스크립트의 출력 예이다.

[Figre 2]

#!/usr/sbin/dtrace -s

#pragma D option flowindent

pid$1::$2:entry
{
        self->trace = 1;
}

pid$1:::entry, pid$1:::return, fbt:::
/self->trace/
{
        printf("%s", curlwpsinfo->pr_syscall ?
            "K" : "U");
}

pid$1::$2:return
/self->trace/
{
        self->trace = 0;
}
이 스크립트는 D 매크로 인자 변수 $1과 $2를 사용해서 타겟 프로세스 아이디와 사용자 함수를 지정하는데 이 스크립트의 인자로 받는다.

[Figure 3]

# ./all.d `pgrep xclock` XEventsQueued
dtrace: script './all.d' matched 52377 probes
CPU FUNCTION                                
  0  -> XEventsQueued                         U
  0    -> _XEventsQueued                      U
  0      -> _X11TransBytesReadable            U
  0      <- _X11TransBytesReadable            U
  0      -> _X11TransSocketBytesReadable      U
  0      <- _X11TransSocketBytesReadable      U
  0      -> ioctl                             U
  0        -> ioctl                           K
  0          -> getf                          K
  0            -> set_active_fd               K
  0            <- set_active_fd               K
  0          <- getf                          K
  0          -> get_udatamodel                K
  0          <- get_udatamodel                K
...
  0          -> releasef                      K
  0            -> clear_active_fd             K
  0            <- clear_active_fd             K
  0            -> cv_broadcast                K
  0            <- cv_broadcast                K
  0          <- releasef                      K
  0        <- ioctl                           K
  0      <- ioctl                             U
  0    <- _XEventsQueued                      U
  0  <- XEventsQueued                         U

Figure 2 스크립트를 실행한 출력 예. 첫번째 ioctl과 마지막 ioctl을 경계로 사용자 공간과 커널 공간을 넘나들며 트레이싱함. 다른 instrumentation 프레임워크도 통합된 트레이싱을 제공하지만, 위의 출력 예가 사용자 공간과 커널 공간을 넘나드는 제어 흐름을 가장 명백하게 보이는 것이다.

DTrace를 통해 또한 사용자 프로세스의 데이터를 트레이싱 할 수 있다. copyin함수와 copyinstr함수는 현재 프로세스의 데이터를 접근하는데 사용한다.
예를 들어 다음 스크립트는 open 시스템 콜에 주어진 이름 (arg0)를 모은다.

syscall::open:entry
{
        @files[copyinstr(arg0)] = count();
}
DTrace는 커널 프로세스와 사용자 프로세스에서 이벤트들을 트레이싱하고 두 소스에서 데이터를 조합할 수 있다. 이렇게 하여 사용자 공간과 커널 공간 모두 해당되는 시스템 문제를 이해하기 위해 필요한 전체 시스템에 대한 뷰를 DTrace는 제공한다.

[Figure 4]

#pragma D option flowindent

syscall::ioctl:entry
/pid != $pid/
{
        self->spec = speculation();
}

fbt:::
/self->spec/
{
        speculate(self->spec);
        printf("%s: %d", execname, errno);
}

syscall::ioctl:return
/self->spec && errno != 0/
{
        commit(self->spec);
        self->spec = 0;
}

syscall::ioctl:return
/self->spec && errno == 0/
{
        discard(self->spec);
        self->spec = 0;
}


위 스크립트는 ioctl 시스템 콜에 의해 호출되 모든 함수 중 failure를 리턴하는 함수를 (speculatively?) 트레이싱 한다. speculation() 함수를 호출하면 새로운 speculative 트레이싱 버퍼에 대한 아이디를 얻는다. speculate() 함수는 앞으로 데이터를 기록하면 지정된 speculative 버퍼에 기록되게 한다.  이 스크립트는 $pid 변수를 사용해서 dtrace 자체가 부른 ioctl 콜 중 fail한 경우를 트레이싱하지 않는다.


[Figure 5]

# dtrace -s ./ioctl.d
dtrace: script './ioctl.d' matched 27778 probes
CPU FUNCTION                                
  0  -> ioctl                    dhcpagent: 0
  0    -> getf                   dhcpagent: 0
  0      -> set_active_fd        dhcpagent: 0
  0      <- set_active_fd        dhcpagent: 0
  0    <- getf                   dhcpagent: 0
  0    -> fop_ioctl              dhcpagent: 0
  0      -> ufs_ioctl            dhcpagent: 0
  0      <- ufs_ioctl            dhcpagent: 0
  0    <- fop_ioctl              dhcpagent: 0
  0    -> releasef               dhcpagent: 0
  0      -> clear_active_fd      dhcpagent: 0
  0      <- clear_active_fd      dhcpagent: 0
  0      -> cv_broadcast         dhcpagent: 0
  0      <- cv_broadcast         dhcpagent: 0
  0    <- releasef               dhcpagent: 0
  0    -> set_errno              dhcpagent: 0
  0    <- set_errno              dhcpagent: 25
  0  <- ioctl                    dhcpagent: 25


Figure 4 스크립트를 실행한 결과 예다. 이 결과는 ioctl 호출 중 fail을 리턴하는 경우에만 ioctl이 부른 모든 함수들을 트레이싱한 결과를 출력한다. 위의 예제는 DHCP client daemon인 dhcpagent에서 ioctl 콜 했으나 ENOTTY라는 fail 값을 리턴한 경우이다.


8. Speculative Tracing

DTrace에서와 같이 다양한 정보를 제공하는 트레이싱 환경에서는 트레이스 하지 말아야 할 것을 이해하는 것이 중요한 문제가 된다. DTrace는 관련없는 이벤트를 필터링하는 주된 방법으로 Predicate을 사용한다. Predicate 방법이 유용한 경우는 probe를 fire할 시점에 probe 대상 이벤트가 사용자에게 관심의 대상인지 아는 경우이다. [역주. interesting한지 여부가 statically하게 정해진 경우?] 예를 들어 특정 프로세스나 특정 파일 descriptor와 연관된 activity에 관심이 있다면 이들에 대해 probe가 언제 fire할지를 지정할 수 있다.

하지만 probe를 fire한 다음에 주어진 이벤트가 관심 대상인지를 결정할 수 있는 경우가 있다. 예를 들어 시스템 콜이 비주기적으로 fail해서 EIO나 EINVAL과 같은 에러 코드를 낸다면 에러가 난 코드 path를 이해하고자 할 것이다. 코드 path를 캡쳐하기 위해 모든 probe를 enable할 수도 있다. 하지만 fail나는 시스템 콜의 원인을 찾을 수 있는 의미의 predicate을 construcut 할 수 있어야만 그 fail된 경우를 찾을 수 있다. 그 fail나는 시스템 콜의 경우가 이따금 발생하거나 항상 발생하는 경우가 아니라면 원인을 찾기 위해 관련되는 모든 이벤트를 기록하려 할 것이다. 이렇게 기록된 많은 데이터를 나중에 포스트 프로세싱하여 fail나는 시스템 콜의 원인과 무관한 데이터를 필터링한다. 찾고자 하는 원인과 관련된 이벤트들은 사실 양이 많지 않을 것이지만 이렇게 기록해야하는 이벤트들은 매우 많을 것이다. 따라서 그 기록한 이벤트들 양이 포스트프로세싱 가능할지라도 쉽지 않다.

이런 상황들에 대한 해결책으로 DTrace는 speculative 트레이싱 방법을 제공한다. 먼저 임시로 데이터를 기록한 다음 나중에 기록된 데이터가 관련있다라고 판단하면 주된 기록 버퍼로 commit한다. 만일 기록은 했으나 연관성이 없다고 판단되면 기록된 데이터를 버린다. 예를 들어 Figure 4에서 speculative tracing을 사용하는 스크립트가 있다. 이 스크립트는 speculative 트레이싱을 사용하여 ioctl 시스템 콜 중 fail나는 경우만 해당 자세한 상황을 기록한다. Figure 5는 이 스크립트의 결과 예이다.


9. Experience

DTrace는 Sun 내부에서 개발이나 제품 환경에서 모두 시스템을 이해하는데 많이 사용되어 왔다. DTrace가 특히 유용했었던 제품 환경의 예를 들면 Broomfield의 SunRay 서버다. 이 서버를 Sun IT 조직에서 운영하는데 10개의 CPU, 32기가 바이트 메모리로 약 170명이 사용한다. 이 서버는 종종 성능이 느려지곤 했다. DTrace는 이 제품에서 많은 성능 문제를 해결하는데 사용되었다. 다음은 그런 문제 해결 사례이다.

기존의 Solaris 모니터링 툴 mpstat의 출력 결과를 보면 CPU간의 많은 cross-call이 일어나고 있었다. (Cross-call이란 특정 CPU가 수행하도록 지정된 함수 호출이다.) 자연스룹게 누가 이 cross-call을 하는지 의문이 들었다. 일반적으로 이 의문을 간단하게 해결하는 방법은 없다. DTrace의 sysinfo Provider는 SDT에서 나온 provider로 동적으로 mpstat에 의해 수행된 매 스텝의 명령어들을 instrument할 수 있었다. DTrace와 sysinfo의 xcalls probe를 사용해서 쉽게 원하는 답을 얻을 수 있다.


 sysinfo:::xcalls
{
        @[execname] = count();
}

위 스크립트를 실행하면 프로그램 이름과 그 프로그램이 호출한 cross-call의 수를 볼 수 있다. 앞에서 문제가 되었던 서버에서 스크립트를 실행하면 거의 모든 cross-call을 한 프로그램은 Sun X 서버, Xsun임이 밝혀졌다. 그다지 놀랍지는 않다. 왜냐하면 각 SunRay 사용자 별로 X 서버가 하나씩 있고 이들 서버들이 많은 일을 할 것이다. X 서버들이 무엇을 하길래 이 cross-call 호출을 하는지 여전히 조사가 더 필요하다. 이 질문에 답하기 위해 다음과 같이 스크립트를 작성했다.

syscall:::entry
/execname == "Xsun"/
{
        self->sys = probefunc;
}

sysinfo:::xcalls
/execname == "Xsun"/
{
        @[self->sys != NULL ?
            self->sys : "<none>"] = count();
}

syscall:::return
/self->sys != NULL/
{
        self->sys = NULL;
}

이 스크립트는 thread local 변수를 사용해 현재 system call 이름을 기록한다. xcalls probe를 fire할 때 cross-call을 호출한 system call을 모은다. 이 경우 Xsun으로 부터 거의 모든 cross-call이 munmap activity가 시스템 call로 부터 기인함을 밝혔다. munmap이 cross-call을 호출했다는 사실은 당연하다. 왜냐하면 memory demapping은 TLB invalidation할 때 cross-call을 호출한다. 그러나 munmap activity가 그렇게 많이 발생하는 것은 생각지 못했었다. munmap activity는 mmap activity와 쌍으로 발생하므로 그 다음 확인할 것은 X 서버가 mmap하는 것이 무엇인지였다. 그리고 X 서버들 중 유난히 mmap을 많이 하는 X 서버가 있는지였다. 다음 스크립트를 통해 두가지를 즉시 확인할 수 있다.

syscall::mmap:entry
/execname == "Xsun"/
{
        @[pid, arg4] = count();
}

END
{
        printf("%9s %13s %16s\n",
            "PID", "FD", "COUNT");
        printa("%9d %13d %16@d\n", @);
}
이 스크립트는 프로세스 아이디와 mmap 파일 descript 인자를 모아  그 테이블을 만든다. DTrace END probe와 printa 함수를 사용해서 출력을 제어한다. 다음은 SunRay 서버에서 위 D 스크립트를 실행할 때 얻은 결과의 마지막 부분이다.

      PID            FD            COUNT
      ...            ..              ...
    26744             4               50
     2219             4               56
    64907             4               65
    23468             4               65
    45317             4               68
    11077             4             1684
    63574             4             1780
     8477             4             1826
    55758             4             1850
    38710             4             1907
     9973             4             1948

테이블 라벨을 보면 첫번째 컬럼은 프로세스 아이디, 두번째 컬럼은 파일 descriptor, 세번째 컬럼은 카운트이다. dtrace는 항상 aggregation value로 aggregation 결과를 정렬한다. 이 데이터를 보면 두가지 사실을 확인할 수 있었다. 첫번째 각 X 서버의 모든 mmap activity는 파일 descriptor 4에서 일어났다. 두번째 170개 X 서버들 중 6개만이 대부분의 mmap activity와 연관되었다. 기존의 프로세스 위주의 툴 (예를 들어 pfiles)을 사용하면 각 X 서버 파일 descriptor 4는 /dev/zero 파일임을 확인할 수 있었다. 이 zero 디바이스는 대부분의 UNIX 시스템에 있다. /dev/zero를 mmap하는 것은 메모리를 할당하는 기법이지만 X 서버들은 왜 그렇게 많은 메모리를 그렇게 자주 할당하고 해제했을까? 이 점을 확인하기 위해 X 서버들이 mmap을 호출할 때 사용자 스택 트레이스를 모으는 스크립트를 작성했다. 

syscall::mmap:entry
/execname == "Xsun"/
{
        @[ustack()] = count();
}
이 스크립트를 실행하면 스택 트레이스와 카운트로 된 테이블을 만든다. 이 경우 모든 Xsun mmap 스택 트레이스는 동일했다.

  libc.so.1`mmap+0xc
  libcfb32.so.1`cfb32CreatePixmap+0x74
  ddxSUNWsunray.so.1`newt32CreatePixmap+0x20
  Xsun`ProcCreatePixmap+0x118
  Xsun`Dispatch+0x17c
  Xsun`main+0x788
  Xsun`_start+0x108

이 스택 트레이스를 보면 왜 X 서버들이 메모리를 할당하고 해제하는지를 알 수 있다. X 서버들은 Pixmap들을 만들고 없애고 있었다. 앞의 질문에 대한 답은 되었지만 새로운 의문을 갖게 되었다. 어떤 application들이 X 서버로 하여금 Pixmap을 만들고 없애도록 명령하는가였다. 이 점을 확이하기 위해 더 복잡한 스크립트를 작성했다.

syscall::poll:entry
/execname == "Xsun"/
{
        self->interested = 0;
}

syscall::mmap:entry
/execname == "Xsun"/
{
        self->interested = 1;
}

sched:::wakeup
/self->interested/
{
        @[args[1]->pr_fname] = count();
}

이 스크립트를 구현하기 위해 X 서버 구현에 대한 지식이 필요하다. X 서버는 (client와?) connection된 후 poll()을 호출하여 request를 기다린다. Request가 오면 (single threaded 프로세스) X 서버는 이를 처리하고 response를 보낸다. Response를 보내면 X 서버를 block된 client를 깨우고 X 서버는 다시 connections에 대한 poll()을 호출한다. X 서버가 어떤 client의 요청으로 Pixmap을 만드는지 결정하기 위해 thread-local 변수 (interested)를 두었다. X 서버가 mmap을 부를때 이 변수를 set한다. 그리고 sched Provider에서 wakeup probe를 enable했다. sched Provider는 SDT를 변형해 만든 provider로 CPU 스케쥴링과 연관된 probe들을 제공한다. wakeup probe는 쓰레드가 다른 쓰레드를 깨울때 fire된다. 만일 X 서버가 다른 쓰레드를 깨우고 interested 변수를 셋 했다면 우리가 깨우려고 하는 프로세스 상에서 데이터를 모은다. 중요한 가정은 mmap을 수행한 다음 X 서버가 즉시 깨운 프로세스는 이 mmap을 수행한 대상 프로세스라는 것이다. SunRay 서버에서 위 스크립트를 실행하면 다음 결과를 얻었다.

  ...
  gedit                              25
  soffice.bin                        26
  netscape-bin                       44
  gnome-terminal                     81
  dsdm                              487
  gnome-smproxy                     490
  metacity                          546
  gnome-panel                       549
  gtik2_applet2                    6399

이 결과는 명백한 증거(smoking gun)였다. gtik2_applet라는 프로그램에 즉시 주목했다. 이 프로그램은 GNOME 데스크탑을 위한 stock ticker applet이다. 추가로 진행한 DTrace 스크립트로 사용자 스택에 대한 자료를 모아보니 문제의 원인을 발견할 수 있었다. gtik2_apple2는 X graphic context (GC)를 매 10 밀리초 단위로 만들고 없애고 있었다. X 프로그래머라면 모두 알듯이 GC는 서버에 위치한 복잡한 객체이다. 무모하게 제멋대로 생성하는 객체가 아니다. SunRay 서버 상에 겨우 6개의 gtki2_applet2 프로그램들이 떠있지만 각각은 시스템 성능에 중대한 영향을 미치고 있었다. 정말로 이 프로그램들을 모두 멈추게 하자 시스템 성능이 매우 향상되었다. cross-call이 64 퍼센트까지 떨어졌고 무작위 context switch도 35퍼센트까지 떨어졌고, 시스템 시간은 27퍼센트까지 떨어졌고, 사용자 시간은 37퍼센트까지 떨어졌고, idle 시간은 15퍼센트까지 올라갔다. 이것은 중대한, 돌이켜 보면 나쁜 성능 문제였다. 그러나 기존의 툴을 가지고 사실상 디버깅 할 수 없었다. 왜냐하면 시스템 전체에 걸친 (systemic) 문제였기 때문이다. gtik2_applet2 프로세스들 자체는 하는 일이 거의 없었다. 하지만 그 프로세스들로 인해 시스템의 다른 컴포넌트들이 대신 일을 하게 되었다. 문제의 근본 원인을 찾기 위해 aggregations과 thread-local 변수들을 광범위하게 사용했는데 이 두가지는 DTrace의 특징이다.



10. Future Work

DTrace는 제품화된 시스템을 관찰하는 우리 능력을 향상시키기 위한 future work을 위한 안정적이고 확장가능한 기초를 제공한다. 현재 개발 중인 DTrace 확장 기능은 다음과 같다.

  • 성능 카운터. 현대 마이크로프로세서 (SPARC, x86)는 성능 카운터 레지스터를 외부에 노출해 프로그래밍 가능한 구조이다. branch mispredict, cache miss, 다른 프로세서 이벤트를 카운트 할 수 있다. 향후 성능 카운터를 노출해서 probe 액션에서 D언어를 통해 사용하도록 DTrace provider를 구현할 계획이다.
  • 헬퍼 액션. 복잡한 미들웨어는 DTrace에 미들웨어에 한정된 정보를 사용하는 액션을 제공하기를 원할 수 있다. 우리는 헬퍼 액션 프로토타입을 개발했다. 이를 통해 응용 프로그램들은 DTrace에 사용자 스택 트레이스를 얻는데 도움을 제공할 수 있다. Java 가상 머신예서 헬퍼 액션을 구현해서 ustack provider가 Java와 C/C++ 스택 프레임들을 포함하는 사용자 스택 트레이스를 얻을 수 있다.
  • User lock 분석. pid provider는 사용자 프로세스에서 사용자 수준의 동기화 함수들을 포함해서 어떤 함수도 instrument할 수 있다. 커널의 lockstat 유틸리티와 동일한 프로토타입을 개발했다. plockstat이라 명명했는데, 이는 동적으로 멀티 쓰레드 사용자 프로스세들이 lock을 사용하는 형태 (contention)를 분석할 수 있다.


11. Conclusions

DTrace는 제품화된 시스템에서 사용자 소프트웨어와 커널 소프트웨어를 동적으로 instrumentation할 수 있는 도구이다. D언어와 고급 제어 언어를 포함해서 DTrace의 주된 특징을 설명했다. 지면이 부족해서 DTrace의 다른 중요한 특징들 (예를 들어 postmortem 트레이싱, 부트 시점 트레이싱)을 설명하지 못했지만 동적 instrumentation 분야의 다른 도구들과 비교해서 DTrace의 주된 진보된 사항을 하일라이트했다. 예를 들어 쓰레드 지역 변수, associative 배열, 데이터 모음, 사용자와 커널 수준 트레이싱을 자연스럽게 통합, speculative 트레이싱이 그러한 특징들이다. 실제 제품화된 시스템에서 중요한 성능 문제를 DTrace를 사용해서 찾아냈었다. DTrace 전에 나온 도구들로는 아마도 이 문제를 해결할 수 없었을 것이다.


References

[1] Mikhail Auguston, Clinton Jeffery, and Scott Underwood. A monitoring language for run time and post-mortem behavior analysis and visualization. In 5th International Workshop on Automated and Algorithmic Debugging, Ghent, Belgium, 2003.
[2] Brian Bershad, Stefan Savage, Przemyslaw Pardyak, Emin Gun Sirer, David Becker, Marc Fiuczynski, Craig Chambers, and Susan Eggers. Extensibility, safety and performance in the SPIN operating system. In Proceedings of the 15th ACM Symposium on Operating System Principles, 1995.
[3] R. Hastings and B. Joyce. Purify: Fast detection of memory leaks and access errors. In Proceedings of the Winter USENIX Conference, 1992.
[4] Jeffrey K. Hollingsworth, Barton P. Miller, Marcelo J. R. Gonçalves, Oscar Naim, Zhichen Xu, and Ling Zheng. MDL: A language and compiler for dynamic program instrumentation. In Proceedings of the 1997 International Conference on Parallel Architectures and Compilation Techniques, November 1997.
[5] Eric F. Johnson and Kevin Reichard. Professional Graphics Programming in the X Window System. MIS Press, Portland, OR, 1993.
[6] Gregor Kiczales, Erik Hilsdale, Jim Hugunin, Mik Kirsten, Jeffrey Palm, and William G. Griswold. An overview of AspectJ. In Proceedings of the 15th European Conference on Object-Oriented Programming, 2001.
[7] Barton P. Miller, 2003. Personal communication.
[8] Barton P. Miller, Mark D. Callaghan, Jonathan M. Cargille, Jeffrey K. Hollingsworth, R. Bruce Irvin, Karen L. Karavanic, Krishna Kunchithapadam, and Tia Newhall. The Paradyn parallel performance measurement tool. IEEE Computer, 28(11):37-46, 1995.
[9] Richard J. Moore. A universal dynamic trace for Linux and other operating systems. In Proceedings of the FREENIX Track, June 2001.
[10] M. I. Seltzer, Y. Endo, C. Small, and K. A. Smith. Dealing with disaster: Surviving misbehaved kernel extensions. In Proceedings of the Second Symposium on Operating Systems Design and Implementation, 1996.
[11] Amitabh Srivastava and Alan Eustace. ATOM: A system for building customized program analysis tools. In Proceedings of the ACM Symposium on Programming Languages Design and Implementation, 1994.
[12] Sun Microsystems, Santa Clara, California. Solaris Dynamic Tracing Guide, 2004.
[13] Ariel Tamches and Barton P. Miller. Fine-grained dynamic instrumentation of commodity operating system kernels. In Proceedings of the Third Symposium on Operating Systems Design and Implementation, 1999.
[14] Robert W. Wisniewski and Bryan Rosenburg. Efficient, unified, and scalable performance monitoring for multiprocessor operating systems. In SC'2003 Conference CD, 2003.
[15] Zhichen Xu, Barton P. Miller, and Oscar Naim. Dynamic instrumentation of threaded applications. In Proceedings of the 7th ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming, 1999.
[16] Karim Yaghmour and Michel R. Dagenais. Measuring and characterizing system behavior using kernel-level event logging. In Proceedings of the 2000 USENIX Annual Technical Conference, 2000.
[17] Tom Zanussi, Karim Yaghmour, Robert Wisniewski, Richard Moore, and Michel Degenais. relayfs: An efficient unified approach for transmitting data from kernel to user space. In Proceedings of the Ottawa Linux Symposium 2003, July 2003.


Footnotes :1Examples of such misuse include erroneously specifying a non-instruction boundary to instrument or specifying an action that incorrectly changes register values.
2For example, rescheduling during data recording can silently corrupt the data buffer.
3In particular, Kerninst on SPARC makes no attempt to recognize text as being executed at TL=1 or TL > 1 - two highly constrained contexts in the SPARC V9 architecture. Instrumenting such text with Kerninst induces an operating system panic. This has been communicated to Miller et al.; a solution is likely forthcoming[7].
4There do exist some actions that change the state of the system, but they change state only in a well-defined way (e.g. stopping the current process, or inducing a kernel breakpoint). These destructive actions are only permitted to users with sufficient privilege, and can be disabled entirely.
5Consumers may also reduce drops by increasing the size of in-kernel buffers.
6DProbes addressed this problem by allowing loops but introducing a user-tunable, "jmpmax," as an upper-bound on the number of jumps that a probe handler may make.
7In the absence of the sched provider, we would have enabled the FBT probe in the kernel's routine to awaken another thread, "sleepq_unlink()" - but using the well-defined sched provider[12] requires no kernel implementation knowledge.