基於Gor實現流量複製(加middleware功能加強)

最近作功能重構,在上線前要求驗證重構後的代碼與老代碼實現邏輯是否一致,基於這個需求,須要在生產環境作一個功能將生產服務器上的流量複製一份發送到測試服務器上。前端

就這個事情這幾天考察了三種技術,1. 基於 nginx+lua 腳本,2. tcpcopy,3. gor。這裏大概說一下這三種方案:java

  • nginx+lua 腳本

這種方案的思路是在生產服務器前端架一層殼子,將請求攔截,而後基於 lua-nginx-module 模塊,寫 lua 腳本,使用其內置的 ngx.location.capture_multi ,對後端發起多個異步併發請求,而後統一將結果返回給前端。nginx

該方案須要安裝 nginx ,以及依賴 lua-nginx-module ,ngx_devel_kit 等模塊,而後須要寫lua代碼來複制請求。git

  • tcpcopy

這種方案是工做在網絡等 TCP 和 IP 層作請求複製,由於其間架構調整過兩次,如今的實現架構是在生產環境啓動 tcpcopy 進程,測試環境啓動 intecept 進程,而後配置複製請求的路徑。github

  • gor

這是今天搜到的用 Go 語言寫的工具,在生產服務器上安裝一個 tar 包,用 root 權限啓動命令便可正則表達式

1的方案安裝步驟較多,須要理解 nginx 處理請求的過程和 lua 腳本語法以及相關請求調用的 API 。2的安裝簡單一些,只須要在生產和測試服務器分別安裝 tcpcopy 和 intecept 便可(固然前提是他們依賴的 libpcap 之類也有了,不然也要安裝),而後啓動命令加參數便可,但因爲該方案工做在較爲底層,看起來比較重,實際跑了個例子就沒繼續研究。3安裝最簡單,下載一個 tar 包,解壓,sudo 執行即搞定。後端

我的比較傾向於3,因此這裏就介紹一下gor的實現方式。api

  1. 下載

根據操做系統環境下載安裝包 https://github.com/buger/gor/releases,建議選擇 master 分支的,個人是 Mac ,因此選擇了 tar 包bash

  1. 解壓
tar -xvf gor_v0.14.1_mac.tar.gz
複製代碼
  1. 驗證
sudo ./gor --input-raw :8080 --output-http http://192.168.22.33:8080
複製代碼

至此就搞定了,簡單吧!!! 這條命令是監控本地的 8080 端口,並實時複製請求到須要 192.168.22.33 的 8080 端口上,須要本地 root 執行權限。服務器

下面是個人擴展用法:

  1. 保存請求到文件
sudo ./gor --input-raw :8080 --output-file requests.gor
複製代碼

這裏將 8080 端口的請求保存到本地文件上,能夠用於線上請求記錄以後的功能回放

  1. 根據文件回放請求
sudo ./gor --input-file requests.gor --output-http http://192.168.22.33:8080
複製代碼

將上面保存的文件請求回放到 192.168.22.33 服務器的 8080 端口上

  1. url 過濾 包含 /order 的 URL 才發送請求
sudo ./gor --input-raw :8080 --output-http http://192.168.22.33:8080 --http-allow-url ^/order.
複製代碼
  1. url 過濾+記錄文件+請求回放
sudo ./gor --input-raw :8080 --output-file gor-order-requests.gor --output-http http://192.168.22.33:8080 --http-allow-url ^/order.

sudo ./gor --input-file gor-order-requests.gor --output-http http://192.168.22.33:8080
複製代碼
  1. url 過濾+記錄文件+記錄響應
sudo ./gor --input-raw-track-response --input-raw :8080 --output-file gor-order-request-response.gor --http-allow-url ^/order.
複製代碼

下面是別人整理的一些詳細配置說明,能夠參考一下

-cpuprofile string
        write cpu profile to file
  -debug verbose
        打開debug模式,顯示全部接口的流量 
  -http-allow-header value
        用一個正則表達式來匹配http頭部,若是請求的頭部沒有匹配上,則被拒絕 
         gor --input-raw :8080 --output-http staging.com --http-allow-header api-version:^v1 (default [])
  -http-allow-method value
        相似於一個白名單機制來容許經過的http請求方法,除此以外的方法都被拒絕.
        gor --input-raw :8080 --output-http staging.com --http-allow-method GET --http-allow-method OPTIONS (default [])
  -http-allow-url value
        一個正則表達式用來匹配url, 用來過濾徹底匹配的的url,在此以外的都被過濾掉 
         gor --input-raw :8080 --output-http staging.com --http-allow-url ^www. (default [])
  -http-disallow-header value
        用一個正則表達式來匹配http頭部,匹配到的請求會被拒絕掉
         gor --input-raw :8080 --output-http staging.com --http-disallow-header "User-Agent: Replayed by Gor" (default [])
  -http-disallow-url value
        用一個正則表達式來匹配url,若是請求匹配上了,則會被拒絕
         gor --input-raw :8080 --output-http staging.com --http-disallow-url ^www. (default [])
  -http-header-limiter value
        讀取請求,基於FNV32-1A散列來拒絕必定比例的特殊請求 
         gor --input-raw :8080 --output-http staging.com --http-header-imiter user-id:25% (default [])
  -http-original-host
        在--output-http的輸出中,一般gor會使用取代請求的http頭,因此應該禁用該選項,保留原始的主機頭
  -http-param-limiter value
        Takes a fraction of requests, consistently taking or rejecting a request based on the FNV32-1A hash of a specific GET param:
         gor --input-raw :8080 --output-http staging.com --http-param-limiter user_id:25% (default [])
  -http-rewrite-url value
        Rewrite the request url based on a mapping:
        gor --input-raw :8080 --output-http staging.com --http-rewrite-url /v1/user/([^\/]+)/ping:/v2/user/$1/ping (default [])
  -http-set-header value
        Inject additional headers to http reqest:
        gor --input-raw :8080 --output-http staging.com --http-set-header 'User-Agent: Gor' (default [])
  -http-set-param value
        Set request url param, if param already exists it will be overwritten:
        gor --input-raw :8080 --output-http staging.com --http-set-param api_key=1 (default [])
  -input-dummy value
        Used for testing outputs. Emits 'Get /' request every 1s (default [])
  -input-file value
        從一個文件中讀取請求
        gor --input-file ./requests.gor --output-http staging.com (default [])
  -input-http value
        從一個http接口讀取請求
        # Listen for http on 9000
        gor --input-http :9000 --output-http staging.com (default [])
  -input-raw value
        Capture traffic from given port (use RAW sockets and require *sudo* access):
        # Capture traffic from 8080 port
        gor --input-raw :8080 --output-http staging.com (default [])
  -input-tcp value
       用來在多個gor之間流轉流量
        # Receive requests from other Gor instances on 28020 port, and redirect output to staging
        gor --input-tcp :28020 --output-http staging.com (default [])
  -memprofile string
        write memory profile to this file
  -middleware string
        Used for modifying traffic using external command
  -output-dummy value
        用來測試輸入,打印出接收的數據. (default [])
  -output-file value
        把進入的請求寫入一個文件中
        gor --input-raw :80 --output-file ./requests.gor (default [])
  -output-http value
        轉發進入的請求到一個http地址上
        # Redirect all incoming requests to staging.com address 
        gor --input-raw :80 --output-http http://staging.com (default [])
  -output-http-elasticsearch string
        把請求和響應狀態發送到ElasticSearch:
        gor --input-raw :8080 --output-http staging.com --output-http-elasticsearch 'es_host:api_port/index_name'
  -output-http-redirects int
        設置多少次重定向被容許
  -output-http-stats
        每5秒鐘輸出一次輸出隊列的狀態 
  -output-http-timeout duration
       指定http的request/response超時時間,默認是5秒
  -output-http-workers int
        gor默認是動態的擴展工做者數量,你也能夠指定固定數量的工做者
  -output-tcp value
        用來在多個gor之間流轉流量
        # Listen for requests on 80 port and forward them to other Gor instance on 28020 port
        gor --input-raw :80 --output-tcp replay.local:28020 (default [])
  -output-tcp-stats
        每5秒鐘報告一次tcp輸出隊列的狀態
  -split-output true
        By default each output gets same traffic. If set to true it splits traffic equally among all outputs.
  -stats
        打開輸出隊列的狀態
  -verbose
        Turn on more verbose output
複製代碼

固然最好的仍是直接關注做者的 git 項目:https://github.com/buger/gor/wiki

下面是我基於這個工具作的一個 Middleware 的介紹。 關於 Middleware 的原理建議看看 https://github.com/buger/gor/wiki/Middleware ,再也不贅述。這裏介紹下我作的 MiddleWare 實現的功能:

  1. 我須要將生產環境的請求回放到測試環境,而後將生產的響應和測試的響應結果作比對,以校驗功能重構是否正常。因此須要在生產的日誌結果中加一個標記(好比自增加的ID或隨機數等),同時在請求回放的時候可以將該標記帶到測試環境去。我原來想法是加一個自定義的請求 Header ,通過試驗發現這並不能將結果帶到測試輸出的請求響應文件中,致使沒法根據兩份日誌文件比對。因此直接在請求體的第一行開頭中加上一個自定義固定的 URL 參數:GorRequestId=***&,這個 GorRequestId 的值取的就是請求塊中第一行的第二項。根據git上的描述,該值原本就是做者來作 request 和 response 的比對用的。

  2. gor 支持根據 URL 匹配過濾請求,但目前還不能同時過濾出請求對應的響應,我經過自定義的 java 版 middleware 來實現了這個需求,原理是在解析請求塊的時候記錄下須要輸出的 URL 的 requestId 到一個 HashSet 中,在解析響應體的時候根據 requestId 匹配過濾輸出。利用的就是請求和響應公用一個 requestId 這個特性。(這個問題我已經向做者提了 isssue :https://github.com/buger/gor/issues/344 ,根據回覆後續會實現該功能。)

下面就是個人代碼實現:

package go.middleware;

import javax.xml.bind.DatatypeConverter;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.HashSet;
import java.util.Set;

/**
 * Gor中間件Java版本,加強的功能有:
 *
 * 1.在請求體中注入參數GorRequestId,用於請求回放時的原始請求比對
 * 2.支持根據url配置過濾請求和響應的輸出
 * <p>
 * Created by niwei on 16/7/22.
 */
public class Stdout {
    private static final String SPLITTER_HEADER_BODY_SPLITTER = "\r\n\r\n";
    private static final String SPLITTER_HEAD_FIRST_LINE = "\n";
    private static final String SPLITTER_HEADER_ITEM = " ";
    /**
     * payload type, possible values: 1 - request, 2 - original response, 3 - replayed response
     */
    private static final String PAYLOAD_TYPE_REQUEST = "1";
    private static final String PAYLOAD_TYPE_ORIGINAL_RESPONSE = "2";

    /**
     * 定義新增長的requestId參數名稱
     */
    private static String INJECT_TO_REQUEST_ENTITY_REQUEST_ID = "GorRequestId";

    /**
     * 定義須要輸出的請求和響應的requestId
     */
    private static Set<String> recordRequestIds = new HashSet<>();

    /**
     * convert hex to string
     *
     * @param hexStr
     * @return
     * @throws Exception
     */
    public static String hexDecode(String hexStr) throws Exception {
        byte[] decodedHex = DatatypeConverter.parseHexBinary(hexStr);
        String decodedString = new String(decodedHex, "UTF-8");

        return decodedString;
    }

    /**
     * convert string to hex
     *
     * @param str
     * @return
     * @throws Exception
     */
    public static String encodeHex(String str) throws Exception {
        if (str == null) {
            return null;
        }
        byte[] strBytes = str.getBytes();
        String encodeString = DatatypeConverter.printHexBinary(strBytes);

        return encodeString;
    }

    private static String getRequestHeader(String key, String value) {
        StringBuilder result = new StringBuilder(SPLITTER_HEAD_FIRST_LINE);

        result.append(key).append(":").append(SPLITTER_HEADER_ITEM).append(value);

        return result.toString();
    }

    /**
     * gor原始內容加強
     *
     * @param content 原始的gor工具輸出的內容
     * @param allowUrlRegular 容許記錄文件的url正則表達式
     * @return 加強後輸出的內容
     */
    public static String enhanceContent(String content, String allowUrlRegular) {
        if ((allowUrlRegular == null) || (allowUrlRegular.trim().equals(""))){
            allowUrlRegular = "*";
        }

        String result = content;

        /**
         * get first line content
         */
        String[] lines = content.split(SPLITTER_HEAD_FIRST_LINE);
        if (lines == null || lines.length <= 1) {
            return result;
        }
        String firstLine = lines[0];
        String secondLine = lines[1];

        String[] firstLineItems = firstLine.split(SPLITTER_HEADER_ITEM);
        if (firstLineItems.length != 3) {
            return result;
        } else {
            String payloadType = firstLineItems[0];
            String requestId = firstLineItems[1];

            if (PAYLOAD_TYPE_REQUEST.equals(payloadType)) {
                String[] secondLineItems = secondLine.split(SPLITTER_HEADER_ITEM);
                String url = secondLineItems[1];
                String uri = url;
                int urlIndex = url.indexOf("?");
                if (urlIndex > 0) {
                    uri = url.substring(0, urlIndex);
                }

                String requestIdPair = INJECT_TO_REQUEST_ENTITY_REQUEST_ID + "=" + requestId + "&";
                result = content.replaceFirst(SPLITTER_HEADER_BODY_SPLITTER, SPLITTER_HEADER_BODY_SPLITTER + requestIdPair);

                boolean isMatch = false;
                String[] allowUrls = allowUrlRegular.split(",");
                for (String allowUrl : allowUrls) {
                    if (uri.matches(allowUrl)){
                        recordRequestIds.add(requestId);
                        isMatch = true;
                        break;
                    }
                }
                if(!isMatch){
                    //URL不能匹配上的則不輸出到文件
                    result = "";
                }

            } else if (PAYLOAD_TYPE_ORIGINAL_RESPONSE.equals(payloadType)) {
                if (recordRequestIds.contains(requestId)) {
                    recordRequestIds.remove(requestId);
                } else {//再也不recordRequestIds記錄中則不輸出到文件
                    result = "";
                }
            }
        }

        return result;
    }

    /**
     * java go.GorEnhance
     *
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {

        String line;
        StringBuilder allowUrlRegular = new StringBuilder();
        int bytesRead = 0;
        byte[] buffer = new byte[1024];

        try (BufferedInputStream bufferedInput = new BufferedInputStream(Class.class.getClassLoader().getSystemResourceAsStream("go/middleware/allow-url.txt"))) {
            while ((bytesRead = bufferedInput.read(buffer)) != -1) {
                allowUrlRegular.append(new String(buffer, 0, bytesRead));
            }
        }

        BufferedReader stdin = new BufferedReader(new InputStreamReader(
                System.in));
        while ((line = stdin.readLine()) != null) {
            System.out.println(encodeHex(enhanceContent(hexDecode(line), allowUrlRegular.toString())));
        }

    }
}
複製代碼

在運行 gor 命令時,加上參數 --middleware "java go.middleware.Stdout" 就能夠了。代碼中的 go/middleware/allow-url.txt 是在當前類的同級目錄下增長的一個 URL 過濾的配置文件:好比 .confirm. ,就將只記錄 URL 中包含 confirm 的請求,若是有多項 URL 則直接以逗號(,)分割便可。

本項目源碼已經放在 github 上:https://github.com/niweicumt/copyflow

相關文章
相關標籤/搜索