Розрахунок середньої швидкості доріг [закрито]


20

Я пішов на співбесіду з інженера даних. Інтерв'юер задав мені запитання. Він дав мені певну ситуацію і попросив мене розробити потік даних для цієї системи. Я вирішив це, але йому не сподобалось моє рішення, і я провалився. Я хотів би знати, чи є у вас кращі ідеї, як вирішити цю проблему.

Питання:

Наша система отримує чотири потоки даних. Дані містять координати ідентифікатора транспортного засобу, швидкості та геолокації. Кожен возик надсилає свої дані раз на хвилину. Немає зв’язку між певним потоком до певної дороги чи транспортного засобу чи іншого. Існує функція, яка приймає координації та повертає назву ділянки дороги. Нам потрібно знати середню швидкість руху на кожній ділянці дороги за 5 хвилин. Нарешті ми хочемо написати результати до Кафки.

введіть тут опис зображення

Тож моє рішення було:

Спочатку записуйте всі дані в кластер Kafka, в одну тему, розділену 5-6 першими цифрами широти, приєднаними до 5-6 перших цифр довготи. Потім читайте дані за допомогою структуризованої потокової передачі, додаючи для кожного рядка назву ділянки дороги за координатами (для цього є заздалегідь визначений udf), а потім з’єднуєте дані за назвою ділянки дороги.

Оскільки я розбиваю дані в Kafka на 5-6 перших цифр координат, після перекладу координацій на назву розділу, немає необхідності передавати багато даних у правильний розділ, і тому я можу скористатися операцією colesce () це не спричинить повне переміщення.

Потім обчислюють середню швидкість на одного виконавця.

Весь процес відбуватиметься кожні 5 хвилин, і ми запишемо дані в додатковому режимі до остаточної мийки Kafka.

введіть тут опис зображення

Тому знову інтерв'юеру не сподобалось моє рішення. Хтось може підказати, як її вдосконалити, або зовсім іншу та кращу ідею?


Чи не було б краще запитати людину, що йому точно не сподобалось?
Джино

Я думаю, що погана ідея розділяти з’єднані lat-long. Чи не буде повідомлятися точка даних для кожної смуги як дещо інша координата?
webber

@webber тому я беру лише кілька цифр, так що позиція не буде унікальною, але відносно за розміром ділянки дороги.
Алон

Відповіді:


6

Я вважав це питання дуже цікавим і думав про спробу його.

Як я оцінив далі, сама ваша спроба хороша, за винятком наступного:

розділений на 5-6 перших цифр широти, з'єднаних з 5-6 першими цифрами довготи

Якщо у вас уже є метод отримання ідентифікатора / назви ділянки дороги на основі широти та довготи, чому б не зателефонувати спочатку до цього методу та використати ідентифікатор / ім’я ділянки дороги для розділення даних в першу чергу?

А після цього все досить легко, тому топологія буде

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> Не злиття всіх потоків є поганою ідеєю? -> Я думаю, що ні. Паралелізм у Kafka не досягається за допомогою потоків, а через розділи (та завдання), нарізування рідин тощо. Потоки - це спосіб групування даних. > Чи буде це масштабовано? -> так. Оскільки ми керуємо ділянками доріг і припускаючи, що ділянки дороги досить розподілені, ми можемо збільшити кількість розділів для цих тем для паралельної обробки потоку в різних контейнерах. Ми можемо використовувати хороший алгоритм розподілу, заснований на ділянці дороги, щоб розподілити навантаження по репліках.
Іршад PI

1

Проблема як така здається простою, і запропоновані рішення вже мають багато сенсу. Мені цікаво, чи був інтерв'юер стурбований дизайном та виконанням рішення, на якому ви зосередилися, або точністю результату. Оскільки інші зосередилися на коді, дизайні та продуктивності, я зважу на точність.

Потокове рішення

По мірі надходження даних ми можемо дати приблизну оцінку середньої швидкості дороги. Ця оцінка буде корисною для виявлення перевантаженості, але не визначатиме обмеження швидкості.

  1. Об’єднайте всі 4 потоки даних разом.
  2. Створіть вікно в 5 хвилин, щоб за 5 хвилин збирати дані з усіх 4 потоків.
  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. Прочитайте дані за рік із озера даних (або теми Kafka)

  2. Застосуйте UDF за координатами, щоб отримати назву вулиці та назву міста.

  3. Обчисліть середню швидкість за допомогою синтаксису типу -


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

  1. записати результат в озеро даних.

На основі цього більш точного обмеження швидкості ми можемо передбачити повільний трафік у потоковому додатку.


1

Я бачу кілька проблем із вашою стратегією розділення:

  • Коли ви скажете, що збираєтеся розділити свої дані на основі перших 5-6 цифр lat, ви не зможете заздалегідь визначити кількість розділів kafka. У вас будуть перекошені дані, оскільки для деяких ділянок дороги ви будете спостерігати великий обсяг, ніж інші.

  • І ваша комбінація клавіш все одно не гарантує однакові дані про ділянку дороги в одній секції, отже, ви не можете бути впевнені, що перетасування не буде.

Інформація, надана ІМО, недостатня для проектування всього конвеєра даних. Тому що при проектуванні конвеєра важливу роль відіграє спосіб розподілу даних. Вам слід дізнатися більше про дані, які ви отримуєте, як кількість транспортних засобів, розмір потоків вхідних даних, чи фіксовано кількість потоків чи може вона збільшуватися в майбутньому? Чи потоки вхідних даних, які ви отримуєте, є потоками kafka? Скільки даних ви отримаєте за 5 хвилин?

  • Тепер припустимо, що у вас є 4 потоки, записані на 4 теми в kafka або 4 розділи, і у вас немає конкретного ключа, але ваші дані розділяються на основі ключа ключа центру даних або він є хеш-розділеним. Якщо ні, то це слід зробити на стороні даних, а не дедублювати дані в іншому потоці kafka та розділяти їх.
  • Якщо ви отримуєте дані в різних центрах обробки даних, вам потрібно донести дані до одного кластеру, і для цього ви можете використовувати дзеркальний виробник Kafka або щось подібне.
  • Після того, як у вас є всі дані на одному кластері, ви можете запустити туди структуровану потокову роботу та з 5-хвилинним інтервалом триггеру та водяним знаком на основі вашої потреби.
  • Для обчислення середнього та уникнення великої кількості перетасовок ви можете використовувати комбінацію mapValuesта reduceByKeyзамість groupBy. Зверніться до цього .
  • Ви можете записати дані в мийку kafka після обробки.

mapValues ​​та ReduByKey належать до RDD низького рівня. Чи не достатньо розумний Catalyst, щоб генерувати найефективніший RDD, коли я групуюсь і обчислюю середню?
Алон

@Alon Catalyst, безумовно, зможе визначити найкращий план для запуску вашого запиту, але якщо ви використовуєте groupBy, дані з тим самим ключем спершу переміщатимуться в той самий розділ, а потім застосовувати для цього сукупну операцію. mapValuesі reduceByсправді належить до РДД низького рівня, але все-таки буде краще в цій ситуації, оскільки спочатку буде какулювати сукупність за розділом, а потім робити перетасування.
wypul

0

Основні проблеми, які я бачу з цим рішенням:

  • На ділянках доріг, які знаходяться на краю 6-значних квадратів карти, будуть дані в декількох розділах тематики та матимуть декілька середніх швидкостей.
  • Розмір даних про прийом для ваших розділів Kafka може бути незбалансованим (місто проти пустелі). Розбиття перших цифр за ідентифікатором автомобіля може бути хорошою ідеєю IMO.
  • Не впевнений, що я стежив за об'єднаною частиною, але це звучить проблематично.

Я б сказав, що рішення потрібно зробити: прочитати з потоку Kafka -> UDF -> груповий відрізок дороги -> середній -> написати в потік Kafka.


0

Моя конструкція залежатиме від цього

  1. Кількість доріг
  2. Кількість транспортних засобів
  3. Розрахунок вартості дороги за координатами

Якщо я хочу масштабувати будь-яку кількість підрахунків, дизайн виглядав би так введіть тут опис зображення

Поперечні питання щодо цієї конструкції -

  1. Підтримуйте міцний стан вхідних потоків (якщо вхід - кафка, ми можемо зберігати компенсації за допомогою Кафки або зовні)
  2. Періодично стан контрольної точки для зовнішньої системи (я вважаю за краще використовувати бар'єри асинхронних контрольних точок на Flink )

Деякі практичні вдосконалення щодо цього дизайну -

  1. Кешування функцією відображення ділянки дороги, якщо можливо, на основі доріг
  2. Поводження з пропущеними пінгами (на практиці не кожен пінг доступний)
  3. Враховуючи кривизну дороги (підшипник та висоту)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.