设计一个Key-Value缓存去存储最近的Web Server查询的结果

1: 定义Use Case和约束

Use Cases

我们可以定义如下 Scope:

  • User 发送一个 search request, 缓存命中成功返回Data
  • User 发送一个 search request, 缓存未命中,未成功返回Data
  • Service 有高可用

约束和假设

状态假设
  1. Traffic 分布不是均匀的
    • 热度高的查询总是被缓存
    • 需要去确定怎样去 expire/refresh
  2. Cache 服务的查询要快
  3. 在机器之间有低延迟
  4. 在内存中有受限的内存
  • 需要决定神魔时候保留/移除缓存
  • 需要缓存数百万Query
  1. 1千万 user
  2. 100 亿 query/ 月
计算使用量
  1. Cache 存储排序后的 key 的 list: query, value: list
  • query - 50 bytes
  • title - 20 bytes
  • snippet - 200 bytes
  • Total: 270 bytes
  1. 2.7 TB 缓存数据 / 月,如果所有 100 亿query 都是独特的,并且都被存储
  • 270 bytes / 每个搜索 * 100 亿 搜索 / 每月
  • 假设状态受限的内存,需要去决定怎样去 exopire 内容
  1. 4000 请求 / s

简易转换指南:

  1. 250 万 s/ 每个月
  2. 1 request / s = 250 万请求 / 月
  3. 40 request / s = 1 亿 请求 / 月
  4. 400 request / s = 10 亿 请求 / 月

2: 创建一个High Level Design

Design

3: 设计核心组件

Use Case: User 发送一个请求,然后缓存命中

由于Cache的容量是固定的,我们将使用 a least recently used(LRU) 方法去过期的老 entiies.

  1. Client 发送一个请求到 Web Server
  2. Web Server 转发请求到 Query API Server
  3. Query API 会做下面的事情
  • 解析 query
    • 移除 markup
    • 分解 text 进 terms
    • Fix typos
    • 格式化首字母
    • 转换 query 去 使用 bool 操作
  • 检查缓存中和 query 匹配的 content
    • 如果有缓存击中,缓存会做下面的事情
      • 更新缓存entry的位置到LRU List的前面
      • 返回缓存内容
    • 否则,Query API 做下面的事情:
      • 使用 Reverse Index Service 去寻找到匹配 query 的 document
        • Reverse Index Service 排序搜索结果然后返回Top one
      • 使用 Document Service 去返回 title 和 snippets
      • 伴随着 content去更新 Memory Cache,放这个 entry 进 LRU list的前面
Cache 实现

cache 使用双向链表:新的 items 会被添加到头节点,当items 过期时会被为尾节点移除,我们可以使用Hash 表来快速查找每一个 linked list node.

Query API Server 实现:

class QueryApi(object):
	def __init__(self, memory_cache, reverse_index_service):
		self.memory_cache = memory_cache
		self.reverse_index_service = reverse_index_service

	def parse_query(self, query):
		"""Remove markup, break text into terms, deal with typos,
        normalize capitalization, convert to use boolean operations.
      """
	def process_query(self, query):
		query = self.parse_query(query)
		results = self.memory_cache.get(query)
		if results is None:
			results = self.reverse_index_service.process_search(query)
			self.memory_cache.set(query, results)
		return results

Node 实现:

class Node(object):
	def __init__(self, query, results):
		self.query = query
		self.results = results

LinkedList 实现:

class LinkedList(object):
	
	def __init__(self):
		self.head = None
		self.tail = None

	def move_to_front(self, node):
		...
	
	def append_to_front(self, node):
		...
	
	def remove_from_tail(self):
		...

Cache 实现:

class Cache(object):
	def __init__(self, MAX_SIZE):
		self.MAX_SIZE = MAX_SIZE
		self.size = 0
		self.lookup = {}
		self.linked_list = LinkedList()

	def get(self, query)
		""" Get the stored query result from the cache
			Accessing a node update its position to the front of the LRU list.
			"""
			node = self.lookup[query]
			if node is None:
				return None
			self.linked_list.move_to_front(node)
			return node.results

	def set(self, results, query):
		""" Set the result for the given query key in the cache
			
			When updating an entry, updates its position to the front of the LRU list.
			If the entry is new and the cache is at capacity, removes the oldest entry 
			before the new entry is added.
			"""
			node = self.lookup[query]
			if node is not None:
				node.results = results
				self.linked_list.move_to_front(node)
			else:
				if self.size == self.MAX_SIZE:
					self.loopup.pop(self.linked_list.tail.query, None)
					self.linked_list.remove_from_tail()
				else:
					self.size += 1
				new_node = Node(query, results)
				self.linked_list.append_to_front(new_node)
				self.lookup[query] = new_node

会在如下时间Update Cache:

  • Page 的 content 改变
  • Page 被移除 或者 新page 被添加
  • Page 排名改变

处理这些情况最简单的方法是简单地设置缓存条目在更新之前可以保留在缓存中的最大时间,通常称为生存时间(TTL)。

4:扩展设计

扩展缓存到多台机器

为了处理重负载请求和大量需要的内存,我们需要水品扩展,我们有三个选择关于怎样存储数据进 Memory Cache cluster:

  • 每一个在缓存集群中的机器有自己的 Cache - 简单,尽管它会有一个低缓存命中率的结果
  • 每一个在缓存集群中的机器有自己的 Cache 复制 - 简单,尽管hi有无效的内存使用
  • 缓存集群重的缓存会被分散在所有的机器上 - 复杂,尽管这可能是最好的选项,我们可以使用 Hash去确定哪一个机器会得到针对单个查询的结果,通过使用 machine = hash(query. 我们将想要去使用 持续性 hash.
  • 15
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值