【问题标题】:Can you explain theese disturbing anomalies of md5 and modulo?你能解释一下这些令人不安的 md5 和模数异常吗?
【发布时间】:2011-03-29 17:58:40
【问题描述】:

好吧,标题真的很主观。但这正是我的问题所在。

背景是我想将静态 Web 内容的点击平均分配给定义数量的缓存服务器。此外,由于多个域正在使用并且请求不会相互阻塞,因此应该加快向客户端的交付速度。我也不需要经典的负载均衡器,而是立即在我的 html 代码中生成正确的链接。

我还想确保相同的 url 始终由同一台服务器提供服务。

所以我只是定义了一个小函数,它通过对请求 url 进行哈希处理并通过正在使用的服务器数量计算模数来返回要使用的主机:

function pseudocode_statify($url) { // $url looks like /folder1/folder2/file.jpg
 return 'http://' . md5($url) % $num_of_servers .'.mydomain.com' . $url;
}

我首先有一些类似十六进制解码和子字符串的东西来防止溢出,但发现它就像上面的方法一样工作。

但是我的问题是,如果我运行以下测试脚本:

for($i=0;$i<100000;$i++) {
  $md5 = md5(uniqid($i).microtime().rand(1,999999999999));
  $result[$md5%2]++;
}

我希望分布均匀。意味着 $result[0] 将接近 $result[1] 的值;

事实并非如此。

好的,到目前为止这没什么特别的。我会接受 md5 不像我想象的那样均匀分布的事实,并且会采用另一种哈希算法,如 sha1 或其他东西。

但我试图重现这些发现并发现了一个我无法解释的模式。

该比率始终约为 2/1。事实上,这个比率总是在 1/2.16 到 1/2.17 之间

上面脚本的一些运行示例输出:

output was generated by: echo "ratio: ".$result[0]/$result[1]."\n";

ratio: 2.1757121534504
ratio: 2.1729411578062
ratio: 2.1726559360393
ratio: 2.1676895664225
ratio: 2.1667416128848
ratio: 2.1667115284133
ratio: 2.1677791605385
ratio: 2.1658969579688
ratio: 2.1668508131769
ratio: 2.1689292821741

现在奇怪的是 sums % 2 等于 1sums % 2 等于 0 的比率有时会交替出现!

for($j = 0; $j<100;$j++) {
    for($i=0;$i<100000;$i++) {
      $md5 = md5(uniqid($i).microtime().rand(1,999999999999));
      $result[$md5%2]++;
    }
var_dump($result);
}

我从命令行两次运行脚本并在 3 次运行后中止它,它产生了这两个输出:

joe@joe-laptop:/home/flimmit/httpdocs$ php test.php
PHP Notice:  Undefined variable: result in /home/flimmit/httpdocs/test.php on line 6
PHP Notice:  Undefined offset: 0 in /home/flimmit/httpdocs/test.php on line 6
PHP Notice:  Undefined offset: 1 in /home/flimmit/httpdocs/test.php on line 6
array(2) {
  [0]=>
  int(68223)
  [1]=>
  int(31777)
}
array(2) {
  [0]=>
  int(136384)
  [1]=>
  int(63616)
}
array(2) {
  [0]=>
  int(204498)
  [1]=>
  int(95502)
}
^C
joe@joe-laptop:/home/flimmit/httpdocs$ php test.php
PHP Notice:  Undefined variable: result in /home/flimmit/httpdocs/test.php on line 6
PHP Notice:  Undefined offset: 1 in /home/flimmit/httpdocs/test.php on line 6
PHP Notice:  Undefined offset: 0 in /home/flimmit/httpdocs/test.php on line 6
array(2) {
  [1]=>
  int(31612)
  [0]=>
  int(68388)
}
array(2) {
  [1]=>
  int(63318)
  [0]=>
  int(136682)
}
array(2) {
  [1]=>
  int(94954)
  [0]=>
  int(205046)
}
^C
joe@joe-laptop:/home/flimmit/httpdocs$ 

您可以在第一个中看到结果的第一个条目总是更高,而在第二个中则相反。相同的脚本。

奇怪的是,我只能在多次运行脚本时重现这种行为。

我编写了这个小脚本来重现“交换”并生成足够的测量数据:

for($j = 0; $j<100;$j++) {
  for($i=0;$i<rand(1000,10000);$i++) {
    $md5 = md5(uniqid($i).microtime().rand(1,99999999));
    $result[$md5%2]++;
    }
    #var_dump($result);
    echo "ratio: ".$result[0]/$result[1]." ".(($result[0]<$result[1]) ? "A":"B")."\n";
    sleep(rand(2,5));
}

但这里它只打印 b,从不打印 A。这让我觉得脚本中可能存在语义错误,但我没有发现任何错误。

我真的被困住了,这让我很困扰。

所以我的问题:

  • 如果我能更深入地了解 md5,包括发行版等,您能否推荐任何文献/网络链接

  • 您能解释/重现该行为吗?我这里有错误吗? (其实很有可能,但我找不到)

  • 您能否推荐任何其他适合我的用例的算法?它不需要加密或强大,但需要快速、确定性和均匀分布。

【问题讨论】:

  • 附带说明,如果您希望稳定地路由请求,您可能需要研究一致性哈希。

标签: php cryptography md5 checksum


【解决方案1】:

md5() 函数返回一个字符串,而不是一个整数。

这意味着这个字符串将被类型转换为整数来做模;并且由于此字符串将包含 0-9A-F 范围内的字符,并转换为整数,因此您有:

  • 16 次中有 1 次获得 0
  • 16 次中有 9 次机会介于 1 到 9 之间
  • 16 次中有 6 次在 A 和 F 之间切换 -- 将被转换为 0


比如这个:

$a = md5('plop1');
var_dump($a, (int)$a);

$a = md5('plop2');
var_dump($a, (int)$a);

$a = md5('plop5');
var_dump($a, (int)$a);

将为您提供以下输出:

string 'ac4bf0e466417336599b72a8b2f595da' (length=32)
int 0

string 'ed91c463402dd797d0718350f5bd0acd' (length=32)
int 0

string '85782b3afb04072c1bf172a6a7e6bb5e' (length=32)
int 85782

我会让你猜猜这对模运算符的结果可能产生的影响;-)

【讨论】:

  • 这听起来合乎逻辑。我认为错误在我身边的某个地方。试图复制。
  • 使用hexdec(substr(md5($url),0,6))%$num 修复它现在就像一个魅力:) 谢谢!
  • 事实上我在他的第一名中就做到了,但我认为我可以做得更好哈哈 xD 它经常是一样的:D
  • @Joe Hopfgartner 似乎是一个相当缓慢的计算。可能有一个更快的。查看答案。
  • 我认为我将不得不将这个问题作为一个完美的例子来说明无声有损类型转换可能导致的病态。
【解决方案2】:
for ($i = 0; $i < 10; $i++) {
    srand(crc32('test_url1'));
    echo rand().'<br />';
}
for ($i = 0; $i < 10; $i++) {
    srand(crc32('test_url2'));
    echo rand().'<br />';
}

向 rand 函数添加一个范围,你就有了你的服务器值。

【讨论】:

    【解决方案3】:

    “背景是我想将静态 Web 内容的点击平均分配给定义数量的缓存服务器。”

    很多很多负载均衡器已经这样做了。 Squid、nginx、Varnish、HAProxy...

    请不要用 PHP 编写您自己的负载均衡器。请。

    【讨论】:

    • 我不想要负载均衡器,我想通过打印正确的 html 输出来立即寻址正确的服务器。然而,背景也是为了加快客户端的交付,因为使用了多个域并且请求不会相互阻塞。那时负载均衡器就没有多大用处了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-07-06
    • 1970-01-01
    • 2017-05-12
    相关资源
    最近更新 更多