Esta página foi traduzida por máquina. Leia o original em inglês. English

← Recursos do servidor HQbird

fb_streaming — Firebird replication log streaming

fb_streaming is a service (Windows) or daemon (Linux) that reads archived Firebird replication log segments, decodes them into events, and gives the events to a plugin. The plugin sends committed changes to an external system: JSON files, Kafka, RabbitMQ, MongoDB, or Lucene full-text indexes. The database and its applications do not change. The service works only with the replication journal. Main description: fb_streaming.md.

Asynchronous
Works on archived segments after Firebird writes them. No triggers, no load on user transactions.
Transaction-aware
Changes are buffered per transaction and savepoint. Only committed work is published.
Crash-safe
A control file stores the position and open transactions. After a restart, the service replays them.
Multi-task
One service instance runs many tasks. Each task has its own thread and its own source folder.
Pluggable
Plugins are shared libraries (.dll / lib*.so). You can write your own in C++ or Delphi.
Diagnostics
fb_repl_print prints any segment in readable form, with record values and offsets.

2. Data flow

Firebird writes changes of published tables to the replication journal. When a segment is full (or after journal_archive_timeout), Firebird archives it. The journal_archive_command can copy the archived segment to an extra folder. Each fb_streaming task watches one folder. See “How it works?” in fb_streaming.md.

Applications INSERT / UPDATE / DDL Firebird publication on journal_directory archive → replica (optional) task folder copy of each segment fb_streaming segment parser control file plugin Firebird connection reads record formats (RDB$FORMATS) JSON filessimple_json_plugin Apache Kafkakafka_cdc_plugin RabbitMQrabbitmq_plugin MongoDBmongodb_events_plugin Lucene FTS indexesfts_lucene_plugin journal_archive_command && copy $(pathname) <taskDir>
One task uses one plugin. To feed several targets, give each task its own copy of the archive folder.
Processing loop of one task
  1. Scan the task folder for unprocessed segments (state archive).
  2. Parse each segment block by block and raise events.
  3. The plugin buffers transactional events and publishes them on COMMIT.
  4. Write the new position to the control file.
  5. Delete segments that are no longer needed (if deleteProcessedFile = true).
  6. On error: log it, wait errorTimeout seconds (60 by default), rescan, restart the task.
What the service needs
  • Firebird 4.0 or higher (HQbird) with replication publication on.
  • Archived segments in the task folder. Active segments are not read.
  • A database connection (database, username, password). The service reads table record formats to decode records.
  • The plugin library in stream_plugins (or pluginDir).

3. Replication events

The parser raises these events. Each plugin decides which events it uses and which it ignores. The same list is in fb_streaming.md and fb_repl_print.md.

STARTPREPARECOMMITROLLBACK SAVERELEASEUNDO INSERTUPDATEDELETE EXECUTE SQL (DDL only)SET SEQUENCEBLOB
GroupEventsNotes
TransactionSTART, PREPARE (2PC phase 1), COMMIT, ROLLBACKEach has a transaction number (tnx). A transaction can start in one segment and end in another.
SavepointSAVE, RELEASE, UNDO (rollback to savepoint)Firebird creates savepoints for each statement, so you see them often.
DataINSERT, UPDATE, DELETEINSERT has the new record. UPDATE has old and new records. DELETE has the old record.
BLOBBLOB (store BLOB)Written before the record that points to it. Only new BLOB values are in the log.
OtherEXECUTE SQL, SET SEQUENCEEXECUTE SQL appears only for DDL. SET SEQUENCE has no transaction: generators work outside transactions.

4. Transaction buffering (interactive)

The message plugins (Kafka, RabbitMQ, MongoDB, Lucene) keep one buffer per active transaction. Inside the buffer there is a stack of savepoints. Documents go to the top savepoint. RELEASE moves documents one level down. UNDO drops the level and its documents. COMMIT sends everything. ROLLBACK drops everything. SET SEQUENCE does not wait: it is sent at once. Algorithms: rabbitmq_plugin.md, mongodb_events_plugin.md, fts_lucene_plugin.md.

Replication segment (transaction 1937)
    Plugin memory
    no active transaction
    Target (Kafka topic / RabbitMQ exchange / MongoDB collection)
    Press Play or Step.

    5. Control file and crash recovery

    For each task the service keeps a control file named <database GUID>. It is created in controlFileDir (default: the task folder). Source: fb_streaming.md. It holds:

    • control data (database GUID, size, and so on);
    • the last processed segment and the position in it;
    • the segment with the oldest active transaction;
    • the list of active transactions as pairs {tnxNumber, segmentNumber} (segment where each started).

    Delete rule: a segment is deleted only when its number is lower than the segment of the oldest active transaction. Restart rule: after a crash, the service rereads segments from the oldest-active-transaction segment up to the last processed one, replays the events of the open transactions, and then goes on as usual.

    Blue = processed, orange ring = current, violet = replay after restart, faded = deleted.
    Control file
    What happens
    Press Play scenario.

    6. Configuration

    replication.conf (Firebird side)

    Turn on the journal and archive for the database. A task folder must be used by only one task, and processed segments are deleted. So, if you also run a replica or more tasks, copy each archived segment to one folder per consumer:

    database = d:\fbdata\5.0\test.fdb
    {
       journal_directory         = d:\fbdata\5.0\replication\test\journal
       journal_archive_directory = d:\fbdata\5.0\replication\test\archive
       journal_archive_command   = "copy $(pathname) $(archivepathname) && copy $(pathname) d:\fbdata\5.0\replication\test\kafka_source"
       journal_archive_timeout   = 10
    }

    If the journal is used only for streaming, set journal_archive_directory straight to the task folder and omit the command. Then publish the tables:

    ALTER DATABASE INCLUDE ALL TO PUBLICATION;      -- or: INCLUDE CUSTOMERS TO PUBLICATION
    ALTER DATABASE ENABLE PUBLICATION;
    journal segment TEST.FDB.journal-000000042 archive → replica kafka_source → task 1 archive_rabbit → task 2 applied by the replica server deleted by task 1 when done deleted by task 2 when done

    fb_streaming.conf (service side)

    The file sits next to the executable. Global settings come first, then one task block per source folder. Full parameter list: fb_streaming.md (“Configuring the service”) and the plugin documents below.

    # logLevel  = info                     # trace, debug, info, warning, error, critical, off
    # pluginDir = $(root)/stream_plugins   # $(root) = service folder
    
    task = d:\fbdata\5.0\replication\test\kafka_source
    {
        database            = inet://localhost:3055/test
        username            = SYSDBA
        password            = masterkey
        plugin              = kafka_cdc_plugin
        deleteProcessedFile = true
        # controlFileDir    = ...            # default: the task folder
        # errorTimeout      = 60
        # plugin-specific parameters follow
    }
    ParameterScopeMeaningDefault
    logLevelglobalLog level of the serviceinfo
    pluginDirglobalFolder with plugin libraries$(root)/stream_plugins
    database *taskConnection string to the source database—
    username, passwordtaskCredentials for the connection—
    plugin *taskPlugin name (name.dll / libname.so)—
    controlFileDirtaskWhere the control file livestask folder
    errorTimeouttaskSeconds to wait after an error before restart60
    deleteProcessedFiletaskDelete segments after processing (set false to debug)true
    warn_tnx_not_foundtask (Kafka)Warn and skip, instead of error, if a transaction start is missingfalse

    * required. Common plugin filters: include_tables / exclude_tables (regular expressions), dumpBlobs, register_ddl_events, register_sequence_events, charset_for_none_fields.

    Install and run

    Windows Administrator
    fb_streaming install [service_name]
    fb_streaming start   [service_name]
    fb_streaming stop    [service_name]
    fb_streaming remove  [service_name]
    Several services on one host need unique names. Run fb_streaming with no arguments to test it as a console app (Enter stops it).
    Linux /opt/fb_streaming
    sudo systemctl enable fb_streaming
    sudo systemctl start  fb_streaming
    sudo systemctl stop    fb_streaming
    sudo systemctl disable fb_streaming
    Kafka plugin on Linux needs librdkafka1 and liblz4-1 from the package manager (see kafka_cdc_plugin.md, “Requirements”).

    7. Plugins

    PluginTargetFormatPublishesWhen
    simple_json_pluginFolder (outputDir)One .json per segmentAll events, incl. savepointsPer segment
    kafka_cdc_pluginKafka topicsDebezium-like JSON, optional schemaINSERT / UPDATE / DELETE (+ tx BEGIN/END)On COMMIT
    rabbitmq_pluginRabbitMQ exchangeJSON message per eventDML, DDL, SET SEQUENCEOn COMMIT (sequences at once)
    mongodb_events_pluginMongoDB events collectionBSON document per eventDML, DDL, SET SEQUENCEOn COMMIT (sequences at once)
    fts_lucene_pluginLucene FTS indexes (UDR)Index documentsDML on indexed tablesOn COMMIT

    simple_json_plugin — segment to JSON file

    Demo plugin shipped with the service. It turns each binary segment into a JSON file with the same name plus .json. Full description: simple_json_plugin.md. The root object has header (guid, sequence, state, version) and an events array.

    {
      "header": { "version": 1, "sequence": 1, "state": "archive",
                  "guid": "{F396449D-F6E4-4812-875E-248AB7C2BEE7}" },
      "events": [
        { "event": "START TRANSACTION", "tnx": 1937 },
        { "event": "SAVEPOINT", "tnx": 1937 },
        { "event": "UPDATE", "table": "CUSTOMER", "tnx": 1937,
          "changedFields": ["ADDRESS"],
          "oldRecord": { "CUSTOMER_ID": 8, "ADDRESS": null, ... },
          "record":    { "CUSTOMER_ID": 8, "ADDRESS": "555", ... } },
        { "event": "RELEASE SAVEPOINT", "tnx": 1937 },
        { "event": "COMMIT", "tnx": 1937 }
      ]
    }
    Event fields
    • event, tnx (not for SET SEQUENCE)
    • table, record, oldRecord, changedFields — DML
    • sql — EXECUTE SQL
    • sequence, value — SET SEQUENCE
    • blobId, data (hex) — STORE BLOB
    Parameters: outputDir (required), dumpBlobs, register_ddl_events, register_sequence_events, include_tables, exclude_tables, charset_for_none_fields.

    kafka_cdc_plugin — Change Data Capture to Kafka

    Writes row-level change events in a format close to Debezium. Events are buffered per transaction and sent on commit. Each event has a key (primary key, or unique key if no PK) and a value (an envelope with before / after). Full description with schemas, all kafka_* options and a Docker Kafka setup: kafka_cdc_plugin.md.

    Kafka message KEY schemafbstreaming.CUSTOMERS.Key only if key_converter_schemas_enable payload{ "ID": 1 } used by Kafka for partitioning Kafka message VALUE schema…CUSTOMERS.Envelope only if value_converter_schemas_enable payload (envelope) before after op c/u/d source: dbguid, sequence, filename, table, tnx, ts_ms (file mtime) ts_ms (sent) transaction (opt.) lag ≈ payload.ts_ms − payload.source.ts_ms
    Event payload by operation
    { "before": null,
      "after":  { "ID": 1, "FIRST_NAME": "Anne", "LAST_NAME": "Kretchmar", "EMAIL": "[email protected]" },
      "source": { "dbguid": "{9D66A972-...}", "sequence": 1, "filename": "TEST.FDB.journal-000000001",
                  "table": "CUSTOMERS", "tnx": 200, "ts_ms": 1711288254908 },
      "op": "c", "ts_ms": 1711288255056 }
    { "before": { "ID": 1, "FIRST_NAME": "Anne",       ... },
      "after":  { "ID": 1, "FIRST_NAME": "Anne Marie", ... },
      "source": { ..., "sequence": 2, "tnx": 219 },
      "op": "u", "ts_ms": 1711288256121 }
    { "before": { "ID": 1, "FIRST_NAME": "Anne Marie", ... },
      "after":  null,
      "source": { ..., "sequence": 3, "tnx": 258 },
      "op": "d", "ts_ms": 1711288256152 }
    // topic $[topicPrefix].transaction  (transaction_metadata_enable = true)
    { "status": "BEGIN", "id": 901, "ts_ms": 1713099401529, "event_count": null, "data_collections": null }
    { "status": "END",   "id": 901, "ts_ms": 1713099404605, "event_count": 2,
      "data_collections": [ { "data_collection": "fbstreaming.cdc.CUSTOMERS", "event_count": 2 } ] }
    
    // each change event then also gets:
    "transaction": { "id": 901, "total_order": 1, "data_collection_order": 1 }
    Topic names
    • kafka_topic_prefix sets $[topicPrefix] (default fb_streaming).
    • kafka_topic_cdc_event = $[topicPrefix].cdc → all tables in one topic.
    • $[topicPrefix].cdc.$[tableName] → one topic per table.
    • kafka_topic_transaction = $[topicPrefix].transaction.
    Main parameters
    • kafka_brokers — comma-separated list.
    • key_cdc_events_enable — send keys (else key is null).
    • key_converter_schemas_enable, value_converter_schemas_enable.
    • transaction_metadata_enable — BEGIN/END events.
    • async_producer — do not wait for delivery ack (buffer 500).
    • kafka_* — librdkafka options, dots replaced by _: SSL, SASL (64-bit only), OAUTHBEARER/OIDC.
    Firebird → Kafka type mapping
    Firebird typeJSON typeNotes
    BOOLEANboolean
    SMALLINT / INTEGER / BIGINTint16 / int32 / int64
    FLOAT / DOUBLE PRECISIONfloat / double
    INT128, NUMERIC, DECIMAL, DECFLOATstringExact value as text
    CHAR, VARCHARstringNONE charset: see charset_for_none_fields
    BINARY, VARBINARYstringHex, two chars per byte
    DATE / TIME / TIMESTAMP [WITH TIME ZONE]stringY-M-D, HH24:MI:SS.F (F = 1/10000 s), plus zone name
    BLOB SUB_TYPE TEXT / 0stringbefore is always null; binary as hex

    rabbitmq_plugin — events to a RabbitMQ exchange

    Each event is a JSON message sent to exchange_name. With exchange_type = fanout (default) every bound queue gets it; with direct / topic the routing_key decides. If queue_name is set, the queue is declared and bound. The exchange is created if it does not exist. Full description: rabbitmq_plugin.md.

    task = d:\fbdata\4.0\replication\horses\archive_rabbit
    {
        database = inet://localhost:3054/d:\fbdata\4.0\horses.fdb
        username = SYSDBA
        password = masterkey
        plugin   = rabbitmq_plugin
        rabbit_uri    = amqp://guest:guest@localhost
        exchange_name = horses
        exchange_type = direct
        queue_name    = horses_events
        routing_key   = 123
        dumpBlobs = true
    }
    { "event": "UPDATE", "table": "COLOR", "tnx": 11771, "changedFields": ["NAME_DE"],
      "oldRecord": { "CODE_COLOR": 14, "NAME_DE": "",  ... },
      "record":    { "CODE_COLOR": 14, "NAME_DE": "g", ... } }
    { "event": "SET SEQUENCE", "sequence": "SEQ1", "value": 1 }
    { "event": "EXECUTE SQL", "sql": "CREATE SEQUENCE SEQ1", "tnx": 6590 }

    mongodb_events_plugin — events to MongoDB

    Same event model and same buffering as RabbitMQ. Each event becomes a document in the events collection of mongo_database (created on first write). Parameters: mongo_uri (for example mongodb://localhost:27017), mongo_database, plus the common filters. Full description: mongodb_events_plugin.md.

    use horses
    db.events.find()
    { _id: ObjectId("638f37f9022b0000ad005779"), event: 'INSERT', table: 'TABLE1', tnx: 6597, record: { ID: 1, S: 'Hello' } }

    fts_lucene_plugin — keep full-text indexes current

    Updates indexes made by IBSurgeon Full Text Search UDR. On each iteration the plugin rereads index metadata, takes only active indexes, and filters DML events by their tables. On COMMIT it adds, updates, or deletes index documents. Full description: fts_lucene_plugin.md.

    INSERT
    New document per index. Skipped if all indexed fields are empty.
    UPDATE
    Skipped if no indexed field changed. Deleted from index if all indexed fields are now empty.
    DELETE
    Key is saved; the document is removed on commit.
    Not supported: indexes keyed by RDB$DB_KEY (not in the log) and indexes built on views.

    8. fb_repl_print utility

    Prints a binary segment in readable form. Use it to check what a segment holds, or to find the record that broke replication. Full description: fb_repl_print.md.

    fb_repl_print [file_path] <options>
      -i, --input-file  file      segment file (or first argument)
      -H, --print-header          segment header: version, GUID, sequence, state, length
      -B, --print-blobs           BLOB contents in hex
      -R, --dump-records          record values as JSON (needs --database)
      -F, --print-format          record format: field names and types (needs -R)
      -d, --database / -u, --username / -p, --password
      -c, --charset-none-fields   charset for NONE fields
    BLOCK (offset: 48)
    [200] START (offset: 64)
    [200] SAVE (offset: 65)
    [200] SAVE (offset: 66)
    [200] INSERT CUSTOMERS (length: 3082) (offset: 78)
    [200] RELEASE (offset: 3169)
    [200] RELEASE (offset: 3170)
    BLOCK (offset: 3171)
    [] SET SEQUENCE RDB$2 = 1 (offset: 3194)
    [200] COMMIT (offset: 3207)
    • [200] — transaction number. [] — outside a transaction.
    • offset — byte offset in the segment file.
    • Records are stored in table format. To decode them, -R connects to the database (UTF8) and reads RDB$FORMATS. If the table or format is gone (for example after DROP TABLE), it prints a warning.
    • Output is UTF8. On Windows run chcp 65001 first.
    replication.log on replica FOREIGN KEY violation … At segment 106, offset 130012 fb_repl_print …journal-000000106 -B -R -d inet://localhost:mydb record at offset INSERT TRIAL_LINE "CODE_HORSE": 12345

    9. Writing your own plugin

    A plugin is a shared library in C++ or Delphi. It implements three interfaces and exports the entry point fb_stream_plugin, which registers a factory by name. Helper templates with the Impl suffix reduce boilerplate. Full examples: IBSurgeon/FBStreamingExamples.

    IStreamPluginFactory
    createPlugin(config, encodeUtils, logger)
    IStreamPlugin
    init, finish, startSegment, finishSegment, startBlock, setSegmentOffset, startTransaction, getTransaction, setSequence, matchTable, cleanupTransaction(s), log
    IStreamedTransaction
    prepare, commit, rollback, start/release/rollbackSavepoint, insertRecord, updateRecord, deleteRecord, executeSql[Intl], storeBlob

    Records come as IStreamedRecord (getCount, getField, raw data). Fields are IStreamedField (getName, getType, getScale, getCharSet, getData, isKey, keyPosition, isNullable). IStreamLogger writes to the service log. Every function is described in writing_plugin.md.

    fb_streaming core IStreamPlugin IStreamedTransaction factory.createPlugin() → init(attachment) startSegment(header) startBlock / setSegmentOffset startTransaction(tnx) creates matchTable(name)? startSavepoint / insertRecord / updateRecord / … setSequence(name, value) commit() → publish buffered events finishSegment → control file saved finish()
    On restart the core calls getTransaction(tnx) for transactions continued from an earlier segment, and cleanupTransaction(s) to free them.
    extern "C" FB_DLL_EXPORT void fb_stream_plugin(IMaster* master, IStreamPluginManager* pm)
    {
        auto factory = new MyPlugin::MyStreamPluginFactory(master);
        pm->registerPluginFactory("my_plugin", factory);   // plugin = my_plugin in fb_streaming.conf
    }

    10. Limits and rules

    Archived segments only. The service does not read segments that Firebird still writes. Use journal_archive_timeout to limit the delay.
    One folder, one task. Processed segments are deleted. Give each consumer (replica, each task) its own copy.
    Old BLOB values are not in the log. Only the BLOB ID of old records is known. dumpBlobs publishes new values only.
    Event time. The log has no timestamps. source.ts_ms is the segment file modification time.
    NONE charset. JSON needs UTF-8. Set charset_for_none_fields to the real encoding of such columns.
    Record formats. Firebird 4+ logs have no logical record format. The service reads formats from the database, so a dropped table cannot be decoded.
    Lost transaction start. If a START is missing (segments deleted by hand), the task stops with an error. Kafka: warn_tnx_not_found = true logs a warning and skips it.
    Kafka delivery. async_producer = true is faster but does not wait for broker confirmation (buffer 500 messages).

    Further reading

    Source documents in this folder (Markdown):

    DocumentRussianContent
    fb_streaming.mdRUService (daemon): how it works, control file, install, configuration
    kafka_cdc_plugin.mdRUKafka CDC plugin: event format, schemas, type mapping, SSL/SASL options, Docker setup
    rabbitmq_plugin.mdRURabbitMQ plugin: algorithm, exchange and queue settings, message examples
    mongodb_events_plugin.mdRUMongoDB plugin: algorithm, settings, events collection example
    simple_json_plugin.mdRUJSON plugin: file format and settings
    fts_lucene_plugin.mdRULucene full-text index plugin: algorithm and setup
    fb_repl_print.mdRUfb_repl_print utility: options, output, finding replication errors
    writing_plugin.mdRUPlugin SDK: all interfaces and functions (C++ / Delphi)
    kafka_cdc_plugin.adocRUKafka CDC plugin, AsciiDoc version

    External: FBStreamingExamples (plugin source examples), lucene_udr (Full Text Search UDR), Debezium (event format reference).