我們現在介面的線上問題主要有三個,第一:啟動時有些機器會有短暫的線程池滿。第二:併發量上不去,怕服務被打死,不敢調高限流閾值。第三:499超時現象。 今天已上線 今天終於把那天說的全量執行時間延長,從圖中可以看到,中午12點發版之後,記憶體使用率有明顯下降,晚上是介面調用高峰,會有上浮,但是總體來看還 ...
我們現在介面的線上問題主要有三個,第一:啟動時有些機器會有短暫的線程池滿。第二:併發量上不去,怕服務被打死,不敢調高限流閾值。第三: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命令。需指定節點 * 編碼:靜兒([email protected]) * 時間: 2017.08.01 * ********************************************************************************************************* * 修改歷史 * ********************************************************************************************************* * 修改者 修改內容 修改時間 * 靜兒([email protected]) 新建 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); }