지난달 약 10개월가량의 여정이 끝마치고 배포가 마무리되었다.
운영을 하던 와중 CNS에서 하나의 메일이 왔는데 그게 이번 챕터의 주제 내용이다.
메일의 답변을 쓰기위해서 찾아본 내용과 다르게 알고 있던 부분에 대해서 작성하고자 한다.
어느 날 느닷없이 CNS모니터링 팀으로부터 날아온 메일. MySQL의 Sleep Thread를 죽여도 되는지 문의를 하는 문의글이었다.
우선 답변을 하기 위해서 당시 제공된 이미지를 어떻게 얻어와야 할지 당최 알지를 못했다...
Sleep Thread는 유휴커넥션을 의미할 텐데 왜? 내가 작성한 숫자보다 많은지 이해하기 어려웠다.
그때 당시 아래 작성된 예시보다 훨씬 더 많은 프로세스? 의 목록들이 존재했다.
MySQL을 사용하면서 한 번도 고민해 본 적 없고, 심지어 쿼리를 작성하여 날려본 적도 없다. 어떤 역할을 하는지 확인해 보자.
MYSQL의 show processList
프로세스 목록은 서버 내에서 실행 중인 스레드 집합에서 현재 수행 중인 작업을 나타냅니다. (MySQL 공식홈페이지)
information_schema와 performance_schema 둘 다 해당 테이블이 존재하나, performance_schema를 사용할 것을 권장한다.
information_schema는 다음 업데이트에 더 이상 사용하지 않는다고 한다.
MySQL의 InnoDB는 하나의 MySQL 프로세스 서버와 각각의 커넥션 스레드로 이루어져 있다.
다시 말해 하나하나의 모든 커넥션은 스레드라는 의미가 된다.
좋다 그렇다면 보통 우리가 연결하는 db connection의 유휴커넥션을 비롯한 최대 커넥션 등은 모두 스레드의 단위로 MySQL과 연결이 된다는 사실을 알게 되었다.
잘못된 추측
내가 작성한 Go Application은 유휴커넥션 3과 최고 커넥션 10을 직접 설정하였다.
심지어 공통으로 사용하는 커넥션의 코드작성을 내가 하였다.
내가 의도한 대로라면 Go Appliaction(쿠버네티스 환경에 있으므로 Pods가 된다.)이 기동 됨과 동시에 3개의 유휴커넥션을 애플리케이션에서 가지고 있어야 한다.
아래처럼 동작한다고 생각했다.
특정 DB를 바라보고 있으며, 모든 Go Application보다 내가 작성하지 않은 Java Application이 제일 먼저 의심 갔다.
현재 워크노드 A의 구조는
노드A
- User Pods (Go)
- Storage Pods (Go)
- etc.... Pods (Go)
- Auth Pods
위와 같이 구성되어 있었으며 내가 작성하지 않은 Auth Pods를 제일 먼저 의심했다.
다른 어떤 것도 설정하지 않고 Jpa를 사용하고 있으며 검색한 결과 Hikari를 사용하고 있다는 판단이 섰으며 connection pooling을 제공하는 JDBC DataSource의 구현체이다.
열심히 구글링과 소스코드를 확인한 결과 Default Idle 연결이 10, 최대 연결이 10이다.
(https://github.com/brettwooldridge/HikariCP/blob/dev/src/main/java/com/zaxxer/hikari/HikariConfig.java)
// https://github.com/brettwooldridge/HikariCP/blob/dev/src/main/java/com/zaxxer/hikari/HikariConfig.java
public final class HikariPool extends PoolBase implements HikariPoolMXBean, IBagStateListener
{
// 중략
private void validateNumerics(){
//중략
if (minIdle < 0 || minIdle > maxPoolSize) {
minIdle = maxPoolSize;
}
}
}
spring 부트를 이용하기 때문에 해당 hikari connection의 요소를 조정해 주었다.
spring.datasource.hikari.minimum-idle : 3
좀 더 확실한 결과를 위해 java_test라는 mysql 계정을 새로 생성하여 확인한 결과 아래와 같이 예상대로 동작하였다.
생각한 대로 정상작동하고 있다.
스프링부투의 설정값을 변경하니 예상한 대로 동작하고 있다.
모든 유휴커넥션에 대해서 올바르게 내가 예상한대로 동작하고 있다.
그러나 go_test라는 계정을 통해 생서한 계정의 커넥션이 위의 사진에는 존재하지 않는다.
뭐가 문제일까?
유휴 커넥션이란? 네트워크가 연결된 상태이지만 데이터의 전송이 없는 상태를 통상적으로 말한다.
왜 MySQL은 필요할까? 새로운 Connection을 가져가는 것보다 이미 연결된 유휴커넥션을 통해 DB 작업을 수행하는 것이 빠른 동작시간을 가지기 때문이다. 이로 인한 자원의 소모 또한 트레이드오프로 가져오고 있다.
내가 알고 있는 내용과 동일하다. 유휴커넥션이 설정됨이라 하면 애플리케이션에서 지속적으로 커넥션을 들고 있음을 의미해야 한다고 생각했다.
왜 내가 설정한 값이 정상동작하지 않는가. 설정 코드를 확인해 보자.
"데이터베이스.SetMaxIdleConns(3)"
뭔가 이름이 이상하다. 최소 연결개수도 아니고 최대 유휴커넥션 연결개수이다.
해당 소스코드를 확인해 보니
// SetMaxIdleConns sets the maximum number of connections in the idle connection pool.
최대 유휴 연결 개수의 풀을 설정한다고 한다.
세상에 정말 잘못된 방식으로 사용하고 있는 것이 아닌가. 코드는 정상작동 한 것이다.
Go Apllication에서 사용하는 커넥션이 없으니 모든 유휴커넥션을 날려버린 것이고
Java Application에서 사용하는 최소 유휴커넥션의 설정이 있으니 최소한의 커넥션을 유지하고 있는 것이다.
왜 위와 같은 견해차이가 발생하게 된 것일까?
유휴커넥션을 유지하는 비용보다 새로 연결하는 비용 즉 자원의 효율성을 위해 위와 같은 코드가 발생되고,
자바의 경우 성능의 중요성 때문에 위와 같은 설정값의 견해차이가 발생되었다고 본다.
그렇다면 유휴 커넥션의 살아있는 시간과 MySQL의 wait_time 시간이 다른경우 어떤 방식으로 동작될까? 라는 의문점이 남는다.
위 사진과 같이 유휴 대기시간 과 인터렉티브 시간 모두 60초로 설정하였다.
어플리케이션 상에는 2분(120초) 으로 설정하였다.
DB 스텟의 결과 60초가 지나면
아직 3개의 유휴 커넥션이 존재한다. ?
반면 mysql 서버 상의 유휴 커넥션은 ?
위의 사진과 동일하게 존재하지 않는다.
만약 새로운 쿼리 작업이 발생하게 된다면 ?
보이는 것처럼 새로운 스레드를 생성하게 된다.(ID46번)
아무리 어플리케이션 상의 설정을 하더라도 MYSQL 서버상의 설정값이 이와 같이 상이하다면 의도한대로 동작하지 않는다.
결론 :
Go에서 구현한 라이브러리와, Spring의 HikariCP는 다르게 동작하고 있다는 사실을 명확하게 인지하지 않는다면 이번과 같은 다양한 값들을 확인하면서 VOC를 처리해야한다.
각 라이브러리에서 설정하는 설정값들에 대해 조금더 기민하게 받아들이고 모두 동일한 개념을 사용하고 있지는 않다는 사실을 알게 되었다.
MySQL 상의 다양한 설정값이 존재하고, 어플리케이션의 설정값이 존재하여 상이하게 작동하는 방식이 생겨날수 있기 때문에 기존 설정값의 가능여부와 어플리케이션의 설정값에 대해 명확하게 인지하고 사용을 해야한다.
전체코드
package config
import (
"context"
"database/sql"
"fmt"
"gorm.io/driver/mysql"
"testing"
"time"
)
func TestIdleConn(t *testing.T) {
cfg := mysql.Config{
DSN: "test 하고자 하는 dsn",
}
db, err := sql.Open("mysql", cfg.DSN)
if err != nil {
panic(err)
}
db.SetMaxIdleConns(3)
db.SetConnMaxIdleTime(2 * time.Minute)
db.SetMaxOpenConns(10)
ch := make(chan bool)
done := make(chan bool)
go func() {
time.Sleep(5 * time.Second)
fmt.Println("run")
ch <- true
}()
query := func() {
ctx := context.Background()
var data string
rows := db.QueryRowContext(ctx, "Select name from tb_contest").Scan(&data)
if rows != nil {
panic(err)
}
fmt.Println(data)
}
for i := 0; i < 50; i++ {
go func() {
query()
}()
}
go func() {
for {
select {
case <-ch:
fmt.Println("hit")
for i := 0; i < 300; i++ {
if i == 77 {
query()
}
fmt.Printf("%+v\n", db.Stats())
time.Sleep(1 * time.Second)
fmt.Printf("cur time is %d \n", i+1)
}
done <- true
return
}
}
}()
<-done
}
'Go > Gorm 삽질기' 카테고리의 다른 글
Gorm BulkUpsert ,BulkInsert, BulkUpdate (0) | 2023.09.07 |
---|