<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Streaming on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/streaming/</link><description>Recent content in Streaming on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Thu, 04 Jun 2026 11:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/streaming/index.xml" rel="self" type="application/rss+xml"/><item><title>Debezium y CDC: el notario que escucha los cambios antes de que nadie los pida</title><link>https://blog.lo0.es/posts/debezium-cdc-fundamentos/</link><pubDate>Thu, 04 Jun 2026 11:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/debezium-cdc-fundamentos/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Change Data Capture (CDC) con Debezium escucha el Write-Ahead Log de PostgreSQL y convierte cada INSERT, UPDATE y DELETE en un evento Kafka estructurado. A diferencia del polling tradicional (&lt;code>SELECT ... WHERE updated_at &amp;gt; ?&lt;/code>), detecta borrados, tiene latencia de decenas de milisegundos y no añade carga extra a la base de datos. En pipelines RAG, esto significa que cuando se borra un documento de Postgres, los chunks de Qdrant desaparecen también —automáticamente, en tiempo real—. La infraestructura de soporte es modesta: el connector consume 2-4 cores y 4-8 GB RAM para procesar miles de eventos por segundo.&lt;/p>
&lt;hr>
&lt;h2 id="la-analogía-maestra-el-notario-del-registro-de-la-propiedad">La analogía maestra: el notario del registro de la propiedad&lt;/h2>
&lt;p>Imagina el Registro de la Propiedad. Cada vez que se vende un piso, se hipoteca, o se cancela una hipoteca, el registrador anota la operación en el &lt;strong>libro del registro&lt;/strong> —un diario cronológico e inmutable. Si quieres saber qué ha cambiado en el registro, tienes dos opciones:&lt;/p>
&lt;p>&lt;strong>Opción A (polling):&lt;/strong> envías a alguien cada 5 minutos con una lista de fincas a preguntar «¿ha cambiado algo?». Problemas: si se canceló una titularidad (DELETE), la finca ya no existe cuando tu enviado llega —no hay rastro—. Si hay 20 departamentos distintos haciendo lo mismo, hay 20 personas molestando al registrador cada 5 minutos. Y la latencia mínima es el intervalo: 5 minutos.&lt;/p>
&lt;p>&lt;strong>Opción B (Debezium):&lt;/strong> contratas a un &lt;strong>notario&lt;/strong> que se sienta directamente en la mesa del registrador. Cada vez que el registrador firma una operación en el libro, el notario la anota al momento y notifica a quien corresponda. Cancelación de titularidad incluida —el notario la ve tan claro como cualquier otra operación, porque estaba allí cuando se firmó—.&lt;/p>
&lt;p>En esta analogía:&lt;/p>
&lt;ul>
&lt;li>El &lt;strong>libro del registro&lt;/strong> es el &lt;strong>WAL&lt;/strong> (Write-Ahead Log) de PostgreSQL.&lt;/li>
&lt;li>El &lt;strong>notario&lt;/strong> es el &lt;strong>Debezium connector&lt;/strong>.&lt;/li>
&lt;li>El &lt;strong>marcapáginas&lt;/strong> del notario —que garantiza que no pierde ninguna página aunque salga un momento— es el &lt;strong>slot de replicación lógica&lt;/strong>.&lt;/li>
&lt;li>El &lt;strong>mensajero&lt;/strong> que lleva las notificaciones a los interesados es &lt;strong>Kafka&lt;/strong> (o Redpanda, o NATS JetStream).&lt;/li>
&lt;/ul>
&lt;p>Este hilo lo vamos a retomar en cada sección. Cuando algo no quede claro en los detalles técnicos, vuelve a la imagen del notario.&lt;/p>
&lt;hr>
&lt;h2 id="1-el-problema-que-cdc-resuelve">1. El problema que CDC resuelve&lt;/h2>
&lt;p>El patrón de sincronización más habitual entre servicios que comparten PostgreSQL es el polling periódico:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">content&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">updated_at&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">documents&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">updated_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">updated_at&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">LIMIT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1000&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Este patrón tiene tres problemas estructurales:&lt;/p>
&lt;p>&lt;strong>Los DELETEs son invisibles.&lt;/strong> Cuando borras una fila, &lt;code>updated_at&lt;/code> no se actualiza —la fila desaparece—. La próxima vez que el poller consulte, la fila no existe y no hay forma de saber que existió. En un pipeline RAG, esto se traduce en &lt;strong>chunks huérfanos en Qdrant&lt;/strong>: el documento ya no existe en Postgres, pero sus vectores siguen contaminando los resultados de búsqueda.&lt;/p>
&lt;p>&lt;strong>La latencia mínima es el intervalo.&lt;/strong> Si el poller corre cada 5 segundos, la latencia media es 2,5 segundos. Para sincronización near-real-time (dashboards, alertas, RAG con documentos que cambian frecuentemente) esto es demasiado.&lt;/p>
&lt;p>&lt;strong>La carga escala con el número de consumidores.&lt;/strong> Si 10 servicios hacen polling cada 5 segundos sobre la misma tabla, son 10 × 12 = 120 queries/minuto que no producen trabajo útil —solo verifican si hay algo nuevo—. En tablas grandes con índices complejos, esto es carga real en la base de datos.&lt;/p>
&lt;p>CDC invierte el modelo: &lt;strong>la base de datos notifica, los consumidores escuchan&lt;/strong>. Cero polling, cero carga extra, DELETEs incluidos, latencia de decenas de milisegundos.&lt;/p>
&lt;hr>
&lt;h2 id="2-qué-es-el-wal-de-postgresql">2. Qué es el WAL de PostgreSQL&lt;/h2>
&lt;h3 id="el-diario-de-operaciones">El diario de operaciones&lt;/h3>
&lt;p>El Write-Ahead Log (WAL) es el registro cronológico e inmutable de todas las operaciones que Postgres realiza. Antes de modificar cualquier página de datos en disco, Postgres escribe la operación en el WAL. Esta secuencia —primero el log, luego los datos— es lo que garantiza la durabilidad (D de ACID) y permite el crash recovery: si Postgres cae a mitad de una transacción, al reiniciar replaye el WAL para devolver la base de datos a un estado consistente.&lt;/p>
&lt;p>El WAL es el &lt;strong>libro del registro&lt;/strong> de nuestra analogía: cronológico, inmutable, completo.&lt;/p>
&lt;h3 id="replicación-física-vs-lógica">Replicación física vs. lógica&lt;/h3>
&lt;p>PostgreSQL soporta dos modos de replicación basados en el WAL:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>Replicación física:&lt;/strong> replica bloques de disco tal cual. El standby recibe los mismos bytes que el primario. Sirve para high availability y failover, pero el destino debe ser una copia exacta de Postgres —no puedes enviar los cambios a una aplicación externa—.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Replicación lógica:&lt;/strong> en vez de bloques de disco, replica &lt;strong>operaciones semánticas&lt;/strong>: «se insertó la fila con id=42 en la tabla &lt;code>documents&lt;/code> con estos valores». El destino puede ser cualquier cosa que entienda el protocolo: otro Postgres, Debezium, o cualquier consumer personalizado.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>CDC usa replicación lógica. Es la que permite que Debezium entienda «qué cambió y en qué tabla» en lugar de «qué bloque de disco cambió en qué offset».&lt;/p>
&lt;h3 id="el-slot-de-replicación-el-marcapáginas-del-notario">El slot de replicación: el marcapáginas del notario&lt;/h3>
&lt;p>Un &lt;strong>slot de replicación lógica&lt;/strong> es un cursor persistente en el WAL. Postgres mantiene un registro de hasta qué posición del WAL ha consumido cada slot. Mientras un slot existe, Postgres &lt;strong>garantiza que no descarta los segmentos WAL que el slot aún no ha leído&lt;/strong>.&lt;/p>
&lt;p>Esto es exactamente el marcapáginas del notario: aunque el notario salga a comer, el libro permanece abierto en la última página que leyó. Cuando vuelve, continúa exactamente donde lo dejó, sin haber perdido nada.&lt;/p>
&lt;p>El riesgo es el inverso: &lt;strong>si el notario no vuelve&lt;/strong>, el marcapáginas impide que el registrador archive las páginas antiguas. Si el Debezium connector se cae y no se recupera durante horas, el WAL crece indefinidamente en disco hasta que el slot se elimine manualmente o el consumer vuelva a consumir. Esto se llama &lt;strong>WAL disk blowup&lt;/strong> y es el riesgo operacional más importante de Debezium.&lt;/p>
&lt;p>Monitorización obligatoria:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">slot_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">confirmed_flush_lsn&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">pg_current_wal_lsn&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">pg_wal_lsn_diff&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">pg_current_wal_lsn&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">confirmed_flush_lsn&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">lag_bytes&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_replication_slots&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">slot_type&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;logical&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="el-plugin-pgoutput">El plugin pgoutput&lt;/h3>
&lt;p>El WAL almacena las operaciones en formato binario interno. Para que Debezium las entienda, Postgres necesita &lt;strong>decodificarlas&lt;/strong> en un formato legible. El plugin de decodificación &lt;code>pgoutput&lt;/code> —incluido en el core de Postgres desde la versión 10— hace exactamente esto: traduce los eventos binarios del WAL en mensajes con la estructura antes/después de cada fila.&lt;/p>
&lt;p>Debezium usa &lt;code>pgoutput&lt;/code> por defecto. No requiere instalar extensiones externas (a diferencia del plugin &lt;code>wal2json&lt;/code> que fue popular antes de Postgres 10).&lt;/p>
&lt;hr>
&lt;h2 id="3-arquitectura-de-debezium">3. Arquitectura de Debezium&lt;/h2>
&lt;h3 id="el-connector-como-plugin-de-kafka-connect">El connector como plugin de Kafka Connect&lt;/h3>
&lt;p>Debezium no es un servicio standalone —es un plugin del framework &lt;strong>Kafka Connect&lt;/strong>. Kafka Connect gestiona el ciclo de vida del connector (arranque, parada, reconexión, offset tracking) y provee la infraestructura de paralelismo y fault tolerance.&lt;/p>
&lt;p>El connector se comunica con Postgres a través del protocolo de replicación lógica (no por JDBC), usando las credenciales de un usuario con rol &lt;code>REPLICATION&lt;/code>.&lt;/p>
&lt;pre tabindex="0">&lt;code>PostgreSQL (WAL + pgoutput)
│
│ protocolo de replicación lógica
▼
Debezium Connector (Kafka Connect worker)
│
│ Kafka Producer API
▼
Kafka topic: rag.public.documents
│
▼
Consumer (sync a Qdrant, audit log, fine-tuning pipeline...)
&lt;/code>&lt;/pre>&lt;h3 id="estructura-de-un-evento-debezium">Estructura de un evento Debezium&lt;/h3>
&lt;p>Cada cambio en la tabla se convierte en un mensaje JSON con esta estructura:&lt;/p>
&lt;p>&lt;strong>INSERT (&lt;code>&amp;quot;op&amp;quot;: &amp;quot;c&amp;quot;&lt;/code> — create):&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;before&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;after&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">42&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Contrato de arrendamiento...&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;tenant_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;acme&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;updated_at&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1748934000000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;op&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;c&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;source&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2.7.0.Final&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;connector&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;postgresql&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;db&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;rag_db&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;schema&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;public&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;table&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;documents&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;lsn&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">29823948&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;txId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1047&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ts_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1748934000123&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>DELETE (&lt;code>&amp;quot;op&amp;quot;: &amp;quot;d&amp;quot;&lt;/code>):&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;before&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">42&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Contrato de arrendamiento...&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;tenant_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;acme&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;updated_at&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1748934000000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;after&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;op&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;d&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;source&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;lsn&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">29824102&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;txId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1051&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;ts_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1748934060200&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El campo &lt;code>before&lt;/code> contiene el estado anterior de la fila —disponible porque Postgres puede configurar &lt;strong>REPLICA IDENTITY FULL&lt;/strong> para incluir la fila completa en el WAL al borrar/actualizar—. Sin esta configuración, &lt;code>before&lt;/code> solo contiene la clave primaria.&lt;/p>
&lt;p>&lt;strong>Esta es la clave para el pipeline RAG&lt;/strong>: el evento DELETE lleva el &lt;code>id&lt;/code> del documento. El consumer lo usa para borrar todos los chunks asociados en Qdrant con un filtro &lt;code>doc_id = 42&lt;/code>. Sin CDC, esos chunks nunca se habrían borrado.&lt;/p>
&lt;h3 id="snapshot-inicial">Snapshot inicial&lt;/h3>
&lt;p>Cuando el connector arranca por primera vez (o tras un reset), no puede empezar a consumir el WAL desde «el principio de los tiempos» —solo desde el momento en que se crea el slot—. ¿Cómo garantiza la consistencia del estado inicial?&lt;/p>
&lt;p>Mediante un &lt;strong>snapshot transaccional&lt;/strong>: el connector abre una transacción en modo &lt;code>REPEATABLE READ&lt;/code>, exporta el snapshot ID (&lt;code>pg_export_snapshot()&lt;/code>), y hace un &lt;code>SELECT&lt;/code> completo de las tablas configuradas dentro de esa transacción. Después empieza a consumir el WAL desde el LSN del snapshot. Así no hay gap: el snapshot cubre el estado hasta un instante, y el WAL cubre desde ese instante en adelante.&lt;/p>
&lt;h3 id="transformaciones-smt-single-message-transforms">Transformaciones SMT (Single Message Transforms)&lt;/h3>
&lt;p>Antes de emitir el evento al topic de Kafka, el connector puede aplicar transformaciones inline llamadas &lt;strong>SMT&lt;/strong>. Casos de uso habituales:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Filtrar columnas sensibles&lt;/strong> (&lt;code>ReplaceField&lt;/code> con &lt;code>blacklist&lt;/code>): eliminar &lt;code>password_hash&lt;/code>, &lt;code>phone_number&lt;/code> antes de que lleguen al topic.&lt;/li>
&lt;li>&lt;strong>Añadir metadata&lt;/strong> (&lt;code>InsertField&lt;/code>): enriquecer el evento con &lt;code>tenant_id&lt;/code> extraído del header HTTP original (si está en la fila).&lt;/li>
&lt;li>&lt;strong>Ruting condicional&lt;/strong> (&lt;code>Filter&lt;/code>): descartar eventos de filas con &lt;code>status = 'draft'&lt;/code> antes de emitirlos.&lt;/li>
&lt;/ul>
&lt;p>Las SMT son configuración pura —no requieren código— y se aplican dentro del proceso del connector, sin latencia adicional perceptible.&lt;/p>
&lt;hr>
&lt;h2 id="4-debezium-vs-outbox-pattern">4. Debezium vs Outbox pattern&lt;/h2>
&lt;p>El &lt;strong>Outbox pattern&lt;/strong> es la alternativa más común a CDC puro. La aplicación, en lugar de emitir eventos directamente a Kafka, escribe en una tabla &lt;code>outbox&lt;/code> de Postgres dentro de la misma transacción que modifica los datos. Un worker separado lee esa tabla y publica los eventos.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Criterio&lt;/th>
&lt;th>Debezium (CDC puro)&lt;/th>
&lt;th>Outbox pattern&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Latencia del evento&lt;/strong>&lt;/td>
&lt;td>~50-200 ms desde el commit&lt;/td>
&lt;td>Depende del intervalo del worker (típico: 1-5 s)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Consistencia&lt;/strong>&lt;/td>
&lt;td>At-least-once&lt;/td>
&lt;td>At-least-once&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Detección de DELETEs&lt;/strong>&lt;/td>
&lt;td>Nativa (el evento DELETE incluye &lt;code>before&lt;/code>)&lt;/td>
&lt;td>Solo si la app escribe en outbox al borrar&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Complejidad de setup&lt;/strong>&lt;/td>
&lt;td>Alta (Kafka Connect, slot de replicación, permisos)&lt;/td>
&lt;td>Baja (tabla extra + worker simple)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Dependencia de infraestructura&lt;/strong>&lt;/td>
&lt;td>Requiere Kafka/Redpanda/NATS JetStream&lt;/td>
&lt;td>Solo Postgres + worker; Kafka opcional&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Riesgo WAL disk blowup&lt;/strong>&lt;/td>
&lt;td>Sí, si el slot deja de consumir&lt;/td>
&lt;td>No&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Visibilidad del esquema&lt;/strong>&lt;/td>
&lt;td>Lee el esquema real de la tabla&lt;/td>
&lt;td>El esquema del evento lo define la app&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Migración de esquema&lt;/strong>&lt;/td>
&lt;td>Requiere cuidado (los eventos reflejan DDL changes)&lt;/td>
&lt;td>Más flexible (el evento es lo que la app pone)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Cuándo usarlo&lt;/strong>&lt;/td>
&lt;td>Cuando necesitas DELETEs, latencia baja o no puedes modificar la app&lt;/td>
&lt;td>Cuando la app controla el dominio del evento y la infraestructura es limitada&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Regla práctica:&lt;/strong> si controlas el código de la aplicación y no necesitas DELETEs nativos, el Outbox es más simple. Si no controlas el código (base de datos legacy, aplicación de terceros) o los DELETEs son críticos (pipeline RAG con borrado de documentos), Debezium es la elección correcta.&lt;/p>
&lt;hr>
&lt;h2 id="5-matemáticas">5. Matemáticas&lt;/h2>
&lt;h3 id="throughput">Throughput&lt;/h3>
&lt;p>Debezium en un connector con 4 workers puede procesar entre &lt;strong>10.000 y 50.000 eventos/segundo&lt;/strong> en hardware modesto (4 cores, 8 GB RAM). El cuello de botella real no es el connector sino el broker de Kafka: con 3 brokers y particiones adecuadas, Kafka puede sostener fácilmente 500.000 mensajes/segundo con mensajes de 1 KB (fuente: benchmarks públicos de Confluent, 2023).&lt;/p>
&lt;p>Para un pipeline RAG típico con 100 documentos modificados por minuto:&lt;/p>
&lt;p>$$\text{eventos/s} = \frac{100}{60} \approx 1{,}7 \text{ eventos/s}$$&lt;/p>
&lt;p>Esto es el 0,0034% de la capacidad del connector. Debezium no será el cuello de botella en ningún escenario RAG realista.&lt;/p>
&lt;h3 id="latencia-end-to-end">Latencia end-to-end&lt;/h3>
&lt;p>El camino de un commit en Postgres hasta un upsert en Qdrant tiene estas etapas:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Etapa&lt;/th>
&lt;th>Latencia típica&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Commit en Postgres → WAL escrito&lt;/td>
&lt;td>&amp;lt; 1 ms (sincrónico al commit)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>WAL escrito → Debezium lo lee (WAL lag)&lt;/td>
&lt;td>10-50 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Debezium → Kafka produce (ack)&lt;/td>
&lt;td>5-20 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Kafka → Consumer (poll interval)&lt;/td>
&lt;td>0-100 ms (configurable)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Consumer → Qdrant upsert/delete&lt;/td>
&lt;td>5-15 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Total típico&lt;/strong>&lt;/td>
&lt;td>&lt;strong>30-200 ms&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Con &lt;code>fetch.min.bytes=1&lt;/code> y &lt;code>fetch.max.wait.ms=10&lt;/code> en el consumer, la latencia del Kafka poll se reduce a ~10 ms. El rango realista para un pipeline optimizado es &lt;strong>30-100 ms&lt;/strong>.&lt;/p>
&lt;h3 id="riesgo-de-wal-disk-blowup">Riesgo de WAL disk blowup&lt;/h3>
&lt;p>Si el connector deja de consumir, Postgres retiene el WAL a partir del &lt;code>confirmed_flush_lsn&lt;/code> del slot. El volumen retenido crece linealmente con el tiempo y la tasa de escrituras:&lt;/p>
&lt;p>$$\text{WAL retenido} = \text{tasa de escrituras} \times \text{tamaño medio del evento WAL} \times \text{tiempo sin consumir}$$&lt;/p>
&lt;p>Ejemplo con carga moderada (50.000 escrituras/hora, 500 bytes de media por evento WAL):&lt;/p>
&lt;p>$$50{.}000 \times 500 \text{ B} \times 1 \text{ h} = 25 \text{ MB/h}$$&lt;/p>
&lt;p>Con carga alta (1.000.000 escrituras/hora):&lt;/p>
&lt;p>$$1{.}000{.}000 \times 500 \text{ B} \times 1 \text{ h} = 500 \text{ MB/h}$$&lt;/p>
&lt;p>Si el connector está caído durante 48 horas con carga alta: &lt;strong>24 GB de WAL retenido&lt;/strong>. Esto puede llenar el disco y bloquear completamente Postgres.&lt;/p>
&lt;p>&lt;strong>Alerta recomendada:&lt;/strong> configurar una alerta cuando &lt;code>lag_bytes &amp;gt; 1 GB&lt;/code> o cuando &lt;code>confirmed_flush_lsn&lt;/code> no avanza durante más de 15 minutos. Ver la query de monitorización en la sección 2.&lt;/p>
&lt;hr>
&lt;h2 id="6-casos-de-uso-en-llmops--rag">6. Casos de uso en LLMOps / RAG&lt;/h2>
&lt;h3 id="sincronización-rag-con-borrado-real">Sincronización RAG con borrado real&lt;/h3>
&lt;p>Este es el caso de uso que más claramente justifica Debezium sobre el polling. El flujo:&lt;/p>
&lt;ol>
&lt;li>Un usuario borra el documento &lt;code>id=42&lt;/code> de la interfaz de gestión documental.&lt;/li>
&lt;li>Postgres ejecuta &lt;code>DELETE FROM documents WHERE id = 42&lt;/code>.&lt;/li>
&lt;li>Debezium detecta el DELETE en el WAL, emite el evento con &lt;code>&amp;quot;op&amp;quot;: &amp;quot;d&amp;quot;&lt;/code> y &lt;code>&amp;quot;before&amp;quot;: {&amp;quot;id&amp;quot;: 42, ...}&lt;/code>.&lt;/li>
&lt;li>El consumer recibe el evento y ejecuta:
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">qdrant_client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">delete&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">collection_name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;documents&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">points_selector&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">Filter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">must&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">FieldCondition&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;doc_id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">match&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">MatchValue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">42&lt;/span>&lt;span class="p">))])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>Todos los chunks con &lt;code>doc_id=42&lt;/code> desaparecen de Qdrant en ~100 ms.&lt;/li>
&lt;/ol>
&lt;p>Sin Debezium, esos chunks permanecerían indefinidamente, contaminando los resultados de retrieval con fragmentos de documentos que ya no existen en la fuente de verdad.&lt;/p>
&lt;h3 id="event-sourcing-para-datasets-de-fine-tuning">Event sourcing para datasets de fine-tuning&lt;/h3>
&lt;p>Cada vez que un anotador humano actualiza una fila en la tabla &lt;code>annotations&lt;/code> (corrigiendo un output del LLM), Debezium emite el UPDATE con &lt;code>before&lt;/code> y &lt;code>after&lt;/code>. El consumer escribe el par (output_original, corrección) en el pipeline de curación de datasets, sin necesidad de que el anotador haga nada más allá de guardar en la interfaz. El pipeline de fine-tuning sabe exactamente qué cambió y cuándo —sin polling, sin riesgo de duplicados por ventanas de tiempo solapadas—.&lt;/p>
&lt;h3 id="audit-log-inmutable">Audit log inmutable&lt;/h3>
&lt;p>Los eventos del WAL son, por definición, el registro más fiel de lo que ocurrió en la base de datos —son los mismos datos que Postgres usa para crash recovery—. Kafka con retention larga (90 días, o retención por tamaño) sirve de &lt;strong>audit log inmutable&lt;/strong> sin modificar el esquema de la aplicación ni añadir triggers. Esto es especialmente útil en entornos regulados donde se requiere trazabilidad de modificaciones de datos.&lt;/p>
&lt;hr>
&lt;h2 id="7-diagrama-de-arquitectura">7. Diagrama de arquitectura&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 340" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Diagrama de arquitectura Debezium CDC: PostgreSQL, Debezium, Kafka, Consumer y Qdrant">
&lt;defs>
&lt;marker id="arrow-deb" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">&lt;path d="M0,0 L0,6 L8,3 z" fill="#64748b"/>&lt;/marker>
&lt;/defs>
&lt;rect x="0" y="0" width="820" height="340" fill="#f8f9fa" rx="8"/>
&lt;rect x="20" y="60" width="170" height="220" fill="#dbeafe" stroke="#3b82f6" stroke-width="1.5" rx="8"/>
&lt;text x="105" y="84" font-family="monospace" font-size="13" font-weight="bold" fill="#1e40af" text-anchor="middle">PostgreSQL&lt;/text>
&lt;rect x="36" y="96" width="138" height="52" fill="#bfdbfe" stroke="#3b82f6" stroke-width="1" rx="4"/>
&lt;text x="105" y="116" font-family="monospace" font-size="11" fill="#1e3a8a" text-anchor="middle">WAL&lt;/text>
&lt;text x="105" y="131" font-family="monospace" font-size="10" fill="#1e3a8a" text-anchor="middle">(libro del registro)&lt;/text>
&lt;rect x="36" y="160" width="138" height="52" fill="#bfdbfe" stroke="#3b82f6" stroke-width="1" rx="4"/>
&lt;text x="105" y="180" font-family="monospace" font-size="11" fill="#1e3a8a" text-anchor="middle">Slot replicación&lt;/text>
&lt;text x="105" y="195" font-family="monospace" font-size="10" fill="#1e3a8a" text-anchor="middle">(marcapáginas)&lt;/text>
&lt;rect x="36" y="224" width="138" height="40" fill="#bfdbfe" stroke="#3b82f6" stroke-width="1" rx="4"/>
&lt;text x="105" y="244" font-family="monospace" font-size="11" fill="#1e3a8a" text-anchor="middle">pgoutput&lt;/text>
&lt;text x="105" y="257" font-family="monospace" font-size="10" fill="#1e3a8a" text-anchor="middle">(decodificación)&lt;/text>
&lt;line x1="190" y1="170" x2="248" y2="170" stroke="#64748b" stroke-width="2" marker-end="url(#arrow-deb)"/>
&lt;text x="219" y="162" font-family="monospace" font-size="9" fill="#475569" text-anchor="middle">replicación&lt;/text>
&lt;text x="219" y="173" font-family="monospace" font-size="9" fill="#475569" text-anchor="middle">lógica&lt;/text>
&lt;rect x="248" y="110" width="160" height="120" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5" rx="8"/>
&lt;text x="328" y="134" font-family="monospace" font-size="13" font-weight="bold" fill="#14532d" text-anchor="middle">Debezium&lt;/text>
&lt;text x="328" y="151" font-family="monospace" font-size="10" fill="#14532d" text-anchor="middle">Connector&lt;/text>
&lt;text x="328" y="168" font-family="monospace" font-size="10" fill="#166534" text-anchor="middle">(Kafka Connect)&lt;/text>
&lt;rect x="264" y="178" width="112" height="38" fill="#bbf7d0" stroke="#16a34a" stroke-width="1" rx="4"/>
&lt;text x="320" y="193" font-family="monospace" font-size="9" fill="#14532d" text-anchor="middle">op: c / u / d&lt;/text>
&lt;text x="320" y="207" font-family="monospace" font-size="9" fill="#14532d" text-anchor="middle">before + after + LSN&lt;/text>
&lt;line x1="408" y1="170" x2="464" y2="170" stroke="#64748b" stroke-width="2" marker-end="url(#arrow-deb)"/>
&lt;text x="436" y="162" font-family="monospace" font-size="9" fill="#475569" text-anchor="middle">Kafka&lt;/text>
&lt;text x="436" y="173" font-family="monospace" font-size="9" fill="#475569" text-anchor="middle">Producer&lt;/text>
&lt;rect x="464" y="110" width="150" height="120" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5" rx="8"/>
&lt;text x="539" y="134" font-family="monospace" font-size="13" font-weight="bold" fill="#713f12" text-anchor="middle">Kafka&lt;/text>
&lt;text x="539" y="151" font-family="monospace" font-size="10" fill="#713f12" text-anchor="middle">topic:&lt;/text>
&lt;text x="539" y="165" font-family="monospace" font-size="9" fill="#92400e" text-anchor="middle">rag.public.documents&lt;/text>
&lt;rect x="478" y="178" width="122" height="38" fill="#fef08a" stroke="#ca8a04" stroke-width="1" rx="4"/>
&lt;text x="539" y="193" font-family="monospace" font-size="9" fill="#713f12" text-anchor="middle">retención configurable&lt;/text>
&lt;text x="539" y="207" font-family="monospace" font-size="9" fill="#713f12" text-anchor="middle">at-least-once&lt;/text>
&lt;line x1="614" y1="170" x2="668" y2="170" stroke="#64748b" stroke-width="2" marker-end="url(#arrow-deb)"/>
&lt;text x="641" y="162" font-family="monospace" font-size="9" fill="#475569" text-anchor="middle">Kafka&lt;/text>
&lt;text x="641" y="173" font-family="monospace" font-size="9" fill="#475569" text-anchor="middle">Consumer&lt;/text>
&lt;rect x="668" y="110" width="132" height="120" fill="#fce7f3" stroke="#db2777" stroke-width="1.5" rx="8"/>
&lt;text x="734" y="134" font-family="monospace" font-size="12" font-weight="bold" fill="#831843" text-anchor="middle">Consumer&lt;/text>
&lt;rect x="682" y="146" width="104" height="36" fill="#fbcfe8" stroke="#db2777" stroke-width="1" rx="4"/>
&lt;text x="734" y="162" font-family="monospace" font-size="9" fill="#831843" text-anchor="middle">INSERT/UPDATE&lt;/text>
&lt;text x="734" y="175" font-family="monospace" font-size="9" fill="#831843" text-anchor="middle">→ upsert Qdrant&lt;/text>
&lt;rect x="682" y="190" width="104" height="28" fill="#fbcfe8" stroke="#db2777" stroke-width="1" rx="4"/>
&lt;text x="734" y="204" font-family="monospace" font-size="9" fill="#831843" text-anchor="middle">DELETE&lt;/text>
&lt;text x="734" y="215" font-family="monospace" font-size="9" fill="#831843" text-anchor="middle">→ delete Qdrant&lt;/text>
&lt;line x1="734" y1="230" x2="734" y2="288" stroke="#64748b" stroke-width="2" marker-end="url(#arrow-deb)"/>
&lt;rect x="668" y="288" width="132" height="40" fill="#ede9fe" stroke="#7c3aed" stroke-width="1.5" rx="8"/>
&lt;text x="734" y="313" font-family="monospace" font-size="12" font-weight="bold" fill="#4c1d95" text-anchor="middle">Qdrant&lt;/text>
&lt;rect x="20" y="295" width="620" height="28" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1" rx="4"/>
&lt;text x="330" y="313" font-family="monospace" font-size="10" fill="#475569" text-anchor="middle">Latencia end-to-end típica: 30-100 ms desde commit en Postgres hasta upsert/delete en Qdrant&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="8-configuración-mínima">8. Configuración mínima&lt;/h2>
&lt;h3 id="postgresql-activar-replicación-lógica">PostgreSQL: activar replicación lógica&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- Requiere reiniciar Postgres
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">ALTER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SYSTEM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SET&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">wal_level&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">logical&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">ALTER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SYSTEM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SET&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">max_replication_slots&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">ALTER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SYSTEM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SET&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">max_wal_senders&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- Recargar configuración (wal_level requiere restart completo)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pg_reload_conf&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- Usuario dedicado para Debezium
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">USER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">debezium&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WITH&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">REPLICATION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LOGIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">PASSWORD&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;cambiar_esto&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GRANT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">public&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">documents&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">debezium&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- REPLICA IDENTITY FULL para tener &amp;#39;before&amp;#39; completo en DELETEs y UPDATEs
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">ALTER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">public&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">documents&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">REPLICA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IDENTITY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FULL&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="debezium-connector-kafka-connect-rest-api">Debezium connector (Kafka Connect REST API)&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;postgres-debezium&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;config&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;connector.class&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;io.debezium.connector.postgresql.PostgresConnector&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.hostname&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;postgres&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.port&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;5432&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.user&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;debezium&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.password&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;cambiar_esto&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.dbname&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;rag_db&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;topic.prefix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;rag&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;table.include.list&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;public.documents&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;plugin.name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;pgoutput&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;slot.name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;debezium_rag&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;publication.autocreate.mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;filtered&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;snapshot.mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;initial&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;tombstones.on.delete&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transforms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;unwrap&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transforms.unwrap.type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;io.debezium.transforms.ExtractNewRecordState&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transforms.unwrap.drop.tombstones&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;false&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transforms.unwrap.delete.handling.mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;rewrite&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Registrar el connector:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -X POST http://kafka-connect:8083/connectors &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s1">&amp;#39;Content-Type: application/json&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d @connector-config.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verificar estado:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl http://kafka-connect:8083/connectors/postgres-debezium/status
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="consumer-mínimo-en-python">Consumer mínimo en Python&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">confluent_kafka&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Consumer&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">qdrant_client&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">QdrantClient&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">qdrant_client.models&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Filter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">FieldCondition&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">MatchValue&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">json&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">consumer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Consumer&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;bootstrap.servers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;kafka:9092&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;group.id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;qdrant-sync&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;auto.offset.reset&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;earliest&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;enable.auto.commit&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">consumer&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">subscribe&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s2">&amp;#34;rag.public.documents&amp;#34;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">qdrant&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">QdrantClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;qdrant&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">port&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">6333&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">while&lt;/span> &lt;span class="kc">True&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">msg&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">consumer&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">poll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">timeout&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">0.1&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">msg&lt;/span> &lt;span class="ow">is&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">event&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loads&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">msg&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">op&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">event&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;op&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">op&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;c&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;u&amp;#34;&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="c1"># INSERT o UPDATE&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">doc&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">event&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;after&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># ... vectorizar y upsert en Qdrant&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">op&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;d&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="c1"># DELETE&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">doc_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">event&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;before&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">qdrant&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">delete&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">collection_name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;documents&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">points_selector&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">Filter&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">must&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">FieldCondition&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;doc_id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">match&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">MatchValue&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">doc_id&lt;/span>&lt;span class="p">))]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">consumer&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">commit&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="9-despliegue-on-premise">9. Despliegue on-premise&lt;/h2>
&lt;p>El stack Debezium no compite por GPU. En un nodo con &lt;strong>4×H100 SXM (320 GB, NVLink)&lt;/strong> sirviendo el LLM de inferencia, el pipeline CDC corre enteramente en nodos de propósito general (CPU-only):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Componente&lt;/th>
&lt;th>Recursos recomendados&lt;/th>
&lt;th>Rol&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Kafka Connect + Debezium&lt;/td>
&lt;td>2-4 cores, 4-8 GB RAM&lt;/td>
&lt;td>Leer WAL, emitir eventos&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Kafka brokers (×3)&lt;/td>
&lt;td>4 cores, 32 GB RAM c/u&lt;/td>
&lt;td>Alta disponibilidad, retención&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Consumer Qdrant-sync&lt;/td>
&lt;td>2 cores, 4 GB RAM&lt;/td>
&lt;td>Vectorizar + upsert/delete&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Qdrant&lt;/td>
&lt;td>8 cores, 64 GB RAM&lt;/td>
&lt;td>Vector store&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>El Debezium connector es notablemente ligero: en producción con 10.000 eventos/segundo, el connector consume habitualmente menos de 1 core y 2 GB de RAM. La memoria de la JVM (Kafka Connect corre en JVM) debe limitarse explícitamente con &lt;code>-Xmx4g&lt;/code> para evitar que el GC cause pausas.&lt;/p>
&lt;p>Para alta disponibilidad, Kafka Connect soporta modo &lt;strong>distribuido&lt;/strong> con múltiples workers. Si un worker cae, el connector se reasigna automáticamente a otro worker en segundos —el slot de replicación garantiza que no se pierden eventos durante la conmutación—.&lt;/p>
&lt;hr>
&lt;h2 id="lo-que-no-hemos-cubierto">Lo que no hemos cubierto&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Debezium con MySQL, MongoDB y Oracle&lt;/strong>: cada conector usa el mecanismo de log nativo (binlog en MySQL, oplog en MongoDB, LogMiner en Oracle). La API de eventos resultante es similar, pero los detalles de configuración y las limitaciones difieren.&lt;/li>
&lt;li>&lt;strong>Debezium Server&lt;/strong>: modo standalone sin Kafka Connect, con sinks directos a HTTP, S3, Redis Streams o NATS. Útil cuando la infraestructura de Kafka es demasiado compleja para el caso de uso.&lt;/li>
&lt;li>&lt;strong>Schema Registry&lt;/strong>: cómo Avro con Confluent Schema Registry o Apicurio gestiona la evolución del esquema de los eventos —añadir columnas, cambiar tipos— sin romper a los consumers existentes.&lt;/li>
&lt;li>&lt;strong>Exactly-once semantics&lt;/strong>: por qué at-least-once es suficiente para la mayoría de casos RAG (un upsert idempotente en Qdrant con el mismo vector no hace daño) y cuándo se necesita exactly-once (contadores financieros, deducciones de inventario).&lt;/li>
&lt;li>&lt;strong>Outbox pattern + Debezium combinados&lt;/strong>: Debezium leyendo la tabla &lt;code>outbox&lt;/code> en lugar del WAL de la tabla de negocio directamente —el patrón Transactional Outbox + CDC que combina lo mejor de ambos mundos—.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/postgresql-qdrant-ingestion-microservicios/">PostgreSQL + Qdrant: ingestión por microservicios&lt;/a> — el post donde CDC con Debezium se usa como alternativa al outbox pattern para mantener sincronizados PostgreSQL y Qdrant.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/rag-corpus-curation-fundamentos/">RAG corpus curation: fundamentos&lt;/a> — la curación del corpus que Debezium mantiene fresco en near-real-time.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">Pipeline LLMOps: las seis etapas&lt;/a> — la etapa Data del mapa maestro donde CDC es el mecanismo de ingestión continua.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/data-versioning-dvc-lakefs/">Data versioning con DVC y lakeFS&lt;/a> — versioning del corpus que Debezium alimenta incrementalmente.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/observabilidad-gpu-dcgm-llm/">Observabilidad GPU con DCGM y LLM&lt;/a> — monitorización del cluster donde corre el consumer de Debezium junto al stack de inferencia.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ol>
&lt;li>Debezium Documentation — PostgreSQL Connector. &lt;a href="https://debezium.io/documentation/reference/stable/connectors/postgresql.html">debezium.io/documentation/reference/stable/connectors/postgresql.html&lt;/a>&lt;/li>
&lt;li>PostgreSQL Documentation — Logical Replication. &lt;a href="https://www.postgresql.org/docs/current/logical-replication.html">postgresql.org/docs/current/logical-replication.html&lt;/a>&lt;/li>
&lt;li>PostgreSQL Documentation — Write-Ahead Logging. &lt;a href="https://www.postgresql.org/docs/current/wal-intro.html">postgresql.org/docs/current/wal-intro.html&lt;/a>&lt;/li>
&lt;li>PostgreSQL Documentation — Replication Slots. &lt;a href="https://www.postgresql.org/docs/current/logicaldecoding-explanation.html">postgresql.org/docs/current/logicaldecoding-explanation.html&lt;/a>&lt;/li>
&lt;li>Confluent — Kafka Performance Benchmarks (2023). &lt;a href="https://www.confluent.io/blog/kafka-fastest-messaging-system/">confluent.io/blog/kafka-fastest-messaging-system&lt;/a>&lt;/li>
&lt;li>Gunnar Morling — Outbox Pattern. &lt;a href="https://www.morling.dev/blog/sending-messages-as-part-of-database-transactions/">morling.dev/blog/sending-messages-as-part-of-database-transactions&lt;/a>&lt;/li>
&lt;li>Debezium — SMT documentation. &lt;a href="https://debezium.io/documentation/reference/stable/transformations/">debezium.io/documentation/reference/stable/transformations&lt;/a>&lt;/li>
&lt;li>Qdrant Documentation — Filtering. &lt;a href="https://qdrant.tech/documentation/concepts/filtering/">qdrant.tech/documentation/concepts/filtering&lt;/a>&lt;/li>
&lt;/ol></description></item></channel></rss>