Building a complete social networking platform presents many challenges at scale. Socialite is a reference architecture and open source Java implementation of a scalable social feed service built on DropWizard and MongoDB. We'll provide an architectural overview of the platform, explaining how you can store an infinite timeline of data while optimizing indexing and sharding configuration for access to the most recent window of data. We'll also dive into the details of storing a social user graph in MongoDB.
2. Solutions Engineering
• Identify Popular Use Cases
– Directly from MongoDB Users
– Addressing "limitations"
• Go beyond documentation and blogs
• Create open source project
• Run it!
8. Pluggable Services
• Major components each have an interface
– see com.mongodb.socialite.services
• Configuration selects implementation to use
• ServiceManager organizes :
– Default implementations
– Lifecycle
– Binding configuration
– Wiring dependencies
– see com.mongodb.socialite.ServiceManager
9. Simple Interface
https://github.com/10gen-labs/socialite
GET /users/{user_id} Get a User by their ID
DELETE /users/{user_id} Remove a user by their ID
POST /users/{user_id}/posts Send a message from this user
GET /users/{user_id}/followers Get a list of followers of a user
GET /users/{user_id}/followers_count Get the number of followers of a user
GET /users/{user_id}/following Get the list of users this user is following
GET /users/{user_id}/following count Get the number of users this user follows
GET /users/{user_id}/posts Get the messages sent by a user
GET /users/{user_id}/timeline Get the timeline for this user
PUT /users/{user_id} Create a new user
PUT /users/{user_id}/following/{target} Follow a user
DELETE /users/{user_id}/following/{target} Unfollow a user
11. Operational Testing
Real life validation of our choices.
Most important criteria?
User facing latency
Linear scaling of resources
12. Scaling Goals
• Realistic real-life-scale workload
– compared to Twitter, etc.
• Understanding of HW required
– containing costs
• Confirm architecture scales linearly
– without loss of responsiveness
14. Operational Testing
• All hosts in AWS
• Each service used its own DB, cluster or shards
• All benchmarks through `mongos` (sharded config)
• Used MMS monitoring for measuring throughput
• Used internal benchmarks for measuring latency
• Based volume tested on real life social metrics
17. Socialite Content Service
• System of record for all user content
• Initially very simple (no search)
• Mainly designed to support feed
– Lookup/indexed by _id and userid
– Time based anchors/pagination
18. Social Data Ages Fast
• Half life of most content is 1 day !
• Popular content usually < 1 month
• Access to old data is rare
19. Content Service
• Index by userId, _id
• Shard by userId (or userId, _id)
• Supports “user data” as pass-through
{
"_id" : ObjectId("52aaaa14a0ee0d44323e623a"),
"_a" : "user1",
"_m" : "this is a post”,
"_d" : {
"geohash" : "6gkzwgjzn820"
}
}
35. Operational comparison
• Updates of embedded arrays
– grow non-linearly with number of indexed array elements
• Updating edge collection => inserts
– grows close to linearly with existing number of edges/user
38. Finding Followers
Consider our single follower collection :
> db.followers.find({from : "djw"}, {_id:0, to:1})
{
"to" : "jsr"
}
Using index :
{
"v" : 1,
"key" : { "from" : 1, "to" : 1 },
"unique" : true,
"ns" : "socialite.followers",
"name" : "from_1_to_1"
}
Covered index
when searching on
"from" for all
followers
Specify only if
multiple edges
cannot exist
39. Finding Following
What about who a user is following?
Can use a reverse covered index :
{
"v" : 1,
"key" : { "from" : 1, "to" : 1 },
"unique" : true,
"ns" : "socialite.followers",
"name" : "from_1_to_1"
}
{
"v" : 1,
"key" : { "to" : 1, "from" : 1 },
"unique" : true,
"ns" : "socialite.followers",
"name" : "to_1_from_1"
}
Notice the flipped
field order here
40. Finding Following
Wait ! There is an issue with the reverse index…..
SHARDING !
{
"v" : 1,
"key" : { "from" : 1, "to" : 1 },
"unique" : true,
"ns" : "socialite.followers",
"name" : "from_1_to_1"
}
{
"v" : 1,
"key" : { "to" : 1, "from" : 1 },
"unique" : true,
"ns" : "socialite.followers",
"name" : "to_1_from_1"
}
If we shard this collection
by "from", looking up
followers for a specific
user is "targeted" to a
shard
To find who the user is
following however, it must
scatter-gather the query to
all shards
42. Dual Edge Collections
When "following" queries are common
– Not always the case
– Consider overhead carefully
Can use dual collections storing
– One for each direction
– Edges are duplicated reversed
– Can be sharded independently
43. Edge Query Rate Comparison
Number of shards
vs
Number of queries
Followers collection
with forward and
reverse indexes
Two collections,
followers, following
one index each
1 10,000 10,000
3 90,000 30,000
6 360,000 60,000
12 1,440,000 120,000
45. Feed Service
• Two main functions :
– Aggregating “followed” content for a user
– Forwarding user’s content to “followers”
• Common implementation models :
– Fanout on read
• Query content of all followed users on fly
– Fanout on write
• Add to “cache” of each user’s timeline for every post
• Various storage models for the timeline
47. Fanout On Read
Pros
Simple implementation
No extra storage for timelines
Cons
– Timeline reads (typically) hit all shards
– Often involves reading more data than required
– May require additional indexing on Content
49. Fanout On Write
Pros
Timeline can be single document read
Dormant users easily excluded
Working set minimized
Cons
– Fanout for large follower lists can be expensive
– Additional storage for materialized timelines
50. Fanout On Write
• Three different approaches
– Time buckets
– Size buckets
– Cache
• Each has different pros & cons
51. Timeline Buckets - Time
Upsert to time range buckets for each user
> db.timed_buckets.find().pretty()
{
"_id" : {"_u" : "jsr", "_t" : 516935},
"_c" : [
{"_id" : ObjectId("...dc1"), "_a" : "djw", "_m" : "message from daz"},
{"_id" : ObjectId("...dd2"), "_a" : "ian", "_m" : "message from ian"}
]
}
{
"_id" : {"_u" : "ian", "_t" : 516935},
"_c" : [
{"_id" : ObjectId("...dc1"), "_a" : "djw", "_m" : "message from daz"}
]
}
{
"_id" : {"_u" : "jsr", "_t" : 516934 },
"_c" : [
{"_id" : ObjectId("...da7"), "_a" : "ian", "_m" : "earlier from ian"}
]
}
53. Timeline - Cache
Store a limited cache, fall back to "fanout on read"
– Create single cache doc on demand with upsert
– Limit size of cache with $slice
– Timeout docs with TTL for inactive users
> db.timeline_cache.find().pretty()
{
"_c" : [
{"_id" : ObjectId("...dc1"), "_a" : "djw", "_m" : "message from daz"},
{"_id" : ObjectId("...dd2"), "_a" : "ian", "_m" : "message from ian"},
{"_id" : ObjectId("...da7"), "_a" : "ian", "_m" : "earlier from ian"}
],
"_u" : "jsr"
}
{
"_c" : [
{"_id" : ObjectId("...dc1"), "_a" : "djw", "_m" : "message from daz"}
],
"_u" : "ian"
}
54. Embedding vs Linking Content
Embedded content for direct access
– Great when it is small, predictable in size
Link to content, store only metadata
– Read only desired content on demand
– Further stabilizes cache document sizes
> db.timeline_cache.findOne({”_id" : "jsr"})
{
"_c" : [
{"_id" : ObjectId("...dc1”)},
{"_id" : ObjectId("...dd2”)},
{"_id" : ObjectId("...da7”)}
],
”_id" : "jsr"
}
55. Socialite Feed Service
• Implemented four models as plugins
– FanoutOnRead
– FanoutOnWrite – Buckets (size)
– FanoutOnWrite – Buckets (time)
– FanoutOnWrite - Cache
• Switchable by config
• Store content by reference or value
• Benchmark-able back to back
60. Benchmarking the Feed
• Results
– over two weeks
– ran load with one million users
– ran load with ten million users
– used avg send rate 1K/s; 2K/s; reads 10K-20k/s
– 22 AWS c3.2xlarge servers (7.5GB RAM)
– 18 across six shards (3 content, 3 user graph)
– 4 mongos and app machines
– 2 c2x4xlarge servers (30GB RAM)
– timeline feed cache (six shards)
62. Socialite
https://github.com/10gen-labs/socialite
• Real Working Implementation
– Implements All Components
– Configurable models and options
• Built-in benchmarking
• Questions?
– I will be at "Ask The Experts" this afternoon!
https://github.com/10gen-labs/socialite
News/Social Status Feed: popular and common
Internal goals: implement different schema options, builtin benchmarking for comparison
External goals: low latency from end-user perspective, linear scaling from operational perspective
News/Social Status Feed: popular and common
Internal goals: implement different schema options, builtin benchmarking for comparison
External goals: low latency from end-user perspective, linear scaling from operational perspective
image at https://dropwizard.github.io/dropwizard of the hat
add REST API calls
How to test, show how growing documents are very painful to update.
Add the MTV or appmetrics mtools plot showing what happens to outliers.
actual performance – show how inserting million users was easy – no point even trying to update embedded documents...
side-point of
Variants?
Should you embed the messages/content into "cache"/buckets/etc. or just store references?
WHICH ONE DID WE IMPLEMENT IN SOCIALITE???
All work with Async Service(? or mention later)
And we did benchmark them! -> Asya
examining latency of reading content by fanout type - note two types of latency – for sender and for recipient.
scaling throughput... THIS WILL NOT SCALE LINEARLY(!)
*RERUN WITH SEVERAL SHARDS* replace with new screenshot
MongoDB as a cache
Storage amplification on a feed service – Justin Bieber makes a single post and we need to write it to 2 million timelines.... ???
Cache only for active users.
Number of updates across all cache / number of documents updated
MongoDB as a cache
Storage amplification on a feed service – Justin Bieber makes a single post and we need to write it to 2 million timelines.... ???
Cache only for active users.
MongoDB as a cache
Storage amplification on a feed service – Justin Bieber makes a single post and we need to write it to 2 million timelines.... ???
Cache only for active users.