一个 operator 拖慢了整个集群
한국어 원문으로 표시합니다.
목표
기본 FlowSchema 와 PriorityLevelConfiguration 을 순서대로 읽고, 폭주하는 서비스 어카운트를 위한 격리 칸을 만들어 분류가 실제로 바뀌는 것을 응답 헤더로 증명한 뒤, 우선순위 값이 겹쳤을 때의 규칙과 exempt 남용을 막는 감시 스크립트, 그리고 좌석 수 지표까지 확인한다.
왜 중요한가
API 서버는 들어오는 모든 요청을 분류하고 격리한다. FlowSchema 가 분류표이고, 요청은 matchingPrecedence 가 작은 것부터 대조해 처음 맞는 하나로 결정된다. 그 스키마가 가리키는 PriorityLevelConfiguration 이 그 칸의 폭 — 동시에 몇 개를 실행할 수 있고 넘치면 줄을 세울지 바로 거절할지 — 를 정한다. 이 구조가 중요한 이유는 하나다. 폭주하는 클라이언트가 있을 때 누가 굶는지를 설계할 수 있기 때문이다. 분류표를 읽을 줄 모르면 429 가 쌓이는 날 할 수 있는 일이 재시작뿐이고, 그건 같은 폭주를 다시 맞겠다는 뜻이다.
단계
- 클러스터가 기본으로 넣어 둔 FlowSchema(이 실습에서 만들 것들과 구분하기 위해 이름이
ops-나zz-ops-로 시작하지 않는 것으로 정의합니다)를 전부/root/ops-apf/flowschemas.tsv에 저장하세요 —<matchingPrecedence>탭<이름>탭<우선순위 등급 이름>한 줄씩이고, 요청이 심사되는 순서대로 정렬합니다(숫자 정렬입니다). - 클러스터가 기본으로 넣어 둔 PriorityLevelConfiguration(이름이
ops-로 시작하지 않는 것)을 전부/root/ops-apf/levels.tsv에 저장하세요 —<이름>탭<type>탭<nominalConcurrencyShares>탭<limitResponse.type>한 줄씩, 이름 오름차순입니다.Exempt등급처럼 값이 없는 칸은-로 적으세요. - 네임스페이스
ops-apf를 만들고 서비스 어카운트 셋을 만드세요 —widget-operator,plain-reader,bulk-writer. 그다음/root/ops-apf/classify.sh를 만드세요 — 인자로 받은 서비스 어카운트를 흉내 내어 요청 하나를 보내고 응답 헤더의X-Kubernetes-Pf-Flowschema-Uid로 걸린 FlowSchema 이름만 표준출력에 찍습니다.plain-reader로 돌려 그 결과를/root/ops-apf/baseline.tsv에 한 줄로 저장하세요 —plain-reader탭<스키마 이름>. /root/ops-apf/ops-noisy.yaml에 PriorityLevelConfigurationops-noisy를 쓰세요 —type: Limited,nominalConcurrencyShares: 5,lendablePercent: 50,borrowingLimitPercent: 20,limitResponse는type: Queue이고queuing은queues: 16,handSize: 4,queueLengthLimit: 50입니다. 적용하세요./root/ops-apf/ops-noisy-operator.yaml에 FlowSchemaops-noisy-operator를 쓰세요 —matchingPrecedence: 950,priorityLevelConfiguration.name은ops-noisy,distinguisherMethod.type은ByUser, 규칙은 subject 가kind: ServiceAccount로ops-apf네임스페이스의widget-operator이고 resourceRules 는verbs·apiGroups·resources·namespaces를 모두*로 둡니다. 적용하세요.classify.sh를widget-operator와plain-reader로 각각 돌려 결과를/root/ops-apf/classified.tsv에 두 줄로 저장하세요 —<어카운트 이름>탭<스키마 이름>이고 어카운트 이름 오름차순입니다./root/ops-apf/ops-bulk-a.yaml과/root/ops-apf/zz-ops-bulk-b.yaml에 FlowSchema 두 개를 쓰세요. 둘 다ops-apf의 서비스 어카운트bulk-writer를 대상으로 하고ops-noisy를 가리키며 resourceRules 는 모두*입니다.ops-bulk-a는matchingPrecedence: 960·distinguisherMethod.type: ByUser,zz-ops-bulk-b는matchingPrecedence: 940·distinguisherMethod.type: ByNamespace입니다. 둘 다 적용한 뒤classify.sh bulk-writer의 결과를/root/ops-apf/precedence.tsv에 두 줄로 저장하세요 —ops-bulk-a탭960탭<win 또는 lose>,zz-ops-bulk-b탭940탭<win 또는 lose>. 그리고 만약 두 스키마의 값이 똑같이 960 이었다면 어느 쪽이 이겼을지 그 이름만/root/ops-apf/tie.txt에 한 줄로 쓰세요./root/ops-apf/exempt-allow.txt에exempt우선순위 등급을 가리켜도 되는 FlowSchema 이름을 한 줄씩 적으세요(지금 클러스터에 실제로 그런 스키마가 어떤 것들인지 먼저 확인하고, 그 이름들만 적습니다). 그리고/root/ops-apf/exempt-guard.sh를 만드세요 —exempt를 가리키는 FlowSchema 를 모두 찾아 허용 목록에 있으면OK <이름>, 없으면VIOLATION <이름>을 이름 순으로 표준출력에만 찍고, 위반이 하나라도 있으면 0 이 아닌 코드로 끝나야 합니다. 지금 상태에서 돌리면 위반이 없어야 합니다.- API 서버의 지표에서
apiserver_flowcontrol_nominal_limit_seats를 읽어/root/ops-apf/seats.tsv에 저장하세요 —<우선순위 등급 이름>탭<좌석 수>한 줄씩, 이름 오름차순입니다.ops-noisy가 그 안에 있어야 합니다.
참고
matchingPrecedence는 값이 작을수록 먼저 심사됩니다.kubectl --v=8은 응답 헤더를 찍어 줍니다 — 분류 결과가 그 안에 UID 로 들어 있습니다.- APF 분류는 인가보다 먼저라서 403 이 돌아와도 헤더는 붙습니다.
- FlowSchema 의 status 에 Dangling 이 True 면 그 스키마는 무시되고 있는 것입니다.
- 흔한 실수: 문자열 정렬로 우선순위를 줄 세워 1000 이 2 보다 앞에 온다.
- 흔한 실수: 급하다고 exempt 를 가리키게 해서 서버를 지키는 장치를 통째로 없앤다.
- 참고: https://kubernetes.io/docs/concepts/cluster-administration/flow-control/
분류표를 우선순위 순으로 읽는다
클러스터가 기본으로 넣어 둔 FlowSchema(이 실습에서 만들 것들과 구분하기 위해 이름이 ops- 나 zz-ops- 로 시작하지 않는 것으로 정의합니다)를 전부 /root/ops-apf/flowschemas.tsv 에 저장하세요 — <matchingPrecedence> 탭 <이름> 탭 <우선순위 등급 이름> 한 줄씩이고, 요청이 심사되는 순서대로 정렬합니다(숫자 정렬입니다).
모든 들어오는 요청은 FlowSchema 를 하나씩 대조하며 내려가고 처음 맞는 것에서 멈춥니다. 대조 순서를 정하는 것이 matchingPrecedence 인데, 이름과 달리 값이 작을수록 먼저 봅니다. 문자열 정렬로는 1000 이 2 보다 앞에 오니 숫자 정렬을 쓰세요.
각 칸의 폭을 읽는다
클러스터가 기본으로 넣어 둔 PriorityLevelConfiguration(이름이 ops- 로 시작하지 않는 것)을 전부 /root/ops-apf/levels.tsv 에 저장하세요 — <이름> 탭 <type> 탭 <nominalConcurrencyShares> 탭 <limitResponse.type> 한 줄씩, 이름 오름차순입니다. Exempt 등급처럼 값이 없는 칸은 - 로 적으세요.
Limited 등급만 동시 실행 수를 나눠 가집니다. Exempt 는 아예 심사를 받지 않아 관련 필드가 없습니다. 값이 없는 칸을 jq 로 다룰 때 // 는 false 를 빈 값으로 취급하니, 여기서는 null 만 비교하는 편이 안전합니다.
지금은 어느 칸으로 가는지 실측한다
네임스페이스 ops-apf 를 만들고 서비스 어카운트 셋을 만드세요 — widget-operator, plain-reader, bulk-writer. 그다음 /root/ops-apf/classify.sh 를 만드세요 — 인자로 받은 서비스 어카운트를 흉내 내어 요청 하나를 보내고 응답 헤더의 X-Kubernetes-Pf-Flowschema-Uid 로 걸린 FlowSchema 이름만 표준출력에 찍습니다. plain-reader 로 돌려 그 결과를 /root/ops-apf/baseline.tsv 에 한 줄로 저장하세요 — plain-reader 탭 <스키마 이름>.
APF 분류는 인가보다 먼저 일어납니다 — 그래서 권한이 없어 403 이 돌아와도 헤더는 붙어 있습니다. 헤더를 보려면 kubectl --v=8 을 쓰고 표준오류까지 받아야 합니다. 흉내 내기는 --as=system:serviceaccount:<네임스페이스>:<이름> 입니다.
폭주하는 오퍼레이터를 위한 칸을 만든다
/root/ops-apf/ops-noisy.yaml 에 PriorityLevelConfiguration ops-noisy 를 쓰세요 — type: Limited, nominalConcurrencyShares: 5, lendablePercent: 50, borrowingLimitPercent: 20, limitResponse 는 type: Queue 이고 queuing 은 queues: 16, handSize: 4, queueLengthLimit: 50 입니다. 적용하세요.
좌석 수를 직접 적지 않고 지분으로 적는 이유가 있습니다 — 서버의 총 동시 실행 수가 바뀌어도 모든 등급이 같은 비율로 함께 늘고 줄기 때문입니다. lendablePercent 는 남는 좌석을 빌려 줄 수 있는 비율이고, borrowingLimitPercent 는 남에게서 빌려 올 수 있는 상한입니다.
그 칸으로 보내는 규칙을 쓴다
/root/ops-apf/ops-noisy-operator.yaml 에 FlowSchema ops-noisy-operator 를 쓰세요 — matchingPrecedence: 950, priorityLevelConfiguration.name 은 ops-noisy, distinguisherMethod.type 은 ByUser, 규칙은 subject 가 kind: ServiceAccount 로 ops-apf 네임스페이스의 widget-operator 이고 resourceRules 는 verbs·apiGroups·resources·namespaces 를 모두 * 로 둡니다. 적용하세요.
FlowSchema 의 status 에 Dangling 조건이 있습니다 — 가리키는 우선순위 등급이 없으면 True 가 되고 그 스키마는 무시됩니다. 적용한 뒤 이 조건이 False 인지 확인하는 습관을 들이면 오타로 스키마가 통째로 죽는 사고를 바로 잡을 수 있습니다.
분류가 정말 바뀌었는지 헤더로 증명한다
classify.sh 를 widget-operator 와 plain-reader 로 각각 돌려 결과를 /root/ops-apf/classified.tsv 에 두 줄로 저장하세요 — <어카운트 이름> 탭 <스키마 이름> 이고 어카운트 이름 오름차순입니다.
규칙을 만들었다고 분류가 바뀐 것은 아닙니다. 대상이 아닌 어카운트는 그대로 예전 스키마에 걸려야 하고, 대상인 어카운트만 새 스키마로 가야 합니다. 두 줄을 함께 남겨야 규칙이 너무 넓게 걸리지 않았다는 증거가 됩니다.
같은 요청에 두 규칙이 걸릴 때
/root/ops-apf/ops-bulk-a.yaml 과 /root/ops-apf/zz-ops-bulk-b.yaml 에 FlowSchema 두 개를 쓰세요. 둘 다 ops-apf 의 서비스 어카운트 bulk-writer 를 대상으로 하고 ops-noisy 를 가리키며 resourceRules 는 모두 * 입니다. ops-bulk-a 는 matchingPrecedence: 960·distinguisherMethod.type: ByUser, zz-ops-bulk-b 는 matchingPrecedence: 940·distinguisherMethod.type: ByNamespace 입니다. 둘 다 적용한 뒤 classify.sh bulk-writer 의 결과를 /root/ops-apf/precedence.tsv 에 두 줄로 저장하세요 — ops-bulk-a 탭 960 탭 <win 또는 lose>, zz-ops-bulk-b 탭 940 탭 <win 또는 lose>. 그리고 만약 두 스키마의 값이 똑같이 960 이었다면 어느 쪽이 이겼을지 그 이름만 /root/ops-apf/tie.txt 에 한 줄로 쓰세요.
두 스키마가 같은 요청에 걸리면 먼저 심사되는 쪽이 이기고 거기서 대조가 끝납니다. 값이 완전히 같을 때의 규칙은 공식 문서에 적혀 있습니다 — 이름을 사전순으로 비교해 작은 쪽이 이깁니다. 다만 문서는 같은 값을 두지 말라고 권합니다.
exempt 를 가리키는 규칙을 감시한다
/root/ops-apf/exempt-allow.txt 에 exempt 우선순위 등급을 가리켜도 되는 FlowSchema 이름을 한 줄씩 적으세요(지금 클러스터에 실제로 그런 스키마가 어떤 것들인지 먼저 확인하고, 그 이름들만 적습니다). 그리고 /root/ops-apf/exempt-guard.sh 를 만드세요 — exempt 를 가리키는 FlowSchema 를 모두 찾아 허용 목록에 있으면 OK <이름>, 없으면 VIOLATION <이름> 을 이름 순으로 표준출력에만 찍고, 위반이 하나라도 있으면 0 이 아닌 코드로 끝나야 합니다. 지금 상태에서 돌리면 위반이 없어야 합니다.
exempt 등급으로 보낸 요청은 동시 실행 수 제한도 대기열도 없이 즉시 처리됩니다. 편해 보이지만 그만큼 서버를 지켜 줄 장치가 사라지므로, 새로 이 등급을 가리키는 스키마가 생기면 사람이 알아야 합니다. 이런 감시는 CI 에서 돌리기 좋습니다.
새 칸이 좌석을 실제로 받았는지 지표로 확인한다
API 서버의 지표에서 apiserver_flowcontrol_nominal_limit_seats 를 읽어 /root/ops-apf/seats.tsv 에 저장하세요 — <우선순위 등급 이름> 탭 <좌석 수> 한 줄씩, 이름 오름차순입니다. ops-noisy 가 그 안에 있어야 합니다.
지표는 kubectl get --raw /metrics 로 받습니다. 등급을 하나 더 만들면 총 좌석이 늘어나는 것이 아니라 같은 총량을 지분대로 다시 나눕니다 — 그래서 새 등급을 만들면 기존 등급들의 좌석 수가 함께 줄어듭니다. 그 숫자를 직접 보면 지분의 뜻이 분명해집니다.