想象一家旋转寿司餐厅,传送带围成一个大环,客人围坐在环边。厨房从入口放上寿司盘,盘子沿传送带顺时针缓缓移动,停在第一个遇到的客人面前——这个简单规则,正是分布式系统里“一致性哈希”的核心思想。数据像寿司盘一样在哈希环上游走,被第一个“迎接”它的节点收下。当客人中途离开或新客人入座,传送带上的盘子只需小幅重新分配,不会全场大乱。本文从最朴素的“取模分片”讲起,分析它为何在节点变动时引发“迁移风暴”,再逐步搭建哈希环模型与数据定位算法,用“虚拟节点”解决均衡难题,最后审视 Redis Cluster 与 Dynamo 等工业系统如何应用这套哲学。
一致性哈希是分布式存储与缓存的基石。掌握它,你才能在节点扩容、故障迁移时让数据只“微调”而非“全体搬家”,这也是高并发架构师做容量规划与排障的基础功。
目录
可视化演示
模拟一致性哈希环的节点定位与最小数据迁移过程,对比虚拟节点改善负载均衡的效果,观察节点增减与数据迁移。
本节配有交互动效(进入视口后自动加载,约 3KB)。
模式1:基础一致性哈希 0~2^32首尾连成哈希环 数据顺时钟找首个节点 migrate = 1/N 数据迁移 节点增减时仅约1/N数据需迁移。
问题背景:取模分片的困境
周末的寿司店客流爆满,老板把食材分成三桌摆放。他给每份寿司盘盖上编号,用“编号对桌子数取余”决定盘子放哪桌。3 张桌子时,1、4、7 号盘去 A 桌,2、5、8 号盘去 B 桌……规则公平又简单,客人取餐也快。可营业中途突然要加第四张桌,老板傻眼了:按“对 3 取余”放的盘,现在要对 4 取余重新计算,几乎每盘都要撤下搬到新桌。
在分布式系统里,“桌子”就是存储节点(Redis 节点、数据库分片),“编号取余”就是取模分片:把数据的键做哈希后,对节点总数 n 取余,得到固定节点编号。这套方案简单直观,但它有一个致命弱点——节点数一变,映射关系整体失效。原来存放在 n 个节点上的数据,只有不到 1/n 能留在原节点,其余全部要搬家,迁移比例高达:
这种全员搬家的现象在行业内叫迁移风暴。老板意识到,必须找到一种“加一张桌子时只有邻近几盘需要挪动”的摆盘方式——于是他想到了把长条桌弯成一个环形传送带。
- 节点总数 n当前参与存储的机器数量,比如 5 台 Redis。
- 变化源 n - 1新增一个节点后,旧映射关系整体失效,原来 n 台机器上几乎都有数据需要挪动。
- 迁移比例 R需要搬家的数据占总数据的比例,n 越大越接近 1。
哈希环模型:旋转寿司传送带
老板把长条取餐台弯成一个巨大的首尾相接的环形传送带。任何一盘寿司都有固定的“出生角度”(比如 12 点方向、3 点方向);每位客人也有固定座位角度。只要用同一个量角器,盘子和座位就能画在同一张环图上。计算机里的“环”就是哈希环:取一个足够大的整数空间(0 到 2^32−1),把它首尾相接成环。数据键与节点名使用同一个哈希函数,各自算出自己落在环上的位置。
哈希函数就像一个“量角器”:任何输入都能得到一个看似随机的角度值。因为所有参与者共用同一个量角器,数据才能清楚地知道“我沿顺时针方向遇到的第一个座位属于谁”。环本身没有起点也没有终点,这正是“一致性”的几何基础。位置计算如下:
现在,每个节点和数据都在环上有了唯一“座位号”。但寿司盘如何知道自己的主人是谁呢?这正是下一节要解决的定位问题。老板看着传送带,心里有了答案:让盘子自己走,遇到第一个客人就停下。
- 哈希函数 hash(x)把任意字符串(数据键或节点名)映射为一个整数,类似把寿司盘编号换算成角度。
- 环总长 2^32哈希环的刻度范围,约 42.9 亿个位置。
- 取模 mod保证位置不超出环的范围,就像角度永远落在 0° 到 360° 之间。
数据定位算法:寿司找主人
传送带上没有调度员,寿司盘的规则只有一个:沿顺时针方向遇到的第一个客人就是主人。数学上,这叫后继查询(successor)——从数据位置出发,沿着环顺时针找到第一个不小于自身位置的节点位置。节点座位都按角度排好了座次表,老板只需要查表,不需要让盘子真的转完一圈。
具体做法是把所有节点(含虚拟节点)的位置排序,用二分查找快速锁定目标。在排好序的数组里找“第一个大于等于我的位置”,是二分查找的标准玩法,复杂度只有 O(log n)。定位公式如下:
这套查表法又快又准。但老板很快发现了新问题:有些座位风水好,面前的盘子络绎不绝;有些座位偏僻,半天不见一盘。哈希值虽然是均匀随机的,但“随机分布在环上”不等于“恰好均匀等距”。于是,虚拟节点登场了。
- 集合 S环上所有节点(含虚拟节点)的位置集合,相当于客人座次表。
- 条件 p ≥ k筛掉所有位于数据位置“上游”的节点,只保留数据前方(顺时针方向)的节点。
- 取最小 min在候选中挑最接近数据位置的那个,就是传送带上第一个遇到的客人。
- 回绕保护如果前方没有任何节点,则绕环一周取最小位置。
虚拟节点机制:多点代取
老板给每位客人发几张“代取卡”,卡片散落在传送带沿线的各个角度。寿司盘遇到任意一张卡,就算这位客人的单。客人名下的卡片越多、散布越广,他实际接到的盘子就越接近公平。这就是虚拟节点的朴素思想:每个物理节点在哈希环上创建 v 个虚拟节点,用“节点名#编号”作为哈希输入,得到 v 个散布的位置。任何数据落到某个虚拟节点上,最终都归属于它对应的物理节点。
虚拟节点解决了两个问题:一是让物理节点的“势力范围”不再只取决于一个随机点,而是由多个随机点共同决定,降低方差;二是可以为不同性能的机器配置不同数量的虚拟节点,让高配机器多“接单”。均衡度可以用变异系数(CV)衡量,它约等于:
工程上 v 取 100 到 1000 已经能获得很好的均衡效果;超过 1000 后收益极低,反而增加元数据开销。老板发现发三四十张卡已经很均匀,再发就纯粹是浪费卡纸。
- 标准差 σ各节点实际负载与平均负载的偏差程度,数值越大越不均衡。
- 平均负载 μ总数据量除以节点数,即每个节点“理想情况下”该分担的量。
- 物理节点数 n 与虚拟节点数 v乘积 n×v 是环上虚拟节点的总数量。
- 平方根反比虚拟节点从 100 提到 400,总虚拟节点翻 4 倍,CV 只降到原来的一半。
工程实战:三条真实传送带
老板的“环形传送带”理念在连锁餐饮界火了,不同品牌有不同的落地方式。Redis Cluster 用的是“固定槽位”方案:把哈希空间预先切成 16384 个槽(slot),每个 key 通过 CRC16 哈希算法落进某个槽,槽再分配给具体节点。表面看它不是经典的一致性哈希环,本质却是“预分片的环”——节点增减时,只需迁移涉及的槽,而不是全量数据。
Dynamo(Amazon 的分布式键值存储,DynamoDB 的前身)则直接采用了经典一致性哈希 + 虚拟节点:每个物理节点在环上拥有多个 token(虚拟节点位置),数据落在两个 token 之间,由顺时针后一个 token 的物理节点管理。这种设计让它在节点频繁上下线时依然保持低迁移量。Memcached 客户端 libketama 也是同款思路,被大量缓存中间层沿用。槽位计算如下:
无论哪种品牌,老板们的核心诉求都一致:节点增减时,只有涉及的那几个区间段的寿司盘需要换主人。接下来要讨论的,就是这种“换主人”到底换多少。
- CRC16(key)一种快速校验和算法,把 key 映射成一个整数。
- 槽位总数 16384Redis 精心挑选的固定格子数,兼顾元数据体积与迁移粒度。
- 取余落槽key 最终归属哪个槽,由余数决定。
故障与扩容:有人离席与新人入座
营业中突然有客人身体不适离席,他座位前的寿司盘没有主人,按照规则,下一站客人会自动接手——数据顺时针交给下一个节点。这是分布式系统故障自愈的基本盘:某个节点宕机,它负责的数据区间顺时针转移给后继节点,系统仍能对外服务。反过来,新客人入座,他只影响自己座位之前一段传送带上的盘子:这些盘子原本要送到下一个客人,现在被新客人“截胡”。
原来有 n 个节点,新节点在环上均匀随机落点,它占据的区间长度期望是环长的 1/(n+1)。因此,一致性哈希的数据迁移比例大约只有:
不过,离席客人的盘子全压到下一站,容易造成“热点爆仓”——比如大流量节点突然宕机,它的全部负载瞬时叠加到顺时针下一个节点,可能把对方也压垮。所以生产环境要用虚拟节点把压力打散,并配合副本机制,让某个节点故障时流量分散到多个后备节点。
- 原节点数 n扩容前集群的机器数量。
- 新节点落点新节点的哈希位置在环上均匀随机,占一段弧长。
- 迁移比例 M新区间长度除以环总长 L。
- 与取模对比取模分片迁移 (n−1)/n = 90%,一致性哈希约 9%,差了 10 倍。
权衡与演进:没有免费的均匀
虚拟节点越多越均匀,但老板的记录本也越来越厚。每个虚拟节点都要保存一份元数据(位置 + 归属),环上总虚拟节点数是 n×v,元数据总量就是 O(n×v);定位时在排序数组上二分查找,复杂度为 O(log(n×v))。所以虚拟节点数量并非越大越好——从 1 涨到 100 收益显著,从 5000 涨到 10000 几乎看不出变化,内存浪费却翻倍。
更棘手的是热度失衡:哈希均匀只保证“数据数量均匀”,不保证“请求均匀”。某些爆款寿司盘(热点 key)被反复点单,无论它在环上哪个位置,都会把对应节点打到高负载。工程上通常要做热点 key 隔离、多副本分摊,或者改用有界负载一致性哈希等变体算法。近年也出现了跳跃一致性哈希(jump consistent hash)等新方案,在虚拟节点数量大时能以更少元数据达到类似效果。成本模型可写成:
老板最终总结出经营哲学:传送带解决的是“客人变动时不让全场大乱”,而不是“让每个人吃到的寿司绝对一样多”。理解了这一点,才算真正读懂了一致性哈希这套架构智慧。
- 物理节点数 n机器越多,需要的虚拟节点总量越大。
- 虚拟节点数 v控制单台机器的“代取卡”数量。
- 总量 n×v环上总节点数,直接决定元数据内存。
- 增长率线性成本随 n 和 v 线性增长,而均衡度只随平方根提升——投入产出不对等。
AI 应用场景
推荐系统特征缓存集群扩容
阶段:在线推理阶段的热点特征缓存扩容与故障恢复。
解决的问题:推荐模型在线服务需要读取用户 Embedding、商品特征,这些特征通常缓存在 Redis 集群中。传统取模分片在扩容时会导致 90% 以上的缓存键重新分布,缓存命中率断崖式下跌,进而引发 DB 打爆。一致性哈希把迁移率压到约 1/(n+1),扩容对在线推理延迟几乎无感。
具体案例:某推荐系统特征缓存从 10 个 Redis 节点扩容到 20 个,采用一致性哈希 + 200 个虚拟节点。扩容期间缓存命中率从 99.2% 仅短暂回落到 98.5%,15 分钟内恢复,P99 延迟无显著抖动;若用取模分片,命中率预计跌破 50%,会直接拖垮下游数据库。
大规模稀疏特征存储分片
阶段:分布式训练中的数据并行与 Embedding 参数分片存储。
解决的问题:大规模推荐模型的稀疏特征表(百亿级 ID 映射 Embedding)无法放入单机内存,需要分片到多台参数服务器。节点故障是常态,若分片方案迁移率过高,训练任务会反复中断重算。一致性哈希 + 虚拟节点让故障节点只影响相邻区间,配合副本能快速恢复。
具体案例:某广告 CTR 模型将稀疏特征表存于自研的类 Dynamo 存储中,每个特征 ID 通过一致性哈希映射到 3 个副本节点。某次 8 节点集群中 1 台物理机宕机,只有约 1/8 的特征重映射到邻居节点,训练任务在 30 秒内完成参数拉取恢复,未触发全局 checkpoint 回滚。
分库分表中间件的一致性路由
阶段:数据仓库与特征平台的分片键路由阶段。
解决的问题:业务数据(如用户行为日志)按分片键写入多张表/多个库。扩容时若采用取模分片需要全量数据重分布,而一致性哈希路由只需迁移少量分片,让数据管道在扩缩容时保持稳定。
具体案例:某数据中台使用 ShardingSphere 做分库分表,分片策略中选择一致性哈希。当用户规模翻倍、分片数从 8 扩到 16 时,只需迁移约 1/16 的历史数据,离线数仓补数任务量大幅下降,避免了周末凌晨全量重跑。
Python 演示
import bisectimport hashlibimport math class ConsistentHashRing: """一致性哈希环, 支持虚拟节点""" def __init__(self, vnodes=100): self.vnodes = vnodes self.ring = {} # 位置 -> 节点id self.sorted_keys = [] # 有序位置列表 self.node_vnodes = {} # 节点id -> [位置列表] def _hash(self, key): """用 md5 转成 128 位整数""" return int(hashlib.md5(key.encode('utf-8')).hexdigest(), 16) def add_node(self, node_id): self.node_vnodes[node_id] = [] for i in range(self.vnodes): pos = self._hash('%s#%d' % (node_id, i)) self.ring[pos] = node_id self.node_vnodes[node_id].append(pos) self.sorted_keys = sorted(self.ring.keys()) def remove_node(self, node_id): for pos in self.node_vnodes[node_id]: del self.ring[pos] del self.node_vnodes[node_id] self.sorted_keys = sorted(self.ring.keys()) def get_node(self, key): """顺时针找到第一个节点""" if not self.sorted_keys: return None pos = self._hash(key) idx = bisect.bisect_left(self.sorted_keys, pos) if idx == len(self.sorted_keys): idx = 0 return self.ring[self.sorted_keys[idx]] def mod_mapping(key, n): """取模分片对照: 对节点数取余""" return int(hashlib.md5(key.encode('utf-8')).hexdigest(), 16) % n def migration_ratio(ring, keys, new_node): """统计加入新节点后的迁移比例""" before = {k: ring.get_node(k) for k in keys} ring.add_node(new_node) after = {k: ring.get_node(k) for k in keys} return sum(1 for k in keys if before[k] != after[k]) / len(keys) if __name__ == '__main__': keys = ['key:%d' % i for i in range(5000)] nodes = ['node-A', 'node-B', 'node-C'] print('=== 不同虚拟节点数下的负载均衡度 (CV) ===') for v in (1, 10, 100, 500): r = ConsistentHashRing(vnodes=v) for node in nodes: r.add_node(node) cnt = {} for k in keys: n = r.get_node(k) cnt[n] = cnt.get(n, 0) + 1 vals = list(cnt.values()) mu = sum(vals) / len(vals) sd = math.sqrt(sum((x - mu) ** 2 for x in vals) / len(vals)) print(' vnodes=%4d CV=%.4f' % (v, sd / mu)) print('=== 新增一个节点后的迁移率对比 ===') ring = ConsistentHashRing(vnodes=100) for node in nodes: ring.add_node(node) ratio = migration_ratio(ring, keys, 'node-D') print(' 一致性哈希迁移率: %.2f%%' % (ratio * 100)) before = [mod_mapping(k, 3) for k in keys] after = [mod_mapping(k, 4) for k in keys] mod_ratio = sum(1 for a, b in zip(before, after) if a != b) / len(keys) print(' 取模分片迁移率: %.2f%%' % (mod_ratio * 100))
pos = self._hash('%s#%d' % (node_id, i))把“节点名+编号”拼起来做哈希,让同一物理节点的多个虚拟节点散落环上。self.ring[pos] = node_id记录虚拟节点位置到物理节点的映射,这是环的核心数据结构。idx = bisect.bisect_left(self.sorted_keys, pos)在有序位置数组中二分查找,定位第一个不小于数据位置的下标。if idx == len(self.sorted_keys): idx = 0处理环回绕:如果数据位置比所有虚拟节点都大,则回到环头取最小位置。before = {k: ring.get_node(k) for k in keys}扩容前记录“钥匙→节点”的归属,用于后续对比。sd = math.sqrt(sum((x - mu) ** 2 for x in vals) / len(vals))计算负载标准差,配合均值 mu 得到变异系数 CV,衡量均衡度。migration_ratio(ring, keys, 'node-D')演示新增节点只触发约 1/4 的键迁移,而取模分片接近 3/4。
常见误区
误区“一致性哈希环上节点数量多,数据自然就均匀了,物理节点越多越均匀。”
正解哈希值是均匀随机的,但少量随机点在环上的“势力区间”方差很大。就像 3 个客人随便坐传送带,肯定有人面前盘子多有人少。只有引入虚拟节点,让每个物理节点在环上拥有多个代表点,均衡度才会按平方根规律提升。物理节点数量本身并不能保证均匀。
为何常见:人们容易把“哈希函数的均匀性”等同于“落点分布的均匀性”,忽略了小样本随机采样的离散性。
误区“既然虚拟节点能提升均衡度,那就设个一万两万,越均衡越好。”
正解均衡度收益是平方根的:从 100 加到 400,均衡只提升一倍;但元数据量线性增长,内存占用和查找时间都变大,定位复杂度 O(log(n×v)) 也在涨。工程上 100~1000 是甜点区间,再多只是自我感动。
为何常见:只看到“提升均衡度”的一面,忽略了系统里没有免费午餐,任何优化都有成本,且收益往往是边际递减的。
误区“一致性哈希的设计目标就是节点增减时数据不动。”
正解一致性哈希只做到了“迁移量最小化”,而不是“零迁移”。新增一个节点,新节点在环上随机落点,总有一个区间段的寿司盘会从旧主人换成新主人,期望迁移率是 1/(n+1)。但相比取模分片的 (n−1)/n,它把“全场搬家”变成了“小区间微调”。
为何常见:宣传中强调“只影响相邻节点”,很多人就误以为完全不受影响,忽略了相邻区间本身也是数据。
误区“数据在环上分布均匀了,每个节点的请求量也就均匀了。”
正解数据量的均匀与访问热度的均匀是两回事。一个爆款商品 key 的访问量可能占全网 50%,无论它落在哪个节点,都会把那个节点打满。哈希只解决“存储分布”问题,不解决“热点倾斜”问题,需要热点隔离、多副本或缓存等手段。
为何常见:初学者把“负载均衡”想得太简单,忽略了真实流量是高度倾斜的幂律分布,而不是均匀分布。
误区“Redis Cluster 用的是经典一致性哈希环,所以我在论文里看到的虚拟节点方案它全都有。”
正解Redis Cluster 用的是固定 16384 槽位的预分片方案,key 经 CRC16 取余落到某个槽,槽再指派给节点。它借鉴了“哈希空间分区”思想,但不是经典环上“顺时针找后继”的一致性哈希,也没有用虚拟节点。Dynamo 和 libketama 才是经典一致性哈希 + 虚拟节点的代表。
为何常见:很多人把“通过哈希把数据分散到多节点”的所有方案都笼统叫作一致性哈希,混淆了具体的技术选型差异。
面试题
Q1. 一致性哈希环上,数据 key 定位负责节点的规则是?
- A.从 key 位置逆时针遇到的第一个节点
- B.从 key 位置顺时针遇到的第一个节点← 正确答案
- C.取全部节点的哈希值平均值
- D.随机挑选一个在线节点
答案:B。一致性哈希的核心规则是“顺时针后继”:数据从自己的哈希位置出发,沿环顺时针遇到的第一个节点就是负责人。逆时针、取平均值、随机挑选都会破坏一致性与可复现性。就像寿司盘沿传送带顺时针遇到的第一位客人接单。
Q2. 取模分片下,节点数从 5 增加到 6,大约有多少数据需要迁移?
- A.大约六分之一的数据
- B.大约五分之一的数据
- C.约五分之四的数据迁移← 正确答案
- D.数据完全不需要迁移
答案:C。取模分片的映射依赖节点数 n,从 5 变 6 后,几乎所有数据的取余结果都变了,迁移比例是 (n−1)/n = 4/5,即 80%。这正是“迁移风暴”的由来。一致性哈希才把迁移率降到约 1/(n+1)。
Q3. 原来有 n 个节点的环,新增一个节点后期望数据迁移比例约为?
- A.约 1/n,n 为原节点数
- B.约 1/(n+1),n 为原节点数← 正确答案
- C.接近 n/(n+1) 的全部数据
- D.固定迁移一半的数据
答案:B。新节点均匀随机落点,它在环上占据的区间长度期望为 L/(n+1),除以环总长 L,迁移比例就是 1/(n+1)。取模分片则是 (n−1)/n。理解这个对比,就理解了一致性哈希为何能扛住频繁扩缩容。
Q4. 虚拟节点数 v 与负载均衡度(变异系数 CV)的关系是?
- A.CV 与 v 成正比关系
- B.CV 与 v 的平方根成正比
- C.CV 与 v 的平方根成反比← 正确答案
- D.CV 与 v 完全无关
答案:C。变异系数 CV ≈ 1/√(n×v),总虚拟节点越多,负载波动越小,因此 CV 与虚拟节点总数的平方根成反比。这也是为什么 v 从 1 提到 100 改善明显,从 5000 再翻倍却几乎无感——平方根收益递减。
Q5. 关于虚拟节点机制,下列说法正确的有?
- A.虚拟节点是物理节点在环上的多副本位置← 正确答案
- B.虚拟节点数量越多均衡度无代价提升
- C.虚拟节点越多哈希环元数据量越大← 正确答案
- D.虚拟节点越多哈希计算开销越小
答案:AC。虚拟节点用一个物理节点创建多个“代取卡”位置,A 正确。均衡度提升伴随元数据量 O(n×v) 增长,并非无代价,B 错误、C 正确。虚拟节点越多,需要计算的哈希次数越多,D 说反了。生产上要控制 v 在 100~1000 的甜点区间。
Q6. Redis Cluster 中哈希槽的固定总数是?
- A.固定 1024 个槽位
- B.固定 4096 个槽位
- C.固定 16384 个槽位← 正确答案
- D.固定 65536 个槽位
答案:C。Redis Cluster 把哈希空间划分为 16384 个槽,key 通过 CRC16 取余落槽,槽在节点间迁移。这个数字兼顾了迁移粒度和元数据大小。它本质是预分片方案,与经典一致性哈希环不同。
Q7. 哈希环节点位置排序后,用二分查找定位“第一个不小于 key 位置的节点”,时间复杂度是?
- A.常数时间 O(1)
- B.对数时间 O(log n)← 正确答案
- C.线性时间 O(n)
- D.线性对数时间 O(n log n)
答案:B。在有序数组上二分查找,每次把搜索区间折半,复杂度为 O(log n)。哈希计算本身是 O(1),但环上节点众多时,排序表上的后继查询用二分是最优实践。这也解释了为什么环元数据越多查找越慢。
Q8. 关于一致性哈希的工程权衡,正确的有?
- A.虚拟节点丰富时元数据总量为 O(nv)← 正确答案
- B.哈希均匀分布不等同于访问请求均匀← 正确答案
- C.一致性哈希能保证各节点负载完全相等
- D.有界负载变体可缓解热点数据倾斜问题← 正确答案
答案:ABD。虚拟节点总量 n×v 直接决定环元数据量,A 正确。数据量均匀不代表请求热度和均,热点 key 仍会打爆单点,B 正确、C 错误。有界负载一致性哈希等变体会把热点key重新分配给低负载节点,D 正确。
Q9. 一致性哈希相比取模分片最大的优势是?
- A.哈希函数计算速度更快
- B.节点增减时数据迁移量最小化← 正确答案
- C.数据请求的平均延迟更低
- D.数据副本数量可以自动增加
答案:B。取模分片在节点数变化时几乎全员搬家(迁移风暴),一致性哈希只迁移新节点相邻区间,迁移量约 1/(n+1)。减少迁移量意味着更高的可用性、更低的带宽成本和更稳定的访问延迟,这才是它成为分布式系统基石的原因。
Q10. 环长为 L,原来有 n 个节点,新增节点均匀随机落点,其导致的迁移量期望为?
- A.迁移区间长度 L/(n+1)
- B.迁移比例是 1/(n+1)← 正确答案
- C.节点平均间距是 n/L
- D.迁移比例恒为 1/n
答案:B。环上原有 n 个节点形成 n 个区间,新节点落入某个区间的概率与该区间长度成正比,等价于把每个区间再切一刀。所有区间加总后,新节点占据的弧长期望为 L/(n+1),因此迁移比例是 1/(n+1)。选项 A 说的是弧长而不是比例,C、D 描述的不是迁移量。