Pull to refresh

Прикручиваем Web Map сервис к ничего не подозревающей OpenSource СУБД

Website development *Geoinformation services *
image
В прошлый раз мы конструировали пространственный индекс, а сейчас просто воспользуемся этим навыком, чтобы сделать живой (не статический) картографический сервис с web интерфейсом и производительностью, скажем, миллион запросов в день на совершенно обычном «железе».

Данные.


  • Их источником будет OSM
  • Форматом данных для хранения в СУБД будет сериализация из shpfile
  • В качестве тестовых данных будем использовать датасет России
  • Из всех слоев возьмем только три — водоемы, леса и здания (water-polygon, vegetation-polygon, building-polygon). Этого будет достаточно, чтобы подтвердить работоспособность технологии и оценить её производительность.

Заливка данных.


  • В качестве СУБД по-прежнему будем использовать готовый Virtuoso V7.0.0
  • Создадим по одноименной таблице на каждый интересующий нас слой
  • Поле типа long varbinary с геометрией будет называться «Shape»
  • Все существовавшие поля сохранятся
  • Добавятся новые
    • _OBJECTID_ — номер записи
    • minx_, miny_, maxx_, maxy_ — экстент записи
  • Создадим пространственный индекс аналогично тому, как мы это делали раньше.
  • Отличие в том, что у нас теперь площадные, а не точечные объекты, поэтому один объект может попасть в индекс несколько раз.
  • Растеризацию будем делать самую примитивную — в индекс попадает весь экстент объекта вне зависимости от его геометрии. А настоящую же растеризацию пока сочтём преждевременной оптимизацией.
  • Еще одно отличие заключается в неравномерности заселенности карты: благодаря Чукотке долгота меняется от -180 до +180 градусов, а широта от ~40 до 80. Поэтому шаг блочного индекса будем делать таким, чтобы в среднем в один блок индекса попал один объект. Почему именно так, будет рассказано позже.
  • Для работы с исходными данными будем использовать открытую библиотеку Shapefile C Library
  • Для работы с BLOB'ами придется добавить в неё функции SHPSerializeObject и SHPDeserializeObject, аналогичные родным SHPWriteObject и SHPReadObject, но для работы с памятью
  • Для общения с сервером станем использовать родной ODBC интерфейс, но используя встроенный драйвер, минуя системный менеджер драйверов
  • Для скорости, выключим автокоммит соединения и будем коммитить самостоятельно через каждые 1000 объектов
  • В таком режиме
    • заливка лесов (~660 тысяч объектов) занимает 1 мин 56 секунд
    • заливка водоемов (~380 тысяч объектов) — 1 мин 9 секунд
    • заливка зданий (~5.3 млн объектов) — 16 мин 18 секунд

Оценка производительности web-интерфейса.


  • Да, virtuoso имеет и web-интерфейс внешне похожий на php. Файлы vsp (virtuoso server pages) со вставками на PL/SQL превращаются в хранимые процедуры, которые формируют html выдачу.
    Мы создадим простенькую страницу (test.vsp), которая будет лишь наращивать счетчик и с её помощью проверим скорострельность сервера.
    <html><body>
    <?vsp
      declare cnt integer;
      cnt := sequence_next ('xxx.YYY.__cnt');
    ?>
    <table width="100%" border="1">
    <tr><td align="center"><?vsp= cnt ?></td></tr>
    </table>
    </body></html>
    
    Встроенная функция sequence_next атомарно наращивает счетчик и возвращает его значение.
  • Чтобы исполнить эту страничку, мы положим её в папку test в корне (параметр ServerRoot секции [HTTPServer] файла virtuoso.ini) и разрешим исполнять файлы из этой папки через isql (isql localhost:1111 dba dba)
    VHOST_DEFINE(lpath=>'/test/',ppath=>'/test/',vsp_user=>'DBA');
    
  • Проверим, что страница работает через
    localhost:8890/test/test.vsp
  • И станем массово требовать её, используя wget
  • Работа 20 параллельных wget-ов, каждый из которых 10 000 раз просил упомянутую страничку, на десктопе под windows (i7, 8 ядер) заняла 28 сек. Следовательно, искомая производительность ~7000 запросов в секунду и для нас она никаким образом (учитывая, что мы нацелились на миллион запросов в день) не может быть узким местом.

Рисуем карту


  • Во-первых, как нам достать данные? Примерно так:
        for select blob_to_string(Shape) as data from 
          xxx.YYY."water-polygon" as x, 
          xxx.YYY."v_water-polygon_spx_enum_items_in_box" as a
        where a.minx = cminx and a.miny = cminy and a.maxx = cmaxx and a.maxy = cmaxy 
          and x."_OBJECTID_" = a.oid
          and x.maxx_ >= cminx and x.minx_ <= cmaxx
          and x.maxy_ >= cminy and x.miny_ <= cmaxy
        do 
        { ... }
    
    Разберемся с этим запросом поподробнее.
    • xxx.YYY."water-polygon" — таблица водоемов, которую мы только что создали и залили данными.
    • xxx.YYY."v_water-polygon_spx_enum_items_in_box" — процедурное view поверх вспомогательной таблицы, выполняющей роль блочного пространственного индекса. Фактически, это грубый пространственный фильтр, работающий с точностью до размера блока пространственного индекса.
    • a.minx = cminx and... — мы задаем параметры вызова view, cminx ... — границы фрагмента карты, который мы хотим нарисовать
    • x."_OBJECTID_" = a.oid — join между таблицей данных и синтетическим резалтсетом, полученным из процедурного view
    • x.maxx_ >= cminx and x.minx_ <= cmaxx ... промежуточный пространственный фильтр, позволяющий не лезть за блобами с геометриями, если они заведомо не пригодятся
    • Все вместе это дает нам возможность в теле цикла заниматься, например, рисованием с хорошим КПД. При этом, само рисование можно рассматривать как тонкий пространственный фильтр.
  • Раз у нас три слоя данных, придется выполнить три подзапроса на каждый клиентский запрос
  • Теперь про графику. Для того, чтобы рисовать непосредственно в процессе работы курсора, придется писать С — plugin. Он представляет собой динамическую библиотеку, которую сервер загружает при старте и дает возможность зарегистрировать новые встроенные функции PL/SQL.
  • Собственно для рисования будем использовать библиотеку GD
  • Добавим следующие функции:
    • img_create — создает контекст для рисования заданного размера
    • img_tostr — делает gif из текущего контекста
    • img_fromptr — магия virtuoso для смены тэга указателя
    • img_destroy — уничтожаем контекст
    • img_alloc_color — делаем идентификатор цвета из RGB значений
    • img_draw_polyline — рисуем полилинию из BLOB-а, куда сохранен shape объект
    • img_draw_polygone — то же для полигона
  • Содержательная часть плагина под спойлером
    #ifdef _USRDLL
    #include "plugin.h"
    #include "import_gate_virtuoso.h"
    #define wi_inst (wi_instance_get()[0])
    #else
    #include <libutil.h>
    #include "sqlnode.h"
    #include "sqlbif.h"
    #include "wi.h"
    #include "Dk.h"
    #endif
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <math.h>
    
    #include "gd_utils.h"
    #include "gd/gd_lib.h"
    #include "shplib/shapefil.h"
    
    typedef struct img_ctx_s {
    	int dx_;
    	int dy_;
    
    	double minx_;
    	double miny_;
    	double maxx_;
    	double maxy_;
    
    	double mulx_;
    	double muly_;
    
    	int black_;
    	int red_;
    	int green_;
    	int blue_;
    
    	int attr_;
    
    	gdImagePtr img_;
    } img_ctx_t;
    
    
    caddr_t 
    img_create_proc (caddr_t * qst, caddr_t * err, state_slot_t ** args)
    {
    	img_ctx_t *ptr = (img_ctx_t *)dk_alloc_box (sizeof (img_ctx_t), DV_STRING);
    	ptr->dx_ = bif_long_arg (qst, args, 0, "img_create_proc");
    	ptr->dy_ = bif_long_arg (qst, args, 1, "img_create_proc");
    	ptr->minx_ = bif_double_arg (qst, args, 2, "img_create_proc");
    	ptr->miny_ = bif_double_arg (qst, args, 3, "img_create_proc");
    	ptr->maxx_ = bif_double_arg (qst, args, 4, "img_create_proc");
    	ptr->maxy_ = bif_double_arg (qst, args, 5, "img_create_proc");
    	ptr->attr_ = bif_long_arg (qst, args, 6, "img_create_proc");
    	ptr->img_ = gdImageCreateTrueColor (ptr->dx_, ptr->dy_);
    
    	ptr->mulx_ = ptr->dx_/fabs(ptr->maxx_ - ptr->minx_);
    	ptr->muly_ = ptr->dy_/fabs(ptr->maxy_ - ptr->miny_);
    
    	ptr->black_ = gdImageColorAllocate (ptr->img_, 0, 0, 0);
    	ptr->blue_ = gdImageColorAllocate (ptr->img_, 0, 0, 255);
    	ptr->red_ = gdImageColorAllocate (ptr->img_, 255, 0, 0);
    	ptr->green_ = gdImageColorAllocate (ptr->img_, 0, 255, 0);
    
    	return (caddr_t)ptr;
    }
    
    caddr_t 
    img_saveas_proc (caddr_t * qst, caddr_t * err, state_slot_t ** args)
    {
    	img_ctx_t *ptr = (img_ctx_t *)bif_arg (qst, args, 0, "img_saveas_proc");
    	gdImagePtr im = ptr->img_;
    	caddr_t fname = bif_string_arg (qst, args, 1, "img_saveas_proc");
    
    	FILE *out = fopen (fname, "wb");
    	if (NULL != out)
    	{
    		gdImageGif (im, out);
    		fclose (out);
    		return (caddr_t)0;
    	}
    	return (caddr_t)-1;
    }
    
    static unsigned char clip(int value)
    {
    	if (value < 0)
    		value = 0;
    	else if (value > 255)
    		value = 255;
    	return value;
    }
    
    caddr_t 
    img_fromptr_proc (caddr_t * qst, caddr_t * err, state_slot_t ** args)
    {
    	ptrlong addr = unbox(bif_long_arg (qst, args, 0, "img_fromptr_proc"));
    	return addr;
    }
    
    caddr_t 
    img_tostr_proc (caddr_t * qst, caddr_t * err, state_slot_t ** args)
    {
    	img_ctx_t *ptr = (img_ctx_t *)bif_arg (qst, args, 0, "img_tostr_proc");
    	gdImagePtr im = ptr->img_;
    	void *rv = NULL;
    	caddr_t ret = NULL;
    	int size = 0;
    	gdIOCtx *out = gdNewDynamicCtx (2048, NULL);
    
    	gdImageGifCtx (im, out);
    	rv = gdDPExtractData (out, &size);
    	if (NULL == rv || size <= 0)
    		return 0;
    	out->gd_free (out);
    	ret = dk_alloc_box (size, DV_STRING);
    	memcpy (ret, rv, size);
    	gdFree(rv);
    	return (caddr_t)box_num (ret);
    }
    
    caddr_t
    img_destroy_proc (caddr_t * qst, caddr_t * err, state_slot_t ** args)
    {
    	img_ctx_t *ptr = (img_ctx_t *)bif_arg (qst, args, 0, "img_destroy_proc");
    	gdImagePtr im = ptr->img_;
    	gdImageDestroy(im);
    	/*dk_free_box (ptr);*/
    	return 0;
    }
    
    caddr_t
    img_draw_polyline_proc (caddr_t * qst, caddr_t * err, state_slot_t ** args)
    {
    	img_ctx_t *ptr = (img_ctx_t *)bif_arg (qst, args, 0, "img_draw_polyline_proc");
    	gdImagePtr im = ptr->img_;
    	caddr_t data = bif_string_arg (qst, args, 1, "img_draw_polyline_proc");
    
    	SHPObject *shp = SHPDeserialize (data);
    	int nparts = shp->nParts;
    	int npoints = shp->nVertices;
    	double *px = shp->padfX;
    	double *py = shp->padfY;
    	int i, j;
    
    	int color = bif_long_arg (qst, args, 2, "img_draw_polyline_proc");
    	gdImageSetAntiAliased (im, color);
    
    	for (i = 0; i < nparts; i++)
    	{
    		long ps = shp->panPartStart[i];
    		long psz = (i==(nparts-1))? npoints-ps : shp->panPartStart[i+1]-ps;
    
    		gdPointPtr points = (gdPointPtr)malloc(sizeof (gdPoint) * psz);
    		for (j = 0; j < psz; j++)
    		{
    			points[j].x = (px[ps+j] - ptr->minx_) * ptr->mulx_;
    			points[j].y = (ptr->attr_ & 1) ?
    				ptr->dy_ - (py[ps+j] - ptr->miny_) * ptr->muly_:
    			(py[ps+j] - ptr->miny_) * ptr->muly_;
    		}
    		for (j = 1; j < psz; j++)
    		{
    			/*if (points[j].x == points[j-1].x && points[j].y == points[j-1].y)
    				gdImageSetPixel (im, points[j].x, points[j].y, ptr->blue_);
    			else*/
    			gdImageLine (im, points[j].x, points[j].y, points[j-1].x, points[j-1].y, gdAntiAliased);
    		}
    		free (points);
    	}
    	SHPDestroyObject (shp);
    	return 0;
    }
    
    caddr_t 
    img_draw_polygone_proc (caddr_t * qst, caddr_t * err, state_slot_t ** args)
    {
    	img_ctx_t *ptr = (img_ctx_t *)bif_arg (qst, args, 0, "img_draw_polygone_proc");
    	gdImagePtr im = ptr->img_;
    	caddr_t data = bif_string_arg (qst, args, 1, "img_draw_polygone_proc");
    
    	SHPObject *shp = SHPDeserialize (data);
    
    	int nparts = shp->nParts;
    	int npoints = shp->nVertices;
    	double *px = shp->padfX;
    	double *py = shp->padfY;
    	int i,j;
    
    	int color = bif_long_arg (qst, args, 2, "img_draw_polygone_proc");
    	int bcolor = bif_long_arg (qst, args, 3, "img_draw_polygone_proc");
    
    	for (i = 0; i < nparts; i++)
    	{
    		long ps = shp->panPartStart[i];
    		long psz = (i==(nparts-1))? npoints-ps : shp->panPartStart[i+1]-ps;
    
    		gdPointPtr points = (gdPointPtr)malloc (sizeof (gdPoint) * psz);
    		for (j = 0; j < psz; j++)
    		{
    			points[j].x = (px[ps+j] - ptr->minx_) * ptr->mulx_;
    			points[j].y = (ptr->attr_ & 1) ?
    				ptr->dy_ - (py[ps+j] - ptr->miny_) * ptr->muly_:
    			(py[ps+j] - ptr->miny_) * ptr->muly_;
    		}
    		gdImageSetAntiAliased (im, color);
    		gdImageFilledPolygon (im, points, psz, gdAntiAliased);
    		if (bcolor >= 0)
    		{
    			gdImageSetAntiAliased (im, bcolor);
    			gdImagePolygon (im, points, psz, gdAntiAliased);
    		}
    		free (points);
    	}
    	SHPDestroyObject(shp);
    	return 0;
    }
    
    caddr_t
    img_alloc_color_proc (caddr_t * qst, caddr_t * err, state_slot_t ** args)
    {
    	img_ctx_t *ptr = (img_ctx_t *)bif_arg (qst, args, 0, "img_alloc_color_proc");
    	gdImagePtr im = ptr->img_;
    	int r = bif_long_arg (qst, args, 1, "img_alloc_color_proc"); 
    	int g = bif_long_arg (qst, args, 2, "img_alloc_color_proc"); 
    	int b = bif_long_arg (qst, args, 3, "img_alloc_color_proc"); 
    
    	return  box_num(gdImageColorAllocateAlpha (ptr->img_, r, g, b, 0));
    }
    
    void 
    init_shcall_gd_utils ()
    {
    	bif_define ("img_create", img_create_proc);
    	bif_define ("img_saveas", img_saveas_proc);
    	bif_define ("img_tostr", img_tostr_proc);
    	bif_define ("img_fromptr", img_fromptr_proc);
    	bif_define ("img_destroy", img_destroy_proc);
    	bif_define ("img_alloc_color", img_alloc_color_proc);
    	bif_define ("img_draw_polyline", img_draw_polyline_proc);
    	bif_define ("img_draw_polygone", img_draw_polygone_proc);
    }
    

  • Теперь рисование водоемов будет выглядеть так:
        declare img any;
        img := img_create (512, 512, cminx, cminy, cmaxx, cmaxy, 1);
        declare cl integer;
        declare bg integer;
        cl := img_alloc_color (img, 0, 0, 255);
        bg := img_alloc_color (img, 0, 0, 200);
    
        whenever not found goto nf;
        for select blob_to_string(Shape) as data from 
            xxx.YYY."water-polygon" as x, 
            xxx.YYY."v_water-polygon_spx_enum_items_in_box" as a
        where 
            a.minx = cminx and a.miny = cminy 
            and a.maxx = cmaxx and a.maxy = cmaxy 
            and x."_OBJECTID_" = a.oid
            and x.maxx_ >= cminx and x.minx_ <= cmaxx
            and x.maxy_ >= cminy and x.miny_ <= cmaxy
        do 
        {
             img_draw_polygone (img, data, cl, bg);
        }
        nf:;
    
        declare ptr integer;
        ptr := img_tostr (img); -- сделали gif
        img_destroy (img);     -- освободили контекст
        declare image any;
        image := img_fromptr(ptr); -- магия деструктора
    
        http_header ('Content-type: image/gif\t\n'); -- формирование заголовка
        http(image);                         -- выдача результата в ответный поток
    
  • Передачу координат страница сама себе будет передавать через параметры URL,
    тело страницы под спойлером

    <?vsp
      if ({?'getfile'} <> '')
        {
          http_header ('Content-type: image/gif\t\n');         -- set the header to jpg
    
          declare s_params any;
          s_params := deserialize (decode_base64 (get_keyword ('params', params, '')));
    --      dbg_obj_print(s_params);
          declare "min_x" double precision;
          declare "min_y" double precision;
          declare "max_x" double precision;
          declare "max_y" double precision;
    
          declare "cminx" double precision;
          declare "cminy" double precision;
          declare "cmaxx" double precision;
          declare "cmaxy" double precision;
          declare "s" varchar;
     
          s := get_keyword ('minx', s_params, '');
          if (s = '') 
            cminx := min_x;
          else
            cminx := atof(s);
          s := get_keyword ('miny', s_params, '');
          if (s = '') 
            cminy := min_y;
          else
            cminy := atof(s);
          s := get_keyword ('maxx', s_params, '');
          if (s = '') 
            cmaxx := max_x;
          else
            cmaxx := atof(s);
          s := get_keyword ('maxy', s_params, '');
          if (s = '') 
            cmaxy := max_y;
          else
            cmaxy := atof(s);
    
          declare img any;
          img := img_create (512, 512, cminx, cminy, cmaxx, cmaxy, 1);
          declare cl integer;
          declare bg integer;
      {
        cl := img_alloc_color (img, 0, 0, 255);
        bg := img_alloc_color (img, 0, 0, 200);
    
        whenever not found goto nf;
        for select blob_to_string(Shape) as data from 
          xxx.YYY."water-polygon" as x, xxx.YYY."v_water-polygon_spx_enum_items_in_box" as a
        where a.minx = cminx and a.miny = cminy and a.maxx = cmaxx and a.maxy = cmaxy 
          and x."_OBJECTID_" = a.oid
          and x.maxx_ >= cminx and x.minx_ <= cmaxx
          and x.maxy_ >= cminy and x.miny_ <= cmaxy
        do 
        {
          img_draw_polygone (img, data, cl, bg);
        }
    nf:;
    
        cl := img_alloc_color (img, 0, 255, 0);
        bg := img_alloc_color (img, 0, 255, 0);
        whenever not found goto nf2;
        for select blob_to_string(Shape) as data from 
          xxx.YYY."vegetation-polygon" as x, xxx.YYY."v_vegetation-polygon_spx_enum_items_in_box" as a
        where a.minx = cminx and a.miny = cminy and a.maxx = cmaxx and a.maxy = cmaxy 
          and x."_OBJECTID_" = a.oid
          and x.maxx_ >= cminx and x.minx_ <= cmaxx
          and x.maxy_ >= cminy and x.miny_ <= cmaxy
        do 
        {
          img_draw_polygone (img, data, cl, bg);
        }
    nf2:;
    
        cl := img_alloc_color (img, 255, 0, 0);
        bg := img_alloc_color (img, 255, 0, 0);
        whenever not found goto nf3;
        for select blob_to_string(Shape) as data from 
          xxx.YYY."building-polygon" as x, xxx.YYY."v_building-polygon_spx_enum_items_in_box" as a
        where a.minx = cminx and a.miny = cminy and a.maxx = cmaxx and a.maxy = cmaxy 
          and x."_OBJECTID_" = a.oid
          and x.maxx_ >= cminx and x.minx_ <= cmaxx
          and x.maxy_ >= cminy and x.miny_ <= cmaxy
        do 
        {
          img_draw_polygone (img, data, cl, bg);
        }
    nf3:;
      }
    
      declare ptr integer;
      ptr := img_tostr (img);
      img_destroy (img);
    
      declare image any;
      image := img_fromptr(ptr);
      http(image);                                         -- table and display
       --dbg_obj_print('boom',{?'id'});
      return;
    }
      ?>
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
    <?vsp 
      declare "min_x" double precision;
      declare "min_y" double precision;
      declare "max_x" double precision;
      declare "max_y" double precision;
      min_x := 82.;
      max_x := 84.;
      min_y := 54.;
      max_y := 56.;
      params := vector_concat (params, 
        vector ('minx', sprintf('%g', min_x), 
                'maxx', sprintf('%g', max_x), 
                'miny', sprintf('%g', min_y), 
                'maxy', sprintf('%g', max_y)));
    ?>
    <html>
    <body zonload="window.location.reload();">
    
    <script language="JavaScript" type="text/javascript">
    function full_extent() 
    {
      document.forms['form1'].minx.value=<?vsp=min_x ?>;
      document.forms['form1'].miny.value=<?vsp=min_y ?>;
      document.forms['form1'].maxx.value=<?vsp=max_x ?>;
      document.forms['form1'].maxy.value=<?vsp=max_y ?>;
    }
    </script>
    <script language="JavaScript" type="text/javascript">
    function zoom_in() 
    {
      var dx = Math.abs(document.forms['form1'].maxx.value - document.forms['form1'].minx.value);
      dx = dx/8;
      var tmp = document.forms['form1'].maxx.value;
      tmp -= dx;
      document.forms['form1'].maxx.value = tmp;
      tmp = document.forms['form1'].minx.value;
      tmp -= -dx;
      document.forms['form1'].minx.value = tmp;
    
      var dy = Math.abs(document.forms['form1'].maxy.value - document.forms['form1'].miny.value);
      dy = dy/8;
      tmp = document.forms['form1'].maxy.value;
      tmp -= dy;
      document.forms['form1'].maxy.value = tmp;
      tmp = document.forms['form1'].miny.value;
      tmp -= -dy;
      document.forms['form1'].miny.value = tmp;
    }
    </script>
    <script language="JavaScript" type="text/javascript">
    function zoom_out() 
    {
      var dx = -Math.abs(document.forms['form1'].maxx.value - document.forms['form1'].minx.value);
      dx = dx/8;
      var tmp = document.forms['form1'].maxx.value;
      tmp -= dx;
      document.forms['form1'].maxx.value = tmp;
      tmp = document.forms['form1'].minx.value;
      tmp -= -dx;
      document.forms['form1'].minx.value = tmp;
    
      var dy = -Math.abs(document.forms['form1'].maxy.value - document.forms['form1'].miny.value);
      dy = dy/8;
      tmp = document.forms['form1'].maxy.value;
      tmp -= dy;
      document.forms['form1'].maxy.value = tmp;
      tmp = document.forms['form1'].miny.value;
      tmp -= -dy;
      document.forms['form1'].miny.value = tmp;
    }
    </script>
    <script language="JavaScript" type="text/javascript">
    function pan_left() 
    {
      var dx = Math.abs(document.forms['form1'].maxx.value - document.forms['form1'].minx.value);
      dx = dx/8;
      document.forms['form1'].maxx.value -= dx;
      document.forms['form1'].minx.value -= dx;
    }
    function pan_right() 
    {
      var dx = -Math.abs(document.forms['form1'].maxx.value - document.forms['form1'].minx.value);
      dx = dx/8;
      document.forms['form1'].maxx.value -= dx;
      document.forms['form1'].minx.value -= dx;
    }
    function pan_up() 
    {
      var dy = -Math.abs(document.forms['form1'].maxy.value - document.forms['form1'].miny.value);
      dy = dy/8;
      document.forms['form1'].maxy.value -= dy;
      document.forms['form1'].miny.value -= dy;
    }
    function pan_down() 
    {
      var dy = Math.abs(document.forms['form1'].maxy.value - document.forms['form1'].miny.value);
      dy = dy/8;
      document.forms['form1'].maxy.value -= dy;
      document.forms['form1'].miny.value -= dy;
    }
    </script>
    <form method="GET" id="form1" name="form1">
    <?vsp
      declare "cminx" double precision;
      declare "cminy" double precision;
      declare "cmaxx" double precision;
      declare "cmaxy" double precision;
      declare "s" varchar;
     
      s := get_keyword ('minx', params, '');
      if (s = '') 
        cminx := min_x;
      else
        cminx := atof(s);
      s := get_keyword ('miny', params, '');
      if (s = '') 
        cminy := min_y;
      else
        cminy := atof(s);
      s := get_keyword ('maxx', params, '');
      if (s = '') 
        cmaxx := max_x;
      else
        cmaxx := atof(s);
      s := get_keyword ('maxy', params, '');
      if (s = '') 
        cmaxy := max_y;
      else
        cmaxy := atof(s);
    
      declare mashtab double precision;
      mashtab := floor((cmaxx-cminx)*96./(512.*2.54));
    
      declare cnt integer;
      cnt := sequence_next ('xxx.YYY.__cnt');
    ?>
    
    <input type="hidden" name="minx" value='<?vsp=cminx ?>'/>
    <input type="hidden" name="miny" value='<?vsp=cminy ?>'/>
    <input type="hidden" name="maxx" value='<?vsp=cmaxx ?>'/>
    <input type="hidden" name="maxy" value='<?vsp=cmaxy ?>'/>
    
    <table width="100%" border="1" cellpadding="0" cellspacing="0" summary="">
    <tr>
    <td width="20%" align="center" > </td>
    <td width="20%" align="center" >Min X</td>
    <td width="20%" align="center" >Min Y</td>
    <td width="20%" align="center" >Max X</td>
    <td width="20%" align="center" >Max Y</td>
    </tr>
    
    <tr>
    <td width="20%" align="center" >Default extent</td>
    <td width="20%" align="center" > <?vsp=min_x ?> </td>
    <td width="20%" align="center" > <?vsp=min_y ?> </td>
    <td width="20%" align="center" > <?vsp=max_x ?> </td>
    <td width="20%" align="center" > <?vsp=max_y ?> </td>
    </tr>
    
    <tr>
    <td width="20%" align="center" >Current extent</td>
    <td width="20%" align="center" > <?vsp=sprintf('%10.3f', cminx) ?> </td>
    <td width="20%" align="center" > <?vsp=sprintf('%10.3f', cminy) ?> </td>
    <td width="20%" align="center" > <?vsp=sprintf('%10.3f', cmaxx) ?> </td>
    <td width="20%" align="center" > <?vsp=sprintf('%10.3f', cmaxy) ?> </td>
    </tr>
    
    <tr>
    <td width="20%" align="center" > </td>
    <td width="20%" align="center" >
    <input type="button" onclick="javascript: full_extent(); document.forms['form1'].submit ();" value="[*]"/>
    </td>
    <td width="20%" align="center" >
    <input type="button" onclick="javascript: zoom_in(); document.forms['form1'].submit ();" value="+" />
    <br><input type="button" onclick="javascript: zoom_out(); document.forms['form1'].submit ();" value="-" />
    </td>
    <td width="20%" align="center" >
    <input type="button" onclick="javascript: pan_up(); document.forms['form1'].submit ();" value="^" />
    <br>
    <input type="button" onclick="javascript: pan_down(); document.forms['form1'].submit ();" value="V" />
    </td>
    <td width="20%" align="center" >
    <input type="button" onclick="javascript: pan_left(); document.forms['form1'].submit ();" value="<" />
    <input type="button" onclick="javascript: pan_right(); document.forms['form1'].submit ();" value=">" />
    </td>
    </tr>
    
    <tr><td colspan=5 align="center">
    <p><img src=""></p>
    </td></tr>
    </table>
    </form></body></html>
    

  • И вот результат:
    image

Оценка производительности.


  • Делать мы это будем примерно так же, как когда исследовали собственно web-интерфейс
  • Запустим параллельно 4 wget'а, каждый из которых сделает 10 000 пар запросов — к странице и картинке. Внутри одного wget'а запросы одинаковые, между ними разные. Сделано это ввиду того, что данных не очень много, и они так или иначе окажутся в памяти, но если запросы сделать случайными, возникнет трудно оцениваемое время на разогрев. А если все запросы сделать одинаковыми, они начнут толкаться локтями т.к. требования транзакционности никто не отменял.
  • Т.к. после запуска процессов в background'е стоит барьер (wait в bash), замер будет проведен по самому медленному запросу.
  • Запросы будут не очень большого размера ~3X3 км,
  • Внимательный читатель скажет, что данные тесты заточены под настройки индекса и здесь нет универсальности. Да, это так. Природа блочного индекса такова, что он эффективно работает только на запросах, соизмеримых с размером его ячейки. Это с одной стороны. А с другой,
    никто не запрещает нам использовать несколько блочных индексов одновременно для разных масштабов карты. Кроме того, на блочных индексах свет клином не сошелся. Если рассматривать экстент как четырехмерную точку, можно использовать, например, и точечные индексы.
  • Итак, общее время работы — 8 мин 12 сек. Или 81 запрос в секунду.
  • На миллион запросов уходит 3 часа 32 мин, т.е. существенной деградации нет.
  • Поток данных через сеть — 52 мбит/сек надо поделить пополам т.к. у нас и клиенты и сервер на одном хосте
  • Загрузка процессора — 68%, сервер занимает 66 из них
  • Сервер занимает от 1.1 до 1.6 Гб оперативной памяти, утечек, по видимому, нет, во всяком случае, после миллиона запросов её было использовано 1.3 Гб
  • Объём файла базы данных — ~3 Гб

Выводы


  • Цель по производительности достигнута, опыт показал, что миллион запросов в день легко обрабатываются с солидным запасом под пиковые нагрузки.
  • При том, что десктопный windows совсем не серверная ОС
  • Да и железо было не серверное
  • Следует отметить, что представлен даже не прототип, а демонстратор технологий. Использовано всего три слоя, пусть и довольно населенных. В реальной жизни слоёв, конечно, десятки, но не все они нужны одновременно. Чтобы облегчить жизнь, на мелких масштабах используются генерализованные слои. И т.д. и т.п. Но, еще раз, не ставилось задачи повторить OSM, только проверить работоспособность технологии.
  • Если кажется, что интерфейс… аскетичный, стоит обратиться к предыдущему пункту.
  • Не реализован labeling…
  • Не реализованы проекции…
  • Важной особенностью данной технологии является императивное описание проекта карты — в виде текста PL/SQL. В этом есть свои минусы, но и несомненнй плюс — гибкость и полная свобода действий. Так можно реализовать систему с мощью полноценной ГИС и производительностью статической пирамиды тайлов. Например, начать подписывать улицы поверх указателей пробок.

PS:

Кто первым правильно назовёт перевал под номером 40 на фрагменте карты в шапке статьи, получит благодарность автора.
Tags: open sourcerdbmsweb mappingspatial index
Hubs: Website development Geoinformation services
Total votes 8: ↑8 and ↓0 +8
Comments 3
Comments Comments 3

Popular right now