No cenário dinâmico do desenvolvimento web, a busca por soluções inovadoras e eficientes é constante. A integração de diferentes tecnologias, visando otimizar o desempenho e a escalabilidade de sistemas, torna-se uma prioridade. Neste contexto, exploraremos uma abordagem interessante: a utilização do PgBench, uma ferramenta de benchmarking do PostgreSQL, para avaliar o desempenho do MongoDB através do Foreign Data Wrapper (FDW). É crucial ressaltar que este é um experimento, não uma recomendação arquitetural definitiva. Camadas de tradução, como o FDW, não necessariamente melhoram o desempenho, seja emulando MongoDB no PostgreSQL ou vice-versa.
O objetivo principal deste artigo é analisar a performance do mongo_fdw, um Foreign Data Wrapper para PostgreSQL que permite acessar dados armazenados em um banco de dados MongoDB. Em vez de criar um benchmark específico, optou-se por utilizar o PgBench, uma ferramenta já estabelecida e amplamente utilizada para testar o desempenho do PostgreSQL.
Entendendo o Contexto: PgBench e MongoDB
O PgBench, por padrão, executa uma carga de trabalho que não representa necessariamente uma aplicação real, pois todas as sessões atualizam a mesma linha – o saldo global. No entanto, ele é valioso para testar a contenção de bloqueios. É nesse ponto que o MongoDB se destaca, oferecendo garantias ACID sem a necessidade de bloqueios. Para simular uma situação de alta concorrência, o teste foi executado com pgbench -c 50, com 50 conexões de cliente competindo para atualizar as mesmas linhas.
Para fins de comparação, o mesmo comando PgBench foi executado em dois bancos de dados PostgreSQL distintos:
- PostgreSQL nativo: Tabelas PostgreSQL criadas com
pgbench -ie o benchmark executado compgbench -T 60 -c 50. - PostgreSQL via MongoDB FDW: Tabelas estrangeiras do PostgreSQL armazenando seus dados em coleções MongoDB, acessadas através do Foreign Data Wrapper. O mesmo comando PgBench foi utilizado, porém com a opção
-n, já que não há necessidade deVACUUMno MongoDB.
Configuração do Ambiente de Teste
O ambiente de teste foi configurado utilizando Docker em um MacBook Pro Apple M4 Max, com uma instância local do MongoDB Atlas. O mongo_fdw foi compilado a partir do repositório da EDB para ser adicionado à imagem do PostgreSQL 18. O Dockerfile utilizado para construir a imagem é detalhado a seguir:
FROM docker.io/postgres:18 AS build
# Install build dependencies including system libmongoc/libbson so autogen.sh doesn't compile them itself
RUN apt-get update && apt-get install -y --no-install-recommends wget unzip ca-certificates make gcc cmake pkg-config postgresql-server-dev-18 libssl-dev libzstd-dev libmongoc-dev libbson-dev libjson-c-dev libsnappy1v5 libmongocrypt0 && rm -rf /var/lib/apt/lists/*
# Build environment
ENV PKG_CONFIG_PATH=/tmp/mongo_fdw/mongo-c-driver/src/libmongoc/src:/tmp/mongo_fdw/mongo-c-driver/src/libbson/src
ENV LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu
ENV MONGOC_INSTALL_DIR=${LD_LIBRARY_PATH}
ENV JSONC_INSTALL_DIR=${LD_LIBRARY_PATH}
# get MongoDB Foreign Data Wrapper sources
RUN apt-get update && apt-get install -y --no-install-recommends wget unzip ca-certificates make gcc cmake pkg-config postgresql-server-dev-18 libssl-dev libzstd-dev libmongoc-dev libjson-c-dev libsnappy1v5 libmongocrypt0
ADD https://github.com/EnterpriseDB/mongo_fdw/archive/refs/heads/master.zip /tmp/sources.zip
RUN mkdir -p /tmp/mongo_fdw && unzip /tmp/sources.zip -d /tmp/mongo_fdw
# Build MongoDB Foreign Data Wrapper
WORKDIR /tmp/mongo_fdw/mongo_fdw-master
# remove useless ping
RUN sed -i -e '/Ping the database using/d' -e 's?if (entry->conn != NULL)?/*&?' -e 's?return entry->conn?*/&?' connection.c
# build with Mongodb client
RUN ./autogen.sh && make USE_PGXS=1 && make USE_PGXS=1 install
# final stage
FROM docker.io/postgres:18
COPY --from=build /usr/share/postgresql/18/extension/mongo_fdw* /usr/share/postgresql/18/extension/
COPY --from=build /usr/lib/postgresql/18/lib/mongo_fdw.so /usr/lib/postgresql/18/lib/
RUN apt-get update && apt-get install -y libmongoc-1.0-0 libbson-1.0-0 libmongocrypt0 libsnappy1v5 libutf8proc-dev && rm -rf /var/lib/apt/lists/*
A imagem foi construída (docker build -t pachot/postgres_mongo_fdw) e iniciada, conectando-a a um container MongoDB Atlas:
# start MongoDB Atlas (use Atlas CLI)
atlas deployments setup mongo --type local --port 27017 --force
# start PostgreSQL with Mongo FDW linked to MongoDB
docker run -d --link mongo:mongo --name mpg -p 5432:5432 \
-e POSTGRES_PASSWORD=x pachot/postgres_mongo_fdw
Bancos de dados separados foram criados para cada teste:
export PGHOST=localhost
export PGPASSWORD=x
export PGUSER=postgres
psql -c 'create database pgbench_mongo_fdw'
psql -c 'create database pgbench_postgres'
Inicialização do Banco de Dados
Para a linha de base do PostgreSQL, o banco de dados foi inicializado com pgbench -i pgbench_postgres, que cria as tabelas com chaves primárias e insere 100.000 contas em uma única branch.
Para o MongoDB, as coleções foram definidas como tabelas estrangeiras e conectadas com psql pgbench_mongo_fdw:
DROP EXTENSION if exists mongo_fdw CASCADE;
-- Enable the FDW extension
CREATE EXTENSION mongo_fdw;
-- Create FDW server pointing to the MongoDB host
CREATE SERVER mongo_srv
FOREIGN DATA WRAPPER mongo_fdw
OPTIONS (address 'mongo', port '27017');
-- Create user mapping for the current Postgres user
CREATE USER MAPPING FOR postgres
SERVER mongo_srv
OPTIONS (username 'postgres', password 'x');
-- Foreign tables for pgbench schema
CREATE FOREIGN TABLE pgbench_accounts(
_id name,
aid int, bid int, abalance int, filler text
)
SERVER mongo_srv OPTIONS (collection 'pgbench_accounts');
CREATE FOREIGN TABLE pgbench_branches(
_id name,
bid int, bbalance int, filler text
)
SERVER mongo_srv OPTIONS (collection 'pgbench_branches');
CREATE FOREIGN TABLE pgbench_tellers(
_id name,
tid int, bid int, tbalance int, filler text
)
SERVER mongo_srv OPTIONS (collection 'pgbench_tellers');
CREATE FOREIGN TABLE pgbench_history(
_id name,
tid int, bid int, aid int, delta int, mtime timestamp, filler text
)
SERVER mongo_srv OPTIONS (collection 'pgbench_history');
No servidor MongoDB, o usuário e as coleções mapeadas do PostgreSQL foram criados (usando mongosh):
db.createUser( {
user: "postgres",
pwd: "x",
roles: [ { role: "readWrite", db: "test" } ]
} )
;
db.dropDatabase("test");
use test;
db.pgbench_branches.createIndex({bid:1},{unique:true});
db.pgbench_tellers.createIndex({tid:1},{unique:true});
db.pgbench_accounts.createIndex({aid:1},{unique:true});
db.createCollection("pgbench_history");
Como pgbench -i trunca tabelas, o que o MongoDB Foreign Data Wrapper não suporta, foram utilizados comandos INSERT (via psql pgbench_mongo_fdw) semelhantes aos executados por pgbench -i:
\set scale 1
INSERT INTO pgbench_branches (bid, bbalance, filler)
SELECT bid, 0, ''
FROM generate_series(1, :scale) AS bid;
INSERT INTO pgbench_tellers (tid, bid, tbalance, filler)
SELECT tid, ((tid - 1) / 10) + 1, 0, ''
FROM generate_series(1, :scale * 10) AS tid;
INSERT INTO pgbench_accounts (aid, bid, abalance, filler)
SELECT aid, ((aid - 1) / 100000) + 1, 0, ''
FROM generate_series(1, :scale * 100000) AS aid;
Resultados do Benchmark
Os seguintes comandos foram executados para realizar o benchmark:
docker exec -it mpg \
pgbench -T 60 -P 5 -c 50 -r -U postgres -M prepared pgbench_postgres
docker exec -it mpg \
pgbench -n -T 60 -P 5 -c 50 -r -U postgres -M prepared pgbench_mongo_fdw
PostgreSQL (tps = 4085, latency average = 12 ms)
Os resultados do benchmark padrão do PgBench em tabelas PostgreSQL foram:
franck.pachot % docker exec -it mpg \
pgbench -T 60 -P 5 -c 50 -r -U postgres -M prepared pgbench_postgres
pgbench (18.1 (Debian 18.1-1.pgdg13+2))
starting vacuum...end.
progress: 5.0 s, 3847.4 tps, lat 12.860 ms stddev 14.474, 0 failed
progress: 10.0 s, 4149.0 tps, lat 12.051 ms stddev 12.893, 0 failed
progress: 15.0 s, 3940.6 tps, lat 12.668 ms stddev 12.576, 0 failed
progress: 20.0 s, 3500.0 tps, lat 14.300 ms stddev 16.424, 0 failed
progress: 25.0 s, 4013.0 tps, lat 12.462 ms stddev 13.175, 0 failed
progress: 30.0 s, 3437.4 tps, lat 14.539 ms stddev 25.607, 0 failed
progress: 35.0 s, 4421.9 tps, lat 11.308 ms stddev 12.100, 0 failed
progress: 40.0 s, 4485.0 tps, lat 11.140 ms stddev 12.031, 0 failed
progress: 45.0 s, 4286.2 tps, lat 11.654 ms stddev 13.244, 0 failed
progress: 50.0 s, 4008.6 tps, lat 12.476 ms stddev 13.586, 0 failed
progress: 55.0 s, 4551.8 tps, lat 10.959 ms stddev 13.791, 0 failed
progress: 60.0 s, 4356.2 tps, lat 11.505 ms stddev 15.813, 0 failed
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: prepared
number of clients: 50
number of threads: 1
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 245035
number of failed transactions: 0 (0.000%)
latency average = 12.234 ms
latency stddev = 14.855 ms
initial connection time = 38.862 ms
tps = 4085.473436 (without initial connection time)
statement latencies in milliseconds and failures:
0.000 0 \set aid random(1, 100000 * :scale)
0.000 0 \set bid random(1, 1 * :scale)
0.000 0 \set tid random(1, 10 * :scale)
0.000 0 \set delta random(-5000, 5000)
0.036 0 BEGIN;
0.058 0 UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid;
0.039 0 SELECT abalance FROM pgbench_accounts WHERE aid = :aid;
10.040 0 UPDATE pgbench_tellers SET tbalance = tbalance + :delta WHERE tid = :tid;
1.817 0 UPDATE pgbench_branches SET bbalance = bbalance + :delta WHERE bid = :bid;
0.041 0 INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP);
0.202 0 END;
A execução obteve uma média de 4.000 transações por segundo com uma latência de 12 ms. A maior parte da latência provém da primeira atualização, quando todas as conexões visam a mesma linha e não podem ser executadas simultaneamente.
MongoDB (tps = 4922, latency average = 10 ms)
A mesma execução, com tabelas estrangeiras lendo e escrevendo no MongoDB em vez do PostgreSQL, apresentou os seguintes resultados:
franck.pachot % docker exec -it mpg \
pgbench -n -T 60 -P 5 -c 50 -r -U postgres -M prepared pgbench_mongo_fdw
pgbench (18.1 (Debian 18.1-1.pgdg13+2))
progress: 5.0 s, 4752.1 tps, lat 10.379 ms stddev 4.488, 0 failed
progress: 10.0 s, 4942.9 tps, lat 10.085 ms stddev 3.356, 0 failed
progress: 15.0 s, 4841.7 tps, lat 10.292 ms stddev 2.256, 0 failed
progress: 20.0 s, 4640.4 tps, lat 10.744 ms stddev 3.498, 0 failed
progress: 25.0 s, 5011.3 tps, lat 9.943 ms stddev 1.724, 0 failed
progress: 30.0 s, 4536.0 tps, lat 10.996 ms stddev 8.739, 0 failed
progress: 35.0 s, 4862.1 tps, lat 10.248 ms stddev