문제 상황
현재 팀에서는 스크래핑 모듈을 운영하고 있다. 우리 회사는 고객들에게 설치를 해주는 방식의 프로그램들이 많기 때문에 해당 프로그램들의 사양에 맞춰 프레임워크 버전을 유지해야 한다. 그래서 현재 총 세 가지 버전의 .NET Framework를 사용하고 있는데 2, 4, 5 이렇게 세 가지 버전의 프레임워크를 사용하고 있다.
현재 팀에서는 고객들의 스크래핑 스케줄을 매 새벽마다 돌리고 있는데, 유독 여신금융협회 스크래핑 모듈만 에러율이 굉장히 높게 발생하고 있었다.
그래서 분석을 해 본 결과.. 2버전과 4버전의 모듈에선 에러가 발생하지 않고 있는데, 5버전에서만 전체 스크래핑 시도 중에 약 14% 정도의 비율로 에러가 발생하고 있었다.. 물론 아침에 출근해서 운영 담당자가 새벽에 일어난 에러에 대해서 모니터링하고, 성공할 때까지 재시도를 수동으로 해주고 있기에(ㅠㅠ) 운영에 있어서 큰 장애가 되지는 않았지만.. 문제는 이 작업에 걸리는 소요 시간이 많이 발생한다는 것이었다. (운영 담당자분은 점심시간 전까지 오전 업무 내내 스크래핑 재시도를 한다..)
해당 문제를 해결하지 못한 채 팀에서 운영을 하던 중, 이 이슈를 담당해 처리하게 되었다.
분석
우선 처음 이 이슈를 분석할 때 문제의 원인을 딱 정의하는게 굉장히 힘들었다..
똑같은 url에 똑같은 header, body로 post 요청을 보내는데 프레임워크 버전에 따라서 간헐적으로 오류가 발생한다..?
그래서 직접 패킷을 확인해보기 위해 Fiddler를 이용해 패킷 분석을 진행해보려 했다.
근데.. 잘 되는데요..?
정말 신기하게도 Fiddler를 켜두고 조회를 하면 에러가 발생하지 않았다..
그럼 Fiddler 끄고 직접 잡아봐야지 뭐..
위 사진에서는 ''현재 연결은 원격 호스트에 의해 강제로 끊겼습니다" 라는 오류를 메세지에 포함하고 있지만 실제로는
- 현재 연결은 원격 호스트에 의해 강제로 끊겼습니다
- The response ended prematurely
- Unable to read data from the transport connection: Connection reset by peer
위 세 가지 오류를 간헐적으로 뱉어내고 있었다.
그래서 그 중 Connection reset by peer 오류에 대해 집중적으로 알아보기 시작했고.. 결국 위 세 가지 오류는 메시지만 다를 뿐, 동일한 원인에 의해 발생되고 있다는 걸 알게되었다.
https://velog.io/@youngerjesus/Connection-Reset-by-Peer-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0
Connection reset by peer 오류를 잘 정리해 두신 포스팅이 있어 링크를 첨부한다
정말 다양한 원인이 있을 수 있는데, 핵심만 요약하자면 클라이언트와 서버 간 연결이 소실되거나 한쪽만 닫힌 상태에서 요청을 시도한 케이스라고 생각하면 된다.
그럼 여신금융협회 서버 쪽의 문제라고 볼 수 있는데.. 도대체 왜 피들러 패킷 캡쳐를 켜두고 스크래핑을 진행하면 오류가 나지 않았던걸까..?
그래서 피들러 로그를 확인해보니..
아.. 에러나고 있었네..
그래서 검색을 해보니..
https://groups.google.com/g/httpfiddler/c/yX6x8PWcvnY?pli=1 Fiddler Retry에 대한 정보를 얻을 수 있는 글
정말 어이없게도 피들러 패킷 캡쳐를 켜놓고 스크래핑을 진행하면 오류가 발생하지 않는 것이 아니라.. 통신 오류에 대해선 피들러가 자체적으로 처리를 해주고 있었던 것이었다..
그래서 Fiddler Script에 Retry 옵션을 Never로 주고, 다시 실행을 시켜보면 아까와는 다르게 에러가 발생하고
에러가 발생한 패킷을 까보면..
정상적으로(?) 에러가 반환된다.
해결
그럼 어떻게 해결해야 하지?
사실 가장 좋은 건 서버 오류를 잡는 것이겠지만..
스크래핑 모듈 특성 상, 내가 관리하는 서버가 아니다보니.. 클라이언트 입장에서 예외를 처리하는 방법이 최선이라고 생각했다.
그렇다면 피들러에서 재처리를 하는 경우 오류가 발생하지 않으므로, 5버전 모듈에서도 재처리 로직을 넣어주면 되지 않을까?
그래서 팀에서 사용하는 HTTP 통신 모듈 클래스에 재처리 로직을 추가해주었다.
try
{
response = _httpClient.GetAsync(url).Result;
}
catch (System.Exception ex)
{
if (!IsRetry) throw; // 해당 재처리 기능이 필요없는 모듈의 경우 무시하고 오류를 반환한다.
if (ex.InnerException.InnerException.Message.Contains("현재 연결은 원격 호스트에 의해 강제로 끊겼습니다") ||
ex.InnerException.InnerException.Message.Contains("The response ended prematurely") ||
ex.InnerException.InnerException.Message.Contains(
"Unable to read data from the transport connection: Connection reset by peer"))
{
response = _httpClient.GetAsync(url).Result; // retry
}
else
{
throw;
}
}
기존에는 response = _httpClient.GetAsync(url).Result; 이 줄 하나만 제외하고는 없는 코드였는데,
예외를 잡기 위해서 try catch를 사용했다.
또한, isRetry라는 boolean 프로퍼티 변수를 이용해 필요한 모듈만 해당 재처리 로직을 이용하도록 수정했다.
아직은 조건에 해당하는 메세지가 많지 않아 || 키워드로 작성했는데.. 지금보니 가독성이 좋지 못한 것 같아 좀 더 클린하게 작성을 해야할 것 같다.
그리고 System.Exception에 담아서 InnerException의 InnerException을 호출하는.. 코드가 있는데 이 부분도 정확한 Exception을 잡아 해결해야 할 것 같다.
결과
서로 다른 계정 3개로 총 999번 번갈아가면서 테스트 한 결과
기존 .NET5 조회 에러율 13~15% -> 0.05%로 개선에 성공했다.
마무리
프레임워크 버전에 따라 에러가 발생하던 현상이어서 해결에 굉장히 애를 먹었다..
문제 해결 과정을 정리해보자면..
- 프레임워크 버전에 따라 에러 발생
- 그럼 프레임워크가 변경되면서 사용하는 HTTP 통신 클래스가 변경되지 않았을까?
- Fiddler를 이용해 패킷 캡쳐 시도
- Fiddler에서는 오류가 발생하지 않네..?
- Fiddler 로그 확인, Fiddler에서도 오류 발생한 것 확인
- Fiddler에서 처리하는 방식대로 모듈도 처리하면 되지 않을까?
- 재처리 도입
- 해결 완료
사실 해결 자체는 정말 별거 없이 재처리 코드 하나만 넣어줬다.
하지만, 이 에러를 해결하느라.. 거의 1~2 주의 시간을 이 문제 해결에만 투자했다..
지금껏 피들러를 쓸 때, 피들러 로그를 볼 일이 잘 없어 미쳐 생각하지 못하고 있었다.
그래서 4 에서 5로 넘어가는 시간이 굉장히 오래 걸렸다..;
하지만 5버전의 스크래핑 모듈을 도입한 이후로 팀에서 계속해서 발생하고 있던 문제이고, 이 때문에 운영 담당자 분이 많은 시간을 투자해야만 했는데
대략 14%의 에러율을 0.05%로 줄이는 성과를 냈기 때문에 굉장히 뿌듯했던 경험이었다.