Calcola la velocità media delle strade [chiuso]


20

Sono andato a un colloquio di lavoro di ingegnere di dati. L'intervistatore mi ha fatto una domanda. Mi ha dato qualche situazione e mi ha chiesto di progettare il flusso di dati per quel sistema. L'ho risolto ma non gli piaceva la mia soluzione e ho fallito. Vorrei sapere se hai idee migliori su come risolvere quella sfida.

La domanda era:

Il nostro sistema riceve quattro flussi di dati. I dati contengono un ID veicolo, la velocità e le coordinate di geolocalizzazione. Ogni veicolo invia i suoi dati una volta al minuto. Non esiste alcun collegamento tra un flusso specifico a una strada o veicolo specifico o qualsiasi altra cosa. C'è una funzione che accetta le coordinazioni e restituisce il nome di una sezione stradale. Dobbiamo conoscere la velocità media per tratto di strada per 5 minuti. Finalmente vogliamo scrivere i risultati su Kafka.

inserisci qui la descrizione dell'immagine

Quindi la mia soluzione era:

Prima scrivendo tutti i dati in un cluster Kafka, in un argomento, partizionato dalle prime 5-6 cifre della latitudine concatenate alle 5-6 prime cifre della longitudine. Quindi, leggendo i dati tramite Structured Streaming, aggiungendo per ogni riga il nome della sezione della strada in base alle coordinate (esiste un udf predefinito per quello), e quindi collegando i dati in base al nome della sezione della strada.

Poiché partiziono i dati in Kafka in base alle 5-6 prime cifre delle coordinate, dopo aver tradotto le coordinate nel nome della sezione, non è necessario trasferire molti dati nella partizione corretta e quindi posso sfruttare l'operazione colesce () ciò non innesca un shuffle completo.

Quindi calcolando la velocità media per esecutore.

L'intero processo avverrà ogni 5 minuti e scriveremo i dati in modalità Append sul sink finale di Kafka.

inserisci qui la descrizione dell'immagine

Quindi, ancora, l'intervista non mi è piaciuta la mia soluzione. Qualcuno potrebbe suggerire come migliorarlo o un'idea completamente diversa e migliore?


Non sarebbe meglio chiedere alla persona cosa non gli piaceva esattamente?
Gino Pane,

Penso che sia una cattiva idea partizionare dal latin concatenato. Il punto dati non verrà riportato per ciascuna corsia come coordinate leggermente diverse?
webber,

@webber quindi prendo solo poche cifre, quindi la posizione non sarà unica ma relativamente delle dimensioni di un tratto di strada.
Alon,

Risposte:


6

Ho trovato questa domanda molto interessante e ho pensato di provarci.

Come ho valutato ulteriormente, il tuo stesso tentativo è buono, tranne quanto segue:

partizionato dalle 5-6 prime cifre della latitudine concatenate alle 5-6 prime cifre della longitudine

Se disponi già di un metodo per ottenere l'ID / nome della sezione stradale in base a latitudine e longitudine, perché non chiamare prima quel metodo e utilizzare l'id / nome della sezione stradale per partizionare i dati?

E dopo, tutto è abbastanza semplice, quindi la topologia sarà

Merge all four streams ->
Select key as the road section id/name ->
Group the stream by Key -> 
Use time windowed aggregation for the given time ->
Materialize it to a store. 

(Una spiegazione più dettagliata può essere trovata nei commenti nel codice qui sotto. Si prega di chiedere se qualcosa non è chiaro)

Ho aggiunto il codice alla fine di questa risposta, si prega di notare che invece della media, ho usato la somma perché è più facile da dimostrare. È possibile fare la media memorizzando alcuni dati extra.

Ho dettagliato la risposta nei commenti. Di seguito è riportato un diagramma della topologia generato dal codice (grazie a https://zz85.github.io/kafka-streams-viz/ )

Topologia:

Diagramma della topologia

    import org.apache.kafka.common.serialization.Serdes;
    import org.apache.kafka.streams.KafkaStreams;
    import org.apache.kafka.streams.StreamsBuilder;
    import org.apache.kafka.streams.StreamsConfig;
    import org.apache.kafka.streams.Topology;
    import org.apache.kafka.streams.kstream.KStream;
    import org.apache.kafka.streams.kstream.Materialized;
    import org.apache.kafka.streams.kstream.TimeWindows;
    import org.apache.kafka.streams.state.Stores;
    import org.apache.kafka.streams.state.WindowBytesStoreSupplier;

    import java.util.Arrays;
    import java.util.List;
    import java.util.Properties;
    import java.util.concurrent.CountDownLatch;

    public class VehicleStream {
        // 5 minutes aggregation window
        private static final long AGGREGATION_WINDOW = 5 * 50 * 1000L;

        public static void main(String[] args) throws Exception {
            Properties properties = new Properties();

            // Setting configs, change accordingly
            properties.put(StreamsConfig.APPLICATION_ID_CONFIG, "vehicle.stream.app");
            properties.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092,kafka2:19092");
            properties.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
            properties.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());

            // initializing  a streambuilder for building topology.
            final StreamsBuilder builder = new StreamsBuilder();

            // Our initial 4 streams.
            List<String> streamInputTopics = Arrays.asList(
                    "vehicle.stream1", "vehicle.stream2",
                    "vehicle.stream3", "vehicle.stream4"
            );
            /*
             * Since there is no connection between a specific stream
             * to a specific road or vehicle or anything else,
             * we can take all four streams as a single stream
             */
            KStream<String, String> source = builder.stream(streamInputTopics);

            /*
             * The initial key is unimportant (which can be ignored),
             * Instead, we will be using the section name/id as key.
             * Data will contain comma separated values in following format.
             * VehicleId,Speed,Latitude,Longitude
             */
            WindowBytesStoreSupplier windowSpeedStore = Stores.persistentWindowStore(
                    "windowSpeedStore",
                    AGGREGATION_WINDOW,
                    2, 10, true
            );
            source
                    .peek((k, v) -> printValues("Initial", k, v))
                    // First, we rekey the stream based on the road section.
                    .selectKey(VehicleStream::selectKeyAsRoadSection)
                    .peek((k, v) -> printValues("After rekey", k, v))
                    .groupByKey()
                    .windowedBy(TimeWindows.of(AGGREGATION_WINDOW))
                    .aggregate(
                            () -> "0.0", // Initialize
                            /*
                             * I'm using summing here for the aggregation as that's easier.
                             * It can be converted to average by storing extra details on number of records, etc..
                             */
                            (k, v, previousSpeed) ->  // Aggregator (summing speed)
                                    String.valueOf(
                                            Double.parseDouble(previousSpeed) +
                                                    VehicleSpeed.getVehicleSpeed(v).speed
                                    ),
                            Materialized.as(windowSpeedStore)
                    );
            // generating the topology
            final Topology topology = builder.build();
            System.out.print(topology.describe());

            // constructing a streams client with the properties and topology
            final KafkaStreams streams = new KafkaStreams(topology, properties);
            final CountDownLatch latch = new CountDownLatch(1);

            // attaching shutdown handler
            Runtime.getRuntime().addShutdownHook(new Thread("streams-shutdown-hook") {
                @Override
                public void run() {
                    streams.close();
                    latch.countDown();
                }
            });
            try {
                streams.start();
                latch.await();
            } catch (Throwable e) {
                System.exit(1);
            }
            System.exit(0);
        }


        private static void printValues(String message, String key, Object value) {
            System.out.printf("===%s=== key: %s value: %s%n", message, key, value.toString());
        }

        private static String selectKeyAsRoadSection(String key, String speedValue) {
            // Would make more sense when it's the section id, rather than a name.
            return coordinateToRoadSection(
                    VehicleSpeed.getVehicleSpeed(speedValue).latitude,
                    VehicleSpeed.getVehicleSpeed(speedValue).longitude
            );
        }

        private static String coordinateToRoadSection(String latitude, String longitude) {
            // Dummy function
            return "Area 51";
        }

        public static class VehicleSpeed {
            public String vehicleId;
            public double speed;
            public String latitude;
            public String longitude;

            public static VehicleSpeed getVehicleSpeed(String data) {
                return new VehicleSpeed(data);
            }

            public VehicleSpeed(String data) {
                String[] dataArray = data.split(",");
                this.vehicleId = dataArray[0];
                this.speed = Double.parseDouble(dataArray[1]);
                this.latitude = dataArray[2];
                this.longitude = dataArray[3];
            }

            @Override
            public String toString() {
                return String.format("veh: %s, speed: %f, latlong : %s,%s", vehicleId, speed, latitude, longitude);
            }
        }
    }

La fusione di tutti i flussi non è una cattiva idea? Questo può diventare un collo di bottiglia per il flusso di dati. Cosa succede quando inizi a ricevere sempre più flussi di input man mano che il tuo sistema cresce? Sarà scalabile?
wypul

@wypul> non è una cattiva idea la fusione di tutti i flussi? -> Penso di no. Il parallelismo in Kafka non si ottiene attraverso flussi, ma attraverso partizioni (e attività), threading, ecc. I flussi sono un modo per raggruppare i dati. > Sarà scalabile? -> si. Dato che stiamo eseguendo la digitazione per sezioni di strada e supponendo che le sezioni di strada siano distribuite in modo equo, possiamo aumentare il numero di partizioni per questi argomenti per elaborare in parallelo il flusso in contenitori diversi. Possiamo usare un buon algoritmo di partizionamento basato sulla sezione stradale per distribuire il carico tra repliche.
Irshad PI,

1

Il problema in quanto tale sembra semplice e le soluzioni offerte hanno già molto senso. Mi chiedo se l'intervistatore fosse preoccupato per il design e le prestazioni della soluzione su cui ti sei concentrato o per l'accuratezza del risultato. Poiché altri si sono concentrati su codice, design e prestazioni, peserò sulla precisione.

Soluzione di streaming

Man mano che i dati scorrono, possiamo fornire una stima approssimativa della velocità media di una strada. Questa stima sarà utile per rilevare la congestione, ma sarà disattivata nel determinare il limite di velocità.

  1. Combina tutti e 4 i flussi di dati insieme.
  2. Crea una finestra di 5 minuti per acquisire i dati da tutti e 4 i flussi in 5 minuti.
  3. Applicare UDF sulle coordinate per ottenere il nome della via e il nome della città. I nomi delle strade sono spesso duplicati in tutte le città, quindi useremo city-name + street-name come chiave.
  4. Calcola la velocità media con una sintassi come -

    vehicle_street_speed
      .groupBy($"city_name_street_name")
      .agg(
        avg($"speed").as("avg_speed")
      )

5. write the result to the Kafka Topic

Soluzione batch

Questa stima sarà disattivata perché la dimensione del campione è piccola. Avremo bisogno di un'elaborazione batch su dati interi mese / trimestre / anno per determinare con precisione il limite di velocità.

  1. Leggi i dati di un anno dal data lake (o argomento Kafka)

  2. Applicare UDF sulle coordinate per ottenere il nome della via e il nome della città.

  3. Calcola la velocità media con una sintassi come -


    vehicle_street_speed
      .groupBy($"city_name_street_name")
      .agg(
        avg($"speed").as("avg_speed")
      )

  1. scrivere il risultato nel data lake.

Sulla base di questo limite di velocità più preciso possiamo prevedere il traffico lento nell'applicazione di streaming.


1

Vedo alcuni problemi con la tua strategia di partizionamento:

  • Quando dici che partizionerai i tuoi dati in base alle prime 5-6 cifre di lat long, non sarai in grado di determinare in anticipo il numero di partizioni kafka. Avrai dati distorti in quanto per alcuni tratti di strada osserverai un volume elevato rispetto ad altri.

  • E la tua combinazione di tasti non garantisce comunque gli stessi dati della sezione stradale nella stessa partizione e quindi non puoi essere sicuro che non ci saranno shuffle.

Le informazioni fornite dall'IMO non sono sufficienti per progettare l'intera pipeline di dati. Perché quando si progetta la pipeline, il modo in cui si partizionano i dati gioca un ruolo importante. Dovresti informarti di più sui dati che stai ricevendo come il numero di veicoli, la dimensione dei flussi di dati di input, il numero di flussi è fisso o può aumentare in futuro? I flussi di dati di input che stai ricevendo sono flussi kafka? Quanti dati ricevi in ​​5 minuti?

  • Ora supponiamo che tu abbia 4 flussi scritti su 4 argomenti in kafka o 4 partizioni e non hai una chiave specifica ma i tuoi dati sono partizionati in base a una chiave del data center o è partizionato con hash. In caso contrario, questo dovrebbe essere fatto sul lato dei dati piuttosto che de-duplicare i dati in un altro flusso e partizionamento kafka.
  • Se si stanno ricevendo i dati su diversi data center, è necessario portare i dati in un cluster e a tale scopo è possibile utilizzare Kafka mirror maker o qualcosa di simile.
  • Dopo aver raccolto tutti i dati su un cluster, è possibile eseguire un processo di streaming strutturato lì e con un intervallo di trigger di 5 minuti e una filigrana in base alle proprie esigenze.
  • Per calcolare la media ed evitare molti shuffle, puoi usare una combinazione di mapValuese reduceByKeyinvece di groupBy. Fare riferimento questo .
  • È possibile scrivere i dati sul kafka sink dopo l'elaborazione.

mapValues ​​e reduceByKey appartengono al RDD di basso livello. Catalyst non è abbastanza intelligente da generare il RDD più efficiente quando mi raggruppo e calcolo la media?
Alon

@Alon Catalyst sarà sicuramente in grado di capire il piano migliore per eseguire la tua query, ma se usi groupBy, i dati con la stessa chiave verranno prima mescolati alla stessa partizione e quindi applicheranno l'operazione aggregata su quello. mapValuese reduceByappartiene davvero a RDD di basso livello, ma funzionerà ancora meglio in questa situazione poiché prima caculerà aggregato per partizione e poi eseguirà lo shuffle.
wypul

0

I principali problemi che vedo con questa soluzione sono:

  • Le sezioni stradali che si trovano ai margini dei quadrati a 6 cifre della mappa avranno dati in più partizioni tematiche e avranno velocità medie multiple.
  • La dimensione dei dati di ingestione per le tue partizioni Kafka potrebbe essere sbilanciata (città vs deserto). Partizionare in base alle prime cifre dell'ID auto potrebbe essere una buona idea IMO.
  • Non sono sicuro di aver seguito la parte di coalescenza, ma sembra problematico.

Direi che la soluzione deve essere fatta: leggi dallo stream Kafka -> UDF -> groupby road section -> media -> scrivi nello stream Kafka.


0

Il mio design dipenderà

  1. Numero di strade
  2. Numero di veicoli
  3. Costo di calcolo della strada dalle coordinate

Se voglio ridimensionare per qualsiasi numero di conteggi, il design sarebbe simile a questo inserisci qui la descrizione dell'immagine

Interessi trasversali su questo disegno -

  1. Mantenere lo stato duraturo dei flussi di input (se l'input è kafka, possiamo memorizzare gli offset con Kafka o esternamente)
  2. Periodicamente gli stati del checkpoint al sistema esterno (preferisco usare le barriere del checkpoint asincrono in Flink )

Alcuni miglioramenti pratici possibili su questo design -

  1. Memorizzazione nella cache della funzione di mappatura della sezione stradale, se possibile, in base alle strade
  2. Gestione dei ping persi (in pratica non tutti i ping sono disponibili)
  3. Tenendo conto della curvatura della strada (rilevamento e altitudine in considerazione)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.