一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|JavaScript|易語言|

服務器之家 - 編程語言 - Java教程 - Spring Cloud Ribbon的踩坑記錄與原理詳析

Spring Cloud Ribbon的踩坑記錄與原理詳析

2021-06-06 14:16aCoder2013 Java教程

這篇文章主要給大家介紹了關于Spring Cloud Ribbon踩坑記錄與原理的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧

簡介

spring cloud ribbon 是一個基于http和tcp的客服端負載均衡工具,它是基于netflix ribbon實現的。它不像服務注冊中心、配置中心、api網關那樣獨立部署,但是它幾乎存在于每個微服務的基礎設施中。包括前面的提供的聲明式服務調用也是基于該ribbon實現的。理解ribbon對于我們使用spring cloud來講非常的重要,因為負載均衡是對系統的高可用、網絡壓力的緩解和處理能力擴容的重要手段之一。在上節的例子中,我們采用了聲明式的方式來實現負載均衡。實際上,內部調用維護了一個resttemplate對象,該對象會使用ribbon的自動化配置,同時通過@loadbalanced開啟客戶端負載均衡。其實resttemplate是spring自己提供的對象,不是新的內容。讀者不知道resttemplate可以查看相關的文檔。

現象

前兩天碰到一個ribbon相關的問題,覺得值得記錄一下。表象是對外的接口返回內部異常,這個是封裝的統

一錯誤信息,spring的異常處理器catch到未捕獲異常統一返回的信息。因此到日志平臺查看實際的異常:

org.springframework.web.client.httpclienterrorexception: 404 null

這里介紹一下背景,出現問題的開放網關,做點事情說白了就是轉發對應的請求給后端的服務。這里用到了ribbon去做服務負載均衡、eureka負責服務發現。

這里出現404,首先看了下請求的url以及對應的參數,都沒有發現問題,對應的后端服務也沒有收到請求。這就比較詭異了,開始懷疑是ribbon或者eureka的緩存導致請求到了錯誤的ip或端口,但由于日志中打印的是eureka的serviceid而不是實際的ip:port,因此先加了個日志:

?
1
2
3
4
5
6
7
8
9
@slf4j
public class customhttprequestinterceptor implements clienthttprequestinterceptor {
 
 @override
 public clienthttpresponse intercept(httprequest request, byte[] body, clienthttprequestexecution execution) throws ioexception {
  log.info("request , url:{},method:{}.", request.geturi(), request.getmethod());
  return execution.execute(request, body);
 }
}

這里是通過給resttemplate添加攔截器的方式,但要注意,ribbon也是通過給resttemplate添加攔截器實現的解析serviceid到實際的ip:port,因此需要注意下優先級添加到ribbon的 loadbalancerinterceptor 之后,我這里是通過spring的初始化完成事件的回調中添加的,另外也添加了另一條日志,在catch到這個異常的時候,利用eureka的 discoveryclient#getinstances 獲取到當前的實例信息。

之后在測試環境中復現了這個問題,看了下日志,eurek中緩存的實例信息是對的,但是實際調用的確實另外一個服務的地址,從而導致了接口404。

源碼解析

從上述的信息中可以知道,問題出在ribbon中,具體的原因后面會說,這里先講一下spring cloud ribbon的初始化流程。

?
1
2
3
4
5
6
7
8
@configuration
@conditionalonclass({ iclient.class, resttemplate.class, asyncresttemplate.class, ribbon.class})
@ribbonclients
@autoconfigureafter(name = "org.springframework.cloud.netflix.eureka.eurekaclientautoconfiguration")
@autoconfigurebefore({loadbalancerautoconfiguration.class, asyncloadbalancerautoconfiguration.class})
@enableconfigurationproperties({ribboneagerloadproperties.class, serverintrospectorproperties.class})
public class ribbonautoconfiguration {
}

注意這個注解 @ribbonclients , 如果想要覆蓋spring cloud提供的默認ribbon配置就可以使用這個注解,最終的解析類是:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class ribbonclientconfigurationregistrar implements importbeandefinitionregistrar {
 
 @override
 public void registerbeandefinitions(annotationmetadata metadata,
 beandefinitionregistry registry) {
 map<string, object> attrs = metadata.getannotationattributes(
 ribbonclients.class.getname(), true);
 if (attrs != null && attrs.containskey("value")) {
 annotationattributes[] clients = (annotationattributes[]) attrs.get("value");
 for (annotationattributes client : clients) {
 registerclientconfiguration(registry, getclientname(client),
  client.get("configuration"));
 }
 }
 if (attrs != null && attrs.containskey("defaultconfiguration")) {
 string name;
 if (metadata.hasenclosingclass()) {
 name = "default." + metadata.getenclosingclassname();
 } else {
 name = "default." + metadata.getclassname();
 }
 registerclientconfiguration(registry, name,
  attrs.get("defaultconfiguration"));
 }
 map<string, object> client = metadata.getannotationattributes(
 ribbonclient.class.getname(), true);
 string name = getclientname(client);
 if (name != null) {
 registerclientconfiguration(registry, name, client.get("configuration"));
 }
 }
 
 private string getclientname(map<string, object> client) {
 if (client == null) {
 return null;
 }
 string value = (string) client.get("value");
 if (!stringutils.hastext(value)) {
 value = (string) client.get("name");
 }
 if (stringutils.hastext(value)) {
 return value;
 }
 throw new illegalstateexception(
 "either 'name' or 'value' must be provided in @ribbonclient");
 }
 
 private void registerclientconfiguration(beandefinitionregistry registry,
 object name, object configuration) {
 beandefinitionbuilder builder = beandefinitionbuilder
 .genericbeandefinition(ribbonclientspecification.class);
 builder.addconstructorargvalue(name);
 builder.addconstructorargvalue(configuration);
 registry.registerbeandefinition(name + ".ribbonclientspecification",
 builder.getbeandefinition());
 }
}

atrrs包含defaultconfiguration,因此會注冊ribbonclientspecification類型的bean,注意名稱以 default. 開頭,類型是ribbonautoconfiguration,注意上面說的ribbonautoconfiguration被@ribbonclients修飾。

然后再回到上面的源碼:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
public class ribbonautoconfiguration {
  
 
  //上文中會解析被@ribbonclients注解修飾的類,然后注冊類型為ribbonclientspecification的bean。
  //主要有兩個: ribbonautoconfiguration、ribboneurekaautoconfiguration
 @autowired(required = false)
 private list<ribbonclientspecification> configurations = new arraylist<>();
  
 @bean
 public springclientfactory springclientfactory() {
    //初始化springclientfactory,并將上面的配置注入進去,這段很重要。
 springclientfactory factory = new springclientfactory();
 factory.setconfigurations(this.configurations);
 return factory;
 }
   //其他的都是提供一些默認的bean配置
 
 @bean
 @conditionalonmissingbean(loadbalancerclient.class)
 public loadbalancerclient loadbalancerclient() {
 return new ribbonloadbalancerclient(springclientfactory());
 }
 
 @bean
 @conditionalonclass(name = "org.springframework.retry.support.retrytemplate")
 @conditionalonmissingbean
 public loadbalancedretrypolicyfactory loadbalancedretrypolicyfactory(springclientfactory clientfactory) {
 return new ribbonloadbalancedretrypolicyfactory(clientfactory);
 }
 
 @bean
 @conditionalonmissingclass(value = "org.springframework.retry.support.retrytemplate")
 @conditionalonmissingbean
 public loadbalancedretrypolicyfactory neverretrypolicyfactory() {
 return new loadbalancedretrypolicyfactory.neverretryfactory();
 }
 
 @bean
 @conditionalonclass(name = "org.springframework.retry.support.retrytemplate")
 @conditionalonmissingbean
 public loadbalancedbackoffpolicyfactory loadbalancedbackoffpolicyfactory() {
 return new loadbalancedbackoffpolicyfactory.nobackoffpolicyfactory();
 }
 
 @bean
 @conditionalonclass(name = "org.springframework.retry.support.retrytemplate")
 @conditionalonmissingbean
 public loadbalancedretrylistenerfactory loadbalancedretrylistenerfactory() {
 return new loadbalancedretrylistenerfactory.defaultretrylistenerfactory();
 }
 
 @bean
 @conditionalonmissingbean
 public propertiesfactory propertiesfactory() {
 return new propertiesfactory();
 }
 
 @bean
 @conditionalonproperty(value = "ribbon.eager-load.enabled", matchifmissing = false)
 public ribbonapplicationcontextinitializer ribbonapplicationcontextinitializer() {
 return new ribbonapplicationcontextinitializer(springclientfactory(),
 ribboneagerloadproperties.getclients());
 }
 
 @configuration
 @conditionalonclass(httprequest.class)
 @conditionalonribbonrestclient
 protected static class ribbonclientconfig {
 
 @autowired
 private springclientfactory springclientfactory;
 
 @bean
 public resttemplatecustomizer resttemplatecustomizer(
 final ribbonclienthttprequestfactory ribbonclienthttprequestfactory) {
 return new resttemplatecustomizer() {
 @override
 public void customize(resttemplate resttemplate) {
  resttemplate.setrequestfactory(ribbonclienthttprequestfactory);
 }
 };
 }
 
 @bean
 public ribbonclienthttprequestfactory ribbonclienthttprequestfactory() {
 return new ribbonclienthttprequestfactory(this.springclientfactory);
 }
 }
 
 //todo: support for autoconfiguring restemplate to use apache http client or okhttp
 
 @target({ elementtype.type, elementtype.method })
 @retention(retentionpolicy.runtime)
 @documented
 @conditional(onribbonrestclientcondition.class)
 @interface conditionalonribbonrestclient { }
 
 private static class onribbonrestclientcondition extends anynestedcondition {
 public onribbonrestclientcondition() {
 super(configurationphase.register_bean);
 }
 
 @deprecated //remove in edgware"
 @conditionalonproperty("ribbon.http.client.enabled")
 static class zuulproperty {}
 
 @conditionalonproperty("ribbon.restclient.enabled")
 static class ribbonproperty {}
 }
}

注意這里的springclientfactory, ribbon默認情況下,每個eureka的serviceid(服務),都會分配自己獨立的spring的上下文,即applicationcontext, 然后這個上下文中包含了必要的一些bean,比如: iloadbalancer 、 serverlistfilter 等。而spring cloud默認是使用resttemplate封裝了ribbon的調用,核心是通過一個攔截器:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@bean
 @conditionalonmissingbean
 public resttemplatecustomizer resttemplatecustomizer(
 final loadbalancerinterceptor loadbalancerinterceptor) {
 return new resttemplatecustomizer() {
 @override
 public void customize(resttemplate resttemplate) {
  list<clienthttprequestinterceptor> list = new arraylist<>(
  resttemplate.getinterceptors());
  list.add(loadbalancerinterceptor);
  resttemplate.setinterceptors(list);
 }
 };
 }

因此核心是通過這個攔截器實現的負載均衡:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class loadbalancerinterceptor implements clienthttprequestinterceptor {
 
 private loadbalancerclient loadbalancer;
 private loadbalancerrequestfactory requestfactory;
 
 @override
 public clienthttpresponse intercept(final httprequest request, final byte[] body,
 final clienthttprequestexecution execution) throws ioexception {
 final uri originaluri = request.geturi(); //這里傳入的url是解析之前的,即http://serviceid/服務地址的形式
 string servicename = originaluri.gethost(); //解析拿到對應的serviceid
 assert.state(servicename != null, "request uri does not contain a valid hostname: " + originaluri);
 return this.loadbalancer.execute(servicename, requestfactory.createrequest(request, body, execution));
 }
}

然后將請求轉發給loadbalancerclient:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ribbonloadbalancerclient implements loadbalancerclient {
@override
 public <t> t execute(string serviceid, loadbalancerrequest<t> request) throws ioexception {
 iloadbalancer loadbalancer = getloadbalancer(serviceid); //獲取對應的loadbalancer
 server server = getserver(loadbalancer); //獲取服務器,這里會執行對應的分流策略,比如輪訓
    //、隨機等
 if (server == null) {
 throw new illegalstateexception("no instances available for " + serviceid);
 }
 ribbonserver ribbonserver = new ribbonserver(serviceid, server, issecure(server,
 serviceid), serverintrospector(serviceid).getmetadata(server));
 
 return execute(serviceid, ribbonserver, request);
 }
}

而這里的loadbalancer是通過上文中提到的springclientfactory獲取到的,這里會初始化一個新的spring上下文,然后將ribbon默認的配置類,比如說: ribbonautoconfiguration 、 ribboneurekaautoconfiguration 等添加進去, 然后將當前spring的上下文設置為parent,再調用refresh方法進行初始化。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class springclientfactory extends namedcontextfactory<ribbonclientspecification> {
 protected annotationconfigapplicationcontext createcontext(string name) {
 annotationconfigapplicationcontext context = new annotationconfigapplicationcontext();
 if (this.configurations.containskey(name)) {
 for (class<?> configuration : this.configurations.get(name)
  .getconfiguration()) {
 context.register(configuration);
 }
 }
 for (map.entry<string, c> entry : this.configurations.entryset()) {
 if (entry.getkey().startswith("default.")) {
 for (class<?> configuration : entry.getvalue().getconfiguration()) {
  context.register(configuration);
 }
 }
 }
 context.register(propertyplaceholderautoconfiguration.class,
 this.defaultconfigtype);
 context.getenvironment().getpropertysources().addfirst(new mappropertysource(
 this.propertysourcename,
 collections.<string, object> singletonmap(this.propertyname, name)));
 if (this.parent != null) {
 // uses environment from parent as well as beans
 context.setparent(this.parent);
 }
 context.refresh();
 return context;
 }
}

最核心的就在這一段,也就是說對于每一個不同的serviceid來說,都擁有一個獨立的spring上下文,并且在第一次調用這個服務的時候,會初始化ribbon相關的所有bean, 如果不存在 才回去父context中去找。

再回到上文中根據分流策略獲取實際的ip:port的代碼段:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ribbonloadbalancerclient implements loadbalancerclient {
@override
 public <t> t execute(string serviceid, loadbalancerrequest<t> request) throws ioexception {
 iloadbalancer loadbalancer = getloadbalancer(serviceid); //獲取對應的loadbalancer
 server server = getserver(loadbalancer); //獲取服務器,這里會執行對應的分流策略,比如輪訓
    //、隨機等
 if (server == null) {
 throw new illegalstateexception("no instances available for " + serviceid);
 }
 ribbonserver ribbonserver = new ribbonserver(serviceid, server, issecure(server,
 serviceid), serverintrospector(serviceid).getmetadata(server));
 
 return execute(serviceid, ribbonserver, request);
 }
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
protected server getserver(iloadbalancer loadbalancer) {
 if (loadbalancer == null) {
 return null;
 }
    // 選擇對應的服務器
 return loadbalancer.chooseserver("default"); // todo: better handling of key
 }
public class zoneawareloadbalancer<t extends server> extends dynamicserverlistloadbalancer<t> {
@override
 public server chooseserver(object key) {
  if (!enabled.get() || getloadbalancerstats().getavailablezones().size() <= 1) {
   logger.debug("zone aware logic disabled or there is only one zone");
   return super.chooseserver(key); //默認不配置可用區,走的是這段
  }
  server server = null;
  try {
   loadbalancerstats lbstats = getloadbalancerstats();
   map<string, zonesnapshot> zonesnapshot = zoneavoidancerule.createsnapshot(lbstats);
   logger.debug("zone snapshots: {}", zonesnapshot);
   if (triggeringload == null) {
    triggeringload = dynamicpropertyfactory.getinstance().getdoubleproperty(
      "zoneawareniwsdiscoveryloadbalancer." + this.getname() + ".triggeringloadperserverthreshold", 0.2d);
   }
 
   if (triggeringblackoutpercentage == null) {
    triggeringblackoutpercentage = dynamicpropertyfactory.getinstance().getdoubleproperty(
      "zoneawareniwsdiscoveryloadbalancer." + this.getname() + ".avoidzonewithblackoutpercetage", 0.99999d);
   }
   set<string> availablezones = zoneavoidancerule.getavailablezones(zonesnapshot, triggeringload.get(), triggeringblackoutpercentage.get());
   logger.debug("available zones: {}", availablezones);
   if (availablezones != null && availablezones.size() < zonesnapshot.keyset().size()) {
    string zone = zoneavoidancerule.randomchoosezone(zonesnapshot, availablezones);
    logger.debug("zone chosen: {}", zone);
    if (zone != null) {
     baseloadbalancer zoneloadbalancer = getloadbalancer(zone);
     server = zoneloadbalancer.chooseserver(key);
    }
   }
  } catch (exception e) {
   logger.error("error choosing server using zone aware logic for load balancer={}", name, e);
  }
  if (server != null) {
   return server;
  } else {
   logger.debug("zone avoidance logic is not invoked.");
   return super.chooseserver(key);
  }
 }
 
 //實際走到的方法
 public server chooseserver(object key) {
  if (counter == null) {
   counter = createcounter();
  }
  counter.increment();
  if (rule == null) {
   return null;
  } else {
   try {
    return rule.choose(key);
   } catch (exception e) {
    logger.warn("loadbalancer [{}]: error choosing server for key {}", name, key, e);
    return null;
   }
  }
 }
}

也就是說最終會調用 irule 選擇到一個節點,這里支持很多策略,比如隨機、輪訓、響應時間權重等:

Spring Cloud Ribbon的踩坑記錄與原理詳析

?
1
2
3
4
5
6
7
8
public interface irule{
 
 public server choose(object key);
 
 public void setloadbalancer(iloadbalancer lb);
 
 public iloadbalancer getloadbalancer();
}

這里的loadbalancer是在baseloadbalancer的構造器中設置的,上文說過,對于每一個serviceid服務來說,當第一次調用的時候會初始化對應的spring上下文,而這個上下文中包含了所有ribbon相關的bean,其中就包括iloadbalancer、irule。

原因

通過跟蹤堆棧,發現不同的serviceid,irule是同一個, 而上文說過,每個serviceid都擁有自己獨立的上下文,包括獨立的loadbalancer、irule,而irule是同一個,因此懷疑是這個bean是通過parent context獲取到的,換句話說應用自己定義了一個這樣的bean。查看代碼果然如此。

這樣就會導致一個問題,irule是共享的,而其他bean是隔離開的,因此后面的serviceid初始化的時候,會修改這個irule的loadbalancer, 導致之前的服務獲取到的實例信息是錯誤的,從而導致接口404。

?
1
2
3
4
5
6
7
8
9
10
public class baseloadbalancer extends abstractloadbalancer implements
  primeconnections.primeconnectionlistener, iclientconfigaware {
 public baseloadbalancer() {
  this.name = default_name;
  this.ping = null;
  setrule(default_rule); // 這里會設置irule的loadbalancer
  setuppingtask();
  lbstats = new loadbalancerstats(default_name);
 }
}

Spring Cloud Ribbon的踩坑記錄與原理詳析

解決方案

解決方法也很簡單,最簡單就將這個自定義的irule的bean干掉,另外更標準的做法是使用ribbonclients注解,具體做法可以參考文檔。

總結

核心原因其實還是對于spring cloud的理解不夠深刻,用法有錯誤,導致出現了一些比較詭異的問題。對于自己使用的組件、框架、甚至于每一個注解,都要了解其原理,能夠清楚的說清楚這個注解有什么效果,有什么影響,而不是只著眼于解決眼前的問題。

再次聲明:代碼不是我寫的=_=

好了,以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對服務器之家的支持。

原文鏈接:https://github.com/aCoder2013/blog/issues/29

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 美女毛片老太婆bbb80岁 | 国产亚洲sss在线播放 | 小辣椒精品福利视频导航 | 爽好舒服使劲添高h视频 | 18成人在线观看 | 国产精品久久久久久爽爽爽 | xxxx野外性xxxx| 国产aaa毛片 | 高清国语自产拍免费视频国产 | 美女被绑着吸下部的故事 | 好爽好粗| boobsmilking流奶水野战 | 国产精品午夜性视频网站 | 午夜人妻理论片天堂影院 | 青青热久久综合网伊人 | 无码人妻丰满熟妇啪啪网不卡 | 成年人免费在线看的惊悚动作片 | 国产欧美一区二区成人影院 | 粗了大了 整进去好爽视频 刺激一区仑乱 | m3u8久久国产精品影院 | 精品国产一区二区三区久久影院 | 攻插受| 美女和男人一起差差 | 星空无限传媒视频在线观看视频 | 2021最新国产成人精品免费 | 波多野结衣之双方调教在线观看 | 俄罗斯妈妈235 | 男同gay玩奴男同玩奴 | 日本漫画工囗全彩内番e绅 日本伦理动漫在线观看 | 色综合久久中文字幕综合网 | 欧美日韩看看2015永久免费 | 免费一级毛片在级播放 | 国产资源中文字幕 | mmkk在线看片| 亚洲品质水蜜桃 | 国产午夜精品久久久久小说 | 国内精品视频免费观看 | 日韩欧美一卡二区 | 性xxxx中国老妇506070 | 乌克兰黄色录像 | 久久久久久久久女黄 |