The Graph with a Trillion Edges

After looking at this thread on Hacker News about this paper from a bunch of Facebook researchers, and this excellent article by Frank McSherry, I tried to understand McSherry’s approach (since I found the paper kind of impenetrable) and this is my take on it.

The Basic Problem

Let’s suppose you’re Facebook and you have about 1B members who have an arbitrary number of links to each other. How do you scale and parallelize queries (especially iterated queries) on the data?

McSherry basically implements a single thread algorithm on his laptop using brute force. (He uses a publicly available database of 128B edges.) As I understand it, his approach is pretty simple:

  • Encode each connection as a 64 bit number (think connection from A (32 bits) to B (32 bits).
  • Store the numbers in RAM as a sequence of variable-length integer encoded differences. E.g. a set of numbers beginning with 4, 100, 2003, 2005,… would be encoded as 4, 96, 1903, 2,… Since the average distance between 128B values scattered among 1T is 8, the expected distance between connections (64-bit values) will be around 9, which we can encode as ~4 bits of data (using variable-length encoding) instead of 64 bits, and storage needed is proportional to number of values stored [1]).
  • Look for a connection from A to anyone by running through the list and looking for values starting with A’s 32 bits. (You can make this search arbitrarily efficient — trading memory requirements for performance — by partitioning the list into a lookup table.)
  • Store the numbers on disk as deltas between the numbers encoded as a Hilbert Curve [2] using variable-length integer encoding for efficiency. (The naive encoding has an average of ~5bits between values; the optimized encoding was 2.

[1] Each entry will be log(n) bits in size, there’ll be c of them, and the average difference (to be encoded) will be log(c/n) bits. If we assume c scales as n^2 (highly dubious — it means if the world’s population increased by a factor of 2, I’d have twice as many friends) then we get n^2 * log(n) for storage. If we assume c scales as n log(n) (probably still generous) then we get n log(n) * log(log(n)). At any given scale that function looks pretty linear, although it is actually faster than linear — the gradient hangs around 5 for n between 1B and 100B — I don’t think it’s a cause for concern.

[2] A simple way of looking at Hilbert Curve encoding is that it treats a binary number as a series of 2-bit numbers, with each pair of bits selecting a sector of a 2×2 square (in a somewhat non-obvious way). So all numbers starting with a given pair of bits are found in the same sector. Then, look at the next two bits and subdivide that sector in a similarly non-obvious way until you run out of bits. (In an earlier post, McSherry explains Hilbert Curve implementation a graphically.) Incidentally, simply interleaving the bits of the two numbers has much the same effect.

That’s it in a nutshell. Obviously there are plenty of simple ways to optimize the lookups.

He points out that the database he used comes in 700 files. These could each be processed by 700 different computers (stored slightly less efficiently, because the average distance between deltas goes up slightly) and any request could easily be split among them.

The interesting thing is that running in one thread on a laptop, this approach runs faster and scales better than the 128 core systems he benchmarks against.

Optimizing

So let’s suppose I wanted to emulate Facebook and have access to 256 virtual machines. (I could easily afford to do this, as a startup, using AWS or similar.) How would I do this in practice? Remember that Facebook also has to deal with users creating new connections all the time.

First of all (kind of obviously) every connection gets stored twice (i.e. connections are two-way). We need this for worst-case scenarios.

Let’s suppose I number each of my virtual machines with an 8-bit value. When I store a connection between A and B I encode the connection twice (As AB and BA) and take the last 8 bits of one value and record them as a connection on machines with the corresponding number. Each machine stores the value in its representation of its link database.

How would this work in practice?

Well, each value being stored is effectively 56-bits (8 bits are pulled off the 64-bit value to pick the machine are and thus the same). Let’s divide that into 32-bits and 24-bits and store (in some efficient manner) 2^24 lists of 32-bit numbers, again stored in some efficient manner (we can break down and use McSherry’s approach at this point — effectively we’re progressively using McSherry’s approach on different portions of the numbers anyway).

So any lookup will entail grabbing 24-bits of the remaining 56-bits leaving us with 1/2^32 of the original data to search. Our efficient encoding algorithm would be to split the remaining data using the same approach among the cores, but I’m sure you get the idea by now.

So in a nutshell:

To record the existence of the edge AB:

  1. Encode as 2 64-bit values.
  2. Reduce to two 56-bit values and 2 8-bit values.
  3. Ask the 2 (or occasionally 1) machines designated for the 2 8-bit values.
  4. Each machine reduces the 56-bit value to a 32-bit value with a 24-bit lookup.
  5. Then inserts the remaining 32-bit value in a list of ~E/2^32 values (where E is the total number of edges, so 232 values for a 1T vertex DB)

Inserting a value in a list of n values is a solved O(log(n)) problem. Note that all connections Ax will be stored on one machine, but connections xA will be scattered (or vice versa) because we’re storing two-way connections. (Important for the worst case, see below.)

To find all edges from A:

  1. Grab 8-bits using the same algorithm used for storing AB.
  2. Ask the designated machine for the list of ~E/2^32 connections from A.

So to sum up — storing a connection AB involves bothering two machines. Determining if A’s connections involves bothering 1 machine in most cases, and (see below) all the machines in rare cases. However, the rare cases should never actually exist.

Worst Case Response

One problem here is that the worst case response could be huge. If A has 10M outgoing edges then one machine is going to have to cough up one giant database. (If one node has more connections than can easily be stored on one machine then we can deal with that by getting our machine names from the entropic bits of both user ids, but let’s ignore that for now.)

In sufficiently bad cases we reverse the lookup. And reverse lookups will never be bad! If we hit the machine containing all of Ax for all connections of the form Ax — there are too many to deal with, so the machine tells us to reverse the lookup, and we ask all our machines for connections xA, and we can reasonably expect those to be evenly distributed (if 256 bots sign up for and follow A simultaneously, there will be 256 new entries of the form Ax on one machine, but only one connection of the form xA on each machine).

So, the worst case performance of this design comes down to the cost of transmitting the results (which we can evenly distribute across our machines) — you can’t really do better than that, and the key step is treating each machine at having responsibility for one square in a grid picked using Hilbert Curves.

Note that the worst case isn’t terribly bad if we just want to count connections, so interrogating the database to find out how many outgoing links A has is practical.

Anyway, that never happens…

One can assume in the case of most social networks that while there will often be worst cases of the form A follows B (i.e. B has 10M followers) there will never be cases where A follows 10M users (indeed anyone like that could be banned for abuse long, long before that point is reached).

It follows that when someone posts something (on Twitter say) it only gets pulled by followers (it doesn’t get pushed to them). A posts something — solved problem, just update one list. B asks for update — we need to check B’s followers — solved problem. So we don’t even need to store two-way connections or worry about reverse lookups.

Incidentally…

This is apparently very similar to PageRank (Google’s algorithm for evaluating websites), which I’ve never bothered to try to understand.

Consider that each edge represents A links to B, and we rank a page by how many incoming links it has and we throw out pages with too many outgoing links (i.e. link farms, sitemaps). Then we iterate, weighting the value by the previously calculated values of each page. Google (et al) add to that by looking at the text in and around the link (e.g. “publicly available database of 128B edges”, above, links to a page about exactly that, so Google might infer that the linked page is “about” things in those words (or in the case of “click here” type links, look at words around the link).

So if from incoming links we infer that a page is about “scalable algorithms”, and from the quality of the pages with incoming links the quality of the page itself — and we maintain a database of connections between topics and pages — then when someone searches for “scalable algorithms”, we take the id of “scalable” and algorithms”, find pages about both, sort them by “quality”, create a web page, and festoon it with sponsored links and ads. By golly we’ve got a search engine!

Living saints and miracles

I heard the tail-end of a story on the radio this morning — something to do with Nelson Mandela being in hospital. (I’m very happy he’s still alive.) It got me to thinking about the amazing things that have happened in my lifetime and are happening today (in Egypt and Tunisia) which I certainly never saw coming.

The world I grew up in was the world of “Mutually Assured Destruction”. Dr. Strangelove. I remember chatting with friends in college about the fact that Canberra (where we were) was full of what we were sure were first strike targets (important communications and intelligence facilities shared with the US). We tried to be cleverly sardonic, but I, for one, certainly expected we were all doomed to die in a nuclear war, and not of old age.

By the end of the 80s, thanks to (depending on whom you ask) Ronald Reagan, Mikhael Gorbachev, John Paul II, Lex Walessa, or the inevitable failure of command economies, those fears were largely assuaged. The Soviet Union, which everyone simply assumed would keep on going forever, imploded. (It’s a lot like a game of Master of Orion. You want to conquer the galaxy by defeating the enemy’s death star, but instead there are one or two good fights early on, a long drawn-out period of one-sided skirmishes, and then your enemy’s economy collapses.)

Some time in the early 90s, one of my favorite science fiction writers (in fact he was “number two” on my list at the time) — Joe Haldeman — showed up in Australia and gave a writing workshop at the Powerhouse Museum in Sydney. Friends of mine told me about it and I drove there to attend. In the end it wasn’t so much a writing workshop as a standup comedy act (his description of the making of Robot Jox was, in particular, one of the funniest things I have ever heard).

When we broke for lunch my friends and I were a bit annoyed that other people monopolized Joe’s time, so at dinner we had a plan. We practically jumped him with a bottle of wine, settled down to dinner, and spent the entire evening with him and his wife chatting and drinking. (Lunch and dinner were both at the pub across the road from the museum.) I remember the entire day, and that evening in particular, as one of the most enjoyable experiences of my life.

Perhaps the most depressing turn of the conversation came when we discussed the likely fate of South Africa. Mandela had already been freed at that point, but no-one could have predicted that South Africa would peacefully transition from minority white rule and apartheid to majority democratic rule without a bloodbath and exodus of both people and money. We all assumed there would be a bloodbath.

I mention all this only because it’s easy to forget just what a stunning thing Nelson Mandela did. He changed one of the most wicked regimes in the world into a democracy pretty much without a shot being fired. He did it with forgiveness. (Let’s not forget the incredible Truth and Reconciliation Commission afterwards.)

On Martin Luther King’s birthday NPR was playing Dr. King’s incredible speech at the Lincoln Memorial. The fragment where he talks about the descendants of slaves and plantation owners playing together one day amazed me. Congresswoman Giffords had just been shot and liberals* were pointing at inflammatory rhetoric from the right while the right tried to label the (alleged) gunman a Marxist.

It struck me how petty the concerns of the political classes in the US are (more or fewer gun rights, lower or higher taxation, health benefits) compared to those of Nelson Mandela and Martin Luther King, and how the former have no compassion or forgiveness or — it seems to me — much desire for reconciliation while the latter can both speak eloquently to the point and, in the case of Nelson Mandela, actually live up to their own rhetoric and be graceful in victory.

Note: * I’d say that I’m one of them, but when the Republicans can call the US Senate majority the “far left” without being laughed at, I’m clearly not actually on the US political spectrum.

John Paul II has just been beatified. It’s the first major step on the path to “sainthood”. (Eventually he’ll have to perform some “miracles”.) Is there anyone alive more deserving of the title “saint” than Nelson Mandela? And surely the transition of South Africa from apartheid to democracy without bloodshed is one of the great miracles of our age.

Perhaps my next proposed saint will be Zuckerberg — his miracles will include the reform of the New Jersey school system and the victory of democracy in the Arab world. Wouldn’t it be funny if an American website created by a Jew/Atheist turns out to do what George W. Bush’s Evangelical Neocons so spectacularly and expensively and bloodily failed to do?