标准 URL

URL 的组成

protocol://username:password@host:port/path?key=value&key=value

  • protocol:URL 的协议。我们常见的就是 HTTP 协议和 HTTPS 协议,当然,还有其他协议,如 FTP 协议、SMTP 协议等。
  • username/password:用户名/密码。 HTTP Basic Authentication 中多会使用在 URL 的协议之后直接携带用户名和密码的方式。
  • host/port:主机/端口。在实践中一般会使用域名,而不是使用具体的 host 和 port。
  • path:请求的路径。
  • parameters:参数键值对。一般在 GET 请求中会将参数放到 URL 中,POST 请求会将参数放到请求体中。

Dubbo 中的URL

dubbo://192.168.0.2:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-annotation-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello,sayHelloAsync&pid=94857&release=&service.name=ServiceBean:/org.apache.dubbo.demo.DemoService&side=provider&timestamp=1631001243901

  • protocol:dubbo 协议。
  • username/password:没有用户名和密码。
  • host/port:192.168.0.2:20880。
  • path:org.apache.dubbo.demo.DemoService。
  • parameters:参数键值对,这里是问号后面的参数。

Dubbo 2.7 的URL

  1. public /*final**/
  2. class URL implements Serializable {
  3. private static final long serialVersionUID = -1985165475234910535L;
  4. protected String protocol;
  5. protected String username;
  6. protected String password;
  7. // by default, host to registry
  8. protected String host;
  9. // by default, port to registry
  10. protected int port;
  11. protected String path;
  12. private final Map<String, String> parameters;
  13. private final Map<String, Map<String, String>> methodParameters;
  14. ...
  15. }

在 Dubbo 2.7 中,Provider 是接口级的,并且任一 Provider 实例接口参数的变更都会导致 Consumer 重新获取所有 Provider 的信息并且全部重新生成一遍 URL,这个情况,在接口数量很大的时候,会引发一定程度的性能问题

两个场景举例:

  1. 某个 Consumer 依赖大量的 Provider,并且其中某个 Provider 因为网络等原因频繁上下线。
  2. 当频繁的扩容缩容导致的 Provider 频繁变更时。

上面任一场景都会导致大批量的 URL 一直不断的创建,且有很大一部分原来便存在,导致了不必要的内存及运行时间的损耗。参照如下文章:
Dubbo 3.0 前瞻:服务发现支持百万集群,带来可伸缩微服务架构

Dubbo 3 的URL

  1. public /*final**/
  2. class URL implements Serializable {
  3. private static final long serialVersionUID = -1985165475234910535L;
  4. private static Map<String, URL> cachedURLs = new LRUCache<>();
  5. private final URLAddress urlAddress;
  6. private final URLParam urlParam;
  7. }

image.png

image.png

image.png

InstanceAddressURL 属于应用级接口地址
ServiceConfigURL 是程序读取配置文件时生成的 URL
ServiceAddressURL 则是注册中心推送一些信息(如 providers)过来时生成的 URL

**ServiceConfigURL**:这个子类中新增了 attribute 这个属性,这个属性主要是针对 URLParam 的 params 做了冗余,仅仅只是将 value 的类型从 String 改为了 Object,减少了代码中每次获取 parameters 的格式转换消耗。

**ServiceAddressURL**:这个子类及其对应的其他子类中则新增了 overrideURL 和 consumerURL 属性。其中 consumerURL 是针对 consumer 端的配置信息,overrideURL 则是在 Dubbo Admin 上进行动态配置时写入的值,当我们调用 URL 的 getParameter() 方法时,优先级为 overrideURL > consumerURL > urlParam

多级缓存

image.png

AbstracrRegistry实现了Registry接口中的注册、订阅、查询、通知等方法,还实现了磁盘文件持久化注册信息这一通用方法,但是注册、订阅、查询、通知等方法只是简单地把URL加入对应的集合,没有具体的注册或订阅逻辑。另外,该类还实现了缓存机制,只不过,它的缓存有两份,一份在内存,一份在磁盘。
FailBackReistry又继承了AbstracrReistry,重写了父类的注册、订阅、查询、通知等方法,并添加了重试机制。

多级缓存主要体现在 CacheableFailbackRegistry 这个类之中

`

`

Provider 因为网络等原因频繁上下线场景
URLParamURLAddress 完全无变更的话,会直接省略 createURL() 步骤,从 stringUrls 中直接获取缓存的值

org.apache.dubbo.registry.support.CacheableFailbackRegistry#toUrlsWithoutEmpty

  1. protected final Map<URL, Map<String, ServiceAddressURL>> stringUrls = new HashMap<>();
  2. // consumer consumer 的URL注册对象
  3. //
  4. protected List<URL> toUrlsWithoutEmpty(URL consumer, Collection<String> providers) {
  5. // keep old urls, 从缓存中查询 consumer 对应的 providers
  6. Map<String, ServiceAddressURL> oldURLs = stringUrls.get(consumer);
  7. // create new urls
  8. Map<String, ServiceAddressURL> newURLs;
  9. URL copyOfConsumer = removeParamsFromConsumer(consumer);
  10. if (oldURLs == null) {
  11. // 如果缓存中没有,直接创建
  12. newURLs = new HashMap<>();
  13. for (String rawProvider : providers) {
  14. rawProvider = stripOffVariableKeys(rawProvider);
  15. ServiceAddressURL cachedURL = createURL(rawProvider, copyOfConsumer, getExtraParameters());
  16. if (cachedURL == null) {
  17. logger.warn("Invalid address, failed to parse into URL " + rawProvider);
  18. continue;
  19. }
  20. newURLs.put(rawProvider, cachedURL);
  21. }
  22. } else {
  23. newURLs = new HashMap<>((int) (oldURLs.size() / .75 + 1));
  24. // maybe only default , or "env" + default
  25. for (String rawProvider : providers) {
  26. rawProvider = stripOffVariableKeys(rawProvider);
  27. ServiceAddressURL cachedURL = oldURLs.remove(rawProvider);
  28. if (cachedURL == null) {
  29. cachedURL = createURL(rawProvider, copyOfConsumer, getExtraParameters());
  30. if (cachedURL == null) {
  31. logger.warn("Invalid address, failed to parse into URL " + rawProvider);
  32. continue;
  33. }
  34. }
  35. newURLs.put(rawProvider, cachedURL);
  36. }
  37. }
  38. evictURLCache(consumer);
  39. stringUrls.put(consumer, newURLs);
  40. return new ArrayList<>(newURLs.values());
  41. }

频繁的扩容缩容导致的 Provider 频繁变更时
URLAddress 变更,URLParam 不会变更

org.apache.dubbo.registry.support.CacheableFailbackRegistry#createURL

  1. protected final static Map<String, URLAddress> stringAddress = new ConcurrentHashMap<>();
  2. protected final static Map<String, URLParam> stringParam = new ConcurrentHashMap<>();
  3. // rawProvider provider 的 full string
  4. // consumerURL consumer url
  5. // extraParameters 额外的属性
  6. protected ServiceAddressURL createURL(String rawProvider, URL consumerURL, Map<String, String> extraParameters) {
  7. boolean encoded = true;
  8. // use encoded value directly to avoid URLDecoder.decode allocation.
  9. int paramStartIdx = rawProvider.indexOf(ENCODED_QUESTION_MARK);
  10. if (paramStartIdx == -1) {// if ENCODED_QUESTION_MARK does not shown, mark as not encoded.
  11. encoded = false;
  12. }
  13. String[] parts = URLStrParser.parseRawURLToArrays(rawProvider, paramStartIdx);
  14. if (parts.length <= 1) {
  15. logger.warn("Received url without any parameters " + rawProvider);
  16. return DubboServiceAddressURL.valueOf(rawProvider, consumerURL);
  17. }
  18. String rawAddress = parts[0];
  19. String rawParams = parts[1];
  20. boolean isEncoded = encoded;
  21. // 查找stringAddress缓存中是否有对应的值,没有则创建
  22. URLAddress address = stringAddress.computeIfAbsent(rawAddress, k -> URLAddress.parse(k, getDefaultURLProtocol(), isEncoded));
  23. address.setTimestamp(System.currentTimeMillis());
  24. // 查找stringParam缓存中是否有对应的值,没有则创建
  25. URLParam param = stringParam.computeIfAbsent(rawParams, k -> URLParam.parse(k, isEncoded, extraParameters));
  26. param.setTimestamp(System.currentTimeMillis());
  27. // 使用urlAddress和urlParam创建新的URL对象
  28. ServiceAddressURL cachedURL = createServiceURL(address, param, consumerURL);
  29. if (isMatch(consumerURL, cachedURL)) {
  30. return cachedURL;
  31. }
  32. return null;
  33. }

其它优化

URL 变更后的通知机制增加了延迟

Zookeeper 的通知机制。当一个 Consumer Watcher 阻塞时,Zookeeper 的通知亦会阻塞,而当阻塞期间对应的节点(此处即为 providers)有多次变更时,Zookeeper 只会保留最后一次变更,在阻塞结束后通知该次变更。

如果某个 Consumer 的 providers 多次变更时,第一次变更会阻塞后续变更。并让 Zookeeper 合并多次通知,便能做到减少中间不必要的通知导致的 URL 重复创建。(一个问题,Consumer Watcher 阻塞时,会将所有接口的 providers 变更通知全部阻塞,导致其他接口通知被延迟接收。???)

  1. private class RegistryChildListenerImpl implements ChildListener {
  2. private RegistryNotifier notifier;
  3. private long lastExecuteTime;
  4. private volatile CountDownLatch latch;
  5. public RegistryChildListenerImpl(URL consumerUrl, String path, NotifyListener listener, CountDownLatch latch) {
  6. this.latch = latch;
  7. notifier = new RegistryNotifier(ZookeeperRegistry.this.getDelay()) {
  8. @Override
  9. public void notify(Object rawAddresses) {
  10. // delay time default 5000
  11. // @see org.apache.dubbo.registry.client.ServiceDiscovery#getDelay
  12. long delayTime = getDelayTime();
  13. if (delayTime <= 0) {
  14. this.doNotify(rawAddresses);
  15. } else {
  16. // 延迟一定时间后才去真正的通知更新
  17. long interval = delayTime - (System.currentTimeMillis() - lastExecuteTime);
  18. if (interval > 0) {
  19. try {
  20. Thread.sleep(interval);
  21. } catch (InterruptedException e) {
  22. // ignore
  23. }
  24. }
  25. lastExecuteTime = System.currentTimeMillis();
  26. this.doNotify(rawAddresses);
  27. }
  28. }
  29. @Override
  30. protected void doNotify(Object rawAddresses) {
  31. ZookeeperRegistry.this.notify(consumerUrl, listener, ZookeeperRegistry.this.toUrlsWithEmpty(consumerUrl, path, (List<String>) rawAddresses));
  32. }
  33. };
  34. }
  35. public void setLatch(CountDownLatch latch) {
  36. this.latch = latch;
  37. }
  38. @Override
  39. public void childChanged(String path, List<String> children) {
  40. try {
  41. latch.await();
  42. } catch (InterruptedException e) {
  43. logger.warn("Zookeeper children listener thread was interrupted unexpectedly, may cause race condition with the main thread.");
  44. }
  45. notifier.notify(children);
  46. }
  47. }