昨天我們對售票 API 進行了壓力測試,可以看到結果是慘不忍睹,RPS 只 27 ,今天我們要好好地來優化購票 API。
首先我們要先知道整個流程裡面到底是哪個操作導致 RPS 低下的,我們需要對購票 API 增加一些 Log 來看看是誰在搞鬼
try
{
// 取得使用者名稱
model.Username = user.Identity!.Name;
// 判斷活動是否存在
var checkEventStopWatch = new Stopwatch();
checkEventStopWatch.Start();
var eventObj = await redisService.HashGetAllAsync($"Event:{model.EventId}");
if (eventObj.Count == 0)
{
return Results.BadRequest("Event not found");
}
checkEventStopWatch.Stop();
app.Logger.LogInformation($"Check Event Elapsed: {checkEventStopWatch.ElapsedMilliseconds} ms");
// 判斷座位是否存在
var checkSeatStopWatch = new Stopwatch();
checkSeatStopWatch.Start();
var seats = JsonSerializer.Deserialize<List<Seat>>(eventObj["Seats"]);
if ((!seats?.Any(t => t.Id == model.SeatId)) ?? true)
{
return Results.BadRequest("Seat not found");
}
checkSeatStopWatch.Stop();
app.Logger.LogInformation($"Check Seat Elapsed: {checkSeatStopWatch.ElapsedMilliseconds} ms");
// 判斷座位是否已售出
var checkTicketStopWatch = new Stopwatch();
checkTicketStopWatch.Start();
var key = $"Ticket:{model.EventId}:{model.SeatId}";
if (await redisService.KeyExistsAsync(key))
{
return Results.BadRequest("Ticket already exists");
}
checkTicketStopWatch.Stop();
app.Logger.LogInformation($"Check Ticket Elapsed: {checkTicketStopWatch.ElapsedMilliseconds} ms");
// 儲存購票紀錄
var saveTicketStopWatch = new Stopwatch();
saveTicketStopWatch.Start();
var addSet = await redisService.SetAddAsync($"Seat:{model.EventId}", model.SeatId.ToString());
saveTicketStopWatch.Stop();
app.Logger.LogInformation($"Save Ticket Elapsed: {saveTicketStopWatch.ElapsedMilliseconds} ms");
// 購票成功在 Redis 暫存購票紀錄
if (addSet)
{
var addTicketStopWatch = new Stopwatch();
addTicketStopWatch.Start();
await redisService.HashSetAllAsync(key, new Dictionary<string, string>
{
{ "EventId", model.EventId.ToString() },
{ "SeatId", model.SeatId.ToString() },
{ "Username", model.Username! },
{ "CreateTime",model.CreateTime.ToString() }
});
await publisherService.Publish(JsonSerializer.Serialize(model));
addTicketStopWatch.Stop();
app.Logger.LogInformation($"Add Ticket Elapsed: {addTicketStopWatch.ElapsedMilliseconds} ms");
}
else
{
return Results.BadRequest("Seat already sold");
}
return Results.Created($"/api/ticket/{model.EventId}/{model.SeatId}", model);
}
catch (Exception ex)
{
return Results.Problem(
detail: ex.StackTrace,
statusCode: 500,
title: ex.Message);
}
我們針對有 每個主要流程加入 Log 再到 Cloud Logging 看看是誰在搞鬼
可以看到除了 Save Ticker 跟 Add Ticket 其他三個操作所需的時間都還有優化的空間。
先從效能最差的步驟開始優化,來看一下 判斷活動是否存在的程式碼片段
// 判斷活動是否存在
var checkEventStopWatch = new Stopwatch();
checkEventStopWatch.Start();
var eventObj = await redisService.HashGetAllAsync($"Event:{model.EventId}");
if (eventObj.Count == 0)
{
return Results.BadRequest("Event not found");
}
checkEventStopWatch.Stop();
可以看到我們這裡是直接嘗試去取出 Event 的物件,因為後續檢查座位時會需要相關的資訊,我們先忽略檢查座位需要的資訊,單純讓此段程式碼就只做檢查活動是否存在,下方檢查座位因為有用到 eventObj 可以先假定座位都沒問題先註解起來。
// 判斷活動是否存在
var checkEventStopWatch = new Stopwatch();
checkEventStopWatch.Start();
if (!await redisService.KeyExistsAsync($"Event:{model.EventId}"))
{
return Results.BadRequest("Event not found");
}
checkEventStopWatch.Stop();
app.Logger.LogInformation($"Check Event Elapsed: {checkEventStopWatch.ElapsedMilliseconds} ms");
接著用相同的條件 100 vus 壓 30 秒,可以看到所需的時間大幅縮小了
但是目前流程是沒有座位是否存在的檢查,我們需要加回去座位檢查,但又不希望每次都從 Redis 取出大量的資料來做來判斷,我們可以怎麼做呢?這裡我想到的方法是調整資料結構,原本 Redis 內 Seat Set 是存放已經被購買的票,可以反過來 Set 內存放所有位置資訊,不做 Add 而是使用 Remove,如果 Remove 失敗表示座位已經售出或是不存在,再判斷是已售出或不存在。
首先修改寫入 Redis 的部分,新增寫入全部座位的 Seat
var entry = await context.Event.Include(e => e.Seats).FirstOrDefaultAsync(e => e.Id == id);
if (entry == null)
{
return Results.NotFound();
}
//HashSetAllAsync
await redisService.HashSetAllAsync($"Event:{entry.Id}", new Dictionary<string, string>
{
{ "Name", entry.Name },
{ "EventDate", entry.EventDate.ToString() },
{ "StartSalesDate", entry.StartSalesDate.ToString() },
{ "EndSalesDate", entry.EndSalesDate.ToString() },
{ "Description", entry.Description },
{ "Remark", entry.Remark },
{ "Seats", JsonSerializer.Serialize(entry.Seats) }
});
await redisService.HashSetAsync($"Event:{entry.Id}", "Seats", JsonSerializer.Serialize(entry.Seats));
// 寫入全部座位
await redisService.SetAddAsync($"Seat:{entry.Id}", entry.Seats?.Select(t => t.Id.ToString()).ToList() ?? []);
return Results.Ok(entry);
再來調整購票 API 檢查座位的部分,改用 Remove 的方式,調整完畢的購票 API 如下
app.MapPost("/api/ticket", async (
TicketViewModel model,
[FromServices] RedisService redisService,
[FromServices] PublisherService publisherService,
ClaimsPrincipal user) =>
{
try
{
// 取得使用者名稱
model.Username = user.Identity!.Name;
// 判斷活動是否存在
var checkEventStopWatch = new Stopwatch();
checkEventStopWatch.Start();
if (!await redisService.KeyExistsAsync($"Event:{model.EventId}"))
{
return Results.BadRequest("Event not found");
}
checkEventStopWatch.Stop();
app.Logger.LogInformation($"Check Event Elapsed: {checkEventStopWatch.ElapsedMilliseconds} ms");
// 判斷座位是否存在
var checkSeatStopWatch = new Stopwatch();
checkSeatStopWatch.Start();
var key = $"Ticket:{model.EventId}:{model.SeatId}";
if (!await redisService.SetRemoveAsync($"Seat:{model.EventId}", model.SeatId.ToString()))
{
// 座位不存在 Set 判斷是否已售出
var checkTicketStopWatch = new Stopwatch();
checkTicketStopWatch.Start();
if (await redisService.KeyExistsAsync(key))
{
checkTicketStopWatch.Stop();
app.Logger.LogInformation($"Check Ticket Elapsed: {checkTicketStopWatch.ElapsedMilliseconds} ms");
return Results.BadRequest("Seat already sold");
}
else
{
checkSeatStopWatch.Stop();
app.Logger.LogInformation($"Check Seat Elapsed: {checkSeatStopWatch.ElapsedMilliseconds} ms");
return Results.BadRequest("Seat not found");
}
}
// 購票成功在 Redis 暫存購票紀錄
var addTicketStopWatch = new Stopwatch();
addTicketStopWatch.Start();
await redisService.HashSetAllAsync(key, new Dictionary<string, string>
{
{ "EventId", model.EventId.ToString() },
{ "SeatId", model.SeatId.ToString() },
{ "Username", model.Username! },
{ "CreateTime",model.CreateTime.ToString() }
});
await publisherService.Publish(JsonSerializer.Serialize(model));
addTicketStopWatch.Stop();
app.Logger.LogInformation($"Add Ticket Elapsed: {addTicketStopWatch.ElapsedMilliseconds} ms");
return Results.Created($"/api/ticket/{model.EventId}/{model.SeatId}", model);
}
catch (Exception ex)
{
return Results.Problem(
detail: ex.StackTrace,
statusCode: 500,
title: ex.Message);
}
})
.RequireAuthorization();
因為資料結構已經有所改變,要建立一個新的活動來做測試,可以用先前寫好的 k6 腳本再建立一個新的活動,再重新針對這個活動進行購票的測試
可以看到調整過後 一樣 100 vus 壓 30 秒 RPS 可以來到 接近 900。
再重新確認一次每個流程所花費的時間
除了推送 Message 給 Pub/Sub 其他流程大致上都是< 1ms 的處理時間,理論上 RPS 應該能到打到更高,我們將 vus 做一個調升,拉到 300 vus 試試
QPS 已經上升到 1338 這個應該是我個人電腦能夠壓到的極限,我同時在我的 MAC 進行壓測
兩個裝置合併大約是 2400 左右的 RPS,實際上能夠承載的 RPS 應該可以到更高,礙於我手邊並沒有其他裝置方便進行測試,壓力測試就先到這個階段。