如何在应用层使用 LaunchDarkly 控制同步开关?
解读
面试官真正想考察的是:
- 你是否理解 CouchDB 复制(replication) 的本质——它只是一个 HTTP 长连接,由应用层主动触发;
- 你是否能把 LaunchDarkly 的特性开关(Feature Flag) 当作“运行时配置中心”,在 不重启进程、不改代码、不碰数据库配置 的前提下,动态启停这条长连接;
- 你是否能在 国内网络与合规环境 下,兼顾 延迟、断网、灰度、回滚、审计 五个维度给出可落地的代码级方案。
一句话:不是问 LaunchDarkly 怎么玩,而是问“在离线优先场景里,怎样用 LaunchDarkly 让 CouchDB 的同步随叫随停,且不出生产事故”。
知识点
-
CouchDB 复制模型
- 单向/双向均为 HTTP POST /_replicate 或 /_replicator 数据库 插入一条 JSON 文档即可启停;
- 复制状态机完全由应用层驱动,数据库本身无常驻开关。
-
LaunchDarkly 最小闭环
- 服务端 SDK(Node/Java/Go)在内存中 维护一条 SSE 长连接 到 ldapi.launchdarkly.com(国内需走 CDN 加速域名 + 动态 IP 白名单 解决防火墙);
- 变更到达后同步到 内存缓存,默认 5 s 轮询兜底;
- 通过 user 的自定义属性(如 tenantId、deviceId、region)做灰度。
-
国内合规要点
- 数据不出境:LaunchDarkly 只传开关标识,不传业务数据,满足 个人信息保护法 第 38 条“最小必要”原则;
- 高可用兜底:SDK 初始化时 开启离线文件缓存(useLdd + external file),即使 VPN 断连,进程重启仍可读到上一次值;
- 审计:利用 LD Relay Proxy 在阿里云 ECS 上自建中继,日志落盘到 SLS,保存 6 个月备查。
-
代码级原子操作
- 把“启动/取消复制”封装成 幂等函数;
- 用 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));
});
}
});
关键步骤总结
- Relay Proxy 解决国内网络抖动,SDK 只连内网域名;
- flag 变更事件 实时推送到应用内存,毫秒级调用
setReplication; - 复制文档 ID 固定,保证多次启停幂等;
- 离线文件兜底,即使 LaunchDarkly 全链路宕机,进程重启仍保持上一次状态;
- 审计日志 同时记录 flag 变更与 CouchDB 复制状态,一键回滚到任意时间点。
拓展思考
-
双向同步冲突
若同一租户在两端同时离线写,冲突文档版本树会迅速膨胀。可结合 LaunchDarkly 再增加一个couchdb-conflict-strategy枚举型 flag,动态切换“先到达获胜”或“业务字段合并”策略,而无需发版。 -
分片级灰度
把 user 属性扩展到{tenantId, shardId},可实现仅让 10% 的分片开启同步,用于夜间的灰度验证,降低全量回滚风险。 -
边缘节点场景
在 IoT 网关里跑 CouchDB Lite,网络通过卫星链路回源。此时 LaunchDarkly 的 移动端 SDK 同样支持 离线缓存,可把 flag 打包进 OTA 固件,实现“空中关闭同步,节省流量”。 -
合规升级
未来若政策要求特征开关数据必须国密加密存储,可在 Relay Proxy 前再加一层 国密 TLS 卸载,并把 flag 落盘到 阿里云 KMS 加密盘,对应用层代码零侵入。