คำนวณความเร็วเฉลี่ยของถนน [ปิด]


20

ฉันไปสัมภาษณ์วิศวกรข้อมูล ผู้สัมภาษณ์ถามคำถามฉัน เขาให้สถานการณ์ฉันและขอให้ฉันออกแบบการไหลของข้อมูลสำหรับระบบนั้น ฉันแก้ไขมันได้ แต่เขาไม่ชอบวิธีแก้ปัญหาของฉันและฉันล้มเหลว ฉันอยากจะรู้ว่าถ้าคุณมีความคิดที่ดีกว่าวิธีการแก้ปัญหาที่ท้าทาย

คำถามคือ:

ระบบของเราได้รับข้อมูลสี่สตรีม ข้อมูลมีรหัสยานพาหนะความเร็วและการประสานตำแหน่งทางภูมิศาสตร์ ทุก vihicle ส่งข้อมูลของมันหนึ่งครั้งต่อนาที ไม่มีการเชื่อมต่อระหว่างสตรีมที่เฉพาะเจาะจงกับถนนหรือถนนหรือสิ่งอื่นใด มีฟังก์ชั่นที่ยอมรับการเรียงลำดับและส่งคืนชื่อส่วนถนน เราจำเป็นต้องรู้ความเร็วของการละควาต่อส่วนถนนต่อ 5 นาที ในที่สุดเราต้องการเขียนผลลัพธ์ไปยังคาฟคา

ป้อนคำอธิบายรูปภาพที่นี่

ดังนั้นทางออกของฉันคือ:

ขั้นแรกให้เขียนข้อมูลทั้งหมดลงในคลัสเตอร์คาฟคาลงในหัวข้อเดียวโดยแบ่งเป็นละติจูดแรก 5-6 หลักของละติจูดที่ต่อกันกับละติจูดแรก 5-6 หลัก จากนั้นอ่านข้อมูลโดยการสตรีมมิ่งแบบมีโครงสร้างเพิ่มชื่อแถวส่วนถนนโดยการเรียงลำดับ (มี udf ที่กำหนดไว้ล่วงหน้าสำหรับแต่ละแถว) จากนั้นทำการจัดเรียงข้อมูลตามชื่อส่วนถนน

เนื่องจากฉันแบ่งพาร์ติชันข้อมูลในคาฟคาโดยการเรียงตัวที่ 5-6 หลักหลังจากแปลการเรียงลำดับเป็นชื่อส่วนแล้วไม่จำเป็นต้องถ่ายโอนข้อมูลจำนวนมากไปยังพาร์ติชันที่ถูกต้องและดังนั้นฉันจึงสามารถใช้ประโยชน์จากการดำเนินการ colesce () ที่ไม่ก่อให้เกิดการสลับแบบเต็ม

จากนั้นคำนวณความเร็วเฉลี่ยต่อผู้ปฏิบัติการ

กระบวนการทั้งหมดจะเกิดขึ้นทุก 5 นาทีและเราจะเขียนข้อมูลในโหมดผนวกไปยังอ่างล้างจานคาฟคาสุดท้าย

ป้อนคำอธิบายรูปภาพที่นี่

ดังนั้นอีกครั้งผู้สัมภาษณ์ไม่ชอบวิธีแก้ไขของฉัน ใครช่วยแนะนำวิธีการปรับปรุงหรือความคิดที่แตกต่างอย่างสมบูรณ์และดีกว่า?


จะดีกว่าไหมถ้าถามคนที่เขาไม่ชอบ
Gino Pane

ฉันคิดว่ามันเป็นความคิดที่ดีที่จะแบ่งพาร์ติชันด้วย lat-long แบบเรียงต่อกัน จะไม่มีการรายงานจุดข้อมูลสำหรับแต่ละเลนในฐานะพิกัดที่แตกต่างกันเล็กน้อยหรือไม่
เว็บเบอร์

@webber ดังนั้นฉันจึงใช้เวลาเพียงไม่กี่หลักดังนั้นตำแหน่งจะไม่ซ้ำกัน แต่มีขนาดของส่วนถนน
Alon

คำตอบ:


6

ฉันพบคำถามนี้น่าสนใจมากและคิดว่าจะลองทำดู

เมื่อฉันประเมินเพิ่มเติมความพยายามของคุณเองก็ดียกเว้นต่อไปนี้:

แบ่งโดยละติจูด 5-6 ตัวแรกของละติจูดที่ต่อกันกับหลักแรกที่ 5-6 ของลองจิจูด

หากคุณมีวิธีในการรับ id / ชื่อส่วนถนนตามละติจูดและลองจิจูดทำไมไม่เรียกใช้วิธีนั้นก่อนและใช้ id / ชื่อส่วนถนนเพื่อแบ่งพาร์ติชันข้อมูลตั้งแต่แรก?

และหลังจากนั้นทุกอย่างค่อนข้างง่ายดังนั้นโทโพโลยีจะเป็น

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. 

(สามารถอ่านคำอธิบายโดยละเอียดเพิ่มเติมได้ในความคิดเห็นในรหัสด้านล่างโปรดถามว่ามีอะไรที่ไม่ชัดเจน)

ฉันได้เพิ่มรหัสในตอนท้ายของคำตอบนี้โปรดทราบว่าแทนที่จะใช้ค่าเฉลี่ยฉันใช้ผลรวมเพื่อง่ายต่อการสาธิต เป็นไปได้ที่จะทำโดยเฉลี่ยโดยจัดเก็บข้อมูลพิเศษบางอย่าง

ฉันให้รายละเอียดคำตอบในความคิดเห็น ต่อไปนี้เป็นแผนภาพโทโพโลยีที่สร้างขึ้นจากรหัส (ขอบคุณhttps://zz85.github.io/kafka-streams-viz/ )

โทโพโลยี:

แผนภาพโทโพโลยี

    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);
            }
        }
    }

การรวมสตรีมทั้งหมดเป็นความคิดที่ไม่ดีใช่หรือไม่ สิ่งนี้อาจกลายเป็นคอขวดสำหรับการรับส่งข้อมูลของคุณ จะเกิดอะไรขึ้นเมื่อคุณเริ่มรับกระแสอินพุตมากขึ้นเมื่อระบบของคุณเติบโต สิ่งนี้จะปรับขนาดได้หรือไม่
wypul

@wypul> ไม่ได้รวมความคิดทั้งหมดเข้าด้วยกัน -> ฉันคิดว่าไม่ Parallelism ใน Kafka นั้นไม่สามารถทำได้ผ่านการสตรีม แต่ผ่านทางพาร์ติชั่น (และภารกิจ) การทำเกลียว ฯลฯ สตรีมเป็นวิธีการจัดกลุ่มข้อมูล > สิ่งนี้จะปรับขนาดได้หรือไม่ -> ใช่ เนื่องจากเราป้อนข้อมูลตามส่วนถนนและสมมติว่ามีการกระจายส่วนถนนอย่างเป็นธรรมเราสามารถเพิ่มจำนวนพาร์ติชันสำหรับหัวข้อเหล่านี้เพื่อประมวลผลสตรีมในคอนเทนเนอร์ที่แตกต่างกัน เราสามารถใช้อัลกอริทึมการแบ่งพาร์ติชันที่ดีตามส่วนของถนนเพื่อแจกจ่ายโหลดข้ามแบบจำลอง
Irshad PI

1

ปัญหาดังกล่าวดูเรียบง่ายและวิธีแก้ปัญหาที่นำเสนอนั้นสมเหตุสมผลมาก ฉันสงสัยว่าผู้สัมภาษณ์กังวลเกี่ยวกับการออกแบบและประสิทธิภาพของการแก้ปัญหาที่คุณมุ่งเน้นหรือความถูกต้องของผลลัพธ์หรือไม่ เนื่องจากคนอื่น ๆ ให้ความสำคัญกับรหัสการออกแบบและประสิทธิภาพฉันจึงต้องคำนึงถึงความแม่นยำ

สตรีมมิ่งโซลูชั่น

เมื่อข้อมูลไหลเข้าเราสามารถประมาณคร่าว ๆ ของความเร็วเฉลี่ยของถนน การประมาณค่านี้จะมีประโยชน์ในการตรวจจับความแออัด แต่จะปิดในการกำหนดขีด จำกัด ความเร็ว

  1. รวมข้อมูลทั้ง 4 สตรีมเข้าด้วยกัน
  2. สร้างหน้าต่าง 5 นาทีเพื่อบันทึกข้อมูลจากสตรีมทั้งหมด 4 รายการใน 5 นาที
  3. ใช้ UDF บนพิกัดเพื่อรับชื่อถนนและชื่อเมือง ชื่อถนนมักจะซ้ำกันทั่วเมืองดังนั้นเราจะใช้ชื่อเมือง + ชื่อถนนเป็นรหัส
  4. คำนวณความเร็วเฉลี่ยด้วยไวยากรณ์เช่น -

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

5. write the result to the Kafka Topic

โซลูชันแบบแบตช์

การประมาณนี้จะถูกปิดเนื่องจากขนาดตัวอย่างมีขนาดเล็ก เราจะต้องทำการประมวลผลแบบแบตช์ในข้อมูลทั้งเดือน / ไตรมาส / ปีเพื่อกำหนดขีด จำกัด ความเร็วให้แม่นยำยิ่งขึ้น

  1. อ่านข้อมูลหนึ่งปีจาก data lake (หรือหัวข้อ Kafka)

  2. ใช้ UDF บนพิกัดเพื่อรับชื่อถนนและชื่อเมือง

  3. คำนวณความเร็วเฉลี่ยด้วยไวยากรณ์เช่น -


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

  1. เขียนผลลัพธ์ไปยัง data lake

จากการ จำกัด ความเร็วที่แม่นยำยิ่งขึ้นนี้เราสามารถทำนายปริมาณการใช้งานที่ช้าในแอพพลิเคชั่นสตรีมมิ่ง


1

ฉันเห็นปัญหาเล็กน้อยเกี่ยวกับกลยุทธ์การแบ่งพาร์ติชันของคุณ:

  • เมื่อคุณบอกว่าคุณกำลังจะแบ่งพาร์ติชั่นข้อมูลของคุณโดยใช้ความยาว 5-6 ตัวแรกของ lat lat คุณจะไม่สามารถกำหนดจำนวนพาร์ติชั่นคาฟคาล่วงหน้าได้ คุณจะมีข้อมูลที่เบ้เหมือนกับบางส่วนของถนนคุณจะสังเกตเห็นปริมาณที่สูงกว่าส่วนอื่น ๆ

  • และชุดคีย์ของคุณไม่รับประกันข้อมูลส่วนถนนเดียวกันในพาร์ติชันเดียวกันดังนั้นคุณจึงไม่สามารถแน่ใจได้ว่าจะไม่มีการสับเปลี่ยน

ข้อมูลที่ได้รับจาก IMO นั้นไม่เพียงพอในการออกแบบไปป์ไลน์ข้อมูลทั้งหมด เนื่องจากเมื่อออกแบบไปป์ไลน์วิธีที่คุณแบ่งพาร์ติชันข้อมูลของคุณมีบทบาทสำคัญ คุณควรสอบถามเพิ่มเติมเกี่ยวกับข้อมูลที่คุณได้รับเช่นจำนวนยานพาหนะขนาดของสตรีมข้อมูลอินพุตจำนวนสตรีมคงที่หรือเพิ่มขึ้นในอนาคตหรือไม่ สตรีมข้อมูลอินพุตที่คุณได้รับเป็นคาฟคาสตรีมหรือไม่ คุณได้รับข้อมูลมากแค่ไหนใน 5 นาที

  • ตอนนี้สมมติว่าคุณมี 4 สตรีมที่เขียนถึง 4 หัวข้อในคาฟคาหรือพาร์ติชั่น 4 และคุณไม่มีคีย์เฉพาะใด ๆ แต่ข้อมูลของคุณจะถูกแบ่งพาร์ติชันตามคีย์ศูนย์ข้อมูลบางส่วน ถ้าไม่เช่นนั้นควรทำที่ด้านข้อมูลแทนที่จะทำซ้ำข้อมูลในสตรีมคาฟคาอื่นและการแบ่งพาร์ติชัน
  • หากคุณได้รับข้อมูลจากศูนย์ข้อมูลที่แตกต่างกันคุณจะต้องนำข้อมูลไปยังกลุ่มเดียวและเพื่อจุดประสงค์นั้นคุณสามารถใช้เครื่องทำแก้ว Kafka หรือสิ่งที่คล้ายกัน
  • หลังจากคุณมีข้อมูลทั้งหมดในหนึ่งคลัสเตอร์คุณสามารถรันงานสตรีมที่มีโครงสร้างที่นั่นและด้วยช่วงเวลาทริกเกอร์และลายน้ำ 5 นาทีตามความต้องการของคุณ
  • ในการคำนวณค่าเฉลี่ยและหลีกเลี่ยงการสับจำนวนมากคุณสามารถใช้การรวมกันmapValuesและreduceByKeyแทน groupBy อ้างถึงนี้
  • คุณสามารถเขียนข้อมูลไปยังคาฟคาซิงก์หลังจากการประมวลผล

mapValues ​​และ reductionByKey เป็นของ RDD ระดับต่ำ Catalyst ไม่ฉลาดพอที่จะสร้าง RDD ที่มีประสิทธิภาพมากที่สุดเมื่อฉันจัดกลุ่มและคำนวณค่าเฉลี่ยหรือไม่
Alon

@Alon Catalyst จะสามารถคิดแผนการที่ดีที่สุดในการรันเคียวรีของคุณได้ แต่ถ้าคุณใช้ groupBy ข้อมูลที่มีคีย์เดียวกันจะถูกสับเปลี่ยนไปที่พาร์ติชันเดียวกันก่อนจากนั้นจึงใช้การดำเนินการรวมในนั้น mapValuesและreduceByเป็นของ RDD ระดับต่ำ แต่ยังคงทำงานได้ดีขึ้นในสถานการณ์นี้เนื่องจากจะคำนวณรวมต่อพาร์ติชันก่อนจากนั้นจึงทำการสับ
wypul

0

ปัญหาหลักที่ฉันเห็นด้วยวิธีนี้คือ:

  • ส่วนของถนนที่อยู่บนขอบของสี่เหลี่ยม 6 หลักของแผนที่จะมีข้อมูลในพาร์ทิชันหลายหัวข้อและจะมีความเร็วเฉลี่ยหลายระดับ
  • ขนาดข้อมูลการนำเข้าสำหรับพาร์ติชัน Kafka ของคุณอาจไม่สมดุลกัน (เมืองกับทะเลทราย) การแบ่งพาร์ทิชันด้วยรหัสประจำตัวของรถอาจเป็นความคิดที่ดี
  • ไม่แน่ใจว่าฉันติดตามส่วนรวม แต่มันฟังดูมีปัญหา

ฉันจะบอกว่าวิธีแก้ปัญหาต้องทำ: อ่านจากกระแส Kafka -> UDF -> ส่วนของถนนตามกลุ่ม -> ค่าเฉลี่ย -> เขียนไปยังกระแส Kafka


0

การออกแบบของฉันจะขึ้นอยู่กับ

  1. จำนวนถนน
  2. จำนวนยานพาหนะ
  3. คำนวณต้นทุนของถนนจากพิกัด

ถ้าฉันต้องการไต่ระดับเป็นจำนวนเท่าใดการออกแบบก็จะเป็นแบบนี้ ป้อนคำอธิบายรูปภาพที่นี่

ข้ามความกังวลในการออกแบบนี้ -

  1. รักษาสถานะอินพุตที่คงทน (หากอินพุตเป็นคาฟคาเราสามารถจัดเก็บออฟเซ็ตด้วยคาฟคาหรือภายนอก)
  2. สถานะจุดตรวจสอบเป็นระยะ ๆ ไปยังระบบภายนอก (ฉันชอบใช้อุปสรรคจุดตรวจจุดเชื่อมใน async )

การปรับปรุงการปฏิบัติที่เป็นไปได้ในการออกแบบนี้ -

  1. ฟังก์ชั่นการแมปส่วนถนนถ้าเป็นไปได้ขึ้นอยู่กับถนน
  2. การจัดการ ping ที่ไม่ได้รับ (ในทางปฏิบัติไม่สามารถใช้ Ping ได้ทุกครั้ง)
  3. คำนึงถึงความโค้งของถนน (คำนึงถึงความสูงและความสูง)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.