作為程序員,我們不能只管上線,不管線上!
作為一名程序員,我們不能只關注代碼的實現(xiàn)和上線,而忽視了線上環(huán)境的運行和優(yōu)化。
近期遇到了兩個線上服務的問題,一個后端應用和一個前端項目,它們存在一些 bug 和歷史遺留問題。為了不影響用戶的使用體驗,決定對它們進行一次優(yōu)化。
后端服務
這個后端服務是年初的時候有同事離職了,交到了我這里,沒接手的時候不知道,沒想到接手后,到處都是問題,天天各種報警,基本上隔三差五就要重啟。
雖然一開始的時候知道這個服務不是很穩(wěn)定,日常會有一些隊列消息堆積,但是不在自己手上,不知道問題會這么多,動不動就堆積上億條消息,天天慢 SQL 和高負載報警。
平時工作日的時候收到報警不是很在意,順手重啟一下就算了,但是當每次周末或者出門在外的時候,收到報警心里還是蠻荒的。
抱著做一個問題的終結者的想法,最后還是準備花時間把這個服務做一下手術,從根本上解決問題。
效果
先說一下效果,這個服務從優(yōu)化過后,基本上除了迭代就再也沒有需要重新啟動過,更不存在隔三差五的重啟,現(xiàn)在每天的報警量從之前的一天幾百條變?yōu)?0,隊列無任何堆積。
優(yōu)化過程
優(yōu)化的過程中最難的是發(fā)現(xiàn)問題,只要能精準的找到問題所在,解決起來還是很容易的。
優(yōu)化主要分兩步:1. 解決慢 SQL;2. 解決堆積報警;
慢 SQL
解決慢 SQL 的思路很簡單,根據(jù)慢 SQL 日志,找到對應的慢 SQL 進行優(yōu)化即可。優(yōu)化可以從兩個方向來進行,一種是基于 SQL 本身來進行優(yōu)化,另一種是可以通過緩存來解決。這里需要根據(jù)具體的業(yè)務來選擇,如果不是經(jīng)常變動的數(shù)據(jù),則可以通過增加緩存來解決,剛好我這里就可以滿足。
經(jīng)過分析可以通過增加 Redis 緩存來解決這個問題,所以通過引入的 Redis 解決了慢 SQL 問題。
消息堆積
隊列消息堆積的處理方式無非也就是兩種,減少數(shù)據(jù)量,加快處理速度。
消息隊列里面的消息因為是上游發(fā)過來的,沒辦法從發(fā)送方進行減少,不過分析了一下消息類型,發(fā)現(xiàn)有很多消息的類型是完全不需要關心的,所以第一步增加消息過濾,將無用的消息直接提交掉。
另外之前遇到消息堆積的時候,觀察到消費消息的 TPS 特別低,有時候只有個位數(shù),完全不正常,而且每次重啟過后 TPS 可以達到幾千的級別,并且每次堆積的時候在日志層面都有一些“斷開連接” 的錯誤。
所以從日志層面分析,肯定是消費線程出了問題,導致消費能力下降從而堆積,從而問題就轉變?yōu)闉槭裁淳€程會出現(xiàn)異常。
仔細查了下應用層面的監(jiān)控,發(fā)現(xiàn)應用有頻繁的 FullGC 發(fā)生,奇怪的是為什么頻繁 FullGc 卻沒有觸發(fā)報警呢?看了一眼簡直要吐血,因為 FullGc 的報警開關被關了。。。
至此基本上能知道問題的原因了,因為發(fā)生了 FullGc 導致 STW,然后消費線程掛了,導致消息堆積,重啟后內(nèi)存釋放重新進行消費。接下來的問題就轉變?yōu)榕挪?nbsp;FullGc 的原因了。
排查 FullGc 的基本流程首先肯定是 dump 一下內(nèi)存的 heap ,然后分析一下內(nèi)存泄露的代碼塊。通過 dump 下來的日志,發(fā)現(xiàn)在代碼中使用 ThreadLocal,但是沒有釋放,從而導致頻次的 FullGc。問題到這基本上也解決了,修改了相關的地方,重新上線,穩(wěn)定運行。
至此沒有堆積,沒有報警,沒有重啟,爽歪歪!
總結
敬畏線上,不要放過任何一個線上的異常和報警!
有時候問題的表象并不是真正的原因,我們需要精準的找到根源,解決問題最難的地方是找到問題!
別干隨便關閉線上監(jiān)控報警的事情!
另外之所以能快速的定位到問題所在也是因為系統(tǒng)有著很好的異常監(jiān)控,可以監(jiān)測到慢 SQL 和堆積報警,這也告訴我們平時的服務監(jiān)控是很重要的。
前端項目
之前有個內(nèi)部服務,在部署服務的時候,nginx 配置了 http 和 https 兩個 server,公司內(nèi)部使用的時候一直都用的是 https,結果今天運營同事突然說訪問不了了,通過觀察發(fā)現(xiàn)是 http 協(xié)議訪問不通。
正常的邏輯是如果用戶在地址欄直接輸入 xxx.com 的時候默認是走的 http 協(xié)議 80 端口,在 nginx 層會轉發(fā)到 https 的 443 端口,也就是會有一個重定向的過程。
檢查了一下 nginx 的配置文件,發(fā)現(xiàn)在 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;
# 實現(xiàn)前端打字效果
proxy_buffering off;
}
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
頁面加載時間優(yōu)化
另外在使用的時候還發(fā)現(xiàn),有的時候網(wǎng)頁或者手機打開網(wǎng)站需要好幾秒才能把整個頁面渲染出來,自己用起來都很不爽更別說什么用戶體驗了。
通過瀏覽器的 network 欄目,發(fā)現(xiàn)網(wǎng)站在加載的時候會聯(lián)網(wǎng)訪問一個 css 文件,這個 css 文件里面會用到很多字體文件,而且這些字體文件也是從網(wǎng)絡實時下載的。
看了下 Issue 發(fā)現(xiàn)也有其他人遇到了這個問題,這個更夸張直接加載了 42 秒。
圖片
通過將這個問題提交下載下來,然后直接訪問,不再從網(wǎng)絡上下載。手動將這個 css 文件下載下來過后,發(fā)現(xiàn)里面還引用的很多字體文件,如下所示,總共 388 個,這樣是手動一個個下載那不是要了老命。
圖片
所以需要通過腳本來進行下載,通過詢問 ChatGPT 讓它幫我們寫一個 go 語言腳本來執(zhí)行這個邏輯。
圖片
完整的代碼如下所示
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()
// 創(chuàng)建字體存儲目錄
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()
// 創(chuàng)建字體文件
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 文件
- 逐行讀取文件內(nèi)容
- 對每一行使用正則表達式進行匹配,查找字體文件鏈接
- 使用 http.Get() 發(fā)送 HTTP 請求下載字體文件
- 創(chuàng)建本地文件,并將字體數(shù)據(jù)寫入該文件
圖片
上面代碼通過 go run download.go 直接運行腳本發(fā)現(xiàn)是可以正常運行的,但是一開始是有個問題的那就是沒有考慮到多個文件會覆蓋,我們簡單修改幾行就可以正常使用了。
index := strings.LastIndex(matches[1], "/")
filename := matches[1][index+1:]
// 創(chuàng)建字體文件
fontPath := fmt.Sprintf("%s/%s", fontDir, filename)
運行后的效果是這樣的,全部下載下來,我們需要做的就是在 css 文件中通過快捷鍵全部替換一下就好了。
圖片
優(yōu)化過后文件的下載速度穩(wěn)定了一秒以內(nèi),雖然還可以通過 CDN 等方式進一步優(yōu)化,但是感覺目前是沒必要的?,F(xiàn)在剩下的就是受限于服務器的寬帶和網(wǎng)絡了,不過整體是可以接受的了。
圖片
試了下移動端打開的速度也有所提升。
總結
通過上面的過程,可以看到 ChatGPT 是真的可以幫我們提高工作效率的,寫一個腳本沒什么難度,花點時間也是可以寫出來的,但是有了這樣的工具大大的節(jié)省了我們的時間,對于生成的內(nèi)容需要能看懂和能進行修改就行了。
但是工具也只是工具,還是要學會使用才行,不能太盲目的依賴。