Spark schemaEvolution i praktiken

KÀra lÀsare, god dag!

I denna artikel beskriver en ledande konsult inom affÀrsomrÄdet Big Data Solutions pÄ företaget "Neoflex" noggrant alternativ för att bygga variabla strukturvitriner med hjÀlp av Apache Spark.

Inom ramen för ett dataanalysprojekt uppstÄr ofta uppgiften att bygga vitriner baserat pÄ svagt strukturerade data.

Oftast handlar det om loggar, eller svar frÄn olika system, som sparas i JSON- eller XML-format. Data exporteras till Hadoop, och dÀrifrÄn behöver vi bygga en vitrin. Vi kan organisera Ätkomsten till den skapade vitrinen, till exempel via Impala.

I det hÀr fallet Àr schemat för den mÄlinriktade vitrinen preliminÀrt okÀnt. Dessutom kan schemat inte faststÀllas i förvÀg, eftersom det beror pÄ data och vi hanterar dessa svagt strukturerade data.

Till exempel, idag loggas följande svar:

{source: "app1", error_code: ""}

och nÀsta dag kommer ett sÄdant svar frÄn samma system:

{source: "app1", error_code: "error", description: "NĂ€tverksfel"}

Som ett resultat ska ett ytterligare fÀlt - description - lÀggas till vitrinen, och ingen vet om det kommer eller inte.

Uppgiften att skapa en vitrin med sÄdana data Àr ganska standard, och Spark har flera verktyg för detta. För att parsa kÀlldata stöds bÄde JSON och XML, och för okÀnt schema finns stöd för schemaEvolution.

Vid första anblicken verkar lösningen enkel. Man behöver bara ta en mapp med JSON och lÀsa in i en dataframe. Spark kommer att skapa ett schema, och inbÀddade data kommer att omvandlas till strukturer. DÀrefter behöver allt sparas i parquet, som Àven stöds av Impala, genom att registrera vitrinen i Hive metastore.

Det verkar vara allt enkelt.

Men av de korta exemplen i dokumentationen framgÄr det inte hur man ska hantera flera problem i praktiken.

I dokumentationen beskrivs tillvÀgagÄngssÀttet inte för att skapa en vitrin, utan för att lÀsa JSON eller XML i en dataframe.

Specifikt visas det bara hur man lÀser och parserar JSON:

df = spark.read.json(path...)

Detta Àr tillrÀckligt för att göra data Ätkomliga för Spark.

I praktiken Àr scenariot mycket mer komplicerat Àn att bara lÀsa JSON-filer frÄn en mapp och skapa en dataframe. Situationen ser ut sÄ hÀr: det finns redan en viss vitrin, nya data kommer varje dag, och de mÄste lÀggas till i vitrinen utan att glömma att schemat kan variera.

Den vanliga strukturen för att bygga en vitrin Àr följande:

Step 1. Data laddas upp i Hadoop med en efterföljande daglig uppdatering och placeras i en ny partition. Det blir en partitionerad mapp med kÀlldata per dag.

Step 2. Under den initierande laddningen lÀses och parseas denna mapp med hjÀlp av Spark. Den erhÄllna dataframe sparas i ett format som Àr tillgÀngligt för analys, till exempel i parquet, vilket sedan kan importeras till Impala. SÄ skapas en mÄlvitrine med alla data som har samlats vid detta tillfÀlle.

Step 3. En laddning skapas som kommer att uppdatera vitrin varje dag.
FrÄgan om inkrementell laddning uppstÄr, behovet av att partitionera vitrin och frÄgan om att stödja den övergripande schemat för vitrin.

LÄt oss ta ett exempel. Antag att det första steget av byggandet av lagret har genomförts och export av JSON-filer till mappen har konfigurerats.

Att skapa en dataframe frÄn dem för att sedan spara som vitrin Àr inget problem. Det Àr just det första steget som lÀtt kan hittas i Spark-dokumentationen:

df = spark.read.option("mergeSchema", True).json(".../*") 
df.printSchema()

root 
|-- a: long (nullable = true) 
|-- b: string (nullable = true) 
|-- c: struct (nullable = true) |    
|-- d: long (nullable = true)

Allt verkar bra.

Vi har lÀst och parseat JSON, och nu sparar vi dataframe som parquet, registrerar den i Hive pÄ ett valfritt bekvÀmt sÀtt:

df.write.format(“parquet”).option('path','').saveAsTable('')

Vi fÄr vitrin.

Men, nÀsta dag kom nya data frÄn kÀllan. Vi har en mapp med JSON och en vitrin som skapades baserat pÄ denna mapp. Efter att ha laddat nÀsta omgÄng data frÄn kÀllan saknas det information för en dag i vitrin.

En logisk lösning vore att partitionera vitrin efter dagar, vilket skulle tillÄta oss att varje nÀsta dag lÀgga till en ny partition. Denna mekanism Àr ocksÄ vÀlkÀnd, Spark tillÄter att skriva partitioner separat.

Först gör vi en initierande laddning, och sparar data som beskrivits ovan, bara med partitioneringen tillagd. Denna ÄtgÀrd kallas initiering av vitrin och görs endast en gÄng:

df.write.partitionBy("date_load").mode("overwrite").parquet(dbpath + "/" + db + "/" + destTable)

NĂ€sta dag laddar vi bara den nya partitionen:

df.coalesce(1).write.mode("overwrite").parquet(dbpath + "/" + db + "/" + destTable +"/date_load=" + date_load + "/")

Det ÄterstÄr bara att registrera om i Hive för att uppdatera schemat.
Men hÀr uppstÄr problemen.

Det första problemet. Förr eller senare kommer den resulterande parquet-filen att bli olÀslig. Detta beror pÄ hur parquet och JSON hanterar tomma fÀlt pÄ olika sÀtt.

LÄt oss övervÀga en typisk situation. Till exempel, igÄr kom JSON:

Dag 1: {"a": {"b": 1}},

och idag ser samma JSON ut sÄ hÀr:

Dag 2: {"a": null}

Anta att vi har tvÄ olika partitioner, dÀr varje partition innehÄller en rad.
NÀr vi lÀser in de ursprungliga data fullstÀndigt, kommer Spark att kunna avgöra typen och förstÄ att "a" Àr ett fÀlt av typ "struktur" med ett underliggande fÀlt "b" av typ INT. Men om varje partition har sparats separat, sÄ blir resultatet en parquet med inkompatibla partitioneringsscheman:

df1 (a: <struct>)
df2 (a: STRING NULLABLE)

Denna situation Ă€r vĂ€lkĂ€nd, dĂ€rför har det lagts till en speciell option – att ta bort tomma fĂ€lt vid parsering av de ursprungliga data:

df = spark.read.json("...", dropFieldIfAllNull=True)

I sÄ fall kommer parquet att bestÄ av partitioner som kan lÀsas tillsammans.
Men de som har gjort detta i praktiken kommer att dra pÄ munnen. Varför? För det kommer sannolikt att dyka upp ytterligare tvÄ situationer. Eller tre. Eller fyra. Den första, som nÀstan garanterat kommer att intrÀffa, Àr att numeriska typer kan se olika ut i olika JSON-filer. Till exempel, {intField: 1} och {intField: 1.1}. Om sÄdana fÀlt hamnar i samma partition, kommer sammanslagningen av scheman att lÀsa allt korrekt och ge den mest exakta typen. Men om de befinner sig i olika partitioner, kommer den ena att ha intField: int, medan den andra fÄr intField: double.

För att hantera denna situation finns följande flagga:

df = spark.read.json("...", dropFieldIfAllNull=True, primitivesAsString=True)

Nu har vi en mapp dÀr partitionerna ligger, som kan lÀsas in i en enhetlig dataram och en giltig parquet för hela vitrin. Eller?

Vi mÄste komma ihÄg att vi registrerade tabellen i Hive. Hive Àr inte skiftlÀgeskÀnsligt nÀr det gÀller fÀltnamn, medan parquet Àr det. DÀrför Àr partitioner med scheman: field1: int, och Field1: int identiska för Hive, men inte för Spark. Kom ihÄg att konvertera fÀltnamn till gemener.

Efter detta verkar allt bra.

Men, det Àr inte sÄ enkelt. Det uppstÄr ett andra, ocksÄ vÀlkÀnt problem. Eftersom varje ny partition sparas separat, kommer det att finnas systemfiler för Spark i partitionsmappen, till exempel flaggan för operationens framgÄng _SUCCESS. Detta kommer att leda till ett fel vid försök att lÀsa parquet. För att undvika detta mÄste vi konfigurera sÄ att Spark inte lÀgger till systemfiler i mappen:

hadoopConf = sc._jsc.hadoopConfiguration()
hadoopConf.set("parquet.enable.summary-metadata", "false")
hadoopConf.set("mapreduce.fileoutputcommitter.marksuccessfuljobs", "false")

Det verkar som att en ny parquet-partition med parsade data för dagen nu lÀggs till i mÄlmappen varje dag. Vi har redan tÀnkt pÄ att undvika partitioner med datatypkonflikter.

Men vi stÄr nu inför det tredje problemet. Den övergripande schemat Àr okÀnd, dessutom har Hive-tabellen en felaktig schema eftersom varje ny partition förmodligen har orsakat en förvrÀngning i schemat.

Vi behöver registrera om tabellen. Detta kan göras enkelt: lÀs om parquet-vitrinen, ta schemat och skapa en DDL baserat pÄ det, för att registrera mappen i Hive som en extern tabell och uppdatera mÄlvinets schema.

Det fjĂ€rde problemet uppstĂ„r. NĂ€r vi registrerade tabellen första gĂ„ngen, baserade vi oss pĂ„ Spark. Nu gör vi det sjĂ€lva, och vi mĂ„ste komma ihĂ„g att parquet-fĂ€lt kan börja med ogiltiga tecken för Hive. Till exempel, Spark utesluter strĂ€ngar som det inte kunde parsa i fĂ€ltet „corrupt_record”. Ett sĂ„dant fĂ€lt kan inte registreras i Hive utan att maskera.

Med detta i Ätanke fÄr vi schemat:

f_def = ""
for f in pf.dtypes:
  if f[0] != "date_load":
    f_def = f_def + "," + f[0].replace("_corrupt_record", "`_corrupt_record`") + " " + f[1].replace(":", "`:").replace("<", "<`").replace(",", ",`").replace("array<`", "array<") 
table_define = "CREATE EXTERNAL TABLE jsonevolvtable (" + f_def[1:] + " ) "
table_define = table_define + "PARTITIONED BY (date_load string) STORED AS PARQUET LOCATION '\/user\/admin\/testJson\/testSchemaEvolution\/pq\/'"
hc.sql("drop table if exists jsonevolvtable")
hc.sql(table_define)

Kod ("_corrupt_record", "`_corrupt_record`") + " " + f[1].replace(":", "`:").replace("<", "<`").replace(",", ",`").replace("array<`", "array<") skapar en sÀker DDL, det vill sÀga istÀllet för:

create table tname (_field1 string, 1field string)

Med sĂ„dana fĂ€ltnamn som „_field1, 1field” skapas en sĂ€ker DDL dĂ€r fĂ€ltnamn maskeras: create table `tname` (`_field1` string, `1field` string).

FrÄgan uppstÄr: hur fÄr man korrekt en dataframe med fullstÀndigt schema (i koden pf)? Hur fÄr man denna pf? Det Àr det femte problemet. Ska man lÀsa schemat för alla partitioner frÄn mappen med parquet-filer i mÄlvitrinen? Det Àr den sÀkraste metoden, men den Àr tung.

Schemat finns redan i Hive. För att fÄ en ny schemalÀggning kan man kombinera schemat för hela tabellen med den nya partitionen. Det innebÀr att man behöver ta schema frÄn Hive och kombinera det med schemat för den nya partitionen. Detta kan göras genom att lÀsa in metadata frÄn Hive, spara dem i en tillfÀllig mapp och lÀsa in bÄda partitionerna samtidigt med Spark.

I grund och botten har vi allt som behövs: den ursprungliga tabellens schema i Hive och den nya partitionen. Vi har ocksÄ data. Det kvarstÄr bara att fÄ det nya schemat, dÀr schemat för de presenterade och de nya fÀlten frÄn den skapade partitionen kombineras:

from pyspark.sql import HiveContext
from pyspark.sql.functions import lit
hc = HiveContext(spark)
df = spark.read.json("...", dropFieldIfAllNull=True)
df.write.mode("overwrite").parquet(".../date_load=12-12-2019")
pe = hc.sql("select * from jsonevolvtable limit 1")
pe.write.mode("overwrite").parquet(".../fakePartiton/")
pf = spark.read.option("mergeSchema", True).parquet(".../date_load=12-12-2019/*", ".../fakePartiton/*")

NÀsta steg Àr att skapa DDL-registreringen av tabellen, precis som i det föregÄende fragmentet.
Om hela kedjan fungerar korrekt, det vill sÀga en initial laddning har gjorts och tabellen i Hive Àr korrekt skapad, fÄr vi den uppdaterade tabellens schema.

Och slutligen, problemet Àr att man inte sÄ enkelt kan lÀgga till en partition i en Hive-tabell eftersom den kommer att bli trasig. Det Àr nödvÀndigt att fÄ Hive att rÀtta till sin partitioneringsstruktur:

from pyspark.sql import HiveContext
hc = HiveContext(spark) 
hc.sql("MSCK REPAIR TABLE " + db + "." + destTable)

En enkel uppgift att lĂ€sa JSON och skapa en presentation av det leder till att man mĂ„ste övervinna flera otydliga svĂ„righeter, vars lösningar fĂ„r sökas separat. Även om dessa lösningar Ă€r enkla tar det lĂ„ng tid att hitta dem.

För att genomföra uppbyggnaden av presentationen var man tvungen att:

  • LĂ€gga till partitioner i presentationen, utan de interna filerna
  • FörstĂ„ de tomma fĂ€lten i de ursprungliga data som Spark typade
  • Konvertera enkla typer till strĂ€ngar
  • Tidigare fĂ€ltnamn till gemener
  • Dela upp dataladdningen och registreringen av tabellen i Hive (skapa DDL)
  • Inte glömma att maskera fĂ€ltnamn som kan vara oförenliga med Hive
  • LĂ€ra sig att uppdatera registreringen av tabellen i Hive

Sammanfattningsvis Àr det vÀrt att notera att lösningen för att bygga presentationer döljer mÄnga fallgropar. DÀrför Àr det bÀttre att vÀnda sig till en erfaren partner med framgÄngsrik expertis om man stöter pÄ svÄrigheter vid genomförandet.

Tack för att du lÀste denna artikel, vi hoppas att informationen Àr anvÀndbar.

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster