做者:lovecindywangjava
來源地址:www.cnblogs.com/lovecindywang/程序員
我認可,本文的標題有一點標題黨,特別是寫業務代碼,你們由於沒有足夠重視一些細節最容易調的坑(側重Java,固然,本文說的這些點不少是不限制於語言的)。web
一、客戶端的使用
咱們在使用Redis、ElasticSearch、RabbitMQ、Mongodb等中間件或存儲的時候確定都會使用客戶端包來和這些系統通信,咱們也會使用Http的一些客戶端來發Http請求。在使用這些客戶端包的時候,很是容易犯錯的一個地方就是Client的使用方式,好比有一個叫作RedisClient的類,是Redis操做的入口。你應該是每次使用new RedisClient().get(KEY)呢仍是注入一個單例的RedisClient呢?redis
咱們知道,這些組件的客戶端每每須要和服務端經過TCP鏈接進行遠程通信,考慮到性能,客戶端通常都維護鏈接池作長連接,若是RedisClient或MongoClient或HttpClient之類的Client在類內部維護了鏈接池,那麼這個Client每每是線程安全的,能夠在多線程環境下使用的,而且嚴格禁止每次都新建一個對象出來的(若是框架作的足夠好通常自己就會是單例模式的,不容許實例化)。數據庫
你想,若是一個Client每次new的時候它會創建5個TCP連接給整個應用程序公用就是考慮到建連的耗時,而由於使用不當每次調用一次Redis都建5個TCP連接,那麼QPS可能就會從10000一會兒到10。更要命的是,有的時候這些Client不但維護用於TCP連接的鏈接池,還會維護用於任務處理的線程池,線程池的可能還會有比較大的默認核心線程,這個時候再去每次使用new一個Client出來,那就是雙重打擊了。緩存
在使用Netty等框架的時候,原本就是基於Event Loop線程公用來作IO處理的,對於客戶端來講Work Group可能只會有2~4個連接就夠了,咱們假設4個連接好了,若是這個時候Client框架的開發者對於Netty使用不當,對於客戶端鏈接池再去每次new一個Bootstrap出來,客戶端鏈接池又搞了所謂的5個,那就至關於每次20個EventLoopGroup(線程),這個時候客戶端的使用者又對於框架使用不當每次再new一個Client出來,至關於作一個請求須要20個線程,這就是三重打擊。安全
那你可能會說,是否是全部的Client都作單例使用就行了呢?並非這樣,這取決於Client的實現,極可能Client只是一個入口,那些鏈接池和線程池維護在另一個類中,這個入口自己是輕量的,自帶狀態的(好比一些配置),是不容許做爲單例的,框架的開發者就是想讓你們經過這個便捷入口來使用API。這個時候若是當作單例來使用說不定會出現串配置的問題。因此Client使用最佳實踐這個問題沒有統一的答案。服務器
這裏我沒有提到數據庫的緣由是,你們使用數據庫通常都使用Mybatis、JPA,已經不會和數據源直接打交道了,通常而言不容易犯錯。可是如今中間件太多了,客戶端更是有官方的有社區的,咱們在使用的時候必定要根據文檔搞清楚到底應該怎麼去使用客戶端(或者請使用關鍵字XXX threadsafe或XXX singleton多搜索一下Google確認),若是搞不清楚就去看下源碼,看下客戶端在鏈接池線程池這塊的處理方式,不然可能會形成巨大的性能問題。還不只僅是性能問題,我見過不少由於對客戶端使用不當致使的內存暴增、TCP連接佔滿等等致使的服務最終癱瘓的重大故障。微信
二、服務調用參數配置
如今你們都在實踐微服務架構,無論是使用什麼微服務框架,是基於HTTP REST仍是TCP的RPC,都會設置一些參數,這些參數在設置的時候若是沒有認真考慮的話可能就會有一些坑。網絡
超時配置
客戶端通常最關注的是兩個參數,鏈接超時(ConnectionTimeout)和讀取超時:(ReadTimeout),指的是創建TCP連接的超時和從Socket讀取(須要的)數據的超時,後者每每不只僅是網絡的耗時,包含了服務端處理任務的耗時。在設置的時候考慮幾個點:
1.鏈接超時相對單純,TCP建鏈通常不會耗時好久,設置太大意義不大,看到有設置60秒甚至更長的,若是超過2秒都連不上還不如直接放棄,快速放棄至少還能重試,何須苦等。
2.讀取超時不只僅涉及到網絡了,還涉及到遠端服務的處理或執行的時間,你們能夠想一下,若是客戶端讀取超時在5秒,遠程服務的執行時間在10秒,那麼客戶端5秒後收到read timed out的錯誤,遠程的服務還在繼續執行,10秒後執行完畢,這個時候若是客戶端重試一次的話服務端就再執行一次。通常而言,建議評估一下服務端執行時間(好比P95在3秒),客戶端的讀取超時參數建議比服務端執行時間設置的略長一點(好比5秒),不然可能遇到重複執行的問題。
3.以前遇到過一個問題,Job調用服務執行定時任務生成對帳單,定時任務執行一次須要30分鐘(完成後再更新數據狀態爲已生成),可是Job客戶端設置的讀取超時是60秒,Job每1分鐘執行一次,至關於Job不斷超時,不斷重試,每1分鐘執行一次超時了接着又執行,這個任務本應該一天處理一次,由於這個問題變爲了執行了30次(請求數量放大),由於任務處理極其消耗資源,執行了還沒到30次後服務端就直接掛了。大多數RPC框架在服務端執行都會在線程池中執行業務邏輯,執行自己不會設定超時時間。仍是前面那個問題,對於耗時比較長的操做,要考慮一下是否須要作同步的遠程服務。即便要作,也要經過鎖控制好狀態,或者經過限流控制好併發。
4.你們可能會以爲奇怪,爲啥大多框架不關注寫入超時(WriteTimeout)這個配置?其實寫入操做自己就是寫入Socket的緩衝,數據發往遠端的過程是異步的,就寫入操做自己而言每每是很快的,除非緩衝滿了,咱們沒法知道寫入操做是否成功寫到遠端,若是要知道的話也要等拿到了響應數據的時候才知道,這個時候就是讀取階段了,因此寫入操做自己的超時配置意義不大。
自動重試
不管是Spring Cloud Ribbon仍是其它的一些RPC客戶端每每都有自動重試功能(MaxAutoRetries
和MaxAutoRetriesNextServer
),考慮到Failover,有的框架會默認狀況下對於節點A掛的狀況下重試一次節點B。咱們須要考慮一下這個功能是不是咱們須要的,咱們的服務端是否支持冪等,框架重試的策略是很對Get請求仍是全部請求,弄的很差就會由於自動重試問題踩坑(不是全部的服務端都對冪等問題處理的足夠好,或者換句話說,和以前那個問題相關的是,不是全部服務端能正確處理請求自己還沒執行完成狀況下的冪等處理,不少時候服務端考慮的冪等處理是基於本身的操做執行完成後提交了事務更新數據表狀態下的冪等處理)。對於遠程服務調用,客戶端和服務端商量好冪等策略,明確超時時間不一致狀況下的處理策略很重要。
三、線程池的使用
線程池配置
阿里Java開發指南中提到:
線程池不容許使用 Executors 去建立,而是經過 ThreadPoolExecutor 的方式,這樣
的處理方式讓寫的同窗更加明確線程池的運行規則,規避資源耗盡的風險。
說明:Executors 返回的線程池對象的弊端以下:
1)FixedThreadPool 和 SingleThreadPool:
容許的請求隊列長度爲 Integer.MAX_VALUE,可能會堆積大量的請求,從而致使 OOM。
2)CachedThreadPool 和 ScheduledThreadPool:
容許的建立線程數量爲 Integer.MAX_VALUE,可能會建立大量的線程,從而致使 OOM。
建議你們熟悉研究一下線程池基本原理,採用手動方式根據實際業務需求來配置線程數、隊列類型長度、拒絕策略等參數。
咱們每每會使用必定的隊列來作任務緩衝(線程池也好,MQ也好),出現隊列滿的狀況下的拒絕策略也值得一提。咱們使用線程池作異步處理,就是考慮到彈性,這些任務會有補償或任務自己丟失並不這麼重要,這個時候若是輕易使用CallerRunsPolicy
策略的話可能會遇到大問題,由於隊列滿了後任務會由調用者線程來執行,這種作法每每是調用者最不但願出現的異步轉同步問題。更嚴重的是這種策略配合NIO框架,好比Netty來使用線程池的時候,若是調用者是IO EventLoopGroup線程,那麼這個時候業務線程池滿了後就會直接把IO線程堵死。遇到任務量太大,任務怎麼處理,是記錄後補償仍是丟棄,仍是調用者執行須要認真考慮。
線程池共享
見過一些業務代碼作了Utils類型在整個項目中的各類操做共享使用一個線程池,也見過一些業務代碼大量Java 8使用parallel stream特性作一些耗時操做可是沒有使用自定義的線程池或是沒有設置更大的線程數(沒有意識到parallel stream的共享ForkJoinPool問題)。共享的問題在於會干擾,若是有一些異步操做的平均耗時是1秒,另一些是100秒,這些操做放在一塊兒共享一個線程池極可能會出現相互影響甚至餓死的問題。建議根據異步業務類型,合理設置隔離的線程池。
四、線程安全
對象是否單例
在使用Spring容器的時候,由於Bean默認是單例的策略,因此咱們特別容易犯錯的地方是讓不該該是單例的類成爲了單例。好比類中有一些數據字段時候類是有狀態的。當咱們配合Spring和其它框架一塊兒使用的時候更容易煩這個錯,好比框架內部是沒有使用Spring的,會本身經過一些緩存機制或池機制來維護對象的聲明週期,若是咱們直接加入容器,用容器來管理框架內部一些類型的建立方式,可能就會遇到不少Bug。對於單例類型的內部數據字段,考慮使用ThreadLocal來封裝,使得類型在多線程狀況下內部數據基於線程隔離不至於錯亂。
單例是否線程安全
前面一點咱們說的是要辨別清楚,對象是否應該是單例的,這裏咱們說的是單例的狀況下是不是線程安全的問題。在使用各類框架提供的各類類的時候,(爲了性能)咱們有的時候會想固然加上static或讓Spring單例注入,在這麼作以前務必須要確認類型是不是線程安全的(好比常見的SimpleDateFormat就不是線程安全的)。我以爲我在開發時候Google搜索的最多的關鍵字就是XXX threadsafe。反過來講,若是你開發框架的話有義務在註釋裏告知使用者類型是否線程安全。線程安全的問題在測試過程當中不容易發現,畢竟測試的時候沒有併發,可是到了生產可能就會有千奇百怪的問題,出現這樣的Bug若是爆出了ConcurrentModificationException這種併發異常還好,在沒有異常的狀況要定位問題真的很難。許多Web程序員其實都沒有意識到本身的項目實際上是多線程環境這個問題。
鎖範圍和粒度
sync(object)這個object究竟是什麼,是類實例,仍是類型,仍是redis的Key(跨進程鎖)值得仔細思考。咱們須要確保鎖能鎖住須要的操做,見到過一些代碼由於沒有鎖到正確級別致使鎖失效。
同時也要儘量減小鎖的粒度,若是什麼操做都方法級別分佈式鎖,那麼這個方法永遠是全局單線程。這個時候加機器就沒意義,系統就沒法伸縮。
最後就是要考慮鎖的超時問題,特別是分佈式鎖,若是沒有設置超時那麼極可能由於代碼中斷致使鎖永遠沒法釋放,對於Redis鎖不建議造輪子,建議使用官方推薦的紅鎖方案(好比Redisson的實現)。
五、異步
數據流順序
若是數據流是異步處理的話,會遇到數據流順序的問題。好比咱們先發請求到其它服務執行異步操做(好比支付),而後再執行本地的數據庫操做(好比建立支付訂單),完成後提交事務可能會遇到外部服務請求處理的很快,先給咱們進行了數據回調(支付成功通知),這個時候咱們本地的事務都沒提交呢,支付訂單尚未落庫,致使外部回調來的時候查不到原始數據致使出現問題。更要命的多是這個時候咱們卻返回了外部回調SUCCESS的狀態致使外部回調也不會進行補償了。
在使用MQ的時候也會遇到補償數據從新進入隊列重發的問題,這個時候可能會先收到更晚的消息,後收到更早的消息,這種狀況咱們的消息消費處理程序是否能應對呢?若是這點沒作好可能會出現邏輯處理錯亂的問題。
異步非阻塞
在使用Spring WebFlux、Netty(特別是前者,Netty的開發者通常會關注這個問題)等非阻塞框架的時候,咱們須要意識到咱們的業務處理不能過多佔用事件循環的IO線程,不然可能會致使爲數很少的IO線程被阻塞的問題。任務是否在IO線程執行也不是絕對的,若是小任務都分到業務線程池執行可能會有線程切換的問題,得不償失,一切仍是要以壓力測試的數聽說話不能想固然。若是這點沒作好可能會出現性能大幅降低的問題。有的時候NIO框架Reactor模式使用不當,其效率性能還如request-per-thread的線程模型。
六、事務
本地事務
如今大多數項目都直接使用了@Transactional註解來開啓事務,可是沒有過多考慮這個註解的實現原理,常見的坑有:
1.由於配置問題致使壓根註解沒有起做用(特別是沒有使用Spring Boot的狀況下)
2.雖然使用了,可是姿式不對,致使事務沒有生效,好比入口沒有@Transactional,而後this.method()標記的方法帶有註解,類由於沒代理致使無效
3.又好比rollbackFor沒有配置,或是方法內部吃了全部異常並不會出現異常致使沒法回滾
由於事務問題致使的代碼Bug至關多,並且通常不出問題不容易發現,不少項目只是裝模做樣使用了@Transactional可是徹底沒考慮到註解壓根不能生效的問題
分佈式事務
無論是最終實現一致也好,兩階段提(只是思想,不是說必定要用中間件)交也好,跨進程的總體事務性須要考慮如何去實現。最難的地方在於要考慮遠程資源的事務性和本地資源的事務性怎麼做爲總體事務。
七、引用根
這裏說的是內存泄露的問題,Java程序其實若是不使用堆外直接內存分配的話不會出現狹義的內存泄露問題。坑在於,有的時候你們會使用static來聲明List或Map,來存檔一些數據,可是有的時候會忽略刪除老數據的問題,一個勁往裏面增長數據不刪除,致使數據無限增多,仍是有一些程序員意識不到引用根的問題的。更隱蔽的是,Spring的Bean默認是單例的,這個時候在Service內聲明使用List之類結構來保存數據,雖然沒有聲明static,可是就是static的屬性(容易讓人造成對象可以本身回收的錯覺)。這個問題要求咱們可以明確:
1.咱們數據所歸屬的類是不是單例或static的(生命週期)
2.咱們數據所歸屬的類所歸屬的類的聲明週期(探尋引用根)
3.咱們數據自己是無限擴大的仍是隻是有限的集合
4.當咱們的數據放入Map中或Set中,是否新數據會替換老的數據(見下面判等問題)
說白了,代碼裏見到非方法體內部聲明的List、Map等數據結構(做爲類成員字段)都要當心。
八、判等
判等只是代碼實現細節中最容易犯錯的一個點,在這裏仍是再次推薦一下阿里的Java開發手冊以及安裝IDE的檢查工具,裏面有不少禁止或強制項,每個項都是一個坑,推薦你們逐一細細品味這些代碼細節。
==的問題
Java程序員最容易犯的錯,也是致使代碼Bug很是多的一個點,這個經過代碼靜態檢查均可以發現。出現這樣的Bug很是難查,也很是惋惜。其實想一下業務代碼中,除了判空,有多少時候咱們須要真正對兩個對象的引用進行判斷。
在數據庫Entity中考慮到空指針問題,咱們每每會使用包裝類型,外部Http請求入參咱們也會考慮到空指針問題用包裝類型,這個時候碰在一塊兒比較使用==就特別容易出問題,尤爲須要關注。並且相等或不等處理的每每是分支邏輯,測試容易覆蓋不到,真正出問題的時候就是大問題。
Map和hashCode()
也是阿里Java開發手冊中提到的一點,若是自定義對象可能做爲Map的Key,那麼必須重寫hashCode()和equals(),這是業務開發時很是容易忽略的。我也遇到過這個問題,犯錯的緣由不是我不知道這點,而是我不知道也意識不到個人類會被某個框架作做爲Map的Key(三方框架,並不是本身所寫)進行緩存,而後由於這個問題致使本身定義的類的多個實例被框架當作一個實例出現沒法預料的Bug。
九、中間件的使用
在使用中間件的時候,咱們最好針對使用場景對中間件或存儲作一次壓力測試,而且研究各類配置參數作到對基本原理心中有數,不然容易由於沒有按照最佳實踐來使用配置而踩坑。遇到坑能夠過去倒沒什麼,最怕的是大面積使用了某個系統好比MongoDb、ElasticSearch、InfluxDb後又遇到了伸縮性問題性能問題一時半會沒法解決,這種坑就大了。
遇到過開發在使用Redis的時候把它當作數據庫而不是Key-Value緩存,去用KEYS命令搜索本身須要的鍵進行批量操做,這種使用方式徹底違背Redis的最佳實踐,在巨大的Redis集羣裏頻繁使用這樣的操做可能致使Redis卡死。對於Redis的使用也遇到過由於不合理的RDB配置致使的IO性能問題,以及快照期間超量的內存佔用致使的OOM問題。
好比使用InfluxDb,它的Tag是一個不錯的特性,咱們能夠針對各類Tag來分組靈活創建各類指標,可是Tag是不能因此使用來保存組合範圍過多的數據的,好比Url、Id等不然可能就會由於巨大的索引(high cardinality問題)拖慢整個InfluxDb的性能甚至OOM。
又好比有一個業務由於壓力大選型Mongodb,最後Mongodb沒有配置開啓write-ahead log和複製,在一次斷電後數據庫由於存儲文件損壞沒法啓動,研究恢復工具和數據存儲結構來修復數據文件花了幾天時間,整個期間全部歷史數據都沒法訪問到。
對於極限追求穩定的項目,建議約簡單約好,哪怕就是依賴MySQL不引入其它東西,在有性能問題的時候再考慮其它中間件,這種方式最不容易出問題。
十、環境和配置
由於環境問題致使的坑太多了,有的時候實際上是你們意識不到環境差別問題。這裏隨便說幾個,我相信開發和運維結合的一些環境配置的問題致使的坑或線上的事故和問題太多太多了。而本地每每由於沒有容器環境、K8S環境和複雜的網絡環境,本地的程序部署到生產可能會出現千奇百怪的問題。
網絡環境
遇到過壓測壓的很好,可是到線上仍是崩潰的問題,緣由在於壓測走的是所有都在內網部署的一套服務,生產不少服務走的是外網(或專線)連接,環境實際上是不同的,網絡的消耗必然帶來請求的延遲,帶來線程的阻塞,帶來更多的資源消耗。也遇到過由於域名錯誤配置(或解析錯誤)問題致使應該走內網的請求走了公網,在測試環境或本地每每都是配置IP不容易出現這種問題。
反過來,也遇到過,本地壓測怎麼都壓不上去的問題,實際上是由於本地有一些請求走的是公網連到了服務器上的一些服務,壓根就不是徹底的本地壓測,若是意識不到這個問題,這個時候對於性能的優化每每很茫然。因此在壓測的時候咱們最好使用相似iftop這樣的工具觀察一下咱們的壓測進程對於網絡流量的使用(以及鏈接的遠端服務的地址)是否在咱們的預期。
容器環境
如今你們都使用了K8S和Docker,在這種環境下,咱們的業務項目不只僅在網絡上從外到內通過多層,並且對於CPU、內存、文件句柄都配置也是層層限制(Pod層面、Docker層面、OS層面)。這個時候特別容易出現某一處配置不匹配致使資源限制的問題。
以前遇到過經過K8S Ingress訪問服務慢的問題,這個時候須要層層排查,畢竟K8S的網絡仍是挺複雜的,不一樣的CNI方案可能會有不一樣的問題,Docker裏訪問慢不慢,經過Service訪問慢不慢,經過Ingress訪問慢不慢來定位問題。
還有,在容器環境下,CPU數量可能會獲取到宿主機的CPU梳理,致使不少框架的線程數配置的過大(好比有些宿主機48核+,CPU數量*2的話就是96線程),JVM的ParallelGCThreads就是一個例子,此類坑不少,不合理的配置可能會致使性能問題。
今天還遇到一位同窗說,死活不知道爲啥系統參數各類修改後仍是沒法生效增大文件描述符和進程數的限制,最後發現原來是由於java進程是supervisord(通常使用Docker都會使用)啓動的,supervisor自己有限制(minfds和minprocs)。
環境隔離
互聯網公司基本都會有灰度環境或Staging環境作上線前的最後測試,可是不少時候會由於這套環境和生產環境共享一些資源致使出現問題。
以前遇到一個問題是使用了七牛作CDN,灰度環境和生產環境都是使用了一樣的CDN,致使在灰度測試的時候新的靜態資源文件就緩存到了CDN節點上致使外部用戶訪問出錯(訪問到了新的靜態資源)。出這個問題以後要立刻回滾解決仍是比較麻煩的,由於CDN已經被污染了。長期解決的辦法很簡單就是作隔離或每次發佈靜態資源文件名不一樣。
總結
總結一下,線程、線程同步、池、網絡鏈接、網絡鏈路、對象實例化、內存等方面的基礎是最容易犯錯的地方,搞清楚框架內部對於這些基礎資源的的使用方式,根據最佳實踐進行合理配置,這是業務開發時須要特別關注的點。有的時候一些代碼在使用三方框架和中間件的時候由於不瞭解細節,不但沒有按照最佳實踐來配置反而配成了最差實踐,形成了很大的問題很是惋惜。
因爲各類坑五花八門,本文也只是拋磚引玉,但願讀者能夠補充本身遇到的神坑,但願你們能在評論區留言。
推薦做品
本文分享自微信公衆號 - java版web項目(java_project)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。