진행 중인 프로젝트에서 서버 성능을 향상 시키기위해 커넥션 풀을 늘리는 작업을 진행했다.

관련해 공부한 내용을 정리해봤다.


서버 성능을 향상시킬 때 중요한 지표는 처리량응답시간이다.

응답시간이 짧을수록 처리량이 높아질수록 성능이 좋아진다.

처리량을 높이는 방법 중에 커넥션 풀을 늘리는 방법이 있는데 오늘은 이에 대해 포스팅 해보려고한다.

 

DB connection?

WAS는 http 요청에 따라 쓰레드를 생성해 비즈니스 로직을 수행한다.

이때 비즈니스 로직 수행을 위해 DB 서버로부터 데이터를 얻어와야하는데

DB 접속을 위해 드라이버를 로드한 후 디비 커넥션 객체를 생성해 물리적으로 DB 서버에 접근하게된다.

자바는 주로 jdbc를 활용한다.

 

connection pool이란?

WAS가 실행될 때 미리 설정된 일정량만큼의 디비 커넥션된 객체들을 만들어 풀에 저장해둔다.

클라이언트 요청이 올 경우 이 디비 커넥션 객체를 빌려주었다가 요청이 완료되면 다시 반납받아 풀에 저장해둔다.

커넥션 풀이 커지면 메모리 소모가 커지지만 동시성이 좋아져 클라이언트 요청의 대기 시간이 감소한다.

 

왜 미리 DB connection 객체를 준비해둘까?

서버의 부하를 줄일 수 있다.

-> DB 커넥션을 맺는 과정의 부하가 크다. 미리 생성하고 재활용하는 방식을 통해 부하를 줄일 수 있다.

 

왜 미리 설정한 일정량 만큼만 DB connection 객체를 만들까? 많을수록 좋은 거 아닐까?

-서버는 한정적인 자원이기 때문에 디비 커넥션 수를 제한해야지만 서버 자원 고갈을 방지할 수 있다.

-커넥션을 활용하는 주체는 쓰레드로 커넥션이 많아도 쓰레드의 수가 뒷받침되지 않으면 의미없다.

->활용되는 커넥션이 많다는 의미는 쓰레드를 많이 사용한다는 의미로 이는 곧 context switching이 많이 일어나 오버헤드가 많이 발생한다는 의미이며 성능적인 한계가 존재하게 된다.

 

connection pool 설정 값

initialSize

:커넥션 풀 생성 시 최소 생성한 connection 객체의 수

minIdle

:최소한으로 유지할 connection 객체 수

maxIdle

:반납된 유휴 connection 유지할 최대 객체 수

maxActive

:동시 사용가능한 최대 connection 객체 수

 

maxActive>=initialSize

maxIdle>=minIdle 이어야하며

maxIdle = maxActive 이어야 좋다.

 

적절한 connection pool 크기

Thread Pool 크기 < Connection Pool 크기

 

적절한 connection 크기 = ((core_count) * 2) + effective_spindle_count)

core_count: 현재 사용하는 서버 환경에서의 CPU 개수

effective_spindle_count: DB 서버가 관리할 수 있는 동시 I/O 요청 수

 

RDS max_connections 디폴트 값: DBInstanceClassMemory/12582880

=>RDS 서버의 종류에 따라 다르다.

 

MySQL max_connections 현재 값 확인 명령어

SHOW GLOBAL VARIABLES LIKE 'max_connections';

 

HikariCP

스프링 부트 2.0부터 default JDBC connection pool

application.properties에서 간단하게 HikariCP의 설정 가능

 

스프링부트 yml 파일 HikariCP 설정 예시코드

spring:
 datasource:
   url: 주소주소
   username: root
   password: Password
   hikari:
     maximum-pool-size: 100 #최대 pool 크기
     minimum-idle: 10 #최소 pool 크기
     idle-timeout: 600000 #연결위한 최대 유후 시간
     max-lifetime: 1800000 #반납된 커넥션의 최대 수명

 

 

참고 사이트

https://aws.amazon.com/ko/premiumsupport/knowledge-center/rds-mysql-max-connections/

https://linked2ev.github.io/spring/2019/08/14/Spring-3-%EC%BB%A4%EB%84%A5%EC%85%98-%ED%92%80%EC%9D%B4%EB%9E%80/

https://steady-coding.tistory.com/564

https://programmer93.tistory.com/74

https://juinthyme.tistory.com/70

https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=dnjsjd11&logNo=221270990625 

https://velog.io/@miot2j/Spring-DB%EC%BB%A4%EB%84%A5%EC%85%98%ED%92%80%EA%B3%BC-Hikari-CP-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0

진행 중인 프로젝트에서 갑자기 서버 재배포가 안되는 문제가 발생했었다.

급하게 원인을 분석해보니 서버 메모리가 부족해 빌드 파일이 제대로 생성 안되고 있었고 nohup.out 파일이 문제였다.

이를 해결하기 위해 스크립트를 활용해 배포시마다 기존 nohup 파일을  tar.gz로 압축 후 기존 파일을 삭제하고 새로운 nohup 파일을 생성해 사용하는 로직으로 수정했다.

현재 nohup 외에 날짜 별로 로그가 남고 일정 기간 이후 삭제되는 로직을 검토하고있는데 관련 로직이 완성되면 nohup 실행시 nohup.out이 생성안되도록 처리해야할 것 같다. 

관련해 리눅스 문법들을 서치하며 알게된 내용들을 정리하고자 글을 쓰게 됐다.


리눅스에서 압축을 한다하면 크게 3가지 용어가 튀어나오더라.

tar tar.gz zip이 그것인데 각각을 비교해보면 아래와 같다.

 

1. tar

리눅스 환경에서 가장 일반적으로 사용

압축보단 여러개 파일을 하나로 묶는 거에 가까움

따라서, 용량 압축이 거의 없다.

 

tar 명령어

압축

$ tar -cf 압축명.tar 파일명

압축해제

$ tar -xf 파일명.tar

 

2. tar.gz

리눅스 환경에서 가장 일반적으로 사용

합쳐진 tar 파일은 압축하는 방식으로 가장 좋은 압축 옵션

용량압축이 높음에도 cpu가 적게 사용된다.

 

tar.gz 명령어

압축

$ tar -zcvf 압축명.tar.gz 파일명

압축해제

$ tar -zxvf 압축명.tar.gz

3. zip

윈도우 환경에서 가장 많이 쓰는 확장자

거의 모든 os에서 호환되지만 용량 압축이 낮다.

 

zip 명령어

압축

$ zip -r 압축명.zip 파일명

압축해제

$ unzip 압축명.zip

 

 

추가로 알아두면 좋을만한 참고사항

tar  옵션값 

 -c  파일을 tar로 묶음
 -p  파일 권한을 저장
 -v  묶거나 파일을 풀 때 과정을 화면으로 출력
 -f  파일 이름을 지정
 -C  경로를 지정
 -x  tar 압축을 풂
 -z  gzip으로 압축하거나 해제함

tar 경로 지정

 

1. 압축시 경로 지정

압축될 파일명 앞에 위치시킬 폴더명을 같이 입력

ex) var/log/압축명.tar.gz

2. 압축해제시 경로 지정

끝에 -C 폴더위치명 기존 명령어 뒤에 추가로 붙여 입력

ex) -C /var/logs

 

+ 압축전후 파일 용량이 분명 궁금해질텐데 이때 아래 명령어를 사용하길 추천한다.

G,K,M 단위로 표시해줘서 보기 편하다.

$ ls -lh

 

참고사이트

https://suzxc2468.tistory.com/166

https://brownbears.tistory.com/161

https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=oiktoail&logNo=220852476226 

 

top 명령어

현재 CPU의 사용률, 메모리 사용률을 종합적으로 확인

$ top

명령어 실행 후 화면에서 Shift + m 키 입력으로 메모리 사용량으로 정렬해서 확인 가능

$ top -d 1 | egrep "PID|systemd"

top 명령어 입력 후 grep 명령을 사용해서 특정 프로세서의 메모리 사용량을 1초마다 확인

 

free 명령어

$ free

메모리 사용량 정보 확인

 

ps 명령어

$ ps -ef --sort -rss

현재 실행 중인 모든 프로세스의 메모리 사용량을 기준으로 정렬 후 pid 와 프로세스 보여줌

$ ps -eo user,pid,ppid,rss,size,vsize,pmem,pcpu,time,cmd --sort -rss | head -n 11

전체 프로세스 메모리 사용량 상위 10개 확인

 

meminfo 명령어

$ cat /proc/meminfo | grep Mem

 

 현재 시스템의 전체 메모리와 가용 메모리 확인

 

 

참고 사이트

https://118k.tistory.com/953

https://www.runit.cloud/2020/11/linux-process-memory-usage.html

https://zetawiki.com/wiki/%EB%A6%AC%EB%88%85%EC%8A%A4_%EB%A9%94%EB%AA%A8%EB%A6%AC_%EC%82%AC%EC%9A%A9%EB%A5%A0_%ED%99%95%EC%9D%B8

프로젝트를 진행하면서 로그 파일을 읽기 위해 vi/vim 명령어로 파일을 열 경우 용량이 커 화면이 멈추는 이슈가 계속 발생했다. 

로그를 확인하는 건 필수적인 부분이므로 관련해 해결 방법을 찾다가 less 명령어를 발견했다.

vi/vim의 경우 파일을 편집모드로 열지만 less 명령어의 경우 읽기 전용 모드로만 열리기 때문에 메모리 사용량이 높지않다.

 

$less 파일명

PAGE UP 또는 b : 한 페이지 위로

PAGE DOWN 또는 Space bar : 한 페이지 아래로

g : 텍스트 파일의 처음 부분으로 이동

G : 텍스트 파일의 마지막 부분으로 이동

/문자열 : 입력된 문자열 찾기

n : 이전 검색어의 다음 찾기

Ng : 파일의 N번째 줄로 이동합니다.

h : 도움말 보기

q : 프로그램 종료

 

참고 사이트

http://1004lucifer.blogspot.com/2016/07/linux-vi-less_29.html

https://tychejin.tistory.com/94

https://jjeongil.tistory.com/1629

코드 예시들은 실 사용 코드에서 관련된 내용들만 추려 간략화한 예시들입니다. 

 

특정 테이블 A가 있다고 했을 때 해당 테이블의 칼럼 C2를 group의 기준으로 행들을 뽑아내야했는데 이때 대표로 뽑아지는 행은 각 그룹의 칼럼 C1의 최대값을 가지고있는 행이어야했다. 이 결과값을 다시 C1을 기준으로 오름차순으로 정렬하는 게 필요한 결과 값이었다.

 

먼저 생각한 로직은 해당 테이블의 행들을 칼럼 C1 내림차순으로 정렬을 하고 그 결과를 C2 칼럼으로 groupby를 한 후 그 결과를 다시 C1 오름차순으로 정렬을 해야되는 로직이었다. 

이를 querydsl로 아래와 같이 직관적으로 코드를 짰는데 짜고 실행해보니 제대로 값이 나오지 않았다.

        return jpaQueryFactory.selectFrom(A)
                .where(A.id.in(JPAExpressions.select(A.id)
                        .from(A)
                        .orderBy(A.C1.desc())
                        .groupBy(A.C2)
                ))
                .orderBy(A.C1.asc())
                .fetch();

이후에 orderby와 groupby를 같이 연달아쓰면 안될 것 같다는 생각이 들어 구글링을 하던 중 아래와 같은 쿼리를 발견했다.

select *
from(
    select *
    from(
        select *
        from A ud
        order by ud.C1 DESC
        LIMIT 18446744073709551615
        )as ordered
    group by ordered.C2 )as grouped
order by grouped.C1;

예전에 부트캠프에서 수업을 들을 때 프로젝트에서 위와 같이 limit를 활용해 orderby와 groupby를 썼던 기억이 있는데 그때와 같은 방법이었다. limit을 지우면 DBMS가 성능 향상을 위해 자동으로 orderby를 무시한다.

-> 성능에 안좋은 쿼리라는 말,,

 

문제는 그때는 작은 프로젝트라 LIMIT 18446744073709551615를 지정해도 상관없었지만 지금 진행 중인 프로젝트는 규모가 크고 실사용자들이 많이 들어온다. 게다가 해당 쿼리와 연관된 api가 앱 핵심 기능 사용에 있어 필수적인 아주 중요한 api였다.

A 테이블 역시 한 사용자당 데이터양이 끝없이 늘어날 수 있는 데이터의 규모가 테이블들 중 특히 더 사이즈가 큰 테이블이었다.

 

18446744073709551615라는 숫자가 충분히 큰 숫자인지, 내가 일을 하고 있을 동안엔 문제가 될 것 같지 않은데 해당 서비스가 과거를 돌이켜봤을 때 단순한게 몇 년동안 돌아가고 끝날 서비스가 아닌데 차후에라도 문제가 생긴다면 그건 돈을 받고 개발을 하는 입장에서 잘못된 일이라는 생각이 들었다. 

차후에 문제가 생기지않는다하더라도 제한이 없는 코드가 더 좋은 코드라는 생각이 들어 다시 구글링을 통해 아래와 같이 코드를 수정했다.

select A.*
from A inner join
(select max(ud.C1) as updateAt
from A ud
group by ud.C2
) b on A.C1 = b.updateAt
order by A.C1 ASC;

아마 코드를 보면 알겠지만 로직자체가 약간 다르다

원래는 orderby를 한 후 그 결과를 groupby를 하려고했는데 max를 통해 그룹에서 필요한 값을 뽑아냈다.

이 방법이 훨씬 더 깔끔하고 좋은 코드라는 생각이 들었고 성능측면에서도 훨씬 더 좋은 코드인 것 같다.

다른 사람들 코드를 많이 보는 게 중요하다는 걸 다시 한 번 느꼈다..

 

위 쿼리는 JPQL을 활용해 프로젝트에 적용했다. 위 코드대로 하면 오류가 발생해서 네이티브쿼리로 처리하면 된다는 글을 발견해 네이티브 쿼리로 처리 후 정상 작동 확인했다.

    @Query(value=" select A.*\n" +
            "from A inner join\n" +
            "(select\n" +
            "max(ud.C1) as updateAt\n" +
            "from A ud\n" +
            "group by ud.C2\n" +
            ") b on A.C1 = b.updateAt\n" +
            "order by A.C1 ASC",nativeQuery = true)
    List<A> findLatestDevices(@Param("userIdx") Long userId);

 

참고사이트 

https://okky.kr/article/237434

https://simp1e.tistory.com/entry/MYSQL-order-by-%ED%9B%84%EC%97%90-group-%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95

https://mingggu.tistory.com/115

https://stackoverflow.com/questions/54136211/spring-boot-jpa-how-to-find-entity-with-max-value

https://jaenjoy.tistory.com/39

    public UserDevice findLatestDevice (User user){
        return jpaQueryFactory.selectFrom(userDevice)
                .where(userDevice.id.eq(
                        JPAExpressions.select(userDevice.id.max())
                                .from(userDevice)
                                .where(userDevice.user.id.eq(user.getId()))
                                .where(userDevice.deviceCategory.name.eq("핸드폰"))
                ))
                .fetchFirst();
    }

인자로 받은 유저의 핸드폰이라는 이름의 유저기기 정보 테이블들의 행 중 가장 id 값이 큰 엔티티 반환

+ Recent posts