計(jì):為對(duì)話(huà)機(jī)器人打造地理編碼與POI檢索能力)
lingbot-map這個(gè)項(xiàng)目說(shuō)起來(lái)起因特別樸素我們內(nèi)部那套叫l(wèi)ingbot的對(duì)話(huà)機(jī)器人框架被業(yè)務(wù)方追著要一個(gè)能認(rèn)路、能找店、能算距離的地圖能力。一開(kāi)始我滿(mǎn)腦子想的都是接第三方地圖SDK結(jié)果一調(diào)研就僵住了——商業(yè)地圖服務(wù)的配額費(fèi)、流量費(fèi)在演示環(huán)境很友好一上生產(chǎn)就是另一套價(jià)碼而且底圖樣式、POI數(shù)據(jù)、路線(xiàn)引擎全都綁在別人生態(tài)里想換底圖或者混入企業(yè)私有點(diǎn)位幾乎沒(méi)有操作空間。于是我們決定自己寫(xiě)一個(gè)輕量的、自托管的、可以嵌進(jìn)lingbot框架的地圖模塊也就是lingbot-map。lingbot-map做的事不復(fù)雜瓦片渲染、地理編碼、逆地理編碼、POI檢索、距離和路線(xiàn)估算外加一套適合對(duì)話(huà)場(chǎng)景的查詢(xún)接口。它不打算跟商業(yè)地圖App比豐富度目標(biāo)只有一個(gè)——讓機(jī)器人在對(duì)話(huà)里說(shuō)到位置的時(shí)候不至于一問(wèn)三不知。這篇文章把整個(gè)模塊從需求拆解、核心原理、工程決策到實(shí)測(cè)優(yōu)化的完整過(guò)程都捋一遍適合正在做機(jī)器人或助手類(lèi)產(chǎn)品、想給自家應(yīng)用加一個(gè)自托管輕量地圖能力的同學(xué)參考。1. 為什么會(huì)有l(wèi)ingbot-map商業(yè)地圖API之外的現(xiàn)實(shí)選擇1.1 最初的需求機(jī)器人先得知道自己在哪很多對(duì)話(huà)場(chǎng)景一旦涉及位置問(wèn)題就立刻復(fù)雜起來(lái)。用戶(hù)報(bào)修時(shí)說(shuō)2號(hào)樓東側(cè)門(mén)禁壞了會(huì)議紀(jì)要里寫(xiě)在園區(qū)B棟集合外賣(mài)場(chǎng)景說(shuō)送到樓下前臺(tái)這些句子里的2號(hào)樓B棟樓下都是位置可機(jī)器人拿到的是一段文本如果不知道園區(qū)的經(jīng)緯度范圍根本沒(méi)法把這些話(huà)變成可計(jì)算的坐標(biāo)。我們最初梳理出的需求很明確機(jī)器人需要把自然語(yǔ)言里的方位表達(dá)解析成結(jié)構(gòu)化坐標(biāo)再基于坐標(biāo)完成兩個(gè)動(dòng)作——查找附近的POI興趣點(diǎn)以及告訴用戶(hù)大概要跑多遠(yuǎn)。除此之外的需求比如實(shí)時(shí)交通、導(dǎo)航語(yǔ)音播報(bào)當(dāng)時(shí)沒(méi)有一個(gè)人提。這也是lingbot-map最后能被控制在一個(gè)合理規(guī)模里的前提所有功能都由真實(shí)對(duì)話(huà)需求驅(qū)動(dòng)而不是為了做一個(gè)地圖而做。1.2 商業(yè)地圖服務(wù)的三道隱形門(mén)檻不接商業(yè)SDK不是因?yàn)樗缓枚且驗(yàn)槿篱T(mén)檻在對(duì)話(huà)機(jī)器人這個(gè)場(chǎng)景里太難受了。第一是費(fèi)用結(jié)構(gòu)。演示環(huán)境和個(gè)人測(cè)試通常有免費(fèi)額度但生產(chǎn)環(huán)境的調(diào)用成本跟業(yè)務(wù)量強(qiáng)相關(guān)對(duì)話(huà)機(jī)器人是高頻交互用戶(hù)每問(wèn)一次附近有什么就是一次地理編碼加一次POI檢索。日活上去之后費(fèi)用是線(xiàn)性增長(zhǎng)的而且這部分成本很難轉(zhuǎn)嫁給用戶(hù)。第二是數(shù)據(jù)鎖定。商業(yè)平臺(tái)的底圖、POI、路線(xiàn)規(guī)則是一套封閉體系企業(yè)內(nèi)部的位置數(shù)據(jù)——比如自有園區(qū)、合作門(mén)店、敏感點(diǎn)位——要么沒(méi)有入口錄入要么受平臺(tái)審核規(guī)則限制。我們要的是一個(gè)公用數(shù)據(jù)私有數(shù)據(jù)混排的地圖商業(yè)平臺(tái)給不了。第三是數(shù)據(jù)出域風(fēng)險(xiǎn)。企業(yè)內(nèi)部位置信息屬于業(yè)務(wù)數(shù)據(jù)讓這類(lèi)數(shù)據(jù)在每次查詢(xún)時(shí)都經(jīng)過(guò)外部服務(wù)對(duì)很多團(tuán)隊(duì)來(lái)說(shuō)是不可接受的數(shù)據(jù)安全紅線(xiàn)。自托管方案把數(shù)據(jù)留在自己手里日志、緩存、訪問(wèn)鏈路都能完全管控。1.3 主動(dòng)砍掉的功能地圖模塊的邊界自研最大的陷阱是功能蔓延。我們開(kāi)工時(shí)立了幾條規(guī)矩不做實(shí)時(shí)路況不做衛(wèi)星影像不做室內(nèi)地圖不做導(dǎo)航語(yǔ)音。所有這些在對(duì)話(huà)場(chǎng)景里要么數(shù)據(jù)源貴得離譜要么用戶(hù)根本用不上。最終的功能范圍很明確能力是否實(shí)現(xiàn)說(shuō)明柵格瓦片底圖渲染是縮放級(jí)別覆蓋10到17滿(mǎn)足城市級(jí)到街區(qū)級(jí)地理編碼是文本地址轉(zhuǎn)坐標(biāo)返回候選列表逆地理編碼是坐標(biāo)反查行政區(qū)與最近POI關(guān)鍵詞POI檢索是按關(guān)鍵詞和半徑篩選支持排序距離估算是球面距離為主可選道路距離路線(xiàn)規(guī)劃部分輕量路網(wǎng)的預(yù)估方案不承諾實(shí)時(shí)路況實(shí)時(shí)路況否數(shù)據(jù)獲取和維護(hù)成本太高衛(wèi)星影像否瓦片體量大交互場(chǎng)景收益低砍功能的同時(shí)我們反反復(fù)復(fù)跟使用者確認(rèn)了一點(diǎn)所有查詢(xún)接口必須能在100毫秒內(nèi)返回因?yàn)閷?duì)話(huà)系統(tǒng)對(duì)延遲的敏感度遠(yuǎn)超Web頁(yè)面。這句話(huà)后來(lái)成了整個(gè)模塊設(shè)計(jì)的最高優(yōu)先級(jí)。2. 瓦片渲染與坐標(biāo)先把地圖畫(huà)出來(lái)2.1 瓦片金字塔一張大地圖怎么切成小方塊地圖繪制的核心不是畫(huà)而是切。一張完整的世界地圖在屏幕上一次性渲染是不可能的業(yè)界通行做法是瓦片金字塔把某一縮放級(jí)別下的地圖切成固定大小的正方形小圖瀏覽器或客戶(hù)端只加載當(dāng)前視野內(nèi)需要的那些塊。瓦片編號(hào)有一套全球統(tǒng)一的計(jì)算方式俗稱(chēng)slippy map規(guī)則在第z級(jí)縮放級(jí)別整個(gè)地球被分成2^z × 2^z個(gè)瓦片x從西到東遞增y從北到南遞增。給定經(jīng)緯度換算成瓦片坐標(biāo)的公式很固定function lonLatToTile(lon, lat, zoom) { const x Math.floor((lon 180) / 360 * Math.pow(2, zoom)); const latRad lat * Math.PI / 180; const y Math.floor( (1 - Math.log(Math.tan(latRad) 1 / Math.cos(latRad)) / Math.PI) / 2 * Math.pow(2, zoom) ); return { x, y, z: zoom }; }這個(gè)公式我建議直接背下來(lái)或者存到工具庫(kù)最顯眼的位置。整個(gè)模塊里幾乎所有功能——定位、搜索、路線(xiàn)——最終都要落到給定坐標(biāo)知道它在哪個(gè)瓦片上這一步。忘了括號(hào)或者把y方向取反地圖就會(huì)出現(xiàn)南北顛倒或者錯(cuò)位一整行這種看著特別詭異的毛病。2.2 Web Mercator投影為什么地圖上高緯度地區(qū)會(huì)變形瓦片系統(tǒng)建立在Web Mercator投影EPSG:3857上。它把地球近似成一個(gè)球體然后展開(kāi)成正方形平面。好處是計(jì)算簡(jiǎn)單、方向恒定壞處是越靠近極地變形越嚴(yán)重所以Web Mercator的實(shí)際覆蓋范圍被限制在南北緯85.05113度以?xún)?nèi)超出這個(gè)范圍沒(méi)有瓦片。這套投影對(duì)工程實(shí)現(xiàn)的影響在邊界上體現(xiàn)得最明顯。瓦片y坐標(biāo)在緯度為85.05113度時(shí)正好是0緯度往后再高就是負(fù)值。我們?cè)缙诘拇a沒(méi)做截?cái)嘣诟呔暥葴y(cè)試點(diǎn)出現(xiàn)了一批坐標(biāo)異常排查了半天才發(fā)現(xiàn)是投影邊界問(wèn)題。所以無(wú)論你用哪個(gè)地圖庫(kù)第一課都是經(jīng)緯度進(jìn)瓦片計(jì)算之前先在緯度上做一次clamp(-85.05113, 85.05113)。瓦片尺寸方面主流有兩種規(guī)格256×256和512×512。256的兼容性好512在Retina屏幕上更清晰。我建議先在256規(guī)格上把整個(gè)鏈路跑通確認(rèn)渲染無(wú)誤之后再考慮是否切512因?yàn)橥咂w量會(huì)隨著邊長(zhǎng)平方增長(zhǎng)。2.3 坐標(biāo)偏移混用底圖與坐標(biāo)源時(shí)最隱蔽的坑國(guó)內(nèi)做地圖繞不開(kāi)一個(gè)概念坐標(biāo)偏移。GPS拿到的原始坐標(biāo)是WGS-84橢球體系而國(guó)內(nèi)合規(guī)發(fā)布的地圖產(chǎn)品通常使用經(jīng)過(guò)偏移加密的坐標(biāo)體系。這兩種坐標(biāo)系之間的偏移不是一個(gè)固定常數(shù)而是隨位置變化的非線(xiàn)性變換。換句話(huà)說(shuō)你不能靠統(tǒng)一減掉0.001度來(lái)校正那樣在部分地區(qū)誤差會(huì)到幾百米。lingbot-map的定位是自托管輕量模塊我們用的策略是整個(gè)系統(tǒng)內(nèi)部統(tǒng)一使用WGS-84作為基準(zhǔn)坐標(biāo)所有外部來(lái)源的坐標(biāo)在入口處經(jīng)過(guò)一個(gè)可插拔的CoordinateTransformer轉(zhuǎn)換器底圖瓦片源則按照它實(shí)際采用的坐標(biāo)體系選擇對(duì)應(yīng)的請(qǐng)求參數(shù)。核心經(jīng)驗(yàn)是坐標(biāo)轉(zhuǎn)換必須在數(shù)據(jù)進(jìn)入系統(tǒng)的第一道關(guān)口完成絕不能散落在各個(gè)業(yè)務(wù)接口里各轉(zhuǎn)各的否則維護(hù)成本會(huì)失控。另外一個(gè)很容易忽略的點(diǎn)公共網(wǎng)絡(luò)上流傳的糾偏算法公式大多是通過(guò)采樣點(diǎn)擬合出來(lái)的近似解在某些地區(qū)精度可以在個(gè)別區(qū)域會(huì)出現(xiàn)突變。線(xiàn)上環(huán)境如果對(duì)精度有硬要求建議在關(guān)鍵區(qū)域布設(shè)本地控制點(diǎn)做二次修正這個(gè)話(huà)題后面的踩坑章節(jié)還會(huì)詳細(xì)講。3. 地名解析自建地理編碼服務(wù)的輕量實(shí)現(xiàn)3.1 詞庫(kù)與索引十萬(wàn)條POI要怎么塞進(jìn)內(nèi)存地理編碼就是把文本地址翻譯成坐標(biāo)。商業(yè)服務(wù)做這件事靠海量地址庫(kù)和復(fù)雜的解析模型我們自托管顯然不能那么奢侈路徑是公開(kāi)地名數(shù)據(jù) 企業(yè)私有POI合并成一個(gè)精簡(jiǎn)詞庫(kù)再用內(nèi)存索引加速檢索。詞庫(kù)的索引結(jié)構(gòu)我最終選擇了前綴樹(shù) 拼音前綴 編輯距離三層組合。首先是基于漢字的前綴樹(shù)保證科技園區(qū)能快速命中所有以這四個(gè)字開(kāi)頭的記錄其次是把地名轉(zhuǎn)成拼音后建立拼音前綴索引解決用戶(hù)輸入拼音的場(chǎng)景最后是編輯距離兜底用來(lái)處理用戶(hù)輸入少個(gè)字、多個(gè)字或順序顛倒的情況。這個(gè)方案每增加一條POI記錄內(nèi)存開(kāi)銷(xiāo)大概在幾百字節(jié)量級(jí)十萬(wàn)條POI僅索引部分占用約50MB左右在現(xiàn)在的服務(wù)器配置下完全不是問(wèn)題。真正要控制的是數(shù)據(jù)質(zhì)量詞庫(kù)里同名地點(diǎn)太多會(huì)讓候選列表變得不可用所以每條記錄都帶行政區(qū)劃和類(lèi)型標(biāo)簽檢索時(shí)先按行政區(qū)過(guò)濾再按文本相似度排序。3.2 解析流程從附近咖啡館到坐標(biāo)候選一次完整的地理編碼查詢(xún)走四步。第一步是標(biāo)準(zhǔn)化把大約附近大概那個(gè)這類(lèi)修飾詞剝掉再把東門(mén)西門(mén)這樣的相對(duì)方位詞解析成更明確的方位描述。第二步是候選召回用前面建的索引去找所有可能的匹配項(xiàng)這一步要求快寧多勿漏。第三步是評(píng)分排序綜合文本相似度、行政層級(jí)匹配度和距離權(quán)重給每個(gè)候選算出置信度。第四步是輸出返回Top N個(gè)候選而不是強(qiáng)行只給一個(gè)。這么做非常重要。對(duì)話(huà)場(chǎng)景的地址往往殘缺不全去上次那家店公司樓下老地方見(jiàn)——這些表達(dá)缺乏結(jié)構(gòu)化信息硬解析成單一坐標(biāo)必然出錯(cuò)。返回候選列表讓上層機(jī)器人把選擇權(quán)交給用戶(hù)或者結(jié)合會(huì)話(huà)上下文判斷才是符合對(duì)話(huà)語(yǔ)義的做法。{ status: 0, query: 園區(qū)B棟咖啡店, candidates: [ {name: 園區(qū)B棟咖啡店, lat: 30.2501, lng: 120.2012, score: 0.95, type: poi}, {name: B棟咖啡工坊, lat: 30.2510, lng: 120.1998, score: 0.71, type: poi} ] }3.3 逆地理編碼從坐標(biāo)反查這是哪兒逆地理編碼比正向解析簡(jiǎn)單但坑也不少。我們給出的坐標(biāo)點(diǎn)要先判斷落在哪個(gè)行政區(qū)劃內(nèi)再找距離最近的POI。行政區(qū)劃判斷本質(zhì)是點(diǎn)是否在多邊形內(nèi)的問(wèn)題經(jīng)典算法是射線(xiàn)法從目標(biāo)點(diǎn)引一條水平射線(xiàn)統(tǒng)計(jì)它與多邊形邊的交點(diǎn)數(shù)奇數(shù)在內(nèi)偶數(shù)在外。這個(gè)算法本身很快真正的問(wèn)題是行政邊界數(shù)據(jù)往往是幾萬(wàn)個(gè)頂點(diǎn)的大多邊形逐點(diǎn)判斷性能上不去。我的優(yōu)化方案是把每個(gè)行政區(qū)的外包矩形作為粗篩網(wǎng)格一個(gè)坐標(biāo)點(diǎn)先通過(guò)外包矩形快速過(guò)濾掉絕大多數(shù)不可能的多邊形剩下的再走射線(xiàn)法精判。實(shí)測(cè)下來(lái)這個(gè)兩級(jí)判斷能讓逆地理編碼的單次延遲控制在幾十毫秒量級(jí)。逆地理編碼還有一個(gè)業(yè)務(wù)層面的細(xì)節(jié)也是踩坑章節(jié)的重點(diǎn)行政區(qū)劃邊界存在飛地和重疊歸屬的情況一個(gè)點(diǎn)落在兩個(gè)區(qū)劃的邊界附近時(shí)返回哪個(gè)區(qū)劃必須要有明確的業(yè)務(wù)規(guī)則不能靠算法隨機(jī)。4. 距離與路線(xiàn)對(duì)話(huà)場(chǎng)景夠用即可4.1 三種距離別拿直線(xiàn)距離騙用戶(hù)距離計(jì)算是地圖模塊里用戶(hù)感知最強(qiáng)的部分。同樣是3公里遠(yuǎn)直線(xiàn)距離和實(shí)際駕車(chē)距離差別很大在道路繞行嚴(yán)重的城區(qū)可以差到一倍以上。最基礎(chǔ)的是球面距離用Haversine公式實(shí)現(xiàn)import math def haversine_km(lat1, lon1, lat2, lon2): r 6371.0 p1, p2 math.radians(lat1), math.radians(lat2) dp math.radians(lat2 - lat1) dl math.radians(lon2 - lon1) a math.sin(dp / 2) ** 2 math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2 return 2 * r * math.asin(math.sqrt(a))但對(duì)話(huà)機(jī)器人直接跟用戶(hù)說(shuō)1.8公里直線(xiàn)距離是會(huì)被罵的。我們對(duì)距離估算做了一個(gè)道路系數(shù)修正先按城市類(lèi)型和區(qū)域特征給直線(xiàn)距離乘一個(gè)繞路系數(shù)——城市密集區(qū)取值1.4到1.6郊區(qū)取值1.7到2.0。這個(gè)方案的準(zhǔn)確度在大多數(shù)場(chǎng)景下足夠而且?guī)缀醪辉黾佑?jì)算量。如果你需要更高精度再考慮引入真實(shí)路網(wǎng)。4.2 輕量路網(wǎng)加A星路線(xiàn)規(guī)劃的壓縮版要實(shí)現(xiàn)真正的道路距離就得有路網(wǎng)數(shù)據(jù)。我們做的不是全量精細(xì)路網(wǎng)而是只保留主干道和次干道的輕量拓?fù)淙缓蠡贏星算法計(jì)算最短路徑。A星的啟發(fā)函數(shù)直接用Haversine距離加上一個(gè)當(dāng)前邊權(quán)重的估計(jì)。輕量路網(wǎng)因?yàn)楣?jié)點(diǎn)少搜索空間有限單次路線(xiàn)規(guī)劃在幾十毫秒內(nèi)可以完成。這套方案的局限也很明顯它不區(qū)分車(chē)道、匝道和紅綠燈算出來(lái)的是一條理論上可行的路線(xiàn)不能當(dāng)導(dǎo)航用。但在對(duì)話(huà)場(chǎng)景里用戶(hù)要的答案是打車(chē)大概20分鐘還是騎車(chē)15分鐘輕量路網(wǎng)恰好夠用。路線(xiàn)數(shù)據(jù)和路網(wǎng)拓?fù)涞木S護(hù)是成本大頭。我們的策略是只對(duì)業(yè)務(wù)覆蓋的高頻區(qū)域生成路網(wǎng)全國(guó)范圍內(nèi)的完整路網(wǎng)規(guī)劃優(yōu)先級(jí)往后排。每次新增區(qū)域時(shí)跑一遍離線(xiàn)路網(wǎng)生成任務(wù)產(chǎn)出結(jié)果提交到瓦片包一起發(fā)布。4.3 為對(duì)話(huà)設(shè)計(jì)的查詢(xún)協(xié)議地圖模塊最后提供服務(wù)的方式是一組REST接口但設(shè)計(jì)這批接口時(shí)我們想的不是地圖怎么暴露能力而是機(jī)器人對(duì)話(huà)時(shí)最需要什么。接口被設(shè)計(jì)成三類(lèi)/v1/geocode用于文本轉(zhuǎn)坐標(biāo)/v1/search用于POI檢索/v1/route用于路線(xiàn)和距離。每個(gè)接口都支持在請(qǐng)求體里帶一個(gè)session_id模塊會(huì)把當(dāng)前會(huì)話(huà)最近的錨點(diǎn)位置緩存起來(lái)。這樣多輪對(duì)話(huà)里用戶(hù)先說(shuō)我在C棟樓下機(jī)器人記住這個(gè)位置下一句說(shuō)附近有打印店嗎就不需要用戶(hù)再重復(fù)一遍位置信息。異步和限流也是必須的。地理編碼批量導(dǎo)入任務(wù)會(huì)占用較多CPU如果跟線(xiàn)上查詢(xún)搶資源會(huì)導(dǎo)致對(duì)話(huà)延遲飆高。我們?cè)诜?wù)里給查詢(xún)?cè)O(shè)置了獨(dú)立的線(xiàn)程池重任務(wù)走消息隊(duì)列保證線(xiàn)上查詢(xún)通道始終有冗余資源。這個(gè)設(shè)計(jì)在后來(lái)的壓測(cè)里證明非常值——有一版我們把導(dǎo)入任務(wù)直接放在主進(jìn)程跑結(jié)果查詢(xún)p95延遲從60毫秒被拉到900毫秒回滾之后立刻恢復(fù)。5. 性能實(shí)測(cè)從零散請(qǐng)求到穩(wěn)定低延遲5.1 瓦片訪問(wèn)優(yōu)化首屏從4.8秒壓到780毫秒地圖模塊被接入機(jī)器人之后第一個(gè)性能瓶頸出現(xiàn)在瓦片加載。對(duì)話(huà)端的歷史消息里嵌入了地圖卡片用戶(hù)每次翻開(kāi)詳情都要重新加載瓦片冷啟動(dòng)狀態(tài)下首屏要4.8秒體驗(yàn)完全不能接受。我們對(duì)癥下藥做了三件事。第一是兩級(jí)LRU緩存內(nèi)存緩存最近訪問(wèn)的瓦片磁盤(pán)緩存做持久化內(nèi)存不夠時(shí)逐出到磁盤(pán)。第二是熱門(mén)區(qū)域預(yù)取根據(jù)歷史查詢(xún)熱度提前把常用區(qū)域的低級(jí)別瓦片生成好放進(jìn)緩存用戶(hù)一來(lái)就是熱的。第三是靜態(tài)資源走對(duì)象存儲(chǔ)并掛內(nèi)容分發(fā)網(wǎng)絡(luò)把重復(fù)請(qǐng)求的壓力從應(yīng)用服務(wù)上卸掉。優(yōu)化后的首屏加載從4.8秒降到780毫秒這個(gè)結(jié)果說(shuō)明了一個(gè)道理在自托管地圖模塊里瓶頸通常不是計(jì)算能力而是數(shù)據(jù)組織方式。5.2 地名索引預(yù)熱與查詢(xún)優(yōu)化地理編碼服務(wù)的性能取決于索引是否在內(nèi)存里常駐。我們采用的方式是啟動(dòng)時(shí)一次性加載全部索引到內(nèi)存用內(nèi)存映射方式訪問(wèn)索引文件查詢(xún)時(shí)不落盤(pán)。優(yōu)化前單次地理編碼平均320毫秒排查發(fā)現(xiàn)大部分時(shí)間耗在冷啟動(dòng)后的首次磁盤(pán)讀取上。索引預(yù)熱之后單查降到38毫秒p95也穩(wěn)定在60毫秒以?xún)?nèi)。附近POI檢索5公里半徑從900毫秒降到60毫秒靠的是在檢索前先用geohash網(wǎng)格做粗篩命中候選后再計(jì)算精確距離排序。這里有個(gè)計(jì)算細(xì)節(jié)值得強(qiáng)調(diào)全量POI兩兩算Haversine的距離排序非常昂貴但如果你先按geohash定位到同一網(wǎng)格把候選集縮到十幾條再對(duì)這十幾條算精確距離成本就完全可以忽略。粗篩用索引精算用公式這套組合拳在空間檢索里通用。5.3 低配環(huán)境下的資源核算很多讀者關(guān)心自托管地圖到底要什么配置。我們做了一組基準(zhǔn)測(cè)試部署環(huán)境數(shù)據(jù)規(guī)模實(shí)測(cè)表現(xiàn)2核4G單城市POI 局部瓦片緩存支撐內(nèi)部演示與低流量驗(yàn)證穩(wěn)態(tài)QPS約304核8G省級(jí)POI 多級(jí)瓦片緩存支撐正式對(duì)話(huà)流量穩(wěn)態(tài)QPS約120p95 100ms8核16G多點(diǎn)區(qū)域POI 大尺寸瓦片包可支撐多業(yè)務(wù)線(xiàn)并發(fā)主要瓶頸在帶寬瓦片包體量也需要提前算清楚只做單城市的常用縮放級(jí)別柵格瓦片大概1到3GB如果覆蓋到一個(gè)省且包含高縮放級(jí)別體量會(huì)漲到幾十GB。我們的經(jīng)驗(yàn)是瓦片包體積直接決定了部署成本和更新頻率所以默認(rèn)只在業(yè)務(wù)熱點(diǎn)區(qū)域生成高縮放級(jí)別瓦片低級(jí)別全局鋪底把總包控制在5GB以?xún)?nèi)。6. 踩坑記錄文檔里查不到的五個(gè)細(xì)節(jié)6.1 瓦片索引的邊界取整錯(cuò)誤第一個(gè)坑在瓦片坐標(biāo)計(jì)算。x和y必須用向下取整這個(gè)大多數(shù)人都知道但y方向在北半球高緯度地區(qū)有個(gè)隱藏問(wèn)題當(dāng)緯度超過(guò)Web Mercator的85.05113度上限時(shí)y值變成負(fù)的數(shù)組下標(biāo)越界瓦片渲染直接崩潰。我們?cè)谝淮胃呔暥葴y(cè)試點(diǎn)的聯(lián)調(diào)里遇到了整個(gè)地圖區(qū)域灰屏的現(xiàn)象查了一下午發(fā)現(xiàn)是測(cè)試數(shù)據(jù)里混入了一個(gè)緯度85.5度的異常坐標(biāo)點(diǎn)。修復(fù)方式就是在進(jìn)入瓦片計(jì)算前對(duì)緯度做clamp。后來(lái)我把這行clamp寫(xiě)成了公共函數(shù)的一個(gè)固定步驟所有調(diào)用方都從這里走一勞永逸。6.2 坐標(biāo)偏移在邊緣區(qū)域的突變坐標(biāo)轉(zhuǎn)換是另一個(gè)反復(fù)出問(wèn)題的地方。我們一開(kāi)始直接采用網(wǎng)上流傳的公開(kāi)糾偏算法在大多數(shù)區(qū)域表現(xiàn)正常但在某些地區(qū)誤差突然放大到幾百米。地圖上一開(kāi)始幾百米的偏移很難肉眼發(fā)現(xiàn)直到用戶(hù)反饋機(jī)器人說(shuō)的位置永遠(yuǎn)偏了一條街我們才意識(shí)到是轉(zhuǎn)換精度在這些區(qū)域失效了。解決方案是用本地控制點(diǎn)做校正收集一批在該區(qū)域已知真實(shí)坐標(biāo)與展示坐標(biāo)對(duì)應(yīng)的樣本點(diǎn)對(duì)偏差建一個(gè)區(qū)域修正表轉(zhuǎn)換時(shí)先查表再套用通用算法。這個(gè)方案比單純調(diào)參可靠得多也讓我徹底明白了一個(gè)道理坐標(biāo)偏移問(wèn)題是空間相關(guān)的任何一勞永逸的近似公式都要在目標(biāo)區(qū)域做局部驗(yàn)證。6.3 瓦片預(yù)取把帶寬打滿(mǎn)了瓦片預(yù)取功能上線(xiàn)的第一天我們就把自己給打掛了。預(yù)取程序用20個(gè)并發(fā)線(xiàn)程去熱門(mén)區(qū)域抓瓦片結(jié)果應(yīng)用服務(wù)的連接池和帶寬瞬間被打滿(mǎn)在線(xiàn)查詢(xún)?nèi)砍瑫r(shí)。修復(fù)方式是給預(yù)取任務(wù)加信號(hào)量限流把并發(fā)控制在2到3個(gè)線(xiàn)程并且對(duì)失敗請(qǐng)求做指數(shù)退避。實(shí)際上瓦片服務(wù)的穩(wěn)定性從來(lái)不由總帶寬決定而由瞬時(shí)并發(fā)尖峰決定。后來(lái)我們給所有內(nèi)部批處理任務(wù)都立了規(guī)矩批處理不得與線(xiàn)上查詢(xún)共享同一資源池。6.4 逆地理編碼的飛地與邊界重疊逆地理編碼返回錯(cuò)誤行政區(qū)是線(xiàn)上反饋里最難排查的一類(lèi)問(wèn)題?,F(xiàn)象是用戶(hù)在地圖上定位機(jī)器人報(bào)出的行政區(qū)劃和他實(shí)際所在的位置對(duì)不上。最終定位到兩類(lèi)原因一是行政邊界存在飛地即某行政區(qū)的一塊區(qū)域完全被另一個(gè)行政區(qū)包圍二是多邊形邊界重疊一個(gè)點(diǎn)同時(shí)落在多個(gè)區(qū)劃內(nèi)。單靠多邊形包含算法沒(méi)法處理這類(lèi)問(wèn)題。我們的做法是對(duì)重疊區(qū)域建立網(wǎng)格歸屬表以固定間隔把有沖突的區(qū)域切成網(wǎng)格每個(gè)網(wǎng)格預(yù)先指定一個(gè)業(yè)務(wù)歸屬同時(shí)把這種歸屬結(jié)果緩存起來(lái)避免每次查詢(xún)都跑一遍多邊形判斷。這套機(jī)制上線(xiàn)后逆地理編碼的實(shí)際錯(cuò)誤率降了一個(gè)數(shù)量級(jí)。6.5 日志里不能出現(xiàn)一根坐標(biāo)最后這個(gè)坑不是技術(shù)問(wèn)題而是意識(shí)問(wèn)題。地圖模塊的日志里天然會(huì)記錄經(jīng)緯度、查詢(xún)關(guān)鍵詞、逆地理編碼結(jié)果這些信息與具體用戶(hù)關(guān)聯(lián)后就是敏感數(shù)據(jù)。早期排查問(wèn)題時(shí)我們直接把查詢(xún)參數(shù)打到了日志里后來(lái)安全評(píng)審被一票否決。改造方式是在日志輸出層做統(tǒng)一脫敏坐標(biāo)只保留到小數(shù)點(diǎn)后一位POI名稱(chēng)做模糊化會(huì)話(huà)ID替換成不可逆散列。日志脫敏這件事不能靠每個(gè)開(kāi)發(fā)者自覺(jué)必須做成框架層強(qiáng)制執(zhí)行的過(guò)濾器。我在實(shí)際維護(hù)lingbot-map的過(guò)程中最大的體會(huì)是地圖模塊是一個(gè)很容易把自己做成無(wú)底洞的東西。如果你一開(kāi)始就想著覆蓋全國(guó)數(shù)據(jù)、做完整導(dǎo)航、接實(shí)時(shí)路況那這項(xiàng)目一年都交付不了。反過(guò)來(lái)先圈定一個(gè)城市或者一個(gè)園區(qū)把對(duì)話(huà)場(chǎng)景里的閉環(huán)跑通——能定位、能找店、能算距離——然后再往下一個(gè)區(qū)域擴(kuò)展才是自托管地圖的正確打開(kāi)方式。最后再分享一個(gè)小技巧附近POI檢索的排序永遠(yuǎn)先做geohash網(wǎng)格粗篩再做Haversine精算千萬(wàn)別讓全量候選集都走一遍球面距離計(jì)算否則接口延遲會(huì)隨著POI數(shù)量線(xiàn)性膨脹。這是我被壓測(cè)數(shù)據(jù)教育出來(lái)的經(jīng)驗(yàn)照著做能省掉你后面大半的優(yōu)化時(shí)間。