iT邦幫忙

2025 iThome 鐵人賽

DAY 30
0
Software Development

Temporal 開發指南:掌握 Workflow as Code 打造穩定可靠的分散式流程系列 第 30

Day30 - Temporal Search Attributes(下)設計策略

  • 分享至 

  • xImage
  •  

上一篇我們學習了 Search Attributes 的基礎概念與實作。本篇將深入探討如何在生產環境中做出好的設計決策,特別是在複雜的分散式系統架構下。

1. Search Attributes 作為可觀測性的語義層

1.1 為什麼 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 設計
除錯 翻記錄檔、查資料庫、拼湊上下文 直接定位問題流程
監控 只能看系統指標 可按業務維度(租戶、地區、功能)監控
告警 通用告警,難以分流 精準告警到業務單元
追蹤 難以串聯多個服務 以共同識別符串聯整條鏈路

1.2 設計原則:先問問題,再設計欄位

在設計 Search Attributes 前,先問這些問題:

  1. 定位問題:當客戶回報問題時,我需要什麼資訊來找到對應的 Workflow?
  2. 業務洞察:我需要按什麼維度來分析流程的成功率、延遲、錯誤分布?
  3. 告警精度:出現異常時,我希望按什麼條件來分群通知?
  4. 成本追蹤:如何按租戶、地區、功能來計算資源使用?

範例:電商訂單系統:

// 問題 1:客戶說「我的訂單沒有送到」
// 需要:CustomerId, OrderId

// 問題 2:「VIP 客戶的訂單處理太慢」
// 需要:CustomerTier, OrderStatus, CreateTime

2. 進階欄位設計模式

2.1 六大類別的 Search Attributes

類別 欄位範例 型別 使用場景
業務識別符 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 時間範圍查詢、逾期告警

2.2 實戰案例:多維度訂單系統

// 完整的 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'],
  });
}

2.3 命名規範的重要性

跨團隊一致性:

  • ✅ 好的命名:清晰、一致、有前後文
    • CustomerId // 不是 UserId, ClientId, uid
    • OrderStatus // 不是 Status, OrderState, order_status
    • TenantId // 不是 TenantID, tenant_id, Tenant
  • ✅ 遵循規範
    • PascalCase
    • 完整單詞,不縮寫(除非業界通用,如 SLA, ID)
    • 前綴表達所屬(Order-, Customer-, Payment-)

跨服務共享欄位:確保相同概念用相同名稱:

  • 訂單服務
    • (OrderId: 'ORD-12345')
  • 支付服務(使用相同的 OrderId)
    • OrderId: 'ORD-12345'
    • PaymentId: 'PAY-67890'
  • 物流服務(使用相同的 OrderId)
    • OrderId: 'ORD-12345'
    • ShipmentId: 'SHIP-11111'

3. 高基數問題與解決策略

3.1 什麼是高基數問題

基數(Cardinality) = 該欄位的唯一值數量

  • 低基數(好)
    • Region: 'us-east', 'us-west', 'eu-central', 'ap-southeast' // 約 10 個值
    • OrderStatus: 'pending', 'paid', 'shipped', 'completed' // 4 個值
  • 中基數(需注意)
    • CustomerId: 'user_1', 'user_2', ..., 'user_100000' // 10萬個使用者
  • 高基數(危險)
    • Email: 每個使用者都不同 // 數百萬個唯一值
    • OrderId: 每個訂單都不同 // 數千萬個唯一值
    • Timestamp: 每秒都不同 // 無限增長

高基數的影響:

  • 索引體積暴增
  • 查詢效能下降
  • 記憶體佔用增加
  • 影響整個叢集效能

3.2 降維與雜湊策略

// ❌ 直接存 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 個值
}

3.3 控制 upsert 頻率

// ❌ 太頻繁更新(增加歷史與索引壓力)
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'] });
}

4. 多租戶與跨區架構設計

4.1 多租戶隔離策略

方案一、單 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 自動隔離

4.2 跨區架構的追蹤

// 全球化電商:不同地區的訂單處理
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()}"
  `,
});

4.3 分散式追蹤的串聯

// 用共同的 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}"`,
});

5. 欄位生命週期管理

5.1 欄位演進策略:雙寫過渡

// 情境:需要將 CustomerType 改名為 CustomerTier

// Phase 1:新增新欄位並雙寫
await client.workflow.start(orderWorkflow, {
  searchAttributes: {
    CustomerType: ['vip'],    // 舊欄位(保留相容性)
    CustomerTier: ['vip'],    // 新欄位
  },
});

// Phase 2:更新所有查詢邏輯使用新欄位後,停止寫入舊欄位
await client.workflow.start(orderWorkflow, {
  searchAttributes: {
    CustomerTier: ['vip'],    // 只寫新欄位
  },
});

5.2 版本控制與灰度發布

// 啟動時標記版本
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 失敗,需回滾');

6. 效能優化進階技巧

6.1 查詢效能優化

// ❌ 低效查詢
'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"'

6.2 使用分頁避免一次取出大量結果

// ✅ 使用分頁處理大量結果
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);

7. 企業級設計檢查清單

在上線前,用這份檢查清單確保 Search Attributes 設計完善:

7.1 基礎設計

  • [ ] 每個 SA 都有明確的業務目的
  • [ ] 命名遵循團隊規範(PascalCase, 完整單詞)
  • [ ] 型別選擇正確(Text/Keyword/Int/Datetime)
  • [ ] 欄位數量控制在 5 個以內

7.2 效能考量

  • [ ] 沒有極高基數欄位(Email, UUID 等)
  • [ ] 高基數欄位已做降維或雜湊處理
  • [ ] upsert 只在關鍵節點執行(不是迴圈中)
  • [ ] 查詢優先使用高選擇性欄位
  • [ ] 避免否定查詢和前綴查詢

7.3 多租戶與擴展性

  • [ ] 多租戶系統必有 TenantId
  • [ ] 跨區系統有 Region 欄位
  • [ ] 有版本追蹤欄位(BuildID 或 Version)
  • [ ] 有追蹤識別符(TraceGroupId 或 CorrelationId)

7.4 可觀測性整合

  • [ ] SA 與 Metrics 維度對齊
  • [ ] Tracing span 包含 WorkflowId 和關鍵 SA
  • [ ] 記錄檔包含 WorkflowId 和 RunId
  • [ ] 有從告警到 Workflow 的快速導航路徑
  • [ ] 儀表板包含 SA 維度的分析

7.5 維運與安全

  • [ ] 不包含敏感資訊(PII, 密碼, Token)
  • [ ] 有欄位生命週期管理計畫
  • [ ] 各環境(Dev/Staging/Prod)命名一致
  • [ ] 有權限控制(應用層或 Namespace 層)
  • [ ] 有監控 SA 的使用率與效能影響

結語

Search Attributes 的價值遠超過「查詢」:

  1. 業務語言層:讓你用業務概念(訂單、客戶、狀態)而非技術概念(記錄檔、追蹤)來觀測系統
  2. 整合樞紐:連接 Workflow、Metrics、Tracing、Logging 的關鍵介面
  3. 擴展基礎:支援多租戶、跨區、灰度發布等企業級場景
  4. 維運利器:快速定位問題、精準告警、成本追蹤

設計建議總結:

  • 少而精:2-5 個核心欄位,每個都有明確目的
  • 穩定優先:選擇長期穩定的業務鍵
  • 效能意識:避免高基數、減少 upsert、優化查詢
  • 一致性:命名、型別、使用方式保持一致
  • 可演進:用雙寫策略平滑過渡,不破壞現有系統

從這個角度設計 Search Attributes,Temporal 不僅是執行引擎,更是可觀測性引擎。


上一篇
Day29 - Temporal Search Attributes(上)入門實作
下一篇
Temporal 開發指南 - 完整系列文章導覽
系列文
Temporal 開發指南:掌握 Workflow as Code 打造穩定可靠的分散式流程31
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言