Tính tốc độ trung bình của đường [đóng]


20

Tôi đã đi đến một cuộc phỏng vấn công việc kỹ sư dữ liệu. Người phỏng vấn hỏi tôi một câu hỏi. Anh ấy đã cho tôi một số tình huống và yêu cầu tôi thiết kế luồng dữ liệu cho hệ thống đó. Tôi đã giải quyết điều đó nhưng anh ấy không thích giải pháp của tôi và tôi đã thất bại. Tôi muốn biết nếu bạn có ý tưởng tốt hơn làm thế nào để giải quyết thách thức đó.

Câu hỏi là:

Hệ thống của chúng tôi nhận được bốn luồng dữ liệu. Dữ liệu chứa id xe, tốc độ và phối hợp định vị địa lý. Mỗi vihicle gửi dữ liệu của nó một lần một phút. Không có kết nối giữa một luồng cụ thể với một con đường hoặc vihicle cụ thể hoặc bất cứ điều gì khác. Có một chức năng chấp nhận phối hợp và trả về tên phần đường. Chúng ta cần biết tốc độ bay trên mỗi đoạn đường trong 5 phút. Cuối cùng chúng tôi muốn viết kết quả cho Kafka.

nhập mô tả hình ảnh ở đây

Vì vậy, giải pháp của tôi là:

Đầu tiên viết tất cả dữ liệu vào một cụm Kafka, vào một chủ đề, được phân chia theo 5-6 chữ số đầu tiên của vĩ độ được nối với 5-6 chữ số đầu tiên của kinh độ. Sau đó đọc dữ liệu theo Luồng có cấu trúc, thêm cho mỗi hàng tên phần đường bằng cách phối hợp (có một udf được xác định trước cho điều đó), và sau đó loại bỏ dữ liệu theo tên phần đường.

Vì tôi phân vùng dữ liệu trong Kafka theo 5-6 chữ số đầu tiên của các tọa độ, sau khi dịch các tọa độ sang tên phần, không cần chuyển nhiều dữ liệu vào phân vùng chính xác và do đó tôi có thể tận dụng thao tác colesce () Điều đó không kích hoạt một shuffle đầy đủ.

Sau đó tính toán tốc độ trung bình trên mỗi người thi hành.

Toàn bộ quá trình sẽ diễn ra cứ sau 5 phút và chúng tôi sẽ ghi dữ liệu ở chế độ Nối vào bồn rửa Kafka cuối cùng.

nhập mô tả hình ảnh ở đây

Vì vậy, một lần nữa, người phỏng vấn không thích giải pháp của tôi. Bất cứ ai có thể đề nghị làm thế nào để cải thiện nó hoặc một ý tưởng hoàn toàn khác và tốt hơn?


Sẽ không tốt hơn nếu hỏi người đó chính xác những gì anh ta không thích?
Gino Pane

Tôi nghĩ rằng đó là một ý tưởng tồi để phân vùng bởi lat-nối dài. Điểm dữ liệu sẽ được báo cáo cho mỗi làn là một tọa độ hơi khác nhau?
webber

@webber do đó tôi chỉ lấy một vài chữ số, vì vậy vị trí sẽ không phải là duy nhất mà tương đối ở kích thước của một đoạn đường.
Alon

Câu trả lời:


6

Tôi thấy câu hỏi này rất thú vị và nghĩ đến việc thử nó.

Như tôi đã đánh giá thêm, bản thân nỗ lực của bạn là tốt, ngoại trừ những điều sau đây:

được phân chia bởi 5-6 chữ số đầu tiên của vĩ độ được nối với 5-6 chữ số đầu tiên của kinh độ

Nếu bạn đã có một phương pháp để lấy id / name của phần đường dựa trên vĩ độ và kinh độ, tại sao không gọi phương thức đó trước và sử dụng id / name của phần đường để phân vùng dữ liệu ở vị trí đầu tiên?

Và sau đó, mọi thứ khá dễ dàng, vì vậy cấu trúc liên kết sẽ là

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. 

(Giải thích chi tiết hơn có thể được tìm thấy trong các ý kiến ​​trong mã dưới đây. Vui lòng hỏi nếu có gì không rõ ràng)

Tôi đã thêm mã vào cuối câu trả lời này, xin lưu ý rằng thay vì trung bình, tôi đã sử dụng tổng vì dễ chứng minh hơn. Có thể làm trung bình bằng cách lưu trữ một số dữ liệu bổ sung.

Tôi đã nêu chi tiết câu trả lời trong các bình luận. Sau đây là sơ đồ cấu trúc liên kết được tạo từ mã (nhờ https://zz85.github.io/kafka-streams-viz/ )

Cấu trúc liên kết:

Sơ đồ cấu trúc liên kết

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

Không hợp nhất tất cả các luồng là một ý tưởng tồi? Điều này có thể trở thành một nút cổ chai cho luồng dữ liệu của bạn. Điều gì xảy ra khi bạn bắt đầu nhận được ngày càng nhiều luồng đầu vào khi hệ thống của bạn phát triển? Điều này sẽ có thể mở rộng?
wypul

@wypul> không hợp nhất tất cả các luồng là một ý tưởng tồi? -> Tôi nghĩ là không. Tính song song trong Kafka không đạt được thông qua các luồng, nhưng thông qua các phân vùng (và tác vụ), phân luồng, v.v ... Luồng là cách để nhóm dữ liệu. > Điều này sẽ có thể mở rộng? -> có. Vì chúng tôi đang khóa bởi các phần đường và giả sử các phần đường được phân phối khá công bằng, chúng tôi có thể tăng số lượng phân vùng cho các chủ đề này để xử lý luồng trong các container khác nhau. Chúng tôi có thể sử dụng thuật toán phân vùng tốt dựa trên phần đường để phân phối tải trên các bản sao.
Irshad PI

1

Vấn đề như vậy có vẻ đơn giản và các giải pháp được đưa ra đã có rất nhiều ý nghĩa. Tôi tự hỏi nếu người phỏng vấn quan tâm đến thiết kế và hiệu suất của giải pháp mà bạn đã tập trung vào hoặc tính chính xác của kết quả. Vì những người khác đã tập trung vào mã, thiết kế và hiệu suất, tôi sẽ cân nhắc về độ chính xác.

Giải pháp truyền phát

Khi dữ liệu đang chảy, chúng tôi có thể đưa ra ước tính sơ bộ về tốc độ trung bình của đường. Ước tính này sẽ hữu ích trong việc phát hiện tắc nghẽn nhưng sẽ tắt trong việc xác định giới hạn tốc độ.

  1. Kết hợp cả 4 luồng dữ liệu lại với nhau.
  2. Tạo một cửa sổ 5 phút để thu thập dữ liệu từ cả 4 luồng trong 5 phút.
  3. Áp dụng UDF trên tọa độ để lấy tên đường và tên thành phố. Tên đường phố thường được nhân đôi trên các thành phố, vì vậy chúng tôi sẽ sử dụng tên thành phố + tên đường phố làm khóa.
  4. Tính tốc độ trung bình với một cú pháp như -

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

5. write the result to the Kafka Topic

Giải pháp hàng loạt

Dự toán này sẽ bị tắt vì kích thước mẫu nhỏ. Chúng tôi sẽ cần xử lý hàng loạt dữ liệu cả tháng / quý / năm để xác định chính xác hơn giới hạn tốc độ.

  1. Đọc dữ liệu một năm từ hồ dữ liệu (hoặc Chủ đề Kafka)

  2. Áp dụng UDF trên tọa độ để lấy tên đường và tên thành phố.

  3. Tính tốc độ trung bình với một cú pháp như -


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

  1. ghi kết quả vào dữ liệu hồ.

Dựa trên giới hạn tốc độ chính xác hơn này, chúng tôi có thể dự đoán lưu lượng chậm trong ứng dụng phát trực tuyến.


1

Tôi thấy một vài vấn đề với chiến lược phân vùng của bạn:

  • Khi bạn nói rằng bạn sẽ phân vùng dữ liệu của mình dựa trên 5-6 chữ số đầu tiên của lat lat, bạn sẽ không thể xác định số lượng phân vùng kafka trước đó. Bạn sẽ có dữ liệu sai lệch vì đối với một số đoạn đường bạn sẽ quan sát thấy âm lượng lớn hơn các đoạn khác.

  • Và tổ hợp khóa của bạn không đảm bảo cùng một dữ liệu phần đường trong cùng một phân vùng và do đó bạn không thể chắc chắn rằng sẽ không bị xáo trộn.

IMO cung cấp thông tin là không đủ để thiết kế toàn bộ đường ống dữ liệu. Bởi vì khi thiết kế đường ống, cách bạn phân vùng dữ liệu của bạn đóng một vai trò quan trọng. Bạn nên hỏi thêm về dữ liệu mà bạn đang nhận như số lượng phương tiện, kích thước của luồng dữ liệu đầu vào, số lượng luồng được cố định hay nó có thể tăng trong tương lai không? Các luồng dữ liệu đầu vào bạn đang nhận có phải là các luồng kafka không? Bao nhiêu dữ liệu bạn nhận được trong 5 phút?

  • Bây giờ, giả sử rằng bạn có 4 luồng được ghi vào 4 chủ đề trong kafka hoặc 4 phân vùng và bạn không có bất kỳ khóa cụ thể nào nhưng dữ liệu của bạn được phân vùng dựa trên một số khóa trung tâm dữ liệu hoặc nó được phân vùng băm. Nếu không thì điều này nên được thực hiện ở phía dữ liệu thay vì sao chép lại dữ liệu trong một luồng và phân vùng kafka khác.
  • Nếu bạn đang nhận dữ liệu trên các trung tâm dữ liệu khác nhau thì bạn cần đưa dữ liệu vào một cụm và với mục đích đó, bạn có thể sử dụng trình tạo gương Kafka hoặc một cái gì đó tương tự.
  • Sau khi bạn có tất cả dữ liệu trên một cụm, bạn có thể chạy một công việc truyền phát có cấu trúc ở đó và với khoảng thời gian kích hoạt 5 phút và hình mờ dựa trên yêu cầu của bạn.
  • Để tính trung bình và tránh xáo trộn nhiều, bạn có thể sử dụng kết hợp mapValuesreduceByKeythay vì nhómBy. Tham khảo điều này .
  • Bạn có thể ghi dữ liệu vào kafka chìm sau khi xử lý.

mapValues ​​và lessByKey thuộc về RDD cấp thấp. Không phải Catalyst đủ thông minh để tạo RDD hiệu quả nhất khi tôi nhóm và tính trung bình?
Alon

@Alon Catalyst chắc chắn sẽ có thể tìm ra kế hoạch tốt nhất để chạy truy vấn của bạn, nhưng nếu bạn sử dụng groupBy, dữ liệu có cùng khóa sẽ được xáo trộn vào cùng một phân vùng trước và sau đó áp dụng thao tác tổng hợp trên đó. mapValuesreduceBythực sự thuộc về RDD cấp thấp nhưng vẫn sẽ hoạt động tốt hơn trong tình huống này vì trước tiên nó sẽ tính tổng hợp trên mỗi phân vùng và sau đó thực hiện xáo trộn.
wypul

0

Các vấn đề chính tôi thấy với giải pháp này là:

  • Các đoạn đường nằm ở rìa của các ô vuông 6 chữ số của bản đồ sẽ có dữ liệu trong nhiều phân vùng chủ đề và sẽ có nhiều tốc độ trung bình.
  • Kích thước dữ liệu nhập vào cho các phân vùng Kafka của bạn có thể bị mất cân bằng (thành phố và sa mạc). Phân vùng bằng chữ số id xe đầu tiên có thể là một ý tưởng tốt IMO.
  • Không chắc chắn tôi đã theo dõi phần kết hợp, nhưng nó có vẻ có vấn đề.

Tôi muốn nói giải pháp cần làm: đọc từ luồng Kafka -> UDF -> đoạn đường nhóm -> trung bình -> ghi vào luồng Kafka.


0

Thiết kế của tôi sẽ phụ thuộc vào

  1. Số đường
  2. Số lượng xe
  3. Chi phí tính toán đường từ tọa độ

Nếu tôi muốn chia tỷ lệ cho bất kỳ số lượng đếm nào, thiết kế sẽ trông như thế này nhập mô tả hình ảnh ở đây

Lo ngại chéo về thiết kế này -

  1. Duy trì trạng thái bền của các luồng đầu vào (nếu đầu vào là kafka, chúng ta có thể lưu trữ bù trừ với Kafka hoặc bên ngoài)
  2. Trạng thái điểm kiểm tra định kỳ cho hệ thống bên ngoài (Tôi thích sử dụng các rào cản điểm kiểm tra không đồng bộ trong Flink )

Một số cải tiến thực tế có thể có trên thiết kế này -

  1. Chức năng ánh xạ phần đường bộ đệm nếu có thể, dựa trên đường
  2. Xử lý ping bị bỏ lỡ (trong thực tế không phải mọi ping đều có sẵn)
  3. Tính độ cong của đường vào tài khoản (mang và độ cao vào tài khoản)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.