- Wafris는 Rails 미들웨어 클라이언트 v1의 사용자 소유 Redis 저장소를 v2에서 SQLite 기반 저장소로 바꾸며, 배포 난이도와 요청 평가 지연을 함께 줄이려 함
- 기존 Redis 선택은 Heroku와 Sidekiq 같은 Rails 생태계의 관성에 영향을 받았지만, 실제 운영에서는 사용자가 Redis 관리자 역할까지 떠안는 문제가 커짐
- 모든 인바운드 HTTP 요청을 규칙과 비교하는 읽기 경로가 핵심 병목이며, 보고용 쓰기는 느리게 처리하거나 배치·비동기로 분리할 수 있음
- 로컬 MacBook Air M2에서 120만 개 범위 데이터셋으로 최악의 IP 범위 조회를 테스트한 결과, SQLite는 로컬 Redis보다 약 3배 빠른 성능을 보였고 네트워크 지연은 포함되지 않음
- v2는 일정 시간이나 요청 수 기준으로 새 규칙을 확인한 뒤 새 SQLite DB 전체를 내려받는 동기화 구조를 쓰며, 성공적인 설치가 약 3배 증가함
Redis 기반 v1에서 드러난 배포 마찰
- Wafris는 오픈소스 웹 애플리케이션 방화벽 회사로, Rails 애플리케이션에 붙는 미들웨어 클라이언트를 제공함
- v1 클라이언트는 애플리케이션과 함께 배포되는 사용자 소유 Redis 저장소를 필요로 했음
- 초기 Redis 선택에는 Heroku에서 Redis를 쉽게 붙일 수 있었던 환경, 원격 접근의 편의성, Sidekiq 같은 성공 사례가 영향을 줌
- 실제 사용자 환경은 더 다양했고, 많은 사용자가 Redis 배포와 설정 문제를 디버깅하는 데 어려움을 겪음
- RailsWorld 2023에서도 Rails 애플리케이션 옆에 Redis 서버가 당연히 필요하다는 가정에 부정적인 분위기가 있었음
속도 문제의 핵심은 네트워크 지연
- Redis는 전통적인 RDBMS와 비교하면 빠르지만, 별도 데이터베이스인 만큼 연결·메모리·프로세스 관리를 요구함
- 클라우드 환경에서는 네트워크 지연이 요청 처리 성능을 직접 흔듦
- Wafris는 애플리케이션으로 들어오는 모든 HTTP 요청을 저장된 규칙과 비교해야 함
- v1 클라이언트를 최대한 빠르게 만들어도, 애플리케이션이 배치된 네트워크가 느리면 전체 응답이 느려질 수 있었음
- Rails 앱이 항상 단일한 “majestic monolith” 형태로 배포되는 것도 아니었음
- 여러 존에 배포되는 앱
- 책임이 겹치는 여러 서버로 나뉜 앱
- 일부만 Rails이고 다른 언어나 프레임워크와 함께 배포되는 앱
- 이런 운영 환경에서는 Redis 사용 마찰이 더 커짐
Wafris 요청 처리에서 읽기와 쓰기 분리
- Wafris는 Rails 미들웨어로 설치되며, “IP 1.2.3.4 차단” 같은 규칙을 설정한 뒤 들어오는 요청을 해당 규칙과 비교함
- 단순화한 처리 흐름은 두 단계임
- HTTP 요청을 규칙과 비교해 매칭되면 403, 아니면 200 처리
- 차단·허용·통과 같은 처리 결과를 보고
- 첫 번째 단계인 규칙 읽기는 보고 쓰기보다 훨씬 중요함
- 요청은 순차적으로 처리되어야 함
- 필터링이 동작하지 않으면 나쁜 요청이 통과할 수 있음
- 요청 평가는 사용자가 체감하는 사이트 성능에 영향을 줌
- 보고 쓰기는 더 느리게, 배치로, 비동기로 처리 가능함
SQLite를 선택한 이유
- Wafris의 주요 병목은 네트워크 I/O였고, SQLite 문서의 “SQLite는 클라이언트/서버 데이터베이스와 경쟁하지 않는다. SQLite는 fopen()과 경쟁한다”는 문장이 선택에 영향을 줌
- 네트워크 왕복을 없애는 것만으로도 Redis 기반 구조보다 빨라질 수 있다는 가정 아래 SQLite와 Redis를 벤치마크함
- 참고 자료는 다음과 같음
- Aaron Francis의 “High Performance SQLite”: https://highperformancesqlite.com/
- Stephen Margheim의 SQLite on Rails - the how and why of optimal performance
- Oldmoe: https://oldmoe.blog/
벤치마크 범위와 의도적 한계
- 벤치마크는 보편적인 데이터베이스 성능 비교가 아니라, Wafris의 핫 패스와 최악의 쿼리를 겨냥한 편향된 테스트였음
- 최악의 쿼리는 IP 범위와 카테고리를 매핑하는 “lexical decimal” 데이터 구조에 대한 조회였음
- 단순한 예로, IP 주소가 두 주소 사이 범위에 있는지 확인해 국가를 반환하는 IP → 국가 매핑이 있음
- 이 구조는 수백만 행 규모이고, IPv6에서는 각 항목이 큼
- 범위 조회는 미리 계산한 뒤 두 저장소에 기록함
- SQLite의 테이블
- Redis의 sorted set
- 병적인 경우 각 인바운드 HTTP 요청은 요청 IP를 다음 범위들과 비교해야 함
- 사용자 정의 허용 범위
- 사용자 정의 차단 범위
- GeoIP 범위
- IP 평판 범위
- 이 쿼리 유형이 충분히 중요했기 때문에, 다른 쿼리나 기능은 포팅하지 않고 이 한 종류만 테스트함
테스트 방식과 결과
- 테스트는 로컬 MacBook Air M2에서 Homebrew로 설치한 Redis와 로컬 SQLite DB를 사용함
- 프로토콜은 다음과 같았음
- 기존 범위 데이터셋 120만 개 항목을 사용
- 여러 IP 집합을 같은 순서로 SQLite와 Redis에 실행
- 각 배수마다 테스트를 5회 실행하고 평균을 사용
- Wafris의 특정 사용 사례에서는 SQLite가 로컬 Redis보다 약 3배 빠른 성능을 보임
- 이 결과는 네트워크 지연을 고려하기 전의 수치임
- 테스트는 의도적으로 단순한 설정과 현실 사용의 결함을 반영했으며, 보편적인 데이터베이스 비교로 일반화하기는 어려움
차트에 드러나지 않는 운영상의 차이
- SQLite 성능이 벤치마크에서 Redis보다 크게 나빴더라도, 같은 데이터센터나 리전에 있는 Redis까지의 네트워크 지연 때문에 실제 환경에서는 더 빠를 수 있다고 판단함
- Redis 서버가 클러스터나 샤딩으로 견고하게 구성돼도 네트워크 대역폭, 연결 수, 리전 간 지연 같은 제한은 남아 있음
- SQLite는 각 컴퓨트 인스턴스에 로컬로 존재하므로, Wafris의 이 사용 사례에서는 수평 확장 비용이 거의 사라짐
- 온보딩도 SQLite 쪽이 더 단순함
- 사용자는 SQLite가 쓰이는지 몰라도 gem을 웹 앱에 추가하고 실행할 수 있음
- Redis에도 추가 최적화 여지는 많지만, Wafris는 사용자에게 캐시 축출 정책 같은 기본 설정 변경조차 일관되게 설득하기 어려웠음
SQLite 전환 뒤 필요한 구조 변경
- Redis 기반 v1의 업데이트 흐름은 단순했음
- 사용자가 Wafris Hub에서 규칙을 업데이트
- Wafris Hub가 사용자 Redis 저장소의 규칙을 업데이트
- SQLite에서는 Wafris Hub가 웹 서버로 SQLite 데이터베이스를 직접 “푸시”할 수 없었음
- 일부 SQLite as a service 제공자는 유사한 방식을 가능하게 하지만, 비용·성능·보안 고려 때문에 Wafris에는 맞지 않았음
- 개별 사용자가 이를 배포해야 함
- 포트를 열어야 함
- 인바운드 연결을 허용해야 함
- SQLite 기반 v2의 업데이트 흐름은 다음과 같음
- 사용자가 Wafris Hub에서 규칙을 업데이트
- 클라이언트가 시간 또는 요청 수 기준의 일정 간격으로 업데이트된 규칙을 확인
- 규칙이 바뀌었으면 클라이언트가 완전히 새로운 SQLite 데이터베이스를 다운로드
- 이 구조는 사용자 설치와 설정 책임을 크게 줄였고, v2 클라이언트의 성공적인 설치가 약 3배 증가함
분산 배포에서의 SQLite 구조
- AWS, Heroku, Fly 같은 클라우드 환경에서 Rails 앱이 오토스케일링되면 컴퓨트 인스턴스는 늘어나지만 데이터베이스가 함께 늘어나지는 않는 경우가 많음
- 요청이 100 req/s에서 10,000 req/s로 늘면 dyno, machine, EC2 인스턴스는 올라오지만 데이터베이스 병목은 그대로 남을 수 있음
- Wafris가 관찰한 Rails 앱 장애의 주요 원인은 실제 DDoS보다 크리덴셜 스터핑이나 나쁜 봇 트래픽인 경우가 많았음
- 이런 트래픽은 오토스케일링을 유발함
- 이후 데이터베이스 연결이 소진되며 앱이 넘어질 수 있음
- SQLite DB를 각 컴퓨트 인스턴스로 동기화하면, 새 인스턴스의 모든 호출을 로컬로 유지할 수 있음
쓰기 경로는 클라이언트에서 제거
- 앞선 테스트는 쓰기 작업을 고려하지 않았고, SQLite가 모든 역할에 적합하다고 보지도 않음
- Wafris는 SQLite를 읽기 중심 역할에 맞춰 사용하고, 쓰기 경로는 별도로 다시 설계함
- v2의 보고 경로는 다음처럼 바뀜
- Wafris Hub로 비동기 연결해 보고
- 보고 데이터를 배치 전송
- 클라이언트에서 데이터베이스 쓰기를 완전히 제거
- 이 구조는 다른 대부분의 서비스에는 맞지 않을 수 있지만, Wafris 사용자가 원하는 쉬운 배포와 빠른 클라이언트라는 목표에는 맞음
- SQLite 기반 v2 아키텍처는 이미 여러 사이트가 공격을 견디고 온라인 상태를 유지하는 데 도움을 줬고, Wafris의 지원 작업과 사용자 불편도 줄였음