目录
一、项目说明
该项目用于调用第三方图片识别接口,识别出拍摄图片中的饮品sku数量及排面数量,用于计算销售人员巡店拜访商超店面公司饮品的陈列质量。
用户用手机拍摄图片是存储在阿里云对象存储oss上的,客户端将图片在oss的url作为请求参数传递给服务端,服务端接收到请求后将url转为图片base64用于调用第三方接口获取识别结果。
二、第一次优化
由于读取到的部分图片有翻转情况(手机拍摄导致图片旋转90度),第三方服务无法识别出有翻转情况的图片内容,所以需要服务端自行判断图片是否有翻转情况并矫正,以下是工具类代码:
/**
* 获取图片旋转角度
* @param imageUrl
* @return 图片角度
*/
public static int getAngle(URL imageUrl) throws Exception {
InputStream intstream = null;
try{
intstream = imageUrl.openStream();
Metadata metadata = ImageMetadataReader.readMetadata(intstream);
for (Directory directory : metadata.getDirectories()) {
for (Tag tag : directory.getTags()) {
if ("Exif IFD0".equals(tag.getDirectoryName()) && "Orientation".equals(tag.getTagName())) {
String orientation = tag.getDescription();
if (orientation.contains("90")) {
return 90;
} else if (orientation.contains("180")) {
return 180;
} else if (orientation.contains("270")) {
return 270;
}else {
return 0;
}
}
}
}
}finally {
if(intstream != null) {
intstream.close();
}
}
return 0;
}
/**
* 纠正图片旋转并返回base64编码
* @param imageUrl
* @param angle
* @return 图片base64编码
*/
public static String correctImgAndToBase64(URL imageUrl, int angle) throws Exception {
BufferedImage srcImg = null;
BufferedImage srcImgNew = null;
Graphics graphics = null;
BufferedImage targetImg = null;
Graphics2D g = null;
ByteArrayOutputStream outputStream = null;
try {
// 原始图片缓存
srcImg = ImageIO.read(imageUrl);
// 宽高互换
// 原始宽度
int imgWidth = srcImg.getHeight();
// 原始高度
int imgHeight = srcImg.getWidth
// 中心点位置
double centerWidth = ((double) imgWidth) / 2;
double centerHeight = ((double) imgHeight) / 2;
// 图片缓存
targetImg = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_RGB);
// 旋转对应角度
g = targetImg.createGraphics();
g.rotate(Math.toRadians(angle), centerWidth, centerHeight);
g.drawImage(srcImg, (imgWidth - srcImg.getWidth()) / 2, (imgHeight - srcImg.getHeight()) / 2, null);
g.rotate(Math.toRadians(-angle), centerWidth, centerHeigh);
outputStream = new ByteArrayOutputStream();
ImageIO.write(targetImg, "jpg", outputStream);
// 对字节数组Base64编码
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(outputStream.toByteArray());// 返回Base64编码过的字节数组字符串
}finally {
if(Objects.nonNull(outputStream)) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(Objects.nonNull(g)) {
g.dispose();
}
if(Objects.nonNull(targetImg)) {
targetImg.getGraphics().dispose();
}
if(Objects.nonNull(graphics)) {
graphics.dispose();
}
if(Objects.nonNull(srcImgNew)) {
srcImgNew.getGraphics().dispose();
}
if(Objects.nonNull(srcImg)) {
srcImg.getGraphics().dispose();
}
}
}
三、OOM事故的发生与分析
监控发现识别服务有明显堆积,大量用户反馈图片识别结果很慢。查询nacos注册的3台服务下线了2台,查询日志发现发生了oom,导出hprof文件后使用jdk自带的jvisualvm进行分析
1、使用scp命令导出线上服务器的hprof文件到本地
scp 服务器用户名@服务器ip:服务器文件路径 本地路径
2、进入jdk安装目录,进入bin文件夹,启动jvisualvm
/Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home/bin
3、启用jvisualvm后载入hprof文件
4、点击跳转查看导致 OutOfMemoryError 异常错误的线程
找到了项目中的代码,结合堆栈信息上下文,找到最终发生oom的代码位置,根据上面的图跟踪查看以下几个位置代码:
at java.awt.image.DataBufferInt.<init>(DataBufferInt.java:75)
Local Variable: java.awt.image.DataBufferInt#1
at java.awt.image.Raster.createPackedRaster(Raster.java:467)
Local Variable: int[]#203
at java.awt.image.DirectColorModel.createCompatibleWritableRaster(DirectColorModel.java:1032)
at java.awt.image.BufferedImage.<init>(BufferedImage.java:324)
5、根据堆栈信息得出结论:
关键代码是 BufferedImage targetImg = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_RGB);
可以看到传入图片的宽和高最终会创建一个DataBuffer d = new DataBufferInt(w*h);
然后创建一个int[],而数组长度大小是图片的宽*高,如果图片很大,这个int[]就会很大,排查事故发生时的图片大小,发现部分图片竟然有20多M,这是导致oom的根本原因
四、解决方案
知道问题导致的原因是图片过大后,想到的解决方案是:判断图片大小如果超过某个值时可以将图片等比例缩小,然后再进行后续的纠正图片旋转和调用第三方接口识别图片内容
五、最终优化代码
/**
* 纠正图片旋转并base64编码
* @param imageUrl
* @param angle
* @return 图片base64编码
*/
public static String correctImgAndToBase64(URL imageUrl, int angle) throws Exception {
BufferedImage srcImg = null;
BufferedImage srcImgNew = null;
Graphics graphics = null;
BufferedImage targetImg = null;
Graphics2D g = null;
ByteArrayOutputStream outputStream = null;
try {
// 原始图片缓存
srcImg = ImageIO.read(imageUrl);
// 这里添加判断,如果图片宽度大于4096时,将图片等比例缩小。为什么选择4096呢?因为第三方识别接口建议图片最长边不大于4096
if(srcImg.getWidth() > 4096) {
int targetWidth = srcImg.getWidth() * 20 /100;
int targetHeight = srcImg.getHeight() * 20 /100;
srcImgNew = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
graphics = srcImgNew.getGraphics();
//将原始位图缩小后绘制到bufferedImage对象中
graphics.drawImage(srcImg,0,0,targetWidth,targetHeight,null);
srcImg = srcImgNew;
}
// 宽高互换
// 原始宽度
int imgWidth = srcImg.getHeight();
// 原始高度
int imgHeight = srcImg.getWidth
// 中心点位置
double centerWidth = ((double) imgWidth) / 2;
double centerHeight = ((double) imgHeight) / 2;
// 图片缓存
targetImg = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_RGB);
// 旋转对应角度
g = targetImg.createGraphics();
g.rotate(Math.toRadians(angle), centerWidth, centerHeight);
g.drawImage(srcImg, (imgWidth - srcImg.getWidth()) / 2, (imgHeight - srcImg.getHeight()) / 2, null);
g.rotate(Math.toRadians(-angle), centerWidth, centerHeigh);
outputStream = new ByteArrayOutputStream();
ImageIO.write(targetImg, "jpg", outputStream);
// 对字节数组Base64编码
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(outputStream.toByteArray());// 返回Base64编码过的字节数组字符串
}finally {
if(Objects.nonNull(outputStream)) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(Objects.nonNull(g)) {
g.dispose();
}
if(Objects.nonNull(targetImg)) {
targetImg.getGraphics().dispose();
}
if(Objects.nonNull(graphics)) {
graphics.dispose();
}
if(Objects.nonNull(srcImgNew)) {
srcImgNew.getGraphics().dispose();
}
if(Objects.nonNull(srcImg)) {
srcImg.getGraphics().dispose();
}
}
}