라우트별 메트릭으로 HTTP 애플리케이션 디버깅하기
라우트별 메트릭으로 HTTP 애플리케이션 디버깅하기 (Debugging HTTP applications with per-route metrics)
books라는 Ruby 서점 관리 애플리케이션으로 Linkerd의 라우트별 메트릭을 이용해 간헐적 장애의 근본 원인을 찾아보는 데모예요.
출처: Linkerd Debugging HTTP applications with per-route metrics
본문
이 데모는 책장을 관리해 주는 Ruby 애플리케이션이에요. 여러 마이크로서비스로 구성되어 있으며 HTTP 위에서 JSON을 사용해 다른 서비스와 통신해요. 세 가지 서비스가 있어요.
- webapp: 프론트엔드
- authors: 시스템의 작가를 관리하는 API
- books: 시스템의 책을 관리하는 API
데모용으로 애플리케이션에는 간단한 트래픽 생성기가 함께 제공돼요. 전체적인 구조는 다음과 같아요.
Topology
사전 준비 (Prerequisites)
이 가이드를 사용하려면 클러스터에 Linkerd가 설치되어 있어야 해요. 아직 설치하지 않았다면 Installing Linkerd Guide를 따라 하세요.
앱 설치하기
시작하려면 books 앱을 클러스터에 설치해 볼게요. 로컬 터미널에서 실행하세요.
kubectl create ns booksapp && \
curl --proto '=https' --tlsv1.2 -sSfL https://run.linkerd.io/booksapp.yml \
| kubectl -n booksapp apply -f -
이 명령은 데모용 네임스페이스를 만들고, Kubernetes 리소스 매니페스트를 다운로드해 kubectl로 클러스터에 적용해요. 앱은 booksapp 네임스페이스에서 실행되는 Kubernetes 디플로이먼트와 서비스로 구성돼요.
처음으로 여러 컨테이너를 다운로드하는 데는 시간이 좀 걸려요. Kubernetes가 모든 서비스가 실행 중이고 트래픽을 받을 준비가 됐는지 알려줄 수 있어요. 다음을 실행해 그때까지 기다리세요.
kubectl -n booksapp rollout status deploy webapp
클러스터에 추가된 모든 구성 요소를 빠르게 확인하려면 다음을 실행하세요.
kubectl -n booksapp get all
롤아웃이 성공적으로 완료되면 webapp을 로컬로 포트 포워딩해 앱에 접근할 수 있어요.
kubectl -n booksapp port-forward svc/webapp 7000 >/dev/null &
(나머지 과정 동안 "Handling connection" 메시지에 시달리지 않도록 /dev/null로 리다이렉션한 거예요.)
브라우저에서 http://localhost:7000/ 을 열면 프론트엔드를 볼 수 있어요.
Frontend
아쉽게도 앱에 오류가 하나 있어요. Add Book 을 클릭하면 50% 확률로 실패해요. 이는 전형적인, 잘 드러나지 않는 간헐적 장애예요. 디버깅하기 어려워서 서비스 담당자를 미치게 만드는 유형이죠. Kubernetes 자체는 이 오류를 탐지하거나 드러낼 수 없어요. Kubernetes 입장에서는 모든 게 정상으로 보이지만, 애플리케이션은 오류를 반환하고 있음을 알 수 있어요.
Failure
서비스에 Linkerd 추가하기
이제 서비스에 Linkerd 데이터 플레인 프록시를 추가해야 해요. 가장 쉬운 방법은 다음과 같이 하는 거예요.
kubectl get -n booksapp deploy -o yaml \
| linkerd inject - \
| kubectl apply -f -
이 명령은 booksapp 네임스페이스의 모든 디플로이먼트 매니페스트를 가져와 linkerd inject로 처리한 뒤 kubectl apply로 다시 적용해요. linkerd inject 명령은 각 리소스에 Linkerd 데이터 플레인 프록시를 추가하도록 어노테이션을 지정하고, 매니페스트가 클러스터에 다시 적용될 때 Kubernetes가 이를 수행해요. 가장 좋은 점은 Kubernetes가 롤링 디플로이를 수행하므로 애플리케이션이 내내 계속 실행된다는 거예요. (이 동작 방식에 대한 자세한 내용은 Automatic Proxy Injection 문서를 참고하세요.)
디버깅 (Debugging)
Linkerd로 이 앱 장애의 근본 원인을 찾아볼게요. stat-inbound 명령어로 webapp 디플로이먼트의 성공률을 확인할 수 있어요.
linkerd viz -n booksapp stat-inbound deploy/webapp
NAME SERVER ROUTE TYPE SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99
webapp [default]:4191 [default] 100.00% 0.30 4ms 9ms 10ms
webapp [default]:4191 probe 100.00% 0.60 0ms 1ms 1ms
webapp [default]:7000 probe 100.00% 0.30 2ms 2ms 2ms
webapp [default]:7000 [default] 75.66% 8.22 18ms 65ms 93ms
이는 인바운드 트래픽 통계를 보여줘요. 즉 webapp이 포트 7000에서 초당 8.22개의 요청을 받고 있으며, 그중 75.66%만 성공하고 있음을 알 수 있어요.
더 파고들어 근본 원인을 찾으려면 webapp의 아웃바운드 트래픽을 살펴볼 수 있어요. 그러면 webapp이 다른 서비스에 보내는 요청에 대한 정보를 알 수 있어요.
linkerd viz -n booksapp stat-outbound deploy/webapp
NAME SERVICE ROUTE TYPE BACKEND SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99 TIMEOUTS RETRIES
webapp books:7002 [default] 77.36% 7.95 25ms 48ms 176ms 0.00% 0.00%
└──────────────────► books:7002 77.36% 7.95 15ms 44ms 64ms 0.00%
webapp authors:7001 [default] 100.00% 3.53 26ms 72ms 415ms 0.00% 0.00%
└──────────────────► authors:7001 100.00% 3.53 16ms 52ms 91ms 0.00%
webapp이 books 서비스와 authors 서비스 양쪽에 트래픽을 보내며, 문제는 books 서비스로 가는 트래픽에 있음을 알 수 있어요.
HTTPRoute
webapp 구성 요소가 books 구성 요소에서 실패를 받고 있다는 것은 알지만, 이를 더 좁혀 라우트별 메트릭을 얻으면 좋겠어요. 이를 위해 Gateway API를 활용해 각각 books Service를 parent_ref로 지정해 연결하는 HTTPRoute 리소스 집합을 정의해요.
kubectl apply -f - kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1beta1
metadata:
name: books-list
namespace: booksapp
spec:
parentRefs:
- name: books
group: core
kind: Service
port: 7002
rules:
- matches:
- path:
type: Exact
value: "/books.json"
---
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1beta1
metadata:
name: books-create
namespace: booksapp
spec:
parentRefs:
- name: books
group: core
kind: Service
port: 7002
rules:
- matches:
- path:
type: Exact
value: "/books.json"
method: POST
---
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1beta1
metadata:
name: books-delete
namespace: booksapp
spec:
parentRefs:
- name: books
group: core
kind: Service
port: 7002
rules:
- matches:
- path:
type: RegularExpression
value: "/books/\\\d+.json"
method: DELETE
EOF
그다음 이 HTTPRoute들이 부모 Service에 의해 수용되었는지 상태 서브리소스로 확인할 수 있어요.
kubectl -n booksapp get httproutes.gateway.networking.k8s.io \
-ojsonpath='{.items[*].status.parents[*].conditions[*]}' | jq .
Accepted와 ResolvedRefs 조건이 True인 것을 확인하세요.
{
"lastTransitionTime": "2024-08-03T01:38:25Z",
"message": "",
"reason": "Accepted",
"status": "True",
"type": "Accepted"
}
{
"lastTransitionTime": "2024-08-03T01:38:25Z",
"message": "",
"reason": "ResolvedRefs",
"status": "True",
"type": "ResolvedRefs"
}
[...]
이 HTTPRoute들이 준비되면 아웃바운드 통계를 다시 살펴볼 수 있어요.
linkerd viz -n booksapp stat-outbound deploy/webapp
NAME SERVICE ROUTE TYPE BACKEND SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99 TIMEOUTS RETRIES
webapp authors:7001 [default] 100.00% 2.80 25ms 48ms 50ms 0.00% 0.00%
└─────────────────────► authors:7001 100.00% 2.80 16ms 45ms 49ms 0.00%
webapp books:7002 books-list HTTPRoute 100.00% 1.43 25ms 48ms 50ms 0.00% 0.00%
└─────────────────────► books:7002 100.00% 1.43 12ms 24ms 25ms 0.00%
webapp books:7002 books-create HTTPRoute 54.27% 2.73 27ms 207ms 441ms 0.00% 0.00%
└─────────────────────► books:7002 54.27% 2.73 14ms 152ms 230ms 0.00%
webapp books:7002 books-delete HTTPRoute 100.00% 0.72 25ms 48ms 50ms 0.00% 0.00%
└─────────────────────► books:7002 100.00% 0.72 12ms 24ms 25ms 0.00%
이는 실패하고 있는 것이 books-create HTTPRoute로 가는 요청임을 알려줘요.
재시도 (Retries)
코드를 업데이트하고 새 버전을 롤아웃하는 데는 시간이 걸릴 수 있으니, Linkerd가 실패하는 엔드포인트로 가는 요청을 재시도할 수 있게 해 볼게요. 이렇게 하면 요청이 여러 번 재시도되어 요청 지연 시간은 늘어나지만, 새 버전을 롤아웃할 필요는 없어요. books-create HTTPRoute에 재시도 어노테이션을 추가해서 5xx 응답에 대해 재시도하도록 Linkerd에 알려주세요.
kubectl -n booksapp annotate httproutes.gateway.networking.k8s.io/books-create \
retry.linkerd.io/http=5xx
그러면 이 재시도의 효과를 볼 수 있어요.
linkerd viz -n booksapp stat-outbound deploy/webapp
NAME SERVICE ROUTE TYPE BACKEND SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99 TIMEOUTS RETRIES
webapp books:7002 books-create HTTPRoute 73.17% 2.05 98ms 460ms 492ms 0.00% 34.22%
└─────────────────────► books:7002 48.13% 3.12 29ms 93ms 99ms 0.00%
webapp books:7002 books-list HTTPRoute 100.00% 1.50 25ms 48ms 49ms 0.00% 0.00%
└─────────────────────► books:7002 100.00% 1.50 12ms 24ms 25ms 0.00%
webapp books:7002 books-delete HTTPRoute 100.00% 0.73 25ms 48ms 50ms 0.00% 0.00%
└─────────────────────► books:7002 100.00% 0.73 12ms 24ms 25ms 0.00%
webapp authors:7001 [default] 100.00% 2.98 25ms 48ms 50ms 0.00% 0.00%
└─────────────────────► authors:7001 100.00% 2.98 16ms 44ms 49ms 0.00%
books-create 라우트의 books 백엔드로 가는 개별 요청의 성공률은 여전히 약 50%에 불과하지만, 재시도 덕분에 해당 라우트의 전체 성공률이 73%로 올라간 것을 주목하세요. 또한 이 라우트의 요청 중 34.22%가 재시도이며, 개선된 성공률은 백엔드로 가는 추가 RPS와 증가된 전체 지연 시간을 대가로 얻은 것임을 볼 수 있어요.
기본적으로 Linkerd는 실패당 1번만 재시도해요. 이 제한을 늘려 요청당 1번 이상의 재시도를 허용하면 성공률을 더 높일 수 있어요.
kubectl -n booksapp annotate httproutes.gateway.networking.k8s.io/books-create \
retry.linkerd.io/limit=3
통계를 다시 살펴보면:
linkerd viz -n booksapp stat-outbound deploy/webapp
NAME SERVICE ROUTE TYPE BACKEND SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99 TIMEOUTS RETRIES
webapp books:7002 books-delete HTTPRoute 100.00% 0.75 25ms 48ms 50ms 0.00% 0.00%
└─────────────────────► books:7002 100.00% 0.75 12ms 24ms 25ms 0.00%
webapp authors:7001 [default] 100.00% 2.92 25ms 48ms 50ms 0.00% 0.00%
└─────────────────────► authors:7001 100.00% 2.92 18ms 46ms 49ms 0.00%
webapp books:7002 books-create HTTPRoute 92.78% 1.62 111ms 461ms 492ms 0.00% 47.28%
└─────────────────────► books:7002 48.91% 3.07 42ms 179ms 236ms 0.00%
webapp books:7002 books-list HTTPRoute 100.00% 1.45 25ms 48ms 50ms 0.00% 0.00%
└─────────────────────► books:7002 100.00% 1.45 12ms 24ms 25ms 0.00%
추가된 재시도 덕분에 이 라우트의 전체 성공률이 92.78%로 올라간 것을 볼 수 있어요.
타임아웃 (Timeouts)
Linkerd는 다른 서비스로 나가는 요청이 실패하기 전에 기다릴 시간을 제한할 수 있어요. 이 데모를 위해 books-create 라우트로 가는 호출에 15ms 타임아웃을 설정해 볼게요.
kubectl -n booksapp annotate httproutes.gateway.networking.k8s.io/books-create \
timeout.linkerd.io/request=15ms
(클러스터에 따라 타임아웃 값을 조정해야 할 수 있어요. 15ms면 분명히 타임아웃이 일부 나타날 거예요. 너무 많이 나타나서 상황 파악이 어렵다면 값을 올려도 좋아요!)
이 타임아웃의 효과를 다음 실행으로 확인할 수 있어요.
linkerd viz -n booksapp stat-outbound deploy/webapp
NAME SERVICE ROUTE TYPE BACKEND SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99 TIMEOUTS RETRIES
webapp authors:7001 [default] 100.00% 2.85 26ms 49ms 370ms 0.00% 0.00%
└─────────────────────► authors:7001 100.00% 2.85 19ms 49ms 86ms 0.00%
webapp books:7002 books-create HTTPRoute 78.90% 1.82 45ms 449ms 490ms 21.10% 47.34%
└─────────────────────► books:7002 41.55% 3.45 24ms 134ms 227ms 11.11%
webapp books:7002 books-list HTTPRoute 100.00% 1.40 25ms 47ms 49ms 0.00% 0.00%
└─────────────────────► books:7002 100.00% 1.40 12ms 24ms 25ms 0.00%
webapp books:7002 books-delete HTTPRoute 100.00% 0.70 25ms 48ms 50ms 0.00% 0.00%
└─────────────────────► books:7002 100.00% 0.70 12ms 24ms 25ms 0.00%
요청의 21.10%가 이 타임아웃에 걸리는 것을 볼 수 있어요.
정리 (Clean Up)
클러스터에서 books 앱과 booksapp 네임스페이스를 제거하려면 다음을 실행하세요.
kubectl delete ns booksapp