1. 精准营销
在一次营销活动中, 用户填写电子问卷,然后我们根据用户填写的问卷信息去给用户推荐相关产品
由于推荐规则的多样性,易变性,以及时效性,须支持动态配置规则
一开始我考虑似乎可以用策略模式,后来发现完全不是一回事, 一般策略模式是根据某个字段的不同值选择不同的处理逻辑,但此处有多个字段,不同字段的组合有不同的逻辑,所以策略模式 pass;
后来考虑用责任链模型, 每一种规则判断是责任链的一环,将每个逻辑封装到一个方法中,然后将类信息配置到数据库,这样就可以组合判断, 但是这种模式有一个问题,就是每个逻辑一个方法,需要写在java代码里,这样不就是硬编码了吗, 考虑到规则很容易改变,所以也不考虑这个方案
最后研究发现有个规则引擎可以满足需求, 将判断逻辑配置在数据库中,根据一些标签可以批量拉取到对应的规则以及符合规则的推荐结果, 统一执行,这样就做到了推荐逻辑的动态可配置
规则引擎我是用jexl3实现的,可以配置js代码块,包装下可以执行java类
2. SAAS多租户方案
saas系统由一个单租户系统改造而来, 故需要在原系统上增加多租户代码改造
首先考虑的是库隔离,使用动态数据源的方式去切换库,但是很明显 如果saas租户少的话没问题,如果租户一多起来肯定对资源由极大浪费
其次我们考虑表隔离,发现也不现实,表实在太多了,而且租户多起来肯定也不行
后来我们考虑引入一张映射表,每次查库先查映射表,找到对应租户的数据id后再去查实际表, 考虑到映射字段难以确认,比如我需要根据创建时间范围查询,这个就很难操作,映射表中一般只存租户id与对应数据id的关系, 这样很容易导致索引失效
最后我们决定只改造orm层,使用mybatis拦截器对sql进行拦截,拼接多租户字段实现,这导致部分表索引失效,目前没有更好的方案
我个人认为saas的核心就是不同租户的需求定制和资源要求不一样,需求定制有时候很难抽象出来,资源的话也难评估,要考虑租户后期的扩缩容问题, 很多时候会出现执行大租户操作拖垮小租户,或者多个小租户拖垮大租户的情况, 不过这种问题也好解决,大租户单独拿出来单独部署数据库等资源, 其他中小租户直接扔saas中,毕竟saas的面向客户都是中小型为主的企业.
3. MinorGC问题
闲来无事观看每个服务的监控,突然发现堆内存使用为有规律的峰谷, 但是这个服务没啥流量,半夜里竟然也是这种规律波形图, 猜想肯定是哪个代码里在不停得new对象出来;
//启动参数
java -DAPP_DOMAIN=nova-activity -Xms1900m -Xmx1900m -XX:NewRatio=1 -XX:MaxDirectMemorySize=2m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -jar nova-activity.jar
可以看到分配固定得1900MB堆内存,new ratio的比例是1,即 新生代老年代1:1 ,默认应该是1:2的,这里故意调大,因为这个项目是活动项目,有可能瞬时并发上去,而且有大数据量的文件读写,很需要新生代的大空间
然后使用jinfo看一下参数
Non-default VM flags: -XX:CICompilerCount=3 -XX:InitialHeapSize=1992294400
-XX:MaxDirectMemorySize=2097152
-XX:MaxHeapSize=1992294400
-XX:MaxMetaspaceSize=536870912
-XX:MaxNewSize=996147200
-XX:MetaspaceSize=268435456
-XX:MinHeapDeltaBytes=524288
-XX:NewRatio=1
-XX:NewSize=996147200
-XX:OldSize=996147200
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-XX:+UseFastUnorderedTimeStamps -XX:+UseParallelGC
没毛病 新生代老年大空间都是996147200Byte,约996MB,
再看下服务内存使用图,差不多每次都是到900MB的时候内存就掉下去了
问题定位就是minorGC异常, 频率倒不高,10分钟一次,也不存在内存泄漏,不用管他也没问题
生产: jmap一下拿到内存以及线程结构,MAT分析下......
本地: 直接jvisualvm,鼠标点点就行了
首先映入眼帘的是线程结构
redisson为啥这么多活动线程?? 而且redisson配置的参数是5个active线程,这点存疑,至今未解决
不过看下数据是没啥问题的,虽然线程多,却没消耗什么资源
再看下堆内存, 内存占用最大的当然是byte[],string,object[], 看的头疼,没意思,估计不是这个问题
排查很久,突然发现有个线程在疯狂分配内存
RMI connection!!
这居然是jdk的东西,jdk有bug??肯定不可能,细想一下,不会是监控引起的吧
试验下,随便写个main方法,sleep住,打开jvisualvm看下,果然,也是这个线程引起的,
线上监控同理也会引起这个问题, 不过10分钟一次minorGC 一点毛病也没有, 正常高并发项目可能几十秒一次minorGC吧
对于minorGC,频率是相当高的,我们需要关注两个点,
- GC的STW时间,正常一般几十毫秒,如果达到秒级别,就得排查下了
- 对象存活时间是否太长? 主要原因比如高并发量挤占资源(或者锁竞争局部变量周期变长),string table里字符串太多(YGC扫描时间会变长,常见string.intern),缓存使用不当,累计大量对象不被释放,
排查方法:
- 局部变量的原因, 考虑并发数等原因
- 全局变量或者静态变量,可以排查下大对象,但是一般全局变量都是在线程启动的时候加载,内存基本不太会膨胀,这种情况下很快就会进入老年代的
- 看下线程堆栈,哪些线程的内存占用大或者内存增量大
- 最终只能看堆内存啦,大对象,多对象一个一个排查,总归会排查到的
4. 线上直接内存OOM问题
由于线上有个定时任务需要去扫描数据库里某张表的全量数据,然后写入某个文件上传 由于数据量不小 (百万级别),并且在快速膨胀,所以考虑分片读写 在分片读写的过程中,我使用了filechannel的类(filechannel在写大文件的时候是有一个直接内存以及零拷贝的优化),性能上应该会快一些,大概的实现方法是这样的
RandomAccessFile rcf = new RandomAccessFile(fName, "rw");
FileChannel channel = rcf.getChannel();
int outLoopSize = 100;
int innerLoopSize = 10000;
long offset = 0;
try {
for (int i = 0; i < outLoopSize; i++) {
StringBuilder builder = new StringBuilder();
//造数据
for (int j = 0; j < innerLoopSize; j++) {
builder.append("139xxxxxxxx").append("||").append("||").append("固定值").append("||").append("139xxxxxxxx")
.append("||").append("0").append("||").append("12324123").append("||").append("139xxxxxxxx")
.append("||").append("三个字").append("||").append("3204811995432111231").append("||")
.append("2").append("||").append("0").append("||").append("123412").append("||")
.append("||").append("||").append(new Date()).append("||").append("X100").append("\n");
}
byte[] bytes = builder.toString().getBytes();
System.out.println("分配直接内存空间" + bytes.length);
//写文件
channel.write(ByteBuffer.wrap(bytes), offset);
channel.force(true);
offset += bytes.length;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
channel.close();
rcf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
channel.write方法如果传入的是HeapByteBuffer,则会在内存创建一个临时直接内存,然后将堆内存buffer拷贝过去,减少内核态与用户态的来回切换(因为写文件的时候用的nio,当写入缓冲区不够时会直接返回,这样就会引起内核态与用户态的来回切换)
然后就出现了一个奇怪的现象,在一次调用中,无论怎么去创建临时直接内存,每次gc都会回收掉,也就不会发生oom 但是在多次调用中,居然会OOM,这里的多次调用理解为多线程调用
问题方法定位:
查看channel.write方法,底层使用的时IOUtil.write方法(循环调用这个方法)
其内部又分配了临时直接内存,Util.getTemporaryDirectBuffer()内部如下
public static ByteBuffer getTemporaryDirectBuffer(int var0) {
if (isBufferTooLarge(var0)) {
return ByteBuffer.allocateDirect(var0);
} else {
Util.BufferCache var1 = (Util.BufferCache)bufferCache.get();
ByteBuffer var2 = var1.get(var0);
if (var2 != null) {
return var2;
} else {
if (!var1.isEmpty()) {
var2 = var1.removeFirst();
free(var2);
}
return ByteBuffer.allocateDirect(var0);
}
}
}
很明显,这里有两个点:
- isBufferTooLarge,内部是根据MAX_CACHED_BUFFER_SIZE判断,这个值受jdk.nio.maxCachedBufferSize参数控制,如果这个参数为空,则为无限大
- bufferCache, 居然又cache, 而且点进去,threadlocal对象,完犊子,肯定时直接内存对象被缓存住了,而且是每个线程缓存一次,这就可以解释OOM的问题了
问题找到了,channel.write(ByteBuffer.wrap(bytes), offset)这个地方有问题, 那么我们可以针对这个点优化下
RandomAccessFile rcf = new RandomAccessFile(fName, "rw");
FileChannel channel = rcf.getChannel();
int outLoopSize = 10;
int innerLoopSize = 2000;
long offset = 0;
//创建局部变量 bytebuffer ,这个对象被gc掉则会回收对应的直接内存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(2 * 1024 * 1024);
try {
for (int i = 0; i < outLoopSize; i++) {
StringBuilder builder = new StringBuilder();
for (int j = 0; j < innerLoopSize; j++) {
builder.append(i + "39xxxxxxxx").append("||").append("||").append("固定值").append("||").append("139xxxxxxxx")
.append("||").append("0").append("||").append("12324123").append("||").append("139xxxxxxxx")
.append("||").append("三个字").append("||").append("3204811995432111231").append("||")
.append("2").append("||").append("0").append("||").append("123412").append("||")
.append("||").append("||").append(new Date()).append("||").append("X100").append("\n");
}
byte[] bytes = builder.toString().getBytes();
System.out.println("bytes大小" + bytes.length);
byteBuffer.put(bytes, 0, bytes.length);
byteBuffer.flip();
channel.write(byteBuffer); //写入直接内存
channel.force(true);
byteBuffer.clear();
offset += bytes.length;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
channel.close();
rcf.close();
} catch (IOException e) {
e.printStackTrace();
}
}