Executando verificação de segurança...
25

O Postgres consegue substituir o Redis como um cache?

Esse artigo também está disponível no YouTube!

Hoje, decidi perguntar para a galera do Twitter qual era o primeiro serviço de message queue que vinha a cabeça deles. Para a minha surpresa, uma das respostas foi: Postgres.

Eu abri o link e fiquei surpreso não apenas pela possibilidade de usar Postgres como um Message Broker:

“Use Postgres as a message queue with SKIP LOCKED instead of Kafka (If you only need a message queue)”. — Stephan Schmidt

Mas também, com a possibilidade de usar Postgres como um cache no lugar do Redis:

“Use Postgres for caching instead of Redis with UNLOGGED tables and TEXT as a JSON data type. Use stored procedures or do as I do, use ChatGPT to write them for you, to add and enforce an expiry date for the data just like in Redis”. — Stephan Schmidt

E a razão pela qual eu fiquei tão surpreso foi porque durante minha jornada com o Redis, uma das coisas que eu frequentemente ouvir algumas pessoas falarem (da Redis) era que o "Redis é uma base de dados e portanto deveria ser sua base de dados primária."

E na verdade isso faz sentido. Redis é uma base de dados completa que por acaso funciona muito bem como um cache. E a razão pela qual ela funciona tão bem como um cache é porque ela é rápida. Muito rápida. Ao ponto de realizar milhões de operações em um único segundo.

E assim... ler que o Postgres, a minha base de dados relacional favorita, poderia agora substituir o Redis, a minha base de dados não relacional favorita, meio que virou meu mundo de cabeça para baixo. Afinal de contas, eu deveria substituir o Redis com o Postgres ou o Postgres com o Redis?

Mas antes mesmo de considerar essa questão, eu queria entender: É mesmo uma boa ideia usar o Postgres como um cache? Ele realmente consegue substituir o Redis? É isso que eu quero descobrir hoje.

O artigo que foi compartilhado comigo, e que eu descobri depois que estava bombando no Twitter, tinha sido escrito pelo Stephan Schmidt.

O Stephan não só defende a ideia de substituir o Redis pelo Postgres, na verdade ele defende a ideia de substituir tudo com o Postgres. De acordo com ele, ao fazer isso, estaríamos removendo e complexidade e aumentando a nossa habilidade de evoluir mais rapidamente.

“Just Use Postgres for Everything (How to reduce complexity and move faster)” — Stephan Schmidt

Entretanto, ele não poderia ser o único defendendo a ideia de substituir o Redis, e na verdade, algumas outras pessoas estavam fazendo a mesma coisa:

Mas primeiramente, por que eu gostaria de substituir o Redis pelo Postgres?

O Stephan já nos deu duas razões: menos complexidade e mudanças mais rápidas. Mas tem mais?

Usar o Postgres como um cache não é a escolha mais comum, mas existem certos cenários em que isso possa fazer sentido. Vamos dar uma olhada neles:

Uma Tech Stack Unificada

Postgres é uma das base de dados mais populares por aí. Ela é gratuita, open source, e provavelmente você já a está utilizando na sua aplicação.

Usá-lo como um cache pode simplificar sua tech stack ao reduzir a necessidade de gerenciar e manter multiplas base de dados.

Interface Familiar

O Postgres supporta queries complexas e também indexação, o que tornaria mais fácil retornar e transformar dados de forma avançada diretamente na camada de cache. Usar SQL para na camada de cache também é vantajoso se sua equipe já é fluente em SQL.

Custo

Em alguns casos, o custo benefício em usar um Postgres que já existe para cache ao invés de implementar uma solução de cache separada pode ser maior. Principalmente em ambientes onde o orçamento para infraestrutura é limitado.

O que eu deveria esperar de um cache?

Serviços de cache tradicionais, como o Redis, vêm como uma série de funcionalidades que melhoraram a performance e a escalabilidade das nossas aplicações. Para entender se o Postgres poderia realmente substituir o Redis, nós também precisamos entender quais funcionalidades são essas. Vamos dar uma olhada em alguns aspectos chaves que deveríamos esperar de um serviço de cache:

Performance

O objetivo principal de um serviço de cache é melhorar a performance das nossas aplicações ao tornar o acesso aos nossos dados mais rápido.

Soluções de cache de alta performance podem lidar processos de alto throughput e fornecer tempos de resposta abaixo de um milissegundo, acelerando significativamente os processos de acesso aos dados.

Expiração

Ao configurar tempo de expiração (TTL) para os dados em cache, podemos ter a certeza de que dados desatualizados são automaticamente removidos do cache depois um período específico. Ter essa garantia é um aspecto essencial de um serviço de cache.

Eviction

Serviços de cache geralmente armazenam seus dados na memória, que geralmente é mais limitada. Por isso, configurar uma política de eviction permite remover automaticamente os dados menos utilizados para abrir espaço para novos dados.

Key-value storage (Armazenamento chave-valor)

A maioria dos serviços de cache armazenam dados no formato key-value (chave-valor). Isso permite que os dados sejam acessados de forma rápida, tornando mais fácil o armazenamento e o acesso aos dados frequentemente utilizados mais eficiente.

Em resumo, o que esperamos de um serviço de cache é que ele permitar que acessemos nossos dados de modo mais rápido e que ele retorne nossos dados ainda atualizados o mais rápido possível.

Como converter o Postgres num cache?

De acordo com o Martin Heinz, no seu blog [You Don’t Need a Dedicated Cache Service — PostgreSQL as a Cache], o Postgres oferece quase tudo que foi mencionado na seção anterior deste artigo.

Tanto o Stephan quanto o Martin dizem que podemos transformar o Postgres num cache usando UNLOGGED tables. Quase todos os exemplos que vou mostrar daqui para a frente foram criados pelo Martin na sua publicação.

Unlogged tables e Write Ahead Log

Unlogged tables no Postgres é uma forma de prevenir que tabelas específicas escrevam no WAL (Write Ahead Log).

WAL, por sua vez, garante que todas as alterações feitas na base de dados sejam gravadas antes de serem realmente escritas nos arquivos da base de dados. Isso ajuda a manter a integridade dos dados, espepcialmente num evento de crash ou uma falha de energia.

Curiosidade: O Redis oferece um mecanismo similar chamado Append Only File (AOF), que não só é um mecanismo para persistir seus dados, mas também funciona de maneira similar, registrando todas as operações executadas no Redis. Para usar o Redis como nossa base de dados primária, nós desligamos o AOF, enquanto para usar o Postgres como um cache, nós desligamos (em tabelas específicas), o WAL.

Desligar o WAL significa melhorar a performance

Toda vez que um dado é modificado, o Postgres escreve essa mudança tanto no WAL quanto nos seus arquivos. Naturalmente, isso dobra o número de operações necessárias.

Além disso, para garantir que toda transação comitada é fisicamente escrita no disco, o WAL é desenhado para forçar um flush no disco (fsync). E flushes frequentes no disco impactam a performance já que adicionam latência enquanto esperam que o disco reconheça que os dados foram escritos de forma segura.

Mas também signfica abrir mão da persistência

O que precisamos realmente saber sobre unlogged tables é que elas não são persistentes.

O Postgres usa o WAL para reproduzir e aplicar quaisquer alterações que foram feita desde o último checkpoint. Se não temos esse registro, a base de dados não consegue ser restaurada a um estado consistence ao reproduzir os registros do WAL. De qualquer forma, isso é meio que esperado de um cache, não é mesmo?

CREATE UNLOGGED TABLE cache (
    id serial PRIMARY KEY,
    key text UNIQUE NOT NULL,
    value jsonb,
    inserted_at timestamp);

CREATE INDEX idx_cache_key ON cache (key);

Expiration com Stored Procedures

Tanto o Martin quanto o Stephan dizem que expiração pode ser implementada com o uso de stored procedures. E é aqui que as coisas começam a ficar um pouco complexas.

CREATE OR REPLACE PROCEDURE expire_rows (retention_period INTERVAL) AS
$$
BEGIN
    DELETE FROM cache
    WHERE inserted_at < NOW() - retention_period;

    COMMIT;
END;
$$ LANGUAGE plpgsql;

CALL expire_rows('60 minutes'); -- This will remove rows older than 1 hour

A verdade é que a maioria das aplicações hoje em dia não dependendem mais de stored procedures. Muitos desenvolvedores inclusive são contra o uso excessivo de stored procedures hoje em dia.

Geralmente, o uso de stored procedures é desencorajado por que queremos evitar que lógica de negócio acabe vazando para dentro das nossas base de dados. Para além disso, conforme o número de stored procedures cresce, gerenciá-las e entende-las pode se tornar uma tarefa difícil.

Além do mais, nós também precisamos chamar essas stored procedures regularmente. E para isso, precisamos usar uma extensão chamada pg_cron.

Depois de instalar nossa extensão, ainda precisamos criar nossos schedulers:

-- Create a schedule to run the procedure every hour
SELECT cron.schedule('0 * * * *', $$CALL expire_rows('1 hour');$$);

-- List all scheduled jobs
SELECT * FROM cron.job;

A complexidade está aumentando, não está?

Eviction com Stored Procedures

O Stephan nem sequer menciona eviction enquanto Martin diz que isso é opcional já que expiration manteria o tamanho do cache baixo.

Mas se você quiser implementar uma solução para eviction, ele sugere que você adicione uma coluna chamada last_read_timestamp a tabela e rode outra stored procedure de vez em quando para alcançar uma política de eviction LRU (last recently used).

CREATE OR REPLACE PROCEDURE lru_eviction(eviction_count INTEGER) AS
$$
BEGIN
    DELETE FROM cache
    WHERE ctid IN (
        SELECT ctid
        FROM cache
        ORDER BY last_read_timestamp ASC
        LIMIT eviction_count
    );

    COMMIT;
END;
$$ LANGUAGE plpgsql;

-- Call the procedure to evict a specified number of rows
CALL lru_eviction(10); -- This will remove the 10 least recently accessed rows

Redis offers eight types of eviction policy out of the box. Precisa de outra política de eviction? Pede pro ChatGPT.

E a performance?

Performance é o que mais importa nesse caso, não é? Afinal, o que realmento buscamos com um caching service é acessar nossos dados de um modo mais rápido.

Greg Sabino Mullane mandou muito bem escrevendo o seu artigo [PostgreSQL Unlogged Tables — Look Ma, No WAL!] comparando a performance de tabelas UNLOGGED e LOGGED no Postgres. Seus dados mostram que a performance de escrita nas tabelas de escrita é duas vezes mais rápida do que a mesma operação na tabela LOGGED. Especificamente:

Unlogged Table:
Latency: 2.059 ms
TPS: 485,706168

Logged Table:
Latency: 5.949 ms
TPS: 168,087557

Mas e a performance de leitura?

E aqui está a pegadinha. A estratégia de otimização do Postgres depende de shared buffers.

Shared buffers armazenam dados frequentemente acessados e índices diretamente na memória, o que permite que eles sejam rapidamente acessados e se reduza a necessidade de leituras do disco. Melhorando assim a performance das queries e o acesso de dados tanto das tabelas logged quanto das unlogged.

É verdade que as tabelas unlogged podem viver nesses buffers, mas elas também podem, e irão, ser escritas no disco se crecerem muito e o espaço na memória for limitada. Portanto, tabelas unlogged melhoram primeiramente a velocidade de escrita, não de leitura.

Para provar, eu conduzi um rápido experimento utilizando o pgbench. Você pode ver como eu fiz aqui.

E os resultados mostram que a performance tanto das tabelas logged quanto das unlogged são, de fato, muito similares. Ler dos dois tipos de tabela levou mais ou menos 0,650 ms na média. Especificamente:

Unlogged Table:
Latency: 0,679 ms
TPS: 14.724,204

Logged Table:
Latency: 0,627 ms
TPS: 15.946,025

Esse resultado reenforça que tabelas unlogged primeiramente melhoram a performance de escrita. Para operaçoes de leitura, não é evidente que as tabelas unlogged tenham uma melhora na performance, já que tanto as tabelas logged, quanto unlogged se beneficiam de modo similar da estratégia de cache e otimização do Postgres.

Como a performance se compara ao Redis?

Em conjunto ao benchmark do Postgres, eu também conduzi um experimento com o Redis. Veja os resultados aqui. Os resultados do Redis mostram uma vantagem significativa na performance em tanto em termos de leitura quanto de escrita:

Leitura:
Latency (p50): 0,095 ms
Requests per second (RPS): 892.857,12

Escrita:
Latency (p50): 0,103 ms
Requests per second (RPS): 892.857,12

A comparação das performances nos mostram que o Redis tem um desempenho melhor do que o Postgres tanto nas operações de leitura quanto de escrita:

O Redis alcança uma latência de 0,095 ms, que é aproxidamente 85% mais rápida do que a latência de 0.679 ms observada na tabela unlogged do Postgres.

O Redis também lida com um request rate muito maior com 892.857,12 requests por segundo comparada as 15.946,025 transações por segundo do Postgres.

E quando falamos de operações de escrita, como ficou evidente pelo maior throughput e a menor latência, podemos ver que o Redis também entrega uma performance superior.

E se eu rodasse Postgres na memória?

Durante a revisão desse artigo, um colega da Xebia, [Maksym Fedorov], disse:

"E se tabelas unlogged fossem criadas num tablespace que corresponde ao memory mapped file? Acredito que veríamos números completamente diferentes.

Para testar isso, eu conduzi benchmarks com os dados do Postgres persistidos na memória. Surpreendemente não houveram melhroas nos resultados. O benchmark demonstrou:

Leitura:
Latency: 0.652 ms
Requests per second (TPS): 15.329,776954

Depois de uma pesquisa mais longa, eu entendi que apesas dos dados serem persistidos na RAM, acessá-los pelo shared buffers do Postgres ainda causa custos adicionais. Esses custos vêm do gerenciamento de locks e outros processos internos necessários para a integridade dos dados e acesso simultâneo.

O Postgres sempre verifica se os dados estão no shared buffers primeiro. Se não estiverem, ele copia os dados do tmpfs para o shared buffers antes de retorná-los ao cliente mesmo quando a base de dados é persistida na RAM.

Eu deveria substituir o Redis pelo Postgres?

Baseado nesse estudo, se você precisa de um serviço de caching para melhorar a performance de escrita, o Postgres pode ser otimizado utilizando unlogged tables. Entretanto, apesar das tabelas unlogged oferecerem uma melhor performance de escrita, elas ainda são significativamente menos performáticas em comparação com o Redis.

A maior razão para se utilizar um serviço de cache é melhorar o tempo de acesso aos dados. Tabelas unlogged não melhoram a performance de leitura enquanto o Redis é excelente com operações extremamente rápidas.

Para além disso, o Redis ajuda a prevenir que um grande volume de queries de baixo custo sejam enviadas para a sua base de dados, um benefício que tabelas unlogged não conseguem oferer. O Redis também oferece funcionalidades nativas como expiration, políticas de eviction, e mais, que são complexas quando implementadas no Postgres.

Embora gererenciar o Postgres possa parecer mais fácil para aguns, converter o Postgres em um cache não traz as vantagens de um serviço de cache dedicado. Ao mesmo tempo, o Redis é fácil e prazerozo de aprender e utilizar.

Para uma maior performance e simplicidade, escolher um serviço de cache verdadeiro como o Redis é a escolha certa.

Espero que tenham curtido essa história! Eu curti muito escreve-la e fazer toda essa pesquisa!

Um agradecimento especial ao Maksym Fedorov, João Paulo Gomes, and Hernani Fernandes pela revisão desse artigo.

Continuem curiosos!

Carregando publicação patrocinada...
3

Cara, que artigo massinha!

Eu confesso que fiquei cético no começo, porque eu penso que uma ferramenta criada especificamente pra um fim não pode perder pra uma ferramenta feita pra outro fim, mas que consegue trabalhar em outro.

E de fato, foi o que o teu artigo comprovou. Pesquisa legal pra caramba.
Deve ter sido mó legal fazer o bench disso.

Parabéns. Tomara que cê traga outros artigos assim pra cá :D

1
3

Excelente publicação e você me lembrou de uma forma ou outra o Pikuma, conhece ele? É uma pessoa fantástica e que também tem a habilidade de se aprofundar nos temas.

Seria uma honra no futuro trabalhar com você em algum projeto educacional.

2

Infelizmente não o conheço, vou pesquisar aqui. Mas seria uma honra para mim poder trabalhar contigo em algum projeto também! Estou disposto!

2

Na minha opinião não faz sentido usar o postgresql como cache só pela dificuldade de fazer isso, não quer dizer q n tenha como, mas imagina o cenário:

Voce tem uma aplicação bem estruturada com postgres como banco de dados e outros serviços organizados em containeres e um dockercompose para tudo isso.

No seu artigo mencionou que a desvantagem de implementar o redis seria custo, base unificada e interface familiar, vou discutir cada ponto:

  • custo: ao implementar um serviço de cache, seja integrando ao banco de dados existente(postgres) ou criando uma nova instancia para rodar o serviço (redis) no pior dos casos o postgres vai consumir a mais os mesmos recursos que a aplicação redis consumiria como serviço separado (imaginando que redis e postgresql tenham mesmo desempenho), e mais, se não redimensionar corretamente seu serviço postgres, tu pode perder performance em consultas para usar a cpu para caches, ou seja, piorar a performance no geral. com serviços separados n tem esse problema, ao subir um redis que esteja tendo 100% de uso de cpu, teu postgres n é afetado pq estao separados(pq vc é um bom desenvolvedor e n coloca serviços diferentes na mesma maquina)
  • base unificada: acho q o maior ponto nisso é que se não redimensionar sua base de dados corretamente o uso do cache pode afetar as consultas no banco. (mesmo ponto dos custos)
  • interface familiar: esse ponto é oq eu mais discordo pelo seguinte fato que o redis é uma base de dados chave-valor, ou seja, o mesmo que um dicionário no python, um vetor no php, um objeto ou dicionário em js e assim por diante. entao para quem já estudou estrutura de dados tem um conhecimento no funcionamento de um dicionário, claro que o redis adiciona algumas funalidades a mais, porém não é obrigatório entende-las visto que postgresl adiciona uma penca de coisa que quem sabe sql não sabe, e nem precisa saber para mexer no sql.

Agora o ponto que eu acho mais importante é que a complexidade envolvida para criar uma tabela cache e usa-la sem ter perda de performance no postgres pela escrita e leitura e por conta do journaling é mt grande, coisa que um dev junior poderia n saber fazer ou cometer mts erros. Oq não ocorre no redis, ele é um dicionário por si só, se tu subir uma instancia dele é só sair usando que vai ter a performance de um cache, e mais, se tu n quiser salvar nada no disco, usar só como cache puro tem uma flag pra isso e pronto, sem config, sem ter q verificar cada detalhe, só plug and play.

Minha opinião final: da pra substituir o redis pelo postgres? Sim, da mesma forma que consigo trocar o postgres por um txt e mt esforço. (Meio exagerado, mas entendam a analogia)
Devo fazer? Somente se precisar fazer, tipo, mt mesmo, senão, so sobe uma instancia redis ou memcache ou dice e seja feliz.

2

Exatamente. A complexidade para tornar o Postgres num cache com toda a funcionalidade que o Redis traz nativamente, desde expiration até eviction, sem falar em performance, já torna a ideia ruim. Se ainda por cima falarmos em performance, a ideia é pior ainda, é trocar 6 por meia dúzia mas gastando o dobro nessa troca 😅

2

Ótimo artigo! Percebi que após a revisão acabou ficando um parágrafo em inglês ainda que traduziu logo em seguida, se quiser removê-lo:

In a nutshell, what you want from a caching service...

0
2
2

Na verdade é essa ideia que desmistifico no artigo. Embora algumas pessoas defendam que o Postgres possa funcionar bem como um cache, na realidade a performance não chega nem perto do que se espera de um cache e a complexidade para implementar funcionalidades essenciais como eviction e expiration são altas (na minha opinião são ainda mais altas do que simplesmente levantar uma bd Redis).

Inclusive, o artigo que refuto no meu texto é o artigo que é mencionado nessa lista que você compartilhou.

1

Nessa pegada de colocar o Postgres para substituir tudo, há quem coloque até ele como servidor REST e algumas coisas de modelos de machine learning dentro dele, confesso que perdi essa experiência, mas muito interesante mesmo

1

Muito fácil chegar dizendo "não" sem demonstrar por "B + C que a menina dos teus olhos não substitui o redis". O bom de ler artigos assim, é que aprofundam o conhecimento em ambas as ferramentas, redis e postgres. Mesmos que tenhamos uma delas como a "menina dos nossos olhos", bom saber os limites de cada uma delas.

1
0
-2
1