在微服务的开发中, 我们会开发很多功能, 并在最短时间内上线, 也就是说经过了单元测试, API 测试, 自动化的集成测试, 以及少量的手动端到端的测试, 新增的改动就会在几天内部署到产品线上.
这样做符合"唯快不破"的理念, 而且很多开发人员也充满自信, 不过毕竟风险太大, 产线出问题, 影响了客户使用可不是闹着玩的. 为稳妥起见, 最好还是做灰度发布, 并且加一个开关, 一旦出问题, 马上对部分或全体用户关闭最新改动. 这个开关, 我们就叫做 Feature Toggle 特性开关.
我所在的开发团队以前采用过一种叫 Train Release 方式来发布产品, 一个大的 Train Release 包含多个组件, 既有PC客户端 (windows/macos/linux), 移动客户端 (ios/android), Web 站点和服务, 后台多种专用服务器.
我们一般分为三个大类
- Web: 各种 Web 站点以及管理工具
- Client: PC/Mobile 客户端
- Server: 各种后台服务
每个大类又分为多个子类, 一个 train release 通常要做大半年, 有专门的 release manager 负责协调, 开发工程师发布新版本后, 还要经历 ATS/BTS/Production 的升级, 部署, 测试, 试用, 最终新版本交付到用户手中经常历时一年, 这就算比较顺利的了, 如果中间出现了什么周折风波, 一年多新版本才能上线.
这个玩法显然没办法满足用户的急迫的需求, 于是新的版本管理方法应运而生. 对于单个微服务来说, 我们希望能随时发布和上线,只要它通过了构建流水线的层层检查和测试, 可是对于整个系统来说, 成千上百个微服务每天不停地升级部署, 难免会在彼此集成的接口和流程方面有所疏漏, 所有我们对主要的几个产品线实施了 monthly release, 即每月发布一个版本. 当然, 重大问题的紧急修复是可以随时发布上线的.
- 月初: 主要需要和设计基本就绪
- 月中: 主要功能完成, 集成测试开始
- 月底: 经项目负责人及主要干系人验收通过, 发布新版本
现代的微服务提倡小迭代, 快修改, 快上线, 每月只能上一个版本, 这显然不能满足快速持续交付的需求, 但是产品的稳定性是必需保证的, 既不能由于频繁部署而导致系统出现问题, 也不能因噎废食限制新版本上线的频率.
如果我们想每天上线新的版本, 我们就必须要做好分支管理, 版本管理和特性开关的控制
分支管理
分支类型 Branch Types:
- master branch 主分支
- feature branch 功能分支
- dev branch 开发分支
- hot-fix branch 紧急修复分支
分支策略 Branch Strategy :
- 小步快走, 多个开发分支, 一人一个, 未完成单元测试不准合并到其他分支
- 采用少量或很短生命周期的 feature branch, 未完成集成测试不准合并到主分支
- 产品分布只能在主分支上
- 月度新功能在测试验收之前放在 feature branch 上
每日从主分支更新改动,并在月底测试验收通过后合并到主分支, 并打上标签 tag - 紧急修复放在 hotfix 分支上
代码审查和测试过后, 立即合并到主分支
版本管理
软件应用都有一个版本, 微服务也是如此, 版本可以用如下形式
mainVersion.minorVersion.microVersion.patchVersion
比如 2.2.0.147, 2.2.1.12, 四个小节够了, 不宜过多
在产品的 API 中, 我们通常还会加上 git commit 信息
比如
GET https://mdd.example.com/mdd/api/v1/build_info
{
"version": "2.2.1",
"buildNumber": "247",
"gitTag": "2.2.1.247",
"gitCommit": "c8c30d043982c902202d436564b2fd726e61de32",
"gitBranch": "master"
}
新版本替换旧版本总有一个过程, 如果服务器比较少, 可以快速一次级升级, 但是对于大型 SaaS 服务提供商, 服务器数量至少上千台, 集群上百个, 数据中心遍布全球, 时区都不相同, 不可能一步到位. 同时, 由于对新版本新功能的稳定性,性能和运行效果并非十分有底, 也会采用灰度发布的策略, 逐个集群升级,
- 先升级副集群, 再升级主集群
- 先升级小客户, 再升级大客户
- 先升级亚太区, 再升级欧洲区, 最后升级北美区
为避免对客户造成影响, 时间一般都在当地时间的凌晨
升级的时候应按以下步骤进行
- 先检查度量数据, 确认流量在主集群上运行正常,
- 把备份集群设置为 suspend 状态, 也就是在共享的数据存储表中改一下状态, 确保流量不会跑到备份集群上.
- 把备份集群中服务器逐个升级到最新版本
- 在备份集群上作一些简单的验证测试, 运行若干主要的 TaP 测试用例, 看测试结果是否有问题, 如果有问题, 且不是环境问题, 严重性不容轻视, 则立即回滚, 此次升级取消
- 观察度量数据, 确认已经没有活跃的用户在主集群上
- 将主集群设为 suspend 状态, GSLB 会根据 Health Check 的响应把流量分派到备份集群, 再重复上述 3, 4步, 升级主集群
由于升级需要一个过程, 产线上总有一段时间存在两个或两个以上的版本, 如果不加区分, 用户会比较困惑, 一会儿界面不一样了, 一会儿功能又变了, 过了一会儿, 可能又变回来了.
在 Sharding 比较严格的系统, 这个问题并不突出, 不同的用户会落在对应的特定集群上, 不会出现上述变来变去的情况.
我们做过的一个系统为了充分利用系统资源, 采用了在数据中心内部根据用量分派流量的设计, 这时就必须要好好考虑这个问题.
于是, 我们就引入了特性开关这个利器
特性开关 Feature Toggle
仅靠版本难以解决上面提到的用户体验因为版本变化造成的功能与界面来回变化的问题, 这时候, 我们应该使用特性开关 Feature Toggle
开关的类型
- feature/function toggle 功能开关
- release toggle 发布开关
- operation toggle 运维开关
- Permission toggle 授权开关
- Experiment toggle 体验开关
这里, 我们主要讨论功能或特性开关和发布开关
特性开关的层次
- organization 组织
- site 站点
- user 用户
- device 设备
特性开关的使用
前面我们提到开关有五种类型, 不同类型有不同的应用场景
A/B 测试
在一部分站点上打开开关, 另一部分站点上关闭开关, 互相对照测试, 收集并观察度量数据
也可以直接把开关的设置开放给用户, 用户按自己的喜好选择, 在站点改版常用这一招来看用户是否接受和喜欢新的变化
用户试用
针对特定的用户群体, 打开开关供用户试用和测试
特性发布
待所有所需最低版本都已升级部署完毕, 打开所有的相关特性开关, 正式发布这一新功能
特性开关服务 Feature Service
我们可以开发一个 Feature Service, 可以供开发和运维人员调用它的 API 来设置 Feature Toggle, 其他的 Service 可以调用它的 API 来读取这些 Feature Toggle 来决定应用不同的逻辑和行为, 而所有这些操作无需重启服务进程和更改配置文件.
FeatureToggle 比较简单的就定义一个布尔值 true or false , yes or no, 或者 on or off
比如
{
"enableAutoLogout": true
}
稍微复杂的可以有枚举值: off, test, on, 并且加上失效时间
{
"enableAutoLogout": "test",
"expireTime": "2018-12-31T23:59:59Z"
}
以 user level 的 feature toggle 举例如下
- 设置 Feature Toggle
POST /featuretoggles/users
{
"userIds": ["123", "456", "789"],
"featureToggles":
[
{
"key": "enableAutoLock",
"value" "true"
},
{
"key": "enableAutoLogin",
"value" "true"
}
]
}
- 读取 Feature Toggle
GET /featuretoggles/users/:userId
{
"featureToggles":
[
{
"key": "enableAutoLock",
"value" "true"
},
{
"key": "enableAutoLogin",
"value" "true"
}
]
}
具体的实现很简单, 利用传统DB 或者 Redis, Cassandra 这样的 NOSQL 存储读写就可以了, 存储结构示例如下
Table FeatureToggle:
level | Id | toggleName | toggleValue |
---|---|---|---|
org | 123 | enableRateLimit | true |
site | 456 | enableCircuitBreaker | true |
user | 789 | enableAutoLock | true |
相关类库
Java 工具库
- Togglz
- FF4J
- Fitchy
- Flip
Python 工具库
- Gargoyle
- Gutter
.NET, C# 工具库
- FeatureSwitcher
- NFeature
- FlipIt
- FeatureToggleNET
- List on NUget
- FeatureBee
Ruby and Ruby on Rails 工具库
- rollout
- feature_flipper
- flip
- setler
- switches
- FluidFeatures