Skip to main content

5 posts tagged with "AWS"

View All Tags

什么是BGP

· 4 min read

什么是BGP?

BGP(Border Gateway Protocol)是互聯網的路由大腦,負責告訴全球所有的路由器某個IP地址在哪裡,怎麼去。

當你在瀏覽器輸入google.com,你的ISP路由器查詢BGP路由表,找到去往google.com的最佳路徑,封包才能跨越數十甚至數百個自治系統(AS)到達Google的服務器。

BGP是唯一在互聯網規模下工作的協議,管理全球九十萬以上的路由前綴

AS自治系統

AS(Autonomous System)是BGP的基本單位,代表一個由單一機構管理的網路。

每個AS都有一個唯一的編號ASN,例如Google是ASN15196,AWS是ASN16509。 AS之間通過eBGP建立Peering,互相交換我能連結到哪些IP地址的路由資訊。

BGP路由機制

BGP的路由通知被叫做Update消息,每當有一個AS有新路由或者路由失效,就發送Update給所有的BGP鄰居。

關鍵機制:AS_PATH,每當一個AS宣告一個路由,它會把自己的ASN添加到AS_PATH中。這樣其他AS就知道這條路由經過了哪些AS,避免路由循環。

AS_PATH Prepend 就是利用這個機制:把自己的 ASN 重複追加幾次,讓路徑看起來更長,其他 AS 就會優先選更短的路徑(即 DX)

BGP路徑屬性

BGP 路徑屬性決定了當有多條路徑到達同一目的地時,路由器選哪條。理解這四個屬性是掌握混合雲路由控制的關鍵:

  • LOCAL_PREF:只在同一 AS 內有效。值越高,優先級越高。用來控制「本 AS 的流量從哪個出口出去」。
  • AS_PATH:路徑所經過的 AS 序列。越短越優先。跨 AS 可見,是影響對端選路的主要手段。
  • Community:可自定義的標籤。AWS 預定義了一些 Community 值,可以讓你告訴 AWS「這條路由請低優先/不要傳播」。

決策過程

當路由器收到多條去往同一目的地的路由時,按照嚴格的優先級順序逐條比較,找到第一個能分出勝負的屬性就停止。

生產中最常用的兩個手段:LOCAL_PREF 控制本端選路(出方向),AS_PATH Prepend 影響對端選路(入方向)。

iBGP和eBGP

eBGP(external BGP):不同 AS 之間的 BGP 會話。會自動把自己的 ASN 追加到 AS_PATH,修改 NEXT_HOP 為自己的地址,LOCAL_PREF 不會傳遞給對方。

iBGP(internal BGP):同一 AS 內部的 BGP 會話。不修改 AS_PATH,LOCAL_PREF 可以傳遞。但有個重要限制:iBGP 收到的路由不能再傳給其他 iBGP 鄰居(防環路),所以 AS 內部要麼全互聯,要麼用 Route Reflector 解決。

BGP在AWS中的應用

在 AWS 混合雲場景中,BGP 扮演三個角色:

  • 路由自動發現:機房路由器把機房網段(如 192.168.0.0/16)通過 BGP 告訴 AWS VGW,VGW 自動把這條路由注入 VPC Route Table,無需手動配置靜態路由。
  • 故障自動切換:DX 斷開時,BGP 會話超時,VGW 刪除從 DX 學到的路由,自動轉為使用從 VPN 學到的備份路由。
  • 路由策略控制:通過 LOCAL_PREF 和 AS_PATH Prepend 控制哪條路徑優先,實現精細的流量工程。

BGP為什麼是TCP連線

BGP工作在TCP 179端口,選擇TCP是因為BGP要保證路由更新消息一條不漏地傳遞,丟包就意味著路由表不一致,後果很嚴重。TCP 的可靠傳輸幫 BGP 省去了自己實現重傳的麻煩。這也意味著 BGP Peering 建立需要 TCP 三次握手,所以 Security Group 要放行雙向的 TCP 179。

AWS AS 7224

這是AWS為Direct Connect和VPN分配的公有ASN,代表了AWS這一側的網路身分。當你的機房路由器和 AWS VGW 建立 BGP 會話,對端 ASN 就填 7224。有趣的是,AWS 在不同服務裡有不同的 ASN:DX 和 VPN 用 7224,AWS Transit Gateway 可以自定義 ASN(預設 64512),而面向公網的 AWS IP 地址段則用 AS16509。

一條奇怪的CNAME記錄

· 3 min read

一條奇怪的CNAME記錄

在CloudFlare的DNS管理界面上,有一條奇怪的CNAME記錄:

Type: CNAME
Name:_222ec04618be45...
Content: _b8c270d13e0adf5a673...

另外一條則是由qingshiyuu.com指向cloudfront.net的CNAME記錄。

這條CNAME有什麼用呢?

CA與SSL證書

當你在瀏覽器地址欄看到qingshiyuu.com那把綠色鎖頭,你其實在信任一個承諾。

這個承諾是:有一個受信任的第三方替這個網站擔保,確認你現在連接的 qingshiyuu.com,確實是 qingshiyuu.com 的主人運營的,不是別人偽裝的。

這個第三方就是CA(Certificate Authority),AWS的ACM(AWS Certificate Manager)就是一個CA。

此時的問題是如果任何人都有資格申請任何域名的證書,我就能申請一張google.com的證書,然後冒充google.com來騙取用戶的信任。

所以,在CA頒布證書值錢,必須先問一個問題,你能證明你是這個域名的主人嗎?

驗證的邏輯非常簡單,只有能修改DNS記錄的人才能證明他是域名的主人。

道理很簡單——DNS 記錄只有登錄域名管理後台才能修改,這個後台只有域名的購買者才有訪問權限。所以如果你能在 DNS 裡加一條特定的記錄,就等於你在向全世界說:我能控制這個域名,這個後台在我手上。

ACM 用的就是這個邏輯:它給你一個只有它自己知道的隨機字符串,讓你把它加進 DNS。它去查,查到了,就知道你是域名的主人,然後才頒發證書。

這條記錄,就是在 Cloudflare 後台看到的那條奇怪的 CNAME。

申請的四個階段

階段一:申請證書

ACM生成一對全局唯一的隨機字符串,告訴我把其中一個字符串(_222ec04618be45...)加到DNS裡,然後它會去查這個記錄的值。

階段二:DNS驗證

我把這個字符串加到DNS裡,ACM去查這個記錄的值,查到了,就知道我能控制這個域名,然後才頒發證書。

階段三:ACM掃描

DNS查詢那條字符串。

返回的值和ACM生成的值一樣。

驗證通過,證書狀態從Pending變為Issued。

階段四:證書頒發。

可以在cloudfront中使用證書了。

總結

這整個過程展示了如何通過DNS記錄來驗證域名所有權,並最終獲得SSL證書。這不僅保護了用戶的數據安全,也增強了網站的可信度。

為什麼我的網站qingyuu刷新後後會出現403錯誤?

· 2 min read

起因

近日我在瀏覽我的個人網站qingshiyuu.com時候,發現每次刷新頁面都會出現403 Access Denied錯誤,在搜索了相關資料後,發現這是因為CloudFront的配置問題。

原因

這是我Docusaurus Build出來後的文件結構:

錯誤

docs/intro 這個路由對應的實際文件是 docs/intro/index.html,當我在首頁點擊intro鏈接之後,Docusaurus的React路由在前端接管了導航,沒有發送HTTP請求給CloudFront。

但是當我刷新頁面之後,頁面會發送實際的HTTP請求到CloudFront,請求的路徑是 /docs/intro,CloudFront會去S3桶裡尋找這個路徑對應的文件,但是S3桶裡沒有 /docs/intro 這個文件,只有 /docs/intro/index.html,所以CloudFront返回403 Access Denied錯誤。

在使用 OAC(Origin Access Control)私有訪問的情況下,S3 對於找不到的對象返回的是 403 Access Denied,而不是直觀的 404 Not Found。這是因為從 S3 的角度看,它無法區分「對象不存在」和「你沒權限訪問」——統一都用 403 來回應,避免洩露 bucket 裡的對象信息。

解決方案一

最簡單的方案是告訴 CloudFront:遇到 403/404 時,把 /index.html 返回給用戶,讓前端路由接管。

custom_error_response {
error_code = 403
response_code = 200
response_page_path = "/index.html"
}

custom_error_response {
error_code = 404
response_code = 200
response_page_path = "/index.html"
}

解決方案二

更好的做法是在請求到達S3之前,就把URL自動改寫為正確的格式。CloudFront Function可以在邊緣節點執行輕量級的JavaScript代碼,適合做這件事。

resource "aws_cloudfront_function" "rewrite_uri" {
name = "rewrite-uri"
runtime = "cloudfront-js-2.0"
publish = true
code = <<-EOT
function handler(event) {
var request = event.request;
var uri = request.uri;

if (uri.endsWith('/')) {
request.uri += 'index.html';
} else if (!uri.includes('.')) {
request.uri += '/index.html';
}

return request;
}
EOT
}

邏輯很簡單,如果請求的路徑是以 /docs/intro 結尾的話,就自動加上 /index.html,這樣就能正確地找到對應的文件了。

然後在cahce_behavior裡面把這個function掛上去:

function_association {
event_type = "viewer-request"
function_arn = aws_cloudfront_function.rewrite_uri.arn
}

配置完後,清除一下CloudFront的緩存,刷新頁面就不會再出現403錯誤了。

aws cloudfront create-invalidation \
--distribution-id YOUR_DISTRIBUTION_ID \
--paths "/*"

Lambda Snapshot機制:冷啟動黑科技

· 5 min read

Lambda Snapshot機制

目前的問題

Lambda冷啟動的延遲主要來自Init階段,下載程式碼、啟動Runtime、執行初始化邏輯。對Node.js/python來說可能只需要幾百毫秒,但是對於java來說,JVM需要啟動,類加載器需要跑、Spring Boot要掃描一堆Bean、反射、DI容器初始化。

一個典型的Spring Boot Lambda,Init Duration會達到5-10秒。用戶第一個請求等待7秒,這在API場景下完全不可以接受。

Snapstart的核心機制: Firecracker Snapshot/Restore

SnapStart的本質就是一句話:把Init從每次冷啟動做變成發版時候只做一次,之後從快照恢復。

具體流程:

  • 當你啟用SnapStart並且發布一個新的函數版本時候,Lambda會觸法快照流程:先運行函數的完整初始化階段。
  • 然後對已經初始化的執行環境拍攝一個不可變的、加密的Firecracker microVM快照,包含記憶體和磁碟的完整狀態,再將快照分快緩存以供快速取回。
  • 之後冷啟動就不需要再走Init了,Lambda直接從緩存的快照恢復新的執行環境,而不是從頭初始化,用一個更快的resume階段取代了原本最耗時的初始化階段。

快照的存儲機制

Lambda將快照存儲在S3中,分割成512KB的Chunk以優化取回延遲。由於S3的取回需要數百毫秒,Lambda使用兩層緩存加速:L2是專門構建的分布式緩存艦隊,每個AZ都有一份獨立副本,Chunk從L2取回的性能通常在個位數毫秒級別。

這意味著恢復一個快照並不是去S3拉一整個大文件,而是按需、分塊、多層緩存地載入。

CRaC 讓應用層也能配合快照

Firecracker層面的snapshot/restore解決了VM狀態保存和恢復,但應用層也有自己的問題:數據庫連線、打開的socket、隨機數種子、臨時憑證等,這些東西被放進快照之後,恢復出來可能就已經失效了。

這就是CRaC發揮作用的地方。

CRaC在Java應用生命週期引入了新的before-checkpoint和after-restore階段。與非協調式的checkpoint/restore不同,協調式的做法允許被恢復的Java應用根據執行環境的變化做出不同的反應。

它提供兩個核心hook:

  • beforeCheckpoint() --快照拍攝前調用,你在這裡關閉數據庫連線、釋放文件句柄、清除臨時狀態
  • afterRestore() --快照恢復時候調用,你在這裡重新建立連線、刷新憑證、重新播種隨機數生成器
public class MyHandler implements RequestHandler<Event, Response>, Resource {

private Connection dbConn;

public MyHandler() {
// 這段在 Init 階段執行,會被快照捕獲
Core.getGlobalContext().register(this);
dbConn = DriverManager.getConnection(DB_URL);
}

@Override
public void beforeCheckpoint(Context<? extends Resource> context) {
// 快照前:關閉連線(因為恢復後可能已過期)
dbConn.close();
}

@Override
public void afterRestore(Context<? extends Resource> context) {
// 恢復後:重新建立連線
dbConn = DriverManager.getConnection(DB_URL);
}
}

Spring Boot、Quarkus、Micronaut 等主流框架已經內建了 CRaC 支援,多數情況下開發者不需要手動改程式碼就能使用。

類加載時機對快照的影響 - Priming

在初始化未被執行的代碼路徑--比如透過依賴注入延遲加載的類--不會被包含在快照之中。

這意味著假設你有一個Lambda handler裡用了10個Service類,但只有3個在Init階段被觸及,剩下7個要等到第一次請求進來時才會被JVM類加載器載入。那些7個類的加載器就會落在快照恢復後的第一次請求上。

解法是Priming預熱:在beforeCheckpoint階段主動觸發延遲加載的類

@Override
public void beforeCheckpoint(Context<? extends Resource> context) {
// 主動觸發類加載和 JIT,讓它們被快照捕獲
new ObjectMapper().readValue("{\"test\":1}", Map.class);
// 甚至可以做一次模擬調用
handleRequest(dummyEvent, dummyContext);
}

唯一性陷阱

因為SnapStart 用同一份快照作為多個執行環境的初始狀態,所以在初始化階段生成的「唯一」內容被快照捕獲後,恢復到多個實例中就不再唯一了。 Amazon Web Services 典型翻車場景:

  • Init 時生成了一個 UUID 作為實例 ID → 所有從同一快照恢復的實例拿到相同的 UUID
  • Init 時用 SecureRandom 播了種 → 所有實例的隨機數序列完全一致,在加密場景下是致命漏洞
  • Init 時拿了一個 STS 臨時憑證 → 恢復時可能已經過期

解法:所有需要唯一性的內容,必須從 Init 階段移到 handler 中,或者放到 afterRestore() hook 裡生成。

發版時(一次性):
Code Deploy → Init(JVM 啟動、類加載、DI、連線建立)

CRaC beforeCheckpoint() → 關閉外部資源

Firecracker snapshot → 記憶體 + 磁碟狀態 → 加密 → 分成 512KB chunks

S3(持久層)→ L2 分布式緩存 → L1 Worker 本地緩存

每次冷啟動(毫秒級):
請求進來 → 無暖實例可用

從緩存拉取 chunks → 恢復 Firecracker microVM

CRaC afterRestore() → 重建連線、刷新憑證、重新播種

Handler 執行 → 回應請求

將Lambda放到VPC中,發生了什麼?

· 5 min read

為什麼把Lambda放進VPC後冷啟動會從毫秒級變成十幾秒?

為什麼Lambda函數需要走進VPC?

預設的情況下,Lambda函數運行在AWS託管的網絡空間中,可以存取公網的網際網路端點,但是無法直接存取你VPC內部的私有資源,例如RDS、ElastiCache等。如果你的Lambda函數需要訪問這些私有資源,就必須將它放入VPC中。

一旦你為Lambda設定中指定了VPC、子網與安全群組,AWS就會為這個函數建立Elastic Network Interface,讓它伸出一隻手伸進VPC。而這隻手就是問題的根源。

現在矛盾的點就是,Lambda追求毫秒級的啟動,但是建立ENI是一個VPC層級的控制平面操作,天生就非常慢。

舊時代(2019年前):每次呼叫都會卡ENI

原始流程

在2019年9月之前,VPC Lambda冷啟動流程如下:

  • 請求建立ENI:Lambda服務向EC2控制平台發出CreateNetworkInterface API請求,要求在指定子網中創建一個ENI。
  • 分配私有IP:從你指定的子網CIDR中取得一個可用的私有IP地址。
  • 附加安全組:從你指定的Security Group規則套用到這張ENI上。
  • Attach 到 Lambda MicroVM:ENI被連接到Firecracker MicroVM的虛擬網卡上。
  • 函數啟動:Runtime初始化,載入你的handler,開始處理請求。

Firecracker MicroVM是什麼?

Firecracker是AWS開源的一套輕量級虛擬機器監控器VMM,專門為serverless場景設計。它用Rust編寫,基於Linux KVM來建立和運行所謂的microVM。

為什麼需要它?

Lambda是多租戶環境,你的函數和其他人的函數跑在同一台實體服務器上。AWS需要在隔離性和啟動速度之間取得平衡:

  • 傳統VM(類似QEMU)隔離性強,但啟動要好幾秒,而且模擬了一堆Lambda不需要的硬體--USB、顯示卡、音效卡、PCI
  • 容器(類似Docker)啟動快,但容器共享宿主機的kernel,在多租戶環境中安全風險較高。

AWS最早採用傳統VM來跑Lambda,有安全性,但是效能和密度不好,於是開發了Firecra--結合了VM的硬體級隔離和容器的資源效率與快速啟動。

Firecracker精簡程度

Firecracker的每個microVM記憶體開銷低於5MiB,啟動時間最快只要125毫秒,單台伺服器每秒可建立最多150個microVM。之所以能這麼快,是因為完全不實作BIOS、PCI等傳統硬體模擬,只透過精簡的virtio介面和guest kernel溝通。

和ENI之間的關係

每次Lambda冷啟動,AWS會啟動一個Firecracker microVM來執行代碼,如果你的Lambda配置了VPC,這個microVM就需要一張ENI來接入你的VPC網路,在舊架構下,每啟動一個microVM就要即時建立一張ENI,這就是冷啟動卡住的原因。

架構

┌──────────────────────────────────────────────────────────┐
│ 你的 VPC ─ Subnet: 10.0.1.0/24 (可用 IP: 251) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ ENI-001 │ │ ENI-002 │ │ ENI-003 │ ... │
│ │ 10.0.1.5 │ │ 10.0.1.6 │ │ 10.0.1.7 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
└───────┼──────────────┼──────────────┼─────────────────────┘
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ Lambda #1 │ │ Lambda #2 │ │ Lambda #3 │
│ (MicroVM) │ │ (MicroVM) │ │ (MicroVM) │
└───────────┘ └───────────┘ └───────────┘

三大問題

IP地址耗盡

一個/24子網只有251個可用IP,扣掉AWS保留的五個,如果你有多個Lambda函數共用相同的子網,並發量大了之後,ENI佔滿IP後新的呼叫會收到ENIlimitException。

ENI數量上限

每個Region/Account預設的ENI上限大約在5000左右,當你有多個高併發的VPC Lambda,加上其他EC2實例也在消耗ENI,很容易達到上限。

安全組規則變化

有人修改了Lambda使用的SG,加了一條規則,Lambda下次冷啟動才會套用新規則,而已經運行的實例仍使用舊規則--導致行為不一致。

新時代:AWS Hyperplan架構

2019年9約,AWS宣佈了VPC Lambda的重大架構--引入Hyperplan ENI(也稱為VPC-to-VPC NAT)。

核心變化:ENI在函數建立/更新時預建

新架構下,ENI不再是呼叫時才建立,而是在你Create/Update Function的時候就預先建立好,這些ENI被稱為Hyperplan ENI,它們是共享的--同一個函數的多個併發實例可以共用一張ENI。

┌──────────────────────────────────────────────────────────┐
│ 你的 VPC ─ Subnet: 10.0.1.0/24 │
│ │
│ ┌──────────────────────┐ │
│ │ Hyperplane ENI │ ← 函數部署時就建好 │
│ │ 10.0.1.5 │ ← 多個實例共用 │
│ └──────────┬───────────┘ │
│ │ │
│ ┌────────┴─────────┐ │
│ │ VPC-to-VPC NAT │ ← AWS Hyperplane 服務 │
│ │ (Mapping Table) │ │
│ └────────┬─────────┘ │
│ │ │
└─────────────┼────────────────────────────────────────────┘

┌─────────┼─────────────────────┐
│ │ Lambda Service │
│ ┌─────▼─────┐ ┌───────────┐│
│ │ Lambda #1 │ │ Lambda #2 ││
│ │ (MicroVM) │ │ (MicroVM) ││ ... 可能上百個
│ └───────────┘ └───────────┘│
└───────────────────────────────┘

由於ENI已經存在,冷啟動時不需要走CreateNetworkInterface,Lambda實例通過Hyperplan的NAT mapping就能進入VPC,時間大幅度下降。

Hyperplan也不是完美的

部署時候ENI建立仍然需要時間

當你第一次部署VPC Lambda或更新其子網/安全群組設定時,AWS仍然需要建立Hyperplan ENI。這個過程需要時間,這個期間函數會處於Pending狀態。

需要足夠的IAM權限

Lambda執行角色需要EC2網路相關權限,如果你使用自定的IAM policy,確認包含以下權限:

{
"Effect": "Allow",
"Action": [
"ec2:CreateNetworkInterface",
"ec2:DescribeNetworkInterfaces",
"ec2:DeleteNetworkInterface",
"ec2:AssignPrivateIpAddresses",
"ec2:UnassignPrivateIpAddresses"
],
"Resource": "*"
}

最簡單的方式是附加AWS託管策略AWSLambdaVPCAccessExecutionRole,它包含了上述權限。

子網規劃仍然重要

雖然IP消耗大幅度降低,但是如果你子網仍然很少,加上其他服務也在使用,仍可能出現問題,建議至少使用/24子網。

跨AZ的高可用配置

設定Lambda VPC,應選擇至少兩個不同的AZ子網,如果某個AZ出問題,Lambda可以自動使用另一個AZ的ENI,確保可用性。

VPC Lambda要存取公網資源,需要搭配NAT Gateway(放在public subnet)Lambda自己所在的private subnet路由表要將0.0.0.0/0指向NAT Gateway,不然Lambda放進VPC就會出不去了。

參考文檔