Spark代碼可讀性與性能優化——示例九(數據傳輸與解析)
1. 前言
- 通常數據傳輸與解析是開發人員不常關心的一個方面,會直接使用最便利的方式處理。但是,在大量數據的處理中,無論是數據在網絡中的傳輸還是數據的解析方式都會對性能產生影響(積少成多)。下面就舉幾個例子來說明該如何處理數據。
2. Kyro序列化
- Kyro序列化是常談的話題,將數據對象序列化,減小其大小,便於在網絡中快速傳輸,再進行反序列化解析。
- 性能方面,你應當考慮的是,序列化/反序列化所付出代價是否小於網絡傳輸的提升
- 一般來說編寫代碼時會存在以下問題:
- 設置SparkConf(“spark.kryo.registrationRequired”, “true”)後,會強制要求所有的傳輸數據必須採用Kyro序列化
- 序列化報錯,不知道這個類怎麼配置序列化,因爲JVM報錯展示的是class信息(也就是Java的形式)
- Scala數組書寫方式與Java不同,使用scala註冊時應該寫 kryo.register(classOf[Array[String]])
- JVM打印的類中帶有$符號,你可以直接複製該類信息,直接使用Java的Class.forName(“java.util.HashMap$EntrySet”) (萬能大法^_^)
3. csv解析
- 原始數據是csv格式的比較普遍,如何快速解析?我們以逗號分隔的數據做示例
- 例如,原始數據是"姓名, 年齡, 地址, 性別……",你需要根據年齡過濾掉不需要的數據,一般可以按以下方式書寫代碼
data.filter {line => val fields = line.split(",") fields(1).toInt > 16 }
- 代碼邏輯沒有問題,但是我們根本不需要解析每一個逗號分隔符,如果數據的字段數很多,就會無意義的浪費大量性能。split方法的源碼如下:
public String[] split(String regex, int limit) { /* fastpath if the regex is a (1)one-char String and this character is not one of the RegEx's meta characters ".$|()[{^?*+\\", or (2)two-char String and the first char is the backslash and the second is not the ascii digit or ascii letter. */ char ch = 0; if (((regex.value.length == 1 && ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) || (regex.length() == 2 && regex.charAt(0) == '\\' && (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 && ((ch-'a')|('z'-ch)) < 0 && ((ch-'A')|('Z'-ch)) < 0)) && (ch < Character.MIN_HIGH_SURROGATE || ch > Character.MAX_LOW_SURROGATE)) { int off = 0; int next = 0; boolean limited = limit > 0; ArrayList<String> list = new ArrayList<>(); while ((next = indexOf(ch, off)) != -1) { if (!limited || list.size() < limit - 1) { list.add(substring(off, next)); off = next + 1; } else { // last one //assert (list.size() == limit - 1); list.add(substring(off, value.length)); off = value.length; break; } } // If no match was found, return this if (off == 0) return new String[]{this}; // Add remaining segment if (!limited || list.size() < limit) list.add(substring(off, value.length)); // Construct result int resultSize = list.size(); if (limit == 0) { while (resultSize > 0 && list.get(resultSize - 1).length() == 0) { resultSize--; } } String[] result = new String[resultSize]; return list.subList(0, resultSize).toArray(result); } return Pattern.compile(regex).split(this, limit); }
- 從源碼可以看出,這樣做非常影響效率
- 我們要的是第一個逗號分隔符與第二個逗號分隔符之間的年齡,所以你可以這樣獲取年齡
public static void main(String[] args) { String content = "小明,18,北京,男"; String age = subStringByIndex(content, ',', 1); System.out.println("age = " + age); } /** * 獲取第number到第number+1個ch之間的字符串 */ public static String subStringByIndex(String content, int ch, int number) { StringBuilder builder = new StringBuilder(); int i = 0; while (i < content.length()) { char current = content.charAt(i); if (current == ch) { number--; if (number < 0) break; } else { if (number == 0) { builder.append(current); } } i++; } return builder.toString(); }
- 給一個參考數據:該示例中,這樣寫的效率是split寫法的4倍以上,同時內存佔用更少(暫時不好測)
- 另外,如果一個CSV格式數據,能夠約定好每個字段的長度,那麼解析時可以直接知道其字段的index,速度會更快
4. json解析
- 數據源是json格式的字符串,也很常見,我們通常會使用JSON解析框架(FastJson、Gson、Jackson等)對數據進行解析。
- json解析的問題就在於:我們習慣於編寫一個JavaBean,使用JSON解析框架提供的便捷方法直接將數據反射到JavaBean中,因爲這樣寫簡單。但是每條數據需要反射到JavaBean中,效率不高。
- 如果你能確定JSON數據的格式時,最好自己編寫解析代碼,這裏用FastJson做示例。
- 原始數據
{"name":"Bill" , "age":"18", "address":"BeiJing"}
- 封裝JavaBean,提供工廠方法(用於解析)
import com.alibaba.fastjson.JSONObject; public class Person { private String name; private int age; private String address; public Person(String name, int age, String address) { this.name = name; this.age = age; this.address = address; } // // 省略set、get、toString方法 // public static Person parse(String json) { JSONObject jsonObject = (JSONObject) JSONObject.parse(json); String name = jsonObject.getString("name"); int age = jsonObject.getInteger("age"); String address = jsonObject.getString("address"); return new Person(name, age, address); } }
- json解析
String json = "{\"name\":\"Bill\" , \"age\":\"18\", \"address\":\"BeiJing\"}"; Person person = Person.parse(json); System.out.println(person);
- 原始數據
- 當你的數據只需要根據JSON數據中的某個字段做處理時,那麼不要對JSON做完全解析,只需要解析對應字段即可,例如:
JSONObject jsonObject = (JSONObject) JSONObject.parse(json); int age = jsonObject.getInteger("age");
5. 其他
- 其他格式解析提升效率的方式,同上。再舉幾個例子看看吧:
- xml格式,推薦使用SAX解析,逐行解析,後面的不需要就不解析。效率高,但較DOM更復雜。
- http協議,應該先解析請求頭,看是否符合條件(是否有該服務),再決定是否解析後面部分
- 數據是字節數組的,應根據約定好的解析方式解析,跳過不要的部分
- 關於protobuf
- protobuf會比kyro快一點,相差不大。但是每變一次RDD所用到的數據類型,就需要修改protobuf協議文件生成新的類。而在Spark開發中,經常變動RDD傳輸的數據類型是很正常的,protobuf將難以使用。Kyro專爲Java設計,能夠直接跟隨數據類型變動而變動(只需要你寫一下注冊代碼即可),所以更爲簡便。
- 原始數據接入時可以使用protobuf。如果將原始數據以二進制存儲,例如發送到Kafka,再用SparkStream接入,可以直接先進行protobuf解析。