blob: 74dbf359640f63fb9d307a2779bbdd2734067c91 [file] [log] [blame]
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pegasus | Geo</title>
<link rel="stylesheet" href="/zh/assets/css/app.css">
<link rel="shortcut icon" href="/zh/assets/images/favicon.ico">
<link rel="stylesheet" href="/zh/assets/css/utilities.min.css">
<link rel="stylesheet" href="/zh/assets/css/docsearch.v3.css">
<script src="/assets/js/jquery.min.js"></script>
<script src="/assets/js/all.min.js"></script>
<script src="/assets/js/docsearch.v3.js"></script>
<!-- Begin Jekyll SEO tag v2.8.0 -->
<title>Geo | Pegasus</title>
<meta name="generator" content="Jekyll v4.3.3" />
<meta property="og:title" content="Geo" />
<meta property="og:locale" content="en_US" />
<meta name="description" content="Pegasus GEO" />
<meta property="og:description" content="Pegasus GEO" />
<meta property="og:site_name" content="Pegasus" />
<meta property="og:type" content="article" />
<meta property="article:published_time" content="2024-04-22T06:39:52+00:00" />
<meta name="twitter:card" content="summary" />
<meta property="twitter:title" content="Geo" />
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"BlogPosting","dateModified":"2024-04-22T06:39:52+00:00","datePublished":"2024-04-22T06:39:52+00:00","description":"Pegasus GEO","headline":"Geo","mainEntityOfPage":{"@type":"WebPage","@id":"/api/geo"},"url":"/api/geo"}</script>
<!-- End Jekyll SEO tag -->
</head>
<body>
<div class="dashboard is-full-height">
<!-- left panel -->
<div class="dashboard-panel is-medium is-hidden-mobile pl-0">
<div class="dashboard-panel-header has-text-centered">
<a href="/zh/">
<img src="/assets/images/pegasus-logo-inv.png" style="width: 80%;">
</a>
</div>
<div class="dashboard-panel-main is-scrollable pl-6">
<aside class="menu">
<p class="menu-label">Pegasus 产品文档</p>
<ul class="menu-list">
<li>
<a href="/zh/docs/downloads"
class="">
下载
</a>
</li>
</ul>
<p class="menu-label">编译构建</p>
<ul class="menu-list">
<li>
<a href="/zh/docs/build/compile-by-docker"
class="">
使用 Docker 完成编译(推荐)
</a>
</li>
<li>
<a href="/zh/docs/build/compile-from-source"
class="">
从源码编译
</a>
</li>
</ul>
<p class="menu-label">客户端库</p>
<ul class="menu-list">
<li>
<a href="/zh/clients/java-client"
class="">
Java 客户端
</a>
</li>
<li>
<a href="/zh/clients/cpp-client"
class="">
C++ 客户端
</a>
</li>
<li>
<a href="https://github.com/apache/incubator-pegasus/tree/master/go-client"
class="">
Golang 客户端
</a>
</li>
<li>
<a href="/zh/clients/python-client"
class="">
Python 客户端
</a>
</li>
<li>
<a href="/zh/clients/node-client"
class="">
NodeJS 客户端
</a>
</li>
<li>
<a href="/zh/clients/scala-client"
class="">
Scala 客户端
</a>
</li>
</ul>
<p class="menu-label">生态工具</p>
<ul class="menu-list">
<li>
<a href="/zh/docs/tools/shell"
class="">
Pegasus Shell 工具
</a>
</li>
<li>
<a href="https://github.com/pegasus-kv/admin-cli"
class="">
集群管理命令行
</a>
</li>
<li>
<a href="https://github.com/pegasus-kv/pegic"
class="">
数据访问命令行
</a>
</li>
</ul>
<p class="menu-label">用户接口</p>
<ul class="menu-list">
<li>
<a href="/zh/api/ttl"
class="">
TTL
</a>
</li>
<li>
<a href="/zh/api/single-atomic"
class="">
单行原子操作
</a>
</li>
<li>
<a href="/zh/api/redis"
class="">
Redis 适配
</a>
</li>
<li>
<a href="/zh/api/geo"
class="is-active">
GEO 支持
</a>
</li>
<li>
<a href="/zh/api/http"
class="">
HTTP 接口
</a>
</li>
</ul>
<p class="menu-label">高效运维</p>
<ul class="menu-list">
<li>
<a href="/zh/administration/deployment"
class="">
集群部署
</a>
</li>
<li>
<a href="/zh/administration/config"
class="">
配置说明
</a>
</li>
<li>
<a href="/zh/administration/rebalance"
class="">
负载均衡
</a>
</li>
<li>
<a href="/zh/administration/monitoring"
class="">
可视化监控
</a>
</li>
<li>
<a href="/zh/administration/rolling-update"
class="">
集群重启和升级
</a>
</li>
<li>
<a href="/zh/administration/scale-in-out"
class="">
集群扩容缩容
</a>
</li>
<li>
<a href="/zh/administration/resource-management"
class="">
资源管理
</a>
</li>
<li>
<a href="/zh/administration/cold-backup"
class="">
冷备份
</a>
</li>
<li>
<a href="/zh/administration/meta-recovery"
class="">
元数据恢复
</a>
</li>
<li>
<a href="/zh/administration/replica-recovery"
class="">
Replica 数据恢复
</a>
</li>
<li>
<a href="/zh/administration/zk-migration"
class="">
Zookeeper 迁移
</a>
</li>
<li>
<a href="/zh/administration/table-migration"
class="">
Table 迁移
</a>
</li>
<li>
<a href="/zh/administration/table-soft-delete"
class="">
Table 软删除
</a>
</li>
<li>
<a href="/zh/administration/table-env"
class="">
Table 环境变量
</a>
</li>
<li>
<a href="/zh/administration/remote-commands"
class="">
远程命令
</a>
</li>
<li>
<a href="/zh/administration/partition-split"
class="">
Partition-Split
</a>
</li>
<li>
<a href="/zh/administration/duplication"
class="">
跨机房同步
</a>
</li>
<li>
<a href="/zh/administration/compression"
class="">
数据压缩
</a>
</li>
<li>
<a href="/zh/administration/throttling"
class="">
流量控制
</a>
</li>
<li>
<a href="/zh/administration/experiences"
class="">
运维经验
</a>
</li>
<li>
<a href="/zh/administration/manual-compact"
class="">
Manual Compact 功能
</a>
</li>
<li>
<a href="/zh/administration/usage-scenario"
class="">
Usage Scenario 功能
</a>
</li>
<li>
<a href="/zh/administration/bad-disk"
class="">
坏盘检修
</a>
</li>
<li>
<a href="/zh/administration/whitelist"
class="">
Replica Server 白名单
</a>
</li>
<li>
<a href="/zh/administration/backup-request"
class="">
Backup Request
</a>
</li>
<li>
<a href="/zh/administration/hotspot-detection"
class="">
热点检测
</a>
</li>
</ul>
</aside>
</div>
</div>
<!-- main section -->
<div class="dashboard-main is-scrollable">
<nav class="navbar is-hidden-desktop">
<div class="navbar-brand">
<a href="/zh/" class="navbar-item">
<!-- Pegasus Icon -->
<img src="/assets/images/pegasus-square.png">
</a>
<div class="navbar-item">
<!--A simple language switch button that only supports zh and en.-->
<!--IF its language is zh, then switches to en.-->
<!--If you don't want a url to be relativized, you can add a space explicitly into the href to
prevents a url from being relativized by polyglot.-->
<a class="button is-light is-outlined is-inverted" href=" /api/geo"><strong>En</strong></a>
</div>
<a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false" data-target="navMenu">
<!-- Appears in mobile mode only -->
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div class="navbar-menu" id="navMenu">
<div class="navbar-end">
<!--dropdown-->
<div class="navbar-item has-dropdown is-hoverable">
<a href=""
class="navbar-link ">
<span>
Pegasus 产品文档
</span>
</a>
<div class="navbar-dropdown">
<a href="/zh/docs/downloads"
class="navbar-item ">
下载
</a>
</div>
</div>
<!--dropdown-->
<div class="navbar-item has-dropdown is-hoverable">
<a href=""
class="navbar-link ">
<span>
编译构建
</span>
</a>
<div class="navbar-dropdown">
<a href="/zh/docs/build/compile-by-docker"
class="navbar-item ">
使用 Docker 完成编译(推荐)
</a>
<a href="/zh/docs/build/compile-from-source"
class="navbar-item ">
从源码编译
</a>
</div>
</div>
<!--dropdown-->
<div class="navbar-item has-dropdown is-hoverable">
<a href=""
class="navbar-link ">
<span>
客户端库
</span>
</a>
<div class="navbar-dropdown">
<a href="/zh/clients/java-client"
class="navbar-item ">
Java 客户端
</a>
<a href="/zh/clients/cpp-client"
class="navbar-item ">
C++ 客户端
</a>
<a href="https://github.com/apache/incubator-pegasus/tree/master/go-client"
class="navbar-item ">
Golang 客户端
</a>
<a href="/zh/clients/python-client"
class="navbar-item ">
Python 客户端
</a>
<a href="/zh/clients/node-client"
class="navbar-item ">
NodeJS 客户端
</a>
<a href="/zh/clients/scala-client"
class="navbar-item ">
Scala 客户端
</a>
</div>
</div>
<!--dropdown-->
<div class="navbar-item has-dropdown is-hoverable">
<a href=""
class="navbar-link ">
<span>
生态工具
</span>
</a>
<div class="navbar-dropdown">
<a href="/zh/docs/tools/shell"
class="navbar-item ">
Pegasus Shell 工具
</a>
<a href="https://github.com/pegasus-kv/admin-cli"
class="navbar-item ">
集群管理命令行
</a>
<a href="https://github.com/pegasus-kv/pegic"
class="navbar-item ">
数据访问命令行
</a>
</div>
</div>
<!--dropdown-->
<div class="navbar-item has-dropdown is-hoverable">
<a href=""
class="navbar-link ">
<span>
用户接口
</span>
</a>
<div class="navbar-dropdown">
<a href="/zh/api/ttl"
class="navbar-item ">
TTL
</a>
<a href="/zh/api/single-atomic"
class="navbar-item ">
单行原子操作
</a>
<a href="/zh/api/redis"
class="navbar-item ">
Redis 适配
</a>
<a href="/zh/api/geo"
class="navbar-item is-active">
GEO 支持
</a>
<a href="/zh/api/http"
class="navbar-item ">
HTTP 接口
</a>
</div>
</div>
<!--dropdown-->
<div class="navbar-item has-dropdown is-hoverable">
<a href=""
class="navbar-link ">
<span>
高效运维
</span>
</a>
<div class="navbar-dropdown">
<a href="/zh/administration/deployment"
class="navbar-item ">
集群部署
</a>
<a href="/zh/administration/config"
class="navbar-item ">
配置说明
</a>
<a href="/zh/administration/rebalance"
class="navbar-item ">
负载均衡
</a>
<a href="/zh/administration/monitoring"
class="navbar-item ">
可视化监控
</a>
<a href="/zh/administration/rolling-update"
class="navbar-item ">
集群重启和升级
</a>
<a href="/zh/administration/scale-in-out"
class="navbar-item ">
集群扩容缩容
</a>
<a href="/zh/administration/resource-management"
class="navbar-item ">
资源管理
</a>
<a href="/zh/administration/cold-backup"
class="navbar-item ">
冷备份
</a>
<a href="/zh/administration/meta-recovery"
class="navbar-item ">
元数据恢复
</a>
<a href="/zh/administration/replica-recovery"
class="navbar-item ">
Replica 数据恢复
</a>
<a href="/zh/administration/zk-migration"
class="navbar-item ">
Zookeeper 迁移
</a>
<a href="/zh/administration/table-migration"
class="navbar-item ">
Table 迁移
</a>
<a href="/zh/administration/table-soft-delete"
class="navbar-item ">
Table 软删除
</a>
<a href="/zh/administration/table-env"
class="navbar-item ">
Table 环境变量
</a>
<a href="/zh/administration/remote-commands"
class="navbar-item ">
远程命令
</a>
<a href="/zh/administration/partition-split"
class="navbar-item ">
Partition-Split
</a>
<a href="/zh/administration/duplication"
class="navbar-item ">
跨机房同步
</a>
<a href="/zh/administration/compression"
class="navbar-item ">
数据压缩
</a>
<a href="/zh/administration/throttling"
class="navbar-item ">
流量控制
</a>
<a href="/zh/administration/experiences"
class="navbar-item ">
运维经验
</a>
<a href="/zh/administration/manual-compact"
class="navbar-item ">
Manual Compact 功能
</a>
<a href="/zh/administration/usage-scenario"
class="navbar-item ">
Usage Scenario 功能
</a>
<a href="/zh/administration/bad-disk"
class="navbar-item ">
坏盘检修
</a>
<a href="/zh/administration/whitelist"
class="navbar-item ">
Replica Server 白名单
</a>
<a href="/zh/administration/backup-request"
class="navbar-item ">
Backup Request
</a>
<a href="/zh/administration/hotspot-detection"
class="navbar-item ">
热点检测
</a>
</div>
</div>
</div>
</div>
</nav>
<nav class="navbar is-hidden-mobile">
<div class="navbar-start w-full">
<div class="navbar-item pl-0 w-full">
<!--TODO(wutao): Given the limitation of docsearch that couldn't handle multiple input,
I make searchbox only shown in desktop. Fix this issue when docsearch.js v3 released.
Related issue: https://github.com/algolia/docsearch/issues/230-->
<div id="docsearch"></div>
</div>
</div>
<div class="navbar-end">
<div class="navbar-item">
<!--A simple language switch button that only supports zh and en.-->
<!--IF its language is zh, then switches to en.-->
<!--If you don't want a url to be relativized, you can add a space explicitly into the href to
prevents a url from being relativized by polyglot.-->
<a class="button is-light is-outlined is-inverted" href=" /api/geo"><strong>En</strong></a>
</div>
</div>
</nav>
<section class="hero is-info lg:mr-3">
<div class="hero-body">
<p class="title is-size-2 is-centered">GEO 支持</p>
</div>
</section>
<section class="section" style="padding-top: 2rem;">
<div class="content">
<h1 id="pegasus-geo">Pegasus GEO</h1>
<h2 id="背景">背景</h2>
<p>在 Pegasus 中,当用户数据属于 POI (Points of Interest) 数据,其中含有地理信息,比如 value 中包含有经纬度,需要 Pegasus 提供接口进行 GEO 特性的支持。比如给定一个中心点坐标和一个半径,查找这个范围内的所有数据;给定两条 POI 数据的 hashkey 和 sortkey,求这两条数据地理上的距离等。</p>
<p>Pegasus 的 GEO (Geographic) 支持使用了 <a href="https://github.com/google/s2geometry">S2</a> 库, 主要用于将二维地理坐标(经度 + 纬度)与一维编码的相互转换、基于圆形的范围查询、Hilbert 曲线规则等特性。</p>
<p>本文将说明在 Pegasus 中是如何充分利用 S2 的特性,并结合 Pegasus 的数据分布与数据存储特性,来支持 GEO 特性的。</p>
<p>关于 S2 的实现原理请参考 <a href="http://s2geometry.io/">S2官网</a></p>
<h2 id="坐标转换">坐标转换</h2>
<p>在 S2 中,可以把二维经纬度编码成一维编码,一维编码由两部分组成:立方体面、平面坐标编码,比如:</p>
<p>经纬度(116.334441, 40.030202)的编码是:<code class="language-plaintext highlighter-rouge">1/223320022232200331010110113301</code>(总共32位),这个编码在 S2 中称为 <strong>CellId</strong></p>
<p>其中:</p>
<ul>
<li>首位的<code class="language-plaintext highlighter-rouge">1</code>代表地球立方体投影的面索引,索引范围是0~5,如下图所示:</li>
</ul>
<p><img src="/assets/images/geo_faces.png" alt="geo_faces.png" class="img-responsive" /></p>
<ul>
<li><code class="language-plaintext highlighter-rouge">/</code>是分隔符</li>
<li><code class="language-plaintext highlighter-rouge">223320022232200331010110113301</code>(30位),是经纬度坐标经过一系列转换得到的编码,具体转换过程这里不详细描述。需要指出的是,这是一个名为 Hilbert 曲线编码,它最大的特点是具有稳定性、连续性。</li>
</ul>
<p><img src="/assets/images/hilbert.png" alt="hilbert.png" class="img-responsive" /></p>
<p>S2 中的 Hilbert 曲线编码:</p>
<ul>
<li>编码可以看作是一个 4 进制的数值编码</li>
<li>编码由左往右按层进行,最多 30 层</li>
<li>一个编码代表地理上的一个方块区域,编码越长,区域越小</li>
<li>完整编码是前缀编码的子区域,每个父区域由4个子区域组成,比如<code class="language-plaintext highlighter-rouge">00</code>,<code class="language-plaintext highlighter-rouge">01</code>,<code class="language-plaintext highlighter-rouge">02</code>,<code class="language-plaintext highlighter-rouge">03</code><code class="language-plaintext highlighter-rouge">0</code>的子区域,且前者的区域范围的并集就是后者的区域范围</li>
<li>在数值上连续的值,在地理位置上也是连续的,比如<code class="language-plaintext highlighter-rouge">00</code><code class="language-plaintext highlighter-rouge">01</code>的区域范围是相邻的,<code class="language-plaintext highlighter-rouge">0122</code><code class="language-plaintext highlighter-rouge">0123</code>的区域范围也是相邻的</li>
</ul>
<h2 id="编码精度">编码精度</h2>
<p>S2 中的 Hilbert 曲线编码由 30 位组成,每一位代表一层划分。下表是各层单个 cell 的面积和 cell 个数。</p>
<table>
<thead>
<tr>
<th><strong>level</strong></th>
<th><strong>min area</strong></th>
<th><strong>max area</strong></th>
<th><strong>average area</strong></th>
<th><strong>units</strong></th>
<th><strong>Number of cells</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>00</td>
<td>85011012.19</td>
<td>85011012.19</td>
<td>85011012.19</td>
<td>km^2</td>
<td>6</td>
</tr>
<tr>
<td>01</td>
<td>21252753.05</td>
<td>21252753.05</td>
<td>21252753.05</td>
<td>km^2</td>
<td>24</td>
</tr>
<tr>
<td>02</td>
<td>4919708.23</td>
<td>6026521.16</td>
<td>5313188.26</td>
<td>km^2</td>
<td>96</td>
</tr>
<tr>
<td>03</td>
<td>1055377.48</td>
<td>1646455.50</td>
<td>1328297.07</td>
<td>km^2</td>
<td>384</td>
</tr>
<tr>
<td>04</td>
<td>231564.06</td>
<td>413918.15</td>
<td>332074.27</td>
<td>km^2</td>
<td>1536</td>
</tr>
<tr>
<td>05</td>
<td>53798.67</td>
<td>104297.91</td>
<td>83018.57</td>
<td>km^2</td>
<td>6K</td>
</tr>
<tr>
<td>06</td>
<td>12948.81</td>
<td>26113.30</td>
<td>20754.64</td>
<td>km^2</td>
<td>24K</td>
</tr>
<tr>
<td>07</td>
<td>3175.44</td>
<td>6529.09</td>
<td>5188.66</td>
<td>km^2</td>
<td>98K</td>
</tr>
<tr>
<td>08</td>
<td>786.20</td>
<td>1632.45</td>
<td>1297.17</td>
<td>km^2</td>
<td>393K</td>
</tr>
<tr>
<td>09</td>
<td>195.59</td>
<td>408.12</td>
<td>324.29</td>
<td>km^2</td>
<td>1573K</td>
</tr>
<tr>
<td>10</td>
<td>48.78</td>
<td>102.03</td>
<td>81.07</td>
<td>km^2</td>
<td>6M</td>
</tr>
<tr>
<td>11</td>
<td>12.18</td>
<td>25.51</td>
<td>20.27</td>
<td>km^2</td>
<td>25M</td>
</tr>
<tr>
<td>12</td>
<td>3.04</td>
<td>6.38</td>
<td>5.07</td>
<td>km^2</td>
<td>100M</td>
</tr>
<tr>
<td>13</td>
<td>0.76</td>
<td>1.59</td>
<td>1.27</td>
<td>km^2</td>
<td>402M</td>
</tr>
<tr>
<td>14</td>
<td>0.19</td>
<td>0.40</td>
<td>0.32</td>
<td>km^2</td>
<td>1610M</td>
</tr>
<tr>
<td>15</td>
<td>47520.30</td>
<td>99638.93</td>
<td>79172.67</td>
<td>m^2</td>
<td>6B</td>
</tr>
<tr>
<td>16</td>
<td>11880.08</td>
<td>24909.73</td>
<td>19793.17</td>
<td>m^2</td>
<td>25B</td>
</tr>
<tr>
<td>17</td>
<td>2970.02</td>
<td>6227.43</td>
<td>4948.29</td>
<td>m^2</td>
<td>103B</td>
</tr>
<tr>
<td>18</td>
<td>742.50</td>
<td>1556.86</td>
<td>1237.07</td>
<td>m^2</td>
<td>412B</td>
</tr>
<tr>
<td>19</td>
<td>185.63</td>
<td>389.21</td>
<td>309.27</td>
<td>m^2</td>
<td>1649B</td>
</tr>
<tr>
<td>20</td>
<td>46.41</td>
<td>97.30</td>
<td>77.32</td>
<td>m^2</td>
<td>7T</td>
</tr>
<tr>
<td>21</td>
<td>11.60</td>
<td>24.33</td>
<td>19.33</td>
<td>m^2</td>
<td>26T</td>
</tr>
<tr>
<td>22</td>
<td>2.90</td>
<td>6.08</td>
<td>4.83</td>
<td>m^2</td>
<td>105T</td>
</tr>
<tr>
<td>23</td>
<td>0.73</td>
<td>1.52</td>
<td>1.21</td>
<td>m^2</td>
<td>422T</td>
</tr>
<tr>
<td>24</td>
<td>0.18</td>
<td>0.38</td>
<td>0.30</td>
<td>m^2</td>
<td>1689T</td>
</tr>
<tr>
<td>25</td>
<td>453.19</td>
<td>950.23</td>
<td>755.05</td>
<td>cm^2</td>
<td>7e15</td>
</tr>
<tr>
<td>26</td>
<td>113.30</td>
<td>237.56</td>
<td>188.76</td>
<td>cm^2</td>
<td>27e15</td>
</tr>
<tr>
<td>27</td>
<td>28.32</td>
<td>59.39</td>
<td>47.19</td>
<td>cm^2</td>
<td>108e15</td>
</tr>
<tr>
<td>28</td>
<td>7.08</td>
<td>14.85</td>
<td>11.80</td>
<td>cm^2</td>
<td>432e15</td>
</tr>
<tr>
<td>29</td>
<td>1.77</td>
<td>3.71</td>
<td>2.95</td>
<td>cm^2</td>
<td>1729e15</td>
</tr>
<tr>
<td>30</td>
<td>0.44</td>
<td>0.93</td>
<td>0.74</td>
<td>cm^2</td>
<td>7e18</td>
</tr>
</tbody>
</table>
<h2 id="数据存储">数据存储</h2>
<p>在 Pegasus 中,数据存储的 key 是 hashkey + sortkey:hashkey 用于确定数据所处的 partition,同一 hashkey 的数据存储在同一 Replica Server 的一块逻辑连续的区域中,sortkey 用于在这块区域中做数据排序。</p>
<p>经纬度经过坐标转换得到一维编码 CellId 后,就可以把这个一维编码作为 key 存储起来做<strong>GEO索引数据</strong>了,Pegasus 将这个一维编码拆分成 hashkey 和 sortkey 两部分,可以根据实际的用户场景采取不同的位数划分策略。</p>
<p>GEO 索引数据独立于原始数据,两类数据存储在不同的 Pegasus 表中,通过 <a href="https://github.com/apache/incubator-pegasus/blob/master/src/geo/lib/geo_client.h">GEO Client</a> 做数据同步,同时支持原生 Pegasus API 和 GEO API 的访问。</p>
<p>所以,在使用 Pegasus GEO 特性时,需要创建两个 Pegasus 表,一个是原始表,用于存储用户写入的原始数据,一个是 GEO 索引表,用于存储 GEO Client 自动转换原始数据生成的 GEO 索引数据。</p>
<h3 id="hashkey">hashkey</h3>
<p>hashkey 由一维编码的前缀构成。比如在一个用户场景中,将 hashkey 长度定为<code class="language-plaintext highlighter-rouge">14</code>(1位face,1位分隔符<code class="language-plaintext highlighter-rouge">/</code>,12位Hilbert编码)能取得更好的性能。</p>
<blockquote>
<p>那么,最小搜索层就为12</p>
</blockquote>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> CellId
|1/223320022232..................|
|-------------32 bytes-----------|
|---14 bytes--|
hashkey
</code></pre></div></div>
<h3 id="sortkey">sortkey</h3>
<p>为了满足不同半径范围、不同精度的查询,我们把 CellId 剩下的 18 位全部放到 sortkey 中。</p>
<ul>
<li>在进行较大半径的范围的查询时,取更少的 sortkey 位数(对应的 CellId 更短)作为前缀,进行数据 scan 查询,这样可以减少数据 scan 的次数</li>
<li>在进行较小半径的范围的查询或点查询时,取更多的 sortkey 位数(对应的 CellId 更长)作为前缀,进行数据 scan 查询,这样可以减少数据 scan 的范围</li>
</ul>
<p>这可以在不修改底层存储数据的前提下,让应用层保持比较高的灵活性。</p>
<blockquote>
<p>查询相同地理区域内(例如一个圆形区域)的数据时,使用短 CellId 查询数据查询的范围更大,查询的次数更少,但得到的在区域外的无用数据更多。而使用长 CellId 查询数据查询的范围更小,得到的在区域外的无用数据更少,但查询的次数更多</p>
<p>参考:<a href="http://s2geometry.io/devguide/examples/coverings">S2 coverings</a></p>
</blockquote>
<p>尽管在第30层时,cell 的面积已经足够小( &lt; 1cm^2),但仍有可能两条数据落在同一个 cell 里,所以需要在 CellId 编码的基础上,解决 key 冲突问题。Pegasus 将<strong>原始表</strong>的 hashkey 和 sortkey 联合起来,追加在 GEO 索引表的 sortkey 之后。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> CellId
|1/223320022232200331010110113301|
|-------------32 bytes-----------|
|---14 bytes--||-----18 bytes----||--原始hashkey--||--原始sortkey--|
|-GEO hashkey-||-------------------GEO sortkey-------------------|
</code></pre></div></div>
<h3 id="value">value</h3>
<p>使用 Pegasus GEO 特性时,所存储的 value 必须能够解析出经纬度,具体的解析方式参考<a href="/zh/api/geo#value_extractor">Value extractor</a></p>
<p>GEO 索引表的 value 跟原始表的 value 完全相同,因此会存在一份冗余数据,使用空间换时间的方式避免二次索引。</p>
<blockquote>
<p>如果确实有在单条 POI 中存储较大数据的需求,又想节省磁盘空间,可以手动实现二次索引,即在 GEO value 中存储二级索引的 key,再在另外的表中存储实际的大 value。</p>
</blockquote>
<h2 id="数据更新">数据更新</h2>
<h3 id="set">set</h3>
<p><code class="language-plaintext highlighter-rouge">set</code> 操作会同时更新上述两个表的数据,即 Pegasus 原始表数据和 GEO 索引表数据,数据构造方式也如上所述。</p>
<p><code class="language-plaintext highlighter-rouge">set</code>操作的 hashkey,sortkey 是用户自己的格式,使用 GEO API 时并不做约束。两个表的数据同步对用户是透明的,由 GEO Client 自动完成。</p>
<p>使用 Redis GEO API 时, 参考 <a href="/zh/api/redis#geo-api">GEO API</a></p>
<p>在 Pegasus 实现中,<code class="language-plaintext highlighter-rouge">set</code>操作会首先尝试读取出已有的数据,如果数据不存在,则直接向两个表中写入数据。如果数据已存在,会先将老的 GEO 索引数据清理掉后,再写入新数据。因为新老数据的索引数据 <code class="language-plaintext highlighter-rouge">&lt;hashkey, sortkey&gt;</code> 可能是不一样的(即新老 value 根据 extractor 解析得到的经纬度不一样),若不清理,GEO 索引表中将存在垃圾数据,造成磁盘空间的浪费,也会在进行地理范围查询时(即<code class="language-plaintext highlighter-rouge">GEORADIUS</code>)查到脏数据。</p>
<h3 id="del">del</h3>
<p><code class="language-plaintext highlighter-rouge">del</code>操作会同时删除两个表的数据,原理同上。</p>
<h2 id="数据查询">数据查询</h2>
<h3 id="设计">设计</h3>
<p>地理范围查询会转换成 Pegasus 的多次 scan 操作,一次 scan 对应为一个 CellId 范围内的所有数据扫描。 要想获得更高的性能,就需要减少 scan 的总次数和单次 scan 的数据量,也就是需要减少总的 CellId 数量和单个 CellId 的面积。</p>
<p>比如,在做如下红色圆圈的范围查询时,可以采取蓝色方框的 CellId 查询集合:</p>
<p><img src="/assets/images/s2_cap_1.png" alt="s2_cap_1.png" class="img-responsive" /></p>
<p>虽然这样的结果更精确,但参与计算的 CellId 的数量更多,带来的 client-server RPC 次数更多,网络开销更大,延迟更高。此外,在真实的应用场景中,太小的 CellId 可能并没有数据,但依然会消耗一次 RPC。</p>
<p>所以,在当前的 Pegasus 实现中,只联合使用两层 cell,<code class="language-plaintext highlighter-rouge">最小搜索层</code><code class="language-plaintext highlighter-rouge">最大搜索层</code>, 以 12 层和 16 层为例,得到的 CellId 查询集合如蓝色方框所示:</p>
<p><img src="/assets/images/s2_cap_2.png" alt="s2_cap_2.png" class="img-responsive" /></p>
<h3 id="查询流程">查询流程</h3>
<p><code class="language-plaintext highlighter-rouge">search_radial</code>为例,此 API 的意义是给定点和半径,查询出该圆形区域内的所有 POI 数据。</p>
<blockquote>
<p>这里我们只讨论圆形区域的数据查询,其他的比如多边形区域的思想是类似的。</p>
</blockquote>
<p>使用 S2 API 来查询覆盖了给定区域的 CellId 集合:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// Returns an S2CellUnion that covers the given region and satisfies the current options.
S2CellUnion GetCovering(const S2Region&amp; region);
</code></pre></div></div>
<blockquote>
<p><code class="language-plaintext highlighter-rouge">search_radial</code> API 有两个重载函数,一个是输入经纬度,一个是输入 hashky 和 sortkey,后者是通过 key 从原始表中取到 value,解析出 value 中的经纬度,再转调前者。</p>
</blockquote>
<p>查询流程如下:</p>
<ol>
<li>根据经纬度、半径,求出 S2Cap 圆形区域<code class="language-plaintext highlighter-rouge">C</code></li>
<li>根据圆形区域、指定的<code class="language-plaintext highlighter-rouge">最小搜索层</code>,通过<code class="language-plaintext highlighter-rouge">GetCovering</code>,求出在<code class="language-plaintext highlighter-rouge">最小搜索层</code>上的 CellId 集合</li>
<li>遍历这些 CellId,判断 CellId 区域跟圆形区域<code class="language-plaintext highlighter-rouge">C</code>的关系
<ol>
<li>全覆盖:取该 CellId 内的所有 POI 数据</li>
<li>半覆盖:将该 CellId 按<code class="language-plaintext highlighter-rouge">最大搜索层</code>继续拆分,判断拆分后的 sub_CellId 区域与圆形区域<code class="language-plaintext highlighter-rouge">C</code>的关系
<ol>
<li>覆盖/相交:取该 sub_CellId 的所有 POI 数据</li>
<li>不相交:丢弃</li>
</ol>
</li>
</ol>
</li>
</ol>
<blockquote>
<p><code class="language-plaintext highlighter-rouge">最小搜索层</code><code class="language-plaintext highlighter-rouge">最大搜索层</code>的配置参考后文。
<code class="language-plaintext highlighter-rouge">最小搜索层</code>的 CellId 长度确定 GEO 索引表中数据的 hashkey 长度。</p>
</blockquote>
<p>取一个 CellId 的所有 POI 数据时,会根据上文的 key 构造规则,构造一对包含这个 CellId 所有数据的<code class="language-plaintext highlighter-rouge">start_sortkey</code><code class="language-plaintext highlighter-rouge">stop_sortkey</code>,再使用Pegasus的<code class="language-plaintext highlighter-rouge">scan</code>接口进行数据搜索。</p>
<ul>
<li>对于<code class="language-plaintext highlighter-rouge">3.1</code>步取到的<code class="language-plaintext highlighter-rouge">最小搜索层</code> CellId 的编码,它也就是 GEO 索引表中的 hashkey,调用<code class="language-plaintext highlighter-rouge">scan(CellId, "", "")</code>查询所有 POI 数据
<ul>
<li>比如,一个 12 层的 cell <code class="language-plaintext highlighter-rouge">1/223320022232</code>被区域完全覆盖,则调用<code class="language-plaintext highlighter-rouge">scan("1/223320022232", "", "")</code>查询所有 POI 数据</li>
</ul>
</li>
<li>对于<code class="language-plaintext highlighter-rouge">3.2.1</code>步取到的 sub_CellId 集合,hashkey 是它的前缀,调用<code class="language-plaintext highlighter-rouge">scan(sub_CellId_common_prefix, sub_CellId1, sub_CellId2)</code>搜索 POI 数据
<ul>
<li>其中,sub_CellId_common_prefix 是 sub_CellId 集合的公共前缀,长度是 hashkey 的长度。sub_CellId1 和 sub_CellId2 之间连续的所有 sub_CellId 都在集合中,字符串长度是<code class="language-plaintext highlighter-rouge">最大搜索层</code><code class="language-plaintext highlighter-rouge">最小搜索层</code>的长度</li>
<li>比如,一个12层的 cell <code class="language-plaintext highlighter-rouge">1/223320022232</code>的子区域<code class="language-plaintext highlighter-rouge">0001</code>,<code class="language-plaintext highlighter-rouge">0002</code>,<code class="language-plaintext highlighter-rouge">0003</code>,<code class="language-plaintext highlighter-rouge">0100</code>才跟目标区域相交时,则调用<code class="language-plaintext highlighter-rouge">scan("1/223320022232", "0001", "0003")</code><code class="language-plaintext highlighter-rouge">scan("1/223320022232", "0100", "0100")</code></li>
</ul>
</li>
</ul>
<p>得到<code class="language-plaintext highlighter-rouge">scan</code>的结果后,还需处理:</p>
<ul>
<li>计算距离:因为 CellId 可能只与输入区域部分重合,该点若在区域外,需丢弃</li>
<li>排序:当有升序/降序要求时</li>
</ul>
<h3 id="灵活性">灵活性</h3>
<p>由于我们存储了完整的 30 层 CellId,所以在实际使用中,可以根据地理数据密度、网络 IO、磁盘 IO等情况调整<code class="language-plaintext highlighter-rouge">最大搜索层</code></p>
<blockquote>
<p><code class="language-plaintext highlighter-rouge">最大搜索层</code>默认为<code class="language-plaintext highlighter-rouge">16</code></p>
</blockquote>
<h4 id="api方式">API方式</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dsn::error_s set_max_level(int level);
</code></pre></div></div>
<h4 id="配置文件方式">配置文件方式</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[geo_client.lib]
max_level = 16
</code></pre></div></div>
<h3 id="不变性">不变性</h3>
<p>由于<code class="language-plaintext highlighter-rouge">最小搜索层</code>确定了 GEO 索引数据的 hashkey 的长度,数据一旦写入 Pegasus 后,<code class="language-plaintext highlighter-rouge">最小搜索层</code>便不可修改了,因为数据已按这个 hashkey 长度规则固化下来。</p>
<p>若要修改,需要重建数据。</p>
<blockquote>
<p>默认为<code class="language-plaintext highlighter-rouge">12</code></p>
</blockquote>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[geo_client.lib]
;NOTE: 'min_level' is immutable after some data has been inserted into DB by geo_client.
min_level = 12
</code></pre></div></div>
<h2 id="value-extractor">Value extractor</h2>
<p>目前 Pegasus 支持从固定格式的 value 中解析出经纬度。经纬度以字符串形式嵌入在 value 中,以<code class="language-plaintext highlighter-rouge">|</code>分割。</p>
<p>比如:value 可以是<code class="language-plaintext highlighter-rouge">.*|115.886447|41.269031|.*</code>,经纬度在 value 中的索引由配置文件中的<code class="language-plaintext highlighter-rouge">latitude_index</code><code class="language-plaintext highlighter-rouge">longitude_index</code>确定。</p>
<h2 id="api--redis-proxy">API &amp; Redis Proxy</h2>
<p>Pegasus GEO 特性的使用有两种方式,一是直接使用 C++ GEO Client,二是使用 Redis Proxy。</p>
<p><a href="https://github.com/apache/incubator-pegasus/blob/master/src/geo/lib/geo_client.h">C++ GEO client代码</a>中有详细的 API 说明。</p>
<h2 id="配置文件">配置文件</h2>
<p>Redis Proxy 的使用请参考<a href="redis">Redis适配</a></p>
<p>GEO API 添加的配置文件如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[geo_client.lib]
;NOTE: 'min_level' is immutable after some data has been inserted into DB by geo_client.
min_level = 12
max_level = 16
; 用于经纬度的extractor
latitude_index = 5
longitude_index = 4
</code></pre></div></div>
<h2 id="批量数据导入">批量数据导入</h2>
<p>在一些使用场景中,用户已经有 value 中包含经纬度的原始数据表,需要构建上述的 GEO 索引表,则可以使用 shell 工具里的<a href="/zh/docs/tools/shell/#copy_data">copy_data</a>功能来实现。比如:</p>
<p>在进行<code class="language-plaintext highlighter-rouge">copy_data</code>操作之前,目标集群以及两个目标表(即,原始数据表<code class="language-plaintext highlighter-rouge">temp</code>,GEO 索引数据表 <code class="language-plaintext highlighter-rouge">temp_geo</code>)都需要提前创建好。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>copy_data -c target_cluster -a temp -g
</code></pre></div></div>
<p>数据导入完成后就可以搭建 Redis Proxy 了,具体的说明参考<a href="redis">Redis适配</a>,需要注意的是配置项:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[apps.proxy]
; if using GEO APIs, an extra table name which store geo index data should be appened, i.e.
arguments = redis_cluster temp temp_geo
</code></pre></div></div>
<h2 id="benchmark">Benchmark</h2>
<h3 id="测试环境">测试环境</h3>
<h4 id="服务器配置">服务器配置</h4>
<ul>
<li>CPU:E5-2620v3 * 2</li>
<li>内存:128GB</li>
<li>磁盘:容量 480GB SSD * 8</li>
<li>网卡:带宽 1Gb</li>
</ul>
<h4 id="集群配置">集群配置</h4>
<ul>
<li>Replica Server 节点数:5 个</li>
<li>版本:v1.9.2</li>
<li>测试表的 Partition 数:128</li>
<li>单条数据大小:120 字节</li>
</ul>
<h4 id="测试接口">测试接口</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>void async_search_radial(double lat_degrees,
double lng_degrees,
double radius_m,
int count,
SortType sort_type,
int timeout_ms,
geo_search_callback_t &amp;&amp;callback);
</code></pre></div></div>
<p><strong>参数</strong></p>
<ul>
<li>lat_degrees、lng_degrees:每次都选取北京五环内的随机点</li>
<li>radius_m:如下表第一列,单位米</li>
<li>count:-1,表示不限定结果数量</li>
<li>sort_type:不排序</li>
</ul>
<h3 id="测试结果">测试结果</h3>
<table>
<thead>
<tr>
<th>Radius(m)</th>
<th>P50(ms)</th>
<th>P75(ms)</th>
<th>P99(ms)</th>
<th>P99.9(ms)</th>
<th>Avg result count</th>
<th>QPS per node</th>
</tr>
</thead>
<tbody>
<tr>
<td>50</td>
<td>1.63071622</td>
<td>1.84607433</td>
<td>4.04545455</td>
<td>6.28</td>
<td>9.4608</td>
<td>740.287</td>
</tr>
<tr>
<td>100</td>
<td>1.76</td>
<td>2.33614794</td>
<td>5.4</td>
<td>6.45319149</td>
<td>38.0296</td>
<td>656.66</td>
</tr>
<tr>
<td>200</td>
<td>2.41017042</td>
<td>3.31062092</td>
<td>6.41781609</td>
<td>9.60588235</td>
<td>154.3682</td>
<td>536.624</td>
</tr>
<tr>
<td>300</td>
<td>3.30833333</td>
<td>4.21979167</td>
<td>9.4310559</td>
<td>18</td>
<td>350.9676</td>
<td>434.491</td>
</tr>
<tr>
<td>500</td>
<td>5.07763975</td>
<td>6.84964682</td>
<td>16.84931507</td>
<td>21.78082192</td>
<td>986.0826</td>
<td>347.231</td>
</tr>
<tr>
<td>1000</td>
<td>12.28164727</td>
<td>18.70972532</td>
<td>43.18181818</td>
<td>57.049698</td>
<td>3947.5294</td>
<td>204.23</td>
</tr>
<tr>
<td>2000</td>
<td>35.78666667</td>
<td>54.7300885</td>
<td>108.7331378</td>
<td>148.616578</td>
<td>15674.1198</td>
<td>98.7633</td>
</tr>
</tbody>
</table>
</div>
</section>
<footer class="footer">
<div class="container">
<div class="content is-small has-text-centered">
<div style="margin-bottom: 20px;">
<a href="http://incubator.apache.org">
<img src="/assets/images/egg-logo.png"
width="15%"
alt="Apache Incubator"/>
</a>
</div>
Copyright &copy; 2023 <a href="http://www.apache.org">The Apache Software Foundation</a>.
Licensed under the <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache License, Version
2.0</a>.
<br><br>
Apache Pegasus is an effort undergoing incubation at The Apache Software Foundation (ASF),
sponsored by the Apache Incubator. Incubation is required of all newly accepted projects
until a further review indicates that the infrastructure, communications, and decision making process
have stabilized in a manner consistent with other successful ASF projects. While incubation status is
not necessarily a reflection of the completeness or stability of the code, it does indicate that the
project has yet to be fully endorsed by the ASF.
<br><br>
Apache Pegasus, Pegasus, Apache, the Apache feather logo, and the Apache Pegasus project logo are either
registered trademarks or trademarks of The Apache Software Foundation in the United States and other
countries.
</div>
</div>
</footer>
</div>
<!-- right panel -->
<div class="dashboard-panel is-small is-scrollable is-hidden-mobile">
<p class="menu-label">
<span class="icon">
<i class="fa fa-bars" aria-hidden="true"></i>
</span>
本页导航
</p>
<ul class="menu-list">
<li><a href="#pegasus-geo">Pegasus GEO</a>
<ul>
<li><a href="#背景">背景</a></li>
<li><a href="#坐标转换">坐标转换</a></li>
<li><a href="#编码精度">编码精度</a></li>
<li><a href="#数据存储">数据存储</a>
<ul>
<li><a href="#hashkey">hashkey</a></li>
<li><a href="#sortkey">sortkey</a></li>
<li><a href="#value">value</a></li>
</ul>
</li>
<li><a href="#数据更新">数据更新</a>
<ul>
<li><a href="#set">set</a></li>
<li><a href="#del">del</a></li>
</ul>
</li>
<li><a href="#数据查询">数据查询</a>
<ul>
<li><a href="#设计">设计</a></li>
<li><a href="#查询流程">查询流程</a></li>
<li><a href="#灵活性">灵活性</a>
<ul>
<li><a href="#api方式">API方式</a></li>
<li><a href="#配置文件方式">配置文件方式</a></li>
</ul>
</li>
<li><a href="#不变性">不变性</a></li>
</ul>
</li>
<li><a href="#value-extractor">Value extractor</a></li>
<li><a href="#api--redis-proxy">API &amp; Redis Proxy</a></li>
<li><a href="#配置文件">配置文件</a></li>
<li><a href="#批量数据导入">批量数据导入</a></li>
<li><a href="#benchmark">Benchmark</a>
<ul>
<li><a href="#测试环境">测试环境</a>
<ul>
<li><a href="#服务器配置">服务器配置</a></li>
<li><a href="#集群配置">集群配置</a></li>
<li><a href="#测试接口">测试接口</a></li>
</ul>
</li>
<li><a href="#测试结果">测试结果</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
</div>
<script src="/assets/js/app.js" type="text/javascript"></script>
<script>
docsearch({
container: '#docsearch',
appId: 'QRN30RBW0S',
indexName: 'pegasus-apache',
apiKey: 'd3a3252fa344359766707a106c4ed88f',
debug: true
});
</script>
</body>
</html>