В конце прошлой недели я начал думать о том, как обращаться с большими объемами данных XML без ущерба для ресурсов. Основная проблема, которую я хотел решить, — это как обрабатывать большие файлы XML порциями, в то же время предоставляя upstream / последующие системы с некоторыми данными для обработки.
Конечно, я использую технологию JAXB уже несколько лет; Основным преимуществом использования JAXB является быстрое выход на рынок; если у кого-то есть XML-схема, то существуют инструменты для автоматической генерации соответствующих классов модели домена Java автоматически (Eclipse Indigo, плагины Maven jaxb для различных соусов, задачи муравья и многие другие). Затем JAXB API предлагает Marshaller и Unmarshaller для записи / чтения данных XML, отображая модель домена Java.
Думая о JAXB как о решении моей проблемы, я неожиданно осознал, что JAXB сохраняет всю объективизацию схемы XML в памяти, поэтому очевидным вопросом было: «Как наша инфраструктура справится с большими файлами XML (например, в моем случае с рядом элементы> 100 000), если бы мы использовали JAXB? ». Я мог бы просто создать большой XML-файл, а затем клиента для него и узнать о потреблении памяти.
Как известно, существует два подхода к обработке данных XML в Java: DOM и SAX. С DOM документ XML представляется в память в виде дерева; DOM полезен, если вам нужен доступ к узлам дерева или если вы хотите написать краткие XML-документы. На другой стороне спектра находится SAX, управляемая событиями технология, в которой весь документ анализируется по одному элементу XML за раз, и для каждого значимого события XML обратные вызовы «передаются» клиенту Java, который затем обрабатывает их (например, START_DOCUMENT, START_ELEMENT, END_ELEMENT и т. д.). Поскольку SAX не переносит весь документ в память, но применяет курсорный подход к обработке XML, он не потребляет огромных объемов памяти. Недостаток SAX заключается в том, что он обрабатывает весь документ от начала до конца; это не обязательно то, что нужно для больших XML-документов. Например, в моем сценарии я хотел бы иметь возможность передавать XML-элементам нижестоящих систем по мере их доступности, но в то же время, возможно, мне бы хотелось передавать только 100 элементов одновременно, реализуя своего рода разбиение на страницы решение. DOM кажется слишком требовательным с точки зрения потребления памяти, в то время как SAX, по-видимому, грубоват для моих нужд.
Я вспомнил, что читал кое-что о STax, технологии Java, которая предлагала золотую середину между возможностью извлечения XML-элементов (в отличие от проталкивания XML-элементов, например SAX), в то же время поддерживая RAM. Затем я изучил технологию и решил, что STax — это, вероятно, компромисс, который я искал; однако я хотел сохранить простую модель программирования, предлагаемую JAXB, поэтому мне действительно нужно было сочетание этих двух факторов. Исследуя STax, я наткнулся на Woodstox; Этот проект с открытым исходным кодом обещает быть более быстрым анализатором XML, чем многие другие, поэтому я решил включить его и в свой тест. Теперь у меня были все элементы для создания эталона, который давал бы мне показатели потребления памяти и скорости обработки при обработке больших документов XML.
Контрольный план
Для создания эталонного теста мне нужно было сделать следующее:
- Создайте схему XML, которая определила мою модель домена. Это будет вход для JAXB для создания модели домена Java
- Создайте три больших XML-файла, представляющих модель, с 10 000/100 000/1 000 000 элементов соответственно
- Иметь чистый JAXB-клиент, который бы полностью распаковывал большие XML-файлы в памяти
- Иметь клиента STax / JAXB, который бы сочетал низкое потребление памяти SAX-технологиями с простотой модели программирования, предлагаемой JAXB
- Иметь клиента Woodstox / JAXB с такими же характеристиками клиента STax / JAXB (в нескольких словах я просто хотел изменить базовый анализатор и посмотреть, смогу ли я получить какое-либо повышение производительности)
- Запишите потребление памяти и скорость обработки (например, как быстро каждое решение сделает фрагменты XML доступными в памяти как классы модели домена JAXB)
- Сделайте результаты доступными графически, так как, как мы знаем, одна картинка говорит тысячу слов.
XML-схема модели предметной области
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
<? xml version = "1.0" encoding = "UTF-8" ?> elementFormDefault = "qualified" > < complexType name = "PersonType" > < sequence > < element name = "firstName" type = "string" ></ element > < element name = "lastName" type = "string" ></ element > < element name = "address1" type = "string" ></ element > < element name = "address2" type = "string" ></ element > < element name = "postCode" type = "string" ></ element > < element name = "city" type = "string" ></ element > < element name = "country" type = "string" ></ element > </ sequence > < attribute name = "active" type = "boolean" use = "required" /> </ complexType > < complexType name = "PersonsType" > < sequence > < element name = "person" type = "tns:PersonType" maxOccurs = "unbounded" minOccurs = "1" ></ element > </ sequence > </ complexType > < element name = "persons" type = "tns:PersonsType" > </ element > </ schema > |
Я выбрал относительно простую модель предметной области, где элементы XML представляют людей с их именами и адресами. Я также хотел записать, был ли человек активным.
Использование JAXB для создания модели Java
Я фанат Maven и использую его как инструмент по умолчанию для построения систем. Это POM, который я определил для этого небольшого теста:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 < modelVersion >4.0.0</ modelVersion > < groupId >uk.co.jemos.tests.xml</ groupId > < artifactId >large-xml-parser</ artifactId > < version >1.0.0-SNAPSHOT</ version > < packaging >jar</ packaging > < name >large-xml-parser</ name > < properties > < project.build.sourceEncoding >UTF-8</ project.build.sourceEncoding > </ properties > < build > < plugins > < plugin > < groupId >org.apache.maven.plugins</ groupId > < artifactId >maven-compiler-plugin</ artifactId > < version >2.3.2</ version > < configuration > < source >1.6</ source > < target >1.6</ target > </ configuration > </ plugin > < plugin > < groupId >org.jvnet.jaxb2.maven2</ groupId > < artifactId >maven-jaxb2-plugin</ artifactId > < version >0.7.5</ version > < executions > < execution > < goals > < goal >generate</ goal > </ goals > </ execution > </ executions > < configuration > < schemaDirectory >${basedir}/src/main/resources</ schemaDirectory > < includeSchemas > < includeSchema >**/*.xsd</ includeSchema > </ includeSchemas > < extension >true</ extension > < args > < arg >-enableIntrospection</ arg > < arg >-XtoString</ arg > < arg >-Xequals</ arg > < arg >-XhashCode</ arg > </ args > < removeOldOutput >true</ removeOldOutput > < verbose >true</ verbose > < plugins > < plugin > < groupId >org.jvnet.jaxb2_commons</ groupId > < artifactId >jaxb2-basics</ artifactId > < version >0.6.1</ version > </ plugin > </ plugins > </ configuration > </ plugin > < plugin > < groupId >org.apache.maven.plugins</ groupId > < artifactId >maven-jar-plugin</ artifactId > < version >2.3.1</ version > < configuration > < archive > < manifest > < addClasspath >true</ addClasspath > < mainClass >uk.co.jemos.tests.xml.XmlPullBenchmarker</ mainClass > </ manifest > </ archive > </ configuration > </ plugin > < plugin > < groupId >org.apache.maven.plugins</ groupId > < artifactId >maven-assembly-plugin</ artifactId > < version >2.2</ version > < configuration > < outputDirectory >${project.build.directory}/site/downloads</ outputDirectory > < descriptors > < descriptor >src/main/assembly/project.xml</ descriptor > < descriptor >src/main/assembly/bin.xml</ descriptor > </ descriptors > </ configuration > </ plugin > </ plugins > </ build > < dependencies > < dependency > < groupId >junit</ groupId > < artifactId >junit</ artifactId > < version >4.5</ version > < scope >test</ scope > </ dependency > < dependency > < groupId >uk.co.jemos.podam</ groupId > < artifactId >podam</ artifactId > < version >2.3.11.RELEASE</ version > </ dependency > < dependency > < groupId >commons-io</ groupId > < artifactId >commons-io</ artifactId > < version >2.0.1</ version > </ dependency > <!-- XML binding stuff --> < dependency > < groupId >com.sun.xml.bind</ groupId > < artifactId >jaxb-impl</ artifactId > < version >2.1.3</ version > </ dependency > < dependency > < groupId >org.jvnet.jaxb2_commons</ groupId > < artifactId >jaxb2-basics-runtime</ artifactId > < version >0.6.0</ version > </ dependency > < dependency > < groupId >org.codehaus.woodstox</ groupId > < artifactId >stax2-api</ artifactId > < version >3.0.3</ version > </ dependency > </ dependencies > </ project > |
Только несколько вещей, чтобы заметить об этом pom.xml.
- Я использую Java 6, так как начиная с версии 6, Java содержит все библиотеки XML для JAXB, DOM, SAX и STax.
- Чтобы автоматически генерировать классы модели предметной области из схемы XSD, я использовал отличный плагин maven-jaxb2-, который позволяет, помимо прочего, получать POJO с поддержкой toString, equals и hashcode.
Я также объявил плагин jar, чтобы создать исполняемый файл jar для теста и плагин сборки для распространения исполняемой версии теста. Код для теста прилагается к этому сообщению, поэтому, если вы хотите собрать его и запустить его самостоятельно, просто разархивируйте файл проекта, откройте командную строку и запустите:
$ mvn чистая установка сборка: сборка
Эта команда поместит файлы * -bin. * В папку target / site / downloads. Разархивируйте одно из ваших предпочтений и запустите тестирование (-Dcreate.xml = true создаст файлы XML. Не передавайте его, если у вас уже есть эти файлы, например, после первого запуска):
$ java -jar -Dcreate.xml = true large-xml-parser-1.0.0-SNAPSHOT.jar
Создание тестовых данных
Для создания тестовых данных я использовал PODAM , инструмент Java для автоматического заполнения POJO и JavaBeans данными. Код так же прост, как:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
JAXBContext context = JAXBContext.newInstance( "xml.integration.jemos.co.uk.large_file" ); Marshaller marshaller = context.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8" ); PersonsType personsType = new ObjectFactory().createPersonsType(); List<PersonType> persons = personsType.getPerson(); PodamFactory factory = new PodamFactoryImpl(); for ( int i = 0 ; i < nbrElements; i++) { persons.add(factory.manufacturePojo(PersonType. class )); } JAXBElement<PersonsType> toWrite = new ObjectFactory().createPersons(personsType); File file = new File(fileName); BufferedOutputStream bos = new BufferedOutputStream( new FileOutputStream(file), 4096 ); try { marshaller.marshal(toWrite, bos); bos.flush(); } finally { IOUtils.closeQuietly(bos); } |
XmlPullBenchmarker генерирует три больших XML-файла в ~ / xml-benchmark:
- large-person-10000.xml (приблизительно 3 мес.)
- large-person-100000.xml (приблизительно 30 млн.)
- large-person-1000000.xml (приблизительно 300 м)
Каждый файл выглядит следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
<?xml version= "1.0" encoding= "UTF-8" standalone= "yes" ?> <person active= "false" > <firstName>Ult6yn0D7L</firstName> <lastName>U8DJoUTlK2</lastName> <address1>DxwlpOw6X3</address1> <address2>O4GGvxIMo7</address2> <postCode>Io7Kuz0xmz</postCode> <city>lMIY1uqKXs</city> <country>ZhTukbtwti</country> </person> <person active= "false" > <firstName>gBc7KeX9Tn</firstName> <lastName>kxmWNLPREp</lastName> <address1>9BIBS1m5GR</address1> <address2>hmtqpXjcpW</address2> <postCode>bHpF1rRldM</postCode> <city>YDJJillYrw</city> <country>xgsTDJcfjc</country> </person> [..etc] </persons> |
Каждый файл содержит 10 000/100 000/1 000 000 элементов <person>.
Беговые среды
Я попробовал бенчмаркер на трех разных средах:
- Ubuntu 10, 64-разрядная версия, работающая как виртуальная машина на Windows 7 Ultimate, с процессором i5, 750 @ 2,67 ГГц и 2,66 ГГц, 8 ГБ ОЗУ из которых 4 ГБ выделено для ВМ. JVM: 1.6.0_25, Hotspot
- Windows 7 Ultimate , на которой размещена вышеуказанная виртуальная машина, поэтому с тем же процессором. JVM, 1.6.0_24, Hotspot
- Ubuntu 10, 32-разрядная , 3 ГБ оперативной памяти, двухъядерный. JVM, 1.6.0_24, OpenJDK
XML демаршаллинг
Чтобы разобрать код, я использовал три разные стратегии:
- Чистый JAXB
- STax + JAXB
- Woodstox + JAXB
Чистый JAXB неустрашимый
Ниже приведен код, который я использовал для демаршаллинга больших файлов XML с использованием JAXB:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
private void readLargeFileWithJaxb(File file, int nbrRecords) throws Exception { JAXBContext ucontext = JAXBContext.newInstance( "xml.integration.jemos.co.uk.large_file" ); Unmarshaller unmarshaller = ucontext.createUnmarshaller(); BufferedInputStream bis = new BufferedInputStream( new FileInputStream(file)); long start = System.currentTimeMillis(); long memstart = Runtime.getRuntime().freeMemory(); long memend = 0L; try { JAXBElement<PersonsType> root = (JAXBElement<PersonsType>) unmarshaller.unmarshal(bis); root.getValue().getPerson().size(); memend = Runtime.getRuntime().freeMemory(); long end = System.currentTimeMillis(); LOG.info( "JAXB (" + nbrRecords + "): - Total Memory used: " + (memstart - memend)); LOG.info( "JAXB (" + nbrRecords + "): Time taken in ms: " + (end - start)); } finally { IOUtils.closeQuietly(bis); } } |
Код использует однострочную строку для демаршаллизации каждого XML-файла:
1
|
JAXBElement<PersonsType> root = (JAXBElement<PersonsType>) unmarshaller.unmarshal(bis); |
Я также получил доступ к размеру базовой коллекции PersonType, чтобы «прикоснуться» к данным в памяти. Кстати, отладка приложения показала, что все 10 000 элементов действительно были доступны в памяти после этой строки кода.
JAXB + STax
В STax мне просто нужно было использовать XMLStreamReader, перебрать все элементы <person> и передать каждый из них в JAXB, чтобы разобрать его в объектную модель PersonType. Код следует:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
// set up a StAX reader XMLInputFactory xmlif = XMLInputFactory.newInstance(); XMLStreamReader xmlr = xmlif.createXMLStreamReader( new FileReader(file)); JAXBContext ucontext = JAXBContext.newInstance(PersonType. class ); Unmarshaller unmarshaller = ucontext.createUnmarshaller(); long start = System.currentTimeMillis(); long memstart = Runtime.getRuntime().freeMemory(); long memend = 0L; try { xmlr.nextTag(); xmlr.require(XMLStreamConstants.START_ELEMENT, null , "persons" ); xmlr.nextTag(); while (xmlr.getEventType() == XMLStreamConstants.START_ELEMENT) { JAXBElement<PersonType> pt = unmarshaller.unmarshal(xmlr,PersonType. class ); if (xmlr.getEventType() == XMLStreamConstants.CHARACTERS) { xmlr.next(); } } memend = Runtime.getRuntime().freeMemory(); long end = System.currentTimeMillis(); LOG.info( "STax - (" + nbrRecords + "): - Total memory used: " + (memstart - memend)); LOG.info( "STax - (" + nbrRecords + "): Time taken in ms: " + (end - start)); } finally { xmlr.close(); } } |
Обратите внимание, что на этот раз при создании контекста мне пришлось указать, что это для объекта PersonType, и при вызове демаршаллинга JAXB мне пришлось также передать желаемый тип возвращаемого класса с помощью:
1
|
JAXBElement<PersonType> pt = unmarshaller.unmarshal(xmlr, PersonType. class ); |
Обратите внимание, что я ничего не делаю с объектом, просто создаю его, чтобы сохранить эталон как правдивый и возможный, не вводя никаких ненужных шагов.
JAXB + Woodstox
В случае с Woodstox этот подход очень похож на тот, который используется в STax. Фактически, Woodstox предоставляет API, совместимый с STax2, поэтому все, что мне нужно было сделать, — это обеспечить правильную фабрику и… взрыв! У меня был Woodstox под крышкой.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
private void readLargeXmlWithFasterStax(File file, int nbrRecords) throws FactoryConfigurationError, XMLStreamException,FileNotFoundException, JAXBException { // set up a Woodstox reader XMLInputFactory xmlif = XMLInputFactory2.newInstance(); XMLStreamReader xmlr = xmlif.createXMLStreamReader( new FileReader(file)); JAXBContext ucontext = JAXBContext.newInstance(PersonType. class ); Unmarshaller unmarshaller = ucontext.createUnmarshaller(); long start = System.currentTimeMillis(); long memstart = Runtime.getRuntime().freeMemory(); long memend = 0L; try { xmlr.nextTag(); xmlr.require(XMLStreamConstants.START_ELEMENT, null , "persons" ); xmlr.nextTag(); while (xmlr.getEventType() == XMLStreamConstants.START_ELEMENT) { JAXBElement<PersonType> pt = unmarshaller.unmarshal(xmlr,PersonType. class ); if (xmlr.getEventType() == XMLStreamConstants.CHARACTERS) { xmlr.next(); } } memend = Runtime.getRuntime().freeMemory(); long end = System.currentTimeMillis(); LOG.info( "Woodstox - (" + nbrRecords + "): Total memory used: " + (memstart - memend)); LOG.info( "Woodstox - (" + nbrRecords + "): Time taken in ms: " + (end - start)); } finally { xmlr.close(); } } |
Обратите внимание на следующую строку:
1
|
XMLInputFactory xmlif = XMLInputFactory2.newInstance(); |
Где я прохожу в STax2 XMLInputFactory. Это использует реализацию Woodstox.
Основной цикл
После того, как файлы на месте (вы получаете это путем передачи -Dcreate.xml = true), main выполняет следующее:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
System.gc(); System.gc(); for ( int i = 0 ; i < 10 ; i++) { main.readLargeFileWithJaxb( new File(OUTPUT_FOLDER + File.separatorChar + "large-person-10000.xml" ), 10000 ); main.readLargeFileWithJaxb( new File(OUTPUT_FOLDER + File.separatorChar + "large-person-100000.xml" ), 100000 ); main.readLargeFileWithJaxb( new File(OUTPUT_FOLDER + File.separatorChar + "large-person-1000000.xml" ), 1000000 ); main.readLargeXmlWithStax( new File(OUTPUT_FOLDER + File.separatorChar + "large-person-10000.xml" ), 10000 ); main.readLargeXmlWithStax( new File(OUTPUT_FOLDER + File.separatorChar + "large-person-100000.xml" ), 100000 ); main.readLargeXmlWithStax( new File(OUTPUT_FOLDER + File.separatorChar + "large-person-1000000.xml" ), 1000000 ); main.readLargeXmlWithFasterStax( new File(OUTPUT_FOLDER + File.separatorChar + "large-person-10000.xml" ), 10000 ); main.readLargeXmlWithFasterStax( new File(OUTPUT_FOLDER + File.separatorChar + "large-person-100000.xml" ), 100000 ); main.readLargeXmlWithFasterStax( new File(OUTPUT_FOLDER + File.separatorChar + "large-person-1000000.xml" ), 1000000 ); } |
Он приглашает GC к запуску, хотя, как мы знаем, это по усмотрению GC Thread. Затем он выполняет каждую стратегию 10 раз, чтобы нормализовать использование ОЗУ и ЦП. Окончательные данные затем собираются путем прогона в среднем по десяти прогонам.
Результаты тестов по потреблению памяти
Ниже приведены некоторые диаграммы, показывающие потребление памяти в разных рабочих средах при разборе 10 000/100 000/1 000 000 файлов.
Вы, вероятно, заметите, что потребление памяти для стратегий, связанных с STax, часто показывает отрицательное значение. Это означает, что после демонтажа всех элементов было больше свободной памяти, чем было в начале цикла демаршаллинга; это, в свою очередь, говорит о том, что GC работал намного больше с STax, чем с JAXB. Это логично, если подумать; поскольку с STax мы не храним все объекты в памяти, есть больше объектов, доступных для сборки мусора. В этом конкретном случае я считаю, что объект PersonType, созданный в цикле while, получает право на сборку мусора, попадает в область молодого поколения и затем восстанавливается сборщиком мусора. Это, однако, должно оказать минимальное влияние на производительность, поскольку мы знаем, что получение объектов из пространства молодого поколения выполняется очень эффективно.
Сводка для 10 000 элементов XML
Сводка для 100 000 элементов XML
Сводка для 1 000 000 элементов XML
Результаты теста скорости обработки
Результаты для 10 000 элементов
Результаты для 100 000 элементов
Результаты для 1 000 000 элементов
Выводы
Результаты во всех трех различных средах, хотя и с некоторыми отличиями, все говорят нам одну и ту же историю:
- Если вы ищете производительность (например, скорость разбора XML), выберите JAXB
- Если вы ищете нехватку памяти (и готовы пожертвовать некоторой скоростью работы), используйте STax.
Мое личное мнение также заключается в том, что я бы не стал использовать Woodstox, но я бы выбрал либо JAXB (если мне требовалась вычислительная мощность и я мог позволить себе оперативную память), либо STax (если мне не требовалась максимальная скорость и было мало ресурсов инфраструктуры). ). Обе эти технологии являются стандартами Java и являются частью JDK, начиная с Java 6.
Ресурсы Benchmarker исходный код
- Zip-версия: Загрузить Large-xml-parser-1.0.0-SNAPSHOT-проект
- Версия tar.gz: Загрузить Large-xml-parser-1.0.0-SNAPSHOT-project.tar
- Версия tar.bz2: Загрузить Large-xml-parser-1.0.0-SNAPSHOT-project.tar
Исполняемые файлы Benchmarker:
- Zip-версия: Загрузить Large-xml-parser-1.0.0-SNAPSHOT-bin
- Версия tar.gz: Загрузить Large-xml-parser-1.0.0-SNAPSHOT-bin.tar
- Версия tar.bz2: Загрузить Large-xml-parser-1.0.0-SNAPSHOT-bin.tar
Дата файлы:
- Среда с 64-битной виртуальной машиной Ubuntu : Загрузите Stax-vs-jaxb-ubuntu-64-vm
- 32-разрядная среда Ubuntu: загрузите 32-разрядную версию Stax-vs-jaxb-ubuntu
- Окружающая среда Windows 7 Ultimate: Загрузить Stax-vs-jaxb-windows7
Ссылка: эталонный тест XML в Java: JAXB против STax против Woodstox от нашего партнера по JCG