Category: Backend

11
Июн
2020

⏳ 10 способов ускорить загрузку вашего сайта

Веб-проект, над которым вы работаете, стал медленнее загружаться? Рассказываем о десятке моментов, на которые стоит обратить внимание, чтобы сократить время загрузки страниц.

Существует множество причин, почему у пользователей может медленно загружаться сайт. Следуя лучшим практикам, можно, по крайней мере, смягчить проблему, убедившись, что мы сделали всё возможное.

1. Используйте CDN

CDN (Content Delivery Network) – географически распределённая сетевая инфраструктура, которая оптимизирует доставку контента конечным пользователям, давая доступ к сотням серверов по всему миру, размещающих копию
вашего сайта.

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

2. Включите gzip-сжатие

В некоторых CDN gzip-сжатие можно включить флажком Enable
compression
. Сжатие файлов обеспечит более быструю загрузку контента пользователями сайта.

3. Используйте оптимизацию изображений

Уменьшайте все изображения, которые не потеряют в качестве и не будут масштабироваться
Уменьшайте все изображения, которые не потеряют в качестве и не будут масштабироваться

Распространённая причина долгой загрузки страниц – избыточное качество изображений или выбор неподходящего формата. Можно уменьшить количество пикселей по каждой из сторон и применить сжатие изображений. Прогресс не стоит на месте – обратите внимание на формат сжатия изображений WebP.

Если у вас сайт на WordPress, можно использовать плагины, которые уменьшат размер автоматически в процессе загрузки.

4. Уменьшите количество
запросов, совершаемых страницей

Сократите число запросов, необходимых для загрузки верхней части страницы. Есть два способа:

  • уменьшить количество запросов, удалив то, что не влияет на работу сайта;
  • отложить загрузку низкоприоритетного контента, используя функцию ленивой загрузки.

5. Избегайте
перенаправлений


Редиректы значительно
замедляют работу сайта. Чтобы не держать специальный поддомен для
мобильных пользователей, используйте адаптивный CSS.

Некоторые редиректы
неизбежны, например, www -> root domain или root domain -> www, но
основная часть трафика не должна поступать через перенаправления.

6. Сократите время до первого байта

Время до первого байта (TTFB) –
это время, которое браузер тратит на ожидание данных с сервера после отправки запроса на ресурс.

На этот показатель влияют два параметра:

  1. Время, потраченное на сервере.
  2. Время, потраченное на отправку данных.

Первый параметр можно уменьшить, оптимизировав работу на стороне сервера: запросы к базам данных, вызовы API, балансировку нагрузки и т. д..

О втором параметре мы уже поговорили – на него мы можем повлиять, используя CDN.

7. Решите вопрос блокировки рендеринга JavaScript

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

        https://example.com/external.js
    

Можно отложить
загрузку сценариев до тех пор, пока пользователь не начнёт совершать
активные действия:

        window.addEventListener(
  'scroll',
  () =>
    setTimeout(() => {
      //insert marketing snippets here
    }, 1000),
  { once: true }
);
    

8. Минимизируйте CSS и JavaScript


Минимизация подразумевает
использование инструментов для удаления пробелов, символов перевода строки и
сокращения длины имён переменных. Как правило, это делается автоматически в рамках
процесса сборки. Есть специальные инструменты: например,
UglifyJS для JavaScript или cssnano для CSS.

9. Удалите
неиспользуемый код CSS и JavaScript

Чем больше проект, тем больше вероятность, что какой-то код стал неактуальным. И лучший способ его минимизировать и сократить время загрузки – просто удалить ненужный код.

Начиная с Chrome 59, в Chrome DevTools
можно анализировать использование JavaScript и CSS.
Для этого откройте DevTools, перейдите во вкладку Console, нажмите на три точки и
откройте Coverage. При нажатии кнопки со значком перезагрузки будет проведен аудит использования CSS и JavaScript. Помеченные красным блоки кода загружаются, но не используются.

Аудит использования CSS и JS
Аудит использования CSS и JS
Примечание
Об использовании средств DevTools для ускорения загрузки сайта читайте в нашем подробном мануале «Ускоряем загрузку сайта с помощью Chrome DevTools».

10. Регулярно
отслеживайте скорость загрузки сайта

Если вы возьмете за
правило обращать внимание на проверку скорости загрузки, это станет гораздо меньшей проблемой.

Есть бесплатные
инструменты для мониторинга скорости, например,
WebPageTest и Google Lighthouse. Но нужно не забывать запускать их до и после внесения изменений. Для автоматизации запуска GoogleLighthouse можно использовать PerfBeacon.

Заключение

Несомненно, скорость
сайта – важный параметр. Важно следить за этим показателем и вносить по мере необходимости изменения в структуру веб-проекта.
Но кроме скорости сайта поисковые системы оценивает полезность контента – в погоне за улучшением производительности сайта не забывайте о его наполнении.

Другие материалы по теме

08
Июн
2020

javascript для backend разработчика

Всех приветствую, сразу прошу прощения за расплывчатый вопрос.

Хочу стать бекенд разработчиком, доучился до уровня кривого сайта на Django/Bootstrap. Перехожу к изучению Django Rest Framework ибо углубляться в стоковый Django можно очень …

03
Июн
2020

Почему когда делают переход по URL с GET параметром, то в начало адреса добавляется приставка /public. Laravel

Такая проблема. Есть кнопка на сайте, а к ней добавлен GET параметр. Когда я нажимаю перейти то осуществляется переход, но к адресу добавляется /public. Если убрать GET параметр и оставить ссылку, то все хорошо и ссылка стандартная.

Так в…

28
Май
2020

Почему cURL возвращает NULL?

Помогите, пожалуйста, разобраться в проблеме. Отказывается работать cURL. Раньше работал, сейчас по неизвестной мне причине возвращает ответ NULL.

$curl = curl_init();
curl_setopt($curl, CURLOPT_AUTOREFERER, TRUE);
curl_setopt($curl, CURL…

26
Май
2020

Безопасная загрузка изображений в веб-приложении на Django

🖼️ 💾 Почти в любом веб-приложении есть необходимость принимать от пользователей картинки. В Django это можно делать лаконично и безопасно, используя ImageField и Pillow.

Загрузка изображений в
Django

3411d48a-91e2-4b1b-8d23-b701155f3593В Django есть два поля, позволяющих загружать картинки: FileField и ImageField. Второй вариант – специализированная версия FileField, использующая подтверждение от библиотеки Pillow, что файл является изображением.

dcf6fa4c-fcb2-4441-97ca-a7102649a58cНачнем с
создания моделей. Создадим файл models.py и поместим в него следующее содержимое:

models.py
            from django.db import models

class Image(models.Model):
    title = models.CharField(max_length=200)
    image = models.ImageField(upload_to='images')

    def __str__(self):
        return self.title
        

0ba04c44-3fdc-4eda-b4e4-a7750d38f502Переменная image – это
поле ImageField, которое работает с API хранилища, который обеспечивает способ
хранения/извлечения, а также чтения/записи файлов.

5a38b4c6-df6e-45e9-908c-c7481e611bbaПараметр upload_to
указывает путь, где будут храниться изображения. Для этой модели он будет соответствовать MEDIA_ROOT/images/

64094f69-8bc4-4e98-a12b-addaedfb2649Также возможна
установка динамических путей для изображений:

            image = models.ImageField(upload_to='users/%Y/%m/%d/', blank=True)
        

babb3c17-94c7-4b60-9ee6-e3ee8b02b3ccЭто позволит хранить
изображения в каталогах вида MEDIA_ROOT/users/2020/05/26.

1f77f8b1-eecc-425b-87ce-8c0173639338Установим Pillow, выполнив следующую команду:

            pip install Pillow
        

aa7d5d86-8f11-4550-aa7e-9199ab4124ffЧтобы Django
обслуживал медиафайлы, загруженные пользователями, добавим следующие настройки
в файл settings.py вашего проекта:

            # Основной url для управления медиафайлами
MEDIA_URL = '/media/'

# Путь хранения картинок
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
        

330875b3-4003-4fd1-ace7-ffa6f40a70baMEDIA_URL – это
URL-адрес, который будет обслуживать медиафайлы, а MEDIA_ROOT – это путь к
корневому каталогу, в котором хранятся файлы.

ad33a632-b4bb-41a2-958b-42cf044d9367Добавим следующий код
в urls.py:

urls.py
            from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    ...]
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL,
                          document_root=settings.MEDIA_ROOT)
        

2234025d-7599-43d3-b003-8cccee895b3aТеперь нам нужно создать форму
для модели изображения. В файл forms.py добавим следующий код:

forms.py
            from django import forms
from .models import Image


class ImageForm(forms.ModelForm):
    class Meta:
        model = Image
        fields = ('title', 'image')
        

68f12e0c-5b94-4ef5-8275-037610e36726Это создаст форму с
полями title и image, которые будут отображаться в шаблонах. А сейчас создадим
шаблон для загрузки файлов. В index.html внесем следующее:

            <form method="post" enctype="multipart/form-data">
  {% csrf_token %}
   {{ form.as_p }}
  <button type="submit">Upload</button>
</form>

{% if img_obj %}
  <h3>Succesfully uploaded : {{img_obj.title}}</h3>
  <img src="{{ img_obj.image.url}}" alt="connect" style="max-height:300px">
{% endif %}
        

2a8dfb39-55b7-4438-a3f8-e55a209a7399Включение свойства enctype для формы обеспечивает правильное прикрепление загруженного файла к запросу.

753ed0d4-88fa-4d51-b539-49ff7c23455bНапишем обработчик формы в views.py:

            from django.shortcuts import render
from .forms import ImageForm


def image_upload_view(request):
    """Process images uploaded by users"""
    if request.method == 'POST':
        form = ImageForm(request.POST, request.FILES)
        if form.is_valid():
            form.save()
            # Get the current instance object to display in the template
            img_obj = form.instance
            return render(request, 'index.html', {'form': form, 'img_obj': img_obj})
    else:
        form = ImageForm()
    return render(request, 'index.html', {'form': form})
        

ef51f263-dc45-4576-81c1-e450c638dcf1Django делает всю
работу сам, а мы просто прогоняем форму через валидацию и сохраняем ее при
успешной загрузке файла. Теперь, когда обработчик готов, сопоставим все
с URL-адресом в urls.py:

            urlpatterns = [
    ......
    path('upload/', views.image_upload_view)
    ......
]
        

038a29b9-408f-4259-baaa-9036db34326aСохраним файлы,
запустим сервер и проверим работу программы.


Заключение

75ddcc17-ed61-402b-9365-691684711a8dМы рассмотрели
простейший вариант безопасной загрузки картинки на сервер с помощью Django.
Вариант прост в реализации и понятен не только для специалистов с опытом. Удачи
в обучении.

03
Апр
2020

при поиске 404 ошибка

На сайте есть поиск товаров, но при использовании /, перекидывает на 404 страницу.
Такая проблема, только у поиска, который находиться в header.

Я не backend, по этому не знаю что это. Мне хотя бы нужно знать откуда ноги растут.

Сам сайт…

23
Мар
2020

как правильно писать http api запрос на node.js что бы получилось синхронно

Всем привет, я новичок на бэкенд разработках и node.js, проблема с которой я столкнулся это асинхронные функции. Моя программа должна работать по порядку, но там есть http запросы которые я зову через циклы.

Здесь я хочу показать идею кот…

05
Мар
2020

MYSQL, Единый запрос и все, все, все

Не сомневаюсь, что среди здешних пользователей есть профи SQL, поэтому хочу спросить следующее:
можно ли получить несколько разных SELECT одним запросом?

Есть следующий SELECT:

SELECT `ID`, `Teamname`, `User`, `Date_made` FROM `Team`

В…

27
Фев
2020

Действия, входящие в backend

Нашел в интернете https://skillbox.ru/media/code/frontend_i_backend_razrabotka/ :

Backend-разработка — это набор аппаратно-программных средств, при помощи которых реализована логика работы сайта. Попросту говоря, это то, что скрыто от …

25
Фев
2020

Как мне загружать файлы на Google Drive без авторизации?

Знатоки Google API, помогите. Как мне загружать файлы на Google Drive БЕЗ АВТОРИЗАЦИИ? Обычно создают OAuth страницу и там ты даешь доступ к своему аккаунту. Так вот, как этот шаг пропустить? Может гугл дает какие-то ключи, благодаря котор…

18
Фев
2020

Back end и Front end

Для чего нужны front-end библиотеки(React,Angular,Vue), если можно рисовать html в том же JSP.Я явно чего-то не понимаю. Накидайте пожалуйста ссылки на объяснение взаимосвязи Front-end и back-end.Даже если писать на Java EE, то как наприме…

15
Фев
2020

Взаимодействия бэкенда и фронтенда

Подскажите плз в общих чертах как взаимодействует фронт с бэком на примере какого нибудь динамического веб сайта, где есть фронт на JS и бэк на Java Spring. Должен ли фронт и бэк (REST API) запускаьться на разных портах/серверах? И потом и…

31
Янв
2020

Миграция на новую версию Elacticsearch с нулевым временем простоя

Доводилось ли вам переводить крупную систему на современную версию без потери данных и простоя? Рассматриваем пример реального проекта на Elasticsearch.

В этом туториале вы узнаете,
как перейти на новую версию Elasticsearch. В рассматриваемом проекте осуществлялся переход с 1.6 на 6.8, но общие идеи справедливы и для других версий. Конечно, требовалось не просто перейти, но и соблюсти ряд жёстких ограничений: нулевое время простоя, отсутствие ошибок и потерь данных.


1. Зачем понадобился перенос данных?

В рассматриваемом примере проекта были следующие проблемы, повлиявшие на необходимость переноса:

  • Проблемы с производительностью и стабильностью – наличие большого количества перебоев с длительным MTTR. Это отражалось в частых задержках и высокой загрузке процессора.
  • Отсутствие поддержки для старых версий Elasticsearch.
  • Негативное влияние dynamic mapping на работу кластера.
  • Нехватка инструментов экспорта и метрик, встроенных в новую версию.

2. Рассмотрим условия

Какие условия нужно было выполнить:

  • Миграция с нулевым временем простоя. Поскольку в обновляемой системе имеются активные пользователи, нельзя допустить падения системы.
  • План восстановления. Нельзя потерять или испортить данные, независимо от их ценности. Нужно иметь план восстановления на случай, если что-то пойдёт не так.
  • Отсутствие ошибок. Для конечных пользователей привычные функции поиска не должны измениться.

3. Обдумаем план

Ошибки. Для удовлетворения требования по отсутствию ошибок необходимо изучить все возможные результаты и запросы, которые
получает работающая система, а также обеспечить тестами ответственные куски
кода. В качестве проверки
результата нужно добавить метрики для отслеживания задержки, пропускной
способности и производительности, чтобы проверить динамику.

План восстановления. На данном этапе
подготовьтесь к ситуации, когда сервис будет работать не так, как ожидалось. В
общих чертах нужно создать копию кластера и перенести на него инфу незаметно
для юзеров.

Миграция с нулевым
временем простоя
. Работающий сервис
всегда онлайн и не может быть недоступен более 5-10 минут. Чтобы сделать всё правильно, поступаем так:

  • Храним логи всех выполняемых действий (в продакшене используется Kafka).
  • Запускаем процесс миграции в офлайне с отслеживанием смещения с момента начала миграции.
  • Когда миграция завершится, запускаем новую службу, учитывая логирование и «догоняем» отставание.
  • Когда отставание сведено к нулю, изменяем версию фронтенда.

4. План действий

Текущий сервис имеет
следующую архитектуру:


  • Event topic содержит события, созданные другими приложениями (например, UserId 3 created);
  • Command topic содержит трансляцию этих событий в конкретные команды, используемые приложением (например: Add userId 3);
  • Elasticsearch 1.7 – хранилище, обслуживаемое индексатором Indexer.

По плану необходимо
добавить еще одного клиента (new Indexer) для Command topic, который будет параллельно читать и
записывать данные в Elasticsearch 6.8.


С чего начать?

Вот несколько полезныхвещей,
которые помогут:

  • Документация. Найдите время, чтобы прочитать о Mapping и QueryDsl.
  • API. Всё крутится на CAT API. Это очень полезный инструмент для локального дебага и проверки ответа от Elastic.
  • Метрики. Настройте мониторинг с метриками и ресурсами из elasticsearch-exporter-for-Prometheus, которые помогут лучше понимать происходящее.

Часть 5. Проблемы из-за
Mapping

Опишем подробнее пример
использования, вот наша модель:

            class InsertMessageCommand(tags: Map[String,String])
        

Пример сообщения по
вышеописанной схеме:

            new InsertMessageCommand(Map("name"->"dor","lastName"->"sever"))
        

Эта модель поддерживает
запросы по значению, а также по тегу со значением. Динамический шаблон вызывал постоянные
сбои. Схема выглядела так:

            curl -X PUT "localhost:9200/_template/my_template?pretty" -H 'Content-Type: application/json' -d '
{
    "index_patterns": [
        "your-index-names*"
    ],
    "mappings": {
            "_doc": {
                "dynamic_templates": [
                    {
                        "tags": {
                            "mapping": {
                                "type": "text"
                            },
                            "path_match": "actions.tags.*"
                        }
                    }
                ]
            }
        },
    "aliases": {}
}'  

curl -X PUT "localhost:9200/your-index-names-1/_doc/1?pretty" -H 'Content-Type: application/json' -d'
{
  "actions": {
    "tags" : {
        "name": "John",
        "lname" : "Smith"
    }
  }
}
'

curl -X PUT "localhost:9200/your-index-names-1/_doc/2?pretty" -H 'Content-Type: application/json' -d'
{
  "actions": {
    "tags" : {
        "name": "Dor",
        "lname" : "Sever"
  }
}
}
'

curl -X PUT "localhost:9200/your-index-names-1/_doc/3?pretty" -H 'Content-Type: application/json' -d'
{
  "actions": {
    "tags" : {
        "name": "AnotherName",
        "lname" : "AnotherLastName"
  }
}
}
'
  
curl -X GET "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d'
{
    "query": {
        "match" : {
            "actions.tags.name" : {
                "query" : "John"
            }
        }
    }
}
'
# returns 1 match(doc 1)


curl -X GET "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d'
{
    "query": {
        "match" : {
            "actions.tags.lname" : {
                "query" : "John"
            }
        }
    }
}
'
# returns zero matches

# search by value
curl -X GET "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d
{
    "query": {
        "query_string" : {
            "fields": ["actions.tags.*" ],
            "query" : "Dor"
        }
    }
}
'
        

Предположительно, причина сбоев состояла в использовании вложенных документов.

В обновлённой схеме
были применены следующие запросы:

            curl -X PUT "localhost:9200/my_index?pretty" -H 'Content-Type: application/json' -d'
{
        "mappings": {
            "_doc": {
            "properties": {
            "tags": {
                "type": "nested" 
                }                
            }
        }
        }
}
'

curl -X PUT "localhost:9200/my_index/_doc/1?pretty" -H 'Content-Type: application/json' -d'
{
  "tags" : [
    {
      "key" : "John",
      "value" :  "Smith"
    },
    {
      "key" : "Alice",
      "value" :  "White"
    }
  ]
}
'


# Query by tag key and value
curl -X GET "localhost:9200/my_index/_search?pretty" -H 'Content-Type: application/json' -d'
{
  "query": {
    "nested": {
      "path": "tags",
      "query": {
        "bool": {
          "must": [
            { "match": { "tags.key": "Alice" }},
            { "match": { "tags.value":  "White" }} 
          ]
        }
      }
    }
  }
}
'

# Returns 1 document


curl -X GET "localhost:9200/my_index/_search?pretty" -H 'Content-Type: application/json' -d'
{
  "query": {
    "nested": {
      "path": "tags",
      "query": {
        "bool": {
          "must": [
            { "match": { "tags.value":  "Smith" }} 
          ]
        }
      }
    }
  }
}
'

# Query by tag value
# Returns 1 result
        

Количество документов в
кластере составляло около 500 млн, с использованием вложенных документов в
новой схеме цифра может вырасти до 250 млрд документов. Этот пост
объясняет, что всему виной проблемы с использованием кучи, так как она может
вызвать высокую задержку в запросах.

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

            curl -X PUT "localhost:9200/my_index_2?pretty" -H 'Content-Type: application/json' -d'
{
    "mappings": {
        "_doc": {
            "properties": {
                "tags": {
                    "type": "object",
                    "properties": {
                        "keyToValue": {
                            "type": "keyword"
                        },
                        "value": {
                            "type": "keyword"
                        }
                    }
                }
            }
        }
    }
}
'


curl -X PUT "localhost:9200/my_index_2/_doc/1?pretty" -H 'Content-Type: application/json' -d'
{
  "tags" : [
    {
      "keyToValue" : "John:Smith",
      "value" : "Smith"
    },
    {
      "keyToValue" : "Alice:White",
      "value" : "White"
    }
  ]
}
'

# Query by key,value
# User queries for key: Alice, and value : White , we then query elastic with this query:

curl -X GET "localhost:9200/my_index_2/_search?pretty" -H 'Content-Type: application/json' -d'
{
  "query": {
        "bool": {
          "must": [ { "match": { "tags.keyToValue": "Alice:White" }}]
  }}}
'

# Query by value only
curl -X GET "localhost:9200/my_index_2/_search?pretty" -H 'Content-Type: application/json' -d'
{
  "query": {
        "bool": {
          "must": [ { "match": { "tags.value": "White" }}]
  }}}
'
        

6. Миграция

Для миграции потребуется:

  1. Перенести данные из старого Elastic в новый.
  2. Закрыть отставание между началом миграции и её окончанием.

С первым всё понятно, а
вот решения второго шага:

  • Система основана на Kafka, поэтому используем текущее смещение до начала миграции, а после завершения миграции переносим данные из точки смещения. Это решение подразумевает гору ручного труда.
  • Другой подход к решению этой проблемы – начать перенос сообщений из Kafka и сделать все действия в Elasticsearch неизменяемыми, то есть если изменение уже было «применено», в Elastic store ничего не изменится.

Второй вариант более
предпочтителен, так как не требует ручной работы.

Перенос данных

Среди массы вариантов переноса данных самый подходящий основан на передаче сообщений между старым и новым Elastic.
Для этого создаём скрипт на Python, который будет ходить на старый кластер, таскать оттуда
инфу и параллельно преобразовывать её под новую схему.

            dictor==0.1.2 - to copy and transform our Elasticsearch documents
elasticsearch==1.9.0 - to connect to "old" Elasticsearch
elasticsearch6==6.4.2 - to connect to the "new" Elasticsearch
statsd==3.3.0 - to report metrics
        
            from elasticsearch import Elasticsearch
from elasticsearch6 import Elasticsearch as Elasticsearch6
import sys
from elasticsearch.helpers import scan
from elasticsearch6.helpers import parallel_bulk
import statsd

ES_SOURCE = Elasticsearch(sys.argv[1])
ES_TARGET = Elasticsearch6(sys.argv[2])
INDEX_SOURCE = sys.argv[3]
INDEX_TARGET = sys.argv[4]
QUERY_MATCH_ALL = {"query": {"match_all": {}}}
SCAN_SIZE = 1000
SCAN_REQUEST_TIMEOUT = '3m'
REQUEST_TIMEOUT = 180
MAX_CHUNK_BYTES = 15 * 1024 * 1024
RAISE_ON_ERROR = False


def transform_item(item, index_target):
    # implement your logic transformation here
    transformed_source_doc = item.get("_source")
    return {"_index": index_target,
            "_type": "_doc",
            "_id": item['_id'],
            "_source": transformed_source_doc}

def transformedStream(es_source, match_query, index_source, index_target, transform_logic_func):

    for item in scan(es_source, query=match_query, index=index_source, size=SCAN_SIZE,
                     timeout=SCAN_REQUEST_TIMEOUT):
        yield transform_logic_func(item, index_target)


def index_source_to_target(es_source, es_target, match_query, index_source, index_target, bulk_size, statsd_client,
                           logger, transform_logic_func):

    ok_count = 0
    fail_count = 0
    count_response = es_source.count(index=index_source, body=match_query)
    count_result = count_response['count']
    statsd_client.gauge(stat='elastic_migration_document_total_count,index={0},type=success'.format(index_target),
                        value=count_result)
    with statsd_client.timer('elastic_migration_time_ms,index={0}'.format(index_target)):
        actions_stream = transformedStream(es_source, match_query, index_source, index_target, transform_logic_func)
        for (ok, item) in parallel_bulk(es_target,
                                        chunk_size=bulk_size,
                                        max_chunk_bytes=MAX_CHUNK_BYTES,
                                        actions=actions_stream,
                                        request_timeout=REQUEST_TIMEOUT,
                                        raise_on_error=RAISE_ON_ERROR):
            if not ok:
                logger.error("got error on index {} which is : {}".format(index_target, item))
                fail_count += 1
                statsd_client.incr('elastic_migration_document_count,index={0},type=failure'.format(index_target),
                                   1)
            else:
                ok_count += 1
                statsd_client.incr('elastic_migration_document_count,index={0},type=success'.format(index_target),
                                   1)

    return ok_count, fail_count


statsd_client = statsd.StatsClient(host='localhost', port=8125)

if __name__ == "__main__":
    index_source_to_target(ES_SOURCE, ES_TARGET, QUERY_MATCH_ALL, INDEX_SOURCE, INDEX_TARGET, BULK_SIZE,
                           statsd_client, transform_item)
        

Заключение

Перенос данных в живом
продакшене – сложная задача, требующая большого внимания и тщательного
планирования. Мы рекомендуем потратить время, проработать шаги на тестовых
машинах и выяснить, что лучше всего подойдёт для вашей конкретной ситуации.

Всегда старайтесь
максимально снизить влияющие факторы. Например, требуется ли нулевой простой
или можно “тормознуть” некоторый функционал сервиса? Не станет ли потеря данных
проблемой?

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

31
Янв
2020

Миграция на новую версию Elasticsearch с нулевым временем простоя

Доводилось ли вам переводить крупную систему на современную версию без потери данных и простоя? Рассматриваем пример реального проекта на Elasticsearch.

В этом туториале вы узнаете,
как перейти на новую версию Elasticsearch. В рассматриваемом проекте осуществлялся переход с 1.6 на 6.8, но общие идеи справедливы и для других версий. Конечно, требовалось не просто перейти, но и соблюсти ряд жёстких ограничений: нулевое время простоя, отсутствие ошибок и потерь данных.


1. Зачем понадобился перенос данных?

В рассматриваемом примере проекта были следующие проблемы, повлиявшие на необходимость переноса:

  • Проблемы с производительностью и стабильностью – наличие большого количества перебоев с длительным MTTR. Это отражалось в частых задержках и высокой загрузке процессора.
  • Отсутствие поддержки для старых версий Elasticsearch.
  • Негативное влияние dynamic mapping на работу кластера.
  • Нехватка инструментов экспорта и метрик, встроенных в новую версию.

2. Рассмотрим условия

Какие условия нужно было выполнить:

  • Миграция с нулевым временем простоя. Поскольку в обновляемой системе имеются активные пользователи, нельзя допустить падения системы.
  • План восстановления. Нельзя потерять или испортить данные, независимо от их ценности. Нужно иметь план восстановления на случай, если что-то пойдёт не так.
  • Отсутствие ошибок. Для конечных пользователей привычные функции поиска не должны измениться.

3. Обдумаем план

Ошибки. Для удовлетворения требования по отсутствию ошибок необходимо изучить все возможные результаты и запросы, которые
получает работающая система, а также обеспечить тестами ответственные куски
кода. В качестве проверки
результата нужно добавить метрики для отслеживания задержки, пропускной
способности и производительности, чтобы проверить динамику.

План восстановления. На данном этапе
подготовьтесь к ситуации, когда сервис будет работать не так, как ожидалось. В
общих чертах нужно создать копию кластера и перенести на него инфу незаметно
для юзеров.

Миграция с нулевым
временем простоя
. Работающий сервис
всегда онлайн и не может быть недоступен более 5-10 минут. Чтобы сделать всё правильно, поступаем так:

  • Храним логи всех выполняемых действий (в продакшене используется Kafka).
  • Запускаем процесс миграции в офлайне с отслеживанием смещения с момента начала миграции.
  • Когда миграция завершится, запускаем новую службу, учитывая логирование и «догоняем» отставание.
  • Когда отставание сведено к нулю, изменяем версию фронтенда.

4. План действий

Текущий сервис имеет
следующую архитектуру:


  • Event topic содержит события, созданные другими приложениями (например, UserId 3 created);
  • Command topic содержит трансляцию этих событий в конкретные команды, используемые приложением (например: Add userId 3);
  • Elasticsearch 1.7 – хранилище, обслуживаемое индексатором Indexer.

По плану необходимо
добавить еще одного клиента (new Indexer) для Command topic, который будет параллельно читать и
записывать данные в Elasticsearch 6.8.


С чего начать?

Вот несколько полезныхвещей,
которые помогут:

  • Документация. Найдите время, чтобы прочитать о Mapping и QueryDsl.
  • API. Всё крутится на CAT API. Это очень полезный инструмент для локального дебага и проверки ответа от Elastic.
  • Метрики. Настройте мониторинг с метриками и ресурсами из elasticsearch-exporter-for-Prometheus, которые помогут лучше понимать происходящее.

Часть 5. Проблемы из-за
Mapping

Опишем подробнее пример
использования, вот наша модель:

            class InsertMessageCommand(tags: Map[String,String])
        

Пример сообщения по
вышеописанной схеме:

            new InsertMessageCommand(Map("name"->"dor","lastName"->"sever"))
        

Эта модель поддерживает
запросы по значению, а также по тегу со значением. Динамический шаблон вызывал постоянные
сбои. Схема выглядела так:

            curl -X PUT "localhost:9200/_template/my_template?pretty" -H 'Content-Type: application/json' -d '
{
    "index_patterns": [
        "your-index-names*"
    ],
    "mappings": {
            "_doc": {
                "dynamic_templates": [
                    {
                        "tags": {
                            "mapping": {
                                "type": "text"
                            },
                            "path_match": "actions.tags.*"
                        }
                    }
                ]
            }
        },
    "aliases": {}
}'  

curl -X PUT "localhost:9200/your-index-names-1/_doc/1?pretty" -H 'Content-Type: application/json' -d'
{
  "actions": {
    "tags" : {
        "name": "John",
        "lname" : "Smith"
    }
  }
}
'

curl -X PUT "localhost:9200/your-index-names-1/_doc/2?pretty" -H 'Content-Type: application/json' -d'
{
  "actions": {
    "tags" : {
        "name": "Dor",
        "lname" : "Sever"
  }
}
}
'

curl -X PUT "localhost:9200/your-index-names-1/_doc/3?pretty" -H 'Content-Type: application/json' -d'
{
  "actions": {
    "tags" : {
        "name": "AnotherName",
        "lname" : "AnotherLastName"
  }
}
}
'
  
curl -X GET "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d'
{
    "query": {
        "match" : {
            "actions.tags.name" : {
                "query" : "John"
            }
        }
    }
}
'
# returns 1 match(doc 1)


curl -X GET "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d'
{
    "query": {
        "match" : {
            "actions.tags.lname" : {
                "query" : "John"
            }
        }
    }
}
'
# returns zero matches

# search by value
curl -X GET "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d
{
    "query": {
        "query_string" : {
            "fields": ["actions.tags.*" ],
            "query" : "Dor"
        }
    }
}
'
        

Предположительно, причина сбоев состояла в использовании вложенных документов.

В обновлённой схеме
были применены следующие запросы:

            curl -X PUT "localhost:9200/my_index?pretty" -H 'Content-Type: application/json' -d'
{
        "mappings": {
            "_doc": {
            "properties": {
            "tags": {
                "type": "nested" 
                }                
            }
        }
        }
}
'

curl -X PUT "localhost:9200/my_index/_doc/1?pretty" -H 'Content-Type: application/json' -d'
{
  "tags" : [
    {
      "key" : "John",
      "value" :  "Smith"
    },
    {
      "key" : "Alice",
      "value" :  "White"
    }
  ]
}
'


# Query by tag key and value
curl -X GET "localhost:9200/my_index/_search?pretty" -H 'Content-Type: application/json' -d'
{
  "query": {
    "nested": {
      "path": "tags",
      "query": {
        "bool": {
          "must": [
            { "match": { "tags.key": "Alice" }},
            { "match": { "tags.value":  "White" }} 
          ]
        }
      }
    }
  }
}
'

# Returns 1 document


curl -X GET "localhost:9200/my_index/_search?pretty" -H 'Content-Type: application/json' -d'
{
  "query": {
    "nested": {
      "path": "tags",
      "query": {
        "bool": {
          "must": [
            { "match": { "tags.value":  "Smith" }} 
          ]
        }
      }
    }
  }
}
'

# Query by tag value
# Returns 1 result
        

Количество документов в
кластере составляло около 500 млн, с использованием вложенных документов в
новой схеме цифра может вырасти до 250 млрд документов. Этот пост
объясняет, что всему виной проблемы с использованием кучи, так как она может
вызвать высокую задержку в запросах.

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

            curl -X PUT "localhost:9200/my_index_2?pretty" -H 'Content-Type: application/json' -d'
{
    "mappings": {
        "_doc": {
            "properties": {
                "tags": {
                    "type": "object",
                    "properties": {
                        "keyToValue": {
                            "type": "keyword"
                        },
                        "value": {
                            "type": "keyword"
                        }
                    }
                }
            }
        }
    }
}
'


curl -X PUT "localhost:9200/my_index_2/_doc/1?pretty" -H 'Content-Type: application/json' -d'
{
  "tags" : [
    {
      "keyToValue" : "John:Smith",
      "value" : "Smith"
    },
    {
      "keyToValue" : "Alice:White",
      "value" : "White"
    }
  ]
}
'

# Query by key,value
# User queries for key: Alice, and value : White , we then query elastic with this query:

curl -X GET "localhost:9200/my_index_2/_search?pretty" -H 'Content-Type: application/json' -d'
{
  "query": {
        "bool": {
          "must": [ { "match": { "tags.keyToValue": "Alice:White" }}]
  }}}
'

# Query by value only
curl -X GET "localhost:9200/my_index_2/_search?pretty" -H 'Content-Type: application/json' -d'
{
  "query": {
        "bool": {
          "must": [ { "match": { "tags.value": "White" }}]
  }}}
'
        

6. Миграция

Для миграции потребуется:

  1. Перенести данные из старого Elastic в новый.
  2. Закрыть отставание между началом миграции и её окончанием.

С первым всё понятно, а
вот решения второго шага:

  • Система основана на Kafka, поэтому используем текущее смещение до начала миграции, а после завершения миграции переносим данные из точки смещения. Это решение подразумевает гору ручного труда.
  • Другой подход к решению этой проблемы – начать перенос сообщений из Kafka и сделать все действия в Elasticsearch неизменяемыми, то есть если изменение уже было «применено», в Elastic store ничего не изменится.

Второй вариант более
предпочтителен, так как не требует ручной работы.

Перенос данных

Среди массы вариантов переноса данных самый подходящий основан на передаче сообщений между старым и новым Elastic.
Для этого создаём скрипт на Python, который будет ходить на старый кластер, таскать оттуда
инфу и параллельно преобразовывать её под новую схему.

            dictor==0.1.2 - to copy and transform our Elasticsearch documents
elasticsearch==1.9.0 - to connect to "old" Elasticsearch
elasticsearch6==6.4.2 - to connect to the "new" Elasticsearch
statsd==3.3.0 - to report metrics
        
            from elasticsearch import Elasticsearch
from elasticsearch6 import Elasticsearch as Elasticsearch6
import sys
from elasticsearch.helpers import scan
from elasticsearch6.helpers import parallel_bulk
import statsd

ES_SOURCE = Elasticsearch(sys.argv[1])
ES_TARGET = Elasticsearch6(sys.argv[2])
INDEX_SOURCE = sys.argv[3]
INDEX_TARGET = sys.argv[4]
QUERY_MATCH_ALL = {"query": {"match_all": {}}}
SCAN_SIZE = 1000
SCAN_REQUEST_TIMEOUT = '3m'
REQUEST_TIMEOUT = 180
MAX_CHUNK_BYTES = 15 * 1024 * 1024
RAISE_ON_ERROR = False


def transform_item(item, index_target):
    # implement your logic transformation here
    transformed_source_doc = item.get("_source")
    return {"_index": index_target,
            "_type": "_doc",
            "_id": item['_id'],
            "_source": transformed_source_doc}

def transformedStream(es_source, match_query, index_source, index_target, transform_logic_func):

    for item in scan(es_source, query=match_query, index=index_source, size=SCAN_SIZE,
                     timeout=SCAN_REQUEST_TIMEOUT):
        yield transform_logic_func(item, index_target)


def index_source_to_target(es_source, es_target, match_query, index_source, index_target, bulk_size, statsd_client,
                           logger, transform_logic_func):

    ok_count = 0
    fail_count = 0
    count_response = es_source.count(index=index_source, body=match_query)
    count_result = count_response['count']
    statsd_client.gauge(stat='elastic_migration_document_total_count,index={0},type=success'.format(index_target),
                        value=count_result)
    with statsd_client.timer('elastic_migration_time_ms,index={0}'.format(index_target)):
        actions_stream = transformedStream(es_source, match_query, index_source, index_target, transform_logic_func)
        for (ok, item) in parallel_bulk(es_target,
                                        chunk_size=bulk_size,
                                        max_chunk_bytes=MAX_CHUNK_BYTES,
                                        actions=actions_stream,
                                        request_timeout=REQUEST_TIMEOUT,
                                        raise_on_error=RAISE_ON_ERROR):
            if not ok:
                logger.error("got error on index {} which is : {}".format(index_target, item))
                fail_count += 1
                statsd_client.incr('elastic_migration_document_count,index={0},type=failure'.format(index_target),
                                   1)
            else:
                ok_count += 1
                statsd_client.incr('elastic_migration_document_count,index={0},type=success'.format(index_target),
                                   1)

    return ok_count, fail_count


statsd_client = statsd.StatsClient(host='localhost', port=8125)

if __name__ == "__main__":
    index_source_to_target(ES_SOURCE, ES_TARGET, QUERY_MATCH_ALL, INDEX_SOURCE, INDEX_TARGET, BULK_SIZE,
                           statsd_client, transform_item)
        

Заключение

Перенос данных в живом
продакшене – сложная задача, требующая большого внимания и тщательного
планирования. Мы рекомендуем потратить время, проработать шаги на тестовых
машинах и выяснить, что лучше всего подойдёт для вашей конкретной ситуации.

Всегда старайтесь
максимально снизить влияющие факторы. Например, требуется ли нулевой простой
или можно “тормознуть” некоторый функционал сервиса? Не станет ли потеря данных
проблемой?

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

17
Янв
2020

Веб-скрапинг по расписанию с Django и Heroku

оздаём Django-приложение, ежедневно проверяющее доску объявлений о работе. Парсим в BeautifulSoup, сохраняем в PostgreSQL, развёртываем на сервере Heroku.

Веб-скрапинг используется для сбора различной информации, в том числе для таких видов данных, которые регулярно обновляются, например, сообщений с досок объявлений о новых вакансиях. В этом проекте мы развернём на сервере Heroku приложение Django, которое посылает соответствующие уведомления о публикации новых мест работы.

12
Апр
2018

Выбор backend-сервер технологий для mobile social app

Прошу совета вот по какому вопросу.
Сейчас находится в зачаточном состоянии мобильный проект. Сам я Джавист с крайне небольшим опытом работы. Пока стартап будет писаться самим на коленках, а если выстрелит, то будет нанята команда программ…

12
Апр
2018

Выбор backend-сервер технологий для mobile social app

Прошу совета вот по какому вопросу.
Сейчас находится в зачаточном состоянии мобильный проект. Сам я Джавист с крайне небольшим опытом работы. Пока стартап будет писаться самим на коленках, а если выстрелит, то будет нанята команда программ…