Наткнувшись на заметку "10 Easy Steps to use Google App Engine as your own CDN", мне тут же захотелось применить ее для Web Optimizator (чтобы сделать самый быстрый сайт еще быстрее). Благо у меня уже был аккаунт на Google Apps Engine (его выдавали в порядке очереди где-то полгода назад, сейчас вроде можно получить свободно). Ниже я постарался свести все описанные в статье шаги к минимуму и дополнить своими размышлениями на тему.
Итак, мы готовы использовать Google для хостинга своих файлов? Тогда вперед!
Для загрузки файлов на CDN нам нужен будет Python. Ну просто потому, что нам нем работает сам Google Apps. Для корректной работы рекомендуют версию 2.5 (у меня на 3.0 Google SDK не запустился, на 2.5 работал исправно). Загружаем Python отсюда: http://www.python.org/download/, устанавливаем, запоминаем директорию установки (она нам пригодится в дальнейшем).
Загружаем последнюю версию Google Aps SDK.
Устанавливаем ее (так как мы выполнили уже п.1 и поставили Python, то проблем у нас не возникнет). Если в ходе установки выбираем нестандартную директорию, то опять-таки запоминаем к ней путь.
Регистрируемся на appengine.google.com (для этого понадобится аккаунт Google). Если в Google Apps Engine аккаунт уже был, то пропускаем этот пункт.
После регистрации заходим и создаем свое приложение. Нужно выбрать уникальный URL (поддомен appspot.com
) и название:
Дополнительно нужно будет подтвердить аккаунт через SMS, но ведь мы собираемся там просто CDN развернуть, а не спамить, правда? :)
Теперь (или параллельно ожиданию подтверждения от Google) готовим рабочую директорию с файлами у себя на машине (ведь мы для этого устанавливали сначала Pyhon, а потом SDK). Называем ее произвольным образом, в корне создаем файл app.yaml
, в который записываем:
application: ваш_идентификатор_приложения version: 1 runtime: python api_version: 1 handlers: - url: /favicon.ico static_files: favicon.ico upload: favicon.ico - url: /.* script: cacheheaders.py
В моем случае идентификатор был просто webo
, он соответствует адресу webo.appspot.com
. version
соответствует версии приложения. Очень удобно отлаживать новую версию, в то время как более старая замечательно работает. Переключение между версиями происходит из панели управления Google Apps.
Сюда же в директорию закидываем файлик favicon.ico
от рабочего сайта и создаем еще один файл, cacheheaders.py
(оригинальная идея этой Python-скрипта):
import wsgiref.handlers from google.appengine.ext import webapp class MainPage(webapp.RequestHandler): def output_file(self, path, lastmod): import datetime try: self.response.headers['Last-Modified'] = lastmod.strftime("%a, %d %b %Y %H:%M:%S GMT") expires=lastmod+datetime.timedelta(days=365) self.response.headers['Expires'] = expires.strftime("%a, %d %b %Y %H:%M:%S GMT") fh = open(path, 'r') self.response.out.write(fh.read()) fh.close return except IOError: self.error(404) return def get(self, dir, file, extension): if (dir != 'i' and extension != 'jpg' and extension != 'png' and extension != 'gif' and extension != 'css' and extension != 'js'): self.error(404) return if extension == "jpg": self.response.headers['Content-Type'] = "image/jpeg" elif extension == "gif": self.response.headers['Content-Type'] = "image/gif" elif extension == "png": self.response.headers['Content-Type'] = "image/png" elif extension == "css": self.response.headers['Content-Type'] = "text/css" elif extension == "js": self.response.headers['Content-Type'] = "application/javascript" try: import os import datetime path = dir+"/"+file+"."+extension info = os.stat(path) lastmod = datetime.datetime.fromtimestamp(info[8]) self.response.headers['Cache-Control'] = 'public, max-age=31536000' if self.request.headers.has_key('If-Modified-Since'): dt = self.request.headers.get('If-Modified-Since').split(';')[0] modsince = datetime.datetime.strptime(dt, "%a, %d %b %Y %H:%M:%S %Z") if modsince >= lastmod: # The file is older than the cached copy (or exactly the same) self.error(304) return else: # The file is newer self.output_file(path, lastmod) else: self.output_file(path, lastmod) except: self.error(404) return def main(): application = webapp.WSGIApplication([(r'/(.*)/([^.]*).(.*)', MainPage)], debug=False) wsgiref.handlers.CGIHandler().run(application) if __name__ == "__main__": main()
По поводу этого файла небольшое лирическое отступление. Как выяснилось в ходе исследования, Google Apps по умолчанию не поддерживает Last-Modified
/ ETag
(только Expires
, который настраивается простой строкой в app.yaml
— default_expiration: "365d"
). Чтобы обеспечить поддержку этого необходимого для CDN функционала (для 304-ответов), мы и заводим обработчик cacheheaders.py
.
Конечно, можно обойтись простым кэшированием, но мы же хотим максимально правильный CDN? Сам файл cacheheaders.py
просто проверяет, что запрос пришел к папке i
для нашего приложения и расширение у файла .png
, .gif
или .jpg
, после этого он отдает либо сам файл, либо 304-ответ (сравнивая заголовок браузера If-Modified-Since
с меткой времени файла).
Теперь настроим скрипт для загрузки файлов из нашей директории на Google. Для этого нужно завести в нашей папочке (или еще где-нибудь, это уже не важно) файл upload.bat
(если вы собираетесь загружать файлы из-под другой операционной системы нежели Windows, то логику файла придется переписать на соответствующем скриптовом языке). В файле записываем:
"путь_к_установленному_Python_из_пункта_1" "C:\Program Files\Google\google_appengine\appcfg.py" update "путь_к_рабочей_папочке_с_файлами"
Если в пункте 2 вы выбрали нестандартную директорию для Google Apps Engine SDK, то ее придется подставить вместо C:\Program Files\Google\google_appengine
.
Создаем папку i
в рабочей директории, в которую можно загрузить все файлы, которые предполагается отдавать с CDN. В имени файла должна отсутствовать точка (иначе cacheheaders.py
будет некорректно обрабатывать расширение для файла — и его придется подправить).
Запускаем наш upload.bat
, вводим логин / пароль от Google Apps Engine (только в первый раз), и смотрим за процессом загрузки файлов на CDN.
И вот сейчас уже любой файл по адресу ваш_идентификатор.appspot.com/i/
будет отдаваться через сеть серверов Google по всему миру (для меня это, например, http://webo.appspot.com/i/b.png). Радуемся!
В итоге, у меня в рабочей папке оказалась следующая структура.
app.yaml
— конфигурационный файл, описывающий приложение.cacheheaders.py
— скрипт на Python для выставления кэширующих заголовков.favicon.ico
— просто для красоты.i
— собственно, сама директория с файлами, которые отдаются через CDN.upload.bat
— скрипт для загрузки файлов на Google.Если ваш проект не создает большой статической нагрузки (оценочно не более 250–500 Кб/с), то вы с легкостью можете воспользоваться серверами Google для выдачи своих файлов.
Отмеченные минусы:
Last-Modified
требует дополнительной логики и нагрузки на процессор (может стать критичной при большом количестве мелких файлов).Content-Encoding
. Это означает, что текстовые файлы можно отдавать либо стандартным образом (без обработчиков, т.е. без Last-Modified
), но в сжатом виде. gzip
, но со всеми кэширующими заголовками. На данный момент лично я не вижу смысла отдавать CSS/JS-файлы через CDN при таком раскладе событий.Во всем остальном — это идеальный выбор. Web Optimizator уже использует эту CDN для выдачи всех фоновых изображений (они обслуживаются с адреса webo.appspot.com/i/
). Вообще говоря, можно подключить свои домены как альтернатива хостам на appspot.com
— но это уже тема для отдельной дискуссии.