Development of NodeExporter
that collect hardware resource by container
Node Exporter는 다양한 하드웨어 상태와 커널관련 메트릭을 수집하는 모니터링 도구이다.
전체적인 구조와 분석은 이전 게시물에 정리 해 두었다.
해당 게시물에서는 prometheus 오픈소스 node exporter에 추가적인 코드를 작성하여
container별 하드웨어 리소스를 얻기위한 collector를 등록하고, 원하는 메트릭 정보를 수집하는 데 사용된 주요 함수와 개념들을 정리하였다.
Metrics
CPU, Memory, Network
# CPU
cat /proc/$PID/stat
fields Info
pid, comm, state, ppid, pgrp, seeeion, tty_nr, tpgid, flags, minflt, cminflt, majflt, cmajflt, utime, stime, cutime, cstime, proitiry, nice, num_threads, iteralvalue, starttime, vsize, rss, rsslim, startcode, endcode, ....
utime(14) + cutime(16) = user
stime(15) + cstime(17) = sys
CPU 사용량 계산 방법 : (cur(value) - prev(value))/(cur(time) - prev(time)) = usage persent(%)
따라서 14, 15, 16, 17 컬럼 데이터 파싱하여 전달
# Memory
cat /proc/$PID/status
vmSize : [ top VIRT ]
VmRSS : [ top RES ]
RssFile : [ top SHR ]
# Network
cat /proc/$PID/net/dev
Inter-| Receive | Transmit
face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
eth0: 487276 3778 0 0 0 0 0 0 390223 3727 0 0 0 0 0 0
lo: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rxBytes, rxPackets, txBytes, txPackets 파싱하여 전달
1. docker api로 container 정보 받고 container별 pid list 파싱
현재 실행되고있는 컨테이너의 정보를 가져오기 위해서는 docker api를 활용할 수 있다.
container type은 id, name, command, state 등의 값을 갖는다.
참고 : https://github.com/moby/moby/blob/master/api/types/types.go
//컨테이너 정보 받기
ctx := context.Background()
cli, _ := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
containers, err := cli.ContainerList(ctx, types.ContainerListOptions{})
if err != nil {
panic(err)
}
//컨테이너 별 pid 목록 파싱하여 slice에 저장
for _, container := range containers {
m := make(map[string]interface{})
pidpath := exec.Command("bash", "-c", "cd /run/docker/runtime-runc/moby/"+container.ID+" && cat state.json")
outputPath, _ := pidpath.Output()
err := json.Unmarshal(outputPath, &m)
if err != nil {
panic(err)
}
jsondata, _ := json.Marshal(m["cgroup_paths"].(map[string]interface{})["pids"])
pid, _ := (exec.Command("bash", "-c", "cd "+string(jsondata)+" && cat tasks")).Output()
slice := strings.Split(string(pid), "\n")
}
참고 :
실제 cmd 에시
[root@k8s-dev /]# cd /run/docker/runtime-runc/moby/${container-id}
[root@k8s-dev ${container-id}]# ls
state.json
[root@k8s-dev ${container-id}]# cat state.json | grep pids
"pids":"/sys/fs/cgroup/pids/system.slice/docker-${container-id}.scope"}
[root@k8s-dev ${container-id}]# cd /sys/fs/cgroup/pids/system.slice/docker-${container-id}.scope
[root@k8s-dev ${container-id}]# cat tasks
pids 목록 출력
단 이때 실제 pid가 아닌 경우도 있으니 golang의 directory exits 통해 확인하기
2. 출력 형식 맞추기
형식
{구조체 필드}: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "{출력할 리소스 이름}"),
"{설명}",
[]string{라벨 이름}, nil,
),
활용
return &containerCollector{
logger: logger,
utime: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "container_cpu_utime"),
"Current CPU utime by container.",
[]string{"id", "name", "type", "pid"}, nil,
),
[생략]
2. pid별 CPU, Memory, Network 정보 받아와서 채널에 저장
형식
ch <- prometheus.MustNewConstMetric(
{출력 형식으로 활용할 구조체 요소},
prometheus.GaugeValue, {출력값}, {라벨 값},
)
활용
cpuStatList := getCpuStat(slice)
for id, list := range cpuStatList {
ch <- prometheus.MustNewConstMetric(
c.utime,
prometheus.GaugeValue, list.utime, container.ID, containerName, "utime", id,
)
[생략]
}
memoryStatList := getMemoryStat(slice)
for id, list := range memoryStatList {
ch <- prometheus.MustNewConstMetric(
c.vmsize,
prometheus.GaugeValue, list.VmSize, container.ID, containerName, "vmsize", id,
)
[생략]
}
//Network 정보는 pid에 관계없이 모두 동일하므로 가장 앞에있는 pid를 통해 정보를 받아온다.
networkStatList := getNetworkStat(slice[0])
ch <- prometheus.MustNewConstMetric(
c.rxBytes,
prometheus.GaugeValue, networkStatList.rx_bytes, container.ID, containerName, "rx_bytes",
)
[생략]
3. 정보를 받아오는 함수
getCpuStat
golang의 cli 함수를 사용하여 stat정보를 받아서
pid를 id로 갖는 ContainerCpuStat 구조체 형식으로 저장
(pid의 갯수만큼 반복)
//pids 슬라이스의 마지막 요소는 ""이기 때문에 마지막 요소는 제외하고 그 앞 원소까지 반복해준다.
for i := 0; i < len(pids)-1; i++ {
//exec패키지의 Command 함수를 활용하여 cli 실행
cpus, err := (exec.Command("bash", "-c", "cat /proc/"+pids[i]+"/stat")).Output()
if err != nil {
panic(err)
}
cpu := strings.Split(string(cpus), " ")
utime, _ := strconv.ParseFloat(cpu[13], 64)
stime, _ := strconv.ParseFloat(cpu[14], 64)
cutime, _ := strconv.ParseFloat(cpu[15], 64)
cstime, _ := strconv.ParseFloat(cpu[16], 64)
stat = ContainerCpuStat{
pid: pids[i],
utime: utime,
stime: stime,
cutime: cutime,
cstime: cstime,
}
resultList[pids[i]] = stat //pid를 id값으로 가짐
}
getMemoryStat
for i := 0; i < len(pids)-1; i++ {
lines, err := readLines("/proc/" + pids[i] + "/status")
if err != nil {
panic(err)
}
//string에서 숫자요소만 남기고 나머지는 제거
re := regexp.MustCompile("[0-9]+")
m1 := re.FindAllString(string(lines[13]), -1)
m2 := re.FindAllString(string(lines[17]), -1)
m3 := re.FindAllString(string(lines[19]), -1)
vmsize, _ := strconv.ParseFloat(strings.Join(m1, ""), 64)
vmrss, _ := strconv.ParseFloat(strings.Join(m2, ""), 64)
rssfile, _ := strconv.ParseFloat(strings.Join(m3, ""), 64)
stat = ContainerMemoryStat{
pid: pids[i],
VmSize: vmsize,
VmRss: vmrss,
RssFile: rssfile,
}
resultList[pids[i]] = stat
}
getNetworkStat
func getNetworkStat(pid string) ContainerNetworkStat {
var resultList ContainerNetworkStat
stat := ContainerNetworkStat{}
lines, err := readLines("/proc/" + pid + "/net/dev")
if err != nil {
panic(err)
}
nw := strings.Fields(string(lines[2]))
rx_bytes, _ := strconv.ParseFloat(nw[1], 64)
rx_packets, _ := strconv.ParseFloat(nw[2], 64)
tx_bytes, _ := strconv.ParseFloat(nw[9], 64)
tx_packets, _ := strconv.ParseFloat(nw[10], 64)
stat = ContainerNetworkStat{
rx_bytes: rx_bytes,
rx_packets: rx_packets,
tx_bytes: tx_bytes,
tx_packets: tx_packets,
}
resultList = stat
return resultList
}