wenmo8 发布的文章

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相比传统微服务框架以下几方面有明显优势:

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

数据库容灾等级


在信息化时代,企业的数据安全和业务连续性变得至关重要,容灾备份作为确保数据不丢失和业务不中断的重要措施备受关注。

我们通常将容灾备份分为四个等级,从最基本的本地备份到复杂的异地多活系统,每个等级的特点和适用场景各不相同。

下面我们就来详细了解一下这四个等级的容灾备份方案。

容灾备份容灾等级

1、第0级:没有备份数据中心
这一级容灾备份,实际上没有灾难恢复能力,它只在本地进行数据备份,并且备份的数据只在本地保存,没有送往异地。
描述:一般新业务、即将下架的业务、边缘业务会使用这个等级,所以数据库选型上要求也不高,一般单机MySQL,SQL Server等都能支撑。


2、第1级:本地磁带备份,异地保存
在本地将关键数据备份,然后送到异地保存。当灾难发送后,按预定数据恢复程序进行恢复。这种方案成本低、易于配置。但当数据量增大时,存在存储介质难以管理的问题;
并且当灾难发生时,存在大量数据难以及时恢复的问题。为了解决这些问题,当灾难发生时,可先恢复关键数据,后恢复非关键数据。
描述:当新业务到一定规模,会考虑使用这个等级,使用更高性价比的存储介质存储数据库备份,例如磁带库,并且备份保留相当长的时间,需要人工或者通过网络定期把存储介质存放异地保存。



3、第2级:热备份站点备份
在异地建立一个热备份站点,通过网络进行数据备份。也就是通过网络以同步或异步方式,把主站点的数据备份到备份站点。备份站点一般只备份数据,不承担业务。
当出现灾难时,备份站点接替主站点业务,从而维护业务运行的连续性。
描述:这个等级一般是业务进入稳定期,需要考虑把容灾等级提升一个档次,这时候数据库选型上,一般需要具备跨机房数据同步能力,例如,SQL Server的AlwaysOn、MySQL的MGR、PostgreSQL的流复制等能满足;
如果是用公有云,那么RDS数据库的多可用区就能满足要求。



4、第3级:活动备份中心
在相隔较远的地方分别建立两个数据中心,它们都处于工作状态,并相互进行数据备份。当某个数据中心发生灾难时,另一个数据中心接替其工作任务。
这种级别的备份根据实际要求和投入资金的多少可以分为两种:
(1)两个数据中心之间只限于关键数据的相互备份
(2)两个数据中心之间互为镜像,即零数据丢失
零数据丢失是目前要求最高的一种容灾备份方案,它要求不管发生什么灾难,系统都能保证数据安全。所以它需要配置复杂的管理软件和专用硬件设备,相对而言投资是最高的,但恢复速度是最快的。
描述:一般金融业务等对数据安全要求比较高的需要达到这个等级,也就是我们常说的,异地双活、异地多活,数据库选型上一般需要具备分布式多节点数据同步能力,例如,某Base,某SQL等能满足。

不同的容灾备份对应的灾难恢复能力

建设成本和灾难恢复目标时间对比


总结

本文介绍了数据库容灾备份的四个等级,从本地备份(第0级)到复杂的异地双活系统(第3级),每个等级适用于不同的业务场景。
文章重点分析了各级备份方案的特点和应用,帮助企业根据RTO、RPO等需求选择合适的方案,以确保数据安全和业务连续性。

其实从第0级到第3级,本质上都是为了满足更高要求的RTO和RPO,满足更苛刻的SLA,所以在数据库选型和方案选择上都要结合实际,选出最适合的方案。

参考文章
https://news.west.cn/39450.html
https://e.huawei.com/cn/industries/commercial-market/active-active-data-center-solution
https://stor.zol.com.cn/374/3741281.html
https://blog.csdn.net/hjx020/article/details/106588133/

本文版权归作者所有,未经作者同意不得转载。

在现代 Web 开发中,Web 组件已经成为创建模块化、可复用 UI 组件的标准工具。而
Shadow DOM
是 Web 组件技术的核心部分,它允许开发人员封装组件的内部结构和样式,避免组件的样式和行为影响全局页面。然而,传统的 Shadow DOM 实现方式需要通过 JavaScript 显式地创建和附加 Shadow DOM,这增加了开发复杂性。

为了简化 Web 组件开发,
声明式 Shadow DOM(Declarative Shadow DOM)
提供了一种新的方法,允许开发人员直接通过 HTML 定义 Shadow DOM,而无需过多依赖 JavaScript。这一特性特别适用于服务端渲染(SSR)和静态页面生成(SSG)场景,大大提高了页面的加载效率和开发体验。

本文将详细介绍声明式 Shadow DOM 的基础语法、与 Javascript 的结合使用以及其主要应用场景和优势。


一、什么是 Shadow DOM?

Shadow DOM 是 Web 组件的一个重要组成部分,它通过创建封装的 DOM 树,让组件的内部 DOM 和样式与外部页面隔离。这使得组件可以拥有独立的样式和功能,而不会与页面的其他部分发生冲突。

传统上,开发人员需要通过 JavaScript 调用
attachShadow()
方法来手动创建 Shadow DOM,并附加到自定义元素上。这样的方式增加了代码的复杂性,同时在服务端渲染和静态页面生成中也难以直接使用。

二、声明式 Shadow DOM 的基本语法

声明式 Shadow DOM 允许开发人员直接在 HTML 模板中定义 Shadow DOM,而无需通过 JavaScript 来创建。这种方式依赖于 HTML 中的
<template>
标签,并通过
shadowroot
属性来指定 DOM 应作为 Shadow DOM 存在。

示例代码:

<my-element>
  <template shadowrootmode="open">
    <style>
      p {
        color: blue;
      }
    </style>
    <p>这是声明式 Shadow DOM 内的内容!</p>
  </template>
</my-element>

在这个例子中,
<template>
标签用于定义组件的内部结构和样式,而
shadowrootmode="open"
表示这是一个开放的 Shadow DOM,可以从外部访问。

相比传统的创建方式,这种声明式的语法更加简洁,也更利于服务器端预渲染。

三、声明式 Shadow DOM 与 Javascript 结合

虽然声明式 Shadow DOM 允许在 HTML 中直接声明组件结构,但自定义元素的行为和逻辑仍然需要通过 Javascript 来定义。例如,如果需要为组件添加交互行为,我们仍然需要编写 JavaScript 代码来注册自定义元素。

示例:声明式 Shadow DOM + Javascript 实现计数按钮

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <title>声明式 Shadow DOM 示例</title>
</head>

<body>
    <!-- 定义组件的模板 -->
    <count-button>
        <template shadowrootmode="open">
            <style>
                button {
                    font-size: 16px;
                    padding: 10px 20px;
                }
            </style>
            <button id="increment-btn">点击次数:<span id="count">0</span></button>
        </template>
    </count-button>
    <script>
        // 定义自定义元素类
        class CountButton extends HTMLElement {
            constructor() {
                super();

                // 获取按钮和计数显示元素
                this.button = this.shadowRoot.querySelector('#increment-btn');
                this.countDisplay = this.shadowRoot.querySelector('#count');
                this.count = 0; // 初始化计数

                // 绑定事件处理程序
                this.button.addEventListener('click', () => {
                    this.increment();
                });
            }

            // 定义一个方法来增加计数
            increment() {
                this.count++;
                this.countDisplay.textContent = this.count;
            }
        }

        // 注册自定义元素
        customElements.define('count-button', CountButton);
    </script>

</body>

</html>

预览

jcode

代码解释:

  1. HTML 部分


    • 使用
      <template>
      标签定义了计数按钮组件的结构和样式,并通过
      shadowrootmode="open"
      声明为开放的 Shadow DOM。
    • 组件的样式和内容在 HTML 中声明,减少了 Javascript 中的 DOM 操作。
  2. Javascript 部分


    • 使用 Javascript 定义了一个自定义元素
      CountButton
    • 添加了按钮点击事件,每次点击按钮时,计数器加一并更新显示。
  3. 自定义元素注册


    • 使用
      customElements.define
      方法注册了自定义元素
      <count-button>

四、声明式 Shadow DOM 的应用场景

1. 服务端渲染(SSR)

声明式 Shadow DOM 对服务端渲染非常友好。由于组件结构和样式已经声明在 HTML 中,服务端可以预先生成完整的组件,并将其直接发送给客户端。这不仅减少了页面的初始加载时间,还提高了搜索引擎的抓取能力,有利于 SEO。

2. 静态页面生成(SSG)

在静态页面生成中,声明式 Shadow DOM 允许开发人员将预定义的组件结构嵌入到静态 HTML 文件中,从而提升页面的加载速度,减少客户端的 Javascript 计算量。

五、声明式 Shadow DOM 的优势与限制

优势:

  • 简化开发流程
    :通过 HTML 直接声明 Shadow DOM,减少了对 Javascript 的依赖,降低了开发难度。
  • 性能提升
    :在 SSR 和 SSG 场景下,预渲染的组件可以直接发送给客户端,减少了首次渲染的时间。
  • SEO 友好
    :组件内容可以直接包含在 HTML 中,便于搜索引擎抓取。

限制:

  • Javascript 仍不可或缺
    :虽然组件的结构和样式可以声明式定义,但组件的交互和逻辑仍需通过 JavaScript 实现。
  • 浏览器兼容性
    :目前声明式 Shadow DOM 已基本支持所有的浏览器,但是所需的浏览器的版本较新,需要开发者考虑兼容性问题。


六、总结

声明式 Shadow DOM
是 Web 组件开发的一项强大新功能,它通过简化 Shadow DOM 的创建过程,减少了 Javascript 的依赖,特别适用于服务端渲染和静态页面生成场景。虽然其优势明显,但在实际开发中,开发者仍需结合 Javascript 来实现组件的交互和逻辑。

随着浏览器对这一新特性的支持逐步增加,声明式 Shadow DOM 将会成为 Web 组件开发中的主流方式之一。对于需要高性能、模块化的 Web 开发项目,声明式 Shadow DOM 是一个值得尝试的新工具。

参考资料: