記錄日誌
記錄最近日誌
# 思路:將日誌加入list,然後修剪到規定大小
def log_recent(conn, name, message, severity=logging.INFO, pipe=None):
severity = str(SEVERITY.get(severity, severity)).lower()
# 日誌的KEY,構成:recent:日誌名稱:日誌級別
destination = 'recent:%s:%s'%(name, severity)
message = time.asctime() + ' ' + message # 日誌資訊前面新增時間資訊
pipe = pipe or conn.pipeline()
pipe.lpush(destination, message) # 1. 加入list
pipe.ltrim(destination, 0, 99) # 2. 擷取最近100條記錄
pipe.execute() # 執行以上兩步
# 執行:log_recent(conn, 'test', 'test_msg')
# 結果:(key)recent:test:info (value)Sun Jun 23 11:57:38 2019 test_msg
記錄常見日誌
記錄出現頻率較高的日誌,每小時進行一次輪換
# 思路:訊息作為成員記錄到有序集合,訊息出現頻率作為成員的分值(score)
# 記錄的時間範圍為1小時,記錄的時候發現已經過了一小時,
# 則把已有的記錄歸檔到上一小時(通過把KEY重新命名來實現)
# 則新的一小時訊息頻率有從0開始記錄
# 用於記錄的KEY:[common:日誌名稱:日誌級別]
def log_common(conn, name, message, severity=logging.INFO, timeout=5):
severity = str(SEVERITY.get(severity, severity)).lower()
destination = 'common:%s:%s'%(name, severity)
#當前所處小時數
start_key = destination + ':start' # common:日誌名稱:日誌級別:start
pipe = conn.pipeline()
end = time.time() + timeout
while time.time() < end:
try:
pipe.watch(start_key)
# datetime(*now[:4]).isoformat() --> '2019-06-23T06:00:00'
now = datetime.utcnow().timetuple()
# 簡單獲取小時數(原書方法行不通,這裡加以修改)
hour_start = now.tm_hour
# 獲取[common:日誌名稱:日誌級別:start]的值
# 這裡返回字串型別,注意轉為整型
existing = pipe.get(start_key)
pipe.multi()
# 如果值存在 且 小於當前小時數
if existing and int(existing) < hour_start:
# 進行歸檔
# KEY [common:日誌名稱:日誌級別] 重新命名為 [common:日誌名稱:日誌級別:last]
pipe.rename(destination, destination + ':last')
# KEY [common:日誌名稱:日誌級別:start] 重新命名為 [common:日誌名稱:日誌級別:pstart]
pipe.rename(start_key, destination + ':pstart')
# 之前的KEY已經歸檔,這裡是新的
pipe.set(start_key, hour_start)
# 不存在則新增該日誌開始時間記錄
elif not existing:
# KEY [common:日誌名稱:日誌級別:start] 的值設定為當前小時數
pipe.set(start_key, hour_start)
# 對有序集合destination的成員message自增1
# 注意:zincrby在redis-py3.0+的用法
pipe.zincrby(destination, 1, message)
# 記錄到最新日誌
log_recent(pipe, name, message, severity, pipe)
return
# 如果其他客戶端剛好有操作,修改了watch的key,進行重試
except redis.exceptions.WatchError:
continue
# 執行
log_common(conn, 'test', 'msg')
#結果
#(zset)common:test:info msg --> 1
# -->1小時後再次記錄日誌的話,該KEY就會變成common:test:info:last
#(string)common:test:info:start 14(小時數)
# -->1小時後再次記錄日誌的話,該KEY就會變成common:test:info:pstart
#(list)recent:test:info Wed Jun 26 22:53:17 2019 msg
計數器
記錄,比如,1秒鐘、5秒鐘……頁面點選數,下圖為5秒鐘點選計數器,型別為hash,各鍵值對為 [ 時間戳:點選數 ]。
[know:]有序聚合用於清理舊資料時,按精度大小順序逐個迭代計數器。
更新計數器
# 以秒為單位的計數器精度
PRECISION = [1, 5, 60, 300, 3600, 18000, 86400]
QUIT = False
SAMPLE_COUNT = 100
# 實現:獲取每個時間片段的開始時間,將次數統計到每個時間段的開始時間
def update_counter(conn, name, count=1, now=None):
now = now or time.time() # 當前時間
pipe = conn.pipeline()
for prec in PRECISION:
pnow = int(now / prec) * prec # 獲取當前時間片的開始時間
hash = '%s:%s'%(prec, name) # 建立儲存計數資訊的hash
# zadd在redis-py 3.0之後更改了,第二個引數應該傳入一個字典 √
# pipe.zadd('known:', hash, 0) ×
# 有序集合,用於後期可以按順序迭代清理計數器(並不使用expire,因為expire只能對hash整個鍵過期)
# 這裡可以組合使用set和list,但用zset,可以排序,又可以避免重複元素
# **這個zset的score值都是0,所以最後排序的時候按member字串的二進位制來排序**
pipe.zadd('known:', {hash: 0}) # 這個記錄在後面的清理程式有用
pipe.hincrby('count:' + hash, pnow, count) # 更新對應精度時間片的計數
pipe.execute()
獲取計數器
def get_counter(conn, name, precision):
hash = '%s:%s'%(precision, name) # 要獲取的hash的key
data = conn.hgetall('count:' + hash)
to_return = []
for key, value in data.items():
to_return.append((int(key), int(value)))
to_return.sort()
return to_return
清理計數器
計數器中的元素隨著時間的推移越來越多,需要對其定期清理,保持合理的數量,以減少記憶體消耗。
# 清理規則: 1s,5s計數器,1min清理一次
# 後面的,5min計數器5min清理一次,以此類推
def clean_counters(conn):
pipe = conn.pipeline(True)
passes = 0
# 按時間片段從小到大迭代已知的計數器
while not QUIT:
start = time.time()
index = 0
while index < conn.zcard('known:'):
# 取出有序集合的一個元素(列印hash,發現返回的是一個byte型別)
hash = conn.zrange('known:', index, index)
index += 1
if not hash:
break
hash = hash[0]
# 得到時間精度
prec = int(hash.partition(b':')[0])
# 按上面說明的清理規則計算時間間隔
# 小於60s的計數器至少1min清理一次
bprec = int(prec // 60) or 1 # '//'操作是取整除法
# 實現幾分鐘清理一次的邏輯
# 不整除的時候continue --> 重新while迴圈
# 比如,1分鐘,每次都整除,所以每次判斷後後執行continue下面的語句
# 10分鐘,要等10次到passes=10才整除
if passes % bprec: #
continue
# 清理邏輯開始
hkey = 'count:' + hash.decode('utf-8') # 注意將byte轉換成str,書中沒有轉換
# 根據要保留的個數*精度,計算要擷取的時間點
cutoff = time.time() - SAMPLE_COUNT * prec
# python3的map返回可迭代物件而不是list,原書的這句需要加上list轉換
samples = list(map(int, conn.hkeys(hkey)))
samples.sort()
# 二分法找出cutoff右邊的位置(index)
remove = bisect.bisect_right(samples, cutoff)
# 如果有需要移除的
if remove:
# 刪除0-remove位置的元素
conn.hdel(hkey, *samples[:remove])
# 判斷是否全部被移除
if remove == len(samples):
try:
pipe.watch(hkey)
# 再次確保hash中已經沒有元素
if not pipe.hlen(hkey):
pipe.multi()
# 同時將known:中的相應元素移除
pipe.zrem('known:', hash)
pipe.execute()
# 減少了一個計數器
index -= 1
else:
pipe.unwatch()
except redis.exceptions.WatchError:
pass
# 累計次數,直到整除,才開始清理程式
passes += 1
# 以下兩句保證一次while迴圈時間是1min
# 計算程式執行時間,且保證至少1s,最多是1min
duration = min(int(time.time() - start) + 1, 60)
# 休息,時間為:1min減去程式執行時間,也即1min中剩餘的時間,且保證至少是1s
time.sleep(max(60 - duration, 1))
統計資料
# 將所有統計量放在一個有序集合裡
def update_stats(conn, context, type, value, timeout=5):
destination = 'stats:%s:%s'%(context, type)
start_key = destination + ':start'
pipe = conn.pipeline(True)
end = time.time() + timeout
while time.time() < end:
try:
pipe.watch(start_key)
now = datetime.utcnow().timetuple()
hour_start = now.tm_hour
existing = pipe.get(start_key)
pipe.multi()
if existing and existing < hour_start:
pipe.rename(destination, destination + ':last')
pipe.rename(start_key, destination + ':pstart')
pipe.set(start_key, hour_start)
# 以上部分跟記錄常見日誌一樣
tkey1 = str(uuid.uuid4())
tkey2 = str(uuid.uuid4())
# 這裡比較巧妙地統計到最大值和最小值
# 構造兩個有序集合,成員為min、max,score為要統計的值
# 然後跟目標集合求並集,聚合方式為min、max
pipe.zadd(tkey1, {'min': value})
pipe.zadd(tkey2, {'max': value})
pipe.zunionstore(destination,
[destination, tkey1], aggregate='min')
pipe.zunionstore(destination,
[destination, tkey2], aggregate='max')
pipe.delete(tkey1, tkey2)
pipe.zincrby(destination, 1, 'count') # +1
pipe.zincrby(destination, value, 'sum') # 總和
pipe.zincrby(destination, value*value,'sumsq') #平方和
return pipe.execute()[-3:]
except redis.exceptions.WatchError:
continue
# 計算平均值、方差
def get_stats(conn, context, type):
key = 'stats:%s:%s'%(context, type)
data = dict(conn.zrange(key, 0, -1, withscores=True))
data['average'] = data['sum'] / data['count']
numerator = data['sumsq'] - data['sum'] ** 2 / data['count']
data['stddev'] = (numerator / (data['count'] - 1 or 1)) ** .5
return data
完整程式碼: