在Clojure中并行处理巨大的JSON

时间:2018-04-12 12:26:31

标签: json clojure

我们的数据来自数据库,我们需要在发送之前应用一些业务逻辑,因此我们将转换为Clojure地图格式以进行处理。数据是多级嵌套映射,我们必须处理所有级别映射中的每个键和值,以便我们使用clojure.walk.postwalk进行处理。由于数据庞大,需要花费更多时间。

在数据中,第一级包含大约5个键,每个键的值可以是另一个映射或向量。同样,它可能会达到10到15个级别。我们在第一级尝试了pmap,但速度很慢。如果数据是简单的向量,我们可以使用分区,但由于嵌套的复杂结构,很难使用分区。

无论如何都要使这个过程更快,基本上我们的要求是将函数应用于每个键,并为每个值应用单独的函数。

3 个答案:

答案 0 :(得分:1)

我很幸运使用future进行两次传球。基本上,您将整个树行走一次,将每个转换包裹在future中。然后你第二次走树,deref放下每一个未来。我认为两次通过的成本太高,但是我用一个相当大的嵌套树来尝试它,它比使用postwalk要快得多。

我正在使用的测试用例是finding the nth prime来模拟昂贵的操作。树是关键字/数字对的嵌套映射。找到的所有数字都被转换为找到的第250个素数。

我正在使用的测试数据是这个混乱:

(def giant-tree
  {:a 28,
   :e {:d {:a 37,
           :e 92,
           :d {:b {:c 91,
                   :d {:e 12,
                       :a 22,
                       :d {:e {:a {:a 53}, :d 98},
                           :d {:b 23,
                               :a {:a {:a 97},
                                   :c {:c 47,
                                       :d {:c {:d {}},
                                           :e {:e 57,
                                               :d {:a 57,
                                                   :d 42,
                                                   :e {:d {:e 64,
                                                           :a {:d {:b 14,
                                                                   :d {:c {},
                                                                       :b {},
                                                                       :a {:b {:b 86,
                                                                               :a {:d 86, :c 52},
                                                                               :d {:d {:a {},
                                                                                       :c {:a {}, :c 0, :b {:c 29}},
                                                                                       :d 88},
                                                                                   :c {:c 88},
                                                                                   :a {:c 89, :a {:a 42, :c 62}},
                                                                                   :b 30},
                                                                               :e 60},
                                                                           :c {:e 18,
                                                                               :d {:e {}, :d 70, :b 90},
                                                                               :b {:a {:a 1}}}}},
                                                                   :e 47,
                                                                   :c 19},
                                                               :c {:a 56,
                                                                   :c {:a {:a 73,
                                                                           :e 39,
                                                                           :d 21,
                                                                           :b {:e {:d {}, :b 82, :c 12, :a 80},
                                                                               :a {:a 22,
                                                                                   :e {:b {:b {:b 20, :a 50}}, :c 23},
                                                                                   :b 55,
                                                                                   :d 80},
                                                                               :c 13}},
                                                                       :e 15},
                                                                   :b 68,
                                                                   :d 58},
                                                               :a 49},
                                                           :b 5},
                                                       :c 38}},
                                               :a {:a {:d 35, :a 99}},
                                               :c {:d {}}},
                                           :b {},
                                           :d 95}}},
                               :d {:b {:c 99}, :c 83, :e 61, :d 55},
                               :c {:b {:c 97,
                                       :a {:a {:b 86, :a {}, :e {:a 52, :c 20, :e 20}, :d 49}, :c 62},
                                       :d {:c 97,
                                           :d {:d {:d {:a 46, :c 90, :d {}, :e 88}, :e {:a 14, :c 48}},
                                               :c {},
                                               :a 87,
                                               :e 66}},
                                       :e 9}}}},
                       :b 64},
                   :a 4,
                   :e 19},
               :a {},
               :e 9}}}})

我正在使用Criterium进行基准测试。

这是我正在测试的代码:

(ns fast-tree-transform.fast-tree-transform
  (:require [fast-tree-transform.test-data :as td]

            [clojure.walk :as w]

            [criterium.core :as c]))

(def default-price 250)

(defn prime? [n]
  (not
    (or (zero? n)
        (some #(zero? (rem n %)) (range 2 n)))))

(defn nth-prime [n]
  (nth (filter prime? (range))
       n))

(defn expensive-transform [e]
  (if (number? e)
    (nth-prime default-price)
    e))

; ----- Simple usage without any parallel aspect
(defn transform-data [nested-map]
  (w/postwalk expensive-transform nested-map))

; ----- Puts each call in a future so it's run in a thread pool
(defn future-transform [e]
  (if (number? e)
    (future (expensive-transform e))
    e))

; ----- The second pass to resolve each future
(defn resolve-transform [e]
  (if (future? e)
    @e
    e))

; ----- Tie them both together
(defn future-transform-data [nested-map]
  (->> nested-map
      (w/postwalk future-transform)
      (w/postwalk resolve-transform)))

感兴趣的两个主要功能是transform-datafuture-transform-data

结果如下:

(c/bench
  (transform-data td/giant-tree))

Evaluation count : 60 in 60 samples of 1 calls.
             Execution time mean : 1.085124 sec
    Execution time std-deviation : 38.049523 ms
   Execution time lower quantile : 1.062980 sec ( 2.5%)
   Execution time upper quantile : 1.193548 sec (97.5%)
                   Overhead used : 3.088370 ns

Found 4 outliers in 60 samples (6.6667 %)
    low-severe   4 (6.6667 %)
 Variance from outliers : 22.1802 % Variance is moderately inflated by outliers

(c/bench
  (future-transform-data td/giant-tree))

Evaluation count : 120 in 60 samples of 2 calls.
             Execution time mean : 526.771107 ms
    Execution time std-deviation : 14.202895 ms
   Execution time lower quantile : 513.002517 ms ( 2.5%)
   Execution time upper quantile : 568.856393 ms (97.5%)
                   Overhead used : 3.088370 ns

Found 5 outliers in 60 samples (8.3333 %)
    low-severe   1 (1.6667 %)
    low-mild     4 (6.6667 %)
 Variance from outliers : 14.1940 % Variance is moderately inflated by outliers

你可以看到它快两倍。

答案 1 :(得分:0)

根据数据的性质(例如,第一级密钥的数量以及嵌套在这些密钥下的平衡程度)和硬件(CPU核心数),可能是您尝试过的方法( pmap在第一级)是你能做的最好的。

在嵌套地图结构上进行并行化的一种相对简单的方法实质上就是“展平”地图,以便每个键实际上是一个键的向量,表示值的路径(原始嵌套中的一个叶子)地图)。例如:

(defn extract-keys
  "Returns a seq of vectors that are the paths of keys to the leaves of map m."
  [m]
  (mapcat (fn [[k v]]
            (if (map? v)
              (map #(cons k %)
                   (extract-keys v))
              [[k]]))
          m))

(def data {:a {:b {:c {:d [1 2] :e [3 4 5 6]}
                   :f [7]}
               :g [8 9 10]}
           :h [11 12 13 14 15 16]})

;; Prints ((:a :b :c :d) (:a :b :c :e) (:a :b :f) (:a :g) [:h])
(println (extract-keys data))

然后,您可以在此展平地图上使用pmap

(defn- map-leaves
  [f m]
  (->> (extract-keys m)
       (pmap #(vector % (f (get-in data %))))
       (reduce (fn [m [k v]]
                 (assoc-in m k v))
               {})))

;; Prints {:a {:b {:c {:d 3, :e 18}, :f 7}, :g 27}, :h 81}
(println (map-leaves #(apply + %) data))

这可以直接修改以突变键(以及值),或者在[k v]之前对pmap对进行分区以减少并行化开销。当然,扁平化/不平整也会产生相当大的开销,因此这是否会比您已经尝试过的更快,取决于数据的性质,硬件和转换。

答案 2 :(得分:0)

你可以使用https://github.com/clojure/data.json json/read-str 会做的。 +你可以从db发送数据作为字符串。没有? :) 而且你可以再次使用pr-str & json/read-str组合。