APISIX Ingress Controller는 어떻게 수천 개의 Pod Replica를 지원할까요?
Xin Rong
October 21, 2022
1,000개 이상의 Kubernetes Pod
Pod는 Kubernetes에서 배포 가능한 가장 작은 객체입니다. 우리는 애플리케이션의 단일 인스턴스를 실행하기 위해 Pod를 사용하므로 확장성을 고려하여 Pod에 제한된 리소스만 할당합니다. 그러나 높은 트래픽 시나리오를 만나면 수평 확장을 사용하여 이를 처리합니다.
예를 들어, 온라인 소매업체의 경우 블랙 프라이데이로 인해 트래픽이 급증할 수 있습니다. 이러한 시나리오를 처리하기 위해 서비스를 자동 확장하여 더 많은 트래픽을 처리해야 합니다. 자동 확장 전략에 따라 더 많은 복제본을 배포하여 더 많은 Pod를 생성합니다.
각 Pod는 동적 IP 주소를 가지고 있습니다. Endpoints API는 Kubernetes에서 네트워크 엔드포인트를 추적하는 간단한 방법을 제공하므로 Pod IP의 변경 사항을 실시간으로 모니터링하여 로드 밸런싱을 달성할 수 있습니다. 그러나 Kubernetes 클러스터와 서비스가 더 많은 트래픽을 처리하기 위해 성장함에 따라, 예를 들어 위에서 언급한 블랙 프라이데이 시나리오와 같이 Pod의 수가 계속 증가하고 Endpoints API가 더 커집니다. 결과적으로 Endpoints API의 한계가 더 두드러지게 되고, 심지어 성능 병목 현상이 되기도 합니다.
Endpoints API의 한계를 해결하기 위해 Kubernetes는 v1.21 버전부터 EndpointSlice API를 지원하기 시작했습니다. EndpointSlice API는 Endpoints API에서 방대한 네트워크 엔드포인트를 처리하는 성능 문제를 해결하고 뛰어난 확장성과 확장성을 제공합니다.
아래 다이어그램에서 둘 사이의 차이점을 직접 확인할 수 있습니다:
- 트래픽 급증 시 Endpoints의 변화
- 트래픽 급증 시 Endpointslices의 변화
Kubernetes에서 애플리케이션은 어떻게 서로 통신할까요? Endpoints와 EndpointSlice의 구체적인 차이점은 무엇인가요? Pod와 Endpoints/EndpointSlice의 관계는 무엇인가요? APISIX는 이러한 기능을 어떻게 지원하며, 어떻게 설치하고 사용할 수 있을까요? 이 글에서는 이러한 질문에 초점을 맞출 것입니다.
Kubernetes에서 애플리케이션에 접근하는 방법
서비스를 통해
Kubernetes에서 각 Pod는 고유한 IP 주소를 가지고 있습니다. 일반적으로 서비스는 selector
를 사용하여 Pod와 연결을 구축하고 동일한 DNS 서브도메인 이름을 제공하며 로드 밸런싱을 달성합니다. 또한 Kubernetes 클러스터 내부의 애플리케이션은 DNS를 사용하여 서로 통신할 수 있습니다.
서비스가 생성되면 Kubernetes는 서비스와 Endpoints 리소스를 연결합니다. 그러나 서비스가 어떤 selector도 지정하지 않은 경우 Kubernetes는 서비스를 위한 Endpoints를 자동으로 생성하지 않습니다.
Endpoints란 무엇이며 Pod와의 관계는 무엇인가
Endpoints는 Kubernetes의 리소스 객체로, etcd에 저장되며 서비스와 일치하는 Pod의 접근 주소 집합에 대한 참조를 포함합니다. 따라서 각 서비스는 하나의 Endpoints 리소스만 가질 수 있습니다. Endpoints 리소스는 Pod 클러스터를 모니터링하고 서비스 내의 어떤 Pod가 변경되면 동기적으로 업데이트됩니다.
- 3개의
httpbin
복제본을 배포하고 Pod의 상태, IP 정보를 확인합니다.
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
httpbin-deployment-fdd7d8dfb-8sxxq 1/1 Running 0 49m 10.1.36.133 docker-desktop <none> <none>
httpbin-deployment-fdd7d8dfb-bjw99 1/1 Running 4 (5h39m ago) 23d 10.1.36.125 docker-desktop <none> <none>
httpbin-deployment-fdd7d8dfb-r5nf9 1/1 Running 0 49m 10.1.36.131 docker-desktop <none> <none>
httpbin
서비스를 생성하고 Endpoints 엔드포인트의 상태를 확인합니다.
$ kubectl get endpoints httpbin
NAME ENDPOINTS AGE
httpbin 10.1.36.125:80,10.1.36.131:80,10.1.36.133:80 23d
위의 두 예제에서 볼 수 있듯이 Endpoints의 httpbin
리소스의 각 네트워크 엔드포인트는 Pod의 IP 주소와 일치합니다.
Endpoints의 단점
- Endpoints는 저장 제한이 있습니다. 어떤 Endpoints 리소스가 1000개 이상의 엔드포인트를 가지고 있다면, Endpoints 컨트롤러는 엔드포인트를 1000개로 잘라냅니다.
- 하나의 서비스는 하나의 Endpoints 리소스만 가질 수 있습니다. 이는 Endpoints 리소스가 해당 서비스를 지원하는 모든 Pod의 IP 주소와 기타 네트워크 정보를 저장해야 함을 의미합니다. 결과적으로 Endpoint API 리소스가 거대해지고, 리소스 내의 단일 네트워크 엔드포인트가 변경될 때마다 업데이트해야 합니다. 비즈니스가 빈번한 엔드포인트 변경을 필요로 할 때, 거대한 API 리소스가 서로 전송되어 Kubernetes 컴포넌트의 성능에 영향을 미칩니다.
Endpointslices란 무엇인가
Endpointslices는 Endpoints보다 더 확장 가능하고 확장성이 뛰어난 대안으로, 방대한 네트워크 엔드포인트를 처리하는 성능 문제를 해결하는 데 도움을 줍니다. 또한 토폴로지 라우팅과 같은 추가 기능을 위한 확장 가능한 플랫폼을 제공합니다. 이 기능은 Kubernetes v1.21에서 지원됩니다.
EndpointSlice API는 샤딩과 유사한 접근 방식으로 이 문제를 해결하기 위해 설계되었습니다. 하나의 Endpoints 리소스로 서비스의 모든 Pod IP를 추적하는 대신, 이를 여러 개의 작은 EndpointSlices로 분할합니다.
기본적으로 컨트롤 플레인은 각 EndpointSlice가 100개 이하의 엔드포인트를 가지도록 생성하고 관리합니다. 이를 --max-endpoints-per-slice
kube-controller-manager 플래그를 사용하여 최대 1000개까지 구성할 수 있습니다.
왜 필요한가
시나리오를 고려해보자
2000개의 Pod로 지원되는 서비스가 있다고 가정하면, 이는 1.0 MB의 Endpoints 리소스로 끝날 수 있습니다. 프로덕션 환경에서 이 서비스가 롤링 업데이트나 엔드포인트 마이그레이션을 수행하면 Endpoint 리소스가 빈번하게 업데이트됩니다. 롤링 업데이트는 모든 Pod가 교체되도록 할 수 있습니다. etcd의 요청 크기 제한으로 인해 Kubernetes는 Endpoints에 1000개의 엔드포인트 제한을 설정했습니다. 1000개 이상의 엔드포인트가 있는 경우 Endpoints 리소스는 추가 네트워크 엔드포인트에 대한 참조를 가지지 않습니다.
서비스가 특별한 요구 사항으로 인해 여러 번의 롤링 업데이트가 필요한 경우, 거대한 API 리소스 객체가 Kubernetes 컴포넌트 간에 전송되어 Kubernetes 컴포넌트의 성능에 상당한 영향을 미칩니다.
EndpointSlice를 사용하면 어떻게 될까
2000개의 Pod로 지원되는 서비스가 있고, 구성에서 각 Endpointslices에 100개의 엔드포인트를 할당하면 20개의 Endpointslices가 생성됩니다. 이제 Pod가 추가되거나 제거될 때 하나의 작은 EndpointSlice만 업데이트하면 됩니다. 분명히 이는 확장성과 네트워크 확장성에서 뛰어난 개선입니다. 모든 서비스가 자동 확장 요구 사항을 가지고 있다면, 서비스는 더 많은 Pod를 배포하고 트래픽 급증 시 증가하는 트래픽을 처리하기 위해 Endpoints 리소스가 빈번하게 업데이트되며, 그 차이는 더 두드러지게 됩니다. 더 중요한 것은 이제 서비스의 모든 Pod IP를 단일 리소스에 저장할 필요가 없으므로 etcd에 저장된 객체의 크기 제한에 대해 걱정할 필요가 없습니다.
Endpoints VS EndpointSlice 결론
EndpointSlice는 Kubernetes v1.21부터 지원되므로 모든 결론은 Kubernetes v1.21을 참조합니다.
Endpoints의 사용 사례:
- 자동 확장이 필요하지만 Pod의 수가 상대적으로 적고 리소스 전송이 큰 네트워크 트래픽과 추가 처리 요구를 유발하지 않는 경우.
- 자동 확장이 필요하지 않고 Pod의 수가 많지 않은 경우. 그러나 Pod의 수가 고정되어 있어도 서비스는 롤링 업데이트와 장애를 생략할 수 없습니다.
EndpointSlice의 사용 사례:
- 자동 확장이 필요하고 Pod의 수가 많은 경우 (수백 개의 Pod)
- Pod의 수가 매우 많아 Endpoints의 최대 엔드포인트 제한인 1000개를 초과하는 경우; 1000개 이상의 엔드포인트를 가진 Pod는 EndpointSlice를 사용해야 합니다.
APISIX Ingress Controller에서의 실습
APISIX Ingress Controller는 Endpoints 또는 EndpointSlice 리소스의 변경 사항을 감시하여 로드 밸런싱과 상태 확인을 달성합니다. Kubernetes v1.16+를 지원하기 위해 APISIX Ingress Controller는 기본적으로 설치 시 Endpoints를 사용합니다.
클러스터의 버전이 Kubernetes v1.21+인 경우, APISIX Ingress Controller 설치 시 watchEndpointSlice=true
플래그를 지정하여 EndpointSlice 기능을 지원해야 합니다.
참고: Kubernetes v1.21+ 클러스터에서는 Endpointslice 기능을 사용하는 것을 권장합니다. 그렇지 않으면 Pod의 수가
--max-endpoints-per-slice
플래그의 구성 값을 초과할 때 APISIX Ingress Controller가 Endpoints 리소스 객체를 감시하므로 구성이 손실될 수 있습니다.
20개의 Pod 복제본으로 지원되는 서비스 생성
Kubernetes에서 httpbin
애플리케이션 서비스를 구성하고 20개의 Pod 복제본을 생성합니다.
- kubectl apply -f httpbin-deploy.yaml
# htppbin-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: httpbin-deployment
spec:
replicas: 20
selector:
matchLabels:
app: httpbin-deployment
strategy:
rollingUpdate:
maxSurge: 50%
maxUnavailable: 1
type: RollingUpdate
template:
metadata:
labels:
app: httpbin-deployment
spec:
terminationGracePeriodSeconds: 0
containers:
- livenessProbe:
failureThreshold: 3
initialDelaySeconds: 2
periodSeconds: 5
successThreshold: 1
tcpSocket:
port: 80
timeoutSeconds: 2
readinessProbe:
failureThreshold: 3
initialDelaySeconds: 2
periodSeconds: 5
successThreshold: 1
tcpSocket:
port: 80
timeoutSeconds: 2
image: "kennethreitz/httpbin:latest"
imagePullPolicy: IfNotPresent
name: httpbin-deployment
ports:
- containerPort: 80
name: "http"
protocol: "TCP"
---
apiVersion: v1
kind: Service
metadata:
name: httpbin
spec:
selector:
app: httpbin-deployment
ports:
- name: http
port: 80
protocol: TCP
targetPort: 80
type: ClusterIP
APISIX Ingress를 통한 프록시
- Helm을 사용하여 APISIX Ingress Controller 설치
--set ingress-controller.config.kubernetes.watchEndpointSlice=true
를 사용하여 EndpointSlice 기능 지원을 활성화합니다.
helm repo add apisix https://charts.apiseven.com
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
kubectl create ns ingress-apisix
helm install apisix apisix/apisix \
--set gateway.type=NodePort \
--set ingress-controller.enabled=true \
--namespace ingress-apisix \
--set ingress-controller.config.apisix.serviceNamespace=ingress-apisix \
--set ingress-controller.config.kubernetes.watchEndpointSlice=true
- CRD 리소스를 사용하여 프록시
사용자는 APISIX Ingress Controller에서 Endpoints와 EndpointSlice 기능 지원을 알 수 없으며, 구성은 동일합니다.
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: httpbin-route
spec:
http:
- name: rule
match:
hosts:
- httpbin.org
paths:
- /get
backends:
- serviceName: httpbin
servicePort: 80
- APISIX Pod를 확인하면 APISIX의 업스트림 객체의 nodes 필드에 20개의 Pod IP 주소가 포함되어 있음을 확인할 수 있습니다.
kubectl exec -it ${Pod for APISIX} -n ingress-apisix -- curl "http://127.0.0.1:9180/apisix/admin/upstreams" -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'
{
"action": "get",
"count": 1,
"node": {
"key": "\/apisix\/upstreams",
"nodes": [
{
"value": {
"hash_on": "vars",
"desc": "Created by apisix-ingress-controller, DO NOT modify it manually",
"pass_host": "pass",
"nodes": [
{
"weight": 100,
"host": "10.1.36.100",
"priority": 0,
"port": 80
},
{
"weight": 100,
"host": "10.1.36.101",
"priority": 0,
"port": 80
},
{
"weight": 100,
"host": "10.1.36.102",
"priority": 0,
"port": 80
},
{
"weight": 100,
"host": "10.1.36.103",
"priority": 0,
"port": 80
},
{
"weight": 100,
"host": "10.1.36.104",
"priority": 0,
"port": 80
},
{
"weight": 100,
"host": "10.1.36.109",
"priority": 0,
"port": 80
},
{
"weight": 100,
"host": "10.1.36.92",
"priority": 0,
"port": 80
}
... // 아래 13개 노드 무시
// 10.1.36.118
// 10.1.36.115
// 10.1.36.116
// 10.1.36.106
// 10.1.36.113
// 10.1.36.111
// 10.1.36.108
// 10.1.36.114
// 10.1.36.107
// 10.1.36.110
// 10.1.36.105
// 10.1.36.112
// 10.1.36.117
],
"labels": {
"managed-by": "apisix-ingress-controller"
},
"type": "roundrobin",
"name": "default_httpbin_80",
"scheme": "http"
},
"key": "\/apisix\/upstreams\/5ce57b8e"
}
],
"dir": true
}
}
- EndpointSlice의 네트워크 엔드포인트와 일치
addressType: IPv4
apiVersion: discovery.k8s.io/v1
endpoints:
- addresses:
- 10.1.36.92
...
- addresses:
- 10.1.36.100
...
- addresses:
- 10.1.36.104
...
- addresses:
- 10.1.36.102
...
- addresses:
- 10.1.36.101
...
- addresses:
- 10.1.36.103
...
- addresses:
- 10.1.36.109
...
- addresses:
- 10.1.36.118
...
- addresses:
- 10.1.36.115
...
- addresses:
- 10.1.36.116
...
- addresses:
- 10.1.36.106
...
- addresses:
- 10.1.36.113
...
- addresses:
- 10.1.36.111
...
- addresses:
- 10.1.36.108
...
- addresses:
- 10.1.36.114
...
- addresses:
- 10.1.36.107
...
- addresses:
- 10.1.36.110
...
- addresses:
- 10.1.36.105
...
- addresses:
- 10.1.36.112
...
- addresses:
- 10.1.36.117
...
kind: EndpointSlice
metadata:
labels:
endpointslice.kubernetes.io/managed-by: endpointslice-controller.k8s.io
kubernetes.io/service-name: httpbin
name: httpbin-dkvtr
namespace: default
ports:
- name: http
port: 80
protocol: TCP
결론
이 글은 Kubernetes가 대량의 Pod를 배포해야 하는 시나리오와 우리가 직면하는 문제를 소개합니다. 또한 Endpoints와 EndpointSlice의 차이점을 비교하고, APISIX Ingress Controller 설치 시 EndpointSlice 기능을 활성화하는 방법을 소개합니다. 클러스터의 버전이 Kubernetes v1.21+인 경우, APISIX Ingress Controller 설치 시 EndpointSlice 기능을 활성화하는 것을 권장합니다. 따라서 구성 손실을 방지할 수 있으며 --max-endpoints-per-slice
플래그의 구성 값에 대해 걱정할 필요가 없습니다.