天天看點

一份平民化的應用性能優化檢查清單(完整篇)

1.總原則

一些正确但稍顯廢話的原則,但能指導後面每個章節的優化,是以還是要啰嗦一次。

可擴充性架構,堆機器能不能解決問題是最最優先考慮的問題

去中心化的點對點通信,優于通過中心代理的通信

池化的長連接配接,優于短連接配接

二進制資料,優于文本資料

盡量減少互動,一次調用的粗粒度聚合接口 優于 多次調用的細粒度接口

盡量減少互動,批量接口優于循環調用

盡量隻互動必要的資料

盡量就近通路

盡量使用緩存

總是設定逾時

在合适的場景,并行化執行

在合适的場景,異步化執行

2.環境準備

保證符合自家各種規範(沒有的話趕緊回家寫一個),尤其線下壓測伺服器的配置要與生産環境一緻。

2.1 作業系統

自家規範調優應包含TCP核心參數,網卡參數及多隊列綁定,IO&Swap核心參數,ulimit資源限制等。

2.2 JVM與應用伺服器

使用JDK7.0 u80 或 JDK8 最新版。

檢查JVM啟動參數已按自家規範調優,見《關鍵業務系統的JVM參數推薦》

檢查應用伺服器(Tomcat或微服務容器) 已按自家指南調優,如線程數等。

2.3 周邊依賴系統

檢查資料庫,緩存,消息系統,已按自家指南調優。

2.4 背景輔助程式

檢查日志收集,系統監控等,已使用最新版本,最優配置。

最好其最大消耗已被控制(通過cgroup,taskset等方式)。

2.5 測試程式

壓測工具如JMeter,啟動參數要參考真實應用用戶端的參數優化(如JVM參數,Netty參數等)。

測試腳本和用戶端程式經過review,不存在影響性能的步驟,不存在System.out.println()等明顯的瓶頸。

2.5 流量模型

扇入模型:平時與高峰期的流量估算,各接口的流量比例,響應時間要求

扇出模型:各接口對遠端服務、資料庫、緩存、消息系統的調用比例,響應時間估算。

大家在心裡都有這麼一個大概的模型,但很少認真寫出來。

行文到此,大家大概可以感受到這份checklist的風格,都是大家明白的道理,但可能一時也會忘掉的,這裡啰啰嗦嗦的給寫下來。

3.資料庫

特别鳴謝,我司DBA。

3.1 拓撲

根據擴充性原則考慮:

垂直拆分:按業務将不同的表拆分到不同的庫。

水準拆分:水準分庫分表。

讀寫分離:在業務允許的情況下,在從庫讀取非實時資料。

3.2 Schema

自家規範應包含:

統一的存儲引擎,主鍵政策。

禁用存儲過程,函數,觸發器,外鍵限制。

列類型永遠越短越好,建議:布爾/枚舉:tinyint,日期與時間戳:timestamp或int,char/text/blob: 盡量用符合實際長度的varchar(n),小數及貨币:移位轉為int 或 decimal,IP位址:int。

索引政策:索引字段的順序需要考慮字段值去重之後的個數,較多的放前面,合理建立聯合索引,避免備援索引,合理利用覆寫索引等。

3.3 SQL

如禁止多于3表join,禁用子查詢

禁止where子句中對字段施加函數,如to_date(add_time)>xxxxx

避免MySQL進行隐式類型轉化,如ISENDED&eq;1 與 ISENDED&eq;

1

不建議使用%字首模糊查詢,模糊查詢較多時建議使用ElasticSearch

根據盡量少資料原則與盡量少互動的原則來設計SQL:

禁止select *

合理的SQL語句,減少互動次數

根據擴充性原則,将負載放在更容易伸縮的應用服務執行個體上:

盡量不要做數學運算,函數運算, 或者輸出格式轉換等非必要操作

避免count(*),計數統計實時要求較強使用memcache或者redis,非實時統計使用單獨統計表,定時更新。

甚至排序都是不鼓勵的,盡量在應用側進行。另外避免多餘的排序,使用GROUP BY 時,預設會進行排序,當你不需要排序時,可以使用order by null。

  1. 聯系DBA進行MySQL統計的慢查詢的Review,解析SQL查詢計劃時盡量避免extra列出現:Using File Sort,Using Temporary

3.4 DAO架構

根據盡量少互動與盡量少資料的原則,需使用對SQL完全可控的DAO架構,建議為MyBatis 或 Spring JDBC Template。

必須使用prepareStatement,提升性能與防注入。

根據一切皆有逾時的原則,配置SQL執行的逾時。可在連接配接池裡設定default值,可在MyBatis的Mapper定義裡可設定每個請求的逾時,可惜規範是秒級的。

JDBC driver 規範本身不支援異步模式,如果一定要異步,可以像Quasar那樣把請求封裝成Callable交給另外的線程池執行,但要注意其額外開銷。

3.5 事務

不使用事務,連接配接池設定autocommit,使用其他方式來保持資料一緻性。

通過Transaction Annotation控制事務,事務跨度盡量短,把非事務範圍内的業務邏輯剔除到被标注的函數之外。

隻讀事務可以不加事務标注。

連接配接池

選型:

在分庫分表時,根據點對點通信優先的原則,盡量使用用戶端分片的實作。功能不滿足時才用MyCat中央代理。

推薦使用性能最ikariCP,或者Druid,不推薦c3p0與DBCP。

連接配接池的配置:

配置初始值,再聯系DBA獲得線上資料庫支援的連接配接數,計算最大連接配接數。

連接配接有效性檢查,隻在連接配接空閑檢測時執行,不在拿出和歸還連接配接時執行,最好是直接使用資料的Ping方案,不要配置檢查SQL。

根據總是設定逾時的原則,配置擷取連接配接逾時的時間。

配置合理的空閑連接配接回收間隔和空閑時間。

番外篇:在分庫分表時,可考慮基于HikariCP二次開發,減少總的空閑連接配接檢查線程數(比如128個分區,可能有256條線程),重用同一個執行個體上的庫的連接配接等。

4.緩存

4.1 多級緩存

根據緩存原則, 緩存 > 資料庫/遠端調用

根據就近原則, 堆内緩存 > 堆外緩存 > 集中式緩存

堆内緩存受大小限制,并影響GC

堆内緩存與堆外緩存,分布在每一台應用伺服器上,重新整理方式比集中式緩存複雜

堆外緩存與集中式緩存,需要序列化/反序列化對象

集中式緩存,有網絡傳輸的成本,特别是資料超過一個網絡包的大小。

集中式緩存,一次擷取多個鍵時,在有分區的情況下,需要收發多個網絡包。

使用上述條件選擇合适的緩存方案,或同時使用多級緩存,逐層回源。

4.2 綜述

需要對回源進行并發控制,當key失效時,隻有單一線程對該key回源。

基于二進制優于文本資料的原則,JSON的序列化方案較通用與更高的可讀性。而對于較大,結構較複雜的對象,基于Kyro,PB,Thrift的二進制序列化方案的性能更高,見後面的序列化方案部分。

4.3 堆内緩存

推薦Guava Cache。

Ehcache較重,性能也較差。更不要使用存在嚴重bug的Jodd Cache。

GuavaCache:

正确設定并行度等參數。

重載load()參數,實作單一線程回源。

Guava Cache能背景定時重新整理,在重新整理的過程中,依然使用舊資料響應請求,不會造成卡頓,但需要重載實作reload()函數。

Guava Cache同時還支援并發安全版的WeakHashMap。

4.4 堆外緩存

推薦Cassandra的OHC 或者 OpenHFT的Chronical map2。

OHC夠簡單,其實R大不喜歡Chronical,玩的太深,換個JDK都可能跑不起來。

Chronical map3的license則較不友好,複雜度高且要求JDK8。

其他的Ehcache的Terracota Offheap 一向不喜歡。

4.5 Memcached

用戶端:

基于點對點通信優于網關的原則,使用用戶端一緻性哈希分區。

推薦Spymemcached。 XMemcached 太久沒更新,Folsom知名度不高。

注意Spymemcached為單線程單連接配接架構(一個MemcachedClient隻有一條IO線程,與每台Memcached隻有一條連接配接),必要時可多建幾個

手機靓号買号平台

MemcachedClient随機選擇,但不要用Commons Pool去封裝它,把Spy原本的設計一筆抹殺。

根據在合适場景使用并發的原則,Spymemcached支援異步API。

根據一切皆設逾時的原則,可在連接配接工廠中設定最大逾時數,預設值兩秒半太長。

資料結構:

Key必須設定失效時間。

Key必須有長度限制。

Value長度需要控制,以不超過1個網絡包(MTU,千五位元組)為佳。

Value大小差别較大的緩存類型,建議拆分到不同MC叢集,否則會造成低使用率并且産生踢出。

4.6 Redis as Cache

Redis拓撲:

基于點對點通信優于網關的原則,使用如下兩種拓撲

無HA的普通分片:由Jedis用戶端完成分片路由。

Redis Cluster:同樣由Jedis用戶端封裝分區,跳轉,重試等邏輯,需要使用最新版的Jedis版本。

服務端:

Cache節點與持久化資料節點不要混用。

Cache節點是否需要持久化要仔細衡量。

由于Redis是單線程,使用taskset進行cpu綁定後可以有效地利用cpu,并在單機上運作多個redis執行個體。

對熱鍵進行監控,發現不合理的熱健要進行分拆等處理。

Jedis基于Apache Commons Pool進行了多連接配接的封裝,正确配置總連接配接數不超過Redis Server的允許連接配接數。

性能考慮,空閑連接配接檢查不要過于頻繁(建議30秒以上),另不要打開testOnBorrow等測試參數。

根據一切皆有逾時的原則,設定統一的調用逾時,擷取連接配接的最長等待時間參數,重試次數

根據在合适的地方異步的原則,Jedis本身沒有異步API,隻在PipleLine模式下支援。

必須對Key設定失效時間。

Value長度需要控制,不要超過一個網絡包。另外集合的元素不要超過五千個。

除了使用序列化的String,同樣可以考慮用Hash來存儲對象,注意内部結構為ZipList與HashTable時,hmget 與hgetall的不同複雜度。

指令:

慎用的指令:LANGE(0, -1), HGETALL, SMEMBER

高複雜度的指令: ZINTERSTORE, SINTERSTORE, ZUNIONSTORE, ZREM

盡量使用多參數的指令:MGET/MSET,HMGET/HMSET, LPUSH/RPUSH, LRANGE

盡量使用pipeline

根據減少互動的原則,必要時可使用Redis的Lua腳本

5.服務調用

5.1 接口設計

  1. 盡量少互動的原則:

支援批量接口,最大的批量,綜合考慮調用者的需求與 後端存儲的能力。

支援粗粒度接口,在支援原子細粒度接口的同時,支援粗粒度接口/聚合層接口,将多個資料源的擷取,多個動作,合并成一個粗粒度接口。

  1. 盡量少資料的原則:

在提供傳回所有資料的大接口的同時,提供隻提供滿足部分調用者需要的輕量接口。

最好再提供能定制傳回字段的接口。

  1. 二進制資料優于文本資料

同樣是一個簡單通用性,與性能的選擇,特别是大資料量時。

5.2 RESTful

僅以Apache HttpClient為例,大部分Restful架構都是對Apache HttpClient的封裝。

另外OkHttp也值得看看。

不要重複建立ApacheClient執行個體,使用連接配接池,正确配置連接配接池的連接配接數。

連接配接池總是有鎖,針對不同的服務,使用不同的Apache HttpClient執行個體,将鎖分散開來。在高并發時比使用全局單例的ApacheClient,有很大的性能提升。

根據一切調用皆有逾時的原則,每次調用均設定逾時時間。RequestConfig裡共有Connect Timeout, Socket Timout 和 從Pool中擷取連接配接的Timeout三種逾時。

需要異步或并行的場景,使用Apache AsyncHttpClient項目。但要注意AsyncHttpClient項目,檢查調用逾時的周期預設為1秒。

5.3 自家RPC架構

每家的RPC架構特性不同,但考慮點都類似。

6.消息異步

6.1 選型

根據就近原則,可以先嘗試用JVM内的隊列來解決,然後再考慮中央消息系統。

可靠性要求極高的選擇RabbitMQ,可支援單條消息确認。

海量消息場景,允許極端情況下少量丢失則使用Kafka。

6.2 Kafka

在同步和異步之間做好權衡,異步批量發送可以極大的提高發送的速度。

關注消費者如下參數:commitInterval(自動送出offset間隔),prefetchSize(指單次從伺服器批量拉取消息的大小),過大和過小都會影響性能,建議保持預設。

6.3 RabbitMQ

根據擴充性原則,RabbitMQ本身沒有分片功能,但可以在用戶端自行分片。

如非必要情況,應該保持預設的同步發送模式。

關注消費者如下參數:autocommit(自動送出确認,預設false) ,在消息拉取到本地即認為消費成功,而不是真正消費成功後送出。prefetchCount(預取消息條數,預設64條)

生産者在必要時也可以臨時降級不進行confirm。

  1. 日志

7.1 綜述

Log4j2或logback,不要再使用Log4j。

除了應用啟停日志,不允許使用超慢的System.out.println() 或 e.printStack();

嚴格控制日志量避免過高IO,對海量日志,應該有開關可以動态關停。

如果可能出現海量異常資訊,可仿效JDK的優化,用RateLimiter進行限流,丢棄過多的異常日志。

7.2 内容

嚴格控制日志格式,避免出現消耗較大的輸出如類名,方法名,行号等。

業務日志不要濫用toJSONString()來列印對象,盡量使用對象自身的toString()函數,因為JSON轉換的消耗并不低。

在生産環境必定輸出的日志,不要使用logger.info("hello {}", name)的模式,而是使用正确估算大小的StringBuilder直接拼裝輸出資訊。

7.3 異步日志

同步日志的堵塞非常嚴重,特别是發生IO的時候,是以盡量使用異步日志。

Logback的異步方案存在一定問題,需要正确配置Queue長度,閥值達到多少時丢棄Warn以下的日志,最新版還可以設定如果隊列已滿,是等待還是直接丢棄日志。

如果覺得Logback的異步日志每次插入都要詢問隊列容量太過消耗,可重寫一個直接入列,不成功則直接丢棄的版本。

  1. 工具類

8.1 JSON

使用Jackson 或 FastJSON。GSON的性能較前兩者為差,尤其是大對象時。

超大對象可以使用Jackson或FastJSON的流式 API進行處理。

将不需要序列化的屬性,通過Annotation排除掉。

FastJson:

盡量使用最新的版本。

SerializerFeature.DisableCircularReferenceDetect 關閉循環引用檢查。

Jackson:

設定參數,不序列化為空的屬性,等于預設值的屬性。

除了jackson-databinding,可試用簡化版沒那麼多花樣的jackon-jr。

8.2 二進制序列化

需要定義IDL的PB與Thrift,不需要定義的Storm等用的Kyro 都可選擇,其他一些比較舊就算了。

8.3 Bean複制

在VO,BO之間複制時,使用Orika(生成代碼) 或 Dozer(緩存反射),不要使用需要每次進行反射的Apache BeanUitls,Spring BeanUtils。

8.4 日期

JDK的日期類與字元串之間的轉換很慢且非線程安全。

繼續用Java日期不想大動作的,就用CommonsLang的FastDateFormat。

能大動作就用joda time,或者JDK8的新日期API。

9.Java代碼優化 與 業務邏輯優化

參考《Java調優指南1.8版》,對記憶體使用,并發與鎖等方面進行優化。

規則前置,将消耗較大的操作放後面,如果前面的條件不滿足時可。

另外前面提到的一堆原則,比如盡量緩存,盡量少互動,盡量少資料,并行,異步等,都可在此使用。