fluxa · 多商户支付网关后端
fluxa 是运行在 platform 命名空间的多商户支付网关(backend),源自 everyapi-ai/payment 仓库的 backend/,admin-console 的 SPA(web/)被打进同一镜像由后端同源提供。它由同一个镜像 ghcr.io/yldm-tech/fluxa 派生出四个工作负载:server(HTTP API:商户 API 走 HMAC、admin API 走 JWT、provider webhooks、checkout 页面)、worker(后台循环:watcher 链上扫描与入账、settlement 归集对账、notify webhook 投递、billing、autopayout)、mcp-merchant(商户档案的 MCP 工具面,按商户 API key 隔离)、mcp-admin(运营级 MCP 工具面,OAuth 2.1 + admin session JWT 鉴权)。
server 与 worker 都在 Pod 启动时跑同一套 schema migration 并 bootstrap 初始 admin,迁移在事务级 pg_advisory_xact_lock 下串行化,多 Pod 并发安全。下文事实均来自该服务的 manifest 与目录内 README。
部署形态
- Name
- 命名空间
- Description
- platform(kustomization 设置
namespace: platform与namePrefix: platform-,生成资源名platform-fluxa-*)
- Name
- 工作负载
- Description
- 四个 Deployment 共用一个镜像:
fluxa-server、fluxa-worker、fluxa-mcp-merchant、fluxa-mcp-admin,均revisionHistoryLimit: 2,均标注reloader.stakater.com/auto: "true"(ConfigMap/Secret 变更时由 Reloader 触发滚动重启)
- Name
- 镜像与版本
- Description
ghcr.io/yldm-tech/fluxa:1.0.9(kustomizationimages:块;tag 由 argocd-image-updater 按 semver 写回,首发为0.0.1,容器内 image 字段仍写死0.0.1由 kustomize 覆盖。一个 entry 同时覆盖所有工作负载,保持版本一致)
- Name
- 副本数
- Description
- server 2、worker 1、mcp-merchant 2、mcp-admin 1(kustomization
replicas与各 Deployment 一致)。注:ArgoCD 忽略 Deployment 的/spec/replicas,此值只在首次创建生效,在线扩缩用kubectl scale
- Name
- 调度
- Description
- 所有工作负载
nodeSelector: workload=app;server 用priorityClassName: production-high,worker / mcp-merchant / mcp-admin 用production-medium;server 与 mcp-merchant 配podAntiAffinity软反亲和(weight 100,topologyKey: kubernetes.io/hostname),按component分散副本
- Name
- 端口
- Description
- server 容器 8090(name
http)、worker 8081(namemetrics,健康/指标共用)、mcp-merchant 8091(namehttp)、mcp-admin 8092(namehttp)
- Name
- 滚动策略
- Description
- server 与 mcp-merchant 用
rollingUpdate(maxUnavailable: 0/maxSurge: 1);worker 与 mcp-admin 用Recreate
- Name
- 资源
- Description
- server requests
cpu 100m/mem 128Mi,limitscpu 1/mem 512Mi;worker requests50m/64Mi,limits500m/256Mi;两个 MCP requests50m/64Mi,limits500m/256Mi;unseal init 容器统一 requests20m/32Mi,limits200m/64Mi
- Name
- 健康探针
- Description
- 四个工作负载同一套:
startupProbe给启动最多 5 分钟(failureThreshold: 60×periodSeconds: 5),原因是 HTTP 监听器要等app.Build通过 RPC seed 链上 channel(可达数十秒),否则 liveness 会在启动中误杀导致 crash-loop;之后livenessProbe GET /healthz、readinessProbe GET /readyz
- Name
- 存储
- Description
- 无业务 PVC。每个 Pod 挂一个
medium: Memory的 emptyDir(tmpfs)卷vault-unseal,init 容器把解密后的 APP_SECRET 写到这里,主容器只读挂载,明文不落盘
- Name
- ServiceAccount
- Description
- 专用 SA
fluxa,作为 Pod 身份向 Vault kubernetes auth(rolefluxa)换取短时 transit/decrypt token
reencrypt-job.yaml(Job fluxa-reencrypt)是 APP_SECRET 轮换的 phase 2 手动一次性作业,刻意不在 kustomization 里,ArgoCD 永不应用,只在轮换密钥时手动 apply(默认 dry-run,加 -apply 才真写)。
配置与依赖
非密配置集中在 ConfigMap fluxa-config,server 与 worker 通过 envFrom 共享:
| key | 值 / 含义 |
|---|---|
APP_ENV | production |
HTTP_ADDR | :8090 |
APP_SECRET_FILE | /vault/secrets/app_secret(读 init 写出的解密文件,而非 APP_SECRET env;应用本身从不连 Vault) |
PUBLIC_URL | https://pay.fluxa.cash(构建 checkout / webhook 回调 / 跳转 URL 的对外基址) |
BASE_DOMAIN | fluxa.cash(每商户子域 <slug>.fluxa.cash 的根域) |
TENANT_DNS_ENABLED / TENANT_DNS_PROVIDER | true / cloudflare(商户设 slug 时自动开 proxied CNAME 指向 account-B Cloudflare Tunnel) |
CLOUDFLARE_ZONE_ID / TENANT_DNS_TARGET | 58eba1cdf91cc00216979e2125b49258 / 2b822c22-21be-4d1e-98d4-c02a83f1cace.cfargotunnel.com |
DB_DRIVER / DB_MAX_CONNS | postgres / 20(worker 在自身 Deployment 覆盖为 10,两个 MCP 各覆盖为 10 / 5) |
CORS_ALLOWED_ORIGINS | https://pay.fluxa.cash |
WORKER_HEALTH_ADDR | :8081 |
KYC_ENFORCED | false |
ALERT_SMTP_* | Lark SMTP:host smtp.larksuite.com、port 587、user/from no-reply@fluxa.cash(密码走 ExternalSecret) |
STRIPE_ENABLED / CRYPTO_GATEWAY_ENABLED / AUTO_PAYOUT_ENABLED | 全部 false(渠道默认关闭,按上线范围逐个开启) |
CONFIRMATION_BUFFER | 2(链上确认数之外的 reorg 安全余量) |
密钥经 ExternalSecret fluxa-secrets(ClusterSecretStore vault-backend,refreshInterval: 1h)从 Vault KV 路径 yldm/production/fluxa 拉取,目标 Secret 名固定为 fluxa-secrets(非 namePrefix),各工作负载经 envFrom.secretRef 注入。映射的 key:APP_SECRET(app_secret,32+ 字节随机,AES-256-GCM 静态加密主密钥)、VAULT_TOKEN(vault_transit_token,仅 transit/decrypt/fluxa 的受限 token)、JWT_SECRET(jwt_secret)、ADMIN_PASSWORD(admin_password,prod guard 拒绝 admin12345/空)、DATABASE_URL(database_url)、REDIS_URL(redis_url)、CLOUDFLARE_API_TOKEN(cloudflare_api_token,需 Zone:DNS:Edit)、ALERT_SMTP_PASSWORD(smtp_password)。渠道 / 对象存储 / 告警等额外密钥仅在开启对应功能时再加进 ExternalSecret——Vault 里不存在的 key 会让整次 sync 失败。
依赖:共享集群 Postgres(经 pgbouncer.postgres.svc.cluster.local:5432,专用 fluxa 库,DSN 用 sslmode=disable,fluxa 自管并迁移自己的 schema)、Redis(redis.redis.svc.cluster.local:6379,分布式限流器)、Vault(vault.vault.svc.cluster.local:8200,Transit mount transit、key fluxa,用于 unseal init 解封 APP_SECRET)。
每个工作负载都带一个 unseal init 容器:若 APP_SECRET 是 Vault Transit 密文,则以 k8s-auth 向 Vault 解密并把明文写入 tmpfs;在 APP_SECRET 为明文时该容器 INERT(透传,不调 Vault),因此可在密钥激活前安全部署。
访问与监控
四个 Service:fluxa(server,80→8090,ingress 后端 + 指标抓取)、fluxa-worker(headless clusterIP: None,仅暴露 8081 给 ServiceMonitor,不收 ingress)、fluxa-mcp-merchant(80→8091)、fluxa-mcp-admin(80→8092)。
Ingress 全部走 ingressClassName: traefik,TLS 在 Cloudflare edge 终止(account-B Cloudflare Tunnel → Traefik :80,proxied),因此没有 cert-manager 证书 / tls 块;DNS 由 Cloudflare API 带外创建,external-dns 不托管 fluxa.cash 区。三条 Ingress:
| Ingress | host / path | 后端 |
|---|---|---|
fluxa | api.fluxa.cash 与 *.fluxa.cash,path /(/api /admin /webhooks /pay /docs 全到 server,<slug>.fluxa.cash 由应用按 Host 解析租户) | fluxa:80 |
fluxa-mcp | mcp.fluxa.cash,path /mcp(router priority 1000 压过 * 通配) | fluxa-mcp-merchant:80 |
fluxa-mcp-admin | mcp-admin.fluxa.cash,path /mcp 与 /.well-known/oauth-protected-resource(OAuth 2.1,RFC 9728 发现) | fluxa-mcp-admin:80 |
server ingress 上有 nginx 注解 proxy-body-size: 2m 及 connect/send/read 超时,作为 provider webhook / checkout POST 的外层兜底(应用自身另有每端点限流)。
监控:四个 ServiceMonitor(fluxa / fluxa-worker / fluxa-mcp-merchant / fluxa-mcp-admin),均 interval: 30s、path: /metrics,relabel 出 pod / namespace / service;MCP 的 /metrics 与 /mcp 同端口但 ingress 只放 /mcp,故 Prometheus 是唯一读者。PrometheusRule fluxa(group fluxa.rules)告警:FluxaServerDown(critical,2m)、FluxaWorkerDown(critical,3m)、FluxaPodRestarting(warning,5m)、FluxaWebhookDeliveryFailing(>20% 失败,warning,10m)、FluxaExternalErrors(RPC/provider/KYT >30% 错误,warning,10m)、FluxaHighMemory(>85% limit,warning,10m)。
弹性:HPA fluxa-server(min 2 / max 4,CPU 70% + memory 80%);VPA 覆盖 fluxa-server 与 fluxa-worker,均 updateMode: "Off"(仅给建议,不自动改),server 上限 cpu 1/mem 512Mi,worker 上限 cpu 500m/mem 256Mi。无 PDB。
NetworkPolicy allow-fluxa-ingress:platform 命名空间默认 default-deny-ingress,共享策略只放 app=gateway,所以该策略为 app=fluxa 开两类来源——kube-system(Traefik)到 8090/8091/8092,prometheus 到 8090/8081/8091/8092;Pod 不监听的端口该规则对其为 no-op。
注意事项
以下转述自该服务目录内 README,围绕首次同步、密钥与渠道:
- 首次 sync 前置:镜像由
Releaseworkflow 自动发布(按 conventional commits 推导 semver);需先在共享 Postgres 建专用fluxa库与 role;并在 Vaultyldm/production/fluxa写入app_secret/jwt_secret/admin_password/database_url/redis_url等。首发后不要再手改newTag,argocd-image-updater 是 tag 的 source of truth。 - APP_SECRET 轮换:应用支持零停机轮换(解密 key ring + 再加密步骤)。流程为 Vault
kv patch写app_secret+app_secret_previous→ 在 ExternalSecret 加APP_SECRET_PREVIOUS(property: app_secret_previous)并确认两 key 都落地 →rollout restartserver 与 worker → 手动跑 reencrypt-job(先 dry-run 期望undecryptable=0,再-apply)→ 从 Vault 与 ExternalSecret 移除app_secret_previous再 roll 一次。绝不可原地替换app_secret,否则既有密文不可解。 - 渠道默认全关,按上线范围在 ConfigMap 翻 toggle 并把渠道密钥加进 Vault + ExternalSecret。自建链上渠道不走 env,channel 配置以 DB 为权威(
channel_configs表,在 admin console 管理),运行时只读 DB;启用但字段不全的链上渠道会被 fail-fast 拒绝。EVM 路径已生产可用(USDT@BSC live)。 - 不要启用 TRON 链(
KIND=tron):上游 watcher 会把 base58 地址小写化,永远检测不到 TRON 充值;EVM 不受影响。 - 无 PreSync migrate Job:应用 Pod 启动自迁移已安全(事务级 advisory lock),单独的 PreSync Job 既冗余又会因先于主 wave 创建 ConfigMap/Secret 而卡在
CreateContainerConfigError阻塞整次 sync。 - 其他已知缺口:
database_url的sslmode=disable对集群内 pgbouncer 流量可接受但非 TLS;渠道tolerance保持 0(否则平台吸收少付差额);disputes 仅追踪、无资金回拨。
返回 platform 服务总览