본문 바로가기
WIKI 기술 지식 베이스

라우트별 메트릭으로 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

더 알아보기 (Learn more)