[토이프로젝트] MailBadara - (5) 트러블슈팅

2023. 10. 13. 15:55·프로젝트/MailBadara

트러블슈팅

axios timeout

  • 상황: 특정 시간이 되면 서버에서 크롤링 작업을 수행 후, DB의 유저들에게 메일을 뿌려야 한다. 그러나 서버에서 크롤링 작업을 수행할 때 크롤링 대상 사이트의 속도가 너무 느려서 axios.get의 반환이 늦어짐에 따라 timeout: 3초로 제한을 두었다. 그런데 어떤 때는 크롤링을 빠르게 잘하는 반면, 어떤 때는 너무 느려서 timeout: 3초가 계속해서 터진다.
  • 솔루션: 크롤링 대상인 학과 홈페이지들의 서버 상태가 시시각각 달라져서 timeout을 지정하기가 애매했다.
  • 작업이 길어질수록 메모리 사용량이 커질 것 같았지만, timeout을 무제한으로 두는 수밖에 없었다.

 

  • 서버 로그
AxiosError: timeout of 3000ms exceeded at RedirectableRequest.handleRequestTimeout (file:///workspace/server/node_modules/axios/lib/adapters/http.js:630:16) at RedirectableRequest.emit (node:events:514:28) at Timeout.<anonymous> (/workspace/server/node_modules/follow-redirects/index.js:179:12) at listOnTimeout (node:internal/timers:573:17) at process.processTimers (node:internal/timers:514:7) { code: 'ECONNABORTED', config: { transitional: { silentJSONParsing: true, forcedJSONParsing: true, clarifyTimeoutError: false }, adapter: [ 'xhr', 'http' ], transformRequest: [ [Function: transformRequest] ], transformResponse: [ [Function: transformResponse] ], timeout: 3000, xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', maxContentLength: -1, maxBodyLength: -1, env: { FormData: [Function], Blob: [class Blob] }, validateStatus: [Function: validateStatus], headers: Object [AxiosHeaders] { Accept: 'text/xml', 'Content-Type': 'application/rss+xml', 'User-Agent': 'axios/1.5.1', 'Accept-Encoding': 'gzip, compress, deflate, br' }, method: 'get', url: 'https://biz.pusan.ac.kr/bbs/biz/2556/rssList.do?row=5', data: undefined }, request: <ref *1> Writable { _writableState: WritableState { objectMode: false, highWaterMark: 16384, finalCalled: false, needDrain: false, ending: false, ended: false, finished: false, destroyed: false, decodeStrings: true, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: true, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, afterWriteTickInfo: null, buffered: [], bufferedIndex: 0, allBuffers: true, allNoop: true, pendingcb: 0, constructed: true, prefinished: false, errorEmitted: false, emitClose: true, autoDestroy: true, errored: null, closed: false, closeEmitted: false, [Symbol(kOnFinished)]: [] }, _events: [Object: null prototype] { response: [Function: handleResponse], error: [Function: handleRequestError], socket: [Array] }, _eventsCount: 3, _maxListeners: undefined, _options: { maxRedirects: 21, maxBodyLength: Infinity, protocol: 'https:', path: '/bbs/biz/2556/rssList.do?row=5', method: 'GET', headers: [Object: null prototype], agents: [Object], auth: undefined, family: undefined, beforeRedirect: [Function: dispatchBeforeRedirect], beforeRedirects: [Object], hostname: 'biz.pusan.ac.kr', port: '', agent: undefined, nativeProtocols: [Object], pathname: '/bbs/biz/2556/rssList.do', search: '?row=5' }, _ended: true, _ending: true, _redirectCount: 0, _redirects: [], _requestBodyLength: 0, _requestBodyBuffers: [], _onNativeResponse: [Function (anonymous)], _currentRequest: ClientRequest { _events: [Object: null prototype], _eventsCount: 7, _maxListeners: undefined, outputData: [], outputSize: 0, writable: true, destroyed: false, _last: true, chunkedEncoding: false, shouldKeepAlive: true, maxRequestsOnConnectionReached: false, _defaultKeepAlive: true, useChunkedEncodingByDefault: false, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, strictContentLength: false, _contentLength: 0, _hasBody: true, _trailer: '', finished: true, _headerSent: true, _closed: false, socket: [TLSSocket], _header: 'GET /bbs/biz/2556/rssList.do?row=5 HTTP/1.1\r\n' + 'Accept: text/xml\r\n' + 'Content-Type: application/rss+xml\r\n' + 'User-Agent: axios/1.5.1\r\n' + 'Accept-Encoding: gzip, compress, deflate, br\r\n' + 'Host: biz.pusan.ac.kr\r\n' + 'Connection: keep-alive\r\n' + '\r\n', _keepAliveTimeout: 0, _onPendingData: [Function: nop], agent: [Agent], socketPath: undefined, method: 'GET', maxHeaderSize: undefined, insecureHTTPParser: undefined, joinDuplicateHeaders: undefined, path: '/bbs/biz/2556/rssList.do?row=5', _ended: false, res: null, aborted: false, timeoutCb: [Function: emitRequestTimeout], upgradeOrConnect: false, parser: [HTTPParser], maxHeadersCount: null, reusedSocket: false, host: 'biz.pusan.ac.kr', protocol: 'https:', _redirectable: [Circular *1], [Symbol(kCapture)]: false, [Symbol(kBytesWritten)]: 0, [Symbol(kNeedDrain)]: false, [Symbol(corked)]: 0, [Symbol(kOutHeaders)]: [Object: null prototype], [Symbol(errored)]: null, [Symbol(kHighWaterMark)]: 16384, [Symbol(kRejectNonStandardBodyWrites)]: false, [Symbol(kUniqueHeaders)]: null }, _currentUrl: 'https://biz.pusan.ac.kr/bbs/biz/2556/rssList.do?row=5', _timeout: null, [Symbol(kCapture)]: false } } Killed error Command failed with exit code 137. error Command failed with exit code 137. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. Instance is unhealthy yarn run v1.22.19 Instance is healthy $ cd server && yarn start $ node app.js

 

Client network socket disconnected before secure TLS connection was established.

  • 상황: 특정 학과 홈페이지를 크롤링할 때는 80%의 확률로 해당 에러가 발생한다. 다른 학과 홈페이지를 크롤링할 때는 문제가 없었기 때문에 특정 학과의 서버 문제임을 알 수 있다.
  • 솔루션: 학과 서버 문제는 내가 할 수 있는게 없기 때문에 해당 학과는 DB에서 제거하는 수 밖에 없었다. 20%의 확률로 크롤해오기는 해서, 성공할 때까지 schedulingJobs를 재실행할 수도 있었지만 괜히 리소스를 낭비할거라 생각해 시도하지는 않았다.

AxiosError: Client network socket disconnected before secure TLS connection was established
  at AxiosError.from (file:///usr/src/app/server/node_modules/axios/lib/core/AxiosError.js:89:14)
  at RedirectableRequest.handleRequestError (file:///usr/src/app/server/node_modules/axios/lib/adapters/http.js:593:25)
  at RedirectableRequest.emit (node:events:514:28)
  at eventHandlers.<computed> (/usr/src/app/server/node_modules/follow-redirects/index.js:14:24)
  at ClientRequest.emit (node:events:514:28)
  at TLSSocket.socketErrorListener (node:_http_client:501:9)
  at TLSSocket.emit (node:events:514:28)
  at emitErrorNT (node:internal/streams/destroy:151:8)
  at emitErrorCloseNT (node:internal/streams/destroy:116:3)
  at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
localAddress: undefined,
port: 443,
host: 'cse.snu.ac.kr',
path: null,
code: 'ECONNRESET',
config: {
  transitional: {
    silentJSONParsing: true,
    forcedJSONParsing: true,
    clarifyTimeoutError: false
  },
  adapter: [ 'xhr', 'http' ],
  transformRequest: [ [Function: transformRequest] ],
  transformResponse: [ [Function: transformResponse] ],
  timeout: 0,
  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',
  maxContentLength: -1,
  maxBodyLength: -1,
  env: { FormData: [Function], Blob: [class Blob] },
  validateStatus: [Function: validateStatus],
  headers: Object [AxiosHeaders] {
    Accept: 'text/xml',
    'Content-Type': 'application/rss+xml',
    'User-Agent': 'axios/1.5.1',
    'Accept-Encoding': 'gzip, compress, deflate, br'
  },
  method: 'get',
  url: 'https://cse.snu.ac.kr/department-notices.xml?c%5B0%5D=40',
  data: undefined
},
request: <ref *1> Writable {
  _writableState: WritableState {
    objectMode: false,
    highWaterMark: 16384,
    finalCalled: false,
    needDrain: false,
    ending: false,
    ended: false,
    finished: false,
    destroyed: false,
    decodeStrings: true,
    defaultEncoding: 'utf8',
    length: 0,
    writing: false,
    corked: 0,
    sync: true,
    bufferProcessing: false,
    onwrite: [Function: bound onwrite],
    writecb: null,
    writelen: 0,
    afterWriteTickInfo: null,
    buffered: [],
    bufferedIndex: 0,
    allBuffers: true,
    allNoop: true,
    pendingcb: 0,
    constructed: true,
    prefinished: false,
    errorEmitted: false,
    emitClose: true,
    autoDestroy: true,
    errored: null,
    closed: false,
    closeEmitted: false,
    [Symbol(kOnFinished)]: []
  },
  _events: [Object: null prototype] {
    response: [Function: handleResponse],
    error: [Function: handleRequestError],
    socket: [Function: handleRequestSocket]
  },
  _eventsCount: 3,
  _maxListeners: undefined,
  _options: {
    maxRedirects: 21,
    maxBodyLength: Infinity,
    protocol: 'https:',
    path: '/department-notices.xml?c%5B0%5D=40',
    method: 'GET',
    headers: [Object: null prototype],
    agents: [Object],
    auth: undefined,
    family: undefined,
    beforeRedirect: [Function: dispatchBeforeRedirect],
    beforeRedirects: [Object],
    hostname: 'cse.snu.ac.kr',
    port: '',
    agent: undefined,
    nativeProtocols: [Object],
    pathname: '/department-notices.xml',
    search: '?c%5B0%5D=40'
  },
  _ended: true,
  _ending: true,
  _redirectCount: 0,
  _redirects: [],
  _requestBodyLength: 0,
  _requestBodyBuffers: [],
  _onNativeResponse: [Function (anonymous)],
  _currentRequest: ClientRequest {
    _events: [Object: null prototype],
    _eventsCount: 7,
    _maxListeners: undefined,
    outputData: [],
    outputSize: 0,
    writable: true,
    destroyed: false,
    _last: true,
    chunkedEncoding: false,
    shouldKeepAlive: false,
    maxRequestsOnConnectionReached: false,
    _defaultKeepAlive: true,
    useChunkedEncodingByDefault: false,
    sendDate: false,
    _removedConnection: false,
    _removedContLen: false,
    _removedTE: false,
    strictContentLength: false,
    _contentLength: 0,
    _hasBody: true,
    _trailer: '',
    finished: true,
    _headerSent: true,
    _closed: false,
    socket: [TLSSocket],
    _header: 'GET /department-notices.xml?c%5B0%5D=40 HTTP/1.1\r\n' +
      'Accept: text/xml\r\n' +
      'Content-Type: application/rss+xml\r\n' +
      'User-Agent: axios/1.5.1\r\n' +
      'Accept-Encoding: gzip, compress, deflate, br\r\n' +
      'Host: cse.snu.ac.kr\r\n' +
      'Connection: close\r\n' +
      '\r\n',
    _keepAliveTimeout: 0,
    _onPendingData: [Function: nop],
    agent: [Agent],
    socketPath: undefined,
    method: 'GET',
    maxHeaderSize: undefined,
    insecureHTTPParser: undefined,
    joinDuplicateHeaders: undefined,
    path: '/department-notices.xml?c%5B0%5D=40',
    _ended: false,
    res: null,
    aborted: false,
    timeoutCb: null,
    upgradeOrConnect: false,
    parser: null,
    maxHeadersCount: null,
    reusedSocket: false,
    host: 'cse.snu.ac.kr',
    protocol: 'https:',
    _redirectable: [Circular *1],
    [Symbol(kCapture)]: false,
    [Symbol(kBytesWritten)]: 0,
    [Symbol(kNeedDrain)]: false,
    [Symbol(corked)]: 0,
    [Symbol(kOutHeaders)]: [Object: null prototype],
    [Symbol(errored)]: null,
    [Symbol(kHighWaterMark)]: 16384,
    [Symbol(kRejectNonStandardBodyWrites)]: false,
    [Symbol(kUniqueHeaders)]: null
  },
  _currentUrl: 'https://cse.snu.ac.kr/department-notices.xml?c%5B0%5D=40',
  [Symbol(kCapture)]: false
},
cause: Error: Client network socket disconnected before secure TLS connection was established
    at connResetException (node:internal/errors:720:14)
    at TLSSocket.onConnectEnd (node:_tls_wrap:1605:19)
    at TLSSocket.emit (node:events:526:35)
    at endReadableNT (node:internal/streams/readable:1359:12)
    at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
  code: 'ECONNRESET',
  path: null,
  host: 'cse.snu.ac.kr',
  port: 443,
  localAddress: undefined
}
}
Killed
error Command failed with exit code 137.
error Command failed with exit code 137.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
yarn run v1.22.19
$ cd server && yarn server
$ node app.js

failed with exit code 137.

  • 상황: cron 작업을 수행할 때마다 메모리 오버(137) 때문에 서버가 죽는 문제가 발생한다. koyeb의 무료 플랜을 사용 중이라 메모리(256mb)가 적긴 하다.
  • 솔루션: 단순하게 유료 플랜으로 올려서 메모리를 업그레이드시키면 편하겠지만, 할 수 있는 데까지 서버 최적화를 해보기로 했다.

전역변수 tranporter 제거

// create email transporter.
global.transporter = nodemailer.createTransport({
  service: "gmail",
  host: "smtp.gmail.com",
  port: 587,
  secure: false,
  auth: {
    user: process.env.GOOGLE_MAIL_USER_ID,
    pass: process.env.GOOGLE_MAIL_APP_PASSWORD,
  },
});

nodemailer 라이브러리를 사용해서 메일을 전송하기 위해선 transporter를 생성해야 했고, 전역으로 한 번만 선언해서 사용 중이었다. 그러나 전역변수가 항상 메모리를 점유한다고 생각했고, 전역으로 선언하는 대신 schedulingJobs() 호출마다 생성해주는 방식으로 변경했다.

전역변수 waitingQueue 제거

global.waitingQueue = {};

// 구독 요청 시
global.waitingQueue[email] = {
      department_code: department,
      start_time: startTime,
    };

// check if email exist in waiting queue.
  if (
    !(email in global.waitingQueue) ||
    global.waitingQueue[email] === undefined
  ) {
    res.status(500).json({
      type: "ERROR",
      message: "Server error! Your email don't exist in waiting queue.",
    });
  }

// check if email validation is expired.
  if (isExpired(global.waitingQueue[email].start_time)) {
    delete global.waitingQueue[email];
    res.status(500).json({
      type: "ERROR",
      message: "Your email validation is expired.",
    });
  }
  • 구독을 요청한 사람들을 검증하기 위해 이메일로 검증 메일을 전송함과 동시에 waitingQueue에 push한다. push된 시간부터 10분 이내에 검증 메일을 확인하지 못하면 expired 처리되며 waitingQueue에서 삭제한다.
  • 마찬가지로 전역변수가 메모리를 점유한다 생각했고, 특히 object 타입이기 때문에 waitingQueue에 데이터가 쌓일수록 메모리를 잡아먹을 것 같았다.
  • 솔루션: 전역변수를 제거하고, redis를 waitingQueue로 사용했다. waitingQueue에 대한 처리를 redis cloud에 맡김으로써 서버의 메모리 부담을 줄이고자 하였다.
// server/utils/Redis.js
import Redis from "ioredis";

export const initializeRedis = async () => {
  const port =
    process.env.NODE_ENV === "production"
      ? process.env.REDIS_LABS_PORT
      : process.env.REDIS_DOCKER_PORT;
  const host =
    process.env.NODE_ENV === "production"
      ? process.env.REDIS_LABS_HOST
      : process.env.REDIS_DOCKER_HOST;
  const options =
    process.env.NODE_ENV === "production"
      ? {
          password: process.env.REDIS_LABS_PASSWORD,
        }
      : {};
  const redisClient = new Redis(port, host, options);
  redisClient.on("connect", () => {
    console.log("[Success] Redis Connected");
  });
  redisClient.on("error", (err) => {
    console.log("[Error] Redis error: " + err);
  });
  return redisClient;
};

비동기 작업 분리

  • 상황: 모든 학과의 여러 게시판에서 데이터를 긁어온 후, 모든 유저들에게 메일을 전송하는 작업을 수행해야 했기에 async/await 작업이 많이 들어갈 수 밖에 없었다. 여기서 비동기로 동시에 실행되는 작업이 많아서 메모리 부담이 가중된다 생각했고 리팩토링을 통해 비동기 코드를 전부 분리시켰다.
  • 변경 전
// server/app.js
cron.schedule("0 2,9 * * 1-5", () => {
  const now = new Date();
  now.setHours(now.getHours() + 9);
  console.log(`[Cron] Fetching RSS data (${now}).`);
  Department.find({})
    .then(async (departments) => {
      if (departments.length === 0) {
        console.log("[Cron] Department is nothing.");
        // 이후 작업..
}
  • 변경 후
// server/app.js
cron.schedule("0 2,9 * * 1-5", async () => {
  const now = new Date();
  now.setHours(now.getHours() + 9);
  console.log(`[Cron] Fetching RSS data (${now}).`);
  try {
    const departments = await Department.find({});
    if (departments.length === 0) {
      console.log("[Cron] Department is nothing.");
      return;
    }

    for (const department of departments) {
      if (department.boards.length === 0) {
        console.log("[Cron] No RSS data on", department.code);
        // 이후 작업..
}

'프로젝트 > MailBadara' 카테고리의 다른 글

[토이프로젝트] MailBadara - (7) 1차 서버 리팩토링  (1) 2023.11.22
[토이프로젝트] MailBadara - (6) 서버 최적화 생각 중  (0) 2023.10.16
[토이프로젝트] MailBadara - (4) 프론트 최적화  (0) 2023.10.13
[토이프로젝트] MailBadara - (3) 베타테스트 및 피드백  (1) 2023.10.08
[토이프로젝트] MailBadara - (2) MVP 개발  (1) 2023.10.08
'프로젝트/MailBadara' 카테고리의 다른 글
  • [토이프로젝트] MailBadara - (7) 1차 서버 리팩토링
  • [토이프로젝트] MailBadara - (6) 서버 최적화 생각 중
  • [토이프로젝트] MailBadara - (4) 프론트 최적화
  • [토이프로젝트] MailBadara - (3) 베타테스트 및 피드백
KimCookieYa
KimCookieYa
무엇이 나를 살아있게 만드는가
  • KimCookieYa
    쿠키의 주저리
    KimCookieYa
  • 전체
    오늘
    어제
    • 분류 전체보기 (576)
      • 혼잣말 (88)
      • TIL (3)
      • 커리어 (24)
        • Sendy (21)
        • 외부활동 기록 (2)
      • 프로젝트 (186)
        • 티스토리 API (5)
        • 코드프레소 체험단 (89)
        • Web3 (3)
        • Pint OS (16)
        • 나만무 (14)
        • 대회 (6)
        • 정글 FE 스터디 (16)
        • MailBadara (12)
        • github.io (1)
        • 인공지능 동아리, AID (5)
        • 졸업과제 (18)
        • OSSCA 2024 (1)
      • 크래프톤 정글 2기 (80)
      • IT (169)
        • 코딩 (4)
        • CS (18)
        • 에러 (5)
        • 블록체인 (23)
        • Front-End (40)
        • 알고리즘&자료구조 정리 (3)
        • 코딩테스트 (3)
        • BOJ 문제정리 (41)
        • WILT (12)
        • ML-Agents (4)
        • 강화학습 (1)
        • Android (0)
        • LLM (2)
      • 전공 (1)
        • 머신러닝 (1)
      • 자기계발 (20)
        • 빡공단X베어유 (2)
        • 독서 (15)
  • 블로그 메뉴

    • 홈
    • 방명록
    • Github
    • Velog
    • 관리
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    글리치해커톤
    센디
    자바스크립트
    Flutter
    OS
    react
    docker
    크래프톤정글
    MailBadara
    JavaScript
    부산대
    나만무
    알고리즘
    사이드프로젝트
    블록체인
    코드프레소
    RNN
    pintos
    프로그래머스
    해커톤
    Pint OS
    파이썬
    딥러닝
    numpy
    졸업과제
    NEAR Protocol
    머신러닝
    리액트
    핀토스
    니어프로토콜
  • hELLO· Designed By정상우.v4.10.3
KimCookieYa
[토이프로젝트] MailBadara - (5) 트러블슈팅
상단으로

티스토리툴바