广告倒排:突破速度和内存的双重极限
2023-12-01 16:59:14
广告倒排服务:突破速度与内存极限的极致优化
在广告搜索系统中,广告倒排服务扮演着至关重要的角色,它负责从海量广告库中快速检索和召回与用户查询相关的广告。随着广告数据的不断膨胀和用户搜索需求的日益复杂,广告倒排服务面临着严峻的挑战:
- 海量数据: 广告倒排服务需要处理数千亿甚至万亿级的广告数据,如何高效地存储和检索这些数据是一个巨大的挑战。
- 高并发访问: 在高峰时段,广告倒排服务需要同时处理来自数百万甚至上千万用户的并发搜索请求,如何保证低延迟响应是一个关键的问题。
- 低内存占用: 广告倒排数据庞大,如何在有限的内存空间内存储和处理这些数据,对系统的性能和成本提出了很高的要求。
- 无截断召回: 广告倒排服务需要保证检索结果的完整性,即不截断任何满足用户查询条件的广告,这对于广告平台的收入和用户体验至关重要。
为了满足这些挑战,广告倒排服务需要进行极致优化,突破速度和内存的双重极限。本文将深入探究广告倒排服务的优化策略,涵盖数据结构设计、压缩技术、缓存策略、多级存储、并行处理、异步处理、流式处理、实时更新、无截断召回等各个方面。
倒排数据结构设计:
广告倒排数据结构是广告倒排服务的核心。它将广告信息按照关键词进行组织,并存储着广告在文档中的位置信息。为了提高检索速度,倒排数据结构通常采用树形结构或哈希表结构。在广告倒排服务中,我们采用了自研的基于跳表结构的倒排数据结构。跳表是一种平衡二叉树,它允许在 O(log n) 的时间复杂度内进行查找和插入操作。同时,为了进一步提高检索速度,我们还采用了分段存储和内存映射技术,将倒排数据存储在多个文件中,并在内存中映射这些文件,从而避免了文件系统 I/O 的开销。
public class SkipList {
private Node head;
private Node tail;
private int level;
public SkipList() {
head = new Node(null, Integer.MIN_VALUE);
tail = new Node(null, Integer.MAX_VALUE);
head.right = tail;
tail.left = head;
level = 1;
}
public void insert(int key, Object value) {
Node newNode = new Node(value, key);
Node current = head;
while (current.right != tail && current.right.key < key) {
current = current.right;
}
int newLevel = randomLevel();
if (newLevel > level) {
for (int i = level + 1; i <= newLevel; i++) {
Node newHeader = new Node(null, Integer.MIN_VALUE);
newHeader.down = head;
newHeader.right = new Node(null, Integer.MAX_VALUE);
newHeader.right.left = newHeader;
head = newHeader;
level++;
}
}
for (int i = 0; i < newLevel; i++) {
while (current.right != tail && current.right.key < key) {
current = current.right;
}
Node next = current.right;
newNode.right = next;
next.left = newNode;
newNode.left = current;
current.right = newNode;
current = current.up;
}
}
public Object search(int key) {
Node current = head;
while (current.right != tail && current.right.key < key) {
current = current.right;
}
if (current.right != tail && current.right.key == key) {
return current.right.value;
} else {
return null;
}
}
private int randomLevel() {
int level = 1;
while (Math.random() < 0.5) {
level++;
}
return level;
}
private static class Node {
private Object value;
private int key;
private Node up;
private Node down;
private Node left;
private Node right;
public Node(Object value, int key) {
this.value = value;
this.key = key;
}
}
}
压缩技术:
为了降低内存占用,广告倒排服务需要对倒排数据进行压缩。常用的压缩技术包括:
- 位图压缩: 将广告在文档中的位置信息存储为位图,从而减少存储空间。
- 字典编码: 将广告关键词编码成较短的整数,从而减少存储空间。
- 前缀树压缩: 将广告关键词的前缀存储为一颗前缀树,从而减少存储空间。
在广告倒排服务中,我们采用了多种压缩技术相结合的方式,以实现最佳的压缩效果。例如,我们对广告在文档中的位置信息采用位图压缩,对广告关键词采用字典编码,对广告关键词的前缀采用前缀树压缩。通过这些压缩技术,我们实现了倒排数据在内存中的存储空间大幅减少,从而提高了内存利用率。
public class BitMapCompression {
public static byte[] compress(int[] array) {
int bitCount = 0;
for (int num : array) {
bitCount += Integer.bitCount(num);
}
byte[] bytes = new byte[(bitCount + 7) / 8];
int index = 0;
for (int num : array) {
for (int i = 0; i < 32; i++) {
if ((num & (1 << i)) != 0) {
bytes[index / 8] |= (1 << (index % 8));
}
index++;
}
}
return bytes;
}
public static int[] decompress(byte[] bytes) {
int[] array = new int[(bytes.length * 8) / 32];
int index = 0;
for (byte b : bytes) {
for (int i = 0; i < 8; i++) {
if ((b & (1 << i)) != 0) {
array[index / 32] |= (1 << (index % 32));
}
index++;
}
}
return array;
}
}
缓存策略:
为了进一步提高检索速度,广告倒排服务通常采用缓存技术。缓存技术将最近访问过的倒排数据存储在内存中,当用户再次访问这些数据时,可以直接从内存中读取,从而避免了对磁盘或数据库的访问。在广告倒排服务中,我们采用了多种缓存策略相结合的方式,以实现最佳的缓存效果。例如,我们对热门的广告关键词采用 LRU 缓存策略,对冷门的广告关键词采用 LFU 缓存策略。通过这些缓存策略,我们实现了倒排数据在内存中的命中率大幅提高,从而进一步提高了检索速度。
import java.util.HashMap;
import java.util.LinkedHashMap;
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}
多级存储:
为了满足大规模数据的存储需求,广告倒排服务通常采用多级存储架构。多级存储架构将倒排数据存储在不同的存储介质上,如内存、SSD、HDD 等。当用户访问倒排数据时,系统会根据数据的热度和访问频率,将数据加载到合适的存储介质上。在广告倒排服务中,我们采用了基于 SSD 和 HDD 的多级存储架构。我们将热门的广告关键词存储在 SSD 上,冷门的广告关键词存储在 HDD 上。通过这种方式,我们实现了倒排数据的快速访问和低成本存储。
import java.util.HashMap;
public class MultiLevelStorage<K, V> {
private HashMap<K, V> memoryCache;
private HashMap<K, V> ssdCache;
private HashMap<K, V> hddCache;
public MultiLevelStorage() {
memoryCache = new HashMap<>();
ssdCache = new HashMap<>();
hddCache = new HashMap<>();
}
public V get(K key) {
V value = memoryCache.get(key);
if (