cgroups의 현재(Linux 3.15) 구현을 통해 제어가 어떻게 이루어지는지 살펴보고, 여러 서브시스템이 계층과 상호작용하는 방식을 비교한다.
지난 두 편의 글에서 프로세스의 그룹화와 계층의 과제에 관한 배경을 살펴봤으니, 이제는 제어(control)에 대한 이해를 얻어야 한다. 이를 위해 현재(Linux 3.15) cgroups 구현의 몇 가지 세부를 살펴볼 것이다. 먼저 cgroups에서 제어가 어떻게 행사되는지부터 보고, 이후 글에서 계층이 어떻게 사용되는지 살펴보겠다.
$ sudo subscribe today 오늘 구독하고 LWN 권한을 한 단계 높이세요. 게시되는 즉시 LWN의 고품질 기사에 접근할 수 있고, 그 과정에서 LWN을 지원하는 데도 도움이 됩니다. 지금 행동하세요. 무료 체험 구독으로 시작할 수 있습니다.
물론 cgroups가 등장하기 훨씬 이전부터도 어느 정도의 제어는 존재했다. 각 프로세스가 받는 CPU 시간을 조절하는 "nice" 설정이 있었고, setrlimit() 시스템 호출로 제어할 수 있는 메모리 및 기타 자원에 대한 다양한 제한도 있었다. 하지만 그런 제어는 언제나 각 프로세스에 개별적으로 적용되거나, 많아야 시스템 전체 맥락에서 각 프로세스에 적용되는 정도였다. 따라서 그런 제어만으로는 프로세스를 그룹으로 묶는 것이 제어 문제에 어떤 영향을 미치는지에 대한 힌트를 거의 주지 못한다.
Linux의 cgroups 기능은 cgroups가 제공하는 그룹화를 각각 독립적으로 활용하는 별도의 "서브시스템"을 허용한다. "서브시스템"이라는 용어는 불행히도 매우 포괄적이다. 흔히 쓰이는 "resource controller"(자원 컨트롤러)라는 용어를 쓰는 편이 더 나을 것이다. 하지만 모든 "서브시스템"이 진정한 의미의 "컨트롤러"는 아니므로, 여기서는 더 일반적인 용어를 유지하겠다.
Linux 3.15에는 12개의 서로 구분되는 서브시스템이 있으며, 이들을 함께 보면 cgroups를 둘러싼 여러 이슈에 대해 유용한 통찰을 제공한다. 우리는 대략 복잡도가 증가하는 순서로 살펴볼 것이다. 여기서 말하는 복잡도는 무엇을 제어하는지가 아니라, 그 제어에 그룹, 특히 그룹의 계층이 얼마나 관여하는지의 복잡도다. 이 글에서는 그중 약간 절반을 다루고 나머지는 다음 글로 미루겠다.
첫 7개를 자세히 보기 전에, 서브시스템이 할 수 있는 일을 간단히 개괄하면 도움이 된다. 각 서브시스템은 다음을 할 수 있다.
물론 서브시스템은 원하는 결과를 구현하기 위해 프로세스 및 다른 커널 내부와 임의의 방식으로 상호작용할 수도 있다. 위 항목들은 프로세스 그룹 인프라와 상호작용하기 위한 공통 인터페이스에 해당한다.
debug 서브시스템은 아무것도 "제어"하지 않는 것들 중 하나이며, (불행히도) 버그를 제거하지도 않는다. 어떤 cgroup에도 추가 데이터를 붙이지 않고, 어떤 "attach"나 "create" 요청도 거부하지 않으며, fork나 exit에도 관심이 없다.
이 서브시스템을 활성화했을 때의 유일한 효과는, 개별 그룹의 내부 세부 사항이나 cgroup 시스템 전체의 내부 세부 사항 일부를 cgroup 파일시스템 안의 가상 파일을 통해 보이게 만든다는 점이다. 여기에는 어떤 자료구조의 현재 참조 카운트나 내부 플래그 설정 같은 것들이 포함된다. 이런 정보는 cgroups 인프라 자체를 작업하는 사람에게나 흥미로울 가능성이 있다.
사람들에게 신분증을 휴대하도록 요구하는 것이 때로는 그들을 통제하기 위한 첫 단계가 될 수 있다는 생각을 내게 처음 접하게 해준 사람은 Robert Heinlein이었다. 사람을 통제하기 위해 이런 일을 하는 것은 달갑지 않을 수 있지만, 프로세스를 통제하기 위한 명확한 식별을 제공하는 것은 꽤 실용적이고 유용할 수 있다. 이것이 주로 net_cl과 net_prio cgroup 서브시스템이 관심을 두는 부분이다.
이 두 서브시스템은 각각 각 cgroup에 작은 식별 번호를 연관시키고, 그 번호를 해당 그룹의 프로세스가 생성한 모든 소켓에 복사한다. net_prio는 cgroups 코어가 각 그룹에 제공하는 시퀀스 번호(cgroup->id)를 사용해 이를 sk_cgrp_prioidx 속성에 저장한다. 이는 각 cgroup마다 유일하다. net_cl은 각 cgroup에 식별 번호를 명시적으로 지정할 수 있게 하고, 이를 sk_classid 속성에 저장한다. 이는 cgroup마다 반드시 유일할 필요는 없다. 이 두 가지 서로 다른 그룹 식별자는 세 가지 서로 다른 작업에 사용된다.
net_cl이 설정한 sk_classid는 사용될 수 있으며, iptables와 함께 어떤 cgroup이 원본 소켓을 소유하는지에 따라 패킷을 선택적으로 필터링할 수 있다.sk_classid는 또한 사용될 수 있으며, 네트워크 스케줄링 동안 패킷 분류(packet classification)에 쓰인다. 패킷 분류기는 cgroup과 다른 세부 사항에 따라 결정을 내릴 수 있고, 이 결정은 각 메시지의 우선순위 설정을 포함한 다양한 스케줄링 세부에 영향을 줄 수 있다.sk_cgrp_prioidx는 순수하게 네트워크 패킷의 우선순위를 설정하는 데만 사용되며, 사용될 경우 SO_PRIORITY 소켓 옵션이나 다른 어떤 수단으로 설정된 우선순위를 덮어쓴다. sk_classid와 패킷 분류기를 사용해 유사한 효과를 낼 수도 있다. 하지만 net_prio 서브시스템을 도입한 커밋에 따르면, 패킷 분류기만으로는 항상 충분하지 않으며, 특히 데이터센터 브리징(DCB)이 활성화된 시스템에서 그렇다고 한다.서로 다른 두 서브시스템이 소켓을 세 가지 방식으로 제어하면서 약간의 중복까지 갖는다는 것은 이상해 보인다. 하지만 이는 앞서 프로세스 그룹에서 보았던 중복과도 약간 닮아 있다. 서브시스템이 더 경량이어서(각 제어 요소당 하나씩) 많이 두는 것이 싸야 하는지, 아니면 더 강력해서 하나로 모든 요소에 사용할 수 있어야 하는지에 대해서는 아직 분명하지 않다. 뒤의 서브시스템들에서도 더 많은 중복을 만나게 될 텐데, 그것이 이 문제를 더 명확히 해줄지도 모른다.
지금으로서는 남은 중요한 쟁점은 이 서브시스템들이 cgroups의 계층적 성격과 어떻게 상호작용하느냐이다. 답은 대체로 "거의 상호작용하지 않는다"이다.
net_cl에 설정된 class ID와 net_prio에 설정된(장치별) 우선순위는 오직 그 cgroup과 그 cgroup에 속한 프로세스와 연관된 소켓에만 적용된다. 자식 cgroup의 프로세스 소켓에는 자동으로 적용되지 않는다. 따라서 이 서브시스템들에서는 그룹의 재귀적 멤버십이 아니라, 오직 즉각적인 멤버십만이 중요하다.
이 제한은 부분적으로는, 새로 생성된 그룹이 부모로부터 중요한 값을 상속받는다는 사실로 상쇄된다. 예를 들어 어떤 cgroup 계층의 특정 서브트리 안 모든 그룹에 대해 특정 네트워크 우선순위를 설정해두었다면, 새 그룹이 만들어지더라도 그 우선순위는 그 서브트리 안의 모든 그룹에 계속 적용된다. 많은 실용적 경우에 이는 유용한 결과를 내기에 충분할 것이다.
계층을 거의 무시한다는 점은, 이 서브시스템들의 관점에서 cgroups 트리가 (계층 글에서 논의한) 분류 계층이라기보다 조직 계층에 더 가깝게 보이게 만든다. 하위 그룹은 진정한 하위 분류라기보다, 그저 가까이에 있어 편리한 어떤 자리일 뿐이다.
다른 서브시스템들은 다양한 방식으로 계층에 더 초점을 맞춘다. 대비해볼 만한 세 가지는 devices, freezer, perf_event이다.
cgroups에 대해 큰 그림으로 생각할 때의 어려움 중 하나는, 개별 사용 사례들이 극단적으로 다양할 수 있고, 그에 따라 기반 인프라에 요구하는 바도 다르다는 점이다. 다음 세 서브시스템은 모두 cgroups의 계층적 배치를 활용하지만, cgroup의 설정이 프로세스에 미치는 효과로 흘러가는(control flow) 세부 방식이 다르다.
devices 서브시스템은 장치 특수 파일(즉, 블록 장치와 문자 장치)에 대한 일부 또는 모든 접근에 대해 강제 접근 제어(mandatory access control)를 부과한다. 각 그룹은 기본적으로 모든 접근을 허용하거나 거부할 수 있고, 그 다음 예외 목록을 가져 접근을 각각 거부하거나 허용할 수 있다.
예외 목록을 갱신하는 코드는, 부모가 허용하지 않는 변경을 거부하거나 변경을 자식에게 전파함으로써, 자식이 부모보다 더 많은 권한을 갖지 못하도록 보장한다. 이는 권한 검사를 수행할 때 한 그룹에 있는 기본값과 예외만 확인하면 된다는 뜻이다. 조상(ancestor)들이 그 접근도 허용하는지 확인하기 위해 트리를 거슬러 올라갈 필요가 없다. 조상들이 강제한 규칙은 이미 각 그룹에 반영되어 있기 때문이다.
여기에는 명확한 트레이드오프가 있다. 권한 갱신 작업은 더 복잡해지는 대신, 권한 테스트 작업은 단순해진다. 전자는 보통 후자보다 훨씬 덜 자주 발생하므로, 이는 합리적인 트레이드오프다.
devices 서브시스템은 이 트레이드오프 선택지 범위에서 중간 지점에 해당한다. 어떤 cgroup에서의 설정은 계층의 리프까지 모든 자식에게 아래로 밀어 넣지만, 그 이상은 하지 않는다. 개별 프로세스는 접근 결정을 내려야 할 때 여전히 관련 cgroup을 참조해야 한다.
freezer 서브시스템은 완전히 다른 요구를 가지므로, 범위에서 다른 지점을 택한다. 이 서브시스템은 각 cgroup에 "freezer.state" 파일을 제공하며, 여기에 "FROZEN" 또는 "THAWED"를 쓸 수 있다. 이는 첫 번째 글에서 만났던 프로세스 그룹 중 하나에 SIGSTOP 또는 SIGCONT를 보내는 것과 약간 비슷하다. 즉 전체 프로세스 집합이 멈추거나 다시 시작된다. 하지만 나머지 세부는 대부분 다르다.
프로세스를 freeze 또는 thaw하기 위해 freezer 서브시스템은 devices 서브시스템과 비슷하게 cgroup 계층을 아래로 내려가며, 모든 하위 그룹을 frozen 또는 thawed로 표시한다. 그 다음 한 단계 더 나아가야 한다. 프로세스는 자신의 cgroup이 frozen 되었는지 정기적으로 확인하지 않으므로, freezer는 모든 하위 cgroup을 넘어 모든 멤버 프로세스를 순회하며 그들을 실제로 freezer로 이동시키거나 다시 밖으로 빼내야 한다. freeze는 각 프로세스에 가상 시그널을 보내는 방식으로 준비되는데, 시그널 처리 코드는 cgroup이 frozen으로 표시되었는지 확인하고 그에 따라 동작하기 때문이다. 어떤 프로세스도 freeze를 빠져나가지 못하게 하기 위해 freezer는 프로세스가 fork할 때 알림을 요청하여 새로 만들어진 프로세스를 잡을 수 있게 한다. fork 알림을 요청하는 서브시스템은 이것이 유일하다.
따라서 freezer는 계층 관리 스펙트럼의 한쪽 극단을 차지한다. 설정을 계층의 맨 아래까지, 그리고 프로세스 내부까지 강제로 관철하기 때문이다. 반대 극단은 perf_event가 차지한다.
perf 기능은 어떤 프로세스 집합에 대해 다양한 성능 데이터를 수집한다. 그 집합은 시스템의 모든 프로세스일 수도 있고, 특정 사용자가 소유한 모든 프로세스일 수도 있으며, 특정 부모에서 파생된 모든 프로세스일 수도 있다. 또는 perf_event cgroup 서브시스템을 사용하면 특정 cgroup "안"에 있는 모든 프로세스가 될 수도 있다. perf_event cgroup 서브시스템은 "안"을 완전히 계층적인 의미로 해석한다. 이는 그룹 멤버십을 테스트하긴 하지만 서브트리 내 일부(그러나 전부는 아닌) 그룹이 공유할 수도 있는 ID 번호에 기반해 테스트하는 net_cl과는 다르다.
이 계층적인 "그룹 안" 테스트를 수행하기 위해 perf_event는 cgroup_is_descendant() 함수를 사용하는데, 이 함수는 단순히 ->parent 링크를 따라 위로 올라가며 일치하는 항목이나 루트를 찾는다. 계층이 아주 깊지 않다면 이는 그리 비싼 작업은 아니다. 하지만 두 숫자를 비교하는 것보다는 확실히 더 비싸다. 네트워킹 코드 개발자들은 성능 비용 추가에 특히 민감하기로 유명하며, 그 비용이 모든 패킷에 적용될 수 있다면 더더욱 그렇다. 따라서 네트워킹 코드가 cgroup_is_descendant()를 사용하지 않는 것은 놀랍지 않다.
이상적인 것은 물론, 완전히 계층적이면서도 매우 효율적인 멤버십 테스트 메커니즘이다. 그런 메커니즘은 독자의 연습 문제로 남겨두겠다.
perf를 보면 설정이 계층 아래로 전혀 밀려 내려가지 않는다. 어떤 제어 결정(즉, 이 이벤트를 카운트해야 하는가?)이 필요할 때마다 코드는 프로세스에서 트리를 위로 거슬러 올라가 답을 찾는다.
이제 net_cl과 net_perf로 돌아가서, cgroup에서 설정을 아래로 푸시하는 것과 프로세스가 위로 손을 뻗어 제어를 받는 것을 대비하는 이 스펙트럼에서 이들이 정확히 어디에 놓이는지 묻는다면, 이들은 devices와 가장 비슷하다. 프로세스는 소켓을 만들 때 하나의 cgroup을 참조하긴 하지만 계층 위로 올라가지는 않는다. 차이점은 설정의 "아래로 푸시"를 커널이 강제하지 않고 사용자 공간에 맡긴다는 점이다.
이번 회차에서 살펴볼 마지막 cgroup 서브시스템은 cpuset이며, 이는 Linux에 처음 추가된 것이다. 사실 cpuset의 프로세스 그룹 제어 메커니즘은 더 일반적인 cgroups 구현보다 앞선다. 이를 보여주는 흔적 중 하나는, cpuset 서브시스템으로 구성했을 때 cgroup 가상 파일시스템과 동등한 별도의 cpuset 가상 파일시스템 타입이 존재한다는 점이다. 이 서브시스템은 실제로 새로운 것을 소개하진 않지만, 우리가 이미 본 몇몇 측면을 강조하는 역할을 한다.
cpuset cgroup 서브시스템은 여러 CPU를 가진 어떤 머신에서도 유용하며, 특히 여러 노드를 특징으로 하고 노드 내부/노드 간 접근 속도가 크게 다른 경우가 흔한 NUMA 머신에서 더 유용하다. net_cl과 마찬가지로 cpuset은 서로 구별되지만 관련된 두 가지 제어를 제공한다. 각 그룹의 각 프로세스가 실행될 수 있는 프로세서 집합을 식별하고, 또한 그룹 내 프로세스가 메모리를 할당받을 수 있는 메모리 노드 집합도 식별한다. 이 두 집합은 종종 같을 수도 있지만, 구현은 이들을 완전히 분리해 유지한다. 이 집합들을 강제하는 방식은 전혀 다른 두 접근을 포함한다. 이는 프로세스를 한 cgroup에서 다른 cgroup으로 옮길 때 가장 분명하다. 허용된 프로세서 집합이 다르면, 프로세스는 허용된 새 프로세서의 런큐에 쉽게 올릴 수 있다. 하지만 허용된 메모리 노드 집합이 바뀌었다면, 한 노드에서 다른 노드로 모든 메모리를 마이그레이션하는 것은 결코 쉽지 않다(그래서 이 마이그레이션은 선택 사항이다).
device 서브시스템과 비슷하게, cpuset은 필요할 때 부모에서 이뤄진 변경을 모든 하위로 전파한다. device와 달리, cgroup이 권위 있는 제어 정보를 담고 있는 것이 아니라 각 프로세스가 스케줄러가 검사하는 자신만의 CPU set을 갖고 있다. 하위 그룹에 변경을 강제한 뒤에, cpuset은 freezer와 마찬가지로 마지막 단계로 개별 프로세스에 새 설정을 부과해야 한다. 왜 cpuset은 freezer처럼 fork 알림을 요청하지 않는지는 미스터리로 남겨두어야겠다.
이와 결합되어, cpuset은 때로는 적절한 부모를 찾기 위해 계층을 _위로_도 탐색한다. 그런 경우 중 하나는 CPU가 오프라인되는 등의 이유로 어떤 cgroup이 자신의 집합 내에 동작 가능한 CPU가 하나도 없게 되었을 때다. 또 다른 경우는 고우선순위 메모리 할당이 mems_allowed 집합 안의 모든 노드에서 가용 메모리가 고갈되었음을 발견했을 때다. 이 두 경우 모두, 조상 노드에 부여된 자원을 어느 정도 "빌려" 쓰는 것이 난처한 상황을 빠져나오는 데 도움이 될 수 있다.
이 두 경우는 각 cgroup에 "비상용" 프로세서 및 메모리 노드 집합을 최신으로 유지함으로써 처리할 수도 있을 것이다. 하지만 이런 추가 정보를 최신으로 유지하는 복잡성이, 필요 자원을 찾기 위해 가끔 트리를 위로 검색해야 하는 비용보다 더 클 가능성이 높다. 앞서 어떤 서브시스템은 설정을 트리 아래로 전파하는 반면, 다른 서브시스템은 권한을 찾기 위해 트리 위로 검색한다고 보았다. 여기서는 cpuset이 필요에 따라 둘 다 수행함을 본다.
이 일곱 개의 cgroup 서브시스템에는 공통점이 하나 있는데, 이는 다음 글에서 살펴볼 다섯 개와 구분되는 점이며, 그 차이는 회계(accounting)와 관련이 있다. 대체로 이 일곱 서브시스템은 회계를 유지하지 않고, 제공하는 서비스는 "현재 순간"만을 참조하며 최근 이력에 대한 참조 없이 제공된다.
perf_event는 각 프로세스 그룹에 대한 일부 성능 데이터를 기록하긴 하지만, 그 데이터가 제어를 강제하는 데 사용되지는 않는다는 점에서 이 설명에 완전히 들어맞지는 않는다. 그럼에도 perf_event가 수행하는 회계와 나머지 다섯 서브시스템이 수행하는 회계 사이에는 매우 분명한 차이가 있지만, 그 차이를 명확히 하려면 다음 번까지 기다려야 한다.
그 공통점에도 불구하고, 다양성은 상당하다.
devices, freezer, cpuset). 다른 것들은 별도의 제어를 가능하게 하는 식별을 제공한다(net_cl, net_prio). 또 다른 것들은 제어에 전혀 관여하지 않는다(debug, perf_event).devices)은 모든 접근마다 커널 코어 코드가 cgroup 서브시스템에 확인하도록 요구함으로써 제어를 강제한다. 다른 것들(cpuset, net_cl)은 커널 객체(스레드, 소켓)에 설정을 부과하고 관련 커널 서브시스템이 그 다음을 처리한다.이런 세부 중에는 지난번 계층에서 발견했던 여러 이슈와 직접적으로 관련된 것은 많지 않다. 다만 cgroups를 사용해 프로세스를 식별하는 데 강조점이 있다는 점은, 조직화라기보다 분류가 기대된다는 것을 시사하는지도 모르겠다.
눈에 띄는 한 가지 연결고리는, 이 서브시스템들이 회계에 의존하지 않기 때문에 강한 계층을 요구하지 않는다는 점이다. 여러 cgroup에 대해 net_prio로 우선순위를 설정해야 하는데 이들이 공통 부모를 공유하지 않는다 해도, 그것은 문제가 아닐 수 있다. 필요한 각 그룹에 동일한 우선순위를 설정하면 된다. 어떤 규칙을 통해 어떤 그룹이 그 설정을 필요로 하는지 식별할 수만 있다면, 이를 자동화하기도 꽤 쉬울 것이다. 우리는 이미 net_prio와 net_cl이 기대한 대로 동작하려면 일관된 초기 설정이 필요하다는 것을 보았고, 이는 그 요구를 약간 확장한 것일 뿐이다.
따라서 적어도 이 서브시스템들에 대해서는 분류 계층을 원하는 쪽으로 약간 기울어져 있지만, 다중 계층을 특별히 원한다는 느낌은 없다.
프로세스 그룹화가 제기했던 이슈로 더 거슬러 올라가 보면, 경계선이 계속 흐릿하다는 점을 다시 보게 된다. 서로 다른 서브시스템이 유사한 서비스를 제공하기도 하고, 서로 다른 서비스가 하나의 서브시스템에 결합되기도 한다. 또한 그룹의 이름 짓기가 중요하지만 결코 사소하지 않다는 점도 보이며, 각 cgroup에 대해 서로 다른 두 숫자 식별자를 가질 수도 있다.
물론 아직 끝나지 않았다. 다음 번에는 남은 서브시스템들인 cpu, cpuacct, blkio, memory, hugetlb를 살펴보며, 이들이 무엇을 가르쳐주는지, 그리고 어떤 형태의 계층이 이들에게 가장 잘 맞는지 보겠다.
| 이 글의 인덱스 항목 |
|---|
| Kernel |
| GuestArticles |