圖 1: Winglang Playground 中的簡(jiǎn)單 TODO 服務(wù)

為了簡(jiǎn)單起見,把所有內(nèi)容都放在一個(gè)源中,當(dāng)然也可以分成核心、端口和適配器。讓我們來看看這個(gè)示例的主要部分。

為了簡(jiǎn)單起見,把所有內(nèi)容都放在一個(gè)源代碼中,當(dāng)然也可以分成 Core、Ports 和 Adapters 三部分。讓我們來看看這個(gè)示例的主要部分。

資源(端口)定義

首先,我們需要定義要使用的云資源(又稱端口)。具體步驟如下:

bring ex;
bring cloud;

let tasks = new ex.Table(
name: "Tasks",
columns: {
"id" => ex.ColumnType.STRING,
"title" => ex.ColumnType.STRING
},
primaryKey: "id"
);
let counter = new cloud.Counter();
let api = new cloud.Api();
let path = "/tasks";

在這里,我們定義了一個(gè) Winglang 表來保存 TODO 任務(wù),該表只有兩列:任務(wù) ID 和標(biāo)題。為了保持簡(jiǎn)單,我們使用 Winglang 計(jì)數(shù)器資源將任務(wù) ID 實(shí)現(xiàn)為一個(gè)自動(dòng)遞增的數(shù)字。最后,我們使用 Winglang API 資源公開 TODO 服務(wù) API。

API 請(qǐng)求處理程序(適配器)

現(xiàn)在,我們要為四個(gè) REST API 請(qǐng)求分別定義一個(gè)處理函數(shù)。獲取所有任務(wù)列表的方法如下:

api.get(
path,
inflight (request: cloud.ApiRequest): cloud.ApiResponse => {
let rows = tasks.list();
let var result = MutArray<Json>[];
for row in rows {
result.push(row);
}
return cloud.ApiResponse{
status: 200,
headers: {
"Content-Type" => "application/json"
},
body: Json.stringify(result)
};
});

創(chuàng)建新任務(wù)記錄的方法如下:

api.post(
path,
inflight (request: cloud.ApiRequest): cloud.ApiResponse => {
let id = "{counter.inc()}";
if let task = Json.tryParse(request.body) {
let record = Json{
id: id,
title: task.get("title").asStr()
};
tasks.insert(id, record);
return cloud.ApiResponse {
status: 200,
headers: {
"Content-Type" => "application/json"
},
body: Json.stringify(record)
};
} else {
return cloud.ApiResponse {
status: 400,
headers: {
"Content-Type" => "text/plain"
},
body: "Bad Request"
};
}
});

更新現(xiàn)有任務(wù)的方法如下:

api.put(
"{path}/:id",
inflight (request: cloud.ApiRequest): cloud.ApiResponse => {
let id = request.vars.get("id");
if let task = Json.tryParse(request.body) {
let record = Json{
id: id,
title: task.get("title").asStr()
};
tasks.update(id, record);
return cloud.ApiResponse {
status: 200,
headers: {
"Content-Type" => "application/json"
},
body: Json.stringify(record)
};
} else {
return cloud.ApiResponse {
status: 400,
headers: {
"Content-Type" => "text/plain"
},
body: "Bad Request"
};
}
});

最后,刪除現(xiàn)有任務(wù)的方法如下:

api.delete(
"{path}/:id",
inflight (request: cloud.ApiRequest): cloud.ApiResponse => {
let id = request.vars.get("id");
tasks.delete(id);
return cloud.ApiResponse {
status: 200,
headers: {
"Content-Type" => "text/plain"
},
body: ""
};
});

我們可以使用 Winglang 模擬器來試用這個(gè) API:

圖 2:Winglang模擬器中的 TODO 服務(wù)

我們可以編寫一個(gè)或多個(gè)測(cè)試來自動(dòng)驗(yàn)證應(yīng)用程序接口:

bring http;
bring expect;
let url = "{api.url}{path}";
test "run simple crud scenario" {
let r1 = http.get(url);
expect.equal(r1.status, 200);
let r1_tasks = Json.parse(r1.body);
expect.nil(r1_tasks.tryGetAt(0));
let r2 = http.post(url, body: Json.stringify(Json{title: "First Task"}));
expect.equal(r2.status, 200);
let r2_task = Json.parse(r2.body);
expect.equal(r2_task.get("title").asStr(), "First Task");
let id = r2_task.get("id").asStr();
let r3 = http.put("{url}/{id}", body: Json.stringify(Json{title: "First Task Updated"}));
expect.equal(r3.status, 200);
let r3_task = Json.parse(r3.body);
expect.equal(r3_task.get("title").asStr(), "First Task Updated");
let r4 = http.delete("{url}/{id}");
expect.equal(r4.status, 200);
}

最后但并非最不重要的一點(diǎn)是,這項(xiàng)服務(wù)可以使用 Winglang CLI 部署到任何受支持的云平臺(tái)上。TODO 服務(wù)的代碼是完全云中立的,確保無需修改即可在不同平臺(tái)上兼容。

這個(gè)例子清楚地表明,Winglang 編程環(huán)境是快速開發(fā)此類服務(wù)的一流工具。如果這就是您所需要的一切,那么您就無需繼續(xù)閱讀了。接下來的內(nèi)容就像一個(gè)白兔洞,在我們開始認(rèn)真討論生產(chǎn)部署之前,需要解決多個(gè)非功能性問題。

請(qǐng)注意。即將發(fā)表的文章并非面向所有人,而是面向經(jīng)驗(yàn)豐富的云軟件架構(gòu)師。

縮小生產(chǎn)差距

使用 REST API 快速構(gòu)建 CRUD 服務(wù)與將其部署到生產(chǎn)環(huán)境之間存在巨大差距。生產(chǎn)環(huán)境需要考慮一系列非功能性問題。這些問題因業(yè)務(wù)環(huán)境而異。為小型團(tuán)隊(duì)內(nèi)部部署服務(wù)與為政府或金融機(jī)構(gòu)部署相同功能有很大不同。

專家們對(duì)非功能性需求清單的理想結(jié)構(gòu)爭(zhēng)論不休。作者更喜歡簡(jiǎn)明扼要的高層次概述,并根據(jù)需要進(jìn)行深入分析。以下是列出的四大非功能性需求,同時(shí)也承認(rèn)這份清單并非詳盡無遺:

  1. 可用性:終端客戶如何與服務(wù)溝通
  2. 安全性:如何保護(hù)服務(wù),保護(hù)對(duì)象是誰
  3. 運(yùn)行:我們將如何部署服務(wù),以確保其穩(wěn)健性和/或彈性,并控制其成本
  4. 規(guī)模:需要服務(wù)的并發(fā)客戶數(shù)量以及著名的數(shù)據(jù) V$3$:速度、數(shù)量和種類

這些領(lǐng)域不是孤立的,而是存在重疊。一個(gè)緩慢或失靈的系統(tǒng)是無法使用的。相反,在國防部層面確保雜貨店庫存系統(tǒng)的安全可能會(huì)讓供應(yīng)商滿意,但卻不會(huì)讓銀行家滿意。雖然分類并不完善,但它為進(jìn)一步討論提供了一個(gè)框架。

可用性

上面介紹的 TODO 示例服務(wù)實(shí)現(xiàn)屬于所謂的無頭 REST API。這種方法側(cè)重于核心功能,將用戶體驗(yàn)設(shè)計(jì)留給獨(dú)立的層。其實(shí)現(xiàn)方式通常是客戶端渲染(Client-Side Rendering)或服務(wù)器端渲染(Server Side Rendering),中間有一個(gè)前端后臺(tái)層(Backend for Frontend tier),或者使用多個(gè)作為 GraphQL 解析器(GraphQL Resolvers)運(yùn)行的、范圍較窄的 REST API 服務(wù)。每種方法在特定情況下都有其優(yōu)點(diǎn)。

主張支持 HTTP 內(nèi)容協(xié)商(HTTP Content Negotiation),并為通過瀏覽器直接進(jìn)行 API 交互提供最小用戶界面。雖然 Postman 或 Swagger 等工具可以促進(jìn) API 交互,但作為最終用戶體驗(yàn) API 可以提供寶貴的見解。這種基本的用戶界面,或者稱之為 “工程用戶界面”,通常就足夠了。

更簡(jiǎn)單的解決方案是使用基本的 HTML 模板,并利用 HTMX 的功能和 CSS 框架(如 Bootstrap)進(jìn)行增強(qiáng)。目前,Winglang 本身并不支持 HTML 模板,但對(duì)于基本用例,可以使用 TypeScript 輕松管理。例如,渲染單個(gè)任務(wù)行的實(shí)現(xiàn)方式如下:

import { TaskData } from "core/task";

export function formatTask(path: string, task: TaskData): string {
return `
<li class="list-group-item d-flex justify-content-between align-items-center">
<form hx-put="${path}/${task.taskID}" hx-headers='{"Accept": "text/plain"}' id="${task.taskID}-form">
<span class="task-text">${task.title}</span>
<input
type="text"
name="title"
class="form-control edit-input"
style="display: none;"
value="${task.title}">
</form>
<div class="btn-group">
<button class="btn btn-danger btn-sm delete-btn"
hx-delete="${path}/${task.taskID}"
hx-target="closest li"
hx-swap="outerHTML"
hx-headers='{"Accept": "text/plain"}'>?</button>
<button class="btn btn-primary btn-sm edit-btn">?</button>
</div>
</li>
`
}

這樣就會(huì)出現(xiàn)以下用戶界面畫面:

圖 4:TODO 服務(wù)用戶界面

雖然不是超級(jí)華麗,但對(duì)于演示目的來說已經(jīng)足夠好了。

即使是純粹的無頭 REST 應(yīng)用程序接口(API),也需要考慮很強(qiáng)的可用性。API 調(diào)用應(yīng)遵循 HTTP 方法、URL 格式和有效載荷的 REST 約定。正確記錄 HTTP 方法和潛在錯(cuò)誤處理至關(guān)重要。需要記錄客戶端和服務(wù)器錯(cuò)誤,將其轉(zhuǎn)換為適當(dāng)?shù)?HTTP 狀態(tài)代碼,并在響應(yīng)正文中提供清晰的解釋信息。

由于需要使用 HTTP 請(qǐng)求中的 Content-Type 和 Accept 標(biāo)頭,根據(jù)內(nèi)容協(xié)商處理多個(gè)請(qǐng)求解析器和響應(yīng)格式器,因此采用了以下設(shè)計(jì)方法:

圖 5:TODO 服務(wù)設(shè)計(jì)

遵循依賴反轉(zhuǎn)原則可確保系統(tǒng)核心與端口和適配器完全隔離。雖然可能有人傾向于將核心封裝在由 ResourceData 類型定義的通用 CRUD 框架中,但建議大家謹(jǐn)慎行事。這一建議源于幾個(gè)方面的考慮:

  1. 在實(shí)踐中,即使是 CRUD 請(qǐng)求處理也往往會(huì)帶來超出基本操作的復(fù)雜性。
  2. 核心不應(yīng)該依賴于任何特定的框架,以保持其獨(dú)立性和適應(yīng)性。
  3. 要?jiǎng)?chuàng)建這樣一個(gè)框架,就必須支持通用編程,而 Winglang 目前還不支持這一功能。

另一種選擇是放棄核心數(shù)據(jù)類型定義,完全依賴無類型的 JSON 接口,類似于 Lisp 編程風(fēng)格。不過,考慮到 Winglang 的強(qiáng)類型性,決定不采用這種方法。

總的來說,TodoServiceHandler 非常簡(jiǎn)單易懂:

bring "./data.w" as data;
bring "./parser.w" as parser;
bring "./formatter.w" as formatter;

pub class TodoHandler {
_path: str;
_parser: parser.TodoParser;
_tasks: data.ITaskDataRepository;
_formatter: formatter.ITodoFormatter;

new(
path: str,
tasks_: data.ITaskDataRepository,
parser: parser.TodoParser,
formatter: formatter.ITodoFormatter,
) {
this._path = path;
this._tasks = tasks_;
this._parser = parser;
this._formatter = formatter;
}

pub inflight getHomePage(user: Json, outFormat: str): str {
let userData = this._parser.parseUserData(user);

return this._formatter.formatHomePage(outFormat, this._path, userData);
}

pub inflight getAllTasks(user: Json, query: Map<str>, outFormat: str): str {
let userData = this._parser.parseUserData(user);
let tasks = this._tasks.getTasks(userData.userID);

return this._formatter.formatTasks(outFormat, this._path, tasks);
}

pub inflight createTask(
user: Json,
body: str,
inFormat: str,
outFormat: str
): str {
let taskData = this._parser.parsePartialTaskData(user, body);
this._tasks.addTask(taskData);

return this._formatter.formatTasks(outFormat, this._path, [taskData]);
}

pub inflight replaceTask(
user: Json,
id: str,
body: str,
inFormat: str,
outFormat: str
): str {
let taskData = this._parser.parseFullTaskData(user, id, body);
this._tasks.replaceTask(taskData);

return taskData.title;
}

pub inflight deleteTask(user: Json, id: str): str {
let userData = this._parser.parseUserData(user);
this._tasks.deleteTask(userData.userID, num.fromStr(id));
return "";
}
}

您可能會(huì)注意到,代碼結(jié)構(gòu)與前面的設(shè)計(jì)圖略有不同。這些細(xì)微的調(diào)整在軟件設(shè)計(jì)中很正常;在整個(gè)過程中會(huì)出現(xiàn)新的見解,因此有必要進(jìn)行調(diào)整。最顯著的區(qū)別是每個(gè)函數(shù)都定義了 user.Json 參數(shù):Json 參數(shù)。我們將在下一節(jié)討論該參數(shù)的用途。

安全

在沒有采取安全措施的情況下,將 TODO 服務(wù)暴露在互聯(lián)網(wǎng)上會(huì)帶來災(zāi)難。黑客、無聊的青少年和專業(yè)攻擊者會(huì)迅速鎖定其公共 IP 地址。規(guī)則非常簡(jiǎn)單:

反之,在服務(wù)中加入各種可以想象得到的安全措施會(huì)導(dǎo)致過高的運(yùn)營(yíng)成本。正如在以前的文章中所論述的,讓建筑師對(duì)其設(shè)計(jì)的成本負(fù)責(zé)可能會(huì)大大改變他們的設(shè)計(jì)方法:

我們需要的是對(duì)服務(wù)應(yīng)用程序接口的合理保護(hù),不能少也不能多。既然想嘗試全棧服務(wù)端渲染用戶界面,自然會(huì)選擇在一開始就強(qiáng)制用戶登錄,生成一個(gè)有合理有效期(比如一小時(shí))的 JWT 令牌,然后用它對(duì)所有即將到來的 HTTP 請(qǐng)求進(jìn)行身份驗(yàn)證。

考慮到服務(wù)端渲染的具體情況,使用 HTTP Cookie 來傳遞會(huì)話令牌是一個(gè)自然的選擇(老實(shí)說是 ChatGPT 建議的)。對(duì)于客戶端渲染選項(xiàng),可能需要使用通過 HTTP 請(qǐng)求頭授權(quán)字段傳遞的承載令牌。

有了包含用戶信息的會(huì)話令牌,就可以按用戶匯總 TODO 任務(wù)。雖然有許多方法可以將會(huì)話數(shù)據(jù)(包括用戶詳細(xì)信息)整合到域中,但選擇在本研究中重點(diǎn)關(guān)注 userID 和 fullName 屬性。

在用戶身份驗(yàn)證方面,有多種選擇,尤其是在 AWS 生態(tài)系統(tǒng)內(nèi):

  1. AWS Cognito,利用其用戶池或與外部身份供應(yīng)商(如 Google 或 Facebook)的集成。
  2. 第三方身份驗(yàn)證服務(wù),如 Auth0。
  3. 完全在 Winglang 中開發(fā)的自定義身份驗(yàn)證服務(wù)。
  4. AWS 身份中心
  5. ……

作為一名獨(dú)立的軟件技術(shù)研究人員,作者傾向于使用組件最少的最簡(jiǎn)單解決方案,這樣也能滿足日常操作需求。由于現(xiàn)有的多賬戶/多用戶設(shè)置,利用 AWS 身份中心(詳見另一篇文章)是順理成章的一步。

集成后,AWS Identity Center 主屏幕如下所示:

圖 6: AWS 身份中心主屏幕

這意味著,在作者的系統(tǒng)中,用戶、自己或客人可以使用相同的 AWS 憑據(jù)進(jìn)行開發(fā)、管理以及示例或內(nèi)務(wù)管理應(yīng)用程序。

為了與 AWS Identity Center 集成,需要注冊(cè)應(yīng)用程序,并提供一個(gè)實(shí)現(xiàn)所謂 “斷言消費(fèi)者服務(wù) URL(ACS URL)”的新端點(diǎn)。本文與 SAML 標(biāo)準(zhǔn)無關(guān)。只需指出,在 ChatGPT 和 Google 搜索的幫助下,就可以完成這項(xiàng)工作。在這里可以找到一些有用的信息。非常方便的是 TypeScript samlify 庫,它封裝了 SAML 登錄響應(yīng)驗(yàn)證過程的全部繁重工作。

作者最感興趣的是,這個(gè)可變點(diǎn)如何影響整個(gè)系統(tǒng)的設(shè)計(jì)。讓我們嘗試用半正式的數(shù)據(jù)流符號(hào)將其可視化:

圖 7:TODO 服務(wù)數(shù)據(jù)流

這種表示法看似不尋常,但卻高保真地反映了數(shù)據(jù)是如何在系統(tǒng)中流動(dòng)的。我們?cè)谶@里看到的是著名的管道過濾器架構(gòu)模式的一個(gè)特殊實(shí)例。

在這里,數(shù)據(jù)流經(jīng)一個(gè)管道,每個(gè)過濾器執(zhí)行一個(gè)定義明確的任務(wù),實(shí)際上遵循了單一責(zé)任原則。這樣的安排允許更換過濾器,如果想改用簡(jiǎn)單的 Basic HTTP 身份驗(yàn)證、使用 HTTP 授權(quán)頭或使用不同的秘密管理策略來構(gòu)建和驗(yàn)證 JWT 令牌的話。

如果我們放大 Parse 和 Format 過濾器,就會(huì)看到分別使用 Content-Type 和 Accept HTTP 標(biāo)頭的典型調(diào)度邏輯:

圖 8:內(nèi)容協(xié)商調(diào)度

許多工程師將設(shè)計(jì)和架構(gòu)模式與具體實(shí)現(xiàn)混為一談。這就忽略了模式的本質(zhì)。

模式就是要找出一種合適的方法,以最少的干預(yù)來平衡相互沖突的力量。在構(gòu)建基于云的軟件系統(tǒng)時(shí),安全性至關(guān)重要,但成本或復(fù)雜性不應(yīng)過高,因此這種理解至關(guān)重要。管道過濾器設(shè)計(jì)模式有助于有效解決此類設(shè)計(jì)難題。它允許對(duì)處理步驟進(jìn)行模塊化和靈活配置,在本例中,這些步驟與身份驗(yàn)證機(jī)制有關(guān)。

例如,雖然 SAML 身份驗(yàn)證等強(qiáng)大的安全措施對(duì)于生產(chǎn)環(huán)境是必要的,但在自動(dòng)端到端測(cè)試等場(chǎng)景中,它們可能會(huì)帶來不必要的復(fù)雜性和開銷。在這種情況下,基本 HTTP 身份驗(yàn)證等更簡(jiǎn)單的方法可能就足夠了,既能提供快速、經(jīng)濟(jì)的解決方案,又不會(huì)損害系統(tǒng)的整體完整性。我們的目標(biāo)是保持系統(tǒng)的核心功能和代碼庫的統(tǒng)一性,同時(shí)根據(jù)環(huán)境或特定要求改變身份驗(yàn)證策略。

Winglang 獨(dú)特的預(yù)檢(Preflight)編譯功能允許在構(gòu)建階段調(diào)整配置,從而消除了運(yùn)行時(shí)的開銷。與其他中間件庫(如 Middy 和 AWS Power Tools for Lambda)相比,基于 Winglang 的解決方案的這一功能提供了管理身份驗(yàn)證管道的更高效、更靈活的方法,因而具有顯著優(yōu)勢(shì)。

因此,實(shí)施基本 HTTP 身份驗(yàn)證只需修改身份驗(yàn)證管道中的一個(gè)過濾器,系統(tǒng)的其余部分則保持不變:

圖 9:基本 HTTP 驗(yàn)證過濾器

由于一些技術(shù)上的限制,目前還無法在 Winglang 中直接實(shí)現(xiàn) Pipe-and-Filters 功能,但可以通過結(jié)合 Decorator 和 Factory 設(shè)計(jì)模式來輕松模擬。具體如何實(shí)現(xiàn),我們很快就會(huì)看到。現(xiàn)在,讓我們進(jìn)入下一個(gè)主題。

運(yùn)行

在本刊物中,作者不會(huì)涉及生產(chǎn)運(yùn)營(yíng)的所有方面。這個(gè)主題很大,值得單獨(dú)出版。以下是作者認(rèn)為最基本的內(nèi)容:

圖 10:日志和 Try/Catch 過濾器

要運(yùn)行一項(xiàng)服務(wù),我們需要知道它發(fā)生了什么,尤其是在出錯(cuò)時(shí)。這可以通過結(jié)構(gòu)化日志機(jī)制來實(shí)現(xiàn)。目前,Winglang 只提供了一個(gè)基本的 log(str) 函數(shù)。在調(diào)查中,需要更多的功能,并實(shí)現(xiàn)了一個(gè)可憐的結(jié)構(gòu)化日志類:

// A poor man implementation of configurable Logger
// Similar to that of Python and TypeScript
bring cloud;
bring "./dateTime.w" as dateTime;

pub enum logging {
TRACE,
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
}

//This is just enough configuration
//A serious review including compliance
//with OpenTelemetry and privacy regulations
//Is required. The main insight:
//Serverless Cloud logging is substantially
//different
pub interface ILoggingStrategy {
inflight timestamp(): str;
inflight print(message: Json): void;
}

pub class DefaultLoggerStrategy impl ILoggingStrategy {
pub inflight timestamp(): str {
return dateTime.DateTime.toUtcString(std.Datetime.utcNow());
}
pub inflight print(message: Json): void {
log("{message}");
}
}

//TBD: probably should go into a separate module
bring expect;
bring ex;

pub class MockLoggerStrategy impl ILoggingStrategy {
_name: str;
_counter: cloud.Counter;
_messages: ex.Table;

new(name: str?) {
this._name = name ?? "MockLogger";
this._counter = new cloud.Counter();
this._messages = new ex.Table(
name: "{this._name}Messages",
columns: Map<ex.ColumnType>{
"id" => ex.ColumnType.STRING,
"message" => ex.ColumnType.STRING
},
primaryKey: "id"
);
}
pub inflight timestamp(): str {
return "{this._counter.inc(1, this._name)}";
}
pub inflight expect(messages: Array<Json>): void {
for message in messages {
this._messages.insert(
message.get("timestamp").asStr(),
Json{ message: "{message}"}
);
}
}
pub inflight print(message: Json): void {
let expected = this._messages.get(
message.get("timestamp").asStr()
).get("message").asStr();
expect.equal("{message}", expected);
}
}

pub class Logger {
_labels: Array<str>;
_levels: Array<logging>;
_level: num;
_service: str;
_strategy: ILoggingStrategy;

new (level: logging, service: str, strategy: ILoggingStrategy?) {
this._labels = [
"TRACE",
"DEBUG",
"INFO",
"WARNING",
"ERROR",
"FATAL"
];
this._levels = Array<logging>[
logging.TRACE,
logging.DEBUG,
logging.INFO,
logging.WARNING,
logging.ERROR,
logging.FATAL
];
this._level = this._levels.indexOf(level);
this._service = service;
this._strategy = strategy ?? new DefaultLoggerStrategy();
}
pub inflight log(level_: logging, func: str, message: Json): void {
let level = this._levels.indexOf(level_);
let label = this._labels.at(level);
if this._level <= level {
this._strategy.print(Json {
timestamp: this._strategy.timestamp(),
level: label,
service: this._service,
function: func,
message: message
});
}
}
pub inflight trace(func: str, message: Json): void {
this.log(logging.TRACE, func,message);
}
pub inflight debug(func: str, message: Json): void {
this.log(logging.DEBUG, func, message);
}
pub inflight info(func: str, message: Json): void {
this.log(logging.INFO, func, message);
}
pub inflight warning(func: str, message: Json): void {
this.log(logging.WARNING, func, message);
}
pub inflight error(func: str, message: Json): void {
this.log(logging.ERROR, func, message);
}
pub inflight fatal(func: str, message: Json): void {
this.log(logging.FATAL, func, message);
}
}

正如作者在評(píng)論中寫道的那樣,基于云的日志系統(tǒng)需要認(rèn)真修改。不過,對(duì)于目前的調(diào)查來說,這已經(jīng)足夠了。完全相信,日志記錄是任何服務(wù)規(guī)范不可分割的一部分,必須像測(cè)試核心功能一樣嚴(yán)格。為此,開發(fā)了一種簡(jiǎn)單的機(jī)制來模擬日志,并根據(jù)預(yù)期進(jìn)行檢查。

對(duì)于 REST API CRUD 服務(wù),我們至少需要記錄三類日志:

  1. HTTP 請(qǐng)求
  2. 發(fā)生錯(cuò)誤時(shí)的原始錯(cuò)誤信息
  3. HTTP 響應(yīng)

此外,根據(jù)需要,可能需要將原始錯(cuò)誤信息轉(zhuǎn)換為標(biāo)準(zhǔn)信息,例如,為了不給攻擊者提供可乘之機(jī)。

記錄多少細(xì)節(jié)取決于多種因素:部署目標(biāo)、請(qǐng)求類型、特定用戶、錯(cuò)誤類型、統(tǒng)計(jì)采樣等。在開發(fā)和測(cè)試模式下,我們通常會(huì)選擇記錄幾乎所有內(nèi)容,并將原始錯(cuò)誤信息直接返回到客戶端屏幕,以方便調(diào)試。在生產(chǎn)模式下,我們可能會(huì)選擇刪除一些敏感數(shù)據(jù)(因?yàn)橛蟹ㄒ?guī)要求),返回 “Bad Request”(錯(cuò)誤請(qǐng)求)等一般錯(cuò)誤信息,而不提供任何詳細(xì)信息,并只對(duì)特定類型的請(qǐng)求進(jìn)行統(tǒng)計(jì)抽樣記錄,以節(jié)省成本。

通過在每個(gè)請(qǐng)求處理管道中注入四個(gè)額外的過濾器,實(shí)現(xiàn)了靈活的日志配置:

  1. HTTP 請(qǐng)求記錄過濾器
  2. Try/Catch 裝飾器,用于將任何異常轉(zhuǎn)換為 HTTP 狀態(tài)代碼,并記錄原始錯(cuò)誤信息(這可以提取為一個(gè)單獨(dú)的過濾器,但作者決定保持簡(jiǎn)單)
  3. 錯(cuò)誤信息翻譯器,根據(jù)需要將原始錯(cuò)誤信息轉(zhuǎn)換為標(biāo)準(zhǔn)錯(cuò)誤信息
  4. HTTP 響應(yīng)記錄過濾器

這種結(jié)構(gòu)雖然不是終極結(jié)構(gòu),但它提供了足夠的靈活性,可根據(jù)服務(wù)及其部署目標(biāo)的具體情況,實(shí)施多種日志記錄和錯(cuò)誤處理策略。

與日志一樣,Winglang 目前只提供了一個(gè)基本的 throw <str> 操作符,因此決定實(shí)現(xiàn)窮人版結(jié)構(gòu)化異常:

// A poor man structured exceptions
pub inflight class Exception {
pub tag: str;
pub message: str?;

new(tag: str, message: str?) {
this.tag = tag;
this.message = message;
}
pub raise() {
let err = Json.stringify(this);
throw err;
}
pub static fromJson(err: str): Exception {
let je = Json.parse(err);

return new Exception(
je.get("tag").asStr(),
je.tryGet("message")?.tryAsStr()
);
}
pub toJson(): Json { //for logging
return Json{tag: this.tag, message: this.message};
}
}

// Standard exceptions, similar to those of Python
pub inflight class KeyError extends Exception {
new(message: str?) {
super("KeyError", message);
}
}
pub inflight class ValueError extends Exception {
new(message: str?) {
super("ValueError", message);
}
}
pub inflight class InternalError extends Exception {
new(message: str?) {
super("InternalError", message);
}
}
pub inflight class NotImplementedError extends Exception {
new(message: str?) {
super("NotImplementedError", message);
}
}
//Two more HTTP-specific, yet useful
pub inflight class AuthenticationError extends Exception {
//aka HTTP 401 Unauthorized
new(message: str?) {
super("AuthenticationError", message);
}
}
pub inflight class AuthorizationError extends Exception {
//aka HTTP 403 Forbidden
new(message: str?) {
super("AuthorizationError", message);
}
}

這些經(jīng)驗(yàn)凸顯了開發(fā)人員社區(qū)如何通過臨時(shí)變通辦法來彌補(bǔ)新語言的不足。盡管 Winglang 仍在不斷發(fā)展,但它的創(chuàng)新功能可以被利用來取得進(jìn)步。

現(xiàn)在,是時(shí)候簡(jiǎn)單了解一下清單上的最后一個(gè)生產(chǎn)主題了:

規(guī)模

擴(kuò)展是云計(jì)算開發(fā)的一個(gè)重要方面,但卻經(jīng)常被誤解。有些人完全忽視了這一點(diǎn),導(dǎo)致系統(tǒng)增長(zhǎng)時(shí)出現(xiàn)問題。另一些人則過度工程化,希望從第一天起就成為一個(gè) “FANG “系統(tǒng)。我們?cè)?Kubernetes 上運(yùn)行一切 “是技術(shù)圈子里的一句流行語,不管它是否適合手頭的項(xiàng)目。

忽視和過度設(shè)計(jì)這兩種極端情況都不理想。與安全性一樣,不應(yīng)該忽視擴(kuò)展性,但也不應(yīng)該過分強(qiáng)調(diào)它。

在一定程度上,云平臺(tái)提供了具有成本效益的擴(kuò)展機(jī)制。不同選項(xiàng)之間的選擇往往歸結(jié)為個(gè)人偏好或惰性,而非顯著的技術(shù)優(yōu)勢(shì)。

謹(jǐn)慎的做法是從小規(guī)模、低成本開始,根據(jù)實(shí)際使用情況和性能數(shù)據(jù)而不是假設(shè)進(jìn)行擴(kuò)展。這種方法要求系統(tǒng)在設(shè)計(jì)上易于更改配置以適應(yīng)擴(kuò)展,Winglang本身并不支持這種方法,但通過進(jìn)一步的開發(fā)和研究,這種方法肯定是可行的。舉例來說,讓我們考慮一下 AWS 生態(tài)系統(tǒng)內(nèi)的擴(kuò)展:

  1. 起初,一個(gè)經(jīng)濟(jì)高效的快速部署可能涉及使用 S3 Bucket 進(jìn)行存儲(chǔ)的單個(gè) Lambda 函數(shù) URL,用于帶有服務(wù)器端渲染功能的全棧 CRUD API。這種設(shè)置可以實(shí)現(xiàn)對(duì)早期開發(fā)階段至關(guān)重要的快速反饋。就個(gè)人而言,更傾向于 “用戶體驗(yàn)優(yōu)先 “的方法,而不是 “應(yīng)用程序接口優(yōu)先 “的方法。你可能會(huì)驚訝于使用這種基本的技術(shù)堆棧能走多遠(yuǎn)。雖然 Winglang 目前還不支持 Lambda 函數(shù) URL,但相信通過過濾器組合和系統(tǒng)調(diào)整可以實(shí)現(xiàn)。在這個(gè)層面上,遵循 Marc Van Neerven 的建議,使用標(biāo)準(zhǔn)的 Web 組件而不是厚重的框架,可能會(huì)有所裨益。這是一個(gè)有待今后探索的課題。
  2. 當(dāng)需要外部 API 暴露或 Web Sockets 等高級(jí)功能時(shí),就需要過渡到 API 網(wǎng)關(guān)或 GraphQL 網(wǎng)關(guān)。如果初始數(shù)據(jù)存儲(chǔ)解決方案成為瓶頸,可能就需要考慮切換到 Dynamo DB 這樣更強(qiáng)大、可擴(kuò)展的解決方案了。此時(shí),為每個(gè) API 請(qǐng)求部署單獨(dú)的 Lambda 函數(shù)可能會(huì)簡(jiǎn)化實(shí)施過程,但這并不總是最具成本效益的策略。
  3. 向容器化解決方案的轉(zhuǎn)變應(yīng)該以數(shù)據(jù)為導(dǎo)向,只有當(dāng)有明確證據(jù)表明,基于功能的架構(gòu)要么成本過高,要么因冷啟動(dòng)而存在延遲問題時(shí),才會(huì)考慮采用容器化解決方案。對(duì)容器的初步嘗試可能包括使用 ECS Fargate,因?yàn)樗?jiǎn)單且具有成本效益,而將 EKS 保留給需要其功能的特定運(yùn)營(yíng)需求場(chǎng)景。理想情況下,應(yīng)通過配置調(diào)整和自動(dòng)過濾器生成來管理這種演進(jìn),并利用 Winglang 的獨(dú)特功能來支持動(dòng)態(tài)擴(kuò)展策略。

從本質(zhì)上講,Winglang 的方法強(qiáng)調(diào) “飛行前”(Preflight)和 “飛行中”(Inflight)階段,有望促進(jìn)這些擴(kuò)展策略,盡管它可能仍處于充分實(shí)現(xiàn)這一潛力的早期階段。對(duì)云軟件開發(fā)中可擴(kuò)展性的探索強(qiáng)調(diào)從小處入手,根據(jù)實(shí)際數(shù)據(jù)做出決策,并保持靈活性以適應(yīng)不斷變化的需求。

結(jié)束語

20 世紀(jì) 90 年代中期,作者從 Jim Coplien 那里學(xué)到了共性可變性分析法。從那時(shí)起,這種方法與 Edsger W. Dijkstra 的分層架構(gòu)一起,成為作者軟件工程實(shí)踐的基石。共性變異性分析會(huì)問”在我們的系統(tǒng)中,哪些部分永遠(yuǎn)不變,哪些部分可能需要改變?”開放-封閉原則 “規(guī)定,可變部分應(yīng)可替換,而無需修改核心系統(tǒng)。

決定何時(shí)最終確定系統(tǒng)的穩(wěn)定部分需要在靈活性和效率之間權(quán)衡利弊,從代碼生成到運(yùn)行時(shí)的幾個(gè)階段都提供了固定的機(jī)會(huì)。動(dòng)態(tài)語言的支持者可能會(huì)將這些決定推遲到運(yùn)行時(shí),以獲得最大的靈活性,而靜態(tài)編譯語言的支持者通常會(huì)盡早確保關(guān)鍵系統(tǒng)組件的安全。

Winglang 憑借其獨(dú)特的預(yù)檢編譯階段脫穎而出,允許在開發(fā)過程的早期修復(fù)云資源。在本文中,作者探討了 Winglang 如何通過靈活的過濾器管道解決云服務(wù)的非功能性問題,盡管這種粒度帶來了自身的復(fù)雜性。現(xiàn)在的挑戰(zhàn)是在不影響系統(tǒng)效率或靈活性的前提下管理這種復(fù)雜性。

雖然最終的解決方案還在制定過程中,但作者可以勾勒出一個(gè)能平衡這些力量的高層次設(shè)計(jì)方案:

圖 11:Pipeline Builder

這種設(shè)計(jì)結(jié)合了多種軟件設(shè)計(jì)模式,以達(dá)到理想的平衡。這一過程包括:

  1. Pipeline Builder組件負(fù)責(zé)準(zhǔn)備最終的預(yù)檢組件集。
  2. Pipeline Builder會(huì)讀取一個(gè)配置,該配置可能會(huì)被組織成一個(gè)復(fù)合配置(團(tuán)隊(duì)或組織范圍內(nèi)的配置)。
  3. 配置指定了對(duì)資源(如記錄儀)的能力要求。
  4. 每個(gè)資源都有多個(gè)規(guī)范,每個(gè)規(guī)范都定義了需要調(diào)用工廠生成所需過濾器的條件。我們?cè)O(shè)想了三種過濾器類型:
    – 行 HTTP 請(qǐng)求/響應(yīng)過濾器
    – 擴(kuò)展 HTTP 請(qǐng)求/響應(yīng)過濾器,在令牌驗(yàn)證后提取會(huì)話信息
    – 轉(zhuǎn)發(fā)給核心的通用 CRUD 請(qǐng)求過濾器

這種方法將復(fù)雜性轉(zhuǎn)向?qū)崿F(xiàn) Pipeline Builder機(jī)制和配置規(guī)范。經(jīng)驗(yàn)告訴我們可以實(shí)施這種機(jī)制(例如本文中描述的)。這通常需要一些通用編程和動(dòng)態(tài)導(dǎo)入功能。提出一個(gè)好的配置數(shù)據(jù)模型更具挑戰(zhàn)性。

基于生成式人工智能的協(xié)同駕駛儀的最新進(jìn)展提出了如何實(shí)現(xiàn)最具成本效益的結(jié)果的新問題。為了理解這個(gè)問題,讓我們重溫一下傳統(tǒng)的編譯和配置堆棧:

圖 12:典型的編程工具鏈棧

這種一般情況可能不適用于每個(gè)生態(tài)系統(tǒng)。以下是典型層的細(xì)分:

  1. 核心語言設(shè)計(jì)為小型語言(”C “和 Lisp 傳統(tǒng))。它可能支持反射,也可能不支持反射。
  2. 盡可能多的擴(kuò)展功能由標(biāo)準(zhǔn)庫、第三方庫和框架提供。
  3. 通用元編程:對(duì) C++ 模板或 Lisp 宏等特性的支持會(huì)在早期(C++、Rust)或晚期(Java、C#)引入。泛型一直是爭(zhēng)論的焦點(diǎn):
    – 框架開發(fā)人員認(rèn)為它們的表現(xiàn)力不夠。
    – 應(yīng)用程序開發(fā)人員則為其復(fù)雜性而苦惱。
    – Scala 就是泛型過于復(fù)雜的潛在弊端的一個(gè)例證。
  4. 盡管飽受批評(píng),宏(如 C 預(yù)處理器)仍是自動(dòng)生成代碼的工具,通常可以彌補(bǔ)泛型的局限性。
  5. 第三方供應(yīng)商(通常是開源的)提供的解決方案通常使用外部配置文件(YAML、JSON 等)來增強(qiáng)或彌補(bǔ)標(biāo)準(zhǔn)庫。
  6. 專業(yè)生成器通常使用外部藍(lán)圖或模板。

這種復(fù)雜的結(jié)構(gòu)有其局限性。泛型會(huì)掩蓋核心語言,宏是不安全的,配置文件是偽裝得很差的腳本,代碼生成器依賴于不靈活的靜態(tài)模板。這些局限性正是作者認(rèn)為當(dāng)前內(nèi)部開發(fā)平臺(tái)趨勢(shì)發(fā)展?jié)摿τ邢薜脑颉?/p>

當(dāng)我們期待生成式人工智能在簡(jiǎn)化這些過程中發(fā)揮作用時(shí),問題就來了:在軟件工程中,基于生成式人工智能的協(xié)同駕駛不僅能簡(jiǎn)化流程,還能增強(qiáng)我們平衡共性與變異性的能力嗎?

原文鏈接:Implementing Production-grade CRUD REST API in Winglang

作者:Asher Sterkin

上一篇:

2024年10個(gè)最佳Node JS REST API框架

下一篇:

面向 99% 的人的 SaaS 革命即將到來
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊(cè)

多API并行試用

數(shù)據(jù)驅(qū)動(dòng)選型,提升決策效率

查看全部API→
??

熱門場(chǎng)景實(shí)測(cè),選對(duì)API

#AI文本生成大模型API

對(duì)比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)

#AI深度推理大模型API

對(duì)比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)