반응형

아래 예시는 Linux OS에 Hugepage를 적용하는 절차이다.

##
## /etc/default/grub 파일에서  
## default_hugepagesz, hugepagesz, hugepages, transparent_hugepage
## 항목을 추가 설정한다.
## (아래 예시를 참고)
##

$ cat  /etc/default/grub

GRUB_TIMEOUT=5
GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)"
GRUB_DEFAULT=saved
GRUB_DISABLE_SUBMENU=true
GRUB_TERMINAL_OUTPUT="console"
GRUB_CMDLINE_LINUX="crashkernel=auto rhgb quiet default_hugepagesz=1GB hugepagesz=1G hugepages=320 transparent_hugepage=never"
GRUB_DISABLE_RECOVERY="true"
GRUB_ENABLE_BLSCFG=true

$

##
## 위와 같이 /etc/default/grub 파일을 수정하고,
## grub2 명령으로 설정을 적용한다.
##

## Case: UEFI Mode
$ grub2-mkconfig -o /boot/efi/EFI/redhat/grub.cfg

## Case: Legacy BIOS Mode
$ grub2-mkconfig -o /boot/grub2/grub.cfg

##
## 위와 같이 설정 작업을 마무리했으면, OS를 Reboot하여 설정을 반영한다.
##

$ reboot
반응형

IT 개발자, 운영자는 처음부터 Kubernetes, Helm, Istio 등 Cloud와 관련된 어휘를 IT 용어로 받아들였기 때문에 이 어휘의 어원이 무엇인지 잘 모를 것이다.

대부분 그리스어(Greek)에서 온 어휘이고, 바다 또는 배와 관련된 것이 많다.

 

Kubernetes

뜻은 키잡이, 조타수(Steerman, Helmsman)이고, 그리스어에서 온 어휘이다.

(내가 그리스어를 몰라서, 정확히 이것인지는 모르겠지만 "πηδαλιούχος"가 그나마 어감이 비슷하다)

 

 

 

Helm

뜻은 조타 장치.

 

 

Istio (ιστιο)

원래 그리스어이고, 그리스어 식 표기는 "ιστιο"이다.

Istio 뜻은 '항해, 돛'이다.

ιστιο에 대한 발음은 아래 링크를 클릭하면 들을 수 있다.

 

Google 번역

사용 중인 브라우저에서는 음성 출력이 지원되지 않습니다.

translate.google.com

 

Quay

배를 정박시킬 수 있는 부두, 선착장.

 

 

Docker

부두(Dock)에서 일하는 사람. 항만(Dock) 노동자.

 

 

 

반응형

Go Language로 특정 Process의 CPU, Memory 사용량을 계산하고 싶다면,

아래의 코드를 Build해서 사용하면 된다.

 

참고: Linux, MacOS, Unix, Windows 모두 동작하는 코드임.

 

 

// Filename: proc_usage.go

package main

import (
	"errors"
	"fmt"
	"io/ioutil"
	"math"
	"os/exec"
	"path"
	"runtime"
	"strconv"
	"strings"
	"sync"
)

const (
	statTypePS   = "ps"
	statTypeProc = "proc"
)

// SysInfo will record cpu and memory data
type SysInfo struct {
	CPU    float64
	Memory float64
}

// Stat will store CPU time struct
type Stat struct {
	utime  float64
	stime  float64
	cutime float64
	cstime float64
	start  float64
	rss    float64
	uptime float64
}

type fn func(int) (*SysInfo, error)

var fnMap map[string]fn
var platform string
var history map[int]Stat
var historyLock sync.Mutex
var eol string

// Linux platform
var clkTck float64 = 100    // default
var pageSize float64 = 4096 // default

func init() {
	platform = runtime.GOOS
	if eol = "\n"; strings.Index(platform, "win") == 0 {
		platform = "win"
		eol = "\r\n"
	}
	history = make(map[int]Stat)
	fnMap = make(map[string]fn)
	fnMap["darwin"] = wrapper("ps")
	fnMap["sunos"] = wrapper("ps")
	fnMap["freebsd"] = wrapper("ps")
	fnMap["openbsd"] = wrapper("proc")
	fnMap["aix"] = wrapper("ps")
	fnMap["linux"] = wrapper("proc")
	fnMap["netbsd"] = wrapper("proc")
	fnMap["win"] = wrapper("win")

	if platform == "linux" || platform == "netbsd" || platform == "openbsd" {
		initProc()
	}
}

func initProc() {
	clkTckStdout, err := exec.Command("getconf", "CLK_TCK").Output()
	if err == nil {
		clkTck = parseFloat(formatStdOut(clkTckStdout, 0)[0])
	}

	pageSizeStdout, err := exec.Command("getconf", "PAGESIZE").Output()
	if err == nil {
		pageSize = parseFloat(formatStdOut(pageSizeStdout, 0)[0])
	}

}

func wrapper(statType string) func(pid int) (*SysInfo, error) {
	return func(pid int) (*SysInfo, error) {
		return stat(pid, statType)
	}
}

func formatStdOut(stdout []byte, userfulIndex int) []string {
	infoArr := strings.Split(string(stdout), eol)[userfulIndex]
	ret := strings.Fields(infoArr)
	return ret
}

func parseFloat(val string) float64 {
	floatVal, _ := strconv.ParseFloat(val, 64)
	return floatVal
}

func statFromPS(pid int) (*SysInfo, error) {
	sysInfo := &SysInfo{}
	args := "-o pcpu,rss -p"
	if platform == "aix" {
		args = "-o pcpu,rssize -p"
	}
	stdout, _ := exec.Command("ps", args, strconv.Itoa(pid)).Output()
	ret := formatStdOut(stdout, 1)
	if len(ret) == 0 {
		return sysInfo, errors.New("Can't find process with this PID: " + strconv.Itoa(pid))
	}
	sysInfo.CPU = parseFloat(ret[0])
	sysInfo.Memory = parseFloat(ret[1]) * 1024
	return sysInfo, nil
}

func statFromProc(pid int) (*SysInfo, error) {
	sysInfo := &SysInfo{}
	uptimeFileBytes, err := ioutil.ReadFile(path.Join("/proc", "uptime"))
	if err != nil {
		return nil, err
	}
	uptime := parseFloat(strings.Split(string(uptimeFileBytes), " ")[0])

	procStatFileBytes, err := ioutil.ReadFile(path.Join("/proc", strconv.Itoa(pid), "stat"))
	if err != nil {
		return nil, err
	}
	splitAfter := strings.SplitAfter(string(procStatFileBytes), ")")

	if len(splitAfter) == 0 || len(splitAfter) == 1 {
		return sysInfo, errors.New("Can't find process with this PID: " + strconv.Itoa(pid))
	}
	infos := strings.Split(splitAfter[1], " ")
	stat := &Stat{
		utime:  parseFloat(infos[12]),
		stime:  parseFloat(infos[13]),
		cutime: parseFloat(infos[14]),
		cstime: parseFloat(infos[15]),
		start:  parseFloat(infos[20]) / clkTck,
		rss:    parseFloat(infos[22]),
		uptime: uptime,
	}

	_stime := 0.0
	_utime := 0.0

	historyLock.Lock()
	defer historyLock.Unlock()

	_history := history[pid]

	if _history.stime != 0 {
		_stime = _history.stime
	}

	if _history.utime != 0 {
		_utime = _history.utime
	}
	total := stat.stime - _stime + stat.utime - _utime
	total = total / clkTck

	seconds := stat.start - uptime
	if _history.uptime != 0 {
		seconds = uptime - _history.uptime
	}

	seconds = math.Abs(seconds)
	if seconds == 0 {
		seconds = 1
	}

	history[pid] = *stat
	sysInfo.CPU = (total / seconds) * 100
	sysInfo.Memory = stat.rss * pageSize
	return sysInfo, nil
}

func stat(pid int, statType string) (*SysInfo, error) {
	switch statType {
	case statTypePS:
		return statFromPS(pid)
	case statTypeProc:
		return statFromProc(pid)
	default:
		return nil, fmt.Errorf("Unsupported OS %s", runtime.GOOS)
	}
}

// GetStat will return current system CPU and memory data
func GetStat(pid int) (*SysInfo, error) {
	sysInfo, err := fnMap[platform](pid)
	return sysInfo, err
}

 

 

 

// Filename: main.go

package main

import (
    "os"
    "fmt"
    "time"
    "strconv"
)

func main() {
    myPid, _ := strconv.Atoi(os.Args[1])

    for i:= 0; i < 100; i++ {
        sysInfo, _ := GetStat(myPid)
        fmt.Println("CPU Usage     :", sysInfo.CPU)
        fmt.Println("Mem Usage(RSS):", sysInfo.Memory)
        time.Sleep(5 * time.Second)
    }
}

 

위와 같이 Go source code를 모두 작성했다면, 아래처럼 build하고 실행하면 된다.

 

$ go mod init andrew.space/proc_usage
go: creating new go.mod: module andrew.space/proc_usage
go: to add module requirements and sums:
	go mod tidy
    
$ go mod tidy

$ go build

$ ./proc_usage 4314
CPU Usage     : 52.92167225853122
Mem Usage(RSS): 2.018664448e+09
CPU Usage     : 39.800000000000004
Mem Usage(RSS): 2.018664448e+09
CPU Usage     : 47.30538922366738
Mem Usage(RSS): 2.018664448e+09
...
...

 

top 명령으로 본 것과 결과가 동일했다.

반응형

Kubernetes 1.23 이상 또는 OCP 4.10 이상을 사용하는 Cluster에서 Pod를 구동하다보면, 

Pod Status가 SMTAlignmentError 에러 상태가 되면서 구동하지 못하는 경우를 만난다.

 

CPU Pinning을 위해 아래처럼 Pod Spec을 설정한 경우에 볼 수 있는 에러이다.

아래 YAML 예시에서 cpu 개수를 5개 설정한 것이 문제를 발생시킨다. (홀수로 설명하는 것이 문제)

kind: Pod
metadata:
  name: myapp
spec:
... 중간 생략 ...
  containers:
    resources:
      limits:
        cpu: 5   ## 이렇게 홀수인 정수를 설정한 것이 에러를 발생시킴.
... 중간 생략 ...

 

관련 자료를 찾아보니, 아래 문서가 가장 설명을 잘 해주고 있다.

 

 

Best practices for avoiding noisy neighbor issues using cpu manager behaves wrt hyper-threading - Red Hat Customer Portal

Best practices for avoiding noisy neighbor issues using cpu manager behaves wrt hyper-threading

access.redhat.com

 

위 문서의 요지는 이렇다.

 

X86_64 CPU는 아래 그림처럼 1개의 물리 Core가 2개의 논리 CPU(Thread)로 구성되어 있고, 
이 2개의 논리 CPU(Thread)가 1개의 L2 Cache를 공유하기 때문에
만약 홀수 개로 CPU를 Pinning(즉, Isolation)하면, L2 Cache의 Hit Ratio가 확 떨어지기 때문에
Core의 처리 속도가 겁나게 떨어진다는 것이다.

즉, L2 Cache 1개를 두고 LCore-0과 LCore-1이 치고 박고 시끄럽게 싸우는 꼴~~~
X86_64 CPU 구조에는 늘상 발생하는 현상으로써, "noisy neighbors"라고 표현한다.

 

쉽게 이해하기 위해 일상 생활과 비유해본다면,

2명의 사람이 한 집에 살면서 요리를 하는데

주방이 1개라서 홍길동은 된장찌개(Job-A)를 만들어 먹고 싶고, 이순신은 김밥(Job-B)을 만들어 먹고 싶다면

홍길동이 된장찌개 요리를 마무리하고 주방(L2 Cache)를 비워줘야, 이순신이 그 주방(L2 Cache)에서 김밥을 만들 수 있는 것과 같다.

여기서 핵심은 "주방(L2 Cache)를 비워줘야" 한다에 있다.

Local Thread가 서로 다른 일을 할 경우, L2 Cache에 담을 내용이 서로 다르기 때문에 L2 Cache 메모리에 있는 데이터를 재사용할 수 없고(즉, Hit Ratio가 떨어지고) 실제 L2 Cache는 Cache 로써의 역할을 못하게 된다.

L2 Cache의 내용 싹~~~ 갈아 엎어버리고 다른 CPU Core가 해야 할 일과 관련된 데이터를 복사해야 하니까~~~

 

이렇기 때문에 비슷한 Job(프로그램, 또는 Process)에 대해서 L2 Cache를 같이 사용하도록 2개씩 쌍으로 할당하는 것이 최고의 성능을 낼 수 있다.

그럴 일은 없겠지만, 만약 논리 쓰레드 3개가 1개의 L2 Cache를 공유하는 CPU 제품이 있다면 3개씩 쌍으로 할당해야 최고의 성능을 낼 수 있다. (이것은 그냥 가정이다)

 

 

 

내 생각에는
처음부터 Intel x86 CPU가 Hyper Threading 구조가 아니였다면, 즉 Logical Core가 L2 Cache Memory를 공유하지 않는 구조였다면 CPU Pinning 설정할 때 짝수로 설정해야 하는 제약도 없었을 것 같다.

 

 

 

 

아래 그림은 Red Hat Web Docs에서 인용한 그림.

CPU Core 개수를 홀수로 설정하여 SMTAlignmentError 발생

 

CPU Core 개수를 짝수로 설정하여 CPU의 Virtual Thread가 L2 Cache 영역을 공유하지 않도록 함

 

 

반응형

 

##
## Cluter Network 설정 정보 보기
##

$ kubectl get network.config/cluster -o jsonpath='{.status}{"\n"}'
{"clusterNetwork":[{"cidr":"10.128.0.0/14","hostPrefix":23}],"clusterNetworkMTU":1450,"networkType":"OpenShiftSDN","serviceNetwork":["172.30.0.0/16"]}


##
## CNI Network Type 설정 정보 보기
##

$ oc get network.config/cluster -o jsonpath='{.status.networkType}{"\n"}'
OpenShiftSDN

$
반응형

아래 Script는 Kuberentes Node의 IP Address를 얻어와서, 이 IP Address로 Node에 SSH 접속하여 'shutdown' 명령을 수행하는 예제이다.

 

#!/bin/bash

for ip in $(oc get nodes  -o jsonpath='{.items[*].status.addresses[?(@.type=="InternalIP")].address}')
do
   echo "reboot node $ip"
   ssh -o StrictHostKeyChecking=no core@$ip sudo shutdown -r -t 3
done
반응형

Geneve (Generic Network Virtualization Encapsulation) protocol

 

Compare and Contrast VXLAN to GENEVE Encapsulation Format

 

GENEVE Variable Extension Header

 

 

Comparison Table: VXLAN vs Geneve

PARAMETER VXLAN GENEVE
Abbreviation for VXLAN (Virtual Extensible LAN) GENEVE (Generic Network Virtualization Encapsulation)
Developed by VMware, Arista Networks and Cisco VMware, Microsoft, Red Hat and Intel
Protocol UDP UDP
Port no 4789 6081
Header length 8 byte 16 byte
Transport security, service chaining, in-band telemetry Not supported Supported
RFC VXLAN is officially documented by the IETF in RFC 7348 RFC 8926
Protocol Identifier No Yes
non-client payload indication No Yes
Extensibility. No. Infact all fields in VXLAN header have predefined values Yes
Hardware friendly vendor extensibility mechanism Limited Yes
Term used for Tunnel Endpoints VTEP TEP
반응형

Kuberentes를 사용하면서 이해하기 어려운 부분이 CNI(Cluter Network Interface)이다.

실제로 각 CNI 마다 동작하는 방식과 구현이 다른데 CNI라고 퉁쳐서 표현하다보니 Kubernetes를 사용하는 운영자 입장에서는 CNI가 어떻게 IP Traffic(트래픽)을 처리하는지 알기 어렵다.

여러 종류의 CNI 중에서 내가 사용하고 있는 특정 CNI에 대해서 깊이 있게 설명하는 자료도 별로 없으니 스터디하기가 더욱 어렵다.

 

나의 일터에서는 주로 Openshift SDN을 사용하기 때문에 오늘은 시간을 내서 Openshift SDN을 깊게 파헤쳐보려 한다.

 

우선 아래 그림에 있는 빨간색 숫자를 따라 가면서 보자.

이 빨간 색 숫자의 순서대로 요청 패킷이 처리된다.

 

Openshift SDN 동작 방식

 

 

그림에 순서에 맞게 설명을 적었기 때문에 전반적인 설명은 필요 없을 것 같다.

단, 복잡한 처리가 있는 부분을 좀더 살펴 본다면,

 

(2) Netfilter를 이용하여 DNAT, SNAT 처리

Kubernetes Pod 정보를 조회해보면 Cluster IP를 볼 수 있다. 바로 그 Pod의 Cluster IP로 DNAT 처리되는 것이다.

즉, 10.10.12.208:8080 목적지 주소를 보고, 이 주소 값과 Mapping되는 Kubernetes Pod IP를 찾고 Netfilter를 이용하여 DNAT 처리한다.

Worker 1 Node에서 iptables 명령을 이용하여 Netfilter의 Chain Rule을 조회하면 실제로 아래와 같이 정보를 볼 수 있다.

이 Chain Rule대로 관련 Packet 조건을 찾고, 그 조건에 맞는 Netfilter Chain을 따라가면서 DNAT처리하는 것이다.

 

$  iptables -t nat -L KUBE-SERVICES -nv | grep 10.10.12.208
 1760  106K KUBE-FW-BAR7JSQD2GL6A2KT  tcp  --  *      *       0.0.0.0/0            10.10.12.208         /* almighty/almighty5:web loadbalancer IP */ tcp dpt:8080

… 중간 생략 …

$ iptables -t nat -L KUBE-SEP-ZBWV76PMI2XAVMCD -nv
 1949  117K DNAT       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            /* almighty/almighty5:web */ tcp to:10.128.2.151:8080

—> kube-proxy가 실제 iptables 룰 생성시 사용한 명령은 아래와 같음.
KUBE-SEP-ZBWV76PMI2XAVMCD -p tcp -m comment --comment "almighty/almighty5:web" -m tcp -j DNAT --to-destination 10.128.2.151:8080

 

만약 Destination Pod가 다른 Worker Node에 있다면, 다른 Worker Node로 IP Packet을 보내기 위해 tun0 인터페이스를 이용하여야 한다. 이때 tun0의 IP Address로 SNAT 하도록 하는 Chain Rule 적용을 받는다.

 

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
OPENSHIFT-MASQUERADE  all  --  anywhere             anywhere             /* rules for masquerading OpenShift traffic */
KUBE-POSTROUTING  all  --  anywhere             anywhere             /* kubernetes postrouting rules */

Chain KUBE-POSTROUTING (1 references)
target     prot opt source               destination
RETURN     all  --  anywhere             anywhere             mark match ! 0x1/0x1
MARK       all  --  anywhere             anywhere             MARK xor 0x1
MASQUERADE  all  --  anywhere             anywhere             /* kubernetes service traffic requiring SNAT */ random-fully

 

 

(3) DST IP로 갈 수 있는 Routing Table을 조회하여 tun0로 Packet을 보냄

아마 대부분 Kubernetes 사용자가 헷갈려하는 부분일 것이다.

만약 Destination Pod가 같은 Worker Node에 있다면, 단순히 Netfilter가 처리한 대로 Packet Forward만 하면 된다.

그런데, Destination Pod가 다른 Worker Node에 있다면 관련있는 물리 Network Port를 찾아서 보내야 한다.

위 그림에서 Worker 1과 Worker 2의 tun0 간에는 ARP Packet이 브로드캐스팅되므로, 이미 Worker 1에서는 Destination Pod IP에 해당하는 MAC Address를 알고 있다. 

(참고: arp 명령으로 Worker 1 노드의 ARP Table 조회 가능)

따라서 일반적인 L2 Forward 처리와 동일하게 처리된다.

 

 

(4) ~ (5) Encapsulate, Decapsulate

일반적인 VxLAN 터널링을 사용하여 Worker Node간에 Pod의 메시지를 전송해준다.

VxLAN 터널링 때문에 tun0의 MTU는 1450 byte가 된다. (왜냐하면, 터널링 헤더 메시지 때문에 50 Byte만큼 tun0 포트의 MTU가 작아지게 된다)

 

 

나머지 절차는 모두 L2 Switching이기 때문에 딱히 설명할 것이 없다.

 

응답 메시지는 따로 설명하지 않았다.

왜냐하면,

L2 Switching 구간은 단순히 Source Address와 Destination Address를 반대로 뒤집으면 되고,

Netfilter가 SNAT 처리한 부분의 Connection Track 정보를 찾아서 원래의 주소값으로 치환해주면 되기 때문이다.

 

 

블로그 작성자: sejong.jeonjo@gmail.com

 

+ Recent posts