컨테이너에 대해 공부를 해야겠다는 생각, 그리고 숙제로 미루어 왔던 Control Group에 대해 공부하자는 계획을 세운 뒤 꽤나 오랜 시간이 흘렀습니다. 링크드인의 글을 눈팅하던 중 우연히 “쿠버네티스가 쉬워지는 컨테이너이야기 시리즈” 게시글을 보게 되었고, 해당 글을 바탕으로 공부를 시작했습니다.

앞으로 작성할 cgroup 관련 게시글은 해당 글을 바탕으로 컨테이너에 대한 이해도를 높이고자 작성한 글 입니다.

원글의 출처는 아래와 같습니다.

쿠버네티스가 쉬워지는 컨테이너 이야기 — cgroup, cpu편 (https://medium.com/@7424069/쿠버네티스가-쉬워지는-컨테이너-이야기-cgroup-cpu편-c8f1e2208168), 천강진 님


이 글이 도움 될 사람

  1. 쿠버네티스가 쉬워지는 컨테이너 이야기 시리즈를 읽었지만 이해가 안되는 부분이 있는 사람
  2. 컨테이너가 리눅스의 union filesystem, cgroup, namespace를 이용해 만들어지는 것을 알고 있는 사람
  3. cgroup이 단순히 자원을 격리한다는 것만 알고 있는 사람

 

본 글의 마지막 부분에는 천강진 님의 이번 글의 대상이 아닌 독자에 대한 개인적인 견해를 작성해 놓았습니다.


컨테이너

컨테이너는 리눅스의 union fs (overlayfs), cgroup, namespace를 이용하여 host os의 메인 워크로드와 격리된 워크로드를 제공하는 가상화 솔루션입니다.

union filesystem을 이용하여 애플리케이션을 실행하기 위한 운영체제 환경부터 시작하여 서비스 바이너리까지 레이어로 나뉘어진 하나의 통합된 파일 시스템을 제공하여 언제 어디서든 동일한 환경을 제공하고 namespace를 이용하여 애플리케이션 프로세스를 격리하여 독립된 환경을 제공할 수 있다면,

control group은 이렇게 격리된 환경, 그리고 언제나 동일한 애플리케이션을 실행하기 위한 리눅스 프로세스 그룹에 시스템 자원을 격리, 할당하여 제어할 수 있도록 해 줍니다.

정리하자면,

  1. Union Filesystem을 통해 low layer, upper layer, merged view를 통해 레이어화 된 애플리케이션 실행에 필요한 파일시스템을 제공하고
  2. Namespace를 통해 애플리케이션의 실행 환경을 분리하며
  3. Control Group을 통해 애플리케이션 실행에 필요한 하드웨어 자원을 격리, 제한하여 독립된 환경에서 안정적으로 실행될 수 있도록 하는 가상화 기술입니다.

“천강진”님의 시리즈를 읽으면서 Control Group을 이해한다면 컨테이너 환경에서의 안정적으로 하드웨어 자원을 제공할 수 있을 뿐 아니라, 애플리케이션에 어떤 현상 혹은 문제가 발생했을 때 조금 더 넓은 각도에서 원인을 분석하고 문제를 해결할 수 있을 것이라 느꼈습니다.

Control Group은 /sys/fs/cgroup 아래에 논리 제어 그룹을 확인할 수 있으며 루트 제어 그룹 아래에 자식 제어 그룹이 존재하는 형태입니다. 새로운 논리 그룹을 생성하려면 대상의 루트, 혹은 자식 제어 그룹에 폴더를 생성하면 됩니다.

출처: 쿠버네티스가 쉬워지는 컨테이너 이야기 — cgroup, cpu편, 천강진님

A라는 논리 그룹을 새롭게 만들 경우, 리눅스 시스템에 의해 해당 디렉토리 아래에 자동으로 다음과 같은 구조가 추가됩니다.

단순히 논리 그룹을 만듦으로써 프로세스 격리된 제어 그룹 공간을 활용하는 것은 아닙니다. 생성된 제어 그룹에 실행중인 프로세스의 ID를 할당함으로써 실질적인 제어 그룹을 이용하게 됩니다.

물론 아직 제어 그룹에 대해 자원에 대한 제한을 설정하지 않았기 때문에 아직은 루트 제어 그룹을 그대로 따르는 상태입니다.


Docker의 Control Group과 CPU

잠깐! macOS의 경우 /sys/fs/cgroup을 찾을 수 없습니다. 아래의 명령어를 통해 host의 /sys/fs/cgroup을 확인할 수 있는 컨테이너를 실행해 주세요.

docker run --rm -d -it \
  --privileged --pid=host \
  --name cgroup-host \
  justincormack/nsenter1

예제에서 사용된 컨테이너의 경우 아래 명령어로 생성할 수 있습니다.

docker run -d --rm --privileged --name cgroup docker:27.3.1-dind-alpine3.20

도커는 컨테이너를 생성할 때, 하나의 논리 제어 그룹을 생성합니다. 도커를 운영중인 환경에서 /sys/fs/cgroup을 확인해보면 아래와 같이 docker 디렉토리가 존재하는 것을 확인할 수 있습니다.

/sys/fs/cgroup # ls
cgroup.controllers               cpu.weight.nice                  memory.low
cgroup.events                    cpuset.cpus                      memory.max
cgroup.freeze                    cpuset.cpus.effective            memory.min
cgroup.kill                      cpuset.cpus.exclusive            memory.oom.group
cgroup.max.depth                 cpuset.cpus.exclusive.effective  memory.peak
cgroup.max.descendants           cpuset.cpus.partition            memory.reclaim
cgroup.procs                     cpuset.mems                      memory.stat
cgroup.stat                      cpuset.mems.effective            memory.swap.current
cgroup.subtree_control           **docker**                           memory.swap.events
cgroup.threads                   **init.scope**                       memory.swap.high
cgroup.type                      io.max                           memory.swap.max
cpu.idle                         io.stat                          memory.swap.peak
cpu.max                          **k3s**                              pids.current
cpu.max.burst                    memory.current                   pids.events
cpu.stat                         memory.events                    pids.events.local
cpu.stat.local                   memory.events.local              pids.max
cpu.weight                       memory.high                      pids.peak
/sys/fs/cgroup #

이후 docker 아래의 경로를 확인해보면, 실제로 실행중인 container id 이름으로 제어 그룹이 각각 생성되어 있는것 또한 확인할 수 있습니다.

/sys/fs/cgroup # cd docker
/sys/fs/cgroup/docker # ls
5d16df9b31de84920181fc18e72b4fb5725c9353b3b27f5a758e41cbb039eb54
b1a5dc151e15a4373d30adbc920e8e9ced70e1632d3027c2c4acc55efaeb2395
ba834863df92a0f9919c98e4829d6fb8a4dbdf66e81507f103e75dd710a4c566
cgroup.controllers
cgroup.events
cgroup.freeze
cgroup.kill
cgroup.max.depth
cgroup.max.descendants
cgroup.procs
cgroup.stat
cgroup.subtree_control
cgroup.threads
cgroup.type
cpu.idle
cpu.max
cpu.max.burst
cpu.stat
cpu.stat.local
cpu.weight
cpu.weight.nice
cpuset.cpus
cpuset.cpus.effective
cpuset.cpus.exclusive
cpuset.cpus.exclusive.effective
cpuset.cpus.partition
cpuset.mems
cpuset.mems.effective
e1277fe09be51e878f1b38bb4936982295c68f1f0b0429f9c4ab8319494cc6a5
io.max
io.stat
memory.current
memory.events
memory.events.local
memory.high
memory.low
memory.max
memory.min
memory.oom.group
memory.peak
memory.reclaim
memory.stat
memory.swap.current
memory.swap.events
memory.swap.high
memory.swap.max
memory.swap.peak
pids.current
pids.events
pids.events.local
pids.max
pids.peak
/sys/fs/cgroup/docker #

아래는 Host OS에서 docker ps 명령어를 입력했을 때 보이는 컨테이너 입니다.

$ docker ps
CONTAINER ID   IMAGE                           COMMAND                   CREATED         STATUS         PORTS                                                  NAMES
b1a5dc151e15   justincormack/nsenter1          "/usr/bin/nsenter1"       3 minutes ago   Up 3 minutes                                                          cgroup-host
e1277fe09be5   docker:27.3.1-dind-alpine3.20   "dockerd-entrypoint.…"    7 minutes ago   Up 7 minutes   2375-2376/tcp                                          cgroup
5d16df9b31de   mysql:8.0                       "docker-entrypoint.s…"    4 days ago      Up 7 minutes   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   mysql
ba834863df92   redis:7.4                       "docker-entrypoint.s…"    4 days ago      Up 7 minutes   0.0.0.0:6379->6379/tcp, :::6379->6379/tcp              redis

우리가 생성한 예제의 경우 e1277fe09be5로 실행되고 있는 것을 확인할 수 있으며, 현재 아무 자원 제한을 두지 않았기 때문에 cpu의 max, 가중치를 Host OS에서 확인해 봤을 때 모두 기본 값임을 확인할 수 있습니다.

/sys/fs/cgroup/docker # cat e1277fe*/cpu.weight
100
/sys/fs/cgroup/docker # cat e1277fe*/cpu.max
max 100000
/sys/fs/cgroup/docker # cat e1277fe*/cpu.stat
usage_usec 2073096
user_usec 1489344
system_usec 583751
nr_periods 0
nr_throttled 0
throttled_usec 0
nr_bursts 0
burst_usec 0

컨테이너를 실행할 때 cpu 할당량을 설정한다면 아래와 같은 변화를 확인할 수 있습니다.

$ docker stop cgroup
$ docker run -d --rm --privileged --cpus 0.5 --name cgroup docker:27.3.1-dind-alpine3.20
8292ce42483c59539655d0f727d771cadee933a91c74fe92b53feec96a0b31cd
  • 아래 코드는 Host OS에서 실행한 결과입니다.
/sys/fs/cgroup/docker # cat 8292ce*/cpu.weight
100
/sys/fs/cgroup/docker # cat 8292ce*/cpu.max
50000 100000
/sys/fs/cgroup/docker # cat 8292ce*/cpu.stat
usage_usec 1927030
user_usec 1741681
system_usec 185349
nr_periods 113
nr_throttled 35
throttled_usec 1735216
nr_bursts 0
burst_usec 0

control group에서, 다시 말해 컨테이너에서 cpu 자원을 제한한다는 의미는 CPU 스케줄링에 있어서 얼만큼 자원을 선점할 수 있느냐를 의미하는데, 다시 말해 --cpus 0.5를 설정하는 행위는 100ms 동안 50ms를 선점할 수 있다는 것을 의미하게 됩니다.

  • 해당 부분이 이해되지 않으신다면, 운영 체제의 CPU 스케줄링에 대해 찾아보시면 좋을것 같습니다.

만약 1개를 넘어가면 어떻게 될까요? 간단하게 하기 위해 cpu 개수를 2로 할당한다면, 다음과 같은 결과가 나오게 됩니다.

$ docker stop cgroup
$ docker run -d --rm --privileged --cpus 2 --name cgroup docker:27.3.1-dind-alpine3.20
68b5952c3487249c14eb48c9649d916475ae7d4593386c9e234d90ba0a92d9f1

---

/sys/fs/cgroup # cd docker
/sys/fs/cgroup/docker # cat 68b5952*/cpu.max
200000 100000
$ docker stop cgroup
$ docker run -d --rm --privileged --cpus 2 --name cgroup docker:27.3.1-dind-alpine3.20
68b5952c3487249c14eb48c9649d916475ae7d4593386c9e234d90ba0a92d9f1

---

/sys/fs/cgroup # cd docker
/sys/fs/cgroup/docker # cat 68b5952*/cpu.max
200000 100000

100ms 동안 200ms를 선점할 수 있다는 것이 이상하다 느껴질 수 있습니다.

실제 애플리케이션이 동작하는 환경은 다수의 CPU를 가지는 환경이기 때문에, 단일 CPU의 환경이 아닌 이상 여러 CPU를 통해 각각 100%를 선점하여 200ms를 달성하게 됩니다.

그렇기 때문에 컨테이너 플랫폼 혹은 오케스트레이션 환경에서 cpu 자원을 요청하거나 제한할 때 실제 cpu의 core 수를 넘어서 요청할 수 없게 됩니다.


쿠버네티스의 cpu.weight (cpu.shares)

cpu.weight은 control group을 통해 생성하는 프로세스의 cpu 가중치를 설정할 수 있습니다. 일반적으로 도커, crio 등 컨테이너 플랫폼 환경을 실행한다면 cpu.weight은 일반적으로 100으로 설정되는 것을 확인할 수 있습니다. 이 경우 “천강진”님의 예제를 통해 cpu.weight을 쉽게 이해하실 수 있습니다.

하지만 실제 컨테이너 오케스트레이션 환경(쿠버네티스 등)이나 SaaS, PaaS로 제공되는 환경의 경우 cpu.weight 값이 100으로 설정되어 있지 않는 경우도 있는데, 이 경우 실행하는 서비스 혹은 환경에 의해 cpu.weight 값이 계산되어 할당됩니다. 이는 실제 CPU 리소스를 할당할 때 상대적인 CPU 우선순위를 설정하는 cpu.weight의 특징이자 영향을 받았기 때문입니다.

아래는 온프레미스 환경에 쿠버네티스를 설치하고 제어 그룹 디렉토리로 이동했을 때의 상태입니다. 아직 아무 파드도 설치하지 않았으며, crio v1.31과 쿠버네티스 1.31.2를 설치한 직후의 상태입니다.

root@worker1:/sys/fs/cgroup # ls
A                       cgroup.procs            cpu.pressure           dev-hugepages.mount  io.cost.qos    kubepods.slice    misc.capacity                  sys-kernel-debug.mount
cgroup.controllers      cgroup.stat             cpu.stat               dev-mqueue.mount     io.pressure    memory.numa_stat  proc-sys-fs-binfmt_misc.mount  sys-kernel-tracing.mount
cgroup.max.depth        cgroup.subtree_control  cpuset.cpus.effective  init.scope           io.prio.class  memory.pressure   sys-fs-fuse-connections.mount  system.slice
cgroup.max.descendants  cgroup.threads          cpuset.mems.effective  io.cost.model        io.stat        memory.stat       sys-kernel-config.mount        user.slice
root@worker1:/sys/fs/cgroup # cat kubepods.slice/cpu.weight
79

쿠버네티스의 경우 cpu.weight의 값이 79인 이유는 Control Group V1에서 V2로 넘어오면서 cpu.shares를 cpu.weight로 변환하는 과정에서 스케일링이 발생했기 때문입니다.

  • cpu.shares는 실질적으로 가중치를 할당하여 사용한다기 보다 cpu.shares가 설정된 제어 그룹 간의 비율을 바탕으로 CPU 자원을 나눠 사용할 수 있게 됩니다. 즉 cpu.shares가 512, 1024, 2048로 설정되어 있다면, 1:2:4의 비율로 cpu 자원을 할당받아 사용할 수 있게 됩니다. cpu.shares의 최대 값은 2^18 - 2인 262142를 갖습니다.
    • control group v1 에서 cpu.shares는 최대 값은 이론상 2^32를 갖지만, 시스템의 안정성 등을 위해 최대 값을 2^18로 제한하고 있습니다.
    • 실제로 Ubuntu 등 Linux 계열의 cpu.shares의 옵션을 보면 [2, 262144] 값을 가지고 있습니다.
    • 2를 빼는 이유는 cpu.shares의 최소값인 2를 보장하기 위함입니다.
    • 512, 1024, 2048 등 vCPU에 대해 Timeslice가 2의 배수로 표현되는 이유는 Linux CFS에서 1024 = default nice 0에 대응하는 값이고, 이를 기본 CPU Weight로 사용하는데 Container의 선구자(?)인 Docker가 이 값을 채택했고 Linux Kernel, CGroup 과 동일하게 유지할 수 있습니다.
  • 이러한 cpu.shares를 [1, 10000]의 범위를 갖는 cpu.weight로 변환하기 위해서는 다음의 식이 사용됩니다.
    • 1 + ((cpu shares - 2) * [cpu.weight의 맥스 - 최소값] ) / [cpu.shares의 맥스 - 최소값]
    • 1 + ((cpu shares - 2) * 9999) / 262142
    • 이 때 1을 더해주는 이유는 int 값이기 때문에 버려지는 값을 보완하기 위함입니다.
  • cpu.weight 값을 새롭게 스케일링 한 것이 아니라, cpu.shares와 최대한 유사한 값을 제공하기 위한 식입니다.

이를 바탕으로 2 개의 vCPU (CPU 코어)를 갖는 쿠버네티스 환경에서는 1 + (( 2048 - 2 ) * 9999 ) / 262142 값인 79를 도출하게 됩니다.

자 그렇다면 이제 실질적으로 resources.requests.cpu를 설정하여 파드를 배포해서 확인해 봅시다.

apiVersion: v1
kind: Pod
metadata:
  name: a
  namespace: default
spec:
  containers:
  - image: busybox
    command:
      - sleep
      - "3600"
    imagePullPolicy: IfNotPresent
    name: busybox
    resources:
      requests:
        cpu: 250m
  restartPolicy: Always
  nodeSelector:
    kubernetes.io/hostname: worker1

위 yaml을 바탕으로 pod를 배포하게 되면 cpu.weight의 값은 얼마가 될지 한번 고민해 봅시다.

우리는 전체 CPU shares인 2048에 대해 250m을 요청했고, 2개의 코어를 갖는 노드에서 CPU Weight는 79를 갖고 있습니다. 즉 250 / 2048 만큼 요청했고, 79의 가중치를 갖기 때문에 다음과 같은 식을 갖는다고 예상할 수 있습니다.

1 + 250 / 2048 * 79 ~= 9.6 (10)

이를 직접 워커 노드의 /sys/fs/cgroup을 통해 확인해봅시다.

root@worker1:~# crictl ps
CONTAINER           IMAGE                                                                                                     CREATED             STATE               NAME                ATTEMPT             POD ID              POD
**8a6b6f7c79ece       27a71e19c95622dce4d60d4a3760707495c9875f5c5322c5bd535214799593ce                                          29 minutes ago      Running             busybox             0                   c063f7c199d23       a**
9f61aea39bdf1       registry.k8s.io/coredns/coredns@sha256:f0b8c589314ed010a0c326e987a52b50801f0145ac9b75423af1b5c66dbd6d50   13 hours ago        Running             coredns             0                   fb79be5137b43       coredns-7c65d6cfc9-k65k4
89986ea9962fa       registry.k8s.io/coredns/coredns@sha256:f0b8c589314ed010a0c326e987a52b50801f0145ac9b75423af1b5c66dbd6d50   13 hours ago        Running             coredns             0                   220bc9ce4e46f       coredns-7c65d6cfc9-hrx2k
f34df68b00d35       registry.k8s.io/kube-proxy@sha256:22535649599e9f22b1b857afcbd9a8b36be238b2b3ea68e47f60bedcea48cd3b        13 hours ago        Running             kube-proxy          0                   90179e9aaf2f6       kube-proxy-mb9f7

root@worker1:~# crictl inspect 8a6b6f7 | grep "pid"
    "pid": 16120,
    
root@worker1:~# find /sys/fs/cgroup -name cgroup.procs -exec grep -H 16120 {} \\;
/sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod10e59c76_03e0_447b_bbc0_d66f7ffe0583.slice/crio-8a6b6f7c79ece337d02b1ebdf1fe7984d18c2bb829aa2c75cfed19e18e166fbc.scope/container/cgroup.procs:16120

root@worker1:~# cd /sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod10e59c76_03e0_447b_bbc0_d66f7ffe0583.slice/crio-8a6b6f7c79ece337d02b1ebdf1fe7984d18c2bb829aa2c75cfed19e18e166fbc.scope
root@worker1:/sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod10e59c76_03e0_447b_bbc0_d66f7ffe0583.slice/crio-8a6b6f7c79ece337d02b1ebdf1fe7984d18c2bb829aa2c75cfed19e18e166fbc.scope# cat cpu.weight
10

우리가 계산한 9.6 + 1 = 10.6에 대한 10의 값이 정상적으로 출력되는 것을 확인할 수 있습니다.

혹시 모르니 resources.requests.cpu를 500m으로 설정하고 다시 확인해 봅시다.

root@master1:~/k8s/pods# cat a.yaml
apiVersion: v1
kind: Pod
metadata:
  name: a
  namespace: default
spec:
  containers:
  - image: busybox
    command:
      - sleep
      - "3600"
    imagePullPolicy: IfNotPresent
    name: busybox
    resources:
      requests:
        cpu: 500m
  restartPolicy: Always
  nodeSelector:
    kubernetes.io/hostname: worker1
root@master1:~/k8s/pods# kubectl apply -f a.yaml
pod/a created
---
root@worker1:~# crictl ps
CONTAINER           IMAGE                                                                                                     CREATED             STATE               NAME                ATTEMPT             POD ID              POD
**1618ac0f6157d       27a71e19c95622dce4d60d4a3760707495c9875f5c5322c5bd535214799593ce                                          18 seconds ago      Running             busybox             0                   cfbb9f68873cf       a**
9f61aea39bdf1       registry.k8s.io/coredns/coredns@sha256:f0b8c589314ed010a0c326e987a52b50801f0145ac9b75423af1b5c66dbd6d50   13 hours ago        Running             coredns             0                   fb79be5137b43       coredns-7c65d6cfc9-k65k4
89986ea9962fa       registry.k8s.io/coredns/coredns@sha256:f0b8c589314ed010a0c326e987a52b50801f0145ac9b75423af1b5c66dbd6d50   13 hours ago        Running             coredns             0                   220bc9ce4e46f       coredns-7c65d6cfc9-hrx2k
f34df68b00d35       registry.k8s.io/kube-proxy@sha256:22535649599e9f22b1b857afcbd9a8b36be238b2b3ea68e47f60bedcea48cd3b        13 hours ago        Running             kube-proxy          0                   90179e9aaf2f6       kube-proxy-mb9f7

root@worker1:~# crictl inspect 1618ac0f | grep "pid"
    "pid": 16906,
            "type": "pid"
          "pids": {

root@worker1:~# find /sys/fs/cgroup -name cgroup.procs -exec grep -H 16906 {} \\;
/sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod089ebf2e_1909_430e_aad6_14f1ff19455b.slice/crio-1618ac0f6157df7b07558d8507856af3a5707cb941ef923786bd8d09ba259b4b.scope/container/cgroup.procs:16906

root@worker1:~# cd /sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod089ebf2e_1909_430e_aad6_14f1ff19455b.slice/crio-1618ac0f6157df7b07558d8507856af3a5707cb941ef923786bd8d09ba259b4b.scope
root@worker1:/sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod089ebf2e_1909_430e_aad6_14f1ff19455b.slice/crio-1618ac0f6157df7b07558d8507856af3a5707cb941ef923786bd8d09ba259b4b.scope# cat cpu.weight
20
  • 예상값: 1 + 500 / 2048 * 79 = 20.28 ( 20 )
  • 실제값: 20
  • 450m → 18 (17.35 + 1)

이번 글의 대상이 아닌 독자

  • k8s는 왜 CPU를 압축 가능한 자원으로 보는가?
    • CPU Core에 대한 실행시간, 즉 스케줄링에 대해 가중치를 바탕으로 할당하기 때문에 탄력적으로 조절 가능하기 때문.
  • 쿠버네티스에서 CPU requests/limits는 어떤 의미인가?
    • CPU requests는 cpu weight을 기반으로 “상대적으로” 컨테이너가 보장받을 수 있는 최소 CPU 자원을 설정한다.
      • 예를 들어, 전체 가중치가 100일 때 두 애플리케이션에 각각 50의 가중치가 설정되면, 각 애플리케이션은 최소 50%의 CPU 시간을 보장받는다.
        • 하나의 애플리케이션만 존재할 경우, 우리가 알고 있는 스케줄링 정책과 같이 자원을 더 많이 사용할 수 있다.
      • 만약 50의 가중치를 가진 애플리케이션이 세 개 존재한다면, 피크 타임에 모두 50%의 CPU를 요청하여 병목 현상이 발생할 수 있다.
        • 하지만 쿠버네티스는 스케줄링 정책에서 세 번째 Pod를 Pending 상태로 유지한다.
    • CPU limits는 컨테이너가 사용할 수 있는 최대 CPU 시간을 의미한다. CPU 사용량은 이 limits을 초과할 수 없다.
    • 그렇기 때문에 쿠버네티스 환경에서는 CPU requests만 설정해도 충분한 경우가 많다.
      • 이는 전체 가중치 중 각 애플리케이션의 requests 비율에 따라 CPU 시간이 할당되기 때문이다.
      • 따라서 limits을 불필요하게 설정하여 애플리케이션에 인위적인 병목 현상을 만들 필요는 없다.
        • 어차피 각 비율에 따라 가질수 있는 최대 CPU 타임은 정해져있다.
      • 다만 쿠버네티스의 QoS 정책에 따라 Guaranteed로 사용하기 위해 limits을 설정하는게 좋다.
  • 컨테이너의 CPU 사용률은 어떻게 측정되는가? ( by 천강진 님 )
/sys/fs/cgroup/C # cat cpu.stat
usage_usec 0     # CPU 총 사용 시간(마이크로초 단위)
user_usec 0      # 사용자 모드에서 실행된 시간
system_usec 0    # 커널 모드에서 실행된 시간

### 아래 값은 cpu.max에 MAX(QUOTA)가 설정된 경우 증가
nr_periods 0     # 스케줄러에 의해 CPU가 할당된 횟수
nr_throttled 0   # 사용 가능한 CPU 시간을 초과 또는 남은 시간이 부족하여 쓰로틀링에 걸린 횟수
throttled_usec 0 # 쓰로틀링 걸린 시간

### 아래 값은 cpu.max.burst가 설정된 경우 증가
nr_bursts 0      # CPU 버스트 모드로 전환된 횟수
burst_usec 0     # 버스트 모드에서 사용한 시간
last_cpu_usage_us = [cpu.stat의 usage_usec]
last_time_us = [현재 시간 마이크로초]

N초 휴식

curr_cpu_usage_us = [cpu.stat의 usage_usec]
curr_time_us = [현재 시간 마이크로초]

delta_cpu_usage_us = curr_cpu_usage_us - last_cpu_usage_us
delta_time_us = curr_time_us - last_time_us

print(f"CPU 사용률: {delta_cpu_usage_us / delta_time_us * 100:.2f}%")
  • CPU 사용률이 100%가 아닌데 왜 throttled 상태가 되는가?
    • cpu.max에 설정된 CPU 시간을 초과할 수 있기 때문
  • 일정량의 RPS 또는 TPS가 목표일 때, 내 어플리케이션에 적용해야할 CPU 값은 어떻게 알 수 있는가?
    • 성능 테스트를 통해 CPU 자원 사용량을 계산해야 한 뒤, 목표 값에 도달하기 위해 얼마나 필요할지 예측할 수 있다.
    • 예를 들어 1350 RPS가 목표이고, 1000 RPS (TPS)를 처리할 때 0.5 vCPU가 사용되었다면 0.675 vCPU가 필요한 것을 알 수 있다. ( 1350 / 1000 ) * 0.5
    • 실제 vCPU를 할당할 때는 이보다 넉넉하게 할당하도록 하자.
  • k8s에서 CPU와 관련된 추가 기능은 무엇이 나올 수 있을까?
    • 자식 프로세스와 관련된 기능 ?
      • address space를 공유할 수 있을것 같고
      • CPU의 근본적인 것에 가까워지는 느낌이라, 부모 - 자식 process…….
  • 컨테이너 환경은 CPU 활용에 있어 어떤 문제가 있는가?
    • Host OS를 이용한다는 점
      • 가상 머신의 경우 일반적으로 하드웨어적으로 격리된 자원을 보장받지만
      • 컨테이너는 그렇지 않기 때문에 발생할 수 있는
      • 자원 이슈
  • 7번의 문제는 특정 애플리케이션을 컨테이너로 올리는데 문제가 되는가?
    • Host OS에 영향을 줄 수 있는 만큼의 연산을 필요로 하는 애플리케이션
      • 우리가 자원량을 설정하지 않는다면 문제가 되지 않을까?

참고

안녕하세요, 짧은머리 개발자입니다.

클라우드는 보안이 매우 중요한 환경인데요, 오늘은 Google Cloud Platform에서 관리중인 여러 프로젝트간 서비스 접근을 어떻게 구성하는지 공유하려 해요.

클라우드는 서비스로 제공하는 XaaS에 대한 접근을 제어할 수 있도록 IAM을 제공하는데 AWS, GCP와 같은 거대 클라우드는 IAM Role for Service Account(IRSA)라 불리는 메커니즘을 통해 더 세밀한 제어를 가능하게 해요.

이름에서 유추할 수 있듯이 서비스 계정을 위한 IAM 역할을 부여함으로써 그 메커니즘이 동작해요.

GCP의 서비스에 등록된 Service Account는 구글의 메타데이터 서버를 통해 자격 증명에 접근하고, 필요한 토큰을 관리해요.

  • 이전에 작성한 Github과 같이 GCP 외부에서 접근하는 경우에는 Security Token Services를 통해 단기 자격을 증명해주는 토큰을 발급받아야 하고, 이 때 roles/iam.serviceAccountTokenCreator과 같은 역할이 추가로 필요해요.

GCP는 구조를 계정 수준 리소스부터 서비스 수준 리소스로 나누어 관리할 수 있는데, 베스트 프랙티스로 GCP 내부에 생성할 그 구조를 회사의 조직 구조와 일치하게 만드는 것을 권장해요.

https://cloud.google.com/resource-manager/docs/cloud-platform-resource-hierarchy?hl=ko

이는 곧 분리되어 있는 프로젝트 간 배포된 서비스간 접근을 허용해야 하는 경우가 발생할 수 있어요.

제가 수행했던 프로젝트의 경우에도 분리되어있는 프로젝트간 버킷에 접근해야 할 필요가 있었는데요, 이를 어떻게 구성했는지 공유하려 해요.

프로젝트 구성

프로젝트 구성은 위와 같은 형태로, Project A에서 동작중인 Application A는 Project B의 Bucket B의 오브젝트에 접근할 수 있어야 하며, Bucket B는 Private하며 IAM을 통해 접근을 제한하고 있어요.

이를 해결하기 위해 우리는 싱글 프로젝트와 같이 Project A의 Application A에 서비스 계정을 추가한 뒤, Project B의 IAM을 구성해줘야 해요.

이 때 Project B에서 bucket-sa@project-a.iam.gserviceaccount.com 서비스 계정이 가져야할 역할은 다음과 같아요.

  • Bucket B에서 행할 작업에 따른 Storage 접근 권한, roles/storage.objectViewer
💡 roles/iam.serviceAccountUser의 경우, 특정 사용자 혹은 서비스 계정이
bucket-sa와 같이 IAM 역할을 갖는 서비스 계정으로 가장(impersonate)해야 할 때 필요해요.
이 경우에는 Application에 부착된 서비스 계정이 직접적으로 사용하기 때문에 필요하지 않아요.

 

여기까지 수행하고 나면 RBAC이 구성되고, Application A에서 Bucket B에 정상적으로 접근할 수 있어요.

그런데 만약 Project B에 Bucket B 말고 여러 버킷이 존재한다며면, bucket-sa 서비스 계정을 이용해서 모든 버킷에 접근 가능하게 돼요. 이는 보안상 매우 취약한 설정이며, 따라서 Attribute Based Access Control(ABAC)을 구성해야 해요.


ABAC은 이름에서 유추할 수 있듯 속성 기반의 접근 제어 메커니즘이에요. 기존의 bucket-sa 서비스 계정은 역할을 기반으로 Bucket B에 접근 가능하도록 구성했다면, 여기에 속성을 추가하여 더 세밀한 제어를 할 수 있어요.

우리에게 필요한 세밀한 제어는 다음과 같아요.

  • Bucket B에 접근할 수 있어야 한다.
  • Bucket B 내부의 오브젝트를 볼 수 있어야 한다.
  • Bucket B 내부의 디렉토리 구조를 볼 수 있어야 한다.

이를 해결하기 위해 GCP는 IAM 역할에 조건을 설정할 수 있고, 역할의 조건에 필요한 리소스 유형과 리소스 이름은 다음 링크에서 확인할 수 있어요.

우리에게 필요한 속성을 정리하면 다음과 같아요.

# 허용할 서비스 계정
member = "bucket-sa@project-a.iam.gserviceaccount.com"

# 허용할 역할
roles = [
  "roles/storage.objectViewer",
]

# 역할 + 대상 = 서비스 계정이 접근할 수 있는 대상
target_resources = [
  # 버킷의 이름이 bucket-b와 같아야 한다.
  {
    type   = "storage.googleapis.com/Bucket",
    name   = "projects/_/buckets/bucket-b"
    option = "equal"
  },
  # 접근하고자 하는 오브젝트는 bucket-b의 objects여야 한다.
  {
    type = "storage.googleapis.com/Object",
    name   = "projects/_/buckets/bucket-b/objects/"
    option = "startsWith"
  },
  # 접근하고자 하는 폴더는 bucket-b의 관리형 폴더여야 한다.
  {
    type = "storage.googleapis.com/ManagedFolder",
    name   = "projects/_/buckets/bucket-b/managedFolders/"
    option = "startsWith"
  }
]

버킷 뿐만 아니라 다른 XaaS에 대한 접근이 필요할 때도 위와 같이 서비스 계정을 구성한다면 프로젝트간 IRSA를 구성할 수 있어요.

오늘도 읽어주셔서 감사합니다.

안녕하세요, 짧은머리 개발자에요.

오늘은 초기 스타트업인 회사에서 CI/CD 파이프라인을 도입하고 이에 대한 정책 결정에 대한 고민과 솔루션을 공유하려 해요.


우선 제가 입사하기 전까지 회사는 데브옵스 엔지니어가 없었기에 CI/CD 파이프라인 마찬가지로 없었고, 클라우드 내에 딱히 정해진 정책 없이 애플리케이션을 운영중하고 있었어요. 그렇기 때문에 현재 운영중인 애플리케이션의 버전을 알 수 없었고, 언제 어떻게 배포되었는지 추적하기 어려운 환경이었어요.

이러한 환경에서 제가 CI/CD 파이프라인을 도입하기 위해 회사의 현재 상황을 분석하고 마련한 예비 요구사항은 다음과 같아요.

  1. Continuous Integration, Continuous Delivery, Continuous Deploy 각 단계에서 현재 상태와 결과를 알 수 있어야 한다.
  2. 현재 운영중인 애플리케이션의 버전과 상태를 알 수 있어야 한다.
  3. 자동화된 시스템을 통해 개발자는 CI/CD를 수행할 수 있어야 한다.
    • 기존에는 CI ~ Deploy까지 한 번에 무조건 실행되어서 애플리케이션의 테스트 없이 곧바로 Production에 올라가는 문제가 있었어요. 이를 해결하기 위해 “적절한 자동화”를 통해 각자가 담당한 애플리케이션을 관리, 배포할 수 있게 하고자 했어요.
  4. DevOps Engineer를 제외한 개발자는 CI/CD 각 단계가 어떻게 진행되는지 몰라도 된다.
    • DevOps Engineer가 생긴 만큼 개발자는 각자의 영역에 충실할 수 있게끔 하고자 했어요.
  5. Git Ops와 융합되어야 한다.
    • 회사에서 GitHub Enterprise를 이용중이었고, 따라서 GitOps와 융합하고자 했어요.

1, 2번을 묶어서 기존 시스템의 가장 큰 문제점인 현재 운영중인 애플리케이션의 버전을 알 수 없는 점각 단계에서의 결과와 상태를 앎으로써 배포 후보군이 되는 애플리케이션이 어떤 내용을 갖는지 추적할 수 있게 하고자 했어요.


GCP Artifact Registry에 대한 Git Actions 접근 허가하기

GCP의 경우 워크로드 아이덴티티 제휴(Workload Identity Federation)를 통해 외부 서비스와의 융합을 지원하는데요, Git Actions에서 GCP에 접근 또한 이를 활용하여 해결할 수 있어요.

https://cloud.google.com/blog/products/identity-security/enabling-keyless-authentication-from-github-actions?hl=en

워크로드 아이덴티티 제휴는 쉽게 말해 GCP의 Iam Roles for Service Accounts(IRSA)라 볼 수 있어요. 즉, GCP의 리소스를 이용하는 작업(Workload)에 대해 신원(Identity)을 제공하는 기능이에요.

Git Actions에서 GCP의 서비스인 Artifact Registry에 접근할 필요가 있었고, 이를 위한 워크로드 아이덴티티 제휴를 구성했어요.

생성할 서비스 계정은 워크로드 아이덴티티 풀과 Artifact Registry에 접근하기 위해서 다음의 Role들을 가지고 있어요.

  • roles/iam.workloadIdentityUser
    • 서비스 계정을 통해 GCP의 워크로드 아이덴티티를 사용하기 위함이에요.
  • roles/iam.serviceAccountUser
    • 서비스 계정을 통해 GCP 리소스에 접근하기 위함이에요.
  • roles/artifactregistry.writer
    • 서비스 계정을 통해 Artifact Registry에 Git Actions로 생성된 컨테이너 이미지를 업로드하기 위함이에요.
  • roles/iam.serviceAccountTokenCreator
    • 서비스 계정에 대한 단기 인증정보를 만들 수 있도록 해요. 이를 통해 Git Actions 워크 플로우에 정의된 서비스 계정의 OAuth 2.0 토큰을 획득하고 명시된 권한을 사용할 수 있어요.

또한 아무 Git 리포지터리에서 실행되는 Git Actions의 워크플로우를 통해 우리의 GCP 인프라에 접근하면 안되기 때문에 다음과 같이 GitHub 계정 혹은 리포지터리에 대한 제한 및 격리를 시켜야 합니다.

module "oidc_gh" {
  ...
  sa_mapping = {
    "github-sa" = {
      sa_name   = google_service_account.github_sa.id
      attribute = "attribute.repository_owner/{{ Organization }}"
    }
  }
  ...
}

# module.oidc_gh 중
resource "google_service_account_iam_member" "wif-sa" {
  for_each           = var.sa_mapping
  service_account_id = each.value.sa_name
  role               = "roles/iam.workloadIdentityUser"
  member             = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.this.name}/${each.value.attribute}"
}

이렇게 GCP와 Git Actions에 대한 WIP를 설정했다면, Git Actions에서 실제로 접근 가능한지 테스트해야 해요.

GCP의 경우 Git Actions에서 사용 가능한 Git Action을 제공하고 있는데요, google-github-actions/auth@v2를 통해서 GCP 서비스 계정의 권한을 획득할거에요.

build-and-delivery:
  runs-on: ubuntu-latest
  permissions:
    id-token: "write"
  steps:
	- name: "GCP Auth"
	  id: "gcp-auth"
	  uses: "google-github-actions/auth@v2"
	  with:
	    token_format: "access_token"
	    workload_identity_provider: "{{ 워크로드 아이덴티티 제공자 }}"
	    service_account: "{{ 워크로드 아이덴티티 서비스 계정 }}"
	    access_token_lifetime: "300s"

해당 Git Actions를 실행하면 GCP의 OAuth 2.0 토큰을 해당 잡에서 사용할 수 있게 해요.

  • 이를 위해 Git Actions에서 토큰에 대한 권한을 설정해야 해요. permissions의 id-token 속성을 write로 구성해야 합니다.
  • Github OIDC Provider는 클라우드 제공자로 부터 토큰을 전달받아 사용하는데, 여기서는 GCP의 OAuth 2.0 토큰을 받아 사용합니다.

이렇게 획득한 OAuth 2.0 토큰을 바탕으로 Artifact Registry 로그인 하여 이미지를 업로드 할 수 있어요.

- name: "Login to GAR"
  uses: docker/login-action@v3
  with:
    registry: asia-northeast3-docker.pkg.dev
    username: oauth2accesstoken
    password: ${{ steps.auth.outputs.auth_token }}

- name: "Docker auth with GCP Auth"
  run: |
    gcloud auth configure-docker asia-northeast3-docker.pkg.dev --quiet

Continuous Integration ~ Continuous Delivery 스텝 정의하기

Git Actions를 통해 Google Artifact Registry에 대한 권한을 획득했으니 실질적으로 CI ~ Delivery 까지의 Step을 정의해야 해요.

Continuous Integration은 한 제품을 개발하는 많은 개발자들이 각자의 코드를 융합하고 문제없이 돌아가는지 확인하며 제품으로 빌드하는 행위를 하도록 다음과 같이 정의했어요.

  1. 코드 테스트 단계
    • Git Actions에서 GCP Artifact Registry로 잘 접근이 되는지 확인해요.
    • 코드에 대한 테스트를 수행해요.
  2. 코드 빌드 단계
    • 코드 테스트 단계를 통과했다면 합쳐진 코드를 바탕으로 제품을 빌드해요.
    • 우리의 경우 Container 환경에서 애플리케이션을 운영하기 때문에 Container Image를 생성하는 행위를 의미해요.
  3. 제품 배달 단계
    • 생성된 Container Image를 GCP Artifact Registry로 업로드해요.

특히 우리는 예비 요구사항에서 각 스텝에 대한 결과와 상태를 알 수 있어야 한다.는 요구사항이 있었어요. 이를 지원하기 위해 CI ~ Delivery에 Slack Notification을 전송하는 Job을 추가했어요.

Slack Notification은 총 두 단계로 구성되어 있어요.

  1. Git Actions 시작 알림 메시지
    • 해당 메시지를 통해서 Git Actions의 결과물로 나올 컨테이너 이미지에 대한 정보를 함축적으로 전달해요.
    • 변경에 대한 Commits List를 추가하여 Slack의 쓰레드 메시지로 전달해요.
  2. Git Actions 종료 알림 메시지
    • Git Actions의 성공 여부와 Git Actions 링크 정보를 포함한 메시지를 전달해요
  • 단순히 알림 메시지를 단순히 보내는 것에 끝내지 않고 데이터베이스에 저장하여서 해당 커밋 혹은 릴리즈를 통해 배달된 버전이 어떤 변경점을 갖는지 추후에 확인할 수 있게 했어요.

슬랙에 메시지를 보냄으로써 생성된 이미지에 대해 변경사항을 확인할 수 있고 추후 애플리케이션 배포 시에도 활용하고자 했어요.

CI/CD 파이프라인의 성공 여부 메시지. 부모 슬랙 메시지의 쓰레드로 전달 되어요

독립된 Continuous Deploy

이제 배달 된 제품 빌드에 대해 배포할 차례에요. 기존의 문제점에서 현재 운영중인 애플리케이션의 버전을 알 수 없는 점을 해결하기 위해 배포에 대한 파이프라인을 앞 단계와 분리했어요.

웹 대시보드를 통해 배포하고자 하는 버전과 그 변경사항을 알 수 있게 했고, 현재 배포된 애플리케이션 차트가 어떻게 구성되어 있는지 확인할 수 있도록 배포 서버를 개발 했어요. 해당 서버의 경우 코드를 정리한 다음에 오픈소스로 공개할 예정이에요.

우리 회사는 GKE를 이용해서 애플리케이션을 운영하고 있는데, 실제 쿠버네티스에 배포에 대한 책임은 Argo CD에 위임했어요.

즉, 직접적으로 Continuous Deploy가 해결해야 하는 것은 Argo CD가 알 수 있도록 애플리케이션 매니페스트를 수정해주면 돼요.


여기까지 CI/CD 파이프라인을 설계하며 고민했던 내용을 정리했어요.

읽어주셔서 고맙습니다 🙂

안녕하세요, 짧은머리 개발자에요.

제가 근무하고 있는 회사는 Google Cloud Platform을 이용하여 서비스를 운영하고 있어요. 특히 컨테이너의 시대인 만큼 Google Kubernetes Engine을 이용하여 컨테이너를 관리하고 있는데요, 오늘은 처음 GKE를 도입하면서 GCP의 Cloud Load Balancer를 연동하며 했던 고민을 공유하려 해요.


먼저 GCP는 관리중인 서비스를 외부 혹은 내부에서 접근할 수 있도록 Cload Load Balancing이라는 명칭의 PaaS를 제공하고 있어요. CLB라고 불리는 이 서비스는 GCP에 배포된 서비스들에 고성능 부하 분산 기능을 제공하는 역할을 해요. AWS에는 ALB(NLB)라는 솔루션이 있어요.

우리 회사가 GKE에서 운영중인 서비스들에 대해 외부에서 접근할 수 있도록 하는것이 목표였는데요, GCP에서는 일반적으로 GKE의 외부 노출을 위해 다음의 솔루션을 제공해요.

  1. 쿠버네티스 게이트웨이 API를 이용한 외부 노출
  2. 쿠버네티스 인그레스를 이용한 외부 노출
  3. 서비스 + CLB 직접 관리를 통한 외부 노출

우리 회사가 선택한 방법은 3번인데, 그 이유는 다음과 같아요.

  1. Load Balancer의 직접 관리를 통해 보다 폭 넓은 제어가 필요하다.
  • 2번의 경우 CLB가 자동으로 생성, 관리되는데 이 경우 직접적인 제어가 까다로웠어요. 특히 인그레스 혹은 게이트웨이를 생성할 때 마다 CLB가 생성되는데, 이는 불필요한 CLB의 증가로 인해 관리가 복잡해지고 비용이 늘어나는 원인이 되기도 했어요.
  1. SSL에 대한 폭 넓은 제어가 필요하다.1, 2번의 경우 인그레스, 게이트웨이가 늘어날 때 마다 해당 요청을 처리하는 도메인에 대한 SSL을 명시해 주고 관리해야 하는데 이것은 불필요한 관리를 유발했어요.
  • 특히 초기의 경우 도메인에 대한 asterisk를 SSL에 등록하지 않고, 우리 회사에서 사용하는 도메인들에 대해 SSL을 발급, 관리했는데 이는 매우 까다로운 절차였어요.
  • 인증서를 관리하기 위해 Google Certificates Manager를 이용하고 있어요. 문제는 DevOps 관점에서 SSL의 변경 혹은 추가가 일어날 때, Terraform을 이용한 생성 및 CLB 할당에서 인증서 관리를 마무리 하고 싶었어요.
  1. L7 뿐만 아니라 L4에 대한 트래픽 제어도 필요하다.
  • 블록체인을 운영하는 회사로서 노드에 대한 RPC 통신을 지원해야 했기 때문에 CLB를 통한 직접적인 관리가 필요했어요.
  1. 회사의 규모가 크지 않은만큼 필요한 수의 CLB만 유지하고 싶었어요.
  • GCP의 CLB는 고가용성을 제공하는 만큼 증가하는 트래픽에 대한 CLB를 분리하여 운영할 필요가 없었기에, 필요한 CLB의 숫자만 유지하고 싶었어요.

GCP에서는 최종적으로 GKE의 서비스에 접근하기 위해 GCP Backend Service와 Network Endpoint Group을 이용해야 해요.

GCP는 이를 지원하기 위해 AutoNeg 쿠버네티스 오퍼레이터를 제공하고 있어요. 이를 통해 쿠버네티스에 배포된 서비스를 바탕으로 GCP Backend Service와 Network Endpoint Group을 생성해 줘요.

이를 위해 쿠버네티스 서비스를 생성할 때 다음의 예시처럼 Backend Service와 NEG에 필요한 정보를 어노테이션에 담아 생성해야 해요.

apiVersion: v1
kind: Service
metadata:
  annotations:
    cloud.google.com/neg: '{"exposed_ports": {"80":{"name": "nginx-neg-svc"}}}'
  name: nginx-neg-test
  namespace: devops
  labels:
    app: nginx-neg-test
spec:
  ports:
    - port: 80
      protocol: TCP
  selector:
    app: nginx

이처럼 서비스에 AutoNeg를 통해 GKE와 CLB를 융합하며 운영했을 때 다음의 큰 문제가 발생했어요.

  • GKE 서비스를 배포할 때, Network Endpoint Group이 생성되는 Zone을 명시할 수 없다.
    • 특히 이는 AWS의 EKS의 경우 AWS Load Balancer Controller와 Target Group Binding 그리고 리스너를 통해 배포되어 있는 EKS Pod에 대한 외부 노출이 가능한데 이에 대해 한번에 지원되지 않는게 의아했어요.

이는 Pod의 Spec이 변경될 때 마다 NEG 위치가 변하는 이슈가 있었고, CLB에서는 변경된 NEG로 다시 설정해 줘야 트래픽의 정상적인 처리가 가능했어요. 즉 Downtime이 발생할 수 있는 위험성을 가지고 있어요.

NEG이 하나만 생성된 모습

이를 해결하기 위해 GCP의 GKE 외부 노출 방법중 Ingress를 이용하여 애플리케이션을 외부로 노출해 봤어요.

# nginx.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  namespace: devops
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 1
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-neg-test
  namespace: devops
  labels:
    app: nginx-neg-test
spec:
  ports:
    - port: 80
      protocol: TCP
  selector:
    app: nginx
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-ingress
  namespace: devops
spec:
  rules:
    - http:
        paths:
          - path: /*
            pathType: ImplementationSpecific
            backend:
              service:
                name: nginx-neg-test
                port:
                  number: 80
가능한 모든 zone에 NEG이 생성된 모습

이 경우 AutoNeg과는 다르게 Ingress로 배포한 서비스는 자동으로 현재 GKE가 배포되어 있는 Region 내의 모든 Zone에 대해 Network Endpoint Groups를 배포하는 것을 확인했고, 머리속에서 이거다! 하는 생각이 떠올랐어요.

GCP의 AutoNeg의 경우 배포되는 Service가 사용할 Network Endpoint Groud에 대해 다음의 규칙을 지키면 이미 생성된 NEG을 활용해요.

💡 Note
Updating NEG description is impossible, so this only works if the NEG is used for populating endpoints from a specific set: [Cluster, service name, namespace, port] which does not change.


그래서 저는 AWS의 ALB Controller + Target Group Binding 노하우를 활용하여 다음과 같이 GKE 애플리케이션 외부 노출 정책을 결정했답니다.

  1. 인프라에 배포할 애플리케이션 요구사항에 맞춰 CLB, Backend Service, Network Endpoint Group을 먼저 생성한다.
  1. Backend Services를 생성하며 생성된 NEG을 연결한다.
  • AutoNeg를 이용하여 서비스를 배포할 때 Backend Service와 NEG의 이름을 명시적으로 사용할 수 있어요.
  • 이를 적용하기 위해 Backend Service를 생성할 때 쿠버네티스 서비스에 명시할 이름을 스펙으로 설정해야 해요.

3. 쿠버네티스 서비스 배포시 Backend Service를 생성한 스펙을 바탕으로 AutoNeg Annotation을 설정하여 배포한다.

이렇게 할 경우 Pod가 재배포 되면서 스펙에 변경사항이 발생되더라도 자동으로 AutoNeg이 서비스를 검사하여 GCP의 NEG과 Backend Service에 해당하는 서비스를 연결하는것을 확인했고 Downtime 없이 Application을 배포할 수 있었어요.

Description

  • 본 문제는 그리디로 풀 수 있다.
  • 한 곡괭이로 캘 수 있는 5번 횟수에 대해 각 구간을 정한다.
    • 모든 미네랄에 대해 곡괭이로 캘 수 있는 횟수까지 대해 구간을 정한다.
      • 곡괭이 1개라면, 5회, 2개라면 10회, …
    • 이 때 곡괭이로 캘 수 있는 수량 보다 미네랄이 적을 수 있으므로, 내가 볼 구간의 length는
      • Math.min( 곡괭이 횟수, 광물 수 / 5 )
        • / 5를 해주는 이유는 한 곡괭이로 5번 밖에 못 캐기 때문이다.
    • 이에 대해 모든 곡괭이를 사용하여 얼마만큼의 비용이 발생하는지 확인한다.
      • 곡괭이가 없어도 일단 계산한다. 나중에 조건을 이용해 사용하지 않으면 되기 때문
  • 각 구간에 대해 값을 모두 구했다면, 이를 돌 곡괭이 > 철 곡괭이 > 다이아몬드 곡괭이 순으로 우선순위 큐에 삽입한다.
    • 돌 곡괭이로 많은 비용이 발생했다면, 이는 광물이 비싸다는 뜻이고, 따라서 더 좋은 곡괭이로 캘 수록 효율이 높아지기 때문.
    • 철 곡괭이도 마찬가지
  • 이 후 우선순위 큐 에서 하나씩 꺼내면서 가지고 있는 제일 좋은 곡괭이로 캤을 때 발생하는 비용을 추가하면 정답
    • 이 때 이미 비용은 계산해 놓았기 때문에, 해당 구간에서 해당 곡괭이 값을 찾으면 된다.

Result

import java.util.*;
class Solution {
    public int solution(int[] picks, String[] minerals) {
        int answer = 0;
        int picksCount = 0;
        PriorityQueue<int[]> pq = new PriorityQueue<>((o1, o2) -> {
            if(o2[2] != 0 || o1[2] != 0)
                return o2[2] - o1[2];
            if(o2[1] != 0 || o1[1] != 0)
                return o2[1] - o1[1];
            return o2[0] - o1[0];
        });
        
        for(int i = 0; i < picks.length; i++)   picksCount += picks[i];
        
        //* 좋은 곡괭이로 캘 수록 피로도는 적어진다.
        //* 좋은 곡괭이로 좋은 광물을 캘 수록 피로도는 적다.
        
        //* 다이아몬드가 많을 때 다이아로 캐는게 좋다.
        //* 철이 많을 때 철로 캐는게 좋다.
        //* 돌이 많을 때 돌로 캐는게 좋다.
        //* 한 번에 5개씩 캐야 하니, 각 구간의 threshold를 구한다.
        
        //* 각 구간에 대해 모든 곡괭이 값을 구하고 최소값으로 갱신
        
        //* 필요한 tresh 칸 수 = 최대: 곡괭이 수, 최소: minerals.length / 5 + 1
        
        //* 각 구간에 대한 곡괭이 사용 값
        // int[][] thresh = new int[Math.min(picksCount, (minerals.length/5) + 1)][3];
        int[] temp = { 0, 0, 0 };
        for(int i = 0; i < minerals.length; i++){
            if(i > 0 && i % 5 == 0){
                pq.offer(new int[]{
                    temp[0], temp[1], temp[2]
                });
                
                temp[0] = 0;
                temp[1] = 0;
                temp[2] = 0;
            }
            
            if(i >= picksCount * 5) break;
            
            switch(minerals[i]) {
                case "diamond":
                    temp[0] += 1;
                    temp[1] += 5;
                    temp[2] += 25;
                    break;
                case "iron":
                    temp[0] += 1;
                    temp[1] += 1;
                    temp[2] += 5;
                    break;
                default:
                    temp[0] += 1;
                    temp[1] += 1;
                    temp[2] += 1;
                    break;
            }
        }
        
        if(temp[0] != 0 || temp[1] != 0 || temp[2] != 0){
            pq.offer(new int[]{
                    temp[0], temp[1], temp[2]
                });
        }
        
        
        while(!pq.isEmpty()){
            int[] current = pq.poll();
            
            for(int i = 0; i < picks.length; i++){
                if(picks[i] != 0){
                    answer += current[i];
                    picks[i]--;
                    break;
                }
            }
        }
        
        return answer;
    }
}

Problem

문제 설명

마인은 곡괭이로 광산에서 광석을 캐려고 합니다. 마인은 다이아몬드 곡괭이, 철 곡괭이, 돌 곡괭이를 각각 0개에서 5개까지 가지고 있으며, 곡괭이로 광물을 캘 때는 피로도가 소모됩니다. 각 곡괭이로 광물을 캘 때의 피로도는 아래 표와 같습니다.

https://user-images.githubusercontent.com/62426665/217975815-63c58d04-0421-4c39-85ce-17613b9c9389.png

예를 들어, 철 곡괭이는 다이아몬드를 캘 때 피로도 5가 소모되며, 철과 돌을 캘때는 피로도가 1씩 소모됩니다. 각 곡괭이는 종류에 상관없이 광물 5개를 캔 후에는 더 이상 사용할 수 없습니다.

마인은 다음과 같은 규칙을 지키면서 최소한의 피로도로 광물을 캐려고 합니다.

  • 사용할 수 있는 곡괭이중 아무거나 하나를 선택해 광물을 캡니다.
  • 한 번 사용하기 시작한 곡괭이는 사용할 수 없을 때까지 사용합니다.
  • 광물은 주어진 순서대로만 캘 수 있습니다.
  • 광산에 있는 모든 광물을 캐거나, 더 사용할 곡괭이가 없을 때까지 광물을 캡니다.

즉, 곡괭이를 하나 선택해서 광물 5개를 연속으로 캐고, 다음 곡괭이를 선택해서 광물 5개를 연속으로 캐는 과정을 반복하며, 더 사용할 곡괭이가 없거나 광산에 있는 모든 광물을 캘 때까지 과정을 반복하면 됩니다.

마인이 갖고 있는 곡괭이의 개수를 나타내는 정수 배열 picks와 광물들의 순서를 나타내는 문자열 배열 minerals가 매개변수로 주어질 때, 마인이 작업을 끝내기까지 필요한 최소한의 피로도를 return 하는 solution 함수를 완성해주세요.


제한사항

  • picks는 [dia, iron, stone]과 같은 구조로 이루어져 있습니다.
    • 0 ≤ dia, iron, stone ≤ 5
    • dia는 다이아몬드 곡괭이의 수를 의미합니다.
    • iron은 철 곡괭이의 수를 의미합니다.
    • stone은 돌 곡괭이의 수를 의미합니다.
    • 곡괭이는 최소 1개 이상 가지고 있습니다.
  • 5 ≤ minerals의 길이 ≤ 50
    • minerals는 다음 3개의 문자열로 이루어져 있으며 각각의 의미는 다음과 같습니다.
    • diamond : 다이아몬드
    • iron : 철
    • stone : 돌

입출력 예

picks minerals result
[1, 3, 2] ["diamond", "diamond", "diamond", "iron", "iron", "diamond", "iron", "stone"] 12
[0, 1, 1] ["diamond", "diamond", "diamond", "diamond", "diamond", "iron", "iron", "iron", "iron", "iron", "diamond"] 50

입출력 예 설명

입출력 예 #1

다이아몬드 곡괭이로 앞에 다섯 광물을 캐고 철 곡괭이로 남은 다이아몬드, 철, 돌을 1개씩 캐면 12(1 + 1 + 1 + 1+ 1 + 5 + 1 + 1)의 피로도로 캘 수 있으며 이때가 최소값입니다.

입출력 예 #2

철 곡괭이로 다이아몬드 5개를 캐고 돌 곡괭이고 철 5개를 캐면 50의 피로도로 캘 수 있으며, 이때가 최소값입니다.

본 논문은 쿠버네티스의 전신인 Borg System 논문이다.

오늘의 요약

  • 즉 구글은 수 십만 개의 잡을 돌리는 클러스터 매니저인 Borg를 개발했다.
  • 해당 잡 들은 수 천개의 서로 다른 애플리케이션에서 발생하는데
    • 이들이 어떻게 관리되는지 시스템 유저(서비스 개발자)는 알 필요 없이 Borg System을 만들었다.
      • 자원 관리에 대한 세부 사항 (CPU, Memory, Disk, Network 할당 등)
      • 어느 컴퓨터에서 개발된 서비스가 동작할지
      • 실패되어도 다시 살아난다. (Self Healing)
      • 실패 처리를 유저가 직접하지 않아도 된다.
  • 구글은 이러한 시스템을 만들기 위해 구글이 실행하는 서비스를 두 가지로 분류했다.
    • Long Running Services
      • End User에게 제공되는 Production
        • GMail, Google Search, Google Docs, 그리고 구글 내부의 BigTable 등
      • 즉 절대 죽어서는 안되는, 수 µs ~ 몇백 ms 라는 짧은 latency를 가져 사용자에게 불편함을 주면 안되는 서비스
      • 매우 많은 리소스를 사용한다.
      • 쉽게 말해 Production Level
    • Batch Jobs
      • 짧게는 수 초, 길게는 수 일 동안 작업되는 서비스
      • 단기적 성능 변동에 훨씬 덜 민감하다.
      • 쉽게 말해 Non-Production Level
  • 해당 작업을 수행하는 머신들은 Cell이라 불리는 곳에 저장된다.
    • 셀 안에 많은 머신이 포함되며, 해당 머신들은 고성능 데이터 센터 규모로 정의된 단일 클러스터에 포함된다.
      • 단일 클러스터에 속하는 머신들은
        • High Performance, Datacenter Scale Network 구조로 정의
        • 해당 클러스터는 단일 데이터센터 빌딩에 존재하고, 이러한 빌딩들의 집합이 site를 만든다.
        • 클러스터는 일반적으로 하나의 거대한 셀의 주인이고(hosts)
          • 몇 개의
            • 작은 스케일의 테스트를 갖거나
            • 특수 목적 셀을 가진다.
        • Single Point of Failure를 피한다.
      • 중간 사이즈의 셀들은
        • 테스트 셀을 제외하고 대략 1만대 이상의 머신을 가지는데, 몇 몇은 더 큰 규모를 가진다.
        • 해당 머신들은 서로 이질적인 많은 디멘션을 가지는데,
          • CPU, RAM, Network, Disk 등
          • Processor Type, Performance, 외부 IP 주소 등
    • 놀랍게도 Borg System은 이러한 차이점 및 특징을 시스템 사용자(개발자 등)에게 철저히 숨겨 개발자들이 본연의 업무(개발)에만 집중할 수 있도록 한다.

Abstract

  • 구글의 Borg 시스템은 수 십만 개의 잡을 돌리는 클러스터 매니저다.
    • 해당 잡 들은 수 천 개의 서로 다른 애플리케이션으로 부터 발생하는데
    • 해당 애플리케이션은 수 만 개의 머신에서 동작한다.
  • Borg 시스템은 High Utilization을 달성하는데
    • 제어 허용(admission control), 효율적인 작업 패킹, 과다 할당(over commitment to machine), 그리고 프로세스 레벨의 성능 격리를 통한 머신 공유(자원 공유)
    • 를 조합하여 사용함으로써
  • Bog 시스템은 고가용성 runtime 애플리케이션을 지원한다.
    • Fault Recovery 시간을 최소화하고
    • 관련있는 실패할 가능성을 줄이는 스케줄링 정책을 수립함으로써
  • Borg 시스템은 사용자의 삶을 단순화한다.
    • Declarative Job Specification Language, Name Service 통합, 실시간 작업 모니터링 및 시스템 동작을 분석하고 시뮬레이션하는 도구를 제공하여
  • 우리는 Borg System의 아키텍처와 Features, 중요한 디자인 결정, Borg System의 몇몇 정책 결정에 대한 질적인 분석, 10년간의 운영 경험에서 얻은 교훈에 대한 질적 교훈을 제시한다.

1. Introduction

  • Borg System이라 부르는 클러스터 관리 시스템은
    • 관리하고, 스케줄하고, 시작하고, 재시작하고, 그리고 모니터링한다.
    • 구글이 실행하는 모든 애플리케이션의 Full Range로부터
  • 해당 본문은 이것이 어떻게 되는지 설명한다.
  • Borg는 세 가지 주요 장점을 제공한다.
    • 자원 관리에 대한 세부 사항과 실패 처리와 관련된 세부 사항을 숨김으로써, 유저는 애플리케이션 개발에 집중할 수 있다.
    • 매우 높은 신뢰성과 가용성을 바탕으로 동작하며, Borg에서 관리되는 애플리케이션 또한 이를 제공받는다.
    • 수만 대의 시스템에서 워크로드를 효율적으로 실행할 수 있도록 한다.
  • Borg는 이러한 이슈를 제기한 첫 번째 시스템이 아니다.
    • 하지만 Borg와 같이 큰 스케일, 탄력성과 완전성,에서 실행되는 것은 몇 없다.
  • 해당 논문은 이러한 주제를 중심으로 구성되어 있으며
  • 10년 이상 프로덕션 환경에서 Borg를 운영하면서 얻은 일련의 정성적 관찰로 결론을 내린다.
Untitled

2. The User Perspective

  • Borg 시스템의 유저는 구글의 개발자와 시스템 관리자(Site Reliability Engineers)다.
    • 구글의 애플리케이션과 서비스들을 실행하는
  • 사용자들은 그들의 일을 Borg의 jobs 폼에 맞춰 제출한다.
    • jobs: 하나 이상의 tasks로 구성
    • tasks: 모두 동일한 바이너리 프로그램을 실행
  • 각각의 job은 Borg Cell에서 실행되는데
    • Borg Cell은 하나의 유닛처럼 관리되는 머신의 집합이다.
  • 본 단락에서 중요한 것은 Borg가 유저에게 어떻게 노출되느냐다.
    • 유저 관점에서 Borg를 어떻게 사용하는지

2.1 The Workload

  • Borg Cells는 두 개의 이질적인 주요 파트에서 실행된다.
  • 첫 번 째는, “절대로” 죽어서는 안되고, 수 µs ~ 몇백 ms 라는 짧은 latency를 가지는, Long Running Services다.
    • Gmail, Google Docs, Web Search와 같이 End User에게 제공되는 Product들, 그리고 구글 내부의 인프라 서비스(Big Table) 등이 그 예다.
  • 두 번 째는, 수 초 ~ 수 일 안에 완료되는 batch jobs다.
    • 이러한 작업은 단기적인 성능 변동에 훨씬 덜 민감하다.
  • 워크로드 혼합은 Borg Cell 마다 다르며
    • Borg Cell은 다양한 애플리케이션의 혼합을 실행하는데
      • 애플리케이션은 작업에 따라 다르다. (주요 테넌트에 따라 다양한 애플리케이션 혼합을 실행)
        • 예: 일부 셀은 배치 집약적임
      • 또한 실행 시간에 따라 다르다.
      • 예를 들어
        • 배치 작업은 빠른 시간 안에 실행 되었다가 종료되고
        • End User Facing(Proudcts)는 주간 사용 패턴을 보인다. (오래 사용)
  • Borg 시스템은 이들 케이스 모두를 동일하게 잘 처리할 필요가 있다.
  • 대표적인 Borg workload는 2011년 5월부터 월 단위 추적이 가능한데, 이는 매우 잘 광범위하게 분석되었다.
  • 많은 애플리케이션 프레임워크는 Borg의 Top에서 만들어졌다.
    • Internal Map Reducing System, Flume Java, Millwhell, Pregel 등
  • 이들 대부분은 컨트롤러를 가지고 있는데
  • 구글의 분산 저장소 시스템, GFS, CFS, Bigtable, Megastore 모두 Borg에서 동작한다.
  • 본 문단에서 명시하는 것은
    • workload를 두 가지로 분류 가능한데
    • production 레벨
      • Long Running Services
    • non-production 레벨
      • Batch Jobs
  • 대표적인 셀에
    • production 레벨의 잡은 70% 정도의 CPU, 55%의 메모리를 할당받았으며
      • 이 중 60%, 85% 를 사용중이다.
    • 나머지를 non-production이 할당 및 사용
    • 이 실제 할당량과 사용량의 불일치는 5.5단락에서 자세하게 설명된다.

2.2 Clusters and Cells

셀의 머신은 이를 연결하는 고성능 데이터 센터 규모의 네트워크 패브릭으로 정의된 단일 클러스터에 속한다.

  • 단일 클러스터에 속하는 셀의 머신들은
    • High Performance, Datacenter Scale Network 구조로 정의되는데
    • 해당 클러스터는 단일 데이터센터 빌딩에 존재하고, 이러한 빌딩들의 집합이 site를 만든다.
    • 클러스터는 일반적으로 하나의 거대한 셀의 주인이고(hosts)
      • 몇 개의
        • 작은 스케일의 테스트를 갖거나
        • 특수 목적 셀을 가진다.
    • 우리는 몇 개의 단일 실패 지점을 피한다.
  • 우리의 중간 셀의 사이즈는 테스트 셀을 제외하여 대략 1만대의 머신의 크기인데
    • 몇 몇은 훨씬 더 크다.
    • 해당 머신들은 이질적인, 많은 디멘션에 속하는데
      • CPU, RAM, Disk, Network의 크기가 다르거나
      • processor type이 다르거나
      • performance가 다르거나
      • Flash Storage 혹은 외부 IP 주소 등 능력이 다르거나
  • Borg 시스템은 이러한 차이점 대부분으로부터 사용자를 격리한다.(사용자는 신경쓸 필요 없다.)
    • 셀에서 작업을 실행할 위치를 결정하고
    • 리소스를 할당하고
    • 프로그램 및 기타 종속성을 설치하고
    • 상태를 모니터링하고
    • 실패할 경우 다시 시작하는 등

Problem

  • 연속 펄스 부분 수열의 합

darklight

sublimevimemacs

Java

문제 설명

어떤 수열의 연속 부분 수열에 같은 길이의 펄스 수열을 각 원소끼리 곱하여 연속 펄스 부분 수열을 만들려 합니다. 펄스 수열이란 [1, -1, 1, -1 …] 또는 [-1, 1, -1, 1 …] 과 같이 1 또는 -1로 시작하면서 1과 -1이 번갈아 나오는 수열입니다.예를 들어 수열 [2, 3, -6, 1, 3, -1, 2, 4]의 연속 부분 수열 [3, -6, 1]에 펄스 수열 [1, -1, 1]을 곱하면 연속 펄스 부분수열은 [3, 6, 1]이 됩니다. 또 다른 예시로 연속 부분 수열 [3, -1, 2, 4]에 펄스 수열 [-1, 1, -1, 1]을 곱하면 연속 펄스 부분수열은 [-3, -1, -2, 4]이 됩니다.정수 수열 sequence가 매개변수로 주어질 때, 연속 펄스 부분 수열의 합 중 가장 큰 것을 return 하도록 solution 함수를 완성해주세요.


제한 사항

  • 1 ≤ sequence의 길이 ≤ 500,000
  • 100,000 ≤ sequence의 원소 ≤ 100,000
    • sequence의 원소는 정수입니다.

입출력 예

sequence result
[2, 3, -6, 1, 3, -1, 2, 4] 10

입출력 예 설명

주어진 수열의 연속 부분 수열 [3, -6, 1]에 펄스 수열 [1, -1, 1]을 곱하여 연속 펄스 부분 수열 [3, 6, 1]을 얻을 수 있고 그 합은 10으로서 가장 큽니다.

Description

  • 솔직히 이게 3레벨이라 믿기지 않는 문제. 1레벨이면 충분할 것 같다.
  • 설명에서 주어진 바와 같이 배열의 각 인덱스에 1과 -1을 번갈아가며 곱해준 것과 -1과 1을 번갈아가며 곱해준 것 중 연속 합이 큰 것을 고르면 된다.
  • DP를 이용해 쉽게 풀 수 있는데, 현재 값을 더 했을 때의 최대 값과 더하지 않았을 때의 최대 값을 비교하면 된다.
  • 최대 값을 저장하기 위한 1차원 DP 배열을 선언한다.
    • long[] sum = new long[length]
  • 주어진 예제를 이용해 설명해 보면
    • 먼저 1, -1, 1, -1, … 을 곱한 배열은 다음과 같다.
    • [2, -3, -6, -1, 3, 1, 2, -4]
    • 여기에 대해 0번 인덱스 부터 하나씩 옮겨가며 다음과 같이 비교한다.
      • if 문의 비교연산은 다음 의미를 갖는다.
        • 이전까지의 합과 현재 합을 더했을 때, 0보다 큰가? 즉, 음수가 되지 않았는가에 대한 비교이다.
        • 만약 0 이하라면, sum[i] → 0으로 설정하여 현재 인덱스 까지의 연산을 초기화한다.
    • if(sum[i-1] + sequence[i] > 0) sum[i] = sum[i-1] + sequence[i]; else sum[i] = 0;
    • 그런 다음, answer와 현재까지의 합에 대하여 크기 비교를 수행한다.
      • if 문의 비교 연산은 다음 의미를 갖는다.
        • 이전 index까지 배열의 합이 answer보다 크다는 것은, 이전의 최대 합 보다 현재의 최대 합이 더 크다는 뜻이다. 따라서 answer을 갱신한다.
        • 그렇지 않을 경우, 이전에 찾은 answer가 최대 값이기 때문에 answer를 유지한다.
    • if(sum[i] > answer) answer = sum[i];

Result

import java.util.*;
class Solution {
    public long solution(int[] sequence) {
        long answer = 0;
        int length = sequence.length;
        long[] sum = new long[length];
        int m = 0, n = 1;

        if(length == 1){
            return Math.max(sequence[0], sequence[0] * -1);
        }

        //* [1, -1, 1, -1] ... 을 곱했을 때
        for(int i = 0; i < length; i++){
            if(i % 2 == 1){
                sequence[i] *= -1;
            }

            //* Business Logic
            if(i == 0){
                sum[i] = Math.max(sequence[i], 0);
                continue;
            }
            sum[i] = Math.max(0, sum[i-1] + sequence[i]);
            answer = Math.max(answer, sum[i]);
        }

        //* [-1, 1, -1, 1] ... 을 곱했을 때
        for(int i = 0; i < length; i++){
            sequence[i] *= -1;

            //* Business Logic
            if(i == 0){
                sum[i] = Math.max(sequence[i], 0);
                continue;
            }
            sum[i] = Math.max(0, sum[i-1] + sequence[i]);
            answer = Math.max(answer, sum[i]);
        }

        return answer;
    }
}

오늘은 오주주의 맥세이프 거치대를 리뷰하려 한다.

아이폰 13프로를 쓰면서, 가장 편하다고 생각한 기능 중 하나가 맥세이프인데, 오주주에서 이를 활용한 거치대 체험단을 모집한다 하여 신청하였고, 운이 좋아 체험단에 선정되어 해당 제품을 사용해 볼 수 있게 되었다.


핸드폰 거치대를 사용해 본 많은 분들은 느끼겠지만, 핸드폰을 거치대에 고정 시킬때 꽤나 힘들다. 단순히 휴대폰을 고정하기 위한 고정 작업의 귀찮음 뿐만 아니라, 휴대폰을 거치하거나 탈착할 때 혹시 기스가 나진 않을까하는 조마조마함을 느껴봤을 거라 생각한다.

나 또한 예전에 거치대를 사용했지만, 위와 같은 이유로 더이상 사용하지 않고 있었다.

출처: 오주주 홈페이지

특히, 휴대폰을 사용하기 위해 거치대에서 핸드폰을 분리해야 하는 그 귀찮음이란!!

출처: 오주주

그런데, 오주주에서 이를 해결하는, 사용자의 요구사항을 잘 분석한 맥세이프 거치대를 내놓은 것이다!!!


체험단 신청 후, 바빠서 결과를 확인하지 못하고 있었는데, 나에게 이런 행운의 메시지가 와서, 당첨된 사실을 알게 되었다.

 

감사합니다 담당자님..

제품 패키지 및 구성품

제품 구성품은 정말 심플하다. 오주주도 지구 보호에 앞장 서는지, 박스 패키지부터 친환경(재활용)소재를 사용한 것이 눈에 띄었다.

이게, 사진으로 봐서는 이게 무슨 친환경이야? 할 수 있지만, 친환경 패키지를 많이 써 본 사람들은 해당 박스를 딱 받았을 때, 어!! 하는 느낌을.. 받을수 있다.

조립 방법

조립 방법이 끼우고 끼우면 끝! 이라고 나와있는데, 끼우고! 까지는 인정하지만, 그 뒤의 끼우면!은 살짝, 팁이 필요하다.

맥세이프의 머리에 보면, 조을 수 있게 되어있는데 해당 부품을 풀고, 거치대에 끼운 뒤, 잠궈줘야 한다. 바로 끼우면 부서질 수 있으니 주의해야 한다!!


설치 완료 모습

생각보다 거치대의 클램프 고정 나사(?)가 길어서, 당황하긴 했지만,,, 결국 옷장에 고정시켜 나도 누워서 휴대폰을 할 수 있게 됐다!!

이제 나는 밤을 잃었..

거치대가 상하 좌우, 심지어 핸드폰이 걸려있는 헤드도 360도 회전이 되어서.. 정말 어느 자세로든 휴대폰을 즐길 수 있게 되었다.

 

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.


설치가 쉽고, 강력하게 고정되며, 편리하게 사용할 수 있는 거치대.
이 제품과 함께라면, 여러분의 밤은 이미 빼앗긴 상태

총점: ★★★★★ 별이 다섯 개~

장점

1. 누워서 휴대폰 할 때 전혀 불편하지 않다. (거치대의 포지션을 마음껏 수정할 수 있음)

2. 설치가 진~~짜 쉽고, 강력하게 고정된다.

3. 더이상 휴대폰을 머리위에 떨어트릴 걱정하지 않아도 된다.

단점: 없음!!

 

오주주 구매 링크

 

오주주 맥세이프 자바라 거치대 : 오주주

자바라 거치대, 침대 핸드폰 거치대, 누워서 핸드폰 거치대, 아이폰 거치대, 탁상용 거치대, 집게형 거치대

smartstore.naver.com

본 포스팅은 맥쓰사에서 체험단으로 선정되어 오주주로부터 제품을 지원받아 작성한 글 입니다.

+ Recent posts