JVM 案例
案例一:服務(wù)器內(nèi)存不足,影響Java應(yīng)用
問題: 收到報警,某Java
應(yīng)用集群中一臺服務(wù)器可用內(nèi)存不足,超過報警閾值。
排查過程: 首先,通過Hickwall
查看該應(yīng)用各項指標(biāo),發(fā)現(xiàn)無論是請求數(shù)量、CPU
使用率、還是JMX
的各項指標(biāo)均未發(fā)現(xiàn)異常。僅系統(tǒng)內(nèi)存占用很高,但是從JMX
指標(biāo)中看,Java
應(yīng)用的Heap Memory
、GC
等都是正常的,在合理大小和范圍內(nèi),未發(fā)現(xiàn)內(nèi)存泄漏等問題。故懷疑不是Java
應(yīng)用本身的問題,而是系統(tǒng)上其他組件出了問題,但是從Hickwall
等工具上又看不出其他組件的信息。然后,直接ssh
登錄到該服務(wù)器,由于是內(nèi)存問題,故直接使用簡單的top
命令,根據(jù)內(nèi)存占用排序后發(fā)現(xiàn),是logagent
進程占用了接近一半的系統(tǒng)內(nèi)存。
由于賬號權(quán)限限制,無法進一步處理,誘因找到后,隨即反饋給網(wǎng)站運營中心的同事,幫忙臨時將logagent
進程殺死,系統(tǒng)恢復(fù)正常。后續(xù)經(jīng)網(wǎng)站運營中心同事排查,發(fā)現(xiàn)是logagent
內(nèi)部bug
,導(dǎo)致處理格式異常的日志文件時發(fā)生內(nèi)存泄漏,后續(xù)打上補丁進行修復(fù)。
問題原因及思考: 目前公司各項監(jiān)控工具已經(jīng)比較完善,開發(fā)人員應(yīng)熟練掌握并了解其中各項指標(biāo)的含義,能夠在分析具體問題時靈活運用各個工具,快速定位解決問題。
同時本案例雖然不是業(yè)務(wù)系統(tǒng)的問題,但這一案例也提醒了開發(fā)人員:線上實際問題可能是各方各面的,除了具備Java
技術(shù)棧的相關(guān)的排障技能外,同時也要有基本的Linux
操作能力,在已有工具無法幫助解決問題時,多一種途徑快速定位問題,畢竟運營中心的同事人力有限,可能無法及時提供支持。
擴展
Java Full GC
頻繁: 可通過Hickwall
中的JMX Full gc time/count
指標(biāo)觀察Full GC
情況,正常情況下不應(yīng)有Full GC
出現(xiàn),Full GC
意味著 STW
,JVM
會阻塞其他所有線程來進行垃圾回收,頻繁的Full GC
會嚴重影響應(yīng)用的性能。如果出現(xiàn)Full GC
通常意味著Java
堆內(nèi)存大小無法滿足需求,如果不是代碼缺陷導(dǎo)致(可通過以上OOM
中JVM Sampler
工具相關(guān)方法排查)則需要增加堆內(nèi)存大小。
大數(shù)據(jù)量處理
案例一:大循環(huán)引起的 cpu 負載過高的問題
問題: x
應(yīng)用在一次發(fā)布時,cpu
出現(xiàn)負載過高,其負載率突破200%
,并且響應(yīng)時間也大幅度超時。
代碼:
List<CityDataModel> cities = cityDomainService.allCities();
for (CityDataModel city : cities) {
if (city.getCityCode().equalsIgnoreCase(flight.getDepartCity())) {
dCountry = city.getCountryCode();
}
if (city.getCityCode().equalsIgnoreCase(flight.getArriveCity())) {
aCountry = city.getCountryCode();
}
}
通過在測試環(huán)境嘗試調(diào)用一次服務(wù)請求,發(fā)現(xiàn)其循環(huán)的數(shù)據(jù)是城市列表。該列表的長度達到12000
,而且發(fā)現(xiàn)該循環(huán)本身被執(zhí)行了11
次,String::equalsIgnoreCase
方法執(zhí)行了18
萬次,也就是說這是一個典型的大循環(huán)的代碼。 并且通過記錄日志發(fā)現(xiàn),在生產(chǎn)中該塊代碼平均每次請求都會調(diào)用24
萬次左右,這導(dǎo)致很多cpu
資源都集中在該方法上,使得cpu load
大幅度提高。
問題解決: 由于代碼中的大循環(huán)非常耗費cpu
資源,通過分析,這里的Strings::equalsIgnoreCase
方法的主要作用在于遍歷判斷取數(shù)據(jù)。 根據(jù)這種查找數(shù)據(jù)的情況,優(yōu)先選擇使用HashMap
替代,用空間換時間,經(jīng)過修改后重新發(fā)布,其cpu
利用率明顯下降,恢復(fù)正常。
思考總結(jié):
【1】使用循環(huán)時需要特別注意大循環(huán),優(yōu)先使用O(1)
的HashMap
,大循環(huán)對于cpu
性能的壓榨在這個問題上表現(xiàn)地淋漓盡致。
【2】鏡像機器由于是使用生產(chǎn)流量轉(zhuǎn)發(fā)訪問,所以鏡像發(fā)布高度貼近實際生產(chǎn)發(fā)布。在每次發(fā)布前,先使用鏡像機器預(yù)發(fā)布,可以盡可能地將潛在的問題暴露出來。另外性能實驗室中提供了cpu
熱點、內(nèi)存分配熱點和鎖競爭熱點的Flamegraph
,在預(yù)發(fā)布中遇到問題時也可以更加直觀地幫忙解決問題,并且不會對實際生產(chǎn)機器造成影響。
【3】此次發(fā)布前,雖然在測試環(huán)境進行了壓測,但是并沒有復(fù)現(xiàn)出該問題,分析原因,其與特定的壓測的請求相關(guān)。由于在壓測時使用的請求沒有經(jīng)過某些代碼分支,使得循環(huán)的次數(shù)相比較少,故而在測試環(huán)境壓測時沒有暴露該問題。
【4】以上總結(jié),除了代碼層面的使用注意外,可以進行測試環(huán)境多種請求壓力測試,以及生產(chǎn)鏡像機器預(yù)發(fā)布等手段,來檢測和杜絕這種潛在的問題發(fā)生。
案例二:多層嵌套 map
問題: 某日某查詢服務(wù)器開始不斷拉出集群,造成線上訂單下跌。
遇到問題需咨詢解決。。。
緩存
案例一:篡改緩存
問題: 查詢接口下發(fā)錯誤數(shù)據(jù)故障
故障描述: 包含故障開始時間,發(fā)現(xiàn)時間,控制措施,故障排除細節(jié)
Time | Event |
---|---|
15:00 | 接到產(chǎn)品郵件告知下單調(diào)用查詢接口的結(jié)果與前端的數(shù)據(jù)不一致,要求緊急對問題進行排查。 |
15:05 | 開始對問題進行排查,同時了解問題大概影響范圍。 |
16:00 | 定位到是因為代碼bug 導(dǎo)致的接口在下發(fā)時,輸出的結(jié)果不正確。3月1日接口由.net 切換到了java 版查詢接口。 |
16:20 | 開始著手對bug 進行修復(fù),并進行緊急發(fā)布。 |
16:20 | 與產(chǎn)品溝通影響范圍以及問題訂單的處理辦法。 |
16:30 | 確定影響的單量:10萬
|
16:50 | 著手準(zhǔn)備修復(fù)問題訂單SQL
|
18:00 | 完成緊急修復(fù)的上線 |
故障分析:
【1】為什么接口會下發(fā)錯誤?.Net
接口轉(zhuǎn)Java
過程中引發(fā)的代碼bug
,修改了本地緩存對象。
【2】為什么這個錯誤在代碼review
中沒有被發(fā)現(xiàn)?
代碼review
不充分。雖然接口的邏輯并不復(fù)雜,但是代碼量較多(40
個文件,2000 additions and 1000 deletions
),在review
過程中遺漏了該錯誤。
【3】為什么在測試過程中沒有發(fā)現(xiàn)該bug
?
測試不充分,同時這個bug
的觸發(fā)存在一定概率性,當(dāng)多個訂單引用同一個基礎(chǔ)服務(wù)對象時,在對禮盒進行遍歷計算時,最后一個訂單的計算結(jié)果會覆蓋前面所有引用了該基礎(chǔ)服務(wù)對象的訂單。如果測試時選擇的訂單沒有觸發(fā)該·bug·,·.Net·和·Java·版本的對比結(jié)果是一致的。
【4】為什么影響的單量達·10W·以上規(guī)模?
該bug
從2
月12
日發(fā)布直到3
月3
日才發(fā)現(xiàn),持續(xù)了22
天。
【5】為什么從2
月12
日起該問題直到3
月3
日才發(fā)現(xiàn)?
目前對于這類問題缺乏有效的檢測機制,只能被動的等待客戶投訴發(fā)生后才會反饋到開發(fā)團隊。
分析總結(jié):
【1】通過以上故障示例,我們可以發(fā)現(xiàn)緩存被修改帶來的影響通常具有以下特性:
■ 不容易發(fā)現(xiàn),因為數(shù)據(jù)可能只在特定條件下被修改。
■ 影響面非常廣,因為數(shù)據(jù)本身是被頻繁使用才會被加入緩存。
■ 不確定性,因為數(shù)據(jù)被修改具有“隨機性”,該特性導(dǎo)致影響范圍難以確定,數(shù)據(jù)也難以清洗。
【2】緩存篡改通常如何發(fā)生:
■ 從緩沖獲取一個對象(引用),后續(xù)過程中修改了該對象的內(nèi)部成員。
public class CityCache {
private static final CityCache INSTANCE = new CityCache();
private final Map<String, City> cityMap = new HashMap<>();
public static CityCache getInstance() {
return INSTANCE;
}
private CityCache() {
// 此處為了簡便,沒有寫定時刷新
loadDataFromDB();
}
public City getCityByCode(String cityCode) {
return cityMap.get(cityCode);
}
private void loadDataFromDB() {
// load cities from database and put them into cityMap
}
}
@Data
public class City {
public City() {
}
public City(String code, int id) {
this.code = code;
this.id = id;
}
private String code;
private int id;
}
@Test
public void errorTest() {
// 通過SHA獲取到緩存實體,該實體的三字碼與SHA相同
City city1 = CityCache.getInstance().getCityByCode("SHA");
assertTrue("SHA".equals(city1.getCode()));
// 業(yè)務(wù)代碼直接修改了city1的三字碼(CityCache中的實體被修改)
city1.setCode("BJS");
// ...
// 再次通過SHA獲取到緩存實體,該實體的三字碼與SHA不相同了(非期望值)
City city2 = CityCache.getInstance().getCityByCode("SHA");
assertFalse("SHA".equals(city2.getCode()));
}
@Test
public void correctTest() {
// 通過SHA獲取到緩存實體,該實體的三字碼與SHA相同
City city1 = CityCache.getInstance().getCityByCode("SHA");
assertTrue("SHA".equals(city1.getCode()));
// 業(yè)務(wù)代碼不能直接修改緩存實體,正確做法是先Copy一個對象,修改Copy對象的屬性,后續(xù)業(yè)務(wù)使用該Copy對象
City cityCopy = new City(city1.getCode(), city1.getId());
cityCopy.setCode("BJS");
// ...
// 通過SHA獲取到緩存實體,該實體的三字碼與SHA相同
City city2 = CityCache.getInstance().getCityByCode("SHA");
assertTrue("SHA".equals(city2.getCode()));
}
■ 從緩沖獲取一個集合(引用),后續(xù)過程中往該集合中添加/刪除了元素。
public class CityCache {
private static final CityCache INSTANCE = new CityCache();
@Getter
private final Map<String, City> cityMap = new HashMap<>();
public static CityCache getInstance() {
return INSTANCE;
}
private CityCache() {
// // 此處為了簡便,沒有寫定時刷新
loadDataFromDB();
}
private void loadDataFromDB() {
// load cities from database and put them into cityMap
}
}
@Data
public class City {
public City() {
}
public City(String code, int id) {
this.code = code;
this.id = id;
}
private String code;
private int id;
}
@Test
public void errorTest() {
Map<String, City> cityMap = CityCache.getInstance().getCityMap();
// 通過SHA獲取到緩存實體,該實體的三字碼與SHA相同
City city1 = cityMap.get("SHA");
assertTrue("SHA".equals(city1.getCode()));
// 業(yè)務(wù)代碼直接修改緩存集合
cityMap.put("SHA", new City("BJS", 2));
// cityMap.remove("SHA");
// 再次通過SHA獲取到緩存實體,該實體的三字碼與SHA不相同了(非期望值)
City city2 = CityCache.getInstance().getCityMap().get("SHA");
assertFalse("SHA".equals(city2.getCode()));
}
■ 緩存實體被修改
public class CityCache {
private static final CityCache INSTANCE = new CityCache();
@Getter
private final Map<String, City> cityMap = new HashMap<>();
public static CityCache getInstance() {
return INSTANCE;
}
private CityCache() {
// // 此處為了簡便,沒有寫定時刷新
loadDataFromDB();
}
private void loadDataFromDB() {
// load cities from database and put them into cityMap
}
}
@Data
public class City {
public City() {
}
public City(String code, int id) {
this.code = code;
this.id = id;
}
private String code;
private int id;
}
@Test
public void errorTest() {
Map<String, City> cityMap = CityCache.getInstance().getCityMap();
// 通過SHA獲取到緩存實體,該實體的三字碼與SHA相同
City city1 = cityMap.get("SHA");
assertTrue("SHA".equals(city1.getCode()));
// 運行期間非預(yù)期的修改了緩存集合中的對象
cityMap.forEach((k, v) -> {
if (!"SHA".equals(k)) {
return;
}
// ...
v.setCode("BJS");
});
// 再次通過SHA獲取到緩存實體,該實體的三字碼與SHA不相同了(非期望值)
City city2 = CityCache.getInstance().getCityMap().get("SHA");
assertFalse("SHA".equals(city2.getCode()));
}
【3】如何避免緩存篡改:
■ 在可能需要修改數(shù)據(jù)的場景,從緩存獲取一個深拷貝對象/集合。
■ 將緩存對象設(shè)計為只讀狀態(tài),確保一旦構(gòu)建就不可再修改其內(nèi)部數(shù)據(jù)。
多線程
"多線程"這個話題想必開發(fā)人員或多或少都會接觸到。 使用多線程最主要的原因是提高系統(tǒng)的資源利用率。 但在使用的過程中可能會遇到各種各樣的問題,"死循環(huán)"便是其中比較棘手的一類。 下文分析了多線程環(huán)境下的死循環(huán)場景,希望對大家有所幫助。
死循環(huán)危害
程序進入假死狀態(tài): 當(dāng)某個請求導(dǎo)致死循環(huán),該請求會在很大一段時間內(nèi),都無法獲取接口的返回。
CPU 使用率飆升: 代碼出現(xiàn)死循環(huán)后,由于沒有休眠,一直不斷搶占cpu
資源,導(dǎo)致cpu
長時間處于繁忙狀態(tài),必定會使cpu
使用率飆升。
內(nèi)存使用率飆升: 代碼出現(xiàn)死循環(huán)時,循環(huán)體內(nèi)有大量創(chuàng)建對象的邏輯,垃圾回收器無法及時回收,會導(dǎo)致內(nèi)存使用率飆升。同時,如果垃圾回收器頻繁回收對象,也會造成cpu
使用率飆升。
StackOverflowError
棧溢出: 在一些遞歸調(diào)用的場景,如果出現(xiàn)死循環(huán),多次循環(huán)后,最終會報 StackOverflowError 棧溢出,程序直接掛掉。
案例一:多線程環(huán)境下的死循環(huán)案例
問題: 循環(huán)條件不正確
案例: 這里以二分查找為例
int search(List<Integer> nums, int target) {
int l = 0, r = nums.size() - 1;
while (l < r) {
int mid = (l + r) / 2;
if (nums.get(mid) > target)
r = mid - 1;
else
// 可能出問題位置
l = mid;
}
if (nums.get(l) == target)
return l;
else
return -1;
}
首先,會不會產(chǎn)生死循環(huán)的關(guān)鍵是l
和r
是否在每次循環(huán)后至少有一個的值發(fā)生了改變, 而while
循環(huán)體中,若走入了else
語句,l
的值有可能不發(fā)生變化,就會導(dǎo)致死循環(huán)的產(chǎn)生。 可以對循環(huán)體做以下調(diào)整:
while (l < r) {
int mid = (l + r) / 2;
if (nums.get(mid) >= target)
r = mid;
else
l = mid + 1;
}
案例二:flag 線程間不可見
有時候我們的代碼需要一直做某件事情,直到某個條件達到,有個狀態(tài)告訴它,要終止任務(wù)了,它就會自動退出。 這時候,很多人都會想到用while(flag)
實現(xiàn)這個功能:
public class FlagTest {
private boolean flag = true;
public void setFlag(boolean flag) {
this.flag = flag;
}
public void fun() {
while (flag) {
}
System.out.println("done");
}
public static void main(String[] args) throws InterruptedException {
final FlagTest flagTest = new FlagTest();
new Thread(() -> flagTest.fun()).start();
Thread.sleep(200);
flagTest.setFlag(false);
}
}
這段代碼在子線程中執(zhí)行無限循環(huán),當(dāng)主線程休眠200
毫秒后,將flag
變成false
,這時子線程就會自動退出了。想法是好的,但是實際上這段代碼進入了死循環(huán),不會因為flag
變成false
而自動退出。 為什么會這樣? 線程間flag
是不可見的,這時如果flag
加上了volatile
關(guān)鍵字,變成:
private volatile boolean flag = true;
會強制把共享內(nèi)存中的值刷新到主內(nèi)存中,讓多個線程間可見,程序可以正常退出。
案例三:HashMap JDK7/8 死循環(huán)
問題: JDK7 rehash
(擴容)時和JDK8
鏈表更改為紅黑樹時。鏈接
案例四:自己手動寫死循環(huán)
定時任務(wù)比如有個需求要求每隔5
分鐘,從遠程拉取數(shù)據(jù),覆蓋本地數(shù)據(jù)。 這時候,如果你不想用其他的定時任務(wù)框架,可以實現(xiàn)一個簡單的定時任務(wù),具體代碼如下:文章來源:http://www.zghlxwxcb.cn/news/detail-808862.html
public static void sync() {
new Thread(() -> {
while (true) {
try {
System.out.println("sync data");
Thread.sleep(1000 * 60 * 5);
} catch (Exception e) {
log.error(e);
}
}
}).start();
}
其實很多JDK
中的定時任務(wù),比如:Timer
類的底層,也是用了while(true)
的無限循環(huán)(也就是死循環(huán))來實現(xiàn)的。
?文章來源地址http://www.zghlxwxcb.cn/news/detail-808862.html
到了這里,關(guān)于線上問題整理的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!