[{"data":1,"prerenderedAt":1983},["ShallowReactive",2],{"article-en-\u002Fdsa\u002Fconsistent-hashing":3,"article-sibling-en-\u002Fdsa\u002Fconsistent-hashing":1077,"surround-en-\u002Fdsa\u002Fconsistent-hashing":1947,"related-en-\u002Fdsa\u002Fconsistent-hashing":1954},{"id":4,"title":5,"body":6,"date":1058,"description":1059,"draft":1060,"extension":1061,"img":1062,"meta":1063,"navigation":210,"path":1064,"seo":1065,"slug":1066,"stem":1067,"tags":1068,"topics":1073,"__hash__":1076},"content\u002F1.dsa\u002F4.consistent-hashing.md","Consistent Hashing in Go: Why hash(key) % N Breaks and How a Ring Fixes It",{"type":7,"value":8,"toc":1048},"minimark",[9,18,25,33,38,45,55,66,70,81,104,115,119,126,778,789,795,799,816,819,901,908,915,919,926,932,939,946,950,975,979,1012,1016,1041,1044],[10,11,12,13,17],"p",{},"You run a cache with 10 nodes. To decide where a key lives, you do the obvious thing: ",[14,15,16],"code",{},"hash(key) % 10",". It is fast, stateless, and every client computes the same answer. Then traffic grows and you add an 11th node.",[10,19,20,21,24],{},"Now ",[14,22,23],{},"hash(key) % 11"," gives a different answer for almost every key. Nearly the whole cache is suddenly in the wrong place, every lookup misses, and your database takes the full load at the worst possible moment.",[10,26,27,28,32],{},"This is the first article of a series on the mathematics behind distributed systems. We start with the structure that fixes this problem: ",[29,30,31],"strong",{},"consistent hashing",". The code is Go, and the numbers below come from running it.",[34,35,37],"h2",{"id":36},"the-problem-modulo-ties-every-key-to-n","The problem: modulo ties every key to N",[10,39,40,41,44],{},"With ",[14,42,43],{},"hash(key) % N",", a key's owner depends on N itself. Change N and the whole mapping shifts. I hashed 100,000 keys and went from 10 nodes to 11:",[46,47,52],"pre",{"className":48,"code":50,"language":51},[49],"language-text","modulo 10->11 moved: 91.0%\n","text",[14,53,50],{"__ignoreMap":54},"",[10,56,57,58,61,62,65],{},"For a key to keep its owner after going from N to N+1 nodes, its hash must give the same remainder mod N and mod N+1. That is rare, so about ",[14,59,60],{},"N\u002F(N+1)"," of all keys move. The ideal answer is much smaller: the new node should take roughly ",[14,63,64],{},"1\u002F(N+1)"," of the keys, and nobody else should lose anything they did not have to give up.",[34,67,69],{"id":68},"the-idea-put-keys-and-nodes-on-the-same-ring","The idea: put keys and nodes on the same ring",[10,71,72,73,76,77,80],{},"Hash the space of possible values onto a circle, say ",[14,74,75],{},"0"," to ",[14,78,79],{},"2^32 - 1",", where the end wraps to the start. Then:",[82,83,84,92,97],"ol",{},[85,86,87,88,91],"li",{},"Hash each ",[29,89,90],{},"node"," to a position on the ring.",[85,93,87,94,91],{},[29,95,96],{},"key",[85,98,99,100,103],{},"A key belongs to the ",[29,101,102],{},"first node clockwise"," from its position.",[10,105,106,107,110,111,114],{},"Now a node owns only the arc between its predecessor and itself. Add a node and it takes over a slice of one neighbour's arc. Remove a node and its arc falls to the next one clockwise. Every other key stays exactly where it was. On average only ",[14,108,109],{},"K\u002FN"," keys move, where K is the number of keys and N the number of nodes. That is the mathematical content: ownership depends on ",[29,112,113],{},"relative order on a circle",", not on the count of nodes.",[34,116,118],{"id":117},"implementation-in-go","Implementation in Go",[10,120,121,122,125],{},"The ring is a sorted slice of positions plus a map from position to node. Lookup is a binary search for the first position ",[14,123,124],{},">= hash(key)",", wrapping to index 0 past the end.",[46,127,131],{"className":128,"code":129,"language":130,"meta":54,"style":54},"language-go shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","type Ring struct {\n    replicas int\n    points   []uint32          \u002F\u002F sorted ring positions\n    owner    map[uint32]string \u002F\u002F position -> node\n}\n\nfunc hash32(s string) uint32 {\n    sum := sha256.Sum256([]byte(s))\n    return binary.BigEndian.Uint32(sum[:4])\n}\n\nfunc (r *Ring) Add(node string) {\n    for i := 0; i \u003C r.replicas; i++ {\n        p := hash32(fmt.Sprintf(\"%s#%d\", node, i))\n        r.owner[p] = node\n        r.points = append(r.points, p)\n    }\n    sort.Slice(r.points, func(a, b int) bool { return r.points[a] \u003C r.points[b] })\n}\n\nfunc (r *Ring) Get(key string) string {\n    h := hash32(key)\n    i := sort.Search(len(r.points), func(i int) bool { return r.points[i] >= h })\n    if i == len(r.points) {\n        i = 0 \u002F\u002F past the last point, wrap around\n    }\n    return r.owner[r.points[i]]\n}\n","go",[14,132,133,152,163,179,199,205,212,240,271,306,311,316,348,388,438,462,496,502,579,584,589,619,635,703,729,742,747,773],{"__ignoreMap":54},[134,135,138,142,146,149],"span",{"class":136,"line":137},"line",1,[134,139,141],{"class":140},"sMK4o","type",[134,143,145],{"class":144},"sBMFI"," Ring",[134,147,148],{"class":140}," struct",[134,150,151],{"class":140}," {\n",[134,153,155,159],{"class":136,"line":154},2,[134,156,158],{"class":157},"sTEyZ","    replicas ",[134,160,162],{"class":161},"spNyl","int\n",[134,164,166,169,172,175],{"class":136,"line":165},3,[134,167,168],{"class":157},"    points   ",[134,170,171],{"class":140},"[]",[134,173,174],{"class":161},"uint32",[134,176,178],{"class":177},"sHwdD","          \u002F\u002F sorted ring positions\n",[134,180,182,185,188,190,193,196],{"class":136,"line":181},4,[134,183,184],{"class":157},"    owner    ",[134,186,187],{"class":140},"map[",[134,189,174],{"class":161},[134,191,192],{"class":140},"]",[134,194,195],{"class":161},"string",[134,197,198],{"class":177}," \u002F\u002F position -> node\n",[134,200,202],{"class":136,"line":201},5,[134,203,204],{"class":140},"}\n",[134,206,208],{"class":136,"line":207},6,[134,209,211],{"emptyLinePlaceholder":210},true,"\n",[134,213,215,218,222,225,229,232,235,238],{"class":136,"line":214},7,[134,216,217],{"class":140},"func",[134,219,221],{"class":220},"s2Zo4"," hash32",[134,223,224],{"class":140},"(",[134,226,228],{"class":227},"sHdIc","s",[134,230,231],{"class":161}," string",[134,233,234],{"class":140},")",[134,236,237],{"class":161}," uint32",[134,239,151],{"class":140},[134,241,243,246,249,252,255,258,261,264,266,268],{"class":136,"line":242},8,[134,244,245],{"class":157},"    sum ",[134,247,248],{"class":140},":=",[134,250,251],{"class":157}," sha256",[134,253,254],{"class":140},".",[134,256,257],{"class":220},"Sum256",[134,259,260],{"class":140},"([]",[134,262,263],{"class":161},"byte",[134,265,224],{"class":140},[134,267,228],{"class":157},[134,269,270],{"class":140},"))\n",[134,272,274,278,281,283,286,288,291,293,296,299,303],{"class":136,"line":273},9,[134,275,277],{"class":276},"s7zQu","    return",[134,279,280],{"class":157}," binary",[134,282,254],{"class":140},[134,284,285],{"class":157},"BigEndian",[134,287,254],{"class":140},[134,289,290],{"class":220},"Uint32",[134,292,224],{"class":140},[134,294,295],{"class":157},"sum",[134,297,298],{"class":140},"[:",[134,300,302],{"class":301},"sbssI","4",[134,304,305],{"class":140},"])\n",[134,307,309],{"class":136,"line":308},10,[134,310,204],{"class":140},[134,312,314],{"class":136,"line":313},11,[134,315,211],{"emptyLinePlaceholder":210},[134,317,319,321,324,327,330,333,335,338,340,342,344,346],{"class":136,"line":318},12,[134,320,217],{"class":140},[134,322,323],{"class":140}," (",[134,325,326],{"class":227},"r ",[134,328,329],{"class":140},"*",[134,331,332],{"class":144},"Ring",[134,334,234],{"class":140},[134,336,337],{"class":220}," Add",[134,339,224],{"class":140},[134,341,90],{"class":227},[134,343,231],{"class":161},[134,345,234],{"class":140},[134,347,151],{"class":140},[134,349,351,354,357,359,362,365,367,370,373,375,378,380,383,386],{"class":136,"line":350},13,[134,352,353],{"class":276},"    for",[134,355,356],{"class":157}," i ",[134,358,248],{"class":140},[134,360,361],{"class":301}," 0",[134,363,364],{"class":140},";",[134,366,356],{"class":157},[134,368,369],{"class":140},"\u003C",[134,371,372],{"class":157}," r",[134,374,254],{"class":140},[134,376,377],{"class":157},"replicas",[134,379,364],{"class":140},[134,381,382],{"class":157}," i",[134,384,385],{"class":140},"++",[134,387,151],{"class":140},[134,389,391,394,396,398,400,403,405,408,410,413,417,421,424,426,429,432,434,436],{"class":136,"line":390},14,[134,392,393],{"class":157},"        p ",[134,395,248],{"class":140},[134,397,221],{"class":220},[134,399,224],{"class":140},[134,401,402],{"class":157},"fmt",[134,404,254],{"class":140},[134,406,407],{"class":220},"Sprintf",[134,409,224],{"class":140},[134,411,412],{"class":140},"\"",[134,414,416],{"class":415},"swJcz","%s",[134,418,420],{"class":419},"sfazB","#",[134,422,423],{"class":415},"%d",[134,425,412],{"class":140},[134,427,428],{"class":140},",",[134,430,431],{"class":157}," node",[134,433,428],{"class":140},[134,435,382],{"class":157},[134,437,270],{"class":140},[134,439,441,444,446,449,452,454,456,459],{"class":136,"line":440},15,[134,442,443],{"class":157},"        r",[134,445,254],{"class":140},[134,447,448],{"class":157},"owner",[134,450,451],{"class":140},"[",[134,453,10],{"class":157},[134,455,192],{"class":140},[134,457,458],{"class":140}," =",[134,460,461],{"class":157}," node\n",[134,463,465,467,469,472,475,478,480,483,485,488,490,493],{"class":136,"line":464},16,[134,466,443],{"class":157},[134,468,254],{"class":140},[134,470,471],{"class":157},"points ",[134,473,474],{"class":140},"=",[134,476,477],{"class":220}," append",[134,479,224],{"class":140},[134,481,482],{"class":157},"r",[134,484,254],{"class":140},[134,486,487],{"class":157},"points",[134,489,428],{"class":140},[134,491,492],{"class":157}," p",[134,494,495],{"class":140},")\n",[134,497,499],{"class":136,"line":498},17,[134,500,501],{"class":140},"    }\n",[134,503,505,508,510,513,515,517,519,521,523,526,529,531,534,537,539,542,545,548,550,552,554,556,558,560,563,565,567,569,571,574,576],{"class":136,"line":504},18,[134,506,507],{"class":157},"    sort",[134,509,254],{"class":140},[134,511,512],{"class":220},"Slice",[134,514,224],{"class":140},[134,516,482],{"class":157},[134,518,254],{"class":140},[134,520,487],{"class":157},[134,522,428],{"class":140},[134,524,525],{"class":140}," func(",[134,527,528],{"class":227},"a",[134,530,428],{"class":140},[134,532,533],{"class":227}," b",[134,535,536],{"class":161}," int",[134,538,234],{"class":140},[134,540,541],{"class":161}," bool",[134,543,544],{"class":140}," {",[134,546,547],{"class":276}," return",[134,549,372],{"class":157},[134,551,254],{"class":140},[134,553,487],{"class":157},[134,555,451],{"class":140},[134,557,528],{"class":157},[134,559,192],{"class":140},[134,561,562],{"class":140}," \u003C",[134,564,372],{"class":157},[134,566,254],{"class":140},[134,568,487],{"class":157},[134,570,451],{"class":140},[134,572,573],{"class":157},"b",[134,575,192],{"class":140},[134,577,578],{"class":140}," })\n",[134,580,582],{"class":136,"line":581},19,[134,583,204],{"class":140},[134,585,587],{"class":136,"line":586},20,[134,588,211],{"emptyLinePlaceholder":210},[134,590,592,594,596,598,600,602,604,607,609,611,613,615,617],{"class":136,"line":591},21,[134,593,217],{"class":140},[134,595,323],{"class":140},[134,597,326],{"class":227},[134,599,329],{"class":140},[134,601,332],{"class":144},[134,603,234],{"class":140},[134,605,606],{"class":220}," Get",[134,608,224],{"class":140},[134,610,96],{"class":227},[134,612,231],{"class":161},[134,614,234],{"class":140},[134,616,231],{"class":161},[134,618,151],{"class":140},[134,620,622,625,627,629,631,633],{"class":136,"line":621},22,[134,623,624],{"class":157},"    h ",[134,626,248],{"class":140},[134,628,221],{"class":220},[134,630,224],{"class":140},[134,632,96],{"class":157},[134,634,495],{"class":140},[134,636,638,641,643,646,648,651,653,656,658,660,662,664,667,669,672,674,676,678,680,682,684,686,688,690,692,694,697,700],{"class":136,"line":637},23,[134,639,640],{"class":157},"    i ",[134,642,248],{"class":140},[134,644,645],{"class":157}," sort",[134,647,254],{"class":140},[134,649,650],{"class":220},"Search",[134,652,224],{"class":140},[134,654,655],{"class":220},"len",[134,657,224],{"class":140},[134,659,482],{"class":157},[134,661,254],{"class":140},[134,663,487],{"class":157},[134,665,666],{"class":140},"),",[134,668,525],{"class":140},[134,670,671],{"class":227},"i",[134,673,536],{"class":161},[134,675,234],{"class":140},[134,677,541],{"class":161},[134,679,544],{"class":140},[134,681,547],{"class":276},[134,683,372],{"class":157},[134,685,254],{"class":140},[134,687,487],{"class":157},[134,689,451],{"class":140},[134,691,671],{"class":157},[134,693,192],{"class":140},[134,695,696],{"class":140}," >=",[134,698,699],{"class":157}," h ",[134,701,702],{"class":140},"})\n",[134,704,706,709,711,714,717,719,721,723,725,727],{"class":136,"line":705},24,[134,707,708],{"class":276},"    if",[134,710,356],{"class":157},[134,712,713],{"class":140},"==",[134,715,716],{"class":220}," len",[134,718,224],{"class":140},[134,720,482],{"class":157},[134,722,254],{"class":140},[134,724,487],{"class":157},[134,726,234],{"class":140},[134,728,151],{"class":140},[134,730,732,735,737,739],{"class":136,"line":731},25,[134,733,734],{"class":157},"        i ",[134,736,474],{"class":140},[134,738,361],{"class":301},[134,740,741],{"class":177}," \u002F\u002F past the last point, wrap around\n",[134,743,745],{"class":136,"line":744},26,[134,746,501],{"class":140},[134,748,750,752,754,756,758,760,762,764,766,768,770],{"class":136,"line":749},27,[134,751,277],{"class":276},[134,753,372],{"class":157},[134,755,254],{"class":140},[134,757,448],{"class":157},[134,759,451],{"class":140},[134,761,482],{"class":157},[134,763,254],{"class":140},[134,765,487],{"class":157},[134,767,451],{"class":140},[134,769,671],{"class":157},[134,771,772],{"class":140},"]]\n",[134,774,776],{"class":136,"line":775},28,[134,777,204],{"class":140},[10,779,780,781,784,785,788],{},"Lookup costs ",[14,782,783],{},"O(log P)"," where P is the number of points on the ring. Adding or removing a node costs a re-sort here for clarity. A production version would insert in place, and guard the ring with a ",[14,786,787],{},"sync.RWMutex"," since lookups are far more frequent than membership changes.",[10,790,791,792,794],{},"The ",[14,793,377],{}," field is the next idea, and it matters more than it looks.",[34,796,798],{"id":797},"virtual-nodes-fixing-the-uneven-ring","Virtual nodes: fixing the uneven ring",[10,800,801,802,804,805,808,809,812,813,254],{},"With one point per node, positions are random, and random points on a circle make arcs of very different lengths. One node can own a huge arc by luck. Rather than hash each node once, hash it ",[14,803,377],{}," times (",[14,806,807],{},"node-3#0",", ",[14,810,811],{},"node-3#1",", ...). Each node now owns many small arcs scattered around the ring, and by the law of large numbers its total share converges toward ",[14,814,815],{},"1\u002FN",[10,817,818],{},"I ran 100,000 keys over 10 nodes, then added an 11th, for several virtual node counts:",[820,821,822,841],"table",{},[823,824,825],"thead",{},[826,827,828,832,835,838],"tr",{},[829,830,831],"th",{},"Virtual nodes per node",[829,833,834],{},"Max load \u002F mean",[829,836,837],{},"Std dev of load",[829,839,840],{},"Keys moved on add (ideal 9.1%)",[842,843,844,859,873,887],"tbody",{},[826,845,846,850,853,856],{},[847,848,849],"td",{},"1",[847,851,852],{},"3.09x",[847,854,855],{},"89.9%",[847,857,858],{},"8.8%",[826,860,861,864,867,870],{},[847,862,863],{},"10",[847,865,866],{},"1.38x",[847,868,869],{},"24.7%",[847,871,872],{},"10.7%",[826,874,875,878,881,884],{},[847,876,877],{},"100",[847,879,880],{},"1.18x",[847,882,883],{},"9.7%",[847,885,886],{},"8.4%",[826,888,889,892,895,898],{},[847,890,891],{},"500",[847,893,894],{},"1.07x",[847,896,897],{},"4.5%",[847,899,900],{},"9.0%",[10,902,903,904,907],{},"Two things stand out. Movement on node addition sits near the ideal 9.1% (versus 91% for modulo) at every setting, so the ring's stability does not depend on vnodes. ",[29,905,906],{},"Balance"," does: with a single point the busiest node carries three times its fair share, and you need on the order of 100 or more virtual nodes to get within about 20% of even. The cost is memory and a larger sorted slice, which is cheap.",[10,909,910,911,914],{},"Virtual nodes also let you do ",[29,912,913],{},"weighted"," placement for free: give a node with twice the capacity twice the replicas.",[34,916,918],{"id":917},"the-mistake-that-quietly-wrecks-it-a-weak-hash","The mistake that quietly wrecks it: a weak hash",[10,920,921,922,925],{},"My first run used ",[14,923,924],{},"hash\u002Ffnv",", a fast non-cryptographic hash that feels like the right tool. The numbers were bad:",[46,927,930],{"className":928,"code":929,"language":51},[49],"vnodes= 10  max\u002Fmean=3.96  std dev=102%     (FNV-1a)\nvnodes= 10  max\u002Fmean=1.38  std dev=24.7%    (SHA-256, first 4 bytes)\n",[14,931,929],{"__ignoreMap":54},[10,933,934,935,938],{},"Same code, same keys, same 10 virtual nodes. The difference is the hash. Node labels like ",[14,936,937],{},"node-3#7"," are highly similar strings, and FNV-1a mixes similar inputs poorly, so the virtual nodes cluster instead of spreading. The ring's guarantees assume positions that look uniformly random. If your hash does not deliver that, adding virtual nodes will not save you.",[10,940,941,942,945],{},"Use a hash with good avalanche behaviour. SHA-256 truncated works and is plenty fast for this. In production you will often see MurmurHash3 or xxHash, which are fast and mix well. Whatever you pick, ",[29,943,944],{},"measure the load spread"," as above before trusting it.",[34,947,949],{"id":948},"where-this-runs-in-real-systems","Where this runs in real systems",[951,952,953,959,965],"ul",{},[85,954,955,958],{},[29,956,957],{},"Distributed caches"," (memcached client libraries such as ketama, Redis clients) pick a shard per key this way, so scaling the cache does not flush it.",[85,960,961,964],{},[29,962,963],{},"Dynamo-style databases"," (Cassandra, Riak, DynamoDB's lineage) place data on a token ring and replicate to the next nodes clockwise.",[85,966,967,970,971,254],{},[29,968,969],{},"Load balancers"," use it for sticky routing. The load balancing article covers the same structure from the request-routing angle: ",[528,972,974],{"href":973},"\u002Fblog\u002Fbackend\u002Fload-balancing\u002Fload-balancing-algorithms","Load Balancing Algorithms",[34,976,978],{"id":977},"limits-and-alternatives","Limits and alternatives",[951,980,981,987,996,1006],{},[85,982,983,986],{},[29,984,985],{},"Hot keys."," The ring balances keys, not traffic. One very popular key still overloads one node, and no amount of hashing fixes that. You need replication or caching in front.",[85,988,989,992,993,995],{},[29,990,991],{},"Rebalancing is still real work."," Only ",[14,994,109],{}," keys move, but they do move, and something must actually copy that data.",[85,997,998,1001,1002,1005],{},[29,999,1000],{},"Rendezvous (highest random weight) hashing"," scores every node per key and picks the top one. It needs no ring or vnodes and balances well, at ",[14,1003,1004],{},"O(N)"," per lookup.",[85,1007,1008,1011],{},[29,1009,1010],{},"Jump consistent hash"," needs no memory at all and balances nearly perfectly, but nodes can only be added or removed at the end of a numbered list, which suits sharded storage better than a cache whose nodes fail at random.",[34,1013,1015],{"id":1014},"takeaways","Takeaways",[951,1017,1018,1026,1032,1035,1038],{},[85,1019,1020,1022,1023,1025],{},[14,1021,43],{}," makes ownership depend on N, so changing N moves about ",[14,1024,60],{}," of the keys (91% from 10 to 11 nodes in my run).",[85,1027,1028,1029,1031],{},"A hash ring makes ownership depend on position on a circle: adding or removing a node moves only about ",[14,1030,109],{}," keys.",[85,1033,1034],{},"One point per node gives lopsided arcs. Use roughly 100 or more virtual nodes per node to even out load.",[85,1036,1037],{},"The ring assumes a well-mixing hash. A weak one like FNV-1a on similar labels erased the benefit of vnodes in my test, so measure.",[85,1039,1040],{},"It balances keys, not traffic. Hot keys need a different answer.",[10,1042,1043],{},"Next in the series: how distributed systems order events without a shared clock, with Lamport and vector clocks.",[1045,1046,1047],"style",{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":54,"searchDepth":154,"depth":154,"links":1049},[1050,1051,1052,1053,1054,1055,1056,1057],{"id":36,"depth":154,"text":37},{"id":68,"depth":154,"text":69},{"id":117,"depth":154,"text":118},{"id":797,"depth":154,"text":798},{"id":917,"depth":154,"text":918},{"id":948,"depth":154,"text":949},{"id":977,"depth":154,"text":978},{"id":1014,"depth":154,"text":1015},"2026-11-09","Add one server to a cache cluster that uses hash(key) % N and about 91% of your keys change owner overnight. Consistent hashing cuts that to roughly K\u002FN. We build the ring in Go, add virtual nodes, and measure real remapping and load balance, including the hash function mistake that quietly wrecks it.",false,"md","https:\u002F\u002Fres.cloudinary.com\u002Fdpdwhd6ka\u002Fimage\u002Fupload\u002Fv1691560968\u002FBlog\u002Farticles\u002Falgorithms\u002Fbolohyzwbz34pedr1zpp.png",{},"\u002Fdsa\u002Fconsistent-hashing",{"title":5,"description":1059},"consistent-hashing-go","1.dsa\u002F4.consistent-hashing",[1069,1070,1071,1072],"Consistent-Hashing","Go","Distributed-Systems","Algorithms",[1074,1075],"dsa","backend","_KXDG1XUbLW1NqgYYf788mSVUkrClXFbveCHJpKRH0g",{"id":1078,"title":1079,"body":1080,"date":1058,"description":1938,"draft":1060,"extension":1061,"img":1062,"meta":1939,"navigation":210,"path":1940,"seo":1941,"slug":1942,"stem":1943,"tags":1944,"topics":1945,"__hash__":1946},"content\u002F1.dsa\u002F4.consistent-hashing.fr.md","Consistent hashing en Go : pourquoi hash(key) % N casse et comment un anneau le répare",{"type":7,"value":1081,"toc":1928},[1082,1088,1094,1100,1104,1110,1115,1124,1128,1137,1158,1168,1172,1178,1684,1693,1699,1703,1716,1719,1791,1798,1805,1809,1815,1821,1827,1834,1838,1860,1864,1894,1898,1923,1926],[10,1083,1084,1085,1087],{},"Vous gérez un cache de 10 nœuds. Pour décider où vit une clé, vous faites l'évidence : ",[14,1086,16],{},". C'est rapide, sans état, et chaque client calcule la même réponse. Puis le trafic grimpe et vous ajoutez un 11e nœud.",[10,1089,1090,1091,1093],{},"Désormais ",[14,1092,23],{}," donne une réponse différente pour presque toutes les clés. Quasiment tout le cache se retrouve au mauvais endroit, chaque lecture rate, et votre base de données encaisse toute la charge au pire moment.",[10,1095,1096,1097,1099],{},"Ceci est le premier article d'une série sur les mathématiques derrière les systèmes distribués. On commence par la structure qui règle ce problème : le ",[29,1098,31],{}," (hachage cohérent). Le code est en Go, et les chiffres ci-dessous viennent de son exécution.",[34,1101,1103],{"id":1102},"le-problème-le-modulo-lie-chaque-clé-à-n","Le problème : le modulo lie chaque clé à N",[10,1105,1106,1107,1109],{},"Avec ",[14,1108,43],{},", le propriétaire d'une clé dépend de N lui-même. Changez N et tout le mapping bouge. J'ai haché 100 000 clés en passant de 10 à 11 nœuds :",[46,1111,1113],{"className":1112,"code":50,"language":51},[49],[14,1114,50],{"__ignoreMap":54},[10,1116,1117,1118,1120,1121,1123],{},"Pour qu'une clé garde son propriétaire en passant de N à N+1 nœuds, son hash doit donner le même reste modulo N et modulo N+1. C'est rare, donc environ ",[14,1119,60],{}," des clés bougent. La réponse idéale est bien plus petite : le nouveau nœud devrait prendre environ ",[14,1122,64],{}," des clés, et personne d'autre ne devrait céder plus que nécessaire.",[34,1125,1127],{"id":1126},"lidée-placer-clés-et-nœuds-sur-le-même-anneau","L'idée : placer clés et nœuds sur le même anneau",[10,1129,1130,1131,1133,1134,1136],{},"On projette l'espace des valeurs de hash sur un cercle, disons de ",[14,1132,75],{}," à ",[14,1135,79],{},", où la fin reboucle sur le début. Puis :",[82,1138,1139,1146,1151],{},[85,1140,1141,1142,1145],{},"On hache chaque ",[29,1143,1144],{},"nœud"," vers une position de l'anneau.",[85,1147,1141,1148,1145],{},[29,1149,1150],{},"clé",[85,1152,1153,1154,1157],{},"Une clé appartient au ",[29,1155,1156],{},"premier nœud rencontré dans le sens horaire"," depuis sa position.",[10,1159,1160,1161,1163,1164,1167],{},"Un nœud ne possède plus que l'arc entre son prédécesseur et lui-même. Ajoutez un nœud : il récupère une tranche de l'arc d'un voisin. Retirez un nœud : son arc tombe chez le suivant. Toutes les autres clés restent exactement où elles étaient. En moyenne seules ",[14,1162,109],{}," clés bougent, K étant le nombre de clés et N le nombre de nœuds. C'est le contenu mathématique : la propriété dépend de l'",[29,1165,1166],{},"ordre relatif sur un cercle",", pas du nombre de nœuds.",[34,1169,1171],{"id":1170},"implémentation-en-go","Implémentation en Go",[10,1173,1174,1175,1177],{},"L'anneau est une slice triée de positions plus une map position vers nœud. La recherche est une recherche binaire de la première position ",[14,1176,124],{},", avec retour à l'indice 0 après la fin.",[46,1179,1181],{"className":128,"code":1180,"language":130,"meta":54,"style":54},"type Ring struct {\n    replicas int\n    points   []uint32          \u002F\u002F positions triées sur l'anneau\n    owner    map[uint32]string \u002F\u002F position -> nœud\n}\n\nfunc hash32(s string) uint32 {\n    sum := sha256.Sum256([]byte(s))\n    return binary.BigEndian.Uint32(sum[:4])\n}\n\nfunc (r *Ring) Add(node string) {\n    for i := 0; i \u003C r.replicas; i++ {\n        p := hash32(fmt.Sprintf(\"%s#%d\", node, i))\n        r.owner[p] = node\n        r.points = append(r.points, p)\n    }\n    sort.Slice(r.points, func(a, b int) bool { return r.points[a] \u003C r.points[b] })\n}\n\nfunc (r *Ring) Get(key string) string {\n    h := hash32(key)\n    i := sort.Search(len(r.points), func(i int) bool { return r.points[i] >= h })\n    if i == len(r.points) {\n        i = 0 \u002F\u002F après le dernier point, on reboucle\n    }\n    return r.owner[r.points[i]]\n}\n",[14,1182,1183,1193,1199,1210,1225,1229,1233,1251,1273,1297,1301,1305,1331,1361,1399,1417,1443,1447,1511,1515,1519,1547,1561,1619,1641,1652,1656,1680],{"__ignoreMap":54},[134,1184,1185,1187,1189,1191],{"class":136,"line":137},[134,1186,141],{"class":140},[134,1188,145],{"class":144},[134,1190,148],{"class":140},[134,1192,151],{"class":140},[134,1194,1195,1197],{"class":136,"line":154},[134,1196,158],{"class":157},[134,1198,162],{"class":161},[134,1200,1201,1203,1205,1207],{"class":136,"line":165},[134,1202,168],{"class":157},[134,1204,171],{"class":140},[134,1206,174],{"class":161},[134,1208,1209],{"class":177},"          \u002F\u002F positions triées sur l'anneau\n",[134,1211,1212,1214,1216,1218,1220,1222],{"class":136,"line":181},[134,1213,184],{"class":157},[134,1215,187],{"class":140},[134,1217,174],{"class":161},[134,1219,192],{"class":140},[134,1221,195],{"class":161},[134,1223,1224],{"class":177}," \u002F\u002F position -> nœud\n",[134,1226,1227],{"class":136,"line":201},[134,1228,204],{"class":140},[134,1230,1231],{"class":136,"line":207},[134,1232,211],{"emptyLinePlaceholder":210},[134,1234,1235,1237,1239,1241,1243,1245,1247,1249],{"class":136,"line":214},[134,1236,217],{"class":140},[134,1238,221],{"class":220},[134,1240,224],{"class":140},[134,1242,228],{"class":227},[134,1244,231],{"class":161},[134,1246,234],{"class":140},[134,1248,237],{"class":161},[134,1250,151],{"class":140},[134,1252,1253,1255,1257,1259,1261,1263,1265,1267,1269,1271],{"class":136,"line":242},[134,1254,245],{"class":157},[134,1256,248],{"class":140},[134,1258,251],{"class":157},[134,1260,254],{"class":140},[134,1262,257],{"class":220},[134,1264,260],{"class":140},[134,1266,263],{"class":161},[134,1268,224],{"class":140},[134,1270,228],{"class":157},[134,1272,270],{"class":140},[134,1274,1275,1277,1279,1281,1283,1285,1287,1289,1291,1293,1295],{"class":136,"line":273},[134,1276,277],{"class":276},[134,1278,280],{"class":157},[134,1280,254],{"class":140},[134,1282,285],{"class":157},[134,1284,254],{"class":140},[134,1286,290],{"class":220},[134,1288,224],{"class":140},[134,1290,295],{"class":157},[134,1292,298],{"class":140},[134,1294,302],{"class":301},[134,1296,305],{"class":140},[134,1298,1299],{"class":136,"line":308},[134,1300,204],{"class":140},[134,1302,1303],{"class":136,"line":313},[134,1304,211],{"emptyLinePlaceholder":210},[134,1306,1307,1309,1311,1313,1315,1317,1319,1321,1323,1325,1327,1329],{"class":136,"line":318},[134,1308,217],{"class":140},[134,1310,323],{"class":140},[134,1312,326],{"class":227},[134,1314,329],{"class":140},[134,1316,332],{"class":144},[134,1318,234],{"class":140},[134,1320,337],{"class":220},[134,1322,224],{"class":140},[134,1324,90],{"class":227},[134,1326,231],{"class":161},[134,1328,234],{"class":140},[134,1330,151],{"class":140},[134,1332,1333,1335,1337,1339,1341,1343,1345,1347,1349,1351,1353,1355,1357,1359],{"class":136,"line":350},[134,1334,353],{"class":276},[134,1336,356],{"class":157},[134,1338,248],{"class":140},[134,1340,361],{"class":301},[134,1342,364],{"class":140},[134,1344,356],{"class":157},[134,1346,369],{"class":140},[134,1348,372],{"class":157},[134,1350,254],{"class":140},[134,1352,377],{"class":157},[134,1354,364],{"class":140},[134,1356,382],{"class":157},[134,1358,385],{"class":140},[134,1360,151],{"class":140},[134,1362,1363,1365,1367,1369,1371,1373,1375,1377,1379,1381,1383,1385,1387,1389,1391,1393,1395,1397],{"class":136,"line":390},[134,1364,393],{"class":157},[134,1366,248],{"class":140},[134,1368,221],{"class":220},[134,1370,224],{"class":140},[134,1372,402],{"class":157},[134,1374,254],{"class":140},[134,1376,407],{"class":220},[134,1378,224],{"class":140},[134,1380,412],{"class":140},[134,1382,416],{"class":415},[134,1384,420],{"class":419},[134,1386,423],{"class":415},[134,1388,412],{"class":140},[134,1390,428],{"class":140},[134,1392,431],{"class":157},[134,1394,428],{"class":140},[134,1396,382],{"class":157},[134,1398,270],{"class":140},[134,1400,1401,1403,1405,1407,1409,1411,1413,1415],{"class":136,"line":440},[134,1402,443],{"class":157},[134,1404,254],{"class":140},[134,1406,448],{"class":157},[134,1408,451],{"class":140},[134,1410,10],{"class":157},[134,1412,192],{"class":140},[134,1414,458],{"class":140},[134,1416,461],{"class":157},[134,1418,1419,1421,1423,1425,1427,1429,1431,1433,1435,1437,1439,1441],{"class":136,"line":464},[134,1420,443],{"class":157},[134,1422,254],{"class":140},[134,1424,471],{"class":157},[134,1426,474],{"class":140},[134,1428,477],{"class":220},[134,1430,224],{"class":140},[134,1432,482],{"class":157},[134,1434,254],{"class":140},[134,1436,487],{"class":157},[134,1438,428],{"class":140},[134,1440,492],{"class":157},[134,1442,495],{"class":140},[134,1444,1445],{"class":136,"line":498},[134,1446,501],{"class":140},[134,1448,1449,1451,1453,1455,1457,1459,1461,1463,1465,1467,1469,1471,1473,1475,1477,1479,1481,1483,1485,1487,1489,1491,1493,1495,1497,1499,1501,1503,1505,1507,1509],{"class":136,"line":504},[134,1450,507],{"class":157},[134,1452,254],{"class":140},[134,1454,512],{"class":220},[134,1456,224],{"class":140},[134,1458,482],{"class":157},[134,1460,254],{"class":140},[134,1462,487],{"class":157},[134,1464,428],{"class":140},[134,1466,525],{"class":140},[134,1468,528],{"class":227},[134,1470,428],{"class":140},[134,1472,533],{"class":227},[134,1474,536],{"class":161},[134,1476,234],{"class":140},[134,1478,541],{"class":161},[134,1480,544],{"class":140},[134,1482,547],{"class":276},[134,1484,372],{"class":157},[134,1486,254],{"class":140},[134,1488,487],{"class":157},[134,1490,451],{"class":140},[134,1492,528],{"class":157},[134,1494,192],{"class":140},[134,1496,562],{"class":140},[134,1498,372],{"class":157},[134,1500,254],{"class":140},[134,1502,487],{"class":157},[134,1504,451],{"class":140},[134,1506,573],{"class":157},[134,1508,192],{"class":140},[134,1510,578],{"class":140},[134,1512,1513],{"class":136,"line":581},[134,1514,204],{"class":140},[134,1516,1517],{"class":136,"line":586},[134,1518,211],{"emptyLinePlaceholder":210},[134,1520,1521,1523,1525,1527,1529,1531,1533,1535,1537,1539,1541,1543,1545],{"class":136,"line":591},[134,1522,217],{"class":140},[134,1524,323],{"class":140},[134,1526,326],{"class":227},[134,1528,329],{"class":140},[134,1530,332],{"class":144},[134,1532,234],{"class":140},[134,1534,606],{"class":220},[134,1536,224],{"class":140},[134,1538,96],{"class":227},[134,1540,231],{"class":161},[134,1542,234],{"class":140},[134,1544,231],{"class":161},[134,1546,151],{"class":140},[134,1548,1549,1551,1553,1555,1557,1559],{"class":136,"line":621},[134,1550,624],{"class":157},[134,1552,248],{"class":140},[134,1554,221],{"class":220},[134,1556,224],{"class":140},[134,1558,96],{"class":157},[134,1560,495],{"class":140},[134,1562,1563,1565,1567,1569,1571,1573,1575,1577,1579,1581,1583,1585,1587,1589,1591,1593,1595,1597,1599,1601,1603,1605,1607,1609,1611,1613,1615,1617],{"class":136,"line":637},[134,1564,640],{"class":157},[134,1566,248],{"class":140},[134,1568,645],{"class":157},[134,1570,254],{"class":140},[134,1572,650],{"class":220},[134,1574,224],{"class":140},[134,1576,655],{"class":220},[134,1578,224],{"class":140},[134,1580,482],{"class":157},[134,1582,254],{"class":140},[134,1584,487],{"class":157},[134,1586,666],{"class":140},[134,1588,525],{"class":140},[134,1590,671],{"class":227},[134,1592,536],{"class":161},[134,1594,234],{"class":140},[134,1596,541],{"class":161},[134,1598,544],{"class":140},[134,1600,547],{"class":276},[134,1602,372],{"class":157},[134,1604,254],{"class":140},[134,1606,487],{"class":157},[134,1608,451],{"class":140},[134,1610,671],{"class":157},[134,1612,192],{"class":140},[134,1614,696],{"class":140},[134,1616,699],{"class":157},[134,1618,702],{"class":140},[134,1620,1621,1623,1625,1627,1629,1631,1633,1635,1637,1639],{"class":136,"line":705},[134,1622,708],{"class":276},[134,1624,356],{"class":157},[134,1626,713],{"class":140},[134,1628,716],{"class":220},[134,1630,224],{"class":140},[134,1632,482],{"class":157},[134,1634,254],{"class":140},[134,1636,487],{"class":157},[134,1638,234],{"class":140},[134,1640,151],{"class":140},[134,1642,1643,1645,1647,1649],{"class":136,"line":731},[134,1644,734],{"class":157},[134,1646,474],{"class":140},[134,1648,361],{"class":301},[134,1650,1651],{"class":177}," \u002F\u002F après le dernier point, on reboucle\n",[134,1653,1654],{"class":136,"line":744},[134,1655,501],{"class":140},[134,1657,1658,1660,1662,1664,1666,1668,1670,1672,1674,1676,1678],{"class":136,"line":749},[134,1659,277],{"class":276},[134,1661,372],{"class":157},[134,1663,254],{"class":140},[134,1665,448],{"class":157},[134,1667,451],{"class":140},[134,1669,482],{"class":157},[134,1671,254],{"class":140},[134,1673,487],{"class":157},[134,1675,451],{"class":140},[134,1677,671],{"class":157},[134,1679,772],{"class":140},[134,1681,1682],{"class":136,"line":775},[134,1683,204],{"class":140},[10,1685,1686,1687,1689,1690,1692],{},"La recherche coûte ",[14,1688,783],{}," où P est le nombre de points sur l'anneau. Ajouter ou retirer un nœud coûte ici un re-tri, par souci de clarté. Une version de production insérerait en place, et protégerait l'anneau avec un ",[14,1691,787],{}," puisque les lectures sont bien plus fréquentes que les changements de membres.",[10,1694,1695,1696,1698],{},"Le champ ",[14,1697,377],{}," est l'idée suivante, et elle compte plus qu'il n'y paraît.",[34,1700,1702],{"id":1701},"nœuds-virtuels-corriger-lanneau-déséquilibré","Nœuds virtuels : corriger l'anneau déséquilibré",[10,1704,1705,1706,1708,1709,808,1711,1713,1714,254],{},"Avec un seul point par nœud, les positions sont aléatoires, et des points aléatoires sur un cercle forment des arcs de longueurs très inégales. Un nœud peut hériter d'un énorme arc par hasard. Au lieu de hacher chaque nœud une fois, on le hache ",[14,1707,377],{}," fois (",[14,1710,807],{},[14,1712,811],{},", ...). Chaque nœud possède alors beaucoup de petits arcs éparpillés, et par la loi des grands nombres sa part totale converge vers ",[14,1715,815],{},[10,1717,1718],{},"J'ai fait tourner 100 000 clés sur 10 nœuds, puis ajouté un 11e, pour plusieurs nombres de nœuds virtuels :",[820,1720,1721,1737],{},[823,1722,1723],{},[826,1724,1725,1728,1731,1734],{},[829,1726,1727],{},"Nœuds virtuels par nœud",[829,1729,1730],{},"Charge max \u002F moyenne",[829,1732,1733],{},"Écart-type de la charge",[829,1735,1736],{},"Clés déplacées à l'ajout (idéal 9,1 %)",[842,1738,1739,1752,1765,1778],{},[826,1740,1741,1743,1746,1749],{},[847,1742,849],{},[847,1744,1745],{},"3,09x",[847,1747,1748],{},"89,9 %",[847,1750,1751],{},"8,8 %",[826,1753,1754,1756,1759,1762],{},[847,1755,863],{},[847,1757,1758],{},"1,38x",[847,1760,1761],{},"24,7 %",[847,1763,1764],{},"10,7 %",[826,1766,1767,1769,1772,1775],{},[847,1768,877],{},[847,1770,1771],{},"1,18x",[847,1773,1774],{},"9,7 %",[847,1776,1777],{},"8,4 %",[826,1779,1780,1782,1785,1788],{},[847,1781,891],{},[847,1783,1784],{},"1,07x",[847,1786,1787],{},"4,5 %",[847,1789,1790],{},"9,0 %",[10,1792,1793,1794,1797],{},"Deux choses ressortent. Le mouvement à l'ajout reste près de l'idéal 9,1 % (contre 91 % pour le modulo) quel que soit le réglage : la stabilité de l'anneau ne dépend pas des vnodes. L'",[29,1795,1796],{},"équilibrage",", lui, en dépend : avec un seul point, le nœud le plus chargé porte trois fois sa part, et il faut de l'ordre de 100 nœuds virtuels ou plus pour être à environ 20 % de l'uniforme. Le coût est un peu de mémoire et une slice triée plus grande, ce qui est bon marché.",[10,1799,1800,1801,1804],{},"Les nœuds virtuels permettent aussi un placement ",[29,1802,1803],{},"pondéré"," gratuitement : donnez à un nœud de capacité double deux fois plus de réplicas.",[34,1806,1808],{"id":1807},"lerreur-qui-ruine-tout-en-silence-une-fonction-de-hachage-faible","L'erreur qui ruine tout en silence : une fonction de hachage faible",[10,1810,1811,1812,1814],{},"Mon premier essai utilisait ",[14,1813,924],{},", un hash non cryptographique rapide qui semble le bon outil. Les chiffres étaient mauvais :",[46,1816,1819],{"className":1817,"code":1818,"language":51},[49],"vnodes= 10  max\u002Fmean=3.96  écart-type=102%     (FNV-1a)\nvnodes= 10  max\u002Fmean=1.38  écart-type=24.7%    (SHA-256, 4 premiers octets)\n",[14,1820,1818],{"__ignoreMap":54},[10,1822,1823,1824,1826],{},"Même code, mêmes clés, mêmes 10 nœuds virtuels. La différence, c'est le hash. Des libellés comme ",[14,1825,937],{}," sont des chaînes très proches, et FNV-1a mélange mal les entrées similaires : les nœuds virtuels s'agglutinent au lieu de se répartir. Les garanties de l'anneau supposent des positions qui paraissent uniformément aléatoires. Si votre hash ne le fournit pas, ajouter des nœuds virtuels ne vous sauvera pas.",[10,1828,1829,1830,1833],{},"Utilisez un hash à bonne diffusion (avalanche). SHA-256 tronqué fonctionne et reste largement assez rapide ici. En production on voit souvent MurmurHash3 ou xxHash, rapides et bien mélangés. Quel que soit votre choix, ",[29,1831,1832],{},"mesurez la répartition de charge"," comme ci-dessus avant de lui faire confiance.",[34,1835,1837],{"id":1836},"où-ça-tourne-dans-les-vrais-systèmes","Où ça tourne dans les vrais systèmes",[951,1839,1840,1846,1852],{},[85,1841,1842,1845],{},[29,1843,1844],{},"Caches distribués"," (bibliothèques clientes memcached comme ketama, clients Redis) : choix du shard par clé de cette façon, donc faire grossir le cache ne le vide pas.",[85,1847,1848,1851],{},[29,1849,1850],{},"Bases de type Dynamo"," (Cassandra, Riak, la lignée de DynamoDB) : les données sont placées sur un anneau de tokens et répliquées sur les nœuds suivants dans le sens horaire.",[85,1853,1854,1856,1857,254],{},[29,1855,969],{}," : routage persistant. L'article sur le load balancing couvre la même structure côté routage de requêtes : ",[528,1858,1859],{"href":973},"Algorithmes de load balancing",[34,1861,1863],{"id":1862},"limites-et-alternatives","Limites et alternatives",[951,1865,1866,1871,1880,1889],{},[85,1867,1868,1870],{},[29,1869,985],{}," L'anneau équilibre les clés, pas le trafic. Une clé très populaire surcharge toujours un nœud, et aucun hachage n'y change rien. Il faut de la réplication ou un cache devant.",[85,1872,1873,1876,1877,1879],{},[29,1874,1875],{},"Le rééquilibrage reste un vrai travail."," Seules ",[14,1878,109],{}," clés bougent, mais elles bougent, et quelque chose doit réellement copier ces données.",[85,1881,1882,1885,1886,1888],{},[29,1883,1884],{},"Le rendezvous hashing (highest random weight)"," note chaque nœud pour chaque clé et prend le meilleur. Pas d'anneau ni de vnodes, bon équilibrage, au prix de ",[14,1887,1004],{}," par recherche.",[85,1890,1891,1893],{},[29,1892,1010],{}," n'utilise aucune mémoire et équilibre presque parfaitement, mais on ne peut ajouter ou retirer des nœuds qu'en fin de liste numérotée, ce qui convient mieux à du stockage shardé qu'à un cache dont les nœuds tombent au hasard.",[34,1895,1897],{"id":1896},"à-retenir","À retenir",[951,1899,1900,1908,1914,1917,1920],{},[85,1901,1902,1904,1905,1907],{},[14,1903,43],{}," fait dépendre la propriété de N : changer N déplace environ ",[14,1906,60],{}," des clés (91 % de 10 à 11 nœuds dans mon essai).",[85,1909,1910,1911,1913],{},"Un anneau de hachage la fait dépendre de la position sur un cercle : ajouter ou retirer un nœud ne déplace qu'environ ",[14,1912,109],{}," clés.",[85,1915,1916],{},"Un point par nœud donne des arcs déséquilibrés. Prenez environ 100 nœuds virtuels ou plus par nœud pour lisser la charge.",[85,1918,1919],{},"L'anneau suppose un hash qui mélange bien. Un hash faible comme FNV-1a sur des libellés proches a annulé le bénéfice des vnodes dans mon test : mesurez.",[85,1921,1922],{},"Il équilibre les clés, pas le trafic. Les hot keys demandent une autre réponse.",[10,1924,1925],{},"Prochain article de la série : comment les systèmes distribués ordonnent les événements sans horloge partagée, avec les horloges de Lamport et vectorielles.",[1045,1927,1047],{},{"title":54,"searchDepth":154,"depth":154,"links":1929},[1930,1931,1932,1933,1934,1935,1936,1937],{"id":1102,"depth":154,"text":1103},{"id":1126,"depth":154,"text":1127},{"id":1170,"depth":154,"text":1171},{"id":1701,"depth":154,"text":1702},{"id":1807,"depth":154,"text":1808},{"id":1836,"depth":154,"text":1837},{"id":1862,"depth":154,"text":1863},{"id":1896,"depth":154,"text":1897},"Ajoutez un serveur à un cluster de cache qui utilise hash(key) % N et environ 91 % des clés changent de propriétaire d'un coup. Le consistent hashing ramène ça à environ K\u002FN. On construit l'anneau en Go, on ajoute des nœuds virtuels, et on mesure le remapping et l'équilibrage réels, y compris l'erreur de fonction de hachage qui ruine tout en silence.",{},"\u002Fdsa\u002Fconsistent-hashing.fr",{"title":1079,"description":1938},"hachage-coherent-go","1.dsa\u002F4.consistent-hashing.fr",[1069,1070,1071,1072],[1074,1075],"I4CjMzgrsRBsgQkQyPxD_VnWAZg3CzpqbgNOzOwPAnY",[1948,1951],{"title":1949,"path":1950},"Mastering the Quick Sort Algorithm in Python: A Comprehensive Guide","\u002Fdsa\u002Fquick-sort",{"title":1952,"path":1953},"Mastering NestJS: Your Ultimate Guide to Modern Backend Development","\u002Fbackend\u002Fnest-js\u002Fintroducing-nest-js",[1955,1966,1974],{"path":1956,"title":1957,"description":1958,"date":1959,"tags":1960,"topics":1963},"\u002Fbackend\u002Fload-balancing\u002Fload-balancing-algorithms","Load Balancing Algorithms: Each One Fixes the Last One's Flaw","Spreading requests across N servers sounds like a one-liner: pick a server, send the request. Then one backend is slower, or bigger, or holds a session, and the naive choice falls apart. This walks the classic algorithms as a chain where each one exists to fix the previous one's blind spot: round robin, weighted, least connections, power of two choices, and consistent hashing, in Go, ending with the failure every tutorial forgets.","2026-07-20",[1961,1070,1071,1962],"Load-Balancing","Resilience",[1075,1964,1965],"resilience","platform-engineering",{"path":1967,"title":1968,"description":1969,"date":1970,"tags":1971,"topics":1973},"\u002Fbackend\u002Fapi-design\u002Fretry-storms","Retry Storms: How Good Clients Take Down Healthy Servers","A retry looks harmless: the request failed, so try again. Multiply that by every client, add one slow dependency, and retries turn into a self-inflicted DDoS. This walks from the naive retry loop to exponential backoff, jitter, retry budgets and circuit breakers, the caller-side half of resilience that pairs with rate limiting on the server.","2026-07-11",[1962,1071,1070,1972],"Retries",[1075,1965,1964],{"path":1975,"title":1976,"description":1977,"date":1978,"tags":1979,"topics":1982},"\u002Fbackend\u002Fapi-design\u002Frate-limiting-at-scale","Why Redis Rate Limiting Breaks at Scale (and What Uber Does Instead)","A token bucket in memory is trivial. Put it behind Redis and it works, until it doesn't. This walks through rate limiting from one node, to a shared Redis, to why that model collapses at millions of requests per second, and the shift Uber made: enforce locally, coordinate globally, and drop by probability.","2026-07-10",[1980,1071,1070,1981],"Rate-Limiting","Performance",[1075,1965,1964],1794221351747]