단명 프로세스 CPU 측정 정확도 개선 — 커널·에이전트 시각 동기화와 PID→PUID 전환

1시간 주기 sprocess 상시 전송에서 짧은 수명 프로세스의 CPU 값이 0으로 버려지거나 과대 계상되던 문제를, 커널이 프로세스·시스템 시간을 같은 시점에 함께 읽어 보내고 식별 기준을 PID에서 PUID로 바꿔 해결한 커널 드라이버·에이전트 트러블슈팅 기록.

개요

sprocess를 1시간 주기로 상시 전송할 때 프로세스 CPU 사용률이 부정확하게 잡히는 문제가 있었다. 특히 실행되자마자 곧바로 종료되는 짧은 수명의 프로세스에서 CPU 값이 0으로 버려지거나 실제보다 크게 잡히는 일이 있었다.

원인은 CPU 계산에 필요한 두 값, 즉 프로세스가 쓴 시간과 전체 시스템이 쓴 시간을 서로 다른 시점에 읽어 기준이 어긋난 데 있었다. 이번 작업에서 커널 드라이버가 프로세스 시작과 종료 순간에 두 값을 같은 시점에 함께 읽어 보내고, 에이전트는 그 값을 그대로 사용하도록 정리했다. 아울러 프로세스를 식별하는 기준을 PID에서 PUID로 바꿔 짧은 수명 프로세스의 식별 오류를 바로잡았다.

기존 흐름과 바뀐 흐름

기존 흐름

  • 커널은 프로세스가 종료될 때 전체 시스템 시간은 보내지 않고 프로세스 시간만 보냈다.
  • 에이전트는 메시지를 받은 시점에 스스로 시스템 시간을 다시 읽어 채우고, 프로세스 시간도 자기 핸들로 다시 읽었다.
  • 그 결과 프로세스 시간은 방금 읽은 값인데 시스템 시간은 메시지를 받은 더 늦은 시점의 값이라, 두 값의 기준 시점이 어긋났다. 수명이 짧은 프로세스일수록 이 어긋남이 CPU 값 전체를 왜곡했다.

바뀐 흐름

  • 커널이 프로세스 시작과 종료 순간에 프로세스 시간과 시스템 시간을 한 번에 함께 읽어 보낸다.
  • 에이전트는 시작과 종료 이벤트에서 커널이 보낸 값을 그대로 쓰고, 스스로 다시 읽지 않는다.
  • 두 값이 같은 시점에서 나오므로 기준이 맞는다. 커널이 값을 구하지 못하면 0을 보내며, 이때는 시작과 종료의 차이가 0이 되어 CPU를 0으로 본다. 억지로 값을 채워 오차를 만들지 않는다.

외부 영향도

서버, REST API, MQTT 메시지 형식과 토픽, sprocess 저장 구조는 모두 그대로다. 저장되는 CPU 지표의 정확도만 좋아진다. 다만 커널 드라이버가 바뀌었으므로 재빌드와 서명, 배포가 필요하다.

변경 범위

커널 (ofilter)

  • ofilter/CProcessTable.h — 프로세스를 등록할 때 시스템 시간을 읽은 뒤 메모리를 0으로 지워 값이 사라지던 순서를 바로잡았다. 먼저 0으로 초기화하고, 그 다음에 시스템 시간을 읽는다.
  • ofilter/Process.cpp — 프로세스 종료 순간에 프로세스 시간, 시스템 시간, 성능 카운터를 함께 읽어 메시지에 실어 보내도록 했다.

에이전트 (yagent21)

  • yagent21/CProcessCallback2.cpp — 메시지를 받자마자 시스템 시간을 다시 읽어 커널 값을 덮어쓰던 코드를 제거했다.
  • yagent21/CProcess2.cpp — 시작과 종료 이벤트에서 커널이 보낸 값을 그대로 쓰도록 했고, 프로세스 식별을 PID에서 PUID로 바꿨으며, 부모 프로세스를 안전하게 찾는 FindProcessPtr를 추가했다.
  • yagent21/CProcess2.h — 프로세스 캐시의 조회용 자료구조와 함수 인자를 PUID(64비트) 기준으로 바꿨다.
  • yagent21/CProcessCallback2.h — PUID로 프로세스를 찾는 함수를 노출했다.

주요 변경 사항

커널이 CPU 계산 기준 값을 한 곳에서 정한다. 프로세스 시작과 종료 시점에 프로세스 시간과 시스템 시간을 같은 순간에 함께 읽어 보낸다.

에이전트는 커널 값을 그대로 신뢰한다. 스스로 다시 재지 않으므로, 재측정 때문에 두 값의 시점이 어긋나던 문제가 사라졌다. 커널이 값을 구하지 못하면 0을 보내고, 에이전트는 그 경우 CPU를 0으로 본다. 값을 억지로 채워 넣지 않는다.

프로세스 식별 기준을 PID에서 PUID로 바꿨다. 수명이 짧은 프로세스는 에이전트가 시작을 처리하기도 전에 이미 종료돼 핸들을 열지 못하고, 그 사이 같은 PID가 다른 프로세스에 재사용될 수 있다. PID만으로는 프로세스를 정확히 구분할 수 없어, 프로세스마다 고유한 PUID(윈도우가 부여하는 process start key)로 조회하도록 했다.

이 과정에서 두 가지 결함도 함께 고쳤다.

  1. 종료된 프로세스를 보관 목록에서 지울 때 PUID로 관리되는 목록을 PID로 지우려 해서 실제로는 아무것도 지워지지 않았다.
  2. 프로세스 조회 함수가 64비트인 PUID를 32비트로 받아 값의 뒷부분이 잘려 나갔다.

두 곳 모두 PUID를 64비트 그대로 다루도록 맞췄다.

부모 프로세스 조회의 잠금 문제. 부모 프로세스를 찾는 부분이 잠금 밖에서 공용 목록을 건드리고 있어, 프로세스 종료 정리가 그 목록을 통째로 비우는 순간과 겹치면 문제가 될 수 있었다. 부모를 찾을 때만 잠깐 잠금을 잡아 그 프로세스를 참조로 확보하고, 시간이 걸리는 나머지 작업은 잠금을 놓은 뒤에 하도록 바꿔 충돌과 교착을 모두 피했다.

리뷰 포인트

  • 수명이 짧은 프로세스에서 시작과 종료의 시스템 시간이 같으면 CPU를 0으로 본다. 이는 의도한 동작이며, 커널이 값 자체를 구하지 못하는 극히 짧은 프로세스는 측정을 포기한다.
  • 프로세스 캐시 등록이 준비를 모두 마친 뒤로 미뤄져, 등록 직전에 도착한 초기 이벤트는 프로세스 정보가 붙지 않을 수 있다. 이 부분은 감수하기로 했다.
  • 커널 드라이버가 바뀌었으므로 재빌드와 서명이 반드시 필요하다.

배경 — phase1·phase2에서 드러난 문제

이 작업(phase3)은 앞선 두 단계에서 드러난 짧은 수명 프로세스의 CPU 과대·누락 문제가 출발점이다. 1시간 단위 집계 인프라를 구축(phase1)하고 버킷 경계 오차와 식별자 파편화를 다듬는(phase2) 과정에서, 단명 프로세스의 CPU 측정 정확도가 마지막 과제로 남았다. 근본 원인이 "두 시간 값의 측정 시점 불일치"와 "PID 재사용"이라는 것을 확인하고, 측정 기준을 커널 한 곳으로 모으고 식별자를 PUID로 통일해 마무리했다.

Orange Platform 에이전트의 프로세스 성능 측정 정확도 개선 기록입니다.