앞선 글에 이어서 설명하겠다.
사용자 Identification
UV ( Unique Visitors ) 또는 UB ( Unique Browsers ) 로 구분
1. 검출 단위에서 Access Log 를 읽음
2. WAS Request 부분에서 HashSet 기반 자료구조로 식별
=> Access Log 는 처리 시간이 오래 걸리며 , 실시간 상황 인지 불가능
=> HashSet 역시 문제점이 있음.HashSet 기반의 문제점
- 대규모 사용자를 상대로 할 시 비용이 많이 듬.
- 조회수가 충분히 클 시 완벽히 정확한 조회수는 필요없음.
- HashSet 이 매우 커질 시 , 충돌 위험 및 시간이 오래 걸림.
사실 HLL 이란거에 대해서 완벽하게 조사하고 이해한 것은 아니다.
그저 , 어떻게 개발이 됐는지 와 효율적인지에 대해서만 설명하려고 찾아봤다.
- 2007 년 발표
- 유일 원소 개수 ( cardinality ) 추정 사용하는 확률적 자료구조
- Redis 에서는 자체 지원 ( 2.8.9 >= )
- 셰익스피어 전 작품 나오는 단어 개수 ( Cardinality ) 세기
( http://highscalability.com/blog/2012/4/5/big-data-counting-how-to-count-a-billion-distinct-objects-us.html )
( Byte 는 사용 Memory 크기 의미 , Cardinality 는 유일 단어 개수 , Relative Error 는 상대 오차 )
- HashSet : 10,447,016 Byte Memory , 67,801 Cardinality , 0% Relative Error
- HyperLogLog : 512 Byte Memory , 70,002 , 3% Relative Error
=> 정확도는 3% 차이나나 , 메모리 사용량은 1/2500 정도로 차이남!
- nazar 는 앞서 설명한 , Reddit 의 Consumer
- postId 와 userId 를 활용해서 , 해당 사용자가 조회수를 올릴때 유효한지 검증한다.
Code
export async function nazar(postId:string,userId:string){ const key = `postViewed:${userId}`; try{ const result = await redisClient.sIsMember(key,postId); return result; } catch { throw Error } }
- Redis 에서 제공하는 Set을 이용한다.
- set 에는 userId 를 key 로 , postId들을 value 로 넣는다.
- set 에 있을시 , 중복이므로 밑에 있는 abacus 를 실행하지 않는다.
- Reddit 의 Consumer
- postId 와 userId 를 활용해 , 조회수를 증가시키고 Set에 저장한다.
Code
export async function abacus(postId:string){ try{ const key = `postViews:${postId}`; const log = `postViewed:${userId}`; await redisClient.pfAdd(key,userId); await redisClient.sAdd(log,postId); } catch{ throw Error } }
- Set 은 120개 정도 postId를 넣었을 시 , 9816 Byte 정도를 차지한다. ( memory usage 로 확인 )
- HLL 은 550개 정도 postId를 넣었을 시 , 1592 Byte 정도를 차지한다.
( 550개 넣었는데 , 543 개로 count 되긴 한다. ) - UUID 에서 중복 된걸수도 있음.
- 설명 외 , 불필요한 부분은 삭제 했다.
export default new CronJob( "* 2 * * *", async () => { await deletePostSets(); const expiredTime = new Date(Date.now() - expire); const posts = await PostRepository.find({ where: { deletedAt: LessThan(expiredTime) } }); if (posts) { try { posts.forEach(post=>deletePostViews(post.id)); } catch (err) { logger.error(err); } } }, ); export async function deletePostSets(){ try{ const keys = await redisClient.keys('postViews:*'); if(keys)await redisClient.del(keys); return; } catch{ throw Error } }
- 새벽 2시 실행하는 Cron 으로 , Redis에 존재하는 Set 과 HLL 을 관리한다.
- postViews 를 전부 삭제해서 , 조회수를 초기화 한다.
- 삭제된 post 를 검색한 후 , 해당 post에 대한 HLL을 삭제한다.