标准 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×tamp=1631001243901
- protocol:dubbo 协议。
- username/password:没有用户名和密码。
- host/port:192.168.0.2:20880。
- path:org.apache.dubbo.demo.DemoService。
- parameters:参数键值对,这里是问号后面的参数。
Dubbo 2.7 的URL
public /*final**/class URL implements Serializable {private static final long serialVersionUID = -1985165475234910535L;protected String protocol;protected String username;protected String password;// by default, host to registryprotected String host;// by default, port to registryprotected int port;protected String path;private final Map<String, String> parameters;private final Map<String, Map<String, String>> methodParameters;...}
在 Dubbo 2.7 中,Provider 是接口级的,并且任一 Provider 实例接口参数的变更都会导致 Consumer 重新获取所有 Provider 的信息并且全部重新生成一遍 URL,这个情况,在接口数量很大的时候,会引发一定程度的性能问题
两个场景举例:
- 某个 Consumer 依赖大量的 Provider,并且其中某个 Provider 因为网络等原因频繁上下线。
- 当频繁的扩容缩容导致的 Provider 频繁变更时。
上面任一场景都会导致大批量的 URL 一直不断的创建,且有很大一部分原来便存在,导致了不必要的内存及运行时间的损耗。参照如下文章:
Dubbo 3.0 前瞻:服务发现支持百万集群,带来可伸缩微服务架构
Dubbo 3 的URL
public /*final**/class URL implements Serializable {private static final long serialVersionUID = -1985165475234910535L;private static Map<String, URL> cachedURLs = new LRUCache<>();private final URLAddress urlAddress;private final URLParam urlParam;}



InstanceAddressURL 属于应用级接口地址ServiceConfigURL 是程序读取配置文件时生成的 URLServiceAddressURL 则是注册中心推送一些信息(如 providers)过来时生成的 URL
**ServiceConfigURL**:这个子类中新增了 attribute 这个属性,这个属性主要是针对 URLParam 的 params 做了冗余,仅仅只是将 value 的类型从 String 改为了 Object,减少了代码中每次获取 parameters 的格式转换消耗。
**ServiceAddressURL**:这个子类及其对应的其他子类中则新增了 overrideURL 和 consumerURL 属性。其中 consumerURL 是针对 consumer 端的配置信息,overrideURL 则是在 Dubbo Admin 上进行动态配置时写入的值,当我们调用 URL 的 getParameter() 方法时,优先级为 overrideURL > consumerURL > urlParam。
多级缓存

AbstracrRegistry实现了Registry接口中的注册、订阅、查询、通知等方法,还实现了磁盘文件持久化注册信息这一通用方法,但是注册、订阅、查询、通知等方法只是简单地把URL加入对应的集合,没有具体的注册或订阅逻辑。另外,该类还实现了缓存机制,只不过,它的缓存有两份,一份在内存,一份在磁盘。FailBackReistry又继承了AbstracrReistry,重写了父类的注册、订阅、查询、通知等方法,并添加了重试机制。
多级缓存主要体现在 CacheableFailbackRegistry 这个类之中
`
`
Provider 因为网络等原因频繁上下线场景URLParam 和 URLAddress 完全无变更的话,会直接省略 createURL() 步骤,从 stringUrls 中直接获取缓存的值
org.apache.dubbo.registry.support.CacheableFailbackRegistry#toUrlsWithoutEmpty
protected final Map<URL, Map<String, ServiceAddressURL>> stringUrls = new HashMap<>();// consumer consumer 的URL注册对象//protected List<URL> toUrlsWithoutEmpty(URL consumer, Collection<String> providers) {// keep old urls, 从缓存中查询 consumer 对应的 providersMap<String, ServiceAddressURL> oldURLs = stringUrls.get(consumer);// create new urlsMap<String, ServiceAddressURL> newURLs;URL copyOfConsumer = removeParamsFromConsumer(consumer);if (oldURLs == null) {// 如果缓存中没有,直接创建newURLs = new HashMap<>();for (String rawProvider : providers) {rawProvider = stripOffVariableKeys(rawProvider);ServiceAddressURL cachedURL = createURL(rawProvider, copyOfConsumer, getExtraParameters());if (cachedURL == null) {logger.warn("Invalid address, failed to parse into URL " + rawProvider);continue;}newURLs.put(rawProvider, cachedURL);}} else {newURLs = new HashMap<>((int) (oldURLs.size() / .75 + 1));// maybe only default , or "env" + defaultfor (String rawProvider : providers) {rawProvider = stripOffVariableKeys(rawProvider);ServiceAddressURL cachedURL = oldURLs.remove(rawProvider);if (cachedURL == null) {cachedURL = createURL(rawProvider, copyOfConsumer, getExtraParameters());if (cachedURL == null) {logger.warn("Invalid address, failed to parse into URL " + rawProvider);continue;}}newURLs.put(rawProvider, cachedURL);}}evictURLCache(consumer);stringUrls.put(consumer, newURLs);return new ArrayList<>(newURLs.values());}
频繁的扩容缩容导致的 Provider 频繁变更时
即 URLAddress 变更,URLParam 不会变更
org.apache.dubbo.registry.support.CacheableFailbackRegistry#createURL
protected final static Map<String, URLAddress> stringAddress = new ConcurrentHashMap<>();protected final static Map<String, URLParam> stringParam = new ConcurrentHashMap<>();// rawProvider provider 的 full string// consumerURL consumer url// extraParameters 额外的属性protected ServiceAddressURL createURL(String rawProvider, URL consumerURL, Map<String, String> extraParameters) {boolean encoded = true;// use encoded value directly to avoid URLDecoder.decode allocation.int paramStartIdx = rawProvider.indexOf(ENCODED_QUESTION_MARK);if (paramStartIdx == -1) {// if ENCODED_QUESTION_MARK does not shown, mark as not encoded.encoded = false;}String[] parts = URLStrParser.parseRawURLToArrays(rawProvider, paramStartIdx);if (parts.length <= 1) {logger.warn("Received url without any parameters " + rawProvider);return DubboServiceAddressURL.valueOf(rawProvider, consumerURL);}String rawAddress = parts[0];String rawParams = parts[1];boolean isEncoded = encoded;// 查找stringAddress缓存中是否有对应的值,没有则创建URLAddress address = stringAddress.computeIfAbsent(rawAddress, k -> URLAddress.parse(k, getDefaultURLProtocol(), isEncoded));address.setTimestamp(System.currentTimeMillis());// 查找stringParam缓存中是否有对应的值,没有则创建URLParam param = stringParam.computeIfAbsent(rawParams, k -> URLParam.parse(k, isEncoded, extraParameters));param.setTimestamp(System.currentTimeMillis());// 使用urlAddress和urlParam创建新的URL对象ServiceAddressURL cachedURL = createServiceURL(address, param, consumerURL);if (isMatch(consumerURL, cachedURL)) {return cachedURL;}return null;}
其它优化
URL 变更后的通知机制增加了延迟
Zookeeper 的通知机制。当一个 Consumer Watcher 阻塞时,Zookeeper 的通知亦会阻塞,而当阻塞期间对应的节点(此处即为 providers)有多次变更时,Zookeeper 只会保留最后一次变更,在阻塞结束后通知该次变更。
如果某个 Consumer 的 providers 多次变更时,第一次变更会阻塞后续变更。并让 Zookeeper 合并多次通知,便能做到减少中间不必要的通知导致的 URL 重复创建。(一个问题,Consumer Watcher 阻塞时,会将所有接口的 providers 变更通知全部阻塞,导致其他接口通知被延迟接收。???)
private class RegistryChildListenerImpl implements ChildListener {private RegistryNotifier notifier;private long lastExecuteTime;private volatile CountDownLatch latch;public RegistryChildListenerImpl(URL consumerUrl, String path, NotifyListener listener, CountDownLatch latch) {this.latch = latch;notifier = new RegistryNotifier(ZookeeperRegistry.this.getDelay()) {@Overridepublic void notify(Object rawAddresses) {// delay time default 5000// @see org.apache.dubbo.registry.client.ServiceDiscovery#getDelaylong delayTime = getDelayTime();if (delayTime <= 0) {this.doNotify(rawAddresses);} else {// 延迟一定时间后才去真正的通知更新long interval = delayTime - (System.currentTimeMillis() - lastExecuteTime);if (interval > 0) {try {Thread.sleep(interval);} catch (InterruptedException e) {// ignore}}lastExecuteTime = System.currentTimeMillis();this.doNotify(rawAddresses);}}@Overrideprotected void doNotify(Object rawAddresses) {ZookeeperRegistry.this.notify(consumerUrl, listener, ZookeeperRegistry.this.toUrlsWithEmpty(consumerUrl, path, (List<String>) rawAddresses));}};}public void setLatch(CountDownLatch latch) {this.latch = latch;}@Overridepublic void childChanged(String path, List<String> children) {try {latch.await();} catch (InterruptedException e) {logger.warn("Zookeeper children listener thread was interrupted unexpectedly, may cause race condition with the main thread.");}notifier.notify(children);}}
