1. U2000AICollectorService微服务
描述:U2000AICollectorService是存量采集服务,包括全量采集和增量采集,主要职责为从网管和统一存量库中采集存量CSV文件。我在这个服务中主要负责增量采集模块。
-
全量采集
-
增量采集
增量同步过程中涉及到Kafka消息机制,以生存者与消费者的模式实现,管理域方作为消息的生产者,一旦统一存量的资源发生变化(存量出现增删改操作),就会生成一条消息(Topic)发送到Kafka队列中;U2000AI采集服务方作为消息的消费者,每5S的时间去轮训队列,并订阅相应的Topic,在获取到相应的Topic后,会对Topic进行整理合并(订阅到的Topic不是有序的,需要对Topic以时间先后顺序进行合并,得出单条资源的增删改操作),最后写入增量CSV文件中。
难点: 消息是无序且不经过处理的,也就是有可能对于同一条资源而言,在一秒内可能有多次操作,并将此多次操作都生成Topic发送到队列中。但是消费者不能保证获取到消息是有序的。因此,需要对获取到的Topic进行排序合并,计算出该段时间内资源的操作(例如,先增后删相当于不处理)
2. PrepareService微服务
描述
存量预处理服务,将采集的原始存量CSV文件,按照业务定制的配置文件以及规则,对原始文件进行字段转换,得到处理过后的CSV文件,再根据旧的数据和新存量数据进行比较,得出存量的增删改数据,最后将增删改数据调用InvDCService服务插入/更近/删除到InvDC数据库中。
技术实现
整个实现过程使用Spring的依赖注入进行流程控制。处理逻辑Handler以及策略Strategy均配置在Spring的xml文件中,以Bean的形式定义。
3. UtrafficInvIDGenService微服务
描述
UtrafficInvIDGenService是生成并维护UUID的服务,主要维护了资源ID(ResId)、外键(ExId)和资源UUID的对应关系。
生成UUID过程
根据ResId或ExId生成对应的LongId(也就是UUID),最后将ResId、ExId、UUID以及HashID存放到数据库中,并且将ResId和UUID的映射关系存放到Redis缓存中。
获取UUID过程
先根据ResId去Redis缓存中找,如果找不到再去数据库中找,返回结果(查询到的结果或空)。
存量同步过程生成UUID的接口逻辑
首先判断,该条资源是否已经存在与IDGen数据库中:先根据ResId从Redis缓存中查找,如果找得到则返回数据,如果找不到,再根据EXID的hashId(SHA1算法)去IDGen数据库中查找,如果此时找得到则返回数据,如果找不到则创建一条新的数据入库并更新缓存。
优化:查询速度慢(生成UUID的速度慢):添加Redis缓存以及使用EXID的SHA1算法生成HashID,加快查询速度。
4. InvDCService微服务
描述
提供存量入库接口
5. 增量同步
- 实现:复用了全量同步的流程
- 难点:增量过来的存量信息不完整,容易出现某些资源的字段为空
- 解决:当某些依赖其他资源的字段为空时,读取数据库的信息,获取相应的字段值并填充上
6. 内存优化
工具/手段:使用jdk自带的工具jmap抓取堆栈文件(dump文件)、或者通过配置JVM参数-XX:HeapDumpOnOutOfMemoryError可以得到OOM时的内存快照,再利用Memory Analyzer Tool(MAT)工具进行分析dump文件。
缩短InvResTypeCacheMgr缓存生命周期
通过排查dump文件发现InvResTypeCacheMgr缓存占用内存很大,通过分析代码(调用链)得出该缓存是随着资源的处理而叠加的,没有及时释放。同时,该缓存存放的数据只会在当前资源的处理过程中使用到,后序资源的处理不会再用到该缓存的数据。
做法:在处理完单个资源时,释放该资源对应的缓存数据,避免内存堆积。
重构分解InvTempCacheMgr缓存
重构分解InvTempCacheMgr缓存(该缓存存储的数据比较多,且比较杂),分解成多个缓存,分别存储明确的资源数据,同时在适当的时机释放后序不再使用的缓存。
优化InvRelationCacheMgr缓存的数据结构
优化InvRelationCacheMgr缓存的数据结构,其中该缓存的key为FDN格式类型的数据(例如EM=1638476_123,NE=123456,FR=789),但是该类型的数据一般比较长,占用的字符空间多,一旦在大的数据量场景下,会占用很大的内存。通过将该key更换成long型的UUID,long型数据只占用8个字节,而FDN是一个字符串对象,占用的40+2*n字节
内存削峰
分批分桶操作,避免一次性读取过多的数据到内存中导致OOM
缓存结构优化
本身HashMap就是一个很占用内存的对象,使用LevelDB替换HashMap,该缓存是将一部分热点数据放到内存中,一部分数据放到磁盘中,总体不会对处理速度造成很大的影响。(但是LevelDB有个问题,会莫名其妙丢数据,后来遗弃LevelDB)
附:String字符串对象占用的内存空间计算:
private final char value[];
private int hash;
private static final long serialVersionUID = -6849794470754667710L;
在 Java 里数组也是对象,因而数组也有对象头,故一个数组所占的空间为对象头所占的空间加上数组长度加上数组的引用,即 8 + 4 + 4= 16 字节 。
那么一个空 String 所占空间为:对象头(8 字节)+ 引用 (4 字节 ) + char 数组(16 字节)+ 1个 int(4字节)+ 1个long(8字节)= 40 字节。
String占用内存计算公式:40 + 2*n,n为字符串长度。
7. 存量同步效率优化
PrepareService中的优化
优化部分处理逻辑,时间复杂度从O(N平方)降低到O(N)
优化前的代码:
// 找到该资源中fdnid对应的缓存值,fdnid有可能带有EM头,有可能不带有
private Map<String, Map<String, String>> fdnid2UUID = new HashMap<>();
for (Map.entry<String, String> entry : fdnid2UUID.get(modelName).entrySet()) ---- N
{
if (entry.getKey().endsWith(fdnid) || fdnid.endsWith(entry.getKey())) ---- N
{
return entry.getValue();
}
}
优化后的代码:
private Map<String, Map<String, String>> fdnid2UUID = new HashMap<>();
if (fdnid2UUID.get(modelName).containsKey(fdnid))
{
return fdnid2UUID.get(modelName).get(fdnid);
}
if (fdnid.startsWith(“EM”))
{
if (fdnid2UUID.get(modelName).containsKey(dealWithEMField(fdnid)))
{
return fdnid2UUID.get(modelName).get(dealWithEMField(fdnid));
}
}
else
{
String newFdnid = “EM=XXX,”+ fdnid;
if (fdnid2UUID.get(modelName).containsKey(newFdnid))
{
return fdnid2UUID.get(modelName).get(newFdnid);
}
}
分析: HashMap中自带有一个containsKey方法,该方法在Hash正常运行时,查找一个Key的时间复杂度为O(1),分析如下:
HashMap的get()方法获取一个数据的步骤为
- 先计算出该Key的hash值,再得到该Key的table下标;
- 找到table下标后,遍历链表,直到找到该key对应的值为止
为保证containsKey的时间复杂度为O(1),则上述两个过程的时间复杂度都必须为1.找到数组的下标幅度为为1,遍历链表的时间复杂度为O(N),如果hash散列算法正常运作的话,理想情况下,不会造成Hash冲突,即链表的长度为1,因此遍历链表的时间复杂度为O(1)。最糟糕的情况下,containsKey的时间复杂度为O(N)。
8. UtrafficInvIDGenService中的优化
Redis缓存
将resId和UUID的关系放在缓存中,降低数据库压力,同时也提升效率
分批多次改为文件接口
PrepareService中将resId和ExId写入文件一次性发送给IDGen服务(发送文件路径),原本PrepareService是将resId和ExId分批次请求到IDGen中获取UUID的,当数据量很大时,一个资源将会分出很多批次,在浪费了网络资源以及增加了处理时间。通过一次性写入文件并发送的过程,可以减去网络资源上的浪费。
仍可以优化的地方
IDGen在读取resId和ExId的文件时,是单线程分批读取的,此处可以做成多线程分批读取,提升处理速度。
9. 静态清理
FindBugs、PMD、Codex等。培养良好的编程习惯、提高代码质量
10. 需求开发(做了哪些需求,背景,实现)
需求介绍
背景
实现
优化
11. 这个系统还有哪些可以优化的地方?
资源处理顺序解耦
描述:资源有依赖,会先等依赖的资源处理完,才会继续处理下一个资源。
- 只能单线程执行,无法启动多线程,效率会慢
- 占用较多的内存,很多依赖资源的数据会存在缓存中,知道整个流程结束,造成了内存堆积
- 如果某个资源处理出现异常,依赖该资源的存量也会出现异常,形成了连锁反应,可靠性差
生成UUID的效率比较慢
实现:新增redis缓存(已完成)
增量同步
描述:存量不完整并且有依赖,有可能出现处理出错(某些字段获取不到依赖数据,导致为空)
12. 工作中比较有意思/技术含量的地方
使用JDK工具调试查看内存:jstat、jmap、jvisualvm监控内存
常用:
- jstat -gc {pid} 2000 每2秒查看当前进行的gc情况(新生代[Survivor、Eden区]、老年代等等)
- jmap -F -dump:format=b,file=/opt/tools/heap.bin {pid}
- jvisualvm是远程监控内存的性能检测工具