Что нам стоит CDN построить


Медленные сайты раздражают пользователей. Когда основной контент — фоточки, а сайт тормозит — это раздражает вдвойне. И как бы мы ни оптимизировали свой сервис, всегда остаётся такой фактор, как качество связи между пользователем и нашим ЦОДом. В решении этой проблемы нам помогает CDN.

Мы — это компания «Колёса Крыша Маркет», разработчик самых крупных и посещаемых сайтов частных объявлений в Казахстане и фотографии из объявлений — критически важная часть нашего бизнеса.

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

В этих условиях нам нужно максимально быстро отдать пользователям 1.5 Гбит/сек фотографий автомобилей, недвижимости и товаров личного потребления.

Мы искали публичный CDN под свои нужды и нашли только присутствующий в Алматы Akamai без каких-либо подробностей по стоимости и планах расширения на остальную часть Казахстана. Мы приняли решение строить свой.

Первой идеей было получить по ip-адресу пользователя его географическое положение и отдать ему данные с ближайшего сервера. Однако этот вариант был быстро отвергнут — мы вспомнили кейсы, когда трафик в соседнюю деревню идёт через 1000 км и в таком случае скорость может быть даже ниже, чем без использования CDN.

По тем же причинам не стали мы использовать и любое другое гео-позиционирование. Один из наших админов предложил «пинговать сервер из браузера», что и послужило отправной точкой в реализации текущей схемы.

Мы построили свой CDN на связке OpenResty и Lua с использованием JavaScript. Это не потребовало никаких доработок в коде сайтов (менеджеры и разработчики рады — можно «пилить» фичи вместо инфраструктурных задач :)) и немножко «допилов» в мобильных приложениях.

OpenResty — это прекрасный форк Nginx от китайских разработчиков, о котором неоднократно писали на Хабре. Мы использовали его в качестве реверс-прокси.

Lua — простой, мощный, встраиваемый язык, который тоже получил достаточно внимания на Хабре.

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

function getFastestHost() {     var         fastest         = arguments[0],          fastestDuration = 600000,         timing          = [],         track           = function (host) {             var tracker = new Image();              tracker.src = "/set.gif?cdn=" + host;         };      for (var i = 0; i < arguments.length; i++) {         (function(host) {             var                 image     = new Image(),                 timeStart = (new Date()).getTime();              image.onload = function () {                 var duration = (new Date()).getTime() - timeStart;                  if (duration < fastestDuration) {                     fastestDuration = duration;                     fastest = host;                 }                  timing[timing.length] = duration;                  if (timing.length == arguments.length) {                     track(fastest);                 }             }              image.onerror = function () {                 timing[timing.length] = -1                  if (timing.length == arguments.length) {                     track(fastest);                 }             }              image.src = host + "/empty.gif";         }(arguments[i]));     } }

При последующих запросах OpenResty запускает код на Lua, который проверяет наличие куки, валидирует её и, если всё хорошо, подменяет в URL изображений хост на тот, что был получен из куки.

init_by_lua_block {     -- получение хостов из файла     function getCdnHosts(file)         local hosts = {}         for line in io.lines(file) do             table.insert(hosts, line)         end         return hosts     end      -- разбор строки хостов в массив по регулярному выражению     function stringToTable(t, s)         local it, err = ngx.re.gmatch(s, "(//[^;]+);?")         while true do             local m, err = it()              if not m then                 break             end              table.insert(t, m[1])         end          return t     end      -- поиск значения в таблице     function valueExists(tbl, value)         for k,v in pairs(tbl) do             if value == v then                 return true             end         end          return false     end }  server {     server_name kolesa.kz;     # компонент куки cdn     set $cdn_project kl;     # хост куки cdn     set $cookie_host .kolesa.kz;     # файл с хостами cdn     set $cdn_hosts_file "/etc/nginx/cdn/cdn.data.active";     # хосты статики     set $replace_hosts "//photos-a-kl.kcdn.kz;//photos-b-kl.kcdn.kz";      # проверка наличия куки и подмена ответа с правильными uri     location / {          proxy_set_header Host kolesa.kz;         proxy_pass http://kolesa;          header_filter_by_lua_block {             ngx.header.content_length = nil         }          body_filter_by_lua_block {             allCdnHosts = getCdnHosts(ngx.var["cdn_hosts_file"])             replaceHosts = stringToTable({}, ngx.var["replace_hosts"])             cdnHost = ngx.var["cookie_" .. ngx.var["cdn_project"] .. "_cdn_host"]             replaceEof = ngx.arg[2]              if cdnHost ~= nil and valueExists(allCdnHosts, cdnHost) == true then                 -- кука есть, перезапишем на него всё, что нужно                 for k,v in pairs(replaceHosts) do                     local newStr, n, err = ngx.re.gsub(ngx.arg[1], v, cdnHost)                      if n > 0 then                         ngx.arg[1] = newStr                         replaceEof = false                     end                 end             else                 -- кука ещё не установлена, добавим скрипт и он поставит куку                 local scriptStr = "<script src='/cdn.js' type='text/javascript'></script>" ..                     "<script type='text/javascript'>" ..                     "(function(){" ..                         "getFastestHost('" .. table.concat(allCdnHosts, "', '") .. "')" ..                     "}())" ..                     "</script>"                  local newStr, n, err = ngx.re.gsub(ngx.arg[1], "(</body>)", scriptStr .. "$1", "i")                 if n > 0 then                     ngx.arg[1] = newStr                     replaceEof = false                 end             end              ngx.arg[2] = replaceEof         }     } }

Список доступных хостов лежит в файле, который формируется по результатам опроса хостов на доступность с фронтенда, раздающего html-код сайта. Таким образом, недоступные по какой-то причине хосты мы выводим из сервиса.

Хостов CDN на данный момент у нас 5 штук — три в Алматы и по одному в Астане и Шымкенте. Каждый хост обслуживают два сервера Supermicro (для отказоустойчивости). На каждом крутится OpenResty + Memcached на 120 Gb для кэширования фотографий.

По результатам внедрения мы снизили трафик на основной ЦОД (1.2 Гбит против 400 Мбит) и увеличили общий трафик от нас к пользователям (1.5 Гбит против 1.2 Гбит). Фоточки перестали тормозить у пользователей отдельных интернет-провайдеров (что частенько бывало до внедрения CDN) и в целом наши клиенты стали счастливее.

В ближайших планах установить серверы в ЦОДы мобильных операторов, поскольку для пользователей мобильного интернета проблема ещё более актуальна.
ссылка на оригинал статьи https://habrahabr.ru/post/328844/

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *