Datorramverk som Apache Spark har antagits i stor utsträckning för att bygga storskaliga dataapplikationer. För Uber står data i centrum för strategiskt beslutsfattande och produktutveckling. För att hjälpa oss att bättre utnyttja dessa data hanterar vi massiva installationer av Spark på våra globala tekniska kontor.
Som Spark gör datateknik mer lättillgänglig kräver rätt dimensionering av de resurser som tilldelas Spark-applikationer och optimering av den operativa effektiviteten i vår datainfrastruktur mer finkorniga insikter om dessa system, nämligen deras resursanvändningsmönster.
För att utvinna dessa mönster utan att ändra användarkod byggde vi JVM Profiler, en distribuerad profiler för att samla in prestanda- och resursanvändningsmått och servera dem för vidare analys, och vi har en öppen källkod. Även om den är byggd för Spark gör dess generiska implementering att den kan användas för alla JVM-baserade tjänster eller tillämpningar (Java Virtual Machine). Läs vidare för att lära dig hur Uber använder det här verktyget för att profilera våra Spark-applikationer och hur du kan använda det för dina egna system.
- Profileringsutmaningar
- Korrelera mätvärden över ett stort antal processer på applikationsnivå
- Göra insamling av mätvärden icke störande för godtycklig användarkod
- Introduktion av JVM Profiler
- Vad gör JVM Profiler?
- Typiska användningsfall
- Implementeringsdetaljer och utbyggbarhet
- Hur man utökar JVM Profiler för att skicka mätvärden via en egen rapportör
- Integration med Ubers datainfrastruktur
- Användning av JVM Profiler
- Använd profileraren för att profilera Spark-applikationen
- Exempel på metriska förfrågningar
- Resultat och nästa steg
Profileringsutmaningar
Dagligen stöder Uber tiotusentals applikationer som körs på tusentals maskiner. När vår tekniska stack växte insåg vi snabbt att vår befintliga lösning för prestandaprofilering och optimering inte skulle kunna uppfylla våra behov. I synnerhet ville vi ha möjlighet att:
Korrelera mätvärden över ett stort antal processer på applikationsnivå
I en distribuerad miljö körs flera Spark-applikationer på samma server och varje Spark-applikation har ett stort antal processer (t.ex. tusentals exekutorer) som körs på många servrar, vilket illustreras i figur 1:
Våra befintliga verktyg kunde endast övervaka mätvärden på servernivå och inte mäta mätvärden för enskilda applikationer. Vi behövde en lösning som kunde samla in mätvärden för varje process och korrelera dem mellan processer för varje program. Dessutom vet vi inte när dessa processer kommer att starta och hur lång tid de kommer att ta. För att kunna samla in mätvärden i den här miljön måste profileraren startas automatiskt med varje process.
Göra insamling av mätvärden icke störande för godtycklig användarkod
I sina nuvarande implementationer exporterar Spark- och Apache Hadoop-biblioteken inte prestandamätvärden, men det finns ofta situationer där vi behöver samla in dessa mätvärden utan att ändra användar- eller ramkod. Om vi till exempel upplever hög latenstid på en NameNode i HDFS (Hadoop Distributed File System) vill vi kontrollera den latenstid som observeras från varje Spark-program för att säkerställa att dessa problem inte har replikerats. Eftersom NameNode-klientkoderna är inbäddade i vårt Spark-bibliotek är det besvärligt att ändra källkoden för att lägga till detta specifika mått. För att hålla jämna steg med den ständiga tillväxten av vår datainfrastruktur måste vi kunna göra mätningar av alla program när som helst och utan att göra kodändringar. Dessutom skulle genomförandet av en mer icke-intrusiv process för insamling av mätvärden göra det möjligt för oss att dynamiskt injicera kod i Javametoder under laddningstiden.
Introduktion av JVM Profiler
För att ta itu med dessa två utmaningar byggde vi vår JVM Profiler och öppnade den med öppen källkod. Det finns några befintliga verktyg med öppen källkod, som Etsys statsd-jvm-profiler, som kan samla in mätvärden på individuell applikationsnivå, men de ger inte möjlighet att dynamiskt injicera kod i befintlig binär Java-kod för att samla in mätvärden. Inspirerade av några av dessa verktyg byggde vi vår profiler med ännu fler möjligheter, t.ex. godtycklig profilering av Java-metoder/argument.
Vad gör JVM Profiler?
JVM Profiler består av tre nyckelfunktioner som gör det enklare att samla in prestanda och mätvärden för resursanvändning, och sedan servera dessa mätvärden (t.ex. Apache Kafka) till andra system för vidare analys:
- En java-agent: Genom att införliva en Java-agent i vår profilerare kan användarna samla in olika mätvärden (t.ex. CPU- och minnesanvändning) och stack-traces för JVM-processer på ett distribuerat sätt.
- Avancerade profileringsmöjligheter: Vår JVM-profiler gör det möjligt att spåra godtyckliga Javametoder och argument i användarkoden utan att göra några egentliga kodändringar. Den här funktionen kan användas för att spåra HDFS NameNode RPC-anropslatens för Spark-applikationer och identifiera långsamma metodanrop. Den kan också spåra HDFS-filvägarna som varje Spark-program läser eller skriver för att identifiera heta filer för ytterligare optimering.
- Rapportering av dataanalyser: På Uber använder vi profileraren för att rapportera mätvärden till Kafka-ämnen och Apache Hive-tabeller, vilket gör dataanalys snabbare och enklare.
Typiska användningsfall
Vår JVM Profiler stöder en mängd olika användningsfall, framför allt gör den det möjligt att instrumentera godtycklig Javakod. Med hjälp av en enkel konfigurationsändring kan JVM Profiler kopplas till varje exekutor i en Spark-applikation och samla in körtidsmått för Javametoder. Nedan berör vi några av dessa användningsområden:
- Rätt storlek på utföraren: Vi använder minnesmätningar från JVM Profiler för att spåra den faktiska minnesanvändningen för varje exekutor så att vi kan ställa in rätt värde för Spark-argumentet “executor-memory”.
- Övervaka HDFS NameNode RPC-latens: Vi profilerar metoder i klassen org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolTranslatorPB i en Spark-applikation och identifierar långa väntetider vid NameNode-anrop. Vi övervakar mer än 50 000 Spark-applikationer varje dag med flera miljarder sådana RPC-samtal.
- Övervaka händelser där drivrutinen tappas: Vi profilerar metoder som org.apache.spark.scheduler.LiveListenerBus.onDropEvent för att spåra situationer där Spark-drivrutinens händelsekö blir för lång och tappar händelser.
- Spåra dataledning: Vi profilerar filvägargument på metoden org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolTranslatorPB.getBlockLocations och org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolTranslatorPB.addBlock för att spåra vilka filer som läses och skrivs av Spark-programmet.
Implementeringsdetaljer och utbyggbarhet
För att göra implementeringen så smidig som möjligt har JVM Profiler en mycket enkel och utbyggbar design. Människor kan enkelt lägga till ytterligare profilerimplementationer för att samla in fler mätvärden och även distribuera sina egna anpassade rapportörer för att skicka mätvärden till olika system för dataanalys.
Koden för JVM Profiler laddas in i en Java-process via ett Java-agentargument när processen startar. Den består av tre huvuddelar:
- Class File Transformer: instrumenterar bytecode för Java-metoder inne i processen för att profilera godtycklig användarkod och spara mätvärden i en intern mätbuffert.
- Metric Profilers
- CPU/Memory Profiler: samlar in mätvärden för CPU/Memory-användning via JMX och skickar dem till rapportörerna.
- Method Duration Profiler: läser mätvärden för metodens varaktighet (latens) från mätbufferten och skickar dem till rapportörerna.
- Method Argument Profiler: läser metodargumentvärden från mätbufferten och skickar dem till rapportörerna.
- Rapporterare
- Konsolrapportör: skriver mätvärden i konsolutgången.
- Kafka Reporter: skickar mätvärden till Kafka-ämnen.
Hur man utökar JVM Profiler för att skicka mätvärden via en egen rapportör
Användare kan implementera sina egna rapportörer och ange dem med -javaagent-alternativet, till exempel:
java
-javaagent:jvm-profiler-0.0.5.jar=reporter=com.uber.profiling.reporters.CustomReporter
Integration med Ubers datainfrastruktur
Vi har integrerat vår JVM Profiler-metriker med Ubers interna datainfrastruktur för att möjliggöra:
- Dataanalys för hela klustret: Metrikerna matas först till Kafka och lagras i HDFS, varefter användarna gör sökningar med Hive/Presto/Spark.
- Debuggning av Spark-applikationer i realtid: Vi använder Flink för att sammanställa data för en enskild applikation i realtid och skriver till vår MySQL-databas, varefter användarna kan se mätvärdena via ett webbaserat gränssnitt.
Användning av JVM Profiler
Nedan ger vi instruktioner för hur man använder vår JVM Profiler för att spåra en enkel Java-applikation:
Först klonar vi projektet med git:
Vi bygger projektet genom att köra följande kommando:
Nästan kallar vi JAR-filen med byggresultatet (t.ex. target/jvm-profiler-0.0.5.jar) och kör applikationen i JVM Profiler med hjälp av följande kommando:
Kommandot kör Java-applikationen och rapporterar prestanda och resursanvändning till utdatakonsolen. Till exempel:
Profilern kan också skicka mätvärden till ett Kafka-ämne via ett kommando som följande:
Använd profileraren för att profilera Spark-applikationen
Nu går vi igenom hur man kör profileraren med Spark-applikationen.
Antagen att vi redan har ett HDFS-kluster laddar vi upp JAR-filen JVM Profiler till vår HDFS:
Därefter använder vi kommandoraden spark-submit för att starta Spark-applikationen med profileraren:
Exempel på metriska förfrågningar
På Uber skickar vi våra mätvärden till Kafka-ämnen och programmerar bakgrundsdatapipelines för att automatiskt lägga in dem i Hive-tabeller. Användare kan konfigurera liknande pipelines och använda SQL för att fråga efter profileringsmetriker. De kan också skriva sina egna rapportörer för att skicka mätvärdena till en SQL-databas som MySQL och fråga därifrån. Nedan finns ett exempel på ett schema för en Hive-tabell:
Nedan erbjuder vi ett exempelresultat när vi körde en tidigare SQL-fråga, som visar minnes- och CPU-metrikerna för varje process för Spark-utförarna:
role | processUuid | maxHeapMemoryMB | avgProcessCpuLoad |
executor | 6d3aa4ee-4233-4cc7-a628-657e1a87825d | 2805.255325 | 7.61E-11 |
exekutor | 21f418d1-6d4f-440d-b28a-4eba1a3bb53d | 3993.969582 | 9.56E-11 |
exekutor | a5da6e8c-149b-4722-8fa8-74a16baabcf8 | 3154.484474 | 8.18E-11 |
exekutor | a1994e27-59bd-4cda-9de3-745ead954a27 | 2561.847374 | 8.58E-11 |
Resultat och nästa steg
Vi tillämpade JVM Profiler på en av Ubers största Spark-applikationer (som använder mer än 1 000 exekutorer) och i processen minskade minnesallokeringen för varje exekutor med 2 GB, vilket innebar att vi gick från 7 GB till 5 GB. Bara för den här Spark-applikationen sparade vi 2 TB minne.
Vi tillämpade också JVM Profiler på alla Hive on Spark-applikationer inom Uber och fann några möjligheter att förbättra effektiviteten i minnesanvändningen. Figur 3 nedan visar ett resultat som vi fann: cirka 70 procent av våra applikationer använde mindre än 80 procent av det tilldelade minnet. Våra resultat visade att vi kunde allokera mindre minne för de flesta av dessa applikationer och öka minnesanvändningen med 20 procent.
När vi fortsätter att utveckla vår lösning ser vi fram emot ytterligare minnesminskningar i alla våra JVM:er.
JVM Profiler är ett fristående projekt med öppen källkod och vi välkomnar andra utvecklare att använda verktyget och bidra (t.ex. skicka in pull requests) också!
Vårt Big Data Engineering-team i Seattle, Palo Alto och San Francisco arbetar med verktyg och system för att utöka hela Hadoop-ekosystemet, inklusive HDFS, Hive, Presto och Spark. Vi bygger teknik ovanpå denna familj av programvara med öppen källkod för att hjälpa oss att fatta bättre, datadrivna affärsbeslut. Om det här låter lockande för dig, kolla in våra jobbmöjligheter och överväg att gå med i vårt team!
Photo Credit Header: Seahorse, Roatan, Honduras av Conor Myhrvold.
Prenumerera på vårt nyhetsbrev för att hålla dig uppdaterad om de senaste innovationerna från Uber Engineering.