20140125

Redis 2.8 persistence 옵션 비교

Redis 2.8 새로 깔면서 RDB, AOF, 둘 다 중 뭘 고를지 꽤 오래 고민. 정리해둔다.

RDB는 포인트-인-타임 스냅샷. save 900 1, save 300 10 같은 조건 만족하면 bgsave 트리거. 내부적으로 fork() 떠서 자식 프로세스가 Copy-On-Write로 메모리 스냅샷을 dump.rdb로 덤프. 장점은 파일이 작고(LZF 압축) 복구가 매우 빠름. rdb 로드는 메모리에 순차 write라 10GB도 몇 분이면 올라온다. 단점은 마지막 스냅샷 이후 분 단위 데이터가 날아갈 수 있다는 점.

AOF는 모든 쓰기 커맨드를 파일에 append. 복구할 땐 서버 시작 시 커맨드를 재생해서 상태 복원. appendfsync가 핵심.

  • always: 매 커맨드마다 fsync. 안전하지만 느림. SSD여도 TPS가 2~3배 떨어짐
  • everysec: 초당 한 번. 장애 시 최대 1초 손실. 기본값이고 실용적
  • no: fsync는 OS가 알아서. 빠르지만 데이터 손실 범위가 불명확(수십 초 가능)

AOF 파일은 시간이 갈수록 커지니까 BGREWRITEAOF로 현재 메모리 상태를 복원하는 최소 커맨드 집합으로 압축 재작성. 2.4부터 들어간 기능인데 2.8에서는 조건부 자동 rewrite(auto-aof-rewrite-percentage)가 안정화. 이 rewrite 역시 fork 떠서 자식이 수행.

2.8 신기능: RDB + AOF 병행. 원래도 둘 다 켤 수 있었는데 AOF를 우선 로드하는 규칙이 명확해짐. 재시작 시 appendonly.aof → dump.rdb 순. 더 안전한 쪽(AOF)이 기본 선택.

우리 서비스는 캐시 + 세션 저장소. 세션은 날아가도 재로그인으로 복구 가능하고, 캐시는 정의상 휘발성. 대신 재시작 부팅 시간이 민감해서 RDB만 선택. AOF는 끈다.

save 900 1
save 300 10
save 60 10000
rdbcompression yes
rdbchecksum yes
stop-writes-on-bgsave-error yes
dir /var/lib/redis/
dbfilename dump.rdb
appendonly no

# 2.8에서 유용한 옵션
repl-diskless-sync no   # 마스터→슬레이브 풀싱크를 디스크 경유
maxmemory 12gb
maxmemory-policy allkeys-lru

stop-writes-on-bgsave-error yes — 디스크 풀이면 bgsave 실패하는데, 이 옵션 on이면 redis가 client의 쓰기를 거부함. 알림 안 걸어놓으면 서비스 전체가 죽은 것처럼 보인다. 모니터링에 rdb_last_bgsave_status 체크 걸기 필수.

fork + COW 함정. bgsave 시작할 때 fork() 떠서 자식이 RDB 쓰기를 수행. 부모는 계속 요청 처리. COW라 실제 복사는 "수정된 페이지"만 일어나는데, 쓰기 트래픽이 높으면 짧은 시간에 메모리 점유량이 순간 2배 근처까지 튄다. 피크 메모리 여유가 없으면 OOM killer한테 잡히거나 swap 타면서 latency 급등.

대응: vm.overcommit_memory=1 커널 파라미터 필수(안 켜면 fork 실패). 스왑은 끄는 쪽이 낫고(장애로 swap 타면 차라리 빨리 죽는게 나음) Transparent Huge Pages도 꺼 둔다(echo never > /sys/kernel/mm/transparent_hugepage/enabled). THP가 켜져 있으면 2MB 페이지 단위라 COW 증폭 효과가 커진다.

모니터링용 INFO 값 메모.

  • rdb_changes_since_last_save: 마지막 저장 이후 쓰기 건수
  • rdb_bgsave_in_progress: 현재 bgsave 중인지
  • rdb_last_bgsave_time_sec: 지난 bgsave 소요 시간. 이게 10초 넘으면 슬레이브 replication에 영향
  • used_memory_peak vs used_memory: fork 피크 추정

일단 이 설정으로 하루 돌려보고, 문제없으면 슬레이브 한 대 붙여서 복제 구성까지 붙일 예정. slave-read-only yes로 두고.

20140123

Python 2.7 codecs.open 한글 인코딩 정리

파이썬 2.7에서 한글 들어간 텍스트 파일 읽다가 또 UnicodeDecodeError. 매번 까먹어서 정리.

기본부터 다시 짚고 가자. 2.7은 str이 bytes고 unicode가 유니코드 코드포인트 시퀀스다. 파일 읽기는 기본적으로 bytes를 돌려주고, 그 바이트열을 어떤 인코딩으로 해석할지는 개발자가 결정해야 함. open()은 encoding 인자가 없으니 codecs 써야 한다.

# -*- coding: utf-8 -*-
import codecs

# 읽기 — euc-kr 로 저장된 파일
with codecs.open("input.txt", "r", encoding="euc-kr") as f:
    text = f.read()   # 여기서 text 타입은 unicode

# 쓰기 — utf-8 로 내보내기
with codecs.open("output.txt", "w", encoding="utf-8") as f:
    f.write(text)

주의사항 몇 가지.

1. codecs.open 으로 읽은 결과는 이미 unicode. 여기서 .decode("utf-8") 또 호출하면 에러. 파이썬 2의 유니코드 객체에 .decode를 부르면 내부적으로 먼저 ascii로 encode → 다시 decode 하는 괴상한 경로를 타기 때문에 한글 들어있으면 바로 터진다. 내가 매번 하는 실수.

2. stdout 인코딩 문제. 터미널에 print u"안녕" 하면 되다가, 이걸 파일로 리다이렉트하면 sys.stdout.encoding이 ascii가 되면서 UnicodeEncodeError: 'ascii' codec can't encode .... 해결책:

import sys, codecs
sys.stdout = codecs.getwriter("utf-8")(sys.stdout)

3. cp949 vs euc-kr. 윈도우에서 온 파일은 euc-kr이 아니라 cp949로 읽어야 한다. cp949는 euc-kr 상위집합이라 "똠방각하" 같은 확장문자를 euc-kr로 디코드하면 예외가 난다. 반대로 cp949로 열면 둘 다 읽힘. 맥/리눅스에서 만든 euc-kr 파일도 cp949로 읽어 실용상 문제 없다.

4. BOM. 윈도우 메모장이 UTF-8로 저장하면 앞에 \xef\xbb\xbf(BOM) 붙인다. utf-8로 열면 맨 앞 글자가 \ufeff로 들어와서 "왜 키가 안 맞지"로 하루 날림. utf-8-sig 쓰면 BOM 자동 제거.

5. 모듈 기본 인코딩. 옛날 코드에서 sys.setdefaultencoding("utf-8") 하는 경우 있는데 이건 지양. sitecustomize나 reload(sys) 꼼수 말고, 필요한 시점에 명시적으로 encode/decode 하는 게 맞다. 암묵 변환에 의존하면 다른 사람 코드랑 섞일 때 재앙.

6. CSV 읽기. csv 모듈은 2.7에서 유니코드를 직접 못 다룬다. bytes 레벨로 읽은 뒤 각 필드를 decode 해줘야 함. 정석은 unicodecsv 서드파티 패키지. 회사 내부 배치는 일단 list(csv.reader(open(f))) → 각 row comprehension으로 decode.

import csv
rows = [[c.decode("cp949") for c in row]
        for row in csv.reader(open("a.csv", "rb"))]

7. JSON. json.dumps는 default로 ensure_ascii=True라 한글이 \u314f 식으로 나간다. API 응답 찍어보다가 당황하는 포인트. ensure_ascii=False 주고 대신 encode("utf-8")로 명시적으로 bytes로 변환.

파이썬 3에서는 이 모든 게 많이 정리됐다고 한다(str == unicode, bytes 분리). 레거시가 2.6/2.7이라 당분간 이렇게 살아야 함. 언젠가는 3으로 옮길 텐데 유니코드 → 문자열 리터럴, print() 함수화, urllib 재편 같은 걸 어떻게 처리할지 생각해두는 중.