作為程序員,我們不能只管上線,不管線上!
作為一名程序員,我們不能只關注代碼的實現和上線,而忽視了線上環境的運行和優化。
近期遇到了兩個線上服務的問題,一個后端應用和一個前端項目,它們存在一些 bug 和歷史遺留問題。為了不影響用戶的使用體驗,決定對它們進行一次優化。
后端服務
這個后端服務是年初的時候有同事離職了,交到了我這里,沒接手的時候不知道,沒想到接手后,到處都是問題,天天各種報警,基本上隔三差五就要重啟。
雖然一開始的時候知道這個服務不是很穩定,日常會有一些隊列消息堆積,但是不在自己手上,不知道問題會這么多,動不動就堆積上億條消息,天天慢 SQL 和高負載報警。
平時工作日的時候收到報警不是很在意,順手重啟一下就算了,但是當每次周末或者出門在外的時候,收到報警心里還是蠻荒的。
抱著做一個問題的終結者的想法,最后還是準備花時間把這個服務做一下手術,從根本上解決問題。
效果
先說一下效果,這個服務從優化過后,基本上除了迭代就再也沒有需要重新啟動過,更不存在隔三差五的重啟,現在每天的報警量從之前的一天幾百條變為 0,隊列無任何堆積。
優化過程
優化的過程中最難的是發現問題,只要能精準的找到問題所在,解決起來還是很容易的。
優化主要分兩步:1. 解決慢 SQL;2. 解決堆積報警;
慢 SQL
解決慢 SQL 的思路很簡單,根據慢 SQL 日志,找到對應的慢 SQL 進行優化即可。優化可以從兩個方向來進行,一種是基于 SQL 本身來進行優化,另一種是可以通過緩存來解決。這里需要根據具體的業務來選擇,如果不是經常變動的數據,則可以通過增加緩存來解決,剛好我這里就可以滿足。
經過分析可以通過增加 Redis 緩存來解決這個問題,所以通過引入的 Redis 解決了慢 SQL 問題。
消息堆積
隊列消息堆積的處理方式無非也就是兩種,減少數據量,加快處理速度。
消息隊列里面的消息因為是上游發過來的,沒辦法從發送方進行減少,不過分析了一下消息類型,發現有很多消息的類型是完全不需要關心的,所以第一步增加消息過濾,將無用的消息直接提交掉。
另外之前遇到消息堆積的時候,觀察到消費消息的 TPS 特別低,有時候只有個位數,完全不正常,而且每次重啟過后 TPS 可以達到幾千的級別,并且每次堆積的時候在日志層面都有一些“斷開連接” 的錯誤。
所以從日志層面分析,肯定是消費線程出了問題,導致消費能力下降從而堆積,從而問題就轉變為為什么線程會出現異常。
仔細查了下應用層面的監控,發現應用有頻繁的 FullGC 發生,奇怪的是為什么頻繁 FullGc 卻沒有觸發報警呢?看了一眼簡直要吐血,因為 FullGc 的報警開關被關了。。。
至此基本上能知道問題的原因了,因為發生了 FullGc 導致 STW,然后消費線程掛了,導致消息堆積,重啟后內存釋放重新進行消費。接下來的問題就轉變為排查 FullGc 的原因了。
排查 FullGc 的基本流程首先肯定是 dump 一下內存的 heap ,然后分析一下內存泄露的代碼塊。通過 dump 下來的日志,發現在代碼中使用 ThreadLocal,但是沒有釋放,從而導致頻次的 FullGc。問題到這基本上也解決了,修改了相關的地方,重新上線,穩定運行。
至此沒有堆積,沒有報警,沒有重啟,爽歪歪!
總結
敬畏線上,不要放過任何一個線上的異常和報警!
有時候問題的表象并不是真正的原因,我們需要精準的找到根源,解決問題最難的地方是找到問題!
別干隨便關閉線上監控報警的事情!
另外之所以能快速的定位到問題所在也是因為系統有著很好的異常監控,可以監測到慢 SQL 和堆積報警,這也告訴我們平時的服務監控是很重要的。
前端項目
之前有個內部服務,在部署服務的時候,nginx 配置了 http 和 https 兩個 server,公司內部使用的時候一直都用的是 https,結果今天運營同事突然說訪問不了了,通過觀察發現是 http 協議訪問不通。
正常的邏輯是如果用戶在地址欄直接輸入 xxx.com 的時候默認是走的 http 協議 80 端口,在 nginx 層會轉發到 https 的 443 端口,也就是會有一個重定向的過程。
檢查了一下 nginx 的配置文件,發現在 80 這個 server 里面沒有配置 server_name,修改如下就好了。
只能說太粗心了
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name xxx.com www.xxx.com;
root /usr/share/nginx/html;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
server_name xxx.com www.xxx.com;
root /usr/share/nginx/html;
ssl_certificate "xxx.crt";
ssl_certificate_key "xxx.key";
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;
ssl_ciphers PROFILE=SYSTEM;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://backend$request_uri;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
client_max_body_size 10m;
# 實現前端打字效果
proxy_buffering off;
}
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
頁面加載時間優化
另外在使用的時候還發現,有的時候網頁或者手機打開網站需要好幾秒才能把整個頁面渲染出來,自己用起來都很不爽更別說什么用戶體驗了。
通過瀏覽器的 network 欄目,發現網站在加載的時候會聯網訪問一個 css 文件,這個 css 文件里面會用到很多字體文件,而且這些字體文件也是從網絡實時下載的。
看了下 Issue 發現也有其他人遇到了這個問題,這個更夸張直接加載了 42 秒。
圖片
通過將這個問題提交下載下來,然后直接訪問,不再從網絡上下載。手動將這個 css 文件下載下來過后,發現里面還引用的很多字體文件,如下所示,總共 388 個,這樣是手動一個個下載那不是要了老命。
圖片
所以需要通過腳本來進行下載,通過詢問 ChatGPT 讓它幫我們寫一個 go 語言腳本來執行這個邏輯。
圖片
完整的代碼如下所示
package main
import (
"bufio"
"fmt"
"net/http"
"os"
"regexp"
"strings"
)
func main() {
const cssPath = "css2.css"
const fontDir = "fonts"
const urlPrefix = "https:"
// 讀取 CSS 文件
cssFile, err := os.Open(cssPath)
if err != nil {
panic(fmt.Sprintf("Failed to open %s: %s", cssPath, err))
}
defer cssFile.Close()
// 創建字體存儲目錄
if err := os.MkdirAll(fontDir, 0755); err != nil {
panic(fmt.Sprintf("Failed to create font directory: %s", err))
}
// 解析 CSS 文件
scanner := bufio.NewScanner(cssFile)
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "url(") && strings.Contains(line, ".woff2") {
// 使用正則表達式提取 WOFF2 文件 URL
re := regexp.MustCompile(`url\(["']?([^"']+\.(woff2))`)
matches := re.FindStringSubmatch(line)
if len(matches) >= 2 {
fontUrl := matches[1]
if strings.HasPrefix(fontUrl, "http://") {
fontUrl = urlPrefix + fontUrl
}
// 下載 WOFF2 文件
fmt.Printf("Downloading %s...\n", fontUrl)
res, err := http.Get(fontUrl)
if err != nil {
fmt.Printf("Failed to download %s: %s\n", fontUrl, err)
continue
}
defer res.Body.Close()
// 創建字體文件
fontPath := fmt.Sprintf("%s/%s", fontDir, matches2)
fontFile, err := os.Create(fontPath)
if err != nil {
fmt.Printf("Failed to create font file %s: %s\n", fontPath, err)
continue
}
defer fontFile.Close()
// 寫入字體文件
_, err = fontFile.ReadFrom(res.Body)
if err != nil {
fmt.Printf("Failed to write to font file %s: %s\n", fontPath, err)
} else {
fmt.Printf("Font file %s downloaded.\n", fontPath)
}
}
}
}
}
ChatGPT 不僅給出了代碼,還給出了解釋
此腳本遵循以下步驟:
- 打開 CSS 文件
- 逐行讀取文件內容
- 對每一行使用正則表達式進行匹配,查找字體文件鏈接
- 使用 http.Get() 發送 HTTP 請求下載字體文件
- 創建本地文件,并將字體數據寫入該文件
圖片
上面代碼通過 go run download.go 直接運行腳本發現是可以正常運行的,但是一開始是有個問題的那就是沒有考慮到多個文件會覆蓋,我們簡單修改幾行就可以正常使用了。
index := strings.LastIndex(matches[1], "/")
filename := matches[1][index+1:]
// 創建字體文件
fontPath := fmt.Sprintf("%s/%s", fontDir, filename)
運行后的效果是這樣的,全部下載下來,我們需要做的就是在 css 文件中通過快捷鍵全部替換一下就好了。
圖片
優化過后文件的下載速度穩定了一秒以內,雖然還可以通過 CDN 等方式進一步優化,但是感覺目前是沒必要的。現在剩下的就是受限于服務器的寬帶和網絡了,不過整體是可以接受的了。
圖片
試了下移動端打開的速度也有所提升。
總結
通過上面的過程,可以看到 ChatGPT 是真的可以幫我們提高工作效率的,寫一個腳本沒什么難度,花點時間也是可以寫出來的,但是有了這樣的工具大大的節省了我們的時間,對于生成的內容需要能看懂和能進行修改就行了。
但是工具也只是工具,還是要學會使用才行,不能太盲目的依賴。