На данный момент мы создали минимальный сервер тайлов карты, который отображает их с помощью PostGIS. Наши плитки постепенно приобрели некоторые особенности.

До какой-либо оптимизации среднее время рендеринга приведенных выше карт составляло 1,1, 2,4 и 5,4 секунды на тайл.

Добавление пространственных индексов

Начнем с индексации. Для своей карты я работал с двумя таблицами. Simplified_land_polygons загружается из osm data и planet_osm_line, который включает данные Финляндии из всего набора данных Planet-OSM. Пространственная индексация работает аналогично любому другому индексу в базах данных. Однако только некоторые функции PostGIS используют пространственные индексы, в нашем проекте только ST_intersects использует пространственную индексацию. Пространственные индексы индексируют ограничивающие рамки геометрии. При запуске функций, которые могут использовать пространственные индексы, база данных сначала оценивает ограничивающие рамки, а затем все геометрии, имеющие ограничивающие рамки, выполняют функции. Ограничивающие рамки сохраняются в структуре данных, называемой R-дерево, которая представляет собой сбалансированное дерево поиска. В отличие от B-деревьев, вместо сравнения размеров мы сравниваем, какие ограничивающие рамки находятся внутри друг друга.

Небольшой пример того, как работает пространственная индексация.

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

Добавление индексов

Я добавил индексы напрямую через psql с помощью CREATE INDEX {index_name} ON {table_name} USING GIST ({column name});. После этого я запустил команду ANALYZE {table_name};, чтобы убедиться, что система статистики postgreSQL обновлена. Индексирование таблицы полигонов суши не оказало существенного влияния на время рендеринга, но индексирование таблицы Planet_osm_line уменьшило среднее время рендеринга с 5,4 секунды до 4,3 секунды. Скорее всего, это связано с размерами таблицы: у Land Polygons 63 539 строк, а у Planet_osm_line — 2 835 593 строки. Это, конечно, пока еще неприемлемо большая цифра, но это уже сокращение -19%. Обратите внимание, что я запускаю сервер на своем ноутбуке, поэтому процессор не самый лучший.

Добавление кэша

На данный момент должно быть очевидно, что рендеринг каждого тайла для каждого запроса требует слишком больших вычислительных затрат. Давайте тогда добавим кеш. Я собираюсь использовать довольно простую настройку с помощью Redis. Redis сохраняет все свои данные в памяти, чтобы можно было быстро обрабатывать значения. Я настроил док-контейнер Redis с привязкой порта к порту 6379 (порт по умолчанию, используемый Redis). Поскольку данные работают в памяти, я не хочу, чтобы кеш стал слишком большим, поэтому я создал файл redis.conf со следующей конфигурацией.

maxmemory 200mb
maxmemory-policy allkeys-lru

Политика памяти сообщает Redis, как действовать при достижении максимального значения памяти. Я использовал политику lru (наименее использовавшуюся в последнее время). Другими словами, когда кеш заполнен, Redis удалит из кеша последний использованный фрагмент. Примечание! Как поясняется в документации Redis, Redis на самом деле не знает, какой ключ использовался реже всего, вместо этого он использует приближение, поэтому действительно последний использованный ключ не обязательно удаляется. В нашем случае это не оказывает существенного влияния на работу нашего сервера. Теперь мы можем запустить файл docker с помощью команды $ docker run -v /myredis/conf:/usr/local/etc/redis --name rediscache redis redis-server /usr/local/etc/redis/redis.conf. По умолчанию Redis сохраняет снимки набора данных на диске, поэтому наш кеш сохраняется, даже если нам нужно перезапустить или приостановить контейнер.

Теперь у нас есть следующая функция промежуточного программного обеспечения:

app.get("/tiles/:z/:x/:y", async function (req, res) {
  const { z, x, y } = req.params;
  if (pathMakesSense(parseInt(z), parseInt(x), parseInt(y))) {
    try {
      redisValue = await redis_client.get(`${z}_${x}_${y}`);
      if (redisValue) {
        res.writeHead(200, {
          "Content-Type": "image/png",
          "Content-Length": Buffer.from(redisValue, "hex").length,
        });
        res.end(Buffer.from(redisValue, "hex"));
      } else {
        let response = await pg_client.query(query, [z, x, y]);
        let img = response.rows[0].st_aspng;
        res.writeHead(200, {
          "Content-Type": "image/png",
          "Content-Length": img.length,
        });
        res.end(img);
        redis_client.set(`${z}_${x}_${y}`, img.toString("hex"));
      }
    } catch (error) {
      console.log(error);
    }
  } else {
    res.writeHead(400);
    res.end("Incorrect path");
  }
});

Сначала мы извлекаем переменные z, x, y из объекта req.params. Затем мы проверяем корректность пути с помощью вспомогательной функции pathMakesSense:

const pathMakesSense = (z, x, y) => {
  const maxCoord = 2 ** z;
  return z >= 0 && z <= 20 && x >= 0 && x < maxCoord && y >= 0 && y < maxCoord;
};

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

Предварительно визуализированные плитки

В дополнение к динамическому кешу я также хочу иметь предварительно обработанные тайлы. Давайте проведем несколько простых вычислений, чтобы увидеть, сколько тайлов мы хотим предварительно отрисовать. Как указано в первой части, каждый уровень масштабирования z содержит z^4 тайлов. Если мы хотим визуализировать все тайлы от 0 до n, нам понадобится (1–4^(n+1))/-3 тайлов. Наши png-изображения имеют три полосы, каждая из которых имеет значение 8 бит (один байт). Таким образом, для png размером 256*256 пикселей максимальный размер (без учета заголовков и магического числа) составит 256⋅256⋅3 = 196,6 КБ. К счастью, png использует сжатие без потерь, а функция ST_aspng способна сжимать PNG. Поскольку наши плитки очень просты и не содержат большого количества функций, сжатый png-файл наших плиток не имеет размера и близко к 196 КБ. Я подсчитал, что 5 КБ/тайл более реалистично. Теперь мы можем создать следующую таблицу.

Из приведенной выше таблицы мы можем легко оценить, до какого уровня масштабирования мы хотим создать предварительно обработанный полистный кэш. Я выбираю уровень до 6, потому что помимо требований к размеру данных нам также нужно учитывать вычислительные требования. Моему ноутбуку требуется около 4 секунд для рендеринга каждого тайла, поэтому рендеринг тайлов до уровня 6 занял около 4 часов. Я также реализовал предварительно обработанный кеш с помощью Redis. Я хотел, чтобы предварительно обработанный кэш и динамический кэш были отдельными системами, поэтому я запустил новый контейнер Redis, работающий на другом порту (порт произвольный, я выбрал 6380). Я добавил собственный файл для пререндеринга тайлов (.src/prerenderer.js), который можно запустить командой npm run prerender {n}, где n — уровень масштабирования, до которого мы хотим рендерить наши тайлы. На главном сервере мы добавляем следующее предложение в нашу функцию промежуточного программного обеспечения.

Заключение

Теперь у нас есть сервер тайлов, который отображает тайлы .png напрямую с помощью PostGIS. Мы создали два кеша: динамический и предварительно обработанный. Посмотрим, как это работает. Первая гифка — напоминание о ситуации до того, как мы начали оптимизировать скорость, а второе изображение — текущая ситуация.

Код этой части можно найти в этот репозиторий GitHub в ветке main.
Вероятно, это последняя часть этой серии.
Спасибо за чтение, комментарии, критику и предложения приветствуются. добро пожаловать как всегда.

Оригинально опубликовано на сайте https://dev.to 31 августа 2023 г.