一、背景
最近接到一个需求,公司的一个演示站点上的图片加载非常慢,首次加载需要10s往上,影响用户使用体验,需要优化一下。
首先明确的是站点是部署在华东地区的阿里云服务器上,且后续会有非华东地区的用户使用,如华北或者国外用户。
其次由于这个站点只是演示性质,前期做的较为粗糙,图片直接在多端传输,影响性能。
二、方案
1、阿里云OSS+传输加速
在看到上述背景之后,首先想到的是既然站点是部署在阿里云服务器上,那是否可以借助阿里云的OSS服务来提高图片的上传下载速度。
查阅相关资料之后,常用的方案是OSS+CDN,但是阿里云OSS还直接提供传输加速服务,于是可选的方案就变成了CDN和传输加速的比较。
我们来看看两者有什么区别
- CDN加速OSS:是建立并覆盖在承载网之上,由遍布全球的边缘节点服务器群组成的分布式网络。阿里云CDN能分担源站压力,避免网络拥塞,确保在不同区域、不同场景下加速网站内容的分发,提高资源访问速度。由CDN全球广泛分布的边缘节点缓存OSS存储的静态数据,从而实现客户端从边缘节点直接获取数据的方式来实现访问的加速。
- OSS传输加速:利用全球分布的云机房,将全球各地用户对您存储空间(Bucket)的访问,经过智能路由解析至就近的接入点,使用优化后的网络及协议,为云存储互联网的上传、下载提供端到端的加速方案。
说白了就是阿里云CDN是利用边缘节点缓存进行加速,而传输加速则是对传输链路以及协议策略进行优化,两者还是有本质上的区别的。
最后综合考虑了一下时间以及改造的难易程度,最后还是采用了阿里云的传输加速(偷懒.jpg)
即最终方案是阿里云OSS+传输加速(需额外付费)。
2、服务端签名直传
由于站点上有上传图片的需求,之前是直接传输图片到应用服务器,网络消耗大,速度慢。
和数据直传到OSS相比,存在以下缺点:
- 上传慢:用户数据需先上传到应用服务器,之后再上传到OSS,网络传输时间比直传到OSS多一倍。如果用户数据不通过应用服务器中转,而是直传到OSS,速度将大大提升。而且OSS采用BGP带宽,能保证各地各运营商之间的传输速度。
- 扩展性差:如果后续用户数量逐渐增加,则应用服务器会成为瓶颈。
- 费用高:需要准备多台应用服务器。由于OSS上行流量是免费的,如果数据直传到OSS,将节省多台应用服务器的费用。
现在采用了阿里云OSS+传输加速,需将Web端改造为直传阿里云OSS,提高上传速度,端与端之间的图片皆通过url传输。
Web端直传OSS又有以下三种方案:
- 利用OSS Browser.js SDK将文件上传到OSS
- 使用表单上传方式将文件上传到OSS
- 通过小程序上传文件到OSS
我们采用表单上传方式中的服务端签名直传
服务端签名直传是指在服务端生成签名,将签名返回给客户端,然后客户端使用签名上传文件到 OSS 。由于服务端签名直传无需将访问密钥暴露在前端页面,相比客户端签名直传具有更高的安全性
三、实践
1、新建bucket,并开启传输加速
1.1 创建子用户
注意保存生成的accesskey
1.2 新建bucket
新建bucket,读写权限设置为公共读
1.3 跨域策略
1.4 生命周期设置(可忽略)
上传的的临时文件删除
1.5 开启传输加速
注意:开启之后会有传输加速域名,通过此域名才进行传输加速,并会单独付费
2、后端改造
2.1 Gradle配置SDK
implementation 'com.aliyun.oss:aliyun-sdk-oss:3.17.1'
2.2 OSS初始化
@Data
@Component
@ConfigurationProperties(prefix = "xxx.xxx")
public class AliyunOSSProperties {
private String endpoint;
private String accessKey;
private String secretKey;
private Long expire;
}
@Data
@Configuration
public class AliyunOSSConfiguration {
@Autowired
private AliyunOSSProperties aliyunOSSProperties;
@Bean
public OSS ossClient() {
return new OSSClientBuilder().build(
aliyunOSSProperties.getEndpoint(),
aliyunOSSProperties.getAccessKey(),
aliyunOSSProperties.getSecretKey());
}
}
2.3 接口
//开启OSS客户端
long expireTime = aliyunOSSProperties.getExpire();
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
PolicyConditions policyConditions = new PolicyConditions();
policyConditions.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
policyConditions.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
//根据到期时间生成policy
Date expiration = new Date(expireEndTime);
String postPolicy = oss.generatePostPolicy(expiration, policyConditions);
//对policy进行UTF-8编码后转base64
byte[] binaryData = new byte[0];
try {
binaryData = postPolicy.getBytes("utf-8");
} catch (UnsupportedEncodingException e) {
throw new BusinessException("阿里云上传失败:", e);
}
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
//生成signature
String postSignature = oss.calculatePostSignature(postPolicy);
AliyunOSSPolicyCO aliyunOSSPolicyCO = new AliyunOSSPolicyCO();
aliyunOSSPolicyCO.setAccessKey(aliyunOSSProperties.getAccessKey());
aliyunOSSPolicyCO.setPolicy(encodedPolicy);
aliyunOSSPolicyCO.setSignature(postSignature);
aliyunOSSPolicyCO.setExpire(expireEndTime);
return aliyunOSSPolicyCO;
3、前端改造
前端采用的是Ant Design的Upload组件
import React, {useEffect, useState} from 'react';
import {UploadOutlined} from '@ant-design/icons';
import type {UploadProps} from 'antd';
import {Button, message, Upload} from 'antd';
import type {UploadFile} from 'antd/es/upload/interface';
import {generateSign} from '@/services/xx/AliyunOSS';
import styles from "./index.less";
interface AliyunOSSUploadProps {
value?: UploadFile[];
onChange?: (fileList: UploadFile[]) => void;
setOrginalPicUrl: React.Dispatch;
}
const AliyunOSSUpload : React.FC = (props) => {
const { value, onChange, setOrginalPicUrl } = props;
const [OSSData, setOSSData] = useState();
const init = async () => {
try {
const result = await generateSign({dir: "upload"});
if (!result || !result.success) return;
console.log("签名请求结果: ", result.data);
setOSSData(result.data);
} catch (error) {
message.error(error);
}
};
useEffect(() => {
init();
}, []);
const handleChange: UploadProps['onChange'] = ({ fileList , file}) => {
onChange?.([...fileList]);
if (file.status == 'done') {
var url = "http://xxx" + file.originFileObj.url;
setOrginalPicUrl(url);
}
};
const onRemove = (file: UploadFile) => {
const files = (value || []).filter((v) => v.url !== file.url);
if (onChange) {
onChange(files);
}
};
const getExtraData: UploadProps['data'] = (file) => ({
key: file.url,
OSSAccessKeyId: OSSData?.accessKey,
policy: OSSData?.policy,
Signature: OSSData?.signature,
});
const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
if (!OSSData) return false;
const expire = OSSData.expire;
if (expire < Date.now()) {
await init();
}
const suffix = file.name.slice(file.name.lastIndexOf('.'));
const filename = Date.now() + suffix;
// @ts-ignore
file.url = 'upload/' + filename;
return file;
};
const uploadProps: UploadProps = {
name: 'file',
fileList: value,
action: "http://xxx",
onChange: handleChange,
onRemove,
data: getExtraData,
beforeUpload,
};
return (
Upload
);
};
export default AliyunOSSUpload;
四、后续改进思路
1、采用阿里云OSS+CDN
2、读写权限改为私有
五、参考文档