Json 序列化框架導致 CPU 使用率過高

来源:https://www.cnblogs.com/wxweven/archive/2022/05/20/16294084.html
-Advertisement-
Play Games

知識回顧 上一篇介紹了Spring中三級緩存的singletonObjects、earlySingletonObjects、singletonFactories,Spring在處理迴圈依賴時在實例化後屬性填充前將一個lambda表達式放在了三級緩存中,後續在獲取時進行了判斷,如果不需要進行對象代理, ...


問題現象:CPU 負載過高

我們線上的 jenkins 系統,時不時會發生 CPU 負載過高的現象。

CPU 負載過高後,SRE 同學會收到電話告警。

在我們的監控系統中,可以看到,某些時候,CPU 的負載確實會很高,如下圖:
file

問題排查

Jenkins 系統本身是一個 Java 程式,應對 Java 程式導致的 CPU 使用率過高這一問題,GitHub 上有現成的解決方案:show-busy-java-threads。

下載鏈接如下:

登錄上機器,在 CPU 使用率高時候,執行 show-busy-java-threads 腳本:./show-busy-java-threads

摘選其中的一些輸出如下:

The stack of busy(25.0%) thread(20239/0x4f0f) of java process(248927) of user(jenkins):
"Handling GET /job/jenkins-test-job/api/json from 172.168.1.1 : qtp1641808846-3127" #3127 prio=5 os_prio=0 tid=0x00007f7380014000 nid=0x4f0f runnable [0x00007f722c392000]
   java.lang.Thread.State: RUNNABLE
        at java.util.Arrays.copyOfRange(Arrays.java:3664)
        at java.lang.String.<init>(String.java:207)
        at java.lang.String.substring(String.java:1933)
        at net.sf.json.util.JSONTokener.matches(JSONTokener.java:110)
        at net.sf.json.JSONObject._fromJSONTokener(JSONObject.java:912)
        at net.sf.json.JSONObject.fromObject(JSONObject.java:156)
        at net.sf.json.util.JSONTokener.nextValue(JSONTokener.java:348)
        at net.sf.json.JSONArray._fromJSONTokener(JSONArray.java:1131)
        at net.sf.json.JSONArray.fromObject(JSONArray.java:125)
        at net.sf.json.util.JSONTokener.nextValue(JSONTokener.java:351)
        at net.sf.json.JSONObject._fromJSONTokener(JSONObject.java:955)
        at net.sf.json.JSONObject.fromObject(JSONObject.java:156)
        at net.sf.json.util.JSONTokener.nextValue(JSONTokener.java:348)
        at net.sf.json.JSONObject._fromJSONTokener(JSONObject.java:955)
        at net.sf.json.JSONObject.fromObject(JSONObject.java:156)
        at net.sf.json.util.JSONTokener.nextValue(JSONTokener.java:348)
        at net.sf.json.JSONObject._fromJSONTokener(JSONObject.java:955)
        at net.sf.json.JSONObject.fromObject(JSONObject.java:156)
        at net.sf.json.util.JSONTokener.nextValue(JSONTokener.java:348)
        at net.sf.json.JSONObject._fromJSONTokener(JSONObject.java:955)
        at net.sf.json.JSONObject._fromString(JSONObject.java:1145)
        at net.sf.json.JSONObject.fromObject(JSONObject.java:162)
        at net.sf.json.JSONObject.fromObject(JSONObject.java:132)
        at sam.Sam.sendRequestReturnJson(Sam.java:517)
        at sam.Sam.getPermissionByUser(Sam.java:225)
        at sam.Sam.checkUserPermissionLocal(Sam.java:243)
        at com.michelin.cio.hudson.plugins.rolestrategy.PermissionCache.getPermissionSam(RoleMap.java:155)
        at com.michelin.cio.hudson.plugins.rolestrategy.PermissionCache.getPermission(RoleMap.java:106)
        at com.michelin.cio.hudson.plugins.rolestrategy.RoleMap.hasPermission(RoleMap.java:220)
        at com.michelin.cio.hudson.plugins.rolestrategy.RoleMap.access$000(RoleMap.java:166)
        at com.michelin.cio.hudson.plugins.rolestrategy.RoleMap$AclImpl.hasPermission(RoleMap.java:569)
        at hudson.security.SidACL._hasPermission(SidACL.java:70)

從上面的輸出可以看到,25.0% 的 CPU 資源在處理 Handling GET /job/jenkins-test-job/api/json from 172.168.1.1 這個請求。

運維同學根據這個 ip ,定位到發起請求的是某同學 A。這個同學在跑一些定時任務,定時拉取 job 的執行結果。

問題是當我直接訪問這個介面:/job/jenkins-test-job/api/json 時,返回並不慢,幾乎很快就可以返回。問題應該不是這個介面的問題。

我們接著從 ./show-busy-java-threads 輸出往下看:看到其中有問題的調用棧:

at net.sf.json.JSONObject.fromObject(JSONObject.java:132)
at sam.Sam.sendRequestReturnJson(Sam.java:517)
at sam.Sam.getPermissionByUser(Sam.java:225)
at sam.Sam.checkUserPermissionLocal(Sam.java:243)

看起來是這個 Sam 校驗用戶許可權導致的 CPU 使用率過高,而接著看上面的代碼 net.sf.json.JSONObject.fromObject,這個是在做 json 的反序列化。

通常來說,json 的序列化、反序列化都是比較費 CPU 的,更糟糕的是,這裡用到的 json 序列化框架是 net.sf.json,而不是 Java 常用的 jackson 和 gson 等。

直覺告訴我,肯定是這個 net.sf.json 反序列化引起的 CPU 使用率過高問題。

備註:

通過跟之前維護 jenkins 的同學瞭解到,他們基於 role-strategy 插件,重寫了 jenkins 許可權驗證邏輯,用的就是 Sam 許可權。翻看 sam 許可權插件的代碼,確實有用 net.sf.json 做 json 反序列化。

到這裡,定位到大概率是 Sam 許可權插件的 net.sf.json 反序列化引起的問題。

問題復現

為了驗證這個問題,我們拿到 Sam 許可權插件的代碼。找到出問題的關鍵代碼:

public void getPermissionByUser(String email) {
    JSONObject params = new JSONObject();
    params.put("user_email", email);
    params.put("subsystem_id", SAM_JENKINS_SUM_SYSTEM_ID);

    JSONObject res = sendRequestReturnJson(URL, "GET", params);
    if (res.get("success").equals(true)) {
        cacheUserPermission(params.getString("user_email"), res.getJSONObject("permission").getJSONObject(email).getJSONObject("SERVICE"));
    }
}

public static JSONObject sendRequestReturnJson(String endpoint, String method, JSONObject params) {
    if (method.equals("POST")) {
        return JSONObject.fromObject(sendPostRequest(endpoint, params));
    } else if (method.equals("GET")) {
        return JSONObject.fromObject(sendGetRequest(endpoint, params));
    }
    return new JSONObject();
}
        
        

可以看到,這段代碼會根據用戶郵箱,發送 http 請求調用 Sam 系統,獲取用戶的許可權數據,然後將數據反序列化成 JSONObject,即:
JSONObject.fromObject(sendGetRequest(endpoint, params, token))

在本地,通過復現 A 同學的請求,發現這個請求確實比較慢,而且費 CPU。通過 debug 得知,這個用戶返回的 json 數據有 1M 左右,json 反序列化 CPU 打滿。

而通過其他用戶請求,發現處理很快,返回的 json 數據也比較小。

到這裡,確認就是 net.sf.json 框架的反序列化性能問題,引起的 CPU 使用率過高。我們需要替換成其他高性能的 json 序列化框架。

備選有:gson、jackson、fastjson等。fastjson 因為經常出安全漏洞,暫不考慮,我們考慮從 gson、jackson 選擇一個。

在選定之前,先對 gson、jackson, 的性能做個基準測試,並與 net.sf.json 做對比。

JMH 基準測試 json 框架性能

Json 框架的性能測試,我們選用 JMH 框架。

JMH 框架是 JDK 官方提供的性能基準測試套件,參考:https://github.com/openjdk/jmh

代碼如下:

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import net.sf.json.JSONObject;
import org.apache.commons.io.FileUtils;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.springframework.util.ResourceUtils;

import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class JsonBenchmark {

    @Param({"10", "100", "500"})
    private int length;

    private String json;

    private String email = "[email protected]";
    private String path = "classpath:sam.json";

    @Benchmark
    public void testGson() throws IOException {
        Gson gson = new Gson();
        JsonObject root = gson.fromJson(json, JsonObject.class);

        if (root.getAsJsonObject("success").getAsBoolean()) {
            JsonObject services = root.get("permission").getAsJsonObject()
                    .get(email).getAsJsonObject()
                    .get("SERVICE").getAsJsonObject();
            System.out.println(services.size());
        }
    }

    @Benchmark
    public void testJackson() throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode root = objectMapper.readTree(json);

        if (root.get("success").asBoolean()) {
            JsonNode services = root.get("permission").get(email).get("SERVICE");
            System.out.println(services.size());
        }
    }

    @Benchmark
    public void testJsonObject() throws IOException {
        JSONObject root = JSONObject.fromObject(json);
        if (root.get("success").equals(true)) {
            JSONObject services = root.getJSONObject("permission").getJSONObject(email).getJSONObject("SERVICE");
            System.out.println(services.size());
        }
    }

    @Setup
    public void prepare() throws IOException {
        File file = ResourceUtils.getFile(path);
        json = FileUtils.readFileToString(file);
    }

    public static void main(String[] args) throws RunnerException {
        Options options = new OptionsBuilder()
                .include(JsonBenchmark.class.getSimpleName())
                .forks(1)
                .warmupIterations(5)
                .measurementIterations(2)
//                .output("/Users/wxweven/Benchmark.log")
                .result("result.json")
                .resultFormat(ResultFormatType.JSON)
                .build();
        new Runner(options).run();
    }
}

測試的結果如下:

Benchmark                     (length)  Mode  Cnt     Score   Error  Units
JsonBenchmark.testGson              10  avgt    2     7.979          ms/op
JsonBenchmark.testGson             100  avgt    2     8.958          ms/op
JsonBenchmark.testGson             500  avgt    2     9.975          ms/op
JsonBenchmark.testJackson           10  avgt    2    10.393          ms/op
JsonBenchmark.testJackson          100  avgt    2    12.214          ms/op
JsonBenchmark.testJackson          500  avgt    2    10.548          ms/op
JsonBenchmark.testJsonObject        10  avgt    2  1350.788          ms/op
JsonBenchmark.testJsonObject       100  avgt    2  1350.583          ms/op
JsonBenchmark.testJsonObject       500  avgt    2  1381.046          ms/op

可以看到,gson 和 jackson 性能接近,但是 jsonlib 性能就很差,比另外兩個慢 100 多倍。
綜合考慮性能、api 易用性等,選定 gson 作為替代方案。

替換成 gson

將之前的代碼替換成 gson,代碼如下:

public void getPermissionByUser(String email) {
    JSONObject params = new JSONObject();
    params.put("user_email", email);
    params.put("subsystem_id", SAM_JENKINS_SUM_SYSTEM_ID);

    JsonObject res = sendRequestReturnJsonV2(URL, "GET", params);
    if (res.get("success").getAsBoolean()) {
        cacheUserPermission(params.getString("user_email"), res.getAsJsonObject("permission").getAsJsonObject(email).getAsJsonObject("SERVICE"));
    }
        
}

public static JsonObject sendRequestReturnJsonV2(String endpoint, String method, JSONObject params) throws IOException {
    if (method.equals("POST")) {
        return GSON.fromJson(sendPostRequest(endpoint, params, token), JsonObject.class);
    } else if (method.equals("GET")) {
        return GSON.fromJson(sendGetRequest(endpoint, params, token), JsonObject.class);
    }

    return new JsonObject();
}

重新編譯許可權插件後上線,再次查看 CPU 負載監控,發現 CPU 負載確實降下來了(05/13晚上 0 點左右上線的)。
file

再次重新編譯,問題得到解決。

結束語

這個問題,前前後後花費了不少時間,也困擾了 DevOps 團隊比較久,經過大家的齊心協力,總算是把問題給解決了。

這篇文章也是對之前排查、解決問題的一個總結。
同時,也提醒大家,在使用第三方 jar 包的時候,一定要註意該 jar 包有沒有性能、安全等問題。如果不確定的話,可以用 JMH 等手段自己測試以下。


我是梅小西,最近在某東南亞電商公司做 DevOps 的相關事情。從本期開始,將陸續分享基於 Jenkins 的 CI/CD 工作流,包括 Jenkins On k8s 等。
如果你對 Java 或者 Jenkins 等感興趣,歡迎與我聯繫,微信:wxweven(備註 DevOps)

本文由博客群發一文多發等運營工具平臺 OpenWrite 發佈


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 索引:對象或數組的對應位置的名字 數組的索引就是 number 類型的 0,1,2,3... 對象的索引就是 string 類型的屬性名 數字索引簽名:通過定義介面用來約束數組 type numberIndex{ [index:number]:string } const testArray:num ...
  • DRY原則 DRY 原則,它的英文描述為:Don’t Repeat Yourself。中文直譯為:不要重覆自己。也可以理解為:不要寫重覆的代碼。 我們從實現邏輯重覆、功能語義重覆和代碼執行重覆,這三種代碼重覆來說明DRY原則。 實現邏輯重覆 例如有兩個函數isValidUserName() 和 is ...
  • 從演化歷史看大型網站架構 楊傳偉 (石家莊鐵道大學信息科學與技術學院,河北省,石家莊市,050043) 摘 要:本文以大型網站系統的特點、大型網站架構演化發展歷程以及大數據與高併發為切入和論述點,由淺入深、由簡到繁地對大型網站架構設計展開敘述,首先通述其特點,之後介紹大型網站架構的歷史發展歷程,從其 ...
  • 一些必須提前知道的概念 patition kafka日誌文件是以patition在物理存儲上分割的 是topic物理上的分組,一個topic可以分為多個partition,每個partition是一個有序的隊列 是以文件夾的形式存儲在具體Broker本機上 LEO 表示每個partition的log ...
  • MVC架構設計淺析 楊傳偉 (石家莊鐵道大學信息科學與技術學院,河北省,石家莊市,050043) 摘 要:本文以圖書管理系統為案例(當前主流框架SpringMVC的原理來分析MVC的設計理念等),深入淺出地分析常用的WEB設計模式MVC。將從MVC的歷史、MVC每一層的作用,MVC能為我們帶來什麼好 ...
  • 單例模式 單例模式一般用於全局只需要一個唯一的實例的情況。 例如說,日誌讀寫的功能,一般來說全局只需一個日誌讀寫實例,然後其他的類實例去獲取這個實例進行日誌讀寫。 又例如說,有一個協作的功能,需要各個模塊發送給主控制器,主控制器需要做成單例,這樣子模塊之間操作控制器就是操作實際主控制器的內容。 怎麼 ...
  • 操作系統:Windows10 Python版本:3.9.2 vosk是一個離線開源語音識別工具,它可以識別16種語言,包括中文。 這裡記錄下使用vosk進行中文識別的過程,以便後續查閱。 vosk地址:https://alphacephei.com/vosk/ 使用vosk-server進行語音識別 ...
  • 衡量運行時間 很多時候你需要計算某段代碼執行所需的時間,可以使用 time 模塊來實現這個功能。 import time startTime = time.time() # write your code or functions calls endTime = time.time() totalT ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...