如何在应用层使用 LaunchDarkly 控制同步开关?

解读

面试官真正想考察的是:

  1. 你是否理解 CouchDB 复制(replication) 的本质——它只是一个 HTTP 长连接,由应用层主动触发;
  2. 你是否能把 LaunchDarkly 的特性开关(Feature Flag) 当作“运行时配置中心”,在 不重启进程、不改代码、不碰数据库配置 的前提下,动态启停这条长连接;
  3. 你是否能在 国内网络与合规环境 下,兼顾 延迟、断网、灰度、回滚、审计 五个维度给出可落地的代码级方案。

一句话:不是问 LaunchDarkly 怎么玩,而是问“在离线优先场景里,怎样用 LaunchDarkly 让 CouchDB 的同步随叫随停,且不出生产事故”。

知识点

  1. CouchDB 复制模型

    • 单向/双向均为 HTTP POST /_replicate/_replicator 数据库 插入一条 JSON 文档即可启停;
    • 复制状态机完全由应用层驱动,数据库本身无常驻开关
  2. LaunchDarkly 最小闭环

    • 服务端 SDK(Node/Java/Go)在内存中 维护一条 SSE 长连接 到 ldapi.launchdarkly.com(国内需走 CDN 加速域名 + 动态 IP 白名单 解决防火墙);
    • 变更到达后同步到 内存缓存,默认 5 s 轮询兜底;
    • 通过 user 的自定义属性(如 tenantId、deviceId、region)做灰度。
  3. 国内合规要点

    • 数据不出境:LaunchDarkly 只传开关标识,不传业务数据,满足 个人信息保护法 第 38 条“最小必要”原则;
    • 高可用兜底:SDK 初始化时 开启离线文件缓存(useLdd + external file),即使 VPN 断连,进程重启仍可读到上一次值;
    • 审计:利用 LD Relay Proxy 在阿里云 ECS 上自建中继,日志落盘到 SLS,保存 6 个月备查。
  4. 代码级原子操作

    • 把“启动/取消复制”封装成 幂等函数
    • flag 变更监听器 直接调用该函数,保证 无并发重复启动
    • 复制 ID 固定为 {tenantId}_sync,取消时携带同一 ID,CouchDB 会立即断开长连接

答案

下面给出一条可在生产环境直接落地的 Node.js 代码骨架,演示“用 LaunchDarkly 控制 CouchDB 双向同步开关”的最小闭环。假设租户粒度灰度,flag 名称为 couchdb-sync-enabled

// ldClient.js
const LaunchDarkly = require('launchdarkly-node-server-sdk');
const client = LaunchDarkly.init(process.env.LD_SDK_KEY, {
  streamUri: 'https://ldrelay.my-company.cn', // 国内 Relay 地址
  featureStore: LaunchDarkly.FileDataSource({
    paths: ['/opt/ld-flags/flags.yml'], // 离线兜底
  }),
});
client.waitForInitialization();
module.exports = client;

// couchdb.js
const nano = require('nano')(process.env.COUCH_URL);
const REPLICATOR_DB = nano.use('_replicator');

async function setReplication(tenantId, enabled) {
  const docId = `${tenantId}_sync`;
  if (!enabled) {
    // 取消:删除文档即可立即断流
    try { await REPLICATOR_DB.destroy(docId, await REPLICATOR_DB.get(docId)._rev); } catch (e) {}
    return;
  }
  // 启动:幂等写入
  const repDoc = {
    _id: docId,
    source: process.env.COUCH_URL,
    target: process.env.COUCH_URL_REMOTE,
    create_target: false,
    continuous: true,
    filter: 'app/onlyTenant',
    query_params: { tenantId },
  };
  try {
    const exist = await REPLICATOR_DB.get(docId);
    repDoc._rev = exist._rev;
  } catch (e) {}
  await REPLICATOR_DB.insert(repDoc);
}

// bridge.js
const ld = require('./ldClient');
const { setReplication } = require('./couchdb');

ld.on('update', (key, _) => {
  if (key === 'couchdb-sync-enabled') {
    const tenantList = fetchActiveTenantList(); // 业务函数
    tenantList.forEach(t => {
      const user = { key: t, tenantId: t };
      const enabled = ld.variation('couchdb-sync-enabled', user, false);
      setReplication(t, enabled).catch(err => logger.error(err));
    });
  }
});

关键步骤总结

  1. Relay Proxy 解决国内网络抖动,SDK 只连内网域名
  2. flag 变更事件 实时推送到应用内存,毫秒级调用 setReplication
  3. 复制文档 ID 固定,保证多次启停幂等
  4. 离线文件兜底,即使 LaunchDarkly 全链路宕机,进程重启仍保持上一次状态
  5. 审计日志 同时记录 flag 变更与 CouchDB 复制状态,一键回滚到任意时间点。

拓展思考

  1. 双向同步冲突
    若同一租户在两端同时离线写,冲突文档版本树会迅速膨胀。可结合 LaunchDarkly 再增加一个 couchdb-conflict-strategy 枚举型 flag,动态切换“先到达获胜”或“业务字段合并”策略,而无需发版。

  2. 分片级灰度
    把 user 属性扩展到 {tenantId, shardId},可实现仅让 10% 的分片开启同步,用于夜间的灰度验证,降低全量回滚风险。

  3. 边缘节点场景
    IoT 网关里跑 CouchDB Lite,网络通过卫星链路回源。此时 LaunchDarkly 的 移动端 SDK 同样支持 离线缓存,可把 flag 打包进 OTA 固件,实现“空中关闭同步,节省流量”。

  4. 合规升级
    未来若政策要求特征开关数据必须国密加密存储,可在 Relay Proxy 前再加一层 国密 TLS 卸载,并把 flag 落盘到 阿里云 KMS 加密盘,对应用层代码零侵入