FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
1: 0x104829da0 node::Abort() [/opt/homebrew/Cellar/node/20.2.0/bin/node]
2: 0x10482b2ac node::ModifyCodeGenerationFromStrings(v8::Local<v8::Context>, v8::Local<v8::Value>, bool) [/opt/homebrew/Cellar/node/20.2.0/bin/node]
3: 0x104993dbc v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [/opt/homebrew/Cellar/node/20.2.0/bin/node]
4: 0x104993d68 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [/opt/homebrew/Cellar/node/20.2.0/bin/node]
5: 0x104b226ac v8::internal::Heap::CallGCPrologueCallbacks(v8::GCType, v8::GCCallbackFlags, v8::internal::GCTracer::Scope::ScopeId) [/opt/homebrew/Cellar/node/20.2.0/bin/node]
6: 0x104b213b0 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/opt/homebrew/Cellar/node/20.2.0/bin/node]
7: 0x104b18784 v8::internal::HeapAllocator::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/opt/homebrew/Cellar/node/20.2.0/bin/node]
8: 0x104b18ee8 v8::internal::HeapAllocator::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/opt/homebrew/Cellar/node/20.2.0/bin/node]
9: 0x104b01804 v8::internal::Factory::AllocateRawWithAllocationSite(v8::internal::Handle<v8::internal::Map>, v8::internal::AllocationType, v8::internal::Handle<v8::internal::AllocationSite>) [/opt/homebrew/Cellar/node/20.2.0/bin/node]
10: 0x104b05950 v8::internal::Factory::NewJSObjectFromMap(v8::internal::Handle<v8::internal::Map>, v8::internal::AllocationType, v8::internal::Handle<v8::internal::AllocationSite>) [/opt/homebrew/Cellar/node/20.2.0/bin/node]
11: 0x104ceae5c v8::internal::JSDate::New(v8::internal::Handle<v8::internal::JSFunction>, v8::internal::Handle<v8::internal::JSReceiver>, double) [/opt/homebrew/Cellar/node/20.2.0/bin/node]
12: 0x1049fd0a4 v8::internal::Builtin_DateConstructor(int, unsigned long*, v8::internal::Isolate*) [/opt/homebrew/Cellar/node/20.2.0/bin/node]
스트레스 테스트를 진행하며 메모리 누수 발생문제가 발생하여 서버가 죽는 현상이 발생하였다. 메모리 누수 발생은 처음이다.
async getUserById(userId: number, id: number): Promise<User> {
const user = await this.findUser('id', id, [
'Followings',
'Funding',
'Funded',
]);
// 내가 팔로잉 하고 있는지 여부
const isFollowing = user.Followings.some(
(follower) => follower.id === userId,
);
// 선물게시물 개수, 펀딩 개수
const fundingNum = user.Funding.length;
const fundedNum = user.Funded.length;
return {
...ModelConverter.user(user),
fundingNum: 1,
fundedNum: 1,
isFollowing: true,
};
}
테스트senario
들을 하나씩 돌려가며 문제가 되는 API를 찾았다. 위는 문제가 되는 메서드이다.
const user = await this.findUser('id', id, [
'Followings',
'Funding',
'Funded',
]);
디버깅을 해보니 여기서 **Followings**
들을 가져올때 문제가 발생하는 것을 찾을 수 있었다.
부하 테스트를 하기전 데이터를 seeding하여 유저마다 10000명의 팔로워를 넣어준 상황이였다.
이 문제를 결국 해결하게 되었는데, 그 전에 **Eager Loading**
과 **Lazy Loading**
의 차이를 살펴보자.
Eager Loading은 TypeORM에서 Entity 간의 관계를 로드 하는 방법 중 하나이다.
const user = await this.userRepository.findOne({
where:{ id:userId },
relations: ['Followings']
})
유저를 가져오며 해당 유저가 팔로우하는 유저들을 가져오는 쿼리를 작성해보자.
Eager Loading
은 초기 데이터베이스 쿼리에서 모든 관련 데이터를 로드하는 것을 의미한다. 처음에 **user**
를 가져오면서 relations로 **Followers**
들도 가져오는 방법이다.
Eager Loading
장점은 데이터 로딩 속도가 빠르고, 나중에 해당 데이터 접근 시 추가 쿼리가 필요없다. 하지만 불필요한 데이터 로딩과 메모리 사용량 증가 가능성이 있어서 성능에 영향을 미칠 수 있다.
따라서 관련 데이터가 항상 필요한 경우나 데이터 양이 많지 않거나 메모리 사용에 큰 영향을 미치지 않을 경우에는 **Eager Loading
** 을 사용한다.
const user = await this.userRepository.findOne({ where:{ id:userId } })
const followers = user.Followers;
Lazy Loading
은 필요한 시점에서 관련 데이터를 로드하는 방식이다. 이 때, 초기에는 관련 데이터가 로드되지 않는다.
const users = await userRepository.find();
for (const user of users) {
const followers = await user.Followers; // 여기서 N + 1 쿼리 문제 발생.
}
N + 1 쿼리 문제는 Lazy Loading
을 사용할 때 발생할 수 있는 성능 문제다.
예를 들어 사용자 목록을 검색하고 그 사용자들의 팔로워를 가져오는 경우를 살펴보면 이 경우에 사용자 수만큼 추가 쿼리가 실행되어서 성능에 부담을 줄 수 있다.
따라서 Lazy Loading
는 관련 데이터가 필요한 경우에만 로드되는 것이 효율적인 경우나, 데이터 양이 크거나, 관련 데이터가 자주 사용되지 않는 경우에 사용해야한다.
const user = await this.findUser('id', id, [
'Followings',
'Funding',
'Funded',
]);
// 내가 팔로잉 하고 있는지 여부
const isFollowing = user.Followings.some(
(follower) => follower.id === userId,
);
나는 해당 유저가 특정 유저를 팔로우하고 있는지 여부를 확인하기 위해 Eager Loading
을 사용해 가져왔다. 나는 가져온 유저들의 정보를 다 사용하지도 않을 건데 미리 가져와 버린 것이다. 따라서 메모리 사용량이 계속 증가하였고, 여기서 메모리 누수가 발생하였다.
async getUserById(userId: number, id: number): Promise<User> {
const user = await this.findUser('id', id, [
'Funding',
'Funded',
]);
// 내가 팔로잉 하고 있는지 여부
const isFollowing = await this.followRepository.exist({
where: { followingId: id, followerId: userId },
});
...
따라서 직접 **FollowEntity**
에 접근하여 팔로우하고 있는지의 여부를 가져오는 방식으로 수정해 주었다.
평소에 한 번씩 API를 요청하였을 때는 확인할 수 없었는데, 부하테스트를 진행하였더니 문제가 되는 메서드들을 바로 확인할 수 있었다.