一、序言
OSS(Object Storage Service)是阿里雲提供的一款雲存儲服務,具有海量、安全、低成本、高可靠的特點。
由於客戶選擇了OSS,我們作為開發方也開始接觸它。在實際開發過程中遇到了各種各樣的坑,經自己多次實踐及阿里技術人員的協助,終得以完成任務。
阿里方面為OSS提供了多種語言的開發接口,我們用到了其中兩種:Java和C/C++。本文為Java篇,C/C++的將在另一篇給出。
二、OSS的一些概念
- EndPoint, accessKeyID, accessKeySecret:欲使用OSS,先要在阿里雲上申請相應的空間資源,而EndPoint, accessKeyID, accessKeySecret則相當於域名、賬號和密碼,是所申請資源的使用憑證,需要妥善保管。
- Bucket:是用於存儲對象的容器,所有對象都必須屬於且只屬於一個Bucket,Bucket的屬性(控制地域、訪問權限、生命周期等)對所有對象都同等有效,同一空間資源下Bucket名必須唯一,且創建后不能再改名。
- 對象/文件:對象/文件是 OSS 存儲數據的基本單元。對象/文件由元信息(Object Meta),用戶數據(Data)和文件名(Key)組成。文件名是唯一的,重復上傳同名的對象意味着覆蓋以前內容,但OSS支持在已有對象后部追加數據。
- 目錄:其實是一種特殊的對象(無Data),僅僅是為了管理方便,除此以外並無多大意義。
- 其它概念,如分片、回調、追加、權限等,因開發中未涉及,不再展開,有興趣者請參考:https://help.aliyun.com/document_detail/31827.html?spm=a2c4g.11186623.4.1.TamX1d
三、1號坑:找不到對象拋出OSSException
現在開始總結Java開發時的幾個坑,這些都是SDK文檔和示例代碼中沒有的。先從簡單的說起。
Java SDK提供了兩種搜素OSS對象的方法:按文件名精確匹配和按文件前綴批量查找,不支持其它模糊查詢、正規表達式查詢,也不支持按元信息查詢。這些都好理解,不多說。
在按文件名Key精確查找OSS對象時,如果不存在該Key對應的文件,會拋出OSSException(對應的錯誤信息是ErrorCode為“NoSuchKey”),而不是返回null。
因此,需要在Java代碼里加上對該例外的捕獲處理:
try { OSSObject obj = null; obj = client.getObject(bucketName, ossKey); if (obj != null) obj.close(); } catch (OSSException e) { // OSS在查找不到某對象時,會拋出ErrorCode為“NoSuchKey”的OSSException,而不是返回null if (e.getErrorCode().contains("NoSuchKey")) { System.out.println("找不到OSS文件:" + ossKey); continue; } else e.printStackTrace(); } catch (ClientException | IOException e) { e.printStackTrace(); }
四、2號坑:分頁遍歷時的死循環
OSS Java SDK提供了分頁遍歷的功能,一頁最多可以遍歷1000個文件。但如果一邊遍歷一邊更新對象,則很容易形成死循環。
項目中的一個小工具的示例代碼如下:
String nextMarker = null; ObjectListing objListing; do { if (nextMarker == null) // 第一次的分頁 objListing = client.listObjects(new ListObjectsRequest(bucketName).withMaxKeys(1000)); else // 以后的分頁,附帶nextMarker objListing = client.listObjects( new ListObjectsRequest(bucketName).withMarker(nextMarker).withMaxKeys(1000)); List<OSSObjectSummary> sums = objListing.getObjectSummaries(); for (OSSObjectSummary s : sums) { String ossKey = s.getKey(); ... ossClient.putObject(bucketName, s, new ByteArrayInputStream(mnt.getData())); ... } // 下一次分頁的nextMarker nextMarker = objListing.getNextMarker(); } while (objListing.isTruncated());
因為有了putObject操作(帶顏色處),運行時成了死循環,objListing.isTruncated()永遠為false。
經測試,死循環不僅僅出現在如上的一段代碼中既遍歷又修改的情況下,一個進程循環寫的同時另一個進程分頁遍歷也會出現。猜測遍歷的依據條件主要是修改時間,但沒法區分已經遍歷過的對象。
貌似沒有特別好的解決辦法,我們使用的是一種土辦法:選定一個特殊對象,再次遍歷到它即強行退出循環。
五、3號坑:循環getObject超過1024次的掛起
有一個操作需要批量讀取OSS對象,按示例代碼編寫后測試,發現一旦循環調用getObject()程序就會掛起,不繼續運行也不退出,只能強行關閉。
后來經一步步跟蹤,發現此問題是由於getObject()后沒有及時close對象而引起,臨界值是1024(也可能是1023)。
List<String> pks; //存放的是ossKey for (String pk : pks) { HSBJMntDataPK pk = pks.get(i); OSSObject obj = null; try { obj = ossClient.getObject(bucketName, pk); } catch (OSSException e) { if (e.getErrorCode().contains("NoSuchKey")) continue; e.printStackTrace(); } catch (ClientException e) { e.printStackTrace(); } // 處理obj的代碼,略過 try // 及時釋放OSSObject,否則循環達到1024次會suspend { obj.close(); } catch (IOException e) { e.printStackTrace(); } }
增加了顏色標出的obj.close()后,不再發生程序掛起的現象。
六、4號坑:關於UserMetaData中的大小寫
如果OSS對象帶有一些簡單的自定義屬性,比如本項目中用到的創建者、版本、類型、備注等,可以作為UserMetaData存放到元信息(Object Meta)中。與把它們合並到Data中的方式相比,這樣做不但簡化了Data的構造和解析過程,還可以縮短讀寫時間,在本項目中能提高速度30%左右。
但是,一開始把屬性值寫入元信息后,讀取時卻讀不到值。后來發現,UserMetaData的key值按照項目習慣是首字母大寫的,但在寫入時OSS都自動轉換為全小寫處理,讀取時再按首字母大寫就讀取不到。在將UserMetaData的key值改為全小寫后,問題解決。
public static ObjectMetadata buildMeta(MntData mnt) { ObjectMetadata meta = new ObjectMetadata(); // UserMetadata中,key會被轉換為全部小寫,所以為統一賦值時也用小寫 meta.addUserMetadata("author", author); meta.addUserMetadata("version", version); meta.addUserMetadata("type", type); meta.addUserMetadata("purpose", purpose); return meta; } public static MntData parseObject(OSSObject ossObject) { ObjectMetadata meta = ossObject.getObjectMetadata(); Map<String, String> metadata = meta.getUserMetadata(); MntData mnt = new MntData(); // 從UserMeta獲取value時,key必須為全小寫 mnt.setAuthor(metadata.get("author") == null ? "-" : metadata.get("author")); mnt.setVersion(metadata.get("version") == null ? "1.0" : metadata.get("version")); mnt.setType(metadata.get("type") == null ? "-" : metadata.get("type")); mnt.setPurpose(metadata.get("purpose") == null ? "" : metadata.get("purpose")); // UpdateTime為OSS自帶的元數據 mnt.setUpdateTime(new Timestamp(meta.getLastModified().getTime())); mnt.setData(getBytesFromObj(ossObject)); return mnt; }
七、經驗:使用多進程可提升速度
注意這不是坑,而是一條有益經驗:無論是讀還是寫,使用多線程可顯著提升批量操作的速度。
以下是寫OSS進程的示例代碼:
public class OssPutThread extends Thread { List<MntData> mnts; int index; public OssPutThread(List<MntData> mnts, int index) { this.mnts = mnts; this.index = index; } @Override public void run() { // 線程內新生成一個OSSClient OSSClient ossClient = new OSSClient(endPoint, accessKeyId, accessKeySecret); for (int i = 0; i < mnts.size(); i ++) { HSBJMntData mnt = mnts.get(i); String ossKey = OssUtil.buildOssKey(mnt); // data轉換為InputStream,其它屬性值放入ObjectMetaData ossClient.putObject(bucketName, ossKey, OssUtil.buildOssObject(mnt), OssUtil.buildMeta(mnt)); } //線程結束前釋放OSSClient ossClient.shutdown(); } }
在本項目中,線程數每多一倍,批量讀寫的速度可提升90%,效果相當明顯。