道路の平均速度を計算する[終了]


20

データエンジニアの面接に行ってきました。インタビュアーから質問がありました。彼は私にいくつかの状況を与え、そのシステムのデータフローを設計するように頼みました。私はそれを解決しましたが、彼は私の解決策を嫌い、失敗しました。その課題を解決するためのより良いアイデアがあるかどうか知りたいのですが。

問題は:

私たちのシステムは4つのデータストリームを受信します。データには、車両ID、速度、地理位置情報の調整が含まれています。すべての車両が1分に1回データを送信します。特定の小川と特定の道路や車両などには何の関係もありません。コーディネートを受け付けて道路区間名を返す機能があります。5分あたりの道路セクションごとの平均速度を知る必要があります。最後に、結果をKafkaに書き込みます。

ここに画像の説明を入力してください

だから私の解決策は:

最初に、すべてのデータをカフカクラスターに1つのトピックに書き込み、緯度の最初の5〜6桁を経度の最初の5〜6桁に連結して分割します。次に、Structured Streamingによってデータを読み取り、調整ごとに道路セクション名を行ごとに追加し(そのための事前定義されたudfがあります)、道路セクション名によってデータをまとめます。

Kafkaのデータを調整の最初の5桁から6桁で分割するため、調整をセクション名に変換した後、大量のデータを正しいパーティションに転送する必要がないため、colesce()操作を利用できます。それは完全なシャッフルを引き起こしません。

次に、エグゼキューターごとの平均速度を計算します。

プロセス全体は5分ごとに発生し、追加モードでデータを最後のKafkaシンクに書き込みます。

ここに画像の説明を入力してください

そのため、面接担当者は私の解決策を好まなかった。誰かがそれを改善する方法や完全に異なるより良いアイデアを提案できますか?


彼に正確に何が嫌だったのかを人に尋ねた方がいいのではないでしょうか?
Gino Pane

lat-longを連結して分割することは悪い考えだと思います。各レーンのデータポイントは、わずかに異なる座標として報告されませんか?
ウェバー、

@webberしたがって、私は数桁しかとらないので、位置は一意ではありませんが、道路セクションのサイズは比較的大きくなります。
アロン、

回答:


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. 

(詳細な説明は、以下のコードのコメントに記載されています。不明な点がある場合はお問い合わせください)

この回答の最後にコードを追加しました。平均よりも、デモンストレーションが簡単なsumを使用していることに注意してください。いくつかの追加データを保存することにより、平均化を行うことが可能です。

答えはコメントで詳しく説明しました。以下はコードから生成されたトポロジー図です(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>すべてのストリームをマージすることは悪い考えではありませんか?->私は違うと思います。Kafkaの並列処理は、ストリームではなく、パーティション(およびタスク)、スレッド化などによって実現されます。ストリームは、データをグループ化する方法です。>これはスケーラブルですか?->はい。道路セクションごとにキーイングし、道路セクションが適切に分散されていると想定しているため、これらのトピックのパーティション数を増やして、さまざまなコンテナーでストリームを並列処理することができます。ロードセクションに基づいた適切なパーティショニングアルゴリズムを使用して、レプリカ間で負荷を分散できます。
Irshad PI

1

そのような問題自体は単純に思われ、提供されたソリューションはすでに多くの意味を成しています。インタビュアーが、あなたが焦点を当てたソリューションの設計とパフォーマンス、または結果の正確性について懸念していたのではないかと思います。他の人はコード、デザイン、パフォーマンスに焦点を当てているので、正確さを重視します。

ストリーミングソリューション

データが流れているので、道路の平均速度の大まかな見積もりを提供できます。この推定は、輻輳の検出には役立ちますが、速度制限の決定には役立ちません。

  1. 4つのデータストリームをすべて結合します。
  2. 5分のウィンドウを作成して、4つのストリームすべてから5分間でデータをキャプチャします。
  3. 座標にUDFを適用して、ストリート名と都市名を取得します。多くの場合、ストリート名は都市間で重複しているため、キーとしてcity-name + street-nameを使用します。
  4. 次のような構文で平均速度を計算します-

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

5. write the result to the Kafka Topic

バッチソリューション

サンプルサイズが小さいため、この推定はオフになります。速度制限をより正確に決定するには、月/四半期/年全体のデータをバッチ処理する必要があります。

  1. データレイク(またはKafkaトピック)から年データを読み取る

  2. 座標にUDFを適用して、ストリート名と都市名を取得します。

  3. 次のような構文で平均速度を計算します-


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

  1. 結果をデータレイクに書き込みます。

このより正確な速度制限に基づいて、ストリーミングアプリケーションの低速トラフィックを予測できます。


1

パーティショニング戦略にいくつか問題があるようです。

  • lat longの最初の5〜6桁に基づいてデータをパーティション分割する場合、事前にkafkaパーティションの数を決定することはできません。一部の道路セクションでは、他のセクションよりも大量のデータが観察されるため、データが歪んでいます。

  • そして、あなたのキーの組み合わせは、とにかく同じパーティションの同じ道路セクションデータを保証しないので、シャッフルが行われないことを確信できません。

IMOが提供する情報は、データパイプライン全体を設計するには不十分です。パイプラインを設計するとき、データをどのように分割するかが重要な役割を果たすからです。車両の数、入力データストリームのサイズなど、受信しているデータについて詳しく調べる必要があります。ストリームの数は固定されていますか、それとも将来増加する可能性がありますか?受信している入力データストリームはカフカストリームですか?5分間にどのくらいのデータを受け取りますか?

  • ここで、4つのストリームが4つのトピックにkafkaまたは4つのパーティションに書き込まれていて、特定のキーはないが、データがデータセンターキーに基づいてパーティション化されているか、ハッシュパーティション化されていると仮定します。そうでない場合は、別のkafkaストリームのデータを重複排除してパーティション分割するのではなく、データ側でこれを行う必要があります。
  • 異なるデータセンターでデータを受信して​​いる場合は、データを1つのクラスターに移動する必要があります。そのためには、Kafkaミラーメーカーなどを使用できます。
  • 1つのクラスターにすべてのデータが揃ったら、そこに構造化されたストリーミングジョブを実行し、要件に基づいて5分のトリガー間隔とウォーターマークを指定できます。
  • 平均を計算し、シャッフルを回避するには、groupByの代わりに、mapValuesおよびreduceByKeygroupByの代わりに使用できます。これを参照してください。
  • 処理後、データをkafkaシンクに書き込むことができます。

mapValuesとreduceByKeyは低レベルRDDに属しています。Catalystは、グループ化して平均を計算するときに最も効率的なRDDを生成するのに十分スマートではありませんか?
アロン

@Alon Catalystは確実にクエリを実行するための最良の計画を見つけ出すことができますが、groupByを使用する場合、同じキーを持つデータが最初に同じパーティションにシャッフルされ、次にそれに集計操作が適用されます。mapValuesそしてreduceBy確かに低レベルRDDに属しているが、それでも、それは最初caculateパーティションごとに集計し、シャッフリングんだろうと、このような状況でパフォーマンスが向上します。
wypul

0

このソリューションで発生する主な問題は次のとおりです。

  • マップの6桁の正方形の端にある道路セクションには、複数のトピックパーティションにデータがあり、複数の平均速度があります。
  • Kafkaパーティションの取り込みデータサイズが不均衡である可能性があります(都市と砂漠)。車のIDの最初の桁でパーティション化することは、IMOの良いアイデアかもしれません。
  • 合体部分をたどったかどうかはわかりませんが、問題があるようです。

私はソリューションが行う必要があると思います:Kafkaストリームからの読み取り-> UDF-> groupby道路セクション->平均-> Kafkaストリームへの書き込み。


0

私のデザインは

  1. 道路の数
  2. 車両数
  3. 座標からの道路の計算コスト

カウントをいくつでもスケーリングしたい場合、デザインは次のようになります。 ここに画像の説明を入力してください

この設計に関する相互の懸念-

  1. 入力ストリームの永続的な状態を維持します(入力がkafkaの場合、Kafkaまたは外部でオフセットを保存できます)
  2. 定期的にチェックポイント状態を外部システムに(私はFlinkで非同期チェックポイントバリアを使用することをお勧めします

このデザインで可能ないくつかの実用的な拡張-

  1. 可能であれば、道路に基づいた道路セクションマッピング機能のキャッシュ
  2. 失敗したpingの処理(実際には、すべてのpingが使用できるわけではありません)
  3. 道路の曲率を考慮に入れる(方位と高度を考慮)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.