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
}



전체 코드

https://github.com/hyobins/node-exporter-with-container