揭示Ruby和Rails中UUID版本4的一些情况

301 阅读3分钟

Ruby的标准库和Rails的PostgreSQL适配器默认使用版本4的UUID。这可以在Rails的迁移中通过.参数来改变:

default: 'uuid_generate_v1()'

param,而Ruby的stdlib只支持版本4。

另一个有趣的区别是在实现方面。

在Ruby的stdlib中,UUID方法是在SecureRandom中找到的
 # SecureRandom.uuid generates a random v4 UUID (Universally Unique IDentifier).
  #
  # The version 4 UUID is purely random (except the version).
  # It doesn't contain meaningful information such as MAC addresses, timestamps, etc.
  #
  # See RFC 4122 for details of UUID.
  #
  def uuid
    ary = random_bytes(16).unpack("NnnnnN")
    ary[2] = (ary[2] & 0x0fff) | 0x4000
    ary[3] = (ary[3] & 0x3fff) | 0x8000
    "%08x-%04x-%04x-%04x-%04x%08x" % ary
  end  
Rails版本使用uuid-ossp postgres扩展
 # By default, this will use the +uuid_generate_v4()+ function from the
  # +uuid-ossp+ extension, which MUST be enabled on your database.
  def primary_key(name, type = :primary_key, options = {})
    return super unless type == :uuid
    options[:default] = options.fetch(:default, 'uuid_generate_v4()')
    options[:primary_key] = true
    column name, type, options
  end  

唯一的区别是每个库中实际的数字是如何产生的--这一点很值得研究--让我们从Ruby标准库开始。

Ruby标准库的实现
module SecureRandom
  if defined? OpenSSL::Random
    def self.gen_random(n)
      @pid = 0 unless defined?(@pid)
      pid = $$
      unless @pid == pid
        now = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
        ary = [now, @pid, pid]
        OpenSSL::Random.random_add(ary.join("").to_s, 0.0)
        @pid = pid
      end
      return OpenSSL::Random.random_bytes(n)
    end
  else
    def self.gen_random(n)
      ret = Random.raw_seed(n)
      unless ret
        raise NotImplementedError, "No random device"
      end
      unless ret.length == n
        raise NotImplementedError, "Unexpected partial read from random device: only #{ret.length} for #{n} bytes"
      end
      ret
    end
  end
end

是以OpenSSL::Random 开始的,可以描述为:

OpenSSL不能直接生成真正的随机数。选择是使用一个具有良好随机种子的加密安全PRNG(即用操作系统从有效的随机硬件事件中收获的数据);或者使用一个真正的硬件RNG。[1]

如果OpenSSL不存在,它就会退回到Ruby的伪随机数生成器,该生成器在Random.raw_seed ,它使用一个修改过的Mersenne Twister,周期为2**19937-1。

uuid-ossp扩展实现

查看uuid-ossp 源代码,我们可以发现:

Datum
uuid_generate_v4(PG_FUNCTION_ARGS)
{
  return uuid_generate_internal(UUID_MAKE_V4, NULL, NULL, 0);
}

uuid_generate_internal 的相关部分的功能:

case 4:         /* random uuid */
default:
{
  #ifdef HAVE_UUID_E2FS
    uuid_t      uu;
    uuid_generate_random(uu);
    uuid_unparse(uu, strbuf);
  #endif
   break;
}

我们可以看到有一个对uuid_generate_random 的调用 - 这可能是什么?

uuid_generate_random(3) - Linux man page

它的描述:

uuid_generate_random函数强制使用全随机UUID格式,即使高质量的随机数发生器(即/dev/urandom)不可用,在这种情况下,将用伪随机数发生器代替。注意,使用伪随机生成器可能会损害以这种方式生成的UUID的唯一性。

有趣的是,如果/dev/urandom 不可用--它又回到了PRNG--这里似乎有一个细微的差别,甚至是一个矛盾,因为/dev/urandom 使用csPRNG(加密安全的伪随机数发生器)[3]

结论

这开始是一个简单的好奇心,我可以使用哪些版本的UUID--注意,这不是对真正的随机数与PRGN的批评,因为RFC明确指出。

版本4的UUID是用来从真正的随机数或伪随机数生成UUID的。[0]

这使得两种实现方式都是正确的:

UUID方法在Ruby的SecureRandom中是有意义的,因为它只是版本4的实现,而且默认情况下它使用OpenSSL::Random 。我对此的唯一小抱怨是,在Ruby中没有支持所有版本的通用UUID库 [2]。

关于他们的实现,应该注意到他们的不同,如果随机性的质量很重要,应该进一步调查(例如,由于缺少OpenSSL - Ruby将使用内部的PRNG,乍一看,它似乎相当可靠,因为它用于播种dev/urandom [6],但它可能不在同一级别的csPRNG中)。

对于UUID来说,两种实现都应该产生可用的随机UUID,即使库中有非csPRNG的算法。最后,有一个小问题

在不同主机上生成UUID的分布式应用必须愿意依赖所有主机上的随机数源。如果这不可行,应该使用命名空间的变体。