1. 程式人生 > 其它 >使用 Docker 和 Nginx 打造高效能二維碼服務

使用 Docker 和 Nginx 打造高效能二維碼服務

  寫在前面

  TLDR,如果你是曾經的讀者,可以直接訪問下面的連結,然後搭建屬於你的高效能二維碼服務,映象非常小巧,DockerHub 上顯示只有 13.47MB,如果你下載解壓到本地,也僅有 32.9MB,相比 Nginx 官方相同版本最小的映象只大了 10MB。

  如果你希望瞭解這個服務是怎麼構建的,可以接著閱讀下面的章節。如果你想了解該如何使用,可以直接翻閱至使用部分。

  準備原始碼

  這裡需要準備三份程式碼:Nginx、libqrencode、ngx_http_qrcode_module。

  Nginx的程式碼版本選擇和基礎映象版本一致就好;libqrencode 在 alpine 軟體倉庫中的版本太過陳舊,我們這裡使用最新的釋出版本 4.1.1;ngx_http_qrcode_module 作者沒有準備版本,所以這裡我將程式碼 fork 了一份,做了一些細節修改,並打上了一個名為 2021.01.06 的版本。

  這樣做還有一個好處,如果軟體程式碼沒有版本,我們只能通過 Git 或者 Zipball 方式下載,這兩種方式我們都還需要在映象中多安裝一款對應的軟體進行程式碼下載或者解壓縮,而使用 “release” 後的版本程式碼,則可以直接使用系統映象自帶的 tar 來處理壓縮包,進一步控制映象大小:

  NGINX_VERSION=1.19.6

  LIBQR_VERSION=4.1.1

  NGX_LIBQR_VERSION=20210106

  curl -L github/fukuchi/libqrencode/archive/v${LIBQR_VERSION}.tar.gz -o "v${LIBQR_VERSION}.tar.gz"

  curl -L "nginx/download/nginx-${NGINX_VERSION}.tar.gz" -o nginx.tar.gz

  curl -L "github/soulteary/ngx_http_qrcode_module/archive/${NGX_LIBQR_VERSION}.tar.gz" -o ${NGX_LIBQR_VERSION}.tar.gz

  複製程式碼構建服務映象

  之前構建服務的時候,採用的是使用通用基礎映象編譯 Nginx 和它的“小夥伴”(模組),在三年後的今天,我們不妨直接使用 Nginx 基礎映象,所謂“原湯化原食”,最大限度複用官方提供的環境、配置引數、入口指令碼…畢竟,偷懶是工程師的美德。

  FROM nginx:1.19.6-alpine

  複製程式碼準備 Nginx 構建環境

  雖然我們使用 Nginx 官方映象作為基礎映象,但是因為要再次構建 Nginx,所以基礎構建工具必不可少。從官方映象原始檔中,我們可以找到必備的工具的安裝命令:

  apk add --no-cache --virtual .build-deps gcc libc-dev make openssl-dev pcre-dev zlib-dev linux-headers libxslt-dev gd-dev geoip-dev perl-dev libedit-dev mercurial bash alpine-sdk findutils

  複製程式碼

  這裡在安裝軟體的時候宣告安全列表名稱為 .build-deps,方便用完後一鍵清理,節約映象容量。

  準備 QRCode 構建環境

  根據官方文件,在 alpine 中找到各種依賴包的名稱,和處理 Nginx 構建環境時一樣,將依賴安裝列表宣告為 .build-qrcode:

  apk add --no-cache --virtual .build-qrcode openssl-dev pcre-dev zlib-dev build-base autoconf automake libtool libpng-dev libgd pcre pcre-dev pkgconfig gd-dev

  複製程式碼編譯 QREncode 依賴庫

  編譯 QREncode 非常簡單,先使用 autogen 指令碼生成配置檔案,接著就是“C語言編譯安裝”一鍵三連常規操作:

  tar -zxC /usr/src -f v${LIBQR_VERSION}.tar.gz

  cd /usr/src/libqrencode-${LIBQR_VERSION} && \

  ./autogen.sh && LDFLAGS=-lgd ./configure && \

  make && make install

  複製程式碼編譯 Nginx 執行檔案

  前文提到我們為什麼要使用 Nginx 官方映象來進行編譯構建,因為能“偷懶”,原汁原味複用官方構建配置和執行環境:

  tar -zxC /usr/src -f nginx.tar.gz && \

  cd /usr/src/nginx-$NGINX_VERSION && \

  CONFARGS=$(nginx -V 2>&1 | sed -n -e 's/^.*arguments: //p') \

  CONFARGS=${CONFARGS/-Os -fomit-frame-pointer/-Os} && \

  echo $CONFARGS && \

  ./configure --with-compat $CONFARGS --add-module=../ngx_http_qrcode_module/ && \

  make && make install && \

  複製程式碼

  使用 sed 將 nginx -V 輸出引數進行截斷,然後使用字串替換方式去掉我們不需要的引數,在 Nginx 配置過程中,使用 --with-compat 引數將“官方”引數拼合到命令中即可節約我們大量精力去折騰基礎配置。

  如果你決定使用 ubuntu 或者 debian 版本的映象(比如官方不帶 alpine)的映象,進行構建,這裡獲取引數需要使用臨時 shell 檔案進行中轉,因為不同的 shell 對於引號的處理模式有些不同,例如下面這樣:

  CONFARGS=$(nginx -V 2>&1 | sed -n -e 's/^.*arguments: //p') \

  echo "./configure --with-compat $CONFARGS --add-module=../ngx_http_qrcode_module/">tmpconf.sh && chmod 755 tmpconf.sh

  && ./tmpconf.sh && rm tmpconf.sh

  複製程式碼

  如果使用非 alpine 映象,除了上面的內容外,還需要補全一些基礎依賴,比如 pcre 等。

  完成的配置檔案

  將上面的配置進行整合,稍作調整,就能夠得到完成的 Docker 映象配置檔案了。

  FROM nginx:1.19.6-alpine

  ARG NGINX_VERSION=1.19.6

  ARG LIBQR_VERSION=4.1.1

  ARG NGX_LIBQR_VERSION=20210106

  RUN apk add --no-cache --virtual .build-deps gcc libc-dev make openssl-dev pcre-dev zlib-dev linux-headers libxslt-dev gd-dev geoip-dev perl-dev libedit-dev mercurial bash alpine-sdk findutils && \

  apk add --no-cache --virtual .build-qrcode openssl-dev pcre-dev zlib-dev build-base autoconf automake libtool libpng-dev libgd pcre pcre-dev pkgconfig gd-dev && \

  mkdir -p /usr/src && cd /usr/src && \

  curl -L github/fukuchi/libqrencode/archive/v${LIBQR_VERSION}.tar.gz -o "v${LIBQR_VERSION}.tar.gz" && \

  tar -zxC /usr/src -f v${LIBQR_VERSION}.tar.gz && \

  cd /usr/src/libqrencode-${LIBQR_VERSION} && ./autogen.sh && LDFLAGS=-lgd ./configure && make && make install && cd /usr/src && \

  curl -L "nginx/download/nginx-${NGINX_VERSION}.tar.gz" -o nginx.tar.gz && \

  curl -L "github/soulteary/ngx_http_qrcode_module/archive/${NGX_LIBQR_VERSION}.tar.gz" -o ${NGX_LIBQR_VERSION}.tar.gz && \

  tar zxvf ${NGX_LIBQR_VERSION}.tar.gz && mv ngx_http_qrcode_module-${NGX_LIBQR_VERSION} ngx_http_qrcode_module && \

  tar -zxC /usr/src -f nginx.tar.gz && \

  cd /usr/src/nginx-$NGINX_VERSION && \

  CONFARGS=$(nginx -V 2>&1 | sed -n -e 's/^.*arguments: //p') \

  CONFARGS=${CONFARGS/-Os -fomit-frame-pointer/-Os} && \

  echo $CONFARGS && \

  ./configure --with-compat $CONFARGS --add-module=../ngx_http_qrcode_module/ && \

  make && make install && \

  apk del .build-deps .build-qrcode && \

  rm -rf /tmp/* && rm -rf /var/cache/apk/* && rm -rf /usr/src/ && \

  curl -L raw.githubusercontent/soulteary/ngx_http_qrcode_module/master/conf/nginx.conf -o /etc/nginx/nginx.conf

  複製程式碼

  如果你在國內構建,希望構建速度變快,可以在映象執行軟體安全前新增一句命令,對軟體源進行修改,再進行構建操作:

  RUN cat /etc/apk/repositories | sed -e "s/dl-cdn.alpinelinux/mirrors.aliyun/" | tee /etc/apk/repositories

  複製程式碼基礎使用

  前文提到,我已經將程式碼和映象提到了官方倉庫,所以如果你只是想了解如何做,和想使用,使用下面的命令可以一鍵獲取已經構建好的映象檔案。

  docker pull soulteary/nginx-qrcode-server:release-2021.01.06

  複製程式碼

  如果你希望直接檢視效果,可以使用 docker 基礎命令將服務啟動在本機的某個埠:

  docker run --rm -it -p 8080:80 soulteary/nginx-qrcode-server:release-2021.01.06

  複製程式碼

  然後開啟瀏覽器,訪問 localhost:8080,即可看到服務正常執行(展示一個預設二維碼)。

  和三年前一樣,你可以訪問類似 localhost:8080/?size=150&margin=20&txt=

  https%3A%2F%2Fsoulteary 來嘗試通過調整 URL 引數獲得更加適合你的使用場景的生成結果。

  預設配置

  如果你想進行一些細節調整,可以參考預設配置,將其修改為更符合你使用場景的配置。

  worker_processes 1;

  events {

  worker_connections 1024;

  }

  http {

  include mime.types;

  default_type application/octet-stream;

  sendfile on;

  keepalive_timeout 65;

  server {

  listen 80;

  server_name localhost;

  location / {

  set $fg_color 000000;

  set $bg_color FFFFFF;

  set $level 0;

  set $hint 2;

  set $size 300;

  set $margin 80;

  set $version 2;

  set $case 0;

  set $txt "soulteary";

  if ( $arg_fg_color ){

  set $fg_color $arg_fg_color;

  }

  if ( $arg_bg_color ){

  set $bg_color $arg_bg_color;

  }

  if ( $arg_level ){

  set $level $arg_level;

  }

  if ( $arg_hint ){

  set $hint $arg_hint;

  }

  if ( $arg_size ){

  set $size $arg_size;

  }

  if ( $arg_margin ){

  set $margin $arg_margin;

  }

  if ( $arg_ver ){

  set $version $arg_ver;

  }

  if ( $arg_case ){

  set $case $arg_case;

  }

  if ( $arg_txt ){

  set $txt $arg_txt;

  }

  qrcode_fg_color $fg_color;

  qrcode_bg_color $bg_color;

  qrcode_level $level;

  qrcode_hint $hint;

  qrcode_size $size;

  qrcode_margin $margin;

  qrcode_version $version;

  qrcode_casesensitive $case;

  qrcode_urlencode_txt $txt;

  qrcode_gen;

  }

  }

  }

  複製程式碼簡單效能測試

  這個模式下,預設足夠應對一些基礎場景,我們先以筆記本為環境,使用 ab 進行簡單進行效能測試:

  ab -n 1000 -c 10 -r localhost:8080/

  This is ApacheBench, Version 2.3 <$Revision: 1879490 $>

  Copyright 1996 Adam Twiss, Zeus Technology Ltd, zeustech/

  Licensed to The Apache Software Foundation, apache/

  Benchmarking localhost (be patient)

  Completed 100 requests

  Completed 200 requests

  Completed 300 requests

  Completed 400 requests

  Completed 500 requests

  Completed 600 requests

  Completed 700 requests

  Completed 800 requests

  Completed 900 requests

  Completed 1000 requests

  Finished 1000 requests

  Server Software: nginx/1.19.6

  Server Hostname: localhost

  Server Port: 8080

  Document Path: /

  Document Length: 579 bytes

  Concurrency Level: 10

  Time taken for tests: 2.711 seconds

  Complete requests: 1000

  Failed requests: 0

  Total transferred: 722000 bytes

  HTML transferred: 579000 bytes

  Requests per second: 368.91 [#/sec] (mean)

  Time per request: 27.107 [ms] (mean)

  Time per request: 2.711 [ms] (mean, across all concurrent requests)

  Transfer rate: 260.11 [Kbytes/sec] received

  Connection Times (ms)

  min mean[+/-sd] median max

  Connect: 0 0 0.1 0 1

  Processing: 5 27 2.6 26 39

  Waiting: 5 27 2.7 26 39

  Total: 6 27 2.7 26 39

  Percentage of the requests served within a certain time (ms)

  50% 26

  66% 27

  75% 28

  80% 28

  90% 30

  95% 31

  98% 34

  99% 38

  100% 39 (longest request)

  複製程式碼

  可以看到本地單機在使用預設配置,不進行優化的情況下,預設 QPS 是 368,計算響應時間在 3 毫秒內。足夠一般的業務或小規模場景使用,畢竟你不會真的只使用一臺和筆記本效能差不多的機器作為生產伺服器。

  如果換上一臺小規格的(4C4G)的雲伺服器,並使用另外一臺機器進行訪問效能測試,可以看到單機器單例項,效能並不會有太多波動,依舊是每個請求大概消耗3 毫秒。

  Server Software: nginx/1.19.6

  Server Hostname: 192.168.93.25

  Server Port: 8080

  Document Path: /?txt=123

  Document Length: 557 bytes

  Concurrency Level: 10

  Time taken for tests: 2.960 seconds

  Complete requests: 1000

  Failed requests: 0

  Total transferred: 700000 bytes

  HTML transferred: 557000 bytes

  Requests per second: 337.79 [#/sec] (mean)

  Time per request: 29.604 [ms] (mean)

  Time per request: 2.960 [ms] (mean, across all concurrent requests)

  Transfer rate: 230.91 [Kbytes/sec] received

  Connection Times (ms)

  min mean[+/-sd] median max

  Connect: 0 0 0.1 0 3

  Processing: 4 29 1.5 29 32

  Waiting: 4 29 1.5 29 32

  Total: 4 29 1.4 30 32

  複製程式碼

  如果你的請求密集程度不高,單是這樣的配置的機器和執行方式,假設請求分佈均勻,足夠滿足每分鐘一百萬次的請求。但是現實中請求一定存在高峰和低谷,所以接下來,我們再來進行一些基礎優化,加強應對高併發場景的能力。

  搭配記憶體快取實現高效能展示

  因為本方案中挑戰高效能二維碼生成,本質是依賴高效能的生成工具,以及 Nginx 非同步非阻塞 IO ,依賴 CPU 密集計算實現。所以為了進一步提升服務能力,可以下手的點除了繼續優化程式碼之外,最簡單的方案便是對無狀態的可水平擴充套件例項數量和增加快取,減少不必要的重複計算,把CPU讓給更有計算需要的“請求”。

  同時我們看到響應時間已經在個位數毫秒級別,為了進一步提升效能,這裡務必要避免資源落盤,最優解是 Nginx 本身應用記憶體,次優解是各種能夠保持長連結的記憶體快取應用。

  Nginx 本身並不開放自身的記憶體,但是為了滿足這類需求,從大概十年前就提供了外部記憶體模組(ngx_http_memcached_module),可以在不改動程式碼的情況下使用這個模組來完成計算內容的持久化,以及請求不落磁碟。

  水平擴充套件例項,通過重複啟動容器可以輕鬆做到,搭配 SLB、HAProxy 、甚至是 Nginx 都可以,這裡依舊選擇 Traefik 作為前端,只需要一條啟動命令,服務註冊、負載均衡就都完事了。

  先給出 docker-compose.yml 完整配置,使用 K8S 的同學可以參考修改。

  version: "3.6"

  services:

  qrcode:

  image: soulteary/nginx-qrcode-server:release-2021.01.06

  volumes:

  - ./nginx.conf:/etc/nginx/nginx.conf:ro

  networks:

  - traefik

  labels:

  - traefik.enable=true

  - traefik.docker.network=traefik

  - traefik.http.routers.qrcode-rule=Host(`qrcode.lab.io`)

  - traefik.http.routers.qrcode-entrypoints=http

  - traefik.http.routers.qrcode-ssl.rule=Host(`qrcode.lab.io`)

  - traefik.http.routers.qrcode-ssl.entrypoints=https

  - traefik.http.routers.qrcode-ssl.tls=true

  - traefik.http.services.qrcode-backend.loadbalancer.server.scheme=http

  - traefik.http.services.qrcode-backend.loadbalancer.server.port=80

  expose:

  - 80

  restart: always

  depends_on:

  - memcached

  environment:

  - TZ=Asia/Shanghai

  logging:

  driver: "json-file"

  options:

  max-size: "10m"

  memcached:

  image: memcached:1.6.9-alpine

  expose:

  - 11211

  networks:

  - traefik

  restart: always

  logging:

  driver: "json-file"

  options:

  max-size: "10m"

  networks:

  traefik:

  external: true

  複製程式碼

  因為要和 memcached “夢幻聯動”,所以我們還需要修改預設的 Nginx 配置檔案:

  worker_processes 1;

  events {

  worker_connections 1024;

  }

  http {

  include mime.types;

  default_type application/octet-stream;

  sendfile on;

  keepalive_timeout 65;

  upstream memcache_server {

  server memcached:11211;

  keepalive 512;

  }

  server {

  listen 80;

  server_name localhost;

  location=/favicon.ico {

  access_log off; empty_gif;

  }

  memcached_buffer_size 4k;

  memcached_connect_timeout 100ms;

  memcached_read_timeout 100ms;

  memcached_send_timeout 100ms;

  memcached_socket_keepalive on;

  location / {

  set $memcached_key "$uri?$args";

  #or set $memcached_key $query_string;

  memcached_pass memcache_server;

  error_page 404 502 504=@private;

  }

  location @private {

  # internal;

  add_header X-Cache-Key $memcached_key;

  set $fg_color 000000;

  set $bg_color FFFFFF;

  set $level 0;

  set $hint 2;

  set $size 300;

  set $margin 80;

  set $version 2;

  set $case 0;

  set $txt "soulteary";

  if ( $arg_fg_color ) {

  set $fg_color $arg_fg_color;

  }

  if ( $arg_bg_color ) {

  set $bg_color $arg_bg_color;

  }

  if ( $arg_level ) {

  set $level $arg_level;

  }

  if ( $arg_hint ) {

  set $hint $arg_hint;

  }

  if ( $arg_size ) {

  set $size $arg_size;

  }

  if ( $arg_margin ) {

  set $margin $arg_margin;

  }

  if ( $arg_ver ) {

  set $version $arg_ver;

  }

  if ( $arg_case ) {

  set $case $arg_case;

  }

  if ( $arg_txt ) {

  set $txt $arg_txt;

  }

  qrcode_fg_color $fg_color;

  qrcode_bg_color $bg_color;

  qrcode_level $level;

  qrcode_hint $hint;

  qrcode_size $size;

  qrcode_margin $margin;

  qrcode_version $version;

  qrcode_casesensitive $case;

  qrcode_urlencode_txt $txt;

  qrcode_gen;

  }

  }

  }

  複製程式碼

  使用 docker-compose up --sacale qrcode=4 -d 一鍵啟動四個相同的 QRCode 例項。

  接著使用 ab 再次在另外一臺機器上對這臺機器進行網路請求測試,並適當增大測試請求數量,多次測試可以看到 4C4G 的雲伺服器的 QPS 提升到了 600+,而單個請求的響應時間縮短到了 1.6 毫秒左右,差不多是單機每分鐘可承受200萬次請求的服務能力。

  實際生產場景,我們會使用核心數更多的機器、以及增加機器節點數量,可以帶來更大的服務響應能力,在應對突發流量時,可通過雲服務彈性部署,進一步提升響應能力。

  這裡吐槽一下,我的筆記本單機單例項的情況,居然QPS到達了2700,雲虛擬機器上的效能測試真的是看臉。

  ab -n 10000 -c 10 qrcode.lab/?txt=123

  This is ApacheBench, Version 2.3 <$Revision: 1807734 $>

  Copyright 1996 Adam Twiss, Zeus Technology Ltd, zeustech/

  Licensed to The Apache Software Foundation, apache/

  Benchmarking qrcode.lab.io (be patient)

  Completed 1000 requests

  Completed 2000 requests

  Completed 3000 requests

  Completed 4000 requests

  Completed 5000 requests

  Completed 6000 requests

  Completed 7000 requests

  Completed 8000 requests

  Completed 9000 requests

  Completed 10000 requests

  Finished 10000 requests

  Server Software: nginx/1.19.6

  Server Hostname: qrcode.lab.io

  Server Port: 80

  Document Path: /?txt=123

  Document Length: 557 bytes

  Concurrency Level: 10

  Time taken for tests: 16.185 seconds

  Complete requests: 10000

  Failed requests: 0

  Total transferred: 7050000 bytes

  HTML transferred: 5570000 bytes

  Requests per second: 617.86 [#/sec] (mean)

  Time per request: 16.185 [ms] (mean)

  Time per request: 1.618 [ms] (mean, across all concurrent requests)

  Transfer rate: 425.38 [Kbytes/sec] received

  Connection Times (ms)

  min mean[+/-sd] median max

  Connect: 0 0 0.1 0 5

  Processing: 4 16 7.7 14 67

  Waiting: 4 16 7.7 14 67

  Total: 4 16 7.7 14 67

  Percentage of the requests served within a certain time (ms)

  50% 14

  66% 18

  75% 20

  80% 22

  90% 27

  95% 32

  98% 36

  99% 40

  100% 67 (longest request)

  複製程式碼

  此外,觀察伺服器 CPU 使用情況,發現可以輕鬆將 CPU 打滿,絲毫不會浪費你的每一分錢。

  線上使用,根據自己需求水平擴充套件相同規格的幾臺虛擬機器,並水平擴充套件例項個數,即可實現滿足自己業務需求的高效能 QRCode 服務啦,當然,如果你的二維碼生成需求是確定的,可以減少 Nginx 配置中動態的部分,讓一些配置“常量化”,進一步減少計算量,以及避免一些惡意的請求浪費計算資源。

  因為我們使用了 Nginx,這裡如果想設定服務能力上限,避免資源被濫用,也可以通過 Nginx 常規方式快捷的實現一些功能需求:設定 LimitReq 來限制和避免一些外部惡意請求,以及結合日誌進分析來獲取二維碼生成和訪問統計計數等需求。

  最後

  原本想使用二階段構建,將 Ngx_QRCode 模組構建為動態模組,構建出一套更小的映象。但是因為呼叫 GD 庫編譯存在一些問題,暫時作罷,有時間再搞吧。

  相比三年前的映象,這次構建結果小了一半之多,還是挺欣慰的。