TensorFlow 2.0 y redes neuronales

tf_logo

Buenas, hoy vamos a explicar un poco en esta entrada como funciona el framework que nos ofrece herramientas para programar tareas de aprendizaje automático, crear redes neuronales, deeplearning, deep reinforcement learning, etc. En definitiva lo que suelen denominarse hoy inteligencias artificiales, se llama TensorFlow y va por la versión 2.3

TensorFlow es el sistema de aprendizaje automático de segunda generación de Google Brain, liberado como software libre en noviembre de 2015. El nombre TensorFlow deriva de las operaciones que tales redes neuronales realizan sobre arrays multidimensionales de datos. Estos arrays multidimensionales son llamados tensores

Unidad de procesamiento del tensor (TPU)

En mayo de 2016 Google anunció su unidad de procesamiento del tensor (TPU), una construcción ASIC personalizada específicamente para aprendizaje automático y adaptada para TensorFlow. TPU es un acelerador de IA programable diseñado para proporcionar alta eficiencia aritmética de precisión baja, y orientado a utilizar modelos más que para entrenarlos

TensorFlow

La API de TensorFlow está disponible en varios lenguajes, nosotros usaremos la de NodeJS para el ejemplo de red neuronal artificial que construiremos con el conjunto de datos MNIST

// Cargamos los módulos necesarios
const tfnode = require('@tensorflow/tfjs-node-gpu'),
    fs = require('fs'),
    zlib = require('zlib');

(async () => {
// Leemos el conjunto de datos (dataset) y preparamos los datos para la entrada
    const features = (new Uint8Array(zlib.gunzipSync(fs.readFileSync(__dirname + '/mnist/train-images-idx3-ubyte.gz')))).slice(16),
        labels = (new Uint8Array(zlib.gunzipSync(fs.readFileSync(__dirname + '/mnist/train-labels-idx1-ubyte.gz')))).slice(8),
        featuresTest = (new Uint8Array(zlib.gunzipSync(fs.readFileSync(__dirname + '/mnist/t10k-images-idx3-ubyte.gz')))).slice(16),
        labelsTest = (new Uint8Array(zlib.gunzipSync(fs.readFileSync(__dirname + '/mnist/t10k-labels-idx1-ubyte.gz')))).slice(8),
        feats = tfnode.data.array((new Float32Array(features)).map(f => f / 255.0)).batch(784),
        lbls = tfnode.data.array(labels).batch(1),
        featTests = tfnode.data.array((new Float32Array(featuresTest)).map(f => f / 255.0)).batch(784),
        lblTests = tfnode.data.array(labelsTest).batch(1)
// Instanciación del modelo
    const model = tfnode.sequential()
// Se añaden las capas de la red neuronal
    model.add(tfnode.layers.dense({ units: 128, activation: 'relu', inputShape: [784] }))
    model.add(tfnode.layers.dropout(0.2))
    model.add(tfnode.layers.dense({ units: 10, activation: 'softmax' }))
// Compilación del modelo
    model.compile({ optimizer: 'adam', loss: 'sparseCategoricalCrossentropy', metrics: ['acc'] })
    model.summary()
    console.log(`Training dataset with ${feats.size} features and ${lbls.size} labels...`)
// Fase de entrenamiento del modelo
    const dataTrain = tfnode.data.zip({ xs: feats, ys: lbls }).batch(1),
        testTrain = tfnode.data.zip({ xs: featTests, ys: lblTests }).batch(1),
        history = await model.fitDataset(dataTrain, {
            epochs: 5,
            validationData: testTrain,
            callbacks: {
                onTrainBegin: logs => console.log('Start Training...'),
                onTrainEnd: logs => console.log('Stop Training'),
                onEpochBegin: (epoch, logs) => console.log('Begin epoch: ' + epoch),
                onEpochEnd: (epoch, logs) => console.log('End epoch: ' + epoch + '\n- Loss: ' + logs.loss + '\n- Accuracy: ' + logs.acc)
            }
        }).catch(err => console.error(err))
// Se muestran los valores de salida
    Object.keys(history.history).map(key => console.log(`${key}=${history.history[key]}`))
})()

Necesitamos instalar las dependencias del módulo @tensorflow/tfjs-node-gpu para que funcione, o el módulo @tensorflow/tfjs-node en caso de no usar GPU

En el ejemplo anterior estamos leyendo el dataset y creando una red neuronal artificial con 128 neuronas, la capa de dropout permite desactivar un porcentaje de las neuronas durante el entrenamiento en nuestro caso el 20%, esto es necesario para no tener resultados por sobreentrenamiento. Finalmente tenemos 10 unidades en la capa de salida por lo que el resultado será un tensor de 10 elementos que indica la probabilidad de que la imagen sea correcta. Hacemos 5 pasadas de entrenamiento y validamos los resultados

La salida por lo tanto muestra el resumen del modelo y el entrenamiento, dando los valores de pérdida y acierto en cada entrenamiento y al final el total

_________________________________________________________________
Layer (type)                 Output shape              Param #   
=================================================================
dense_Dense1 (Dense)         [null,128]                100480    
_________________________________________________________________
dropout_Dropout1 (Dropout)   [null,128]                0         
_________________________________________________________________
dense_Dense2 (Dense)         [null,10]                 1290      
=================================================================
Total params: 101770
Trainable params: 101770
Non-trainable params: 0
_________________________________________________________________
Training dataset with 60000 features and 60000 labels...
Start Training...
Epoch 1 / 5
Begin epoch: 0
eta=863.4 ==>-------------------------------- acc=1.00 loss=0.102 

Una vez entrenado el modelo podremos pasarle imágenes para detectar la coincidencia en la probabilidad de los resultados

// Adaptamos las imágenes a la entrada del modelo y se crea el tensor de entrada
    fs.createReadStream('/images.json.gz').pipe(zlib.createGunzip()).on('data', async data => {
        const imagen = Float32Array.from(await sharp(Buffer.from(data.image, 'base64')).resize(28, 28).greyscale().raw().toBuffer().catch(err => next(err))),
            buffer = tfnode.buffer([1, 784], 'float32', imagen.map(f => f / 255.0))
        model.predict(buffer.toTensor()).print()
    })
        .on('end', () => {
            console.log('All images predicted.')
        })

Tensor
     [[0, 0, 0, 0, 0, 0.0427372, 0, 0.9571278, 0, 0.0001349],]
Tensor
     [[0.0002897, 0, 0.209211, 0.0000031, 0, 0.0015369, 0.7871032, 0.001465, 0.0003912, 0],]
Tensor
     [[0.0000626, 0, 0.4744948, 0.0397445, 0, 0.0002614, 0, 0.4854368, 0, 0],]
Tensor
     [[0.0000023, 0, 0.0093701, 0.1584364, 0, 0.3968244, 0.0428764, 0.3924459, 0.0000444, 2e-7],]
Tensor
     [[0.0000071, 0, 0.0316687, 0.0000178, 0, 0.0000988, 0.9680935, 0.0001098, 0.0000045, 0],]
...

Esperamos que os haya gustado, nos leemos en la próxima haciendo redes neuronales de convolución 🤖

Integración Continua: Gitea + Drone + SonarQube

¿Qué es y para que se usa la integración continua?

Es una práctica de ingeniería de software que consiste en hacer integraciones automáticas de un proyecto lo más a menudo posible para así poder detectar fallos cuanto antes. Entendemos por integración la compilación y ejecución de pruebas de todo un proyecto.

Gitea + Drone + SonarQube

Se utiliza el software libre de Gitea para almacenar los repositorios de código y que conectaremos a Drone para realizar la IC

Drone permite ejecutar las pipelines que tiene configuradas el repositorio, cuenta con mucha variedad de plugins dando posibilidad de despliegues o comunicaciones

SonarQube provee de métricas y analiza la calidad y seguridad del código

Con este stack podremos montar un ciclo de IC que cubre las necesidades básicas, también destaca los pocos recursos que necesita esta configuración pudiendo desplegarse en máquinas pequeñas como una Raspberry

Docker

Usamos Docker para desplegar las aplicaciones en distintos contenedores

version: '2'

services:
  gitea:
    image: gitea/gitea
    restart: always
    container_name: gitea
    hostname: gitea
    volumes:
      - ./data:/data
    expose:
      - 3000
    depends_on:
      - db
    networks:
      mynet:

  db:
    image: mariadb:10
    container_name: gitea-db
    hostname: gitea-db
    restart: always
    environment:
      - MYSQL_ROOT_PASSWORD=secretdb
      - MYSQL_DATABASE=gitea
      - MYSQL_USER=gitea
      - MYSQL_PASSWORD=secretpass
    volumes:
      - ./db/:/var/lib/mysql
    networks:
      mynet:

  drone:
    image: drone/drone
    restart: always
    container_name: drone
    hostname: drone
    environment:
      - DRONE_GITEA_SERVER=https://gitea.domain.com
      - DRONE_GITEA_CLIENT_ID=secretid
      - DRONE_GITEA_CLIENT_SECRET=secret
      - DRONE_RPC_SECRET=secret-rpc
      - DRONE_SERVER_HOST=drone.domain.com
      - DRONE_SERVER_PROTO=https
    volumes:
      - ./drone:/data
    expose:
      - 80
    networks:
      mynet:

  runner:
    image: drone/drone-runner-docker
    restart: always
    container_name: runner
    hostname: runner
    environment:
      - DRONE_RPC_PROTO=http
      - DRONE_RPC_HOST=drone
      - DRONE_RPC_SECRET=secret-rpc
      - DRONE_RUNNER_CAPACITY=2
      - DRONE_RUNNER_NAME=runner
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    expose:
      - 3000
    networks:
      mynet:

  sonarqube:
    image: sonarqube
    container_name: sonarqube
    hostname: sonarqube
    expose:
      - 9000
    volumes:
      - ./sonar/conf:/opt/sonarqube/conf
      - ./sonar/data:/opt/sonarqube/data
      - ./sonar/logs:/opt/sonarqube/logs
      - ./sonar/extensions:/opt/sonarqube/extensions
    networks:
      mynet:

networks:
  mynet:

Solo hay que crear la app en Gitea para conectarla a Drone y generar las claves de la configuración del runner de Drone. También habría que generar la token del plugin de sonar para cada proyecto de Drone que configuremos

Con esta receta de docker-compose se puede poner un proxy que ataque los distintos contenedores bajo el mismo dominio.com

Eso es todo, nos vemos en la próxima

Dockerizando una Raspberry Pi 4

En esta entrada vamos a instalar y preparar un ordenador Raspberry Pi 4 con la distribución Buster Debian GNU/Linux y arquitectura arm64 (aarch64) aprovechando al máximo el procesador que lleva de serie

Necesitamos la imagen de RaspiOS oficial con el software arm64 instalado, también necesitaremos una tarjeta microSD para grabarla, esto podemos hacerlo con un solo comando desde un terminal

$ curl -s https://downloads.raspberrypi.org/raspios_arm64/images/raspios_arm64-2020-05-28/2020-05-27-raspios-buster-arm64.zip | gzip -d - | sudo dd if=/dev/stdin of=/dev/sdX status=progress

Donde el dispositivo /dev/sdX será el de la memoria microSD

Una vez termina el comando tendremos la imagen grabada en la tarjeta, la conectamos a la Raspberry Pi 4 y arrancará el sistema por defecto

Abrimos un terminal desde la Raspberry y ejecutamos los siguientes comandos

$ echo 'deb https://download.docker.com/linux/debian buster stable' | sudo tee /etc/apt/sources.list.d/docker.list
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo apt update
$ sudo apt install -y docker-ce

Cuando termine de instalar se necesita reiniciar el sistema para que todo funcione correctamente, y ya tenemos docker en nuestro ordenador arm64

$ docker -v
Docker version 19.03.12, build 48a6621

Kernel Version: 5.4.51-v8+
Operating System: Debian GNU/Linux 10 (buster)
OSType: linux
Architecture: aarch64
CPUs: 4
Total Memory: 7.633GiB
Name: raspberrypi

Hasta la próxima…

Subir vídeos al reproductor IPFS

IPFS logo

Hola, en esta entrada explicaremos como adaptar nuestros vídeos para subirlos a la red IPFS desde nuestra página de Upload y poder visualizarlos con el reproductor IPFS, utilizaremos el software ffmpeg que suele venir disponible en los repositorios de la distribución que estemos usando

Si tenemos por ejemplo un archivo mp4 y queremos adaptarlo al formato HLS con el siguiente comando:

ffmpeg -i video.mp4 -y -acodec aac -vcodec copy -hls_time 60 -hls_list_size 0 -f hls video.m3u8

Y de esta forma trozeamos el archivo mp4 en partes de 60 segundos con formato hls

Una vez termine la transcodificación del archivo vamos a la página de Upload y subimos el directorio generado

Cuando termine de subirse nos devuelve un hash de la carpeta que contiene el vídeo en formato HLS, este hash es el que tenemos que pasarle al reproductor IPFS

Y con esto ya tenemos nuestro vídeo en la red IPFS disponible para verlo desde el reproductor cuando nos apetezca, advertir que se tenga cuidado con los contenidos subidos ya que no pueden borrarse de la red.

Saludos

Docker con IPv6

docker6

Aquí vamos a explicar como habilitar el protocolo IPv6 en Docker, esto permitirá comunicar nuestros contenedores con el mundo exterior por este protocolo aparte del conocido IPv4, funciona solamente en GNU/Linux

Para esto hay que tener configurado en el servidor anfitrión IPv6 en la interfaz WAN (la que llega a internet), en algunos proveedores puede venir activado por defecto pero no es la norma, para probar si tenemos IPv6 en el anfitrión haremos un ping6

$ ping6 2600::

Si responde correctamente las solicitudes ICMPv6 tendremos IPv6 en la máquina, sino hay que negociar con el proveedor como habilitarlo

En el archivo /etc/docker/daemon.json (si no existe lo creamos) hay que añadir lo siguiente como se indica en la documentación de Docker

{
  "ipv6": true,
  "fixed-cidr-v6": "2001:db8:1::/80"
}

El parámetro fixed-cidr-v6 indica la subred interna que usarán los contenedores, es decir, la subred de la interfaz bridge que normalmente identificamos con docker0, se configurará con la primera dirección de la red disponible

Se elige la máscara de red /80 que es menor que la que hay en la interfaz externa /64 para evitar problemas en el enrutado de los paquetes IPv6, lo recomendable es usar una máscara con un rango menor, si ponemos una máscara /64 o del mismo valor que la externa se tiene que usar algún demonio de enrutado como ndppd

Una vez se reinicia Docker puede verse configurada la interfaz interna de red bridge con IPv6 habilitado y la consiguiente red asignada con el comando

$ docker network inspect bridge

Para usar IPv6 en los contenedores habrá que enrutar el tráfico de vuelta hacia la interfaz externa haciendo masquerading con el comando ip6tables

$ ip6tables -t nat -A POSTROUTING -s 2001:db8:1::/80 -j MASQUERADE

Además hay que configurar el sysctl de la máquina para preparar el kernel con un entorno IPv6, añadimos las siguientes líneas al archivo /etc/sysctl.d/99-sysctl.conf modificando la interfaz de red de salida por la correspondiente (hay que reiniciar la máquina)

net.ipv6.conf.default.accept_ra=0
net.ipv6.conf.all.accept_ra=0
net.ipv6.conf.default.forwarding=1
net.ipv6.conf.all.forwarding=1
net.ipv6.bindv6only=0
net.ipv6.conf.default.disable_ipv6=0
net.ipv6.conf.all.disable_ipv6=0
net.ipv6.conf.default.proxy_ndp=0
net.ipv6.conf.all.proxy_ndp=0
net.ipv6.conf.ensXXX.proxy_ndp=1
net.ipv6.conf.ensXXX.accept_ra=2
net.ipv6.conf.ensXXX.forwarding=0

Solo queda instanciar un contenedor y comprobar que tenemos ping6 desde él

$ docker run -ti --rm debian:buster ping6 -c10 2600::
PING 2600::(2600::) 56 data bytes
64 bytes from 2600::: icmp_seq=1 ttl=52 time=140 ms
64 bytes from 2600::: icmp_seq=2 ttl=52 time=140 ms
64 bytes from 2600::: icmp_seq=3 ttl=52 time=140 ms
64 bytes from 2600::: icmp_seq=4 ttl=52 time=140 ms
64 bytes from 2600::: icmp_seq=5 ttl=52 time=140 ms
64 bytes from 2600::: icmp_seq=6 ttl=52 time=140 ms
64 bytes from 2600::: icmp_seq=7 ttl=52 time=141 ms
64 bytes from 2600::: icmp_seq=8 ttl=52 time=140 ms
64 bytes from 2600::: icmp_seq=9 ttl=52 time=140 ms
64 bytes from 2600::: icmp_seq=10 ttl=52 time=140 ms

--- 2600:: ping statistics ---
10 packets transmitted, 10 received, 0% packet loss, time 20ms
rtt min/avg/max/mdev = 139.906/140.204/141.457/0.475 ms

Y colorín colorado este post se ha terminado, esperamos os haya gustado, nos vemos en la próxima

Usa Virt-Manager NO VirtualBox

logo-virt-manager

Desde que se lanzó la versión 10 de Debian GNU/Linux (Buster) en su página nos recomiendan dejar de usar VirtualBox por la aplicación de escritorio Virt-Manager

apt install virt-manager

Ya conocemos desde hace años VirtualBox de Oracle para trabajar con máquinas virtuales, es OpenSource aunque el producto está controlado por la misma empresa. Como alternativa ahora se utiliza Virt-Manager que está basado en KVM y libvirt, aunque también puede gestionar Xen o LXC directamente

Como podéis ver el entorno nos recuerda mucho a otras aplicaciones de virtualización como VMware o VirtualBox, al crear una máquina virtual nos da a elegir el tipo de instalación que queremos realizar

Una vez configurados los parámetros del menú wizard, se generan los recursos necesarios para arrancar la máquina virtual, esto podemos realizarlo también mediante comandos pero la GUI nos facilita mucho la tarea

También hay que configurar las interfaces de red que se asignarán a las máquinas virtuales, aunque la aplicación lo hace todo a golpe de ratón con permisos de root

Como se puede ver el menú nos recuerda mucho a otras aplicaciones de virtualización, aunque difieren muchas de las configuraciones, por ejemplo no hay límites a la hora de asignar los núcleos del procesador a una máquina virtual, pero la aplicación nos avisa que puede incurrir en un bajo rendimiento

Os animo a probarlo y utilizarlo como alternativa a otras aplicaciones de virtualización tanto comerciales como libres y comprobar la potencia que nos ofrece la virtualización del Kernel de GNU/Linux, como ya recomendó hace años fanta en su blog, saludos

Haraka Wildduck

duck

En esta entrada vamos a completar el taller que dimos hace poco de como montar un servidor de correo electrónico completo y autogestionado en NodeJS

Tenemos que bajar el repositorio git que montamos para desplegar la aplicación en contenedores Docker por lo que necesitaremos tener instalado docker y docker-compose

Una vez descargado, generamos los certificados seguros para la aplicación en la carpeta secure con el script start.sh también hay que modificar el dominio sobre el que vamos a tener el correo en el archivo .env, además el DNS del dominio tiene que tener un registro MX apuntando a la máquina donde se despliegue el servidor

Arrancamos los servicios con el siguiente comando

docker-compose up -d

Si queremos mantener la persistencia deberemos copiar las base de datos

docker cp mongo:/data/db ./mongodb && chown -R 999.999 ./mongodb
 docker cp redis:/data ./redis && chown -R 999.999 ./redis

Descomentar las lineas del archivo docker-compose.yml

docker-compose down && docker-compose up -d

Y tendremos un servidor de correo en NodeJS nativo funcionando bajo nuestro control, saludos

Montar una instancia Nextcloud en Docker

nextcloud

En esta entrada vamos a explicar brevemente como montar una instancia de Nextcloud en Docker, Nextcloud es un potente servicio de software libre de cloud (nube), sirve para almacenar, sincronizar y compartir archivos principalmente aunque con sus plugins se pueden añadir muchas funcionalidades, reemplaza al viejo proyecto ownCloud.

Para ello utilizaremos la imagen oficial de Nextcloud que podemos encontrar en el hub de Docker, esto nos facilitará el despliegue y es más fácil que mantener una imagen propia, podemos encontrar distintas versiones de la imagen aunque nos basaremos en la última latest, además contamos con las últimas actualizaciones de seguridad.

Usaremos el programa docker-compose para el despliegue de la instancia, con el que configuramos los contenedores necesarios con nuestras opciones, en vez de hacerlo directamente desde la línea de comandos.

version: '2'

services:
  nextcloud:
    image: nextcloud
    restart: always
    container_name: nextcloud
    hostname: nextcloud
    environment:
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
      - MYSQL_PASSWORD=xxx
      - MYSQL_HOST=mariadb-nextcloud
    depends_on:
      - mariadb-nextcloud
    expose:
      - 80
    links:
      - mariadb-nextcloud
    volumes:
      - ./nextcloud/html/custom_apps:/var/www/html/custom_apps
      - ./nextcloud/html/config:/var/www/html/config
      - ./nextcloud/html/data:/var/www/html/data
      - ./nextcloud/html/themes:/var/www/html/themes
    networks:
      - cloudnet:

  mariadb-nextcloud:
    image: mariadb
    command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
    restart: always
    container_name: mariadb-nextcloud
    hostname: mariadb-nextcloud
    volumes:
      - ./nextcloud/mysql:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=yyy
      - MYSQL_PASSWORD=xxx
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
    networks:
      - cloudnet:

networks:
  cloudnet:

Teniendo este archivo en una carpeta, con el comando docker-compose up -d nos bajaremos la última versión de las imágenes de Nextcloud y MariaDB desde el hub de Docker, y seguidamente se instanciarán ambos contenedores. Hemos preferido compartir los volúmenes de los contenedores con el sistema de archivos local para mantener la persistencia por si se elimina el contenedor, y configurar las imágenes con las variables de entorno necesarias para el correcto funcionamiento del servicio.

También hemos relacionado los contenedores (links) y expuesto el puerto 80/tcp, ya que nuestro servicio está detrás de un proxy nginx, del contenedor Nextcloud para utilizarlo aunque pueden incluirse los que se necesiten.

Una vez se levanten los contenedores podremos acceder al servicio por el puerto correspondiente y comenzar a configurar la instancia con las opciones que necesitemos.

Esperamos que esta entrada os haya gustado y os sirva para futuros despliegues, saludos

Tuneando PostgreSQL

PostgreSQL

Después de una larga ausencia en el blog, vamos a explicar una configuración para la base de datos PostgreSQL que pueda soportar altas cargas y aumentar la disponibilidad del servicio, comentaremos sus opciones y justificaremos los valores que decidimos aplicar en función de los recursos de la máquina.

Nuestra máquina es una servidor cloud que tiene 4 núcleos y 8G de memoria RAM, la máquina actualmente está compartida con contenedores docker de otros servicios por lo que en este contenedor vamos a consumir como máximo 3G de RAM y un pool de 250 conexiones, ya que el software que la usa (Elixir en este caso) es muy rápido y necesita de una alta disponibilidad, así mismo precisa de un alto número de conexiones concurrentes para no saturarla.

Opciones de configuración

Estas opciones las hemos sacado de la página https://pgtune.leopard.in.ua que nos ayudará a afinar la configuración en función de los valores de versión, núcleos, memoria y conexiones que necesitemos, hemos decidido usar el perfil online transaction processing system para nuestro caso.

# DB Version: 12
# OS Type: linux
# DB Type: oltp
# Total Memory (RAM): 3 GB
# CPUs num: 4
# Connections num: 250

max_connections = 250 # máximo número de conexiones concurrentes
shared_buffers = 768MB # cantidad de memoria dedicada a datos en caché
effective_cache_size = 2304MB # cantidad de memoria disponible para memoria intermedia en el disco
maintenance_work_mem = 192MB # cantidad de memoria usada para operaciones de mantenimiento
checkpoint_completion_target = 0.9 # permite escribir lentamente en la instancia: checkpoint_completion_target * checkpoint_timeout (5min)
wal_buffers = 16MB # pequeña memoria que sincroniza los datos, aumentándola se permiten inserciones más grandes
default_statistics_target = 100 # recolecta estadísticas de cada una de las tablas para decidir como se ejecutarán las consultas sobre ellas
random_page_cost = 1.1 # sugiere al optimizador cuanto tiempo le llevará al disco encontrar una página aleatoria de disco
effective_io_concurrency = 200 # número de operaciones de disco I/O concurrentes
work_mem = 1572kB # para operaciones complejas realiza ordenamientos más distendidos en memoria
min_wal_size = 2GB # cantidad mínima de la memoria usada para integridad de los datos
max_wal_size = 4GB # cantidad máxima de la memoria usada para integridad de los datos
max_worker_processes = 4 # cantidad máxima de procesos
max_parallel_workers_per_gather = 2 # cantidad máxima de subprocesos en paralelo por nodo, no puede exceder a max_parallel_workers
max_parallel_workers = 4 # cantidad máxima de subprocesos en paralelo

Con esta configuración el contenedor de PostgreSQL 12 está preparado para trabajar con 250 conexiones concurrentes, pudiendo gestionar correctamente la carga de las consultas que se realicen y mantener la estabilidad del sistema a lo largo del tiempo.

Esperamos que os haya gustado, nos vemos en la próxima entrada, saludos