头图

作为一名程序员,我们不能只关注代码的实现和上线,而忽视了线上环境的运行和优化。

近期遇到了两个线上服务的问题,一个后端应用和一个前端项目,它们存在一些 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 配置了 httphttps 两个 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, "//") {
                    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 不仅给出了代码,还给出了解释

此脚本遵循以下步骤:

  1. 打开 CSS 文件
  2. 逐行读取文件内容
  3. 对每一行使用正则表达式进行匹配,查找字体文件链接
  4. 使用 http.Get() 发送 HTTP 请求下载字体文件
  5. 创建本地文件,并将字体数据写入该文件

上面代码通过 go run download.go 直接运行脚本发现是可以正常运行的,但是一开始是有个问题的那就是没有考虑到多个文件会覆盖,我们简单修改几行就可以正常使用了。

index := strings.LastIndex(matches[1], "/")
filename := matches[1][index+1:]
// 创建字体文件
fontPath := fmt.Sprintf("%s/%s", fontDir, filename)

运行后的效果是这样的,全部下载下来,我们需要做的就是在 css 文件中通过快捷键全部替换一下就好了。

优化过后文件的下载速度稳定了一秒以内,虽然还可以通过 CDN 等方式进一步优化,但是感觉目前是没必要的。现在剩下的就是受限于服务器的宽带和网络了,不过整体是可以接受的了。

试了下移动端打开的速度也有所提升。

总结

通过上面的过程,可以看到 ChatGPT 是真的可以帮我们提高工作效率的,写一个脚本没什么难度,花点时间也是可以写出来的,但是有了这样的工具大大的节省了我们的时间,对于生成的内容需要能看懂和能进行修改就行了。

但是工具也只是工具,还是要学会使用才行,不能太盲目的依赖。


Java极客技术
61 声望16 粉丝