5.0 Setup y configuracion de base de datos

Vamos a utilizar PostgreSQL. Es de código abierto, muy confiable y cuenta con algunas características modernas útiles, incluido el soporte para tipos de datos de array y JSON, búsqueda de texto completo y consultas geoespaciales. Utilizaremos algunas de estas características modernas de PostgreSQL a medida que avancemos en nuestra construcción.

En esta sección aprenderás:

  • Cómo instalar y configurar PostgreSQL en tu máquina local.
  • Cómo utilizar la herramienta interactiva psql para crear bases de datos, extensiones de PostgreSQL y cuentas de usuario.
  • Cómo inicializar un pool de conexiones a la base de datos en Go y configurar sus ajustes para mejorar el rendimiento y la estabilidad.

5.1 Setting up PostgreSQL

5.1 Intalando PostgreSQL

Si estás siguiendo, necesitarás instalar PostgreSQL en tu computadora en este punto. La documentación oficial de PostgreSQL contiene instrucciones completas de descarga e instalación para todos los tipos de sistemas operativos, pero si estás utilizando macOS, deberías poder instalar la última versión con:

$ brew install postgresql@15

O si estás utilizando una distribución de Linux, deberías poder instalar PostgreSQL a través de tu gestor de paquetes. Por ejemplo, si tu sistema operativo es compatible con el gestor de paquetes apt (como Debian y Ubuntu), puedes instalarlo con:

$ sudo apt install postgresql

En máquinas con Windows, puedes instalar PostgreSQL utilizando el administrador de paquetes Chocolatey con el siguiente comando:

> choco install postgresql
5.1 Conectando a la terminal interactiva de PostgreSQL

Cuando PostgreSQL se instaló, también debería haberse creado un ejecutable llamado psql en tu computadora. Este contiene una interfaz de línea de comandos para trabajar con PostgreSQL.

$ psql --version
psql (PostgreSQL) 15.4 (Ubuntu 15.4-1.pgdg22.04+1

Si no estás familiarizado con PostgreSQL, el proceso de conectarse a él por primera vez utilizando psql puede ser un poco confuso. Así que tomémonos un momento para recorrerlo.

Cuando PostgreSQL se instala recién, solo tiene una cuenta de usuario: un superusuario llamado postgres. En la primera instancia, necesitamos conectarnos a PostgreSQL como este superusuario para hacer cualquier cosa, y a partir de ahí podemos realizar cualquier paso de configuración que necesitemos, como crear una base de datos y crear otros usuarios.

Durante la instalación, también debería haberse creado un usuario del sistema operativo llamado postgres en tu máquina. En sistemas basados en Unix, puedes verificar tu archivo /etc/passwd para confirmar esto, de la siguiente manera:

$ cat /etc/passwd | grep 'postgres'
postgres:x:127:134:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash

Esto es importante porque, por defecto, PostgreSQL utiliza un esquema de autenticación llamado autenticación por pares (peer authentication) para cualquier conexión desde la máquina local. La autenticación por pares significa que si el nombre de usuario del usuario actual del sistema operativo coincide con un nombre de usuario válido de PostgreSQL, pueden iniciar sesión en PostgreSQL como ese usuario sin más autenticación. No se utilizan contraseñas en este proceso.

Por lo tanto, si cambiamos a nuestro usuario del sistema operativo llamado postgres, deberíamos poder conectarnos a PostgreSQL utilizando psql sin necesidad de ninguna autenticación adicional. De hecho, puedes realizar ambas acciones en un solo paso con el siguiente comando:

$ sudo -u postgres psql
psql (15.4 (Ubuntu 15.4-1.pgdg22.04+1))
Type "help" for help.
postgres=#

Entonces, solo para confirmar, lo que hemos hecho aquí es utilizar el comando sudo (superusuario) para ejecutar el comando psql como el usuario del sistema operativo llamado postgres. Esto abre una sesión en el terminal interactivo, donde estamos autenticados como el superusuario de PostgreSQL llamado postgres.

Si lo deseas, puedes confirmar esto ejecutando una consulta “SELECT current_user” para ver qué usuario de PostgreSQL eres actualmente:

postgres=# SELECT current_user;
current_user
--------------
postgres
(1 row)
5.1 Creando base de datos, usuarios y extensiones

Mientras estamos conectados como superuser postgres, vamos a crear una nueva base de datos para nuestro proyecto greenlight y luego nos conectamos usando el comando \c :

postgres=# CREATE DATABASE greenlight;
CREATE DATABASE
postgres=# \c greenlight
You are now connected to database "greenlight" as user "postgres".
greenlight=#

Pista: En PostgreSQL, el carácter \ indica un comando meta. Algunos otros comandos meta útiles son \l para listar todas las bases de datos, \dt para listar tablas y \du para listar usuarios. También puedes ejecutar ? para ver la lista completa de comandos meta disponibles.

La primera tarea es crear un nuevo usuario llamado “greenlight”, sin permisos de superusuario, que podamos utilizar para ejecutar migraciones SQL y conectarnos a la base de datos desde nuestra aplicación Go. Queremos configurar este nuevo usuario para utilizar la autenticación basada en contraseña en lugar de la autenticación por pares.

PostgreSQL también tiene el concepto de extensiones, que añaden características adicionales a la funcionalidad estándar. Puedes encontrar una lista de las extensiones que se incluyen con PostgreSQL aquí, y también hay algunas otras que puedes descargar por separado.

En este proyecto, vamos a utilizar la extensión citext. Esto añade un tipo de cadena de caracteres insensible a mayúsculas y minúsculas a PostgreSQL, que utilizaremos más adelante en el libro para almacenar direcciones de correo electrónico de usuarios. Es importante destacar que las extensiones solo pueden ser añadidas por superusuarios a una base de datos específica.

Adelante, ejecuta los siguientes comandos para crear un nuevo usuario llamado “greenlight” con una contraseña específica y añadir la extensión citext a nuestra base de datos:

greenlight=# CREATE ROLE greenlight WITH LOGIN PASSWORD 'pa55word';
CREATE ROLE
greenlight=# CREATE EXTENSION IF NOT EXISTS citext;
CREATE EXTENSION

Una vez que se haya realizado con éxito, puedes escribir exit o \q para cerrar la interfaz de línea de comandos basada en el terminal y volver a ser tu usuario normal del sistema operativo.

greenlight=# exit
5.1 Conectandonos como el nuevo usuario

Antes de continuar, probemos que todo está configurado correctamente y tratemos de conectarnos a la base de datos “greenlight” como el usuario “greenlight”. Cuando se te pida, ingresa la contraseña que configuraste en el paso anterior.

$ psql --host=localhost --dbname=greenlight --username=greenlight
Password for user greenlight:
psql (15.4 (Ubuntu 15.4-1.pgdg22.04+1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.
greenlight=> SELECT current_user;
current_user
--------------
greenlight
(1 row)
greenlight=> exit

¡Genial! Eso confirma que nuestra base de datos y el nuevo usuario “greenlight” con credenciales de contraseña están funcionando correctamente, y que podemos ejecutar declaraciones SQL como ese usuario sin problemas.

5.1 Informacion adicional
5.1 Optimizando configuraciones PostgreSQL

La configuración predeterminada con la que se envía PostgreSQL suele ser bastante conservadora, y a menudo puedes mejorar el rendimiento de tu base de datos ajustando los valores en tu archivo postgresql.conf.

Puedes verificar dónde se encuentra tu archivo postgresql.conf con la siguiente consulta SQL:

$ sudo -u postgres psql -c 'SHOW config_file;'
config_file
-----------------------------------------
/etc/postgresql/15/main/postgresql.conf
(1 row)

Este artículo proporciona una buena introducción a algunos de los ajustes más importantes de PostgreSQL y orientación sobre qué valores son razonables para empezar. Si estás interesado en optimizar PostgreSQL, te recomiendo que lo leas.

Alternativamente, puedes utilizar esta herramienta en línea para generar valores sugeridos basados en el hardware de tu sistema disponible. Una característica interesante de esta herramienta es que también produce declaraciones SQL de __ALTER SYSTEM __, que puedes ejecutar en tu base de datos para cambiar la configuración en lugar de modificar tu archivo __postgresql.conf __ manualmente.

5.2 Conectando a PostgreSQL

Bien, ahora que nuestra nueva base de datos “greenlight” está configurada, veamos cómo conectarnos a ella desde nuestra aplicación Go.

Puedes encontrar una lista de controladores disponibles para PostgreSQL en el wiki de Go, pero para nuestro proyecto optaremos por el paquete pq, que es popular, confiable y bien establecido.

$ go get github.com/lib/pq@v1
go: downloading github.com/lib/pq v1.10.9
go get: added github.com/lib/pq v1.10.9

Para conectarnos a la base de datos, también necesitaremos un nombre de origen de datos (DSN), que es básicamente una cadena que contiene los parámetros de conexión necesarios. El formato exacto del DSN dependerá del controlador de la base de datos que estés utilizando (y debería estar descrito en la documentación del controlador), pero al utilizar pq, deberías poder conectarte a tu base de datos local “greenlight” como el usuario “greenlight” con el siguiente DSN:

postgres://greenlight:pa55word@localhost/greenlight

5.2 Estableciendo pool de conexiones

Queremos que el DSN sea configurable en tiempo de ejecución, así que lo pasaremos a la aplicación utilizando una bandera de línea de comandos en lugar de codificarlo directamente. Para simplificar durante el desarrollo, utilizaremos el DSN anterior como valor predeterminado para la bandera.

En nuestro archivo cmd/api/main.go, crearemos una nueva función auxiliar llamada openDB(). En esta función auxiliar, utilizaremos la función sql.Open() para establecer un nuevo grupo de conexiones sql.DB. Luego, dado que las conexiones a la base de datos se establecen de forma perezosa según sea necesario la primera vez, también necesitaremos utilizar el método db.PingContext() para crear realmente una conexión y verificar que todo esté configurado correctamente.

Vamos a cmd/api/main.go

package main

import (
	"context"
	"database/sql"
	"flag"
	"fmt"
	"net/http"
	"os"
	"time"

	"log/slog"
	// Import the pq driver so that it can register itself with the database/sql
	// package. Note that we alias this import to the blank identifier, to stop the Go
	// compiler complaining that the package isn't being used.
	_ "github.com/lib/pq"
)

const version = "1.0.0"

// Add a db struct field to hold the configuration settings for our database connection
// pool. For now this only holds the DSN, which we will read in from a command-line flag
type config struct {
	port int
	env  string
	db   struct {
		dsn string
	}
}

type application struct {
	config config
	logger *slog.Logger
}

func main() {
	var cfg config

	flag.IntVar(&cfg.port, "port", 4000, "API server port")
	flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)")

	// Read the DSN value from the db-dsn command-line flag into the config struct. We
	// default to using our development DSN if no flag is provided.
	flag.StringVar(&cfg.db.dsn, "db-dsn", "postgres://greenlight:pa55word@localhost/greenlight", "PostgreSQL DSN")
	flag.Parse()

	logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

	// Call the openDB() helper function (see below) to create the connection pool,
	// passing in the config struct. If this returns an error, we log it and exit the
	// application immediately.
	db, err := openDB(cfg)
	if err != nil {
		logger.Error(err.Error())
		os.Exit(1)
	}

	// Defer a call to db.Close() so that the connection pool is closed before the
	// main() function exits.
	defer db.Close()

	app := application{
		config: cfg,
		logger: logger,
	}

	srv := &http.Server{

		Addr:         fmt.Sprintf(":%d", cfg.port),
		Handler:      app.routes(),
		IdleTimeout:  time.Minute,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
		ErrorLog:     slog.NewLogLogger(logger.Handler(), slog.LevelError),
	}

	logger.Info("starting server", "addr", srv.Addr, "env", cfg.env)

	err = srv.ListenAndServe()
	logger.Error(err.Error())
	os.Exit(1)
}

// The openDB() function returns a sql.DB connection pool.
func openDB(cfg config) (*sql.DB, error) {
	// Use sql.Open() to create an empty connection pool, using the DSN from the config
	// struct.
	db, err := sql.Open("postgres", cfg.db.dsn)
	if err != nil {
		return nil, err
	}
	// Create a context with a 5-second timeout deadline.
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	// Use PingContext() to establish a new connection to the database, passing in the
	// context we created above as a parameter. If the connection couldn't be
	// established successfully within the 5 second deadline, then this will return an
	// error.
	err = db.PingContext(ctx)
	if err != nil {
		return nil, err
	}
	// Return the sql.DB connection pool.
	return db, nil
}

Por ahora, es suficiente saber que si la llamada a PingContext() no puede completarse con éxito en 5 segundos, devolverá un error.

Una vez que el archivo cmd/api/main.go esté actualizado, continúa y ejecuta la aplicación nuevamente. Ahora deberías ver un mensaje de registro al inicio confirmando que el grupo de conexiones se ha establecido correctamente. Algo similar a esto:

Nota: Si recibes el mensaje de error “pq: SSL is not enabled on the server”, debes configurar tu DSN de la siguiente manera:

postgres://greenlight:pa55word@localhost/greenlight?sslmode=disable

5.2 Desacoplando el DSN

En este momento, el valor predeterminado de la bandera de línea de comandos para nuestro DSN está incluido explícitamente como una cadena en el archivo cmd/api/main.go. Aunque el nombre de usuario y la contraseña en el DSN son solo para la base de datos de desarrollo en tu máquina local, sería preferible no tener esta información codificada en nuestros archivos de proyecto (que podrían ser compartidos o distribuidos en el futuro).

Entonces, tomemos algunos pasos para desacoplar el DSN de nuestro código de proyecto y, en cambio, almacenarlo como una variable de entorno en tu máquina local. Si estás siguiendo, crea una nueva variable de entorno llamada GREENLIGHT_DB_DSN agregando la siguiente línea a tus archivos $HOME/.profile o $HOME/.bashrc:

export GREENLIGHT_DB_DSN='postgres://greenlight:pa55word@localhost/greenlight'

Una vez hecho esto, deberás reiniciar tu computadora o, si eso no es conveniente en este momento, ejecutar el comando source en el archivo que acabas de editar para aplicar el cambio. Por ejemplo:

source $HOME/.profile

Nota: Ejecutar source afectará solo la ventana de terminal actual. Entonces, si cambias a una ventana de terminal diferente, deberás ejecutarlo nuevamente para que surta efecto.

De cualquier manera, una vez que hayas reiniciado o ejecutado source, deberías poder ver el valor de la variable de entorno GREENLIGHT_DB_DSN en tu terminal ejecutando el comando echo. Algo así:

echo $GREENLIGHT_DB_DSN
postgres://greenlight:pa55word@localhost/greenligh

Si estas usando fish como shell tenes que :

nvim ~/.config/fish/config.fish

set -x GREENLIGHT_DB_DSN "tu_valor_aqui"

source ~/.config/fish/config.fish

Now let’s update our cmd/api/main.go file to access the environment variable using the os.Getenv() function, and set this as the default value for our DSN command-line flag.

package main
...
func main() {
	var cfg config

	flag.IntVar(&cfg.port, "port", 4000, "API server port")
	flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)")

	// Use the value of the GREENLIGHT_DB_DSN environment variable as the default value
	// for our db-dsn command-line flag.
	flag.StringVar(&cfg.db.dsn, "db-dsn", os.Getenv("GREENLIGHT_DB_DSN"), "PostgreSQL DSN")
	flag.Parse()
    ...
}

Si reinicias la aplicación nuevamente ahora, deberías encontrar que se compila correctamente y funciona como antes. También puedes intentar especificar la bandera -help al ejecutar la aplicación. Esto debería mostrar el texto descriptivo y los valores predeterminados de nuestras tres banderas de línea de comandos, incluido el DSN.

El valor se extraerá de la variable de entorno, algo así:

$ go run ./cmd/api -help
Usage of /tmp/go-build417842398/b001/exe/api:
-db-dsn string
PostgreSQL DSN (default "postgres://greenlight:pa55word@localhost/greenlight")
-env string
Environment (development|staging|production) (default "development")
-port int
API server port (default 4000)

5.2 Informacion adicional

5.2 Usando DSN con psql

Un efecto secundario interesante de almacenar el DSN en una variable de entorno es que puedes usarlo para conectarte fácilmente a la base de datos de greenlight como usuario greenlight, en lugar de especificar todas las opciones de conexión manualmente al ejecutar psql. Algo así:

$ psql $GREENLIGHT_DB_DSN
psql (15.4 (Ubuntu 15.4-1.pgdg22.04+1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.
greenlight=>

5.3 Configurando el pool de conexiones de la base de datos

¿cómo funciona el grupo de conexiones sql.DB?

Lo más importante que debes entender es que un grupo de conexiones sql.DB contiene dos tipos de conexiones: conexiones “en uso” y conexiones “inactivas”. Una conexión se marca como “en uso” cuando la estás utilizando para realizar una tarea de base de datos, como ejecutar una declaración SQL o consultar filas, y cuando la tarea está completa, la conexión se marca como “inactiva”.

Cuando instruyes a Go que realice una tarea de base de datos, primero verificará si hay alguna conexión inactiva disponible en el grupo. Si hay una disponible, Go reutilizará esta conexión existente y la marcará como “en uso” durante la duración de la tarea. Si no hay conexiones inactivas en el grupo cuando se necesita una, Go creará una nueva conexión adicional.

Cuando Go reutiliza una conexión inactiva del grupo, cualquier problema con la conexión se maneja de manera elegante. Las conexiones defectuosas se volverán a intentar automáticamente dos veces antes de rendirse, momento en el cual Go eliminará la conexión defectuosa del grupo y creará una nueva para llevar a cabo la tarea.

5.3 Configurando el pool

El grupo de conexiones tiene cuatro métodos que podemos usar para configurar su comportamiento. Vamos a hablar de cada uno de ellos uno por uno.

5.3 El metodo SetMaxOpenConns

El método SetMaxOpenConns() te permite establecer un límite superior (MaxOpenConns) en el número de conexiones ‘abiertas’ (conexiones en uso + conexiones inactivas) en el grupo. Por defecto, el número de conexiones abiertas es ilimitado.

Importante: Ten en cuenta que las conexiones ‘abiertas’ son iguales a las conexiones ‘en uso’ más las conexiones ‘inactivas’; no se refiere solo a las conexiones ‘en uso’.

Hablando en términos generales, cuanto más alto establezcas el límite MaxOpenConns, más consultas de base de datos se pueden realizar de manera concurrente y menor es el riesgo de que el grupo de conexiones en sí mismo sea un cuello de botella en tu aplicación.

Pero dejarlo ilimitado no es necesariamente lo mejor. Por defecto, PostgreSQL tiene un límite máximo de 100 conexiones abiertas y, si este límite máximo se alcanza bajo una carga intensa, provocará que nuestro controlador pq devuelva un error “lo siento, ya hay demasiados clientes”.

Nota: El límite máximo de conexiones abiertas se puede cambiar en el archivo postgresql.conf utilizando la configuración max_connections.

Para evitar este error, tiene sentido limitar el número de conexiones abiertas en nuestro grupo a un número considerablemente inferior a 100, dejando suficiente espacio para otras aplicaciones o sesiones que también necesiten utilizar PostgreSQL.

El otro beneficio de establecer un límite MaxOpenConns es que actúa como un tope muy rudimentario y evita que la base de datos sea abrumada por una gran cantidad de tareas simultáneas.

Pero establecer un límite viene con una advertencia importante. Si se alcanza el límite de MaxOpenConns y todas las conexiones están en uso, entonces cualquier tarea adicional de la base de datos se verá obligada a esperar hasta que se libere una conexión y se marque como inactiva. En el contexto de nuestra API, la solicitud HTTP del usuario podría “quedarse” indefinidamente mientras espera una conexión libre. Por lo tanto, para mitigar esto, es importante establecer siempre un límite de tiempo en las tareas de la base de datos mediante un objeto context.Context. Explicaremos cómo hacerlo más adelante.

5.3 El metodo SetMaxIdleConns

El método SetMaxIdleConns() establece un límite superior (MaxIdleConns) en el número de conexiones inactivas en el pool. De forma predeterminada, el número máximo de conexiones inactivas es 2. En teoría, permitir un mayor número de conexiones inactivas en el pool mejorará el rendimiento porque reduce la probabilidad de que se establezca una nueva conexión desde cero, ayudando así a ahorrar recursos.

Entonces, potencialmente, establecer MaxIdleConns demasiado alto puede resultar en más conexiones inutilizables y más recursos utilizados que si tuvieras un pool de conexiones inactivas más pequeño, con menos conexiones que se utilizan con más frecuencia. Como guía: solo debes mantener una conexión inactiva si es probable que la vuelvas a usar pronto.

Otra cosa a tener en cuenta es que el límite de MaxIdleConns siempre debe ser menor o igual a MaxOpenConns. Go hace cumplir esto y reducirá automáticamente el límite de MaxIdleConns si es necesario.

5.3 El metodo SetConnMaxLifetime

El método SetConnMaxLifetime() establece el límite de ConnMaxLifetime - la duración máxima de tiempo que una conexión puede ser reutilizada. Por defecto, no hay un tiempo máximo de vida y las conexiones se reutilizarán indefinidamente.

Si establecemos ConnMaxLifetime en una hora, por ejemplo, significa que todas las conexiones se marcarán como ‘caducadas’ una hora después de su creación inicial y no se pueden reutilizar después de que hayan caducado. Pero ten en cuenta:

  • Esto no garantiza que una conexión existirá en el pool durante toda una hora; es posible que una conexión se vuelva inutilizable por alguna razón y se cierre automáticamente antes de eso.
  • Una conexión aún puede estar en uso más de una hora después de ser creada, simplemente no puede comenzar a reutilizarse después de ese tiempo.
  • Esto no es un tiempo de espera inactivo. La conexión expirará una hora después de ser creada, no una hora después de que se volvió inactiva por última vez.
  • Una vez por segundo, Go ejecuta una operación de limpieza en segundo plano para eliminar las conexiones caducadas del pool.

En teoría, dejar ConnMaxLifetime ilimitado (o establecer una duración larga) mejorará el rendimiento porque hace menos probable que se necesiten crear nuevas conexiones desde cero. Pero en ciertas situaciones, puede ser útil imponer una duración más corta. Por ejemplo:

  • Si tu base de datos SQL impone un tiempo máximo de vida a las conexiones, tiene sentido establecer ConnMaxLifetime en un valor ligeramente más corto.
  • Para facilitar el cambio de bases de datos de manera adecuada detrás de un equilibrador de carga.

Si decides establecer un ConnMaxLifetime en tu grupo de conexiones, es importante tener en cuenta la frecuencia con la que las conexiones expirarán (y se recrearán). Por ejemplo, si tienes 100 conexiones abiertas en el grupo y un ConnMaxLifetime de 1 minuto, entonces tu aplicación potencialmente puede matar y recrear hasta 1.67 conexiones (en promedio) cada segundo. No quieres que la frecuencia sea tan alta que finalmente perjudique el rendimiento.

5.3 El metodo SetConnMaxIdleTime

El método SetConnMaxIdleTime() establece el límite ConnMaxIdleTime. Esto funciona de manera muy similar a ConnMaxLifetime, excepto que establece la duración máxima que una conexión puede estar inactiva antes de que se marque como caducada. Por defecto, no hay límite.

Si establecemos ConnMaxIdleTime en 1 hora, por ejemplo, cualquier conexión que haya estado inactiva en el grupo durante 1 hora desde su último uso se marcará como caducada y será eliminada por la operación de limpieza en segundo plano.

Esta configuración es realmente útil porque significa que podemos establecer un límite relativamente alto en el número de conexiones inactivas en el grupo, pero liberar periódicamente recursos eliminando cualquier conexión inactiva que sepamos que ya no se está utilizando.

5.3 Poniendolo en practica

Como regla general, deberías establecer explícitamente un valor para MaxOpenConns. Este valor debería estar cómodamente por debajo de cualquier límite estricto en el número de conexiones impuesto por tu base de datos e infraestructura. También puede ser útil mantenerlo bastante bajo para funcionar como un rudimentario regulador de velocidad.

Para este proyecto, estableceremos un límite de MaxOpenConns de 25 conexiones. He encontrado que este es un punto de partida razonable para aplicaciones web y APIs pequeñas a medianas. Sin embargo, lo ideal sería ajustar este valor según los resultados de pruebas de rendimiento y pruebas de carga, adaptándolo a las características específicas de tu hardware.

En general, valores más altos para MaxOpenConns y MaxIdleConns conducirán a un mejor rendimiento. Sin embargo, los beneficios disminuyen, y debes tener en cuenta que tener un conjunto de conexiones inactivas demasiado grande (con conexiones que no se reutilizan con frecuencia) puede conducir a un rendimiento reducido y a un consumo innecesario de recursos.

Dado que MaxIdleConns siempre debe ser menor o igual que MaxOpenConns, también limitaremos MaxIdleConns a 25 conexiones para este proyecto.

Para mitigar el riesgo mencionado en el punto 2 anterior, generalmente debes establecer un valor ConnMaxIdleTime para eliminar las conexiones inactivas que no se han utilizado durante mucho tiempo. En este proyecto, estableceremos una duración ConnMaxIdleTime de 15 minutos. Esto significa que las conexiones inactivas que han estado sin uso durante más de 15 minutos se cerrarán automáticamente para liberar recursos y mejorar la eficiencia del sistema.

Es probablemente aceptable dejar ConnMaxLifetime como ilimitado, a menos que tu base de datos imponga un límite estricto en la duración de la conexión, o lo necesites específicamente para facilitar algo como el intercambio de bases de datos de manera controlada. Ninguna de estas situaciones aplica en este proyecto, así que dejaremos esto con la configuración predeterminada de ilimitado.

5.3 Configurando el pool de conexiones

En lugar de codificar estos ajustes directamente, actualicemos el archivo cmd/api/main.go para que acepte estos valores como argumentos de línea de comandos.

La bandera de línea de comandos para el valor ConnMaxIdleTime es particularmente interesante, porque queremos que transmita una duración de tiempo, como 5s (5 segundos) o 10m (10 minutos). Para ayudar con esto, podemos utilizar la función flag.DurationVar() para leer el valor de la bandera de la línea de comandos, que lo convertirá automáticamente a un tipo time.Duration para nosotros.

Adelante, actualiza el archivo cmd/api/main.go de la siguiente manera:

  package main
  
  import (
  	"context"
  	"database/sql"
  	"flag"
  	"fmt"
  	"net/http"
  	"os"
  	"time"
  
  	"log/slog"
  
  	_ "github.com/lib/pq"
  )
  
  const version = "1.0.0"
  
+  // Add maxOpenConns, maxIdleConns and maxIdleTime fields to hold the configuration
+  // settings for the connection pool.
  type config struct {
  	port int
  	env  string
  	db   struct {
  		dsn          string
+  		maxOpenConns int
+  		maxIdleConns int
+  		maxIdleTime  time.Duration
  	}
  }
  
  type application struct {
  	config config
  	logger *slog.Logger
  }
  
  func main() {
  	var cfg config
  
  	flag.IntVar(&cfg.port, "port", 4000, "API server port")
  	flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)")
  
  	flag.StringVar(&cfg.db.dsn, "db-dsn", os.Getenv("GREENLIGHT_DB_DSN"), "PostgreSQL DSN")
  
+  	// Read the connection pool settings from command-line flags into the config struct.
+  	// Notice that the default values we're using are the ones we discussed above?
+  	flag.IntVar(&cfg.db.maxOpenConns, "db-max-open-conns", 25, "PostgreSQL max open connections")
+  	flag.IntVar(&cfg.db.maxIdleConns, "db-max-idle-conns", 25, "PostgreSQL max idle connections")
+  	flag.DurationVar(&cfg.db.maxIdleTime, "db-max-idle-time", 15*time.Minute, "PostgreSQL max connection idle time")
  
  	flag.Parse()
  
  	logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
  
  	db, err := openDB(cfg)
  	if err != nil {
  		logger.Error(err.Error())
  		os.Exit(1)
  	}
  
  	defer db.Close()
  
  	app := application{
  		config: cfg,
  		logger: logger,
  	}
  
  	srv := &http.Server{
  
  		Addr:         fmt.Sprintf(":%d", cfg.port),
  		Handler:      app.routes(),
  		IdleTimeout:  time.Minute,
  		ReadTimeout:  5 * time.Second,
  		WriteTimeout: 10 * time.Second,
  		ErrorLog:     slog.NewLogLogger(logger.Handler(), slog.LevelError),
  	}
  
  	logger.Info("starting server", "addr", srv.Addr, "env", cfg.env)
  
  	err = srv.ListenAndServe()
  	logger.Error(err.Error())
  	os.Exit(1)
  }
  
  // The openDB() function returns a sql.DB connection pool.
  func openDB(cfg config) (*sql.DB, error) {
  
  	db, err := sql.Open("postgres", cfg.db.dsn)
  	if err != nil {
  		return nil, err
  	}
  
+  	// Set the maximum number of open (in-use + idle) connections in the pool. Note that
+  	// passing a value less than or equal to 0 will mean there is no limit.
+  	db.SetMaxOpenConns(cfg.db.maxOpenConns)
+  	// Set the maximum number of idle connections in the pool. Again, passing a value
+  	// less than or equal to 0 will mean there is no limit.
+  	db.SetMaxIdleConns(cfg.db.maxIdleConns)
+  	// Set the maximum idle timeout for connections in the pool. Passing a duration less
+  	// than or equal to 0 will mean that connections are not closed due to their idle time.
+  	db.SetConnMaxIdleTime(cfg.db.maxIdleTime)
  
  	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  	defer cancel()
  
  	err = db.PingContext(ctx)
  	if err != nil {
  		return nil, err
  	}
  	// Return the sql.DB connection pool.
  	return db, nil
  }

Si continúas y ejecutas la aplicación nuevamente ahora, todo debería seguir funcionando correctamente. Para la bandera db-max-idle-time, puedes proporcionar cualquier valor aceptable por la función time.ParseDuration(), como 300ms (300 milisegundos), 5s (5 segundos) o 2h45m (2 horas y 45 minutos). Las unidades de tiempo válidas son ns, us (o µs), ms, s, m y h. Por ejemplo:

$ go run ./cmd/api -db-max-open-conns=50 -db-max-idle-conns=50 -db-max-idle-time=2h30m

En este punto, es posible que no notes cambios evidentes en la aplicación, y no hay mucho que podamos hacer para demostrar el impacto de estas configuraciones. Sin embargo, más adelante en estos post, realizaremos algunas pruebas de carga y explicaremos cómo monitorear el estado del grupo de conexiones en tiempo real utilizando el método db.Stats(). En ese momento, podrás observar parte del comportamiento que hemos discutido en este capítulo en acción.

Post Relacionados