Real-vaxt Geolokasiya Backend-i: PostGIS Məkan İndeksləri, FastAPI Axını və Docker ilə Dispetçerlik
Yazma axınını gecikməyə həssas oxumadan ayırmaq: canlı heyət üçün Redis GEO, davamlı məkan sorğuları üçün PostGIS və dispetçerlik üçün SSE.
Sifarişə əsaslanan platforma, yəni müştərini ən yaxın boş ustaya, sürücüyə və ya kuryerə bağlayan sistem, kənardan tək bir funksiya kimi görünür: yaxında kim var, göstər və bizi birləşdir. İçəridə isə bu, eyni sətirlər üzərində işləyən və tələbləri bir-birinə birbaşa zidd olan iki ayrı yükdür. Birincisi kiçik yazmaların fasiləsiz axınıdır: hər aktiv işçi bir neçə saniyədən bir yeni koordinat göndərir. İkincisi gecikməyə həssas oxumadır və "beş kilometr radiusda kim var, məsafəyə görə sırala" sualına yazmalar hələ də gəlməkdə ikən cavab verməlidir.
Bu ikisini vahid problem kimi görmək demonstrasiyada sürətli, minlərlə paralel işçidə isə istifadəyə yararsız olan sistem yaradır. Bu qeyddə onları ayıran dizayn addım-addım qurulur: davamlı məkan saxlancı kimi PostGIS-li PostgreSQL, hazırda kimin onlayn olduğunu bilən uçucu indeks kimi Redis, sorğulanmaq əvəzinə hadisələri axınla ötürən asinxron API kimi FastAPI, və bu üçünü bir yerdə saxlayan Docker Compose. Diqqət konkret olaraq ən sadə tətbiqin sıradan çıxdığı yerlərə yönəlib: filtr əlavə edilən kimi istifadə olunmağı dayandıran indeks, PostgreSQL-in ən ucuz yeniləmə yolunu səssizcə söndürən mövqe yeniləməsi və tərs proksinin udduğu axın.
İki fərqli yük, bir xəritə
İlk arxitektur qərar hansı sualın hansı saxlanca aid olduğudur. PostGIS tam hüquqlu geometriya mühərrikidir: poliqonlar, geofence-lər, məkan birləşmələri, sferoid üzərində həqiqi metr məsafəsi, üstəlik yenidən başlatmadan sonra da yaşayır. Redis coğrafiya haqqında nöqtədən başqa demək olar heç nə bilmir, amma radius sorğusuna tamamilə yaddaşdaxili strukturdan cavab verir və eyni açarın dörd saniyə əvvəl üzərinə yazıldığı onun üçün əhəmiyyətsizdir.
| Sual | Saxlanc | Səbəb |
|---|---|---|
| 4471 nömrəli işçi indi haradadır? | Redis | Daim üzərinə yazılır, bir dəqiqədən sonra dəyərsizdir, diskə toxunmamalıdır |
| 5 km radiusda boş olan kimdir? | Əvvəlcə Redis, ehtiyat variant PostGIS | İsti yol, hər axtarışda çağırılır |
| Bu ünvan bizim xidmət zonamızın içindədirmi? | PostGIS | Poliqon daxilolması, nadir hallarda dəyişir |
| Bu işçi keçən ay hansı marşrutlarla getdi? | PostGIS | Davamlı, analitik, sifarişlərlə birləşdirilir |
| 90210 nömrəli sifariş barədə kimə bildiriş getməlidir? | Redis Pub/Sub | Keçici mesaj, tarixçəyə ehtiyac yoxdur |

Hər ikisi ilə danışan yeganə komponent API təbəqəsidir. Müştərilər verilənlər bazasına heç vaxt birbaşa çıxmır və bu, adi bir CRUD xidmətindəkindən qat-qat vacibdir: şəxsiyyəti müəyyən edilə bilən insanların xam koordinatları bu sistemin saxladığı ən həssas məlumat kateqoriyasıdır və onlara hər müraciət autentifikasiya, sorğu limiti və audit qeydiyyatının tətbiq olunduğu tək bir nöqtədən keçməlidir.
Sxem: geometry, geography və əslində indekslədiyiniz sütun
PostGIS iki məkan tipi təklif edir və onlar arasındakı seçim sorğularınızda nə qədər hesablama qalacağını müəyyən edir. geometry koordinatları müstəvi üzərindəki nöqtələr kimi qəbul edir. SRID 4326 ilə bu müstəvi uzunluq və enlikdir, deməli bütün məsafələrin vahidi metr yox, dərəcədir. geography isə eyni koordinatları sferoid üzərindəki nöqtələr kimi götürür və məsafəni metrlə qaytarır.
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE TABLE artisans (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
available BOOLEAN NOT NULL DEFAULT false,
-- Kanonik saxlanma: əvvəl uzunluq, sonra enlik.
location GEOMETRY(Point, 4326) NOT NULL,
-- Törəmə sütun, beləliklə iki təsvir heç vaxt ziddiyyət təşkil edə bilməz.
geog GEOGRAPHY(Point, 4326)
GENERATED ALWAYS AS (location::geography) STORED,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Hər təsvir üçün bir R-ağacı. İndekslərin heç biri digər tipə xidmət etmir.
CREATE INDEX idx_artisans_geom ON artisans USING GIST (location);
CREATE INDEX idx_artisans_geog ON artisans USING GIST (geog);
Törəmə (generated) sütun buradan mütləq götürülməli olan hissədir. Enlik və uzunluğu əl ilə iki dəfə, bir dəfə geometry, bir dəfə geography kimi saxlamaq ilk unudulmuş yeniləmədə məlumat bütövlüyü xətasına çevrilir. GENERATED ALWAYS AS ... STORED bu ziddiyyəti sətir başına bir neçə bayt bahasına tamamilə mümkünsüz edir.
Yalnız geometry ilə işləmək mümkündür, lakin onda vahid çevrilməsi sizin öhdənizə keçir. enliyində metrlik radius dərəcə fəzasında dairə deyil, oxları enliyin kosinusu qədər fərqlənən ellipsdir:
Ekvatorda bir dərəcə uzunluq təxminən 111 km-dir; Bakıda, təxminən 40-cı paraleldə, bu rəqəm təxminən 85 km-ə düşür. Tək bir sabit çevirmə əmsalını koda yazmaq bir şəhərdə düzgün, digərində isə dörddə bir yanlış olan axtarış radiusu verir, üstəlik bu, heç vaxt xəta atmayan, sadəcə bir az səhv adam dəstini qaytaran növ səhvdir. İstifadəçiyə görünən hər şey üçün geography istifadə edin, geometry-ni isə müstəvi fərziyyəsinin həqiqətən təhlükəsiz olduğu daxili işlər üçün saxlayın.
Məkan indeksinin seçilməsi
PostgreSQL PostGIS-in istifadə edə biləcəyi üç indeks ailəsi təklif edir və "məkan indeksi GiST-dir" fikri qayda deyil, sadəcə yaxşı ilkin seçimdir.
| İndeks | Qurulma xərci | Ölçü | Sorğu davranışı | Uyğun olduğu hal |
|---|---|---|---|---|
| GiST (R-ağacı) | Orta | Orta | Üst-üstə düşməni, daxilolmanı və KNN sıralamasını dəstəkləyir; qeyri-bərabər paylanmada da proqnozlaşdırılandır | İlkin seçim. Nöqtələr, poliqonlar, qarışıq yüklər |
| SP-GiST (quad-ağac) | GiST-in təxminən üçdə biri | GiST-in təxminən 80%-i | Bərabər paylanmış, üst-üstə düşməyən nöqtələrdə rəqabətli və ya daha yaxşı | Bərabər paylanmış, yalnız nöqtədən ibarət böyük cədvəllər |
| BRIN | Demək olar ani | Meqabayt yox, kilobayt | Yalnız sətirlərin fiziki sırası məkanla korrelyasiya edəndə faydalıdır | Vaxta və ya regiona görə klasterlənmiş, yalnız əlavə olunan tarixçə cədvəlləri |
Əslində qərarı verən amil fiziki korrelyasiyadır. BRIN indeksi hər blok diapazonu üçün bir əhatə çərçivəsi saxlayır, ona görə də inanılmaz dərəcədə kiçikdir və ardıcıl sətirlər xəritə boyu səpələnəndə eyni dərəcədə inanılmaz dərəcədə faydasızdır. Sətir sırasının qeydiyyat ardıcıllığından başqa heç nəyi əks etdirmədiyi canlı artisans cədvəlində BRIN çərçivəsi bütün xidmət zonasını əhatə edir və hər sorğu ardıcıl skana çevrilir. Zaman damğası ilə əlavə olunan və günə görə bölünmüş location_history cədvəlində isə eyni indeks demək olar pulsuz və həqiqətən effektivdir.
SP-GiST maraqlı ara haldır. Onun quad-ağac bölgüsü nöqtələrin üst-üstə düşmədiyini fərz edir, bu koordinatlar üçün tam doğru, çatdırılma zonaları üçün isə tam yanlışdır. Yalnız nöqtədən ibarət cədvəldə o, GiST-dən daha sürətli qurulur və daha az yer tutur; poliqon olan yerdə isə cavab yenə GiST-dir. Dürüst tövsiyə budur: GiST ilə başlayın və yalnız EXPLAIN (ANALYZE, BUFFERS) darboğazın onun arxasındakı heap oxumaları deyil, məhz indeksin özü olduğunu göstərəndə dəyişin.
Radius və ən yaxın qonşu sorğuları
İki sorğu forması dispetçerlik tələblərinin demək olar hamısını əhatə edir. Radius axtarışı nəticə dəstini xidmət zonası ilə məhdudlaşdırır:
SELECT id,
name,
ST_Distance(geog, $1::geography) AS metres
FROM artisans
WHERE ST_DWithin(geog, $1::geography, $2)
ORDER BY geog <-> $1::geography
LIMIT $3;
ST_DWithin ardıcıl iki iş görür və sürətin sirri məhz bu ardıcıllıqdadır. Əvvəlcə GiST indeksindən əhatə çərçivəsi axtarış zərfi ilə kəsişən bütün sətirləri istəyir, bu ucuz indeks skanıdır, sonra yalnız həmin namizədlər üçün dəqiq sferoid məsafəsini hesablayır. Eyni şərti ST_Distance(geog, $1) <= $2 şəklində yazmaq bu ardıcıllığı tərsinə çevirir: müqayisənin mövcud olması üçün funksiya hər sətirdə işləməlidir, deməli indeksə heç müraciət olunmur və milyon sətirlik cədvəl millisaniyə əvəzinə saniyələr çəkir.
Ən yaxın qonşu forması radiusdan tamamilə imtina edir və <-> məsafə operatoruna əsaslanır:
SELECT id, name
FROM artisans
ORDER BY location <-> ST_SetSRID(ST_MakePoint($1, $2), 4326)
LIMIT 5;
İndeks dəstəkli KNN yalnız <-> operatorunun bir tərəfi sabit və ya bağlanmış parametr olduqda işə düşür. İki sütunu müqayisə etsəniz, PostgreSQL bütün cədvəli hesablanmış məsafəyə görə sıralamağa qayıdır. Bunu fərz etmək yox, yoxlamaq lazımdır: planda Index Scan using idx_artisans_geom olmalıdır, Seq Scan üzərində duran Sort qovşağı isə operatorun bahalı yolla hesablandığını bildirir.
İndeksi səssizcə öldürən filtr
Tətbiqin əslində ehtiyac duyduğu sorğu yuxarıdakı deyil. Ona ən yaxın boş ustalar lazımdır və məhz bu şərtin əlavə edilməsi sürətli sorğunu yavaş sorğuya çevirir:
SELECT id, name
FROM artisans
WHERE available
ORDER BY location <-> $1::geometry
LIMIT 5;
PostgreSQL bu ifadə üçün iki yox, bir indeks istifadə edəcək. Ya available üzərindəki B-ağacını skan edib sağ qalanları məsafəyə görə sıralayacaq, ya da GiST indeksini məsafə sırası ilə gəzərək beş nəticə toplanana qədər boş olmayan sətirləri atacaq. İşçilərin əksəriyyəti oflayn olanda ikinci plan limit dolana qədər indeksin böyük hissəsini oxuyur, yəni performans məhz platformanın ən sakit anında pisləşir, elə həmin anda ki, heç kim monitorinq panellərinə baxmır.
Qismən (partial) indeks şərti indeksin özünə yazmaqla bunu həll edir:
CREATE INDEX idx_artisans_available_geom
ON artisans USING GIST (location)
WHERE available;
İndi indeks yalnız sorğunun istədiyi sətirləri saxlayır, məsafə sıralaması indeks dəstəklidir və indeksin ölçüsü qeydiyyatdan keçmiş bütün heyətin deyil, aktiv heyətin ölçüsünə enir. Alternativ olan çoxsütunlu GIST (location, available) variantı GiST altında boolean indeksləmək üçün ümumiyyətlə btree_gist genişlənməsini tələb edir və daha az fayda verir: o, yenə də bütün sətirləri saxlayır. Filtr kiçik və sabit dəyərlər dəstidirsə, qismən indeks həmişə daha yaxşı alətdir.
Sıradan çıxan hissə yazma yoludur
Yuxarıdakıların hamısı oxuma ilə bağlıdır. Geolokasiya backend-i əslində yazma yolunda dağılır və səbəb bu yükün özünü məhrum etdiyi bir PostgreSQL optimizasiyasıdır.
Normal halda indekslənmiş heç bir sütuna toxunmayan UPDATE əməliyyatı HOT (Heap-Only Tuple) yeniləməsi ola bilər: sətirin yeni versiyası eyni səhifəyə yazılır, heç bir indeks qeydi yaradılmır və xərc bir səhifə yazmasından ibarət olur. Yeniləmə indekslənmiş sütuna toxunan kimi HOT söndürülür. Yeni tuple yazılır, cədvəlin hər indeksinə qeyd əlavə olunur və köhnə versiyalar autovacuum-un təmizləməli olduğu ölü sətirlərə çevrilir.
Mövqe yeniləməsi location sütununu dəyişir. location isə indekslidir. Deməli hər bir mövqe hesabatı bir heap tuple, üstəlik iki GiST qeydi yazır və GiST qeydlərinin əlavə edilməsi B-ağacındakından bahadır, çünki ağac yenidən balanslaşdırıla bilər. Dörd saniyədən bir hesabat göndərən 5000 aktiv işçidə bu, eyni anda axtarış sorğularına da xidmət edən cədvəl üzərində saniyədə 1250 indeks yazan yeniləmə deməkdir, üstəlik hamısının qoyduğu ölü sətirlər. Autovacuum geri qalır, cədvəl və indeksləri şişir, oxuma gecikməsi isə sorğu planında heç vaxt görünməyən bir səbəbdən qalxır.
Üç cavab var və istehsalat sistemi hər üçünü birlikdə tətbiq edir.
İsti dəsti PostgreSQL-dən kənarda saxlayın. Redis cari mövqeləri geohash ilə açarlanmış sıralanmış çoxluqda saxlayır və radius sorğularına diskə toxunmadan cavab verir:
# GEOADD yerində üzərinə yazır: hər şəhər üçün bir açar, hər işçi üçün bir üzv.
await redis.geoadd("live:baku", (lon, lat, f"artisan:{artisan_id}"))
# GEOSEARCH Redis 6.2-də GEORADIUS-u əvəz etdi; GEORADIUS hələ işləyir, lakin
# köhnəlmiş sayılır və yeni kodda istifadə edilməməlidir.
nearby = await redis.geosearch(
"live:baku",
longitude=user_lon,
latitude=user_lat,
radius=5,
unit="km",
sort="ASC",
count=20,
withdist=True,
)
Kodlaşdırma sıralanmış çoxluğun bal dəyəri kimi saxlanan 52 bitlik geohash-dır, bu isə mövqe xətasını bir metrdən xeyli aşağıda, GPS küyünün altında saxlayır. Redis burada arxalanmağa dəyər davamlılıq zəmanəti vermir və bu, düzgün ödənişdir: dörd saniyəlik mövqenin saxlanmağa dəyər heç bir dəyəri yoxdur.
PostGIS-ə asinxron və paketlə yazın. Davamlı qeydin saniyəyə qədər dəqiq olmasına ehtiyac yoxdur. Mövqe hesabatlarını buferə yığın və hər hesabat üçün ayrıca gediş-gəliş əvəzinə tək bir çoxsətirli ifadə ilə vaxtaşırı boşaldın.
Cari mövqeyi tarixçədən ayırın. İşçi başına bir sətir saxlayan, öz indeksi və təxminən 0.02 dəyərində aqressiv autovacuum_vacuum_scale_factor parametri olan dar artisan_positions cədvəli bu yükü ad, reytinq və profil məlumatı daşıyan geniş artisans cədvəlindən uzaqda saxlayır. Tarixi izlər isə günə görə bölünmüş cədvələ gedir; orada hər bölmə bir dəfə yazılır, BRIN ilə indekslənir və saxlama müddəti bitəndə silinmək əvəzinə ayrılır.
API səthi
FastAPI üzərində asyncpg, bağlantı hovuzu isə hər sorğuda deyil, bir dəfə lifespan idarəedicisində yaradılır. İki endpoint bütün oxuma və yazma yolunu daşıyır.
import os
from contextlib import asynccontextmanager
import asyncpg
from fastapi import Depends, FastAPI
from pydantic import BaseModel, Field
@asynccontextmanager
async def lifespan(app: FastAPI):
# Proses üçün bir hovuz. Hər sorğuda yeni bağlantı açmaq asinxron
# xidmətlərdə "Postgres yavaşdır" şikayətinin ən yayğın səbəbidir.
app.state.pool = await asyncpg.create_pool(
dsn=os.environ["DATABASE_URL"],
min_size=5,
max_size=20,
command_timeout=5,
)
yield
await app.state.pool.close()
app = FastAPI(lifespan=lifespan)
class LocationUpdate(BaseModel):
# Sərhəddə validasiya: yerləri dəyişdirilmiş enlik/uzunluq cütü bu sahədə
# ən çox rast gəlinən müştəri xətasıdır və bu hüdudlar olmadan səssizdir.
lon: float = Field(ge=-180, le=180)
lat: float = Field(ge=-90, le=90)
accuracy_m: float | None = Field(default=None, ge=0)
@app.post("/location", status_code=204)
async def update_location(
body: LocationUpdate,
artisan_id: int = Depends(current_artisan_id),
):
await redis.geoadd("live:baku", (body.lon, body.lat, f"artisan:{artisan_id}"))
await position_buffer.put((artisan_id, body.lon, body.lat))
class Nearby(BaseModel):
id: int
name: str
metres: float
@app.get("/search/nearby", response_model=list[Nearby])
async def search_nearby(
lon: float, lat: float, radius_m: int = 5000, limit: int = 20,
):
radius_m = min(radius_m, 25_000) # Çağıranın tələb edə biləcəyi işi məhdudlaşdır.
# Başlanğıc nöqtəsi ifadəsi CTE-yə çıxarılmır, təkrarlanır. O, bağlanmış
# parametrlər üzərində dəyişməz (immutable) funksiyalardan qurulur, ona görə
# planlaşdırıcı onu sabitə çevirir və KNN indeks dəstəyi qalır; CTE sütununa
# istinad bu şərtə uyğun gəlmir və ORDER BY sıralamaya qayıdardı.
sql = """
SELECT id,
name,
ST_Distance(
geog, ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography
) AS metres
FROM artisans
WHERE available
AND ST_DWithin(
geog, ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography, $3
)
ORDER BY geog <-> ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography
LIMIT $4
"""
async with app.state.pool.acquire() as conn:
rows = await conn.fetch(sql, lon, lat, radius_m, limit)
return [dict(r) for r in rows]
Səhv salınması asan olan iki detal. asyncpg SQLAlchemy və ya psycopg-dəki adlandırılmış :param üslubunu deyil, mövqeyə görə $1 yer tutucularını istifadə edir və iki konvensiyanı qarışdırmaq Python-da yox, verilənlər bazasında sintaksis xətası verir. Radius isə server tərəfdə kəsilir: bu tavan olmadan istənilən müştəri 10 000 km radius tələb edərək məhdud indeks skanını bütöv cədvəl sıralamasına çevirə bilər.
Dispetçerliyin SSE ilə axını
Dispetçerlik təbiəti etibarilə push formasındadır. Sərnişin sürücünün sifarişi qəbul etməsini gözləyir; bunu hər iki saniyədən bir sorğu ilə yoxlamaq müştəridə batareyanı, serverdə isə bağlantıları yandırır və vaxtın böyük hissəsində heç nə gətirmir. Server-Sent Events tam bu formaya uyğundur: bir uzunömürlü HTTP cavabı, yalnız serverdən müştəriyə, üstəlik hər brauzerə hazır quraşdırılmış avtomatik yenidən qoşulma.
import json
from fastapi import Request
from sse_starlette.sse import EventSourceResponse
@app.get("/stream/bookings/{booking_id}")
async def booking_stream(booking_id: int, request: Request):
async def events():
pubsub = redis.pubsub()
await pubsub.subscribe(f"booking:{booking_id}")
try:
async for message in pubsub.listen():
if message["type"] != "message":
continue
# Bağlantının qopması həmişə istisna kimi görünmür, yoxla.
if await request.is_disconnected():
break
payload = json.loads(message["data"])
yield {
# id sayəsində yenidən qoşulan müştəri Last-Event-ID ilə
# davam edir, yeniləmələri səssizcə itirmir.
"id": str(payload["seq"]),
"event": payload["type"],
"data": json.dumps(payload),
}
finally:
await pubsub.unsubscribe(f"booking:{booking_id}")
await pubsub.close()
return EventSourceResponse(events(), ping=15)
EventSourceResponse FastAPI-nin özündən deyil, sse-starlette paketindən gəlir. ping parametri hər on beş saniyədən bir şərh kadrı göndərir və bu, aradakı proksilərin bağlantını boş sayıb bağlamasının qarşısını alır.
Müştəri tərəfi brauzerin öz primitividir, heç bir kitabxana lazım deyil:
const source = new EventSource(`/api/stream/bookings/${bookingId}`);
source.addEventListener("driver_confirmed", (e) => {
const { driver, eta_seconds } = JSON.parse(e.data);
setDriver(driver);
setEta(eta_seconds);
});
source.addEventListener("location_update", (e) => {
const { lon, lat } = JSON.parse(e.data);
marker.setLngLat([lon, lat]);
});
// EventSource özü yenidən qoşulur; görünüş sökülərkən onu bağlayın,
// əks halda hər naviqasiya bir bağlantı sızdırır.
return () => source.close();
Bağlantının uzunömürlü olmasından iki əməliyyat məhdudiyyəti doğur. Birincisi tamponlamadır: Nginx proksiləşdirilmiş cavabı ötürməzdən əvvəl yığır, ona görə hadisələr idarəedici funksiya nəhayət bitəndə bir dəstə halında gəlir, sonsuz generator üçünsə bu heç vaxt baş vermir. Həlli cavabda X-Accel-Buffering: no başlığı və ya location blokunda proxy_buffering off, üstəlik ping intervalından uzun proxy_read_timeout dəyəridir. Bu, dil modelindən token axını zamanı da qarşıya çıxan eyni nasazlıqdır və istehsalat RAG arxitekturası haqqında qeyddə təfərrüatı ilə izah olunub.
İkincisi paralellikdir. Abunə olmuş hər müştəri baxdığı müddətcə bir işçi yuvasını tutur. Asinxron yığında bu, sərfəlidir, çünki yuva bir korutindir; sinxron yığında isə dərhal ölümcüldür, çünki orada yuva əməliyyat sistemi sapıdır. Hadisə generatoruna tək bir bloklayıcı çağırış düşsə, o, bütün hadisə dövrünü və həmin işçidəki bütün digər axınları dayandırır.
Bir sifariş, əvvəldən sona
Redis bu axında iki dəfə, özü də tamamilə fərqli iki iş görərək görünür: axtarış üçün coğrafi indeks, və sifariş sorğusunu həmin ustanın axınını tutan API işçisinə çatdıran pub/sub şini. İkinci iş üfüqi miqyaslanmanı ümumiyyətlə mümkün edən şeydir. Paylaşılan şin olmasa, sifariş yalnız qəbul edən işçi hədəfin bağlantısına sahib olan eyni proses olduqda çatdırıla bilər, bu isə ikinci replika işə düşənə qədər doğrudur.
İşə salmaq: Docker Compose
services:
db:
image: postgis/postgis:17-3.5
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
POSTGRES_DB: dispatch
command:
# Məkan indeksləri yalnız yaddaşda olduqları müddətdə sürətlidir.
- "postgres"
- "-c"
- "shared_buffers=1GB"
- "-c"
- "maintenance_work_mem=512MB"
- "-c"
- "random_page_cost=1.1"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d dispatch"]
interval: 10s
retries: 5
secrets:
- db_password
redis:
image: redis:7-alpine
command: ["redis-server", "--save", "", "--appendonly", "no"]
api:
build: ./backend
environment:
DATABASE_URL: postgresql://appuser@db/dispatch
REDIS_URL: redis://redis:6379/0
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
ports:
- "8000:8000"
volumes:
pgdata:
secrets:
db_password:
file: ./secrets/db_password.txt
Buradakı üç parametr həlledicidir. random_page_cost=1.1 planlaşdırıcıya SSD üzərində işlədiyini bildirir; onsuz PostgreSQL indeks skanlarının xərcini sistematik olaraq şişirdir və məhz bu sistemin asılı olduğu sorğular üçün ardıcıl skan seçir. Redis-in davamlılıq bayraqları qəsdən belədir: bu nüsxə diskə yazılmağa dəyər heç nə saxlamır və həm RDB anlıq görüntülərini, həm də append-only jurnalını söndürmək isti yolu dayandıra biləcək fork-və-yaz fasilələrini aradan qaldırır. Sadə depends_on əvəzinə condition: service_healthy isə API-nin TCP bağlantısını qəbul etmiş, lakin PostGIS-i hələ qurmamış Postgres-ə qarşı başlamasının qarşısını alır.
İstismar
Bağlantı hovuzlaması. asyncpg hovuzu proses başına bağlantı sayını məhdudlaşdırır; bunun təhlükəsiz olduğuna qərar verməzdən əvvəl replika sayına vurun. Bir neçə yüz backend prosesindən sonra PostgreSQL işləməkdən çox kontekst dəyişməyə vaxt sərf edir və standart cavab tranzaksiya rejimində PgBouncer-dir. Soyuqdan rast gəlsəniz bir günə başa gələn bir incəlik: asyncpg standart olaraq hazırlanmış ifadələr (prepared statements) yaradır, tranzaksiya hovuzlaması isə onları sındırır, ona görə PgBouncer arxasında statement_cache_size=0 məcburidir.
Sorğu limiti. Hücumçunun və ya səhv yazılmış müştərinin döyəcləyəcəyi endpoint məhz mövqe endpoint-idir və o, yazma əməliyyatıdır. Limiti IP üzrə deyil, autentifikasiya olunmuş şəxsiyyət üzrə qoyun, çünki bütöv bir şəhərin mobil müştəriləri operatorun eyni NAT ünvanını bölüşə bilər. İki saniyədən tez-tez hesabat göndərən işçi hərəkəti deyil, GPS titrəyişini bildirir.
Autentifikasiya və sətir səviyyəli təhlükəsizlik. Hər endpoint-də qısaömürlü JWT, asılılıq (dependency) daxilində yoxlanılır, current_artisan_id isə sorğu gövdəsindən yox, tokendən götürülür. Mövqe yeniləməsində çağıranın nəzarətindəki identifikatoru qəbul etmək istənilən autentifikasiya olunmuş istifadəçiyə başqasını yerindən tərpətmək imkanı verir. Çoxkirayəçili quraşdırmalarda PostgreSQL-in sətir səviyyəli təhlükəsizliyi (RLS) kirayəçi sərhədini hər sorğunun düzgün WHERE şərti daşıyacağına ümid etmək əvəzinə verilənlər bazasında tətbiq edir.
Saxlama müddəti. Mövqe tarixçəsi şəxsi məlumatdır və kiminsə hər dörd saniyədən bir harada olduğunu qeyd edən müddətsiz iz analitik dəyərini xeyli aşan məsuliyyət yaradır. Günlük bölmələr müddətin bitməsini DETACH PARTITION və ardınca DROP TABLE əməliyyatına çevirir; bu, ani baş verir və yeri dərhal geri qaytarır, cədvəli şişmiş halda qoyan və autovacuum-u saatlarla məşğul edən DELETE isə yox.
Nəticə
Davam gətirən arxitektura oxuma yolu ilə yazma yolunun eyni problem olduğunu iddia etməkdən əl çəkəndir. PostGIS yerini düzgünlüklə qazanır: həqiqi sferoid məsafəsi, geofence daxilolması, davamlı tarixçə və sorğular planlaşdırıcının istifadə edə biləcəyi şəkildə yazılanda həqiqətən sürətli olan indeks dəstəyi. Redis yerini davamlılığa laqeydliyi ilə qazanır və bu, dörd saniyədən sonra dəyərsizləşən məlumat üçün tam düzgün xüsusiyyətdir. SSE isə minlərlə sorğulama müraciətini gözləyən istifadəçi başına bir boş bağlantı ilə əvəz edərək yerini qazanır.
Onları bir-birinə bağlayan şey autentifikasiya, validasiya və sorğu limitinin bir dəfə tətbiq olunduğu vahid API sərhədi, üstəlik yalnız şüurlu seçiləndə düzgün olan verilənlər bazası qərarlarıdır: istifadəçiyə görünən məsafələr üçün geography, tətbiqin əslində göndərdiyi filtrə uyğun qismən GiST indeksi, və yeniləmə yükünü axtarılan sətirlərdən uzaqda saxlayan cədvəl quruluşu.
Mənbələr
34
- PostGIS Documentation: ST_DWithinpostgis.net
- PostGIS Documentation: the <-> distance operator and index-assisted KNNpostgis.net
- PostGIS Documentation: ST_Distancepostgis.net
- Introduction to PostGIS: Spatial Indexingpostgis.net
- Introduction to PostGIS: Geographypostgis.net
- PostgreSQL Documentation: GiST Indexespostgresql.org
- PostgreSQL Documentation: SP-GiST Indexespostgresql.org
- PostgreSQL Documentation: BRIN Indexespostgresql.org
- PostgreSQL Documentation: btree_gistpostgresql.org
- PostgreSQL Documentation: Partial Indexespostgresql.org
- PostgreSQL Documentation: Heap-Only Tuples (HOT)postgresql.org
- PostgreSQL Documentation: Routine Vacuuming and autovacuumpostgresql.org
- PostgreSQL Documentation: Table Partitioningpostgresql.org
- PostgreSQL Documentation: Planner Cost Constants (random_page_cost)postgresql.org
- PostgreSQL Documentation: Row Security Policiespostgresql.org
- Redis Documentation: GEOADDredis.io
- Redis Documentation: GEOSEARCHredis.io
- Redis Documentation: GEORADIUS (deprecated as of 6.2)redis.io
- Redis Documentation: Pub/Subredis.io
- FastAPI: Lifespan Eventsfastapi.tiangolo.com
- FastAPI: WebSocketsfastapi.tiangolo.com
- sse-starlette: Server-Sent Events for Starlette and FastAPIgithub.com
- asyncpg Documentationmagicstack.github.io
- asyncpg FAQ: why am I getting prepared statement errors behind PgBouncermagicstack.github.io
- PgBouncer: Features and pooling modespgbouncer.org
- Pydantic Documentationdocs.pydantic.dev
- MDN: Using server-sent eventsdeveloper.mozilla.org
- MDN: EventSourcedeveloper.mozilla.org
- nginx: ngx_http_proxy_module, proxy_buffering and X-Accel-Bufferingnginx.org
- Docker Compose: startup order and depends_on conditionsdocs.docker.com
- postgis/docker-postgis: official PostGIS Docker imagesgithub.com
- h3-pg: Uber H3 bindings for PostgreSQLgithub.com
- MobilityDB: moving object trajectories in PostgreSQL and PostGISgithub.com
- GeoDjango: geographic queries in Djangodocs.djangoproject.com



