prisma보통 설치할 때 2가지 모듈을 설치합니다.
- prisma
- @prisma/client
prisms는 보통 npx prisma migarte dev, npx prisma generate, ...등 cli 도구로 사용하기 위해 설치합니다.
@prisma/client는 우리에게 ORM기능을 제공하며, prisma.user.findMany 같은 직접 DB에 쿼리를 날릴 수 있는 api, 생성한 DB 모델의 Typescript type definitions, query engine(실제 DB 질의, connection pool) 기능을 제공하고 있습니다.
prisma query engine
우리가 작성한 코드(prisma.user.findMany())는 결국 어떠한 변화를 거쳐 db query 질의를 해야합니다. 이 역할을 하는 것이 query engine 입니다. query engien은 Rust로 작성되어 있고 high-level 인터페이스에서 사용할 수 있는 low-level api를 제공하고 있습니다. 즉, 실제 db query하는 기능은 query engine이 담당하고 Prisma Client는 query engine이 제공하는 low-level api를 호출하여 동작합니다.
기본적으로 Prisma 를 설치하거나 업데이트하면 Prisma cli 패키지가 모든 엔진 파일을 자동으로 node_modules/@prisma/engines 폴더에 다운로드 합니다.
query engine파일은 운영체제에 따라 다릅니다. 이 파일은 quqery_engine-PLATFORM 또는 libquery_engine-PLATFORM 형태로 이름이 만들어 지며, 여기서 PLATFORM명은 컴파일 타겟(빌드 대상 플랫폼)의 이름에 해당합니다. 지원 가능한 빌드 대상 플랫폼은 여기에서 확인 가능합니다.
기본적으로 query engine은 node-api 라이브러리 형태이며, Prisma Client 내부에 로드되어 사용됩니다. 하지만, 필요하다면 Prisma 설정을 변경해서, query engine을 실행 파일(executable binary) 형태로 컴파일하고 어플리케이션과 함께 별도의 프로세스(sidecar processor)로 실행할 수도 있습니다.
하지만 Prisma에서는 기본 방식인 node-api 라이브러리 형태를 권장하며, Prisma client와 query engine 사이의 통신 오버헤드가 줄어들어 성능상 더 유리하기 때문입니다.
query engine은 Prisma Client api가 실행되거나(prisma.user.findUnique()), $connect() 메서드가 실행될 때 실행됩니다. query engine이 실행되면 connection pool을 만들고 database와 physical connection을 관리합니다. 이 시점부터 Prisma Client는 database에 쿼리를 보낼 준비가 됩니다.
query engine이 실행되고 Prisma Client API로 query를 보내고 query engine이 종료되는 전체적인 흐름은 다음과 같습니다.
query engine이 하는 역할을 정리하면 다음과 같습니다.
Connection pool
mysql 8.4 기준
mysql에 connection을 맺을 수 있는 개수는 최대 최대 100000이며, 만약 모든 커넥션을 맺는 다면 mysql server는 out of memory가 발생할 것입니다. 따라서 여느 orm과 마찬가지로 connection pool 기능을 지원하며(query engine에서) mysql 서버에 동시에 맺을 수 있는 커넥션 수를 제어하고 있습니다.
connection pool은 query engine에 의해 관리되며 아래 2가지 방법으로 생성됩니다.
- 명시적으로 $connection() 호출
- 첫번째 Prisma Client api(findMany(), findUnique(), ..)를 호출하는 경우(내부적으로 $connection() 호출함)
connection pool이 전체적으로 동작하는 방식은 다음과 같습니다.
- query engine이 2개의 옵션값(pool size, pool timeout)으로 connection pool을 초기화 합니다.
- query engine은 1개의 connection을 만들어 connection pool에 넣습니다.
- 쿼리가 실행되면, query engine은 쿼리를 처리하기 위해 connection pool에서 connection을 획득합니다.
- 만약 connection pool에 여유 connection이 없다면, query engine은 connection을 하나 만들어 connection pool에 넣습니다(무한정 넣는것이 아닌 pool size보다 적을때까지)
- 만약 query engine이 connection pool에서 connection을 획득하지 못하면, query를 메모리에 있는 FIFO(First In First Out) queue에 삽입합니다.
- 만약 query engine이 queue에 있는 query를 pool timeout(s)시간까지 처리 못하면, P2024 code exception 처리를 하고 queue에 있는 다음 작업으로 넘어갑니다.
위에서 설명한 2개의 옵션값으로 pool size, pool timeout을 설정하는데, 이 값은 database connection url의 parameterr값으로 전달할 수 있습니다.
- connection_limit : connection pool 크기
- pool_timeout : pool을 획득하지 못한 query가 queue에 대기하는 시간(pool_timeout을 넘어가면 P2024 에러)
datasource db {
provider = "mysql"
url = "mysql://root:1234@localhost:3306/test?connection_limit=2&pool_timeout=10"
}
connection_limit
connection pool의 크기를 정함
connection_limit 값을 명시하지 않으면 기본적으로 아래 공식대로 connection pool 사이즈가 정해집니다.
num_physical_cpus * 2 + 1
머신1개에 어플리케이션1만 있다면 위 공식이 적합하겠지만, 보통 1개의 머신위에 여러개의 어플리케이션을 동작시키며, 서버리스 환경에서는 여러개의 머신이 올라가기 때문에 위 공식은 현실적으로 적합하지 않습니다. 이 부분은 아래에서 다시 다루겠습니다.
아래처럼 PrismaClient에 log 옵션을 주면, connection pool의 사이즈를 알 수 있습니다. connection pool은 prisma client api를 호출하거나, 명시적으로 $connect()를 호출해야 생성됩니다.
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient({
log: ['info'],
})
await prisma.$connect();
prisma:info Starting a mysql pool with 17 connections.
connection pool의 크기가 17인 이유는 현재 맥북의 cpu core개수가 8개(core * 2 + 1)때문 입니다.
신기한 점은, connection pool의 크기가 17로 초기화가 되었지만, 실제 mysql 서버와의 session을 맺어지지 않았습니다. $connect가 호출 된 이후 performance_schema.threads를 확인해보면, 클라이언트 요청을 처리하는 FOREGROUND THREAD가 17개가 아닌, 1만 생성된 것을 알 수 있습니다(3개가 보이는 것은 나머지는 datagrip에서 연결한 session 입니다).
이는 위에서 설명한, connection pool의 동작설명에서 query engine이 connection pool을 초기화 하면서 connection한개 생성하여 pool에 넣기 때문입니다.
반면에 쿼리를 실행하면(connection이 증가하는 것을 확인하기 위해 sleep 3) DB서버에 session이 증가한 것을 볼 수 있습니다.
const prisma = new PrismaClient({
log: ["query", "info"],
});
await prisma.$connect();
await Promise.all([
prisma.$queryRaw`select sleep(3)`,
prisma.$queryRaw`select sleep(3)`,
prisma.$queryRaw`select sleep(3)`,
prisma.$queryRaw`select sleep(3)`,
prisma.$queryRaw`select sleep(3)`,
prisma.$queryRaw`select sleep(3)`,
prisma.$queryRaw`select sleep(3)`,
prisma.$queryRaw`select sleep(3)`,
prisma.$queryRaw`select sleep(3)`,
prisma.$queryRaw`select sleep(3)`,
prisma.$queryRaw`select sleep(3)`,
prisma.$queryRaw`select sleep(3)`,
prisma.$queryRaw`select sleep(3)`,
prisma.$queryRaw`select sleep(3)`,
prisma.$queryRaw`select sleep(3)`,
prisma.$queryRaw`select sleep(3)`,
prisma.$queryRaw`select sleep(3)`,
]);
pool_timeout
n번째 쿼리가 connection을 얻기 위해 대기하는 시간을 의미
default : 10s
위에서 설명했듯이, pool_timeout은 query engine이 queue에 있는 query를 pool_timeout시간안에 처리하지 못하면, P2024 code exception 처리를 하고, queue에 있는 다음 작업으로 넘어갑니다.
pool_timeout을 비활성화 하면 query engine이 connection을 기다리는데 제한이 없어지며, exception을 던지는 것을 방지하고, queue가 쌓이도록 허용합니다. 다음과 같은 상황에서 이 접근 방식을 고려할 수 있습니다.
- You are submitting a large number of queries for a limited time - for example, as part of a job to import or update every customer in your database.
- You have already increased the connection_limit.
- You are confident that the queue will not grow beyond a certain size, otherwise you will eventually run out of RAM.
connection_limit=1&pool_timeout=1으로 하고 3초 걸리는 쿼리를 한번에 실행하면 에러가 발생하게 됩니다.
connection_limit=1&pool_timeout=1
await Promise.all([
prisma.$queryRaw`select sleep(3)`,
prisma.$queryRaw`select sleep(3)`,
prisma.$queryRaw`select sleep(3)`,
]);
Long-running processes
보통 한개의 서버로 운영하는 경우 connection_limit값을 num_physical_cpus * 2 + 1로 하지만, 복수개의 서버를 운영하는 경우 서버의 개수만큼 나눠줍니다.
(num_physical_cpus * 2 + 1) ÷ number of application instances.
또한, 지속적으로 요청을 처리해야 하기 때문에 쿼리 실행 후 명시적으로 $disconnect()를 호출 할 필요가 없습니다. 새로운 connection을 연결하는 것 또한 시간이 소요되기 때문입니다.
개발환경에서 Next.js처럼 변경된 파일에 대해 hot reloading을 지원하는 경우 추가적은 connection pool을 만들기 때문에 하나의 PrismaClient를 글로벌 변수로 관리해야 합니다.
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
export const prisma =
globalForPrisma.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
Serverless environments
항상 실행되고 있는 서버가 아닌 클라이언트의 요청마다 새로운 함수가 실행되는 gcp cloud run, aws lamda의 경우 매 요청마다 connection pool이 생성됩니다. connection_limit을 3으로 설정했을 때 cloud run은 3개의 connection pool을 생성하게 됩니다.
하지만 클라이언트 요청이 spike를 치게 되는 경우 더 많은 cloud run이 실행되며 그때마다 3크기의 connection pool을 생성하게 됩니다. 만약 N개의 cloud run이 실행되면 N x 3개만큼 database session이 생성되며 이는 결과적으로 database에 부하가 생성됩니다.
따라서 serverless 환경에서는 먼저 connection_limit을 1로 설정하게 됩니다. 하지만 1로 부족한 경우 connection_limit을 늘리게 되고 cloud run가 많이 실행되면서 DB에 연결되는 connection을 제어할 수 없게 되고(cloud run을 실행되면서 단지 connection_limit만큼 connection을 맺음, 하지만 cloud run이 실행되는 개수는 제한이 없음), cloud run이 종료되면서 DB에 연결된 connection은 재사용할 수 없게 됩니다.
따라서 이 경우 Prisma Accelerate, PgBouncer, AWS RDS Proxy같은 External connection poolers를 사용하며 connection pool를 Prisma 내부에서 사용하지 않고 외부에서 별도의 connection pool을 관리할 수 있습니다. DB에 연결되는 connection 개수, cloud run(PrismaClient 생성)에서 사용되는 connection은 종료되더라도 재사용할 수 있게 됩니다.
'개발' 카테고리의 다른 글
promise pool로 비동기 작업 처리량 높이기 (0) | 2025.04.03 |
---|---|
선착순 쿠폰 서비스 개선하기 (0) | 2025.03.27 |
monorepo프로젝트 docker build하기 (0) | 2025.02.27 |
git action에서 Cloud SQL 인증 프록시로 db migration 하기 (0) | 2025.02.13 |
[Redis] 레디스 기본타입(Sorted Sets) (0) | 2024.03.30 |