Implementação do Algoritmo de ID Snowflake em Go

Conceitos Preliminares:

Relógios Monotônicos (Monotonic Clocks) representam um tempo que avança continuamente a partir do início do processo, sendo insensível a ajustes manuais ou sincronizações de rede. Este tipo de temporização é crucial para a precisão na geração de identificadores.

Referência: Understand Time Struct in Go

O Snowflake é um algoritmo aberto pelo Twitter para a geração de IDs únicos em ambientes distribuídos.

Análise do Algoritmo Snowflake do Twitter

A estrutura padrão de um ID Snowflake é um inteiro de 64 bits, dividido da seguinte forma:

+--------------------------------------------------------------------------+
| 1 Bit Não Usado | 41 Bit Timestamp |  10 Bit NodeID  |   12 Bit Sequence ID |
+--------------------------------------------------------------------------+
  • O primeiro bit é reservado e padrão 0.
  • 41 bits armazenam um timestamp em milissegundos.
  • 10 bits identificam o nó (máx. 1024 nós).
  • 12 bits formam uma sequência incremental (máx. 4096 IDs por nó, por milissegundo).

Com essa configuração, uma única máquina pode teoricamente gerar aproximadamente 4 milhões de IDs por segundo (4096 * 1000).

Exemplo de Uso

package main

import (
    "fmt"
    "github.com/bwmarrin/snowflake"
)

func main() {
    // Inicializa um gerador com o número do nó igual a 1.
    generator, err := snowflake.NewNode(1)
    if err != nil {
        panic(err)
    }

    // Gera um novo ID único.
    uniqueID := generator.Generate()

    // Apresenta o ID em diferentes representações.
    fmt.Printf("ID Inteiro  : %d\n", uniqueID)
    fmt.Printf("ID String   : %s\n", uniqueID)
    fmt.Printf("ID Binário  : %s\n", uniqueID.Base2())
    fmt.Printf("ID Base64   : %s\n", uniqueID.Base64())

    // Extrai as informações componentes do ID.
    fmt.Printf("Timestamp   : %d\n", uniqueID.Time())
    fmt.Printf("Nó          : %d\n", uniqueID.Node())
    fmt.Printf("Sequência   : %d\n", uniqueID.Step())
}

Saída esperada (exemplo):

ID Inteiro  : 1283328784765816832
ID String   : 1283328784765816832
ID Binário  : 1000111001111010011001011101011111010000000000001000000000000
ID Base64   : MTI4MzMyODc4NDc2NTgxNjgzMg==
Timestamp   : 1594804400041
Nó          : 1
Sequência   : 0

Formato Binário do ID Gerado

0 00100011100111101001100101110101111101000 0000000001 000000000000

Detalhes da Implementação em Go

Utilizando o pacote github.com/bwmarrin/snowflake:

var(
    Epoch    int64 = 1288834974657 // Epoch base em milissegundos
    NodeBits uint8 = 10           // Bits dedicados ao identificador do nó
    StepBits uint8 = 12           // Bits dedicados à sequência incremental
)

A estrutrua principal que gerencia a geração dos IDs é a Node:

type Node struct {
    mu    sync.Mutex
    epoch time.Time
    time  int64
    node  int64
    step  int64
    // Máscaras e deslocamentos calculados a partir das configurações
    nodeMax   int64
    nodeMask  int64
    stepMask  int64
    timeShift uint8
    nodeShift uint8
}

A função construtora NewNode inicializa a estrutura:

func NewNode(node int64) (*Node, error) {
    // Recalcula as máscaras e deslocamentos com base nas constantes
    mu.Lock()
    nodeMax = -1 ^ (-1 << NodeBits)
    nodeMask = nodeMax << StepBits
    stepMask = -1 ^ (-1 << StepBits)
    timeShift = NodeBits + StepBits
    nodeShift = StepBits
    mu.Unlock()

    n := Node{}
    n.node = node
    // Configura os valores calculados na instância
    n.nodeMax = -1 ^ (-1 << NodeBits)
    n.nodeMask = n.nodeMax << StepBits
    n.stepMask = -1 ^ (-1 << StepBits)
    n.timeShift = NodeBits + StepBits
    n.nodeShift = StepBits

    // Valida o ID do nó fornecido
    if n.node < 0 || n.node > n.nodeMax {
        return nil, errors.New("O número do nó deve estar entre 0 e " + strconv.FormatInt(n.nodeMax, 10))
    }

    // Calcula o offset do epoch usando monotonic time
    var now = time.Now()
    n.epoch = now.Add(time.Unix(Epoch/1000, (Epoch%1000)*1000000).Sub(now))

    return &n, nil
}

O método Generate é o coração da geração:

func (n *Node) Generate() ID {
    n.mu.Lock()

    // Obtém o tempo decorrido desde o epoch em milissegundos (monotônico)
    currentTime := time.Since(n.epoch).Nanoseconds() / 1000000

    if currentTime == n.time {
        // Mesmo milissegundo: incrementa a sequência
        n.step = (n.step + 1) & n.stepMask
        if n.step == 0 {
            // Sequência esgotou: espera até o próximo milissegundo
            for currentTime <= n.time {
                currentTime = time.Since(n.epoch).Nanoseconds() / 1000000
            }
        }
    } else {
        // Novo milissegundo: reinicia a sequência
        n.step = 0
    }

    // Atualiza o timestamp armazenado
    n.time = currentTime

    // Compõe o ID final através de operações de deslocamento e OR
    generatedID := ID((currentTime << n.timeShift) | (n.node << n.nodeShift) | n.step)

    n.mu.Unlock()
    return generatedID
}

Vantagens e Desvantagens do Algoritmo Original

O Snowflake depende fortemente do relógio do sistema. Alterações repentinas de tempo (clock skew) podem levar à geração de IDs duplicados. Estratégias comuns para mitigar isso incluem: desativar a sincronização de tempo (NTP) na máquina geradora; usar servidores NTP confiáveis (como os da Alibaba Cloud, que absorvem gradualmente "leap seconds"); ou implementar uma verificação que, ao detectar um atraso pequeno (e.g., 5ms), aguarde um momento antes de gerar o próximo ID.

Algoritmo Snowflake da Sony (Sonyflaek)

A Sony propôs uma variante com distribuição de bits diferente:

+--------------------------------------------------------------------------+
| 1 Bit Não Usado | 39 Bit Timestamp | 8 Bit Sequência |   16 Bit ID Máquina |
+--------------------------------------------------------------------------+
  • 39 bits para um timestamp com granularidade de 10 milissegundos.
  • 8 bits para a sequência (máx. 521 IDs por nó, por unidade de 10ms).
  • 16 bits para o identificador da máquina ou thread (máx. 65.536).

Exemplo de Uso

func obterIDMaquina() (uint16, error) {
    return uint16(1), nil // ID fixo para exemplo
}

func validarIDMaquina(id uint16) bool {
    return true // Validação simplificada
}

func main() {
    cfg := sonyflake.Settings{
        StartTime:      time.Unix(0, 0),
        MachineID:      obterIDMaquina,
        CheckMachineID: validarIDMaquina,
    }

    sf := sonyflake.NewSonyflake(cfg)
    if sf == nil {
        panic("Falha ao inicializar o Sonyflake")
    }

    id, err := sf.NextID()
    if err != nil {
        panic(err)
    }
    fmt.Printf("ID Gerado: %d\n", id)
}

Implementação Interna do Sonyflake

A estrutura de configuração e o gerador:

type Settings struct {
    StartTime      time.Time
    MachineID      func() (uint16, error)
    CheckMachineID func(uint16) bool
}

type Sonyflake struct {
    mutex       *sync.Mutex
    startTime   int64
    elapsedTime int64 // Tempo da última geração
    sequence    uint16
    machineID   uint16
}

A função NewSonyflake inicializa o gerador:

func NewSonyflake(st Settings) *Sonyflake {
    sf := new(Sonyflake)
    sf.mutex = new(sync.Mutex)
    sf.sequence = uint16(1<<bitlensequence a="" configura="" converte="" da="" de="" do="" e="" else="" err="" error="" func="" id="" if="" in="" inicial="" int64="" m="" nil="" o="" obt="" para="" return="" sequ="" sf="" sf.machineid="" sf.starttime="toSonyflakeTime(time.Date(2014," sonyflake="" sonyflaketimeunit="" st.machineid="=" st.starttime.after="" st.starttime.iszero="" t.utc="" tempo="" time.time="" time.utc="" tosonyflaketime="" um="" unidade="" valida="" var=""></bitlensequence>

A geração do ID com tratmaento de clock skew:

func (sf *Sonyflake) NextID() (uint64, error) {
    const maskSequence = uint16(1<<bitlensequence :="currentElapsedTime(sf.startTime)" a="" alcan="" anterior="" avan="" calcula="" clock="" comp="" contra="" currentelapsed="" da="" de="" decorrido="" defer="defer" dentro="" dorme="" else="" em="" error="" estourou="" final="" func="" id="" if="" mant="" masksequence="" mesma="" necess="" normalmente:="" nova="" o="" overtime="" para="" pelo="" reseta="" return="" sequ="" sf.elapsedtime="" sf.mutex.lock="" sf.mutex.unlock="" sf.sequence="0" sf.toid="" skew="" tempo="" time.sleep="" toid="" total="" unidade="" unidades="">= 1<<bitlentime de="" errors.new="" excedido="" nil="" return="" tempo="" uint64=""></bitlentime></bitlensequence>

O Sonyflake lida com a regressão de relógio (clock skew) de forma direta: a variável elapsedTime nunca diminui. Se o relógio do sistema retroceder, o algoritmo continua usando o último elapsedTime válido, incrementando a sequência. Quando a sequência estoura, elapsedTime é incrementado artificialmente e a execução dorme para sincronizar, evitando colisões de ID.

Tags: Snowflake Sonyflake go IDs Distribuídos Algoritmo de Geração de IDs

Publicado em 6-19 02:26