Un collègue m'a récemment demandé comment déplacer un couple de milliards d'enregistrements vers le magasin de données spatiotemporelles big data (STBDS) de GeoEvent chez un client, en utilisant ArcGIS Data Interoperability. Il s'agit de positions archivées de véhicules pour une entreprise de services publics, stockées dans une géodatabase Oracle. Quelques millions de nouveaux événements arrivent quotidiennement. Les données archivées sont réparties dans plus de 70 tables.<\/P>
Parce qu'un service de fonctionnalités dans le STBDS a la même API REST qu'un service de fonctionnalités hébergé ordinaire, le plan initial était d'exploiter le modèle décrit dans mon blog précédent, à savoir ETL les données archivées en éléments shapefile du portail puis utiliser le point de terminaison Append REST de la couche cible pour charger les données de manière asynchrone. Une complication supplémentaire était de paralléliser l'ETL en plusieurs tâches simultanées. Cette approche réduirait le risque de coupures réseau lors du streaming de petites transactions (1000 entités est la taille par défaut du lot) vers le portail. Il s'est avéré que l'environnement client n'était pas à une version supportant ce flux de travail et nous avons opté pour le streaming, mais cela m'a fait réfléchir à la façon d'ETL des big data vers la nouvelle vague d'entrepôts cloud que vous pouvez utiliser nativement, et dans ArcGIS Pro 2.9.<\/P>
Mon message principal est que vous pouvez effectuer ces tâches de lift and shift en déplaçant de grands ensembles de données sous forme de fichiers, dans plusieurs tâches simultanées, maximisant ainsi le débit et minimisant le risque de transport. Voyons comment.<\/STRONG><\/P>Les entrepôts cloud comme Snowflake, Big Query et Redshift peuvent être interrogés et lus dans ArcGIS Pro 2.9, mais pas écrits avec les outils standards. Cependant, ArcGIS Data Interoperability peut écrire dans ces entrepôts, y compris les données spatiales, mais le mode par défaut est le streaming, qui pourrait ne pas évoluer comme vous le souhaitez. Je vais vous montrer un modèle que vous pouvez utiliser sur les trois entrepôts :<\/P>ETL des ensembles complets de données, y compris les données spatiales, au format Apache Parquet ou autre format comme CSVLa géométrie est encodée dans un format standard basé sur des caractères<\/LI><\/UL><\/LI>Automatiser l'ETL dans plusieurs processus simultanés<\/STRONG><\/LI>Ne pas écrire de code<\/STRONG> au-delà des commandes SQL ou macro requises par l'environnement cible<\/LI><\/UL>Je vais aussi faire un clin d'œil aux codeurs qui traînent dans mon espace blog sans code, voir ci-dessous 😉<\/span>, c'est-à-dire quelques astuces Python pour créer des fichiers Parquet (Note : les fichiers Parquet sont un type d'élément supporté dans ArcGIS Online depuis septembre 2021. Partagez-en !)
<\/span>Des milliards d'entités sont concernées par ce modèle mais j'utilise un ensemble de données plus modeste pour la démonstration, seulement 2,3 millions d'entités ponctuelles.<\/P>
2,3 millions d'entités ponctuelles<\/span><\/span><\/P>Mes données sont réparties en 12 classes d'entités, vous pouvez en avoir autant que vous voulez. Le modèle que je vais montrer fonctionne avec des données divisées en parties séparées pouvant être traitées simultanément. Si vos données sont monolithiques, divisez-les vous-même spatialement (un fishnet orienté ?) ou en ajoutant un champ signifiant un identifiant de lot rempli par position de ligne - vous pouvez supprimer ce champ pendant le traitement.<\/P>J'ai mentionné les entrepôts Snowflake, Big Query et Redshift. Dans tous les cas, vous pouvez mettre en scène des fichiers Parquet où l'environnement cible peut les voir puis charger à partir des fichiers Parquet. Pour les données spatiales, les fichiers Parquet devront avoir la géométrie encodée dans un format compris par l'environnement cible (Snowflake et Big Query supportent GeoJSON et WKT, Redshift supporte WKT). Je fournirai uniquement un exemple fonctionnel avec GeoJSON allant vers Snowflake. Mes données démo sont des géométries ponctuelles et le champ que j'utilise pour stocker le GeoJSON a une largeur de 100 ; si vous utilisez des données polylignes ou polygones, vous devriez étudier la largeur nécessaire pour vos entités les plus riches en points lors du codage du champ. Par exemple j'ai sélectionné un polygone très riche en points dans une couche et en GeoJSON il fait 2 071 156 caractères :<\/P> <\/P>with arcpy.da.SearchCursor('NZ Property Titles','shape@') as cursor:
for row in cursor:
print(len(str(row[0].__geo_interface__)))
2071156<\/code><\/pre><P> <\/P><P>Notez que Data Interoperability peut contrôler la précision décimale utilisée par GeoJSON ; pour les données géographiques une valeur de 7 est raisonnable, le même polygone utilise alors <SPAN>1 274 064 caractères. Par exemple la première coordonnée passe de <\/SPAN><SPAN>(172.90677540000001,-41.12752416699993) <\/SPAN><SPAN>à (<\/SPAN><SPAN>172.9067754,-41.1275242). N'oubliez pas que chaque octet compte !<\/SPAN><\/P><P><STRONG>Note :<\/STRONG> Pour Big Query, Data Interoperability dispose d'un transformateur hub <A title="GoogleBigQueryConnector" href="https:\/\/docs.safe.com\/fme\/html\/FME_Desktop_Documentation\/FME_Transformers\/Transformers\/googlebigqueryconnector-pkg.htm" target="_blank" rel="noopener nofollow noreferrer">GoogleBigQueryConnector</A> qui peut charger des CSV vers des tables. Cela peut être plus simple que d'envoyer du Parquet et d'utiliser l'environnement bq pour charger les données, je n'ai pas étudié ce scénario.<\/P><P>Penchons-nous sur mon flux de travail particulier. La sauce secrète est de créer <STRONG>deux<\/STRONG> outils ETL, le premier gère les tâches et appelle un transformateur <A title="WorkspaceRunner" href="https:\/\/docs.safe.com\/fme\/html\/FME_Desktop_Documentation\/FME_Transformers\/Transformers\/workspacerunner.htm" target="_blank" rel="noopener nofollow noreferrer">WorkspaceRunner</A> qui appelle le second outil, qui fait le travail. C'est très simple, voici <STRONG>LoadManager.fmw<\/STRONG>, il prend une liste d'arguments, dans mon cas des noms de classes d'entités dans une géodatabase :<\/P><P><span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="LoadManager" style="width: 400px;"><img src="https:\/\/us.v-cdn.net\/6038851\/uploads\/images\/23278i26931F24B6DB0008\/LoadManager.jpg" role="button" title="LoadManager.jpg" alt="LoadManager" \/><span class="lia-inline-image-caption" onclick="event.preventDefault();">LoadManager<\/span><\/span><\/P><P> WorkspaceRunner lance jusqu'à 7 processus FME qui exécutent un outil cible jusqu'à ce que la file d'attente soit vide. Le traitement est probablement limité par le CPU pendant que les processus travailleurs extraient, encodent et téléchargent les fichiers d'ensemble de données. J'ai permis à chaque processus d'exécuter deux tâches ce qui a consommé mes ensembles entrants en 6 processus.<\/P><P><span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="WorkspaceRunner" style="width: 400px;"><img src="https:\/\/us.v-cdn.net\/6038851\/uploads\/images\/23279i73187AEDC185DFB7\/WorkspaceRunner.jpg" role="button" title="WorkspaceRunner.jpg" alt="WorkspaceRunner" \/><span class="lia-inline-image-caption" onclick="event.preventDefault();">WorkspaceRunner<\/span><\/span><\/P><P> <\/P><P>Voici <STRONG>LoadWorkerParquet.fmw<\/STRONG>.<br \/></P><P><span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="LoadWorkerParquet" style="width: 400px;"><img src="https:\/\/us.v-cdn.net\/6038851\/uploads\\images\\23280i68720B57C8DAA789\\LoadWorkerParquet.jpg" role=\button\ title=\LoadWorkerParquet.jpg\ alt=\LoadWorkerParquet\ \/><span class=\lia-inline-image-caption\ onclick=\event.preventDefault();\>LoadWorkerParquet<\span></span></span></p>
<p>C'est aussi un outil simple, il lit la géodatabase, écrit un fichier parquet local puis envoie ce fichier parquet à Snowflake où les données sont copiées dans une table. Je vous laisse inspecter vous-mêmes SQLExecutor mais pour aider à comprendre, après substitution des variables voici à quoi ressemble une instruction (vers Snowflake) :<br \/></p>
<p> <br \/></p>
<pre class=\lia-code-sample language-sql\ ><code >create or replace file format Canterburyparquet_format
type = 'parquet';
create or replace temporary stage stageCanterbury
file_format = Canterburyparquet_format;
put file:\/C:\Work\Parquet\TitlesCanterbury.parquet @stageCanterbury;
copy into \INTEROPERABILITY\.PUBLIC\.Titles"
from (select
$1:id::number,
$1:title_no::varchar,
$1:status::varchar,
$1:type::varchar,
$1:land_district::varchar,
$1:issue_date::timestamp,
$1:guarantee_status::varchar,
$1:estate_description::varchar,
$1:number_owners::varchar,
$1:spatial_extents_shared::varchar,
to_geography($1:geom::varchar)
from @stageCanterbury);
Dès que le fichier parquet arrive chez Snowflake l'ingestion est ultra rapide. Au fait, j'ai appris pour faire cela en lisant l'aide, je ne suis pas un DBA Snowflake.<\/P>
Quelle performance devez-vous attendre ? Au moment de l'écriture, je suis coincé à la maison comme le reste d'entre nous mais sur mon WiFi domestique, je charge 2,3 millions de features dans Snowflake en 6 minutes, donc avec un ordinateur décent et un réseau filaire, je pense conservativement à 25 millions de features point par heure. Bien sûr, pour un environnement de production et un travail vraiment important, vous pourriez utiliser plusieurs ordinateurs, certainement les entrepôts cloud cibles évolueront pour prendre en charge le débit.<\/P>
Dans le téléchargement du blog, vous remarquerez que j'inclus un second outil worker, LoadWorker.fmw, c'était pour moi comparer la performance de la manière habituelle d'écrire dans Snowflake avec 100K features par transaction, c'était beaucoup plus lent.<\/P>
Maintenant de retour dans core Pro 2.9 mes données sont chargées dans Snowflake et je peux lancer des requêtes dessus et profiter de l'expérience de calcul évolutif.<\/P>
Snowflake in Catalog Pane<\/span><\/span><\/P>J'ai mentionné une option Python pour créer des fichiers parquet, elle est dans le téléchargement du blog mais la voici aussi :<\/P> <\/P># Exemple Pro 2.9+ création d'un fichier parquet à partir d'une classe d'entités
# La géométrie est encodée en GeoJSON dans un champ 'geom'
import arcpy
import pyarrow.parquet as pq
arcpy.env.overwriteOutput = True
# Classe d'entités source
Canterbury = r"C:\Work\Parquet\Parquet.gdb\Canterbury"
# Créer une classe d'entités en mémoire en WGS84
with arcpy.EnvManager(outputCoordinateSystem='GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]',
geographicTransformations="NZGD_2000_To_WGS_1984_1"):
arcpy.conversion.ExportFeatures(
in_features=Canterbury,
out_features=r"memory\Canterbury")
# Ajouter le champ geom (la géométrie non-point nécessitera un champ plus large)
Canterbury = arcpy.management.AddField("memory\Canterbury","geom","TEXT",None,None,100,'',"NULLABLE","NON_REQUIRED",'').getOutput(0)
# Dériver GeoJSON
with arcpy.da.UpdateCursor(Canterbury,['shape@','geom']) as cursor:
for row in cursor:
row[1] = str(row[0].__geo_interface__)
cursor.updateRow(row)
# Supprimer la géométrie en créant une Table
esriTable = arcpy.conversion.TableToTable(Canterbury,"memory","CanterburyTable").getOutput(0)
# Créer une table arrow
arrowTable = arcpy.da.TableToArrowTable(esriTable)
# Écrire parquet
pq.write_table(arrowTable,r'C:\Work\Parquet\TitlesCanterbury.parquet',
version='1.0',
compression='SNAPPY')<\/code><\/pre> <\/P>Amusez-vous à déplacer ces données à grande échelle !<\/P> <\/P>