如何使用postgis做一個高可用的附近的人服務?

  • 2019 年 10 月 6 日
  • 筆記

假如動物們也用GPS,突然有那麼一天北極的公北極熊有點衝動,想刷一下附近有沒有母熊。要求距離越近越好,不是澳大利亞動物園那隻,也不是格陵蘭島上被囚禁的那群呆企鵝,要是有點共同的嗜好就再好不過了。這種應用場景如何解決?

一個基於LBS的社交應用或者電商應用,或多或少的包含一些地理信息,如經緯度(lat、lng)。如何在既定的時限內響應用戶的請求,如何低成本的存儲這些數據,是LBS應用最關鍵的問題。我們以附近的人為例,看一下如何去做一個生產級別的應用。

方案

你可能已經了解到,目前有多種方法可以實現這樣的功能,如solr、es、mongodb、redis等scheme free的數據庫,也有使用mysql+geohash來實現這些功能的。

為什麼不用geohash將問題一緯化呢?

因為這種做法無法準確計算距離,而且擴展性和維護性都是問題

為什麼不用solr、es、mysql、sphinx呢?

因為這幾位都是gis函數庫的閹割版,多個維度查詢會有問題,優化困難

為什麼不用mongodb

因為mongodb會隨數據量的增加在地理位置查詢時性能會急劇下降,而pg是線性的

為什麼不用redis geo呢?

redis數據全部放在內存中,不支持排序。有誰用在生產環境中了,請告訴我…

本文採用postgis方案,相比較其他方案,開發人員對SQL都比較熟悉。技術選擇上,你選擇了最優,你就節約了時間和成本,人生苦短,作為使用者沒必要在一些半成品上浪費時間。postgresql本身是最優秀的開源RDBMS,postgis是功能最多、最成熟的開源gis數據庫。GIS方面,支持:

  • 空間數據類型,包括:點(POINT)、線(LINESTRING)、多邊形(POLYGON)、多點(MULTIPOINT)、 多線(MULTILINESTRING)、多多邊形(MULTIPOLYGON)和集合對象集(GEOMETRYCOLLECTION)
  • 空間分析函數,包括:面積(Area)、長度(Length)和距離(Distance)
  • 元數據以及函數,包括:GEOMETRY_COLUMNS和SPATIAL_REF_SYS
  • 二元謂詞,包括:Contains、Within、Overlaps和Touches
  • 空間操作符,包括:Union和Difference

實現/單機

我們首先看下單機版的附近的人:

首先,安裝之。

Postgis的依賴比較多,由於CentOS默認是有pg源的,要首先排除它,安裝專用源。

基本數據結構如下:

有三個比較重要的點

  • 通過create extension語句創建postgis插件,每個庫只能創建一次
  • 創建一個gis類型字段,支持POINT、POLYGON等多種數據類型,我們後續的排序和計算都將使用此字段
  • 為loc字段創建空間索引(GIST索引),可以進行排序、計算距離等

如圖,我們要查詢某個用戶最近N天附近的人,根據距離有近到遠進行排序,查詢第一頁,每頁25條

  • 使用planar degrees 4326坐標系計算兩個點之間的距離(Point(x,y))
  • 將查詢的結果轉換為meters 26986坐標系表示的距離,此即普通單位米。為什麼將這一步單獨做一個嵌套查詢呢?因為ST_Transform是不走索引的,距離排序要全表掃,代價太大
  • ST_X,ST_Y等,將坐標轉化為可讀的經緯度,而不是0101000020E61000005C5E792FA2075D4026BC259C750C4440這種天文數字

如圖,查看執行計劃,使用了geom_loc_index索引進行排序,其他條件走過濾匹配。單表300W+數據,2k+ QPS下,執行只花費了7ms(24核、32G、SATA),算得上是非常神奇了。

實現/集群

分佈式計算第一定律:如果不是真正需要就不要讓系統分佈式。但隨着業務擴張,DAU不斷上漲,逐漸達到百萬+,就不得不考慮可用性和擴展性了。我們從以下幾個方面探討如何做一個可伸縮的高可用附近的XX。

需求

  • 要求較高的實時性,不做緩存,讀取和寫入都比較頻繁(1w+ TPS/s)
  • 能夠按照查詢距離進行排序,能夠分頁
  • 支持除位置意外的其他條件過濾(如年齡,性別,用戶標籤等)
  • 支持GIS其他擴展功能,如三維、區塊包含查詢
  • 要求大部分查詢能夠在100ms內返回,部分長尾請求不超過1s
  • 要求支持集群環境基本的failover、SLB功能

分析

系統實時性要求比較高,所以並不能通過折衷方案進行結果緩存。用戶的每次請求都需要實際的計算,這註定了CPU將成為系統的主要爭奪點。由於RDBMS的特性,在內存有限的環境中,IO也會成為瓶頸,建議有條件的盡量掛載SSD硬盤。

由於GIS應用會有熱點問題和各種數據調整問題,傳統的sharding技術(mod、hash、random)並不能很好的工作,我們需要自定義路由表。這種情況下,Greenplum或者Postgresql-XL(GTM會成為瓶頸)這類分佈式解決方案就不在考慮之內,避免陷入額外的技術陷阱和成本陷阱。

路由表可以使用geohash進行分塊或者按照實際的城市區域代碼進行分片映射。使用區域代碼進行分片,會有比較好的效果,因為地理的分界線一般都是山川河流等數據不敏感的地區,但這種方式需要你有一個逆地理服務(根據經緯度查詢城市編碼),搭建成本是比較高的。

geohash就簡單的多,但會有一定的數據瑕疵,假定我們採用的是geohash編碼(請自行解決geohash的問題,簡單來講,就是將地球上的一個區域塊,一維化為一個固定的編碼,然後把地球切分成這麼一群區域塊)。使用這種方式,就可以將熱點進行分片,一個可能的數據映射如下:

每一組機器有一台master,N台slave通過WAL日誌進行複製。每個geohash塊屬於一組或多組機器,都有一個標識來表明節點的權重,以及是否可用。

然後我們將geohash分成十幾個組,比如12個,那麼需要的pg實例個數就為 12*(masterNum+slavesNum) = 36個。實例個數增長,就需要一種集群管理方法,避免被服務癱瘓的報警叫起床。

架構

可以使用如下的架構:

  • Location Service提供用戶位置服務,可以使用簡單的KV數據庫進行保存,目的是可以隨時查看到用戶的位置信息
  • 用戶的位置更新,最好打到Queue里進行緩衝。這種模式有很多好處,比如你可以訂閱一份數據專門去做用戶的軌跡服務
  • PgRouter 將經緯度轉化為geohash,根據路由表信息,定位到pg集群中的一批節點,進行查詢計算
  • 節點的啟停、主從關係,使用repmgr進行管理。Master故障Slave能夠自動提權
  • PgMonitor 是一組腳本,能夠監控節點的存活狀態和主從關係,然後將存活信息更新到Zookeeper或者Etcd中,當然也可以是consul。
  • PgRouter監聽到節點變化,會重建內存路由表信息,隔離故障節點

接下來我們分析這些問題如何解決。

1

熱點問題如何解決,如何應對突發流量?

熱點取決於你對geohash劃分的粒度,你可以通過掛載多個從庫或者將一批cluster進行拆分

2

複製的效率和一致性如何解決?

數據庫採用standby WAL日誌進行複製,速度很快,延遲小。如果從機太多,可以採用級聯複製方式(slave的slave)。由於採用了單master,可以保證一致性問題。唯一的問題是master宕機切換過程會造成寫入失敗,所以消息隊列有必要採用失敗重試的策略。案例中pg既作為一個存儲節點,又作為一個計算節點。如果你的應用對數據的一致性要求不是那麼高,完全可以將事務隔離級別設置為"read uncommitted"

3

負載均衡放在哪個層面去做?

曾經考慮過使用HA或者LVS,再或者kubernetes將pg打造成一個微服務。但萬變不離其宗,這些花拳繡腿會引入額外的複雜性,遠不如簡單的自定義路由來的方便快捷,我們引入節點權重的意義就在這裡,如某些節點因為IO等運算緩慢,就可以降低其權重來解決。

4

迭代過程需要變更scheme,postgis如何動態添加某個字段?

可以直接添加,並不影響服務,但要注意刪除操作可能會有較大的影響。

5

如何動態添加刪除索引?

不建議這麼做,如果確實有這部分需求,建議業務低峰進行此操作

6

如何實現如QQ中用戶標籤的過濾?比如查詢一批擁有"逗逼"標籤的人

我們採用pg的另外一個原因就是,它的數據類型非常豐富,這在使用中就顯得特別簡潔和方便。pg是一個學術派很濃的數據庫,能夠試用一些最前沿功能。比如標籤就可以用hstore或者jsonb數據類型來實現。在可預見的項目生命周期中,pg的支持足夠了

7

如何去做監控?

自己編寫zabbix插件、或者接入nagios,也可以接入grafana,取決於你所使用的監控平台。也有pgcluu等工具。

8

如何監控節點的上下線?

這個比較簡單,可以使用腳本輪訓檢測,也可以使用repmgr的主動通知功能,構造事件寫入配置中心。

下面是一個簡單的腳本例子:

更複雜的,如果PostGIS也無法滿足你的性能需求,你可能已經是行業巨頭了,可以考慮用PostGIS做數據存儲源,用Solr/ES專門提供搜索等。但目前為止,北極熊也已經找到了它的小夥伴,多快樂啊。

鏈接:

postgis: http://www.postgis.net/

postgresql: https://www.postgresql.org/docs/9.5/static/index.html

repmgr: https://github.com/2ndQuadrant/repmgr