maxwell(1.44.0)暂停监听某些表的测试

【 ‌infrastructure】【数据中心】【AI infra】第十篇 智能计算数据中心解决方案集成测试和交付知识体系01 编号类型领域模块子模块学科知识点及详细数学建模与数值设计(推导/边界/示例)关联知识、标准、法律法规、论文、工业实践1.1架构智算集成总体拓扑业务/管理/数据/参数面分离系统架构、队列论、网络流四平面隔离:业务面带宽B_b、数据面B_d(对象/数仓ETL)、参数面B_p(NCCL AllReduce)、管理面B_m(Prometheus/API)。总成本C=Σ(N_server·C_s+N_sw·C_sw+N_dpu·C_dpu)+C_power;约束B_p≥模型梯度量G/t_step。示例:千卡A100、 阅读详情

maxwell(1.44.0)暂停监听某些表的测试

在阅读本文档之前,建议先阅读网博主之前编写的部署Maxwell文档,以便更好地理解本文档Maxwell(1.44.0)部署文档

一、准备工作

1.1编写配置文档(config.properties)

# MySQL 配置
host=xx.xx.xx.xxx
user=maxwell
password=xxxxxx

# Kafka 生产者配置
producer=kafka
kafka.bootstrap.servers=xx.xx.xx.xx1:9092,xx.xx.xx.xx2:9092,xx.xx.xx.xx3:9092

# ✅ 动态 topic 命名规则:maxwell_t_{generator}
kafka_topic=maxwell

# ✅ 使用正则过滤 + 替换规则,将数据库名映射为 generator 名
# filter=exclude:*.*,include:db_enterprise*.t_enterprise_*:maxwell_t_enterprise,include:db_business*.t_general_taxpayer_*:maxwell_t_taxpayer,include:db_business*.t_creditimportexport_data_*:maxwell_t_creditimportexport,include:db_code*.t_econ_kind_code_*:maxwell_t_econ,include:db_sub_enterprises*.t_last_industry_*:maxwell_t_last_industry,include:db_code*.t_industry_code_*:maxwell_t_industry,include:db_enterprise*.t_address_*:maxwell_t_address,include:db_enterprise*.t_history_names_*:maxwell_t_history_names,include:db_enterprise*.t_emails_*:maxwell_t_emails,include:db_enterprise_reports*.t_report_details_*:maxwell_t_report_details
javascript=/home/bigdata/filter.js


# Kafka 优化参数
producer_kafka_batch_size=163840
producer_kafka_linger_ms=20
producer_kafka_buffer_memory=67108864
producer_kafka_compression_type=lz4

# Maxwell 核心优化
producer_async=true
worker_threads=4
maxwell.buffer.size=10000
output_nulls=false
producer_partition_by=primary_key

# 高可用建议配置
producer_kafka_max_in_flight_requests_per_connection=5
producer_kafka_acks=all
producer_kafka_retries=10
producer_kafka_enable_idempotence=true
配置项数值作用说明
producer_kafka_batch_size163 840 B ≈ 160 KB每批最多攒 160 KB 再一起发给 Kafka,提高吞吐;与 linger_ms 配合控制延迟。
producer_kafka_linger_ms20 ms即使批次没满,最多等待 20 ms 就发送,平衡延迟与吞吐。
producer_kafka_buffer_memory67108 864 B ≈ 64 MBKafka Producer 客户端总缓存上限,用来暂存未发出去的消息。
producer_kafka_compression_typelz4消息体使用 LZ4 压缩,显著降低网络 IO 与磁盘占用,压缩/解压速度优于 GZIP。
producer_asynctrueMaxwell 采用异步模式生产消息,批量提交,提高性能;为 false 时每条同步等待。
worker_threads4Maxwell 内部使用 4 条线程并行读取 binlog 事件并转换,加快处理速度。
maxwell.buffer.size10 000内部环形队列最大缓存 10 000 条事件,防止瞬间峰值把内存打爆。
output_nullsfalse字段值为 NULL 时不输出到 JSON,减少消息体积;为 true 时显式输出 "col": null。
producer_partition_byprimary_key按主键哈希选分区,保证同一主键始终进入同一分区,下游可有序消费。
producer_kafka_max_in_flight_requests_per_connection5单个 TCP 连接最多同时发送 5 个请求,提高吞吐;配合幂等设为 ≤5。
producer_kafka_acksall等待所有 ISR 副本确认才认为发送成功,最高级别持久化保障。
producer_kafka_retries10可重试错误(如瞬时网络抖动)自动重试 10 次,避免数据丢失。
producer_kafka_enable_idempotencetrue开启幂等生产者,重试时不会重复写消息,实现端到端精确一次(EOS)。

1.2 编写filter.js文件(对于多库多表的存储,映射到同一个topic)

背景介绍:为何使用 filter.js 进行 Maxwell 动态路由清洗

1.2.1 业务现状:分库分表架构 (Data Sharding)

在当前的大数据架构中,为了应对海量数据的存储与高并发写入,业务数据库采用了分库分表(Sharding)的策略。
根据配置文件中的 filter 规则可以看出,我们的业务数据分散在多个逻辑库和物理表中。例如:

  • 企业数据:分散在 db_enterprise_01, db_enterprise_02 … 等多个库中,表名可能为 t_enterprise_2023, t_enterprise_2024 等。
  • 税务数据:分散在 db_business 系列库中。
  • 代码表数据:分散在 db_code 系列库中。
1.2.2 痛点:静态配置的局限性 (Configuration Complexity)

如果不使用脚本处理,仅依赖 Maxwell 配置文件中的 filter 参数(即配置中被注释掉的那一行长字符串),我们会面临以下严峻挑战:

  • 配置难以维护:随着业务扩展,库表数量增加,filter 规则会变得极度冗长且难以阅读(如您配置中所示,一行规则包含了数十个正则匹配)。
  • 灵活性差:静态配置很难处理复杂的逻辑。例如,“将所有 db_enterprise* 库下的 t_enterprise* 表的数据,统一发送到 Kafka 的 maxwell_t_enterprise 主题中”。
  • Topic 爆炸:如果不做重命名映射,Maxwell 默认可能会为每个物理表生成不同的标识,导致下游 Kafka Topic 数量激增,增加了消费端(Flink/Spark)的处理复杂度。
1.2.3 解决方案:引入 JavaScript 动态过滤器 (Programmable Routing)

通过配置 javascript=/home/bigdata/filter.js,我们启用了 Maxwell 的编程接口。这允许我们在数据进入 Kafka 之前,对 Binlog 数据进行行级拦截和处理。

核心价值在于:

  1. 多对一映射(Data Aggregation):
    可以将分散在成百上千个物理分片表(如 db_enterprise_01.t_enterprise_001)的数据,在传输层直接“清洗”为统一的逻辑表名(如 maxwell_t_enterprise),并发送到同一个 Kafka Topic。
  2. 动态 Topic 路由(Dynamic Topic Routing):
    无需在配置文件中写死 Topic 名称。filter.js 可以根据数据库名或表名的正则特征,动态决定该条数据应该发往哪个 Topic。这完美契合配置中提到的 maxwell_t_{generator} 命名规则。
  3. 数据清洗与脱敏(ETL at Source):
    虽然主要目的是路由,但 JS 文件同时也提供了在源头过滤无用字段(如删除 create_time 或大字段)或对敏感数据进行脱敏的能力,减轻下游计算压力。
1.2.4 总结

编写 filter.js 是为了屏蔽上游分库分表的物理差异,向下游数据仓库提供一个统一、逻辑化的数据视图。它将复杂的正则匹配和重命名逻辑从配置文件中解耦出来,极大地提升了数据同步链路的可维护性和扩展性。

这里主要是添加黑名单,指定不监听某些表(加入blacklist,这里举例是db_test.t_bidding_content):

/**
 * Maxwell row-level filter
 * 投递规则:
 *   1. 老库 + 老表
 *   2. db_test 库下的新表(含 user)
 */
function process_row(row) {
    // 【新增】黑名单控制区
    // 暂停监听的表(格式:数据库名.表名)
    var blacklist = [
        "db_test.t_bidding_content"
    ];

    // 获取当前数据的完整表名
    var fullTableName = row.database + "." + row.table;

    // 如果在黑名单中,直接丢弃,不处理
    if (blacklist.indexOf(fullTableName) !== -1) {
        row.suppress();
        return; // 直接返回,结束函数
    }

    /* ---------- 1. 老库老表 ---------- */
    var dbRegOld = /^db_(enterprise|business|code|sub_enterprises|enterprise_reports)\d+$/;
    var tblRegOld = /^t_(enterprise|general_taxpayer|creditimportexport_data|econ_kind_code|last_industry|industry_code|address|history_names|emails|report_details)_\d+$/;

    /* ---------- 2. 新库新表(含 user) ---------- */
    var dbRegNew = /^db_test$/;
    var tblRegNew = /^t_(bidding_content|bidding_info|bidding_related|user)$/;

    var baseTable = null;

    /* 老规则匹配 */
    if (dbRegOld.test(row.database) && tblRegOld.test(row.table)) {
        baseTable = row.table
            .replace(/_\d+$/, '')      // 去掉分表后缀
            .replace(/^t_/, '')        // 去掉 t_ 前缀
            .replace('_data', '');     // 去掉 _data 后缀
    }
    /* 新规则匹配 */
    else if (dbRegNew.test(row.database) && tblRegNew.test(row.table)) {
        baseTable = row.table
            .replace(/^t_/, '')        // 去掉 t_ 前缀
            .replace(/_\d+$/, '');     // 去掉分表后缀(如果有)
    }

    /* 3. 生成 topic 或丢弃 */
    if (baseTable) {
        row.kafka_topic = 'maxwell_t_' + baseTable;
    } else {
        row.suppress();   // 不匹配则丢弃
    }
}

1.3 删除Kafka(3.9.0)topic

#进入Kafka目录
cd kafka_2.13-3.9.0
#查看当前topic列表
./bin/kafka-topics.sh --bootstrap-server xx.xx.xx.xx1:9092 --list

在这里插入图片描述

删除maxwell_t_bidding_content、maxwell_t_bidding_info、maxwell_t_bidding_related这三个表的topic

bin/kafka-topics.sh --bootstrap-server xx.xx.xx.xx1:9092 --delete --topic maxwell_t_bidding_content

bin/kafka-topics.sh --bootstrap-server xx.xx.xx.xx1:9092 --delete --topic maxwell_t_bidding_info

bin/kafka-topics.sh --bootstrap-server xx.xx.xx.xx1:9092 --delete --topic maxwell_t_bidding_related

在这里插入图片描述

1.4 重新启动Maxwell

#进入Maxwell路径
cd /opt/module/maxwell-1.44.0

#指定jdk11启动
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-11.0.23.0.9-2.el7_9.x86_64
export PATH=$JAVA_HOME/bin:$PATH

#启动Maxwell
./bin/maxwell --config config.properties

在这里插入图片描述

二、开始测试

2.1 启动插入mysql数据脚本

运行环境:python3.8.20

# -*- coding: utf8 -*-
"""
三表并发插入 + 后台「按轮次」批量改/删 + 最终 Kafka 总量统计
修改点:
1. [新增] 启动前打印 Kafka 初始 Offset。
2. 结束后记录 Kafka 最终 Offset。
3. 统计表中 'Kafka Recv' 显示本次运行新增的数据量。
4. 最后显示 Kafka 当前总积压量。
"""
import datetime as dt
from config import Config
import random
import time
import pymysql
import threading
import signal
import re
from typing import Dict, Any, List, Deque
from collections import deque, Counter
from kafka import KafkaAdminClient, KafkaConsumer, TopicPartition
from kafka.admin import NewTopic
from kafka.errors import TopicAlreadyExistsError

# -------------- Kafka 配置 --------------
BOOTSTRAP_SERVERS = ["xx.xx.xx.xx1:9092", "xx.xx.xx.xx2:9092", "xx.xx.xx.xx3:9092"]
KAFKA_TOPICS = ["maxwell_t_bidding_content", "maxwell_t_bidding_info", "maxwell_t_bidding_related"]

# -------------- 全局统计 (MySQL 端) --------------
STATS: Dict[str, Counter] = {
    "t_bidding_content": Counter({"insert": 0, "update": 0, "delete": 0}),
    "t_bidding_info": Counter({"insert": 0, "update": 0, "delete": 0}),
    "t_bidding_related": Counter({"insert": 0, "update": 0, "delete": 0}),
}


def create_topics_once():
    admin = KafkaAdminClient(bootstrap_servers=BOOTSTRAP_SERVERS)
    topics = [NewTopic(name=kt, num_partitions=6, replication_factor=2) for kt in KAFKA_TOPICS]
    try:
        admin.create_topics(new_topics=topics, validate_only=False)
        print(">>> Kafka topics created OK:", KAFKA_TOPICS)
    except TopicAlreadyExistsError:
        print(">>> Topics already exist, skip creation.")
    except Exception as e:
        print(f">>> Create topics warning: {e}")
    finally:
        admin.close()


# -------------- 获取 Kafka 当前总数据量 (Log End Offset) --------------
def get_current_topic_offsets() -> Dict[str, int]:
    """
    不消费数据,直接查询 Kafka Broker 上各 Partition 的 LogEndOffset。
    代表 Topic 当前的总消息数(High Watermark)。
    """
    total_offsets = {t: 0 for t in KAFKA_TOPICS}
    consumer = None
    try:
        consumer = KafkaConsumer(bootstrap_servers=BOOTSTRAP_SERVERS)
        for topic in KAFKA_TOPICS:
            partitions = consumer.partitions_for_topic(topic)
            if not partitions:
                continue
            # 构建分区对象列表
            tp_list = [TopicPartition(topic, p) for p in partitions]
            # 获取所有分区的最新 Offset
            end_offsets = consumer.end_offsets(tp_list)
            # 累加得到 Topic 总量
            total_offsets[topic] = sum(end_offsets.values())
    except Exception as e:
        print(f">>> 获取 Kafka Offsets 失败: {e}")
    finally:
        if consumer:
            consumer.close()
    return total_offsets


# -------------- 雪花 ID --------------
EPOCH = 1_600_000_000_000
WORKER_ID_BITS = 10
SEQUENCE_BITS = 12
MAX_WORKER_ID = -1 ^ (-1 << WORKER_ID_BITS)
MAX_SEQUENCE = -1 ^ (-1 << SEQUENCE_BITS)
WORKER_ID_SHIFT = SEQUENCE_BITS
TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS


class Snowflake:
    _lock = threading.Lock()

    def __init__(self, worker_id: int = 1):
        self.worker_id = worker_id
        self.sequence = 0
        self.last_timestamp = -1

    def _gen_timestamp(self):
        return int(time.time() * 1000)

    def get_id(self) -> int:
        with Snowflake._lock:
            timestamp = self._gen_timestamp()
            if timestamp < self.last_timestamp:
                time.sleep(0.001)
                timestamp = self._gen_timestamp()
            if timestamp == self.last_timestamp:
                self.sequence = (self.sequence + 1) & MAX_SEQUENCE
                if self.sequence == 0:
                    while timestamp <= self.last_timestamp:
                        timestamp = self._gen_timestamp()
            else:
                self.sequence = 0
            self.last_timestamp = timestamp
            return ((timestamp - EPOCH) << TIMESTAMP_SHIFT |
                    self.worker_id << WORKER_ID_SHIFT |
                    self.sequence)


_snow = Snowflake(worker_id=1)


def gen_new_id() -> int: return _snow.get_id()


# -------------- 数据库配置 --------------
DB_CONFIG = {
    "host": "xx.xx.xx.xx",
    "user": "root",
    "password": "xxxx",
    "port": 3306,
    "charset": "utf8mb4",
    "autocommit": False,
}
DATABASE = "db_test"

# ========== 速率配置 ==========
BATCH_SIZE = 100
TARGET_TPS = 100
CRUD_ROUND = 20

TABLE_META = {
    "t_bidding_info": [
        "id", "title", "publish_time", "area_code", "notice_type_main",
        "notice_type_sub", "industry_code", "project_name", "project_number",
        "project_budget_money", "project_time_limit", "entry_start_time",
        "entry_end_time", "bid_open_time", "proprietor_company",
        "agency_company", "winner_company", "winner_candidate",
        "related_construction", "project_fund_source", "attachment_oss",
        "url", "crawl_time", "s_id", "u_id", "u_tags", "qds", "create_time",
        "row_update_time", "local_row_update_time", "cdc_sync_date",
        "products", "partition_date", "insert_mysql"
    ],
    "t_bidding_content": [
        "id", "content_text", "content_html", "content_swf", "u_id",
        "u_tags", "qds", "create_time", "row_update_time",
        "local_row_update_time", "cdc_sync_date", "partition_date",
        "insert_mysql"
    ],
    "t_bidding_related": [
        "id", "u_id", "eid", "role", "title", "publish_time", "area_code",
        "notice_type_main", "notice_type_sub", "industry_code",
        "project_number", "project_bid_money", "qds", "u_tags",
        "create_time", "row_update_time", "local_row_update_time",
        "cdc_sync_date", "products", "partition_date", "insert_mysql"
    ],
}

STOP_EVENT = threading.Event()


# -------------- 令牌桶 --------------
class TokenBucket:
    def __init__(self, rate: int):
        self.rate = rate
        self.tokens = rate
        self.lock = threading.Lock()
        self.last = time.time()

    def consume(self, n: int = 1):
        while not STOP_EVENT.is_set():
            with self.lock:
                now = time.time()
                delta = now - self.last
                self.tokens = min(self.rate, self.tokens + delta * self.rate)
                self.last = now
                if self.tokens >= n:
                    self.tokens -= n
                    return
            time.sleep(0.001)


TOKEN_BUCKET = TokenBucket(TARGET_TPS)

# -------------- 轮次计数 & CRUD 逻辑 --------------
_INSERT_ROUNDS = 0
_CRUD_LOCK = threading.Lock()
_CRUD_SEQ = 0
_CRUD_SEQ_LOCK = threading.Lock()
_BUFFER_LOCK = threading.Lock()
_BUFFERS: Dict[str, Deque[Dict[str, Any]]] = {
    tbl: deque(maxlen=CRUD_ROUND * BATCH_SIZE * 2) for tbl in TABLE_META
}


def _trigger_crud_if_needed():
    global _INSERT_ROUNDS
    with _CRUD_LOCK:
        _INSERT_ROUNDS += 1
        if _INSERT_ROUNDS >= CRUD_ROUND:
            _INSERT_ROUNDS = 0
            threading.Thread(target=_crud_once, daemon=True).start()


def _crud_once():
    conn = pymysql.connect(**DB_CONFIG)
    try:
        with _BUFFER_LOCK:
            if not any(_BUFFERS.values()): return
            snapshot = {k: list(v) for k, v in _BUFFERS.items()}
            for q in _BUFFERS.values(): q.clear()

        with _CRUD_SEQ_LOCK:
            global _CRUD_SEQ
            _CRUD_SEQ += 1
            counter = _CRUD_SEQ

        print(f"\n>>> CRUD 第 {counter} 轮开始")
        for tbl, buf in snapshot.items():
            if not buf: continue
            ids = [r["id"] for r in buf]
            mod_cnt = random.randint(50, 100)
            del_cnt = random.randint(50, 100)
            mod_ids = random.sample(ids, min(mod_cnt, len(ids)))
            candidates = [i for i in ids if i not in mod_ids]
            del_cnt = min(del_cnt, len(candidates))
            del_ids = random.sample(candidates, del_cnt) if del_cnt else []

            with conn.cursor() as cur:
                if mod_ids:
                    col_map = {"t_bidding_info": "title", "t_bidding_related": "title",
                               "t_bidding_content": "content_text"}
                    col = col_map[tbl]
                    new_val = f"[RAND-{counter}]"
                    sql_mod = f"UPDATE `{DATABASE}`.`{tbl}` SET {col} = %s WHERE id = %s"
                    cur.executemany(sql_mod, [(new_val, _id) for _id in mod_ids])
                    STATS[tbl]["update"] += len(mod_ids)
                    print(f"  [{tbl}] 修改 {len(mod_ids)} 行")
                if del_ids:
                    sql_del = f"DELETE FROM `{DATABASE}`.`{tbl}` WHERE id = %s"
                    cur.executemany(sql_del, [(_id,) for _id in del_ids])
                    STATS[tbl]["delete"] += len(del_ids)
                    print(f"  [{tbl}] 删除 {len(del_ids)} 行")
            conn.commit()
        print(f">>> CRUD 第 {counter} 轮完成\n")
    finally:
        conn.close()


def add_to_buffer(table: str, rows: List[Dict[str, Any]]):
    with _BUFFER_LOCK:
        for r in rows:
            _BUFFERS[table].append({"id": r["id"], "table": table})
    _trigger_crud_if_needed()


# -------------- 数据生成 --------------
_SHARED_UIDS = deque(maxlen=2000)
_UID_REUSE_RATE = 0.8
_AREA_CODE_POOL: List[str] = []


def _clean_uid(raw: str) -> str:
    if not raw: return 'default_uid'
    raw = str(raw).strip()
    cleaned = re.sub(r'[^a-zA-Z0-9_-]', '_', raw)
    return cleaned[:120] if cleaned else 'default_uid'


def _load_area_code_pool():
    global _AREA_CODE_POOL
    conn = pymysql.connect(**DB_CONFIG)
    try:
        with conn.cursor() as cur:
            cur.execute(
                f"SELECT DISTINCT area_code FROM `{DATABASE}`.`t_bidding_info` WHERE area_code IS NOT NULL LIMIT 1000")
            _AREA_CODE_POOL = [row[0] for row in cur.fetchall()]
            print(f">>> area_code 池加载完成,共 {len(_AREA_CODE_POOL)} 条")
    finally:
        conn.close()


def transform_one(table: str, row: Dict[str, Any]) -> Dict[str, Any]:
    new_row = row.copy()
    new_row["id"] = gen_new_id()
    new_row["u_id"] = _clean_uid(row.get("u_id"))
    if random.random() < _UID_REUSE_RATE and _SHARED_UIDS:
        new_row["u_id"] = random.choice(_SHARED_UIDS)
    else:
        if new_row["u_id"]: _SHARED_UIDS.append(new_row["u_id"])
    if table == "t_bidding_info":
        if _AREA_CODE_POOL: new_row["area_code"] = random.choice(_AREA_CODE_POOL)
        if new_row.get("title"): new_row["title"] = "[UPD]" + new_row["title"]
        if new_row.get("project_budget_money") is not None: new_row["project_budget_money"] = round(
            float(new_row["project_budget_money"]) * 1.05, 6)
    elif table == "t_bidding_content":
        if new_row.get("content_text"): new_row["content_text"] = "[UPD]" + new_row["content_text"]
    elif table == "t_bidding_related":
        if new_row.get("title"): new_row["title"] = "[UPD]" + new_row["title"]
        if new_row.get("project_bid_money") is not None: new_row["project_bid_money"] = round(
            float(new_row["project_bid_money"]) * 1.05, 6)
    return new_row


def build_insert_sql(table: str, cols: List[str]) -> str:
    back_quote_cols = [f"`{c}`" for c in cols]
    placeholders = ["%s"] * len(cols)
    return f"INSERT INTO `{DATABASE}`.`{table}` ({','.join(back_quote_cols)}) VALUES ({','.join(placeholders)})"


def worker(table: str):
    cols = TABLE_META[table]
    select_cols = ",".join([f"`{c}`" for c in cols])
    sql_select = f"SELECT {select_cols} FROM `{DATABASE}`.`{table}` WHERE id > %s ORDER BY id ASC LIMIT %s"
    sql_insert = build_insert_sql(table, cols)
    conn = pymysql.connect(**DB_CONFIG)
    try:
        lower_id = 0
        total = 0
        while not STOP_EVENT.is_set():
            with conn.cursor() as cur:
                cur.execute(sql_select, (lower_id, BATCH_SIZE))
                rows = cur.fetchall()
                if not rows:
                    lower_id = 0
                    continue
                dict_rows = [dict(zip(cols, r)) for r in rows]
                new_rows = [transform_one(table, r) for r in dict_rows]
                TOKEN_BUCKET.consume(len(new_rows))
                try:
                    cur.executemany(sql_insert, [tuple(r[c] for c in cols) for r in new_rows])
                    conn.commit()
                    total += len(new_rows)
                    print(f"[{table}] +{len(new_rows)} 行,总插入 {total}")
                    STATS[table]["insert"] += len(new_rows)
                    add_to_buffer(table, new_rows)
                except Exception as e:
                    conn.rollback()
                    print(f"[{table}] 插入异常:{e}")
                lower_id = max(r["id"] for r in dict_rows)
    finally:
        conn.close()


# -------------- 核心统计函数 --------------
def print_final_stats(initial_offsets: Dict[str, int], final_offsets: Dict[str, int]):
    """
    initial_offsets: 脚本启动时的 offset
    final_offsets: 脚本结束时的 offset
    """
    print("\n=================== 本次运行统计 ===================")
    print(
        f"{'Table / Topic':<30} | {'MySQL Insert':<12} | {'MySQL Update':<12} | {'MySQL Delete':<12} | {'Kafka Recv':<10}")
    print("-" * 90)

    for tbl in TABLE_META.keys():
        # 获取 MySQL 明细
        ins = STATS[tbl]['insert']
        upd = STATS[tbl]['update']
        dlt = STATS[tbl]['delete']

        # 计算 Kafka 接收量 (本次增量 = 结束 - 初始)
        topic_name = f"maxwell_{tbl}"
        start_off = initial_offsets.get(topic_name, 0)
        end_off = final_offsets.get(topic_name, 0)

        # 这里的 Kafka Recv 指的是本次脚本运行期间 Kafka 新增的数据条数
        kafka_recv = max(0, end_off - start_off)

        print(f"{tbl:<30} | {ins:<12} | {upd:<12} | {dlt:<12} | {kafka_recv:<10}")

    print("===============================================")

    # ------ 单独输出各个 Topic 停止后现在的总数据量 ------
    print("\n>>> Kafka 各 Topic 当前(停止后)总积压量:")
    for topic in KAFKA_TOPICS:
        count = final_offsets.get(topic, 0)
        print(f"  [Topic] {topic:<35} : {count} 条")
    print("===============================================")


def wait_for_schedule_window():
    fmt = "%H:%M"
    start = dt.datetime.strptime(Config.SCHEDULE_START_TIME, fmt).time()
    end = dt.datetime.strptime(Config.SCHEDULE_END_TIME, fmt).time()
    while not STOP_EVENT.is_set():
        now = dt.datetime.now().time()
        if start <= end:
            in_window = start <= now <= end
        else:
            in_window = now >= start or now <= end
        if in_window:
            print(f"[schedule] 进入允许运行时段 {Config.SCHEDULE_START_TIME}~{Config.SCHEDULE_END_TIME},开始工作")
            return
        else:
            print(f"[schedule] 当前 {now.strftime('%H:%M')} 不在允许时段,等待 60 s …")
            STOP_EVENT.wait(60)


# -------------- 主控 --------------
def main():
    wait_for_schedule_window()
    if STOP_EVENT.is_set(): return

    _load_area_code_pool()
    create_topics_once()

    # --- 获取并打印初始数据量 ---
    print(">>> 正在获取 Kafka 初始数据量 (请稍候)...")
    initial_offsets = get_current_topic_offsets()

    # !!!这里是新增的打印部分!!!
    print(f"\n>>> [初始] Kafka 各 Topic 当前数据总量:")
    for topic in KAFKA_TOPICS:
        count = initial_offsets.get(topic, 0)
        print(f"  [Topic] {topic:<35} : {count} 条")
    print("-----------------------------------------------\n")
    # !!!新增部分结束!!!

    print(">>> 初始 Offset 获取完毕,准备启动线程...")

    def _sig_handler(sig, frame):
        print("\n>>> Ctrl+C 捕获,正在停止所有线程 ...")
        STOP_EVENT.set()

    signal.signal(signal.SIGINT, _sig_handler)

    threads = []
    for tbl in TABLE_META.keys():
        t = threading.Thread(target=worker, args=(tbl,), daemon=False)
        t.start()
        threads.append(t)

    try:
        while not STOP_EVENT.is_set():
            time.sleep(1)
    finally:
        print(">>> 等待工作线程结束...")
        for t in threads:
            if not t.daemon: t.join()

        print(">>> 正在等待 Maxwell 将最后的数据同步 (Wait 5s) ...")
        time.sleep(5)

        print(">>> 正在读取 Kafka 结束时的总数据量 ...")
        final_offsets = get_current_topic_offsets()

        # 将初始和结束的 offset 都传进去进行对比
        print_final_stats(initial_offsets, final_offsets)
        print(">>> 已安全退出")


if __name__ == "__main__":
    main()

2.2测试结果

初始三个topic数据都为0

在这里插入图片描述

ctrl+c停止程序,打印输出:

在这里插入图片描述

发现maxwell_t_bidding_content这个topic里面没有数据,但是db_test.t_bidding_content这个表实际插入了2500条数据,更改了256次,然后删除了231条,说明Maxwell成功暂停db_test.t_bidding_content这个表。

确定topic实际条数:

编写sh文件统计topic数量:

vi count_maxwell_total.sh

编写脚本:

#!/bin/bash
# count_maxwell_total_v3.sh  基于 Kafka 3.9.0
BROKERS="xx.xx.xx.xx1:9092,xx.xx.xx.xx2:9092,xx.xx.xx.xx3:9092"
#修改需要统计的topic即可
TOPIC="maxwell_t_bidding_info"
KAFKA_HOME=/home/bigdata/kafka_2.13-3.9.0

# 3.9.0 新版工具
"$KAFKA_HOME/bin/kafka-get-offsets.sh" \
  --bootstrap-server "$BROKERS" \
  --topic "$TOPIC" 2>/dev/null | \
awk -F: '{sum+=$NF} END{print "总条数:", sum}'

在这里插入图片描述

修改topic为maxwell_t_bidding_content

TOPIC="maxwell_t_bidding_content"

在这里插入图片描述

修改topic为maxwell_t_bidding_related

TOPIC="maxwell_t_bidding_related"

在这里插入图片描述

【信息科学与工程学】【通信工程】第一百八十八篇 通信网络中的数学物理01 因素及参数的数学建模与详细配置SDN将控制平面与数据平面分离,控制器负责全局视图与决策。控制器部署问题(Controller Placement Problem, CPP)常用图论建模:给定网络拓扑 G=(V,E),候选控制器位置集合 C⊆V,需选择k个控制器位置,使得交换机到最近控制器的平均时延或最坏时延最小化,同时考虑控制器间同步开销。 阅读详情

相关推荐

【信息科学与工程学】【通信工程】第一百九十一篇 路由器体系结构01

编号类型领域问题详细的数学分析参数列表(表达式 / 数值范围 / 边界 / 测量)关联知识1路由控制平面 · 图论 / 优化最短路径 / 最优下一跳如何计算?收敛边界?图模型​ G=(V,E),w:E→R+; SPF = Dijkstra O(∥E∥log∥V∥);BGP 策略用稳定路径问题 SPP(NP-hard),用博弈/不动点分析。收敛界:失效检测 tdetect​+tspf​+tupdate​,收敛时间 τ≤thold​+4RTT(BGP)。可用 Lyapunov​ 分析路由震荡。V: 节点数(10

weixin_49199313的博客 395

高危工业场景人员定位系统设计:四类场景的架构选型与部署要点

本文由航飞光电一线工程团队整理,重点讨论高危工业场景下人员定位系统的架构选型思路。覆盖危化、矿山、港口、电力四类场景,拆解技术路线分工、融合引擎跨区切换实现,以及私有化/本地化部署中的工程难点,供架构师与技术负责人参考。

HF17513371855的博客 373

【信息科学与工程学】计算机科学与自动化 第二百零一十一 软件开发模型01

编号类型模型领域模型中的知识点/因素/参数关联知识和法律法规1传统顺序模型瀑布模型 (Waterfall)通用软件开发、硬件紧密耦合系统、政府/军工项目、需求稳定的传统信息系统严格线性阶段:可行性研究→需求分析(SRS,含功能/非功能/接口/约束,使用IEEE 830模板)→系统设计(概要设计HLD:架构、模块划分、数据库ER图;详细设计LLD:伪代码、类图、接口定义)→实现(编码规范、代码审查、静态分析工具SonarQube)→测试(单元测试JUnit、集成测试、系统测试、验收测试UAT,测试计划提前编写

weixin_49199313的博客 159

【STM32】 ADC 完全指南:从模拟信号采样量化到五大架构原理

本文系统介绍ADC核心概念与五大主流架构。模拟信号具时间与幅值连续性,需经采样(满足奈奎斯特准则防混叠)、量化(引入±1/2LSB误差,SNR≈6.02N+1.76dB)、编码三步转为数字信号。重点解析Flash(并行高速但受限于比较器数量)、SAR(逐次逼近,低功耗适合MCU如STM32)、双积分(高精度抗工频,用于万用表)、流水线(高速吞吐)及Σ-Δ(高分辨率,靠过采样与噪声整形)五类架构原理、优缺点与应用场景。强调分辨率≠精度,选型本质是速度、精度、功耗与面积的权衡。

专注后端与底层技术,分享实战干货与学习笔记,梦想进入字节跳动,步履不停,热爱不止。 519

全彩6D激光雷达原生RGB-D感知架构深度解析 多传感器冗余融合定义智驾感知新范式

全彩6D激光雷达的诞生,代表智驾行业的感知硬件演进从“追求看得更远、点云更密”的单点性能竞赛,进入了“信息原生对齐、感知端到端融合”的全新发展阶段。它的价值从来不是替代摄像头,而是彻底打通空间深度信息和色彩语义信息的底层关联通道,大幅降低多源数据关联的复杂度。高清摄像头承载的高密度纹理细节、高动态色彩范围、长距离小目标识别能力,是任何激光雷达短期内都无法完全覆盖的能力边界。

getusushu的博客 17

ANSYS仿真工作流闭环:从PPT课件到工程复现的全链路解析

ANSYS仿真不仅是软件操作,更是融合物理建模、数值离散与工程判据的系统性工程活动。其核心在于将傅里叶导热方程、湍流模型选择(如SST vs k-ε)、VOF相变界面追踪等基础原理,转化为可复现的网格策略、求解器配置与结果验证路径。技术价值体现在缩短‘能算’到‘敢用’的跨越周期,支撑电机热-结构耦合、PCB热疲劳、冷凝器气液两相流等典型工业场景。尤其在ANSYS Workbench与Fluent协同建模中,几何简化逻辑、六面体主导网格、License功能模块校验等关键环节,直接决定仿真可信度与项目交付质量。

weixin_34293246的博客 378

热管理系列之八:终局判断 - 浸没式冷却、混合架构与技术路线图

动力电池热管理三条路径的终局判断:液冷极致优化守住基盘,直冷在中高端渗透,浸没式冷却从概念到量产。附2026-2030技术路线图与关键产业信号分析。

weixin_54917114的博客 1204

RT-Thread stm32h7使用串口DMA

stm32h7在rtthread中无法使用串口DMA,通过更改驱动代码使其支持串口DMA

qq_35141454的博客 2287

[学习笔记]超越超级大循环与原生RTOS:事件驱动架构如何改变嵌入式固件的可扩展性

RTC:事件处理器一旦开始,全部状态迁移、进出动作、输出事件必须全部执行完毕才返回;同一个组件,当前处理没结束,不会开始下一条事件。这不是架构给状态机额外施加的限制,这是UML状态图等全部状态机形式化本身固有的语义要求。如果迁移中途可以被打断,状态机会停留在未定义的中间状态。RTC保证两次事件之间,状态机永远处于明确定义的合法状态,便于分析与测试。

MiKiJT的博客 216

【HCIE】FRR+BFD+OSPF与BGP联动

FRR+BFD+OSPF与BGP联动

Wenrs的博客 1997

COMSOL仿真避坑指南:列管反应器模拟中多孔介质与化学反应设置的5个常见错误

本文详细解析了COMSOL仿真中列管反应器模拟的5个常见错误,包括多孔介质模型选择、化学反应接口配置、对流扩散方程处理、薄层热阻设置及网格独立性验证。特别针对多孔介质与化学反应设置的误区,提供了实用的判断标准和修正方法,帮助工程师避免计算偏差,提升仿真精度。

weixin_29597551的博客 325

源网荷储分层控制架构:从设备级到系统级的协调优化

面对分布式光伏、储能与柔性负荷大规模接入带来的运行复杂性,传统粗放管控模式已难适应高比例新能源场景。源网荷储分层控制架构通过“设备级保安全、台区级调协同、系统级统全局”的多层级协同体系,实现单体精准执行、局部动态平衡与全域优化统筹,构建自上而下指令分解、自下而上反馈闭环的精细化调控新模式,有效破解设备冲突、局部最优与全局失衡难题,推动分布式能源从“各自为战”迈向“协同共赢”,为新型电力系统安全高效运行提供核心支撑。

zhuhai_xigedian的博客 285

nnUnet代码解读--优化策略

优化策略 nnUNetTrainer def initialize_optimizer_and_scheduler(self): assert self.network is not None, "self.initialize_network must be called first" self.optimizer = torch.optim.Adam(self.network.parameters(), self.initial_lr, weight_decay=self.weight_

weixin_44858814的博客 3827

政务知识库AI架构选型:强合规约束下的架构决策与落地路径

政务知识库是典型的强合规导向场景,架构选型的第一原则是「合规一票否决」:安全优先级高于效果,可控性优先级高于体验,所有技术决策必须先通过等保、密评、信创、数据不出域四大硬性红线校验,再在此基础上优化检索准确率与用户体验。对应部署在互联网政务服务域、政务外网办公域、涉密内网核心域,三域物理隔离,数据仅能通过审批流程单向摆渡,禁止跨网调用模型与流转数据。• 放弃通用文档加载器,基于公文格式标准开发定制化结构化解析器,自动提取文号、标题、发文机关、成文日期、生效日期、密级、主题词等元数据,与分片内容绑定入库。

2401_89363342的博客 373

win11下的MySQL安装

目前官方最新版本是8.0.26,考虑到其稳定性、可靠性还需一定周期保证,所以使用官方版求稳定仍然建议5.7系列。3、选择MySQL Servers 5.7.38 -x64,然后按绿色箭头将选中的版本移植到右边选中框中。4、点击下方蓝色Advanced Options,选择安装位置。1、在开始界面搜索MySQL,点击command line。4、在“系统变量”中选择“path”,点击编辑。5、新建——>增加MySQL路径——>确定,12、execute,安装好后next。9、不需要做修改,直接点击next。.

rensweet的博客 2万+

Chapter 7 Linear Voltage Regulators

这一章我们介绍LDO.

2304_76736886的博客 2764

零信任架构实战:基于天远单人婚姻查询构建自动化智慧住房资格核验网关

破解公租房资格核查痛点:从传统纸质婚姻凭证抽检到权威民政数据穿透直连 在智慧城市公共租赁住房(公租房)、保障性租赁住房以及共有产权房等民生保障工程中,保障房资源的公平精准分配是住房保障管理中台的核心职责。按照现行住房保障准入政策,申请主体的家庭结构直接决定了配租资格准入与面积配给标准:单身家庭申请人通常享有特定的单人保障配额与独立的财产评估标准,要求申请人具

2501_94042197的博客 207

VMware Workstation 17 Pro 保姆级教程:手把手教你安装银河麒麟 Kylin V10 SP3 国产系统

本文提供VMware Workstation 17 Pro安装银河麒麟Kylin V10 SP3的详细教程,涵盖环境准备、虚拟机配置、系统安装及优化全流程。通过分步指导和实用技巧,帮助用户高效搭建国产操作系统开发测试环境,特别适合技术自主可控需求场景下的虚拟化实践。

weixin_30879169的博客 646

“伪私有化”的技术甄别:模型API外调与本地推理的架构差异

企业采购AI Agent平台时,“支持私有化”几乎成了标配话术。但“私有化”三个字背后,可能藏着一条你看不见的出站链路:管理后台在你的内网,模型推理却调用了外部API。这不是私有化,是“伪私有化”。本文从网络层、进程层、数据层和审计层四个维度,拆解模型API外调与本地推理的架构差异,给出可落地的技术甄别方法,附代码示例、状态机设计和熔断机制。

2501_93684580的博客 113

Gemini-CLI 登录失败?解决报错 This account requires setting the GOOGLE_CLOUD_PROJECT env var 的问题

🚀 刚装好 `gemini-cli` 就遇到登录拦路虎?别慌!本文带你深入解析 `GOOGLE_CLOUD_PROJECT` 环境变量错误的根源,并提供超详细的图文解决方案,让你丝滑开启 AI 命令行之旅!快来抄作业!✍️

XiaoqiangClub的博客 5141

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

Qt网络编程实战指南:从底层Socket到现代异步架构的选型与避坑

本文梳理Qt网络编程的核心模块与演进路线,对比QTcpSocket与QNetworkAccessManager的使用边界,结合HTTP请求与TCP长连接的完整示例,说明信号槽、内存管理与事件循环的配合方式。重点记录连接池设计、证书校验、线程亲和性等实际开发中的关键决策点,帮助读者在C/S或跨平台项目中建立稳定的网络层基座。

2402_83344867的博客 307

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

斯坦福 AI Agent 开发课程指南:从底层架构、工程实践到自主智能体

Agent 开发正在从框架调用走向系统工程。早期学习 Agent,可能只需要掌握 Prompt Engineering、Function Calling,以及 LangChain 等框架。如何设计稳定的任务执行循环。如何管理上下文与长期记忆。如何组织工具、环境和多 Agent 协作。如何保证长时间任务执行的可靠性。如何科学评估 Agent 的效果。如何优化成本、延迟与资源利用。如何持续提升系统能力。因此,

hello! 297

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

司库、共享、战略三大财务系统怎么建、怎么打通:架构分层与接口设计实践

大型央国企财务管理和监管模式已进入以数智化、穿透式为特征的新阶段。三大系统的建设顺序、边界划分与接口设计,决定了这套架构能否支撑未来五到十年的演进。当业务提出一个新的管理需求时,需要改动几个系统?如果答案是"一个",架构是健康的;如果答案是"三个以上",说明分层和收敛还没做到位。说明:本文基于公开政策文本与行业实施经验整理,写作时间为 2026 年 11 月。架构模式与接口设计为通用实践总结,具体方案需结合企业现有系统格局调整。

Yonyou_BIP的博客 209

零信任架构实战:基于海宇人脸身份证比对A构建手机银行大额转账人证强一致性校验网关

破解手机银行大额转账与高危交易核验痛点:从传统短信静态验证到人脸身份证比对A数据穿透直连 在移动互联网金融纵深演进与全渠道无感支付普及的时代浪潮下,商业银行手机银行 App 与开放银行网关已成为个人及个体工商户资金流转、财富管理及移动支付的最核心基础设施。依据中国人民银行关于金融移动支付客户端合规管理、加强支付结算管理防范电信网络新型违法犯罪以及反洗钱客户身

haiyufuwu的博客 99

广州政务信息发布系统信创适配实战:多场景架构选型与踩坑记录-优胜电子

我遇到过叫号系统的消息推送在达梦上延迟从 200ms 涨到 2s 的情况,排查发现是数据库连接池配置在国产环境下需要重新调优。政务大厅的信息发布场景本身不复杂:大厅主屏放叫号和公告,窗口条屏同步状态,走廊屏放指引,户外屏做宣传,会议室屏支持应急插播。建议在正式迁移前,搭一套和目标环境一致的测试平台,把每个场景的业务流程完整跑一遍。信创验收看的是端到端链路,播放盒是国产的但屏幕驱动不兼容,验收时可能卡住。:信创迁移前,把叫号相关的 SQL 在目标数据库上跑一遍压力测试,重点看分页查询和连接池。

Tuotuo_SEO的博客 393

《PCIe + BRAM架构(一):直接把DDR换成BRAM,只有读带宽没变》

本系列用同一块 XC7K325T 板卡、同一条 XDMA 通路,对比四种 PCIe 数据搬运架构:DDR 大缓存、Streaming、BRAM 轻缓存、多通道。本篇讲第三种:BRAM 架构。上一篇《PCIe + DDR架构(二):实测——更改上位机数据处理方式,带宽从1051.4 MB/s 涨到 1926.3 MB/s》

weixin_44017370的博客 376
上一篇: 三节点Kafka(3.9.0-支持jdk8的最高稳定版本)集群部署
下一篇: Spark 2.4.0 三节点集群部署文档 (Standalone 模式)
wangyutian5u
博客等级 码龄4年 94粉丝 15原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值