接口性能優化方案及其理論依據


  我們現在接口的線上問題主要有三個,第一:啟動時有些機器會有短暫的線程池滿。第二:並發量上不去,怕服務被打死,不敢調高限流閾值。第三:499超時現象。

今天已上線

  今天終於把那天說的全量執行時間延長,從圖中可以看到,中午12點發版之后,內存使用率有明顯下降,晚上是接口調用高峰,會有上浮,但是總體來看還是下降了。

 

 再來看看上線后的gc情況。目前是高峰期,gc控制在十幾秒一次,minor gc得到了控制。

  但是這個調優對線上問題一個也不能說是解決,具體真正的作用要靠觀察。期望的效果是高峰時cpu峰值不會猛增。這樣就可以考慮提高限流閾值了。第一個問題,啟動時有些機器會有短暫的線程池滿。今天發版倒是沒有發生,原因是我將dubbo的接口暴露時間延長到66秒。但是8台provider都沒有發生線程池滿,說明我分析出問題的原因是對的。

接下來的方案

  1>將servlet加載本地緩存改為spring task方式(個人時間和精力問題,如果部門內部可以找到人和我一起做就做,否則視情況而定)

  上次開會我說單開一個servlet來加載本地緩存開銷很大,不合理,應該改為spring task方式。德偉男神說別的部門也有這么做的。阿里的陽哥也說servlet沒問題。好吧,牛人都這么說,我自己思維邏輯沒組織好之前就不和你們爭辯了。今天我梳理好了,來具體說一說。

  單看servlet的源碼,除了初始化過程,其他的過程似乎是有請求過來才調用, 沒有請求就不調用,開銷不是很大。但是請不要忽略了servlet放到resin這個servlet容器之后的生命周期。servlet容器是基於觀察者模式設計的,所有的容器都會繼承Lifecycle接口,它管理着容器的整個生命周期,所有容器的修改和狀態的改變都會由它去通知已經注冊的觀察者(Listener)。所以這個開銷在這里,我只是想執行一個定時任務,容器卻給了一個專門的監聽線程來監聽。

  當時用這個來做,我猜測開發者的意圖是想一開始就先加載這個,盡量預加載。我測試過,spring task是spring初始化一完成就開始執行。我單用new Thread().start和spring task同時初始化一個map。誰先執行到是隨機的。所以,大可不必擔心。

  但是如果今天的調整,cpu使用得到控制,瓶頸不在這里的話,這個不是當務之急。

       2>將增量和全量從數據庫定時拉取更新本地緩存的方式改為單獨后台任務更新redis緩存,provider服務初始從redis取數據,只一個阻塞線程監聽redis訂閱事件來增量更新本地緩存(個人時間和精力問題,如果部門內部可以找到人和我一起做就做,否則視情況而定)

  這個我已經在離線項目中進行了性能測試。至少可以將初始加載速度提高10倍,但不穩定,主要取決於網絡和網跳network hoops。

  根據數據的量級,主要細分為兩種方案:

  • 針對總數為500條以下的小數據,本地緩存可以直接采用redis的哈希結構。單有一個key保存最后更新時間(數據庫的unix timestamp,采用服務器時間,各個服務器之間時間不同步就不准了)。后台定時更新redis哈希內的數據。有數據變更時,更新最后更新時間,發一個發布消息帶有更新的id,通知更新。provider服務初始啟動時獲取redis中最后更新時間和哈希數據,直接反序列化整個redis哈希結構,減少了數據庫操作(這個也是需要字符串反序列化成對象的,持久層容器實現)和將list轉成map的時間和cpu操作。看到服務重啟cpu瞬間峰值,長成下面的樣子。provider運行時收到更新,從redis中獲取此最新的ID值更新本地緩存。

  • 針對500條以上的大數據,區別在於此時redis哈希結構效率極低,直接用我自己存成帶壓縮的二進制方式(此帶壓縮的二進制序列化和反序列化方法我上傳到了epiphany項目中,歡迎大家體驗我的開源項目:http://github.com/xiexiaojing/epiphany)進行序列化和反序列化整個全量。provider運行時收到更新,然后可以去數據庫取本地的最后更新時間到當前時間的增量,更新本地緩存。下面是一個2w多個鍵的本地緩存數據,可以看到其數據庫操作時間就已經遠遠超過從redis中取數據的時間。

  但是如果今天的調整,cpu使用得到控制,瓶頸不在這里的話,這個不是當務之急。

  3>dubbo 業務線程數減小

   我看到provider服務的dubbo業務線程數設置了800,而dubbo默認是200,一般的項目也就是100。我很理解,感性上去想業務線程數越大不是處理能力越強嗎?其實還真不是。前段時間我在想,為什么我們一般的499超時都是單台單台的報,不是幾台一起報,按理說平均每台1k多的QPS,采用的是random負載均衡策略不應該是這個效果。問題就在這里了。如果我們設置了業務線程數是200,因為我們的iothread數是0,那么來了800個請求其他600個請求就會分到其他服務器上去。而我們設置800個線程,壓力都集中在這一個上面,負載均衡策略沒有很好的發揮作用。還有就是線程數多了,處理能力下降,響應時間會變慢。處理能力為啥會下降呢?線程的上下文切換啊。而且各種資源消耗都很大。如果降低這個業務線程數,還可以增加棧內存和TLAB,提高處理速度。棧內存為啥能提高處理速度?棧內分配,逃逸分析,內聯優化等等。TLAB是線程本地分配緩存,是新生代的一小塊區域,大小可調。是線程專享的。不加鎖,速度快。這個做了,對499會有顯著效果。

還需要考慮的

   接口還存在一些memcached超時。這就是為什么我搭建了一套redis集群。需要對緩存用數據庫和redis做壓力分散。我在弄一個叫cloudrise的開源項目是spring data redis的升級版,要實現的主要目標是減少network hoops和集成一些高效的序列化和反序列化方法,如protobuf。另外還考慮在redis前加一個類似於memcached的moxi代理的本地緩存和管道key合並提高效率。項目地址是:https://github.com/xiexiaojing/cloudrise。現在還沒有完全實現,但是各種command接口有很詳細的中文注釋,我覺得用這個代碼來學習redis比我現在看過的redis的書要好很多。介紹的很詳細。行到水窮處,坐看雲起時,歡迎使用cloudrise~~

package com.brmayi.cloudrise.connection.commands;

import java.util.List;
import java.util.Properties;

import com.brmayi.cloudrise.connection.RedisClusterNode;
import com.brmayi.cloudrise.core.types.RedisClientInfo;
/**
 * <pre>
 * *********************************************************************************************************
 * 版權所有(C) 2017 cloudrise 
 * 保留所有權利。
 *            .==.       .==.
 *           //'^\\     //^'\\
 *          // ^^\(\__/)/^ ^^\\
 *         //^ ^^ ^/6  6\ ^^^ \\
 *        //^ ^^ ^/( .. )\^ ^^ \\
 *       // ^^  ^/\|v""v|/\^^ ^ \\
 *      // ^^/\/  / '~~' \ \/\^ ^\\
 *      ----------------------------------------
 *      HERE BE DRAGONS WHICH CAN CREATE MIRACLE
 * *********************************************************************************************************
 * 基本信息
 * *********************************************************************************************************
 * 系統:cloudrise
 * 支持:jdk1.7及以上 redis2.6.0及以上
 * 模塊:cloudrise commands
 * 功能:提供對Redis的服務器操作抽象(所有的key和value都不能為null),每一個方式對應於一個redis命令。需指定節點
 * 編碼:靜兒(xiexiaojing@qq.com)
 * 時間: 2017.08.01
 * *********************************************************************************************************
 * 修改歷史
 * *********************************************************************************************************
 * 修改者                            									           修改內容                      修改時間 
 * 靜兒(987489055@qq.com)                            新建                             2017.08.01
 * *********************************************************************************************************
 * </pre>
 */
public interface RedisClusterServerCommands extends RedisServerCommands {


	/**
	 * 執行一個 AOF文件 重寫操作。重寫會創建一個當前 AOF 文件的體積優化版本。
	 * 即使 BGREWRITEAOF 執行失敗,也不會有任何數據丟失,因為舊的 AOF 文件在 BGREWRITEAOF 成功之前不會被修改。
	 * 重寫操作只會在沒有其他持久化工作在后台執行時被觸發,也就是說:
	 * 如果 Redis 的子進程正在執行快照的保存工作,那么 AOF 重寫的操作會被預定(scheduled),等到保存工作完成之后再執行 AOF 重寫。
	 * 在這種情況下, BGREWRITEAOF 的返回值仍然是 OK ,但還會加上一條額外的信息,說明 BGREWRITEAOF 要等到保存操作完成之后才能執行。
	 * 在 Redis 2.6 或以上的版本,可以使用 INFO 命令查看 BGREWRITEAOF 是否被預定。
	 * 如果已經有別的 AOF 文件重寫在執行,那么 BGREWRITEAOF 返回一個錯誤,並且這個新的 BGREWRITEAOF 請求也不會被預定到下次執行。
	 * 從 Redis 2.4 開始, AOF 重寫由 Redis 自行觸發, BGREWRITEAOF 僅僅用於手動觸發重寫操作。
	 * 時間復雜度:O(N), N 為要追加到 AOF 文件中的數據數量。
	 * @param node 節點
	 * @since 1.3
	 * @see <a href="http://redis.io/commands/bgrewriteaof">Redis Documentation: BGREWRITEAOF</a>
	 */
	void bgReWriteAof(RedisClusterNode node);

	/**
	 * 在后台異步(Asynchronously)保存當前數據庫的數據到磁盤。
	 * BGSAVE 命令執行之后立即返回 OK ,然后 Redis fork 出一個新子進程,原來的 Redis 進程(父進程)繼續處理客戶端請求,而子進程則負責將數據保存到磁盤,然后退出。
	 * 客戶端可以通過 LASTSAVE 命令查看相關信息,判斷 BGSAVE 命令是否執行成功。
	 * @param node 節點
	 * @see <a href="http://redis.io/commands/bgsave">Redis Documentation: BGSAVE</a>
	 */
	void bgSave(RedisClusterNode node);


	/**
	 * 返回最近一次 Redis 成功將數據保存到磁盤上的時間,以 UNIX 時間戳格式表示。
	 * 時間復雜度:O(1)
	 * @param node 節點
	 * @return 一個 UNIX 時間戳。
	 * @see <a href="http://redis.io/commands/lastsave">Redis Documentation: LASTSAVE</a>
	 */
	Long lastSave(RedisClusterNode node);


	/**
	 * SAVE 命令執行一個同步保存操作,將當前 Redis 實例的所有數據快照(snapshot)以 RDB 文件的形式保存到硬盤。
	 * 一般來說,在生產環境很少執行 SAVE 操作,因為它會阻塞所有客戶端,保存數據庫的任務通常由 BGSAVE 命令異步地執行。
	 * 然而,如果負責保存數據的后台子進程不幸出現問題時, SAVE 可以作為保存數據的最后手段來使用。
	 * 時間復雜度:O(N), N 為要保存到數據庫中的 key 的數量。
	 * @param node 節點
	 * @see <a href="http://redis.io/commands/save">Redis Documentation: SAVE</a>
	 */
	void save(RedisClusterNode node);


	/**
	 * 返回當前數據庫的 key 的數量。
	 * 時間復雜度:O(1)
	 * @param node 節點
	 * @return 返回當前數據庫的 key 的數量。
	 * @see <a href="http://redis.io/commands/dbsize">Redis Documentation: DBSIZE</a>
	 */
	Long dbSize(RedisClusterNode node);


	/**
	 * 清空當前數據庫中的所有 key。此命令從不失敗。
	 * 時間復雜度:O(1)
	 * @param node 節點
	 * @see <a href="http://redis.io/commands/flushdb">Redis Documentation: FLUSHDB</a>
	 */
	void flushDb(RedisClusterNode node);


	/**
	 * 清空整個 Redis 服務器的數據(刪除所有數據庫的所有 key )。此命令從不失敗。
	 * @param node 節點
	 * @see <a href="http://redis.io/commands/flushall">Redis Documentation: FLUSHALL</a>
	 */
	void flushAll(RedisClusterNode node);


	/**
	 * Redis Info 命令以一種易於理解和閱讀的格式,返回關於 Redis 服務器的各種信息和統計數值。
	 * 命令返回信息示例:
	 * <pre>
	 * # Server
	 * redis_version:3.2.9
	 * redis_git_sha1:00000000
	 * redis_git_dirty:0
	 * redis_build_id:a6f5b91b81bb8d8c
	 * redis_mode:cluster
	 * os:Linux 2.6.32-926.504.30.3.letv.el6.x86_64 x86_64
	 * arch_bits:64
	 * multiplexing_api:epoll
	 * gcc_version:4.4.7
	 * process_id:17612
	 * run_id:5644a791339db3e67cfb4ad4c529fee624e83326
	 * tcp_port:6379
	 * uptime_in_seconds:1299970
	 * uptime_in_days:15
	 * hz:10
	 * lru_clock:8405316
	 * executable:/letv/apps_install/redis-3.2.9/redis0/./redis-server
	 * config_file:/letv/apps_install/redis-3.2.9/redis0/redis.conf
	 * # Clients
	 * connected_clients:5
	 * client_longest_output_list:0
	 * client_biggest_input_buf:0
	 * blocked_clients:0
	 * 
	 * # Memory
	 * used_memory:1452392
	 * used_memory_human:1.39M
	 * used_memory_rss:3137536
	 * used_memory_rss_human:2.99M
	 * used_memory_peak:1491800
	 * used_memory_peak_human:1.42M
	 * total_system_memory:135210921984
	 * total_system_memory_human:125.92G
	 * used_memory_lua:37888
	 * used_memory_lua_human:37.00K
	 * maxmemory:0
	 * maxmemory_human:0B
	 * maxmemory_policy:noeviction
	 * mem_fragmentation_ratio:2.16
	 * mem_allocator:jemalloc-4.0.3
	 * 
	 * # Persistence
	 * loading:0
	 * rdb_changes_since_last_save:0
	 * rdb_bgsave_in_progress:0
	 * rdb_last_save_time:1500277631
	 * rdb_last_bgsave_status:ok
	 * rdb_last_bgsave_time_sec:0
	 * rdb_current_bgsave_time_sec:-1
	 * aof_enabled:1
	 * aof_rewrite_in_progress:0
	 * aof_rewrite_scheduled:0
	 * aof_last_rewrite_time_sec:0
	 * aof_current_rewrite_time_sec:-1
	 * aof_last_bgrewrite_status:ok
	 * aof_last_write_status:ok
	 * aof_current_size:25302
	 * aof_base_size:25302
	 * aof_pending_rewrite:0
	 * aof_buffer_length:0
	 * aof_rewrite_buffer_length:0
	 * aof_pending_bio_fsync:0
	 * aof_delayed_fsync:0
	 * 
	 * # Stats
	 * total_connections_received:15305
	 * total_commands_processed:86898
	 * instantaneous_ops_per_sec:0
	 * total_net_input_bytes:2016530
	 * total_net_output_bytes:859156
	 * instantaneous_input_kbps:0.00
	 * instantaneous_output_kbps:0.00
	 * rejected_connections:0
	 * sync_full:0
	 * sync_partial_ok:0
	 * sync_partial_err:0
	 * expired_keys:0
	 * evicted_keys:0
	 * keyspace_hits:0
	 * keyspace_misses:0
	 * pubsub_channels:0
	 * pubsub_patterns:0
	 * latest_fork_usec:292
	 * migrate_cached_sockets:0
	 * 
	 * # Replication
	 * role:slave
	 * master_host:10.127.95.184
	 * master_port:6381
	 * master_link_status:down
	 * master_last_io_seconds_ago:-1
	 * master_sync_in_progress:0
	 * slave_repl_offset:1
	 * master_link_down_since_seconds:1298428
	 * slave_priority:100
	 * slave_read_only:1
	 * connected_slaves:0
	 * master_repl_offset:0
	 * repl_backlog_active:0
	 * repl_backlog_size:1048576
	 * repl_backlog_first_byte_offset:0
	 * repl_backlog_histlen:0
	 * 
	 * # CPU
	 * used_cpu_sys:1273.56
	 * used_cpu_user:715.02
	 * used_cpu_sys_children:0.00
	 * used_cpu_user_children:0.00
	 * 
	 * # Cluster
	 * cluster_enabled:1
	 * 
	 * # Keyspace
	 * db0:keys=6,expires=0,avg_ttl=0
	 * </pre>
	 * @param node 節點
	 * @return 信息如上
	 * @see <a href="http://redis.io/commands/info">Redis Documentation: INFO</a>
	 */
	Properties info(RedisClusterNode node);


	/**
	 * Load server information for given {@code selection}.
	 * 命令示例如下:
	 * <pre>
	 * 10.183.96.194:6379> info memory
	 * </pre>
	 * 命令返回結果如下:
	 * # Memory
	 * used_memory:1452392
	 * used_memory_human:1.39M
	 * used_memory_rss:3137536
	 * used_memory_rss_human:2.99M
	 * used_memory_peak:1491800
	 * used_memory_peak_human:1.42M
	 * total_system_memory:135210921984
	 * total_system_memory_human:125.92G
	 * used_memory_lua:37888
	 * used_memory_lua_human:37.00K
	 * maxmemory:0
	 * maxmemory_human:0B
	 * maxmemory_policy:noeviction
	 * mem_fragmentation_ratio:2.16
	 * mem_allocator:jemalloc-4.0.3
	 * @param node 節點
	 * @return 信息如上
	 * @see <a href="http://redis.io/commands/info">Redis Documentation: INFO</a>
	 */
	Properties info(RedisClusterNode node, String section);


	/**
	 * SHUTDOWN 命令執行以下操作:
	 * 停止所有客戶端
	 * 如果有至少一個保存點在等待,執行 SAVE 命令
	 * 如果 AOF 選項被打開,更新 AOF 文件
	 * 關閉 redis 服務器(server)
	 * 如果持久化被打開的話, SHUTDOWN 命令會保證服務器正常關閉而不丟失任何數據。
	 * 
	 * 另一方面,假如只是單純地執行 SAVE 命令,然后再執行 QUIT 命令,則沒有這一保證 —— 因為在執行 SAVE 之后、執行 QUIT 之前的這段時間中間,其他客戶端可能正在和服務器進行通訊,這時如果執行 QUIT 就會造成數據丟失。
	 * 
	 * SAVE 和 NOSAVE 修飾符
	 * 通過使用可選的修飾符,可以修改 SHUTDOWN 命令的表現。比如說:
	 * 執行 SHUTDOWN SAVE 會強制讓數據庫執行保存操作,即使沒有設定(configure)保存點
	 * 執行 SHUTDOWN NOSAVE 會阻止數據庫執行保存操作,即使已經設定有一個或多個保存點(你可以將這一用法看作是強制停止服務器的一個假想的 ABORT 命令)
	 * @param node 節點
	 * @see <a href="http://redis.io/commands/shutdown">Redis Documentation: SHUTDOWN</a>
	 */
	void shutdown(RedisClusterNode node);


	/**
	 * CONFIG GET 命令用於取得運行中的 Redis 服務器的配置參數(configuration parameters),
	 * 在 Redis 2.4 版本中, 有部分參數沒有辦法用 CONFIG GET 訪問,但是在最新的 Redis 2.6 版本中,所有配置參數都已經可以用 CONFIG GET 訪問了。
	 * CONFIG GET 接受單個參數 parameter 作為搜索關鍵字,查找所有匹配的配置參數,其中參數和值以“鍵-值對”(key-value pairs)的方式排列。
	 * 比如執行 CONFIG GET s* 命令,服務器就會返回所有以 s 開頭的配置參數及參數的值:
	 *
	 * @param pattern 不能為空
	 * @return 結果示例如下:
	 * <pre>
	 *  1) "slave-announce-ip"
	 *  2) ""
	 *  3) "set-max-intset-entries"
	 *  4) "512"
	 *  5) "slowlog-log-slower-than"
	 *  6) "10000"
	 *  7) "slowlog-max-len"
	 *  8) "128"
	 *  9) "slave-priority"
	 * 10) "100"
	 * 11) "slave-announce-port"
	 * 12) "0"
	 * 13) "slave-serve-stale-data"
	 * 14) "yes"
	 * 15) "slave-read-only"
	 * 16) "yes"
	 * 17) "stop-writes-on-bgsave-error"
	 * 18) "yes"
	 * 19) "supervised"
	 * 20) "no"
	 * 21) "syslog-facility"
	 * 22) "local0"
	 * 23) "save"
	 * 24) "900 1 300 10 60 10000"
	 * 25) "slaveof"
	 * 26) "10.127.95.184 6381"
	 * </pre>
	 * @param node 節點
	 * @see <a href="http://redis.io/commands/config-get">Redis Documentation: CONFIG GET</a>
	 */
	Properties getConfig(RedisClusterNode node, String pattern);


	/**
	 * CONFIG SET 命令可以動態地調整 Redis 服務器的配置(configuration)而無須重啟。
	 * 你可以使用它修改配置參數,或者改變 Redis 的持久化(Persistence)方式。
	 * CONFIG SET 可以修改的配置參數可以使用命令 CONFIG GET * 來列出,所有被 CONFIG SET 修改的配置參數都會立即生效。
	 * @param node 節點
	 * @param param 參數
	 * @param value 要設定的值
	 * @see <a href="http://redis.io/commands/config-set">Redis Documentation: CONFIG SET</a>
	 */
	void setConfig(RedisClusterNode node, String param, String value);


	/**
	 * 重置 INFO 命令中的某些統計數據,包括:
	 * Keyspace hits (鍵空間命中次數)
	 * Keyspace misses (鍵空間不命中次數)
	 * Number of commands processed (執行命令的次數)
	 * Number of connections received (連接服務器的次數)
	 * Number of expired keys (過期key的數量)
	 * Number of rejected connections (被拒絕的連接數量)
	 * Latest fork(2) time(最后執行 fork(2) 的時間)
	 * The aof_delayed_fsync counter(aof_delayed_fsync 計數器的值)
	 * @param node 節點
	 * @see <a href="http://redis.io/commands/config-resetstat">Redis Documentation: CONFIG RESETSTAT</a>
	 */
	void resetConfigStats(RedisClusterNode node);


	/**
	 * 返回當前服務器時間。
	 * @param node 節點
	 * @return TIME命令是一個包含兩個字符串的列表: 第一個字符串是當前時間(以 UNIX 時間戳格式表示),而第二個字符串是當前這一秒鍾已經逝去的微秒數。返回值是前一個
	 * @since 1.1
	 * @see <a href="http://redis.io/commands/time">Redis Documentation: TIME</a>
	 */
	Long time(RedisClusterNode node);

	/**
	 * 以人類可讀的格式,返回所有連接到服務器的客戶端信息和統計數據。
	 * @param node 節點
	 * @return 所有連接到服務器的客戶端信息和統計數據。
	 * @since 1.3
	 * @see <a href="http://redis.io/commands/client-list">Redis Documentation: CLIENT LIST</a>
	 */
	List<RedisClientInfo> getClientList(RedisClusterNode node);
}

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM