1. 程式人生 > >創業公司做資料分析(四)ELK日誌系統

創業公司做資料分析(四)ELK日誌系統

  作為系列文章的第四篇,本文將重點探討資料採集層中的ELK日誌系統。日誌,指的是後臺服務中產生的log資訊,通常會輸入到不同的檔案中,比如Django服務下,一般會有nginx日誌和uWSGI日誌。這些日誌分散地儲存在不同的機器上,取決於服務的部署情況了。如果我們依次登入每臺機器去查閱日誌,顯然非常繁瑣,效率也很低,而且也沒法進行統計和檢索。因此,我們需要對日誌進行集中化管理,將所有機器上的日誌資訊收集、彙總到一起。完整的日誌資料具有非常重要的作用:

  • 資訊查詢。通過檢索日誌資訊,定位相應的bug,找出解決方案。
  • 服務診斷。通過對日誌資訊進行統計、分析,瞭解伺服器的負荷和服務執行狀態,找出耗時請求進行優化等等。
  • 資料分析。如果是格式化的log,可以做進一步的資料分析,統計、聚合出有意義的資訊,比如根據請求中的商品id,找出TOP10使用者感興趣商品。

  ELK是一套開源的集中式日誌資料管理的解決方案,由ElasticsearchLogstashKibana三個系統組成。最初我們建設ELK日誌系統的目的是做資料分析,記得第一個需求是期望利用nginx的日誌,從API請求的引數中挖掘出使用者的位置分佈資訊。後來該系統在追蹤惡意刷量、優化耗時服務等方面都發揮了重要作用,而且隨著對Elasticsearch的認知加深,我們將其應用到了其他方面的資料儲存和分析中。 本文的重點是結合自身實踐來介紹如何使用ELK系統、使用中的問題以及如何解決,文中涉及的ELK版本是:Elasticsearch 2.3、Logstash 2.3、Kibana 4。

ELK整體方案

  ELK中的三個系統分別扮演不同的角色,組成了一個整體的解決方案。Logstash是一個ETL工具,負責從每臺機器抓取日誌資料,對資料進行格式轉換和處理後,輸出到Elasticsearch中儲存。Elasticsearch是一個分散式搜尋引擎和分析引擎,用於資料儲存,可提供實時的資料查詢。Kibana是一個數據視覺化服務,根據使用者的操作從Elasticsearch中查詢資料,形成相應的分析結果,以圖表的形式展現給使用者。
  ELK的安裝很簡單,可以按照“下載->修改配置檔案->啟動”方法分別部署三個系統,也可以使用docker來快速部署。具體的安裝方法這裡不詳細介紹,我們來看一個常見的部署方案,如下圖所示,部署思路是:

  • 第一,在每臺生成日誌檔案的機器上,部署Logstash,作為Shipper的角色,負責從日誌檔案中提取資料,但是不做任何處理,直接將資料輸出到Redis佇列(list)中;
  • 第二,需要一臺機器部署Logstash,作為Indexer的角色,負責從Redis中取出資料,對資料進行格式化和相關處理後,輸出到Elasticsearch中儲存;
  • 第三,部署Elasticsearch叢集,當然取決於你的資料量了,資料量小的話可以使用單臺服務,如果做叢集的話,最好是有3個以上節點,同時還需要部署相關的監控外掛;
  • 第四,部署Kibana服務,提供Web服務。

  在前期部署階段,主要工作是Logstash節點和Elasticsearch叢集的部署,而在後期使用階段,主要工作就是Elasticsearch叢集的監控和使用Kibana來檢索、分析日誌資料了,當然也可以直接編寫程式來消費Elasticsearch中的資料。
  在上面的部署方案中,我們將Logstash分為Shipper和Indexer兩種角色來完成不同的工作,中間通過Redis做資料管道,為什麼要這樣做?為什麼不是直接在每臺機器上使用Logstash提取資料、處理、存入Elasticsearch?
  首先,採用這樣的架構部署,有三點優勢:第一,降低對日誌所在機器的影響,這些機器上一般都部署著反向代理或應用服務,本身負載就很重了,所以儘可能的在這些機器上少做事;第二,如果有很多臺機器需要做日誌收集,那麼讓每臺機器都向Elasticsearch持續寫入資料,必然會對Elasticsearch造成壓力,因此需要對資料進行緩衝,同時,這樣的緩衝也可以一定程度的保護資料不丟失;第三,將日誌資料的格式化與處理放到Indexer中統一做,可以在一處修改程式碼、部署,避免需要到多臺機器上去修改配置。
  其次,我們需要做的是將資料放入一個訊息佇列中進行緩衝,所以Redis只是其中一個選擇,也可以是RabbitMQ、Kafka等等,在實際生產中,Redis與Kafka用的比較多。由於Redis叢集一般都是通過key來做分片,無法對list型別做叢集,在資料量大的時候必然不合適了,而Kafka天生就是分散式的訊息佇列系統。

Logstash

  在官方文件中,Deploying and Scaling Logstash一文詳細介紹了各種Logstash的部署架構,下圖是與我們上述方案相吻合的架構。Logstash由input、filter和output三部分組成,input負責從資料來源提取資料,filter負責解析、處理資料,output負責輸出資料,每部分都有提供豐富的外掛。Logstash的設計思路也非常值得借鑑,以外掛的形式來組織功能,通過配置檔案來描述需要外掛做什麼。我們以nginx日誌為例,來看看如何使用一些常用外掛。

1. 配置nginx日誌格式
  首先需要將nginx日誌格式規範化,便於做解析處理。在nginx.conf檔案中設定:

log_format main '$remote_addr "$time_iso8601" "$request" $status $body_bytes_sent "$http_user_agent" "$http_referer" "$http_x_forwarded_for" "$request_time" "$upstream_response_time" "$http_cookie" "$http_Authorization" "$http_token"';

access_log  /var/log/nginx/example.access.log  main;

2. nginx日誌–>>Logstash–>>訊息佇列
  這部分是Logstash Shipper的工作,涉及input和output兩種外掛。input部分,由於需要提取的是日誌檔案,一般使用file外掛,該外掛常用的幾個引數是:

  • path,指定日誌檔案路徑。
  • type,指定一個名稱,設定type後,可以在後面的filter和output中對不同的type做不同的處理,適用於需要消費多個日誌檔案的場景。
  • start_position,指定起始讀取位置,“beginning”表示從檔案頭開始,“end”表示從檔案尾開始(類似tail -f)。
  • sincedb_path,與Logstash的一個坑有關。通常Logstash會記錄每個檔案已經被讀取到的位置,儲存在sincedb中,如果Logstash重啟,那麼對於同一個檔案,會繼續從上次記錄的位置開始讀取。如果想重新從頭讀取檔案,需要刪除sincedb檔案,sincedb_path則是指定了該檔案的路徑。為了方便,我們可以根據需要將其設定為“/dev/null”,即不儲存位置資訊。
input {
    file {
        type => "example_nginx_access"
        path => ["/var/log/nginx/example.access.log"]

        start_position => "beginning"
        sincedb_path => "/dev/null"
    }
}

  output部分,將資料輸出到訊息佇列,以redis為例,需要指定redis server和list key名稱。另外,在測試階段,可以使用stdout來檢視輸出資訊。

# 輸出到redis
output {
    if [type] == "example_nginx_access" {
        redis {
            host => "127.0.0.1"
            port => "6379"
            data_type => "list"
            key => "logstash:example_nginx_access"
        }
      #  stdout {codec => rubydebug}
    }
}

3. 訊息佇列–>>Logstash–>>Elasticsearch
  這部分是Logstash Indexer的工作,涉及input、filter和output三種外掛。在input部分,我們通過redis外掛將資料從訊息佇列中取出來。在output部分,我們通過elasticsearch外掛將資料寫入Elasticsearch。

# 從redis輸入資料
input {
    redis {
            host => "127.0.0.1"
            port => "6379"
            data_type => "list"
            key => "logstash:example_nginx_access"
    }
}

output {
    elasticsearch {
        index => "logstash-example-nginx-%{+YYYY.MM}"
        hosts => ["127.0.0.1:9200"]
    }
}

  這裡,我們重點關注filter部分,下面列舉幾個常用的外掛,實際使用中根據自身需求從官方文件中查詢適合自己業務的外掛並使用即可,當然也可以編寫自己的外掛。

  • grok,是Logstash最重要的一個外掛,用於將非結構化的文字資料轉化為結構化的資料。grok內部使用正則語法對文字資料進行匹配,為了降低使用複雜度,其提供了一組pattern,我們可以直接呼叫pattern而不需要自己寫正則表示式,參考原始碼grok-patterns。grok解析文字的語法格式是*%{SYNTAX:SEMANTIC}*,SYNTAX是pattern名稱,SEMANTIC是需要生成的欄位名稱,使用工具Grok Debugger可以對解析語法進行除錯。例如,在下面的配置中,我們先使用grok對輸入的原始nginx日誌資訊(預設以message作為欄位名)進行解析,並新增新的欄位request_path_with_verb(該欄位的值是verb和request_path的組合),然後對request_path欄位做進一步解析。
  • kv,用於將某個欄位的值進行分解,類似於程式語言中的字串Split。在下面的配置中,我們將request_args欄位值按照“&”進行分解,分解後的欄位名稱以“request_args_”作為字首,並且丟棄重複的欄位。
  • geoip,用於根據IP資訊生成地理位置資訊,預設使用自帶的一份GeoLiteCity database,也可以自己更換為最新的資料庫,但是需要資料格式需要遵循Maxmind的格式(參考GeoLite),似乎目前只能支援legacy database,資料型別必須是.dat。下載GeoLiteCity.dat.gz後解壓, 並將檔案路徑配置到source中即可。
  • translate,用於檢測某欄位的值是否符合條件,如果符合條件則將其翻譯成新的值,寫入一個新的欄位,匹配pattern可以通過YAML檔案來配置。例如,在下面的配置中,我們對request_api欄位翻譯成更加易懂的文字描述。
filter {
    grok {
        match => {"message" => "%{IPORHOST:client_ip} \"%{TIMESTAMP_ISO8601:timestamp}\" \"%{WORD:verb} %{NOTSPACE:request_path} HTTP/%{NUMBER:httpversion}\" %{NUMBER:response_status:int} %{NUMBER:response_body_bytes:int} \"%{DATA:user_agent}\" \"%{DATA:http_referer}\" \"%{NOTSPACE:http_x_forwarder_for}\" \"%{NUMBER:request_time:float}\" \"%{DATA:upstream_resopnse_time}\" \"%{DATA:http_cookie}\" \"%{DATA:http_authorization}\" \"%{DATA:http_token}\""}
        add_field => {"request_path_with_verb" => "%{verb} %{request_path}"}
    }

    grok {
        match => {"request_path" => "%{URIPATH:request_api}(?:\?%{NOTSPACE:request_args}|)"}
        add_field => {"request_annotation" => "%{request_api}"}
    }

    kv {
        prefix => "request_args_"
        field_split => "&"
        source => "request_args"
        allow_duplicate_values => false
    }

    geoip {
        source => "client_ip"
        database => "/home/elktest/geoip_data/GeoLiteCity.dat"
    }

   translate {
        field => request_path
        destination => request_annotation
        regex => true
        exact => true
        dictionary_path => "/home/elktest/api_annotation.yaml"
        override => true
    }
}


Elasticsearch

  Elasticsearch承載了資料儲存和查詢的功能,其基礎概念和使用方法可以參考另一篇博文Elasticsearch使用總結,這裡主要介紹些實際生產中的問題和方法:

  • 關於叢集配置,重點關注三個引數:第一,discovery.zen.ping.unicast.hosts,Elasticsearch預設使用Zen Discovery來做節點發現機制,推薦使用unicast來做通訊方式,在該配置項中列舉出Master節點。第二,discovery.zen.minimum_master_nodes,該引數表示叢集中可工作的具有Master節點資格的最小數量,預設值是1。為了提高叢集的可用性,避免腦裂現象(所謂腦裂,就是同一個叢集中的不同節點,對叢集的狀態有不一致的理解。),官方推薦設定為(N/2)+1,其中N是具有Master資格的節點的數量。第三,discovery.zen.ping_timeout,表示節點在發現過程中的等待時間,預設值是3秒,可以根據自身網路環境進行調整,一定程度上提供可用性。
discovery.zen.ping.unicast.hosts: ["master1", "master2", "master3"] 
discovery.zen.minimum_master_nodes: 2
discovery.zen.ping_timeout: 10
  • 關於叢集節點,第一,節點型別包括:候選Master節點、資料節點和Client節點。通過設定兩個配置項node.master和node.data為true或false,來決定將一個節點分配為什麼型別的節點。第二,儘量將候選Master節點和Data節點分離開,通常Data節點負載較重,需要考慮單獨部署。
  • 關於記憶體,Elasticsearch預設設定的記憶體是1GB,對於任何一個業務部署來說,這個都太小了。通過指定ES_HEAP_SIZE環境變數,可以修改其堆記憶體大小,服務程序在啟動時候會讀取這個變數,並相應的設定堆的大小。建議設定系統記憶體的一半給Elasticsearch,但是不要超過32GB。參考官方文件
  • 關於硬碟空間,Elasticsearch預設將資料儲存在/var/lib/elasticsearch路徑下,隨著資料的增長,一定會出現硬碟空間不夠用的情形,此時就需要給機器掛載新的硬碟,並將Elasticsearch的路徑配置到新硬碟的路徑下。通過“path.data”配置項來進行設定,比如“path.data: /data1,/var/lib/elasticsearch,/data”。需要注意的是,同一分片下的資料只能寫入到一個路徑下,因此還是需要合理的規劃和監控硬碟的使用。
  • 關於Index的劃分和分片的個數,這個需要根據資料量來做權衡了,Index可以按時間劃分,比如每月一個或者每天一個,在Logstash輸出時進行配置,shard的數量也需要做好控制。
  • 關於監控,筆者使用過head和marvel兩個監控外掛,head免費,功能相對有限,marvel現在需要收費了。另外,不要在資料節點開啟監控外掛。

Kibana

  Kibana提供的是資料查詢和顯示的Web服務,有豐富的圖表樣板,能滿足大部分的資料視覺化需求,這也是很多人選擇ELK的主要原因之一。UI的操作沒有什麼特別需要介紹的,經常使用就會熟練,這裡主要介紹經常遇到的三個問題。
1. 查詢語法
  在Kibana的Discover頁面中,可以輸入一個查詢條件來查詢所需的資料。查詢條件的寫法使用的是Elasticsearch的Query String語法,而不是Query DSL,參考官方文件query-string-syntax,這裡列舉其中部分常用的:

  • 單欄位的全文檢索,比如搜尋args欄位中包含first的文件,寫作 args:first;
  • 單欄位的精確檢索,比如搜尋args欄位值為first的文件,寫作 args: “first”;
  • 多個檢索條件的組合,使用 NOT, AND 和 OR 來組合,注意必須是大寫,比如 args:(“first” OR “second”) AND NOT agent: “third”;
  • 欄位是否存在,_exists_:agent表示要求agent欄位存在,_missing_:agent表示要求agent欄位不存在;
  • 萬用字元:用 ? 表示單字母,* 表示任意個字母。

2. 錯誤“Discover: Request Timeout after 30000ms”
  這個錯誤經常發生在要查詢的資料量比較大的情況下,此時Elasticsearch需要較長時間才能返回,導致Kibana發生Timeout報錯。解決這個問題的方法,就是在Kibana的配置檔案中修改elasticsearch.requestTimeout一項的值,然後重啟Kibana服務即可,注意單位是ms。

3. 疑惑“字串被分解了”
  經常在QQ群裡看到一些人在問這樣一個問題:為什麼查詢結果的欄位值是正確的,可是做圖表時卻發現欄位值被分解了,不是想要的結果?如下圖所示的client_agent_info欄位。


  得到這樣一個不正確結果的原因是使用了Analyzed欄位來做圖表分析,預設情況下Elasticsearch會對字串資料進行分析,建立倒排索引,所以如果對這麼一個欄位進行terms聚合,必然會得到上面所示的錯誤結果了。那麼應該怎麼做才對?預設情況下,Elasticsearch還會建立一個相對應的沒有被Analyzed的欄位,即帶“.raw”字尾的欄位,在這樣的欄位上做聚合分析即可。
  又會有很多人問這樣的問題:為什麼我的Elasticsearch沒有自動建立帶“.raw”字尾的欄位?然而在Logstash中輸出資料時,設定index名稱字首為“logstash-”就有了這個欄位。這個問題的根源是Elasticsearch的dynamic template在搗鬼(可以檢視博文Elasticsearch使用總結中的詳細介紹),dynamic temlate用於指導Elasticsearch如何為插入的資料自動建立Schema對映關係,預設情況下,Logstash會在Elasticsearch中建立一個名為“logstash”的模板,所有字首為“logstash-”的index都會參照這個模板來建立對映關係,在該模板中申明瞭要為每個字串資料建立一個額外的帶“.raw”字尾的欄位。可以向Elasticsearch來查詢你的模板,使用API:GET http://localhost:9200/_template。




  以上便是對ELK日誌系統的總結介紹,還有一個重要的功能沒有提到,就是如何將日誌資料與自身產品業務的資料融合起來。舉個例子,在nginx日誌中,通常會包含API請求訪問時攜帶的使用者Token資訊,由於Token是有時效性的,我們需要及時將這些Token轉換成真實的使用者資訊儲存下來。這樣的需求通常有兩種實現方式,一種是自己寫一個Logstash filter,然後在Logstash處理資料時呼叫;另一種是將Logstash Indexer產生的資料再次輸出到訊息佇列中,由我們自己的指令碼程式從訊息佇列中取出資料,做相應的業務處理後,輸出到Elasticsearch中。目前,團隊對ruby技術棧不是很熟悉,所以我們採用了第二種方案來實施。
  當前,我們的資料增長相對緩慢,遇到的問題也有限,隨著資料量的增加,未來一定會遇到更多的挑戰,也可以進一步探索ELK。


(全文完,本文地址:http://blog.csdn.net/zwgdft/article/details/53842574) Bruce,2017/01/06