Snowflake ID Generator voor Twitter en Gedistribueerde Systemen
Genereer en analyseer Twitter Snowflake ID's, unieke 64-bits identificatoren die worden gebruikt in gedistribueerde systemen. Deze tool stelt je in staat om nieuwe Snowflake ID's te maken en bestaande ID's te parseren, en biedt inzicht in hun tijdstempel, machine-ID en volgnummer componenten.
Snowflake ID Generator
Snowflake ID Generator
Documentatie
Snowflake ID Generator
Introductie
Een Snowflake ID is een unieke identificator die wordt gebruikt in gedistribueerde systemen, oorspronkelijk ontwikkeld door Twitter. Deze tool stelt je in staat om Snowflake IDs te genereren en te analyseren, die 64-bits gehele getallen zijn, samengesteld uit een tijdstempel, machine-ID en volgnummer.
Hoe Snowflake IDs Werken
Snowflake IDs zijn 64-bits gehele getallen die als volgt zijn gestructureerd:
- 41 bits: Tijdstempel (milliseconden sinds een aangepaste epoch)
- 10 bits: Machine-ID (5 bits voor datacenter-ID, 5 bits voor worker-ID)
- 12 bits: Volgnummer
Deze structuur maakt het mogelijk om ongeveer 4.096 unieke IDs per milliseconde per machine te genereren.
Gebruik van de Snowflake ID Generator
- (Optioneel) Stel een aangepaste epoch in (standaard is de epoch van Twitter: 2010-11-04T01:42:54.657Z)
- Voer een machine-ID (0-31) en datacenter-ID (0-31) in
- Klik op "Genereren" om een nieuwe Snowflake ID te maken
- De gegenereerde ID en zijn componenten worden weergegeven
Om een bestaande Snowflake ID te parseren, voer deze in het veld "Parse ID" in en klik op "Parse".
Formule
De Snowflake ID wordt geconstrueerd met behulp van bitwise-operaties:
1ID = (tijdstempel << 22) | (datacenterId << 17) | (workerId << 12) | volgnummer
2
Waarbij:
tijdstempel
het aantal milliseconden sinds de epoch isdatacenterId
een 5-bits geheel getal is (0-31)workerId
een 5-bits geheel getal is (0-31)volgnummer
een 12-bits geheel getal is (0-4095)
Berekening
De Snowflake ID-generator voert de volgende stappen uit:
- Verkrijg de huidige tijdstempel in milliseconden
- Zorg ervoor dat de tijdstempel groter is dan de laatst gebruikte tijdstempel (voor uniciteit)
- Als de tijdstempel hetzelfde is als de laatste, verhoog het volgnummer
- Als het volgnummer overloopt (4096 bereikt), wacht dan op de volgende milliseconde
- Combineer de componenten met behulp van bitwise-operaties om de uiteindelijke ID te creëren
Toepassingen
Snowflake IDs zijn bijzonder nuttig in:
- Gedistribueerde Systemen: Genereer unieke IDs over meerdere machines zonder coördinatie
- Hoge Volumes Gegevens: Maak sorteerebare IDs voor grote datasets
- Microservices: Zorg voor unieke identificatoren tussen verschillende services
- Database Sharding: Gebruik de tijdstempel of machine-ID-component voor efficiënte sharding
Alternatieven
Hoewel Snowflake IDs krachtig zijn, omvatten andere ID-generatiesystemen:
- UUID (Universally Unique Identifier): Nuttig wanneer gedistribueerde generatie nodig is zonder sorteervolgorde
- Auto-incrementerende database-ID's: Eenvoudig maar beperkt tot enkele database-instanties
- ULID (Universally Unique Lexicographically Sortable Identifier): Vergelijkbaar met Snowflake, maar met een andere structuur
Randgevallen en Beperkingen
-
Kloksynchronisatie: Snowflake IDs zijn afhankelijk van de systeemtijd. Als de klok terugloopt door NTP-aanpassingen of wijzigingen in de zomertijd, kan dit problemen veroorzaken bij de ID-generatie.
-
Jaar 2038 Probleem: De 41-bits tijdstempel zal overlopen in 2079 (ervan uitgaande dat de Twitter-epoch wordt gebruikt). Systemen die Snowflake IDs gebruiken, moeten voor deze eventualiteit plannen.
-
Machine-ID Botsingen: In grote gedistribueerde systemen kan het een uitdaging zijn om unieke machine-ID's te waarborgen en kan extra coördinatie vereisen.
-
Volgnummer Overloop: In extreem hoge doorvoerscenario's is het mogelijk om de 4096 volgnummers per milliseconde uit te putten, wat mogelijk vertragingen veroorzaakt.
-
Niet-monotonie Tussen Machines: Hoewel IDs monotonisch toenemen op een enkele machine, zijn ze mogelijk niet strikt monotonisch tussen meerdere machines.
Geschiedenis
Snowflake IDs werden in 2010 door Twitter geïntroduceerd om te voldoen aan de behoefte aan gedistribueerde, tijd-sortable unieke identificatoren. Ze zijn sindsdien door veel andere bedrijven en projecten overgenomen en aangepast.
Voorbeelden
Hier zijn implementaties van Snowflake ID-generators in verschillende talen:
1class SnowflakeGenerator {
2 constructor(epoch = 1288834974657, datacenterIdBits = 5, workerIdBits = 5, sequenceBits = 12) {
3 this.epoch = BigInt(epoch);
4 this.datacenterIdBits = datacenterIdBits;
5 this.workerIdBits = workerIdBits;
6 this.sequenceBits = sequenceBits;
7 this.maxDatacenterId = -1n ^ (-1n << BigInt(datacenterIdBits));
8 this.maxWorkerId = -1n ^ (-1n << BigInt(workerIdBits));
9 this.sequenceMask = -1n ^ (-1n << BigInt(sequenceBits));
10 this.workerIdShift = BigInt(sequenceBits);
11 this.datacenterIdShift = BigInt(sequenceBits + workerIdBits);
12 this.timestampLeftShift = BigInt(sequenceBits + workerIdBits + datacenterIdBits);
13 this.sequence = 0n;
14 this.lastTimestamp = -1n;
15 }
16
17 nextId(datacenterId, workerId) {
18 let timestamp = this.currentTimestamp();
19
20 if (timestamp < this.lastTimestamp) {
21 throw new Error('Klok is teruggelopen. Weigeren om id te genereren');
22 }
23
24 if (timestamp === this.lastTimestamp) {
25 this.sequence = (this.sequence + 1n) & this.sequenceMask;
26 if (this.sequence === 0n) {
27 timestamp = this.tilNextMillis(this.lastTimestamp);
28 }
29 } else {
30 this.sequence = 0n;
31 }
32
33 this.lastTimestamp = timestamp;
34
35 return ((timestamp - this.epoch) << this.timestampLeftShift) |
36 (BigInt(datacenterId) << this.datacenterIdShift) |
37 (BigInt(workerId) << this.workerIdShift) |
38 this.sequence;
39 }
40
41 tilNextMillis(lastTimestamp) {
42 let timestamp = this.currentTimestamp();
43 while (timestamp <= lastTimestamp) {
44 timestamp = this.currentTimestamp();
45 }
46 return timestamp;
47 }
48
49 currentTimestamp() {
50 return BigInt(Date.now());
51 }
52}
53
54// Gebruik
55const generator = new SnowflakeGenerator();
56const id = generator.nextId(1, 1);
57console.log(`Gegenereerde Snowflake ID: ${id}`);
58
1import time
2import threading
3
4class SnowflakeGenerator:
5 def __init__(self, datacenter_id, worker_id, sequence=0):
6 self.datacenter_id = datacenter_id
7 self.worker_id = worker_id
8 self.sequence = sequence
9
10 self.last_timestamp = -1
11 self.epoch = 1288834974657
12
13 self.datacenter_id_bits = 5
14 self.worker_id_bits = 5
15 self.sequence_bits = 12
16
17 self.max_datacenter_id = -1 ^ (-1 << self.datacenter_id_bits)
18 self.max_worker_id = -1 ^ (-1 << self.worker_id_bits)
19
20 self.worker_id_shift = self.sequence_bits
21 self.datacenter_id_shift = self.sequence_bits + self.worker_id_bits
22 self.timestamp_left_shift = self.sequence_bits + self.worker_id_bits + self.datacenter_id_bits
23 self.sequence_mask = -1 ^ (-1 << self.sequence_bits)
24
25 self._lock = threading.Lock()
26
27 def _til_next_millis(self, last_timestamp):
28 timestamp = self._get_timestamp()
29 while timestamp <= last_timestamp:
30 timestamp = self._get_timestamp()
31 return timestamp
32
33 def _get_timestamp(self):
34 return int(time.time() * 1000)
35
36 def next_id(self):
37 with self._lock:
38 timestamp = self._get_timestamp()
39
40 if timestamp < self.last_timestamp:
41 raise ValueError("Klok is teruggelopen. Weigeren om id te genereren")
42
43 if timestamp == self.last_timestamp:
44 self.sequence = (self.sequence + 1) & self.sequence_mask
45 if self.sequence == 0:
46 timestamp = self._til_next_millis(self.last_timestamp)
47 else:
48 self.sequence = 0
49
50 self.last_timestamp = timestamp
51
52 return ((timestamp - self.epoch) << self.timestamp_left_shift) | \
53 (self.datacenter_id << self.datacenter_id_shift) | \
54 (self.worker_id << self.worker_id_shift) | \
55 self.sequence
56
57## Gebruik
58generator = SnowflakeGenerator(datacenter_id=1, worker_id=1)
59snowflake_id = generator.next_id()
60print(f"Gegenereerde Snowflake ID: {snowflake_id}")
61
1import java.util.concurrent.locks.Lock;
2import java.util.concurrent.locks.ReentrantLock;
3
4public class SnowflakeGenerator {
5 private final long epoch;
6 private final long datacenterIdBits;
7 private final long workerIdBits;
8 private final long sequenceBits;
9 private final long maxDatacenterId;
10 private final long maxWorkerId;
11 private final long workerIdShift;
12 private final long datacenterIdShift;
13 private final long timestampLeftShift;
14 private final long sequenceMask;
15
16 private long datacenterId;
17 private long workerId;
18 private long sequence = 0L;
19 private long lastTimestamp = -1L;
20
21 private final Lock lock = new ReentrantLock();
22
23 public SnowflakeGenerator(long datacenterId, long workerId) {
24 this.epoch = 1288834974657L;
25 this.datacenterIdBits = 5L;
26 this.workerIdBits = 5L;
27 this.sequenceBits = 12L;
28
29 this.maxDatacenterId = ~(-1L << datacenterIdBits);
30 this.maxWorkerId = ~(-1L << workerIdBits);
31
32 this.workerIdShift = sequenceBits;
33 this.datacenterIdShift = sequenceBits + workerIdBits;
34 this.timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
35 this.sequenceMask = ~(-1L << sequenceBits);
36
37 if (datacenterId > maxDatacenterId || datacenterId < 0) {
38 throw new IllegalArgumentException("datacenterId kan niet groter zijn dan maxDatacenterId of kleiner dan 0");
39 }
40 if (workerId > maxWorkerId || workerId < 0) {
41 throw new IllegalArgumentException("workerId kan niet groter zijn dan maxWorkerId of kleiner dan 0");
42 }
43 this.datacenterId = datacenterId;
44 this.workerId = workerId;
45 }
46
47 public long nextId() {
48 lock.lock();
49 try {
50 long timestamp = timeGen();
51 if (timestamp < lastTimestamp) {
52 throw new RuntimeException("Klok is teruggelopen. Weigeren om id te genereren");
53 }
54
55 if (lastTimestamp == timestamp) {
56 sequence = (sequence + 1) & sequenceMask;
57 if (sequence == 0) {
58 timestamp = tilNextMillis(lastTimestamp);
59 }
60 } else {
61 sequence = 0L;
62 }
63
64 lastTimestamp = timestamp;
65
66 return ((timestamp - epoch) << timestampLeftShift) |
67 (datacenterId << datacenterIdShift) |
68 (workerId << workerIdShift) |
69 sequence;
70 } finally {
71 lock.unlock();
72 }
73 }
74
75 private long tilNextMillis(long lastTimestamp) {
76 long timestamp = timeGen();
77 while (timestamp <= lastTimestamp) {
78 timestamp = timeGen();
79 }
80 return timestamp;
81 }
82
83 private long timeGen() {
84 return System.currentTimeMillis();
85 }
86
87 public static void main(String[] args) {
88 SnowflakeGenerator generator = new SnowflakeGenerator(1, 1);
89 long id = generator.nextId();
90 System.out.println("Gegenereerde Snowflake ID: " + id);
91 }
92}
93
1require 'time'
2
3class SnowflakeGenerator
4 def initialize(datacenter_id, worker_id, sequence = 0)
5 @datacenter_id = datacenter_id
6 @worker_id = worker_id
7 @sequence = sequence
8 @last_timestamp = -1
9 @epoch = 1288834974657
10
11 @datacenter_id_bits = 5
12 @worker_id_bits = 5
13 @sequence_bits = 12
14
15 @max_datacenter_id = -1 ^ (-1 << @datacenter_id_bits)
16 @max_worker_id = -1 ^ (-1 << @worker_id_bits)
17
18 @worker_id_shift = @sequence_bits
19 @datacenter_id_shift = @sequence_bits + @worker_id_bits
20 @timestamp_left_shift = @sequence_bits + @worker_id_bits + @datacenter_id_bits
21 @sequence_mask = -1 ^ (-1 << @sequence_bits)
22 end
23
24 def next_id
25 timestamp = (Time.now.to_f * 1000).to_i
26
27 raise 'Klok is teruggelopen' if timestamp < @last_timestamp
28
29 if timestamp == @last_timestamp
30 @sequence = (@sequence + 1) & @sequence_mask
31 timestamp = til_next_millis(@last_timestamp) if @sequence == 0
32 else
33 @sequence = 0
34 end
35
36 @last_timestamp = timestamp
37
38 ((timestamp - @epoch) << @timestamp_left_shift) |
39 (@datacenter_id << @datacenter_id_shift) |
40 (@worker_id << @worker_id_shift) |
41 @sequence
42 end
43
44 private
45
46 def til_next_millis(last_timestamp)
47 timestamp = (Time.now.to_f * 1000).to_i
48 timestamp = (Time.now.to_f * 1000).to_i while timestamp <= last_timestamp
49 timestamp
50 end
51end
52
53## Gebruik
54generator = SnowflakeGenerator.new(1, 1)
55snowflake_id = generator.next_id
56puts "Gegenereerde Snowflake ID: #{snowflake_id}"
57
1<?php
2
3class SnowflakeGenerator {
4 private $epoch;
5 private $datacenterIdBits;
6 private $workerIdBits;
7 private $sequenceBits;
8 private $maxDatacenterId;
9 private $maxWorkerId;
10 private $workerIdShift;
11 private $datacenterIdShift;
12 private $timestampLeftShift;
13 private $sequenceMask;
14
15 private $datacenterId;
16 private $workerId;
17 private $sequence = 0;
18 private $lastTimestamp = -1;
19
20 public function __construct($datacenterId, $workerId) {
21 $this->epoch = 1288834974657;
22 $this->datacenterIdBits = 5;
23 $this->workerIdBits = 5;
24 $this->sequenceBits = 12;
25
26 $this->maxDatacenterId = -1 ^ (-1 << $this->datacenterIdBits);
27 $this->maxWorkerId = -1 ^ (-1 << $this->workerIdBits);
28
29 $this->workerIdShift = $this->sequenceBits;
30 $this->datacenterIdShift = $this->sequenceBits + $this->workerIdBits;
31 $this->timestampLeftShift = $this->sequenceBits + $this->workerIdBits + $this->datacenterIdBits;
32 $this->sequenceMask = -1 ^ (-1 << $this->sequenceBits);
33
34 if ($datacenterId > $this->maxDatacenterId || $datacenterId < 0) {
35 throw new Exception("datacenterId kan niet groter zijn dan maxDatacenterId of kleiner dan 0");
36 }
37 if ($workerId > $this->maxWorkerId || $workerId < 0) {
38 throw new Exception("workerId kan niet groter zijn dan maxWorkerId of kleiner dan 0");
39 }
40 $this->datacenterId = $datacenterId;
41 $this->workerId = $workerId;
42 }
43
44 public function nextId() {
45 $timestamp = $this->timeGen();
46
47 if ($timestamp < $this->lastTimestamp) {
48 throw new Exception("Klok is teruggelopen. Weigeren om id te genereren");
49 }
50
51 if ($this->lastTimestamp == $timestamp) {
52 $this->sequence = ($this->sequence + 1) & $this->sequenceMask;
53 if ($this->sequence == 0) {
54 $timestamp = $this->tilNextMillis($this->lastTimestamp);
55 }
56 } else {
57 $this->sequence = 0;
58 }
59
60 $this->lastTimestamp = $timestamp;
61
62 return (($timestamp - $this->epoch) << $this->timestampLeftShift) |
63 ($this->datacenterId << $this->datacenterIdShift) |
64 ($this->workerId << $this->workerIdShift) |
65 $this->sequence;
66 }
67
68 private function tilNextMillis($lastTimestamp) {
69 $timestamp = $this->timeGen();
70 while ($timestamp <= $lastTimestamp) {
71 $timestamp = $this->timeGen();
72 }
73 return $timestamp;
74 }
75
76 private function timeGen() {
77 return floor(microtime(true) * 1000);
78 }
79}
80
81// Gebruik
82$generator = new SnowflakeGenerator(1, 1);
83$id = $generator->nextId();
84echo "Gegenereerde Snowflake ID: " . $id . "\n";
85
1using System;
2using System.Threading;
3
4public class SnowflakeGenerator
5{
6 private readonly long _epoch;
7 private readonly int _datacenterIdBits;
8 private readonly int _workerIdBits;
9 private readonly int _sequenceBits;
10 private readonly long _maxDatacenterId;
11 private readonly long _maxWorkerId;
12 private long _workerId;
13 private long _datacenterId;
14 private long _sequence = 0L;
15 private long _lastTimestamp = -1L;
16
17 private readonly object _lock = new object();
18
19 public SnowflakeGenerator(long datacenterId, long workerId)
20 {
21 _epoch = 1288834974657L;
22 _datacenterIdBits = 5;
23 _workerIdBits = 5;
24 _sequenceBits = 12;
25
26 _maxDatacenterId = -1L ^ (-1L << _datacenterIdBits);
27 _maxWorkerId = -1L ^ (-1L << _workerIdBits);
28
29 _workerIdShift = _sequenceBits;
30 _datacenterIdShift = _sequenceBits + _workerIdBits;
31 _timestampLeftShift = _sequenceBits + _workerIdBits + _datacenterIdBits;
32 _sequenceMask = -1L ^ (-1L << _sequenceBits);
33
34 if (datacenterId > _maxDatacenterId || datacenterId < 0)
35 {
36 throw new ArgumentException($"datacenterId kan niet groter zijn dan {_maxDatacenterId} of kleiner dan 0");
37 }
38 if (workerId > _maxWorkerId || workerId < 0)
39 {
40 throw new ArgumentException($"workerId kan niet groter zijn dan {_maxWorkerId} of kleiner dan 0");
41 }
42 _datacenterId = datacenterId;
43 _workerId = workerId;
44 }
45
46 public long NextId()
47 {
48 lock (_lock)
49 {
50 var timestamp = TimeGen();
51
52 if (timestamp < _lastTimestamp)
53 {
54 throw new Exception("Klok is teruggelopen. Weigeren om id te genereren");
55 }
56
57 if (_lastTimestamp == timestamp)
58 {
59 _sequence = (_sequence + 1) & _sequenceMask;
60 if (_sequence == 0)
61 {
62 timestamp = TilNextMillis(_lastTimestamp);
63 }
64 }
65 else
66 {
67 _sequence = 0L;
68 }
69
70 _lastTimestamp = timestamp;
71
72 return ((timestamp - _epoch) << _timestampLeftShift) |
73 (_datacenterId << _datacenterIdShift) |
74 (_workerId << _workerIdShift) |
75 _sequence;
76 }
77 }
78
79 private long TilNextMillis(long lastTimestamp)
80 {
81 var timestamp = TimeGen();
82 while (timestamp <= lastTimestamp)
83 {
84 timestamp = TimeGen();
85 }
86 return timestamp;
87 }
88
89 private long TimeGen()
90 {
91 return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
92 }
93}
94
95// Gebruik
96class Program
97{
98 static void Main(string[] args)
99 {
100 var generator = new SnowflakeGenerator(1, 1);
101 var id = generator.NextId();
102 Console.WriteLine($"Gegenereerde Snowflake ID: {id}");
103 }
104}
105
1package main
2
3import (
4 "fmt"
5 "sync"
6 "time"
7)
8
9type SnowflakeGenerator struct {
10 epoch int64
11 datacenterIdBits uint
12 workerIdBits uint
13 sequenceBits uint
14 maxDatacenterId int64
15 maxWorkerId int64
16 workerIdShift uint
17 datacenterIdShift uint
18 timestampLeftShift uint
19 sequenceMask int64
20
21 datacenterId int64
22 workerId int64
23 sequence int64
24 lastTimestamp int64
25
26 lock sync.Mutex
27}
28
29func NewSnowflakeGenerator(datacenterId, workerId int64) (*SnowflakeGenerator, error) {
30 g := &SnowflakeGenerator{
31 epoch: 1288834974657,
32 datacenterIdBits: 5,
33 workerIdBits: 5,
34 sequenceBits: 12,
35 lastTimestamp: -1,
36 }
37
38 g.maxDatacenterId = -1 ^ (-1 << g.datacenterIdBits)
39 g.maxWorkerId = -1 ^ (-1 << g.workerIdBits)
40
41 g.workerIdShift = g.sequenceBits
42 g.datacenterIdShift = g.sequenceBits + g.workerIdBits
43 g.timestampLeftShift = g.sequenceBits + g.workerIdBits + g.datacenterIdBits
44 g.sequenceMask = -1 ^ (-1 << g.sequenceBits)
45
46 if datacenterId > g.maxDatacenterId || datacenterId < 0 {
47 return nil, fmt.Errorf("datacenterId kan niet groter zijn dan %d of kleiner dan 0", g.maxDatacenterId)
48 }
49 if workerId > g.maxWorkerId || workerId < 0 {
50 return nil, fmt.Errorf("workerId kan niet groter zijn dan %d of kleiner dan 0", g.maxWorkerId)
51 }
52 g.datacenterId = datacenterId
53 g.workerId = workerId
54
55 return g, nil
56}
57
58func (g *SnowflakeGenerator) NextId() (int64, error) {
59 g.lock.Lock()
60 defer g.lock.Unlock()
61
62 timestamp := g.timeGen()
63
64 if timestamp < g.lastTimestamp {
65 return 0, fmt.Errorf("klok is teruggelopen, weigeren om id te genereren")
66 }
67
68 if g.lastTimestamp == timestamp {
69 g.sequence = (g.sequence + 1) & g.sequenceMask
70 if g.sequence == 0 {
71 timestamp = g.tilNextMillis(g.lastTimestamp)
72 }
73 } else {
74 g.sequence = 0
75 }
76
77 g.lastTimestamp = timestamp
78
79 return ((timestamp - g.epoch) << g.timestampLeftShift) |
80 (g.datacenterId << g.datacenterIdShift) |
81 (g.workerId << g.workerIdShift) |
82 g.sequence, nil
83}
84
85func (g *SnowflakeGenerator) tilNextMillis(lastTimestamp int64) int64 {
86 timestamp := g.timeGen()
87 for timestamp <= lastTimestamp {
88 timestamp = g.timeGen()
89 }
90 return timestamp
91}
92
93func (g *SnowflakeGenerator) timeGen() int64 {
94 return time.Now().UnixNano() / int64(time.Millisecond)
95}
96
97func main() {
98 generator, err := NewSnowflakeGenerator(1, 1)
99 if err != nil {
100 fmt.Printf("Fout bij het maken van de generator: %v\n", err)
101 return
102 }
103
104 id, err := generator.NextId()
105 if err != nil {
106 fmt.Printf("Fout bij het genereren van ID: %v\n", err)
107 return
108 }
109
110 fmt.Printf("Gegenereerde Snowflake ID: %d\n", id)
111}
112
Diagram
Hier is een visuele weergave van de structuur van de Snowflake ID:
Referenties
- "Aankondiging Snowflake." Twitter Engineering Blog, https://blog.twitter.com/engineering/en_us/a/2010/announcing-snowflake
- "Snowflake ID." Wikipedia, https://en.wikipedia.org/wiki/Snowflake_ID
- "Gedistrubueerde ID-generatie in Microservices." Medium, https://medium.com/swlh/distributed-id-generation-in-microservices-b6ce9a8dd93f
Feedback
Klik op de feedback toast om feedback te geven over deze tool
Gerelateerde Tools
Ontdek meer tools die nuttig kunnen zijn voor jouw workflow