반응형
Recent Posts
Recent Comments
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

Bbaktaeho

[Go/DB] Go언어에서 MYSQL connection 다루기 (MYSQL, GORM, Connection pool) 본문

프로그래밍 (Programming)/고랭 (Golang)

[Go/DB] Go언어에서 MYSQL connection 다루기 (MYSQL, GORM, Connection pool)

Bbaktaeho 2022. 8. 28. 22:07
반응형

들어가기 전에

Web 서비스에서 Database는 데이터 저장소뿐만 아니라 서비스 성능에도 중요한 부분입니다.

DML 쿼리 작성, Table 용량 및 Index등의 여러 요소가 있지만 이번 글에서 다룰 내용은 Connection입니다.

Connection을 Go 언어의 MYSQL Driver에서 어떻게 다루는지 살펴보고 Go언어에서 대표 ORM인 GORM에서 어떻게 Connection Pool을 관리하는지 알아보겠습니다.

Connection

먼저 Connection이란, Application과 Database Server와 상호 작용을 위한 연결을 유지하기 위해 생성되는 구조(객체)입니다.

Database Server와 연결을 맺고 끊음을 반복함으로써 Connection을 관리하지만, 일련의 맺고 끊음의 과정이 성능에 영향을 끼칩니다.

Database에도 일정한 Connection만 연결될 수 있도록 max값이 설정되어 있습니다.

따라서 효율적인 Connection 관리는 Web Application 성능과 안정성에 중요한 요인이 됩니다.

Connection Pool

여러 개의 Database와 연결을 유지한 여러 개의 Connection을 생성해서 Pool에 담아 꺼내 쓰는 방식을 뜻합니다.

앞서 말씀드린 Connection을 자주 맺고 끊음으로써 성능 저하가 발생하니 미리 여러 개의 Connection을 만들고 Pool에 담아 재활용하는 방식으로 성능을 개선한 것으로 보면 됩니다.

이것은 Database Server에서 제공하는 것이 아닌, 클라이언트 Driver에서 제공해주는 것으로 오해해선 안됩니다.

MYSQL Conneciton 설정

Web Application을 개발할 때 MYSQL(RDBMS)를 고려한다면 MYSQL의 Global Variables가 매우 유용할 것입니다.

Connection 튜닝에 대해서 더 많은 Global Variables가 있지만 대표적인 변수만 알아보겠습니다.

  • max_connections: MYSQL Server에서 Client 연결이 가능한 최대 수
  • max_user_connections: max_connections가 총 제한이라면, max_user_connections는 사용자 당 제한 수
  • max_connect_errors: 해당 값 만큼 Client가 연결을 실패하면 MYSQL Server와 연결을 차단
  • wait_timeout: 동작이 없는 연결된 Connection을 해제하기 위해 기다리는 시간 (sec)

테스트할 MYSQL Server을 다음과 같이 설정하겠습니다.

set global max_connections = 10
set global max_user_connections = 6

간단하게 connection 개수만 테스트

Go언어와 MYSQL 연동하기

간단한 Database 연동 코드를 작성하겠습니다.

import (
	"database/sql"

	_ "github.com/go-sql-driver/mysql"

	"log"
)

func main() {
	db, err := sql.Open("mysql", "root:9036@tcp(127.0.0.1:3306)/<스키마>") // 1
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("DB 연동: %+v\n", db.Stats()) // 2
	db.Close() // 3
	fmt.Printf("DB 연동 종료: %+v\n", db.Stats()) // 4
}
DB 연동: {MaxOpenConnections:0 OpenConnections:0 InUse:0 Idle:0 WaitCount:0 WaitDuration:0s MaxIdleClosed:0 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}
DB 연동 종료: {MaxOpenConnections:0 OpenConnections:0 InUse:0 Idle:0 WaitCount:0 WaitDuration:0s MaxIdleClosed:0 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}

기본적으로 MYSQL Driver 모듈을 import 해야 됩니다.

해당 Driver는 https://pkg.go.dev/github.com/go-sql-driver/mysql 에서 확인하실 수 있습니다.

 

1번 코드를 보면 Go의 기본 package인 database/sql을 사용합니다.

연동은 TCP 통신으로 Driver 이름과 DataSource를 입력받습니다.

database/sql/sql.go

연결이 완료되면 2번 코드에서 Database 상태를 조회할 수 있습니다.

3번 코드는 Database와 연동을 중단하고, 4번 코드는 중단된 후에 Database 상태를 조회합니다.

 

상태에 대한 구조체는 아래와 같습니다.

database/sql/sql.go

DBStats 구조체로 클라이언트로서 어떻게 커넥션을 관리하는지 확인할 수 있습니다.

Connection 생성 후 Query

import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql"

	"log"
)

func main() {
	db, err := sql.Open("mysql", "root:9036@tcp(127.0.0.1:3306)/<스키마>")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("DB 연동: %+v\n", db.Stats())

	conn, err := db.Query("SHOW GLOBAL VARIABLES LIKE 'max_%connect%'") // 1
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Connection 생성: %+v\n", db.Stats()) // 2

	for conn.Next() { // 3
		name := ""
		value := ""
		if err := conn.Scan(&name, &value); err != nil {
			fmt.Println(err)
		}
		fmt.Println(name, value)
	}

	conn.Close() // 4
	fmt.Printf("Connection 연결 종료: %+v\n", db.Stats())

	db.Close()
	fmt.Printf("DB 연동 종료: %+v\n", db.Stats())
}
DB 연동: {MaxOpenConnections:0 OpenConnections:0 InUse:0 Idle:0 WaitCount:0 WaitDuration:0s MaxIdleClosed:0 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}
Connection 생성: {MaxOpenConnections:0 OpenConnections:1 InUse:1 Idle:0 WaitCount:0 WaitDuration:0s MaxIdleClosed:0 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}
max_connect_errors 5
max_connections 10
max_user_connections 6
Connection 연결 종료: {MaxOpenConnections:0 OpenConnections:1 InUse:0 Idle:1 WaitCount:0 WaitDuration:0s MaxIdleClosed:0 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}
DB 연동 종료: {MaxOpenConnections:0 OpenConnections:0 InUse:0 Idle:0 WaitCount:0 WaitDuration:0s MaxIdleClosed:0 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}

1번 코드에서 Global Variables을 조회하는 Query를 실행했습니다.

2번의 결과를 보시면

  • OpenConnections: 1
  • InUse: 1

OpenConnections 값과 InUse 값이 1이 되었습니다.

새로운 Connection이 연결되어 1로 변경되었고, 현재 생성하고 반환하지 않은 Connection이 있어서 InUse 값이 1이 되었습니다.

4번 코드에선 Connection을 해제했는데 결과를 보시면

  • OpenConnections: 1 
  • InUse: 0 
  • Idle: 1

InUse가 0, Idle이 1이 되었고 OpenConnections은 그대로 1로 출력되었습니다.

다시 말해, MYSQL Server와 연결한 Connection이 아직 살아있다는 의미입니다.

DB 연동 종료 코드를 제거하고 main 함수가 종료되지 않도록 Sleep을 이용해서 MYSQL Console에서 확인해보겠습니다.

// db.Close()
// fmt.Printf("DB 연동 종료: %+v\n", db.Stats())

time.Sleep(time.Hour)

다시 실행하면

현재 연결된 Connection은 2가 됩니다. (한 개는 Console에 연결된 Connection)

여기서 Close() 함수를 통해 Connection을 해제했는데 Idle이 1 증가하고 OpenConnections가 0이 되지 않은 이유는 기본 package인 database/sql 에서 알 수 있었습니다.

database/sql/sql.go

기본 값으로 Connection Pool에 대기하는 Connection을 2개로 되어있었습니다.

Connection이 2개 생성되고 해제해도 OpenConnections가 사라지지 않게 됩니다.

 

Connection을 다룰 수 있는 기본기가 끝났습니다.
이제 여러 개의 Connection을 다루면서 Database Server의 Spec이 왜 중요한지 알아보겠습니다.

Connection Pool 다뤄보기

앞서 설명드린 기본 값 2를 3으로 늘려서 Connection을 다뤄보겠습니다.

import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql"

	"log"
)

func main() {
	db, err := sql.Open("mysql", "root:9036@tcp(127.0.0.1:3306)/<스키마>")
	if err != nil {
		log.Fatal(err)
	}

	db.SetMaxIdleConns(3) // 1
	fmt.Printf("DB 연동: %+v\n\n", db.Stats())

	conns := []*sql.Rows{}

	for i := 1; i <= 5; i++ { // 2
		c, err := db.Query("SHOW GLOBAL VARIABLES LIKE 'max_%connect%'")
		if err != nil {
			fmt.Printf("Connection(%v) 실패: %v\n", i, err)
			continue
		}
		conns = append(conns, c)

		fmt.Printf("Connection(%v) 생성: %+v\n", i, db.Stats())
	}

	fmt.Println()

	for i, c := range conns { // 3
		c.Close()
		fmt.Printf("Connection(%v) 해제: %+v\n", i+1, db.Stats())
	}

	fmt.Println()

	for i := 1; i <= 2; i++ { // 4
		c, err := db.Query("SHOW GLOBAL VARIABLES LIKE 'max_%connect%'")
		if err != nil {
			fmt.Printf("Connection(%v) 실패: %v\n", i, err)
			continue
		}
		conns = append(conns, c)

		fmt.Printf("Connection(%v) 생성: %+v\n", i, db.Stats())
	}

	db.Close()
	fmt.Printf("\nDB 연동 종료: %+v\n", db.Stats())
}
DB 연동: {MaxOpenConnections:0 OpenConnections:0 InUse:0 Idle:0 WaitCount:0 WaitDuration:0s MaxIdleClosed:0 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}

Connection(1) 생성: {MaxOpenConnections:0 OpenConnections:1 InUse:1 Idle:0 WaitCount:0 WaitDuration:0s MaxIdleClosed:0 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}
Connection(2) 생성: {MaxOpenConnections:0 OpenConnections:2 InUse:2 Idle:0 WaitCount:0 WaitDuration:0s MaxIdleClosed:0 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}
Connection(3) 생성: {MaxOpenConnections:0 OpenConnections:3 InUse:3 Idle:0 WaitCount:0 WaitDuration:0s MaxIdleClosed:0 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}
Connection(4) 생성: {MaxOpenConnections:0 OpenConnections:4 InUse:4 Idle:0 WaitCount:0 WaitDuration:0s MaxIdleClosed:0 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}
Connection(5) 생성: {MaxOpenConnections:0 OpenConnections:5 InUse:5 Idle:0 WaitCount:0 WaitDuration:0s MaxIdleClosed:0 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}

Connection(1) 해제: {MaxOpenConnections:0 OpenConnections:5 InUse:4 Idle:1 WaitCount:0 WaitDuration:0s MaxIdleClosed:0 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}
Connection(2) 해제: {MaxOpenConnections:0 OpenConnections:5 InUse:3 Idle:2 WaitCount:0 WaitDuration:0s MaxIdleClosed:0 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}
Connection(3) 해제: {MaxOpenConnections:0 OpenConnections:5 InUse:2 Idle:3 WaitCount:0 WaitDuration:0s MaxIdleClosed:0 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}
Connection(4) 해제: {MaxOpenConnections:0 OpenConnections:4 InUse:1 Idle:3 WaitCount:0 WaitDuration:0s MaxIdleClosed:1 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}
Connection(5) 해제: {MaxOpenConnections:0 OpenConnections:3 InUse:0 Idle:3 WaitCount:0 WaitDuration:0s MaxIdleClosed:2 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}

Connection(1) 생성: {MaxOpenConnections:0 OpenConnections:3 InUse:1 Idle:2 WaitCount:0 WaitDuration:0s MaxIdleClosed:2 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}
Connection(2) 생성: {MaxOpenConnections:0 OpenConnections:3 InUse:2 Idle:1 WaitCount:0 WaitDuration:0s MaxIdleClosed:2 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}

DB 연동 종료: {MaxOpenConnections:0 OpenConnections:2 InUse:2 Idle:0 WaitCount:0 WaitDuration:0s MaxIdleClosed:2 MaxIdleTimeClosed:0 MaxLifetimeClosed:0}

1번 코드에서 대기할 Connection 수를 3개로 세팅했습니다.

2번 코드에서 총 5개의 Connection을 생성했습니다.

3번 코드에서 5개의 Connection을 모두 해제했습니다.

결과를 보시면 1 ~ 3 Connection은 해제가 되면 Pool에 반환되었고, 4 ~ 5 Connection은 즉시 연결이 해제되었습니다.

4번 코드에서 2개의 Connection을 생성했는데 이때 Pool에 대기하고 있던 Connection이 재활용되어 OpenConnections가 증가하지 않았습니다.

다시 말해, SetMaxIdleConns() 함수를 통해서 Connection Pool의 재활용 개수를 조절할 수 있게 됩니다.

하지만 제한 없이 SetMaxIdleConns을 사용하면 Connection을 해제하지 않아서 문제가 발생할 수 있습니다.

그래서 Global Variables를 알아볼 필요가 있습니다.

 

먼저 Connection을 반환하지 않고 무수히 생성했을 때 발생하는 문제입니다.

 위 코드에서 2번 코드만 10개로 변경했습니다. 코드를 실행하면

max_user_connections 변수를 6으로 설정해뒀기 때문에 한 계정이 6개를 초과한 Connection을 생성해서 위와 같은 에러가 발생하게 됩니다.

Connection을 재활용하면 문제가 해결됩니다.

max_connections 변수도 마찬가지입니다.
여러 계정이 MYSQL Server와 연결하여 이용 중이라면 위와 같은 상황을 인지하고 설계해야 합니다.

추가로 database/sql 모듈에서 제공하는 SetMaxOpenConns 함수를 활용하면 위와 같은 max_user_connections, max_connections 에러를 미연에 방지할 수 있습니다.

db.SetMaxIdleConns(3)
db.SetMaxOpenConns(6)

위와 같이 SetMaxOpenConns(6) 으로 세팅하면 6개 이상으로 OpenConnections가 증가하지 않습니다.

따라서 Connection을 다루는 여러 함수와 DBStats를 통해 견고하게 DB와 상호작용할 수 있게 됩니다.

GORM Connection Pool 관리

사실, 위와 다를 게 없습니다.

GORM 라이브러리는 *gorm.DB에서 *sql.DB를 반환하는 함수인 DB()를 제공합니다.

import (
	"fmt"
	"log"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func main() {
	dsn := "root:9036@tcp(127.0.0.1:3306)/<스키마>"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}

	sqlDB, err := db.DB()
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", sqlDB.Stats())
}

database/sql package와 동일하기 때문에 설정도 동일합니다.

자세한 내용은 https://gorm.io/docs/generic_interface.html#Connection-Pool 에서 확인하실 수 있습니다.

마치며

Database를 사용하여 개발하기 전에 사용 중인 Database의 간단한 Spec을 알고 있는 것과 모르고 있는 것은 분명한 차이가 있습니다.

아키텍처를 설계할 때 DBA 또는 DevOps에게 요청사항이 명확해질 뿐만 아니라 예기치 못한 장애도 미리 대처할 수 있을 것입니다.
Database에서 Connection 관리는 극히 일부입니다. 저 또한 Database를 다양한 시점에서 많은 관심을 가져야 할 것 같습니다.

긴 글 읽어주셔서 감사합니다.

참고

https://yhjin.tistory.com/92

https://gorm.io/docs/index.html

반응형