Как стать автором
Обновить

Оптимизация HTTP-сервера через версионность ресурсов. Особенности реализации

Время на прочтение 5 мин
Количество просмотров 3.2K
  1. Суть оптимизации
  2. Page load vs forced refresh
  3. Потребуется автоматизация
  4. Реализация серверной части
  5. Оптимизация серверной части
  6. Особенности google app engine
  7. Исходный код
  8. Резюме


Рассматривается пример реализации для Google App Engine / Python.


Суть оптимизации


Инженеры Yahoo в своей известной статье писали об интересной технике оптимизации обработки HTTP через версионность файлов. Суть её такова… Обычно в HTML пишут просто:

< img src="image.jpg" >


Заполучив однажды image.jpg в кэш, после браузер снова считывает HTML и снова обнаруживает там ссылку на ту же картинку. Понять обновилась ли она на сервере в общем случае браузер самостоятельно не может, так что ему приходится послать запрос на сервер.

Чтобы избежать лишнего запроса, можо указывать версию ресурса в его адресе, сделав адрес уникальным:

< img src="image.v250.jpg" >


Таким образом, браузер может быть уверен что файл версии №250 в будущем не поменяется, и №251 тоже; и если №250 есть в кэше, значит можно использовать его безо всяких вопросов к серверу. Придать браузеру эту уверенность помогут два HTTP-заголовка:

// Будте спокойны, картинка не обновится никогда
Expires: Fri, 30 Oct 2050 14:19:41 GMT
// и может хранится в кэше вечность
Cache-Control: max-age=12345678, public


Таким образом, для просмотра некой страницы в энный раз требуется только скачать HTML, а обращаться к многочисленным ресурсам уже не требуется.

Page Load vs. Forced Refresh


В текущем виде эта оптимизация работает для переходов по ссылкам и для Ctrl+L, Enter. Но если пользователь обновляет текущую страницу через F5, браузер забывает что для ресурсов было указано «более не беспокоить», и вот на сервер несутся «лишние» запросы, по одному на каждый ресурс. Такое поведение браузеров уже не изменишь, но то что можно и нужно сделать — это не отдавать каждый раз файлы по полной программе, но ввести дополнительную логику, по возможости стараясь отвечать «у меня ничего не изменилось, возьмите из своего кэша».

Когда браузер запрашивает «image.v250.jpg», то в случае если у него в кэше есть копия, браузер посылает заголовок «If-Modified-Since: Fri, 01 Jan 1990 00:00:00 GMT». Браузер пришедший за этой картинкой в первый раз такой хидер не отправляет. Соответственно, север должен первому говорить «ничего не изменилось», а второму честно отдать картинку. Конкретно в нашем случае дату можно не анализировать — важен сам факт наличия картинки в кэше, а картинка-то там верная (из-за версионности файлов и уникальных URL'ов).

Но просто так заголовок «If-Modified-Since» на сервер не придёт, даже если картинка лежит в кэше. Чтобы заставить браузер отправлять этот заголовок, в (хронологически) предыдущем ответе нужно было отдать заголовок «Last-Modified: Fri, 01 Jan 1990 00:00:00 GMT». На практике это всего лишь означает что этот заголовок сервер должен отдавать всегда. Можно отдавать честную дату последнего изменения файла, а можно указать любую дату в прошлом — эта же дата потом пойдёт обратно на сервер, а там она, как уже выяснилось, особого интереса не представляет.

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

Потребуется автоматизация


Техника неплохая, но расставлять версии файлов вручную на практике маловозможно. В GAE/django проблема решается через custom tags. В шаблоне пишется код:

< img src="{% static 'image.jpg' %}" >


преобразующийся в HTML:

< img src="/never-expire/12345678/image.jpg" >


И вот реализация такого тэга:

def static(path):
    return StaticFilesInfo.get_versioned_resource_path(path)
register.simple_tag(static)


Реализация серверной части


В основном, данная оптимизация удобна для обработки статических файлов — картинок, css, javascript. Но App Engine обрабатывает файлы означенные как static сама (не очень эффективно) и не даст поменять заголовки HTTP. Поэтому в дополнение к стандартной директории «static» появляется ещё одна — «never-expire».

Сначала обработчик GET-запроса проверяет что запрашиваемая версия файла соответствует последней. Если не соответствует — перенаправляет на новый адрес, для порядку:

# Some previous version of resource requested - redirect to the right version
correct_path = StaticFilesInfo.get_resource_path(resource)
if self.request.path != correct_path:
	self.redirect(correct_path)
	return


Потом выставляет заголовки ответа:
— Content-Type согласно расширению файла
— Expires, Cache-Control, Last-Modified как уже было описано.

Если в запросе замечен заголовок If-Modified-Since, ничего не делаем и выставляем код 304 — ресурс не изменился. Иначе содержимое файла копируется в тело ответа:

if 'If-Modified-Since' in self.request.headers:
	# This flag means the client has its own copy of the resource
	# and we may not return it. We won't.
	# Just set the response code to Not Changed.
	self.response.set_status(304)
else:
	time.sleep(1) # todo: just making resource loading process noticeable
	abs_file = os.path.join(os.path.split(__file__)[0], WHERE_STATIC_FILES_ARE_STORED, resource)
	transmit_file(abs_file, self.response.out)


Возможно, если БД в GAE побыстрее файловой системы, стоит при первом запросе файла копировать содержимое файла в базу и потом обращаться уже только туда. Вопрос для меня открытый.

Оптимизация серверной части


В качестве версии файла можно использовать как версию из VCS, так и время последнего обновления файла — тут принципиальной разницы нет. Я выбрал второе, да с ним и попроще:

os.path.getmtime(file)


Однако опрашивать файловую систему на каждый запрос вроде бы не очень хорошо — I/O всегда медленный. Поэтому можно собрать информацию о текущих версиях (всех) статических файлах при первом запросе и положить информацию в memcache. На выходе получается такой хэш:

{ 'cover.jpg': 123456, 'style.css': 234567 }


который и будет использоваться в custom tag'е для нахождения последней версии. Естественно, понадобится что-то вроде синглтона на случай если memcache протухнет:

class StaticFilesInfo():
    @classmethod
    def __get_static_files_info(cls):
        info = memcache.get(cls.__name__)
        if info is None:
            info = cls.__grab_info()
            time = MEMCACHE_TIME_PRODUCTION if is_production() else MEMCACHE_TIME_DEV_SERVER
            memcache.set(cls.__name__, info, time)
        return info
    @classmethod
	def __grab_info(cls):
		"""
		Obtain info about all files in managed 'static' directory.
		This is done rarely.
		"""
		dir = os.path.join(os.path.split(__file__)[0], WHERE_STATIC_FILES_ARE_STORED)
		hash = {}
		for file in os.listdir(dir):
			abs_file = os.path.join(dir, file)
			hash[file] = int(os.path.getmtime(abs_file))
		return hash


Особенности Google App Engine


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

Но App Engine — случай особенный. В этой системе разработка ведётся на локальной машине, после чего готовый код (и статические файлы) разворачиваются (деплоятся) на сервер. И, что важно, файлы на сервере уже не могут быть изменены (до следующего деплоя). То есть достаточно прочитать версии лишь однажды и более не заботиться о том что они могут поменяться.

Единственное, при локальной разработке файлы меняться очень даже могут, и если в данном случае не действовать альтернативно, браузер будет, например, показывать разработчику старую версию изображения, что неудобно. Но в этом случае производительности важна не очень, так что можно класть данные в memcache на считанные секунды или не класть вовсе.

Исходный код законченного примера


code.google.com/p/investigations/source/browse/#svn%2Ftrunk%2Fnever-expire-http-resources

svn checkout investigations.googlecode.com/svn/trunk/never-expire-http-resources investigations

Резюме


На appspot пока что не заливал, но локально всё работает и летает. Люди, пользуйтесь благами клиентско-серверной оптимизации, не отвечайте тупо 200 OK :)

UPD. В комментариях пишут (и я подтверждаю) что для статических файлов того же эффекта возможно добиться и через стандартный static. То есть для обработки статики подобный «ручной» код вряд ли подходит — с этим лучше справится GAE. Однако подход может быть полезен для обработки динамически создаваемых ресурсов. В таком разрезе ETag может быть удобнее чем Last-Modified для реализации.
Теги:
Хабы:
+28
Комментарии 23
Комментарии Комментарии 23

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн