MaxCompute與OSS非結構化資料讀寫互通(及影像處理實例)

來源:互聯網
上載者:User

摘要: MaxCompute作為阿裡巴巴集團內部絕大多數巨量資料處理需求的核心計算群組件,擁有強大的計算能力,隨著集團內外巨量資料商務的不斷擴充,新的資料使用場景也在不斷產生。在這樣的幕後下,MaxCompute(ODPS)計算架構持續演化,而原來主要面對內部特殊格式資料的強大計算能力,也正在一步步的通過新增的非結構化資料處理架構,開放給不同的外部資料。

0. 前言

MaxCompute作為阿裡巴巴集團內部絕大多數巨量資料處理需求的核心計算群組件,擁有強大的計算能力,隨著集團內外巨量資料商務的不斷擴充,新的資料使用場景也在不斷產生。在這樣的幕後下,MaxCompute(ODPS)計算架構持續演化,而原來主要面對內部特殊格式資料的強大計算能力,也正在一步步的通過新增的非結構化資料處理架構,開放給不同的外部資料。我們相信阿裡巴巴集團的這種需求,也代表著業界巨量資料領域的最前沿實踐和走向,具有相當的普適性。在之前我們已經對MaxCompute2.0新增的非結構化架構做過整體介紹,標題了在MaxCompute上如何處理隱藏在OSS上面的非結構化資料,側重點在怎樣從OSS**讀取**各種非結構化資料並在MaxCompute上進行**計算**。 而一個完整資料連結,讀取和計算處理之後,必然也會涉及到非結構化資料的寫出。 在這裡我們著重介紹一下從MaxCompute往OSS**輸出**非結構化資料,並提供一個具體的在MaxCompute上進行影像處理的實例,來閱聽從【OSS->MaxCompute->OSS】的整個資料連結閉環的實現。 至於對於KVNoSQL類型資料的輸出,在對TableStore資料處理介紹 中已經有所介紹,這裡就不再重複。

1. 使用前提和假設

1.1 MaxCompute 2.0 功能申請與打開

首先要說明的是MaxCompute新一代的2.0計算架構還在灰階上線的程序中,預設設定下許多功能沒有打開,所以要使用新引進的非結構化資料處理架構,**需要申請MaxCompute2.0試用**,具體開通使用方法請參見如何申請試用MaxCompute2.0, 簡單來說就是在開通2.0非結構化功能的前提下,在每個SQLquery執行時必須帶上如下setting:

set odps.task.major.version=2dot0_demo_flighting;
set odps.sql.planner.mode=lot;
set odps.sql.ddl.odps2=true;
set odps.sql.type.system.odps2=true;

下面的範例中就不再重複了,**但是本文介紹的所有功能均基於以上假設**,當然這些特殊設定在近期MaxCompute2.0計算架構完全上線後就可以省略了。

在上面這些settgins中,一個需要特別說明的是set odps.sql.type.system.odps2=true,這個設定其實和非結構化功能本身沒有直接關聯,但是在後面的影像處理例子中,我們用到了MaxCompute2.0新引進的一個BINARY類型,用於表示/隱藏二進位bytes資料。所以這裡指定使用了2.0的類型系統,這個新的類型系統大大擴充了原來MaxCompute(ODPS)1.0的類型,比如BINARY類型就為隱藏非文字的資料提供了一個自然的容器。這裡作為BINARY類型的使用範例,就不對整個新類型系統做展開介紹了。

1.2 網路連通性與存取權限

另外因為MaxCompute與OSS是兩個分開的雲端運算,與雲端儲存體服務,所以在不同的部署集群上的網路連通性有可能影響MaxCompute存取OSS的資料的可達性。關於OSS的節點,實例,服務位址等概念,可以參見OSS相關介紹。 在MaxCompute公共雲端服務存取OSS隱藏,推薦使用OSS**私網**位址(即以-internal.aliyuncs.com結尾的host位址)。

此外需要指出的是,MaxCompute計算服務要存取TableStore資料需要有一個安全的授權通道。在這個問題上,MaxCompute結合了阿裡雲的存取控制服務(RAM)和權杖服務(STS)來實現對資料的安全反問:

首先需要在RAM中授權MaxCompute存取OSS的權限。登入RAM主控台,建立角色AliyunODPSDefaultRole,並將原則內容設定為:

{
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": [
"odps.aliyuncs.com"
]
}
}
],
"Version": "1"
}

然後編輯該角色的授權原則,將權限AliyunODPSRolePolicy授權給該角色。

如果覺得這些步驟太麻煩,還可以登入阿裡雲帳號點選此處完成一鍵授權

2. MaxCompute內建的OSS資料輸出handler

2.1 建立ExternalTable

MaxCompute非結構化資料架構希望從根本上提供MaxCompute與各種資料的聯通,這裡的“各種資料”是兩個維度上的:

  1. 各種隱藏介質,比如OSS
  2. 各種資料格式, 比如文字檔,視訊,影像,音訊,基因,氣象等格式的資料

而資料的這兩個維度特徵,都是通過EXTERNALTABLE的概念來引入MaxCompute的計算體系的。與讀取OSS資料的使用方法類似,對OSS資料進行寫動作,在如上**打開安全授權通道後**,也是先通過CREATEEXTERNAL TABLE語句建立出一個外部表格,再通過標準MaxComputeSQL的INSERTINTO/OVERWRITE等語句來實現的,這裡先用MaxCompute內建的TsvStorageHandler為例來說明一下用法:

DROP TABLE IF EXISTS tpch_lineitem_tsv_external;

CREATE EXTERNAL TABLE IF NOT EXISTS tpch_lineitem_tsv_external
(
orderkey BIGINT,
suppkey BIGINT,
discount DOUBLE,
tax DOUBLE,
shipdate STRING,
linestatus STRING,
shipmode STRING,
comment STRING
)
STORED BY 'com.aliyun.odps.TsvStorageHandler'----------------------------------------- (1)
LOCATION 'oss://oss-cn-shanghai-internal.aliyuncs.com/oss-odps-test/tsv_output_folder/';--(2)

這個DDL語句建立了一個外部表格tpch_lineitem_tsv_external,並將前面提到的**兩個維度**的外部資料資訊關聯到這個外部表格上。

  1. 資料存放區介質: LOCATION 將一個OSS上的位址關聯到外部表格上,對這個外部表格的進行讀寫動作都會反映到這個OSS位址上。
  2. 資料存放區格式: StorageHandler用來表明對這些資料的讀寫動作方式,這裡使用了MaxCompute內建的com.aliyun.odps.TsvStorageHandler, 用戶可以使用這個由系統自帶的實現來讀取和寫出TSV檔案。 同時用戶也可以通過MaxCompute的SDK來自訂StorageHandler, 這個將在後面的章節介紹。

其中OSS資料存放區的具體位址的URI格式為:

LOCATION 'oss://${endpoint}/${bucket}/${userPath}/'

最後還要提到的是,在上面的DDL語句中定義了外部表格的Schema,對於資料輸出而言,這表示輸出的資料格式將由這個Schema標題。就TSV格式而言,這個schema標題比較直觀容易理解;而在用戶自訂的輸出資料格式上,這個schema與輸出資料的連線則更鬆散一些,有著更大的自由度。在後面介紹通過自訂StorageHandler/Outputer的時候會詳細展開。

2.2 通過對ExternalTable的INSERT 動作實現TSV文字檔的寫出

在將OSS資料通過ExternalTable關聯上後,對OSS檔案的寫出可以對ExternalTable做標準的SQLINSERT OVERWRITE/INSERT INTO來動作。具體輸出資料的**來源**可以有兩種

1.資料來源為MaxCompute的內部表:也就是說可以通過對外表INSERT動作來實現MaxCompute**內部表資料到外部隱藏介質的寫出**。

2.資料來源為之前通過ExternalTable引入MaxCompute計算體系的外部資料:這可以用來將外部資料引入MaxCompute進行計算,然後再隱藏到(不同的)外部隱藏位址,或者甚至是不同的外部隱藏介質(比如將TableStore資料經由MaxCompute匯出到OSS)。

2.2.1 從MaxCompute內部表輸出資料到OSS

這裡先來看第一種場景:假設我們已經有一個名為tpch_lineitem的MaxCompute**內部表**,其schema可以通過

DESCRIBE tpch_lineitem;

得到:

+------------------------------------------------------------------------------------+
| InternalTable: YES| Size: 241483831680|
+------------------------------------------------------------------------------------+
| Native Columns:|
+------------------------------------------------------------------------------------+
| Field| Type| Label | Comment|
+------------------------------------------------------------------------------------+
| l_orderkey| bigint|||
| l_partkey| bigint|||
| l_suppkey| bigint|||
| l_linenumber| bigint|||
| l_quantity| double|||
| l_extendedprice | double|||
| l_discount| double|||
| l_tax| double|||
| l_returnflag| string|||
| l_linestatus| string|||
| l_shipdate| string|||
| l_commitdate| string|||
| l_receiptdate| string|||
| l_shipinstruct| string|||
| l_shipmode| string|||
| l_comment| string|||
+------------------------------------------------------------------------------------+

其中有**16**個columns。 現在我們希望將其中的一部分資料以TSV格式匯出到OSS上面。那麼在用上述DDL建立出ExternalTable之後,使用如下INSERTOVERWRITE動作就可以實現:

INSERT OVERWRITE TABLE tpch_lineitem_tsv_external
SELECT l_orderkey, l_suppkey, l_discount, l_tax, l_shipdate, l_linestatus, l_shipmode, l_comment
FROM tpch_lineitem
WHERE l_discount = 0.07 and l_tax = 0.01;

這裡將從內部作業tpch_lineitem表中,在符合l_discount= 0.07 並l_tax = 0.01的行中選出8個列(對應tpch_lineitem_tsv_external這個外部表格的schema)按照TSV的格式寫到OSS上。 在上面這個INSERTOVERWRITE動作成功完成後,就可以看到OSS上的對應LOCATION產生了一系列檔案:

osscmd ls oss://oss-odps-test/tsv_output_folder/

2017-01-14 06:48:27 39.00B Standard oss://oss-odps-test/tsv_output_folder/.odps/.meta
2017-01-14 06:48:12 4.80MB Standard oss://oss-odps-test/tsv_output_folder/.odps/20170113224724561g9m6csz7/M1_0_0-0.tsv
2017-01-14 06:48:05 4.78MB Standard oss://oss-odps-test/tsv_output_folder/.odps/20170113224724561g9m6csz7/M1_1_0-0.tsv
2017-01-14 06:47:48 4.79MB Standard oss://oss-odps-test/tsv_output_folder/.odps/20170113224724561g9m6csz7/M1_2_0-0.tsv
...

這裡可以看到,通過上面LOCATION指定的oss-odps-test這個OSS bucket下的tsv_output_folder資料夾下產生了一個.odps資料夾,這其中將有一些.tsv檔案,以及一個.meta檔案。這樣子的檔案結構是MaxCompute(ODPS)往OSS上輸出所特有的:

  1. 通過MaxCompute對一個OSS位址,使用INSERT INTO/OVERWRITE 外部表格來做寫出動作,所有的資料將在指定的LOCATION下的.odps資料夾產生;
  2. 其中.odps資料夾中的.meta檔案為MaxCompute額外寫出的宏資料檔案,其中用於記錄本期資料夾中有效資料。 正常情況下,如果**INSERT動作成功完成的話**,可以認為本期資料夾的所有資料均是有效資料。 只有在有作業失敗的情況下需要對這個巨集資料進行解析。即使是在作業中途失敗或被kill的情況下,對於INSERT OVERWRITE動作,再跑一次成功即可。 如果對於進階使用者,一定需要解析.meta檔案的話,可以連線MaxCompute技術團隊。

這裡迅速看一下這些tsv檔案的內容:

osscmd cat oss://oss-odps-test/tsv_output_folder/.odps/20170113232648738gam6csz7/M1_0_0-0.tsv

42360000679992377 0.070.011992-11-06FRAILacross the ideas nag
42360002903272628 0.070.011998-04-28ORAILuriously. furiously unusual dinos int
42360003868081402 0.070.011994-02-19FRAILits. express, iron
42360007103879271 0.070.011995-03-10FAIRes are carefully fluffily spe
...

可以看到確實在OSS上產生了對應的TSV資料檔案。

最後,大家可能也注意到了,這個INSERTOVERWRITE動作產生了多個TSV檔案,對於MaxCompute內建的TSV/CSV處理來說,產生的檔案數目與對應SQLstage的並行度是相同的,在上面這個例子中,INSEROVERWITE ... SELECT ... FROM ...; 的動作在來源資料表(tpch_lineitem) 上配置了1000個mapper,所以最後產生了1000個TSV檔案的。如果需要控制TSV檔案的數目,可以配合MaxCompute的各種靈活語義和設定來實現。比如如果需要強制產生一個TSV檔案,那在這個特定例子中,可以在INSEROVERWITE ... SELECT ... FROM ...最後加上一個DISTRIBUTE BY l_discount,就可以在最後插入僅有一個Reducer的Reducestage, 也就會只輸出一個TSV檔案了:

osscmd ls oss://oss-odps-test/tsv_output_folder/

2017-01-14 08:03:41 39.00B Standard oss://oss-odps-test/tsv_output_folder/.odps/.meta
2017-01-14 08:03:35 4.20GB Standard oss://oss-odps-test/tsv_output_folder/.odps/20170113234037735gcm6csz7/R2_1_33_0-0.tsv

可以看到在增加了DISTRIBUTEBY l_discount後,現在同樣的資料只了一個輸出TSV檔案,當然這個檔案的size就大多了。這方面的調控技巧還有很多,都是可以依賴SQL語言的靈活性,資料本身的屬性,以及MaxCompute計算相關設定來實現的,這裡就不深入展開了。

2.2.2 以MaxCompute為計算介質,實現不同隱藏介質之間的資料轉送

External Table作為一個MaxCompute與外部隱藏介質的一個切入點,之前已經介紹過對OSS資料的讀取以及TableStore資料的動作,結合對外部資料讀取和寫出的功能,就可以實現通過ExternalTable實現各種各樣的資料計算/隱藏鏈路,比如:

1.讀取ExternalTable A關聯的**OSS**資料,在MaxCompute上做複雜計算處理,並輸出到ExternalTable B關聯的**OSS**位址

2.讀取ExternalTable A關聯的**TableStore**資料,在MaxCompute上做複雜計算處理,並輸出到ExternalTable B關聯的**OSS**位址

而這些動作與上面資料來源為MaxCompute內部表的場景,唯一的區別只是SELECT的來源變成一個Externaltable,而不是MaxCompute內建表。

3. 通過自訂StorageHandler來實現資料輸出

除了使用內建的StorageHandler來實現在OSS上輸出TSV/CSV等常見文字格式設定,MaxCompute非結構化架構提供了通用的SDK,允許用戶對外輸出自訂資料格式檔案,包括影像,音訊,視訊等等。這種對於用戶自訂的完全非結構化資料格式支援,也是MaxCompute從結構化/文字類資料的一個向外擴充,在這裡我們會以一個影像處理的例子,來走通整個【OSS->MaxCompute->OSS】資料連結,尤其著重介紹對OSS輸出檔案的功能。

為了方便大家理解,這裡先提供一個在**使用用戶自訂代碼的場景**下,資料在MaxCompute計算平臺上的流程:

Fig.1.数据计算链路

從上圖可以看出,從資料的流動和處理邏輯上理解,使用者可以簡單地把非結構化處理架構理解成在MaxCompute計算平臺兩端有機耦合的的資料匯入(Ingres)以及匯出(Egress):

  1. 外部的(OSS)資料經過非結構化架構轉換,會使用Java用戶容易理解的InputStream類提供給自訂代碼介面。 使用者自實現Extract邏輯只需要負責對輸入的InputStream做讀取/解析/轉化/計算,最終返回MaxCompute計算平臺通用的Record格式;
  2. 這些Record可以自由的參與MaxCompute的SQL邏輯運算,這一部分計算是基於MaxCompute內建的強大結構化SQL運算引擎,並可能產生新的Record
  3. 運算過後的Record中再傳遞給使用者自訂的Output邏輯,使用者在這裡可以進行進一步的計算轉換,並最終將Record裡面需要輸出的資訊通過系統提供的OutputStream輸出,由系統負責寫到OSS。

值得指出的是,這裡面所有的步驟都是可以由使用者根據需要來進行**自由的選擇與拼接**的。 比如如果用戶的輸入就是MaxCompute的內部表,那步驟1.就沒有必要了,事實上在前面的章節2中的例子,我們就實現了將內部表直接寫成OSS上的TSV檔案的流程。同理,如果用戶沒有輸出的需求,步驟3.就沒有必要,比如我們之前介紹的OSS資料的讀取。 最後,步驟2.也是可以省略的,比如如果使用者的所有計算邏輯都是在自訂的Extract/Output中完成,沒有進行SQL邏輯運算的需要,那步驟1.是可以直接連接到步驟3.的。

理解了上面這個資料變換的流程,我們就可以來通過一個影像處理例子來看看怎麼具體的通過非結構化架構在MaxComputeSQL上完整的實現非結構化資料的讀取,計算以及輸出了:

3.1 範例:OSS影像檔-> MaxCompute計算處理-> OSS影像輸出

這裡我們先提供實現這整個【OSS->MaxCompute->OSS】資料連結需要用到的MaxComputeSQL query,並做簡單的備註,詳細的使用者代碼實現邏輯將在後面的3.2子章節中介紹SDK介面的時候做展開解釋。

3.1.1 關聯OSS上的原始輸入影像到ExternalTable: images_input_external

DROP TABLE IF EXISTS images_input_external;
CREATE EXTERNAL TABLE IF NOT EXISTS images_input_external
(
name STRING,
width BIGINT,
height BIGINT,
image BINARY
)
STORED BY 'com.aliyun.odps.udf.example.image.ImageStorageHandler'--- (1)
WITH SERDEPROPERTIES ('inputImageFormat'='BMP' , 'transformedImageFormat' = 'JPG')--- (2)
LOCATION 'oss://oss-cn-shanghai-internal.aliyuncs.com/oss-odps-test/dev/SampleData/test_images/mixed_bmp/'--- (3)
USING 'odps-udf-example.jar';--- (4)

說明:

  1. 用戶指明使用的用戶代碼wrapper class名字是com.aliyun.odps.udf.example.image.ImageStorageHandler,這個class及其依賴的三方庫用戶通過jar提供,具體jar名字會通過下面的USING語句(見第4點)指定。
  2. 通過SERDEPROPERTIES來實現參數傳遞,格式為'key'='value', 具體用法可以參見準系統介紹 以及下面的用戶代碼說明
  3. 指定輸入影像位址,這個位址上存放了一系列**不同解析度的bmp影像檔**。
  4. 指定包含用戶JAR包,內含自訂的StorageHandler/Extractor/Outputer,以及需要的三方庫(這裡用到了Java ImageIO庫,具體見下面使用者代碼範例)。JAR包通過ADD JAR指令上傳,可以參見準系統介紹

另外要說明的是這裡指定的ExternalTable的schema就是用戶在進行Extract動作後建構的Record格式,具體怎麼建構這個Schema使用者可以根據需要自己根據能從輸入資料中抽取到的資訊定義。在這裡我們定義了對於輸入圖片資料,會將圖片名稱,圖片的長和寬,以及圖片的二進位bytes抽取出來放進Record(見後面的Extractor代碼說明),所以就有了上面的【STRING,BIGINT,BIGINT,BINARY】的schema。

3.1.2 關聯OSS輸出位址到ExternalTable: images_output_external

CREATE EXTERNAL TABLE IF NOT EXISTS images_output_external
(
image_name STRING,
image_width BIGINT,
image_height BIGINT,
outimage BINARY
)
STORED BY 'com.aliyun.odps.udf.example.image.ImageStorageHandler'
LOCATION 'oss://oss-cn-shanghai-internal.aliyuncs.com/oss-odps-test/dev/output/images_output/' ---(1)
USING 'odps-udf-example.jar';

說明:可以看到這裡建立關聯**輸出影像檔**的ExternalTable,使用的DDL語句,與前面關聯**輸入影像**時使用的DDL語句是非常類似的:只是LOCATION不一樣,表明圖像資料處理後將輸出到另外一個位址。另外還有一點就是這裡我們沒有使用SERDEPROPERTIES來進行傳參,這個只是在這個場景上沒有需求,在有需求的時候可以用同樣的方法把參數傳遞給outputer。 當然這裡兩個DDL語句如此相似,有一個原因是因為我們這個例子中用戶代碼中對於Extract出的Record以及輸入給Outputer的Record使用了一樣的schema,同時這一對Extractor和Outputer都被封裝在了同一個ImageStorageHandler裡放在同一個JAR包裡。在實際套用中,這些都是可以根據實際需求自己調整的,由用戶自己選擇群組和封裝方式

3.1.3 從OSS讀取原始圖片資料到MaxCompute,計算處理,並輸出影像到OSS

在上面的3.1.1以及3.1.2子章節中的兩個DDL語句,分別實現了把輸入OSS資料,以及計劃輸出OSS資料,分別繫結到兩個LOCATION以及指定對應的用戶處理代碼,參數等設定。然而這兩個DDL語句對系統而言,只是進行了一些宏資料的記錄動作,並**不會涉及具體的資料計算動作**。 在這兩個DDL語句執行成功後,執行如下SQL語句才會引發真正的運算。換句話說,在Fig.1中標題的整個【OSS->MaxCompute->OSS】資料讀取/計算/輸出鏈路,實際上都是通過下面一個簡單的SQL陳述式完成的:

INSERT OVERWRITE TABLE images_output_external
SELECT * FROM images_input_external
WHERE width = 1024;

這看起來就是一個標準的MaxComputeSQL語句,只不過因為涉及了images_output_external和images_input_external這兩個外部表格,所以真正進行的物理動作與傳統的SQL動作會有一些區別:在這個程序中,涉及了讀寫OSS,以及通過ImageStorageHandler這個wrapper,叫用自訂的Extractor,Outputer代碼來對資料進行動作。下面就來具體看看在這個例子中的用戶自訂代碼實現了怎樣的功能,以及具體是如何?的。

3.2 ImageStorageHandler實現

如同之前介紹過的,MaxCompute非結構化架構通過StorageHandler這個介面來標題對各種資料存放區格式的處理。具體來說,StorageHandler作為一個wrapperclass, 讓用戶指定自己定義的Exatractor(用於資料的讀入,解析,處理等)以及Outputer(用於資料的處理和輸出等)。 用戶自訂的StorageHandler應該繼承OdpsStorageHandler,實現getExtractorClass以及getOutputerClass兩個介面。

通常作為wrapperclass, StorageHandler的實現都很簡單,比如這裡的ImageStorageHandler 就只是通過這兩個介面指定了我們將使用ImageExtractor以及ImageOutputer:

package com.aliyun.odps.udf.example.image;

public class ImageStorageHandler extends OdpsStorageHandler {
@Override
public Class<? extends Extractor> getExtractorClass() {
return ImageExtractor.class;
}

@Override
public Class<? extends Outputer> getOutputerClass() {
return ImageOutputer.class;
}
}

另外要說明的是如果確定在使用某個StorageHandler的時候,只需要用到Extractor,或者只需要用到Outputer功能,那**不需要的介面則不用實現**。 比如如果我們只需要讀取OSS資料而不需要做INSERT動作,那getOutputerClass()的實現只需要扔個NotImplementedexception就可以了,不會被叫用到。

3.3 ImageExtractor實現

因為對於SDK中Extractor介面的介紹以及對用戶如何寫一個自訂的Extractor,在之前介紹的OSS資料的讀取中已經有所涉及,所以這裡就不再對這方面做深入的介紹。

Extractor的工作在於讀取輸入資料並進行用戶自訂處理,那麼我們首先來看看這裡由images_input_external這個外表繫結的OSS輸入LOCATION上存放的具體資料內容:

osscmd ls oss://oss-odps-test/dev/SampleData/test_images/mixed_bmp/

2017-01-09 14:02:01 1875.05KB Standard oss://oss-odps-test/dev/SampleData/test_images/mixed_bmp/barbara.bmp
2017-01-09 14:02:00 768.05KB Standard oss://oss-odps-test/dev/SampleData/test_images/mixed_bmp/cameraman.bmp
2017-01-09 14:02:00 1054.74KB Standard oss://oss-odps-test/dev/SampleData/test_images/mixed_bmp/fishingboat.bmp
2017-01-09 14:01:59 257.05KB Standard oss://oss-odps-test/dev/SampleData/test_images/mixed_bmp/goldhill.bmp
2017-01-09 14:01:59 468.80KB Standard oss://oss-odps-test/dev/SampleData/test_images/mixed_bmp/house.bmp
2017-01-09 14:01:59 468.80KB Standard oss://oss-odps-test/dev/SampleData/test_images/mixed_bmp/jetplane.bmp
2017-01-09 14:02:01 2.32MB Standard oss://oss-odps-test/dev/SampleData/test_images/mixed_bmp/lake.bmp
2017-01-09 14:01:59 257.05KB Standard oss://oss-odps-test/dev/SampleData/test_images/mixed_bmp/lena.bmp
2017-01-09 14:02:00 768.05KB Standard oss://oss-odps-test/dev/SampleData/test_images/mixed_bmp/livingroom.bmp
2017-01-09 14:02:00 768.05KB Standard oss://oss-odps-test/dev/SampleData/test_images/mixed_bmp/pirate.bmp
2017-01-09 14:02:00 768.05KB Standard oss://oss-odps-test/dev/SampleData/test_images/mixed_bmp/walkbridge.bmp
2017-01-09 14:02:00 1054.74KB Standard oss://oss-odps-test/dev/SampleData/test_images/mixed_bmp/woman_blonde.bmp
2017-01-09 14:02:00 768.05KB Standard oss://oss-odps-test/dev/SampleData/test_images/mixed_bmp/woman_darkhair.bmp

可以看到**這個LOCATION存放了一系列bmp圖像資料,解析度從400 x 400 到1200 x 1200不等**。 具體在這個例子中用到的ImageExtractor的詳細代碼在github上可以找到,這裡只做一些簡單介紹說明該Extractor做了些什麼工作:

1.從輸入的OSS位址上使用非結構化架構提供的InputStream介面讀取圖像資料,並在本地進行如下動作

    • 對於影像寬度小於1024的圖片,統一放大到1024 x 1024; 對於影像寬度大於1024的圖片,跳過不進行處理
    • 處理過的圖片,在記憶體中轉存成由輸入參數指定的格式(JPG)

2.把處理後在記憶體中的JPG資料的原始位元組存入輸出的Record中的BINARYfield, 同一個Record中還將存放處理後影像的長和寬(都是1024), 以及原始的影像名字(這個可以從輸入的InputStream上追蹤);

3.填滿後的Record從Extract介面返回進入MaxCompute系統;

4.在這個程序中,用戶可以靈活的進行各種動作,比如額外的參數驗證等。

另外要說明的是,目前Record作為MaxCompute結構化資料處理的基本單元,有一些額外的節流,比如BINARY/STRING類型都有**8MB**大小的節流,但是在大部分場景下這個大小應該是能滿足隱藏需求的。

3.4 ImageOutputer的實現

接下來我們著重講一下ImageOutputer的實現。首先所有的使用者輸出邏輯都必須實現Outputer介面,具體來說有如下三個:setup,output和close,這和Extractor的setup, extract和close三個介面基本上是對稱的。

// Base outputer class, custom outputer shall extend from this class
public abstract class Outputer{

public abstract void setup(ExecutionContext ctx, OutputStreamSet outputStreamSet, DataAttributes attributes);

public abstract void output(Record record) throws IOException;

public abstract void close() throws IOException;
}

這其中setup()和close()在一個outputer中只會叫用一次。用戶可以在setup裡面做初始化準備工作,另外通常需要把setup()傳遞進來的這三個參數儲存成ouputerd的classvariable, 方便之後output()或者close()介面中使用。而close()這個介面用於方便用戶代碼的掃尾工作。

通常情況下大部分的資料處理發生在output(Record)這個介面內。MaxCompute系統會根據本期outputer配置處理的Record數目不斷叫用,也就是**對每個輸入Record系統會叫用一次**output(Record)。系統假設在一個output(Record)叫用返回的時候,用戶代碼已經消費完這個Record,因此在本期output(Record)返回後,系統可能將這個Record所使用的記憶體用作它用:所以**不推薦一個Record中的資訊在跨多個output()函式呼叫被使用**,如果一定有這個需求的話,用戶必須把相關資訊通過classvariable等方式自行另外儲存。

3.4.1 ImageOutputer.setup()

setup用於初始化整個outputer,在這個介面上提供了整個outputer動作程序中可能需要的參數:

  • ExecutionContext: 用於提供一些系統資訊和介面,比如讀取resource等,在ImageOutputer這個例子中我們沒有用到這個參數;
  • OutputStreamSet: 用戶可以從這個類的next()介面追蹤對外輸出所需要的OutputStream,具體用法我們在下面詳細介紹;
  • DataAttributes: 用戶通過SERDEPROPERTIES設定的key-value參數可以通過這個類追蹤,參數追蹤這裡ImageOutputer例子中沒有用到,但是Extractor上的setup參數中也有這個類,在上面的ImageExtractor用到了改功能,可以參考一下。 同時這個類上面還提供了一些helper介面,比如方便用戶驗證schema等。

在我們這個ImageOutputer裡,setup()的實現比較簡單:

@Override
public void setup(ExecutionContext ctx, OutputStreamSet outputStreamSet, DataAttributes attributes) {
this.outputStreamSet = outputStreamSet;
this.attributes = attributes;
this.attributes.verifySchema(new OdpsType[]{ OdpsType.STRING, OdpsType.BIGINT, OdpsType.BIGINT, OdpsType.BINARY });
}

只是做了簡單的初始化以及對schema的驗證。

3.4.2 ImageOutputer.output(Record) 以及 OutputStreamSet的使用

在介紹具體output()介面之前,首先我們要來看看OutputStreamSet,這個類有兩個介面:

public interface OutputStreamSet{

SinkOutputStream next();
SinkOutputStream next(String fileNamePostfix);

}

兩個介面都是用來追蹤一個新的SinkOutputStream(一個JavaOutputStream的實現,可以按照OutputStream使用),兩個介面唯一的區別是next()追蹤的OutputStream寫出的檔案名稱完全由MaxCompute系統決定,而next(StringfileNamePostfix)則允許用戶提供檔案名稱的postfix。 提供這個postfix的意義是,在輸出檔案具體位址和名字格式母體由MaxCompute系統決定的前提下,用戶依然可以定制一個方便理解的postfix。 比如使用next("_boat.jpg")得到的OutputStream可能對應如下一個輸出文檔案:

oss://oss-odps-test/dev/output/images_output/.odps/20170115148446219dicjab270/M1_0_-1--1-0_boat.jpg

這其中尾端的"_boat.jpg"可以說明使用者理解輸出檔案的涵義。如果這個OutputStream是由next()獲得的話,那對應的輸出檔案可能就是這樣的:

oss://oss-odps-test/dev/output/images_output/.odps/20170115148446219dicjab270/M1_0_-1--1-0

使用者可能就需要具體讀取這個檔案才能知道這個檔案中具體存放了什麼內容。

前面提到output(Record)這個介面會由系統不斷叫用,但是應該強調的是,並不一定在每一個Record都需要叫用一次OutputStreamSet.next()介面來獲得一個新的OutputStream。事實上在大多數情況下,我們建議在一個Outputer裡面盡可能減少叫用next()的次數(最好只叫用一次)。也就是說理想情況下,一個outpuer只應該產生一個輸出檔案。比如處理TSV這種文字格式設定檔案,假設有5000個Record對應5000行TSV資料,那麼最理想的情況是應該把這5000行資料全部寫到一個TSV檔案中。當然用戶可能會有各種各樣不同的切分輸出檔案的需求:比如希望每個檔案大小控制在一定範圍,或比如檔案的邊界有顯著的意義等等。

具體到本期這個影像例子,從下面的ImageOutputer代碼實現中可以看出,這個例子中確實是處理每個Record就叫用一次next()的,因為在本期場景中,每一個輸入的Record都表示一張圖片的資訊(binarybytes, 影像名字,影像長寬),所以這裡通過多次叫用next()來輸出多個圖片檔案。但是我們還是需要再次強調,叫用next()的次數過多可能有一些其他弊端,比如造成分散化小資料在OSS上的隱藏等等。尤其在MaxCompute這種分散式運算系統上,因為系統本身就會調度起多個outputer進行平行計算處理,如果每個outpuer都輸出過多檔案的話,最後產生的檔案數目會有一個乘性效應。回頭來看我們這個例子中,即使在這裡,多個影像其實也可以通過一個OutputStream,按照tar/tar.gz的方式寫到單個檔案中,**這些都是在實現具體系統中使用者需要根據自己的場景,以及處理邏輯,輸出資料類型等資訊來進行優化和tradeoff的。**

在理解了這些之後,現在來具體看看ImageOutputer的實現output介面實現:

@Override
public void output(Record record) throws IOException {
String name = record.getString(0);
Long width = record.getBigint(1);
Long height = record.getBigint(2);
ByteArrayInputStream input =new ByteArrayInputStream(record.getBytes(3));
BufferedImage sobelEdgeImage = getEdgeImage(input);
OutputStream outputStream = this.outputStreamSet.next(name + "_" + width + "x" + height + "." + outputFormat);
ImageIO.write(sobelEdgeImage, this.outputFormat, outputStream);
}

可以看到這裡主要就做了三件事情:

  1. 根據之前儲存的影像名字,長寬資訊,和編碼方式(".jpg")拼出一個帶副檔名的輸出檔案名postfix。
  2. 讀取影像binary bytes,並用getEdgeImage()來利用sobel運算元對影像做邊緣檢測。 具體getEdgeImage()的實現這裡就不進行深入解釋了: 使用了標準的sobel範本卷積演算法, 有興趣看ImageOutputer源碼即可。
  3. 對每一個影像產生一個新的OutputStream並將資料寫出,至此本期Record處理完畢,寫出一張圖片到OSS,output()函數返回。

3.4.3 ImageOutputer.close()

在這個例子中,outputer.close()介面沒有包含具體的實現邏輯,是個no-op。

至此我們就介紹完了一個output的實現,現在可以看看在執行完這個SQLquery,對應OSS位址的資料:

osscmd ls oss://oss-odps-test/dev/output/images_output/

2017-01-15 14:36:50 215.19KB Standard oss://oss-odps-test/dev/output/images_output/.odps/20170115148446219dicjab270/M1_0_-1--1-0-barbara_1024x1024.jpg
2017-01-15 14:36:50 108.90KB Standard oss://oss-odps-test/dev/output/images_output/.odps/20170115148446219dicjab270/M1_0_-1--1-1-cameraman_1024x1024.jpg
2017-01-15 14:36:50 169.54KB Standard oss://oss-odps-test/dev/output/images_output/.odps/20170115148446219dicjab270/M1_0_-1--1-2-fishingboat_1024x1024.jpg
2017-01-15 14:36:50 214.94KB Standard oss://oss-odps-test/dev/output/images_output/.odps/20170115148446219dicjab270/M1_0_-1--1-3-goldhill_1024x1024.jpg
2017-01-15 14:36:50 71.00KB Standard oss://oss-odps-test/dev/output/images_output/.odps/20170115148446219dicjab270/M1_0_-1--1-4-house_1024x1024.jpg
2017-01-15 14:36:50 126.50KB Standard oss://oss-odps-test/dev/output/images_output/.odps/20170115148446219dicjab270/M1_0_-1--1-5-jetplane_1024x1024.jpg
2017-01-15 14:36:50 169.63KB Standard oss://oss-odps-test/dev/output/images_output/.odps/20170115148446219dicjab270/M1_0_-1--1-6-lake_1024x1024.jpg
2017-01-15 14:36:50 194.18KB Standard oss://oss-odps-test/dev/output/images_output/.odps/20170115148446219dicjab270/M1_0_-1--1-7-lena_1024x1024.jpg
...

可以看到圖像資料按照期待格式寫到了指定位址,這裡我們就選一個輸入影像(lena.bmp)以及對應的輸出影像(M1_0_-1--1-7-lena_1024x1024.jpg)看一下對比:

Fig.2 输入输出图像对比

這個例子中整個影像處理流程已經通過如上的SQLquery完成。而從上面閱聽的ImageExtractor以及ImageOutputer 原始程式碼,我們可以看出整個程序中使用者的邏輯基本與寫單機影像處理程式無異,用戶的代碼只需要在Extractor上做InputStream到Record的准換,而在Outputer上做反向的Record到OutputSteam的寫出處理,其他核心的處理邏輯實現基本和單機演算法實現相同,**在用戶的層面,並不用去操心底層分散式系統的細節以及MaxCompute和OSS的互動**。

3.5 資料處理步驟的靈活性

從上面這個例子中我們也可以看出,在一個完整的【OSS->MaxCompute->OSS】資料流程中,Extractor和Outputer中涉及的具體計算邏輯其實也並不一定會有一個非常明確的邊界。Extractor和Outputer只要各自完成所需的轉換Record/Stream的轉換,**具體的額外演算法邏輯在兩個地方都有商機完成**。 比如上面這個例子的整個流程涉及了如下影像處理相關的運算:

  1. 影像的縮放 (統一到 1024 x 1024)
  2. 影像格式的轉換 (BMP -> JPG)
  3. 影像的Sobel邊緣檢測

上面的例子實現中,把1.和2. 放在ImageExtractor中完成,而3.則放在ImageOutputer中完成,**但並不是唯一的選擇**。 我們完全可以把所有3個步驟都放在ImageExtractor中完成,讓ImageOutputer只做Record到寫出最後影像的動作;也可以在ImageExtractor中只做讀取原始binary到Recrod, 而把所有3個影像處理步驟都放在ImageOutputer中進行,等等。具體進行怎樣的選擇,使用者可以完全根據需要自己實現。

另外一個系統設計的點是如果對於一個資料需要做重複的運算,那可以考慮將資料從OSS中通過Extractor讀出進MaxCompute,然後隱藏成MaxCompute的內建表格再進行(多次)的計算。這個對於MaxCompute和OSS沒有進行混布,不在一個物理網路上的場景尤其有意義:MaxCompute從內建表中讀取資料無疑要比從外部OSS隱藏服務中讀出資料要有效得多。在上面3.1.3子章節中的影像處理例子,這個INSEROVERWITE動作:

INSERT OVERWRITE TABLE images_output_external
SELECT * FROM images_input_external
WHERE width = 1024;

就可以改寫成兩個分開的語句:

INSERT OVERWRITE TABLE images_internal
SELECT * FROM images_input_external
WHERE width = 1024;

INSERT OVERWRITE TABLE images_output_external
SELECT * FROM image_internal;

通過把資料寫到一個內部images_internal表中,後面如果有**多次**讀取資料的需求的話,就可以不再去存取外部OSS了。 這裡也可以看到MaxCompute非結構化架構以及SQL文法本身提供了非常高的靈活性和可擴充性,使用者可以根據實際計算的不同圖樣/場景/需求,來在上面完成各種各樣的資料計算工作流程。

5. 結語

非結構化資料處理架構隨著MaxCompute2.0一起推出,意在豐富MaxCompute平臺的資料處理生態,來打通阿裡雲核心計算平臺與阿裡雲各個重要隱藏服務之間的資料連結。在之前介紹過的讀取OSS以及處理TableStore資料的整體方案後,本文側重介紹資料往OSS的輸出方案,並依託一個影像處理的處理實例,閱聽了【OSS->MaxCompute->OSS】整個資料連結的實現。在這些新功能的基礎上,我們希望實現整個阿裡雲計算與資料的生態融合:在不同的項目上,我們已經看到了在MaxCompute上處理OSS上的海量視訊,影像等非結構化資料的巨大潛力。今後隨著這個生態的豐富,我們期望OSS資料,TableStore資料以及MaxCompute內部隱藏的資料,都能在MaxCompute的核心計算引擎上進行融合,從而產生更大的價值。

相關產品:

  1. 巨量資料計算服務(MaxCompute)
  2. 企業級分散式套用服務
  3. 物件隱藏OSS
  4. 雲端服務器ECS
相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.