IBM ICU 简繁转换工具的性能坑

IBM ICU 简繁转换工具的性能坑

背景介绍

由于工作内容的需要,我们处理的文本数据中会有繁体中文,但是我们的产品的使用客户都是习惯使用简体中文(台湾是中国不可割舍的一部分),所以为了方便用户使用简体中文检索到繁体中文,我们需要在建立索引的时候(这里以solr为例子)进行简繁转换。简单的查看了Solr的文档,以及简单的百度或者Google, 我们可以在Solr(7.7.2)中添加以下的字段类型来进行中文分词和简繁转换。

1
2
3
4
5
6
7
8
9
10
11
<fieldType name="test"  class="solr.TextField" >
<analyzer>
<charFilter class="solr.HTMLStripCharFilterFactory"/>
<charFilter class="solr.MappingCharFilterFactory" mapping="mapping-ISOLatin1Accent.txt"/>
<tokenizer class="solr.StandardTokenizerFactory"/>
<filter class="solr.CJKWidthFilterFactory"/>
<filter class="solr.ICUTransformFilterFactory" id="Traditional-Simplified"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.CJKBigramFilterFactory" han="true" outputUnigrams="true"/>
</analyzer>
</fieldType>

可以看到这是最简单的中文分词器-> 二元分词,并且在二元的基础上输出了一元的结果,这里可以简单看下分词结果
二元+一元分词效果
似乎配置+使用起来也就5s(666),但是实际情况是性能不够,射不高,跑不快。

发现问题

因为是需要上线的服务,所以习惯性的需要在本地进行一些索引和查询的性能测试,为了测试索引的性能,我准备了2700万的文档数据(包含简体中文和繁体中文),进行压缩之后这些文档大小在135GB左右,基本上可以做一个长时间的性能摸底测试了。
我们简单的看下用户测试的机器配置

机器类型 CPU 配置 内存配置 磁盘配置
Solr服务机器 12 Core Intel(R) Xeon(R) CPU E5-2420 v2 @ 2.20GHz 30GB 7TB LVM
索引机器 4 Core Intel(R) Core(TM) i5-4590 CPU @ 3.30GHz 16GB 1TB HDD

并且索引机器Solr 服务机器 之前的网络为千兆带宽,所以网络上不会成为瓶颈。
并且Solr 的一些重要配置为

  • 内存配置为SOLR_JAVA_MEM=”-Xms6144m -Xmx6144m”
  • ramBufferSizeMB配置为

    1
    <ramBufferSizeMB>100</ramBufferSizeMB>
  • mergePolicyFactory 为

    1
    2
    3
    4
    5
    6
    <mergePolicyFactory class="org.apache.solr.index.TieredMergePolicyFactory">
    <int name="maxMergeAtOnce">30</int>
    <int name="segmentsPerTier">40</int>
    <double name="noCFSRatio">0</double>
    <int name="maxMergedSegmentMB">1024</int>
    </mergePolicyFactory>
  • mergeScheduler 为

    1
    2
    3
    4
    <mergeScheduler class="org.apache.lucene.index.ConcurrentMergeScheduler">
    <int name="maxMergeCount">12</int>
    <int name="maxThreadCount">6</int>
    </mergeScheduler>
  • autoCommit 的配置为 (防止频繁的autocommit生成太多的小段导致Solr一直在合并段)

    1
    2
    3
    4
    <autoCommit>
    <maxTime>${solr.autoCommit.maxTime:180000}</maxTime>
    <openSearcher>false</openSearcher>
    </autoCommit>

为了仅可能的用尽服务端的CPU,我在客户端开了20个线程往服务端丢数据,理论上应该可以将服务端的cpu跑满。但是实际的服务端cpu使用情况为Solr Server CPU usage
并且客户端的索引速度只有404 doc/s (因为和具体的文档的大小和内容有关系,速度的值并没有太多的参考意义,我们需要观察的更多的速度的变化率)
显然这个结果(cpu使用率)和预期的相差非常大?为啥还有那么多的cpu资源在idl ??

定位问题

为了搞清楚为啥会有这么多cpu在idl,最简单的办法就是使用一些jvm的profile工具,可以看到cpu把大量的资源消耗在什么地方,从而我们可以定位到发生问题的代码的类甚至某一行,这里我们使用的工具是VisualVM,当然为了能够让VisualVM能够访问服务端的jvm进程,我们需要对Solr进行一些配置,这个比较简单,主要是修改solr.in.sh文件的内容,直接上具体的配置点

1
2
3
4
5
SOLR_OPTS="$SOLR_OPTS -Dcom.sun.management.jmxremote"
SOLR_OPTS="$SOLR_OPTS -Dcom.sun.management.jmxremote.port=28983"
SOLR_OPTS="$SOLR_OPTS -Dcom.sun.management.jmxremote.ssl=false"
SOLR_OPTS="$SOLR_OPTS -Dcom.sun.management.jmxremote.authenticate=false"
SOLR_OPTS="$SOLR_OPTS -Djava.rmi.server.hostname=192.168.18.2"

重新开启服务端和客户端进行压力测试(之前的数据已经清理掉了),在程序运行了几分钟之后我们可以通过VisualVM的CPU profile功能发现,大量的cpu时间被消耗在了com.ibm.icu.text.RuleBasedTransliterator.handlerTransliterate() 方法上了,
VisualVM CPU Profile
回想了下Solr的中的字段的定了中有个solr.ICUTransformFilterFactoryfilter,显然就是这个filter耗费了大量的cpu时间(看包名和类型就能联想到),为了简单的验证是不是这个类导致的问题,我简单的做了下对比实验,我可以把这个filter注释掉看下服务端的cpu使用率和客户端的索引速度。直接修改schema中的field type定义

1
2
3
4
5
6
7
8
9
10
11
<fieldType name="test"  class="solr.TextField" >
<analyzer>
<charFilter class="solr.HTMLStripCharFilterFactory"/>
<charFilter class="solr.MappingCharFilterFactory" mapping="mapping-ISOLatin1Accent.txt"/>
<tokenizer class="solr.StandardTokenizerFactory"/>
<filter class="solr.CJKWidthFilterFactory"/>
<!-- <filter class="solr.ICUTransformFilterFactory" id="Traditional-Simplified"/> -->
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.CJKBigramFilterFactory" han="true" outputUnigrams="true"/>
</analyzer>
</fieldType>

然后重新开启服务端和客户端进行压力测试(之前的数据已经清理掉了),直接上cpu使用率和索引速度信息
Solr Server CPU usage
并且客户端的索引速度也飙到了900 doc/s,尼玛坑爹的ICUTransformFilterFactory,搞个简繁转换还能搞出个幺蛾子,再看下VisualVM 的cpu profile结果
VisualVM CPU Profile
症状100%消失,更加肯定就是ICUTransformFilterFactory搞出来的幺蛾子,怎么弄个简繁转换还能出这种问题,弱爆了。。。

源码分析

为了100%确认真的是ICUTransformFilterFactory导致的问题,我下载了Solr-7.7.2的源代码,并且进行了源码的debug,
最终在com.ibm.icu.text.RuleBasedTransliterator.handlerTransliterate()中找到的真正的问题所在,废话不说上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Deprecated
protected void handleTransliterate(Replaceable text, Position index, boolean incremental) {
RuleBasedTransliterator.Data var4 = this.data;
synchronized(this.data) {
int loopCount = 0;
int loopLimit = index.limit - index.start << 4;
if(loopLimit < 0) {
loopLimit = 2147483647;
}

while(index.start < index.limit && loopCount <= loopLimit && this.data.ruleSet.transliterate(text, index, incremental)) {
++loopCount;
}
}
}

可见看见大大的synchronized关键字,难怪嘛,Solr的索引是多线程的,并且在看了ICU的一些初始化过程之后,在简繁转换这个case上的RuleBasedTransliterator.Data只用初始化一次就好,(具体的过程比较复杂,简单的来说这中简繁转换的rule来自于配置文件,显然配置文件不可能加载多次),所以一旦上了多线程就各种卡,cpu全消耗在内斗上了,哎。。。。,不过好消息是这个RuleBasedTransliterator类已经被打上了Deprecated标签了,所以估计在高版本的icu api中这个问题会不复存在。

解决方案

最简单的解决方案就是等—> 等官方干掉这个类,这样期待在未来的版本中icu不再使用这个坑爹的synchronized关键字,当然这显然不是最好的版本,当然还有一种相对简单的方案就是直接干掉synchronized关键字,然后重新编译打包一下就好,如果只是为了写这篇博客确实可以这么干,因为后期的维护成本为0,但是在真是的企业开发中还是不太可取的,如果solr今后版本升级,我们也要下载对应的icu包然后去掉synchronized关键字,然后在重新编译打包等操作,虽然说是一次性工作,但是相当于我们间接的在维护icu的源码了。
还有一种最暴力的做法,但是却是我比较推荐的做法,我们可以自己造一个简繁转换的solrplugin,确实,只是简单的简体中文到繁体中文的转换,我们只需要找到简繁的对照表就可以做了。说干就干,我直接提取了icu包中的简体中文和繁体中文的对照表(当然获取途径有多种,甚至wiki上都有),这里是我提取的简体繁体对照表,有了简繁转换表,那么solrplugin也就相对简单了,不多说,直接上代码,为了验证问题是否解决,我简单做了下对比测试,索引速度直接从原来的401/s上升到了936/s ,服务端的cpu使用率也能够飙升至90%左右,直接上图Solr Server CPU Usage至此icu导致的性能问题,已经得到解决。