2024年9月

作者:来自 vivo 互联网存储团队- Xu Xingbao

Redis 集群经常需要进行在线水平扩缩容,实际操作过程中发现迁移期间服务时延剧烈抖动,业务侧感知明显,为了应对以上问题对原生 Redis 集群 slot 迁移功能进行优化改造。

一、背景介绍

Redis 集群服务在互联网公司被广泛使用,众所周知服务集群化可以突破单节点的能力瓶颈,带来规模、可用性、扩展性等多方面的收益。在实际使用 Redis 集群的过程中,发现在进行涉及集群数据迁移的水平扩缩容操作时,业务侧多次反馈 Redis 请求的时延升高问题,甚至发生过扩容操作导致集群节点下线的可用性故障,并进一步引发迁移流程中断、节点间数据脑裂等一系列严重影响,给运维同事带来极大困扰,严重影响线上服务的稳定。

二、问题分析

2.1 原生迁移介绍

Redis 集群功能采用无中心架构设计,集群中各个节点都维护各自视角的集群拓扑并保存自有的分片数据,集群节点间通过 gossip 协议进行信息协调和变更通知。具体来说 Redis 集群数据管理上采用虚拟哈希槽分区机制,将数据的键通过哈希函数映射到 0~16383 整数槽内,此处的槽位在 Redis 集群设计中被称为 slot。这样实际上每一个节点只需要负责维护一部分 slot 所映射的键值数据,slot 就成为 Redis 集群管理数据的基本单位,集群扩缩容本质上就是 slot 信息和 slot 对应数据数据在节点之间的转移。Redis 集群水平扩展的能力就是基于 slot 维度进行实现,具体流程如下图所示。

上图所示的迁移步骤中,步骤 1-2 是对待迁移 slot 进行状态标记,方便满足迁移过程中数据访问,步骤 3-4 是迁移的核心步骤,这两个步骤操作会在步骤 5 调度下持续不断进行,直到待迁移 slot 的键值数据完全迁移到了目标节点,步骤 6 会在数据转移完成后进行,主要是发起集群广播消息更新集群内节点 slot 拓扑。

由于正常的迁移时一个持续的处理过程,不可避免地会出现正在迁移 slot 数据分布于迁移两端地“分裂”状态,这种状态会随着 slot 迁移的流程进行而持续存在。为了保证迁移期间正在迁移的 slot 数据能够正常读写,Redis 集群实现了下图所示的一种 ask-move 机制,如果请求访问正在迁移的 slot 数据,请求首先会按照集群拓扑正常访问到迁移的源节点,如果在源节点查询到数据则正常处理响应请求;如果在源节点没有找到请求所需数据,则会给客户端回复 ASK {ip}:{port}  消息回包。

Redis 集群智能客户端收到该回包后会按照包内节点信息找到新节点重试命令,但是由于此时目标节点还没有迁移中 slot 的所属权,所以在重试具体命令之前智能客户端会首先向目的节点发送一个 asking 命令,以此保证接下来访问迁移中 slot 数据的请求能被接受处理。由于原生迁移时按照 key 粒度进行的,一个 key 的数据要不存在源节点,要不存在目的节点,所以 Redis 集群可以通过实现上述 ask-move 机制,保证迁移期间数据访问的一致性和完整性。

2.2 迁移问题分析

(1)时延分析
根据上述原生 Redis 集群迁移操作步骤的了解,可以总结出原生迁移功能按照 key 粒度进行的,即不断扫描源节点上正在迁移的 slot 数据并发送数据给目的节点,这是集群数据迁移的核心逻辑。微观来说迁移单个 key 数据对于服务端来说包含以下操作:

  • 序列化待迁移键值对数据;
  • 通过网络连接发送序列化的数据包;
  • 等待回复(目标端接收完包并加载成功才会返回);
  • 删除本地残留的副本,释放内存。

上述操作中涉及多个耗费线程处理时长的操作,首先序列化数据是非常耗费 CPU 时间的操作,如果遇到待迁移 key 比较大线程占用时长也会随之恶化,这对于单工作线程的 Redis 服务来说是不可接受的,进一步地网络发送数据到目标节点时会同步等待结果返回,而迁移目的端又会在进行数据反序列化和入库操作后才会向源节点进行结果返回。需要注意的是在迁移期间会不断循环进行以上步骤的操作,而且这些步骤是在工作线程上连续处理的,期间无法对正常请求进行处理,所以此处就会导致服务响应时延持续突刺,这一点可以通过 slowlog 的监控数据得到验证,迁移期间会在 slowlog 抓取到大量的 migrate 和 restore 命令。

(2)ask-move 开销
正常情况下每个正在迁移的 slot 数据都会一段时间内存在数据分布在迁移的两端的情况,迁移期间该 slot 数据访问请求可以通过 ask-move 机制来保证数据一致性,但是不难看出这样的机制会导致单个请求网络访问次数出现成倍的增加,对客户端也存在一定的开销压力。另外,对于可能存在的用户采用 Lua 或者 Pipline 这种需要对单个 slot 内多 key 连续访问的场景,目前大部分集群智能客户端支持有限,可能会遇到迁移期间相关请求不能正常执行的报错。另外需要说明的是,由于 ask-move 机制的只在迁移两端的主节点上能触发,所以迁移期间从节点是不能保证数据请求结果一致性的,这对于采用读写分离方式访问集群数据的用户也非常不友好。

(3)拓扑变更开销
为了降低迁移期间数据 ask-move 的机制对请求的影响,正常情况下原生迁移每次只会操作一个 slot 迁移,这就导致对每一个迁移完成的 slot 都会触发集群内节点进行一次拓扑更新,而每次集群拓扑的更新都会触发正在执行指令的业务客户端几乎同时发送请求寻求更新集群拓扑,拓扑刷新请求结果计算开销高、结果集大,大大增加了节点的处理开销,也会造成正常服务请求时延的突刺,尤其对于连接数较大、集群节点多的集群,集中的拓扑刷新请求很容易造成节点计算资源紧张和网络拥塞,容易触发出各种服务异常告警。

(4)迁移无高可用
原生的迁移的 slot 标记状态只存在于迁移双端的主节点,其对应的从节点并不知道迁移状态,这也就导致一旦在迁移期间发生节点的 failover,迁移流程将会中断和出现 slot 状态残留,也将进一步导致迁移 slot 数据的访问请求无法正常触发 ask-move 机制而发生异常。例如迁移源节点异常,那么其 slave 节点 failover 上线,由于新主节点并不能同步到迁移状态信息,那么对于迁移中 slot 的请求就不能触发 ask 回复,如果是一个对已经迁移至目标节点的数据的写请求,新主节点会直接在本节点新增 key,导致数据出现脑裂,类似地如果处理的是已经迁移数据的读取请求也无法保证返回正确结果。

三、优化方案

3.1 优化方向思考

通过原生数据迁移机制分析,可以发现由于迁移操作涉及大量的同步阻塞操作会长时间占用工作线程,以及频繁的拓扑刷新操作,会导致请求时延不断出现上升。那么是否可以考虑将阻塞工作线程的同步操作改造成为异步线程处理呢?这样改造有非常大的风险,因为原生迁移之所以能够保证迁移期间数据访问的正确性,正是这些同步接口进行了一致性保证,如果改为异步操作将需要引入并发控制,还要考虑迁移数据请求与 slave 节点的同步协调问题,此方案也无法解决拓扑变动开销问题。所以 vivo 自研 Redis 放弃了原生按照 key 粒度进行迁移的逻辑,结合线上真实扩容需求,采用了类似主从同步的数据迁移逻辑,将迁移目标节点伪装成迁移源节点的从节点,通过主从协议来转移数据。

3.2 功能实现原理

Redis 主从同步机制是指在 Redis 主节点(Master)和从节点(Slave)之间进行数据同步和复制的过程,主从同步机制可以提高 Redis 集群的可用性,避免单点故障和数据丢失等问题。Redis 目前主从同步有全量同步和部分同步两种方式,从节点发送同步位点给主节点,如果是首次同步则需要走全量同步逻辑,主节点通过发送 RDB 基础数据文件和传播增量命令方式将数据同步给从节点;如果不是首次同步,主节点则会通过从节点同步请求中的位点等信息判断是否满足增量同步条件,优先进行增量同步以控制同步开销。由于主节点在同步期间也在持续处理新的命令请求,所以从节点对主节点的数据同步是一个动态追齐的过程,正常情况下,主节点会持续发送写命令给从节点。

基于同步机制,我们设计实现了一套如下图所示的 Redis 集群数据迁移的功能。迁移数据逻辑主要走的全量同步逻辑,迁移数据和同步数据最大的区别在于,正常情况下需要迁移的是源节点部分 slot 数据,目标节点并不需要复制源节点的全量数据,完全复用同步机制会产生不必要的开销,需要对主从同步逻辑进行修改适配。为了解决该问题,我们对相关逻辑做了一些针对性的改造。首先在同步命令交互上,针对迁移场景增加了迁移节点间 slot 信息交互,从而让迁移源节点获知需要迁移哪些 slot 到哪个节点。另外,我们还对 RDB 文件文件结构按照 slot 顺序进行了调整改造,并且将各个 slot 数据的文件起始偏移量数据作为元数据记录到 RDB 文件尾部固定位置,这样在进行迁移操作的 RDB 传输步骤时就可以方便地索引到 RDB 文件中目标 slot 数据片段。

3.3 改造效果分析

(1)时延影响小
对于 slot 迁移操作而言,主要涉及迁移源和目的两端的开销,对于基于主从同步机制实现的新 slot 迁移,其源节点主要开销在于生成 RDB 和传送网络包,正常对于请求时延影响不大。但是因为目的节点需要对较大的 RDB 文件片段数据进行接收、加载,由于目的节点迁移时也需要对正常服务请求响应,此时不再能采用类似 slave 节点将所有数据收取完以后保存本地文件,然后进行阻塞式数据加载的方案,所以新 slot 迁移功能对迁移目的节点的数据加载流程进行了针对性改造,目的节点会按照接收到的网络包粒度将数据按照下图所示进行递进式加载,即 slot 迁移目标节点每接收完一个 RDB 数据网络包就会尝试加载,每次只加载本次网络包内包含的完整元素,这样复合类型数据就可以按照 field 粒度加载,从而降低多元素大 key 数据迁移对访问时延的剧烈影响。通过这样的设计保持原来单线程简洁架构的同时,有效地控制了时延影响,所有数据变更操作都保持在工作线程进行,不需要进行并发控制。通过以上改造,基本消除了迁移大 key 对迁移目的节点时延影响。

(2)数据访问稳定
新 slot 迁移操作期间,正在迁移的数据还是存储在源节点上没有变,请求继续在源节点上正常处理,用户侧的请求不会触发 ask-move 转发机制。这样用户就不需要担心读写分离会出现数据不一致现象,在进行事务、pipeline 等方式封装执行命令时也不会出现大量请求报错的问题。迁移动作一旦完成,残留在源端的已迁移 slot 数据将成为节点的残留数据,这部分数据不会再被访问,对上述残留数据的清理被设计在 serverCron 中逐步进行,这样每一次清理多少数据可以参数化控制,可以根据需要进行个性化设置,保证数据清理对正常服务请求影响完全可控。

(3)拓扑变更少
原生的迁移功能为了降低 ask-move 机制对正常服务请求的影响,每次仅会对一个 slot 进行数据迁移,迁移完了会立即发起拓扑变更通知来集群节点转换 slot 的属主,这就导致拓扑变化的次数随着迁移 slot 的数量增加而变多,客户端也会在每一次感知到拓扑变化后发送命令请求进行拓扑更新。更新拓扑信息的命令计算开销较大,如果多条查询拓扑的命令集中处理,就会导致节点资源的紧张。新的 slot 迁移按照节点进行数据同步,可以支持同时迁移源节点的多个 slot 甚至全部数据,最后可以通过一次拓扑变更转换多个 slot 的属主,大大降低了拓扑刷新的影响。

(4)支持高可用
集群的数据迁移是一个持续的过程,这个过程可能长达几个小时,期间服务可能发生各种异常情况。正常情况下的 Redis 集群具有 failover 机制,从节点可感知节点异常以代替旧主节点进行服务。新 slot 迁移功能为了应对这样的可用性问题,将 slot 迁移状态同步给从节点,这样迁移期间如果集群迁移节点发生 failover,其从节点就可以代替旧主节点继续推进数据迁移流程,保证了迁移流程的高可用能力,避免人工干预,大大简化运维操作复杂度。

四、功能测试对比

为了验证改造后迁移功能的效果,对比自研迁移和原生迁移对请求响应的影响,在三台同样配置物理机上部署了原生和自研两套相同拓扑的集群,选择后对 hash 数据类型的 100k 和 1MB 两种大小数据分别进行了迁移测试,每轮在节点间迁移内存用量 5G 左右的数据。测试主要目的是对比改造前后数转移对节点服务时延影响,所以在实际测试时没有对集群节点进行背景流量操作,节点的时延数据采用每秒钟 ping 10 次节点的方式进行采集,迁移期间源节点和目的节点的时延监控数据入下表所示(纵轴数值单位:ms)。

通过对比以上原生和自研集群 slot 迁移期间的时延监控数据,可以看出自研 slot 迁移功能迁移数据期间迁移两端节点的请求响应时延表现非常平稳,也可以表现出经过主从复制原理改造的 Redis 集群 slot 迁移功能具备的优势和价值。

五、总结和展望

原生 Redis 集群的扩缩容功能按照 key 粒度进行数据转移,较大的 key 会造成工作线程的长时间占用,进而引起正常服务请求时延飙高问题,甚至导致节点长时间无法回复心跳包而被判定下线的情况,存在稳定性风险。通过同步机制改造实现的新 slot 迁移功能,能显著降低数据迁移对用户访问时延的影响,提升线上 Redis 集群稳定性和运维效率,同时新的 slot 迁移功能还存在一些问题,例如新的迁移造成节点频繁的 bgsave 压力,迁移期间节点内存占用增加等问题,未来我们将围绕这些具体问题,继续不断优化总结。

前言

给大家推荐一款开源的 Winform 控件库,可以帮助我们开发更加美观、漂亮的 WinForm 界面。

项目介绍

SunnyUI.NET 是一个基于 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 开源控件库,同时也提供了工具类库、扩展类库和多页面开发框架。

基于 .NET Framework 4.0,采用原生控件开发,参考 Element 主题风格,包含超过 70 个常用控件,如按钮、编辑框、下拉框、数据表格、工控仪表和统计图表,满足常规开发需求,每个控件都精心设计,注重细节。

提供 11 个 Element 风格主题和 6 个其他主题,支持多彩主题模式自定义。包含主题管理组件 UIStyleManager,可自由切换主题。

项目架构

控件库拥有不同的主题、字体和语言、包括了常见的组件Button、Label、CheckBox、TreeView和TabControl、对话框、进度条、消息提示,提供了扩展库和工具库,方便我们开发和使用,具体如下图所示:

项目环境

1、源码编译环境

VS 2022,安装.NET Framework 4.0 目标包

编译源码,.NET 8 需要 VS 2022 17.8+ 版本,或者修改 SunnyUI.csproj 文件的 TargetFrameworks 属性以适应 VS 环境

2、动态库应用环境

VS 2010 及以上均可,支持.NET Framework 4.0+、.NET 6、.NET 7

推荐通过 Nuget 安装

PM->Install-Package SunnyUI

或者通过 Nuget 搜索 SunnyUI 安装,不支持(.NET Framework 4
Client Profile
)。

项目展示

1、主题

SunnyUI为了避免视觉传达差异,使用一套特定的调色板来规定颜色,为你所搭建的产品提供一致的外观视觉感受。

主要颜色参照Element(
https://element.eleme.cn/

主色

SunnyUI主要品牌颜色是鲜艳、友好的蓝色。

Style主题

SunnyUI包含 Element 风格主题 11 个,DotNetBar 主题 3 个,其他主题 2 个,包含主题管理组件 UIStyleManager,可自由切换主题。

UIStyle.Blue

UIStyle.Purple

2、国际化

SunnyUI 控件内部默认使用中文,若希望使用其他语言,则需要进行多语言设置。

本页面所描述的国际化是针对SunnyUI内的按钮、标题等中文资源的国际化

如您开发的系统需要做国际化请自行开发。

常用的按钮、标题、提示等文字已经设置为静态字符串变量,存于ULocalize.cs文件中。

public static classUILocalize
{
public static string InfoTitle = "提示";public static string SuccessTitle = "正确";public static string WarningTitle = "警告";public static string ErrorTitle = "错误";public static string AskTitle = "提示";public static string InputTitle = "输入";public static string CloseAll = "全部关闭";public static string OK = "确定";public static string Cancel = "取消";public static string GridNoData = "[ 无数据 ]";public static string GridDataLoading = "数据加载中 ......";public static string GridDataSourceException = "数据源必须为DataTable或者List";
}

可以重写UILocalize类静态变量值来改变语言。

UILocalizeHelper类已经包含中文和英文的默认配置函数:

public static classUILocalizeHelper
{
public static voidSetEN()
{
UILocalize.InfoTitle
= "Info";
UILocalize.SuccessTitle
= "Success";
UILocalize.WarningTitle
= "Warning";
UILocalize.ErrorTitle
= "Error";
UILocalize.AskTitle
= "Query";
UILocalize.InputTitle
= "Input";
UILocalize.CloseAll
= "Close all";
UILocalize.OK
= "OK";
UILocalize.Cancel
= "Cancel";
UILocalize.GridNoData
= "[ No data ]";
UILocalize.GridDataLoading
= "Data loading ......";
UILocalize.GridDataSourceException
= "The data source must be DataTable or List";
}
public static voidSetCH()
{
UILocalize.InfoTitle
= "提示";
UILocalize.SuccessTitle
= "正确";
UILocalize.WarningTitle
= "警告";
UILocalize.ErrorTitle
= "错误";
UILocalize.AskTitle
= "提示";
UILocalize.InputTitle
= "输入";
UILocalize.CloseAll
= "全部关闭";
UILocalize.OK
= "确定";
UILocalize.Cancel
= "取消";
UILocalize.GridNoData
= "[ 无数据 ]";
UILocalize.GridDataLoading
= "数据加载中 ......";
UILocalize.GridDataSourceException
= "数据源必须为DataTable或者List";
}
}

如需要其他语言,则在自己程序里写函数更新UILocalize类静态变量值即可。

中英文效果展示:

3、字体图标

SunnyUI的字体图标目前主要有两个:

FontAwesome

https://fontawesome.com/

https://github.com/FortAwesome/Font-Awesome

ElegantIcons.ttf V1.0

https://www.elegantthemes.com/blog/resources/elegant-icon-font

这两个都是目前 Web 开发常用的字体图标,SunnyUI经过精心研发,将他们用于.NET Winform开发,省去了到处找图标的麻烦。

SunnyUI 常用字体图标的控件为 UISymbolButton 和 UISymbolLabel

字体图标的选择方法是设置UISymbolButton和UISymbolLabel的以下属性

Symbol:字体图标(int)

SymbolSize:字体图标的大小(int)

4、控件库

常用的各类组件都有,具体内容可以下载源码学习。

5、窗体

UIForm
常用的窗体基类

UILoginForm:登录窗体基类

6、多页面框架

SunnyUI多页面框架由框架和页面构成。最基本的实现是框架(IFrame)由UIForm实现,页面由(UIPage)实现。

在UIForm放置一个UITabControl,将多个UIPage放置于UIForm的UITabControl的TabPage内。

通过PageIndex(正整数,唯一)进行页面的关联和切换。

听起来有点复杂,其实主要就IFrame接口的三个函数:AddPage,ExistPage,SelectPage。

7、工具类库

简易的 Json 静态类库,可以在不引用 NewtonJson 即可简单处理 Json 对象。当然如果有复杂需求,第三方库还是推荐NewtonJson。另外在.NET 5,System.Text.Json的性能已经非常不错了,也可以尝试。

项目地址

GitHub:
https://github.com/yhuse/SunnyUI

Gitee:
https://gitee.com/yhuse/SunnyUI

帮助文档:
https://gitee.com/yhuse/SunnyUI/wikis/pages

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!

Vision Transformer
(
ViT
) 架构传统上采用基于网格的方法进行标记化,而不考虑图像的语义内容。论文提出了一种模块化的超像素非规则标记化策略,该策略将标记化和特征提取解耦,与当前将两者视为不可分割整体的方法形成了对比。通过使用在线内容感知标记化以及尺度和形状不变的位置嵌入,与基于图像块的标记化和随机分区作为基准进行了对比。展示了在提升归因的真实性方面的显著改进,在零样本无监督密集预测任务中提供了像素级的粒度,同时在分类任务中保持了预测性能。

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: A Spitting Image: Modular Superpixel Tokenization in Vision Transformers

Introduction


在卷积架构之后,
Vision Transformers
(
ViTs
) 已成为视觉任务的焦点。在最初的语言模型的
Transformer
中,标记化是一个至关重要的预处理步骤,旨在基于预定的熵度量最佳地分割数据。随着模型被适配于视觉任务,标记化简化为将图像分割为正方形的图像块。这种方法被证明是有效的,很快成为了标准方法,成为架构的一个重要组成部分。

尽管取得了明显的成功,论文认为基于图像块的标记化存在固有的局限性。首先,标记的尺度通过固定的图像块大小与模型架构严格绑定,忽视了原始图像中的冗余。这些局限性导致在较高分辨率下计算量显著增加,因为复杂度和内存随标记数量呈平方级增长。此外,规则的分割假设了语义内容分布的固有均匀性,从而高效地降低了空间分辨率。

随后,若干研究利用注意力图来可视化类标记的归因,以提高可解释性,这常应用于密集预测任务。然而,正方形分割产生的注意力图在图像块表示中会引起分辨率的丧失,进而无法本质上捕捉原始图像的分辨率。对于像素级粒度的密集预测,需要一个单独的解码器进行放大处理。

Motivation

论文从原始的
ViT
架构中退一步,重新评估基于图像块的标记化的作用。通过关注架构中这个被忽视的组件,将图像分割定义为一个自适应模块化标记器的角色,这是
ViTs
中未被充分利用的潜力。

与正方形分割相比,超像素提供了一个机会,通过允许尺度和形状的适应性,同时利用视觉数据中的固有冗余来缓解基于图像块的标记化的缺陷。超像素已被证明与图像中的语义结构更好地对齐,这为在视觉
Transformer
架构中的潜在用途提供了依据。论文将标准
ViTs
中的经典正方形标记化与超像素标记化模型(
SPiT
)进行比较,并使用随机
Voronoi
标记化(
RViT
)(明确定义的数学对象,用于镶嵌平面)作为对照,后者因其作为平面镶嵌的数学对象而被选中,三种标记化方案在图
1
中进行了说明。

Contributions

论文的研究引出了三个具体的问题:(
a
)对正方形图像块的严格遵守是否必要?(
b
)不规则分割对标记化表示有什么影响?(
c
)标记化方案是否可以设计为视觉模型中的一个模块化组件?

经过实验验证,论文得到了以下结论:

  1. Generalized Framework
    :超像素标记化作为模块化方案中推广到了
    ViTs
    ,为视觉任务提供更丰富的
    Transformer
    空间,其中
    Transformer
    主干与标记化框架是独立的。
  2. Efficient Tokenization
    :提出了一种高效的在线标记化方法,该方法在训练和推理时间上具有竞争力,同时在分类任务中表现出色。
  3. Refined Spatial Resolution
    :超像素标记化提供了语义对齐的标记,具有像素级的粒度。与现有的可解释性方法相比,论文的方法得到更显著的归因,并且在无监督分割中表现出色。
  4. Visual Tokenization
    :论文的主要贡献是引入了一种新颖的方法来思考
    ViTs
    中的标记化问题,这是建模过程中的一个被忽视但核心的组成部分。

论文的主要目标是评估
ViTs
的标记化方案,强调不同标记化方法的内在特性。为了进行公平的比较分析,使用基础的
ViT
架构和既定的训练协议进行研究。因此,论文设计实验以确保与知名基线进行公平比较,且不进行架构优化。这种受控的比较对于将观察到的差异归因于标记化策略至关重要,并消除了特定架构或训练方案带来的混杂因素。

Methodology


为了评估和对比不同的标记化策略,需要对图像进行分割并从这些分割中提取有意义的特征。虽然可以使用多种深度架构来完成这些任务,但这些方法会给最终模型增加一层复杂性,从而使任何直接比较标记化策略的尝试失效。此外,这也会使架构之间的有效迁移学习变得复杂。基于这一原因,论文构建了一个有效的启发式超像素标记化器,并提出了一种与经典
ViT
架构一致的非侵入性特征提取方法,以便进行直接比较。

  • Notation

定义
\(H {\mkern1mu\times\mkern1mu} W = \big\{(y, x) : 1 \leq y \leq h, 1 \leq x \leq w\big\}\)
表示一个空间维度为
\((h, w)\)
的图像的坐标,并让
\(\mathcal I\)
为映射
\(i \mapsto (y, x)\)
的索引集。将一个
\(C\)
通道的图像视为信号
\({\xi\colon \mathcal I \to \mathbb R^C}\)
,定义向量化操作符
\(\mathrm{vec}\colon \mathbb{R}^{d_1 {\mkern1mu\times\mkern1mu} \dots {\mkern1mu\times\mkern1mu} d_n} \to \mathbb{R}^{d_1 \dots d_n}\)
,并用
\(f(g(x)) = (f \circ g)(x)\)
表示函数的组合。

Framework

论文通过允许模块化的标记化器和不同的特征提取方法,来对经典
ViT
架构进行泛化。值得注意的是,经典的
ViT
通常被呈现为一个由三部分组成的系统,包括一个标记嵌入器
\(g\)
、一个由一系列注意力块组成的主干网络
\(f\)
,以及一个后续的预测头
\(h\)
。实际上,可以将图像块嵌入模块重写为一个由三个部分组成的模块化系统,包含一个标记化器
\(\tau\)
、一个特征提取器
\(\phi\)
和一个嵌入器
\(\gamma\)
,使得
\(g = \gamma \circ \phi \circ \tau\)

这些是原始架构中的固有组件,但在简化的标记化策略下被掩盖了。这为模型作为一个五部分系统提供了更完整的评估。

\[\label{eqn:pipeline}
\begin{align}
\Phi(\xi;\theta) &= (h \circ f \circ g)(\xi; \theta), \\
&= (h \circ f \circ \gamma \circ \phi \circ \tau)(\xi; \theta),
\end{align}
\]

其中
\(\theta\)
表示模型的可学习参数集合。在标准的
ViT
模型中,标记化器
\(\tau\)
将图像分割为固定大小的方形区域。这直接提供了向量化的特征,因为这些图像块具有统一的维度和顺序,因此在标准的
ViT
架构中,
\(\phi = \mathrm{vec}\)
。嵌入器
\(\gamma\)
通常是一个可学习的线性层,将特征映射到特定架构的嵌入维度。另一种做法是,将
\(g\)
视为一个卷积操作,其卷积核大小和步幅等于所需的图像块大小
\(\rho\)

Partitioning and Tokenization

语言任务中的标记化需要将文本分割为最优信息量的标记,这类似于超像素将空间数据分割为离散的连通区域。层级超像素是一种高度可并行化的基于图的方法,适合用于在线标记化。基于此,论文提出了一种新方法,该方法在每一步
\(t\)
中进行批量图片图的完全并行聚合,此外还包括对大小和紧凑性的正则化。在每一步产生不同数量的超像素,动态适应图像的复杂性。

  • Superpixel Graphs


\(E^{(0)} \subset \mathcal I {\mkern1mu\times\mkern1mu} \mathcal I\)
表示在
\(H {\mkern1mu\times\mkern1mu} W\)
下的四向邻接边。将超像素视为一个集合
\(S \subset \mathcal I\)
,并且如果对于
\(S\)
中的任意两个像素
\(p\)

\(q\)
,存在一个边的序列
\(\big((i_j, i_{j+1}) \in E^{(0)}\big)_{j=1}^{k-1}\)
,使得
\(i_1 = p\)

\(i_k = q\)
,则认为
\(S\)
是连通的。如果对于任意两个不同的超像素
\(S\)

\(S' \in \pi\)
,它们的交集
\(S \cap S' = \emptyset\)
,并且所有超像素的并集等于图像中所有像素位置的集合,即
\(\bigcup_{S \in \pi^{(t)}} S = \mathcal I\)
,那么一组超像素就形成了图像的分割
\(\pi\)


\(\Pi(\mathcal I) \subset 2^{2^{\mathcal I}}\)
表示图像的所有分割的空间,并且有一系列分割
\((\pi^{(t)})_{t=0}^T\)
。如果对于
\(\pi^{(t)}\)
中的所有超像素
\(S\)
,存在一个超像素
\(S' \in \pi^{(t+1)}\)
使得
\(S \subseteq S'\)
,则认为分割
\(\pi^{(t)}\)
是另分割
\(\pi^{(t+1)}\)
的细化,用
\(\pi^{(t)} \sqsubseteq \pi^{(t+1)}\)
来表示。目标是构造一个 像素索引的
\(T\)
级层级分割
\({\mathcal H = \big( \pi^{(t)} \in \Pi(\mathcal I) : \pi^{(t)} \sqsubseteq \pi^{(t+1)} \big)_{t=0}^T}\)
,使得每个超像素都是连通的。

为了构造
\(\mathcal H\)
,通过并行边收缩(用一个顶点代替多个顶点,被代替的点的内部边去掉,外部边由代替的顶点继承)的方式逐步连接顶点,以更新分割
\({\pi^{(t)} \mapsto \pi^{(t+1)}}\)
。通过将每个层级视为图
\(G^{(t)} = (V^{(t)}, E^{(t)})\)
来实现,其中每个顶点
\(v \in V^{(t)}\)
是分割
\(\pi^{(t)}\)
中一个超像素的索引,每条边
\((u, v) \in E^{(t)}\)
代表在
\(t = 0, \dots, T\)
层级中相邻的超像素。因此,初始图像可以表示为一个网格图
\({G^{(0)} = (V^{(0)}, E^{(0)})}\)
,对应于单像素分割
\({\pi^{(0)} = \big\{\{i\} : i \in \mathcal I \big\}}\)

  • Weight function

为了应用边收缩,定义一个边权重函数
\(w_\xi^{(t)}\colon E^{(t)} \to \mathbb R\)
。保留图中的自环(超像素包含的节点互指,合并后表现为超像素指向自身。这里保留自环是因为不一定每一次都需要加入新像素,自环权重高于其它节点时则不加),通过相对大小对自环边进行加权作为正则化器,对区域大小的方差进行约束。对于非自环边,使用平均特征
\(\mu_\xi^{(t)}(v) = \sum_{i \in \pi^{(t)}_v} \xi(i) / \lvert \pi^{(t)}_v \rvert\)
并应用相似性函数
\(\mathrm{sim}\colon E^{(t)} \to \mathbb{R}\)
作为权重。自环的权重使用在层级
\(t\)
时,区域大小的特征均值
\(\mu^{(t)}_{\lvert \pi \rvert}\)
和特征标准差
\(\sigma^{(t)}_{\lvert \pi \rvert}\)
进行加权。

整体权重计算如下:

\[\begin{align}
w_\xi(u, v) = \begin{cases}
\mathrm{sim}\Big(\mu_\xi^{(t)}(u), \mu_\xi^{(t)}(v)\Big), & \text{for $u \neq v$;} \\
\Big(\lvert \pi^{(t)}_u \rvert - \mu_{\lvert \pi \rvert}^{(t)}\Big) / \sigma_{\lvert \pi \rvert}^{(t)}, & \text{otherwise.}
\end{cases}
\end{align}
\]

紧凑性可以通过计算无穷范数密度来选择性地进行调节:

\[\begin{equation}
\delta_\infty(u, v) = \frac{4 (\lvert \pi_u \rvert^{(t)} + \lvert \pi_v \rvert^{(t)})}{\mathrm{per}_\infty(u,v)^2},
\end{equation}
\]

其中
\(\mathrm{per}_\infty\)
是包围超像素
\(u\)

\(v\)
的边界框的周长。这突出了两个相邻的超像素
\(u\)

\(v\)
在其边界框内的紧密程度,从而得出了一个正则化的权重函数。

\[\begin{equation}
w_\xi^{(t)}(u,v;\lambda) = \lambda \delta_\infty(u,v) + (1 - \lambda)w_\xi^{(t)}(u, v)
\end{equation}
\]

其中
\(\lambda \in [0,1]\)
作为紧凑性的超参数。

  • Update rule

使用贪婪的并行更新规则进行边收缩,使得每个超像素与具有最高边权重的相邻超像素连接,包括所有
\(G^{(t)}\)
中的自环,适用于
\(t \geq 1\)
。设
\(\mathfrak{N}^{(t)}(v)\)
表示在第
\(t\)
层中索引为
\(v\)
的超像素的相邻顶点的邻域,构造一个中间边集:

\[\begin{align}
\hat E^{(t)} = \bigg(v, \underset{u \in \mathfrak{N}^{(t)}(v)}{\text{arg\ max}}\ w_\xi(u, v; \lambda) : v \in V^{(t)}\bigg).
\end{align}
\]

然后,传递闭包
\(\hat E_+^{(t)}\)
(传递闭包是指多个二元关系存在传递性,通过该传递性推导出更多的关系,比如可从A->B和B->C中推导出A->C,这里即是
\(\hat E^{(t)}\)
的连通分量)可明确地得出一个映射
\({V^{(t)} \mapsto V^{(t+1)}}\)
,使得

\[\begin{align}
\pi^{(t+1)}_v = \bigcup_{u \in \hat{\mathfrak{N}}_+^{(t)}(v)} \pi^{(t)}_u,
\end{align}
\]

其中
\(\hat{\mathfrak{N}}_+^{(t)}(v)\)
表示在
\(\hat E_+^{(t)}\)
中顶点
\(v\)
的连通分量。这个分区更新规则确保了在
\((t+1)\)
层的每个分区都是一个连通区域,因为它是通过合并具有最高边权重的相邻超像素形成的,如图
3
中所示。

  • Iterative refinement

重复计算聚合映射、正则化边权重和边收缩的步骤,直到达到所需的层级数
\(T\)
。在每一层,分区变得更加粗糙,表示图像中更大的同质区域。层级结构提供了图像的多尺度表示,捕捉了局部和全局结构。在第
\(T\)
层,即可获得一系列分区
\((\pi^{(t)})_{t=0}^T\)
,其中每一层的分区在层级
\(t\)
时是一个连通区域,并且对所有
\(t\)

\({\pi^{(t)} \sqsubseteq \pi^{(t+1)}}\)

在经典的
ViT
分词器中,论文尝试验证不同的
\(T\)
和图像块大小
\(\rho\)
分别产生的标记数量之间的关系。设
\(N_\mathrm{SPiT}\)

\(N_\mathrm{ViT}\)
分别表示
SPiT
分词器和
ViT
分词器的标记数量,这种关系为
\(\mathbb{E}(T \mid N_\mathrm{SPiT} = N_\mathrm{ViT}) = \log_2 \rho\)
,无论图像大小如何。

Feature Extraction with Irregular Patches

虽然
ViT
架构中选择正方形图像块是出于简洁性的考虑,但这自然也反映了替代方案所带来的挑战。非规则的图像块是不对齐的,表现出不同的形状和维度,并且通常是非凸的(形状非常不规则)。这些因素使得将非规则图像块嵌入到一个共同的内积空间中变得不容易。除了保持一致性和统一的维度外,论文还提出任何此类特征需要捕捉的最小属性集;即颜色、纹理、形状、尺度和位置。

  • Positional Encoding

ViTs
通常为图像网格中的每个图像块使用可学习的位置嵌入。论文注意到这对应于下采样图像的位置直方图,可以通过使用核化方法将可学习的位置嵌入扩展到处理更复杂的形状、尺度和位置,对每个
\(n=1,\dots,N\)
分区的超像素
\(S_n\)
的坐标应用联合直方图。首先,将位置归一化,使得所有
\((y', x') \in S_n\)
都落在
\([-1, 1]^2\)
范围内。设定固定
\(\beta\)
为每个空间方向上的特征维度,特征由高斯核
\(K_\sigma\)
提取:

\[\begin{equation}
\hat\xi^{\text{pos}}_{n,y,x} = \mathrm{vec}\Bigg(\sum_{(y_j, x_j) \in S_n} K_\sigma (y - y_j, x - x_j) \Bigg),
\end{equation}
\]

通常,带宽
\(\sigma\)
取值较低,范围为
\([0.01, 0.05]\)
。这样,实际上就编码了图像块在图像中的位置,以及其形状和尺度。

  • Color Features

为了将原始像素数据中的光强信息编码到特征中,使用双线性插值将每个图像块的边界框插值到固定分辨率
\(\beta {\mkern1mu\times\mkern1mu} \beta\)
,同时屏蔽其他周围图像块中的像素信息。这些特征本质上捕捉了原始图像块的原始像素信息,但经过重采样并缩放到统一的维度。将特征提取器
\(\phi\)
称为插值特征提取器,
RGB
特征也被归一化到
\([-1, 1]\)
并向量化,使得
\(\hat\xi^{\text{col}} \in \mathbb{R}^{3\beta^2}\)

  • Texture Features

梯度算子提供了一种简单而稳健的纹理信息提取方法。基于改进的旋转对称性和离散化误差,论文选择使用
Scharr
提出的梯度算子。将该算子归一化,使得
\(\nabla \xi \in [-1, 1]^{H{\mkern1mu\times\mkern1mu} W{\mkern1mu\times\mkern1mu} 2}\)
,其中最后两个维度对应于梯度方向
\(\nabla y\)

\(\nabla x\)
。与位置特征的处理过程类似,在每个超像素
\(S_n\)
内部对梯度应用高斯核构建联合直方图,使得
\(\hat\xi^{\text{grad}}_n \in \mathbb{R}^{\beta^2}\)

最终特征模态被拼接为
\(\hat\xi_n = [\hat\xi^{\text{col}}_n, \hat\xi^{\text{pos}}_n, \hat\xi^{\text{grad}}_n] \in \mathbb{R}^{5\beta^2}\)
。虽然论文提出的梯度特征与标准的
ViT
架构相同,但它们代表了额外的信息维度。因此,论文评估了包括或省略梯度特征的效果。对于那些省略这些特征的模型,即
\(\hat\xi_n \setminus \hat\xi^{\text{grad}}_n = [\hat\xi^{\text{col}}_n, \hat\xi^{\text{pos}}_n] \in \mathbb{R}^{4\beta^2}\)
,称该提取器
\(\phi\)
为不包括梯度的提取器。

Generalization of Canonical ViT

在设计上,论文的框架是对标准
ViT
标记化的一个概括,等同于使用固定图像块大小
\(\rho\)
和排除梯度的插值特征提取的标准图像块嵌入器。


\(\tau^*\)
表示一个固定图像块大小
\(\rho\)
的标准
ViT
标记化器,
\(\phi\)
表示一个排除梯度的插值特征提取器,
\(\gamma^*\)

\(\gamma\)
表示具有等效线性投影的嵌入层,其中
\(L^*_\theta = L_\theta\)
。设
\(\hat\xi^{\text{pos}} \in \mathbb{R}^{N {\mkern1mu\times\mkern1mu} \beta^2}\)
表示在
\(\tau^*\)
分割下的联合直方图位置嵌入矩阵。那么,对于维度
\(H = W = \beta^2 = \rho^2\)
,由
\(\gamma \circ \phi \circ \tau^*\)
给出的嵌入与由
\(\gamma^* \circ \phi^* \circ \tau^*\)
给出的标准
ViT
嵌入在数量上是等效的。

Experiments and Results




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

经过前面的Redis基础学习,今天正式进入编码阶段了,进入编码阶段我们又同样面临一道多选题,选择什么客户端库?要是有选择困难症的又要头疼了。不过别担心我先头疼,今天就给大家介绍6款.NET系Redis客户端库: ServiceStack.Redis、StackExchange.Redis、CSRedisCore、FreeRedis、NewLife.Redis、BeetleX.Redis。

01
、ServiceStack.Redis

ServiceStack.Redis算的上最老牌、最有名的一款Redis C#/.NET客户端库了,但是因为商业性导致对于大多数人来说不是首选。

ServiceStack.Redis是一款功能丰富、操作简单、高性能的C#/.NET客户端库,对原生的功能和特性提供很好的支持,同时又做了更高级的功能抽象,使得对简单对象或复杂类型序列化操作更容易。当然也同时提供了同步和异步API。

下面我们写个简单的使用小例子:

public static void Run()
{
    Console.WriteLine($"ServiceStack.Redis 使用示例");
    //创建连接池
    var pool = new RedisManagerPool("127.0.0.1:6379");
    //获取一个redis实例
    using var redis = pool.GetClient();
    //设置键值对
    var setResult = redis.Set("key1", "value1");
    Console.WriteLine($"设置键值对key1/value1操作结果:{setResult}");
    //获取键对应的值
    var value = redis.Get<string>("key1");
    Console.WriteLine($"获取键key1对应的值为:{value}");
    // 删除键
    var delResult = redis.Remove("key1");
    Console.WriteLine($"删除键key1操作结果:{delResult}");
    //检查键是否存在
    var exists = redis.ContainsKey("key1");
    Console.WriteLine($"键key1是否存在: {exists}");
}

执行结果如下:

02
、StackExchange.Redis

StackExchange.Redis是一款基于.NET的、高性能的、免费的、功能全面的、通用的老牌Redis客户端。并且支持Redis多节点,Redis集群,IO多路复用,同步/异步双编程模型等技术,这也使得其与Redis交互同时兼具灵活性与高效性,大大提升了Redis读写的性能与并发。

同时它还提供了丰富的高级功能,包括但不限于管道,连接池,事务,Lua脚本、订阅/发布等。序列化与压缩也提供了多种方式供以选择,很方便与.NET应用程序集成。

下面我们写个简单的使用小例子:

public static void Run()
{
    Console.WriteLine($"StackExchange.Redis 使用示例");
    // 创建 ConnectionMultiplexer 实例
    using var connection = ConnectionMultiplexer.Connect("127.0.0.1:6379");
    //获取 Redis 数据库实例
    var redis = connection.GetDatabase();
    //设置键值对
    var setResult = redis.StringSet("key1", "value1");
    Console.WriteLine($"设置键值对key1/value1操作结果:{setResult}");
    //获取键对应的值
    var value = redis.StringGet("key1");
    Console.WriteLine($"获取键key1对应的值为:{value}");
    // 删除键
    var delResult = redis.KeyDelete("key1");
    Console.WriteLine($"删除键key1操作结果:{delResult}");
    //检查键是否存在
    var exists = redis.KeyExists("key1");
    Console.WriteLine($"键key1是否存在: {exists}");
}

执行结果如下:

03
、CSRedisCore

CSRedisCore是一款国人基于开源项目csredis上实现的著名Redis C#/.NET客户端库。它做到了所有方法名和redis-cli方法名保持一致。它支持Redis 集群、Redis 哨兵和Redis主从分离,以及geo类型、流类型命令,同时支持同步/异步接口。

下面我们写个简单的使用小例子:

public static void Run()
{
    Console.WriteLine($"CSRedisRedis 使用示例");
    // 创建 CSRedisClient 实例
    var redis = new CSRedisClient("127.0.0.1:6379");
    //设置键值对
    var setResult = redis.Set("key1", "value1");
    Console.WriteLine($"设置键值对key1/value1操作结果:{setResult}");
    //获取键对应的值
    var value = redis.Get("key1");
    Console.WriteLine($"获取键key1对应的值为:{value}");
    // 删除键
    var delResult = redis.Del("key1");
    Console.WriteLine($"删除键key1操作结果:{delResult}");
    //检查键是否存在
    var exists = redis.Exists("key1");
    Console.WriteLine($"键key1是否存在: {exists}");
}

执行结果如下:

04
、FreeRedis

FreeRedis是CSRedisCore作者的另一个大作。至少从逻辑上来说也应该比CSRedisCore更优秀,事实也是如此,FreeRedis在内存使用、存储效率都做了优化,在持久化、容错方面也做了改进,同时还提供了更多的高级功能以及自定义选项。我们直接看官方介绍。

单从介绍上来说CSRedisCore有的功能它有,CSRedisCore没有的功能它也有。总的来说功能更强大了。另外CSRedisCore目前处于维护阶段已经不新增功能了。因此更推荐FreeRedis。

下面我们写个简单的使用小例子:

public static void Run()
{
    Console.WriteLine($"FreeRedis 使用示例");
    // 创建 CSRedisClient 实例
    var redis = new RedisClient("127.0.0.1:6379");
    //设置键值对
    redis.Set("key1", "value1");
    Console.WriteLine($"设置键值对key1/value1操作成功");
    //获取键对应的值
    var value = redis.Get("key1");
    Console.WriteLine($"获取键key1对应的值为:{value}");
    // 删除键
    var delResult = redis.Del("key1");
    Console.WriteLine($"删除键key1操作结果:{delResult}");
    //检查键是否存在
    var exists = redis.Exists("key1");
    Console.WriteLine($"键key1是否存在: {exists}");
}

执行结果如下:

05
、NewLife.Redis

NewLife.Redis具有低延时,高性能,高吞吐量以及稳定性、可靠性良好,因此在大量实时数据计算的应用场景有很好的发挥。它为针对大数据和消息队列做了优化,使得其可以用支撑日均百亿级的调用量,而它的连接池可以做到100000个连接并发。在包含网络通讯的前提下可以把get/set操作做到平均耗时200~600微秒。其二进制序列化方式也更有助于提升数据存储和读取效率。

下面我们写个简单的使用小例子:

public static void Run()
{
    Console.WriteLine($"NewLife.Redis 使用示例");
    // 创建 CSRedisClient 实例
    var redis =  new FullRedis("127.0.0.1:6379", "", 0);
    //设置键值对
    var setResult = redis.Set("key1", "value1");
    Console.WriteLine($"设置键值对key1/value1操作结果:{setResult}");
    //获取键对应的值
    var value = redis.Get<string>("key1");
    Console.WriteLine($"获取键key1对应的值为:{value}");
    // 删除键
    var delResult = redis.Remove("key1");
    Console.WriteLine($"删除键key1操作结果:{delResult}");
    //检查键是否存在
    var exists = redis.ContainsKey("key1");
    Console.WriteLine($"键key1是否存在: {exists}");
}

执行结果如下:

06
、BeetleX.Redis。

BeetleX.Redis是一款高可用、高性能、异步非阻塞设计的.net core客户端库。并且基本全面覆盖redis-cli指令,提供了多种序列化方式,使用简单轻松。

下面我们写个简单的使用小例子:

public static async Task RunAsync()
{
    Console.WriteLine($"BeetleX.Redis 使用示例");
    // 创建 CSRedisClient 实例
    RedisDB redis = new RedisDB(0)
    {
        DataFormater = new JsonFormater()
    };
    //添加写主机
    redis.Host.AddWriteHost("127.0.0.1", 6379);
    //添加读主机
    redis.Host.AddReadHost("127.0.0.1", 6379);
    //设置键值对
    var setResult = await redis.Set("key1", "value1");
    Console.WriteLine($"设置键值对key1/value1操作结果:{setResult}");
    //获取键对应的值
    var value = await redis.Get<string>("key1");
    Console.WriteLine($"获取键key1对应的值为:{value}");
    // 删除键
    var delResult = await redis.Del("key1");
    Console.WriteLine($"删除键key1操作结果:{delResult}");
    //检查键是否存在
    var exists = await redis.Exists("key1");
    Console.WriteLine($"键key1是否存在: {exists}");
}

执行结果如下:

07
、总结

  • ServiceStack.Redis:综合功能全面,适合需要商业支持的用户。

  • StackExchange.Redis:官方推荐,功能全面,社区支持良好,文档丰富。

  • CSRedisCore:功能齐全,简单易用,适合快速开发。

  • FreeRedis:高性能,功能齐全,简单易用,适合快速开发。

  • NewLife.Redis:高性能,高并发,低延迟,分布式场景适合使用。

  • BeetleX.Redis。:高可用,高性能,异步操作,适合高负载场景。


:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。
https://gitee.com/hugogoos/Planner

1 关于云原生

云原生计算基金会(Cloud Native Computing Foundation, CNCF)的官方描述是:
云原生是一类技术的统称,通过云原生技术,我们可以构建出更易于弹性扩展、极具分布式优势的应用程序。
这些应用可以被运行在不同的环境当中,比如说 私有云、公有云、混合云、还有多云场景。
云原生包含了 容器、微服务(涵盖服务网格)、Serverless、DevOps,API管理、不可变基础架构等能力。通过云原生技术构建出来的应用程序,对
底层基础架构的耦合很低,易于迁移,可以充分地利用云所提供的能力,因此云原生应用的研发、部署、管理相对于传统的应用程序更加高效和便捷。

image

1.1 微服务

微服务是一种架构模式,是面向服务的体系结构(SOA)软件架构模式的一种演变,
它提倡将单一应用程序划分成一组松散耦合的细粒度小型服务,辅助轻量级的协议,互相协调、互相配合,为用户提供最终价值。
具有 单一职责、轻量级通信、独立性、进程隔离、混合技术栈和混合部署方式、简化治理 等特点。

1.2 DevOps

DevOps 作为一种工程模式,本质上是通过对开发、运维、测试,配管等角色职责的分工,实现工程效率最大化,进而满足业务的需求。

1.3 持续交付

在不影响用户使用服务的前提下频繁把新功能发布给用户使用,要做到这点比较难。需要强大的流量管理能力,动态的服务扩缩容为平滑发布、ABTesting提供保障。

1.4 容器化

容器化的好处在于运维的时候不需要再关心每个服务所使用的技术栈了,每个服务都被无差别地封装在容器里,可以被无差别地管理和维护,现在比较流行的技术是docker和k8s。

2 关于ServiceMesh

2.1 什么是ServiceMesh

ServiceMesh 是最新一代的微服务架构,作为一个基础设施层,能够与业务解耦,主要解决复杂网络拓扑下微服务与微服务之间的通信,其实现形态一般为轻量级网络代理,并与应用SideCar部署,同时对业务应用透明。
image
如果从一个单独链路调用可以得到以下的结构图:
image
如果我们从一个全局视角来看,绿色的为应用服务,蓝色的为SideCar,就会得到如下部署图:
image

2.2 相较传统微服务的区别

以SpringCloud与Dubbo为代表的微服务开发框架非常受欢迎。但我们发现,他有优秀的服务治理能力,也有明显的痛点:
1. 侵入性强。
想要集成SDK的能力,除了需要添加相关依赖,业务层中入侵的代码、注解、配置,与治理层界限不清晰。可以想想Dubbo、SpringCloud 等的做法
2. 升级成本高。
每次升级都需要业务应用修改SDK版本,重新进行功能回归测试,并对每一台服务进行部署上线,与快速迭代开发相悖。
3. 版本碎片化严重。
由于升级成本高,而中间件版本更新快,导致线上不同服务引用的SDK版本不统一、能力参差不齐,造成很难统一治理。
4. 中间件演变困难。
由于版本碎片化严重,导致中间件向前演进的过程中就需要在代码中兼容各种各样的老版本逻辑,带着"枷锁”前行,无法实现快速迭代。
5. 内容多、门槛高。
依赖组件多,学习成本高。
6. 治理功能不全。
不同于RPC框架,SpringCloud作为治理全家桶的典型,也不是万能的,诸如协议转换支持、多重授权机制、动态请求路由、故障注入、灰度发布等高级功能并没有覆盖到。

2.3 ServiceMesh的价值 — 赋能基础架构

  1. 统一解决多语言框架问题,降低开发成本
  2. 降低测试成本,提升质量
  3. 控制逻辑集中到控制面
  4. 为新架构演进提供支持,如Serverless
  5. 网格半覆盖 转 统一覆盖(弥补service-center并逐渐过度)
  6. 完整的闭环微服务统筹和管理能力

image

2.4 ServiceMesh的价值 — 赋能业务

  • 框架与业务解耦,减少业务限制。
  • 简化服务所依赖SDK版本管理。
  • 依托热升级能力,版本召回周期短。
  • SDK瘦身,减少业务依赖冲突。
  • 丰富的流量治理、安全策略、分布式Trace、日志监控,下沉服务治理底座,让业务专注业务。

image

3 ServiceMesh 核心能力

3.1 流量治理

微服务应用最大的痛点就是处理服务间的通信,而这一问题的核心其实就是流量管理。

3.1.1 请求路由

将请求路由到服务的版本,应用根据 HTTP 请求 header 的值、Uri的值 路由流量到不同的地方。匹配规则可以是流量端口、header字段、URI等内容。
RuleMatch参考
image

3.1.2 流量转移

当微服务的一个版本逐步迁移到另一个版本时,我们可以将流量从旧版本迁移到新版本。如下图,使用weight参数进行权重分配,
这个很典型的应用场景就是灰度发布或者ABTesting。
image

3.1.3 负载均衡

同3.1.2的图,Service B 有多个实例,所以可以另外制定负载均衡策略。
负载均衡策略支持简单的负载策略(ROUND_ROBIN、LEAST_CONN、RANDOM、PASSTHROUGH)、一致性 Hash 策略和区域性负载均衡策略。

3.1.4 超时

对上游的请求设置,设置一个一定时长(0.5s)的超时,请求超过这个时间不响应,可以直接fallback。目标还是过载保护。
image

3.1.5 重试

当请求在固定的时间内没有返回正确值的时候,可以配置重试次数。设置如果服务在 1 秒内没有返回正确的返回值,就进行重试,重试的条件为返回码为5xx,重试 3 次。
分布式环境下,重试是高可用的重要技术,重试方案慎用。

retries:
      attempts: 3
      perTryTimeout: 1s
      retryOn: 5xx

3.1.6 熔断/限流/降级

熔断的策略比较多,可以配置 最大连接数、连接超时时间、最大请求数、请求重试次数、请求超时时间等,我们都可以给他熔断掉,fallback回去。
但是目前看,Istio 对更灵活、更细粒度的限流、降级等能力支持的还不够好,合理应该有漏斗池算法(如
阿里开源限流框架Sentinel
)或者令牌桶算法(如
Google Guava 提供的限流工具类 RateLimiter
)这样的灵活做法。
但是可以采用其他方式处理,比如可以通过流量转发将部分流量流动到默认服务去,该服务启用默认的fallback,但是需要控制好采样时间、熔断半开的策略。

3.1.7 离群检测(Outlier Detection)

当集群中的服务故障的时候,其实我们最优先的做法是先进行离群,然后再检查问题,处理问题并恢复故障。所以,能否快速的离群对系统的可用性很重要。
Outlier Detection 允许你对上游的服务进行扫描,然后根据你设置的参数来判断是否对服务进行离群。
下面的配置表示每秒钟扫描一次上游主机,连续失败 2 次返回 5xx 错误码的所有主机会被移出负载均衡连接池 3 分钟,上游被离群的主机在集群中占比不应该超过10%。
但无论比例多少,只要你集群下的服务实例>=2个,都将弹出至少1个主机。它有很详细的配置,
参考这边

注意:3分钟之后回群,如果再被离群,则为上次离群时间+本次离群时间,即 3+3;默认超过50%(可调整比例)被离群,进入恐慌模式。

outlierDetection:
      consecutiveErrors: 2
      interval: 1s
      baseEjectionTime: 3m
      maxEjectionPercent: 10

3.1.8 故障注入

就是用来模拟上游服务对请求返回指定异常码时,当前的服务是否具备处理能力。系统上线前,可以配置注入的httpStatus和比例,来验证下游服务对故障的处理能力。
image

3.1.9 流量镜像(Mirroring)

这个也叫做影子流量。是指通过一定的配置将线上的真实流量复制一份到镜像服务中去,可以设置流量比例,只转发不响应。
个人觉得这个还是比较有用的,好处是 完整的线上正式环境模拟、流量分析、压力测试;全真的线上问题再现,方便问题排查。
image

3.2 可观察性

3.2.1 监控与可视化

Prometheus(标配,默认抓取指标数据)、kiali监控(服务视图,Istion链路的可观察性) 、Grafana(BI报表)(数据面、控制面、xDS Service 各健康指标)
后续章节会逐一展开...

3.2.2 访问日志

ELK、EFK (Envoy记录AccessLog,包含SideCard的InBound、OutBound记录)
后续章节会详细展开...

3.2.3 分布式追踪

本质上查找多个HTTP请求之间的相关性的一种方法是使用相关性ID。该ID应该传递给所有请求,以便跟踪平台知道哪些请求属于同一请求。如下图:
image
尽管Istio利用Envoy的分布式跟踪功能提供开箱即用的跟踪集成,但是其实这是一个误解,我们的应用程序需要做一些工作。应用程序需要传播以下header:

  • x-request-id
  • x-b3-traceid
  • x-b3-spanid
  • x-b3-parentspanid
  • x-b3-sampled
  • x-b3-flags
  • x-ot-span-context

image
Istio Sidecar内的Envoy代理接收这些标头,并将它们传递给配置的tracing系统。所以实际上,Istio中服务追踪默认只会追踪到2级,
例如A -> B -> C, 在Istio中会出现2条追踪链路:A -> B 和B -> C,而不会出现我们期望的A -> B -> C的形式,如果想要服务串联起来,需要对服务间调用进行改造,
在Istio中应用程序通过传播http header来将span关联到同一个trace。

image

3.3 安全机制

  • Service Mesh可以在服务间通信中引入双向TLS加密,确保数据在传输过程中不被篡改和窃听。控制平面负责管理和分发证书,Sidecar Proxy在通信过程中进行加密和解密操作。
  • 通过引入身份认证和访问控制策略,可以细粒度地控制哪些服务可以访问其他服务。

3.4 策略执行

Service Mesh通过在每个服务实例旁边部署Sidecar Proxy,实现了对服务间通信的透明代理。这些代理负责拦截出入的所有流量,并根据控制平面下发的配置和策略执行相应的操作。具体工作原理如下:

3.4.1 服务发现:

当一个服务实例启动时,它会向服务注册中心注册自己的信息。控制平面负责管理这些服务实例信息,并将更新的服务列表分发给所有Sidecar Proxy。

3.4.2 流量管理:

当一个服务需要与另一个服务通信时,流量首先经过本地的Sidecar Proxy。代理根据配置的路由规则和负载均衡策略,将流量转发到目标服务实例。
控制平面可以动态更新这些路由规则,实现蓝绿部署、金丝雀发布等高级流量管理功能。

3.4.3 安全认证:

Service Mesh可以在服务间通信中引入双向TLS加密,确保数据在传输过程中不被篡改和窃听。控制平面负责管理和分发证书,Sidecar Proxy在通信过程中进行加密和解密操作。
通过引入身份认证和访问控制策略,可以细粒度地控制哪些服务可以访问其他服务。

3.4.4 可观察性:

Service Mesh中的代理会收集每个请求的日志、监控数据和追踪信息,并将这些数据发送到可观察性组件进行处理和存储。
运维人员可以通过控制平面提供的接口和仪表盘,实时监控服务间的流量情况、延迟、错误率等指标,并进行故障排查和性能优化。

4 总结

Service Mesh相比传统微服务框架以下几方面有明显优势:

  • 解耦应用程序和通信逻辑
  • 提供增强的服务治理能力
  • 提高可观察性和可调试性
  • 支持多语言和协议以
  • 提高系统可靠性和可扩展性