* Dedicate ruby processes to a particular subset of locales
* Parallelize your memcache queries
* Break up locale files into MRU/LRU strings to reduce size
* Denormalize locales (in memory, cache, whatever) into single values for most common pages. (use with MRU/LRU above)
As an aside, still don't understand how process->kernelspace driver->platter is faster than process->kernelspace socket->process->RAM? Especially for random access patterns. I suspect a memcache misconfiguration?
We did initially try dedicating ruby processes to particular locales -- when we saw a big improvement we knew we were on the right track. Doing so permanently would be more difficult. To "follow the sun" we would need to shift capacity to Europe and Asia during some hours and back to the US in others.
Parallelizing memcache queries is difficult because we don't know ahead of time what translations will be required to render a page.
We /are/ only working with the most recently used strings. Strings not accessed in the last 4 days are not loaded.
I'm not sure what "denormalize locales" means exactly.
Sparkey is fast because the files end up in the filesystem cache and most of the work is done in C. Going through the dalli gem to grab the translations out of memcache causes a lot of temporary ruby objects to be created.
1. Have a database of rows which are say: translation_id, locale_id, translation_text
2. That is really a 2-D array, translation[translation_id][locale_id] = translation_text
3. Reshuffle to translation[locale_id][translation_id] = translation_text (note the swapping of the indexes)
4. Generate a map of page_url => [arary of tranlation IDs]. You can do this because the number of translation_ids of a given page doesn't change any faster than the translation_ids themselves.
5. Create an in-memory object of all the specific translation_ids for a specific page in a specific locale
For an implementation of 5 I think it'd be preferable to have one giant string that contains all the translation text in it and then a list/array of indices that you use to decide where in that one big string you pull your substring from. That might well be more memory efficient than many individual substrings (this is what they did to make HBase about 5x faster) and it'll definitely give you some cache locality goodness.
As an aside, still don't understand how process->kernelspace driver->platter is faster than process->kernelspace socket->process->RAM? Especially for random access patterns. I suspect a memcache misconfiguration?
The point is to use the file system cache, so in the common case you're not hitting platter. I'm assuming all these machines do is serve tons of web pages, so the common case will be VERY common and you will almost never hit platter (e.g. only on a deployment of new strings).
My intuition says that memcache is slower because of the extra process switching (and potential context switching). With their solution, you go from Ruby -> kernel in the common case. With the memcache solution, you have to go from Ruby -> kernel -> back to user space for memcache, EVERY time.
Also as far as I remember, using a local TCP socket isn't as fast as using a Unix socket or pipe. The network stack actually does a lot of stuff.
I like their solution a lot better. Fewer moving parts. Let the kernel manage the file system cache. The kernel knows how much physical memory you have and will aggressively use it. Memcache has no idea (AFAIK), and if you are too aggressive you could end up with page faults in the memcache process anyway.
The problem with using the kernel-managed the fs cache is that you don't control it. If the kernel decides it wants to reclaim that memory for some reason, well, guess what, you're going to the spinny metal bits.
Two memcached points:
1. You can run memcached over a unix socket.
2. You can have it use locked memory so it will never page fault.
I don't really see your point, because the kernel does plenty of other things on your behalf.
You don't control the process switching to memcached either. It's possible memcached will get descheduled, its data removed from L* caches, and your web server process will have to wait for it to be run again. With the FS cache solution you don't have that issue.
You could tune it (e.g. try to pin memcache to a dedicated core and pin web servers to other cores), but there are settings to tune FS cache behavior as well.
Same with mmap. The kernel can swap pages back to disk in that case too. It seems to boil down to the same thing, so not sure why people are so negative about using the file system. I bet you can write a test comparing serving data via mmap() vs via the file system and they will behave nearly identically.
Comments
Armchair quarterbacking:
* Dedicate ruby processes to a particular subset of locales
* Parallelize your memcache queries
* Break up locale files into MRU/LRU strings to reduce size
* Denormalize locales (in memory, cache, whatever) into single values for most common pages. (use with MRU/LRU above)
As an aside, still don't understand how process->kernelspace driver->platter is faster than process->kernelspace socket->process->RAM? Especially for random access patterns. I suspect a memcache misconfiguration?
Good points!
We did initially try dedicating ruby processes to particular locales -- when we saw a big improvement we knew we were on the right track. Doing so permanently would be more difficult. To "follow the sun" we would need to shift capacity to Europe and Asia during some hours and back to the US in others.
Parallelizing memcache queries is difficult because we don't know ahead of time what translations will be required to render a page.
We /are/ only working with the most recently used strings. Strings not accessed in the last 4 days are not loaded.
I'm not sure what "denormalize locales" means exactly.
Sparkey is fast because the files end up in the filesystem cache and most of the work is done in C. Going through the dalli gem to grab the translations out of memcache causes a lot of temporary ruby objects to be created.
How I read "denormalize locales" as:
1. Have a database of rows which are say: translation_id, locale_id, translation_text
2. That is really a 2-D array, translation[translation_id][locale_id] = translation_text
3. Reshuffle to translation[locale_id][translation_id] = translation_text (note the swapping of the indexes)
4. Generate a map of page_url => [arary of tranlation IDs]. You can do this because the number of translation_ids of a given page doesn't change any faster than the translation_ids themselves.
5. Create an in-memory object of all the specific translation_ids for a specific page in a specific locale
For an implementation of 5 I think it'd be preferable to have one giant string that contains all the translation text in it and then a list/array of indices that you use to decide where in that one big string you pull your substring from. That might well be more memory efficient than many individual substrings (this is what they did to make HBase about 5x faster) and it'll definitely give you some cache locality goodness.
As an aside, still don't understand how process->kernelspace driver->platter is faster than process->kernelspace socket->process->RAM? Especially for random access patterns. I suspect a memcache misconfiguration?
The point is to use the file system cache, so in the common case you're not hitting platter. I'm assuming all these machines do is serve tons of web pages, so the common case will be VERY common and you will almost never hit platter (e.g. only on a deployment of new strings).
My intuition says that memcache is slower because of the extra process switching (and potential context switching). With their solution, you go from Ruby -> kernel in the common case. With the memcache solution, you have to go from Ruby -> kernel -> back to user space for memcache, EVERY time.
Also as far as I remember, using a local TCP socket isn't as fast as using a Unix socket or pipe. The network stack actually does a lot of stuff.
I like their solution a lot better. Fewer moving parts. Let the kernel manage the file system cache. The kernel knows how much physical memory you have and will aggressively use it. Memcache has no idea (AFAIK), and if you are too aggressive you could end up with page faults in the memcache process anyway.
The problem with using the kernel-managed the fs cache is that you don't control it. If the kernel decides it wants to reclaim that memory for some reason, well, guess what, you're going to the spinny metal bits.
Two memcached points: 1. You can run memcached over a unix socket. 2. You can have it use locked memory so it will never page fault.
As others have noted, mmap is another option.
I don't really see your point, because the kernel does plenty of other things on your behalf.
You don't control the process switching to memcached either. It's possible memcached will get descheduled, its data removed from L* caches, and your web server process will have to wait for it to be run again. With the FS cache solution you don't have that issue.
You could tune it (e.g. try to pin memcache to a dedicated core and pin web servers to other cores), but there are settings to tune FS cache behavior as well.
Same with mmap. The kernel can swap pages back to disk in that case too. It seems to boil down to the same thing, so not sure why people are so negative about using the file system. I bet you can write a test comparing serving data via mmap() vs via the file system and they will behave nearly identically.