iT邦幫忙

2025 iThome 鐵人賽

DAY 16
0

Hippo 將螢幕呈現給洛基,畫面上滿是紅色的錯誤訊息:

ValidationException: One or more parameter values were invalid
ConditionalCheckFailedException: The conditional request failed
ProvisionedThroughputExceededException: Request rate is too high
ResourceNotFoundException: Requested resource not found

「我不知道這些錯誤是什麼意思,」洛基氣餒地說,「雖然看起來好像知道是什麼意思,但又不知道究竟在說什麼。」

諾斯克大師仔細看了看螢幕,微笑道:「這些是 DynamoDB 在告訴你具體發生了什麼問題。今天我們要學習如何『聽懂』這些訊息。」

Hippo 接著說:「錯誤訊息可是 DynamoDB 給你的免費除錯提示,可別把它們當作噪音。」

洛基的第一個錯誤遭遇

Hipoo 向洛基展示他的第一個問題:「新增活動報名功能,一直出現 ValidationException。」,出錯的程式碼如下

async function registerForEvent(userId, eventId, timestamp) {
  try {
    const params = {
      TableName: "IntergalacticEvents",
      Item: {
        PK: `USER#${userId}`,
        SK: `EVENT#${eventId}`,
        timestamp: timestamp,
        status: "registered",
      },
    };

    await docClient.put(params).promise();
    console.log("報名成功!");
  } catch (error) {
    console.error("報名失敗:", error);
  }
}

registerForEvent(12345, "MUSIC_FESTIVAL", "2210-05-15T10:00:00Z");

執行結果:

報名失敗: ValidationException: One or more parameter values were invalid: Number AttributeValue length is too big

洛基困惑地說:「什麼是 'Number AttributeValue length is too big'?我明明沒有用到很大的數字啊!」

大師引導他思考:「讓我們仔細看看你傳入的參數。userId 是什麼型別?」

「數字 12345。」

「在 JavaScript 中,你是怎麼傳入的?」

洛基重新檢查:「userId 12345...等等,我把它當作數字傳入,所以 PK 變成了 USER#12345,但 12345 是數字型別!」

大師點頭:「DynamoDB 看到的是數字 12345,而不是字串 '12345'。而且在構建複合鍵 USER#${userId} 時,JavaScript 會自動轉換,但這可能產生意想不到的結果。」

發現型別轉換的陷阱

洛基開始調查:「讓我看看實際發生了什麼。」

他修改程式來除錯:

async function debugRegisterForEvent(userId, eventId, timestamp) {
  console.log("=== 除錯資訊 ===");
  console.log("userId 型別:", typeof userId, "值:", userId);
  console.log("eventId 型別:", typeof eventId, "值:", eventId);
  console.log("timestamp 型別:", typeof timestamp, "值:", timestamp);

  const pk = `USER#${userId}`;
  const sk = `EVENT#${eventId}`;

  console.log("生成的 PK:", pk, "型別:", typeof pk);
  console.log("生成的 SK:", sk, "型別:", typeof sk);

  const item = {
    PK: pk,
    SK: sk,
    timestamp: timestamp,
    status: "registered",
  };

  console.log("完整 Item:", JSON.stringify(item, null, 2));
}

debugRegisterForEvent(12345, "MUSIC_FESTIVAL", "2210-05-15T10:00:00Z");

執行結果:

=== 除錯資訊 ===
userId 型別: number 值: 12345
eventId 型別: string 值: MUSIC_FESTIVAL
timestamp 型別: string 值: 2210-05-15T10:00:00Z
生成的 PK: USER#12345 型別: string
生成的 SK: EVENT#MUSIC_FESTIVAL 型別: string
完整 Item: {
  "PK": "USER#12345",
  "SK": "EVENT#MUSIC_FESTIVAL",
  "timestamp": "2210-05-15T10:00:00Z",
  "status": "registered"
}

「看起來沒問題啊,」洛基說,「所有的字串都是字串型別。」

大師提醒:「但你忽略了一個重要細節。在 DynamoDB 中,timestamp 應該是什麼型別?」

洛基恍然大悟:「等等...我應該檢查 DynamoDB 實際收到的資料型別!」

深入 DynamoDB 的型別系統

大師解釋:「DynamoDB 有嚴格的型別系統。讓我們來理解它如何解釋你的資料。」

洛基學著加入更詳細的除錯:

// 洛基學會檢查 DynamoDB 型別
async function debugDynamoDBTypes(userId, eventId, timestamp) {
  const AWS = require("aws-sdk");
  const converter = AWS.DynamoDB.Converter;

  const item = {
    PK: `USER#${userId}`,
    SK: `EVENT#${eventId}`,
    timestamp: timestamp,
    status: "registered",
  };

  console.log("=== JavaScript 物件 ===");
  console.log(JSON.stringify(item, null, 2));

  console.log("\n=== DynamoDB 格式 ===");
  const dynamoItem = converter.marshall(item);
  console.log(JSON.stringify(dynamoItem, null, 2));

  // 檢查每個屬性的 DynamoDB 型別
  Object.keys(dynamoItem).forEach((key) => {
    const attr = dynamoItem[key];
    const type = Object.keys(attr)[0];
    console.log(`${key}: ${type} = ${attr[type]}`);
  });
}

debugDynamoDBTypes(12345, "MUSIC_FESTIVAL", "2210-05-15T10:00:00Z");

執行結果:

=== JavaScript 物件 ===
{
  "PK": "USER#12345",
  "SK": "EVENT#MUSIC_FESTIVAL",
  "timestamp": "2210-05-15T10:00:00Z",
  "status": "registered"
}

=== DynamoDB 格式 ===
{
  "PK": {"S": "USER#12345"},
  "SK": {"S": "EVENT#MUSIC_FESTIVAL"},
  "timestamp": {"S": "2210-05-15T10:00:00Z"},
  "status": {"S": "registered"}
}

PK: S = USER#12345
SK: S = EVENT#MUSIC_FESTIVAL
timestamp: S = 2210-05-15T10:00:00Z
status: S = registered

「這看起來都正確啊,」洛基說,「所有的都是字串型別(S)。那為什麼還是報錯?」

大師引導他:「錯誤訊息提到『Number AttributeValue length is too big』。你有注意到任何數字相關的問題嗎?」

洛基重新審視:「等等...讓我嘗試用字串型別的 userId!」

// 修正版本
debugDynamoDBTypes("12345", "MUSIC_FESTIVAL", "2210-05-15T10:00:00Z");

「還是一樣!」洛基困惑。

發現真正的問題根源

大師提示:「讓我們回到最原始的錯誤。試試看真正執行 put 操作,但加入更詳細的錯誤處理。」

洛基修改程式:

async function detailedErrorHandling(userId, eventId, timestamp) {
  try {
    const params = {
      TableName: "IntergalacticEvents",
      Item: {
        PK: `USER#${userId}`,
        SK: `EVENT#${eventId}`,
        timestamp: timestamp,
        status: "registered",
      },
    };

    console.log("準備發送到 DynamoDB 的參數:");
    console.log(JSON.stringify(params, null, 2));

    await docClient.put(params).promise();
    console.log("成功!");
  } catch (error) {
    console.log("=== 詳細錯誤資訊 ===");
    console.log("錯誤名稱:", error.name);
    console.log("錯誤訊息:", error.message);
    console.log("錯誤代碼:", error.code);
    console.log("HTTP 狀態:", error.statusCode);
    console.log("完整錯誤:", error);
  }
}

detailedErrorHandling("12345", "MUSIC_FESTIVAL", "2210-05-15T10:00:00Z");

執行結果:

準備發送到 DynamoDB 的參數:
{
  "TableName": "IntergalacticEvents",
  "Item": {
    "PK": "USER#12345",
    "SK": "EVENT#MUSIC_FESTIVAL",
    "timestamp": "2210-05-15T10:00:00Z",
    "status": "registered"
  }
}

=== 詳細錯誤資訊 ===
錯誤名稱: ValidationException
錯誤訊息: One or more parameter values were invalid: Number AttributeValue length is too big
錯誤代碼: ValidationException
HTTP 狀態: 400

洛基還是困惑:「數據看起來完全正確,為什麼還是說數字太大?」

大師微笑:「讓我們嘗試一個實驗。用一個更簡單的測試案例。」

// 大師的實驗
async function simpleTest() {
  try {
    await docClient
      .put({
        TableName: "IntergalacticEvents",
        Item: {
          PK: "TEST",
          SK: "TEST",
          value: 12345,
        },
      })
      .promise();
    console.log("成功!");
  } catch (error) {
    console.log("錯誤:", error.message);
  }
}

simpleTest();

「成功!」洛基驚呼,「那問題到底在哪裡?」

發現真正的罪魁禍首

洛基嘗試檢查表格:

// 檢查表格是否存在
async function checkTable() {
  try {
    const result = await docClient
      .scan({
        TableName: "IntergalacticEvents",
        Limit: 1,
      })
      .promise();
    console.log("表格存在,範例資料:", result.Items[0]);
  } catch (error) {
    console.log("表格檢查錯誤:", error.message);
  }
}

checkTable();

執行結果:

表格檢查錯誤: Requested resource not found

洛基恍然大悟:「表格根本不存在!但為什麼錯誤訊息說的是 'Number AttributeValue length is too big' 而不是 'Table not found'?」

大師微笑:「這就是錯誤處理的第一課:錯誤訊息可能會誤導你。有時候真正的問題被其他問題掩蓋了。這就像醫生看病,頭痛不一定是頭的問題,可能是頸椎、眼睛,甚至是壓力造成的!」

洛基創建表格後再次測試:

// 創建表格後重試
async function retryAfterCreateTable() {
  // 假設表格已創建
  try {
    await docClient
      .put({
        TableName: "IntergalacticEvents",
        Item: {
          PK: "USER#12345",
          SK: "EVENT#MUSIC_FESTIVAL",
          timestamp: "2210-05-15T10:00:00Z",
          status: "registered",
        },
      })
      .promise();
    console.log("成功!原來問題真的是表格不存在!");
  } catch (error) {
    console.log("還是有錯誤:", error.message);
  }
}

「成功了!」洛基驚呼,「但這讓我很困惑,為什麼 DynamoDB 不直接告訴我表格不存在?」

理解錯誤訊息的藝術

大師解釋:「這涉及到錯誤處理的順序。DynamoDB 會先驗證請求格式,再檢查資源是否存在。如果格式驗證發現問題,就不會進行後續檢查。」

洛基學會了系統性的除錯方法:

async function systematicDebug(tableName, item) {
  console.log("=== 系統性除錯流程 ===");

  // 步驟 1:檢查基本連線
  try {
    await docClient.listTables().promise();
    console.log("✅ DynamoDB 連線正常");
  } catch (error) {
    console.log("❌ DynamoDB 連線失敗:", error.message);
    return;
  }

  // 步驟 2:檢查表格是否存在
  try {
    await docClient.describeTable({ TableName: tableName }).promise();
    console.log("✅ 表格存在");
  } catch (error) {
    if (error.code === "ResourceNotFoundException") {
      console.log("❌ 表格不存在:", tableName);
      return;
    }
    console.log("❌ 表格檢查失敗:", error.message);
    return;
  }

  // 步驟 3:驗證資料格式
  try {
    const AWS = require("aws-sdk");
    const marshalled = AWS.DynamoDB.Converter.marshall(item);
    console.log("✅ 資料格式正確");
    console.log("DynamoDB 格式:", JSON.stringify(marshalled, null, 2));
  } catch (error) {
    console.log("❌ 資料格式錯誤:", error.message);
    return;
  }

  // 步驟 4:嘗試實際操作
  try {
    await docClient
      .put({
        TableName: tableName,
        Item: item,
      })
      .promise();
    console.log("✅ 操作成功");
  } catch (error) {
    console.log("❌ 操作失敗:", error.message);
    console.log("錯誤詳情:", {
      name: error.name,
      code: error.code,
      statusCode: error.statusCode,
    });
  }
}

// 測試
systematicDebug("IntergalacticEvents", {
  PK: "USER#12345",
  SK: "EVENT#MUSIC_FESTIVAL",
  timestamp: "2210-05-15T10:00:00Z",
  status: "registered",
});

洛基遇到第二個錯誤

成功解決第一個問題後,洛基繼續測試  Hippo 給的題目,很快就遇到了新的錯誤:

async function updateEventStatus(userId, eventId, oldStatus, newStatus) {
  try {
    const params = {
      TableName: "IntergalacticEvents",
      Key: {
        PK: `USER#${userId}`,
        SK: `EVENT#${eventId}`,
      },
      UpdateExpression: "SET #status = :newStatus",
      ConditionExpression: "#status = :oldStatus",
      ExpressionAttributeNames: {
        "#status": "status",
      },
      ExpressionAttributeValues: {
        ":newStatus": newStatus,
        ":oldStatus": oldStatus,
      },
    };

    await docClient.update(params).promise();
    console.log("狀態更新成功!");
  } catch (error) {
    console.error("更新失敗:", error.message);
  }
}

// 測試:嘗試將狀態從 'registered' 改為 'confirmed'
updateEventStatus("12345", "MUSIC_FESTIVAL", "registered", "confirmed");

執行結果:

更新失敗:The conditional request failed

洛基困惑:「ConditionalCheckFailedException...這是什麼意思?」

大師引導他思考:「條件檢查失敗。你的條件是什麼?」

「檢查目前狀態是 'registered',」洛基說,「但失敗了,所以...」

「所以目前狀態不是 'registered'!」洛基恍然大悟,「讓我檢查一下實際的狀態。」

學會診斷條件檢查失敗

洛基開發了診斷工具:

// 診斷條件檢查失敗
async function diagnoseConditionalCheck(userId, eventId, expectedStatus) {
  console.log("=== 診斷條件檢查 ===");

  // 先讀取當前狀態
  try {
    const result = await docClient
      .get({
        TableName: "IntergalacticEvents",
        Key: {
          PK: `USER#${userId}`,
          SK: `EVENT#${eventId}`,
        },
      })
      .promise();

    if (!result.Item) {
      console.log("❌ 項目不存在");
      return;
    }

    console.log("目前項目:", result.Item);
    console.log("目前狀態:", result.Item.status);
    console.log("期望狀態:", expectedStatus);

    if (result.Item.status === expectedStatus) {
      console.log("✅ 狀態匹配,條件應該成功");
    } else {
      console.log("❌ 狀態不匹配,這就是條件失敗的原因");
    }
  } catch (error) {
    console.log("❌ 讀取失敗:", error.message);
  }
}

diagnoseConditionalCheck("12345", "MUSIC_FESTIVAL", "registered");

執行結果:

=== 診斷條件檢查 ===
目前項目: {
  PK: 'USER#12345',
  SK: 'EVENT#MUSIC_FESTIVAL',
  timestamp: '2210-05-15T10:00:00Z',
  status: 'pending'
}
目前狀態: pending
期望狀態: registered
❌ 狀態不匹配,這就是條件失敗的原因

「原來如此!」洛基說,「目前狀態是 'pending',不是 'registered'。但我明明記得設定為 'registered'...」

大師提醒:「也許之前的測試資料和你想的不一樣,或者有其他程式修改了狀態。這就是為什麼診斷很重要。」

遇到容量限制錯誤

洛基繼續測試,這次問題是模擬大量使用者同時報名的情況:

// 大量並發測試
async function massRegistration() {
  const promises = [];

  for (let i = 0; i < 100; i++) {
    promises.push(
      docClient
        .put({
          TableName: "IntergalacticEvents",
          Item: {
            PK: `USER#${i}`,
            SK: "EVENT#MUSIC_FESTIVAL",
            timestamp: new Date().toISOString(),
            status: "registered",
          },
        })
        .promise()
    );
  }

  try {
    await Promise.all(promises);
    console.log("100 個用戶報名成功!");
  } catch (error) {
    console.error("大量報名失敗:", error.message);
  }
}

massRegistration();

執行結果:

大量報名失敗:Request rate is too high

洛基問:「ProvisionedThroughputExceededException?這是什麼?」

大師解釋:「這是容量限制。你的表格設定了每秒能處理的請求數量,當超過這個限制時,DynamoDB 會拒絕額外的請求。」

洛基接著處理容量錯誤:

// 處理容量限制的智慧重試
async function intelligentMassRegistration() {
  const users = Array.from({ length: 100 }, (_, i) => i);
  let successCount = 0;
  let failCount = 0;

  console.log("開始智慧大量報名...");

  for (const userId of users) {
    let retryCount = 0;
    const maxRetries = 3;

    while (retryCount < maxRetries) {
      try {
        await docClient
          .put({
            TableName: "IntergalacticEvents",
            Item: {
              PK: `USER#${userId}`,
              SK: "EVENT#MUSIC_FESTIVAL",
              timestamp: new Date().toISOString(),
              status: "registered",
            },
          })
          .promise();

        successCount++;
        console.log(`✅ USER#${userId} 報名成功`);
        break; // 成功,跳出重試循環
      } catch (error) {
        if (error.code === "ProvisionedThroughputExceededException") {
          retryCount++;
          const delay = 100 * Math.pow(2, retryCount); // 指數退避
          console.log(`⏳ USER#${userId} 遇到容量限制,${delay}ms 後重試...`);
          await new Promise((resolve) => setTimeout(resolve, delay));
        } else {
          console.log(`❌ USER#${userId} 其他錯誤:${error.message}`);
          failCount++;
          break; // 非容量問題,不重試
        }
      }
    }

    if (retryCount === maxRetries) {
      console.log(`❌ USER#${userId} 重試次數用完,最終失敗`);
      failCount++;
    }
  }

  console.log(`\n=== 結果統計 ===`);
  console.log(`成功:${successCount}`);
  console.log(`失敗:${failCount}`);
}

intelligentMassRegistration();

學會錯誤分類與策略

經過多次實驗,洛基開始理解錯誤的模式:

// 洛基的錯誤分類器
function classifyError(error) {
  const errorCode = error.code || error.name;

  console.log(`=== 錯誤分析:${errorCode} ===`);

  switch (errorCode) {
    case "ValidationException":
      console.log("類型:參數驗證錯誤");
      console.log("原因:請求格式不正確");
      console.log("策略:檢查參數格式,修正後重試");
      console.log("是否可重試:否(需要修正)");
      break;

    case "ConditionalCheckFailedException":
      console.log("類型:條件檢查失敗");
      console.log("原因:資料狀態與期望不符");
      console.log("策略:重新讀取資料,確認當前狀態");
      console.log("是否可重試:視情況而定");
      break;

    case "ProvisionedThroughputExceededException":
      console.log("類型:容量限制");
      console.log("原因:請求速率超過表格容量");
      console.log("策略:指數退避重試");
      console.log("是否可重試:是");
      break;

    case "ResourceNotFoundException":
      console.log("類型:資源不存在");
      console.log("原因:表格或項目不存在");
      console.log("策略:確認資源名稱,必要時創建");
      console.log("是否可重試:否(需要修正)");
      break;

    case "InternalServerError":
      console.log("類型:服務內部錯誤");
      console.log("原因:AWS 內部問題");
      console.log("策略:重試");
      console.log("是否可重試:是");
      break;

    default:
      console.log("類型:未知錯誤");
      console.log("策略:詳細檢查錯誤訊息");
      console.log("是否可重試:謹慎重試");
  }

  console.log("完整錯誤:", error);
}

// 測試錯誤分類器
try {
  throw new Error("ConditionalCheckFailedException");
} catch (error) {
  error.code = "ConditionalCheckFailedException";
  classifyError(error);
}

從新手到專家的轉變

大師觀察洛基一天的變化,微笑道:「你知道新手和專家的最大差別是什麼嗎?」

洛基思考:「技術能力?」

「不,」大師說,「是對錯誤的態度。新手害怕錯誤、逃避錯誤;專家擁抱錯誤、學習錯誤。」

Hippo 補充:「今天早上你看到錯誤訊息時是什麼感覺?」

洛基回想:「沮喪、困惑、想放棄...」

「現在呢?」

洛基笑了:「好奇、興奮,想要解決它們!」

「這就是成長,」大師說,「你已經從被錯誤困擾,變成與錯誤共舞。」

大師看著洛基滿意的笑容,突然拿出一張圖表:「今天你學會了與錯誤對話。明天,我們要面對一個更根本的挑戰。」

洛基看著那張圖表,上面畫滿了實體與關聯線:「這是...ER 圖?」

「正是,」大師說,「是時候進入下一個階段,讓我們從實務的流程來看看 DynamoDB。」

Hippo:「看看明天菜鳥是不是真的好準備變成專家了!」


上一篇
Day 15:批次操作的效能藝術
下一篇
Day 17:關聯式思維的現實撞擊
系列文
DynamoDB銀河傳說首部曲-打造宇宙都打不倒的高效服務19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言