Comparing PostGIS-backend Vectortile servers: tegola/martin/pg_tileserv
Tags: postgres, gis, webdev, postgis
Published: 2022-09-24 16:03:34

## Abstract

In ordinary cases, vectortiles are statically served as files converted from GeoJSON or other datasources. This is the best way to serve tiles in terms of performances and costs for servers but there are some typical problems.

- Need computing costs for converting, this can be a bigger problem when a number of tiles is huger.
- Impossible to serve data equal to database.

Therefore, it is very important to be able to dynamically serve tiles directly from DB. Offcourse, in this approach has other challenges such as performances so you need to consider which approach should be taken and why, how.

There are some implementations in PostGIS-backend Vectortile server in mid 2022. These can be used to serve vectortiles from PostGIS tables.

- [t-rex(Rust)](https://github.com/t-rex-tileserver/t-rex)
- [tegola(Go)](https://github.com/go-spatial/tegola)
- [martin(Rust)](https://github.com/maplibre/martin)
- [pg_tileserv(Go)](https://github.com/CrunchyData/pg_tileserv)

martin and pg_tileserv is based on `ST_AsMVT` in PostGIS functions and t-rex and tegola are not. This is because martin and pg_tileserv are newer than others. In this article, I compared tegola, martin and pg_tileserv.

## Setting servers

### PostgreSQL

First, let's launch PostGIS and insert sample data, using Docker…

```yml
  postgis:
    image: kartoza/postgis:12.4
    environment:
      - POSTGRES_USER=docker
      - POSTGRES_PASS=docker
      - POSTGRES_DB=postgres
    ports:
      - "5432:5432"
    volumes:
      - postgis-data:/var/lib/postgresql
      - ./postgres:/usr/src/app
    networks:
      - default
    healthcheck:
      test: [ "CMD-SHELL", "pg_isready" ]
      interval: 10s
      timeout: 5s
      retries: 5
```

launch PostgreSQL...

```sh
docker-compose up -d postgis
```

Next, get and insert data. I used geofabrik shapefiles.

```sh
wget https://download.geofabrik.de/asia/japan/kanto-latest-free.shp.zip
unzip kanto-latest-free.shp.zip
ogr2ogr PG:postgresql://docker:docker@localhost:5432/postgres gis_osm_roads_free_1.shp
ogr2ogr PG:postgresql://docker:docker@localhost:5432/postgres gis_osm_buildings_a_free_1.shp -nlt MultiPolygon
```

Then, you can see tables in PostgreSQL.

```sql
SELECT * FROM gis_osm_roads_free_1 LIMIT 10;
SELECT * FROM gis_osm_buildings_a_free_1 LIMIT 10;
```

### tegola

writting compose.yml...

```yml
  tegola:
    image: gospatial/tegola
    ports:
      - "8080:8080"
    depends_on:
      postgis:
        condition: service_healthy
    volumes:
      - ./tegola:/opt/tegola_config
    command: --config /opt/tegola_config/config.toml serve
    networks:
      - default
```

`tegola` uses `config.toml` to define settings how generate and serve vectortiles, as following

```toml
# /tegola/config.toml
[webserver]
port = ":8080"
CORSAllowedOrigin = "*"

# register data providers
[[providers]]
name = "japan"         # provider name is referenced from map layers (required)
type = "postgis"      # the type of data provider. currently only supports postgis (required)
host = "postgis"      # postgis database host (required)
port = 5432           # postgis database port (required)
database = "postgres"     # postgis database name (required)
user = "docker"       # postgis database user (required)
password = "docker"         # postgis database password (required)
srid = 4326             # The default srid for this provider. If not provided it will be WebMercator (3857)

  [[providers.layers]]
  name = "roads"
  geometry_fieldname = "wkb_geometry"
  id_fieldname = "ogc_fid"
  tablename = "gis_osm_roads_free_1"

  [[providers.layers]]
  name = "buildings"
  geometry_fieldname = "wkb_geometry"
  id_fieldname = "ogc_fid"
  tablename = "gis_osm_buildings_a_free_1"

[[maps]]
name = "japan"
center = [139.72120, 35.73273, 11.0] # set the center of the map so the user is auto navigated to Bonn


  [[maps.layers]]
  provider_layer = "japan.buildings"
  min_zoom = 5
  max_zoom = 20

  [[maps.layers]]
  provider_layer = "japan.roads"
  min_zoom = 5
  max_zoom = 20
```
With these settings, vectortiles will be served as `http://localhost:8080/maps/japan/{z}/{x}/{y}.vector.pbf`.

### martin

compose

```yml
  martin:
    image: urbica/martin
    restart: unless-stopped
    ports:
      - 3000:3000
    depends_on:
      postgis:
        condition: service_healthy
    networks:
      - default
    volumes:
      - ./martin:/opt/martin_config
    command: martin --config /opt/martin_config/config.yaml
```

`martin` takes a similar approach to `tegola`, using setting file in yml.

```yml
# The socket address to bind [default: 0.0.0.0:3000]
listen_addresses: '0.0.0.0:3000'
# Database connection string
connection_string: 'postgres://docker:docker@postgis/postgres'
# Maximum connections pool size [default: 20]
pool_size: 20
# Connection keep alive timeout [default: 75]
keep_alive: 75
# Number of web server workers
worker_processes: 8
# If a spatial table has SRID 0, then this default SRID will be used as a fallback
default_srid: 4326
# Enable watch mode
watch: false
# Trust invalid certificates. This introduces significant vulnerabilities, and should only be used as a last resort.
danger_accept_invalid_certs: true

# Associative arrays of table sources
table_sources:
  public.buildings:
    # Table source id (required)
    id: public.gis_osm_buildings_a_free_1
    # Table schema (required)
    schema: public
    # Table name (required)
    table: gis_osm_buildings_a_free_1
    # Geometry SRID (required)
    srid: 4326
    # Geometry column name (required)
    geometry_column: wkb_geometry
    # Feature id column name
    id_column: ogc_fid
    # An integer specifying the minimum zoom level
    minzoom: 0
    # An integer specifying the maximum zoom level. MUST be >= minzoom
    maxzoom: 30
    # The maximum extent of available map tiles. Bounds MUST define an area
    # covered by all zoom levels. The bounds are represented in WGS:84
    # latitude and longitude values, in the order left, bottom, right, top.
    # Values may be integers or floating point numbers.
    bounds: [-180.0, -90.0, 180.0, 90.0]
    # Tile extent in tile coordinate space
    extent: 4096
    # Buffer distance in tile coordinate space to optionally clip geometries
    buffer: 64
    # Boolean to control if geometries should be clipped or encoded as is
    clip_geom: true
    # Geometry type
    geometry_type: GEOMETRY
    # List of columns, that should be encoded as tile properties (required)
    properties:
      ogc_fid: integer
      name: string
      fclass: string

  public.roads:
    id: public.gis_osm_roads_free_1
    schema: public
    table: gis_osm_roads_free_1
    srid: 4326
    geometry_column: wkb_geometry
    id_column: ogc_fid
    minzoom: 0
    maxzoom: 30
    bounds: [-180.0, -90.0, 180.0, 90.0]
    extent: 4096
    buffer: 64
    clip_geom: true
    geometry_type: GEOMETRY
    properties:
      ogc_fid: integer
      name: string
      fclass: string
      maxspeed: smallint
```

Vectortiles will be served as `http://localhost:3000/public.roads,public.buildings/{z}/{x}/{y}.pbf`.

### pg_tileserv

compose

```yml
  pg_tileserv:
    image: pramsey/pg_tileserv
    container_name: pg_tileserv
    environment:
      - DATABASE_URL=postgres://docker:docker@postgis/postgres
    depends_on:
      - postgis
    ports:
      - 7800:7800
```

pg_tileserv can be launched without config files. Tables are automatically loaded.

Vectortiles will be served as `http://localhost:7800/public.gis_osm_buildings_a_free_1,public.gis_osm_roads_free_1/{z}/{x}/{y}.vector.pbf`.

## Comparing

Now we can launch vectortile server!

```
docker compose up
```

In QGIS, you can get tiles as following...

1. add vectortile source
2. zoom into a area data exists in
3. add vectortile layer

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/11txix52syszq602hepk.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ak6kpp2ywfe32s8pkjmi.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qyruf6bprkvc14u4pv0x.png)

Then you can see no difference between three servers in terms of apperance because we use same database, but there should be a big differnce in performances. martin and pg_tileserv should be much faster than tegola.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3j5g2d7l4s94vn86gvw0.png)

### why different?

I'm not familiar with details in implementation of tegola and martin/pg_tileserv but I guess it is because martin/pg_tileserv uses `ST_AsMVT` and tegola doesn't.

As far as I digged, `ST_AsMVT` was implemented in 2018 first, and [the performance improved in 2019](https://www.crunchydata.com/blog/waiting-for-postgis-3-st_asmvt-performance).

Actually newest version of tegola seems to support `ST_AsMVT` but I couldn't work it, troubled in settings.

Besides, martin is clearly faster than pg_tileserv. The reason of this difference is less sure than between tegola and martin/pg_tileserv but martin uses Actix-webserver, which is one of the fastest server(https://www.techempower.com/benchmarks/#section=data-r21)) and this could explain a certain extent of the difference.

## Conclusion

- martin is the fastest PostGIS-backend vectortile server.
- pg_tileserv is second but easy config and rich functions.

## Full Codes

https://github.com/Kanahiro/postgis-tileservers-compare