跳转到主要内容
MoleSignal 是一个单一二进制(single binary):同一份可执行文件(同一个镜像)服务所有角色,你来选择某个进程运行哪些角色。这让它既能当作一条命令启动的沙箱,也能横向扩展成多角色集群。 本篇面向运维 / SRE,系统说明角色划分、后台 worker 归属、数据流、集群发现、外部依赖与端口,以及单机 / Docker Compose / Kubernetes 三种部署拓扑。

概述:设计哲学

单一二进制

所有角色编译进同一个二进制、打进同一个镜像。进程之间只靠配置([node].roles)区分职责,不需要不同的构建产物。

角色由配置选择

[node].roles 决定本进程对外暴露什么、承载哪些前台职责。默认 ["standalone"],即单进程跑全部职责。

状态尽量外置

元数据落 Postgres,数据落对象存储。除 Ingester 的 WAL/缓冲外,大多数角色是无状态的,可水平扩。

发现走 Postgres

没有独立的 gossip / 共识组件。节点通过心跳把自己写进 cluster_nodes 表,其他节点直接查表发现存活对等节点。
何时单机、何时分角色:
  • 评估、开发、PoC、小流量生产。
  • 一个进程暴露 HTTP + gRPC,并在进程内运行全部内部职责(写入、查询、压实、告警评估等)。
  • 依赖仍然是外置的 Postgres + 对象存储(或本地文件系统后端)。
角色枚举值在 TOML / 环境变量里使用 snake_casestandalonealert_manager),与内部实现的命名约定一致。下文所有配置示例均使用 snake_case。

节点角色总表

[node].roles 是一个数组(Vec<Role>),合法取值:standalonerouteringesterqueriercompactoralert_manager。默认 ["standalone"]
当前实现只取数组的第一个角色用于前台分发与集群心跳的 PeerRole 标注。 形如 [node].roles = ["ingester", "compactor"] 的多角色写法可以被解析,但目前仅第一个元素生效,“单进程同时承担多个前台角色” 属于尚未落地的设计意图,请勿在生产里依赖。如需多职责合一,请使用 standalone
角色前台暴露有/无状态扩缩特性
standaloneHTTP + gRPC,并在进程内运行全部内部职责含状态(内含 Ingester)评估用单实例;不适合作为横向扩展单元
routerHTTP 反向代理 + 限流;不直接暴露业务 HTTP listener 之外的内部服务无状态(限流器为进程内、临时)可水平扩,前置一个 L4/L7 LB 即可
ingester承载 WAL 重放 + 缓冲 + 周期 flush(实际工作在进程启动时预拉起)有状态(WAL、内存缓冲、file_meta 缓存)一致性哈希分片;扩缩需考虑 WAL 持久卷与再平衡
querier设计上为分布式扫描端无状态可水平扩
compactor后台 tick 循环(预拉起),角色本体空转无状态(逐 tick 处理)建议单实例(见高可用一节)
alert_manager后台评估 + 派发两条循环(预拉起),角色本体空转有状态(评估状态、事件状态在进程内)建议单实例
Querier 角色当前为占位 / 未完成。 角色分发函数目前只记录一条 “querier role started (stub)” 日志并空转,独立的分布式扫描服务端实现尚未接线。分布式查询的散播协议(见数据流章节)在代码中存在并被协调端调用,但作为独立 querier 进程对外提供扫描 RPC 的那一层尚未完成。当前分布式查询的可用形态是 standalone 进程内的引擎组合。

后台 worker 由哪个角色承载

一个关键事实:几乎所有后台 worker 都在进程启动的统一装配阶段被无条件拉起,而不是在各角色的分发函数里。 它们通过 feature flag 或配置自我门控(gating)。这意味着:在 standalone 进程里,它们全部运行;在分角色镜像里,是否真正”干活”取决于该角色是否被注入了对应依赖与配置。下表给出建议的承载角色与周期。
后台 worker建议承载角色周期配置键现状
心跳(heartbeat)所有角色heartbeat_interval_secs,默认 5s(首次立即)[cluster].heartbeat_interval_secs已接线
stale 节点清理(sweeper)所有角色固定 60s无(硬编码)已接线
对象存储健康探针所有角色(启动时阻塞探测一次)health_probe_interval_secs,默认 30s[store.object].health_probe_interval_secs已接线
告警评估(evaluator)alert_managereval_interval_secs,默认 30s[alert_manager].eval_interval_secs已接线
告警派发(dispatcher)alert_managerdispatch_interval_secs,默认 10s[alert_manager].dispatch_interval_secs已接线
压实(compaction)compactorinterval_secs,默认 300s(5min)[compactor].interval_secs已接线
file_meta_dumper(冷分区落盘)compactor(与 compaction 同进程)interval_secs,默认 3600s(1h)[storage.file_meta_dump].interval_secsenabled=false 可关闭已接线
scheduled_reports(定时报表)alert_manager(需渲染依赖见下)固定 60s tick;report 自身 cron 决定是否到期report 的 cron(tick 间隔不可配)已接线
search_jobs(异步搜索池)承载 AppState 的角色(通常 standalone / 暴露查询的节点)空闲轮询 idle_poll_secs,默认 2s;清理 cleanup_interval_secs,默认 3600s[search_jobs].workers(默认 2)等已接线
ACME 签发 / 续期router(TLS 入口)签发 issue_poll_secs(默认 60s);续期 renewal_retry_secs(默认 21600s/6h)[http.tls].issue_poll_secs / renewal_retry_secs当前为占位 / 未启用
ACME 签发 / 续期循环当前为占位(stub),且未在装配阶段拉起。 循环的间隔字段、每域名冷却、单次签发方法都已具备,但扫描待处理域名与续期域名的函数目前直接返回成功、不做任何处理,也没有任何地方把该 runner spawn 起来。结论:自动证书签发 / 续期尚未生效。TLS 本身还要求以 domain-acme-tls feature 编译;OSS 构建不支持 TLS。
compactor 与 alert_manager 的循环属于”预拉起”模式: 它们在统一装配阶段就以独立后台任务运行,而 compactor::run() / alert_manager::run() 这两个角色本体函数只是空转占位。因此把进程的 [node].roles 设为 compactoralert_manager,效果是”该进程的角色本体空转 + 后台循环照常跑”。

如何选择角色

角色通过 [node].roles 配置;可用环境变量覆盖。环境变量统一前缀 MS_section 与 field 之间用 .(点)分隔(去掉 MS_ 前缀后,其余按 . 切分)。例如 MS_NODE.ROLESMS_STORE.META.DSNMS_HTTP.PORT
[node]
# 节点唯一标识,留空则由进程生成
id = "ingester-a1"
# 角色数组;当前仅第一个元素生效
roles = ["ingester"]

[cluster]
# 对等节点用于互联的 gRPC 地址(host:port)
advertise_addr = "10.0.1.21:5082"
heartbeat_interval_secs = 5
peer_timeout_secs = 15
少数 bootstrap / 密钥类变量是扁平单下划线且不进 Settings,直接从环境读取:MS_CIPHER_KEYMS_AUTH_JWT_SECRET_OVERRIDEMS_LICENSE_FILEMS_COPILOT_<PROVIDER>_API_KEY 等。其余结构化字段一律走 MS_<SECTION>.<FIELD> 点分形式,docker-compose 与 k8s 清单中即如此(如 MS_NODE.ROLESMS_STORE.META.DSNMS_CLUSTER.ADVERTISE_ADDR)。

数据流

写入路径

入口经 Router(限流 + 一致性哈希)落到某个 Ingester;Ingester 先写 WAL(落盘持久),再写内存缓冲;后台 flush 循环按时间窗或大小阈值把缓冲编码成列式文件 + 检索索引上传对象存储,最后把 FileMeta 落 Postgres,并截断已 flush 的 WAL 段。
                 X-Org-Id            一致性哈希( org_id|stream )
 client ──HTTP/gRPC──► Router ──一致性哈希──► Ingester (有状态)
 :5080/:5082          (无状态,限流)            │
                       │ 429 + Retry-After     │ 1) 写 WAL(落盘, 可配 fsync 策略)
                       │ 超过 org QPS 时        │ 2) 写内存缓冲(按 seq 累积)

                              后台 flush 循环(flush_interval_secs / 缓冲阈值触发)

                          snapshot → 列式文件 + 检索索引侧车
                                               │ 上传
                          ┌────────────────────┴────────────────────┐
                          ▼                                          ▼
                  对象存储(local / s3 / azure / gcs)        Postgres:FileMeta
                  {org}/{type}/{stream}/{date}/{file-id}  (time_range, rows,
                  …每个文件一个检索索引侧车              min/max, object_key)

                          flush 成功后 seal + 截断已落盘的 WAL 段
关键配置:[wal].dir[wal].segment_size_mb[wal].flush_strategybatch/none/every_write)、[wal].sync_leveldata/all);[ingester].buffer_max_mb(默认 256)、flush_interval_secs(默认 30)、flush_parallelism(默认 4);[router.rate_limit].ingest_qps(默认每 org 1000,0=不限)。
Router 限流粒度为 (org_id, route_class),超限返回 429 并带 Retry-Afterorg_id 来自请求头 X-Org-Id,缺省为 default

查询路径

查询入口在暴露 HTTP 的节点上。引擎按集群规模逐层包装:本地查询引擎 →(≥2 个 querier 对等节点时)分布式引擎 →(指定了远程集群时)联邦引擎。
 client ──POST /api/v1/query──► QueryService
                                   │  Prefer: respond-async / respond-sync
                                   │  或 auto_async_threshold_rows 触发自动异步
                          ┌────────┴─────────┐
                       同步执行            异步 → 建 search_jobs 行,返回 202 + job_id

       引擎组合:查询引擎(本地) → 分布式(≥2 querier) → 联邦(远程集群)

            FileMeta 分区裁剪(org/stream/stream_type/time_range)+ 全文 (MATCH) 裁剪

      分布式分片:hash(object_key) % peer_count → 每片编码为扫描请求(ticket)
                          │  内部扫描 RPC(:5082)
            ┌─────────────┼─────────────┐
            ▼             ▼             ▼
        Querier 对等   Querier 对等   …(各自读列式文件, SELECT * 扫描, 回传结果批次)
            └─────────────┼─────────────┘

        协调端 UNION ALL → 注册内存表 → 执行完整用户 SQL(聚合/过滤/投影)

                   QueryResult(columns, rows, scanned_rows, took_ms)
分布式查询的分片对 object_key 取哈希散到对等节点;分片 SQL 仅做扫描(SELECT * FROM <stream>),完整聚合在协调端执行以避免部分/最终聚合不一致。仅当集群中有 ≥2 个 querier 对等节点时才走分布式;否则回退到本地引擎,无网络跳。

异步搜索作业管线

 POST /query (Prefer: respond-async 或超阈值)

        ▼  写一行 search_jobs(state=Pending, request_json, expires_at = now+7d)
   返回 202 + job_id + 监控 URL

        ▼  search_jobs worker 池(默认 2 个 worker)
   claim_next_pending()  ──PG FOR UPDATE SKIP LOCKED──► 反序列化 QueryRequest

        ▼  QueryService::run() 执行
   结果编码为 NDJSON 上传对象存储:{org_id}/search_jobs/{job_id}.ndjson

        ▼  mark_done(result_object_key, rows) / 失败则 mark_failed(error)
   清理循环每 cleanup_interval_secs 删除过期作业(库 + 对象存储)

 客户端轮询 /api/v1/query/jobs/{job_id},state=Done 后下载 NDJSON
配置:[search_jobs].workers(默认 2)、idle_poll_secs(默认 2)、cleanup_interval_secs(默认 3600);自动异步阈值 [querier].auto_async_threshold_rows(默认 5000 万行)。
FOR UPDATE SKIP LOCKED 的领取语义对多 worker / 多节点安全:多个承载 search_jobs 的进程可以共享同一张 search_jobs 表并发领取,不会重复执行同一作业。

联邦 / 多集群查询

通过 ?clusters=local,sf,nyc 指定目标集群。协调端本地扫描后,对每个启用的远程集群发起一次内部扫描 RPC(带 Bearer token),把各集群回传的批次与本地 UNION ALL 后执行完整 SQL。
 POST /api/v1/query?clusters=local,sf,nyc
        │  license 校验:含非 "local" 目标 且 无 federated_search 特性 → 403 Forbidden

   本地扫描(单线程, 保证正确性) + 对每个 enabled 远程集群 fan-out:
        │   token: env:VAR / cipher_keys:<id>(后者需注入 secret 仓库解密)
        │   连 advertise_addr(按 tls_verify 决定 http:// 或 https://)

   远程返回 → 失败(鉴权/不可达)记入 degraded_clusters 并继续(优雅降级)

   合并所有成功批次 → 执行完整用户 SQL
   返回 federation: { scanned_clusters, degraded_clusters, degraded_reason }
联邦查询受 license 门控: 只要 clusters 含非 local 目标且当前 license 不含 federated_search 特性,HTTP 层直接返回 403。OSS / 社区版会回退到单集群。远程集群定义存于 Postgres remote_clusters 表(advertise_addrtoken_secret_reftls_verifyenabled),enabled=false 的集群在 fan-out 时跳过。当前远程鉴权仅支持 Bearer token;tls_verify=false 映射为 http://(而非”https 跳过校验”)。

集群成员与发现

1

节点注册

心跳任务周期性 upsert (node_id, role, advertise_addr, last_heartbeat_at_micros) 到 Postgres cluster_nodes 表(主键 node_idON CONFLICT DO UPDATE)。首次心跳立即发出,之后按间隔。
2

心跳间隔

[cluster].heartbeat_interval_secs,默认 5s。advertise_addr 默认 127.0.0.1:5082,即对等节点用来互联的 gRPC 地址(host:port)。
3

存活窗口与 stale 清理

存活窗口由 [cluster].peer_timeout_secs 控制,默认 15s:注册表只返回 last_heartbeat_at >= now - peer_timeout 的节点。另有 sweeper 每 60s 删除超过 5 分钟未心跳的 cluster_nodes 行。
4

发现对等节点

没有 gossip / 共识;分布式模式下各角色直接查 cluster_nodes 表按角色筛选存活对等节点。standalone 模式跳过整套发现,只返回自身、永不触达 cluster_nodes
5

选址算法

Router 选 Ingester:对 org_id|stream_name 做一致性哈希后取模,确定性落到某个 Ingester。Router 选 Querier:朴素轮询(now_ns % peer_count),不是完整一致性哈希。分布式查询分片:对 object_key 取哈希取模散到 querier 对等节点。
6

分布式扫描 RPC

协调端把扫描请求(含 org/stream/sql/file_metas/time_range)编码成 ticket,经内部扫描 RPC 发到对等节点;对等节点读列式文件、注册内存表、跑分片 SQL、回传结果流。该 RPC 与 gRPC 节点服务、数据接入服务共用同一端口(默认 5082)。
集群内部的本地扫描 RPC 调用不带鉴权;只有联邦 / 远程集群调用使用可选的 Bearer token。
cluster_nodes 表结构:node_id VARCHAR(64) PKrole VARCHAR(32)advertise_addr VARCHAR(255)started_at_micros BIGINTlast_heartbeat_at_micros BIGINT,在 rolelast_heartbeat_at_micros 上建索引。

外部依赖与端口

PostgreSQL

元数据库:FileMeta、streams、rules、incidents、users、orgs、audit、quotas、证书、cluster_nodessearch_jobsremote_clusters 等。[store.meta]backend(默认 sqlite,生产请置 postgres)、dsnmax_connections(默认 16)。迁移在编译期内嵌。

对象存储

列式文件 + 检索索引侧车。[store.object].backendlocal(默认,root=./data/objects)/ s3(含 MinIO、R2、阿里云 OSS,用 endpoint 覆盖)/ azure / gcs。凭据优先级:环境变量 > 凭据文件 > 内联 TOML。

无外部缓存 / 共识

仅进程内 LRU+TTL 缓存、限流器、异步运行时。无 Redis / Memcached,无外部共识组件。

渲染依赖(可选)

定时报表的 PNG/PDF 渲染需 headless Chromium,为 feature 门控;OSS 构建一律回退 SVG 占位。镜像运行层已带 chromium。

监听端口

端口协议 / 服务配置键说明
5080HTTP[http].bind(默认 0.0.0.0)、[http].port/api/v1/*/metrics/healthz/readyz/.well-known/acme-challenge/
5082gRPC[grpc].bind[grpc].portmax_message_size_mb(默认 32MB)数据接入 + 扫描 RPC + 集群心跳,三服务共用
80TLS 模式下的明文 HTTP[http.tls].plain_port健康检查 + ACME HTTP-01 挑战 + 301 跳转 HTTPS
443TLS 模式下的 HTTPS(SNI)[http.tls].port完整路由;需 domain-acme-tls feature
/metrics(GET,Prometheus 文本 0.0.4)由 [telemetry].metrics_enabled(默认 true)控制,暴露缓存命中 / 失效、object_store_*wal_*file_meta_dump_*tantivy_pruned_files_totalalert_rule_eval_timeout_total 等指标。

健康 / 就绪探针语义

探针路径200 条件503 条件用途
LivenessGET /healthzWAL 重放完成(仅 Ingester 相关;其他角色绕过)对象存储未降级重放进行中 对象存储连接丢失进程存活与依赖健康
ReadinessGET /readyzreplay_done = true(即使对象存储已降级)重放仍在进行中允许 K8s 在写路径降级时仍放读流量进来
1

启动时对象存储探测(阻塞)

startup_ping() 同步对 _health/{uuid}.probe 做 PUT→GET→DELETE(128 字节)。失败则进程不启动。
2

后台周期探针

[store.object].health_probe_interval_secs(默认 30s)做一次同样的往返;连续 3 次失败才置 object_store_degraded=true,成功则计数归零。
3

Ingester WAL 重放

Ingester 启动时扫描 [wal].dir 下的段文件,按 (org, stream_type, stream) 重放进内存缓冲,全部载入后强制 flush 一次,再置 replay_done=true。重放未完成前 /readyz 返回 503。

认证与配置覆盖

  • JWT 密钥在首次启动时由数据库自动 bootstrap;旧的 jwt_secret TOML 字段已废弃(仅为兼容旧配置而保留解析)。需要固定密钥时用环境变量 MS_AUTH_JWT_SECRET_OVERRIDE
  • API token 形如 ms_<prefix>_<secret>,secret 经 argon2id 哈希存储。
  • 启动时 store.meta.dsnwal.dirhttp.portgrpc.portnode.id 等被视为不可变字段,运行中变更会告警。

部署拓扑

所有拓扑共用同一镜像(如 molesignal:dev),靠 MS_NODE.ROLES 区分。Web 前端是独立的 nginx 镜像(如 molesignal-web:dev)。
单进程,全部角色合一,最快上手。
[node]
roles = ["standalone"]

[store.meta]
backend = "postgres"
dsn = "postgres://molesignal:molesignal@localhost:5432/molesignal"

[store.object]
backend = "local"
root = "./data/objects"

[wal]
dir = "./data/wal"
molesignal --config ./conf/config.toml
# HTTP :5080  gRPC :5082
本地后端(store.object.backend=local)适合开发;生产请用对象存储后端。

扩缩与高可用

Router — 可水平扩

无状态,副本数随入口流量扩。限流器是进程内、临时的:多副本下每副本各自计数,实际 org QPS 上限约为 配置值 × 副本数,需要时把限流前置到统一网关或下调单副本阈值。

Querier — 可水平扩

无状态。≥2 个对等节点才触发分布式扫描。注意:独立 querier 的扫描 RPC 服务端当前为占位,分布式查询的完整落地形态以 standalone 内引擎为准。瓶颈在列式文件读取 + 查询引擎内存。

Ingester — 有状态

用 StatefulSet + 每副本 PVC 持久 WAL。Router 按 org|stream 一致性哈希落点,扩缩会改变取模结果导致再平衡;缩容前应确保 WAL 已 flush。就绪探针留足重放窗口(清单设 10s 初始延迟)。

Compactor / AlertManager — 单实例

两者均建议单副本:Compactor 多实例会产生合并冲突(lease 表加锁为未来计划),AlertManager 评估 / 事件状态为进程内。可靠性靠快速重启而非多副本。
监控建议指标:写路径看 wal_append_lock_wait_secondswal_append_inflightwal_fsync_errors_totalfile_meta_dump_*;对象存储看 object_store_operations_totalobject_store_errors_totalobject_store_op_duration_secondsobject_store_probe_*;查询看缓存命中率 cache_*tantivy_pruned_files_total;告警看 alert_rule_eval_timeout_total。配合 /healthz/readyzcluster_nodes 表观察成员存活。

最小生产清单 / 校验清单

1

外部依赖就绪

Postgres 可达、[store.meta].backend = "postgres"dsn 正确;对象存储后端选 s3/azure/gcs(非 local),凭据按”环境变量 > 凭据文件 > 内联”优先级注入。
2

角色与互联地址

每个进程 MS_NODE.ROLES 明确;MS_CLUSTER.ADVERTISE_ADDR 设为对等节点可达的 host:5082(K8s 用 $(POD_IP):5082)。确认仅设单一前台角色(多角色数组只取首元素)。
3

Ingester 持久化

Ingester 用 StatefulSet + PVC 挂 WAL 目录;[wal].flush_strategy / sync_level 按持久性要求设置;就绪探针留足重放延迟。
4

单实例角色

Compactor、AlertManager 各保持 1 副本。确认 [compactor].interval_secsretention_days[storage.file_meta_dump].enabled 符合预期。
5

入口与限流

Router 前置 LB;按 org 设 [router.rate_limit].ingest_qps / query_qps;注意多副本下限流为近似值。Ingress 对 /api/v1/query/stream 关闭缓冲。
6

可观测与探针

/metrics 接入 Prometheus;K8s 探针指向 /api/v1/healthz(liveness)与 /readyz(readiness)。确认对象存储启动探测能通过(否则进程不启动)。
7

安全与 license

需要固定 JWT 密钥时设 MS_AUTH_JWT_SECRET_OVERRIDE,否则由 DB 自动 bootstrap。注入 cipher key(生产勿用 dev 全零值)。联邦查询需 federated_search license 特性;OSS 不支持 TLS/ACME(需 domain-acme-tls feature 编译),证书自动签发/续期当前为占位、未启用。
落地前请明确以下”现状”边界:querier 独立角色为占位(分布式扫描 RPC 服务端未完成);多角色数组仅首元素生效;ACME 签发/续期为占位且未拉起;分布式共识 WAL term source 仍为静态占位(多节点共识未实现);联邦查询与 SSO(仅 OIDC,SAML 未实现)等为 license/feature 门控。