上一篇我們學習了 Search Attributes 的基礎概念與實作。本篇將深入探討如何在生產環境中做出好的設計決策,特別是在複雜的分散式系統架構下。
Search Attributes 不只是「查詢」功能,它是讓你用業務語言來觀測系統的介面:
# ❌ 傳統做法:查記錄檔找線索
grep "user_12345" /var/log/app.log | grep "payment"
// ✅ 有了 Search Attributes:可以用業務語言直接查詢對應的 workflow
client.workflow.list({
query: 'CustomerId = "user_12345" AND OrderStatus = "payment_failed"'
});
影響面向:
層面 | 沒有好的 SA 設計 | 有好的 SA 設計 |
---|---|---|
除錯 | 翻記錄檔、查資料庫、拼湊上下文 | 直接定位問題流程 |
監控 | 只能看系統指標 | 可按業務維度(租戶、地區、功能)監控 |
告警 | 通用告警,難以分流 | 精準告警到業務單元 |
追蹤 | 難以串聯多個服務 | 以共同識別符串聯整條鏈路 |
在設計 Search Attributes 前,先問這些問題:
範例:電商訂單系統:
// 問題 1:客戶說「我的訂單沒有送到」
// 需要:CustomerId, OrderId
// 問題 2:「VIP 客戶的訂單處理太慢」
// 需要:CustomerTier, OrderStatus, CreateTime
類別 | 欄位範例 | 型別 | 使用場景 |
---|---|---|---|
業務識別符 | OrderId , CustomerId , TransactionId |
Text/Keyword | 精確定位特定業務實體 |
租戶與分群 | TenantId , CustomerTier , Region |
Keyword | 多租戶隔離、SLA 分層 |
狀態與階段 | OrderStatus , PaymentStage , RetryPhase |
Keyword | 狀態機追蹤、流程監控 |
版本與部署 | BuildID , ReleaseChannel , FeatureFlag |
Keyword | 灰度發布、版本管理 |
拓樸關係 | ParentWorkflowId , TraceGroupId , CorrelationId |
Keyword | 分散式追蹤 |
時間與 SLA | CreateTime , DueTime , SlaDeadline |
Datetime | 時間範圍查詢、逾期告警 |
// 完整的 Search Attributes 設計
interface OrderSearchAttributes {
// 業務識別符
OrderId: string; // 訂單編號(Text)
CustomerId: string; // 客戶 ID(Text)
// 租戶與分群
TenantId: string; // 租戶 ID(Keyword)
CustomerTier: string; // VIP/Premium/Basic(Keyword)
Region: string; // 地區(Keyword)
// 狀態
OrderStatus: string; // pending/paid/shipped/completed(Keyword)
PaymentMethod: string; // credit_card/paypal/bank_transfer(Keyword)
// 版本
BuildID: string; // 部署版本(Keyword)
// 時間
CreateTime: Date; // 建立時間(Datetime)
SlaDeadline: Date; // SLA 截止時間(Datetime)
}
// 在 Workflow 中設定
export async function orderWorkflow(input: OrderInput) {
const slaDeadline = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24小時
// 啟動時設定不變的屬性
// (這部分在 client 端的 start() 中設定)
// 狀態變化時更新
await processPayment(input.paymentMethod);
upsertSearchAttributes({
OrderStatus: ['paid'],
PaymentMethod: [input.paymentMethod],
});
await shipOrder();
upsertSearchAttributes({
OrderStatus: ['shipped'],
});
}
跨團隊一致性:
跨服務共享欄位:確保相同概念用相同名稱:
基數(Cardinality) = 該欄位的唯一值數量
高基數的影響:
// ❌ 直接存 Email(高基數)
searchAttributes: {
Email: ['user@example.com'], // 數百萬個唯一值
}
// ✅ 策略 1:只存 domain(降維)
searchAttributes: {
EmailDomain: ['example.com'], // 降到數千個值
}
// ✅ 策略 2:雜湊(用於去重或冪等)
import { createHash } from 'crypto';
const emailHash = createHash('sha256')
.update('user@example.com')
.digest('hex')
.substring(0, 16); // 取前 16 碼
searchAttributes: {
EmailHash: [emailHash], // 仍然高基數,但只用於精確查詢
}
// ✅ 策略 3:分桶
const userBucket = parseInt(customerId.slice(-2), 16) % 10; // 0-9
searchAttributes: {
UserBucket: [userBucket.toString()], // 只有 10 個值
}
// ❌ 太頻繁更新(增加歷史與索引壓力)
export async function badWorkflow() {
for (let i = 0; i < 100; i++) {
await someActivity();
upsertSearchAttributes({ ProcessedCount: [i] }); // 每次都更新
}
}
// ✅ 只在關鍵節點更新
export async function goodWorkflow() {
await processPayment();
upsertSearchAttributes({ OrderStatus: ['paid'] });
await shipOrder();
upsertSearchAttributes({ OrderStatus: ['shipped'] });
}
方案一、單 Namespace + TenantId:
// 適用:租戶間無嚴格隔離需求
await client.workflow.start(orderWorkflow, {
searchAttributes: {
TenantId: ['tenant_abc'],
CustomerTier: ['enterprise'], // 可做 SLA 分層
},
});
// 查詢時必須帶 TenantId
const workflows = await client.workflow.list({
query: 'TenantId = "tenant_abc" AND OrderStatus = "pending"',
});
// 權限控制:在應用層檢查
if (user.tenantId !== workflow.searchAttributes.TenantId) {
throw new Error('Access denied');
}
方案二、多 Namespace:
// 適用:租戶間需嚴格隔離(資料、配額、權限)
const tenantAClient = new Client({
namespace: 'tenant-a',
});
const tenantBClient = new Client({
namespace: 'tenant-b',
});
// 每個租戶完全隔離
await tenantAClient.workflow.start(orderWorkflow, {
searchAttributes: {
CustomerId: ['user_123'],
},
});
對照表:
考量 | 單 Namespace | 多 Namespace |
---|---|---|
隔離程度 | 應用層隔離 | 基礎設施層隔離 |
管理複雜度 | 低 | 高(需要管理多個 Namespace) |
配額控制 | 共享 | 獨立 |
成本 | 低 | 高(資源分散) |
查詢 | 需帶 TenantId | 自動隔離 |
// 全球化電商:不同地區的訂單處理
interface GlobalOrderSA {
OrderId: string;
CustomerId: string;
Region: string; // 'us-east', 'eu-west', 'ap-south'
AvailabilityZone: string; // 'us-east-1a', 'us-east-1b'
DataCenter: string; // 更細粒度的位置
}
// 啟動時設定地區資訊
await client.workflow.start(orderWorkflow, {
searchAttributes: {
OrderId: ['ORD-12345'],
Region: ['us-east'],
AvailabilityZone: ['us-east-1a'],
},
});
// 查詢:找出特定地區的慢訂單
const slowOrders = await client.workflow.list({
query: `
Region = "us-east" AND
OrderStatus = "processing" AND
CreateTime < "${new Date(Date.now() - 60*60*1000).toISOString()}"
`,
});
// 用共同的 TraceGroupId 串聯多個相關 Workflow
const traceGroupId = `trace_${orderId}_${Date.now()}`;
// 1. 主訂單 Workflow
await client.workflow.start(orderWorkflow, {
searchAttributes: {
OrderId: [orderId],
TraceGroupId: [traceGroupId],
},
});
// 2. 支付 Workflow(傳遞相同的 TraceGroupId)
await client.workflow.start(paymentWorkflow, {
searchAttributes: {
OrderId: [orderId],
TraceGroupId: [traceGroupId], // 相同!
},
});
// 查詢:找出整條鏈路的所有 Workflow
const allWorkflows = await client.workflow.list({
query: `TraceGroupId = "${traceGroupId}"`,
});
// 情境:需要將 CustomerType 改名為 CustomerTier
// Phase 1:新增新欄位並雙寫
await client.workflow.start(orderWorkflow, {
searchAttributes: {
CustomerType: ['vip'], // 舊欄位(保留相容性)
CustomerTier: ['vip'], // 新欄位
},
});
// Phase 2:更新所有查詢邏輯使用新欄位後,停止寫入舊欄位
await client.workflow.start(orderWorkflow, {
searchAttributes: {
CustomerTier: ['vip'], // 只寫新欄位
},
});
// 啟動時標記版本
await client.workflow.start(orderWorkflow, {
searchAttributes: {
BuildID: ['v2.1.0'],
ReleaseChannel: ['canary'],
},
});
// 灰度發布:查詢特定版本的執行情況
const canaryWorkflows = await client.workflow.list({
query: 'BuildID = "v2.1.0" AND ReleaseChannel = "canary"',
});
const successRate = calculateSuccessRate(canaryWorkflows);
console.log(successRate > 0.95 ? 'Canary 成功' : 'Canary 失敗,需回滾');
// ❌ 低效查詢
'OrderStatus != "cancelled"' // 否定查詢,掃描大量資料
// ✅ 高效查詢模式
'CustomerId = "user_12345"' // 精確匹配
'CustomerId = "user_12345" AND OrderStatus = "pending"' // 多條件縮小範圍
'Region = "us-east" AND CreateTime > "2024-01-01T00:00:00Z"' // 區間查詢
// ✅ 使用高選擇性欄位優先
// 選擇性:能夠區分出少量結果的能力
// 高選擇性:CustomerId(每個使用者唯一)
// 中選擇性:Region(10-20 個值)
// 低選擇性:OrderStatus(5-10 個值)
// 組合查詢:高選擇性欄位在前
'CustomerId = "user_123" AND Region = "us-east" AND OrderStatus = "pending"'
// ✅ 使用分頁處理大量結果
let nextPageToken: Uint8Array | undefined;
do {
const page = await client.workflow.list({
query: 'OrderStatus = "pending"',
pageSize: 100,
nextPageToken,
});
for await (const workflow of page.workflows) {
await processWorkflow(workflow);
}
nextPageToken = page.nextPageToken;
} while (nextPageToken);
在上線前,用這份檢查清單確保 Search Attributes 設計完善:
Search Attributes 的價值遠超過「查詢」:
設計建議總結:
從這個角度設計 Search Attributes,Temporal 不僅是執行引擎,更是可觀測性引擎。