2025-06-01

vSAN OSA 存储架构示意图vSAN OSA 存储架构示意图


VMware vSAN OSA存储策略:虚拟机分布式对象存储的深度解析

一、引言

VMware vSAN(Virtual SAN)是 VMware 提供的超融合软件定义存储 (SDS) 解决方案,将本地服务器(ESXi 主机)的直连存储(SSD+HDD) 聚合成一个分布式存储池。vSAN 支持两种存储架构:传统的 OSA(Original Storage Architecture)和全新的 ESA(Express Storage Architecture)。本篇重点讲解 OSA 模式下的存储策略(Storage Policy)原理、对象分布机理,以及如何使用 PowerCLI 和 vSphere REST API 配置与验证策略。


二、vSAN OSA 存储架构概述

2.1 OSA 架构要点

  1. 磁盘组 (Disk Group)

    • 每个 ESXi 主机可配置一个或多个磁盘组。
    • 每个磁盘组包含一个或多个缓存盘(Cache Tier,通常为 NVMe/SSD)和若干容量盘(Capacity Tier,HDD 或 SSD)。
    • 缓存盘分为读写缓存:前 70% 用于写缓存(写缓冲),后 30% 用于读取缓存(读加速)。
  2. 对象与组件 (Object & Component)

    • vSAN 将虚拟机的 VMDK、快照等对象切分为更小的“组件”(Component)。
    • 每个组件会根据存储策略在多个主机磁盘组之间以镜像 (RAID-1) 或条带 (RAID-0/5/6) 方式分布。
    • 对象最小组件大小为 1MB。
  3. 见证 (Witness)

    • 在 FTT(Failures To Tolerate,可容忍故障数)> 0 情况下,vSAN 会在“见证”主机上存储一个只包含元数据的小型组件(Witness)。
    • Witness 用于仲裁故障期间数据可用性。
  4. 策略关键属性

    • FTT(故障容忍数):决定对象需要几个副本。
    • Stripe Width(条带宽度):决定对象条带数,即将对象切分为多少组件并分布到不同磁盘组。
    • Object Space Reservation (OSR,对象空间保留率):决定预留的容量百分比(例如 100% 保留表示 Full-thick)。
    • Caching / Checksum / Flash Read Cache Reservation / IOPS Limit 等:影响性能和保护机制。

2.2 OSA 与 ESA 的差异

  • OSA:基于 ESXi 传统的存储架构,依赖 VMkernel 存储栈,将磁盘组中的缓存盘和容量盘通过 VMFS-like 逻辑聚合。组件以普通文件方式存储在本地磁盘。
  • ESA:引入 Linux 用户态 vSAN 代理、更高 IO 处理性能、更灵活的 SSD+NVMe 支持以及更优的去重/压缩性能。本文暂不展开 ESA,重点关注广泛应用的 OSA 架构。

三、vSAN 存储策略详细分析

3.1 FTT (Failures To Tolerate)

  • FTT 指定可容忍多少台主机或磁盘组故障。

    • FTT=0:无容错,所有组件仅存一份;
    • FTT=1:可容忍 1 个故障,需要两份数据副本(镜像)+见证;
    • FTT=2:可容忍 2 个故障,需要三份副本+见证;
    • 以此类推。
  • 影响:FTT 越高,占用磁盘容量越多,但数据可靠性越强。

3.2 Stripe Width(条带宽度)

  • 决定对象被拆分成多少个组件,分别分布在不同磁盘组中。
  • 例如:Stripe Width=2,FTT=1,则 vSAN 将对象拆成 2 个数据组件,分别放在不同主机的磁盘组,以实现并行读写。再加上 1 个 Witness(只存元数据),共 3 个组件。
  • 注意:Stripe Width 最多不能超过 (主机数 * 每主机磁盘组数) 减去 FTT。配置过高会导致无法部署对象。

3.3 Object Space Reservation (OSR)

  • 定义为 Full-Thick、Thin 或者某个百分比保留。

    • 100% OSR = Full-Thick:立即为整个对象分配所有容量,在容量盘上形成连续空间。
    • <100% OSR = Thin:仅为写入的数据分配存储空间,节省容量但会产生碎片、写扩散。
  • 影响:Full-Thick 提供最优性能,Thin 野置空间更节省。

3.4 Flash Read Cache Reservation & IOPS Limit

  • 可以为特定存储策略指定读缓存保留容量(Cache Reservation),保证某些关键虚拟机能使用足够 SSD 缓存。
  • IOPS Limit 用于限制单个对象的最大 IOPS,以防止热点干扰集群。

3.5 Checksum & Force Provisioning

  • Checksum:开启后组件写入时会计算 CRC,以检测数据损坏。
  • Force Provisioning:在集群资源不足时(例如可容忍分布式 RAID 需求不足)仍强制创建对象,但可能降低保护级别,需谨慎使用。

四、vSAN OSA 对象分布机理图解

(请参考上方“图1:vSAN OSA 存储架构示意图”)

图1 展示了典型3节点 OSA 集群中的磁盘组布局与组件分布:

  • 每台主机拥有一个磁盘组,包含 1 个 SSD 缓存与 2 个 HDD 组成容量层。
  • 对象 A(红色节点)为 FTT=1、StripeWidth=1 的 VM 磁盘:两个数据副本分别放在 Host1 和 Host2 的 HDD 上;见证组件 W 放在 Host3 上的 SSD 上。
  • 对象 B(蓝色节点)为 FTT=1、StripeWidth=2 的 VM 磁盘:拆成两个数据组件,分别分布在 Host2、Host3;见证组件放置在 Host1 上。这样读写可以并行访问两组组件。

通过上述图示,可以直观理解 OSA 模式下 vSAN 如何在不同主机之间分散对象组件,实现性能与容错的平衡。


五、PowerCLI / REST API 代码示例

以下示例将演示如何在 OSA 集群上创建并应用自定义存储策略。

5.1 PowerCLI 示例:创建 vSAN 存储策略

# 连接 vCenter
Connect-VIServer -Server vcsa.example.com -User administrator@vsphere.local -Password 'YourPassword!'

# 创建一个新的存储策略名为 "OSA-Policy"
$policyName = "OSA-Policy"
$profile = New-SpbmProfile -Name $policyName -Description "vSAN OSA 自定义策略"

# 添加规则:FTT = 1
Add-SpbmRule -SPBMProfile $profile -RuleId "hostFailuresToTolerate" -Value 1

# 添加规则:Stripe Width = 2
Add-SpbmRule -SPBMProfile $profile -RuleId "proportionalCapacity" -Value 2

# 添加规则:OSR=100% (Full Thick)
Add-SpbmRule -SPBMProfile $profile -RuleId "objectSpaceReservation" -Value 100

# 添加规则:开启数据校验 (Checksum = true)
Add-SpbmRule -SPBMProfile $profile -RuleId "checksumEnabled" -Value $true

# 添加规则:Flash Read Cache Reservation 10%
Add-SpbmRule -SPBMProfile $profile -RuleId "cacheReservation" -Value 10

# 添加规则:IOPS 限制 10000
Add-SpbmRule -SPBMProfile $profile -RuleId "iopsLimit" -Value 10000

Write-Host "已创建并配置存储策略:$policyName"

# 查看规则
Get-SpbmRule -SPBMProfile $profile | Format-Table

解释

  1. 通过 New-SpbmProfile 创建一个空白策略,然后使用 Add-SpbmRule 添加每个关键属性。
  2. hostFailuresToTolerate 对应 FTT;proportionalCapacity 对应 Strike Width;objectSpaceReservation 对应 OSR;checksumEnabled 开启校验;cacheReservation 指定读缓存保留;iopsLimit 限制 IOPS。

完成后,可将此策略应用到虚拟机磁盘(VMDK)或虚拟机级别。

5.2 PowerCLI 示例:将存储策略应用到虚拟机磁盘

# 假设已有虚拟机名为 "WebVM",获取其硬盘信息
$vm = Get-VM -Name "WebVM"
$hardDisk = Get-HardDisk -VM $vm

# 应用存储策略到第一个硬盘
Set-SpbmEntityConfiguration -Entity $hardDisk -StoragePolicy $policyName

Write-Host "已将存储策略 $policyName 应用到 WebVM 的硬盘。"

5.3 vSphere REST API 示例:创建与应用存储策略

下面以 curl 调用为例,假设 vCenter 已获取到访问 Token VC_TOKEN

5.3.1 获取所有现有规则 ID
curl -k -u "${VC_USER}:${VC_PASS}" -X GET "https://vcsa.example.com/rest/appliance/storage/policy/property" \
     -H "vmware-api-session-id: ${VC_TOKEN}"

输出示例(简化):

{
  "value": [
    { "id": "hostFailuresToTolerate", "display_name": "FTT" },
    { "id": "proportionalCapacity", "display_name": "Stripe Width" },
    { "id": "objectSpaceReservation", "display_name": "OSR" },
    { "id": "checksumEnabled", "display_name": "Checksum" },
    { "id": "cacheReservation", "display_name": "Flash Read Cache Reservation" },
    { "id": "iopsLimit", "display_name": "IOPS Limit" }
  ]
}
5.3.2 创建自定义存储策略
curl -k -u "${VC_USER}:${VC_PASS}" -X POST "https://vcsa.example.com/rest/appliance/storage/policy" \
     -H "vmware-api-session-id: ${VC_TOKEN}" \
     -H "Content-Type: application/json" \
     -d '{
           "create_spec": {
             "name": "OSA-Policy-API",
             "description": "通过 API 创建的 OSA 存储策略",
             "rules": [
               {
                 "id": "hostFailuresToTolerate",
                 "properties": { "hostFailuresToTolerate": 1 }
               },
               {
                 "id": "proportionalCapacity",
                 "properties": { "proportionalCapacity": 2 }
               },
               {
                 "id": "objectSpaceReservation",
                 "properties": { "objectSpaceReservation": 100 }
               },
               {
                 "id": "checksumEnabled",
                 "properties": { "checksumEnabled": true }
               },
               {
                 "id": "cacheReservation",
                 "properties": { "cacheReservation": 10 }
               },
               {
                 "id": "iopsLimit",
                 "properties": { "iopsLimit": 10000 }
               }
             ]
           }
         }'
返回示例:
{
  "value": {
    "policy_id": "policy-12345",
    "name": "OSA-Policy-API",
    "description": "通过 API 创建的 OSA 存储策略"
  }
}
5.3.3 将策略应用到虚拟机硬盘
# 获取虚拟机 ID
VM_ID=$(curl -k -u "${VC_USER}:${VC_PASS}" -X GET "https://vcsa.example.com/rest/vcenter/vm?filter.names=WebVM" \
          -H "vmware-api-session-id: ${VC_TOKEN}" | jq -r '.value[0].vm')

# 获取硬盘设备 ID(假设只一个硬盘)
DISK_ID=$(curl -k -u "${VC_USER}:${VC_PASS}" -X GET "https://vcsa.example.com/rest/vcenter/vm/${VM_ID}/hardware/disk" \
            -H "vmware-api-session-id: ${VC_TOKEN}" | jq -r '.value[0].disk')

# 应用策略
curl -k -u "${VC_USER}:${VC_PASS}" -X POST "https://vcsa.example.com/rest/vcenter/vm/${VM_ID}/hardware/disk/${DISK_ID}/storage/policy" \
     -H "vmware-api-session-id: ${VC_TOKEN}" \
     -H "Content-Type: application/json" \
     -d '{
           "policy": "policy-12345"
         }'

说明

  1. 先通过 API 获取各规则 ID;
  2. 然后通过 POST /rest/appliance/storage/policy 创建自定义策略,返回 policy_id
  3. 最后查出虚拟机和硬盘 ID,将策略通过 POST /rest/vcenter/vm/.../hardware/disk/.../storage/policy 应用。

六、实战注意事项与最佳实践

  1. 跨故障域部署

    • 在机架或机房级别设置故障域 (Fault Domain),确保副本分布在不同物理区域。
    • 配合 FTT=1 或更高,保证单机柜断电也能继续提供服务。
  2. 磁盘组配置

    • 建议每个磁盘组使用至少 1 个高速 NVMe/SSD 作为缓存盘与 1-2 块容量盘;
    • 对于 I/O 密集型工作负载,可选用全 SSD 磁盘组。
  3. 策略验证(SPBM 策略健康检查)

    • 在 vSphere Client → vSAN → 监控 → 策略健康中,可看到各对象是否满足策略。
    • 定期检查对象重建 (Resync) 状态,防止因节点故障导致数据重分发过慢。
  4. 容量与性能监控

    • 利用 vRealize Operations Manager (vROps) 对 vSAN 性能进行监控,包括延迟、吞吐、缓存命中率等。
    • 注意 IOPS Limit 设置,避免对关键 VM 预留不够的缓存引发性能瓶颈。
  5. 升级与兼容

    • 升级 ESXi/vSAN 版本时,注意 OSA 架构在高版本中可能会被 ESA 功能限制。
    • 升级 vCenter 及 ESXi 时,先在非生产环境进行验证,确保策略正常迁移与应用。

七、常见问题解答

  1. Q1:为什么 FTT=1 下还需要 Witness?

    • Witness 组件只存储元数据,不占用大容量的空间。其作用在于当一个数据副本所在主机宕机时,通过仲裁见证组件决定哪个副本为活动副本,保证 quorum。
  2. Q2:Stripe Width 设置为 1 与 2 的区别?

    • Stripe Width=1:对象只有一个数据组件和一个 Witness(FTT=1)。仅利用单个磁盘组写入,性能偏低但资源消耗最少。
    • Stripe Width=2:对象拆为 2 个数据组件,可并行写入两组磁盘组,提高性能;代价是占用更多磁盘组资源,并且需要更多磁盘组满足策略。
  3. Q3:为什么在 OSA 中不建议使用 RAID-5/6(Erasure Coding)?

    • 在 vSAN 6.6 前版本,Erasure Coding 仅支持 ESA 架构;OSA 只支持镜像 (RAID-1)。Erasure Coding 带来更高空间利用率,但在 OSA 中性能开销较高且不灵活。
  4. Q4:如何排查对象无法满足策略?

    • 在 vSphere Client → vSAN → 对象浏览器 中,查看 “组件不满足策略” 警报,定位哪些对象因哪些原因失败(磁盘组空间不足、主机离线、故障域不足等)。

八、总结

本文全面介绍了 VMware vSAN OSA 存储策略的关键属性(FTT、Stripe Width、OSR、缓存保留、IOPS 限制、校验等),并通过“图1”直观演示了 OSA 模式下对象组件的分布机理。同时给出了 PowerCLI 与 vSphere REST API 代码示例,演示如何创建、配置并验证策略。

2025-06-01

分布式系统中的Quorum NWR算法:一致性协议的关键

Quorum示意图Quorum示意图

一、引言

在分布式系统中,实现数据的一致性是一个核心挑战。节点可能出现故障、网络延迟或分区(Partition),如何保证客户端读写操作能够在多数节点之间保持一致性?Quorum(仲裁)机制是一种经典的解决方案。本文将重点介绍Quorum 的N-W-R(节点数 N、写仲裁大小 W、读仲裁大小 R)算法原理,并通过代码示例与图解帮助理解。


二、Quorum 基础

2.1 什么是 Quorum?

Quorum 指的是在一组副本(Replica)中,为了保证读写操作的正确性,必须与一定数量的副本进行交互才能完成。这三个参数通常记作 (N, W, R),定义如下:

  • N:数据的副本总数(节点总数)。
  • W:执行写操作时,需要写入并确认成功的副本数(写仲裁大小)。
  • R:执行读操作时,需要读取并确认返回的副本数(读仲裁大小)。

为了保证强一致性,通常要求:

W + R > N

W > N / 2

或者

R > N / 2

其中,第一个约束保证每次读操作至少会“看到”最新的写;第二个约束保证写操作会覆盖大多数节点,避免数据丢失。

2.2 NWR 的工作原理

  • 写操作:客户端将数据写入集群时,需要等待至少 W 个节点写入成功后,才向客户端返回写成功。这样即使部分节点宕机,只要剩余的 W 节点具备最新数据,后续读操作仍能读取到最新值。
  • 读操作:客户端发起读请求时,需要从至少 R 个节点读取数据,并选择最新的那个版本返回给客户端。由于 W + R > N,读操作与任意一次写操作在副本集上至少有一个交集节点能够保证读取到最新数据。

三、NWR 算法原理与保证

3.1 一致性保证

如前所述,当满足以下条件时:

  1. W + R > N:任何一次读操作所依赖的 R 个节点,至少与上一次写操作所依赖的 W 个节点有一个节点重叠。假设上次写操作在节点集合 SW(|SW| = W)中完成,而本次读操作从节点集合 SR(|SR| = R)读取,则:
    $|S_W ∩ S_R| \ge W + R - N \ge 1$
    因此,读操作至少会从一个已经写入最新数据的节点读取到最新值。
  2. W > N / 2:如果写操作写入了超过半数的节点,则任何新的写操作都无法与之“错过”——新的写操作还必须写入超过半数节点,至少有一个节点持有旧值,保证数据最终不丢失。

综合来看,NWR 算法保证了在网络分区、节点失败等情况下,依然能够提供强一致性读写语义。

3.2 延迟与可用性权衡

  • 较大的 W:写操作需要确认更多节点才能返回成功,写延迟增加;但读操作可设置 R 较小,读延迟较低。
  • 较大的 R:读操作需要等待更多节点返回结果,读延迟增加;但写操作可以设置 W 较小,写延迟较低。
  • W 与 R 的平衡:一般在读多写少的场景中,会选择 R=1(或较小),W=N/2+1;而在写多读少的场景中,则反之。这样可以优化典型工作负载下的性能。

四、示例场景与代码示例

4.1 示例场景:N=5,W=3,R=3

  • 节点总数 N=5(N1, N2, N3, N4, N5)
  • 写仲裁 W=3:写操作需要在 3 个节点上写成功
  • 读仲裁 R=3:读操作需要从 3 个节点读出结果并取最新版本

如“图1(上方生成的示意图)”所示,红色节点表示写仲裁所选节点(例如 N1,N2,N3),蓝色表示读仲裁所选节点(例如 N3,N4,N5),紫色(N3)为它们的交集,保证读操作可读到最新写数据。

4.2 代码示例:Python 风格伪代码

下面以简化的 Python 伪代码示例,演示如何在客户端与节点之间实现 NWR Quorum 读写。假设我们有 5 个节点,每个节点简单存储键值对,并维护本地版本号 timestamp。

import threading
import time
import random

# 模拟节点
class ReplicaNode:
    def __init__(self, node_id):
        self.node_id = node_id
        self.data_store = {}         # 键 -> (value, timestamp)
        self.lock = threading.Lock()

    def write(self, key, value, timestamp):
        """
        处理写请求:如果 timestamp 大于本地记录,则更新;否则丢弃。
        """
        with self.lock:
            local = self.data_store.get(key)
            if local is None or timestamp > local[1]:
                self.data_store[key] = (value, timestamp)
                return True
            else:
                # 本地版本更新,忽略旧写
                return False

    def read(self, key):
        """
        处理读请求:返回 (value, timestamp),如果不存在则返回 (None, 0)。
        """
        with self.lock:
            return self.data_store.get(key, (None, 0))


# 客户端实现 Quorum 读写
class QuorumClient:
    def __init__(self, nodes, W, R):
        self.nodes = nodes        # ReplicaNode 实例列表
        self.W = W                # 写仲裁大小
        self.R = R                # 读仲裁大小

    def write(self, key, value):
        """
        Quorum 写实现:为每次写生成一个 timestamp(例如当前时间戳)
        """
        timestamp = int(time.time() * 1000)  # 毫秒级时间戳
        ack_count = 0
        responses = []
        
        # 并行发送写请求
        def send_write(node):
            nonlocal ack_count
            ok = node.write(key, value, timestamp)
            if ok:
                ack_count += 1
        
        threads = []
        for node in self.nodes:
            t = threading.Thread(target=send_write, args=(node,))
            t.start()
            threads.append(t)
        
        # 等待所有请求返回或超过超时时间(简化:阻塞等待)
        for t in threads:
            t.join()
        
        # 判断是否满足写仲裁 W
        if ack_count >= self.W:
            print(f"[Write Success] key={key}, value={value}, timestamp={timestamp}, acks={ack_count}")
            return True
        else:
            print(f"[Write Fail] key={key}, value={value}, timestamp={timestamp}, acks={ack_count}")
            return False

    def read(self, key):
        """
        Quorum 读实现:从各节点读取 (value, timestamp),取最高 timestamp 的结果。
        """
        responses = []
        def send_read(node):
            val, ts = node.read(key)
            responses.append((val, ts, node.node_id))

        threads = []
        for node in self.nodes:
            t = threading.Thread(target=send_read, args=(node,))
            t.start()
            threads.append(t)
        for t in threads:
            t.join()

        # 按 timestamp 倒序排序,取前 R 个
        responses.sort(key=lambda x: x[1], reverse=True)
        top_responses = responses[:self.R]
        # 从这 R 个中再选出最大 timestamp 的值(原则上这一步可以省略,因为已排序)
        freshest = top_responses[0]
        val, ts, nid = freshest
        print(f"[Read] key={key}, returning value={val}, timestamp={ts} from node {nid}")
        return val

# ---- 测试示例 ----
if __name__ == "__main__":
    # 启动 5 个节点
    nodes = [ReplicaNode(f"N{i}") for i in range(1, 6)]
    client = QuorumClient(nodes, W=3, R=3)

    # 写入 key="x", value="foo"
    client.write("x", "foo")
    # 随机模拟节点延迟或失败(此处省略)
    
    # 读出 key="x"
    result = client.read("x")
    print("最终读取结果:", result)

解释

  1. 每次写操作先生成一个基于时间戳的 timestamp,并并行发往所有节点;
  2. 当写操作在至少 W=3 个节点上成功,才向客户端返回写入成功;
  3. 读操作并行向所有节点请求数据,收集所有 (value, timestamp),并选出 timestamp 最大的 R=3 条,再从这 3 条中选出最新值返回;
  4. 由于 W + R = 3 + 3 = 6 > N = 5,保证每次读操作至少能够看到最新的写。

五、图解(“图1”)

上方已展示的“图1:Quorum示意图”简要说明了 5 个副本节点中,写仲裁(红色:N1,N2,N3)和读仲裁(蓝色:N3,N4,N5)的关系,其中紫色节点 N3 为两者的交集。由此保证:任何“写”至少写入 N3,任何“读”也必定读取 N3,从而读操作一定读取到最新数据。


六、详细说明

6.1 为什么需要 W + R > N

  • 假设第 1 次写依赖节点集合 A(|A| = W),第 2 次读依赖节点集合 B(|B| = R)。若 A ∩ B = ∅,则读操作可能无法看到第 1 次写的结果,导致读-写不一致。由集合交集原理:
    $|A ∩ B| = |A| + |B| - |A ∪ B| \ge W + R - N$
    W + R > N 时,W + R - N ≥ 1,即两集合至少有 1 个公共节点。

6.2 写延迟与读延迟

  • 写延迟依赖于 W 个节点的写响应速度;
  • 读延迟依赖于 R 个节点的读响应速度;
  • 在实际部署时可根据读写比例进行权衡。例如:如果读操作远多于写操作,可以选择 R=1(只需从一个节点读取),W=N/2+1 保证强一致性;反之亦然。

6.3 可能出现的”幻读“问题

  • 在 NWR 模型下,若客户端连续两次读操作且中间无写操作,可能出现节点之间数据版本不同导致”幻读“。通过引入版本(timestamp)排序,读 R 次得到一批候选结果后,总能选出最新版本,防止读到旧数据。若业务需要严格线性一致性,还需在客户端(或协调层)追踪最新 timestamp 并带到下一次读操作中,确保”读-修改-写“流程的正确性。

七、代码示例扩展:加入节点故障模拟

下面示例在上文基础上,增加对节点随机延迟或不可用的模拟,以更贴近真实分布式环境:

import threading
import time
import random

class ReplicaNode:
    def __init__(self, node_id, fail_rate=0.1, delay_range=(0.01, 0.1)):
        self.node_id = node_id
        self.data_store = {}
        self.lock = threading.Lock()
        self.fail_rate = fail_rate
        self.delay_range = delay_range

    def write(self, key, value, timestamp):
        # 模拟延迟
        time.sleep(random.uniform(*self.delay_range))
        # 模拟失败
        if random.random() < self.fail_rate:
            return False
        with self.lock:
            local = self.data_store.get(key)
            if local is None or timestamp > local[1]:
                self.data_store[key] = (value, timestamp)
                return True
            return False

    def read(self, key):
        time.sleep(random.uniform(*self.delay_range))
        if random.random() < self.fail_rate:
            return (None, 0)  # 模拟读失败
        with self.lock:
            return self.data_store.get(key, (None, 0))


class QuorumClient:
    def __init__(self, nodes, W, R, timeout=1.0):
        self.nodes = nodes
        self.W = W
        self.R = R
        self.timeout = timeout  # 超时控制

    def write(self, key, value):
        timestamp = int(time.time() * 1000)
        ack_count = 0
        acks_lock = threading.Lock()

        def send_write(node):
            nonlocal ack_count
            success = node.write(key, value, timestamp)
            if success:
                with acks_lock:
                    ack_count += 1

        threads = []
        for node in self.nodes:
            t = threading.Thread(target=send_write, args=(node,))
            t.daemon = True
            t.start()
            threads.append(t)

        start = time.time()
        while time.time() - start < self.timeout:
            with acks_lock:
                if ack_count >= self.W:
                    break
            time.sleep(0.01)

        if ack_count >= self.W:
            print(f"[Write Success] key={key}, ts={timestamp}, acks={ack_count}")
            return True
        else:
            print(f"[Write Fail] key={key}, ts={timestamp}, acks={ack_count}")
            return False

    def read(self, key):
        responses = []
        resp_lock = threading.Lock()

        def send_read(node):
            val, ts = node.read(key)
            # 仅统计非故障读
            if ts > 0:
                with resp_lock:
                    responses.append((val, ts, node.node_id))

        threads = []
        for node in self.nodes:
            t = threading.Thread(target=send_read, args=(node,))
            t.daemon = True
            t.start()
            threads.append(t)

        start = time.time()
        while time.time() - start < self.timeout:
            with resp_lock:
                if len(responses) >= self.R:
                    break
            time.sleep(0.01)

        with resp_lock:
            # 选出 timestamp 最大的 R 条
            responses.sort(key=lambda x: x[1], reverse=True)
            top = responses[:self.R]
        if not top:
            print("[Read Fail] 没有足够节点响应")
            return None

        freshest = top[0]
        val, ts, nid = freshest
        print(f"[Read] key={key}, value={val}, ts={ts}, from node={nid}")
        return val


if __name__ == "__main__":
    # 启动 5 个节点,随机失败率 20%
    nodes = [ReplicaNode(f"N{i}", fail_rate=0.2) for i in range(1, 6)]
    client = QuorumClient(nodes, W=3, R=3, timeout=0.5)

    # 写入和读
    client.write("x", "bar")
    result = client.read("x")
    print("最终读取结果:", result)

要点说明

  1. 每个节点模拟随机延迟(delay\_range)和随机失败(fail\_rate),更贴近真实网络环境;
  2. 客户端在写和读操作中加入超时控制 timeout,防止因部分节点长期不响应导致阻塞;
  3. Quorum 条件不变:写至少等待 W 个成功,读至少收集 R 个有效响应并取最大 timestamp。

八、总结

  1. Quorum NWR 算法通过设定节点总数 N、写仲裁 W、读仲裁 R,满足 W + R > N,确保任意读操作都能读取到最新写入的数据,从而实现强一致性。
  2. 性能权衡:W 与 R 的选择将直接影响读写延迟与系统可用性,应根据应用场景(读多写少或写多读少)进行调整。
  3. 容错性:即使部分节点宕机,Quorum 算法只要保证可用节点数 ≥ W(写)或 ≥ R(读),仍能完成操作;若可用节点不足,则会告警或失败。
  4. 图解示意:图1 展示了五个节点中写仲裁与读仲裁的交集,直观说明了为何能保证读取到最新数据。
  5. 实际系统应用:如 Cassandra、DynamoDB、Riak 等分布式存储系统都采用类似 Quorum 设计(或其变种)以实现可扩展、高可用且一致的读写。

2024-09-09

在Spring Cloud学习系列中,我们已经介绍了Elasticsearch的基本概念和安装配置。在这一部分中,我们将使用Spring Data Elasticsearch来实现分布式搜索功能。

首先,我们需要在Spring Boot项目中添加Spring Data Elasticsearch的依赖。




<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>
    <!-- 其他依赖 -->
</dependencies>

接下来,我们需要配置Elasticsearch的客户端。




@Configuration
public class ElasticsearchConfig {
 
    @Value("${elasticsearch.host}")
    private String hostname;
 
    @Value("${elasticsearch.port}")
    private int port;
 
    @Bean
    public RestHighLevelClient elasticsearchClient() {
        RestClientBuilder builder = RestClient.builder(new HttpHost(hostname, port));
        return new RestHighLevelClient(builder);
    }
}

然后,我们可以创建一个Elasticsearch的Repository接口。




public interface ProductRepository extends ElasticsearchRepository<Product, String> {
}

最后,我们可以使用这个Repository来执行搜索操作。




@Service
public class ProductSearchService {
 
    @Autowired
    private ProductRepository productRepository;
 
    public List<Product> searchByName(String name) {
        return productRepository.findByName(name);
    }
}

在这个例子中,我们定义了一个简单的搜索方法searchByName,它使用了Elasticsearch的Repository提供的查询方法findByName。这个方法会在Elasticsearch中搜索所有名称字段包含指定关键字的文档。

这只是一个基本的例子,实际应用中可能需要更复杂的查询逻辑,例如基于多个字段的搜索、分页、排序等。Spring Data Elasticsearch提供了丰富的查询方法定义,可以通过定义相应的接口来实现复杂的搜索需求。

2024-09-09

这是一个关于如何使用Scrapy-Redis来管理分布式爬虫的文章,它解释了源代码中的相关组件,包括队列管理和去重策略。




# 假设我们有一个Scrapy项目,我们想要使用Scrapy-Redis来管理分布式爬虫。
 
# 首先,我们需要安装Scrapy-Redis:
pip install scrapy-redis
 
# 然后,在Scrapy的settings.py文件中,我们需要配置调度器(scheduler)和队列类(queue class):
 
# settings.py
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER_PERSIST = True
 
# 如果你想使用PriorityQueue来保证高优先级的请求先被爬取,可以这样设置:
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'
# 或者使用FifoQueue按照先进先出顺序排列请求:
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.FifoQueue'
# 或者使用LifoQueue按照后进先出顺序排列请求:
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.LifoQueue'
 
# 最后,指定使用Redis作为数据库和调度器:
ITEM_PIPELINES = {
    'scrapy_redis.pipelines.RedisPipeline': 300,
}
 
# 这里使用了RedisPipeline来存储项目,而不是默认的Scrapy的PicklePipeline。
 
REDIS_HOST = 'localhost'
REDIS_PORT = 6379
 
# 如果你的Redis服务器需要密码,可以设置REDIS_PASSWORD。
# REDIS_PASSWORD = 'your_password'
 
# 现在,当你运行你的Scrapy爬虫时,Scrapy-Redis会使用Redis作为调度和去重的后端,
# 使得爬虫可以跨多个节点分布式地运行。

这段代码展示了如何配置Scrapy项目以使用Scrapy-Redis提供的调度器和去重功能,并且如何设置优先级队列和Last-In-First-Out队列。最后,它指定了使用Redis作为数据库和调度器,以便爬虫可以跨多个节点分布式运行。

2024-09-09



@Configuration
public class RedisConfig {
 
    @Value("${spring.redis.host}")
    private String host;
 
    @Value("${spring.redis.port}")
    private int port;
 
    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port));
    }
 
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory());
        return template;
    }
}

这段代码定义了一个配置类RedisConfig,它使用Lettuce客户端连接到Redis服务器。它创建了一个LettuceConnectionFactory实例,并配置了主机和端口。同时,它还定义了一个RedisTemplate的Bean,这是一个用于操作Redis的Spring数据模板,它使用提供的连接工厂。这个模板可以用于存储、检索和操作Redis数据。

2024-09-09

Redis 提供了 SETNX 命令,可以实现分布式锁。SETNX 是 "SET if Not eXists" 的缩写,也就是只有键不存在时,才会设置值。

以下是使用 SETNX 命令实现 Redis 分布式锁的伪代码:




def acquire_lock(lock_name, acquire_timeout=5, lock_timeout=10):
    end_time = time.time() + acquire_timeout
    lock_name = 'lock:' + lock_name
    while time.time() < end_time:
        if redis_client.setnx(lock_name, 'locked'):
            redis_client.expire(lock_name, lock_timeout)
            return True
        time.sleep(0.001)
    return False
 
def release_lock(lock_name):
    lock_name = 'lock:' + lock_name
    redis_client.delete(lock_name)

在这个例子中,acquire_lock 尝试获取锁,如果在指定时间内未能获得锁,则返回 False。release_lock 释放锁,使得其他等待的进程可以获取锁。

注意,这个实现没有考虑可能的 Redis 故障或网络分区问题,在这种情况下,锁可能不会被释放。因此,在生产环境中,通常会结合使用 Redlock 算法来更安全地实现分布式锁。

2024-09-09

在分布式系统中,缓存是常用的提高性能的手段之一。Redis作为一种内存数据结构存储系统,可以用来作为分布式缓存。

一、Redis集群的数据分片机制

Redis Cluster采用虚拟槽(hash slot)的机制,所有的键根据[0-16383]个整数映射,用哈希函数HASH\_SLOT=CRC16(key) mod 16384来实现。每个节点负责维护一部分槽以及映射的键值数据。

二、Redis集群的数据迁移

Redis Cluster的数据迁移通过Redis Cluster rebalance工具进行。

三、Redis集群的可用性

Redis Cluster可用性保证取决于集群中master的数量,至少需要一个。如果一个master失效,它的slave会接管。

四、Redis集群的一致性保证

Redis Cluster通过使用Paxos协议实现数据的最终一致性。

五、Redis集群的可靠性

Redis Cluster的可靠性依赖于大多数节点可用,至少需要有一半以上的节点可用,才能对外提供服务。

六、Redis集群的主从切换

当主节点失效时,Redis Cluster会从从节点中选举一个新的主节点。

七、Redis集群的动态扩容

可以动态添加新的节点到集群,并且可以重新分配槽位。

八、Redis集群的动态缩容

可以动态移除节点,并重新分配槽位。

以上是分布式缓存的一些基本概念和处理方式,在实际应用中,还需要考虑数据的一致性、可靠性、可用性等问题,并根据实际需求进行调整和优化。

2024-09-09

Spring Cloud是一系列框架的有序集合,它提供了一些简单的编程模型,使得开发者可以快速地构建出一套分布式的系统。

以下是一个使用Spring Cloud构建微服务的简单例子:




// 引入Eureka客户端依赖
@EnableEurekaClient
@SpringBootApplication
public class MyServiceApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(MyServiceApplication.class, args);
    }
 
    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder.build();
    }
 
}
 
@RestController
public class MyController {
 
    @Autowired
    private RestTemplate restTemplate;
 
    @GetMapping("/call-service")
    public String callService() {
        return restTemplate.getForObject("http://other-service/get-data", String.class);
    }
 
}

在这个例子中,我们创建了一个简单的Spring Boot应用,并通过@EnableEurekaClient注解标明这是一个Eureka客户端,意味着它会在Eureka服务注册中心进行服务注册。RestTemplate被用来调用其他服务。

Spring Cloud为开发者提供了服务发现(Eureka),配置管理(Spring Cloud Config),路由(Zuul),负载均衡(Ribbon),断路器(Hystrix)等一系列的工具,使得开发者可以更加专注于业务逻辑的开发,而不用过多关注底层的服务发现和服务管理问题。

2024-09-09



import redis
import uuid
 
class RedisDistributedLock:
    def __init__(self, redis_conn):
        self.redis_conn = redis_conn
        self.lock_key = "distributed_lock"
 
    def acquire_lock(self, acquire_timeout=10, lock_timeout=10):
        identifier = str(uuid.uuid4())
        end = time.time() + acquire_timeout
 
        while time.time() < end:
            if self.redis_conn.set(self.lock_key, identifier, ex=lock_timeout, nx=True):
                return identifier
            time.sleep(0.001)
 
        return False
 
    def release_lock(self, identifier):
        while True:
            with self.redis_conn.pipeline() as pipe:
                try:
                    pipe.watch(self.lock_key)
                    if pipe.get(self.lock_key) == identifier:
                        pipe.multi()
                        pipe.delete(self.lock_key)
                        pipe.execute()
                        return True
                    pipe.unwatch()
                    break
                except redis.exceptions.WatchError:
                    pass
 
        return False
 
# 使用示例
redis_conn = redis.StrictRedis(host='localhost', port=6379, db=0)
lock = RedisDistributedLock(redis_conn)
identifier = lock.acquire_lock()
if identifier:
    try:
        # 这里执行你的业务逻辑
        pass
    finally:
        lock.release_lock(identifier)
else:
    # 未能获得锁,执行备选方案或抛出异常
    pass

这段代码实现了一个基于Redis的分布式锁,用于解决秒杀等高并发场景下的同步问题。它使用了Redis的SET命令的NX(只在键不存在时设置)和PX(设置键的过期时间)选项来尝试获取锁,并通过一个唯一标识符来确保只有获得锁的客户端能够释放它。代码提供了获取和释放锁的方法,并且在获取锁之后,提供了使用try-finally语句来确保即使发生异常也能释放锁。

2024-09-09

在分析这三种分布式锁实现之前,我们需要明确一点,分布式锁主要用于在分布式系统中控制同一资源被多个节点同时操作。

  1. 使用Redis的setnx命令实现分布式锁

Redis的setnx命令可以设置一个键,当且仅当这个键不存在的时候。我们可以利用这个特性来实现分布式锁。




public boolean lock(String key, String uniqueId, int expireTime) {
    long result = jedis.setnx(key, uniqueId);
    if (result == 1) {
        jedis.expire(key, expireTime);
        return true;
    }
    return false;
}
 
public void unlock(String key, String uniqueId) {
    if (uniqueId.equals(jedis.get(key))) {
        jedis.del(key);
    }
}
  1. 使用Java的synchronized关键字实现线程安全

在Java中,我们可以使用synchronized关键字来实现线程安全。




public synchronized void criticalSection() {
    // 需要线程安全保护的代码
}
  1. 使用RedLock算法实现分布式锁

RedLock算法是Redlock-redis实现分布式锁的一种协议。它是一种更为复杂的分布式锁解决方案,通过使用N个独立的Redis节点来实现锁。




public boolean lock(List<Jedis> nodes, String resourceId, long ttl) {
    int quorum = nodes.size() / 2 + 1;
    long time = System.currentTimeMillis();
    long validityTime = time + ttl + 1;
    String lockKey = "lock:" + resourceId;
    String lockValue = String.valueOf(validityTime);
 
    int lockAcquired = 0;
 
    for (Jedis node : nodes) {
        if (node.setnx(lockKey, lockValue) == 1) {
            node.pexpire(lockKey, ttl);
            lockAcquired++;
        } else {
            long lockValidity = Long.parseLong(node.get(lockKey));
            if (lockValidity < time) {
                String oldLockValue = node.getSet(lockKey, lockValue);
                if (oldLockValue != null && Long.parseLong(oldLockValue) < time) {
                    node.pexpire(lockKey, ttl);
                    lockAcquired++;
                }
            }
        }
    }
 
    return lockAcquired >= quorum;
}
 
public void unlock(List<Jedis> nodes, String resourceId) {
    String lockKey = "lock:" + resourceId;
    for (Jedis node : nodes) {
        node.del(lockKey);
    }
}

以上三种方法各有优缺点,具体使用哪种方法需要根据实际情况来决定。例如,如果你的应用是单机应用,那么使用synchronized就足够了。如果你的应用是分布式应用,那么可能需要使用Redlock或者Redis的setnx命令来实现分布式锁。而Redis的setnx命令实现的分布式锁简单易用,但是如果Redis服务器宕机,那么所有的节点都将无法获得锁。而Redlock算法虽然复杂,但是能够提供更好的可靠性和安全性。