基於 Nginx&Lua 實現自建服務端埋點系統

亞馬遜雲開發者發表於2023-04-10

前言

埋點資料一般取決於服務提供商想從使用者身上獲取什麼資訊。通常來講,主要分為使用者的基本屬性資訊和行為資訊。使用者的基本屬性資訊主要包括:年齡、性別、裝置等。行為資訊即使用者的點選行為和瀏覽行為,在什麼時間,哪個使用者點選了哪個按鈕,瀏覽了哪個頁面,瀏覽時長等等的資料。 基本屬性資訊和行為資訊又可以稱之為一個簡單的報文。報文是網路中交換與傳輸的資料單元,即站點一次性要傳送的資料塊。報文包含了將要傳送的完整的資料資訊,其長短很不一致,長度不限且可變。簡單來說就是使用者在 App 內有一個操作行為,就會上報一組帶有資料的欄位。

亞馬遜雲科技開發者社群為開發者們提供全球的開發技術資源。這裡有技術文件、開發案例、技術專欄、培訓影片、活動與競賽等。幫助中國開發者對接世界最前沿技術,觀點,和專案,並將中國優秀開發者或技術推薦給全球雲社群。如果你還沒有關注/收藏,看到這裡請一定不要匆匆劃過,點這裡讓它成為你的技術寶庫!

本文會演示如何利用開源軟體和 Amazon 服務來構建服務端埋點系統,客戶端部分不在本文的討論範圍內。

軟體架構

Lua 是一種輕量級、可嵌入式的指令碼語言,可以非常容易的嵌入到其他語言中使用。另外 Lua 提供了協程併發,即以同步呼叫的方式進行非同步執行,從而實現併發,比起回撥機制的併發來說程式碼更容易編寫和理解,排查問題也會容易。Lua 還提供了閉包機制,函式可以作為 First Class Value 進行引數傳遞,另外其實現了標記清除垃圾收集。

OpenResty® 是一個基於 Nginx 與 Lua 的高效能 Web 平臺,其內部整合了大量精良的 Lua 庫、第三方模組以及大多數的依賴項。用於方便地搭建能夠處理超高併發、擴充套件性極高的動態 Web 應用、Web 服務和動態閘道器。OpenResty® 透過匯聚各種設計精良的 Nginx 模組,從而將 Nginx 有效地變成一個強大的通用 Web 應用平臺。這樣,Web 開發人員和系統工程師可以使用 Lua 指令碼語言調動 Nginx 支援的各種 C 以及 Lua 模組,快速構造出足以勝任 10K 乃至 1000K 以上單機併發連線的高效能 Web 應用系統。

通常會利用 Nginx&Lua 實現服務端日誌的統一收集,筆者會利用這項技術實現埋點資料的收集,思路如下:

  • 以 Http 請求的方式將埋點資料傳送至 Nginx 端;
  • 透過 Lua 模組解析請求體,再將埋點資料以非同步的方式傳送至後端 Kafka。這個過程中資料不用落盤,大大節約了儲存空間和提高了效率;
  • 最終後端會有一組消費者(例如 Spark)從 Kafka 中將資料落盤(例如 S3);

下圖是本文軟體層面的架構圖。

整體架構

架構分為四大塊:

  • Amazon EKS,本文會將 Nginx 和 Lua 以容器的形式部署在 Amazon EKS 中,充分利用 EKS 的彈性;
  • Amazon MSK,本文會使用託管的 Kafka,也就是 Amazon MSK,降低部署和後期運維的壓力;
  • Amazon EFS,考慮到整體架構的可用性和永續性,如果在 MSK 端發生了故障,雖然機率極低,本文會使用 Amazon EFS 來儲存 Nginx 的錯誤日誌,儘量保證訊息的完整性;
  • Amazon NLB,本文會使用 NLB 來暴露服務;
  • Amazon ECR,本文會使用 ECR 來儲存容器映象;

步驟

在開始之前,請先確保您具有登入 Amazon 全球區控制檯的賬號,並具備管理員許可權。

前提條件

  • 一臺 Linux 終端
  • 足夠的 Amazon 賬號許可權
  • 安裝 Amazon CLI
  • 安裝 Docker
  • 安裝 eksctl
  • 安裝 kubectl

    一、建立 Amazon VPC和Security Group

    參考此連結,建立 1個 VPC、3個公有子網、3個私有子網和其他 VPC 資源。接下來筆者會使用 vpcid, publicsubnetid01, publicsubnetid02, publicsubnetid03, privatesubnetid01, privatesubnetid02, privatesubnetid03來代替相關 VPC 和子網資源。

建立一個安全組供後續其他服務使用,為了簡便配置,筆者會將本文使用到的資源放入同一個安全組中。讀者可以在自己環境中將安全組進行拆分。

###
$ aws ec2 create-security-group --group-name my-poc-sg --description " my-poc-sg " --vpc-id vpcid
###

記錄 Security Group ID, 筆者會使用 securitygroupid 來代替它。

###
$ aws ec2 authorize-security-group-ingress \
     --group-id securitygroupid\
     --protocol all\
     --port -1 \
     --source-group securitygroupid
###

二、建立 Amazon MSK

建立 Amazon MSK 叢集來接收訊息,並記錄 Broker 地址,筆者會使用 broker01, broker02, broker03 來代替。

###
$ aws kafka create-cluster \
     --cluster-name "my-poc-msk-cluster" \
     --broker-node-group-info file://brokernodegroupinfo.json \
     --kafka-version "2.6.2" \
     --number-of-broker-nodes 3 \
     --encryption-info EncryptionInTransit={ClientBroker=TLS_PLAINTEXT}
###

brokernodegroupinfo.json
~~~
{
    "InstanceType": "kafka.m5.large",
    "BrokerAZDistribution": "DEFAULT",
    "ClientSubnets": [
      "privatesubnetid01",
      "privatesubnetid02",
      "privatesubnetid03"
    ],
    "SecurityGroups": [
      "securitygroupid "
    ],
    "StorageInfo": {
      "EbsStorageInfo": {
        "VolumeSize": 100
      }
    }
}
~~~

三、構建映象

使用到的檔案包含:

  • Dockerfile
  • sh
  • conf
  • conf
  • my-poc-send2kafka.lua
###
$ mkdir workdir
$ cd workdir
###

依次按照如下內容建立檔案。

Dockerfile
~~~
FROM amazonlinux
COPY install.sh /
RUN chmod +x /install.sh
RUN /install.sh
COPY nginx.conf /opt/openresty/nginx/conf/nginx.conf
COPY common.conf /opt/openresty/nginx/conf/conf.d/common.conf
COPY my-poc-send2kafka.lua /opt/openresty/nginx/lua/my-poc-send2kafka.lua
EXPOSE 80
CMD sed -i "s/\$mypodip/$(hostname -i)/g" /opt/openresty/nginx/conf/conf.d/common.conf && /opt/openresty/nginx/sbin/nginx -c /opt/openresty/nginx/conf/nginx.conf
~~~

install.sh
~~~
#!/bin/sh
yum -y install readline-devel pcre-devel openssl-devel gcc wget tar gzip perl make unzip hostname
mkdir /opt/software
mkdir /opt/module
cd /opt/software/
wget https://openresty.org/download/openresty-1.9.7.4.tar.gz
tar -xzf openresty-1.9.7.4.tar.gz -C /opt/module/
cd /opt/module/openresty-1.9.7.4
./configure --prefix=/opt/openresty \
--with-luajit \
--without-http_redis2_module \
--with-http_iconv_module
make
make install
cd /opt/software/
wget https://github.com/doujiang24/lua-resty-kafka/archive/master.zip
unzip master.zip -d /opt/module/
cp -rf /opt/module/lua-resty-kafka-master/lib/resty/kafka/ /opt/openresty/lualib/resty/
mkdir /opt/openresty/nginx/lua/
mkdir /var/log/nginx/
~~~

nginx.conf
~~~
worker_processes auto;
worker_rlimit_nofile 100000;
daemon off;

events {
    worker_connections 102400;
    multi_accept on;
    use epoll;
}

http {
    include mime.types;
    default_type application/octet-stream;
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
    '$status $body_bytes_sent "$http_referer" '
    '"$http_user_agent" "$http_x_forwarded_for"';
    access_log /var/log/nginx/access.log main;
    resolver 8.8.8.8;
    #resolver 127.0.0.1 valid=3600s;
    sendfile on;
    keepalive_timeout 65;
    underscores_in_headers on;
    gzip on;
    include /opt/openresty/nginx/conf/conf.d/common.conf;
}
~~~

common.conf
~~~
lua_package_path "/opt/openresty/lualib/resty/kafka/?.lua;;";
lua_package_cpath "/opt/openresty/lualib/?.so;;";
lua_shared_dict ngx_cache 128m;
lua_shared_dict cache_lock 100k;

server {
    listen 80;
    server_name 127.0.0.1;
    root html;
    lua_need_request_body on;
    access_log /var/log/nginx/access.log main;
    error_log /var/log/nginx/error-$mypodip.log notice;
    location = /putmessage {
        lua_code_cache on;
        charset utf-8;
        default_type 'application/json';
        content_by_lua_file "/opt/openresty/nginx/lua/my-poc-send2kafka.lua";
    }
}
~~~

my-poc-send2kafka.lua
~~~
local producer = require("resty.kafka.producer")
local json = require("cjson")

local broker_list = {
  {host = "broker01", port = 9092},
  {host = "broker02", port = 9092},
  {host = "broker03", port = 9092}
}

local log_json = {}

log_json["body"] = ngx.req.read_body()
log_json["body_data"] = ngx.req.get_body_data()

local topic = ngx.req.get_headers()["topic"]


local producer_error_handle = function(topic, partition_id, queue, index, err, retryable)
  ngx.log(ngx.ERR, "Error handle: index ", index, ' partition_id ', partition_id, ' retryable ', retryable, ' json ', json.encode(queue))
end


local bp = producer:new(broker_list, { producer_type = "async", batch_num = 200, error_handle = producer_error_handle})

local sendMsg = json.encode(log_json)

local ok, err = bp:send(topic, nil, sendMsg)
~~~

###
$ docker build -t  my-poc-image .
###

四、建立 Amazon ECR 並上傳映象

aws ecr create-repository \
    --repository-name my-poc-ecr/nginx-lua

###
$ aws ecr get-login-password --region regioncode | docker login --username AWS --password-stdin youraccountid.dkr.ecr.regioncode.amazonaws.com
$ docker tag my-poc-image:latest youraccountid.dkr.ecr.regioncode.amazonaws.com/my-poc-ecr/nginx-lua:latest
$ docker push youraccountid.dkr.ecr.regioncode.amazonaws.com/my-poc-ecr/nginx-lua:latest
###

五、建立 Amazon EFS

###
aws efs create-file-system \
    --performance-mode generalPurpose \
    --throughput-mode bursting \
    --encrypted \
    --region ap-northeast-1 \
    --tags Key=Name,Value=my-poc-efs
###

記錄 FileSystemId,筆者會使用 fsid 來代替它。

###
aws efs create-mount-target \
    --file-system-id fsid  \
    --subnet-id privatesubnetid01 \
    --security-groups securitygroupid

aws efs create-mount-target \
    --file-system-id fsid \
    --subnet-id privatesubnetid02 \
    --security-groups securitygroupid

aws efs create-mount-target \
    --file-system-id fsid \
    --subnet-id privatesubnetid03 \
    --security-groups securitygroupid
###

六、建立Amazon EKS叢集並安裝元件

cluster.yaml
###
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: my-poc-eks-cluster
  region: ap-northeast-1
  version: "1.21"

iam:
  withOIDC: true

addons:
- name: vpc-cni
  version: v1.11.2-eksbuild.1
- name: coredns
  version: v1.8.4-eksbuild.1
- name: kube-proxy
  version: v1.21.2-eksbuild.2

vpc:
  subnets:
    private:
      ap-northeast-1a: { id: "privatesubnetid01" }
      ap-northeast-1c: { id: "privatesubnetid02" }
      ap-northeast-1d: { id: "privatesubnetid03" }

managedNodeGroups:
  - name: my-poc-ng-1
    instanceType: m5.large
    desiredCapacity: 2
    volumeSize: 100
    privateNetworking: true
###

###
$ eksctl create cluster -f cluster.yaml
###

參考此連結安裝 Amazon Load Balancer Controller。

參考此連結安裝 Amazon EFS CSI driver。

七、在 Amazon EKS 中部署

###
$ kubectl apply -f deploy.yaml
###

deploy.yaml
~~~
---
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: efs-sc
provisioner: efs.csi.aws.com
parameters:
  provisioningMode: efs-ap
  fileSystemId: fsid
  directoryPerms: "700"
  gidRangeStart: "1000"
  gidRangeEnd: "2000"
  basePath: "/dynamic_provisioning"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-poc-pvc
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: efs-sc
  resources:
    requests:
      storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-lua
  labels:
    app: nginx-lua
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-lua
  template:
    metadata:
      labels:
        app: nginx-lua
    spec:
      containers:
      - name: nginx-lua
        image: youraccountid.dkr.ecr.regioncode.amazonaws.com/my-poc-ecr/nginx-lua:latest
        ports:
        - containerPort: 80
        resources:
          limits:
            cpu: 500m
          requests:
            cpu: 200m
        volumeMounts:
        - name: my-poc-volume
          mountPath: /var/log/nginx
      volumes:
      - name: my-poc-volume
        persistentVolumeClaim:
          claimName: my-poc-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-lua-svc
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: external
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
    service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
    service.beta.kubernetes.io/aws-load-balancer-subnets: publicsubnetid01, publicsubnetid02, publicsubnetid03
    service.beta.kubernetes.io/aws-load-balancer-name: my-poc-nginx-lua
    service.beta.kubernetes.io/aws-load-balancer-attributes: load_balancing.cross_zone.enabled=true
spec:
  selector:
    app: nginx-lua
  type: LoadBalancer
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
~~~

使用以下命令獲取 EXTERNAL-IP 地址,筆者會使用 nlbdns 來代替。

###
$ kubectl get svc nginx-lua-svc
###

八、功能測試

參考此連結安裝 kafka 客戶端。

建立測試用 Topic。

###
$ ./kafka-topics.sh --topic my-poc-topic-01 --create --bootstrap-server broker01:9092, broker02:9092, broker03:9092 --partitions 3 --replication-factor 2
###

安裝 ApacheBench 測試工具,並進行測試。

###
$ sudo yum -y install httpd-tools
$ ab -T 'application/json' -H "topic: my-poc-topic-01" -n 1000 -p postdata.json http://nlbdns/putmessage
###

postdata.json
~~~
{
    "uuid": "2b8c376e-bd20-11e6-9ebf-525499b45be6",
    "event_time": "2016-12-08T18:08:12",
    "page": "www.example.com/poster.html",
    "element": "register",
    "attrs": 
    {
        "title": "test",
        "user_id": 1234
    }
}
~~~

檢視訊息是否可以成功消費。

###
./kafka-console-consumer.sh --bootstrap-server broker01:9092, broker02:9092, broker03:9092 --topic my-poc-topic-01 --from-beginning
###

訊息已經成功消費。

接下來筆者會給一個不存在的 topic 傳送訊息,用來模擬生產環境中後端 MSK 不可用的情況。

###
$ ab -T 'application/json' -H "topic: my-poc-topic-noexist" -n 1000 -p postdata.json http://nlbdns/putmessage
###

按照預想狀況,這部分訊息會以錯誤日誌的形式保留在 Amazon EFS 中。

###
$ ab -T 'application/json' -H "topic: my-poc-topic-noexist" -n 1000 -p postdata.json http://nlbdns/putmessage
###

進入 EFS 中,開啟帶有 pod ip 的錯誤日誌,可以看到錯誤資訊被記錄了下來。

本篇作者

楊探
亞馬遜雲科技解決方案架構師,負責網際網路行業雲端架構諮詢和設計。

文章來源:https://dev.amazoncloud.cn/column/article/6309d47976658473a32...

相關文章