상품 전시(display)쪽이라 호출이 엄청 많기도 하고, 상품 보여줄 때마다 이 상품이 특정 상태의 상품인지 확인해야 하는 로직이 있다.
그 특정 상태는 PM분이 상태를 등록할 수 있도록 slack 메시지로 등록할 수 있게 만들어 둔 상태고, redis에 올려놓고 쓴다.
그런데 그 특정 상태를 체크하기 위해 매번 redis에 찌르면 redis 호출하는 횟수가 너무 많을 것 같아, client를 싱글톤 인스턴스로 만들고, 프로세스 전역에서 그 인스턴스만 쓰도록 구현을 해놓았다.
class _SpecificGoodsRedisClient:
def __init__(self):
self._cached_specific_goods_list = []
def get_cached_specific_goods_list(self) -> list[Goods]:
self._set_cache_attributes()
return self._cached_specific_goods_list
def _set_cache_attributes(self) -> None:
if not self._cached_specific_goods_list:
self._cached_specific_goods_list = con.get(KEY)
# 전역에서 이 인스턴스만 쓴다
specific_goods_client = _SpecificGoodsRedisClient()
간략화시켜서 대충 위와 같은 느낌인데, slack에서 등록할 때도 다음과 같은 식으로 한다
# 등록한 상품
june_goods_list = args[0]
# 오늘부터 6월 30일까지의 초 차이
expired_at = diff_from_last_day_of_june()
con.set(june_goods_list, expired_at)
위 코드에서 문제점은 무엇일까? 바로 만료기한이다. _set_cache_attributes를 보면 cached_goods_list의 내용이 없을 때 redis에서 값을 얻어온다. 근데 redis에 있던 키가 만료되어 값이 없다면? 매번 _set_cache_attributes()가 호출될 때마다 redis를 찌른다
그래서 위와 같은 그래프를 얻었다. 심할 땐 10초에 거의 150만 번씩 찌르고 있던 걸로 보이는데, 작은 실수 때문에 redis에 너무 큰 부하를 주었다.
사실, 처음에 저 키에 해당하는 상품을 6월까지만 사용하기로 내부에서 결정 했었다. 그래서 저 상품 운용 여부가 6월 내로 정해질 거라, 그 전에 저 key에 해당하는 상품이 빠질 것인지 안 빠질 것인지 결정될 것이라고 생각했다. 그런데 지금은 7월인데 중요도가 더 높은 다른 일을 하느라 잊혀졌다 하하...
이번에 이 일을 겪으면서 느낀 점 몇가지
1. 사람의 기억 / 변수명 등만 보고 나중에 이거 수정하겠지~ 라는 나이브한 생각하지 말고, 무언가의 장치를 만들어 두기. 실제로도 june list인데, 6월이 다 끝나가는데도 이게 갱신되어야 하는 것을 잊고 있었다
2. 캐시 사용할 때는 키가 만료되었을 때까지 고려해서 로직 작성하기
3. read 작업이면 신경써서 read용 인스턴스에 찌를 것
4. 한 편으론 내가 자리를 비우면 이 컨텍스트를 알고 있는 사람이 없기 때문에 문제가 생길 수도 있겠다. 주위에 컨텍스트를 더 잘 알리거나, 알릴 필요도 없게 장치를 만들어 두면 어떨까
사내에서도 몇 가지 개선점 이야기가 나왔다
1. redis에서도 N+1과 같이 특정 anormally 패턴이 감지되면, 개발자에게 알릴 수 있는 시스템을 만드는게 어떨지
2. read / write redis를 선정할 때 의식적하지 않으면 무의식적으로 기대와는 다른 요청을 찌를 수 있으니 라우터를 만들어야 하는 건 아닌지