Qcon London 2012에서 Arya Asemanfar의 발표입니다.
Timelines @ Twitter by Arya Asemanfa
Timeline Delivery
Twitter 의 사용자별 Timeline은 사용자가 자신이 following하는 사용자들의 tweet 목록을 볼 수 있는 화면으로 Twitter 서비스의 중심을 구성하는 UI 입니다.
Twitter의 Timeline 서비스는 기본적으로 새로운 Tweet이 발생할 때, 이를 구독하는 사용자들의 Timeline에 해당하는 목록에 Tweet ID를 추가해주고, 각 사용자들이 자신의 Timeline을 읽을 때는 이 목록을 가져가서 빠르게 Timeline을 표시할 수 있도록 하는 구조입니다.
Timeline에 대한 poll-based query는 200K qps 정도가 발생하고 response time은 1ms (median) 정도로, throughput과 response time 모두에서 높은 수준을 요하는 서비스입니다. 이는 Twitter가 전세계적으로 성공한 서비스임을 감안할 때 그리 높지 않은 수치들인 것 같은데, 어쩌면 모바일 사용자가 증가하면서 이제는 Timeline에 대한 조회가 poll-based가 아니라 push 위주이기 때문일지도 모르겠습니다.
새로운 Tweet이 쓰여지는 속도는 피크 시간대에 5k/sec 정도로 역시 생각보다는 그리 많지 않은 양이지만, follower로 인한 fan-out이 높은 서비스 특성 상, timeline에 대한 delivery는 300k/sec 정도라고 합니다. 100만명의 follower에게 delivery하는데에도 불과 3.5초 밖에 걸리지 않는다고 합니다.
Architecture
아래의 그림은 발표자료로부터 가져온 Search는 제외한 Timeline 관련 Architecture 그림입니다.
특이할만한 사항들을 간략하게 정리하면 다음과 같습니다.
- HTTP Proxy에서 어떤 API를 호출할지 결정이 됩니다.
- API layer와 Service layer가 잘 분리되어있습니다. 필요하다면 API가 다수의 Service를 사용할 수 있습니다.
- Tweet API의 경우 queue를 가지고 있어서 asynchronous하게 처리될 뿐만 아니라, 여러 시스템 (예를 들어, tweet 기록을 위한 tweet daemon과 검색 인덱싱을 위한 search blender)으로 이 request를 전송할 수 있는 것 같습니다.
- 다수의 follower에 대한 Timeline delivery를 위해서 수천명의 follower에 대한 delivery로 쪼개어 동시에 보낼 수 있습니다.
- Timeline cache는 Redis에 대한 partitioning layer로서 동작합니다.
- 사용자별 Timeline은 Redis의 list로 표현되며, list의 element에는 Tweet ID (8 bytes), User ID (8 bytes), bitfield (4 bytes), optional하게 Retweet ID가 들어갑니다.
- 특정 사용자에 대한 Timeline에 대한 query가 몰린다면 이를 탐지하여 Timeline service에서 해당 쿼리에 대해 in-process cache를 활성화합니다.
Finagle
이러한 여러 컴포넌트가 논리적, 물리적으로 분산되어 있는 Service-oriented Architecture를 위해서 Twitter에서는 JVM 기반의 RPC Library인 Finagle을 모든 컴포넌트에서 사용하고 있다고 합니다.
Finagle은 connection pooling, connection 수 제약 등의 기본적인 connection 관리는 물론 여러 프로토콜을 플러그인 방식으로 사용할 수 있어서 그야말로 모든 컴포넌트에서 사용할 수 있습니다. HTTP Proxy에서 필요로 하는 HTTP 프로토콜, API layer와 Service 컴포넌트가 통신하는데 사용하는 Thirft, Redis 까지도 모두 Finagle로 처리가 되고 있는 것 같습니다.
Finagle은 ZooKeeper 기반의 service discovery (혹은 location service)를 지원하기 때문에, RPC 호출을 위한 endpoint를 지정할 때는 호스트 이름이 아닌 클러스터의 이름을 사용할 수 있습니다.
Q&A
- HTTP Proxy는 JVM 기반으로 만들어져있고, Finagle을 기반으로 하고 있고 Routing, SPDY 지원등이 주요한 기능이라고 합니다. 초기에는 GC 문제로 고생했지만, 튜닝을 거쳐 CMS를 사용하고 나서 어느 정도 안정화 되었다고 합니다.
- HTTP Proxy, Timeline Service 등은 Scala로 만들어져 있고, Search에 관련된 시스템은 Java로 만들어져 있다고 합니다. Timeline 팀과 Search 팀이 나뉘어져 있어서 그런 모양입니다. 참고로 Finagle은 Scala로 만들어진 라이브러리입니다.
- Timeline은 디스크에 저장되지 않고 메모리에만 저장된다고 합니다.
- Durable Store – 아마도 MySQL 또는 Cassandra에 저장되는 데이터는 Tweets, Tweets에 대한 index 등 이라고 합니다. Twitter에서의 Durable Store에 관해서는 QCon 2011에서의 발표를 참고하라고 합니다.
- Partitioning을 위해서는 역시 github에 공개되어 있는 gizzard를 사용하고 있는 것 같습니다. (그런데 발표자는 snowflake라고 대답했군요.)
Closing
Timelines 아키텍쳐를 보고 가장 크게 느낀 점은 매우 높은 수준의 response time 요구사항을 가지고 있음에도 불구하고, 물리적으로 분리되어 있는 컴포넌트 사이의 RPC를 다수 포함하고 있는 아키텍쳐를 가지고 있다는 것입니다. 현재 고민하고 있는 시스템에서는 단 하나의 layer를 물리적으로 나누는 것에도 매우 망설이고 있는데, Twitter의 경우에는 tweets API만 하더라도 몇 단계의 RPC 통신을 필요로 하고 있습니다. 이것은 queue를 이용한 asynchronous한 API 처리 때문에 가능한 것 같습니다. 그 이외에도 물리적으로는 나누어져 있다고 하더라도 로컬 머신 내의 다른 daemon으로 리퀘스트를 먼저 보낸다든가 하는 최적화가 되어있을지도 모르겠습니다. 이론적으로는, 논리적인 분리만 이루어져있다면 성능 제약이 있지 않는 한 컴포넌트의 물리적인 분리는 불필요하지만, 실제로는 논리적인 분리의 강화, 성능 분석의 용이성 등의 여러가지 감추어진 이점이 있다고 생각합니다. 어쩌면 저는 물리적인 분리가 두려워서 가능한 해결책을 생각해보지도 않고 두려워하는 것일지도 모르겠습니다.
두번째는, Finagle과 같은 훌륭한 기반이 있기 때문에, 이러한 서비스 기반 아키텍쳐가 만들어질 수 있었다는 것입니다. Twitter의 경우에도 처음에는 단일한 서비스로 이루어져 있었을지도 모르겠지만, 분리의 필요성이 생겼을 때 Finagle과 같은 라이브러리 또는 Finagle이 이용하는 시스템이 기반이 되어준다면, 분리의 비용이 훨씬 낮기 때문에, 훨씬 쉽게 분리할 수 있을 것입니다.
세번째는 관찰가능성 (observability)입니다. 발표자가 Finagle에 대해서 설명할 때 살짝 얘기하고 지나갔지만, Finagle을 통한 RPC call에 관련한 여러가지 statistics들은 물론, 여러 컴포넌트들을 따라 일어난 RPC call의 trace도 역시 별도의 시스템으로 보내진다고 합니다. Twitter와 같은 여러 컴포넌트가 연동되어 있는 시스템에서는 한 컴포넌트의 실패가 다른 컴포넌트들의 연속적인 실패로 이어질 수 있으며 (Cascading Failure) 엄청난 재난이 될 수 있습니다. Twitter의 경우 (Finagle을 통해) 이러한 문제에 대한 대비가 모든 컴포넌트 사이에서 되어있고, 컴포넌트의 실패 또는 연속적인 실패 등을 쉽게 발견할 수 있도록 준비되어 있는 것 같습니다.
전반적으로 구조는 매우 단순해보이지만, 그러한 단순함을 얻기 위해, 수많은 경험을 바탕으로 쌓아올린 노력이 엿보이는 아키텍쳐인 것 같습니다.