摘要:從主關注點中分離出橫切關注點是面向側面的程序設計的核心概念。最終我們采用的是通過的模塊來將以面向側面的思路耦合。原文使用優化面向側面的架構
面向側面的程序設計(aspect-oriented programming,AOP),通過將解決特定領域問題的代碼從業務邏輯中獨立出來,從而提高代碼的可維護性。
從主關注點中分離出橫切關注點是面向側面的程序設計的核心概念。分離關注點使得解決特定領域問題的代碼從業務邏輯中獨立出來,業務邏輯的代碼中不再含有針對特定領域問題代碼的調用,業務邏輯同特定領域問題的關系通過側面來封裝、維護,這樣原本分散在在整個應用程序中的變動就可以很好的管理起來。 - 維基百科
示例是根據最近正在負責的 APP 后端項目簡化版,需求簡單說如下:
APP 端會對所有請求進行加密,服務器端要對加密結果進行校驗,確保正確以及未篡改;
通過手機號來登錄,采用基本的 token 機制驗證登錄;
有企業、小組以及員工的層級關系,后期必須考慮根據公司來分表/集群;
提供涉及到權限的 REST 風格的接口(某種程度上類似 Postgrest,但是進行了拓展,后面會有專門文章介紹)
Version 1st.思路:首先整個目前項目的主關注點 (core concern) 是 REST 風格的資源服務器 —— 即通過約定俗成的風格來對應具體的數據/資源操作。在這個功能外,需要完成的其他關注點包括:
所有請求加密校驗
登錄驗證
資源的權限管理以及獲取
于是,在 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 的話該如何實現呢?
當一個請求進入后,通過 request_to_server 的能力,把請求依次轉發給負責 橫切關注點 的服務,例如用戶請求校驗以及登錄校驗服務、用戶的組織架構服務,最終再去調用主關注點,即本文中的資源服務器;
每次請求完后,根據前一個流程的返回值決定是否進入下一個流程,例如示例中的 lua 腳本是通過返回的 json 里面的 next 參數來決定是否繼續往下走。如果沒有這個參數則直接返回當前服務的返回值不再繼續請求下去;
如果出現了 next: true 這個關鍵字,則將返回值中的其他內容以請求頭的形式傳遞給下一個服務,且每個服務都會完全信賴這些請求頭(所以請求剛進來的時候需要做一些請求頭和請求地址處理)。
如果到這里都沒有太大問題,你應該可以理解我的意圖了。即 Nginx 通過 Lua 腳本來依次請求 橫切關注點服務器,如果一路順暢(每次都有 next: true),最終會把攜帶有 橫切關注點服務返回的內容的 headers 帶給主關注點服務。
于是,在本需求里面,為了保證可拓展性和低耦合性,最終分為了三個服務:
負責請求加密鑒權,用戶登錄、密碼修改的用戶驗證服務
負責管理企業結構、獲取用戶權限的組織架構服務
負責具體的 REST 請求處理的資源服務
當一個用戶登錄的請求過來,因為密碼錯誤或者加密鑒權失敗,會在用戶驗證服務就直接返回錯誤。因為返回內容中沒有 next: true 字段,所以直接返回結果;
當一個用戶發起了一個發送短信驗證碼的服務,這個只是用戶驗證服務的職能,沒有必要繼續向下走,于是返回了一個沒有 next: true 字段的返回值,于是 Nginx 直接返回結果;
當用戶登錄的時候,雖然通過了用戶驗證服務的校驗,但是該服務無法獲取更多的用戶信息,于是把該請求繼續傳遞到組織架構服務,組織架構服務在請求頭中拿到了手機號信息,于是直接返回了該手機號所對應的詳細信息;
現在發起了一個資源操作的請求,因為用戶驗證服務無法識別,所以只返回了手機號給 nginx,nginx 繼續請求組織架構服務,因為組織架構服務也不能處理,所以繼續返回了詳細的個人信息給 nginx,nginx 最終拿到這些信息,都通過頭部請求了資源服務,然后因為這里是主關注點,也是流程里面最后一個節點,所以通過 proxy_pass 給了資源服務。
最終,這樣做的優勢:
利用 Nginx 異步的優勢來彌補 ruby 服務先天性 IO 處理的不足;
目前只實現了第一條線,即從用戶驗證 -> 組織信息 -> 資源服務器的順序,后面如果有需要,可以隨時實現其他順序,而只需要按照在請求頭里面加上相應的參數即可,減低耦合性;
三個模塊都有各自的業務和特點,可以針對模塊去設計緩存方案,而且可以分模塊去設計集群方案;
對于開發者而言,更容易完成單個服務的測試用例,而不需要過多在開發過程中關注聯調。
原文: 使用 Nginx 優化面向側面的架構
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/39194.html
閱讀 522·2021-10-09 09:44
閱讀 2105·2021-09-02 15:41
閱讀 3559·2019-08-30 15:53
閱讀 1839·2019-08-30 15:44
閱讀 1293·2019-08-30 13:10
閱讀 1201·2019-08-30 11:25
閱讀 1479·2019-08-30 10:51
閱讀 3371·2019-08-30 10:49