Преамбула
В связи с выходными потратил немного времени на реализацию сервера 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]\r\n cas [noreply]\r\n Получение данных: get *\r\n gets *\r\n delete \r\n Ну и тому подобное.
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
4 комментария:
привет помоги новечку не могу понять как запустить твой код при запуске через twistd -y %
File "nested_cache.py", line 7, in ?
from mck import *
exceptions.ImportError: No module named mck
Failed to load application: No module named mck
Слишком много вариантов. Попробуй выполнить команду из того же каталога, где mck.py лежит.
+ обновил git.
тот же результат обновил git
запускаю twistd -y %
результат тот же не видет хотя фаил находиться в той же папке
какие настройки для twistd должны быть сам питон видит mck и импортирует его проблема похоже в twistd
Отправить комментарий