MaxCompute實戰之資料儲存

sibenx發表於2016-08-30

MaxCompute(原名ODPS),是阿里巴巴自主研發的海量資料處理平臺。 主要服務於批量結構化資料的儲存和計算,可以提供海量資料倉儲的解決方案以及針對大資料的分析建模服務。支援PB級別資料分析,常用場景如下:大型網際網路企業的資料倉儲和BI分析、網站的日誌分析、電子商務網站的交易分析、使用者特徵和興趣挖掘等。

我們接入MaxCompute主要做兩個工作,一是網站的日誌分析,二是使用者特徵和興趣挖掘。其中網站日誌分析專案組內(就是高德開放平臺)已經玩的比較溜了,“使用者特徵和興趣挖掘”還在探索中。

無論是做資料分析還是資料探勘,都離不開資料。而MaxCompute不負責收集資料,他只負責處理資料,就好比你有臺酸奶機但是沒牛奶(MaxCompute比作酸奶機,資料比作牛奶)。所以先把海量的日誌資料收集起來是第一步要做的工作。

MaxCompute中的後設資料儲存在阿里雲端計算的另一個開放服務Table Store(表格儲存服務)中,後設資料內容主要包括使用者空間後設資料、Table/Partition Schema、ACL、Job後設資料、安全體系等。

概念清晰了,我們就可以開始動手,下面說說如何將海量的日誌收集起來。

一. 資料收集

資料來源我們並不關心,可以是來自MySQL,也可以是文字檔案。不過我們的業務場景主要是生產大量的log(文字檔案)。比如Nginx請求日誌、PHP,Java,Node的服務日誌、自建的埋點日誌等。

如果你的資料不是文字檔案,可以參考這篇文章將資料匯入MaxCompute的方法彙總

首先我們要將這些日誌收集起來,這裡我們使用Fluentd服務(類似的服務還有kafka、LogHubDataX等,都大同小異,這裡我用Fluentd作描述只是方便),通過Fluentd我們輕鬆的建立任務去按時讀取各臺伺服器上的日誌檔案。簡單點說就是你只需要配置伺服器上日誌的路徑,Fluentd就幫你把日誌儲存到MaxCompute的Table Store中,然後你就能愉快的通過MaxCompute分析資料了。

使用Fluentd去管理資料收集方式確實能幫我們節省大量人力,而且方式相當便捷,分分鐘就能上線泡咖啡!但Fluentd帶來便利的同時就需要我們遵守它的規則。比如建立的Table Store表(這樣說好彆扭,還是叫MaxCompute表吧),必須按照Fluentd定義的欄位寫,預設四個欄位如下

  1. content 日誌內容
  2. ds 天,Fluentd自動生成
  3. hh 小時,Fluentd自動生成
  4. mm 分鐘,Fluentd自動生成

PS:如果個人使用Fluentd是可以自定義欄位的,不需要像我們一樣。我們團隊之所以規範欄位資訊,主要是為了之後的擴充套件和統一管理(上百個專案)。當然我也建議大家像我們一樣規範欄位,這樣做的好處是之後你能基於Fluentd做平臺。

如上可以看到真正能控制的欄位只有content,其他欄位Fluentd都幫你自動生成。所以在你要做資料分析時就需要用到無數的LIKE,這樣不但會導致查詢速度慢,最主要和最痛苦的是稍微複雜點的資料分析都無法實現,用LIKE去模仿等於、不等於、包含、不包含難度太高。你好不容易(其實並沒有很不容易)把平臺搭建好,並說這個服務多牛。一個月後PM要你產出一個報表你發現沒辦法,PM會覺得你做的啥玩意兒…

這個時候我們可以在埋點時對資料格式做下處理,用些小技巧。比如用各種符號做分隔符,定義一套log格式標準,總歸有很多總辦法解決問題。但是馬總不是說過嘛,解決問題的最好辦法就是不給他發生的機會……所以看下文

二. 自建MaxCompute資料來源

MaxCompute本身提供了Fluentd的所有功能介面,不過呼叫介面雖然簡單,配置環境卻很複雜。如果業務需要對資料做深入的分析和挖掘,就不得不自己配置環境。當然,這也是MaxCompute強大的體現(一般使用成本高的都讓人覺得很強大)……

  1. 首先,我們需要建立Table,這和MySQL基本一樣

    create table if not exists sale_detail(
    shop_name     string,
    customer_id   string,
    total_price   double)
    partitioned by (sale_date string,region string); //設定分割槽

    分割槽一定要考慮到,因為MaxCompute的查詢最多隻能顯示5000條資料,limit不支援offect,所以資料量一大就無法通過開發套件(Data IDE)做線上查詢。匯出資料到本地也需要使用分割槽欄位,分割槽越大一次請求能匯出的資料就越多,合理的設定分割槽非常重要。

  2. 通過MaxCompute DataHub Service(DHS) 通常稱為Datahub服務,去上傳資料。

    Datahub服務提供了SDK,不過是Java的,通過SDK可以實現實時上傳功能。因為Datahub服務介面不用建立MaxCompute任務(Task),所以速度非常快。可以向較高的QPS(Query Per Second)和較大的吞吐量的服務提供資料儲存支援。Datahub上的資料只會被儲存7天,之後會被刪除,被刪除之前會儲存到MaxCompute的表中。也可通過非同步的方式,呼叫介面同步Datahub中的資料到MaxCompute的表中。

    如果服務本身就使用Java可以直接使用SDK,非Java服務就需要權衡成本。應該還有類似Fluentd的平臺,可以到 阿里雲MaxCompute工具查詢。

三. 流程介紹

在實際專案中使用MaxCompute有4種通用的方案,這裡貼出流程圖供大家參考;

  1. 最簡單也是最通用的方法,使用Fluentd上傳資料,適用於簡單的資料儲存。如果只是用來收集日誌,出問題時容易排查問題可以選擇這種方案;流程圖如下:

    ![使用Fluentd上傳資料](https://yqfile.alicdn.com/e5081df1919fe1c35a4a1ff6f8638696dbacb1fa.png)
    
  2. 比較進階的方法,使用PyODPS上傳資料,適用於較為複雜的資料儲存。比如我們需要對收集的日誌做轉換,例如需要將IP地址轉換成城市等;流程圖如下:

    ![使用PyODPS上傳資料](https://yqfile.alicdn.com/21f3115c8b15449008412847a548622a5bfa1aef.png)
    
  3. 比較複雜,易於擴充套件的方法,需要使用Fluentd和PyODPS上傳資料,適用於需要對資料做大量分析的場景。例如多個部門需要的資料埋點也不一樣,分析的方法和想得到的報表也不一樣等;流程圖如下:

    ![使用Fluentd和PyODPS上傳資料](https://yqfile.alicdn.com/c1a818941f5442d317bcaaf85e34c8a89ec36ea9.png)
    
  4. 最實時的方法,使用Datahub上傳資料,適用於需要實時同步資料的業務場景。例如遊戲日誌處理,電商充值、購物等要求低延的場景;流程圖如下:

    ![使用Datahub上傳資料](https://yqfile.alicdn.com/9864a654ee39f8a6d02618c50f90827c6c489efe.png)
    

四. 總結

直接在生產環境上呼叫DHS對我們的PHP環境來說成本太高,所以暫時使用的是Fluentd收集日誌資料,然後通過PyODPS將資料下載到本地,處理後再傳到MaxCompute Table中。我們的業務場景比較麻煩所以流程顯得略複雜,如果你不需要做複雜的資料分析,建議直接使用Fluentd上傳資料就行。

五. 最後

最後貼出核心程式碼,供參考,語言使用Python。

  1. 封裝MaxCompute的連線,這樣方便我們隨時隨地使用MaxCompute的強大功能,只需要在程式碼中呼叫一句 “odps = OdpsConnect().getIntense()” ;

    #/usr/bin/python3
    __author__ = `layne.fyc@gmail.com`
    #coding=utf-8
    from odps import ODPS
    # 測試地址:endpoint=`http://service-corp.odps.aliyun-inc.com/api`
    # 正式地址:endpoint=`http://service.odps.aliyun-inc.com/api`
    debug = True
    onlineUrl = `http://service.odps.aliyun-inc.com/api`
    localUrl = `http://service-corp.odps.aliyun-inc.com/api`
    accessId = `填自己的`
    accessKey = `填自己的`
    
    class OdpsConnect:
        model = object
        def __init__(self):
            self.model = ODPS(accessId,accessKey,project=`專案名`, endpoint=(localUrl if debug else onlineUrl))         
        def getIntense(self):
            return self.model
        def exe(self,sql):
            return self.model.execute_sql(sql)
  2. 建立MaxCompute Table用來儲存我們的海量日誌,上萬億條資料都不在話下。

    from OdpsConnect import OdpsConnect
    from odps.models import Schema, Column, Partition
    odps = OdpsConnect().getIntense()
    odps.delete_table(`test_amap_analys`, if_exists=True)  #表存在時刪除
    
    #各種項
    columns = [
        Column(name=`uid`, type=`bigint`, comment=`user id`),
        Column(name=`ctime`, type=`bigint`, comment=`time stamp`),
        Column(name=`url`, type=`string`, comment=`url`),
        Column(name=`param`, type=`string`, comment=`param`),
        Column(name=`ip`, type=`string`, comment=`ip`),
        Column(name=`city`, type=`string`, comment=`city`)
    ]
    #分割槽資訊
    partitions = [
        Partition(name=`dt`, type=`bigint`, comment=`the partition day`)
    ]
    schema = Schema(columns=columns, partitions=partitions)
    table = odps.create_table(`test_amap_analys`, schema,if_not_exists=True)
    

    有幾點需要特別注意:

    1. MaxCompute Table只支援新增資料,不支援刪除與修改資料。如果要刪除髒資料只能先備份整張表到B,再刪除這張表A,再新建表A,最後將表B的備份資訊處理後重新匯入表A;所以,匯入資料一定要慎重。
    2. 分割槽資訊可以建立很多個,但是在匯入、匯出、某些特殊查詢時都要全量帶上。比如你的分割槽欄位為“a,b,c,d”,最後你在匯出資料時必須指定”a,b,c,d”的內容,只指定‘a,b’或者‘a.c’都是不行的。所以設定分割槽欄位也要慎重,儘量欄位設定少點,這裡建議通過資料量來設定,建議每個分割槽儲存2W條左右的資料。
  3. 通過Tunnel上傳資料到MaxCompute;

    描述:我們通過程式在伺服器上埋點了大量日誌檔案,檔名為“log/20160605.log”每天通過日期定時生成。每條埋點日誌的格式為:

    “name:amap|ip:19.19.19.19|uuid:110112|param:sdfsdf=123&fsdf=123|url:get-user-info|time:123123123”

    使用 ‘|’和‘:’號分隔,現在我們需要將所有日誌資料儲存到我們上一步建立的“test_amap_analys” 表中。

    #/usr/bin/python3
    __author__ = `layne.xfl@alibaba-inc.com`
    #coding=utf-8
    import datetime
    import urllib.parse
    #自己封裝的IP庫
    import db.IP
    from OdpsConnect import OdpsConnect
    from odps.models import Schema, Column, Partition
    from odps.tunnel import TableTunnel
    
    
    odps = OdpsConnect().getIntense()
    t = odps.get_table(`test_amap_analys`)
    records = []
    tunnel = TableTunnel(odps)
    #日誌檔名格式為 log/20160605.log
    #這裡預設上傳前5天資料
    sz = 5
    while sz > 0 :
        #get Yestoday
        last_time = (datetime.datetime.now() + datetime.timedelta(days=-sz)).strftime(`%Y%m%d`)
        # 分割槽不存在的時候需要建立,不然會報錯
        t.create_partition(`dt=`+last_time, if_not_exists=True)
        #建立連線會話
        upload_session = tunnel.create_upload_session(t.name, partition_spec=`dt=`+last_time)
        #通過日期規則構造的檔名
        file = `log/%s.log` % last_time
        with upload_session.open_record_writer(0) as writer:
            for line in open(file,`r`,encoding=`utf8`):
                arr = {}
                #我們的日誌使用 ‘|’和‘:’號分隔,
                #例如:‘name:amap|ip:19.19.19.19|uuid:110112|param:sdfsdf=123&fsdf=123|url:get-user-info|time:123123123’
                for tm in raw.split(`|`):
                    pstm = tm.split(`:`)
                    if(len(pstm)==2):
                        arr[pstm[0]] = pstm[1]
                #nginx中的ip引數可能被偽造,我們需要做過濾
                ip = arr.get(`ip`,``)
                if(len(ip)>15):
                    iparr = ip.split(`,`)
                    ip = iparr[-1].strip(" ")
                city = ``
                if ip != ``:
                    #通過ip獲取城市資訊
                    city = db.IP.find(ip)
                writer.write(t.new_record(
                    [
                        arr.get(`uid`,0),
                        arr.get(`time`,0),
                        arr.get(`url`,``),
                        urllib.parse.unquote(arr.get(`param`,``)).replace("\","*").replace(`"`,"*"),  #做URL_DECODE
                        ip,
                        city,
                        last_time
                    ]))
        upload_session.commit([0])
        sz = sz - 1;

    有幾點需要特別注意:

    1. 使用MaxCompute一定要記住,資料為重,分割槽先行。儲存資料,下載資料都要先設定好分割槽再運算元據。
    2. Nginx中拿到的IP引數能被偽造,不能直接使用,需要了解的話可看這篇文章 HTTP_X_FORWARDED_FOR偽造

有幫助的URL列表(不定期更新):

  1. MaxCompute WIKI
  2. PyODPS
  3. 產品連結MaxCompute官網
  4. 大資料開發套件(Data IDE)
  5. 資料匯入MaxCompute的方法彙總


相關文章