---
# System prepended metadata

title: SLA and Load Testing
tags: [Load Testing, 分散式系統, essay, SLA, SLO]

---

{%hackmd SybccZ6XD %}
<style>
.toc > ul > li:first-child {
  list-style: none;
}
.toc > ul > li:first-child > a {
  display: none;
}
</style>

SLA and Load Testing
===

###### tags: `essay` `分散式系統` `SLA` `SLO` `Load Testing`

> [name=Shueh Chou Lu][time=June, 2021]

[TOC]

---

## 說明

產品都需要向使用者展示部分的**承諾**，例如：

- 要求在 100ms 內回應（Latency）
- 1 年當中僅有 1 小時以內的時間服務可能無法使用
- 任何變動都會保證一年內的向後相容

這類保證，稱為 SLA（Service Level Agreement）。

> SLA 通常由多個 SLO（Service Level Objective）組成，例如：
> 一份 SLA 提供 `快速且安全的支付金錢`，其中的 `保證不會重複扣款`即為 SLO

> SLI（Service Level Indicator）即是 SLO 的指標，例如
> 目標在 100ms 內回應，實際測試為平均於 80ms 內回應

好的服務 `SLO / SLI` 需大於等於 `1`。

### 分散式系統中的意義

若要求在一個服務有特定的 SLO，就必須同時計算該服務中所有會使用的子服務的 SLO。

```mermaid
graph TD
  0[user]
  subgraph Application
    1[service]
    2[api1]
    3[api2]
  end
  0 --> 1
  1 -- 100ms --> 0
  1 --> 2
  2 -- 30ms --> 1
  1 --> 3
  3 -- 20ms --> 1
```

> 有時候，產品過於複雜沒辦法產出完整的流程圖，事實的簡化或增加 Tracing 都可以幫助產出。

----

## Load Test

Load Test 目的即是計算出 SLI。

> Load Test 和 Stress Test 差在哪裡？

Latency 代表從`事件觸發`到`事件回應`的時間差，此處並不包含錯誤回應的狀況。

> 以下範例，都將以 Latency 為 SLO，並計算之。

---

## Baseline

測試重要的一點是要有一個基準點，一個產品可以有多種 Baseline

例如：

- 在不做任何外部請求之下的 Latency
- 使用的框架所限制的 Latency

> 不同的 Baseline 會有自己的意義，根據需求制定出理想的 Baseline。

### 範例

以 Node.js 這語言所能做出最單純的 server 為 Baseline：

```javascript=
require("http")
  .createServer((req, res) => res.end('ok'))
  .listen(80, () => null);
```

依此觀察出，在統一機器規格下任何要求在 Node.js 這語言有超越其 Latency 都是沒意義的。

> 在追求更好的 Latency 時，或許該考慮其他語言，如 C++ 或 Rust。
> 但此時便需要權衡其他考量，如：會使用該語言的人數，社群發展程度等等。

### Latency

| Stat    | 2.5% | 50% | 97.5% | 99% | Avg    | Stdev  | Max    |
| ------- | ---- | --- | ----- | --- | ------ | ------ | ------ |
| Latency | 0ms  | 0ms | 0ms   | 0ms | 0.01ms | 0.08ms | 9.45ms |
| Req/Sec | 42,751 | 39,039 | 36,703 | 29,487 | 38,884.14 | 1,748.17 | 29,477 |

![](https://i.imgur.com/sDcMyqC.png)

其中的 `29487 個每秒請求量`即是 TP99（Top Percentile）下的基準點。

> 有時會認為 1% 是極端值，應該忽略。然而在網路世界中，一個使用者常常會需要針對一個網頁做出很多請求。若以一個頁面需要 40 個資源來計算，在跑第五個頁面之後，有近乎 0.003 % 的機率使用者 **不會** 觸發到 95% 的狀況。
> [How NOT to Measure Latency](https://www.youtube.com/watch?v=lJ8ydIuPFeU)


## 使用 Reverse Proxy - HAProxy

| Percentile | With Proxy | Without |
| ---------- | ---------- |-------- |
| 99.9%      | 1ms        | 1ms     |
| 99.99%     | 2ms        | 2ms     |
| 99.999%    | 5ms        | 3ms     |

得到 `19967 個每秒請求量`，相比於基準點 `29487`，看得出在最單純的應用程式下增加 r-proxy 會讓應用程式變慢。

但若考慮真正的應用程式，假如回應時間為 100ms，使用 r-proxy 雖會增加回應時間，卻僅僅增加 1~2ms，整體效益還是大於其消耗的效能。

## 若考慮 HTTP Compression

上述例子僅考慮最基礎的框架效能，若為了壓縮網路流量

- 套用 compression，對於效能會有什麼影響？
- 再加上 r-prxoy 又會有什麼影響？

### Latency

| Percentile | With Proxy | Without |
| ---------- | ---------- |-------- |
| 99%        | 47ms       | 53ms    |
| 99.9%      | 50ms       | 57ms    |
| 99.99%     | 52ms       | 62ms    |
| 99.999%    | 53ms       | 64ms    |


## Protocol

上一份報告討論各種服務間的溝通方式：

- JSON over HTTP
- GraphQL
- gRPC


究竟哪一項是真正有效率的？

```mermaid
graph LR
  subgraph VPS server
    2[recipe-api]
  end
  subgraph VPS client
    0[load test] -- HTTP --> 1[web-api]
    1 -- protocol --> 2
  end
```

### 結果

| Percentile | JSON | GraphQL | gRPC |
| ---------- | ---- | ------- | ---- |
| 99%        | 10ms | 13ms    | 24ms |
| 99.9%      | 18ms | 22ms    | 32ms |
| 99.99%     | 26ms | 36ms    | 82ms |
| 99.999%    | 48ms | 67ms    | 82ms |

### 討論

我們知道 GraphQL 的價值在於可以在一個 request 中取得所有訊息，且不需要針對每個場景對外開出一個 endpoint。

有時為了追求開發效率，而會捨棄部分產品效率，這時便要權衡產品的特性較偏向於哪邊。

除此之外 JSON 的解析在 v8 engine 中，效率已經被極致的壓縮了，所以相對而言，利用 Buffer 做 binary 解析的 gRPC 在效能上就矮了一截。由於其特性，讓他在 C++ 這類編譯過的程式碼中有較高的效能，而不額外處理 GC 這類事件。

## 結論

1. SLA 是面向很多的指標，有時必須權衡
2. 正確的評估符合自己的指標，例如：自動填寫 vs 銀行開戶
3. 在做 load testing 時，需要注意產品可能的流量高低峰
4. 要盡量減少雜音（noisy neighbor），盡量在類似線上的環境中測試
5. 一個產品究竟該開幾個（多少 CPU/Memory）機器來滿足線上流量？

### 方向

1. 觀察線上環境現有的流量高峰，並制定出符合商務邏輯的 Latency（SLO）。
2. 制定單位：一個 Docker container、一台機器、EC2
3. 找出在滿足 TP99 下的請求數
4. 增加單位，反覆測試直到達到能容納線上環境的數量

#### 範例

若希望產品能在 50ms 內回應請求，並得出線上環境高峰約每秒 20 （或每分 1200 或每時 72000）個請求。

啟用一個 Docker container 時，如要在 TP99 內回應 50ms 以下，僅能接受每秒 6 個請求。

再增加一個 Docker container，此時能接受每秒 10 個，依序往下加。
最終得到 4 個 container 為合理數量。

### 工具

上述所有測試，皆是使用 [autocannon](https://github.com/mcollina/autocannon) 這軟體測試，相關代碼模式皆為：

```bash=
$ autocannon -d 60 -c 10 -l http://localhost:3000
```

> 其意義在於：建立 10 個連線（`-c`），並持續（`-d`）60 秒，並展示詳盡的結果（`-l`），預設每個連線每秒打 1 次請求（`-p`）

對於該使用何種工具，其實不無特別要求，但是對於如何解讀結果，仍推薦 [How NOT to Measure Latency](https://youtu.be/lJ8ydIuPFeU?t=2042) 這部影片

若是多項服務合計的 latency（或甚至全公司），這時使用同一種工具就顯得重要了。

### 微實作

[auth](https://github.com/104corp/vip3-auth/tree/master/stress-test/2021-05)