데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      쿠버네티스가 쉬워지는 컨테이너 이야기 — cgroup, cpu편

      천강민 24.10.24
      4,110 14 0
      DEVOTEE 요약
      본 블로그는 쿠버네티스(Kubernetes) 사용 전 컨테이너 기술에 대한 이해를 돕기 위한 시리즈로, 컨테이너 배포 시 고려해야 할 사항과 k8s가 제공하는 기능들을 이해하는 데 중점을 둔다. 또한, 시리즈를 통해 cgroup v2와 같은 기반 기술을 설명하며, CPU 관련 설정과 사용률 측정 방법 등 실무에서 겪을 수 있는 다양한 CPU 제어 관련 현상에 대해 다룬다. 이를 통해 독자들은 쿠버네티스 환경에서 적절한 자원 관리 및 최적의 애플리케이션 배포 방식을 판단하는 데 도움을 받을 수 있다.
      DEVOTEE 추천 블로그

      들어가며,

      이 글은 쿠버네티스(이하 k8s)를 접하기 전에 알았다면 좋았을 컨테이너를 이루는 기술들을 설명하고

      실무에서 겪을 수 있는 현상들 또는 아직 겪지 못했지만 발생할 수 있는 내용들을 설명하기 위한 시리즈입니다.

      이를 통해 기반이 되는 기술들에 대해 이해하고,

      1. k8s가 오케스트레이션 도구로써 제공하는 것이 무엇인지,

      2. 앞으로 나올 또는 지금까지 나온 기능들이 무엇인지,

      3. 컨테이너를 배포할 때 무엇을 고려해야 하는지,

      4. k8s가 갖고 있는 다양한 논쟁거리들(X 유형의 어플리케이션은 올리면 안된다 또는 어울리지 않는다 등)을 스스로 판단하고 더욱 잘 활용할 수 있기를 바랍니다.


      시리즈 대상독자

      1. 도커 사용 경험이 있는 사람

      2. 컨테이너는 프로세스라고 말할 수 있는 사람

      3. k8s를 공부하고 있지만 어려운 사람


      이번 글의 대상이 아닌 독자

      1. k8s는 CPU를 왜 압축 가능한 자원으로 보는가?

      2. k8s에서 CPU requests/limits는 어떤 의미인가?

      3. 컨테이너의 CPU 사용률은 어떻게 측정되는가?

      4. CPU 사용률이 100%가 아닌데 왜 throttled 상태가 되는가?

      5. 일정량의 RPS 또는 TPS가 목표일 때, 내 어플리케이션에 적용해야할 CPU값은 어떻게 알 수 있는가?

      6. k8s에서 CPU와 관련된 추가 기능은 무엇이 나올 수 있을까?

      7. 컨테이너 환경은 CPU 활용에 있어 어떤 문제가 있는가?

      8. 7번의 문제는 특정 유형의 어플리케이션을 컨테이너로 올리는데 문제가 되는가?

      1 ~ 5번에 대한 답과, 6 ~ 8번에 대한 개인적인 생각이 있다면 이번 글은 읽지 않으셔도 됩니다.


      최종 결과물


      쿠버네티스는 잘 몰라도, 환경은 만들 줄 아는 사람이 되자!

      시리즈를 마치게 되면 우리는 위 그림과 같은 아키텍처를 만들 수 있게 될겁니다.

      다만, 만들어 보는 것 자체에 의의를 두기 보단 앞서 말씀드린 기술들을 이해하는 시리즈가 되기를 바랍니다.


      사전준비

      docker run -d --rm --privileged --name [이름] docker:27.3.1-dind-alpine3.20

      위와 같이 컨테이너를 띄우고 docker exec -it [이름] sh 로 붙으면 끝입니다.(이름 지정, 별도 이미지 사용 등은 선택) 필요한 툴은 그때 그때 다운로드 받을 예정입니다.

      이외에도 실습을 위한 제한이 필요한 경우 추가적인 플래그가 들어갈 예정입니다. (이번 글에서는 결과를 간략히 보여드리기 위해 도커에게 1개의 CPU만 사용 가능하도록 설정 했습니다.)


      Control Group(cgroup) v2

      cgroup(이하 제어그룹) v2는 프로세스를 계층적으로 구성하고 각 계층 별로 시스템 리소스를 제어할 수 있도록 지원하는 기능입니다.

      이러한 제어그룹은 크게 **“코어"**와 **“컨트롤러"**로 나뉘어 있으며, 우리가 주로 살펴볼 내용은 실제 제어를 담당하고 있는 컨트롤러 부분입니다.

      v1과 v2? v1의 다양한 문제들을 해결하기 위해 v2가 출시 됐습니다. 자세한 내용은 Issues with v1 and Rationales for v2, Understanding the new control groups API를 참고하세요.


      제어그룹 디렉토리 살펴보기

      본격적인 설명에 앞서 시스템에서 확인해보면 아래와 같습니다.

      / # cd /sys/fs/cgroup/sys/fs/cgroup 
      # ls
      cgroup.controllers               cpu.stat.local         ......
      cgroup.events                    cpu.weight             ......

      (직접 해보시면) 위에 보여드린 결과 보다 훨씬 더 많은 내용들이 나오는 걸 볼 수 있습니다. 자, 그럼 이제 디렉토리를 하나 만들어 보겠습니다.

      /sys/fs/cgroup # mkdir rex
      /sys/fs/cgroup # ls rex
      cgroup.controllers               cpu.stat.local         ......
      cgroup.events                    cpu.weight             ......

      디렉토리만 만들었을 뿐인데 신기하게도 거의 비슷한 형태로 파일이 만들어진 것을 볼 수 있습니다.

      이는 디렉토리 생성 시 커널이 자동으로 해당 디렉토리 내부에 파일을 생성해주기 때문입니다.

      결국 제어그룹은 아래 그림과 같은 계층을 유지하게 됩니다.


      최상단의 루트 제어그룹(디렉토리)과 자식 제어그룹(디렉토리)들


      각 파일의 용도를 쉽게 파악하기

      파일들이 매우 많기 때문에 항상 문서를 참고해야 할까요? 어느 정도는 맞는 말이고, 어느 정도는 틀린 말입니다. 파일들의 자세한 정보를 확인해보면,

      /sys/fs/cgroup # ls -l
      total 0
      -r--r--r--    1 root     root             0 Oct  3 07:57 cgroup.controllers
      -r--r--r--    1 root     root             0 Oct  3 07:57 cgroup.events
      -rw-r--r--    1 root     root             0 Oct  3 07:57 cgroup.freeze
      --w-------    1 root     root             0 Oct  3 07:57 cgroup.kill
      ......

      r, rw, w 이렇게 세 가지가(디렉토리 제외) 보일텐데요.

      1. r: 읽기만 가능 (보통 상태나 통계를 보여줌)

      2. w: 쓰기만 가능 (입력 시 특정 행위를 하는 파일)

      3. rw: 읽고 쓰기 가능 (제한을 설정하는 파일)

      위와 같이 구분하시면 적어도 각각이 어떤 역할을 하는 것인지 쉽게 파악이 가능합니다.


      진짜 들어가기 전에

      앞서 말씀드렸다시피 기본적으로 도커에 컨테이너를 띄워서 결과를 보여드릴 예정입니다.

      그렇다 보니 실제 문서와는 다르게 보이거나 도커에서는 테스트가 불가능한 경우가 발생할 수 있습니다.

      이런 경우에는 별도 VM 또는 직접 서버를 구성하여 보여 드릴 예정이고, 환경이 다른 경우 미리 말씀 드리겠습니다.

      1. 지금 보고 계신 /sys/fs/cgroup은 실제로는 루트 제어그룹이 아닙니다.(루트 제어그룹은 아래와 같이 입력하면 볼 수 있습니다.)

      # docker run -it --rm --privileged --pid=host justincormack/nsenter1# ls /sys/fs/cgroup01-docker               cgroup.stat             cpuset.cpus.effective  ......cgroup.controllers      cgroup.subtree_control  cpuset.cpus.isolated   ......

      2. 왜 저렇게 명령을 입력해야 하는지는 시리즈 중 네임스페이스 부분에서 설명 드릴 예정입니다.


      cgroup

      앞에서 살펴본 파일들 중 “cgroup”이라는 이름으로 시작하는 파일들을 **“코어 인터페이스 파일”**이라고 부릅니다.

      말 그대로 ‘코어' 기능을 활용하려면 이 파일들을 이용하라는 뜻 입니다.

      cgroup 외 다른 파일들은 X(cpu, memory 등) 컨트롤러에 속한 X 인터페이스 파일이라고 부릅니다.

      (ex. cpu 컨트롤러에 속한 cpu 인터페이스 파일) 그럼 cgroup으로 시작하는 파일들에 대해 알아 보겠습니다.

      cgroup.procs

      읽기/쓰기가 가능한 파일로 현재 제어그룹에 의해 제어되고 있거나 제어할 PID(Process ID)의 목록이 존재하는 파일입니다.

      예를 들어, /sys/fs/cgroup/init에 속한 프로세스를 확인해보면 아래와 같습니다.

      /sys/fs/cgroup/init # cat cgroup.procs
      1
      9
      ......

      추가적으로 해당 파일에 명시된 프로세스들이 자식 프로세스를 만들면 자동으로 동일한 제어그룹에 속하게 됩니다. (변경 가능함)

      ✅ 잠깐! 특정 프로세스가 어느 제어그룹에 속하는지 찾고 싶다면?

      / # find /sys/fs/cgroup -name cgroup.procs -exec grep -H 9 {} \;
      ./cgroup.procs:9

      cgroup.controllers

      읽기 전용 파일로 현재 제어그룹에서 사용 가능한 컨트롤러의 목록이 포함되어 있습니다.

      /sys/fs/cgroup # ls -l cgroup.controllers
      -r--r--r--    1 root     root             0 Oct  3 07:57 cgroup.controllers
      
      /sys/fs/cgroup # cat cgroup.controllers
      cpuset cpu io memory hugetlb pids rdma

      cgroup.subtree_control

      읽기/쓰기가 가능한 파일로 하위 제어그룹들이 사용 가능한 제어그룹 목록을 지정 할 수 있습니다.

      /sys/fs/cgroup # cat cgroup.subtree_control
      cpuset cpu io memory hugetlb pids rdma
      
      # 하위 제어그룹에서 cpuset, pids 컨트롤러 사용 불가 (제거)
      /sys/fs/cgroup 
      # echo "-cpuset -pids" > cgroup.subtree_control
      /sys/fs/cgroup 
      # cat cgroup.subtree_control
      cpu io memory hugetlb rdma
      
      # 하위 제어그룹에서 cpuset, pids 컨트롤러 사용 가능 (복구)
      /sys/fs/cgroup 
      # echo "+cpuset +pids" > cgroup.subtree_control
      /sys/fs/cgroup 
      # cat cgroup.subtree_control
      cpuset cpu io memory hugetlb pids rdma

      컨테이너를 이해하기 위해 꼭 필요한 파일은 아니지만, 두 파일의 존재를 모르는 상태로 테스트를 진행하다 보면 ‘왜 안되지?’ 라는 상황이 나올 수 있어 설명 드렸습니다.

      cgroup과 관련한 내용은 여기까지 입니다.

      사실 이외에도 제어그룹 자체에 대한 규칙이나 안내 사항들이 존재하지만, 해당 내용들은 필요 할 때에 하나씩 설명 드리도록 하겠습니다.


      CPU 컨트롤러

      아마 서버에서 가장 중요한 두 가지를 뽑으라고 하면 CPU와 메모리를 선택하시는 분들이 많을 것 같습니다.(물론 다른 모든 것들도 중요합니다.)

      그런 만큼 CPU 컨트롤러 부분은 조금 더 자세하게, 그리고 깊이있게(?) 알아보려고 합니다.

      cpu.weight

      루트 제어그룹이 아닌 자식 제어그룹에만 존재하며, 루트 하위의 모든 제어그룹에 존재하는 cpu.weight(가중치, 기본 값 100)를 기반으로 CPU를 분배 받을 수 있도록 하는 파일입니다. 쉽게 표현하면 아래와 같습니다.

      [CPU를 100% 활용하는 상황 가정]A 제어그룹 cpu.weight: 100B 제어그룹 cpu.weight: 200A = 100 / (100 + 200) = 약 33% 활용 가능B = 200 / (100 + 200) = 약 67% 활용 가능

      생각보다 어려운 얘기는 아니죠? 그렇다면 B 제어그룹은 CPU를 사용하지 않고, A 제어그룹에서만 CPU를 사용한다면 어떻게 될까요? 먼저 테스트 가능한 환경을 만들어 보겠습니다.

      # 1번 터미널/sys/fs/cgroup # apk add stress-ng htop # 툴 다운로드/sys/fs/cgroup # mkdir A B # A, B 제어그룹 생성# 2번 터미널/ # echo $$ > /sys/fs/cgroup/A/cgroup.procs/ # echo 100 > /sys/fs/cgroup/A/cpu.weight# 3번 터미널/ # echo $$ > /sys/fs/cgroup/B/cgroup.procs/ # echo 200 > /sys/fs/cgroup/B/cpu.weight

      위 작업을 그림으로 나타내면 아래와 같습니다.


      먼저, A 제어그룹에 속한 쉘에서만 stress-ng 명령(stress-ng — cpu 1)을 실행해보겠습니다.


      cpu.weight이 낮아도 100% 사용 가능!

      이제 이 상태에서 B 제어그룹에 속한 쉘에서 동일한 명령을 실행해보면,


      CPU가 바빠지는 경우, cpu.weight 비율대로 분배됨!

      계산했던 방식대로 CPU가 분배되는 것을 확인할 수 있습니다. k8s에서는 spec.containers[].resources.requests.cpu 값을 통해 이러한 설정을 지원하고 있습니다.

      (예상과 달리(?) 실제 시간이나 코어의 갯수를 설정하는 것이 아님.) 이를 통해 아래 2가지의 사실을 알 수 있습니다.

      1. CPU를 압축 가능한 자원이라고 하는 이유

      2. k8s에서 CPU는 requests만 지정하면 된다! 라는 주장의 근거

      cpu.max

      루트 제어그룹이 아닌 자식 제어그룹에만 존재하며, 해당 제어그룹에 속한 프로세스들이 일정 기간(PERIOD) 내 사용 가능한 시간(MAX, QUOTA)을 설정하는 파일입니다.

      설정된 값의 형태는 아래와 같습니다.

      /sys/fs/cgroup/A # cat cpu.maxmax 100000 # MAX(QUOTA) PERIOD

      설정 값의 단위는 us(마이크로초)이며, PERIOD(기간) 값은 최소 1000(1ms)/최대 1000000(1초)로 설정 가능합니다.

      MAX(QUOTA) 값을 max로 두는 경우 throttled 상태가 되지 않습니다. (throttled 상태가 되지 않는다는 것이 기간 내에서 무한히 CPU를 사용한다는 뜻은 아닙니다.)

      throttled 상태에 대해서는 cpu.stat 파일을 살펴 볼 때 더욱 자세히 설명 드리겠습니다. 그럼 이제 cpu.max를 설정한 후 사용량을 보겠습니다.

      /sys/fs/cgroup/A # echo "10000" > cpu.max/sys/fs/cgroup/A # cat cpu.max10000 100000/sys/fs/cgroup/A # stress-ng --cpu 1

      값을 하나만 입력하는 경우 앞의 값(MAX, QUOTA)만 바뀌게 됩니다.

      현재 설정한 값을 해석하면 “100ms 당 10ms만 사용 가능" 이라고 볼 수 있습니다. 이제 stress-ng 명령을 실행 후 결과를 살펴보면,


      대략 10% 가까이에서 표현됨

      대략 10% 가까이에서 결과가 나오는 것을 볼 수 있습니다. k8s에서는 spec.containers[].resources.limits.cpu 값을 통해 이러한 설정을 지원하고 있습니다.

      (이 또한 예상과 달리 코어의 갯수를 설정하는 것이 아님.)

      cpu.weight을 통해 CPU가 분배되는데 왜 굳이 limits를 설정하는 걸까요? 개인적인 생각으론 아래 두 가지 정도라고 생각합니다.

      1. 뒤에서 살펴 볼 throttled 상태를 통한 개선의 기회

      2. cpu.max를 통한 어플리케이션에 적용할 CPU 값 식별

      물론, 이 또한 개인적인 생각이고 누군가의 주장이 틀렸다고 생각하진 않습니다.

      다만, 이 글을 읽으시는 분들께선 자신의 상황에서 무엇이 더 적절할지 판단 할 수 있다면 그것으로 좋다고 생각합니다.

      (추가로 고민해봐야 되는 부분이 있는데 이건 memory 편에서 살펴 보겠습니다.)

      이것으로 CPU 사용량을 분배하고 제한하는 파일들을 모두 살펴 봤습니다. 이제 마지막 파일인 cpu.stat을 보겠습니다.

      cpu.stat

      cpu.stat은 모든 제어그룹에 존재하는 읽기 전용 파일로, 각 제어그룹에 속한 프로세스들이 사용한 CPU 관련 통계를 제공합니다. 실제 형태는 아래와 같습니다.

      /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     # 버스트 모드에서 사용한 시간

      위 값 자체의 의미가 크게 중요한건 아니고, 이걸 어떻게 활용할지를 살펴보려고 합니다. 다만, burst 관련 기능은 따로 사용해보시기 바랍니다.

      nr_throttled와 throttled_usec 값은 cpu.max에 설정된 CPU 사용 가능 시간을 모두 소모하게 되면 증가하게 됩니다. (각각 throttled가 된 횟수, throttling에 걸렸던 마이크로초)

      “이번 글의 대상이 아닌 독자"에서 언급했던 “3. 컨테이너의 CPU 사용률은 어떻게 측정되는가?”를 한 번 살펴보려고 합니다.

      사실 엄청나게 간단합니다. 잠깐 생각해보시고 아래 내용을 보시기 바랍니다.

      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}%")

      생각보다 되게 간단하죠? 결국 1) 현재 CPU 사용 시간과 시간을 이전 데이터로 저장해두고,

      원하는 수치 만큼 휴식(sleep) 후에 다시 2) 현재 값을 구해 3) 빼고 나누는게 끝입니다. (글로 설명하니까 오히려 더 어려운 느낌이네요.)


      제가 작성한 간단한 코드 한 번 살펴 보겠습니다.

      import time
      
      INTERVAL_SECONDS = 1
      CGROUP_PATH = "/sys/fs/cgroup/A/cpu.stat"
      
      def get_cpu_stat():    
          result = {}    
          with open(CGROUP_PATH) as fp:        
              for i in fp.readlines():
                 name, value = i.split(" ")
                  result[name] = int(value) 
             return result
      
      
      def get_cpu_period():
          with open(CGROUP_PATH) as fp:
              _, cpu_period = fp.readline().split(" ")
              return int(cpu_period)    
      def get_curr_time_us(): 
         return int(time.time_ns() // 1000)
      
      cpu_period = get_cpu_period()
      last_usage_us = get_cpu_stat()["usage_usec"]
      last_time_us = get_curr_time_us()
      time.sleep(INTERVAL_SECONDS)
      
      while True:
          curr_total_usage = get_cpu_stat()["usage_usec"]
          curr_time_usec = get_curr_time_us()
      
          delta_total_usage = curr_total_usage - last_usage_us
          delta_time_usec = curr_time_usec - last_time_us
      
          usage_percent = delta_total_usage / delta_time_usec * 100
          print(f"{usage_percent:.2f}%")
      
          last_usage_us = curr_total_usage
          last_time_us = curr_time_usec
      
          time.sleep(INTERVAL_SECONDS)

      어려운 내용은 아니니 천천히 살펴보시기 바랍니다. 그럼 실제 값은 어떻게 나오는지 살펴보겠습니다.

      / # apk add python3
      
      / # python3 cpu_stat_only_cpu_usage.py 
      99.89%
      99.86%
      ......

      이제 cpu.max를 설정 했을 때 증가하는 값들도 살펴 보겠습니다.

      /sys/fs/cgroup/A # echo 20000 > cpu.max
      
      /sys/fs/cgroup/A # stress-ng --cpu 1 & # 백그라운드에서 실행
      
      /sys/fs/cgroup/A # cat cpu.stat
      ......
      nr_periods 208
      nr_throttled 176
      throttled_usec 14205095
      ......


      기존엔 오르지 않았던 3개의 값이 증가한 것을 볼 수 있습니다.

      이 또한 값 자체가 중요하다기 보다, 남은 질문과 고민에 대해서 해결해보시면 좋을 것 같습니다.


      CPU 사용률이 100%가 아닌데 왜 throttled 상태가 되는가?

      CPU 사용률 측정 주기를 5초로 설정하고, 중간에 stress-ng --cpu 1 명령을 2초 정도 실행해보자.

      1) CPU 사용률이 100%인가?

      2) throttled 비율은 어떻게 되는가?


      일정량의 RPS 또는 TPS가 목표일 때, 내 어플리케이션에 적용해야할 CPU값은 어떻게 알 수 있는가?

      1) 어플리케이션을 띄우는데 사용될 CPU 시간을 구할 수 있는가?

      2) RPS 1, 100, 10000일 때 내 어플리케이션은 CPU를 어느 정도 사용하는가?

      3) 실시한 테스트가 현실적인가?

      4) 현실적이지 않다면 어떤 부분 때문인가?

      5) 결과가 더욱 좋게 나오는가? 나쁘게 나오는가? 예상과 같을까?


      이것으로 cgroup과 cpu에 대한 이야기가 끝이 났습니다.

      다음 편은 memory와 관련되 얘기로, 여러가지 사실과 오해에 대해서 알아 볼 예정입니다.


      참고

      1. https://docs.kernel.org/admin-guide/cgroup-v2.html

      2. https://github.com/google/cadvisor

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      천강민 님의 최신 블로그

      더보기

      DEVOTEE 추천 블로그

      동영상 기고하기