清理旧计数器
经过前面的介绍,我们已经知道了怎样将计数器存储到Redis里面,已经怎样从计数器里面取出数据。但是,如果我们只是一味地对计数器进行更新而不执行任何清理操作的话,那么程序最终将会因为存储了过多的数据而导致内存不足。好在我们事先已将所有已知的计数器记录到了一个有序集合里面,所以对计数器进行清理只需要遍历有序集合并删除其中的旧计数器旧可以了。
为什么不使用expire?
expire命令的其中一个限制就是它只能应用整个键,而不能只对键的某一部分数据进行过期处理。并且因为我们将同一个计数器在不同精度下的所有计数器数据都存放到了同一个键里面,所以我们必须定期地对计数器进行清理。如果读者感兴趣的话,也可以试试改变计数器组织数据的方式,使用Redis的过期键功能来代替手工的清理操作。
在处理和清理旧数据的时候,有几件事情是需要我们格外留心的,其中包括以下几件:
- 任何时候都可能会有新的计数器被添加进来
- 同一时间可能会有多个不同的清理操作在执行
- 对于一个每天只更新一次的计数器来说,以每分钟一次的频率尝试清理这个计数器只会浪费计算资源。
- 如果一个计数器不包含任何数据,那么程序就不应该尝试对它进行清理。
我们接下来要构建一个守护进程函数,这个函数的工作方式和第三章中展示的守护进程函数类似,并且会严格遵守上面列出的各个注意事项。和之前展示的守护进程函数一样,这个守护进程函数会不断地重复循环知道系统终止这个进程为止。为了尽可能地降低清理操作的执行负载,守护进程会以每分钟一次的频率清理那些每分钟更新一次或者每分钟更新多次的计数器,而对于那些更新频率低于每分钟一次的计数器,守护进程则会根据计数器自身的更新频率来决定对他们进行清理的频率。比如说,对于每秒更新一次或者每5秒更新一次的计数器,守护进程将以每分钟一次的频率清理这些计数器;而对于每5分钟更新一次的计数器,守护进程将以每5分钟一次的频率清理这些计数器。
清理程序通过对记录已知计数器的有序集合执行zrange命令来一个接一个的遍历所有已知的计数器。在对计数器执行清理操作的时候,程序会取出计数器记录的所有计数样本的开始时间,并移除那些开始时间位于指定截止时间之前的样本,清理之后的计数器最多只会保留最新的120个样本。如果一个计数器在执行清理操作之后不再包含任何样本,那么程序将从记录已知计数器的有序集合里面移除这个计数器的引用信息。以上给出的描述大致地说明了计数器清理函数的运作原理,至于程序的一些边界情况最好还是通过代码来说明,要了解该函数的所有细节,请看下面代码:
import bisect
import time
import redis
QUIT=True
SAMPLE_COUNT=1
def clean_counters(conn):
pipe=conn.pipeline(True)
#为了平等的处理更新频率各不相同的多个计数器,程序需要记录清理操作执行的次数
passes=0
#持续地对计数器进行清理,知道退出为止
while not QUIT:
#记录清理操作开始执行的时间,这个值将被用于计算清理操作的执行时长
start=time.time()
index=0
#渐进的遍历所有已知计数器
while index<conn.zcard('known:'):
#取得被检查的计数器的数据
hash=conn.zrange('known:',index,index)
index+=1
if not hash:
break
hash=hash[0]
#取得计数器的精度
prec=int(hash.partition(':')[0])
#因为清理程序每60秒就会循环一次,所以这里需要根据计数器的更新频率来判断是否真的有必要对计数器进行清理
bprec=int(prec//60) or 1
#如果这个计数器在这次循环里不需要进行清理,那么检查下一个计数器。
#举个例子:如果清理程序只循环了3次,而计数器的更新频率是5分钟一次,那么程序暂时还不需要对这个计数器进行清理
if passes % bprec:
continue
hkey='count:'+hash
#根据给定的精度以及需要保留的样本数量,计算出我们需要保留什么时间之前的样本。
cutoff=time.time()-SAMPLE_COUNT*prec
#将conn.hkeys(hkey)得到的数据都转换成int类型
samples=map(int,conn.hkeys(hkey))
samples.sort()
#计算出需要移除的样本数量。
remove=bisect.bisect_right(samples,cutoff)
#按需要移除技术样本
if remove:
conn.hdel(hkey,*samples[:remove])
#这个散列可能以及被清空
if remove==len(samples):
try:
#在尝试修改计数器散列之前,对其进行监视
pipe.watch(hkey)
#验证计数器散列是否为空,如果是的话,那么从记录已知计数器的有序集合里面移除它。
if not pipe.hlen(hkey):
pipe.multi()
pipe.zrem('known:',hash)
pipe.execute()
#在删除了一个计数器的情况下,下次循环可以使用与本次循环相同的索引
index-=1
else:
#计数器散列并不为空,继续让它留在记录已知计数器的有序集合里面
pipe.unwatch()
except redis.exceptions.WatchError:
#有其他程序向这个计算器散列添加了新的数据,它已经不再是空的了,
# 继续让它留在记录已知计数器的有序集合里面。
pass
passes+=1
# 为了让清理操作的执行频率与计数器更新的频率保持一致
# 对记录循环次数的变量以及记录执行时长的变量进行更新。
duration=min(int(time.time()-start)+1,60)
#如果这次循环未耗尽60秒,那么在余下的时间内进行休眠,如果60秒已经耗尽,那么休眠1秒以便稍作休息
time.sleep(max(60-duration,1))
正如之前所说,clean_counters()函数会一个接一个地遍历有序集合里面记录的计数器,查找需要进行清理的计数器。程序在每次遍历时都会对计数器进行检查,确保只清理应该清理的计数器。当程序尝试清理一个计数器的时候,它会取出计数器记录的所有数据样本,并判断哪些样本是需要被删除的。如果程序在对一个计数器执行清理操作之后,然后这个计数器已经不再包含任何数据,那么程序会检查这个计数器是否已经被清空,并在确认了它已经被清空之后,将它从记录已知计数器的有序集合中移除。最后,在遍历完所有计数器之后,程序会计算此次遍历耗费的时长,如果为了执行清理操作而预留的一分钟时间没有完全耗尽,那么程序将休眠直到这一分钟过去为止,然后继续进行下次遍历。
现在我们已经知道怎样记录、获取和清理计数器数据了,接下来要做的视乎就是构建一个界面来展示这些数据了。遗憾的是,这些内容设计到前端,并不在本内容介绍范围内,如果感兴趣,可以试试jqplot、Highcharts、dygraphs已经D3,这几个JavaScript绘图库无论是个人使用还是专业使用都非常合适。
在和一个真实的网站打交道的时候,知道页面每天的点击可以帮助我们判断是否需要对页面进行缓存。但是,如果被频繁访问的页面只需要花费2毫秒来进行渲染,而其他流量只要十分之一的页面却需要花费2秒来进行渲染,那么在缓存被频繁访问的页面之前,我们可以先将注意力放到优化渲染速度较慢的页面上去。在接下来的一节中,我们将不再使用计数器来记录页面的点击量,而是通过记录聚合统计数据来更准确地判断哪些地方需要进行优化。