LinkedIn-DataHub專題: 初識DataHub


本文僅從普及角度讓大家對元數據中心系統及其DataHub有個初步了解。DataHub部署、實戰、更深入的技術剖析會單獨給出

介紹

DataHub是由LinkedIn的數據團隊開源的一款提供元數據搜索與發現的工具,在數據資產越來越重視的當下,探索數據治理解決方案,以滿足不斷增長的大數據復雜生態系統需求。
在這之前我們有必要先了解下整個大環境及其發展歷程。

為什么需要元數據管理系統

隨着企業的發展,不同的業務場景產生了不同形式的海量技術、業務數據。如何提取出有用的數據來幫助解決特定場景、發現潛在價值成為數據科學家的核心難題之一。業界通過元數據來提高數據科學家的生產力,一種描述數據的數據。(元數據概念請自行了解)不同的用例通常都會有自己特殊的元數據定義及其關系,最常見的如用戶元數據、報表元數據、關系元數據。我們需要一套完備的系統幫助專業人員收集、組織、訪問和豐富元數據,以支持數據發現和管理(俗稱:數據治理,在數據資產化下數據治理尤為重要)。而如何設計一套行之有效的系統,更方便、快速的豐富、查詢、使用元數據成為各大企業探索的目標。(國外更多的稱呼該系統為 元數據中心\元數據目錄)


簡單點說,你想要一份數據,如果通過一個人就能拿到,那可能就不需要該系統;但往往在大公司里,這些數據散落在不同系統、存儲、地域,你甚至不知道有什么數據,找誰拿,而元數據中心系統就可以幫助你快速准確定位到想要的數據,同時還能知道誰在使用、誰創建的、數據依賴,什么時候數據從A變成B等等。

元數據中心系統發展歷程

本文檔編寫時,元數據中心系統架構經歷了三代演變。在該領域內Lyft’s Amundsen、DataHub處於領先者,Amundsen是社區最活躍的,DataHub也越來越被關注。

更詳細的架構演進請跳轉此文:LinkedIn-DataHub專題: 元數據中心系統架構演進

image.png

第三代架構確保我們能夠以最具伸縮性和靈活性的方式集成、存儲和處理元數據。本文的主角DataHub就是基於三代架構進行構建,市面上具有三代架構特性的還有Apache AtlasEgeriaUber Databook(非開源)。Atlas與Hadoop生態系統緊密耦合,最活躍的Amundsen現已可以與Atlas做整合;Egeria支持事件,但功能還不完整;Databook與DataHub較接近,但不開源。DataHub經歷了WhereHows(第二代)的過渡,也存在內部版本(開源版版本與內部版本區別看這),在LinkedIn內部被廣泛使用,每天處理超過千萬實體和關系的變更事件,總計索引超過500萬個實體和關系,毫秒級查詢,用戶體驗也獲得了極大的改進。不難看出LinkedIn的野心:推進DataHub成為數據資產的基礎設施進程。

初探DataHub

DataHub功能清單(20210219)

最新功能清單以官方在線版為准: https://github.com/linkedin/datahub/blob/master/docs/features.md

開源版的數據結構僅支持Datasets、People;數據集支持:Hive、Kafka、RDBMS(如果需要額外的數據集,需編程式定義);存儲源支持Oracle、Postgres、MySQL、H2等主流RDBMS 、Elasticsearch和Neo4j。除了以下列的這些,還有部分功能也在規划中,比如儀表盤、指標信息、元數據結構變更記錄、數據抓取任務執行記錄等等。

數據集集合

  • [x] 搜索:全文高級搜索,搜索排名
  • [x] 瀏覽可配置的瀏覽層次結構
  • [x] Schema:表格和JSON格式的表文檔結構
  • [x] 粗粒度血緣:支持數據集級別的上下游血緣圖形可視化
  • [x] Ownership:呈現數據集的所有者,查看您擁有的數據集
  • [x] 數據集生命周期管理
  • [x] 數據共享:支持向任何數據集添加自由格式文檔
  • [ ] 細粒度血緣:支持字段級別
  • [ ] 喜歡、關注、書簽
  • [ ] Compliance management:field level tag based compliance editing
  • [ ] Top 榜:數據集使用頻次、或用戶使用頻次榜單

用戶

  • [x] 搜索:全文高級搜索,搜索排名
  • [x] 個人資料編輯:如摘要、技能
  • [ ] 瀏覽:browsing through a configurable hierarchy

DataHub (& GMA)架構

DatahHub采用前后端分離+微服務/容器架構,但其完整的技術棧給我們帶來了一定的挑戰。(頭大,會的越多越不會了)

  • 前端:Ember + TypeScript + ES9 + ES.Next + Yarn + ESLint
  • 服務端:Play Framework(web框架) + Spring + Rest.li(restful框架)+ Pegasus(數據建模語言) + Apache Samza (流處理框架)
  • 基礎設施:elastic search(5.6) + Mysql + neo4j + kafka
  • 構建工具:Gradlew + Docker + Docker compose

image.png
DataHub 組成

  • datahub-gms (Generalized Metadata Store) : 元數據存儲服務
  • datahub-gma (Generalized Metadata Architecture) : 通用元數據體系結構

GMA是datahub的基礎設施,提供標准化的元數據模型和訪問層

  • datahub-frontend : 應用前端
  • datahub-mxe 元數據事件
    • datahub-mce-consumer (MetadataChangeEvent):元數據變更事件,由平台或爬蟲程序發起,寫入到GMS
    • datahub-mae-consumer (MetadataAuditEvent): 元數據審計事件,只有被成功處理的MCE才會產生相應的MAE,由GMS發起 ,寫入到es&Neo4j

一個完整DataHub應用所需部署的組件清單
image.png

DataHub web應用截圖

前端提供三種類型的交互:(1)搜索,(2)瀏覽,(3)查看/編輯元數據。
以下是一些實際應用的截圖。
v2-139d4719c96146cfb3c56cef5ac3d1ec_r.jpg

元數據建模

DataHub選擇了Pegasus對元數據建模。由於Pegasus沒有提供模型關系或關聯的明確方法,因此引入了一些自定義擴展來支持這些用例。
image.png
以上圖實體關系團來說,包含了三種類型實體:用戶、組、數據集;同時也包含了三種關系:OwnedBy,HasMember和HasAdmin。與傳統的ERD不同,我們將實體和關系的屬性分別直接放在圓圈內和關系名稱下面,以便將新類型的組件(稱為“元數據方面”)附加到實體。不同的團隊可以擁有和發展同一實體元數據的不同方面,而不會相互干擾,從而實現分布式元數據建模要求。三種類型的元數據方面:所有權,配置文件和成員資格在上面的示例中呈現為綠色矩形。虛線表示元數據方面與實體的關聯。例如,配置文件可以與用戶相關聯,且所有權可以與數據集等相關聯。
每個實體,關系和“元數據方面”都是單獨的Pegasus文件(PDSC/PDL),User(PDL文件)實體和OwnedBy(PDL文件)關系分別如下(DataHub內部維護了兩種文件類型 pdl和avsc (json格式),看官方說明,內部建模都會改成pdl,而網絡傳輸(MCE)則用avsc格式):

關於PDSC/PDL, AVSC相關的請看該文檔:https://linkedin.github.io/rest.li/pdl_schema

image.png
image.png
以OwnerBy為例,編譯完會生成以下兩個文件OwnerBy.avsc、OwnerBy.java:

{
  "type" : "record",
  "name" : "OwnedBy",
  "namespace" : "com.linkedin.metadata.relationship",
  "doc" : "A generic model for the Owned-By relationship",
  "fields" : [ {
    "name" : "source",
    "type" : "string",
    "doc" : "Urn for the source of the relationship",
    "java" : {
      "class" : "com.linkedin.common.urn.Urn"
    }
  }, {
    "name" : "destination",
    "type" : "string",
    "doc" : "Urn for the destination of the relationship",
    "java" : {
      "class" : "com.linkedin.common.urn.Urn"
    }
  }, {
    "name" : "type",
    "type" : {
      "type" : "enum",
      "name" : "OwnershipType",
      "namespace" : "com.linkedin.common",
      "doc" : "Owner category or owner role",
      "symbols" : [ "DEVELOPER", "DATAOWNER", "DELEGATE", "PRODUCER", "CONSUMER", "STAKEHOLDER" ],
      "symbolDocs" : {
        "CONSUMER" : "A person, group, or service that consumes the data",
        "DATAOWNER" : "A person or group that is owning the data",
        "DELEGATE" : "A person or a group that overseas the operation, e.g. a DBA or SRE.",
        "DEVELOPER" : "A person or group that is in charge of developing the code",
        "PRODUCER" : "A person, group, or service that produces/generates the data",
        "STAKEHOLDER" : "A person or a group that has direct business interest"
      }
    },
    "doc" : "The type of the ownership"
  } ],
  "pairings" : [ {
    "destination" : "com.linkedin.common.urn.CorpuserUrn",
    "source" : "com.linkedin.common.urn.DatasetUrn"
  }, {
    "destination" : "com.linkedin.common.urn.CorpuserUrn",
    "source" : "com.linkedin.common.urn.DataProcessUrn"
  } ]
}

package com.linkedin.metadata.relationship;

/**
 * A generic model for the Owned-By relationship
 * 
 */
@Generated(value = "com.linkedin.pegasus.generator.JavaCodeUtil", comments = "Rest.li Data Template. Generated from metadata-models/src/main/pegasus/com/linkedin/metadata/relationship/OwnedBy.pdl.")
public class OwnedBy
    extends RecordTemplate
{

    private final static OwnedBy.Fields _fields = new OwnedBy.Fields();
    private final static RecordDataSchema SCHEMA = ((RecordDataSchema) DataTemplateUtil.parseSchema("namespace com.linkedin.metadata.relationship/**A generic model for the Owned-By relationship*/@pairings=[{\"destination\":\"com.linkedin.common.urn.CorpuserUrn\",\"source\":\"com.linkedin.common.urn.DatasetUrn\"},{\"destination\":\"com.linkedin.common.urn.CorpuserUrn\",\"source\":\"com.linkedin.common.urn.DataProcessUrn\"}]record OwnedBy includes/**Common fields that apply to all relationships*/record BaseRelationship{/**Urn for the source of the relationship*/source:{namespace com.linkedin.common@java.class=\"com.linkedin.common.urn.Urn\"typeref Urn=string}/**Urn for the destination of the relationship*/destination:com.linkedin.common.Urn}{/**The type of the ownership*/type:{namespace com.linkedin.common/**Owner category or owner role*/enum OwnershipType{/**A person or group that is in charge of developing the code*/DEVELOPER/**A person or group that is owning the data*/DATAOWNER/**A person or a group that overseas the operation, e.g. a DBA or SRE.*/DELEGATE/**A person, group, or service that produces/generates the data*/PRODUCER/**A person, group, or service that consumes the data*/CONSUMER/**A person or a group that has direct business interest*/STAKEHOLDER}}}", SchemaFormatType.PDL));
    private final static RecordDataSchema.Field FIELD_Source = SCHEMA.getField("source");
    private final static RecordDataSchema.Field FIELD_Destination = SCHEMA.getField("destination");
    private final static RecordDataSchema.Field FIELD_Type = SCHEMA.getField("type");

    static {
        Custom.initializeCustomClass(com.linkedin.common.urn.Urn.class);
    }

    public OwnedBy() {
        super(new DataMap(4, 0.75F), SCHEMA);
    }

    public OwnedBy(DataMap data) {
        super(data, SCHEMA);
    }

    public static OwnedBy.Fields fields() {
        return _fields;
    }

    /**
     * Existence checker for source
     * 
     * @see OwnedBy.Fields#source
     */
    public boolean hasSource() {
        return contains(FIELD_Source);
    }

    /**
     * Remover for source
     * 
     * @see OwnedBy.Fields#source
     */
    public void removeSource() {
        remove(FIELD_Source);
    }

    /**
     * Getter for source
     * 
     * @see OwnedBy.Fields#source
     */
    public com.linkedin.common.urn.Urn getSource(GetMode mode) {
        return obtainCustomType(FIELD_Source, com.linkedin.common.urn.Urn.class, mode);
    }

    /**
     * Getter for source
     * 
     * @return
     *     Required field. Could be null for partial record.
     * @see OwnedBy.Fields#source
     */
    @Nonnull
    public com.linkedin.common.urn.Urn getSource() {
        return obtainCustomType(FIELD_Source, com.linkedin.common.urn.Urn.class, GetMode.STRICT);
    }

    /**
     * Setter for source
     * 
     * @see OwnedBy.Fields#source
     */
    public OwnedBy setSource(com.linkedin.common.urn.Urn value, SetMode mode) {
        putCustomType(FIELD_Source, com.linkedin.common.urn.Urn.class, String.class, value, mode);
        return this;
    }

    /**
     * Setter for source
     * 
     * @param value
     *     Must not be null. For more control, use setters with mode instead.
     * @see OwnedBy.Fields#source
     */
    public OwnedBy setSource(
        @Nonnull
        com.linkedin.common.urn.Urn value) {
        putCustomType(FIELD_Source, com.linkedin.common.urn.Urn.class, String.class, value, SetMode.DISALLOW_NULL);
        return this;
    }

    /**
     * Existence checker for destination
     * 
     * @see OwnedBy.Fields#destination
     */
    public boolean hasDestination() {
        return contains(FIELD_Destination);
    }

    /**
     * Remover for destination
     * 
     * @see OwnedBy.Fields#destination
     */
    public void removeDestination() {
        remove(FIELD_Destination);
    }

    /**
     * Getter for destination
     * 
     * @see OwnedBy.Fields#destination
     */
    public com.linkedin.common.urn.Urn getDestination(GetMode mode) {
        return obtainCustomType(FIELD_Destination, com.linkedin.common.urn.Urn.class, mode);
    }

    /**
     * Getter for destination
     * 
     * @return
     *     Required field. Could be null for partial record.
     * @see OwnedBy.Fields#destination
     */
    @Nonnull
    public com.linkedin.common.urn.Urn getDestination() {
        return obtainCustomType(FIELD_Destination, com.linkedin.common.urn.Urn.class, GetMode.STRICT);
    }

    /**
     * Setter for destination
     * 
     * @see OwnedBy.Fields#destination
     */
    public OwnedBy setDestination(com.linkedin.common.urn.Urn value, SetMode mode) {
        putCustomType(FIELD_Destination, com.linkedin.common.urn.Urn.class, String.class, value, mode);
        return this;
    }

    /**
     * Setter for destination
     * 
     * @param value
     *     Must not be null. For more control, use setters with mode instead.
     * @see OwnedBy.Fields#destination
     */
    public OwnedBy setDestination(
        @Nonnull
        com.linkedin.common.urn.Urn value) {
        putCustomType(FIELD_Destination, com.linkedin.common.urn.Urn.class, String.class, value, SetMode.DISALLOW_NULL);
        return this;
    }

    /**
     * Existence checker for type
     * 
     * @see OwnedBy.Fields#type
     */
    public boolean hasType() {
        return contains(FIELD_Type);
    }

    /**
     * Remover for type
     * 
     * @see OwnedBy.Fields#type
     */
    public void removeType() {
        remove(FIELD_Type);
    }

    /**
     * Getter for type
     * 
     * @see OwnedBy.Fields#type
     */
    public OwnershipType getType(GetMode mode) {
        return obtainDirect(FIELD_Type, OwnershipType.class, mode);
    }

    /**
     * Getter for type
     * 
     * @return
     *     Required field. Could be null for partial record.
     * @see OwnedBy.Fields#type
     */
    @Nonnull
    public OwnershipType getType() {
        return obtainDirect(FIELD_Type, OwnershipType.class, GetMode.STRICT);
    }

    /**
     * Setter for type
     * 
     * @see OwnedBy.Fields#type
     */
    public OwnedBy setType(OwnershipType value, SetMode mode) {
        putDirect(FIELD_Type, OwnershipType.class, String.class, value, mode);
        return this;
    }

    /**
     * Setter for type
     * 
     * @param value
     *     Must not be null. For more control, use setters with mode instead.
     * @see OwnedBy.Fields#type
     */
    public OwnedBy setType(
        @Nonnull
        OwnershipType value) {
        putDirect(FIELD_Type, OwnershipType.class, String.class, value, SetMode.DISALLOW_NULL);
        return this;
    }

    @Override
    public OwnedBy clone()
        throws CloneNotSupportedException
    {
        return ((OwnedBy) super.clone());
    }

    @Override
    public OwnedBy copy()
        throws CloneNotSupportedException
    {
        return ((OwnedBy) super.copy());
    }

    public static class Fields
        extends PathSpec
    {


        public Fields(List<String> path, String name) {
            super(path, name);
        }

        public Fields() {
            super();
        }

        /**
         * Urn for the source of the relationship
         * 
         */
        public PathSpec source() {
            return new PathSpec(getPathComponents(), "source");
        }

        /**
         * Urn for the destination of the relationship
         * 
         */
        public PathSpec destination() {
            return new PathSpec(getPathComponents(), "destination");
        }

        /**
         * The type of the ownership
         * 
         */
        public PathSpec type() {
            return new PathSpec(getPathComponents(), "type");
        }

    }

}

可以看下如果要新增一個元模型/實體要怎么操作。特別提下,URN類似於唯一標識/類型,數據建模相關后面會單開一篇來講,暫不展開。
image.png

數據接入

DataHub提供兩種數據接入方式:API調用或Kafka流。
DataHub的API基於Rest.li,Rest.li使用的是Pegasus作為接口定義,因此可以復用元數據模型。Kafka方式接收MCE,傳輸的格式為Avro(json格式),由Pegasus自動生成。由Apache Samza作為流處理框架,將Avro數據格式轉換回Pegasus,並調用相應API。
image.png

數據服務&索引

DataHub支持四中常見查詢:1、面向文檔的查詢;2、面向圖形的查詢;3、支持連接的復雜查詢;4、全文檢索
DataHub底層采用多級存儲,以適配以上檢索場景。並抽象出DAO層,以滿足上層無感知調用。

image.png

結論

可以看出DataHub在元數據中心領域所做的努力,不但其架構的迭代、擴展性,以及未來將引入的新功能。希望將LinkedIn內部在元數據中心建設的經驗分享並輸出成業界通用的解決方案,借助LinkedIn內部和社區的發展,在未來還真有望成為下一代數據資產的基礎設施。只是對國內來說,小眾化的技術組件和不多的實踐文檔讓企業決策者和開發者望而卻步。但其先進的理念和架構,還是值得大家研究借鑒。

參考文獻

[1] Open sourcing DataHub: LinkedIn’s metadata search and discovery platform
[2] DataHub: Popular metadata architectures explained
[3] A Dive Into Metadata Hubs
[4] 數據治理篇-元數據: datahub概述
[5] DataPipeline丨LinkedIn元數據之旅的最新進展—Data Hub 【譯】


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM