Micro Service Architecture — Timeout

1 Overview

常見的微服務(wù)架構(gòu):


微服務(wù)架構(gòu)

做好超時(shí)時(shí)間的限定,用于判定超時(shí)后資源能夠及時(shí)被釋放,用于處理其它的請(qǐng)求,從而提升的性能。

  • 前端:Ajax、Node
  • 代理層:DNS、LB(SLB、F5、Keepalived+LVS、Haproxy、A10等)、Ngixn、Gateway
  • 服務(wù)容器:Tomcat、Jetty
  • 中間件:Feign、Dubbo、HTTPClient、ES、MongoDB、Redis
  • 數(shù)據(jù)庫:MySQL、Oracle

2 Solution

2.1 Front-end Timeout

2.1.1 ajax Timeout

? ajax底層使用的是XMLHttpRequest,其超時(shí)參數(shù)可以設(shè)置:連接超時(shí)、讀超時(shí)和寫超時(shí)。但對(duì)于包裝后的ajax,我們通常只需要設(shè)置請(qǐng)求超時(shí)時(shí)間(timeout)即可,具體案例如下:

var ajaxTimeoutTest = $.ajax({
  url:'/demo',
    // 設(shè)置請(qǐng)求超時(shí)時(shí)間(毫秒),此設(shè)置將覆蓋全局設(shè)置
  timeout : 1000,
  type : 'get',
  data :{},
  dataType:'json',
  success:function(data){
    alert("成功");
  },
  complete : function(XMLHttpRequest,status){
      // 超時(shí)處理: status還有success,error等值的情況
      if(status=='timeout'){
       ajaxTimeoutTest.abort();
       alert("超時(shí)");
    }
  }
});

2.1.2 Node.js Timeout

  • Server Timeout
const http = require("http");
const server = http.createServer( function(req, res){
    // ......
});

// 設(shè)置服務(wù)端請(qǐng)求處理的超時(shí)時(shí)間
server.setTimeout(30 * 1000);
server.listen(3000, "localhost", function(){
    console.log("開始監(jiān)聽"+server.address().port+"......");
});
  • Client Timeout
const http = require('http');

const options = {host: 'localhost', method: 'GET', port: 8080, path: '/test'}
var req = http.request(options);

// 設(shè)置客戶端每個(gè)外調(diào)的超時(shí)時(shí)間
req.setTimeout(20 * 1000);
req.on('response', (res) => {
  res.setEncoding('utf8');
  res.on('data', function(chunk){
    console.log('收到數(shù)據(jù):%s', chunk);
  });
  res.on('end', function(){
    console.log(res.trailers);
  });
});
req.end();

2.2 Ngixn Timeout

2.2.1 keepalive_timeout

? HTTP是一種無狀態(tài)協(xié)議,其客戶端底層向服務(wù)器發(fā)送一個(gè)TCP請(qǐng)求,服務(wù)端響應(yīng)完畢后就會(huì)斷開連接。如果客戶端向服務(wù)器發(fā)送多個(gè)請(qǐng)求,每個(gè)請(qǐng)求都要建立各自獨(dú)立的連接以傳輸數(shù)據(jù)。

? HTTP的KeepAlive就用于告訴服務(wù)器在處理完請(qǐng)求后保持一段這個(gè)TCP連接的打開狀態(tài)。若接收到來自客戶端的其它請(qǐng)求,服務(wù)端會(huì)利用這個(gè)未被關(guān)閉的連接,而不需要再建立一個(gè)連接。KeepAlive在一段時(shí)間內(nèi)保持打開狀態(tài),它們會(huì)在這段時(shí)間內(nèi)占用資源,但占用過多就會(huì)影響性能。

? 因此,Nginx使用 keepalive_timeout 來指定KeepAlive的超時(shí)時(shí)間,用于指定每個(gè)TCP 連接最多可以保持多長時(shí)間。Nginx的默認(rèn)值是75 秒,然而有些瀏覽器最多只保持 60秒,所以可以設(shè)定為 60 秒 更安全。若將它設(shè)置為 0,就禁止了 keepalive 連接。

# 配置段: http、server、location, 默認(rèn)值是75秒
keepalive_timeout 60s;

2.2.2 client_body_timeout

? 用于指定客戶端與服務(wù)端建立連接后發(fā)送 request body 的超時(shí)時(shí)間,如果客戶端在指定時(shí)間內(nèi)沒有發(fā)送一個(gè)完整的 request body,Nginx就會(huì)返回 HTTP 408(Request Timed Out)。

# 配置段: http、server、location
client_body_timeout 20s;

2.2.3 client_header_timeout

? 客戶端向服務(wù)端發(fā)送一個(gè)完整的 request header 的超時(shí)時(shí)間,如果客戶端在指定時(shí)間內(nèi)沒有發(fā)送一個(gè)完整的 request header,Nginx 返回 HTTP 408(Request Timed Out)。

# 配置段: http、server、location
client_header_timeout 10s;

2.2.4 proxy_upstream_fail_timeout

? fail_timeout通常是配合max_fails一起來使用的,實(shí)現(xiàn)熔斷隔離的功能。其作用主要是指在 30 秒內(nèi)請(qǐng)求某一應(yīng)用失敗 3 次,則認(rèn)為該應(yīng)用宕機(jī),之后會(huì)等待 30 秒,這期間內(nèi)不會(huì)再把新請(qǐng)求發(fā)送到宕機(jī)應(yīng)用,而是直接發(fā)到正常的那一臺(tái)。時(shí)間到后再有請(qǐng)求進(jìn)來,則繼續(xù)嘗試連接宕機(jī)應(yīng)用且僅嘗試 1 次,如果還是失敗,則繼續(xù)等待 30 秒…...以此循環(huán),直到恢復(fù)。

# 配置段: upstream, fail_timeout默認(rèn)為10s, max_fails默認(rèn)為1
upstream  web_tomcat {
    server 127.0.0.1:8081 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:8082 max_fails=3 fail_timeout=30s;
}

2.2.5 proxy_connect_timeout

? 用于設(shè)置Nginx向后端服務(wù)器的連接超時(shí)時(shí)間,即為發(fā)起TCP握手等候響應(yīng)的超時(shí)時(shí)間。

# 配置段: http、server、location, 默認(rèn)為60s
location / {
    proxy_connect_timeout 500s;
    proxy_pass http://web_tomcat;
 }

2.2.6 proxy_read_timeout

? 連接成功后,等候后端服務(wù)器響應(yīng)時(shí)間,其實(shí)已經(jīng)進(jìn)入后端的排隊(duì)之中等候處理,也可以說是后端服務(wù)器處理請(qǐng)求的 時(shí)間。

# 配置段: http、server、location, 默認(rèn)為60s
location / {
    proxy_read_timeout 500s;
    proxy_pass http://web_tomcat;
 }

2.2.7 proxy_send_timeout

? 用于設(shè)置后端服務(wù)器數(shù)據(jù)回傳時(shí)間,就是在規(guī)定時(shí)間之內(nèi)后端服務(wù)器必須傳完所有的數(shù)據(jù)。

# 配置段: http、server、location, 默認(rèn)為60s
location / {
    proxy_send_timeout 500s;
    proxy_pass http://web_tomcat;
 }

2.2.8 Others

  • resolver_timeout:域名解析超時(shí),默認(rèn)30s。配置段:http、server、location
  • lingering_timeout:設(shè)置TCP連接關(guān)閉時(shí)的SO_LINGER延時(shí),默認(rèn)為5s。配置段:http、server、location
  • tcp_nodelay:默認(rèn)情況下,當(dāng)數(shù)據(jù)發(fā)送時(shí),內(nèi)核并不會(huì)馬上發(fā)送,可能會(huì)等待更多的字節(jié)組成一個(gè)數(shù)據(jù)包,這樣可以提高 I/O 性能,但是在每次只發(fā)送很少字節(jié)的業(yè)務(wù)場(chǎng)景中,等待時(shí)間會(huì)比較長

注意事項(xiàng)

  • 客戶端連接Nginx超時(shí),建議5s內(nèi)
  • proxy_connect_timeout的值不能超過75s
  • 通常client_body_timeout應(yīng)該比keepalive_timeout小

擴(kuò)展:tcp_nodelay與tcp_nopush

  • tcp_nodelay:開啟或關(guān)閉Nginx使用TCP_NODELAY選項(xiàng)的功能
  • tcp_nopush:開啟或者關(guān)閉Nginx在FreeBSD上使用TCP_NOPUSH套接字選項(xiàng)的功能
# tcp_nodelay配置段: http、server、location, 默認(rèn)值為 tcp_nodelay on;
# tcp_nopush配置段: http、server、location, 默認(rèn)值為 tcp_nopush off;
http {
    tcp_nodelay on;
}

2.3 Gateway Timeout

2.3.1 Zuul Timeout

  • 使用Ribbon路由
    Zuul的超時(shí)與Ribbon、Hystrix相關(guān)(RibbonRoutingFilter整合了Hystrix和Ribbon),此時(shí)Zuul的超時(shí)可以配置如下:

    # Hystrix,設(shè)置調(diào)用者等待命令執(zhí)行的超時(shí)限制,超過此時(shí)間,HystrixCommand被標(biāo)記為TIMEOUT,并執(zhí)行回退邏輯
    hystrix.command.xxx.execution.isolation.thread.timeoutInMilliseconds: 1000
    
    # Ribbon
    ribbon:
     read-timeout: 1000
     connect-timeout: 1000
    
  • 未使用Ribbo路由(SimpleHostRoutingFilter整合了Apache HttpClient)

    zuul.routes.xxx.path: /user/**
    zuul.routes.xxx.url: http://localhost:8000/
    # TCP連接超時(shí)時(shí)間
    zuul.host.connect-timeout-millis: 2000
    # Socket超時(shí),即數(shù)據(jù)傳輸?shù)某瑫r(shí)時(shí)間
    zuul.host.socket-timeout-millis: 10000
    

2.4 Middleware Timeout

2.4.1 Ribbon Timeout

全局配置:

ribbon:
    read-timeout: 60000
    connect-timeout: 60000

局部配置:

service-id:
    ribbon:
        read-timeout: 1000
        connect-timeout: 1000

2.4.2 Feign Timeout

? 從Spring Cloud Edgware開始,F(xiàn)eign支持使用屬性配置超時(shí)(對(duì)于老版本,可以寫個(gè)feign.Request.Options 即可):

feign.client.config:
    feign-name:
    connect-timeout: 5000
    read-timeout: 5000

2.4.3 RestTemplate Timeout

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
    factory.setConnectTimeout(1000);
    factory.setReadTimeout(1000);
    return new RestTemplate(factory);
}

2.4.4 Hystrix Timeout

# 默認(rèn)開啟超時(shí)機(jī)制
hystrix.command.default|xxx.execution.timeout.enabled: true
# 是否打開超時(shí)線程中斷, Thread模式有效
hystrix.command.default|xxx.execution.isolation.thread.interruptOnTimeout: true
# 超時(shí)時(shí)間, 默認(rèn)為1秒:
# 1.在THREAD模式下,達(dá)到超時(shí)時(shí)間,可以中斷
# 2.在SEMAPHORE模式下,會(huì)等待執(zhí)行完成后,再去判斷是否超時(shí)
hystrix.command.default|xxx.execution.isolation.thread.timeoutInMilliseconds: 1000

2.4.5 Tomcat Timeout

? tomcat對(duì)每個(gè)請(qǐng)求的超時(shí)時(shí)間是通過connectionTimeout參數(shù)設(shè)置的。默認(rèn)的server.xml里的設(shè)置是20秒,如果不設(shè)置這個(gè)參數(shù)代碼里會(huì)使用60秒。這個(gè)參數(shù)也會(huì)對(duì)POST請(qǐng)求有影響,但并不是指上傳完的時(shí)間限制,而是指兩次數(shù)據(jù)發(fā)送中間的間隔超過connectionTimeout會(huì)被服務(wù)器斷開。

<Connector port="7001" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

? 如果connectionTimeout配置為20000,這個(gè)配置導(dǎo)致建立一個(gè)socket連接后,如果一直沒有收到客戶端的FIN,也沒有數(shù)據(jù)過來,那么此連接也必須等到20s后,才能被超時(shí)釋放。

2.4.6 Dubbo Timeout

? Dubbo協(xié)議超時(shí)實(shí)現(xiàn)使用了Future模式。ResponseFuture.get()在請(qǐng)求還未處理完或未到超時(shí)前一直是wait狀態(tài);響應(yīng)達(dá)到后,設(shè)置請(qǐng)求狀態(tài),并進(jìn)行notify喚醒。即使用了Object的 await-notify-notifyAll 機(jī)制。

Dubbo Architecture

Dubbo消費(fèi)端

  • 全局超時(shí)配置
    <dubbo:consumer timeout="5000" />
    
  • 指定接口以及特定方法超時(shí)配置
    <dubbo:reference interface="com.foo.BarService" timeout="2000">
        <dubbo:method name="sayHello" timeout="3000" />
    </dubbo:reference>
    

Dubbo服務(wù)端

  • 全局超時(shí)配置
    <dubbo:provider timeout="5000" />
    
  • 指定接口以及特定方法超時(shí)配置
    <dubbo:provider interface="com.foo.BarService" timeout="2000">
        <dubbo:method name="sayHello" timeout="3000" />
    </dubbo:provider>
    

2.5 DB Timeout

以下是應(yīng)用(WAS/BLOC)、連接池(DBCP)、Timeout層級(jí)和DBMS直接的關(guān)系圖:


應(yīng)用&數(shù)據(jù)庫間 — Timeout架構(gòu)

解釋說明

  • statement timeout無法處理網(wǎng)絡(luò)連接失敗時(shí)的超時(shí),它能做的僅僅是限制statement的操作時(shí)間
  • 網(wǎng)絡(luò)連接失敗時(shí)的timeout必須交由JDBC來處理
  • JDBC的socket timeout會(huì)受到操作系統(tǒng)socket timeout設(shè)置的影響
  • timeout層級(jí)與DBCP是相互獨(dú)立,DBCP負(fù)責(zé)的是數(shù)據(jù)庫連接的創(chuàng)建和管理,并不干涉timeout的處理
  • 在應(yīng)用中調(diào)用DBCP的getConnection()時(shí),你可以設(shè)置獲取數(shù)據(jù)庫連接的超時(shí)時(shí)間,但是這和JDBC的timeout無關(guān)

案例:JDBC連接會(huì)在網(wǎng)絡(luò)出錯(cuò)后阻塞30分鐘,然后又奇跡般恢復(fù),即使并沒有對(duì)JDBC的socket timeout進(jìn)行設(shè)置

2.5.1 Transaction Timeout

? 一般存在于框架或應(yīng)用級(jí),用于設(shè)置是一個(gè)事務(wù)的執(zhí)行總時(shí)間,其中可能包含多個(gè)statement。在Spring中可以使用XML或在源碼中使用@Transactional注解來進(jìn)行設(shè)置。

  • 1個(gè)statement ~ 0.1s,10w個(gè)statement ~ 1w秒(約7個(gè)小時(shí))
  • 1個(gè)statement × 1個(gè)statement執(zhí)行200ms,則transaction timeout至少應(yīng)該設(shè)置為:1100ms(200×5+100)

2.5.2 Statement Timeout

? 用于設(shè)置單個(gè)statement的執(zhí)行超時(shí)時(shí)間,即Driver等待statement執(zhí)行完成,接收到數(shù)據(jù)的超時(shí)時(shí)間。timeout的值通過調(diào)用JDBC的java.sql.Statement.setQueryTimeout(int timeout) API進(jìn)行設(shè)置,但更多的是是通過框架來進(jìn)行設(shè)置。

注意

  • statement timeout的具體值需要依據(jù)應(yīng)用本身的特性而定,并沒有可供推薦的配置
  • statement的timeout不是整個(gè)查詢的timeout,只是statement執(zhí)行完成并拉取數(shù)據(jù)返回的超時(shí)時(shí)間

MySQL JDBC Statement的QueryTimeout處理過程

MySQL JDBC Statement的QueryTimeout處理過程

解釋說明

  • statement創(chuàng)建一個(gè)新的timeout-execution線程用于超時(shí)處理,5.1版本后改為每個(gè)connection分配一個(gè)timeout-execution線程
  • 達(dá)到超時(shí)時(shí)間,TimerThread調(diào)用JtdsStatement實(shí)例中的TsdCore.cancel()方法,timeout-execution線程創(chuàng)建一個(gè)和statement配置相同的connection,向超時(shí)query發(fā)送:cancel query(KILL QUERY “connectionId”)

2.5.3 JDBC socket timeout

? 用于設(shè)置jdbc I/O socket read and write operations的超時(shí)時(shí)間,防止因網(wǎng)絡(luò)問題或數(shù)據(jù)庫問題,導(dǎo)致Driver會(huì)一直阻塞等待。(建議比statement timeout的時(shí)間長)

  • mysql(單位為毫秒)

    jdbc:mysql://localhost:3306/ag_admin?useUnicode=true&characterEncoding=UTF8&connectTimeout=60000&socketTimeout=60000
    
  • pg(單位為秒)

    jdbc:postgresql://localhost/test?user=fred&password=secret&&connectTimeout=60&socketTimeout=60
    
  • oracle
    ? oracle需要通過oracle.jdbc.ReadTimeout參數(shù)來設(shè)置,連接超時(shí)參數(shù)是oracle.net.CONNECT_TIMEOUT??梢酝ㄟ^以下兩種方式進(jìn)行設(shè)置:

    通過properties設(shè)置

    Class.forName("oracle.jdbc.driver.OracleDriver");
    Properties props = new Properties() ;
    props.put( "user" , "test_schema") ;
    props.put( "password" , "pwd") ;
    props.put( "oracle.net.CONNECT_TIMEOUT" , "10000000") ;
    props.put( "oracle.jdbc.ReadTimeout" , "2000" ) ;
    Connection conn = DriverManager.getConnection( "jdbc:oracle:thin:@127.0.0.1:1521:orcl" , props ) ;
    

    通過環(huán)境變量設(shè)置 —— 注意需要在connection連接之前設(shè)置環(huán)境變量

    String readTimeout = "10000"; // ms
    System.setProperty("oracle.jdbc.ReadTimeout", readTimeout);
    Class.forName("oracle.jdbc.OracleDriver");
    Connection conn = DriverManager.getConnection(jdbcUrl, user, pwd);
    

2.5.4 OS socket timeout

? 這是操作系統(tǒng)級(jí)別的socket設(shè)置,用來檢測(cè)壞死socket連接,Linux一般默認(rèn)2小時(shí)。如果jdbc socket timeout沒有設(shè)置,而OS級(jí)別的socket timeout有設(shè)置,則使用系統(tǒng)的socket timeout值。

# 查看OS的keepalive配置信息
sudo sysctl -a|grep keepalive

# 修改OS的keepalive配置信息,并修改以下配置信息
vim /etc/sysctl.conf

# 表示TCP連接在多少秒之后沒有數(shù)據(jù)報(bào)文傳輸時(shí)啟動(dòng)探測(cè)報(bào)文(發(fā)送空的報(bào)文),單位為秒(s)
net.ipv4.tcp_keepalive_time = 7200
# 表示前一個(gè)探測(cè)報(bào)文和后一個(gè)探測(cè)報(bào)文之間的時(shí)間間隔,單位為秒(s)
net.ipv4.tcp_keepalive_intvl = 75
# 表示探測(cè)的次數(shù)
net.ipv4.tcp_keepalive_probes = 9

# 讓修改的參數(shù)即時(shí)生效
sysctl -p

總結(jié)
? jdbc的socketTimeout值的設(shè)置要非常小心,不同數(shù)據(jù)庫的jdbc driver設(shè)置不一樣,特別是使用不同連接池的話,設(shè)置也可能不盡相同。對(duì)于嚴(yán)重依賴數(shù)據(jù)庫操作的服務(wù)來說,非常有必要設(shè)置這個(gè)值,否則萬一網(wǎng)絡(luò)或數(shù)據(jù)庫異常,會(huì)導(dǎo)致服務(wù)線程一直阻塞在java.net.SocketInputStream.socketRead0。

  • 如果查詢數(shù)據(jù)多,則會(huì)導(dǎo)致該線程持有的data list不能釋放,相當(dāng)于內(nèi)存泄露,最后導(dǎo)致OOM
  • 如果請(qǐng)求數(shù)據(jù)庫操作很多且阻塞住了,會(huì)導(dǎo)致服務(wù)器可用的woker線程變少,嚴(yán)重則會(huì)導(dǎo)致服務(wù)不可用

3 Practice

3.1 Focus

各層組件的超時(shí)時(shí)間,主要是設(shè)置以下兩個(gè)參數(shù):

  • connectTimeout
  • socketTimeout

當(dāng)然針對(duì)特殊的場(chǎng)景,則可以設(shè)置更詳細(xì)的超時(shí)參數(shù),如:

  • readTimeout
  • writeTimeout

3.2 Suggest

ajax —— 5s ~ 60s

  • 建議全局設(shè)置一個(gè)統(tǒng)一的超時(shí)時(shí)間,如60s
    • 從使用的互聯(lián)網(wǎng)產(chǎn)品來看,一般網(wǎng)絡(luò)較差時(shí),加載網(wǎng)頁可能需要等待30秒或1分鐘左右后才出現(xiàn)網(wǎng)絡(luò)異常等的情況
  • 特殊場(chǎng)景自定義設(shè)置超時(shí)時(shí)間,從而覆蓋全局超時(shí)時(shí)間
    • 如上傳較大文件時(shí),則可以設(shè)置時(shí)間更長(當(dāng)然太大的文件,則建議單獨(dú)考慮,如分塊處理等)
    • 如實(shí)時(shí)性要求較高的場(chǎng)景,則可以設(shè)置更短,如5s等

Ngixn

  • 建議設(shè)置 keepalived_time 來提高Ngixn支持的并發(fā)能力與復(fù)用HTTP建立的TCP連接,如設(shè)置為5s
  • 建議設(shè)置 client_body_timeoutclient_header_timeout,用于防止客戶攻擊Dos攻擊,如分別20s、10s
  • 建議設(shè)置 max_failsfail_timeout ,解決每次請(qǐng)求宕機(jī)服務(wù)端時(shí),都需要等待超時(shí)問題,如分別為3次、30s
  • 建議設(shè)置 proxy_connect_timeout、proxy_send_timeoutproxy_read_timeout 參數(shù),用于控制Ngixn轉(zhuǎn)發(fā)到后臺(tái)的超時(shí)控制

Node
使用Node作為網(wǎng)關(guān)代理轉(zhuǎn)發(fā)請(qǐng)求時(shí):

  • Server —— 60s
    • 如果代理層有一定的功能邏輯,則建議加上Server的處理超時(shí)時(shí)間
    • 如果代理層幾乎沒有邏輯,則Server層的超時(shí)可以不配置
  • Client
    Client用于代理轉(zhuǎn)發(fā),而后端業(yè)務(wù)場(chǎng)景不同,要求也有所不同,所以建議設(shè)置較長的默認(rèn)值,并支持請(qǐng)求自定義

Middleware
Ribbon、Zuul(Apache HTTPClient)、Feign、RestTemplate和Netty等,都建議必須設(shè)置以下兩個(gè)參數(shù):

  • connectTimeout
  • socketTimeout

Hystrix
使用Hystrix時(shí),建議設(shè)置提交線程后的等待超時(shí)時(shí)間:thread.timeoutInMilliseconds ,默認(rèn)為1000ms

DB

  • Transaction Timeout
  • connectTimeout
  • socketTimeout
  • OS socket timeout

3.3 Timeout Architecture

微服務(wù)超時(shí)架構(gòu)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容