OpenResty

小郑[努力版]發表於2024-07-11

OpenResty

簡介與安裝

1. 簡介

OpenResty是一個基於Nginx與Lua的高效能web平臺,其內部繼承了大量精良的Lua庫,第三方模組以及大多數的依賴項。用於方便地搭建能夠處理超高併發擴充套件性極高的動態的web應用

讓你的web服務直接跑在Nginx服務內部,充分利用Nginx的非阻塞I/O模型,不僅僅對HTTP客戶端請求,甚至於對遠端後端諸如MySQL,PostgreSQL以及Redis等都進行一些列的高效能響應。

OpenResty的主要作用和功能包括:

  1. web伺服器:OpenResty 作為一個高效能的 Web 伺服器,可以處理大量的併發請求,提供高效的靜態檔案服務和動態內容生成。
  2. 反向代理:OpenResty可以作為反向代理伺服器,用於將請求轉發到後端的應用伺服器,並且可以進行負載均衡,快取,SSL終止等操作。
  3. 動態內容處理:OpenResty提供了強大的lua程式設計能力,可以用Lua編寫指令碼來處理請求,實現動態內容生成,訪問控制,日誌記錄等功能。
  4. 擴充套件性:OpenResty 的模組化架構使得它可以輕鬆地整合各種第三方模組,擴充套件了 NGINX 的功能,例如整合了各種快取模組、安全模組、訪問控制模組等。
  5. 高效能代理:OpenResty 可以作為高效能代理伺服器,用於構建 CDN、快取代理等場景,提供高效能的資料傳輸和快取服務。

2. Linux安裝

  • 安裝OpenResty的依賴庫

    yum install -y pcre-devel openssl-devel gcc --skip-broken

  • 安裝OpenResty倉庫

    yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

    • 如果提示說命令不存在,則執行

      yum install -y yum-utils

    • 然後重複上面的命令

  • 安裝OpenResty

    yum install -y openresty

  • 安裝opm工具

    yum install -y openresty-opm

  • 目錄結構

    預設情況下,OpenResty安裝的目錄是: /usr/local/openresty OpenResty就是在Nginx基礎上繼承了一些lua模組。

    • 配置Nginx環境變數

      • 開啟配置檔案

        vi /etc/profile

      • 在最下面加入兩行

        export NGINX_HOME=/usr/local/openresty/nginx

        export PATH=${NGINX_HOME}/sbin:$PATH

      • NGINX_HOME:後面是OpenResty安裝目錄下的Nginx的目錄

      • 然後讓配置生效

        source /etc/profile

    • OpenResty啟動

      # 啟動nginx

      nginx

      # 重新載入配置

      nginx -s reload

      # 停止

      nginx -s stop

    • nginx的預設配置檔案註釋太多,影響後續我們的編輯,這裡將nginx.conf中的註釋部分刪除,保留有效部分。

      修改/usr/local/openresty/nginx/conf/nginx.conf檔案,內容如下:

      #user  nobody;
      worker_processes  1;
      error_log  logs/error.log;
      
      events {
          worker_connections  1024;
      }
      
      http {
          include       mime.types;
          default_type  application/octet-stream;
          sendfile        on;
          keepalive_timeout  65;
      
          server {
              listen       8081;
              server_name  localhost;
              location / {
                  root   html;
                  index  index.html index.htm;
              }
              error_page   500 502 503 504  /50x.html;
              location = /50x.html {
                  root   html;
              }
          }
      }
      
      
    • 在Linux的控制檯輸入命令以啟動nginx:

      nginx
      

    3. Windows安裝

    • 下載: http://openresty.org/cn/download.html

    • 解壓

    • 雙擊nginx.exe 或者執行 start nginx啟動nginx

    • 驗證是否成功 tasklist /fi "imagename eq nginx.exe" 其中一個是 master 程序,另一個是 worker 程序

    另外當 nginx 成功啟動後,master 程序的 pid 存放在 logs\nginx.pid 檔案中。

HelloWorld

為了工作目錄與安裝目錄互不干擾,並順便學下簡單的配置檔案編寫,就另外建立一個OpenResty的工作目錄來練習,並且另外寫一個配置檔案。

  1. 建立OpenResty-test目錄,並在該目錄下建立logs和conf子目錄分別用於存放日誌和配置檔案

    $ mkdir ~/openresty-test ~/openresty-test/logs/ ~/openresty-test/conf/
    $
    $ tree ~/openresty-test
    /Users/yuansheng/openresty-test
    ├── conf
    └── logs
    
    2 directories, 0 files
    
  2. 建立配置檔案

    在 conf 目錄下建立一個文字檔案作為配置檔案,命名為 nginx.conf,檔案內容如下:

    worker_processes  1;        #nginx worker 數量
    error_log logs/error.log;   #指定錯誤日誌檔案路徑
    events {
        worker_connections 1024;
    }
    
    http {
        server {
            #監聽埠,若你的6699埠已經被佔用,則需要修改
            listen 6699;
            location / {
                default_type text/html;
    
                content_by_lua_block {
                    ngx.say("HelloWorld")
                }
            }
        }
    }
    

    提示:如果你安裝的是 openresty 1.9.3.1 及以下版本,請使用 content_by_lua 命令代替示例中的 content_by_lua_block。可使用 nginx -V 命令檢視版本號。

    ---  啟動nginx
    ➜  ~ nginx -p ~/openresty-test
    
    ---  檢視nginx程序
    ➜  ~ ps -ef | grep nginx
      501 88620     1   0 10:58AM ?? 0:00.00 nginx: master process nginx -p
                                        /Users/yuansheng/openresty-test
      501 88622 88620   0 10:58AM ?? 0:00.00 nginx: worker process
    
    --- 訪問nginx
    ➜  ~ curl http://localhost:6699 -i
    HTTP/1.1 200 OK
    Server: openresty/1.9.7.3
    Date: Sun, 20 Mar 2016 03:01:35 GMT
    Content-Type: text/html
    Transfer-Encoding: chunked
    Connection: keep-alive
    
    HelloWorld
    

與其他Location配合

利用不同的Location的功能組合,可以完成內部呼叫,流水線方式跳轉,外部重定向等幾大不同方式,

內部呼叫

例如對資料庫,內部公共函式的統一介面,可以把他們放到統一的Location中,通常情況下,為了保護這些內部介面,都會把這些介面設定為internal。這麼做的好處就是可以讓這個內部介面相互獨立,不受外界干擾。

示例程式碼

location = /sum {
    # 只允許內部呼叫
    internal;

    # 這裡做了一個求和運算只是一個例子,可以在這裡完成一些資料庫、
    # 快取伺服器的操作,達到基礎模組和業務邏輯分離目的
    content_by_lua_block {
        local args = ngx.req.get_uri_args()
        ngx.say(tonumber(args.a) + tonumber(args.b))
    }
}

location = /app/test {
    content_by_lua_block {
        local res = ngx.location.capture(
                        "/sum", {args={a=3, b=8}}
                        )
        ngx.say("status:", res.status, " response:", res.body)
    }
}

稍微擴充一下, 並行請求效果,示例如下:

location = /sum {
    internal;
    content_by_lua_block {
        ngx.sleep(0.1)
        local args = ngx.req.get_uri_args()
        ngx.print(tonumber(args.a) + tonumber(args.b))
    }
}

location = /subduction {
    internal;
    content_by_lua_block {
        ngx.sleep(0.1)
        local args = ngx.req.get_uri_args()
        ngx.print(tonumber(args.a) - tonumber(args.b))
    }
}

location = /app/test_parallels {
    content_by_lua_block {
        local start_time = ngx.now()
        local res1, res2 = ngx.location.capture_multi( {
                        {"/sum", {args={a=3, b=8}}},
                        {"/subduction", {args={a=3, b=8}}}
                    })
        ngx.say("status:", res1.status, " response:", res1.body)
        ngx.say("status:", res2.status, " response:", res2.body)
        ngx.say("time used:", ngx.now() - start_time)
    }
}

-- ngx.location.capture_multi 方法允許在同一個請求中併發地發起多個子請求
location = /app/test_queue {
    content_by_lua_block {
        local start_time = ngx.now()
        local res1 = ngx.location.capture_multi( {
                        {"/sum", {args={a=3, b=8}}}
                    })
        local res2 = ngx.location.capture_multi( {
                        {"/subduction", {args={a=3, b=8}}}
                    })
        ngx.say("status:", res1.status, " response:", res1.body)
        ngx.say("status:", res2.status, " response:", res2.body)
        ngx.say("time used:", ngx.now() - start_time)
    }
}
➜  ~ curl 127.0.0.1/app/test_parallels
status:200 response:11
status:200 response:-5
time used:0.10099983215332
➜  ~ curl 127.0.0.1/app/test_queue
status:200 response:11
status:200 response:-5
time used:0.20199990272522

利用 nginx.location.capture_multi 函式,直接完成了兩個子請求並執行。當兩個請求沒有相互依賴,這種方法可以極大提高查詢效率

該方法,可以被廣泛應用於廣告系統(1:N模型,一個請求,後端從N家供應商中獲取條件最優的廣告)、高併發前段頁面展示(並行無依賴介面,降級開關等)。

流水線方式跳轉

各種不同的API,下載請求混雜在一起,要求廠商對下載的動態調整有各種不同的定製策略,而這些策略在一天的不同時間段,規則可能還不一樣。這個時候還可以效仿工廠的流水線模式,逐層過濾,處理。

示例程式碼

location ~ ^/static/([-_a-zA-Z0-9/]+).jpg {
    set $image_name $1;
    content_by_lua_block {
        ngx.exec("/download_internal/images/"
                .. ngx.var.image_name .. ".jpg");
    };
}

location /download_internal {
    internal;
    # 這裡還可以有其他統一的 download 下載設定,例如限速等
    alias ../download;
}

注意,ngx.exec 方法與 ngx.redirect 是完全不同的,前者是個純粹的內部跳轉並且沒有引入任何額外 HTTP 訊號。 這裡的兩個 location 更像是流水線上工人之間的協作關係。第一環節的工人對完成自己處理部分後,直接交給第二環節處理人(實際上可以有更多環節),它們之間的資料流是定向的。

外部重定向

百度的首頁已經不再是 HTTP 協議,它已經全面修改到了 HTTPS 協議上。但是對於大家的輸入習慣,估計還是在位址列裡面輸入 baidu.com ,回車後發現它會自動跳轉到 https://www.baidu.com ,這時候就需要的外部重定向了。

location = /foo {
    content_by_lua_block {
        ngx.say([[I am foo]])
    }
}

location = / {
    rewrite_by_lua_block {
        return ngx.redirect('/foo');
    }
}

測試結果如下

--   -i  表示輸出響應頭資訊 
➜  ~  curl 127.0.0.1 -i
HTTP/1.1 302 Moved Temporarily
Server: openresty/1.9.3.2rc3
Date: Sun, 22 Nov 2015 11:04:03 GMT
Content-Type: text/html
Content-Length: 169
Connection: keep-alive
Location: /foo

<html>
<head><title>302 Found</title></head>
<body bgcolor="white">
<center><h1>302 Found</h1></center>
<hr><center>openresty/1.9.3.2rc3</center>
</body>
</html>

➜  ~  curl 127.0.0.1/foo -i
HTTP/1.1 200 OK
Server: openresty/1.9.3.2rc3
Date: Sun, 22 Nov 2015 10:43:51 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive

I am foo

外部重定向是可以跨域名的。例如從 A 網站跳轉到 B 網站是絕對允許的。在 CDN 場景的大量下載應用中,一般分為排程、儲存兩個重要環節。排程就是透過根據請求方 IP 、下載檔案等資訊尋找最近、最快節點,應答跳轉給請求方完成下載。

獲取uri引數

獲取請求uri引數

獲取一個uri有兩個方法:ngx.req.get_uri_argsngx.req.get_post_args,二者主要的區別是引數來源有區別。前者來自uri引數,後者來自post請求內容

示例

server {
   listen    80;
   server_name  localhost;

   location /print_param {
       content_by_lua_block {
           local arg = ngx.req.get_uri_args()
           for k,v in pairs(arg) do
               ngx.say("[GET ] key:", k, " v:", v)
           end

           ngx.req.read_body() -- 解析 body 引數之前一定要先讀取 body
           local arg = ngx.req.get_post_args()
           for k,v in pairs(arg) do
               ngx.say("[POST] key:", k, " v:", v)
           end
       }
   }
}

輸出結果:

➜  ~  curl '127.0.0.1/print_param?a=1&b=2%26' -d 'c=3&d=4%26'
[GET ] key:b v:2&
[GET ] key:a v:1
[POST] key:d v:4&
[POST] key:c v:3

傳遞uri引數

ngx.location.capture 函式用於發起一個內部子請求,並且等待子請求的響應

  location /test {
       content_by_lua_block {
           local res = ngx.location.capture(
                    '/print_param',
                    {
                       method = ngx.HTTP_POST,
                       args = ngx.encode_args({a = 1, b = '2&'}),
                       body = ngx.encode_args({c = 3, d = '4&'})
                   }
                )
           ngx.say(res.body)
       }
   }

輸出響應結果

➜  ~  curl '127.0.0.1/test'
[GET]  key:b v:2&
[GET]  key:a v:1
[POST] key:d v:4&
[POST] key:c v:3

如果這裡不呼叫ngx.encode_args ,可能就會比較醜了,看下面例子:

local res = ngx.location.capture('/print_param',
         {
            method = ngx.HTTP_POST,
            args = 'a=1&b=2%26',  -- 注意這裡的 %26 ,代表的是 & 字元
            body = 'c=3&d=4%26'
        }
     )
ngx.say(res.body)

PS:對於 ngx.location.capture 這裡有個小技巧,args 引數可以接受字串或Lua 表的,這樣我們的程式碼就更加簡潔直觀。

local res = ngx.location.capture('/print_param',
         {
            method = ngx.HTTP_POST,
            args = {a = 1, b = '2&'},
            body = 'c=3&d=4%26'
        }
     )
ngx.say(res.body)

獲取請求body

在nginx的典型應用場景中,幾乎都是隻讀取HTTP請求頭即可,例如負載均衡,正反向代理等場景。但是對於API server 或者 web Application,對body可以說比較敏感了。

最簡單的“hello *******”

我們先來構造最簡單的一個請求,POST 一個名字給服務端,服務端應答一個 “Hello ********”。

http {
    server {
        listen    80;

        location /test {
            content_by_lua_block {
                local data = ngx.req.get_body_data()
                ngx.say("hello ", data)
            }
        }
    }
}

測試結果:

➜  ~  curl 127.0.0.1/test -d jack
hello nil

從結果中可以看出data部分獲取為nil,原因是還需要新增指令 lua_need_request_body

http {
    server {
        listen    80;

        # 預設讀取 body  設定全域性行為
        lua_need_request_body on;

        location /test {
            content_by_lua_block {
                local data = ngx.req.get_body_data()
                ngx.say("hello ", data)
            }
        }
    }
}

再次測試,符合我們預期:

➜  ~  curl 127.0.0.1/test -d jack
hello jack

如果讀取body並非全域性行為,也可以顯示的呼叫ngx.req.read_body() 介面,參看下面示例:

http {
    server {
        listen    80;

        location /test {
            content_by_lua_block {
                ngx.req.read_body()
                local data = ngx.req.get_body_data()
                ngx.say("hello ", data)
            }
        }
    }
}

輸出響應體

HTTP響應報文分為三個部分:

  1. 響應行
  2. 響應頭
  3. 響應體

對於HTTP響應體的輸出,在OpenResty中呼叫 ngx.sayngx.print即可。

區別:

  • ngx.say會對輸出響應體多輸出一個 \n

ngx.say 與 ngx.print 均為非同步輸出,也就是說當呼叫 ngx.say 後並不會立刻輸出響應體。

  server {
        listen    80;

        location /test {
            content_by_lua_block {
                ngx.say("hello")
                ngx.sleep(3)
                ngx.say("the world")
            }
        }

        location /test2 {
            content_by_lua_block {
                ngx.say("hello")
                ngx.flush() -- 顯式的向客戶端重新整理響應輸出
                ngx.sleep(3)
                ngx.say("the world")
            }
        }
    }

測試介面可以觀察到, /test 響應內容實在觸發請求 3s 後一起接收到響應體,而 /test2 則是先收到一個 hello 停頓 3s 後又接收到後面的 the world

再看下面的例子:

 server {
        listen    80;
        lua_code_cache off;

        location /test {
            content_by_lua_block {
                ngx.say(string.rep("hello", 1000))
                ngx.sleep(3)
                ngx.say("the world")
            }
        }
    }

執行測試,可以發現首先收到了所有的 "hello" ,停頓大約 3 秒後,接著又收到了 "the world" 。

透過兩個例子對比,可以知道,因為是非同步輸出,兩個響應體的輸出時機是 不一樣 的。

處理響應體過大的輸出

當響應體過大(例如超過2G),是不能直接呼叫API完成響應體輸出的。響應體過大,分兩種情況:

  1. 輸出內容本身體積很大,例如超過2G的檔案下載
  2. 輸出內容本身是由各種碎片拼湊的,碎片數量龐大,例如應答資料是某地區所有人的姓名

第①個情況,要利用 HTTP 1.1 特性 CHUNKED 編碼來完成

image-20240709152214681

可以利用CHUNKED格式,把一個大的響應體拆分成多個小的響應體,分批,有節制的響應給請求方

location /test {
    content_by_lua_block {
        -- ngx.var.limit_rate = 1024*1024
        local file, err = io.open(ngx.config.prefix() .. "data.db","r")
        if not file then
            ngx.log(ngx.ERR, "open file error:", err)
            ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
        end

        local data
        while true do
            data = file:read(1024)
            if nil == data then
                break
            end
            ngx.print(data)
            ngx.flush(true)
        end
        file:close()
    }
}

日誌輸出

標準日誌輸出

OpenResty的標準日誌輸入原句為 ngx.log(log_level, ...),幾乎可以在任何ngx_lua階段進行日誌的輸出

worker_processes  1;

error_log  logs/error.log error;    # 日誌級別
#pid        logs/nginx.pid;

events {
    worker_connections  1024;
}

http {
    server {
        listen    80;
        location / {
            content_by_lua_block {
                local num = 55
                local str = "string"
                local obj
                ngx.log(ngx.ERR, "num:", num)
                ngx.log(ngx.INFO, " string:", str)
                print([[i am print]])
                ngx.log(ngx.ERR, " object:", obj)
            }
        }
    }
}

訪問網頁,生成日誌(logs/error.log檔案)結果如下:

2016/01/22 16:43:34 [error] 61610#0: *10 [lua] content_by_lua(nginx.conf:26):5:
 num:55, client: 127.0.0.1, server: , request: "GET /hello HTTP/1.1",
 host: "127.0.0.1"

2016/01/22 16:43:34 [error] 61610#0: *10 [lua] content_by_lua(nginx.conf:26):7:
 object:nil, client: 127.0.0.1, server: , request: "GET /hello HTTP/1.1",
 host: "127.0.0.1"

Nginx的日誌級別:

ngx.STDERR     -- 標準輸出
ngx.EMERG      -- 緊急報錯
ngx.ALERT      -- 報警
ngx.CRIT       -- 嚴重,系統故障,觸發運維告警系統
ngx.ERR        -- 錯誤,業務不可恢復性錯誤
ngx.WARN       -- 告警,業務中可忽略錯誤
ngx.NOTICE     -- 提醒,業務比較重要資訊
ngx.INFO       -- 資訊,業務瑣碎日誌資訊,包含不同情況判斷等
ngx.DEBUG      -- 除錯

網路日誌輸出

如果你的日誌需要歸集,並且對時效性要求比較高那麼這裡要推薦的庫可能就讓你很喜歡了。 lua-resty-logger-socket ,可以說很好的解決了上面提及的幾個特性。

lua-resty-logger-socket 的目標是替代 Nginx 標準的 ngx_http_log_module 以非阻塞 IO 方式推送 access log 到遠端伺服器上。對遠端伺服器的要求是支援 syslog-ng 的日誌服務。

lua_package_path "/path/to/lua-resty-logger-socket/lib/?.lua;;";

    server {
        location / {
            log_by_lua_block {
                local logger = require "resty.logger.socket"
                if not logger.initted() then
                    local ok, err = logger.init{
                        host = 'xxx',
                        port = 1234,
                        flush_limit = 1234,
                        drop_limit = 5678,
                    }
                    if not ok then
                        ngx.log(ngx.ERR, "failed to initialize the logger: ",
                                err)
                        return
                    end
                end

                -- construct the custom access log message in
                -- the Lua variable "msg"

                local bytes, err = logger.log(msg)
                if err then
                    ngx.log(ngx.ERR, "failed to log message: ", err)
                    return
                end
            }
        }
    }

例舉幾個好處:

  • 基於cosocket非阻塞IO實現
  • 日誌累計到一定量,集體提交,增加網路傳輸利用率
  • 短時間的網路抖動,自動容錯
  • 日誌累計到一定量,如果沒有傳輸完畢,直接丟棄
  • 日誌傳輸過程完全不落地,沒有任何磁碟 IO 消耗

相關文章