Network Layer에서의 모니터링 시스템 (feat. 토스 SLASH24)

꾸준히·2025년 4월 6일

토스 SLAHS 2024 - 대외계 구조 개선과 모니터링 강화로 시스템 연속성 확보하기 해당 영상을 보고 내용을 추가해서 만든 포스팅이다.

나는 열정 넘치는 신입 개발자 동동이다.

어느날, 대표님이 내게 찾아와 한 마디를 던지고 갔다.

요새 서비스가 좀 느려진 거 같네요... 1ms 개선될 때마다 월급 만원씩 올려 드릴게요...😉

내가 만든 서비스가 느리다고...? 절대 월급이 올라가서가 아니다. 내 자존심이 걸린 문제다. 꼭 원인을 찾아서 해결하고 말겠어.🔥

원인을 찾아서

구조 파악

우리 서버는 굉장히 간단한 구조이다.
위와 같이 메인 서버가 중심에서 DB와 대외계와 소통하며
client에 결과값을 전달한다.

앞에서부터 모든 경우의 수를 살펴보자.

1. 고객 WIFI 문제 아니야?

나의 서버가 잘못되었을 리 없다. 일단 고객이 의심스럽다.
고객 와이파이 or 5G가 잘못된 거 아닌가?

일단 사내 와이파이를 이용해 API 요청을 날려 보았다.

curl -w "result.json" -o /dev/null -s "https://api.example.com"
{"latency":["total":1.265323]}

에헤에에? API 하나에 1265ms란 말인가..

내가 범인인가?

2. 내가... 범인?

먼저 내가 작성한 코드들을 살펴보았다.

import { Injectable } from '@nestjs/common';
import axios from 'axios';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from './entities/user.entity';
import { Repository } from 'typeorm';

@Injectable()
export class ApplicationService {
  constructor(
    @InjectRepository(UserEntity)
    private userRepository: Repository<UserEntity>,
  ) {}
  async myCode() {
    const user = await this.getDataFromDatabase();
    const externalData = await this.getDataFromExternalApi();
    return {
      user,
      externalData,
    };
  }

  async getDataFromDatabase() {
    return this.userRepository.find();
  }

  async getDataFromExternalApi() {
    const response = await axios.get('https://api.협력해요.com/data');
    return response.data;
  }
}

getDataFromDatabase가 범인?

흠,,, DB가 문제인건가..
쿼리 로깅을 통해 속도를 확인해 봐야겠다.

query: SELECT * FROM user 
execution time: 34 ms

직접 서버에 로그를 남긴 것을 확인해 보아도
MySQL에서 slow_query 옵션을 사용해 보아도
DataBase 성능은 확실한 것으로 보인다.

getDataFromExternalApi 범인?

네이놈 협력서버!! 네 죄를 네가 알렸다. 부리나케 증거 수집을 위해 axios 로깅 시스템을 구축하였다.

@Injectable()
export class AxiosLoggingService {
  constructor(
    @Inject(API_LOGGER) private readonly logger: Logger,
    private readonly requestContextService: RequestContextService,
  ) {
    axios.interceptors.request.use((config) => {
      this.logger.log({
        level: 'info',
        type: 'axios',
        url: config.url,
        method: config.method,
        message: `[Request]`,
        request: config.data,
        startTime: Date.now(),
      });
      return config;
    });

    axios.interceptors.response.use((response) => {
      const executionTime = Date.now() - response.config.startTime;

      this.logger.log({
        level: 'info',
        executionTime: `${executionTime}ms`,
        url: response.config.url,
        method: response.config.method,
        statusCode: response.status,
        statusText: response.statusText,
        type: 'axios',
        message: `[Response]`,
        request: response.config.data || '',
        response: response.data || '',
      });
      return response;
    });
  }
}

axios의 interceptor를 사용해 외부 api 응답속도를 확인해 보았다.

{
  "executionTime": "1100ms"
}

...빙고! 🎯
검거완료

바로 해당 자료와 함께 이메일을 보내보았다.

너도... 아니라고?

안녕하세요, 동동이님! 협력사 기술전문가 나전문입니다.
요청 주신 API 응답시간은 확인해 보니 134ms로 지연 현상을 확인할 수 없었습니다.

해당 요청에 대한 로그 첨부해 보내드립니다. 안_느림.json

항상 더 나은 서비스를 제공하기 위해 노력하겠습니다.
감사합니다,
나전문 드림

하지만 협력사 측에서도 요청에 대한 로그을 확인해 주며 이상이 없다는 것을 보았다.

아니 그럼 대체 어디가 문제인건데!

내 월급!!! 아니 내 자존심!!!

찾았다 빈틈의 실..

아까 시스템 구조를 다시 살펴보자.

어어 저기 뭔가 보이는데

어어?!?

기둥 뒤에 인터넷 제공업체 있어요..
아니 진짜로..? 믿었던 인터넷이 문제인가

RTT를 측정해 보자 (토스는 어떻게 하는데?)

1. curl을 이용한 네트워크 메트릭 확인

curl -so /dev/null --noproxy "*" -w \
"{\"latency\":[{\"dnslookup\":%{time_namelookup},\"tcp\":%{time_connect},\"ssldone\":%{time_appconnect}},\"total\":%{time_total}]}\n" \
https://www.협력업체.com

아래는 위 명령어에 대한 응답예시이다.

{
  "latency":[
    {
      "dnslookup":0.147391,
      "tcp":0.152435,
      "ssldone":0.185845
    },
    "total":0.265323
  ]
}

https 통신의 경우

  1. DNS 확인
  2. tcp 3 handshake
  3. ssl 인증서 교환

통신 전체에 있어서 이러한 과정을 모두 정밀하게 측정할 수 있다.
또한 curl 옵션 설정을 변경하여 원하는 정보를 추가로 로깅할 수 있다.

2. mtr 을 이용한 네트워크 메트릭 확인

sudo mtr -z -r -c1 -w -b -p --json www.협력업체.com
{
    "report": {
        "mtr": {
            "src": "MacBookPro",
            "dst": "www.naver.com",
            "tos": 0,
            "tests": 1,
            "psize": "64",
            "bitpattern": "0x00"
        },
        "hubs": [
            {
                "count": 1,
                "host": "rt-ac59u_v2-03e0 (192.168.50.1)",
                "ASN": "AS???",
                "Loss%": 0.0,
                "Snt": 1,
                "Last": 3.437,
                "Avg": 3.437,
                "Best": 3.437,
                "Wrst": 3.437,
                "StDev": 0.0
            },
            {
                "count": 2,
                "host": "192.168.0.1",
                "ASN": "AS???",
                "Loss%": 0.0,
                "Snt": 1,
                "Last": 6.511,
                "Avg": 6.511,
                "Best": 6.511,
                "Wrst": 6.511,
                "StDev": 0.0
            },
            {
                "count": 3,
                "host": "175.208.209.254",
                "ASN": "AS4766",
                "Loss%": 0.0,
                "Snt": 1,
                "Last": 6.416,
                "Avg": 6.416,
                "Best": 6.416,
                "Wrst": 6.416,
                "StDev": 0.0
            },
            {
                "count": 4,
                "host": "61.78.43.90",
                "ASN": "AS4766",
                "Loss%": 0.0,
                "Snt": 1,
                "Last": 5.09,
                "Avg": 5.09,
                "Best": 5.09,
                "Wrst": 5.09,
                "StDev": 0.0
            },
            {
                "count": 5,
                "host": "112.189.15.89",
                "ASN": "AS4766",
                "Loss%": 0.0,
                "Snt": 1,
                "Last": 7.687,
                "Avg": 7.687,
                "Best": 7.687,
                "Wrst": 7.687,
                "StDev": 0.0
            },
            {
                "count": 6,
                "host": "???",
                "ASN": "AS???",
                "Loss%": 100.0,
                "Snt": 1,
                "Last": 0.0,
                "Avg": 0.0,
                "Best": 0.0,
                "Wrst": 0.0,
                "StDev": 0.0
            },
            {
                "count": 7,
                "host": "112.174.75.34",
                "ASN": "AS4766",
                "Loss%": 0.0,
                "Snt": 1,
                "Last": 11.512,
                "Avg": 11.512,
                "Best": 11.512,
                "Wrst": 11.512,
                "StDev": 0.0
            },
            {
                "count": 8,
                "host": "???",
                "ASN": "AS???",
                "Loss%": 100.0,
                "Snt": 1,
                "Last": 0.0,
                "Avg": 0.0,
                "Best": 0.0,
                "Wrst": 0.0,
                "StDev": 0.0
            }
        ]
    }
}

위 데이터를 토대로 그림을 그려보면

192.으로 시작하는 사내 private ip를 통해 라우팅을 하다
(AS???로 표기됨)

AS4766부터는 통신사 중계망을 통해 전달되고

목적지(대외계 채널)에 도착하고 있다.

curl을 통해 전체 통신에 소요되는 시간을 확인할 수 있고
mtr을 통해 목적지로 가는 hop마다 소요되는 시간을 더 정밀하게 확인할 수 있다.

보통 위 메트릭들을 grafana와 같은 툴을 이용해 시각화를 한다.

위 메트릭을 수집하여 분석해 보니
ISP 업체가 문제가 확실해 보였다..

해결

ISP망은 우리가 직접 관리하는 부분이 아니기 때문에 우리가 해결할 수 있는 부분이 아니였다.

나는 ISP 제공업체로 한 편의 메일에 지금까지 기록했던 메트릭과 함께 개선 요청을 하였다.

하지만 해당 문제는 그렇게 빠르게 해결될 거 같지 않아 보였다.
사내에서 무언가 조정을 통해 바꿀 수 있는 부분이 있을까?

이중화 서비스

다행히도 우리 서비스는 만약을 대비해서 이중화가 잘 되어 있었다. ISP 제공 업체 또한 2곳을 사용하고 있었고 한 곳에서만 인터넷 지연 현상이 발견 되었다.

Bgp를 조정하여 ISP 네트워크의 지연이 확인될 경우 다른 ISP로 트래픽을 옮길 수 있도록 하여 문제를 해결하였다.

그리고 나는 연봉 2억의 사나이가 되었다.

profile
타닥..타닥

1개의 댓글

comment-user-thumbnail
2025년 4월 6일

연봉 2억 개발자님 메로나 하나만 사주세요

답글 달기