Skip to content

Commit c4b2452

Browse files
committed
Optimizations for cache: use xxHash3 instead of Blake3b for cache key hashing, avoid some memory allocations and copies
1 parent 30ffb2a commit c4b2452

4 files changed

Lines changed: 78 additions & 69 deletions

File tree

pingora-cache/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ http = { workspace = true }
2929
indexmap = "1"
3030
once_cell = { workspace = true }
3131
regex = "1"
32-
blake2 = "0.10"
32+
xxhash-rust = { version = "0.8", features = ["xxh3"] }
3333
serde = { version = "1.0", features = ["derive"] }
3434
rmp-serde = "1.3.0"
3535
bytes = { workspace = true }

pingora-cache/src/key.rs

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
1717
use super::*;
1818

19-
use blake2::{Blake2b, Digest};
2019
use http::Extensions;
2120
use serde::{Deserialize, Serialize};
2221
use std::fmt::{Display, Formatter, Result as FmtResult};
22+
use xxhash_rust::xxh3::Xxh3;
2323

2424
// 16-byte / 128-bit key: large enough to avoid collision
2525
const KEY_SIZE: usize = 16;
@@ -63,10 +63,11 @@ pub trait CacheHashKey {
6363
fn combined_bin(&self) -> HashBinary {
6464
let key = self.primary_bin();
6565
if let Some(v) = self.variance_bin() {
66-
let mut hasher = Blake2b128::new();
67-
hasher.update(key);
68-
hasher.update(v);
69-
hasher.finalize().into()
66+
let mut hasher = Xxh3::new();
67+
hasher.update(&key);
68+
hasher.update(&v);
69+
let hash = hasher.digest128();
70+
hash.to_le_bytes()
7071
} else {
7172
// if there is no variance, combined_bin should return the same as primary_bin
7273
key
@@ -180,38 +181,37 @@ impl CacheHashKey for CompactCacheKey {
180181
}
181182

182183
/*
183-
* We use blake2 hashing, which is faster and more secure, to replace md5.
184-
* We have not given too much thought on whether non-crypto hash can be safely
185-
* use because hashing performance is not critical.
186-
* Note: we should avoid hashes like ahash which does not have consistent output
187-
* across machines because it is designed purely for in memory hashtable
188-
*/
189-
190-
// hash output: we use 128 bits (16 bytes) hash which will map to 32 bytes hex string
191-
pub(crate) type Blake2b128 = Blake2b<blake2::digest::consts::U16>;
184+
* We use xxHash3, which is a fast non-cryptographic hash function.
185+
* Cache keys don't require cryptographic security, and xxHash3 is ~10x faster
186+
* than Blake2 while providing excellent collision resistance for cache use cases.
187+
* We use the 128-bit variant (xxh3_128) for consistency with the previous Blake2b128.
188+
* Note: we avoid hashes like ahash which don't have consistent output across
189+
* machines because they're designed purely for in-memory hashtables.
190+
*/
192191

193192
/// helper function: hash str to u8
194193
pub fn hash_u8(key: &str) -> u8 {
195-
let mut hasher = Blake2b128::new();
196-
hasher.update(key);
197-
let raw = hasher.finalize();
198-
raw[0]
194+
let mut hasher = Xxh3::new();
195+
hasher.update(key.as_bytes());
196+
let hash = hasher.digest128();
197+
(hash & 0xFF) as u8
199198
}
200199

201200
/// helper function: hash key (String or Bytes) to [HashBinary]
202201
pub fn hash_key<K: AsRef<[u8]>>(key: K) -> HashBinary {
203-
let mut hasher = Blake2b128::new();
202+
let mut hasher = Xxh3::new();
204203
hasher.update(key.as_ref());
205-
let raw = hasher.finalize();
206-
raw.into()
204+
let hash = hasher.digest128();
205+
hash.to_le_bytes()
207206
}
208207

209208
impl CacheKey {
210-
fn primary_hasher(&self) -> Blake2b128 {
211-
let mut hasher = Blake2b128::new();
209+
fn primary_hash(&self) -> HashBinary {
210+
let mut hasher = Xxh3::new();
212211
hasher.update(&self.namespace);
213212
hasher.update(&self.primary);
214-
hasher
213+
let hash = hasher.digest128();
214+
hash.to_le_bytes()
215215
}
216216

217217
/// Create a default [CacheKey] from a request, which just takes its URI as the primary key.
@@ -271,7 +271,7 @@ impl CacheHashKey for CacheKey {
271271
if let Some(primary_bin_override) = self.primary_bin_override {
272272
primary_bin_override
273273
} else {
274-
self.primary_hasher().finalize().into()
274+
self.primary_hash()
275275
}
276276
}
277277

@@ -299,7 +299,7 @@ mod tests {
299299
extensions: Extensions::new(),
300300
};
301301
let hash = key.primary();
302-
assert_eq!(hash, "ac10f2aef117729f8dad056b3059eb7e");
302+
assert_eq!(hash, "3393a146a6429236209bd346d394feb9");
303303
assert!(key.variance().is_none());
304304
assert_eq!(key.combined(), hash);
305305
let compact = key.to_compact();
@@ -350,39 +350,39 @@ mod tests {
350350
extensions: Extensions::new(),
351351
};
352352
let hash = key.primary();
353-
assert_eq!(hash, "ac10f2aef117729f8dad056b3059eb7e");
353+
assert_eq!(hash, "3393a146a6429236209bd346d394feb9");
354354
assert_eq!(key.variance().unwrap(), "00000000000000000000000000000000");
355-
assert_eq!(key.combined(), "004174d3e75a811a5b44c46b3856f3ee");
355+
assert_eq!(key.combined(), "b03c278e7fd4cc0630a352947348e37f");
356356
let compact = key.to_compact();
357-
assert_eq!(compact.primary(), "ac10f2aef117729f8dad056b3059eb7e");
357+
assert_eq!(compact.primary(), "3393a146a6429236209bd346d394feb9");
358358
assert_eq!(
359359
compact.variance().unwrap(),
360360
"00000000000000000000000000000000"
361361
);
362-
assert_eq!(compact.combined(), "004174d3e75a811a5b44c46b3856f3ee");
362+
assert_eq!(compact.combined(), "b03c278e7fd4cc0630a352947348e37f");
363363
}
364364

365365
#[test]
366366
fn test_cache_key_vary_hash_override() {
367367
let key = CacheKey {
368368
namespace: Vec::new(),
369369
primary: b"saaaad".to_vec(),
370-
primary_bin_override: str2hex("ac10f2aef117729f8dad056b3059eb7e"),
370+
primary_bin_override: str2hex("3393a146a6429236209bd346d394feb9"),
371371
variance: Some([0u8; 16]),
372372
user_tag: "1".into(),
373373
extensions: Extensions::new(),
374374
};
375375
let hash = key.primary();
376-
assert_eq!(hash, "ac10f2aef117729f8dad056b3059eb7e");
376+
assert_eq!(hash, "3393a146a6429236209bd346d394feb9");
377377
assert_eq!(key.variance().unwrap(), "00000000000000000000000000000000");
378-
assert_eq!(key.combined(), "004174d3e75a811a5b44c46b3856f3ee");
378+
assert_eq!(key.combined(), "b03c278e7fd4cc0630a352947348e37f");
379379
let compact = key.to_compact();
380-
assert_eq!(compact.primary(), "ac10f2aef117729f8dad056b3059eb7e");
380+
assert_eq!(compact.primary(), "3393a146a6429236209bd346d394feb9");
381381
assert_eq!(
382382
compact.variance().unwrap(),
383383
"00000000000000000000000000000000"
384384
);
385-
assert_eq!(compact.combined(), "004174d3e75a811a5b44c46b3856f3ee");
385+
assert_eq!(compact.combined(), "b03c278e7fd4cc0630a352947348e37f");
386386
}
387387

388388
#[test]

pingora-cache/src/memory.rs

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
//TODO: Mark this module #[test] only
2020

2121
use super::*;
22-
use crate::key::CompactCacheKey;
22+
use crate::key::{CompactCacheKey, HashBinary};
2323
use crate::storage::{streaming_write::U64WriteId, HandleHit, HandleMiss};
2424
use crate::trace::SpanHandle;
2525

@@ -37,7 +37,7 @@ type BinaryMeta = (Vec<u8>, Vec<u8>);
3737

3838
pub(crate) struct CacheObject {
3939
pub meta: BinaryMeta,
40-
pub body: Arc<Vec<u8>>,
40+
pub body: Bytes,
4141
}
4242

4343
pub(crate) struct TempObject {
@@ -56,10 +56,10 @@ impl TempObject {
5656
bytes_written: Arc::new(tx),
5757
}
5858
}
59-
// this is not at all optimized
6059
fn make_cache_object(&self) -> CacheObject {
6160
let meta = self.meta.clone();
62-
let body = Arc::new(self.body.read().clone());
61+
// Convert Vec<u8> to Bytes for zero-copy slicing
62+
let body = Bytes::from(self.body.read().clone());
6363
CacheObject { meta, body }
6464
}
6565
}
@@ -68,8 +68,12 @@ impl TempObject {
6868
///
6969
/// For testing only, not for production use.
7070
pub struct MemCache {
71-
pub(crate) cached: Arc<RwLock<HashMap<String, CacheObject>>>,
72-
pub(crate) temp: Arc<RwLock<HashMap<String, HashMap<u64, TempObject>>>>,
71+
// Use HashBinary ([u8; 16]) keys instead of String for better performance:
72+
// - Avoids hex string conversion (6% CPU savings)
73+
// - Smaller key size (16 bytes vs 32+ bytes)
74+
// - No heap allocation for keys
75+
pub(crate) cached: Arc<RwLock<HashMap<HashBinary, CacheObject>>>,
76+
pub(crate) temp: Arc<RwLock<HashMap<HashBinary, HashMap<u64, TempObject>>>>,
7377
pub(crate) last_temp_id: AtomicU64,
7478
}
7579

@@ -96,7 +100,7 @@ enum PartialState {
96100
}
97101

98102
pub struct CompleteHit {
99-
body: Arc<Vec<u8>>,
103+
body: Bytes,
100104
done: bool,
101105
range_start: usize,
102106
range_end: usize,
@@ -108,9 +112,8 @@ impl CompleteHit {
108112
None
109113
} else {
110114
self.done = true;
111-
Some(Bytes::copy_from_slice(
112-
&self.body.as_slice()[self.range_start..self.range_end],
113-
))
115+
// Zero-copy slice instead of copy_from_slice
116+
Some(self.body.slice(self.range_start..self.range_end))
114117
}
115118
}
116119

@@ -236,12 +239,12 @@ pub struct MemMissHandler {
236239
body: Arc<RwLock<Vec<u8>>>,
237240
bytes_written: Arc<watch::Sender<PartialState>>,
238241
// these are used only in finish() to data from temp to cache
239-
key: String,
242+
key: HashBinary,
240243
temp_id: U64WriteId,
241244
// key -> cache object
242-
cache: Arc<RwLock<HashMap<String, CacheObject>>>,
245+
cache: Arc<RwLock<HashMap<HashBinary, CacheObject>>>,
243246
// key -> (temp writer id -> temp object) to support concurrent writers
244-
temp: Arc<RwLock<HashMap<String, HashMap<u64, TempObject>>>>,
247+
temp: Arc<RwLock<HashMap<HashBinary, HashMap<u64, TempObject>>>>,
245248
}
246249

247250
#[async_trait]
@@ -273,7 +276,8 @@ impl HandleMiss for MemMissHandler {
273276
.unwrap()
274277
.make_cache_object();
275278
let size = cache_object.body.len(); // FIXME: this just body size, also track meta size
276-
self.cache.write().insert(self.key.clone(), cache_object);
279+
280+
self.cache.write().insert(self.key, cache_object);
277281
self.temp
278282
.write()
279283
.get_mut(&self.key)
@@ -313,7 +317,8 @@ impl Storage for MemCache {
313317
key: &CacheKey,
314318
_trace: &SpanHandle,
315319
) -> Result<Option<(CacheMeta, HitHandler)>> {
316-
let hash = key.combined();
320+
// Use combined_bin() to get binary hash directly, avoiding hex string conversion
321+
let hash = key.combined_bin();
317322
// always prefer partial read otherwise fresh asset will not be visible on expired asset
318323
// until it is fully updated
319324
// no preference on which partial read we get (if there are multiple writers)
@@ -345,7 +350,7 @@ impl Storage for MemCache {
345350
streaming_write_tag: Option<&[u8]>,
346351
_trace: &SpanHandle,
347352
) -> Result<Option<(CacheMeta, HitHandler)>> {
348-
let hash = key.combined();
353+
let hash = key.combined_bin();
349354
let write_tag: U64WriteId = streaming_write_tag
350355
.expect("tag must be set during streaming write")
351356
.try_into()
@@ -365,14 +370,15 @@ impl Storage for MemCache {
365370
meta: &CacheMeta,
366371
_trace: &SpanHandle,
367372
) -> Result<MissHandler> {
368-
let hash = key.combined();
373+
let hash = key.combined_bin();
369374
let meta = meta.serialize()?;
370375
let temp_obj = TempObject::new(meta);
371376
let temp_id = self.last_temp_id.fetch_add(1, Ordering::Relaxed);
372377
let miss_handler = MemMissHandler {
373378
body: temp_obj.body.clone(),
374379
bytes_written: temp_obj.bytes_written.clone(),
375-
key: hash.clone(),
380+
381+
key: hash,
376382
cache: self.cached.clone(),
377383
temp: self.temp.clone(),
378384
temp_id: temp_id.into(),
@@ -393,7 +399,7 @@ impl Storage for MemCache {
393399
) -> Result<bool> {
394400
// This usually purges the primary key because, without a lookup, the variance key is usually
395401
// empty
396-
let hash = key.combined();
402+
let hash = key.combined_bin();
397403
let temp_removed = self.temp.write().remove(&hash).is_some();
398404
let cache_removed = self.cached.write().remove(&hash).is_some();
399405
Ok(temp_removed || cache_removed)
@@ -405,7 +411,7 @@ impl Storage for MemCache {
405411
meta: &CacheMeta,
406412
_trace: &SpanHandle,
407413
) -> Result<bool> {
408-
let hash = key.combined();
414+
let hash = key.combined_bin();
409415
if let Some(obj) = self.cached.write().get_mut(&hash) {
410416
obj.meta = meta.serialize()?;
411417
Ok(true)
@@ -598,7 +604,7 @@ mod test {
598604
let cache = &MEM_CACHE;
599605

600606
let key = CacheKey::new("", "a", "1").to_compact();
601-
let hash = key.combined();
607+
let hash = key.combined_bin();
602608
let meta = (
603609
"meta_key".as_bytes().to_vec(),
604610
"meta_value".as_bytes().to_vec(),
@@ -607,7 +613,8 @@ mod test {
607613
let temp_obj = TempObject::new(meta);
608614
let mut map = HashMap::new();
609615
map.insert(0, temp_obj);
610-
cache.temp.write().insert(hash.clone(), map);
616+
617+
cache.temp.write().insert(hash, map);
611618

612619
assert!(cache.temp.read().contains_key(&hash));
613620

@@ -625,17 +632,18 @@ mod test {
625632
let cache = &MEM_CACHE;
626633

627634
let key = CacheKey::new("", "a", "1").to_compact();
628-
let hash = key.combined();
635+
let hash = key.combined_bin();
629636
let meta = (
630637
"meta_key".as_bytes().to_vec(),
631638
"meta_value".as_bytes().to_vec(),
632639
);
633640
let body = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
634641
let cache_obj = CacheObject {
635642
meta,
636-
body: Arc::new(body),
643+
body: Bytes::from(body),
637644
};
638-
cache.cached.write().insert(hash.clone(), cache_obj);
645+
646+
cache.cached.write().insert(hash, cache_obj);
639647

640648
assert!(cache.cached.read().contains_key(&hash));
641649

pingora-cache/src/variance.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use std::{borrow::Cow, collections::BTreeMap};
22

3-
use blake2::Digest;
3+
use xxhash_rust::xxh3::Xxh3;
44

5-
use crate::key::{Blake2b128, HashBinary};
5+
use crate::key::HashBinary;
66

77
/// A builder for variance keys, used for distinguishing multiple cached assets
88
/// at the same URL. This is intended to be easily passed to helper functions,
@@ -44,14 +44,15 @@ impl<'a> VarianceBuilder<'a> {
4444
pub fn finalize(self) -> Option<HashBinary> {
4545
const SALT: &[u8; 1] = &[0u8; 1];
4646
if self.has_variance() {
47-
let mut hash = Blake2b128::new();
47+
let mut hasher = Xxh3::new();
4848
for (name, value) in self.values.iter() {
49-
hash.update(name.as_bytes());
50-
hash.update(SALT);
51-
hash.update(value);
52-
hash.update(SALT);
49+
hasher.update(name.as_bytes());
50+
hasher.update(SALT);
51+
hasher.update(value);
52+
hasher.update(SALT);
5353
}
54-
Some(hash.finalize().into())
54+
let hash = hasher.digest128();
55+
Some(hash.to_le_bytes())
5556
} else {
5657
None
5758
}

0 commit comments

Comments
 (0)