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, char 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解析。