[트러블 슈팅] 멀티프로세스 도입 테스트 Value is not an object: null - 100-hours-a-week/5-yeosa-wiki GitHub Wiki

1. 문제 상황

a. 테스트 환경

  • 단일 프로세스 → 다중 프로세스 전환에서 속도 향상 테스트
  • 시나리오
    • 10명의 vu가 20장의 사진에 대해 embedding→categories 요청 보냄
    • iteration 간 3~5초 sleep

b. 테스트 결과

image

  • k6 로그에서 위와 같은 에러 발생
  • 다만 cpu, gpu 서버에서는 에러 로그 발견되지 않음

2. 상황 식별 및 가설 설정

a. 상황 식별

  • TypeError: Value is not an object: null
    • JavaScript에서 null 값을 객체처럼 다루려 할 때 발생.
  • 예상 원인
    • 서버에서 응답을 정상적으로 반환하지 못했는데 그걸 json으로 파싱하려 해서 null 반환됨.
    • 해당 값을 객체처럼 이용하려 할 때, 문제 발생.

image

import http from 'k6/http';
import { sleep } from 'k6';
import { randomSeed, randomIntBetween } from 'k6';

randomSeed(__VU);

export const options = {
    vus: 10,
    duration: '10m',
};

const TOTAL_IMAGES = 600;
const BATCH_SIZE = 20;

const embeddingUrl = 'http://localhost:8000/api/albums/embedding';

const params = {
    headers: {
        'Content-Type': 'application/json',
    },
};

export default function () {
    const vu = __VU;
    const iter = __ITER;

    const groupIndex = (vu - 1) * 1000 + iter;
    const start = (groupIndex * BATCH_SIZE) % TOTAL_IMAGES + 1;
    const end = start + BATCH_SIZE - 1;

    const images = [];
    for (let i = start; i <= end; i++) {
        const idx = ((i - 1) % TOTAL_IMAGES) + 1;
        images.push(`img${String(idx).padStart(3, '0')}.jpg`);
    }

    const payload = JSON.stringify({ images });

    // ✅ Embedding 요청만 전송
    const embedRes = http.post(embeddingUrl, payload, params);
    
    if (!embedRes || embedRes.status === 0) {
        console.error(`❌ [VU:${vu} ITER:${iter}] 임베딩 요청 실패 - 연결 거부됨`);
    } else if (!embedRes.body) {
        console.error(`❌ [VU:${vu} ITER:${iter}] 임베딩 응답 body 없음`);
    } else {
        let embedBody = {};
        try {
            embedBody = embedRes.json();
        } catch (err) {
            console.error(`❌ [VU:${vu} ITER:${iter}] embedRes JSON 파싱 실패: ${err.message}`);
        }

        if (embedRes.status !== 201) {
            console.warn(`[WARN] [VU:${vu} ITER:${iter}] 임베딩 실패 - status: ${embedRes.status}`);
        } else {
            console.log(`[OK] [VU:${vu} ITER:${iter}] 임베딩 응답 - message: ${embedBody?.message}`);
        }
    }

    sleep(randomIntBetween(3, 5));
}

b. 가설 설정

  • 요청이 어떠한 이유로 서버에 전달되지 못한 상태에서 응답을 가져와 res.status와 같이 이용하려 해서 null은 객체가 아니라는 에러가 발생했을 것이다.

3. 가설 검증

import http from 'k6/http';
import { sleep } from 'k6';
import { randomSeed, randomIntBetween } from 'k6';

randomSeed(__VU);

export const options = {
    vus: 10,
    duration: '10m',
};

const TOTAL_IMAGES = 600;
const BATCH_SIZE = 20;

const embeddingUrl = 'http://localhost:8000/api/albums/embedding';

const params = {
    headers: {
        'Content-Type': 'application/json',
    },
};

export default function () {
    const vu = __VU;
    const iter = __ITER;

    const groupIndex = (vu - 1) * 1000 + iter;
    const start = (groupIndex * BATCH_SIZE) % TOTAL_IMAGES + 1;
    const end = start + BATCH_SIZE - 1;

    const images = [];
    for (let i = start; i <= end; i++) {
        const idx = ((i - 1) % TOTAL_IMAGES) + 1;
        images.push(`img${String(idx).padStart(3, '0')}.jpg`);
    }

    const payload = JSON.stringify({ images });

    // ✅ embedRes가 null인지 여부만 확인
    const embedRes = http.post(embeddingUrl, payload, params);
    const isNull = embedRes === null;
    console.log(`[VU:${vu} ITER:${iter}] embedRes is null: ${isNull}`);

    sleep(randomIntBetween(3, 5));
}
  • 위와 같이 객체의 필드를 이용하는 부분을 모두 제거하고 테스트를 재진행했다. 그러나 동일한 에러가 발생했다.

4. 문제 재식별

ERRO[0001] TypeError: Value is not an object: null
running at default (file:///Users/kimdahyun/dahyun1008/k6_script/test2.js:44:27(106))  executor=constant-vus scenario=default source=stacktrace
INFO[0001] [VU:3 ITER:0] embedRes is null: false         source=console
ERRO[0001] TypeError: Value is not an object: null
running at default (file:///Users/kimdahyun/dahyun1008/k6_script/test2.js:44:27(106))  executor=constant-vus scenario=default source=stacktrace
INFO[0002] [VU:7 ITER:0] embedRes is null: false         source=console
ERRO[0002] TypeError: Value is not an object: null
running at default (file:///Users/kimdahyun/dahyun1008/k6_script/test2.js:44:27(106))  executor=constant-vus scenario=default source=stacktrace
INFO[0002] [VU:4 ITER:0] embedRes is null: false         source=console
ERRO[0002] TypeError: Value is not an object: null
running at default (file:///Users/kimdahyun/dahyun1008/k6_script/test2.js:44:27(106))  executor=constant-vus scenario=default source=stacktrace
  • 위 에러 로그에서 44번 줄에 문제가 있다고 되어 있는것을 뒤늦게 발견했다.

  • 코드를 vscode에 옮겨 확인해보니

        sleep(randomIntBetween(3, 5));
    

    이 부분이었다. randomIntBetween가 문제의 원인이었고 sleep(3)과 같이 수정해서 문제를 해결했다.

  • grafana 공식 문서에서

    import { sleep } from 'k6';
    import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
    
    export default function () {
      // code ...
    
      sleep(randomIntBetween(1, 5)); // sleep between 1 and 5 seconds.
    }
    

    해당 함수를 이용하는 부분이 있는데 null에 대한 설명은 없다.

    → import해온 라이브러리가 다른게 원인이었을 것 같다.


5. 가설 재검증

import http from 'k6/http';
import { sleep } from 'k6';
import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';

export const options = {
    vus: 10,
    duration: '10m',
};

const TOTAL_IMAGES = 600;
const BATCH_SIZE = 20;

const embeddingUrl = 'http://localhost:8000/api/albums/embedding';

const params = {
    headers: {
        'Content-Type': 'application/json',
    },
};

export default function () {
    const vu = __VU;
    const iter = __ITER;

    const groupIndex = (vu - 1) * 1000 + iter;
    const start = (groupIndex * BATCH_SIZE) % TOTAL_IMAGES + 1;
    const end = start + BATCH_SIZE - 1;

    const images = [];
    for (let i = start; i <= end; i++) {
        const idx = ((i - 1) % TOTAL_IMAGES) + 1;
        images.push(`img${String(idx).padStart(3, '0')}.jpg`);
    }

    const payload = JSON.stringify({ images });

    // ✅ embedRes가 null인지 여부만 확인
    const embedRes = http.post(embeddingUrl, payload, params);
    const isNull = embedRes === null;
    console.log(`[VU:${vu} ITER:${iter}] embedRes is null: ${isNull}`);

    sleep(randomIntBetween(3, 5));
}
  • 임포트 경로를 수정하니 정상 동작한다.

6. 정리


7. 참고자료

https://grafana.com/docs/k6/latest/javascript-api/jslib/utils/randomintbetween/