Dubbo之父——梁飞技术博客拾遗(3)
2010-02-03反省日志
2010-02-04 https://www.iteye.com/blog/javatar-587159
抽查自己的一天。
今天做了什么?
- 试验了ViewCache和LimitedWord功能并入Morgan
- 国际站Morgan业务模型及开发实施计划讨论
- 阿里金融Dubbo使用讨论
- 确定了Morgan对UDB功能的开发测试资源
- 写了Dubbo使用培训文档
- 参加了聚餐
有什么计划?
- 阿里金融Dubbo使用培训
- 实现Person&VAccount同时返回功能和ViewCache功能
- 实现Hessian服务互操作
- 思考Dubbo的重构
- 熟悉国际站Morgan相关业务
充当的角色平衡吗?
- 作为Morgan&Dubbo的开发者,做了相关功能的开发,但计划性不强
- 作为Morgan&Dubbo的推进者,对产品未来发展的思考不够,过于局限当前实现细节
- 作为国际站Morgan会议的参与者,没有发表自己的意见,没有事前了解国际站业务
- 作为UDB与Morgan的接口人,没有主动推进UDB业务的确定
- 作为阿里金融Dubbo技术支持,有参与初步讨论,并计划下一步培训,但对阿里金融的架构和使用Dubbo的点没有进一步分析
- 作为Maven的技术支持,对Maven规范的制定及相关工作没有关心
- 作为瑞的指导,有安排她相关工作,但关注和跟进反馈不够
- 作为磊的团队成员,有配合他工作,但没有主动为他考虑问题和备份他的支持工作内容
- 作为同事的一份子,参加了聚会
- 作为自我提升的主体,没有学习英语,没有看书
- 作为健康保证的主体,没有跑步,没有锻炼活动
- 作为家庭的一份子,没有打电话给爸妈或姐姐
- 作为业余项目的参与者,没有开发业余项目的相关功能
哪些方面做的好?
- 各工作事项都有完成
- 没有浪费时间
哪些方面做的不好?
- 没有计划的做事
- 会议上没有发表自己的意见
- 没有主动推进各个事情的发展
- 没有考虑和关心他人
- 没有看书,没有锻炼
当前工作状态是什么样的?
- 紧急事情为主导
- 重要事情被动
- 空闲时间较少
- 有以忙活为充实感趋势
- 懒惰心理存在
- 时间观念弱
谈谈扩充式扩展与增量式扩展
2010-06-12 https://www.iteye.com/blog/javatar-690845
我们平台的产品越来越多,产品的功能也越来越多,
平台的产品为了适应各BU和部门以及产品线的需求,
势必会将很多不相干的功能凑在一起,客户可以选择性的使用,
为了兼容更多的需求,每个产品,每个框架,都在不停的扩展,
而我们经常会选择一些扩展的扩展方式,也就是将新旧功能扩展成一个通用实现,
我想讨论是,有些情况下也可以考虑增量式的扩展方式,也就是保留原功能的简单性,新功能独立实现,
我最近一直做分布式服务框架的开发,就拿我们项目中的问题开涮吧。
比如:远程调用框架,肯定少不了序列化功能,功能很简单,就是把流转成对象,对象转成流,
但因有些地方可能会使用osgi,这样序列化时,IO所在的ClassLoader可能和业务方的ClassLoader是隔离的,
需要将流转换成byte[]数组,然后传给业务方的ClassLoader进行序列化,
为了适应osgi需求,把原来非osgi与osgi的场景扩展了一下,
这样,不管是不是osgi环境,都先将流转成byte[]数组,拷贝一次,
然而,大部分场景都用不上osgi,却为osgi付出了代价,
而如果采用增量式扩展方式,非osgi的代码原封不动,
再加一个osgi的实现,要用osgi的时候,直接依赖osgi实现即可。
再比如:最开始,远程服务都是基于接口方法,进行透明化调用的,
这样,扩展接口就是,invoke(Method method, Object[] args),
后来,有了无接口调用的需求,就是没有接口方法也能调用,并将POJO对象都转换成Map表示,
因为Method对象是不能直接new出来的,我们不自觉选了一个扩展式扩展,
把扩展接口改成了invoke(String methodName, String[] parameterTypes, String returnTypes, Object[] args),
导致不管是不是无接口调用,都得把parameterTypes从Class[]转成String[],
如果选用增量式扩展,应该是保持原有接口不变,
增加一个GeneralService接口,里面有一个通用的invoke()方法,
和其它正常业务上的接口一样的调用方式,扩展接口也不用变,
只是GeneralServiceImpl的invoke()实现会将收到的调用转给目标接口,
这样就能将新功能增量到旧功能上,并保持原来结构的简单性。
再再比如:无状态消息发送,很简单,序列化一个对象发过去就行,
后来有了同步消息发送需求,需要一个Request/Response进行配对,
采用扩展式扩展,自然想到,无状态消息其实是一个没有Response的Request,
所以在Request里加一个boolean状态,表示要不要返回Response,
如果再来一个会话消息发送需求,那就再加一个Session交互,
然后发现,原来同步消息发送是会话消息的一种特殊情况,
所有场景都传Session,不需要Session的地方无视即可。
如果采用增量式扩展,无状态消息发送原封不动,
同步消息发送,在无状态消息基础上加一个Request/Response处理,
会话消息发送,再加一个SessionRequest/SessionResponse处理。
一些设计上的基本常识
2010-07-05 https://www.iteye.com/blog/javatar-706098
最近给团队新人讲了一些设计上的常识,可能会对其它的新人也有些帮助,
把暂时想到的几条,先记在这里。
0. API与SPI分离
框架或组件通常有两类客户,一个是使用者,一个是扩展者,
API(Application Programming Interface)是给使用者用的,
而SPI(Service Provide Interface)是给扩展者用的,
在设计时,尽量把它们隔离开,而不要混在一起,
也就是说,使用者是看不到扩展者写的实现的,
比如:一个Web框架,它有一个API接口叫Action,
里面有个execute()方法,是给使用者用来写业务逻辑的,
然后,Web框架有一个SPI接口给扩展者控制输出方式,
比如用velocity模板输出还是用json输出等,
如果这个Web框架使用一个都继承Action的VelocityAction和一个JsonAction做为扩展方式,
要用velocity模板输出的就继承VelocityAction,要用json输出的就继承JsonAction,
这就是API和SPI没有分离的反面例子,SPI接口混在了API接口中,
合理的方式是,有一个单独的Renderer接口,有VelocityRenderer和JsonRenderer实现,
Web框架将Action的输出转交给Renderer接口做渲染输出。
1. 服务域/实体域/会话域分离
任何框架或组件,总会有核心领域模型,比如:
Spring的Bean,Struts的Action,Dubbo的Service,Napoli的Queue等等
这个核心领域模型及其组成部分称为实体域,它代表着我们要操作的目标本身,
实体域通常是线程安全的,不管是通过不变类,同步状态,或复制的方式,
服务域也就是行为域,它是组件的功能集,同时也负责实体域和会话域的生命周期管理,
比如Spring的ApplicationContext,Dubbo的ServiceManager等,
服务域的对象通常会比较重,而且是线程安全的,并以单一实例服务于所有调用,
什么是会话?就是一次交互过程,
会话中重要的概念是上下文,什么是上下文?
比如我们说:“老地方见”,这里的“老地方”就是上下文信息,
为什么说“老地方”对方会知道,因为我们前面定义了“老地方”的具体内容,
所以说,上下文通常持有交互过程中的状态变量等,
会话对象通常较轻,每次请求都重新创建实例,请求结束后销毁。
简而言之:
把元信息交由实体域持有, 把一次请求中的临时状态由会话域持有, 由服务域贯穿整个过程。
2. 在重要的过程上设置拦截接口
如果你要写个远程调用框架,那远程调用的过程应该有一个统一的拦截接口,
如果你要写一个ORM框架,那至少SQL的执行过程,Mapping过程要有拦截接口,
如果你要写一个Web框架,那请求的执行过程应该要有拦截接口,
等等,没有哪个公用的框架可以Cover住所有需求,允许外置行为,是框架的基本扩展方式,
这样,如果有人想在远程调用前,验证下令牌,验证下黑白名单,统计下日志,
如果有人想在SQL执行前加下分页包装,做下数据权限控制,统计下SQL执行时间,
如果有人想在请求执行前检查下角色,包装下输入输出流,统计下请求量,
等等,就可以自行完成,而不用侵入框架内部,
拦截接口,通常是把过程本身用一个对象封装起来,传给拦截器链,
比如:远程调用主过程为invoke(),那拦截器接口通常为invoke(Invocation),
Invocation对象封装了本来要执行过程的上下文,并且Invocation里有一个invoke()方法,
由拦截器决定什么时候执行,同时,Invocation也代表拦截器行为本身,
这样上一拦截器的Invocation其实是包装的下一拦截器的过程,
直到最后一个拦截器的Invocation是包装的最终的invoke()过程,
同理,SQL主过程为execute(),那拦截器接口通常为execute(Execution),原理一样,
当然,实现方式可以任意,上面只是举例。
3. 重要的状态的变更发送事件并留出监听接口
这里先要讲一个事件和上面拦截器的区别,拦截器是干预过程的,它是过程的一部分,是基于过程行为的,
而事件是基于状态数据的,任何行为改变的相同状态,对事件应该是一致的,
事件通常是事后通知,是一个Callback接口,方法名通常是过去式的,比如onChanged(),
比如远程调用框架,当网络断开或连上应该发出一个事件,当出现错误也可以考虑发出一个事件,
这样外围应用就有可能观察到框架内部的变化,做相应适应。
4. 扩展接口职责尽可能单一,具有可组合性
比如,远程调用框架它的协议是可以替换的,
如果只提供一个总的扩展接口,当然可以做到切换协议,
但协议支持是可以细分为底层通讯,序列化,动态代理方式等等,
如果将接口拆细,正交分解,会更便于扩展者复用已有逻辑,而只是替换某部分实现策略,
当然这个分解的粒度需要把握好。
5. 微核插件式,平等对待第三方
大凡发展的比较好的框架,都遵守微核的理念,
Eclipse的微核是OSGi, Spring的微核是BeanFactory,Maven的微核是Plexus,
通常核心是不应该带有功能性的,而是一个生命周期和集成容器,
这样各功能可以通过相同的方式交互及扩展,并且任何功能都可以被替换,
如果做不到微核,至少要平等对待第三方,
即原作者能实现的功能,扩展者应该可以通过扩展的方式全部做到,
原作者要把自己也当作扩展者,这样才能保证框架的可持续性及由内向外的稳定性。
6. 不要控制外部对象的生命周期
比如上面说的Action使用接口和Renderer扩展接口,
框架如果让使用者或扩展者把Action或Renderer实现类的类名或类元信息报上来,
然后在内部通过反射newInstance()创建一个实例,
这样框架就控制了Action或Renderer实现类的生命周期,
Action或Renderer的生老病死,框架都自己做了,外部扩展或集成都无能为力,
好的办法是让使用者或扩展者把Action或Renderer实现类的实例报上来,
框架只是使用这些实例,这些对象是怎么创建的,怎么销毁的,都和框架无关,
框架最多提供工具类辅助管理,而不是绝对控制。
7. 可配置一定可编程,并保持友好的CoC约定
因为使用环境的不确定因素很多,框架总会有一些配置,
一般都会到classpath直扫某个指定名称的配置,或者启动时允许指定配置路径,
做为一个通用框架,应该做到凡是能配置文件做的一定要能通过编程方式进行,
否则当使用者需要将你的框架与另一个框架集成时就会带来很多不必要的麻烦,
另外,尽可能做一个标准约定,如果用户按某种约定做事时,就不需要该配置项。
比如:配置模板位置,你可以约定,如果放在templates目录下就不用配了,
如果你想换个目录,就配置下。
8. 区分命令与查询,明确前置条件与后置条件
这个是契约式设计的一部分,尽量遵守有返回值的方法是查询方法,void返回的方法是命令,
查询方法通常是幂等性的,无副作用的,也就是不改变任何状态,调n次结果都是一样的,
比如get某个属性值,或查询一条数据库记录,
命令是指有副作用的,也就是会修改状态,比如set某个值,或update某条数据库记录,
如果你的方法即做了修改状态的操作,又做了查询返回,如果可能,将其拆成写读分离的两个方法,
比如:User deleteUser(id),删除用户并返回被删除的用户,考虑改为getUser()和void的deleteUser()。
另外,每个方法都尽量前置断言传入参数的合法性,后置断言返回结果的合法性,并文档化。
8. 增量式扩展,而不要扩充原始核心概念 参见:http://javatar.iteye.com/blog/690845
分布式服务框架常被质疑的价值
2010-11-05 https://www.iteye.com/blog/javatar-804182
每次分享分布式服务框架,讲到带来的价值时,
像什么可靠高性能,服务治理等等一些常规价值,大家还能听我们吹吹,
但有几条不明显的价值经常被质疑,所以写下来,省点口舌,
1. 可以减少DB连接数:
其实原因很简单,当集群特别大时,比如应用集群上万台时,
如果每台连接池最小连接数为一,也要持有一万连接,
当加一个中间层,让很少的中间层集群访问数据库,就会减少很多,
因为某个兄弟公司就是因为这个原因才做分布式拆分的,所以我们才把它列为价值的一条。
2. 可以提高资源利用率:
因为服务通常是无状态或少量状态的可并行的一些业务逻辑,
可以说是计算密集型程序,基本上适用Amdahl’s Law原则:
Amdahl’s Law:http://en.wikipedia.org/wiki/Amdahl’s_law
加速度S等于:
其中,P为程序的可并行比率,N为处理器个数(也就是机器数)。
分布式切分应用后:
- 缩小了集群规模,小规模增加机器收益最高。
- 分离了串行因素,使多数集群并行因子增大。
所以可以用更少的机器来加速应用,也就提升了资源的利用率。
负载均衡扩展接口重构
2010-11-05 https://www.iteye.com/blog/javatar-804183
项目中的一个重构的过程及理由,用于知会团队成员,在这里备一个。
RPC远程调用框架中有很多可选的负载均衡策略,
比如:随机,轮循,最少连接等等,
这个时候就需要一个SPI扩展点,为后续增加新的策略提供可能,
重构前:
原接口形式,如下:
1
2
3
4
5
6
public class LoadBalance<T> {
// 给定资源和权重,返回选中资源的下标号
int select(T[] resources, int[] weights);
}
问题一:
返回下标号,使接口输入输出不一致,并且限制了资源的包装,如:
1
2
3
4
5
6
public class LoadBalance {
// 给定资源和权重,返回选中资源
<T> T select(T[] resources, int[] weights);
}
问题二:
作为扩展接口,即然使用泛型,表示策略实现不限制资源类型,接口本身定义泛型没有意义,泛型声明应该定义在方法上,如:
1
2
3
4
5
6
public class LoadBalance {
// 给定资源和权重,返回选中资源
<T> T select(T[] resources, int[] weights);
}
问题三:
作为扩展接口,权重信息的传递过于特殊化,
比如现在最小连接数策略要用到当前活跃连接数,
需增加新的参数actives,如:
1
2
3
4
5
6
public class LoadBalance {
// 给定资源和权重,返回选中资源
<T> T select(T[] resources, int[] weights, int[] actives);
}
问题四:
但像上面这样,你并不清楚后续还有什么参数要加入,接口的契约不容扩展,
另一种办法是,在最小连接数策略实现中将T强制转型成RpcInvoker接口,然后调用getActive(),
但即然active数通过get方获取,为什么weight却通过另一个参数传入,明显的不一致,
而且这里的强制转型,也会导致策略的实现并不能像原来期望的通用,
作为一个框架的扩展点,通用意义并不大,越通用越难用,上面的泛型T有过度设计之嫌,
直接用RpcInvoker作为参数,更能保证契约的完备性,如:
1
2
3
4
5
6
7
public class LoadBalance {
// RpcInvoker接口中有getWeight(), getActive()等获取参数方法
// 给定资源和权重,返回选中资源
RpcInvoker select(RpcInvoker[] resources);
}
如果有通用性需求,也可以考虑再抽取一个接口,如:
1
2
3
4
5
6
public class LoadBalance {
// 给定资源和权重,返回选中资源
Selectable select(Selectable[] resources);
}
问题五:
有一种需求是基于客户端一致性的,要求执行所有RpcInvoker,而不是选其中一个RpcInvoker执行,
原有实现,是将其作为特例,写死在代码中的,基于上面的LoadBalance接口,
完全可以将传入的所有RpcInvoker[]包装成一个总的RpcInvoker,里面用for循环委派所有调用。
如果有w + r > n(写节点 + 读节点 > 总节点)的一致性需求,也可以用相应方法处理,只是增加一些配置项,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AllLoadBalance {
public RpcInvoker select(RpcInvoker[] resources) {
return new AllRpcInvoker(resources);
}
}
class AllRpcInvoker implements RpcInvoker {
private RpcInvoker[] invokers;
public AllRpcInvoker(RpcInvoker[] invokers) {
this.invokers = invokers;
}
// Delegate all methods to invokers
}
问题六:
有的策略实现是带状态的,比如轮循策略需记录轮循序号,
也就是并不能单实例使用LoadBalance实例,
这对框架的维护非常不利,容易给后来的维护者埋下地雷,
而且,同一个resources集合,必需用同一个LoadBalance实例,
也就是LoadBalance实例的变更是跟随resources集合的变更,
即然如此,资源集合的设定,可以在select()之前确定,如:
1
2
3
4
5
6
7
8
9
public class LoadBalance {
// 初始化资源集合
void init(RpcInvoker[] resources);
// 返回选中资源
RpcInvoker select();
}
这样的好处是,LoadBalance的实现可以在init()时做预处理及缓存,
比如随机策略,需要统计总权重,如果在init()方法中统计,
select()时可以减少一次for循环,
而且,可以通过重复调用init()方法,复用单一LoadBalance实例,
当然,LoadBalance的实现需确保线程安全性。
防痴呆设计
2010-11-05 https://www.iteye.com/blog/javatar-804187
最近有点痴呆,因为解决了太多的痴呆问题,
服务框架实施面超来超广,已有50多个项目在使用,
每天都要去帮应用查问题,来来回回,
发现大部分都是配置错误,或者重复的文件或类,或者网络不通等,
所以准备在新版本中加入防痴呆设计,估且这么叫吧,
可能很简单,但对排错速度还是有点帮助,
希望能抛砖引玉,也希望大家多给力,想出更多的防范措施共享出来。
1. 检查重复的jar包
最痴呆的问题,就是有多个版本的相同jar包,
会出现新版本的A类,调用了旧版本的B类,
而且和JVM加载顺序有关,问题带有偶然性,误导性,
遇到这种莫名其妙的问题,最头疼,
所以,第一条,先把它防住,
在每个jar包中挑一个一定会加载的类,加上重复类检查,
给个示例:
1
2
3
static {
Duplicate.checkDuplicate(Xxx.class);
}
检查重复工具类:
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
public final class Duplicate {
private Duplicate() {}
public static void checkDuplicate(Class cls) {
checkDuplicate(cls.getName().replace('.', '/') + ".class");
}
public static void checkDuplicate(String path) {
try {
// 在ClassPath搜文件
Enumeration urls = Thread.currentThread().getContextClassLoader().getResources(path);
Set files = new HashSet();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
if (url != null) {
String file = url.getFile();
if (file != null && file.length() > 0) {
files.add(file);
}
}
}
// 如果有多个,就表示重复
if (files.size() > 1) {
logger.error("Duplicate class " + path + " in " + files.size() + " jar " + files);
}
} catch (Throwable e) { // 防御性容错
logger.error(e.getMessage(), e);
}
}
}
2. 检查重复的配置文件
配置文件加载错,也是经常碰到的问题,
用户通常会和你说:“我配置的很正确啊,不信我发给你看下,但就是报错”,
然后查一圈下来,原来他发过来的配置根本没加载,
平台很多产品都会在classpath下放一个约定的配置,
如果项目中有多个,通常会取JVM加载的第一个,
为了不被这么低级的问题折腾,
和上面的重复jar包一样,在配置加载的地方,加上:
1
Duplicate.checkDuplicate("xxx.properties");
3. 检查所有可选配置
必填配置估计大家都会检查,因为没有的话,根本没法运行,
但对一些可选参数,也应该做一些检查,
比如:服务框架允许通过注册中心关联服务消费者和服务提供者,
也允许直接配置服务提供者地址点对点直连,
这时候,注册中心地址是可选的,
但如果没有配点对点直连配置,注册中心地址就一定要配,
这时候也要做相应检查。
4. 异常信息给出解决方案
在给应用排错时,最怕的就是那种只有简单的一句错误描述,啥信息都没有的异常信息,
比如上次碰到一个Failed to get session异常,
就这几个单词,啥都没有,哪个session出错? 什么原因Failed?
看了都快疯掉,因是线上环境不好调试,而且有些场景不是每次都能重现,
异常最基本要带有上下文信息,包括操作者,操作目标,原因等,
最好的异常信息,应给出解决方案,比如上面可以给出:
“从10.20.16.3到10.20.130.20:20880之间的网络不通,
请在10.20.16.3使用telnet 10.20.130.20 20880测试一下网络,
如果是跨机房调用,可能是防火墙阻挡,请联系SA开通访问权限”
等等,上面甚至可以根据IP段判断是不是跨机房。
另外一个例子,是spring-web的context加载,
如果在getBean时spring没有被启动,
spring会报一个错,错误信息写着:
请在web.xml中加入:
多好的同学,看到错误的人复制一下就完事了,我们该学学,
可以把常见的错误故意犯一遍,看看错误信息能否自我搞定问题,
或者把平时支持应用时遇到的问题及解决办法都写到异常信息里。
5. 日志信息包含环境信息
每次应用一出错,应用的开发或测试就会把出错信息发过来,询问原因,
这时候我都会问一大堆套话,
用的哪个版本呀?
是生产环境还是开发测试环境?
哪个注册中心呀?
哪个项目中的?
哪台机器呀?
哪个服务?
。。。
累啊,最主要的是,有些开发或测试人员根本分不清,
没办法,只好提供上门服务,浪费的时间可不是浮云,
所以,日志中最好把需要的环境信息一并打进去,
最好给日志输出做个包装,统一处理掉,免得忘了。
包装Logger接口如:
public void error(String msg, Throwable e) {
delegate.error(msg + " on server " + InetAddress.getLocalHost() + " using version " + Version.getVersion(), e);
}
获取版本号工具类:
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
public final class Version {
private Version() {}
private static final Logger logger = LoggerFactory.getLogger(Version.class);
private static final Pattern VERSION_PATTERN = Pattern.compile("([0-9][0-9\\.\\-]*)\\.jar");
private static final String VERSION = getVersion(Version.class, "2.0.0");
public static String getVersion(){
return VERSION;
}
public static String getVersion(Class cls, String defaultVersion) {
try {
// 首先查找MANIFEST.MF规范中的版本号
String version = cls.getPackage().getImplementationVersion();
if (version == null || version.length() == 0) {
version = cls.getPackage().getSpecificationVersion();
}
if (version == null || version.length() == 0) {
// 如果MANIFEST.MF规范中没有版本号,基于jar包名获取版本号
String file = cls.getProtectionDomain().getCodeSource().getLocation().getFile();
if (file != null && file.length() > 0 && file.endsWith(".jar")) {
Matcher matcher = VERSION_PATTERN.matcher(file);
while (matcher.find() && matcher.groupCount() > 0) {
version = matcher.group(1);
}
}
}
// 返回版本号,如果为空返回缺省版本号
return version == null || version.length() == 0 ? defaultVersion : version;
} catch (Throwable e) { // 防御性容错
// 忽略异常,返回缺省版本号
logger.error(e.getMessage(), e);
return defaultVersion;
}
}
}
6. kill之前先dump
每次线上环境一出问题,大家就慌了,
通常最直接的办法回滚重启,以减少故障时间,
这样现场就被破坏了,要想事后查问题就麻烦了,
有些问题必须在线上的大压力下才会发生,
线下测试环境很难重现,
不太可能让开发或Appops在重启前,
先手工将出错现场所有数据备份一下,
所以最好在kill脚本之前调用dump,
进行自动备份,这样就不会有人为疏忽。
dump脚本示例:
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
JAVA_HOME=/usr/java
OUTPUT_HOME=~/output
DEPLOY_HOME=`dirname $0`
HOST_NAME=`hostname`
DUMP_PIDS=`ps --no-heading -C java -f --width 1000 | grep "$DEPLOY_HOME" |awk '{print $2}'`
if [ -z "$DUMP_PIDS" ]; then
echo "The server $HOST_NAME is not started!"
exit 1;
fi
DUMP_ROOT=$OUTPUT_HOME/dump
if [ ! -d $DUMP_ROOT ]; then
mkdir $DUMP_ROOT
fi
DUMP_DATE=`date +%Y%m%d%H%M%S`
DUMP_DIR=$DUMP_ROOT/dump-$DUMP_DATE
if [ ! -d $DUMP_DIR ]; then
mkdir $DUMP_DIR
fi
echo -e "Dumping the server $HOST_NAME ...\c"
for PID in $DUMP_PIDS ; do
$JAVA_HOME/bin/jstack $PID > $DUMP_DIR/jstack-$PID.dump 2>&1
echo -e ".\c"
$JAVA_HOME/bin/jinfo $PID > $DUMP_DIR/jinfo-$PID.dump 2>&1
echo -e ".\c"
$JAVA_HOME/bin/jstat -gcutil $PID > $DUMP_DIR/jstat-gcutil-$PID.dump 2>&1
echo -e ".\c"
$JAVA_HOME/bin/jstat -gccapacity $PID > $DUMP_DIR/jstat-gccapacity-$PID.dump 2>&1
echo -e ".\c"
$JAVA_HOME/bin/jmap $PID > $DUMP_DIR/jmap-$PID.dump 2>&1
echo -e ".\c"
$JAVA_HOME/bin/jmap -heap $PID > $DUMP_DIR/jmap-heap-$PID.dump 2>&1
echo -e ".\c"
$JAVA_HOME/bin/jmap -histo $PID > $DUMP_DIR/jmap-histo-$PID.dump 2>&1
echo -e ".\c"
if [ -r /usr/sbin/lsof ]; then
/usr/sbin/lsof -p $PID > $DUMP_DIR/lsof-$PID.dump
echo -e ".\c"
fi
done
if [ -r /usr/bin/sar ]; then
/usr/bin/sar > $DUMP_DIR/sar.dump
echo -e ".\c"
fi
if [ -r /usr/bin/uptime ]; then
/usr/bin/uptime > $DUMP_DIR/uptime.dump
echo -e ".\c"
fi
if [ -r /usr/bin/free ]; then
/usr/bin/free -t > $DUMP_DIR/free.dump
echo -e ".\c"
fi
if [ -r /usr/bin/vmstat ]; then
/usr/bin/vmstat > $DUMP_DIR/vmstat.dump
echo -e ".\c"
fi
if [ -r /usr/bin/mpstat ]; then
/usr/bin/mpstat > $DUMP_DIR/mpstat.dump
echo -e ".\c"
fi
if [ -r /usr/bin/iostat ]; then
/usr/bin/iostat > $DUMP_DIR/iostat.dump
echo -e ".\c"
fi
if [ -r /bin/netstat ]; then
/bin/netstat > $DUMP_DIR/netstat.dump
echo -e ".\c"
fi
echo "OK!"
Hessian序列化不设SerializerFactory性能问题
2010-12-27 https://www.iteye.com/blog/javatar-852663
服务框架全面重构后,因换了通讯协议,采用Magic头识别新旧版本,
性能测试发现,在兼容旧版本模式下,性能下降10倍,
原来一个1ms到2ms的请求,现在需要11ms到12ms,
对比新旧版本代码,发现四个不同点:
- UnsafeByteArrayOutputStream是不是比ByteArrayOutputStream慢很多?
- 通过java.nio.ByteBuffer转换到mina的ByteBuffer映射写入慢很多?
- 重复使用一个ByteArrayOutputStream是不是比多个ByteArrayOutputStream慢很多?
- 没有设置SerializerFactory会比设置了慢很多?
逐个验证,前面三个对性能几乎没有影响,修改第四个,性能立马提升。
旧版本代码:
1
2
3
4
5
6
7
private static final SerializerFactory _serializerFactory = new SerializerFactory();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
Hessian2Output h2out = new Hessian2Output(bout);
h2out.setSerializerFactory(_serializerFactory); // 问题所在
h2out.writeObject(msg);
h2out.flush();
byte[] content = bout.toByteArray();
新版本代码:
1
2
3
4
5
UnsafeByteArrayOutputStream bos = new UnsafeByteArrayOutputStream(1024);
Hessian2Output h2o = new Hessian2Output(bos);
h2o.writeObject(connReq);
h2o.flush();
byte[] content = bos.toByteArray();
新代码没有调用h2o.setSerializerFactory(serializerFactory);
就因为这一句,性能下降10倍。
我们来看一下Hessian3.2.1的源代码:
public void writeObject(Object object) throws IOException {
if (object == null) {
writeNull();
return;
}
Serializer serializer;
serializer = findSerializerFactory().getSerializer(object.getClass());
serializer.writeObject(object, this);
}
public final SerializerFactory findSerializerFactory() {
SerializerFactory factory = _serializerFactory;
if (factory == null)
_serializerFactory = factory = new SerializerFactory();
return factory;
}
看代码,在writeObject()时,如果发现没有设置SerializerFactory,会自动设一个SerializerFactory,
看起来好像没有问题,我们自己设的SerializerFactory也是直接new出来的,没做什么手脚,
那为什么性能会下降这么快呢?开始还真被这行代码唬住了,看起来没啥区别,
仔细一想,才发现,Hessian2Output对象是prototype的,
每次请求,都会创建一个实例,用完即销毁,这样的话,基于下面的方式:
1
2
if (factory == null)
_serializerFactory = factory = new SerializerFactory(); // 问题所在
每一个Hessian2Output内部都会重新new SerializerFactory();
应改为:
1
2
3
private static final SerializerFactory DEFAULT_SERIALIZER_FACTORY =new SerializerFactory();
if (factory == null)
_serializerFactory = factory = DEFAULT_SERIALIZER_FACTORY;
从这里可以看出是创建SerializerFactory的开销非常大,导致性能下降严重,
这个应该算是Hessian的BUG,这种线程安全的工厂类,就不应该在设默认值时,每次都new一个。
大家用Hessian的时候请小心这个问题。
后续还发现,不设置SerializerFactory,会出现大量线程被阻塞:
(下图为VisualVM截图,红色标识的片断为Blocked状态)
配置设计
2011-03-09 https://www.iteye.com/blog/javatar-949527
Dubbo现在的设计是完全无侵入,也就是使用者只依赖于配置契约,
经过多个版本的发展,为了满足各种需求场景,配置越来越多,
为了保持兼容,配置只增不减,里面潜伏着各种风格,约定,规则,
新版本也将配置做了一次调整,去掉了dubbo.properties,改为全spring配置,
将想到的一些记在这,备忘。
1. 配置分类
首先,配置的用途是有多种的,大致可以分为:
(1) 环境配置,比如:连接数,超时等配置。
(2) 描述配置,比如:服务接口描述,服务版本等。
(3) 扩展配置,比如:协议扩展,策略扩展等。
2. 配置格式
(1) 通常环境配置,用properties配置会比较方便,
因为都是一些离散的简单值,用key-value配置可以减少配置的学习成本。
(2) 而描述配置,通常信息比较多,甚至有层次关系,
用xml配置会比较方便,因为树结构的配置表现力更强,
如果非常复杂,也可以考自定义DSL做为配置,
有时候这类配置也可以用Annotation代替,
因为这些配置和业务逻辑相关,放在代码里也是合理的。
(3) 另外扩展配置,可能不尽相同,
如果只是策略接口实现类替换,可以考虑properties等结构,
如果有复杂的生命周期管理,可能需要XML等配置,
有时候扩展会通过注册接口的方式提供。
3. 配置加载
(1) 对于环境配置,
在java世界里,比较常规的做法,
是在classpath下约定一个以项目为名称的properties配置,
比如:log4j.properties,velocity.properties等,
产品在初始化时,自动从classpath下加载该配置,
我们平台的很多项目也使用类似策略,
如:dubbo.properties,comsat.xml等,
这样有它的优势,就是基于约定,简化了用户对配置加载过程的干预,
但同样有它的缺点,当classpath存在同样的配置时,可能误加载,
以及在ClassLoader隔离时,可能找不到配置,
并且,当用户希望将配置放到统一的目录时,不太方便。
Dubbo新版本去掉了dubbo.properties,因为该约定经常造成配置冲突。
(2) 而对于描述配置,
因为要参与业务逻辑,通常会嵌到应用的生命周期管理中,
现在使用spring的项目越来越多,直接使用spring配置的比较普遍,
而且spring允许自定义schema,配置简化后很方便,
当然,也有它的缺点,就是强依赖spring,
可以提编程接口做了配套方案。
在Dubbo即存在描述配置,也有环境配置,
一部分用spring的schame配置加载,一部分从classpath扫描properties配置加载,
用户感觉非常不便,所以在新版本中进行了合并,
统一放到spring的schame配置加载,也增加了配置的灵活性。
(3) 扩展配置,通常对配置的聚合要求比较高,
因为产品需要发现第三方实现,将其加入产品内部,
在java世里,通常是约定在每个jar包下放一个指定文件加载,
比如:eclipse的plugin.xml,struts2的struts-plugin.xml等,
这类配置可以考虑java标准的服务发现机制,
即在jar包的META-INF/services下放置接口类全名文件,内容为每行一个实现类类名,
就像jdk中的加密算法扩展,脚本引擎扩展,新的JDBC驱动等,都是采用这种方式,
参见:ServiceProvider规范
Dubbo旧版本通过约定在每个jar包下,
放置名为dubbo-context.xml的spring配置进行扩展与集成,
新版本改成用jdk自带的META-INF/services方式,
去掉过多的spring依赖。
4. 可编程配置
配置的可编程性是非常必要的,不管你以何种方式加载配置文件,
都应该提供一个编程的配置方式,允许用户不使用配置文件,直接用代码完成配置过程,
因为一个产品,尤其是组件类产品,通常需要和其它产品协作使用,
当用户集成你的产品时,可能需要适配配置方式。
Dubbo新版本提供了与xml配置一对一的配置类,
如:ServiceConfig对应
这样有利于文件配置与编程配置的一致性理解,减少学习成本。
5. 配置缺省值
配置的缺省值,通常是设置一个常规环境的合理值,这样可以减少用户的配置量,
通常建议以线上环境为参考值,开发环境可以通过修改配置适应,
缺省值的设置,最好在最外层的配置加载就做处理,
程序底层如果发现配置不正确,就应该直接报错,容错在最外层做,
如果在程序底层使用时,发现配置值不合理,就填一个缺省值,
很容易掩盖表面问题,而引发更深层次的问题,
并且配置的中间传递层,很可能并不知道底层使用了一个缺省值,
一些中间的检测条件就可能失效,
Dubbo就出现过这样的问题,中间层用“地址”做为缓存Key,
而底层,给“地址”加了一个缺省端口号,
导致不加端口号的“地址”和加了缺省端口的“地址”并没有使用相同的缓存。
6. 配置一致性
配置总会隐含一些风格或潜规则,应尽可能保持其一致性,
比如:很多功能都有开关,然后有一个配置值:
(1) 是否使用注册中心,注册中心地址。
(2) 是否允许重试,重试次数。
你可以约定:
(1) 每个都是先配置一个boolean类型的开关,再配置一个值。
(2) 用一个无效值代表关闭,N/A地址,0重试次数等。
不管选哪种方式,所有配置项,都应保持同一风格,Dubbo选的是第二种,
相似的还有,超时时间,重试时间,定时器间隔时间,
如果一个单位是秒,另一个单位是毫秒(C3P0的配置项就是这样),配置人员会疯掉。
7. 配置覆盖
提供配置时,要同时考虑开发人员,测试人员,配管人员,系统管理员,
测试人员是不能修改代码的,而测试的环境很可能较为复杂,
需要为测试人员留一些“后门”,可以在外围修改配置项,
就像spring的PropertyPlaceholderConfigurer配置,支持SYSTEM_PROPERTIES_MODE_OVERRIDE,
可以通过JVM的-D参数,或者像hosts一样约定一个覆盖配置文件,
在程序外部,修改部分配置,便于测试。
Dubbo支持通过JVM参数-Dcom.xxx.XxxService=dubbo://10.1.1.1:1234
直接使远程服务调用绕过注册中心,进行点对点测试。
还有一种情况,开发人员增加配置时,都会按线上的部署情况做配置,如:
因为线上只有一个注册中心,这样的配置是没有问题的,
而测试环境可能有两个注册中心,测试人员不可能去修改配置,改为:
所以这个地方,Dubbo支持在${dubbo.registry.address}的值中,
通过竖号分隔多个注册中心地址,用于表示多注册中心地址。
8. 配置继承
配置也存在“重复代码”,也存在“泛化与精化”的问题,
比如:Dubbo的超时时间设置,每个服务,每个方法,都应该可以设置超时时间,
但很多服务不关心超时,如果要求每个方法都配置,是不现实的,
所以Dubbo采用了,方法超时继承服务超时,服务超时再继承缺省超时,没配置时,一层层向上查找。
另外,Dubbo旧版本所有的超时时间,重试次数,负载均衡策略等都只能在服务消费方配置,
但实际使用过程中发现,服务提供方比消费方更清楚,但这些配置项是在消费方执行时才用到的,
新版本,就加入了在服务提供方也能配这些参数,通过注册中心传递到消费方,
做为参考值,如果消费方没有配置,就以提供方的配置为准,相当于消费方继承了提供方的建议配置值,
而注册中心在传递配置时,也可以在中途修改配置,这样就达到了治理的目的,继承关系相当于:
服务消费者 –> 注册中心 –> 服务提供者
9. 配置向后兼容
向前兼容很好办,你只要保证配置只增不减,就基本上能保证向前兼容,
但向后兼容,也是要注意的,要为后续加入新的配置项做好准备,
如果配置出现一个特殊配置,就应该为这个“特殊”情况约定一个兼容规则,
因为这个特殊情况,很有可能在以后还会发生,
比如:有一个配置文件是保存“服务=地址”映射关系的,
其中有一行特殊,保存的是“注册中心=地址”,
现在程序加载时,约定“注册中心”这个Key是特殊的,
做特别处理,其它的都是“服务”,
然而,新版本发现,要加一项“监控中心=地址”,
这时,旧版本的程序会把“监控中心”做为“服务”处理,
因为旧代码是不能改的,兼容性就很会很麻烦,
如果先前约定“特殊标识+XXX”为特殊处理,后续就会方便很多。
向后兼容性,可以多向HTML5学习,参见:HTML5设计原理
关于产品的落地
2011-03-12 https://www.iteye.com/blog/javatar-957588
前些天和老庄讨论MinasDynamic的Scope,讲到了产品的落地。
在09年的时候,我和付大叔一起做过Minas,最终没有推广开来,
导致中文站和国际站各自发展了自己的配置管理中心,
失败的原因可能很多,但有一部分原因,就是Scope一直扩大,没法落地,
而且长时间不稳定,连基本功能都没做好,就开始做花哨的功能,
当时网站最担心的就是线上的稳定性,Minas挂了的影响面会非常大,
不稳定的主要因素来自复杂,当时将静态配置和动态配置合二为一,
而实际上两者根本不是一码事,它们的区别比共同点还大,拆成两个项目来做都不为过,
统一后的模型,可想而知,难用,复杂。
到现在,为了统一配置的管理,准备重写新版本的Minas,
新版本的Minas,第一点,先把静态和动态配置,彻底分开来做,
现在静态配置已经可以Work了,开始着手动态配置这一块,
也就是现在讨论的MinasDynamic,需求讨论了很久,趋势也是越来越复杂,
模型又要适应KV,又要适应Tree,又是ad-hoc无中心交叉组网,又是gossip/paxos一致性算法。
都很重要,但MinasDynamic的“应力”在哪?我们为什么要一个MinasDynamic?
我想大家心里都清楚,最大的需求,来自于平台很多产品都有一个管理中心,
每个中心,都面临数据变更推送,单点故障,HA可用性,节点和网络Failover等问题,
也就是说,大家关心MinasDynamic的“非功能性需求”,而不是特别关心MinasDynamic的“功能性需求”,
就算MinasDynamic只做成一个KVEngine,只要稳定性好,大家都会想办法适应MinasDynamic的模型,
而如果不稳定,就算做成一朵花,大家还是会各做各的。
这让我想起陈在讲《战略分解思路》时讲到的“三面镜子”,
做任何一件事一个产品,你手里都应拿着三面镜子:望远镜,凸透镜,显微镜,
首先,是要拿着望远镜,站在一定高度,知道你的产品在整个战略格局中的位置,并从梦想,使命,愿景,战略,组织,人,整个都想通透,
第二,是要拿着凸透镜,找到焦点,找到落地点,不要什么都想做,要从想做,能做,可做中抽取出该做的事,
就像陈讲到的事例,在04年公司就让他做交易这一块,当时,他觉得市场越大,挣的钱越多,所有市场都做,
觉得淘宝客户不够多,还发展了线下客户,最终到06年交易产品线还没有什么成果,只能停了,
而在09年,公司让他重新做交易产品,他只选服装市场,只选淘宝大卖家做为客户,做细做好,迅速就产生了效应,
成功后,公司给了更多资源,再推广到其它市场,这就是落地的重要。
第三,是要拿着显微镜,贴近用户,从用户的细微处观察,比用户更懂用户的问题所在,解决用户最迫切的需求,而不是去做调查出来的重要但无关痛痒的事,用户当然是什么都想要。
完美主义与功利主义
2011-03-25 https://www.iteye.com/blog/javatar-974742
最近在想,每个人都有追求完美的时候,也都有功利的时候,
太追求完美不好,太功利也不好,那应该如何权衡呢。
看到Andy2在内网的帖子,他的完美主义,碰上了功利主义,只能发泄一下闪人,
低成本的数据中心,不只是节省公司那些台机器的问题,
而是关系到商业模式能不能运转,云计算能不能落地的问题,
而不是步ChinaCache的后尘,不计成本的扩张销售,
最后云计算的成本比用户自建数据中心的成本还高,没有一点规模效应,
当用户遇到资金问题,开始退出,云计算中心的高昂机器何去何从,
而国外大多数成功的云计算中心,像Amazon等,都有很强的价格优势,
在波特的企业竞争策略中,低成本,差异化,集中化,是竞争的三大要素,
像云计算这种差异化不大的产品中,低成本是至关重要的,
采用定制机,是节省成本的必经之路,虽然可靠性有所下降,但这正是研发飞天系统的作用,
并且,现在有哪个应用不是集群的,哪个应用会因为挂一台机器而出故障?
当然,运维出于自身利益,采购品牌机,增加可靠性,虽然有些功利主义,但也是可以理解的,
因为让网站稳定运行才是他们的KPI,云计算是死是活与他们关系不是那么大。
记得蔡学镛离职的时候,写了一篇《KPI心理学》,讲公司KPI制度的问题,
但个人认为没有KPI可能更糟,公司可能会变得很官僚,失去动力,
蔡学镛将入职后对KPI看重程度分为四个阶段:
70%(照着做) -> 30%(不关心) -> 100%(担心前途) -> 0%(离职)
第二阶段对公司最有利,员工趋向完美主义,只想把事情做好,不关心什么KPI。
第三阶段对公司最不利,员工趋向功利主义,只想做能体现绩效,能升职,能加薪,能拿奖的事,其它不管。
发现现在技术部的奖项也开始驱向功利,能节省多少成本,有多少商业价值,决定能不能拿奖,
当然,这个做法没错,让大家务实的做产品,不要搞些虚无飘渺的东西,
但如果不停的向全员灌输功利思想,也有不好的一面,使大家过于看重短期利益,
一个产品有没有商业价值,要不要做,
在管理层,PD,运营做选择时,可能要功利些,否则可能会好看不中用。
但基层员工没有选择权,他们不能决定参加什么项目,也不能从大方向上改变项目的价值,
如果一个开发人员参与“有商业价值”的项目,另一个开发人员参与“没有明显商业价值”的基础项目,
两个人同样努力,而奖惩不一样,或许会有失公平。
在管理学薪酬体系中,技术和科研类的大多是为能力付薪的,
当然也有为职位,为绩效,为市场付薪的,
所以只有苦劳的员工最容易被淘汰,
要么就完美主义,向着自己的梦想前进,
要么就功利主义,积累些经验和资质再说,
埋头苦干,尤其是重复劳动,只会剩下十个“一年”工作经验。
分布式事务
2011-03-31 https://www.iteye.com/blog/javatar-981787
关于Dubbo服务框架的分布式事务,虽然现在不急着做,但可以讨论一下。
我觉得事务的管理不应该属于Dubbo框架,
Dubbo只需实现可被事务管理即可,
像JDBC和JMS都是可被事务管理的分布式资源,
Dubbo只要实现相同的可被事务管理的行为,比如可以回滚,
其它事务的调度,都应该由专门的事务管理器实现。
在Java中,分布式事务主要的规范是JTA/XA,
其中:JTA是Java的事务管理器规范,
XA是工业标准的X/Open CAE规范,可被两阶段提交及回滚的事务资源定义,
比如某数据库实现了XA规范,则不管是JTA,还是MSDTC,都可以基于同样的行为对该数据库进行事务处理。
在JTA/XA中,主要有两个扩展点:
1. TransactionManager
JTA事务管理器接口,实现该接口,即可完成对所有XA资源的事务调度,比如BEA的Tuxedo,JBossJTA等。
2. XAResource
XA资源接口,实现该接口,即可被任意TransactionManager调度,比如:JDBC的XAConnection,JMS的XAMQ等。
而Dubbo的远程服务,也应该是一个XAResource,比如:XAInvoker和XAExporter,
Dubbo只需在第一次提交时,将请求发到服务提供方进行缓存和写盘,
在第二次提交时,再基于缓存调用服务的Impl实现,
当然一些健状性分支流程要考虑清楚。
JTA/XA的基本原理如下:
1. 用户启动一个事务:
1
transactionManager.begin();
2. 事务管理器在当前线程中初始化一个事务实例:
1
threadLocal.set(new TransactionImpl());
3. 用户调用JDBC或JMS或Dubbo请求,请求内部初始化一个XAResource实例:
1
XAResource xaResource = new XAResourceImpl(); // 比如:XAConnection
4. JDBC或JMS或Dubbo内部从当前线程获取事务:
1
Transaction transaction = transactionManager.getTransaction(); // 其内部为:threadLocal.get();
5. 将当前XAResource注册到事务中:
1
transaction.enlistResource(xaResource);
6. 用户提交一个事务:
1
transactionManager.commit(); // 其内部为:getTransaction().commit();
7. 事务for循环调用所有注册的XAResource的两阶段提交:
1
2
3
4
5
6
Xid xid = new XidImpl();
for (XAResource xaResource: xaResources) {
xaResource.prepare(xid);
xaResource.commit(xid, true);
xaResource.commit(xid, false);
}
8. 当然,还有一些异常流程,比如rollback和forget等。
1
2
3
4
5
6
7
8
9
10
TransactionManager transactionManager = ...; // 从JNDI进行lookup等方式获取
transactionManager.begin(); // 启动事务
try {
jdbcConn.executeUpdate(sql); // 执行SQL语句,DB写入binlog,但不更新表
jmsMQ.send(message); // 发送消息,MQ记录消息,但不进入队列
dubboService.invoke(parameters); // 调用远程服务,Provider缓存请求信息,但不执行
transactionManager.commit(); // 提交事务,数据库,消息队列,远程服务同时提交
} catch(Throwable t) {
transactionManager.rollback(); // 回滚事务,数据库,消息队列,远程服务同时回滚
}
Dubbo扩展点重构
2011-05-12 https://www.iteye.com/blog/javatar-1041832
随着服务化的推广,网站对Dubbo服务框架的需求逐渐增多,
Dubbo的现有开发人员能实现的需求有限,很多需求都被delay,
而网站的同学也希望参与进来,加上领域的推动,
所以平台计划将部分项目对公司内部开放,让大家一起来实现,
Dubbo为试点项目之一。
既然要开放,那Dubbo就要留一些扩展点,
让参与者尽量黑盒扩展,而不是白盒的修改代码,
否则分支,质量,合并,冲突都会很难管理。
先看一下Dubbo现有的设计:
这里面虽然有部分扩展接口,但并不能很好的协作,
而且扩展点的加载和配置都没有统一处理,所以下面对它进行重构。
第一步,微核心,插件式,平等对待第三方。
即然要扩展,扩展点的加载方式,首先要统一,
微核心+插件式,是比较能达到OCP原则的思路,
由一个插件生命周期管理容器,构成微核心,
核心不包括任何功能,这样可以确保所有功能都能被替换,
并且,框架作者能做到的功能,扩展者也一定要能做到,以保证平等对待第三方,
所以,框架自身的功能也要用插件的方式实现,不能有任何硬编码。
通常微核心都会采用Factory,IoC,OSGi等方式管理插件生命周期,
考虑Dubbo的适用面,不想强依赖Spring等IoC容器,
自已造一个小的IoC容器,也觉得有点过度设计,
所以打算采用最简单的Factory方式管理插件,
最终决定采用的是JDK标准的SPI扩展机制,参见:java.util.ServiceLoader
也就是扩展者在jar包的META-INF/services/目录下放置与接口同名的文本文件,
内容为接口实现类名,多个实现类名用换行符分隔,
比如,需要扩展Dubbo的协议,只需在xxx.jar中放置:
文件:META-INF/services/com.alibaba.dubbo.rpc.Protocol
内容为:com.alibaba.xxx.XxxProtocol
Dubbo通过ServiceLoader扫描到所有Protocol实现。
并约定所有插件,都必须标注:@Extension(“name”),
作为加载后的标识性名称,用于配置选择。
第二步,每个扩展点只封装一个变化因子,最大化复用。
每个扩展点的实现者,往往都只是关心一件事,
现在的扩展点,并没有完全分离,
比如:Failover, Route, LoadBalance, Directory没有完全分开,全由RoutingInvokerGroup写死了。
再比如,协议扩展,扩展者可能只是想替换序列化方式,或者只替换传输方式,
并且Remoting和Http也能复用序列化等实现,
这样,需为传输方式,客户端实现,服务器端实现,协议头解析,数据序列化,都留出不同扩展点。
拆分后,设计如下:
第三步,全管道式设计,框架自身逻辑,均使用截面拦截实现。
现在很多的逻辑,都是放在基类中实现,然后通过模板方法回调子类的实现,
包括:local, mock, generic, echo, token, accesslog, monitor, count, limit等等,
可以全部拆分使用Filter实现,每个功能都是调用链上的一环。
比如:(基类模板方法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public abstract AbstractInvoker implements Invoker {
public Result invoke(Invocation inv) throws RpcException {
// 伪代码
active ++;
if (active > max)
wait();
doInvoke(inv);
active --;
notify();
}
protected abstract Result doInvoke(Invocation inv) throws RpcException
}
改成:(链式过滤器)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract LimitFilter implements Filter {
public Result invoke(Invoker chain, Invocation inv) throws RpcException {
// 伪代码
active ++;
if (active > max)
wait();
chain.invoke(inv);
active --;
notify();
}
}
第四步,最少概念,一致性概念模型。
保持尽可能少的概念,有助于理解,对于开放的系统尤其重要,
另外,各接口都使用一致的概念模型,能相互指引,并减少模型转换,
比如,Invoker的方法签名为:
1
Result invoke(Invocation invocation) throws RpcException;
而Exporter的方法签名为:
1
Object invoke(Method method, Object[] args) throws Throwable;
但它们的作用是一样的,只是一个在客户端,一个在服务器端,却采用了不一样的模型类。
再比如,URL以字符串传递,不停的解析和拼装,没有一个URL模型类, 而URL的参数,却时而Map, 时而Parameters类包装,
1
2
export(String url)
createExporter(String host, int port, Parameters params);
使用一致模型:
1
2
export(URL url)
createExporter(URL url);
再比如,现有的:Invoker, Exporter, InvocationHandler, FilterChain
其实都是invoke行为的不同阶段,完全可以抽象掉,统一为Invoker,减少概念。
第五步,分层,组合式扩展,而不是泛化式扩展。
原因参见:http://javatar.iteye.com/blog/690845
泛化式扩展指:将扩展点逐渐抽象,取所有功能并集,新加功能总是套入并扩充旧功能的概念。
组合式扩展指:将扩展点正交分解,取所有功能交集,新加功能总是基于旧功能之上实现。
上面的设计,不自觉的就将Dubbo现有功能都当成了核心功能,
上面的概念包含了Dubbo现有RPC的所有功能,包括:Proxy, Router, Failover, LoadBalance, Subscriber, Publisher, Invoker, Exporter, Filter等,
但这些都是核心吗?踢掉哪些,RPC一样可以Run?而哪些又是不能踢掉的?
基于这样考虑,可以将RPC分解成两个层次,只是Protocol和Invoker才是RPC的核心,
其它,包括Router, Failover, Loadbalance, Subscriber, Publisher都不核心,而是Routing,
所以,将Routing作为Rpc核心的一个扩展,设计如下:
第六步,整理,梳理关系。
整理后,设计如下:
RPC框架几行代码就够了
2011-07-14 https://www.iteye.com/blog/javatar-1123915
因为要给百技上实训课,让新同学们自行实现一个简易RPC框架,在准备PPT时,就想写个示例,发现原来一个RPC框架只要一个类,10来分钟就可以写完了,虽然简陋,也晒晒:
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/*
* Copyright 2011 Alibaba.com All right reserved. This software is the
* confidential and proprietary information of Alibaba.com ("Confidential
* Information"). You shall not disclose such Confidential Information and shall
* use it only in accordance with the terms of the license agreement you entered
* into with Alibaba.com.
*/
package com.alibaba.study.rpc.framework;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.ServerSocket;
import java.net.Socket;
/**
* RpcFramework
*
* @author william.liangf
*/
public class RpcFramework {
/**
* 暴露服务
*
* @param service 服务实现
* @param port 服务端口
* @throws Exception
*/
public static void export(final Object service, int port) throws Exception {
if (service == null)
throw new IllegalArgumentException("service instance == null");
if (port <= 0 || port > 65535)
throw new IllegalArgumentException("Invalid port " + port);
System.out.println("Export service " + service.getClass().getName() + " on port " + port);
ServerSocket server = new ServerSocket(port);
for(;;) {
try {
final Socket socket = server.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
try {
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
try {
String methodName = input.readUTF();
Class<?>[] parameterTypes = (Class<?>[])input.readObject();
Object[] arguments = (Object[])input.readObject();
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
try {
Method method = service.getClass().getMethod(methodName, parameterTypes);
Object result = method.invoke(service, arguments);
output.writeObject(result);
} catch (Throwable t) {
output.writeObject(t);
} finally {
output.close();
}
} finally {
input.close();
}
} finally {
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 引用服务
*
* @param <T> 接口泛型
* @param interfaceClass 接口类型
* @param host 服务器主机名
* @param port 服务器端口
* @return 远程服务
* @throws Exception
*/
@SuppressWarnings("unchecked")
public static <T> T refer(final Class<T> interfaceClass, final String host, final int port) throws Exception {
if (interfaceClass == null)
throw new IllegalArgumentException("Interface class == null");
if (! interfaceClass.isInterface())
throw new IllegalArgumentException("The " + interfaceClass.getName() + " must be interface class!");
if (host == null || host.length() == 0)
throw new IllegalArgumentException("Host == null!");
if (port <= 0 || port > 65535)
throw new IllegalArgumentException("Invalid port " + port);
System.out.println("Get remote service " + interfaceClass.getName() + " from server " + host + ":" + port);
return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class<?>[] {interfaceClass}, new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable {
Socket socket = new Socket(host, port);
try {
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
try {
output.writeUTF(method.getName());
output.writeObject(method.getParameterTypes());
output.writeObject(arguments);
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
try {
Object result = input.readObject();
if (result instanceof Throwable) {
throw (Throwable) result;
}
return result;
} finally {
input.close();
}
} finally {
output.close();
}
} finally {
socket.close();
}
}
});
}
}
用起来也像模像样: (1) 定义服务接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* Copyright 2011 Alibaba.com All right reserved. This software is the
* confidential and proprietary information of Alibaba.com ("Confidential
* Information"). You shall not disclose such Confidential Information and shall
* use it only in accordance with the terms of the license agreement you entered
* into with Alibaba.com.
*/
package com.alibaba.study.rpc.test;
/**
* HelloService
*
* @author william.liangf
*/
public interface HelloService {
String hello(String name);
}
(2) 实现服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* Copyright 2011 Alibaba.com All right reserved. This software is the
* confidential and proprietary information of Alibaba.com ("Confidential
* Information"). You shall not disclose such Confidential Information and shall
* use it only in accordance with the terms of the license agreement you entered
* into with Alibaba.com.
*/
package com.alibaba.study.rpc.test;
/**
* HelloServiceImpl
*
* @author william.liangf
*/
public class HelloServiceImpl implements HelloService {
public String hello(String name) {
return "Hello " + name;
}
}
(3) 暴露服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* Copyright 2011 Alibaba.com All right reserved. This software is the
* confidential and proprietary information of Alibaba.com ("Confidential
* Information"). You shall not disclose such Confidential Information and shall
* use it only in accordance with the terms of the license agreement you entered
* into with Alibaba.com.
*/
package com.alibaba.study.rpc.test;
import com.alibaba.study.rpc.framework.RpcFramework;
/**
* RpcProvider
*
* @author william.liangf
*/
public class RpcProvider {
public static void main(String[] args) throws Exception {
HelloService service = new HelloServiceImpl();
RpcFramework.export(service, 1234);
}
}
(4) 引用服务
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
/*
* Copyright 2011 Alibaba.com All right reserved. This software is the
* confidential and proprietary information of Alibaba.com ("Confidential
* Information"). You shall not disclose such Confidential Information and shall
* use it only in accordance with the terms of the license agreement you entered
* into with Alibaba.com.
*/
package com.alibaba.study.rpc.test;
import com.alibaba.study.rpc.framework.RpcFramework;
/**
* RpcConsumer
*
* @author william.liangf
*/
public class RpcConsumer {
public static void main(String[] args) throws Exception {
HelloService service = RpcFramework.refer(HelloService.class, "127.0.0.1", 1234);
for (int i = 0; i < Integer.MAX_VALUE; i ++) {
String hello = service.hello("World" + i);
System.out.println(hello);
Thread.sleep(1000);
}
}
}
以 HTTL 为例讲讲模块分包 & 领域模型 & 扩展框架
2011-10-09 https://www.iteye.com/blog/javatar-1188028
类结构设计
模型划分原则:按实体域、服务域、会话域划分
不管你做一个什么产品,都一定有以下三个维度的划分:
- 实体域:被操作的主体。如:服务框架管理的 Service,Spring 管理的 Bean。
- 服务域:操作者。它管理实体的生命周期,发起动作。如:Spring 的 BeanFactory。
- 会话域:执行过程中的临时状态存储交换。如:Invocation、Request。
HTTL 中的核心模型
- Engine (服务域)
- API 入口,负责
Template的生命周期管理。 - 特性:Singleton(单例),加载后不可变,线程安全。初始化过程重,建议复用。
- API 入口,负责
- Template (实体域)
- 代表被操作者。
- 特性:Prototype(原型实例),即每个模板对应一个实例。加载后不可变,线程安全。
- Context (会话域)
- 持有操作过程中的所有可变状态。
- 特性:ThreadLocal 实例,线程内安全。初始化轻量,随渲染过程创建与销毁。
扩展点组装原则:微核 + 插件
HTTL 的扩展性基于一个核心理念:平等对待第三方。原作者实现功能的方式应与第三方完全一致,不拥有特权。
微核机制
内核只负责插件组装,不带功能逻辑。HTTL 使用内置的 httl.util.BeanFactory 作为组装微核。
- 组装规则:基于 Setter 注入。
- 示例:在
httl.properties中配置parser=httl.spi.parsers.CommentParser,微核会自动调用DefaultEngine的setParser方法进行注入。
功能组装插件化
大插件组装小插件,形成级联。例如,入口类 Engine 本身也是一个插件,它负责调度缓存、加载和解析。
- SPI (Service Provider Interface):这是暴露给扩展者的最小粒度替换单元。
分包原则:复用度、抽象度、稳定度
核心指标公式
- 不稳定度 ($I$):$I = Ce / (Ca + Ce)$
- $Ca$ (Afferent): 传入依赖;$Ce$ (Efferent): 输出依赖。
- 抽象度 ($A$):$A = Na / Nc$
- $Na$: 抽象类/接口数;$Nc$: 类总数。
偏差 ($D$):$D = 1 - I - A \cdot \sin(45^\circ)$ - 偏差越小,包的设计越合理。
HTTL 的三层包结构
| 层级 | 包路径 | 职责 |
|---|---|---|
| API | httl.* | 核心领域模型,使用者依赖。隐藏实现细节,保持概念最少。 |
| SPI | httl.spi.* | 功能正交分解的抽象层,扩展者依赖。 |
| BUILT-IN | httl.spi.xxx | SPI 的标准实现,也是可替换的。确保没有功能“换不掉”。 |
扩展点执行顺序
获取模板流程 (Get Template)
- 查找缓存:命中则直接返回。
- 加载模板:由
Loader加载资源。 - 语法解析:
- 将表达式转译为 Java 代码。
- 过滤静态文本(如删空白)。
- 编译与实例化:编译 Java 代码并实例化模板对象。
写入缓存。
渲染模板流程 (Render Template)
- 类型转换:将变量转为 Map,输出对象转为 Writer/Stream。
- Context 入栈:压入 ThreadLocal 栈。
- 拦截器执行:
- 封装渲染过程为
Listener传给Interceptor。 - 拦截逻辑执行后,回调
doRender。
- 封装渲染过程为
- 变量查找:
- 逐级从
Context向上查找,最终追溯至Resolver。
- 逐级从
- 输出处理:
Formatter:值对象 toString。Filter:过滤 XML 特殊符号。
- Context 出栈。
/20260119104017709.png)
/20260119104017719-8790417.png)
/20260119104017711.png)
/20260119104017727-8790417.png)
/20260119104017719.png)
/20260119104017727.png)
/20260119104017860.png)
/20260119104017878-8790417.png)
/20260119104017866.png)
/20260119104017878.png)
/20260119104017872.png)
/20260119104018050.png)
/20260119104017849.png)
/20260119104018048.png)
/20260119104018036.png)
/20260119104018071.png)
/20260119104018055.png)
/20260119104018074.png)
/20260119104018267.png)
/20260119104018223.png)
/20260119104018227.png)
/20260119104018228.png)
/20260119104018244.png)
/20260119104018246.png)
/20260119104018371.png)
/20260119104018388.png)