Skip to main content

· 8 min read

一、什么是 Keystone ?

对软件研发团队来说,越是频繁地集成他们的代码,工作就越轻松。同时,越频繁发布功能迭代,产品就越有价值。但是团队并不想把开发了一半的功能暴露给用户。对这种矛盾的一个有效的处理机制就是先构建所有的后端代码,集成到产品,但不提供用户界面。这个功能可以在用户端无感知的情况下被集成和测试,直到全部完成上线后,再将这个功能展现给用户。就像是 Keystone(拱顶石,建筑学术语,通常引申为确保其他部件就位的核心关键点)。

keystone.png

二、限时特价促销活动

举一个简单的例子,比如说给用户推送一个限时特价商品。这样的订单一般都需要根据用户位置、配送情况等信息确定价格。所以根据用户位置、时间、商品类型等因素,决定了用户是否会收到这种限时特价商品的推送信息。

总而言之,这是一个很复杂的电商运作逻辑,因为需要涉及仓储量、商品目录、客户服务等多个系统的协同。完成这样一个流程的开发,可能需要几周的的时间,同时,另一些功能可能需要每隔几天就发布一次。而对客户而言,特价商品推送只是订单表格上的一个选择框。

在这个项目中,可以让选择框作为 Keystone。研发团队可以跨多个产品发布周期进行内部系统的业务逻辑和接口开发。用户感知不到这些代码改动。最后一步是让用户看到这个特价推送的选择框 UI 界面,通常这用不了多少开发时间。这种模式下,所有中间代码都能够参与集成,并随着产品发布周期部署在线上,这样就避免了长时间使用 feature branch(特性分支——一种分支管理模式)带来的风险。

图片 1.png

三、中间代码和UI界面的测试方式

中间代码需要像线上代码一样接受严格的测试。这需要系统(测试)分层搭建,而不是所有测试都依赖于用户页面的触发。单元测试和 Test Pyramid(测试金字塔)中的低层测试都应当可以正常执行。甚至 Broad Stack Test 都可以正常执行,只要提供一定的机制使它们成为 Subcutaneous Tests。某些情况下,UI 层本身包含了复杂的行为,不过只要设计得当,UI 也可以通过进入 Humble Object 的方式得到测试。

并非所有应用程序的构建方式都支持这种大覆盖面的"皮下"测试,但即使无法使用 Keystone 模式,这种设计原则也是有价值的。即使用最好的工具去自动化这一过程,从 UI 层触发的测试也总是很难搭建的。将更多的测试转移到界面层以下各层级,特别是单元测试层,可以显著提升部署流水线的速度,实现持续交付。

当然,大多数的 UI 变化会比添加一个选择框复杂,即便如此,应用 Keystone模式也并不会增加太多工作量。在 Web 应用中,一个复杂的功能通常都是一个独立页面,可以作为一个整体构建和测试。这种场景下,Keystone 就是一个链接。桌面应用可能设计多个界面变化,这种情况下,Keystone 可以是一个能展示这些界面的菜单项。

尽管如此,确实存在一些场景用户界面不能被简单地打包通过一个 Keystone 控制。这时候就需要用到功能开关了。即便在这种情况下,Keystone 的概念也能够帮助我们将功能开关的实现限定在UI层控制上。这样可以避免开关四处散落在后端代码中,降低了开关应用的复杂性,更好地贯彻单开关机制,也为后续的开关清理降低了难度。

四、总结

后端先行,最后再开发 UI 界面的方式也存在一个潜在的风险,就是后端代码的设计可能无法与后开发的UI协调一致,或者在后期UI实现时才发现设计点遗漏,这会导致反馈延迟并带来糟糕的用户体验。因此,只有在产品上支持功能垂直划分,研发上能够按功能粒度快速发布的团队中,Keystone 模式才能够发挥最大的价值。

在这里我只是举例了一个用户界面的小例子,但同样的方法适用于任何界面变化,例如 API。通过最后再提供用户界面,并且保持简洁的方式,即使是很大的功能升级,我们也可以通过逐个部分增量构建、集成来完成。

在 FeatureProbe  就可以实现 Keystone 模式,做到后端代码与UI 界面分开部署测试。研发团队可以先开发后端代码部署,用户侧无感知这一块功能核心功能已经部署到系统上了,确保新功能后端代码没有问题后,在 FeatureProbe 后台操作页面,可以一键开启 UI 界面功能,测试 UI 界面功能没有问题后,再将这个新功能开放给用户。

· 12 min read

FeatureProbe 作为一个开源的『功能』管理服务,包含了灰度放量、AB实验、实时配置变更等针对『功能粒度』的一系列管理操作。需要提供各个语言的 SDK 接入,其中就包括移动端的 iOS 和 Android 的 SDK,那么要怎么解决跨平台 SDK 的问题呢?

一、为什么要跨平台?

  • 减少人力成本,减少开发时间。
  • 两个平台共享一套代码,后期产品维护简单。

二、目前常见的跨平台方案

  • C++

很多公司的跨平台移动基础库基本都有 C++ 的影子,如微信,腾讯会议,还有早期的 Dropbox,知名的开源库如微信的 Mars 等。好处是一套代码多端适配,但是需要大公司对 C++ 有强大的工具链支持,还需要花重金聘请 C++ 研发人员,随着团队人员变动,产品维护成本也不可忽视,所以 Dropbox 后期也放弃了使用 C++ 的跨端方案。

  • Rust + FFI

Rust 和对应平台的 FFI 封装。常见的方法如飞书和 AppFlow 是通过类似 RPC 的理念,暴露少量的接口,用作数据传输。好处是复杂度可控,缺点是要进行大量的序列化和反序列化,同时代码的表达会受到限制,比如不好表达回调函数。

  • Flutter

更适合于有 UI 功能的跨平台完整 APP 解决方案,不适用于跨平台移动端 SDK 的方案。

三、为什么用 Rust ?

  • 开发成本

不考虑投入成本的话,原生方案在发布、集成和用户 Debug 等方面都会更有优势。但考虑到初创团队配置两个资深的研发人员来维护两套 SDK 需要面临成本问题。

  • 有丰富的 Rust 跨平台经验

我们之前有用过 Rust 实现过跨平台的网络栈,用 tokio 和 quinn 等高质量的 crate 实现了一个长连接的客户端和服务端。

  • 安全稳定

(1) FeatureProbe 作为灰度发布的功能平台,肩负了降级的职责,对 SDK 的稳定性要求更高。

(2) 原生移动端 SDK 一旦出现多线程崩溃的问题,难以定位和排查,需要较长的修复周期。

(3) Rust 的代码天生是线程安全的,无需依赖于丰富经验的移动端开发人员,也可以保证提供高质量、稳定的 SDK。

四、Uniffi-rs

uniffi-rs 是 Mozilla 出品, 应用在 Firefox mobile browser 上的 Rust 公共组件,uniffi-rs 有以下特点:

安全

  • uniffi-rs 的设计目标第一条就是“安全优先”,所有暴露给调用语言的 Rust 生成的方法,都不应该触发未定义的行为。

  • 所有暴露给外部语言的 Rust Object 实例都要求是 Send + Sync。

简单

  • 不需要使用者去学习 FFI 的使用
  • 只定义一个 DSL 的接口抽象,框架生成对应平台实现,不用操心跨语言的调用封装。

高质量

  • 完善的文档和测试。
  • 所有生成的对应语言,都符合风格要求。

五、Uniffi-rs是如何工作的?

首先我们 clone uniffi-rs 的项目到本地, 用喜欢的 IDE 打开 arithmetic 这个项目:

git clone https://github.com/mozilla/uniffi-rs.git
cd examples/arithmetic/src

我们看下这个样例代码具体做了什么:

[Error]
enum ArithmeticError {
"IntegerOverflow",
};
namespace arithmetic {
[Throws=ArithmeticError]
u64 add(u64 a, u64 b);
};

在 arithmetic.udl 中,我们看到定义里一个 Error 类型,还定义了 add, sub, div, equal 四个方法,namespace 的作用是在代码生成时,作为对应语言的包名是必须的。我们接下来看看 lib.rs 中 rust 部分是怎么写的:

#[derive(Debug, thiserror::Error)]
pub enum ArithmeticError {
#[error("Integer overflow on an operation with {a} and {b}")]
IntegerOverflow { a: u64, b: u64 },
}
fn add(a: u64, b: u64) -> Result<u64> {
a.checked_add(b)
.ok_or(ArithmeticError::IntegerOverflow { a, b })
}
type Result<T, E = ArithmeticError> = std::result::Result<T, E>;

uniffi_macros::include_scaffolding!("arithmetic");

下图是一张 uniffi-rs 各个文件示意图,我们一起来看下,上面的 udl 和 lib.rs 属于图中的哪个部分:

1 图中最左边 Interface Definition File 对应 arithmetic.udl 文件,图中最下面红色的 Rust Business Logic 对应到 example 中的 lib.rs,test/bindings/ 目录下的各平台的调用文件对应最上面绿色的方块,那方框中蓝色的绑定文件去哪里了呢, 我们发现 lib.rs 最下面有这样一行代码 uniffi_macros::include_scaffolding!("arithmetic"); 这句代码会在编译的时候引入生成的代码做依赖,我们这就执行一下测试用例,看看编译出来的文件是什么:

cargo test

如果顺利的话,你会看到:

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

这个测试用例,运行了 python, ruby, swift 和 kotlin 四种语言的调用,需要本地有对应语言的环境,具体如何安装对应环境超出了本文的范围,但是这里给大家一个方法看具体测试用例是如何启动的,我们以 kotlin 为例,在 uniffi-rs/uniffi_bindgen/src/bindings/kotlin/mod.rs 文件中的 run_script 方法里,在 Ok(()) 前面加上一行 println!("{:?}", cmd); 再次运行:

cargo test -- --nocapture

对应平台下的 run_script 方法都可以这样拿到实际执行的命令行内容,接下来我们就能在 uniffi-rs/target/debug 中看到生成的代码:

arithmetic.jar
arithmetic.py
arithmetic.rb
arithmetic.swift
arithmetic.swiftmodule
arithmeticFFI.h
arithmeticFFI.modulemap

其中的 jar 包是 kotlin, py 是 python,rb 是 ruby,剩下4个都是 swift,这些文件是图中上面的平台绑定文件,我们以 swift 的代码为例,看下里面的 add 方法:

public
func add(a: UInt64, b: UInt64)
throws
->
UInt64
{
return try FfiConverterUInt64.lift(
try rustCallWithError(FfiConverterTypeArithmeticError.self) {
arithmetic_77d6_add(
FfiConverterUInt64.lower(a),
FfiConverterUInt64.lower(b), $0)
}
)
}

可以看到实际调用的是 FFI 中的 arithmetic_77d6_add 方法,我们记住这个奇怪名字。目前还缺图中的 Rust scaffolding 文件没找到,它实际藏在 /uniffi-rs/target/debug/build/uniffi-example-arithmetic 开头目录的 out 文件夹中,注意多次编译可能有多个相同前缀的文件夹。我们以 add 方法为例:

// Top level functions, corresponding to UDL `namespace` functions.
#[doc(hidden)]
#[no_mangle]
pub extern "C" fn r#arithmetic_77d6_add(
r#a: u64,
r#b: u64,
call_status: &mut uniffi::RustCallStatus
) -> u64 {
// If the provided function does not match the signature specified in the UDL
// then this attempt to call it will not compile, and will give guidance as to why.
uniffi::deps::log::debug!("arithmetic_77d6_add");
uniffi::call_with_result(call_status, || {
let _retval = r#add(
match<u64 as uniffi::FfiConverter>::try_lift(r#a) {
Ok(val) => val,
Err(err) => return Err(uniffi::lower_anyhow_error_or_panic::<FfiConverterTypeArithmeticError>(err, "a")),
},
match<u64 as uniffi::FfiConverter>::try_lift(r#b) {
Ok(val) => val,
Err(err) => return Err(uniffi::lower_anyhow_error_or_panic::<FfiConverterTypeArithmeticError>(err, "b")),
}).map_err(Into::into).map_err(<FfiConverterTypeArithmeticError as uniffi::FfiConverter>::lower)?;
Ok(<u64 as uniffi::FfiConverter>::lower(_retval))
})
}

其中 extern "C" 就是 Rust 用来生成 C 语言绑定的写法。我们终于知道这个奇怪的 add 方法名是如何生成的了,arithmetic_77d6_add 是 namespace 加上代码哈希和方法名 add 拼接而成。接着看 call_status ,实际是封装了 add 方法实际的返回值, call_with_result 方法定义在 uniffi-rs/uniffi/src/ffi/rustcalls.rs 中,主要是设置了 panichook, 让 Rust 代码发生崩溃时有排查的信息。arithmetic_77d6_add 的核心逻辑是 let _retval = r#add(a, b), 其中的 a,b 在一个 match 语句包裹,里面的 lift 和 lower 主要做的是 Rust 类型和 C 的 FFI 中的类型转换,具体可以看 这里。

到这里,我们就凑齐了上图中的所有部分,明白了 uniffi-rs 的整体流程。

六、如何集成到项目中?

现在,我们知道如何用 uniffi-rs 生成对应平台的代码,并通过命令行可以调用执行,但是我们还不知道如何集成到具体的 Android 或者 Xcode 的项目中。在 uniffi-rs 的帮助文档中,有 Gradle 和 XCode 的集成文档,但是读过之后,还是很难操作。

2

简单来说,就是有个 Rust 的壳工程作为唯一生成二进制的 crate,其他组件如 autofill, logins, sync_manager 作为壳工程的依赖,把 udl 文件统一生成到一个路径,最终统一生成绑定文件和二进制。好处是避免了多个 rust crate 之间的调用消耗,只生成一个二进制文件,编译发布集成会更容易。

安卓平台:是生成一个 aar 的包,Mozilla 团队提供了一个 org.mozilla.rust-android-gradle.rust-android 的 gradle 插件,可以在 Mozilla 找到具体使用。

苹果平台:是一个 xcframework,Mozilla 的团队提供了一个 build-xcframework.sh 的脚本,可以在 Mozilla 找到具体的使用。

我们只需要适当的修改下,就可以创建出自己的跨平台的项目。

实际上我们使用 uniffi-rs Mozilla 的项目还是比较复杂的,这里你可以使用 mobile sdk 来学习如何打造自己的跨平台组件:

  • rust-core 是纯 rust 的 crate
  • rust-uniffi 是 udl 和 rust-core 依赖一起生成绑定的 crate - rust-android 是生成 aar 包的安卓项目,具体是通过 gradle 插件来进行集成
  • rust-ios 是生成 xcframework 的苹果项目,通过 build-xcframewok.sh 脚本集成

这里大家也可以参考 Github Actions 编译和构建。

· 8 min read

对于产品研发团队来说,每次软件新版本发布的时候都会面临很大的压力,研发人员、产品经理、测试人员甚至市场运营人员都要在新版本上线的时刻随时待命应对随时可能出现的意外情况,新版发布当天加班熬夜也已经成了常态。一批功能历经一个迭代周期的开发,再从测试环境发布到生产环境上,总会存在较大的风险。如何改善当前软件交付的状况?能否让软件发布简单、快速、安全低风险呢?要实现这一个目标,就要从功能管理说起。

什么是“功能管理”?

"功能管理"是一种软件开发中的理念与实践。将新功能通过带有开关控制的代码部署到生产环境中,并将功能有选择性地释放给终端用户。与以往的版本发布方式不同,按版本为粒度的发布通常揉合了一批新的功能,所有功能只能在这一个批次中全部提供给用户,遇到发布问题只能全部进行回滚。而功能管理可以做到按功能粒度灵活地、有效地、安全地、快速地选择发布规则,并且可以单独验证每个功能的效果。

功能管理与渐进式交付

渐进式交付是将复杂的工程项目进行分阶段拆解,通过持续进行小型迭代闭环,降低交付成本并节省交付时间。功能管理和渐进式交付的目标是一致的,都是降低软件产品风险,快速验证商业目标。功能管理在『功能』粒度为渐进式交付铺平道路,使单个功能的渐进式交付成为了可能。在当前千变万化的商业环境中,通过渐进式功能交付,让产品在软件开发过程中赢在起跑线上。

image

功能管理与DevOps

功能管理也是 DevOps 中的重要一环,通过功能管理,可以更好的达成 DevOps 中的以下目标:

  • 提高部署和发布效率
  • 减少变更准备时间
  • 降低 MTTR(平均修复时间)
  • 降低变更失败率

功能管理的四大支柱

功能管理由构建、运维、复盘、赋能,四大支柱支撑。通过这四大支柱的实践,才能发挥功能管理最大的功效。

  • 构建

构建包含了功能开关的创建、新功能的交付、问题修复和代码变更等。规划和构建功能开关是产品功能发布前,甚至是研发开始前的重要活动。它会影响到软件的产品功能以及研发计划,比如说开发团队采用哪种分支管理模式、测试过程如何安排独立功能与功能组合测试、产品功能维度的推广发布计划等。构建的目标是更快的开发部署,降低产品试错成本,并且可以快速的验证代码变更带来的效果。

  • 运维

在产品运维过程中,可以利用功能开关监控去查看功能的线上运行情况与用户反馈,一旦功能有故障或者用户反馈异常,能随时关闭功能,可以在几秒内快速消除影响,保障应用程序其他功能的正常使用。

  • 复盘

复盘可以让产品团队更好地了解到产品更新是如何影响到系统和用户,整个团队包括设计、产品、研发、管理者都可以通过功能管理平台复盘学习,更全面地了解产品对用户行为的最终影响,最后由数据驱动产品。

  • 赋能

传统的软件交付方式依赖于研发人员,只能由研发人员来操作设置发布。功能管理可以将产品发布授权给运营、产品、销售等非研发技术人员。相比较研发人员来说,产品、销售、运营等工作人员接触到前端的真实用户,可以更了解用户的真实需求,但由于缺乏技术能力,导致他们无法控制发布,也缺乏验证需求以及商业可行性的手段与工具。功能管理可以赋予他们这些应有的能力。他们不需要掌握研发技术,只需要极少的学习成本即可快速上手将产品功能发布给特定条件下的用户,并获取到用户反馈。

FeatureProbe 是为功能管理量身打造的产品,我们的使命是让产品功能发布更有价值,推动行业软件工程能力的提升,由被动的软件交付方式转为主动规划、功能粒度控制的发布模式。

· 11 min read

在产品快速迭代中,要做到高效的功能发布同时还要降低上线风险,需要采用合适的技术对功能发布进行精细化的管控。FeatureProbe 就是一个高效的功能管理(Feature management)开源服务,它提供了灰度放量、AB实验、实时配置变更等针对功能粒度的一系列管理能力,本文将介绍如何使用 FeatureProbe 进行快速、安全地做功能发布和迭代。

一、动态配置

如果你没有接触过功能管理服务,那相信你对配置中心不会陌生。从分布式系统兴起之后,配置中心已经是分布式系统中不可或缺的一部分。从技术上来说,功能管理或配置中心本质上都是通过配置规则动态控制应用程序行为,所带来的好处是省去了修改代码、编译、打包、部署流程。在动态配置的实践中,我们通常会以 Key-Value 的形式将配置规则存储在某个服务中统一管理,并通过数据分发将配置传输至应用程序中,同时还有一个可以给应用程序获取配置的客户端库(SDK)。 下面通过一个示例演示 Key-Value 配置以及如何通过代码获取配置:

// key-value config:
{ "enable_feature_124": true }

// sdk code:
sdkClient.BooleanValue("enable_feature_124") => true

对于一些简单配置需求都可以用这种 Key-Value 方式组织和获取配置,例如:

  • “控制功能 #124 关闭或开启”
  • “将 'name' 文本框的字符大小限制为 256 个字符”
  • “redis 的连接地址是 '172.48.1.4:6379' ”

与上述类似的使用方式已经在功能开关、应用程序配置、快速限流降级等领域被广泛应用。 上述基于一对一的 Key-Value 映射配置虽然已经足够灵活通用,但仍然难以支持一些较复杂的功能场景。比如我们很难在 Key-Value 配置中体现如下场景:

  • 场景1:“只有从北京访问的且'级别'是 VIP 的用户启用功能 #124”
  • 场景2:“只有用户 APP 版本大于 1.0.1 且在每天 18:00~20:00 时开启运营活动,否则关闭活动并显示‘活动已结束’的提示信息”

上述场景的特点是应用程序在运行时需要根据上下文信息计算出相应的值,并且当上下文(需求)发生变化时,例如调整场景一为 “只有北京10%的用户启用功能 #124 ” ,在不更改代码的情况下很难做到快速支持。这也是 FeatureProbe 作为功能管理服务与传统 Key-Value 配置中心最大的区别:

配置定义SDK特点
配置中心Key-Value根据 key 获取 value
  • 难以在配置中体现业务逻辑

  • 难以通过变更配置来快速调业务逻辑
功能管理服务由一组表达业务语义的 if / else 逻辑组成根据 key + user 属性(上下文)来执行配置中定义的逻辑并判定出返回的 value
  • 配置中体现业务逻辑

  • 变更配置规则快速调整业务逻辑

下面通过一个简单示例演示功能管理服务的配置定义以及如何用代码获取相应的值:

// feature management config:
"enable_feature_124" : {
if user ("city" equals "beijing" and "level" equals "vip") : true,
else : false
}

// sdk code:
sdkClient.BoolValue("enable_feature_124", {city: "beijing", level: "vip"}) => true
sdkClient.BoolValue("enable_feature_124", {city: "shanghai", level: "vip"}) => false

二、渐进式发布

接下来我们通过一个示例来介绍如何使用 FeatureProbe,比如当我们需要发布一个新功能时,为了避免新功能的代码对线上产生影响,我们会使用功能开关 (Feature toggles) 来控制新功能的代码只能被某个城市的某些用户访问到。代码如下所示:

user := featureprobe.NewUser(reuqest.userid)
.With("city", request.city)
.With("username": request.username)

enableFeature123 := fpClient.BoolValue("enable_feature_123", user, false)
if enableFeature123 {
// new code: use the feature
} else {
// old code: don't use the feature
}

当我们将新功能代码部署后,对应用程序几乎不会产生任何影响,因为在默认情况下,所有新功能的代码都被功能开关控制,同时是否启用新功能的开关初始默认值为 false。下面为该功能开关配置规则: FeatuteProbe toggle rules

"enable_feature_123": {
"defaultServe": {
"select": 1 // Return "variations[1]" by default => false
},
"variations": [
true,
false
]
}

当我们要对新功能代码线上验证时,这时候希望 “城市为 北京,且用户名为 'test' 或 'admin' 的特定测试用户才能使用该功能”,以便于这些用户进行功能验证。此时我们会对功能开关配置进行修改,最终生成的规则配置如下所示(对应规则执行逻辑为右边注释):

{
"enable_feature_123":{
"rules":[
{
"conditions":[ // if city in ["beijing"]
{
"type":"string",
"subject":"city",
"predicate":"is one of",
"objects":[
"beijing"
]
}, // AND
{
"type":"string", // username in ["test", "admin"]
"subject":"username",
"predicate":"is one of",
"objects":[
"test", "admin"
]
}
]
"serve":{
"select":0 // return "variations[0]" => true
},
}
]
"defaultServe":{ // else
"select":1 // return "variations[1]" => false
},
},
"variations": [
true,
false
]
}

该配置更新后,会通过我们的数据分发服务 (FeatureProbe Server) 将配置下发到所有需要使用的应用程序中。当应用程序每次通过 SDK 获取返回值时,它将根据 key + user 属性以及最新配置规则所定义的逻辑来计算相应的结果。

当测试用户 "test" 在 “北京” 测试该新功能发现问题时,可以通过将开关返回值更新回 false 快速关闭新功能的使用。整个过程不涉及到任何代码的变更,即便将需求调整为 “只有北京10%的用户能访问该功能”,也仅需在页面就能完成逻辑的变更操作,然后将新的配置规则发布应用程序中即可,通常整个过程只需要几秒钟。

当功能开关被开启后,可以通过数据监控或集成测试来观察新功能对应用程序造成影响。当验证符合预期的情况后,可就再进一步修改规则配置来让更多的用户使用该功能,如先让某个城市所有人使用该功能,接着继续将用户扩展到多个城市,并最终扩展所有用户。在整个放量过程中,检测到任何问题,都可以立即更新规则或关闭开关来做到快速回滚。通过这种渐进式功能发布 (Progressive Delivery) 的方式,能够帮助我们实现快速、安全地进行线上变更。

当然,渐进式功能发布只是 FeatureProbe 的使用场景之一,其它基于规则的配置的场景也都能很好地支持,如按访问流量放量、基于时间规则的运营活动控制、A/B实验及配置中心等场景。

三、快速试用

目前 FeatureProbe 使用 Apache 2.0 License 协议已经完全开源。你可以从 GitHubGitee 上搜索 FeatureProbe 获取到所有代码,为了能够让大家快速体验完整的功能服务,我们提供了在线体验环境

四、总结

本文主要介绍了如何使用 FeatureProbe 快速更新迭代产品功能,并且通过一个实际案例介绍如何使用它进行渐进式的功能发布,以降低线上变更的风险。