Skip to main content

· 8 min read

SDK(Software Developer Kit) 是使用 FeatureProbe 服务必不可少的工具之一。SDK能将用户的应用程序连接到 FeatureProbe 服务,根据用户的配置获取开关的结果,还能将开关的访问情况上报给 FeatureProbe,进而实现 A/B 实验的能力。

FeatureProbe 目前对外提供十余种主流开发语言的 SDK,包括用于服务端开发的 Java、Golang、Python、Rust等,以及用于客户端开发的 JavaScript、Android、iOS等。在之前的文章【用 Rust 开发跨平台 SDK 探索和实践】中我们曾介绍过我们选择使用Rust开发了跨平台语言的 Android SDK 和 iOS SDK,这样做的主要原因是:

(1)能减少人力成本和开发时间。

(2)共享一套代码,便于后期维护。

在开发 JavaScript SDK 的过程中,我们也同样采用类似的思路。JavaScript是目前构建Web应用的主要语言,在此基础上产生了很多现代化的 JavaScript 前端框架,比如:React、Vue、Angular等。近几年在国内爆火的微信小程序框架也主要使用 JavaScript 语言进行开发的。如何制作一款能支持所有前端框架使用的通用 SDK,同时在此 SDK 的基础上,能够快速地根据框架的语法特性进行上层封装,是 JavaScript SDK 的核心要求之一。

实现思路​

实现一个功能完善的 JavaScript SDK,能够在普通的 Web 前端工程中使用。在此基础上,根据框架语法特性,进一步封装其它语言的 SDK,不同语言的SDK分别管理和发版。

image.png

React SDK的实现​

React SDK 在实现时将 JavaScript SDK 作为依赖项安装到工程内,主要使用了 React 的 Context API 和 Context hooks 进行上层封装,方便开发者在React工程中的使用。

1、使用 React 的 createContext API 创建一个上下文对象,保存开关 FeatureProbe 实例和开关结果的集合。2、使用 React 的 Context Hooks 封装若干个自定义 Hook,用于在任何组件内快速使用 FeatureProbe 实例和访问开关结果。

这里我们展示了一种以高阶组件的方式使用 React SDK。

1、将 SDK 初始化​

​使用 FPProvider 对根组件

进行初始化,初始化时传入必填参数 remoteUrl、clientSdkKey 和 user 对象等。

image.png

2、SDK 的使用​

使用 withFPConsumer 高阶组件的方式包装业务组件

,组件内部可通过  props 属性访问 FeatureProbe 实例(client)和开关集合(toggles)。

(1)client 实例上可访问 JavaScript SDK 所有对外暴露的 API,比如 booleanValue、jsonDetail、track 等。

(2)toggles 开关集合是同一个用户在一个 clientSdkKey 环境中调用所有开关的返回结果集合,提供了另一种获取开关结果和详情的方式。

image.png

微信小程序 SDK的实现​

相比较 React SDK,在 JavaScript SDK 上的集成微信小程序 SDK 更复杂一些,需要针对微信小程序的语法特性做一些兼容工作。主要的原因是微信小程序和普通的 Web 应用的运行环境不同,前者是在微信客户端运行,后者在浏览器环境中运行的。例如在浏览器环境中支持的 window 和 document 对象,在微信小程序中是不支持的。

下面的表格列举出了两种 SDK 的主要不同点:

JavaScript SDK微信小程序SDK
发送HTTP请求APIfetchwx.request
本地缓存APIlocalStorage.setItem、localStorage.getItemwx.setStorageSync、wx.getStorageSync
长连接工具库socket.io-clientwepapp.socket.io
是否支持自动上报事件支持不支持
UAJS/1.0.1MINIPROGRAM/1.0.1

在代码层面,JavaScript SDK 将上述差异进行抽离,并保存在 platform 对象中,platform对象目前包含的字段有:

UA: 标识SDK名称和版本;

localStorage: 本地存储对象,调用 localStorage.setItem() 方法保存数据,调用localStorage.getItem() 方法获取数据;

httpRequest: 发送请求对象,调用 httpRequest.get() 方法发送GET请求,调用httpRequest.post() 方法发送 POST 请求;

socket: 用于初始化socket.io-client客户端,监听开关的变更。

JavaScript SDK 导出 initializePlatform 方法,其它语言的 SDK 在初始化时可传入 platform 对象来保存配置差异部分,不传入时将使用默认值。

export function initializePlatform(options) {
if (options.platform) {
setPlatform(options.platform);
}
}

以下为微信小程序 SDK 的 platform 对象构成。在发送 HTTP 请求上我们目前选择了一款开源的工具库 wefetch,方便后续支持其它的小程序 SDK,WebSocket 客户端选择了基于 socket.io 实现的 weapp.socket.io。

import wefetch from "wefetch";          // 小程序请求扩展
import pkg from '../package.json';
const PKG_VERSION = pkg.version; // 微信小程序 UA 信息
const io = require("weapp.socket.io"); // 基于 socket.io 实现的构建微信小程序的 WebSocket 客户端

// 基于微信小程序 API 封装的 localStorage 对象
class StorageProvider {
public async getItem(key) {
try {
return wx.getStorageSync(key);
} catch (e) {
console.log(e);
}
}

public async setItem(key, data) {
try {
wx.setStorageSync(key, data);
} catch (e) {
console.log(e);
}
}
}

// 基于微信小程序 API 封装的 httpRequest对象
const httpRequest = {
get: function(url, headers, data, successCb, errorCb) {
wefetch.get(url, {
header: headers,
data,
}).then(json => {
successCb(json.data);
}).catch(e => {
errorCb(e);
});
},
post: function(url, headers, data, successCb, errorCb) {
wefetch.post(url, {
header: headers,
data,
}).then(() => {
successCb();
}).catch(e => {
errorCb(e);
});
}
};

const platform = {
localStorage: new StorageProvider(),
UA: "MINIPROGRAM/" + PKG_VERSION,
httpRequest: httpRequest,
socket: io,
};

// 初始化
initializePlatform({ platform });

总结​

上面我们介绍了在 JavaScript SDK 的基础上去开发其它语言的 SDK。核心思路是首先实现一个「大而全」的通用SDK,然后将各个语言差异的部分进行抽离,其它语言SDK在初始化时进行差异部分的替换。其它语言的SDK再根据对应的语法特性进行上层封装,底层复用 JavaScript SDK 提供的通用能力。

目前除了 JavaScript SDK 、React SDK 和 微信小程序 SDK之外,我们正在准备 Vue SDK。如果 FeatureProbe 目前提供的SDK不满足您的需求,可以通过新建issue的方式告知我们。我们也欢迎社区伙伴能为我们贡献更多语言的 SDK,贡献SDK时可参考文档 SDK 贡献指南:https://docs.featureprobe.io/zh-CN/reference/sdk-contributor/。

· 6 min read

当我们发布新功能时,需要尽可能降低因新功能发布所导致的线上风险,通常会采取灰度放量的方式将新功能逐步发布给用户。在具体实施灰度放量时,我们可以根据业务需求选择相应的放量规则,常见如按白名单放量(如仅 QA 可见)、按特定人群属性放量(如仅某个城市的用户可见)亦或是按用户百分比放量。

当我们选择将功能以用户百分比放量时,如下图所示,会先将功发布给10% 内部用户,此时即便出现问题影响也相对可控,如观察没有问题后逐步扩大需要放量的用户百分比,实现从少量到全量平滑过渡的上线。

那么在 FeatureProbe 上要如何实现百分比放量?​

下面将通过一个实际的例子介绍如何通过 FeatureProbe 实现按百分比放量发布一个新功能。

步骤一:创建一个特性开关​

接着,配置开关百分比信息。以收藏功能百分比发布为例,设置 10%  的用户可用收藏功能,而另外 90% 的用户无法使用收藏功能。

步骤二:将 SDK 接入应用程序​

接下来,将 FeatureProbe SDK 接入应用程序。FeatureProbe 提供完整清晰的接入引导,只需按照步骤即可快速完成 SDK 接入。

1、选择所使用的 SDK​

2、按步骤设置应用程序​

3、测试应用程序 SDK接入情况

步骤三:按百分比放量发布开关​

开关信息配置和 SDK 接入都完成后,点击发布按钮并确认发布。这将会将收藏功能发布给用户,但只有10%的用户可以使用收藏功能。

如果希望逐步扩大灰度范围,可以在开关规则中配置百分比比例。

大部分情况下,我们希望在一个功能的灰度放量过程中,某个特定用户一旦进入了灰度放量组,在灰度比例不减少的情况下,总是进入灰度组。不希望用户因为刷新页面、重新打开APP、请求被分配到另一个服务端实例等原因,一会看到新功能,一会看不到新功能,从而感到迷惑。要达到用户稳定进入灰度组,只需要在上述代码第三步创建 User 时指定stableRollout 即可,具体使用详情见:https://docs.featureprobe.io/zh-CN/tutorials/rollout_tutorial/stable_rollout_tutorial

总结​

灰度按百分比放量是一种软件开发中常用的功能发布方法,它可以帮助提高软件可靠性,提高用户体验,在实施时也需要注意几个方面:

1、确定放量目标:首先需要确定放量的目标,例如增加多少百分比的数据量。这个目标需要根据实际情况进行制定,例如需要考虑数据量的大小、计算资源的限制等因素。

2、确定放量规则:你需要确定在放量过程中,哪些功能会被启用,哪些功能会被禁用。你可以根据开发进度、测试结果和市场需求等因素来确定放量规则。

3、监控放量过程:在实施放量操作时,需要监控放量过程,以确保放量结果的稳定性和可靠性。如果出现异常情况,需要及时采取措施进行调整。

若要了解有关FeatureProbe 灰度发布的更多信息,请查看其官方文档中的教程。该教程可以提供关于如何进行灰度发布的详细说明。文档中还包括其他相关主题的信息,例如如何进行服务降级和指标分析等。请访问以下链接以查看该文档:https://docs.featureprobe.io/zh-CN/tutorials/rollout_tutorial/

· 8 min read

AB实验是一种互联网产品优化方法,通过随机分流用户到不同版本的页面或功能,比较用户行为数据,评估哪个版本能够更好的实现业务目标。

最常见的用于AB实验的假设检验方法是频率学派的零假设检测 (Null Hypothesis Significance Testing) 。但近年来贝叶斯方法因为有较好的鲁棒性和可解释性也逐渐流行起来。

零假设检测(NHST)的问题​

如果大家没听说过零假设检测,也可以跳过本段,直接看后面贝叶斯方法,会发现贝叶斯方法更容易理解。

如果你曾经接触过频率学派的经典假设检验方法,你可能听过P值(P-Value)、显著性、统计功效、一类错误和二类错误这样的术语。下面以P值(显著性)为例,说明一下可解释性的问题。

如果我们想要测试A组和 B组之间有无差异,经典假设检验方法要求先形成一个相反的零假设(即A组和B组没有差异)。然后收集数据并观察A组和 B组 在没有差异这个假设的前提下,有多大的概率得到当前的数据(这个概率即P值)。如果可能性很小(通常P值小于0.05就认为是很小),则认为A组和B组之前有很大可能存在差异,即零假设检测统计意义上的显著。但这个P值(概率)并不描述A组和B组之间的差异程度,也不说明A组和B组哪个更好,只是描述这两个组是有差别的。

但作为普通AB实验使用者,我们其实想知道的是有多大的概率,A组会明显优于B组。然后我们还会想知道,A组可能比B组好多少。但是上面的统计术语都没有直接解释我们的疑问。 大多数人可能会望文生义的认为『统计显著』就代表一个组比另一个组有明显的优势,然而实际上并不是这样。实际上即使是专业人士,也经常会误解 P值的真实含义。

贝叶斯方法的优势​

贝叶斯方法会提供更直观和易于理解的信息,在任何情况下都能派上用场。它不依赖于零假设,大大提升了可解释性。

贝叶斯假设检验中,不同于 P值,而是通常提供以下三个数字(贝叶斯因子)解释AB实验的数据:

• P(B> A):版本B比A更好的概率。

• E(loss | B):如果选择版本B,可能的预期损失。

• E(uplift | B):如果选择版本B,可能的预期收益。

E(loss | B)和 E(uplift | B)以相同单位的方式度量测试指标。对于转化情况,这是一个百分比。让我们通过使用先前的数据来查看结果:

图片

这个结果告诉我们,如果我们选择版本B,但实际上它更糟(概率为3.6%),我们预计转化率会下降0.026%。另一方面,如果我们的选择是正确的(概率为96.4%),我们可以期望获得3.3%的增益,进而将转化率提高到23%。这是一种简单的结果,团队中的成员,包括设计师和产品经理都可以很好地理解实验结果。

在取得实验数据之后,最终我们是要决定使用A还是B,所以我们需要有一个决策规则。您可以基于P(B> A)做出决策,但一个可能的更好的决策规则是仅在 E(loss | B)小于某个预定义阈值时宣布B为赢家。

另一个推荐使用贝叶斯假设检验的原因是:从模拟中的情况来看,达到同样的效果时,使用贝叶斯算法,样本量可以节省大概一半,具体的原理和计算方法,会在后续的文章中详细介绍。

图片

贝叶斯方法的缺点​

贝叶斯的缺点是计算效率低。在统计学发展早期,这是一个完全无法解决的问题,因为这些计算需要用铅笔和纸完成。不过如今我们的电脑只需要不到一秒钟就可以完成这些数据计算。

再者是需要知道先验分布,才能降低样本量,目前在实践中一般都采用无信息的先验分布(例如:先验转化率取值为50%)。      

总结​

FeatureProbe 团队在之前的项目经验中,也使用频率学派的零假设检测数年的时间。在使用过程中,遇到了零假设检测用户理解困难、实验不能中途停止需要特定样本量等一系列问题。作为对比,在没有统计专业人士帮助下,用户能够更好理解和使用贝叶斯方法,贝叶斯方法适用于小样本数据,可以缓解前期数据不足的问题,所以更适合用户量有限的创业公司和小型公司等优点。

作为开源项目,FeatureProbe 团队希望能够帮助更多的非统计学背景的用户,创业和中等规模的公司使用到AB实验这个强大的工具。所以选择了解释性更强,样本量更少,实现结果更快的贝叶斯方法。

· 10 min read

在这个数字化时代,产品的研发效率渐渐地成为企业的核心竞争力,所以越来越多的公司开始使用敏捷开发方法来提高产品研发的效率和质量。在敏捷开发中,技术团队的研发效率可以由以下几个指标来衡量:

  • 代码部署频率: 代码部署到生产环境中的频率,这也是开发速度和效率的体现。

  • 平均修复时间(MTTR): 在生产中故障出现到恢复的时间。

  • 变更的前置时间: 从提出变更请求到部署生产中的时间,变更实施所需的时间。

  • 产品发布频率:团队在一段时间内发布的功能数量和频率。

  • 缺陷率: 在软件开发过程中,所发现和解决的缺陷数量和严重程度。

以上这些因素可以很好地展示产品研发效率,但在当前这个快速变化的市场中,研发过程中的流程和工具必须要跟得上研发速度,一旦产品的研发速度慢于需求变更的速度,新功能上线就会被延迟,而企业也会在快速发展的市场中失去核心竞争力。

有很多开发工具可以帮助团队简化开发流程、改善协作和沟通,提高工作效率。但工具的使用应该被慎重考虑,研发团队应该选择最能满足研发需求并支持敏捷开发实践的工具。而功能开关,就是敏捷开发中的常用工具。

What - 什么是功能开关?

「功能开关」(Feature Flag,又称为“特性开关”) 可以允许开发人员在不影响其他功能的情况部署新功能,并在生产环境中通过控制功能的开关灵活控制功能的可用性和可视性,提高了功能部署效率并且降低了发布风险,让团队能够快速满足应用市场需求的变化。

而功能开关的使用十分简单,功能开关会被包含在一个 if 语句里面,当 if 的状态为 true 时,功能开关会被打开,而新功能就会展示给用户,当 if 状态为 false 时,开关会被关闭,那么新功能会被隐藏不会展示给用户。

FPUser user new FPUser();
user.with("ATTRIBUTE_NAME_IN_RULE",VALUE_OF_ATTRIBUTE);
boolean boolValue = fpClient.boolValue("YOUR_TOGGLE_KEY",user.false);
if(boolValue){
// the code to run if the toggle is on
}else{
// the code to run if the toggle is off
}

用户可以由不同的属性(如角色、地理位置 、设备类型等)或者自定义的标识符号(用户ID、会员等级等)构成用户组。例如,新功能仅对付费用户或者特定地区的用户开启,功能开关就可以让新功能在限定的范围内测试新功能或者针对特定用户群体提供个性化的体验。

在产品研发过程中,我想大部分的技术团队和产品团队都会遇到以下问题:

1. 什么时候发布新功能?发布策略是什么?

2. 线上故障最快恢复的解决方案是什么?

3. 谁可以看到新功能?全量发布?

功能开关允许你在应用程序运行时控制功能的发布,也就是说你可以在应用程序运行的同时开启或关闭某个功能,而无需重新构建或重新部署代码。它还可以允许研发团队将代码部署到生产环境中,但不对用户开放这个功能代码,只有该功能已经确认可以发布生产环境,才开放给用户。

功能开关可以先确定目标用户,限定新功能用户范围减少对用户的影响,实现在生产环境中进行功能测试。

Why - 为什么说功能开关是必不可少的?

功能开关最大的特点就是可以将部署与发布分开,这是以往的软件部署发布方式不容易做到的。研发同学可以随时将新功能代码部署到生产环境中,通过功能开关控制仅让 QA 团队可见,以便于对新功能进行测试,当测试通过后,再逐步开放给更多用户。

1、快速部署​

可以让团队快速、安全地将部分业务代码部署到生产中,不需要像传统开发一样,等待所有功能都开发完成后,再合并部署到生产环境中。

2、提高测试效率​

QA 测试最好的方式是在生产环境中测试,由于生产环境中测试存在一定的风险,功能开关可以让 QA 团队在生产环境中在不影响用户体验的情况下去测试功能,可以更早地发现问题。

3、功能发布更加灵活​

可以在不改变代码情况下开启和关闭功能,更能满足变化莫测的市场环境。

4、提高产品迭代速度​

功能开关可以让开发人员更快地实现、测试和部署新功能,从而提高产品迭代速度,缩短新功能的上线时间,更快地将产品推向市场。

5、加速问题定位和修复​

功能开关可以让开发人员更快速地定位和修复问题,从而提高产品的稳定性和可靠性,开发人员可以根据需要去关闭某些功能,在不影响用户体验的情况下快速修复问题。

总的来说,功能开关可以帮助技术团队更有效地工作,同时还可以改善用户体验,降低发布新功能的风险。

How - 怎么实现功能开关?

**有很多方式可以实现功能开关:**

  • Hardcode方式

代码中使用简单的 if-else 语句,去判断功能是否被开启或关闭,需要更改代码,可能导致部署和维护成本增加。

  • 配置文件

使用配置文件启用或禁用功能,如 XML 或 JSON,这些文件在运行时应用程序去读取。需要重新启动应用程序才能应用新的配置。

  • 数据库

功能开关可以存储在数据库中,应用程序运行时去读取数据库中的数据。增加了数据库的备份和恢复等维护工作。

  • 第三方工具

有许多第三方工具可以提供功能开关管理,如第三方 SaaS 平台 LaunchDarkly、Optimizely、FlagSmith等,或者使用开源解决方案 FeatureProbe。

企业可以根据业务情况去选择功能开关的解决方案,如有充足的时间和人力也可以选择自己来实现,也可以选择成熟的第三方工具。第三方工具可以提供简洁易上手的用户界面,允许开发人员快速创建和配置开关,并提供一系列的分析和监控工具来了解功能开关的使用情况。可以更快速、轻松去创建和管理功能开关的使用情况,不需要额外的代码编写和维护。

· 8 min read

很多人在一开始了解功能管理(Feature Management)的时候,会疑惑功能管理与配置中心有什么区别,在这篇文章中我们来讲讲二者的区别,在对比两者之前我们先看下它们是什么、分别能解决什么问题以及常见的实现方案有哪些。

一、什么是配置中心?​

通过配置中心将应用程序中结构化配置进行统一管理,当配置变更后能够在应用程序中实时生效,有效避免了传统模式下修改应用程序配置需要打包、部署、测试、上线等一系列繁琐流程。广泛用于如微服务应用架构下的配置管理、应用业务参数配置、文案配置等需要满足快速对线上变更的业务场景。

配置中心的具体实现主要有两大方向:自建或使用第三方组件。最简单的自建方案如将配置存储在数据库中,程序定时从数据库中加载最新配置以实现快速变更生效。也可以直接使用成熟且功能完备的第三方开源组件,如 Apollo、Nacos 等。

二、什么是Feature Management?​

功能管理(Feature Management,也有译作特性管理)是管理「功能」生命周期的软件工程实践,它包含了渐进式发布、定向投放、A/B 实验、实时配置变更等针对「功能」粒度全生命周期管理。在持续交付实践中,它使我们能够做到让每一个变更都能独立部署,并通过渐进式发布来减少变更风险;能够感知到每一个功能在线上真实环境下用户的使用情况如何;能够清晰地看到新功能产生的业务价值等等。

一个完备的 Feature Management 系统不仅要实现「功能」的全生命周期管理功能,还要提供高效的「功能开关」规则下发和多语言客户端获取开关结果等能力,而国内原生支持功能管理实践的开源工具平台只有 FeatureProbe 。

三、两者的区别和关系​

从上面定义不难看出两者主要区别是解决的问题不一样,配置中心解决的如何利用配置实现对线上快速变更,而 Feature Management 解决的是如何通过管理「功能」生命周期来实现对功能粒度的精准管控。

从技术角度来看,Feature Management 系统也需要实现对线上应用程序快速变更。例如当我们变更「功能开关」中人群放量规则后,Client 端(应用程序)需要能快速感知规则的变化来按最新配置规则执行放量处理,从这点来看 Feature Management 系统可以依托配置中心来作为开关规则下发链路的底层实现。

这也决定了两者系统组织结构上相似性,都至少需要由管理平台、下发通路及 Client SDK 组成,不同的地方在于两者的管理平台所提供的业务能力不一样。不过它们下发通路及 Client SDK 提供能力类似,对 Feature Management 而言需要下发开关(本质上也是一个配置)并为应用程序提供访问功能开关的 SDK,而配置中心同样也需要下发配置和为应用程序提供访问配置内容的能力。

四、两者相互是否具备替代性​

既然两者部分功能上有一定的相似性,那是否具备替代性呢?

比如我们是否可以直接使用配置中心当成 Feature Management 系统来使用呢?其实前面有提到,配置中心作为通用配置平台并不关注配置内容,也就意味着你可以对配置做任何定义。以一个最简单功能开关场景为例,比如控制功能A开启或关闭,确实可以通过在配置中心上创建一个针对该场景的 K-V 配置当成开关来满足最基础的功能开关使用场景。

但这对于 Feature Management 系统来说是最简单的场景,还要做到对功能粒度的渐进式发布、将功能定向投放给特定人群、A/B 实验及对 Feature 进行价值评估等等,而都是作为配置中心所不具备的。

既然配置中心系统无法替代 Feature Management 系统,那反之用 Feature Management 系统替代配置中心是否可行?答案是肯定的。以 FeatureProbe 为例,在创建「功能开关」时支持4种开关返回值类型,分别是 Number、String、Boolean、JSON,意味着你可以在开关返回值中放上你原本在配置中心的配置内容,再利用  FeatureProbe 提供的 SDK 来获取相应的配置返回值即可。

image.png

五、总结​

最后总结下配置中心与 Feature Management 不同维度的对比:

对比维度配置中心Feature Management
使用场景自定义结构化配置,满足对线上快速变更。持续集成、部署和发布解耦、渐进式发布、定向投放、A/B 实验、预案降级
用户角色开发人员、QA。开发人员、QA、PM、SRE、运营人员。
系统复杂性较高,以纯文本、JSON 或 K/V 方式管理,需要针对配置内容定制开发满足不同需求。较低,可视化配置,大部分功能管理场景开箱即用。
可观测性需要自行定制开发监控逻辑。可以实时监控功能访问情况并对功能进行效果评估
变更快速生效支持。支持,利用下发通路实现规则快速生效。
需要定期清理很少,大多数配置和业务耦合性较高,需要配合业务长期使用。经常,大多数功能开关都是短期性的,在使用完成后即可清理。

· 21 min read

来源:DevOps.com

作者:Pete Hodgson

英文原文链接:https://devops.com/feature-branching-vs-feature-flags-whats-right-tool-job/

软件开发团队的代码分支管理策略会对其发布高质量软件的速度产生重大影响,这篇文章我们将探讨在同一代码库中实现多个并行开发工作流的几种不同方法的利弊。我们将看到两个主要的因素:合并代码冲突的成本以及独立发布功能的能力通常是无法兼得的,但特性开关为我们提供了一种解决这种矛盾思路。

1.jpg

一、代码合并冲突问题

新产品开始时代码库还比较小,通常只有少数几个开发人员在开发,这种情况下不需要太多的正式开发流程规范。然而,即使一个团队只有两个开发人员,在同一时间处理相同的文件时仍需要尽量避免合并冲突。

尽管大家努力避免工作内容互相影响,但看起来毫不相关的工作分支,经常还是会修改到同一个文件。有时,正是这些意外的合并冲突会造成巨大的痛苦 -- 任何一个资深的iOS开发者想必都经历过合并第三方 XIB 文件的冲突问题。在这篇文章中,我们将探讨一些不同的方法来处理多个开发人员在同一代码库中工作时造成合并冲突的挑战。

有些人可能会说,现在的合并冲突并不是什么大问题,使用现代的版本管理工具,例如Git,基本都能解决,这是一种理想状态。现代版本管理工具确实让创建分支变得十分简单,但并不总是能使合并这些分支内的并行开发代码变得简单。Git 有一些强大的功能,在可能的情况下自动合并代码,但 Git 不能解决所有合并问题。如果并行修改影响到了大型的 XML 文件、或 XIB、IDE 工程文件,Git 是无法自动化解决这些合并冲突问题。

最重要的是,Git 无法自动解决代码语义冲突。例如,在一个分支中,Alice 重新命名了一个方法,而在另一个分支中,Bob 增加了该方法的一个新调用。当这两个分支合并时,Git 就无法识别出 Bob 的分支正在调用一个使用旧名称的方法, 而 Bob 的代码正在调用一个已经不存在的方法。事实上,Git 不会给 Alice 或 Bob 任何提示,因为这些语义冲突是我们目前的工具无法检测的。

我们只有在尝试编译合并后的代码库时才会发现语义冲突,而且这只是其中的一个问题。如果代码是动态类型的语言,例如 javascript,那么我们可能直到用户开始报告线上的应用程序崩溃时才会被发现。

当变更的成本越来越高时,在系统中进行一些小优化投入产出比就会降低。这引入了一个微妙的阻力,随着时间的推移,对代码库的内部质量不利。更重要的是,能够逐渐改善代码库或阻止其逐渐退化的那种小而广泛的重构,正是会经常导致合并冲突的那种变更。在我看来,这是基于分支开发的真正隐性成本--它抑制了那种 "童子军军规 "的改进,而这种改进可以防止代码库的某些区域逐渐退化为禁区。(童子军军规:永远把露营地弄得比你到达时还要干净,指随时处理和优化手中的问题)

因此,在介绍了合并冲突会引起的问题后,下面让我们看看如何避免合并冲突。

二、在主干上同步修改代码

一个刚开始开发新应用的小团队,最初可能会试图通过让所有开发人员频繁地推送变更到共享的主干分支来降低发生大的意外合并冲突的风险,这是持续集成的核心原则之一。当每个人都在共享的主干分支中频繁地同步他们的修改时,意外的合并冲突可以在早期时暴露出来并解决掉。

不幸的是,让多个工作落在一个共享的主干分支上,会有一个很大的问题。如果任何一个工作没有准备好发布,那么共享分支上的任何东西都不能被发布。让我们用一个假想的例子来看看为什么。

Alice 和 Bob 是移动应用程序的两个开发人员,他们有两个正在进行的任务。Bob 正在对用户偏好的工作方式进行大修,而 Alice 正在完成一个关键的新功能,大老板规定必须在本周末前将其推送到应用商店。这个团队通过频繁地在主干提交代码来避免出现大的合并冲突。

到了周四,Alice正在结束她的工作同时也开始做功能测试。在对应用程序进行一般的测试时,她注意到用户偏好模块存在闪退现象,便问 Bob 是否知道是什么原因导致的。Bob 解释说,他正对这个模块进行重构,重构时间可能会比预期时间长一些,在完成前可能会存在 bug。但 Alice 的新功能本周又必须上线。经过了两天漫长的时间,Bob 和 Alice 终于一起回滚了 Bob 的更改,因为其中一些更改已经与 Alice 的新功能的修改纠缠在一起了。最终他们赶上了周六在产品商店上架,避免了一场危机。

上面的例子可以看到,由于 Alice 和 Bob 在一个共享分支上提交代码,他们原本相互独立的开发任务互相耦合在了一起。Alice 无法忽视 Bob 的工作而独立发布自己的变更,Bob 也是如此。这是在共享分支上工作的一个大问题,好在后面我们会介绍一些方法可以缓解这种情况。

三、特性分支

为了避免这种工作耦合的问题,开发团队往往会放弃将正在编写的代码推送到共享分支上的方式。有些团队会选择在本地机器 master 分支上工作,在工作完成后再推送到团队的共享仓库。其他团队会选择使用特性分支,在独立的分支上进行独立的工作,只有在开发完成后才合入到主干。

顺便提一下,当使用像 Git 这样的分布式源码控制系统时,这两种方法本质上是等同的;唯一的区别是,未合并的开发中的代码是在远程特性分支中可见,还是隐藏在开发者的本地主干上。因此,我把这两种方式都称为特性分支。

2.jpg

那么,如果一个团队使用了特性分支,那么他们所有的问题都解决了,对吗?可惜不是,我们重新引入了合并风险。特性分支上进行的工作,在被合并到共享的主干分支之前,并没有与其他的修改集成。每当两个工作同时修改到相同的文件时,无论是无意还是有意,都会存在一个潜在的合并冲突,这个潜在冲突会继续扩大,直到其中一个工作的分支合入主干。有些团队会通过频繁地将主干分支的修改合并到特性分支中来缓解这一问题。

3.jpg

然而,当存在并行的开发分支时,这种策略是没有效果的。例如只有当 Bob 的分支合并到主干上之后,Alice 的分支才能合入他的修改。这两个并行分支的潜在冲突只能等到 Alice 下一次合入主干代码的时候去解决了。

图片4.jpg

有些团队还会尝试 “交叉合并” 来解决代码合并冲突的问题,将一个特性分支的代码合并到另一个特性分支代码上,来减少分支之间潜在的合并冲突。然而一旦你将两个特性分支合并后,事实上你还是创建了一个含有进行中工作的共享分支,这和两个团队将进行中的工作合入共享 master 分支是一样的: 两个工作的代码变更内容仍然纠缠在了一起,所有的功能都是耦合在一起的,无法独立发布。

5.jpg

总的来说,特性分支允许团队对工作进行解耦,让部分功能可以独立发布。然而,当特性分支做了大量的修改时,它们会带来大量合并冲突的风险。只有在其他并行工作已经完成并合入的情况下,从主干分支拉取代码合入自己的特性分支才会有帮助,而分支之间的交叉合并与在共享主干分支一样,都会将多个特性耦合在一起。

四、特性开关

如果一个团队想避免特性分支带来的合并冲突风险,他们有什么选择呢?他们可以回到在主干上频繁集成变更的方式,但我们说过这样做的一个主要问题是以前相互独立的开发工作现在被耦合在一起,不能独立发布。

这时,一种叫做特性开关(又称特性标识、比特或翻转器)的技术就可以帮上忙了** 特性开关通过将进行中的代码置于一个 “标志”或“开关” 之后来隔离他们的影响。工作中的代码只有在标志被打开时才会被执行。否则,它将作为 “潜伏代码” **存在于代码库中。下面是一个基本特性开关的使用方法:

if( featureFlags.isOn(“my-new-feature”) ){  
showNewFeatureInUI();
}

除非  my-new-feature 开关被配置为 "on",否则新功能将不会在应用程序的用户界面中展现出来。 这意味着,即使实现新功能的代码完全是错误的,只要该功能的标志是关闭的,应用程序也可以正常发布。通过使用特性开关,可以将正在进行中的代码推送到共享分支,而不妨碍该分支的发布。如果 Bob 的功能已经完成了一半,而 Alice 想发布她的功能,团队可以创建一个发布版本,把 Alice 完成的功能打开,而把 Bob 未完成的功能关闭。我们既得到了将开发工作持续集成的好处(减少了合并冲突的风险),又确保这些工作的发布是解耦的。

这种技术并不是什么新技术。它是 trunk based 的开发技术中的一部分(你可以知道这个技术不是新的,因为这个名字使用了 trunk 而不是 mastermoniker,后者在今天由于 git 的主导地位而更加普遍)。Flickr、Etsy、Github 和 Facebook 是这种技术的一些比较知名的支持者。正是特性开关的使用,使得 Facebook 可以做到 feacebook.com 网站基于主干每天发布两次。

五、特性开关的缺点

与到目前为止讨论的其他方法一样,特性开关也有其缺点。它们在代码库中引入了相当多的噪音,因为我们现在需要为任何开发中的特性显式的实现一个分支逻辑。这种噪音随着时间的推移会产生一些代码异味,除非时刻留意将不再需要的特性开关移除。测试人员可能需要一段时间来适应带特性开关的应用程序——将未完成的、未经测试的代码发布到应用商店的想法需要时间去接受。如果开关可以在运行时远程控制,那么团队需要明白,新的开关配置状态必须经过测试才能在用户的应用程序上生效。

最后,某些类型的改动难以通过特性开关来保护——例如涉及大量文件变化的小改动就是一个挑战。令人欣慰的是,使用特性开关并不是一个非黑即白的方案;特性分支仍然可以用来处理那些不适于使用特性开关的繁琐变更。

六、合理使用特性开关

当一个团队开始采用特性开关时,通常会遇到上面提到的一些挑战,并最终找出解决问题的方法。下面是一些在代码库中成功使用特性开关的技巧。

1、开关要有过期时间​

很多团队在刚开始使用这种方法时,都会有些急功近利。他们倾向于引入大量的开关,却不想花时间去清除那些不再需要控制的开关。请确保花时间清理过期的开关--它们拖累你的代码设计并带来困惑。一些团队采取了相当积极的方法来确保旧开关得到清理,比如在创建开关时设置 "定时炸弹",或者在一定时间后仍在使用时抛出一个异常。其他技术包括在创建开关时就在团队任务列表中添加一个对应的清除任务。

2、开关不是『银弹』​

在一个新的任务中并不总是可以使用特性开关。特性开关更适合于新功能或者替代功能的开发,在这些功能的入口可以在代码中单独的位置进行控制,通常通过可隐藏的按钮或者通过UI界面操作触发。而另一些工作,例如重构,则很难通过一个开关来包裹。对于这种情况,需要考虑使用特性分支,理想情况下最好拆解为可以增量开发的一系列子任务分支而不要使用一个长期的分支,以减少大量合并代码带来的冲突问题。

七、总结

特性开关和特性分支都是解耦并行代码变更的方法,允许团队在发布变更时减少协调开销。特性分支很容易上手,但会导致痛苦的合并冲突。这种对冲突的恐惧往往会抑制对代码库进行增量改进的行为,并可能导致代码库的某些区域最终成为 "禁区"。特性开关允许团队实践真正的持续集成,并将代码变更与特性发布完全脱钩,但也必须付出实现每个开关的成本作为代价。

特性开关不是银弹也不总是正确的选择,但对于研发团队上手很简单,你可以从一个简单的if/else语句开始,去探索特性开关带来的价值。

关于作者

Pete Hodgson 是 Rollout.io 的咨询顾问,同时也是一名软件工程师和架构师。主要是专注于通过测试自动化、特性开关、基于主干的开发和 DevOps 等实践以可持续的速度持续交付软件,他的客户包括旧金山初创公司到财富50强的企业,同时也经常担任播客小组成员,是美国和欧洲的定期会议发言人,也是一名特约作者。

· 8 min read

作为开发人员,我们有很大概率会遇到需要将当前正在使用的数据库迁移到另一个数据库的场景。比如你在项目早期选择了 MySQL 作为数据库 ,虽然它已经能满足大多数业务场景和性能要求,但随着你的数据量越来越大业务日趋复杂,继续使用 MySQL 则可能成为瓶颈,这时候你可能要开始考虑将 MySQL 替换为更适合的数据库比如 HBase(或 Cassandra...)。

对于一个在线业务系统来说,迁移数据库的挑战在于不仅要做到不停机无缝迁移还要保证迁移过程风险可控,这也正是本文要分享的内容,介绍如何使用功能开关无缝、安全地实现数据库迁移。

迁移案例:将 MySQL 迁移到 HBase​

接下来将通过一个案例介绍整个迁移的实现过程。某个在线消息业务由于受限于 MySQL 性能和存储瓶颈,所以需要将数据迁移到 HBase 以便于进一步扩大业务规模。

1、使用功能开关实现迁移控制​

我们这里假设应用程序逻辑都是通过 DAO (数据存取对象)来查询/读取 MySQL 持久化数据。为了能将数据存取切换到 HBase 中,所以第一步是编写一个针对 HBase 的 DAO,并且和 MySQL DAO 实现相同的接口以提供相同数据读写能力。

现在已经有两种 DAO 接口实现,一种有支持 MySQL,另一种是支持 HBase。此时开始引入我们的功能开关管理服务 FeatureProbe。

先在eatureProbe 上创建四个 Boolean 类型功能开关来独立控制对 MySQL 和 HBase 的读写,以其中一个开关(messages-mysql-write)为例配置如下所示:

image.png

接下来我们对外统一提供 saveMessage 方法保存一个消息,代码如下所示:

public void storeMessage(Message message, FPUser fpUser) {
if(fpClient.booleanValue('messages-mysql-write', fpUser, true)) {
mysqlMesseageDao.save(message)
}
if(fpclient.BooleanValue('messages-hbase-write', fpUser, true)) {
hbaseMesseageDao.save(message)
}
}

需要注意的是,我们允许同一消息可能会保存在两个地方,其目的是保证了旧存储(MySQL)的完整性的同时开始将消息写入新数据存储。对于读取来说其实现思路大致和存储类似,例如实现一个能根据 ID 查询消息的方法,代码如下所示:

public Message findMessageById(Long id, FPUser fpUser)  {
// 同时获取查询 mysql 和 hbase 的开关结果
boolean shouldReadMySQL = fpClient.booleanValue('events-mysql-read', fpUser, true)
boolean shouldReadHbase = fpClient.booleanValue('events-hbase-read', fpUser, true)
if (shouldReadMySQL && shouldReadHbase) { // 当开关被同时开启时,将对比两者结果,但仍然返回旧存储数据
mysqlMessage = mysqlMessageDao.findMessageById(id)
hbaseMessage = hbaseMessageDao.findMessageById(id)
if(deepEqualsEntity(mysqlMessage, hbaseMessage)) {
logger.error(
"MySQL and Hbase message differ: mysql: {}, hbase: {}",
mysqlMessage,
hbaseMessage)
}
return mysqlMessage
} else if (shouldReadMySQL) { // 只从 MySQL 查询
return mysqlMessageDao.findMessageById(id)
} else { // 只从 HBase 中查询
return hbaseMessageDao.findMessageById(id)
}
}

这里的点在于,我们检查两个“读取”的开关结果,当发现两者都启用时,我们会分别两个数据库读取数据,并比较结果是否一致,当发现不一致时,我们会记录错误,并最终返回旧存储(MySQL)中的数据。

2、渐进式放量迁移​

现在我们已经将从新老存储读写的代码封装在功能开关中,接下来需要部署代码并进行测试。此时只需要关闭 HBase 读写开关,并打开 mysql 读写开关,便可以安全地将代码部署上线。

我们测试迁移时,只需要在 FeatureProbe 上修改 HBase “写开关”的人群规则,仅为特定用户 QA 开启 ,此时只有 QA 的消息会存储到 HBase,过程中观察性能指标和错误日志,如果一切符合预期,进一步修改人群规则将写 HBase 在更多的用户上生效,直到所有用户的 “写操作” 均同时写入了两个数据库。

当我们写入性能感到满意时,此时可以开始为一小部分用户打开 HBase 的读取开关,然后再次观察性能指标和错误日志,其中特别需要关注是否有** “MySQL and Hbase message differ...”的日志来确保两个存储数据的一致性。当然,这里即便我们看到数据不一致的错误日志,对用户也不会产生任何影响,因为他们仍在使用旧数据存储中的数据。如果数据和指标均符合预期,您可以为部分用户关闭 MySQL 的读关开,将使用 HBase 的数据,并最终直到所有用户均从 HBase 中读取。

最后,当我们向所有用户开启 HBase 读取和写入操作后,应该将所有旧数据从旧数据存储迁移到新数据存储(确保以幂等方式执行此操作)来保证数据的整体完整性。

可见,通过上述功能开关渐进式放量迁移的方式,不仅让使得迁移可以无缝进行(对用户无感知),还有效保障的迁移的安全性。

3、收尾工作​

最后一步,我们需要确保关闭了旧数据库(MySQL)的读写开关,同时删除代码中对所有四个开关的所有引用,使代码中最终只剩下对 HBase (新数据库)的读/写。再次部署上线后,便完成了整个数据库的迁移工作。

在 FeatureProbe 开关管理也很简单,由于以上四个开关已经完成了数据库迁移的使命,对于已经过期、已完成工作的开关都可以使用下线操作进行管理。

image.png

· 8 min read

在上一篇文章,我们讲到了长连接常见的实现方案,相信大家对长连接已经有一定的了解了,这篇文章我们会讲 FeatureProbe 的长连接实现方案。

一、为什么FeatureProbe需要长连接

Feature Toggle 在部分场景下,客户端对实时性有较高的要求,如紧急情况,希望配置立刻下发生效。有的 Feature 在 Web 端加载或 App 启动的时候就要读取到开关的值,虽然缓存能解决一部分问题,但是最快拿到最新的值,会更符合用户的预期。我们在上篇有提到过,长连接可以解决数据推送和请求优化这两个场景。

1、可选协议​

  • SSE :Server Send Event 能满足服务端向客户端发送数据的需求,协议简单,但因为不是双工的数据通路后期无法实现 HTTP 的请求优化。
  • TCP :目前最主流的长连接协议,配合 TLS 1.3 可以做到很好的使用效果。
  • QUIC :本身握手和 TLS1.3 融合,还支持连接恢复,多Stream避免包头阻塞问题,有很多优势,因为基于UDP,可能会有部分特殊网络环境被禁止。
  • UDP :需要自己实现丢包重传,部分网络环境有可能被限制。
  • WebSocket :对浏览器友好,小程序唯一支持的双向收发 (全双工) 协议,很难做连接优化。

2、设计目标​

  • 尽可能支持更多的端,小程序,移动端,多种语言服务端;
  • 尽量降低 SDK 的实现复杂度,方便后期社区贡献;
  • 尽可能使开关快速生效;
  • 尽可能低的数据传输量。

二、FeatureProbe长连接方案

1、协议选择 WebSocket​

小程序是我们一期要优先支持的平台,所以所有不支持小程序的协议都不在一期的考虑范围内。

  • 优点:是可以支持小程序和浏览器环境,小程序是我们优先要支持的部分,在国内的重要性非常高,很多创业团队甚至只开发小程序的 APP 版本。
  • 缺点:是连接建立的优化很难进行.在国内网络环境整体较好的情况下,大部分的请求还是在较快的响应范围之内.我们可以在后面二期的时候再针对其他端做多协议切换。

我们在  Websocket 的基础上进一步选择了 Socektio 这个网络库:

  • 优点:是在 WebSocket 的基础上提供了断开重连,发送缓冲,消息确认,广播,整体的编解码逻辑简单,提供了长轮询(long polling)的回退方案,在不支持 WebSocket 的设备上也能兼容。

  • 缺点:客户端有限,老项目已经比较成熟,目前已经不太活跃。

2、服务端推送​

FeatureProbe Server 发现开关更新后,发送事件给关心这个开关的连接,对端的 SDK 收到事件,触发一次开关拉取。这里面能做的优化是直接下发开关的值,因为 Server SDK 和 Client SDK 的处理逻辑不同,我们放到下个迭代优化。

  • 如何发现变化:开关的规则是存储在 FeatureProbe API 服务中的,目前 FeatureProbe Server 通过接口周期性访问得到,直观的想法是轮询时,去 diff 开关的值,就可以发现变化,但是效率比较低。因为 SDK 是针对项目环境下所有的开关进行获取,这里环境的 SDK KEY 拉取整体的开关规则时,添加一个 version 就可以判断两次之间是否一致。

  • 如何表示 SDK 对某个开关感兴趣: 目前 SDK 向 Featureprobe Server 获取开关,是以 SDK_KEY 为粒度的。在 SocketIO 连接建立后,SDK 会向 Server 注册 SDK_KEY, Server 就可以把这个连接存储在相同 SDK KEY 的列表中,等有开关发生变化,Server 知道开关是发生在哪个 SDK_KEY 中,把 对应 SDK_KEY 列表中所有的连接都发送一个更新事件,就完成了变更的通知。实际实现利用了SocketIO 提供了 Room 的概念,仅需把连接和 SDK KEY做一下关联,变更时直接对 SDK_KEY 发送事件就可以了。

代码示意:

import { createServer } from "http";
import { Server } from "socket.io";
const httpServer = createServer();
const io = new Server(httpServer);
io.on("register", (sdk_key, socket) => {
socket.join(sdk_key);
});
httpServer.listen(3000);
// notify clients
io.to(SDK_KEY).emit("update");

3、客户端接收​

FeatureProbe SDK 目前是 pull 模式和服务端通信,即启动后通过轮询来周期性获取全量开关的数据。在 SocketIO 的帮助下,添加 push 的模式很简单。在原有基础上初始化 SocketIO 的客户端,连接建立后把 SDK KEY 发送给 Server, 然后监听一个 Server 下发的 update 事件,收到事件就立刻触发一次开关全量的拉取。断开重连,心跳,回调等都交给 SocketIO 来做。这里有个优化是下发的数据可以是开关变更的数据,而不仅仅是变更事件,这个也是我们后续准备做的工作。

三、最终实现

FeatureProbe Server 是 Rust 语言实现的,考虑到后续的性能和扩展性等原因,我们不想再引入一个 nodejs 的模块专门做长连接的管理,所以我用 Rust 实现了 SocketIO 的服务端 socketio-rs(实现的rust方案已经开源到GitHub,点击socketio-rs可访问),实际的 FeatureProbe 客户端业务代码和服务端业务代码都相对比较简洁。

· 7 min read

一、什么是长连接?​

长连接可以指 HTTP 持久连接 (persistent connection),也可以指基于 TCP / UDP / QUIC / WebSocket 等一个或多个协议建立后可以持续收发消息的数据通路。本文主要介绍的是后者,其中以微信2017年初开源的 Mars 被大家熟知。从 Mars 的 issue 中我们可以看到 Longlink 这个国内长连接的直译,目前还没有特别好的英文术语。实际上 Mars 只是长链接架构中的客户端,还需要一个服务端来配合。

long1.png

二、国内长连接现状​

目前国内的大厂基本上都有自己的网关团队,长连接服务是网关中的子服务,客户端团队负责端上的网络库(如 Mars),网关相关的公开资料可以查询到的如阿里的 ACCS ,美团的 Shark 等

提供了诸如 消息推送,消息广播,多协议切换,HTTP代理,多接入点容灾等功能。可覆盖 即时通讯,弹幕,互动游戏,竞拍等多个业务。

三、长连接解决的问题​

总的来说主要解决的数据实效性的问题:

1、数据推送​

最常见的案例就是消息通知。

  • 最简单的做法是启动一个计时器,周期性轮询(polling )一个接口。这种方案常见于早期基于浏览器的项目。坏处是间隔设置的太长用户体验不好,设置间隔太短后端服务会进行大量的无效查询。

  • 稍好一点的做法是使用长轮询(long polling),发送 HTTP 请求后,如果没有新数据,服务端就一直不返回 HTTP 的 response。这种方式减少了大量无效的查询,但是如果新数据频繁,会进行大量的连接建立和关闭。常见于早期浏览器 WebSocket 协议支持不完整的时候。

  • 目前主流的方案是浏览器基于 WebSocket,移动端基于 TCP / UDP/ QUIC 的全双工数据通道的方案。一次连接建立,服务端维护连接和业务的对应关系(如用户id),当用户有新数据时,找到对应的连接,将数据发送出去,浏览器或者移动端就可以立刻收到消息,返回给上层。

可以推测手机厂商给 APP 提供的离线消息推送的通道,如果想做到最好的实时性,也需要维护一个手机和厂商服务的连接,在 APP 后端发送推送后,厂商转发给手机,展示给用户。

long2.png

2、请求优化​

大多数业务的API请求还是基于 HTTP(S) 的,在大多数情况下,国内网络质量良好。少部分的网络优化场景可以代理 HTTP(S) 的请求将会是更具有性价比的方案。

常见的方案是移动端的网络库代码中,添加一个拦截逻辑,把请求的参数重新封装成底层报文,通过全双工的数据通路发送给网关,网关组装成 HTTP 的请求,在内网发送给对应的业务服务器,再把返回封装后发送给对应的数据通道。

一次完整的 HTTPS 请求的步骤包括:

long3.png

在海量用户请求的情况下,任何一个环节都可能出错,导致请求失败。如 DNS 污染,错配导致解析错误,或者返回慢。用户在山区或者海外,数据包来回(RTT)延迟大,握手环节过于复杂,证书过大,超时配置不合理,包过小导致慢启动阶段过慢,数据带宽利用不足等。

  • 复用之前建立的底层连接,持续地发送和接收 HTTP 请求,将会节省很多时间。
  • 直接用 IP 连接,自己通过 HTTPS 接口下发 域名的 IP 地址,就绕过了 DNS 解析的环境,也避免 DNS 被污染的问题。
  • 使用更高版本的 TLS 协议,或者使用 QUIC 协议,可以减少建立连接的握手次数。

long4.png

我们以 RTT 为 200ms 举例,采用 TCP + TLS 1.2 的请求首次需要至少 600ms 才能建立连接,打开TLS会话复用需要 400ms,采用 TCP + TLS 1.3 的首次请求 400ms 就可以连接上,复用是200ms,采用 QUIC 协议的话首次是 200ms,复用是 0ms,具体协议的细节超过了本文的范围,当用户处于弱网环境,这个 RTT 可能更差,可见协议的优化就可以带来更好的响应。

本文是长连接介绍的上篇,主要介绍了网上公开的业界长连接技术方案。下篇将会介绍如何针对自己的业务选择合适的技术方案。

目前我们实现长链接的方案已经完全开源。你可以从 GitHub 获取到所有源代码。

· 6 min read

运维开关(Ops Toggles)是特性管理(Feature Management)的核心应用场景之一。这类开关通常从运维的角度管控系统功能,比如当我们上线的新功能出现问题或某个依赖服务出现故障时,运维或研发人员可以禁用功能或服务降级,来减少故障对系统整体的影响。本文将介绍如何使用 FeatureProbe 实现手动降级开关和自动降级开关。

一、什么是降级开关 ?​

服务降级作为服务容错的常用方式之一,其思想是牺牲系统中非核心功能或服务来保证系统整体可用性。常用的服务降级方式有熔断降级、限流降级以及开关降级,这些不同的降级方式分别应对不同的故障场景。

关于熔断降级和限流降级方式不展开介绍,这里的开关降级指的是在代码中预先埋设一些开关,并实时获取开关的状态来控制服务的行为。比如,开关开启的时候访问下游服务获取实时数据,当依赖的下游服务出现故障时,立即关闭开关来访问旧的缓存数据或默认值;再比如,在电商大促期间为了保障核心服务用到的计算资源,通过开关来关闭非核心服务。

下面是利用开关执行降级逻辑的代码示例:

degradeRpcService := fpClient.BoolValue("degrade_rpc_service", false)
if degradeRpcService {
// 执行降级逻辑, 如从缓存中获取旧数据} else {
// 调用正常逻辑,如从远程服务获取实时数据
}

其中 degrade_rpc_service 就是一个典型的降级开关。

二、如何实现手动降级开关?​

实现降级开关通常会考虑使用配置中心、Redis 或数据库等来存储开关值,并用对应的 client 端获取开关结果。但这些通用工具的使用界面对开关场景的用户来说并不十分友好。FeatureProbe 作为专门的开关管理服务,不仅提供了 client 实时获取开关状态的功能,同时还能让你在统一的平台上可视化管理开关和控制开关状态,让开关控制更加高效、安全。

降级开关通常是一个 boolean 类型的开关,对应的返回(分组)值也只有两种情况,如下图所示:

应用程序可通过接入 FeatureProbe SDK 来获取该降级开关返回值,以 Java 代码为例:

boolean isDegrade = fpClient.boolValue("degrade_rpc_service", new User(), false);
if (isDegrade) {
// 降级处理逻辑
return;
}

当 RPC 调用的服务出现故障时,只需要修改默认规则中的返回值为“降级”并发布,即可实现快速人工降级操作。

三、如何实现自动降级开关?​

为了尽可能提高降级效率,某些开关降级场景不希望需要人工干预降级。比如我们希望在 11月10号 23:59 时对某些服务执行降级以应对第二天的大促活动;当我们监控系统发现下游服务触发 P0 报警时,希望立即执行降级等等。这些自动降级场景都可以非常方便地使用 FeatureProbe 实现。

1、基于规则的自动降级​

FeatureProbe 提供了灵活规则配置,让你实现自动降级。例如双十一大促开始时,需要关闭退款服务,以满足大部分消费者在平台上获得稳定的交易体验。如下图所示,提前配置好降级规则后,将在 11.10 23:59:59 时自动执行对服务降级而不需要人工干预。

对于上述降级规则在接入 SDK 的代码也无须特殊处理,FeatureProbe SDK 将自动根据服务器时间来决定是否降级。

2、基于外部触发的自动降级​

另外一种自动降级场景是由外部系统触发,如监控系统。该场景下可以使用 FeatureProbe OpenAPI 来自动变更开关状态。如下脚本所示,通过 OpenAPI 修改开关默认返回值实现自动降级操作:

curl 'https://featureprobe.io/api/projects/{PROJECT KEY}/environments/{ENV KEY}/toggles/{TOGGKE KEY}/targeting' \
-X 'PATCH' \
-H 'Authorization: {YOU API KEY}' -H 'Content-Type: application/json' --data-raw '{
"comment":"执行降级",
"content":{
"defaultServe":{
"select": 0
},
"variations":[
{
"value":"true",
"name":"降级",
},
{
"value":"false",
"name":"不降级",
}
]
}
}'