标准 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 registry
protected String host;
// by default, port to registry
protected 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 对应的 providers
Map<String, ServiceAddressURL> oldURLs = stringUrls.get(consumer);
// create new urls
Map<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" + default
for (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()) {
@Override
public void notify(Object rawAddresses) {
// delay time default 5000
// @see org.apache.dubbo.registry.client.ServiceDiscovery#getDelay
long 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);
}
}
@Override
protected 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;
}
@Override
public 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);
}
}