使用 Nginx 優化面向側面的架構

面向側面的程序設計(aspect-oriented programming,AOP),經過將解決特定領域問題的代碼從業務邏輯中獨立出來,從而提升代碼的可維護性。html

從主關注點中分離出橫切關注點是面向側面的程序設計的核心概念。分離關注點使得解決特定領域問題的代碼從業務邏輯中獨立出來,業務邏輯的代碼中再也不含有針對特定領域問題代碼的調用,業務邏輯同特定領域問題的關係經過側面來封裝、維護,這樣本來分散在在整個應用程序中的變更就能夠很好的管理起來。 - 維基百科nginx


示例是根據最近正在負責的 APP 後端項目簡化版,需求簡單說以下:git

  1. APP 端會對全部請求進行加密,服務器端要對加密結果進行校驗,確保正確以及未篡改;github

  2. 經過手機號來登陸,採用基本的 token 機制驗證登陸;web

  3. 有企業、小組以及員工的層級關係,後期必須考慮根據公司來分表/集羣;redis

  4. 提供涉及到權限的 REST 風格的接口(某種程度上相似 Postgrest,可是進行了拓展,後面會有專門文章介紹)sql

Version 1st.

思路:首先整個目前項目的主關注點 (core concern) 是 REST 風格的資源服務器 —— 即經過約定俗成的風格來對應具體的數據/資源操做。在這個功能外,須要完成的其餘關注點包括:數據庫

  • 全部請求加密校驗json

  • 登陸驗證後端

  • 資源的權限管理以及獲取

因而,在 Sinatra 中,能夠經過 extensions 的方式將請求加密校驗完成,配合 before 來進行統一處理:

require 'sinatra/base'

module Sinatra

  module RequestHeadersVerify

    module Helpers
      def headers_valid?
        # 此處省略真實業務代碼
        false
      end
    end

    def self.registered(app)
      app.helpers RequestHeadersVerify::Helpers

      app.before do
        unless headers_valid?
          halt 400, json(ResponseErrror::InvalidHeadersError.new)
        end
      end
    end
  end

  register RequestHeadersVerify
end

最終採用"中間件"的方式,在請求的最前面一層(橫切關注點 crosscutting concerns)將非法請求進行攔截。

因而緊接着第二個流程,驗證用戶是否登陸,與獲取當前聯繫人所在的公司、小組、以及其管理的小組信息同樣,這裏最快速/方便的作法就是經過 helpers 來實現:

require 'sinatra/base'

module Sinatra 
  module UserSessionHelpers
    HTTP_USER_TOKEN_KEY = 'HTTP_AUTH_TOKEN'

    def current_user
      @current_user ||= (
        user = User.first token: env[HTTP_USER_TOKEN_KEY]
        halt 400, json(ResponseErrror::InvalidTokenError.new) unless user
        user
      )
    end
  end

  module OrganizationHelpers
    # 這裏省略掉相關 helpers 代碼
  end

  helpers UserSessionHelpers
  helpers OrganizationHelpers
end

最終在 REST 相關的構建代碼中,就不須要去考慮用戶請求加密的內容,也不須要去考慮用戶是否登陸(由於若是須要使用到用戶信息可是用戶沒有登陸,會直接拋出錯誤返回)。只須要按照約定的設計風格,把請求的內容在校驗了內容和權限後,轉成對應的數據庫操做,最終再按照約定的內容返回。

Version 2nd.

初版已經儘量的考慮到 解決特定領域問題的代碼從業務邏輯中獨立出來。可是現實開發裏面常常會涉及到多人開發、跨語言合做、更快速的迭代等等的問題,最終須要把他們拆成獨立的低耦合度的 Server。因而隨之而來的是如何在服務間進行通信/共享數據。

這裏的方案選擇一般會根據實際業務以及難易程度來權衡,例如最快捷的 webServer 的方式內部通訊,稍微複雜點的基於 TCP 的 RPC 通訊方案(例如 thrift),或者某些特殊的情景,例如是生產者/消費者關係的話,則可能經過 MQ 來進行通訊。最終咱們採用的是經過 Nginx 的 lua 模塊來將 server 以面向側面的思路耦合

首先,Nginx 的 Lua 模塊能夠作什麼?若是能夠,單純 nginx 和 lua 就能夠完成完整的 web 服務。能夠鏈接 redis、memcache、postgresql 等等,同時能夠取得請求的全部內容,能夠設置返回的頭部、正文。配合 lua 的對數據處理能力,基本功能均可以實現。同時 nginx 的 lua 模塊總體都是異步,因此性能也相對較好。固然也能夠經過 lua 腳原本控制權限,若是驗證經過則繼續下面的操做,例如是 proxy_pass 代理,簡單的示例以下文:

location = /foo {
    access_by_lua_block {
        -- check the client IP address is in our black list
        if ngx.var.remote_addr == "132.5.72.3" then
            ngx.exit(ngx.HTTP_FORBIDDEN)
        end

        -- check if the URI contains bad words
        if ngx.var.uri and
            string.match(ngx.var.request_body, "evil")
        then
            return ngx.redirect("/terms_of_use.html")
        end

        -- tests passed
    }

    proxy_pass http://blah.blah.com;
 }

不過,咱們這裏將用到的主要仍是 proxy_pass, 和 ngx.location.capture,基本代碼以下:

<!-- 禁止任何如下劃線開始的請求地址 -->
if string.sub(ngx.var.uri, 2, 2) == "_" then
  ngx.exit(404)
end

local cjson = require "cjson"

local custom_header_prefix = "V-"
local request_args = ngx.req.get_uri_args(64)
local request_body = ngx.req.read_body()
local request_path = ngx.var.uri
local request_method = ngx["HTTP_"..ngx.req.get_method()]

for header, _ in pairs(ngx.req.get_headers()) do
  if string.upper(string.sub(header, 1, 2)) == crm_header_prefix then
    ngx.req.clear_header(header);
  end
end

function res_with_json(body, status)
  ngx.header["Content-Type"] = "application/json"
  ngx.print(body)
  return ngx.exit(status)
end

function request_to_server(uri)
  <!-- 發起請求至其餘地址並取得結果 -->
  res = ngx.location.capture(uri..request_path, {
    body = request_body,
    args = request_args,
    method = request_method,
  })
  local json_response = cjson.decode(res.body)

  <!-- 解析返回內容 -->
  if not json_response.next == true then
    res_with_json(res.body, res.status)
  end
  for key, value in json_response.params do
     ngx.req.set_header(custom_header_prefix..string.upper(key), value)
  end
  return false
end

上面的代碼主完成了清理用戶惡意提交的請求頭,以及 request_to_server 的代碼,實現了將原請求內容轉發給另外一個接口並得到請求後的內容。獲得請求結果後,驗證請求的參數。

同時在 nginx 裏面經過 stream 和 proxy_pass 的方式來配置多個內部地址:

upstream authentication-server {
    server 192.168.21.1:6011;
    server 192.168.21.2:6011;
}

server {
    location /_authentication {
      proxy_redirect off;
      proxy_set_header Host      $host;
      proxy_set_header X-Real-IP $remote_addr;

      rewrite  ^/_authentication/(.*)  /$1 break;
      proxy_pass http://authentication-server;
    }
}

因而,將兩個結合起來,就能夠實現經過 lua 腳本把原請求的全部參數,包括頭部、正文、請求地址、請求方法都帶過去,請求另外一個地址(和 proxy_pass 相似),而且能夠獲得最終返回的結果處理

擁有這個能力後,即是本文的重點了:在 Sinatra 的初版本中,最終都是 ruby 代碼不斷調用方法,來完成整個請求的流程。那若是咱們把整個流程的打通交給 Nginx 的話該如何實現呢?

  1. 當一個請求進入後,經過 request_to_server 的能力,把請求依次轉發給負責 橫切關注點 的服務,例如用戶請求校驗以及登陸校驗服務、用戶的組織架構服務,最終再去調用主關注點,即本文中的資源服務器;

  2. 每次請求完後,根據前一個流程的返回值決定是否進入下一個流程,例如示例中的 lua 腳本是經過返回的 json 裏面的 next 參數來決定是否繼續往下走。若是沒有這個參數則直接返回當前服務的返回值再也不繼續請求下去;

  3. 若是出現了 next: true 這個關鍵字,則將返回值中的其餘內容以請求頭的形式傳遞給下一個服務,且每一個服務都會徹底信賴這些請求頭(因此請求剛進來的時候須要作一些請求頭和請求地址處理)。

若是到這裏都沒有太大問題,你應該能夠理解個人意圖了。即 Nginx 經過 Lua 腳原本依次請求 橫切關注點服務器,若是一路順暢(每次都有 next: true),最終會把攜帶有 橫切關注點服務返回的內容的 headers 帶給主關注點服務。

因而,在本需求裏面,爲了保證可拓展性和低耦合性,最終分爲了三個服務:

  • 負責請求加密鑑權,用戶登陸、密碼修改的用戶驗證服務

  • 負責管理企業結構、獲取用戶權限的組織架構服務

  • 負責具體的 REST 請求處理的資源服務

  1. 當一個用戶登陸的請求過來,由於密碼錯誤或者加密鑑權失敗,會在用戶驗證服務就直接返回錯誤。由於返回內容中沒有 next: true 字段,因此直接返回結果;

  2. 當一個用戶發起了一個發送短信驗證碼的服務,這個只是用戶驗證服務的職能,沒有必要繼續向下走,因而返回了一個沒有 next: true 字段的返回值,因而 Nginx 直接返回結果;

  3. 當用戶登陸的時候,雖然經過了用戶驗證服務的校驗,可是該服務沒法獲取更多的用戶信息,因而把該請求繼續傳遞到組織架構服務,組織架構服務在請求頭中拿到了手機號信息,因而直接返回了該手機號所對應的詳細信息;

  4. 如今發起了一個資源操做的請求,由於用戶驗證服務沒法識別,因此只返回了手機號給 nginx,nginx 繼續請求組織架構服務,由於組織架構服務也不能處理,因此繼續返回了詳細的我的信息給 nginx,nginx 最終拿到這些信息,都經過頭部請求了資源服務,而後由於這裏是主關注點,也是流程裏面最後一個節點,因此經過 proxy_pass 給了資源服務。


最終,這樣作的優點:

  1. 利用 Nginx 異步的優點來彌補 ruby 服務先天性 IO 處理的不足;

  2. 目前只實現了第一條線,即從用戶驗證 -> 組織信息 -> 資源服務器的順序,後面若是有須要,能夠隨時實現其餘順序,而只須要按照在請求頭裏面加上相應的參數便可,減低耦合性;

  3. 三個模塊都有各自的業務和特色,能夠針對模塊去設計緩存方案,並且能夠分模塊去設計集羣方案;

  4. 對於開發者而言,更容易完成單個服務的測試用例,而不須要過多在開發過程當中關注聯調。

原文: 使用 Nginx 優化面向側面的架構

相關文章
相關標籤/搜索