业务背景

你的团队需要构建一个全球统一的feature flag平台。有数千个backend service和mobile app都需要通过这个平台控制feature rollout。例如:

EnableNewCheckout = true
if (FeatureFlag.isEnabled(userId, "EnableNewCheckout")) {
    ...
}

系统需求

功能需求

1. Configuration:支持:

1.1 创建Feature Flag

1.2 修改Feature Flag

1.3 删除Feature Flag

1.4 Version history

1.5 Rollback

2. Evaluation API

2.1 GET /evaluate

input:

        userId, country, device, appVersion

output: 

        true/false

3. Gradual Rollout

支持: 0% -> 1% -> 5% -> 20% -> 50% -> 100%

基于:consistent hashing, 保证同一个user一直得到一样的结果

4. Targeting

支持:

country == US

AND 

appVersion > 20

AND 

employee == true

AND 

premium == true

任意组合。

5. Feature Kill Switch

如果feature出问题,要求5秒内全球所有机器全部关闭。

6. SDK:提供Java,Python,Go, Node, Android,SDK。 SDK应该尽可能减少网络调用。

非功能需求

假设:

2000 services

500 mobile apps

100 million DAU

1000 QPS config update

要求:

  • High Availability
  • Multi-region
  • Eventually Consistent
  • Audit Log
  • Low Latency

API设计

1. 设置feature flag:

POST: /feature-flag
BODY: {
    "featureId": "<featureId>",
    "version": "1",
    "ratio": "20",
    "condition": "{'contury': 'USA', 'regin': 'US-EAST'}"
}

2. 查询feature flag:

GET feature-flag?device=iphone&version=3&feature-id=ai-agent-chatbot

系统框架设计

系统架构图如下:

对该架构图说明如下:

1. admin,也就是设置feature flag的人,通过configuration service setup feature flag。

2. configuration service会调用audit-service,保存audit的信息。audit部分的详细信息暂时忽略。

3. configuration service会将信息保存在两个数据库表中。一个是version table,保存有feature的全部历史version信息。这些信息可以用来做audit和rollback。另一个是feature table,保存有feature当前的version信息,比如当前active version,dial-up ratio,condition等。

4. 两个数据库表向下游发送data stream,例如kafka,或者kinesis。我们在这里使用global DDB table。这样我们的数据可以通过DDB自动同步到多个region。各个region有自己的distribution service,将configuration向下游同步到自己region内的SDK断点。以此方式增加整个系统的scale支持和latency的控制。

5. 每个region有多个SDK。evaluate user通过SDK或者某个feature的rollout信息。SDK主要查询local cache来获取feature的rollout信息。local cache的信息是又distribution service推送的。这样的好处在于SDK无需做remote query。这样既减少了service端的压力,又为客户端带来scale和latency的好处。同时SDK通过long pull的方式从distribution处获取feature flag的变化。这样的方式作为local cache没有命中的fallback方案。

讨论

1. 为什么我们要将configuration推送到local cache,而不是从数据库中直接读取?因为我们发现我们的情况是读操作的频率远远大于写操作。读操作有100百万级的active用户。假如所有这些操作都通过remote的方式访问remote service或者remote DB,将给service端带来巨大的压力。现在我们在写时做fanout,会大大减轻service的压力,也会给client带来available和低latency的好处。

2. 怎样支持部分rollout:这里要考虑的问题是对于同一个user,我们希望他在每次dial-up时状态是稳定的。也就是说如果feature已经enable了,我们希望在继续dial up时feature仍然是enable的。我们的算法是通过user的信息,比如user-id,feature-id,version,device等,通过稳定的hash算法映射到hash空间,比如0到100.然后dial-up可以从0%逐步提升到100%。用户的hash值被dial-up包括了就代表feature enable。

3. 怎样处理region失效的情况:如果一个region的distribution service失效了,比如无法获取最新的configuration,或者无法正常向SDK同步configuration,evaluate-user应该暂时被其它region接管。当本region的distribution-service重新开始工作,它应该首先通过data stream完成和最新数据的同步,然后重新接管本region内的user的同步工作。

更多推荐