Pull to refresh

Twisted в действии — memcache на python

Reading time 8 min
Views 7.5K

Преамбула


В связи с выходными потратил немного времени на реализацию сервера Memcache с использованием python-фреймворка Twisted. В итоге я получил быстродействие в два раза более низкое, что я не считаю очень критичным, а также возможность реализовать парочку расширений оригинального протокола. Также возможны оптимизации, которые еще улучшат быстродействие.
Протокол не был реализован полностью — есть еще моменты над которыми можно поработать, но стандартные set/get вполне работоспособны и готовы к использованию.

Средства


Для хранения кеша используем базовый класс dict. Как вы догадываетесь, реализация dict в python быстра, этот базовый тип используется в python настолько активно, что его не оставили без детальной оптимизации. Таким образом, мы автоматом имеем структуру для хранения кеша в памяти. Осталось реализовать протокол memcache, для предоставления доступа к dict другим программам.

Для реализации сервера используем Twisted. Есть множество вариаций неблокирующего IO для python на сегодня, но Twisted это уже классика, и имеет в своем арсенале достаточно средств для легкого решения подобных задач.



Реализация сетевого протокола


Как реализуют протоколы? Первым делом вам конечно же нужно найти описание протокола. Я нашел его здесь — code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt

После прочтения протокола становится понятно, что от клиента мы получим одну или две строки, причем первую строку мы можем смело разбивать на элементы по пробелам. Вторая строка используется в командах, которые передают серверу данные — set, add, replace и т.п. Если вам хочется подробнее вникнуть в статью, то отправлю вас почитать описание самостоятельно, цели выложить его перевод сюда не было.

Вооруженные этим знанием, смотрим, что нам может предложить Twisted для решения этой задачи, и сразу находим LineOnlyReceiver — протокол из базовой поставки Twisted, который работает только с протоколами, обменивающимися строками, то есть то, что надо. Еще в Twisted есть уже реализация memcache-протокола, но только клиентской его части, а мы делаем сервер.

 1 class MemcacheProtocol(LineOnlyReceiver):
 2     """
 3     Реализует базис протокола — прием сообщений от клиента
 4     и отдачу результата.
 5     """
 6
 7     def lineReceived(self,line):            
 8         debug(repr(line))
 9         if not 'parameters' in self.instruction:
10             parameters = line.split(' ')
11             debug("Got new command "+parameters[0])
12             self.instruction['parameters']=parameters
13
14             # Если данных не ожидается, то к исполнению
15             if parameters[0] in Cache.oneline_commands:
16                 self.process()
17         else:
18             # Получены данные к двухстрочной команде, к исполнению
19             debug("Got data "+line)
20             self.instruction['data']=line
21             self.process()
22
23     def process(self):
24         # Cache.call возвращает генератор
25         for line in Cache.call(self.instruction):
26             # И мы отсылаем все что он нагенерирует отдельными строками
27             debug("Send line "+line)
28             self.sendLine(line)
29         # Готовы к дальнейшим инструкциям, насяльника!
30         self.instruction={}
31
32     def connectionMade(self):
33         debug("Connected!")
34         self.instruction={}


Как видно из кода, для собственно работы используется Cache. Это синглетон, по сути просто класс, методы которого обернуты декоратором @classmethod. Вызов Cache.call должен вернуть генератор, которые будет возвращать строки, которые, в свою очередь, наша реализация протокола, будет отдавать клиенту.

Разбираем запрос от клиента


Первая строка это команда и параметры, разделенные пробелами, поэтому используем строковый метод split, и на выходе получаем список. Далее его надо разобрать на составляющие, перед тем как с данными начнет работать команда. Я использую класс, так как мне нравится перспектива обращаться к параметрам, указывая их через точку. Приведенный ниже код уже требует прочтения описания протокола, а для ленивых пара наводящих строк:
                                                                                                                        
Команды записи данных:                                                                                                       
<command name> <key> <flags> <exptime> <bytes> [noreply]\r\n                                                                 
cas <key> <flags> <exptime> <bytes> <cas unqiue> [noreply]\r\n                                                               

Получение данных:
get <key>*\r\n   
gets <key>*\r\n  
delete <key>\r\n 

Ну и тому подобное.

Реализация разбора:

 1 class Instruction(object):
 2     def __init__(self, i):
 3         p = i['parameters']
 4         self.cmd = p.pop(0)
 5
 6         # Проверяем noreply
 7         if p[-1]=='noreply':
 8             self.reply=False
 9             # Выкидываем его
10             p.pop(-1)
11         else:
12             self.reply=True
13
14         if self.cmd in Cache.storage_commands:
15             # Если CAS то есть еще один параметр (т.е. особый случай)
16             if self.cmd == "cas":
17                 self.unique = p.pop(-1)
18
19             # Теперь все параметры однозначны, но мы хотим расширить протокол,
20             # потому все не так просто, как dict(zip())
21             self.bytes = p.pop(-1)
22             self.exptime = p.pop(-1)
23             self.flags = p.pop(-1)
24             self.keys=p
25             self.data = i.get('data',None)
26
27         # get и gets
28         elif self.cmd in ["get","gets","getn","delete"]:
29             self.keys = p
30
31     def __str__(self):
32         return str(self.__dict__)


Реализация хранения кеша и работы с ним


Протокол мною сразу же был расширен, а именно есть возможность работы с вложенными данными. Кеш переделан в древовидный, и все операции, которые по стандарту указывают один ключ, могут указывать список ключей, разделенных пробелами. Впрочем от этого легко избавиться, но тогда будет совсем неясен смысл работы.

В качестве единицы хранения реализован класс Entry, в котором содержится словарь(childs типа dict) с дочерними экземплярами Entry. Более того — верхней точкой в иерархии также является экземпляр класса Entry.

Здесь же я приведу фрагмент синглетона Cache:

 1 class Cache(object):
 2     # consts
 3     storage_commands = ["set", "add", "replace", "append", "prepend","cas"]
 4     oneline_commands = ["get", "gets","getn", "delete", "incr", "decr", "stats"]
 5
 6     # cache storage
 7     data = Entry(0,0,0)
 8
 9     # cache operations
10     @classmethod
11     def call(cls, instruction):
12         i = Instruction(instruction)
13         debug(i)
14         command = getattr(cls,i.cmd)
15         return command(i)
16
17     @classmethod
18     def set(cls, i):
19         "set, поддержка вложенных ключей"
20         parent = cls.data.get_child(i.keys[:-1])
21         if parent:
22             parent.set_child(i.keys[-1], Entry(i.data,i.flags,i.exptime))
23             yield "STORED"
24         else:
25             yield "NOT_STORED"


Код Entry и всего остального смотрим тут — github.com/Deepwalker/tx-cache/blob/master/mck.py

Поправки


Как справедливо было отмечено в комментариях, использование синглетона для хранения Cache несколько не оправдано, поэтому на github лежит поправленная версия, в которой исправлен этот досадный недостаток. Спасибо, drJonnie!

Отмазка


Чего я ожидаю от этой статьи? Я ожидаю, что умные люди, которых здесь множество, посмотрят мой код и ткнут носом в недостатки отсутствия академического образования. Возможно, кому-то эта статья окажется полезной, при возможных недостатках, здесь описан путь к реализации сетевых протоколов в ваших программах. Кто-то может использовать этот код для своих расширений memcache.
Tags:
Hubs:
+30
Comments 13
Comments Comments 13

Articles