新聞中心
審校 | 孫淑娟

10多年的鳳泉網(wǎng)站建設(shè)經(jīng)驗,針對設(shè)計、前端、開發(fā)、售后、文案、推廣等六對一服務(wù),響應(yīng)快,48小時及時工作處理。營銷型網(wǎng)站建設(shè)的優(yōu)勢是能夠根據(jù)用戶設(shè)備顯示端的尺寸不同,自動調(diào)整鳳泉建站的顯示方式,使網(wǎng)站能夠適用不同顯示終端,在瀏覽器中調(diào)整網(wǎng)站的寬度,無論在任何一種瀏覽器上瀏覽網(wǎng)站,都能展現(xiàn)優(yōu)雅布局與設(shè)計,從而大程度地提升瀏覽體驗。創(chuàng)新互聯(lián)從事“鳳泉網(wǎng)站設(shè)計”,“鳳泉網(wǎng)站推廣”以來,每個客戶項目都認真落實執(zhí)行。
隨著組織將單體應(yīng)用程序分解成微服務(wù),遇到的主要障礙之一就是識別數(shù)據(jù)庫依賴關(guān)系。
數(shù)據(jù)庫共享可能是復(fù)雜的挑戰(zhàn)。數(shù)據(jù)庫不允許您定義什么是共享的、什么不是。在修改模式以更好地服務(wù)于一個微服務(wù)時,您可能無意中破壞另一微服務(wù)使用這同一數(shù)據(jù)庫的方式。
此外,常常很難識別數(shù)據(jù)所有者、確定處理數(shù)據(jù)的業(yè)務(wù)邏輯。
本文探討如何使用OpenTelemetry來識別共享同一數(shù)據(jù)庫和數(shù)據(jù)庫對象(比如表)的組件。
可觀察性和OpenTelemetry:基礎(chǔ)
在構(gòu)建演示應(yīng)用程序之前,不妨先討論可觀察性和OpenTelemetry。
什么讓應(yīng)用程序高度可觀察?
如果可以通過研究在任何時間點的輸出來推斷系統(tǒng)的內(nèi)部狀態(tài),該系統(tǒng)就被稱為高度可觀察。
比如說,與多個服務(wù)交互的可觀察移動應(yīng)用程序可以重建生成錯誤響應(yīng)的事務(wù),以便開發(fā)人員識別失敗的根本原因。
圖1. 可觀察應(yīng)用程序示例
可觀察應(yīng)用程序為每個事務(wù)收集三種類型的信息:
- 日志:記錄構(gòu)成事務(wù)的各個事件。
- 指標:記錄構(gòu)成事務(wù)的群體事件。
- 跟蹤:記錄操作延遲,以識別事務(wù)中的瓶頸。
OpenTelemetry簡介
OpenTelemetry 是一個以集成方式生成日志、指標和跟蹤的系統(tǒng)。OpenTelemetry定義了一個標準來捕獲可觀察性數(shù)據(jù)。OpenTelemetry數(shù)據(jù)模型有幾個關(guān)鍵部分。
屬性
OpenTelemetry中的每個數(shù)據(jù)結(jié)構(gòu)都由屬性組成,屬性是鍵值對。OpenTelemetry標準定義了任何組件(比如SQL客戶端或HTTP請求)可以指定的屬性。
活動
事件就是時間戳和一組屬性。您可以記錄事件的詳細信息,比如消息和異常細節(jié)。
上下文
上下文包括一組事件共有的屬性。上下文有兩種:靜態(tài)上下文(或資源)定義了事件的位置。在應(yīng)用程序可執(zhí)行文件啟動后,它們的值不變,比如包括服務(wù)的名稱或版本或者庫名稱。
動態(tài)上下文(或span)定義了包含事件的活動操作。span屬性的值在操作執(zhí)行時發(fā)生變化。一些常見的span屬性包括請求的開始時間、HTTP響應(yīng)狀態(tài)代碼或HTTP請求路徑。
在分布式事務(wù)中,上下文需要傳遞給所有關(guān)聯(lián)的服務(wù)。在這種情況下,接收方使用上下文生成新的span??缭椒?wù)邊界的跟蹤稱為分布式跟蹤,將上下文傳輸?shù)狡渌?wù)的過程名為上下文傳播。
日志
日志是僅伴隨資源的事件。一個例子是程序啟動時發(fā)出的事件。
跟蹤
事件可以組織成與資源相關(guān)的操作圖。跟蹤是顯示與事務(wù)相關(guān)的事件的圖形。
指標
一個事件可能在任何應(yīng)用程序中發(fā)生多次,或者它的值可能會變。指標是一種事件,其值可以是相關(guān)事件的計數(shù)或事件值的某種計算。指標的一個例子是系統(tǒng)內(nèi)存事件,它的屬性是使用和利用率。
想詳細了解OpenTelemetry的概念,請參閱文檔:https://opentelemetry.lightstep.com/。
使用OpenTelemetry識別數(shù)據(jù)庫依賴關(guān)系
我們之前討論過,OpenTelemetry規(guī)定了應(yīng)用程序各組件應(yīng)捕獲的屬性。許多流行語言提供了開箱即用的工具庫,以收集用于數(shù)據(jù)庫操作的遙測數(shù)據(jù)。
本文演示使用面向OpenTelemetry的??.NET SQLClient工具??,以及用于遙測數(shù)據(jù)存儲和分析的Lightstep。
不妨討論演示應(yīng)用程序的架構(gòu),以了解遙測數(shù)據(jù)到達Lightstep的路徑。我們僅討論跟蹤,因為跟蹤足以識別數(shù)據(jù)庫和單體組件之間的依賴關(guān)系。
然而,任何企業(yè)應(yīng)用程序都會生成相關(guān)的日志和指標以及跟蹤以實現(xiàn)完整的可見性。
圖2. 從.NET應(yīng)用程序?qū)С鯫TEL跟蹤
首先,我們使用OpenTelemetry SDK檢測單體應(yīng)用程序,以發(fā)出可觀察性信號。雖然檢測應(yīng)用程序是.NET應(yīng)用程序的手動過程,但使用Golang或Java等語言構(gòu)建的應(yīng)用程序可使用自動檢測。
我們使用SDK含有的OpenTelemetry Protocol(OTLP)Exporter。該導(dǎo)出工具讓我們可以將數(shù)據(jù)直接發(fā)送到遙測數(shù)據(jù)攝取服務(wù)。Jaeger和Lightstep等OpenTelemetry平臺聚合跟蹤,幫助您獲得洞察力。
與SDK集成后,應(yīng)用程序的各個部分(比如ASP.NET Core請求處理程序和SQL客戶端)會自動開始生成含有相關(guān)信息的跟蹤。您的代碼可以生成其他跟蹤,以豐富可用信息。
以.NET為例,OpenTelemetry實現(xiàn)基于System.Diagnostics.*命名空間中的現(xiàn)有類型,如下所示:
- System.Diagnostics.ActivitySource代表負責生成Span的OpenTelemetry跟蹤器。
- System.Diagnostics.Activity代表 Span。
- 您可以使用AddTag函數(shù)為span添加屬性。此外,您可以使用AddBaggage功能添加行李。行李被運送到子活動,使用W3C標頭的其他服務(wù)中有子活動。
檢測應(yīng)用程序后,您可以運行自動化測試,或允許用戶使用您的應(yīng)用程序來覆蓋應(yīng)用程序和數(shù)據(jù)庫之間的所有交互路徑。
演示
不妨創(chuàng)建一個簡單的單體員工管理服務(wù)(EMS),以SP.NET Core minimal API為模型。我們的API將具有以下端點:
- POST /ems/billing:記錄員工為項目所花的工時。
- GET /ems/billing/{employeeId}:獲取員工為不同項目所花的工時。
- POST /ems/payroll/add:將員工添加到工資單上。
- GET /ems/payroll/{employeeId}:獲取員工的工資單數(shù)據(jù)。
您會注意到單體應(yīng)用程序服務(wù)于兩個不同的領(lǐng)域:計費和工資單。這種依賴關(guān)系在復(fù)雜的單體應(yīng)用程序中可能不是很明顯,將它們分離開來可能需要大量的代碼重構(gòu)。
然而,如果研究依賴關(guān)系,您可以輕松地將它們分離開來。EMS應(yīng)用程序的完整源代碼可在該??GitHub存儲庫??中找到。
啟動數(shù)據(jù)庫
我們先在Docker中啟動一個SQL server實例:
docker run \
-e "ACCEPT_EULA=Y" \
-e "SA_PASSWORD=Str0ngPa$$w0rd" \
-p 1433:1433 \
--name monolith-db \
--hostname sql1 \
-d mcr.microsoft.com/mssql/server:2019-latest
我們使用下列SQL腳本來創(chuàng)建我們的應(yīng)用程序所使用的EMS數(shù)據(jù)庫和表:
IF NOT EXISTS(SELECT * FROM sys.databases WHERE name = 'EMSDb')
BEGIN
CREATE DATABASE EMSDb
END
GO
USE EMSDb
IF OBJECT_ID('[dbo].[Timekeeping]', 'U') IS NULL
BEGIN
CREATE TABLE [Timekeeping] (
[EmployeeId] INT NOT NULL,
[ProjectId] INT NOT NULL,
[WeekClosingDate] DATETIME NOT NULL,
[HoursWorked] INT NOT NULL,
CONSTRAINT [PK_Timekeeping] PRIMARY KEY CLUSTERED ([EmployeeId] ASC, [ProjectId] ASC, [WeekClosingDate] ASC)
)
END
GO
IF OBJECT_ID('[dbo].[Payroll]', 'U') IS NULL
BEGIN
CREATE TABLE [Payroll] (
[EmployeeId] INT NOT NULL,
[PayRateInUSD] MONEY DEFAULT 0 NOT NULL,
CONSTRAINT [PK_Payroll] PRIMARY KEY CLUSTERED ([EmployeeId] ASC)
)
END
GO
實現(xiàn)API服務(wù)
接下來,我們?yōu)锳PI端點編寫代碼。我們把Program類中的樣板代碼換成以下代碼:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped(_ =>
new SqlConnection(builder.Configuration.GetConnectionString("EmployeeDbConnectionString")));
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.MapPost("/ems/billing", async (Timekeeping timekeepingRecord, SqlConnection db) =>
{
await db.ExecuteAsync(
"INSERT INTO Timekeeping Values(@EmployeeId, @ProjectId, @WeekClosingDate, @HoursWorked)",
timekeepingRecord);
return Results.Created($"/ems/billing/{timekeepingRecord.EmployeeId}", timekeepingRecord);
})
.WithName("RecordProjectWork")
.Produces(StatusCodes.Status201Created);
app.MapGet("/ems/billing/{empId}/", async (int empId, SqlConnection db) =>
{
var result = await db.QueryAsync("SELECT * FROM Timekeeping WHERE EmployeeId=@empId", empId);
return result.Any() ? Results.Ok(result) : Results.NotFound();
})
.WithName("GetBillingDetails")
.Produces>()
.Produces(StatusCodes.Status404NotFound);
app.MapPost("/ems/payroll/add/", async (Payroll payrollRecord, SqlConnection db) =>
{
await db.ExecuteAsync(
"INSERT INTO Payroll Values(@EmployeeId, @PayRateInUSD)", payrollRecord);
return Results.Created($"/ems/payroll/{payrollRecord.EmployeeId}", payrollRecord);
})
.WithName("AddEmployeeToPayroll")
.Produces(StatusCodes.Status201Created);
app.MapGet("/ems/payroll/{empId}", async (int empId, SqlConnection db) =>
{
var result = await db.QueryAsync("SELECT * FROM Payroll WHERE EmployeeId=@empId", empId);
return result.Any() ? Results.Ok(result) : Results.NotFound();
})
.WithName("GetEmployeePayroll")
.Produces>()
.Produces(StatusCodes.Status404NotFound);
app.Run();
public class Timekeeping
{
public int EmployeeId { get; set; }
public int ProjectId { get; set; }
public DateTime WeekClosingDate { get; set; }
public int HoursWorked { get; set; }
}
public class Payroll
{
public int EmployeeId { get; set; }
public decimal PayRateInUSD { get; set; }
}
此時,我們可以運行應(yīng)用程序,測試各端點,并查看保存在數(shù)據(jù)庫中的記錄。雖然各端點和請求路徑的數(shù)據(jù)庫依賴關(guān)系在這個演示示例中很明顯,但在大型應(yīng)用程序中實際情況并非如此。
接下來,不妨使發(fā)現(xiàn)數(shù)據(jù)庫依賴關(guān)系的過程實現(xiàn)自動化。
添加檢測
我們使用OpenTelemetry SDK和面向.NET的SqlClient檢測庫來檢測應(yīng)用程序。我們先將以下NuGet包引用添加到API的項目文件中:
SDK為我們提供了幾種擴展方法,我們可以使用這些方法將OpenTelemetry快速接入到請求處理管道。
以下代碼在我們的API中檢測OpenTelemetry。它還將檢測SqlClient以發(fā)出詳細的遙測數(shù)據(jù)。來自SqlClient的遙測數(shù)據(jù)是詳細識別數(shù)據(jù)庫依賴關(guān)系的關(guān)鍵。
// Configure tracing
builder.Services.AddOpenTelemetryTracing(builder => builder
// Customize the traces gathered by the HTTP request handler
.AddAspNetCoreInstrumentation(options =>
{
// Only capture the spans generated from the ems/* endpoints
options.Filter = context => context.Request.Path.Value?.Contains("ems") ?? false;
options.RecordException = true;
// Add metadata for the request such as the HTTP method and response length
options.Enrich = (activity, eventName, rawObject) =>
{
switch (eventName)
{
case "OnStartActivity":
{
if (rawObject is not HttpRequest httpRequest)
{
return;
}
activity.SetTag("requestProtocol", httpRequest.Protocol);
activity.SetTag("requestMethod", httpRequest.Method);
break;
}
case "OnStopActivity":
{
if (rawObject is HttpResponse httpResponse)
{
activity.SetTag("responseLength", httpResponse.ContentLength);
}
break;
}
}
};
})
// Customize the telemetry generated by the SqlClient
.AddSqlClientInstrumentation(options =>
{
options.EnableConnectionLevelAttributes = true;
options.SetDbStatementForStoredProcedure = true;
options.SetDbStatementForText = true;
options.RecordException = true;
options.Enrich = (activity, x, y) => activity.SetTag("db.type", "sql");
})
.AddSource("my-corp.ems.ems-api")
// Create resources (key-value pairs) that describe your service such as service name and version
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("ems-api")
.AddAttributes(new[] { new KeyValuePair("service.version", "1.0.0.0") }))
// Ensures that all activities are recorded and sent to exporter
.SetSampler(new AlwaysOnSampler())
// Exports spans to Lightstep
.AddOtlpExporter(otlpOptions =>
{
otlpOptions.Endpoint = new
Uri("https://ingest.lightstep.com:443/traces/otlp/v0.9");
otlpOptions.Headers = $"lightstep-access-token={lsToken}";
otlpOptions.Protocol = OtlpExportProtocol.HttpProtobuf;
}));
雖然檢測在當前狀態(tài)下對我們來說足夠了,還是不妨添加相關(guān)跟蹤,進一步豐富數(shù)據(jù)。
首先,我們定義跟蹤器,應(yīng)用程序的span將來自該跟蹤器。
var activitySource = new ActivitySource("my-corp.ems.ems-api");接下來,我們創(chuàng)建一個span,并添加相關(guān)細節(jié)、屬性和事件:
app.MapPost("/ems/billing", async (Timekeeping timekeepingRecord, SqlConnection db) =>
{
using var activity = activitySource.StartActivity("Record project work", ActivityKind.Server);
activity?.AddEvent(new ActivityEvent("Project billed"));
activity?.SetTag(nameof(Timekeeping.EmployeeId), timekeepingRecord.EmployeeId);
activity?.SetTag(nameof(Timekeeping.ProjectId), timekeepingRecord.ProjectId);
activity?.SetTag(nameof(Timekeeping.WeekClosingDate), timekeepingRecord.WeekClosingDate);
await db.ExecuteAsync(
"INSERT INTO Timekeeping Values(@EmployeeId, @ProjectId, @WeekClosingDate, @HoursWorked)",
timekeepingRecord);
return Results.Created($"/ems/billing/{timekeepingRecord.EmployeeId}", timekeepingRecord);
})
.WithName("RecordProjectWork")
.Produces(StatusCodes.Status201Created);我們遵循同樣的程序來檢測剩余的端點。
連接到Lightstep
最后,我們需要一個API密鑰將跟蹤信息發(fā)送到Lightstep。我們先創(chuàng)建一個帳戶。在賬戶的Project設(shè)置頁面中,我們找到令牌(Token),它將充當API密鑰。
圖3. Lightstep中的API密鑰
我們拷貝令牌,并將它粘貼到appsettings文件中。
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"EmployeeDbConnectionString": "Server=localhost;Database=EMSDb;User Id=sa;Password=Str0ngPa$$w0rd;"
},
"LsToken": ""
} 發(fā)送請求
我們的應(yīng)用程序已準備就緒。我們啟動應(yīng)用程序,向每個端點發(fā)送一些請求。以下是我發(fā)送到/ems/billing端點的請求。該請求應(yīng)在數(shù)據(jù)庫的Timekeeping表中創(chuàng)建一條記錄。
圖4. 將請求發(fā)送到計費端點
這是我向/emp/payroll/add端點發(fā)出的另一個請求,用于將記錄添加到Payroll表:
圖5. 將請求發(fā)送到工資單端點
進入到Lightstep可觀察性門戶網(wǎng)站后,我們可以點擊Operations選項卡,查看Lightstep從應(yīng)用程序接收到的所有span。
圖6. Lightstep中查看來自應(yīng)用程序的span
我們點擊/ems/payroll/add操作后,可以查看端到端跟蹤。通過查看span,我們可以確定任何請求的操作順序。點擊span可顯示其事件和屬性,從中我們可以更深入地了解操作。
跟蹤中可見的最后一個span是EMSDb,它是由我們檢測的SQL客戶端生成的。點擊span可查看其屬性和事件,如下所示:
圖7. 工資單端點生成的span詳細信息
我們可以從屬性得到一些關(guān)鍵信息:
- 數(shù)據(jù)庫名稱
- 數(shù)據(jù)庫操作中使用的SQL語句
- SQL語句的類型(文本或存儲過程)
- 發(fā)出請求的服務(wù)的主機名
我們從/ems/billing操作的子span中找到了一組類似的詳細信息。
圖8. 計費端點生成的span的詳細信息
如果梳理來自跟蹤的信息,我們可以推斷出以下內(nèi)容:
- 入站操作(接收外部請求的操作)
- 完成請求的一系列活動,包括外部服務(wù)調(diào)用和數(shù)據(jù)庫操作。
- 每個操作涉及的數(shù)據(jù)庫操作。
總之,這些信息足以讓我們規(guī)劃服務(wù)和數(shù)據(jù)庫的分離,并為微服務(wù)之間的通信建立聯(lián)系。
結(jié)論
本文討論了開發(fā)人員將單體應(yīng)用程序轉(zhuǎn)換成微服務(wù)時遇到的常見挑戰(zhàn)之一。在所有問題中,拆分數(shù)據(jù)庫是一項復(fù)雜的工作,因為訪問數(shù)據(jù)庫的任何服務(wù)都可以處理數(shù)據(jù)庫。
通過使用OpenTelemetry,我們可以識別各組件之間以及組件與數(shù)據(jù)庫之間的依賴關(guān)系。 了解依賴關(guān)系后,我們可以為自己的組件制定重構(gòu)計劃,規(guī)劃它們作為獨立的微服務(wù)應(yīng)如何與時俱進。
原文標題:??How to Use OpenTelemetry to Identify Database Dependencies???,作者:Rahul Rai?
網(wǎng)頁標題:如何利用OpenTelemetry識別數(shù)據(jù)庫依賴關(guān)系?
文章起源:http://m.fisionsoft.com.cn/article/cddsiio.html


咨詢
建站咨詢
