Tx-cache is a project mainly written in Python, it's free.
Memcache server realization with Twisted
В связи с выходными потратил немного времени на реализацию сервера Memcache с использованием python-фреймворка Twisted. В итоге я получил быстродействие в два раза более низкое, что я не считаю очень критичным, а также возможность реализовать парочку расширений оригинального протокола. Также возможны оптимизации, которые еще улучшат быстродействие.
Протокол не был реализован полностью - есть еще моменты над которыми можно поработать, но стандартные set/get вполне работоспособны и готовы к использованию.
Для хранения кеша используем базовый класс dict. Как вы догадываетесь, реализация dict в python быстра, этот базовый тип используется в python настолько активно, что его не оставили без детальной оптимизации. Таким образом, мы автоматом имеем структуру для хранения кеша в памяти. Осталось реализовать протокол memcache, для предоставления доступа к dict другим программам.
Для реализации сервера используем Twisted. Есть множество вариаций неблокирующего IO для python на сегодня, но Twisted это уже классика, и имеет в своем арсенале достаточно средств для легкого решения подобных задач.
Как реализуют протоколы? Первым делом вам конечно же нужно найти описание протокола. Я нашел его здесь - http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt
После прочтения протокола становится понятно, что от клиента мы получим одну или две строки, причем первую строку мы можем смело разбивать на элементы по пробелам. Вторая строка используется в командах, которые передают серверу данные - set, add, replace и т.п. Если вам хочется подробнее вникнуть в статью, то отправлю вас почитать описание самостоятельно, цели выложить его перевод сюда не было.
Вооруженные этим знанием, смотрим, что нам может предложить Twisted для решения этой задачи, и сразу находим LineOnlyReceiver - протокол из базовой поставки Twisted, который работает только с протоколами, обменивающимися строками, то есть то, что надо.
class MemcacheProtocol(LineOnlyReceiver):
"""
Реализует базис протокола - прием сообщений от клиента
и отдачу результата.
"""
def lineReceived(self,line):
debug(repr(line))
if not 'parameters' in self.instruction:
parameters = line.split(' ')
debug("Got new command "+parameters[0])
self.instruction['parameters']=parameters
# Если данных не ожидается, то к исполнению
if parameters[0] in Cache.oneline_commands:
self.process()
else:
# Получены данные к двухстрочной команде, к исполнению
debug("Got data "+line)
self.instruction['data']=line
self.process()
def process(self):
# Cache.call возвращает генератор
for line in Cache.call(self.instruction):
# И мы отсылаем все что он нагенерирует отдельными строками
debug("Send line "+line)
self.sendLine(line)
# Готовы к дальнейшим инструкциям, насяльника!
self.instruction={}
def connectionMade(self):
debug("Connected!")
self.instruction={}
Как видно из кода, для собственно работы используется Cache. Это синглетон, по сути просто класс, методы которого обернуты декоратором @classmethod. Вызов Cache.call должен вернуть генератор, которые будет возвращать строки, которые, в свою очередь, наша реализация протокола, будет отдавать клиенту.
Первая строка это команда и параметры, разделенные пробелами, поэтому используем строковый метод split, и на выходе получаем список. Далее его надо разобрать на составляющие, перед тем как с данными начнет работать команда. Я использую класс, так как мне нравится перспектива обращаться к параметрам, указывая их через точку. Приведенный ниже код уже требует прочтения описания протокола, а для ленивых пара наводящих строк:
Команды записи данных:[noreply] cas [noreply] Получение данных: get * gets * delete Ну и тому подобное.
Реализация разбора:
class Instruction(object):
def __init__(self, i):
p = i['parameters']
self.cmd = p.pop(0)
# Проверяем noreply
if p[-1]=='noreply':
self.reply=False
# Выкидываем его
p.pop(-1)
else:
self.reply=True
if self.cmd in Cache.storage_commands:
# Если CAS то есть еще один параметр (т.е. особый случай)
if self.cmd == "cas":
self.unique = p.pop(-1)
# Теперь все параметры однозначны, но мы хотим расширить протокол,
# потому все не так просто, как dict(zip())
self.bytes = p.pop(-1)
self.exptime = p.pop(-1)
self.flags = p.pop(-1)
self.data = i.get('data',None)
# incr, decr
elif self.cmd in ["incr","decr"]:
self.change_value = p.pop(-1)
self.keys = p
def __str__(self):
return str(self.__dict__)
Протокол мною сразу же был расширен, а именно есть возможность работы с вложенными данными. Кеш переделан в древовидный, и все операции, которые по стандарту указывают один ключ, могут указывать список ключей, разделенных пробелами. Впрочем от этого легко избавиться, но тогда будет совсем неясен смысл работы.
В качестве единицы хранения реализован класс Entry, в котором содержится словарь(childs типа dict) с дочерними экземплярами Entry. Более того - верхней точкой в иерархии также является экземпляр класса Entry.
Здесь же я приведу фрагмент синглетона Cache:
class Cache(object):
# consts
storage_commands = ["set", "add", "replace", "append", "prepend","cas"]
oneline_commands = ["get", "gets","getn", "delete", "incr", "decr", "stats"]
# cache storage
data = Entry(0,0,0)
# cache operations
@classmethod
def call(cls, instruction):
i = Instruction(instruction)
debug(i)
command = getattr(cls,i.cmd)
return command(i)
@classmethod
def set(cls, i):
"set, поддержка вложенных ключей"
parent = cls.data.get_child(i.keys[:-1])
if parent:
parent.set_child(i.keys[-1], Entry(i.data,i.flags,i.exptime))
yield "STORED"
else:
yield "NOT_STORED"
@classmethod
def get(cls, i):
"get, не обрабатывает вложенные ключи"
for key in i.keys:
entry = cls.data.get_child([key])
if entry:
yield ' '.join(( "VALUE", key, entry.flags, str(len(entry.data)) ))
yield entry.data
yield "END"
Код Entry и всего остального смотрим тут - http://github.com/Deepwalker/tx-cache/blob/master/mck.py