技术积累

技术积累

站在岸上看DEVOPS

默认分类admin 发表了文章 • 0 个评论 • 129 次浏览 • 2017-01-17 17:41 • 来自相关话题

今年项目要试水(X)AAS,尝试运维自己的产品,便离不开这几年最热的单词—DEVOPS。趁还在岸上,理解下DEVOPS对开发人员来说意味着什么。

因为没有实战,也就纸上谈兵一下,真正有经验的人, @连城404:为了分摊黑锅,运维发明了devops和full stack。

What is DEVOPS ?

十年前,敏捷出现,以反瀑布流程的方式模糊了需求、架构、开发、测试之间的界限,但对整个软件价值链的最后一环,交付与运维并没有太多的强调,造成一个有趣的现象是,产品以三周为迭代进行开发并交付给客户,但只充当与客户演示并收集反馈的媒介,而客户仍然会有自己独立的上线周期。

十年后,越来越多的互联网站与手机应用,需要频繁而持续的发布,每次发布需要部署到大量的机器集群上,需要不中断和破坏当前服务,在发布前能提供本次发布质量的证明,在发布后对系统有实时的监控,支持错误转移、服务降级,支持快速定位和修复故障。

显然,只靠传统运维人员的知识技能集(操作系统、网络、Shell脚本编写) 以及开发人员编写的的产品管理手册已不足以满足上述的需求了。好在,有敏捷的先例,解决方法是现成的——把开发拖下水吧:

1. 人员上,运维人员要更懂产品的架构与内在,而不只是那份管理员手册。开发人员要懂实际的运维,从实际部署、上线流程,到故障的定位与解决。当然,最重要是打通两个部门间的隔阂。

2. 产品特性上,运维所需的功能甚至运维的基础设施,成为了产品非功能性需求的重要一部分。

3. 流程上,产品交付与运维,接入整个软件生产周期构成一个完整的环。
 

What should developer do?

现在大家最关心的问题来了,DEVOPS的大旗下,开发人员到底要参与多少运维?

这幅图,又是零到一百之间的选择,各个组织根据自己的人员、技术,文化,找到自己最合适的那个平衡点。

理论上,开发人员在DEVOPS中可以有两种的参与:

一,为运维而开发。

比如,在代码中加入Metrics收集的代码,向Graphite吐出服务的调用次数与延时,通常是与运维协作的第一步——运维可以在CPU,内存占用与应用日志之外,不那么黑盒的看待应用了。

又比如,要和运维商量着,在代码端需要做些什么,才能实现监控,无中断升级,动态节点扩展,故障转移,服务降级,跨机房容灾。这些功能以前可能也是要的,但现在需求的采集更注重实效。甚至,运维的一些基础设施,也可以由开发代劳了。

又比如,环境与流程的统一,原来只在简版的集成测试环境上跑完功能测试就完事的开发闭环,现在需要考虑至少延伸到一台从服务器配置到数据库部署模型等都贴近生产环境的Staging服务器,又比如测试环境的安装部署,也会用和运维一样的基于Ansible 或 Saltstack。

二,开发直接参与运维。

也是最存在争议的地方,比如系统的上线升级,日常的运维操作,故障的即时修复。

开发人员参与运维,可以吃自己的狗粮,可以更深切的了解运维功能的需求,开发人员的故障修复更快,但是——

程序员是不是应该做更有创造性的事情?程序员做运维会不会感到厌烦?社会化分工还是不是一个无需争辩的现代社会真理?

其实类似的争论已发生过一次,在开发与测试的分工上。起码在我司,专门的测试人员仍然有不可动摇的作用,而开发人员,则承担了从持续的单元测试和功能测试,最后到手工的性能测试与稳定性测试,总体80%以上的测试工作量,很自然的合作。

所以,还是没有绝对,还是各个组织自己去通过不断的尝试,找到自己最合适的那个平衡点。 查看全部
今年项目要试水(X)AAS,尝试运维自己的产品,便离不开这几年最热的单词—DEVOPS。趁还在岸上,理解下DEVOPS对开发人员来说意味着什么。

因为没有实战,也就纸上谈兵一下,真正有经验的人, @连城404:为了分摊黑锅,运维发明了devops和full stack。

What is DEVOPS ?

十年前,敏捷出现,以反瀑布流程的方式模糊了需求、架构、开发、测试之间的界限,但对整个软件价值链的最后一环,交付与运维并没有太多的强调,造成一个有趣的现象是,产品以三周为迭代进行开发并交付给客户,但只充当与客户演示并收集反馈的媒介,而客户仍然会有自己独立的上线周期。

十年后,越来越多的互联网站与手机应用,需要频繁而持续的发布,每次发布需要部署到大量的机器集群上,需要不中断和破坏当前服务,在发布前能提供本次发布质量的证明,在发布后对系统有实时的监控,支持错误转移、服务降级,支持快速定位和修复故障。

显然,只靠传统运维人员的知识技能集(操作系统、网络、Shell脚本编写) 以及开发人员编写的的产品管理手册已不足以满足上述的需求了。好在,有敏捷的先例,解决方法是现成的——把开发拖下水吧:

1. 人员上,运维人员要更懂产品的架构与内在,而不只是那份管理员手册。开发人员要懂实际的运维,从实际部署、上线流程,到故障的定位与解决。当然,最重要是打通两个部门间的隔阂。

2. 产品特性上,运维所需的功能甚至运维的基础设施,成为了产品非功能性需求的重要一部分。

3. 流程上,产品交付与运维,接入整个软件生产周期构成一个完整的环。
 

What should developer do?

现在大家最关心的问题来了,DEVOPS的大旗下,开发人员到底要参与多少运维?

这幅图,又是零到一百之间的选择,各个组织根据自己的人员、技术,文化,找到自己最合适的那个平衡点。

理论上,开发人员在DEVOPS中可以有两种的参与:

一,为运维而开发。

比如,在代码中加入Metrics收集的代码,向Graphite吐出服务的调用次数与延时,通常是与运维协作的第一步——运维可以在CPU,内存占用与应用日志之外,不那么黑盒的看待应用了。

又比如,要和运维商量着,在代码端需要做些什么,才能实现监控,无中断升级,动态节点扩展,故障转移,服务降级,跨机房容灾。这些功能以前可能也是要的,但现在需求的采集更注重实效。甚至,运维的一些基础设施,也可以由开发代劳了。

又比如,环境与流程的统一,原来只在简版的集成测试环境上跑完功能测试就完事的开发闭环,现在需要考虑至少延伸到一台从服务器配置到数据库部署模型等都贴近生产环境的Staging服务器,又比如测试环境的安装部署,也会用和运维一样的基于Ansible 或 Saltstack。

二,开发直接参与运维。

也是最存在争议的地方,比如系统的上线升级,日常的运维操作,故障的即时修复。

开发人员参与运维,可以吃自己的狗粮,可以更深切的了解运维功能的需求,开发人员的故障修复更快,但是——

程序员是不是应该做更有创造性的事情?程序员做运维会不会感到厌烦?社会化分工还是不是一个无需争辩的现代社会真理?

其实类似的争论已发生过一次,在开发与测试的分工上。起码在我司,专门的测试人员仍然有不可动摇的作用,而开发人员,则承担了从持续的单元测试和功能测试,最后到手工的性能测试与稳定性测试,总体80%以上的测试工作量,很自然的合作。

所以,还是没有绝对,还是各个组织自己去通过不断的尝试,找到自己最合适的那个平衡点。

String.intern() 祛魅

默认分类admin 发表了文章 • 0 个评论 • 153 次浏览 • 2017-01-17 17:31 • 来自相关话题

String.intern(),写应用的同学不知道也没什么损失。但知道的同学,如果只觉得它很省内存,用==比较字符串很酷,或者期待JDK会做什么神奇的事情,然后恨不得所有地方都用上String.intern(),那很可能会误用,所以把上周在群里的讨论整理成此文。

在社会科学中,祛魅(Disenchantment)是指在现代社会中消去神秘主义(魅惑力)的表面并把文化合理化。这里乱用一下。

 

1. String.itern()的基本原理

详细可看占小狼同学的《浅谈Java String内幕(2)》

String.intern()是一个Native方法,底层调用C++的 StringTable::intern 方法,源码注释:当调用 intern 方法时,如果常量池中已经该字符串,则返回池中的字符串;否则将此字符串添加到常量池中,并返回字符串的引用。

所以明面上,它有两大好处,一是重复的字符串,会用同一个引用代替;二是字符串比较,不再需要逐个字符的equals()比较,而用==对比引用是否相同即可。

 

2. 省内存效果只对长期存在的字符串有效

String.intern()没有神奇的地方,只在字符串生成后,再去常量池里查找引用。所以字符串最初生成时所花的内存,是省不掉的。

String s = new String(bytes, “UTF-8”).intern();
String s = String.valueOf(i).intern();

只有大量对象放在长期存在的集合里,里面是大量重复的字符串,或者对象的属性是重复的字符串时,省内存的效果才显现出来。短生命周期的字符串,GC要干的活是一样的。

 

3. 执行路径上多次的==,才能抵消常量池HasHMap查找的代价

==当然比equals()快得多,但常量池其实是个HashMap,依然没有神奇的地方,依然要执行HashMap的get操作,所以,一次hashCode() 和至少一次的equals()已经预付了,如果hash冲突,那equals()次数更多。

 

4. 真的对性能影响甚微吗?

在我的服务化框架测试里,把几个Header字段intern了,性能立马从七万五调到七万一 QPS,原来从七万一升到七万五 ,曾做过多少效果甚微的优化加上一次Netty使用的优化而成,现在它掉下来倒是飞快。

PS. 七万五 20%CPU这个数字,这两周的博客里都没升过了: (

 

5. 小陷阱

来自R大的提醒, s.intern()是无效的,因为String是不变对象, String s1 = s.intern()后,这个s1才是个引用。

Java的常量池也是不省心的,要注意JDK版本,占小狼同学的《浅谈Java String内幕(2)》 查看全部
String.intern(),写应用的同学不知道也没什么损失。但知道的同学,如果只觉得它很省内存,用==比较字符串很酷,或者期待JDK会做什么神奇的事情,然后恨不得所有地方都用上String.intern(),那很可能会误用,所以把上周在群里的讨论整理成此文。

在社会科学中,祛魅(Disenchantment)是指在现代社会中消去神秘主义(魅惑力)的表面并把文化合理化。这里乱用一下。

 

1. String.itern()的基本原理

详细可看占小狼同学的《浅谈Java String内幕(2)》

String.intern()是一个Native方法,底层调用C++的 StringTable::intern 方法,源码注释:当调用 intern 方法时,如果常量池中已经该字符串,则返回池中的字符串;否则将此字符串添加到常量池中,并返回字符串的引用。

所以明面上,它有两大好处,一是重复的字符串,会用同一个引用代替;二是字符串比较,不再需要逐个字符的equals()比较,而用==对比引用是否相同即可。

 

2. 省内存效果只对长期存在的字符串有效

String.intern()没有神奇的地方,只在字符串生成后,再去常量池里查找引用。所以字符串最初生成时所花的内存,是省不掉的。

String s = new String(bytes, “UTF-8”).intern();
String s = String.valueOf(i).intern();

只有大量对象放在长期存在的集合里,里面是大量重复的字符串,或者对象的属性是重复的字符串时,省内存的效果才显现出来。短生命周期的字符串,GC要干的活是一样的。

 

3. 执行路径上多次的==,才能抵消常量池HasHMap查找的代价

==当然比equals()快得多,但常量池其实是个HashMap,依然没有神奇的地方,依然要执行HashMap的get操作,所以,一次hashCode() 和至少一次的equals()已经预付了,如果hash冲突,那equals()次数更多。

 

4. 真的对性能影响甚微吗?

在我的服务化框架测试里,把几个Header字段intern了,性能立马从七万五调到七万一 QPS,原来从七万一升到七万五 ,曾做过多少效果甚微的优化加上一次Netty使用的优化而成,现在它掉下来倒是飞快。

PS. 七万五 20%CPU这个数字,这两周的博客里都没升过了: (

 

5. 小陷阱

来自R大的提醒, s.intern()是无效的,因为String是不变对象, String s1 = s.intern()后,这个s1才是个引用。

Java的常量池也是不省心的,要注意JDK版本,占小狼同学的《浅谈Java String内幕(2)》

另一份Java应用调优指南之-前菜

默认分类admin 发表了文章 • 0 个评论 • 183 次浏览 • 2017-01-17 17:25 • 来自相关话题

每一次成功的调优,都会诞生又一份的调优指南。

一些必须写在前面的军规,虽然与Java应用的调优没直接关联,但是测试同学经常不留神的地方,导致应用的优化变成一场测试环境调优的戏码。
 

1 独占你的测试机器

包括跑JMeter的那些机器。

"top"或者"pidstat 1" 看一下,其他的路人甲乙丙丁的应用都关干净了没。

如果是云主机,确保更多的占有宿主机的资源,比如深夜大家下班了你在家连VPN回来跑。

 

2 了解你的测试机器

必须完完全全的了解你的机器,才知道有没卡在某个瓶颈,或者与线上环境、其他测试结果的比较。

还是那句, 包括跑JMeter的那些机器。

 

2.1 CPU

"cat /proc/cpuinfo", 看最后一条就好,比如

 

processor : 23
model name : Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz
physical id : 1
cpu cores : 6

所有数字都从零开始,physical id:1即两颗cpu, cpu core: 6即6核,processor : 23即24个处理器。

 

2 CPU * 6 Core * 2HT(Intel超线程技术) = 24 Processor

不过也有很多同事喜欢说24核,也懒得纠正了。

 

2.2 内存

"free -g" 没什么好说的。

 

2.3 硬盘

查看大小、分区、文件系统类型: "df -hT"
硬盘是否SCSI:/dev/sdX就是scsi的,hdX就是普通的。
硬盘是否SSD : "cat /sys/block/sda/queue/rotational", 0是SSD,1是传统硬盘,但也不一定灵

普通硬盘的写速度大概100M/s,RAID级别的查看不方便,SSD的速度也不定,所以用dd测一下最靠谱:

 

dd if=/dev/zero of=dd.file bs=8k count=128k conv=fdatasync
dd if=/dev/zero of=./dd.file bs=1G count=1 conv=fdatasync

上面命令测试了分别以每次8k和1g的大小,写入1g文件的速度。

if:输入文件名, /dev/zero 设备无穷尽地提供0
of:输出文件名
bs:块大小
count:次数
conv=fdatasync :实际写盘,而不是写入Page Cache

硬盘读速度的测试同理,不过要先清理缓存,否则直接从Page Cache读了。

 

sh -c "sync && echo 3 > /proc/sys/vm/drop_caches”
dd if=./dd.file of=/dev/null bs=8k

 

2.4 网卡

先用ifconfig看看有多少块网卡和bonding。bonding是个很棒的东西,可以把多块网卡绑起来,突破单块网卡的带宽限制。

然后检查每块网卡的速度,比如"ethtool eth0"。

再检查bonding,比如"cat /proc/net/bonding/bond0", 留意其Bonding Mode是负载均衡的,再留意其捆绑的网卡的速度。

最后检查测试客户机与服务机之间的带宽,先简单ping或traceroute 一下得到RTT时间,iperf之类的可稍后。

 

2.5 操作系统

Linux的内核版本,是否64位: "uname -a"
Redhat/CentOS版本 : "cat /etc/redhat-release"

 

3. 布置好你的机器状态采集工具

实时观察的,我喜欢dstat,详见《从dstat理解Linux性能监控体系》比vmstat,iostat, sar们都好用,起码对得够齐,单位能自动转换。不过dstat需要安装(yum install dstat,或者去它的网站下载解压即用版)

dstat -tamp:推荐,打印时间戳,比默认打印多一个memory信息,一个有否进程在block状态的信息
dstat -tamN bond0,lo: 如果有bonding,dstat会把bond0和eth0 有时会算双份,还有lo的也算到总量里,所以先用-N指定网卡检查下。

参考资料:后台性能测试不可不知的二三事

 

4. JMeter的调优顶一半的事

JMeter的版本越新越好。

 

4.1 JMeter的JVM参数

它默认连个垃圾收集算法都没有配,对延时要求高的,必须配上CMS或G1,内存也整大点降低GC的频率。其他的,给Server配的啥参数,给JMeter也来上一份。如果想动态改的,不写死在脚本里,可以配置环境变量$JVM_ARGS

 

4.2 测试计划的编写

什么if 语句,以及所有其实用动态语言来实现的都挺慢的。
xPath抽取结果集里的字段之类看着就慢的也别写了。
别加任何监听器和图形。
再配置输出日志的格式,能不要的列都别要了,最极端的其实就延时这列有用。

 

4.3 JMeter的运行

在Linux上用命令行跑,别偷懒用Window开着界面跑。
别开超过200条线程(虚拟机上更少)。
可以在不同机器上起多个JMeter,用集群汇总的模式。

 

4.4 结果的统计

初始连接,Server端热身,JVM编译热点方法等都需要时间,所以建议统计前删掉前面的一些日志。

要配置一下才能看到99.9%, 99.99% 分位数的延时,另外因为之前输出日志时省略了很多列,导入日志的时候配置也要如此。

但如果不能XWindows Forward,还要把日志下载回来再导入本地的JMeter,那还不如自己动动手,用sed, awk, sort配合一下自己写个分析延时的脚本直接在服务器上运行。

 

5. 其他被依赖的节点

比如所依赖的数据库够不够快,Restful API的模拟器够不够快,负载均衡器如HAProxy优化配置了没有。

 
唠叨完这些写给测试同学的话,下篇,就可以正式开始调优了。 查看全部
每一次成功的调优,都会诞生又一份的调优指南。

一些必须写在前面的军规,虽然与Java应用的调优没直接关联,但是测试同学经常不留神的地方,导致应用的优化变成一场测试环境调优的戏码。
 

1 独占你的测试机器

包括跑JMeter的那些机器。

"top"或者"pidstat 1" 看一下,其他的路人甲乙丙丁的应用都关干净了没。

如果是云主机,确保更多的占有宿主机的资源,比如深夜大家下班了你在家连VPN回来跑。

 

2 了解你的测试机器

必须完完全全的了解你的机器,才知道有没卡在某个瓶颈,或者与线上环境、其他测试结果的比较。

还是那句, 包括跑JMeter的那些机器。

 

2.1 CPU

"cat /proc/cpuinfo", 看最后一条就好,比如

 

processor : 23
model name : Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz
physical id : 1
cpu cores : 6

所有数字都从零开始,physical id:1即两颗cpu, cpu core: 6即6核,processor : 23即24个处理器。

 

2 CPU * 6 Core * 2HT(Intel超线程技术) = 24 Processor

不过也有很多同事喜欢说24核,也懒得纠正了。

 

2.2 内存

"free -g" 没什么好说的。

 

2.3 硬盘

查看大小、分区、文件系统类型: "df -hT"
硬盘是否SCSI:/dev/sdX就是scsi的,hdX就是普通的。
硬盘是否SSD : "cat /sys/block/sda/queue/rotational", 0是SSD,1是传统硬盘,但也不一定灵

普通硬盘的写速度大概100M/s,RAID级别的查看不方便,SSD的速度也不定,所以用dd测一下最靠谱:

 

dd if=/dev/zero of=dd.file bs=8k count=128k conv=fdatasync
dd if=/dev/zero of=./dd.file bs=1G count=1 conv=fdatasync

上面命令测试了分别以每次8k和1g的大小,写入1g文件的速度。

if:输入文件名, /dev/zero 设备无穷尽地提供0
of:输出文件名
bs:块大小
count:次数
conv=fdatasync :实际写盘,而不是写入Page Cache

硬盘读速度的测试同理,不过要先清理缓存,否则直接从Page Cache读了。

 

sh -c "sync && echo 3 > /proc/sys/vm/drop_caches”
dd if=./dd.file of=/dev/null bs=8k

 

2.4 网卡

先用ifconfig看看有多少块网卡和bonding。bonding是个很棒的东西,可以把多块网卡绑起来,突破单块网卡的带宽限制。

然后检查每块网卡的速度,比如"ethtool eth0"。

再检查bonding,比如"cat /proc/net/bonding/bond0", 留意其Bonding Mode是负载均衡的,再留意其捆绑的网卡的速度。

最后检查测试客户机与服务机之间的带宽,先简单ping或traceroute 一下得到RTT时间,iperf之类的可稍后。

 

2.5 操作系统

Linux的内核版本,是否64位: "uname -a"
Redhat/CentOS版本 : "cat /etc/redhat-release"

 

3. 布置好你的机器状态采集工具

实时观察的,我喜欢dstat,详见《从dstat理解Linux性能监控体系》比vmstat,iostat, sar们都好用,起码对得够齐,单位能自动转换。不过dstat需要安装(yum install dstat,或者去它的网站下载解压即用版)

dstat -tamp:推荐,打印时间戳,比默认打印多一个memory信息,一个有否进程在block状态的信息
dstat -tamN bond0,lo: 如果有bonding,dstat会把bond0和eth0 有时会算双份,还有lo的也算到总量里,所以先用-N指定网卡检查下。

参考资料:后台性能测试不可不知的二三事

 

4. JMeter的调优顶一半的事

JMeter的版本越新越好。

 

4.1 JMeter的JVM参数

它默认连个垃圾收集算法都没有配,对延时要求高的,必须配上CMS或G1,内存也整大点降低GC的频率。其他的,给Server配的啥参数,给JMeter也来上一份。如果想动态改的,不写死在脚本里,可以配置环境变量$JVM_ARGS

 

4.2 测试计划的编写

什么if 语句,以及所有其实用动态语言来实现的都挺慢的。
xPath抽取结果集里的字段之类看着就慢的也别写了。
别加任何监听器和图形。
再配置输出日志的格式,能不要的列都别要了,最极端的其实就延时这列有用。

 

4.3 JMeter的运行

在Linux上用命令行跑,别偷懒用Window开着界面跑。
别开超过200条线程(虚拟机上更少)。
可以在不同机器上起多个JMeter,用集群汇总的模式。

 

4.4 结果的统计

初始连接,Server端热身,JVM编译热点方法等都需要时间,所以建议统计前删掉前面的一些日志。

要配置一下才能看到99.9%, 99.99% 分位数的延时,另外因为之前输出日志时省略了很多列,导入日志的时候配置也要如此。

但如果不能XWindows Forward,还要把日志下载回来再导入本地的JMeter,那还不如自己动动手,用sed, awk, sort配合一下自己写个分析延时的脚本直接在服务器上运行。

 

5. 其他被依赖的节点

比如所依赖的数据库够不够快,Restful API的模拟器够不够快,负载均衡器如HAProxy优化配置了没有。

 
唠叨完这些写给测试同学的话,下篇,就可以正式开始调优了。

软件设计杂谈

默认分类admin 发表了文章 • 0 个评论 • 158 次浏览 • 2017-01-17 17:06 • 来自相关话题

作者:陈天
链接:https://zhuanlan.zhihu.com/p/20005177
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

disclaimer: 本文所讲的设计,非UI/UE的设计,单单指软件代码/功能本身在技术上的设计。UI/UE的主题请出门右转找特赞(Tezign)。:)
在如今这个Lean/Agile横扫一切的年代,设计似乎有了被边缘化的倾向,做事的周期如此之快,似乎已容不下人们更多的思考。MVP(Minimal Viable Produce)在很多团队里演化成一个形而上的图腾,于是工程师们找到了一个完美的借口:我先做个MVP,设计的事,以后再说。
如果纯属个人玩票,有个点子,hack out还说得过去;但要严肃做一个项目,还是要下工夫设计一番,否则,没完没了的返工会让你无语泪千行。
设计首先得搞懂要解决的问题
工程师大多都是很聪明的人,聪明人有个最大的问题就是自负。很多人拿到一个需求,还没太搞明白其外延和内涵,代码就已经在脑袋里流转。这样做出来的系统,纵使再精妙,也免不了承受因需求理解不明确而导致的返工之苦。
搞懂需求这事,说起来简单,做起来难。需求有正确的但表达错误的需求,有正确的但没表达出来的需求,还有过度表达的需求。所以,拿到需求后,先不忙寻找解决方案,多问问自己,工作伙伴,客户follow up questions来澄清需求模糊不清之处。
搞懂需求,还需要了解需求对应的产品,公司,以及(潜在)竞争对手的现状,需求的上下文,以及需求的约束条件。人有二知二不知:
I know that I know

I know that I don’t know

I don’t know that I know

I don’t know that I don’t know

澄清需求的过程,就是不断驱逐无知,掌握现状,上下文和约束条件的过程。
这个主题讲起来很大,且非常重要,但毕竟不是本文的重点,所以就此带过。
寻找(多个)解决方案
如果对问题已经有不错的把握,接下来就是解决方案的发现之旅。这是个考察big picture的活计。同样是满足孩子想要个汽车的愿望,你可以:
去玩具店里买一个现成的

买乐高积木,然后组装

用纸糊一个,或者找块木头,刻一个

这对应软件工程问题的几种解决之道:
购买现成软件(acuquire or licensing),二次开发之(如果需要)

寻找building blocks,组装之(glue)

自己开发(build from scratch, or DIY)

大部分时候,如果a或b的TCO [1] 合理,那就不要选择c。做一个产品的目的是为客户提供某种服务,而不是证明自己能一行行码出出来这个产品。
a是个很重要的点,可惜大部分工程师脑袋里没有钱的概念,或者出于job security的私心,而忽略了。工程师现在越来越贵,能用合理的价格搞定的功能,就不该雇人去打理(自己打脸)。一个产品,最核心的部分不超过整个系统的20%,把人力资源铺在核心的部分,才是软件设计之道。
b我们稍后再讲。
对工程师而言,DIY出一个功能是个极大的诱惑。一种DIY是源自工程师的不满。任何开源软件,在处理某种特定业务逻辑的时候总会有一些不足,眼里如果把这些不足放在,却忽略了人家的好处,是大大的不妥。前两天我听到有人说 "consul sucks, …​, I’ll build our own service discovery framework…​",我就苦笑。我相信他能做出来一个简单的service discovery tool,这不是件特别困难的事情。问题是值不值得去做。如果连处于consul这个层次的基础组件都要自己去做,那要么是心太大,要么是没有定义好自己的软件系统的核心价值(除非系统的核心价值就在于此)。代码一旦写出来,无论是5000行还是50行,都是需要有人去维护的,在系统的生命周期里,每一行自己写的代码都是一笔债务,需要定期不定期地偿还利息。
另外一种DIY是出于工程师的无知。「无知者无畏」在某些场合的效果是正向的,有利于打破陈规。但在软件开发上,还是知识和眼界越丰富越开阔越好。一个无知的工程师在面对某个问题时(比如说service discovery),如果不知道这问题也许有现成的解决方案(consul),自己铆足了劲写一个,大半会有失偏颇(比如说没做上游服务的health check,或者自己本身的high availability),结果bug不断,辛辛苦苦一个个都啃下来,才发现,自己走了很多弯路,费了大半天劲,做了某个开源软件的功能的子集。当然,对工程师而言,这个练手的价值还是很大的,但对公司来说,这是一笔沉重的无意义的支出。
眼界定义了一个人的高度,如果你每天见同类的人,看同质的书籍/视频,(读)写隶属同一domain的代码,那多半眼界不够开阔。互联网的发展一日千里,变化太快,如果把自己禁锢在一方小天地里,很容易成为陶渊明笔下的桃花源中人:乃不知有汉,无论魏晋。
构建灵活且有韧性的系统
如果说之前说的都是废话,那么接下来的和真正的软件设计能扯上些关系。
分解和组合
软件设计是一个把大的问题不断分解,直至原子级的小问题,然后再不断组合的过程。这一点可以类比生物学:原子(keyword/macro)组合成分子(function),分子组合成细胞(module/class),细胞组合成组织(micro service),组织组合成器官(service),进而组合成生物(system)。
一个如此组合而成系统,是满足关注点分离(Separation of Concerns)的。大到一个器官,小到一个细胞,都各司其职,把自己要做的事情做到极致。心脏不必关心肾脏会干什么,它只需要做好自己的事情:把新鲜血液通过动脉排出,再把各个器官用过的血液从静脉回收。
分解和组合在软件设计中的作用如此重要,以至于一个系统如果合理分解,那么日后维护的代价就要小得多。同样讲关注点分离,不同的工程师,分离的方式可能完全不同。但究其根本,还有有一些规律可循。
总线(System Bus)
首先我们要把系统的总线定义出来。人体的总线,大的有几条:血管(动脉,静脉),神经网络,气管,输尿管。它们有的完全负责与外界的交互(气管,输尿管),有的完全是内部的信息中枢(血管),有的内外兼修(神经网络)。
总线把生产者和消费者分离,让彼此互不依赖。心脏往外供血时,把血压入动脉血管就是了。它并不需要知道谁是接收者。
同样的,回到我们熟悉的计算机系统,CPU访问内存也是如此:它发送一条消息给总线,总线通知RAM读取数据,然后RAM把数据返回给总线,CPU再获取之。整个过程中CPU只知道一个内存地址,毋须知道访问的具体是哪个内存槽的哪块内存 —— 总线将二者屏蔽开。
学过计算机系统的同学应该都知道,经典的PC结构有几种总线:数据总线,地址总线,控制总线,扩展总线等;做过网络设备的同学也都知道,一个经典的网络设备,其软件系统的总线分为:control plane和data plane。
路由(routing)
有了总线的概念,接下来必然要有路由。我们看人体的血管:
每一处分叉,就涉及到一次路由。
路由分为外部路由和内部路由。外部路由处理输入,把不同的输入dispatch到系统里不同的组件。做web app的,可能没有意识到,但其实每个web framework,最关键的组件之一就是url dispatch。HTTP的伟大之处就是每个request,都能通过url被dispatch到不同的handler处理。而url是目录式的,可以层层演进 —— 就像分形几何,一个大的系统,通过不断重复的模式,组合起来 —— 非常利于系统的扩展。遗憾的是,我们自己做系统,对于输入既没有总线的考量,又无路由的概念,if-else下去,久而久之,代码便绕成了意大利面条。
再举一例:DOM中的event bubble,在javascript处理起来已然隐含着路由的概念。你只需定义当某个事件(如onclick)发生时的callback函数就好,至于这事件怎么通过eventloop抵达回调函数,无需关心。好的路由系统剥茧抽丝,把繁杂的信息流正确送到处理者手中。
外部路由总还有「底层」为我们完成,内部路由则需工程师考虑。service级别的路由(数据流由哪个service处理)可以用consul等service discovery组件,service内部的路由(数据流到达后怎么处理)则需要自己完成。路由的具体方式有很多种,pattern matching最为常见。
无论用何种方式路由,数据抵达总线前为其定义Identity(ID)非常重要,你可以管这个过程叫data normalization,data encapsulation等,总之,一个消息能被路由,需要有个用于路由的ID。这ID可以是url,可以是一个message header,也可以是一个label(想象MPLS的情况)。当我们为数据赋予一个个合理的ID后,如何路由便清晰可见。
队列(Queue)
对于那些并非需要立即处理的数据,可以使用队列。队列也有把生产者和消费者分离的功效。队列有:
single producer single consumer(SPSC)

single producer multiple consumers(SPMC)

multiple producers single consumer(MPSC)

multiple producers multiple consumers(MPMC)

仔细想想,队列其实就是总线+路由(可选)+存储的一个特殊版本。一般而言,system bus之上是系统的各个service,每个service再用service bus(或者queue)把micro service chain起来,然后每个micro service内部的组件间,再用queue连接起来。
有了队列,有利于提高流水线的效率。一般而言,流水线的处理速度取决于最慢的组件。队列的存在,让慢速组件有机会运行多份,来弥补生产者和消费者速度上的差距。
Pub/Sub
存储在队列中的数据,除路由外,还有一种处理方式:pub/sub。和路由相似,pub/sub将生产者和消费者分离;但二者不同之处在于,路由的目的地由路由表中的表项控制,而pub/sub一般由publisher控制 [2]:任何subscribe某个数据的consumer,都会到publisher处注册,publisher由此可以定向发送消息。
协议(protocol)
一旦我们把系统分解成一个个service,service再分解成micro service,彼此之间互不依赖,仅仅通过总线或者队列来通讯,那么,我们就需要协议来定义彼此的行为。协议听起来很高大上,其实不然。我们写下的每个function(或者每个class),其实就是在定义一个不成文的协议:function的arity是什么,接受什么参数,返回什么结果。调用者需严格按照协议调用方能得到正确的结果。
service级别的协议是一份SLA:服务的endpoint是什么,版本是什么,接收什么格式的消息,返回什么格式的消息,消息在何种网络协议上承载,需要什么样的authorization,可以正常服务的最大吞吐量(throughput)是什么,在什么情况下会触发throttling等等。
头脑中有了总线,路由,队列,协议等这些在computer science 101中介绍的基础概念,系统的分解便有迹可寻:面对一个系统的设计,你要做的不再是一道作文题,而是一道填空题:在若干条system bus里填上其名称和流进流出的数据,在system bus之上的一个个方框里填上服务的名称和服务的功能。然后,每个服务再以此类推,直到感觉毋须再细化为止。
组成系统的必要服务
有些管理性质的服务,尽管和业务逻辑直接关系不大,但无论是任何系统,都需要考虑构建,这里罗列一二。
代谢(sweeping)
一个活着的生物时时刻刻都进行着新陈代谢:每时每刻新的细胞取代老的细胞,同时身体中的「垃圾」通过排泄系统排出体外。一个运转有序的城市也有新陈代谢:下水道,垃圾场,污水处理等维持城市的正常功能。没有了代谢功能,生物会凋零,城市会荒芜。
软件系统也是如此。日志会把硬盘写满,软件会失常,硬件会失效,网络会拥塞等等。一个好的软件系统需要一个好的代谢系统:出现异常的服务会被关闭,同样的服务会被重新启动,恢复运行。
代谢系统可以参考erlang的supervisor/child process结构,以及supervision tree。很多软件,都运行在简单的supervision tree模式下,如nginx。
高可用性(HA)
每个人都有两个肾。为了apple watch卖掉一个肾,另一个还能保证人体的正常工作。当然,人的两个肾是Active-Active工作模式,内部的肾元(micro service)是 N(active)+M(backup) clustering 工作的(看看人家这service的做的),少了一个,performance会一点点有折扣,但可以忽略不计。
大部分软件系统里的各种服务也需要高可用性:除非完全无状态的服务,且服务重启时间在ms级。服务的高可用性和路由是息息相关的:高可用性往往意味着同一服务的冗余,同时也意味着负载分担。好的路由系统(如consul)能够对路由至同一服务的数据在多个冗余服务间进行负载分担,同时在检测出某个失效服务后,将数据路只由至正常运作的服务。
高可用性还意味着非关键服务,即便不可恢复,也只会导致系统降级,而不会让整个系统无法访问。就像壁虎的尾巴断了不妨碍壁虎逃命,人摔伤了手臂还能吃饭一样,一个软件系统里统计模块的异常不该让用户无法访问他的个人页面。
安保(security)
安保服务分为主动安全和被动安全。authentication/authorization + TLS + 敏感信息加密 + 最小化输入输出接口可以算是主动安全,防火墙等安防系统则是被动安全。
继续拿你的肾来比拟 —— 肾脏起码有两大安全系统:
输入安全。肾器的厚厚的器官膜,保护器官的输入输出安全 —— 主要的输入输出只能是肾动脉,肾静脉和输尿管。

环境安全。肾器里有大量脂肪填充,避免在撞击时对核心功能的损伤。

除此之外,人体还提供了包括免疫系统,皮肤,骨骼,空腔等一系列安全系统,从各个维度最大程度保护一个器官的正常运作。如果我们仔细研究生物,就会发现,安保是个一揽子解决方案:小到细胞,大到整个人体,都有各自的安全措施。一个软件系统也需如此考虑系统中各个层次的安全。
透支保护(overdraft protection)
任何系统,任何服务都是有服务能力的 —— 当这能力被透支时,需要一定的应急计划。如果使用拥有auto scaling的云服务(如AWS),动态扩容是最好的解决之道,但受限于所用的解决方案,它并非万灵药,AWS的auto scaling依赖于load balancer,如Amazon自有的ELB,或者第三方的HAProxy,但ELB对某些业务,如websocket,支持不佳;而第三方的load balancer,则需要考虑部署,与Amazon的auto scaling结合(需要写点代码),避免单点故障,保证自身的capacity等一堆头疼事。
在无法auto scaling的场景最通用的做法是back pressure,把压力反馈到源头。就好像你不断熬夜,最后大脑受不了,逼着你睡觉一样。还有一种做法是服务降级,停掉非核心的service/micro-service,如analytical service,ad service,保证核心功能正常。
把设计的成果讲给别人听
完成了分解和组合,也严肃对待了诸多与业务没有直接关系,但又不得不做的必要功能后,接下来就是要把设计在白板上画下来,讲给任何一个利益相关者听。听他们的反馈。设计不是一个闭门造车的过程,全程都需要和各种利益相关者交流。然而,很多人都忽视了设计定型后,继续和外界交流的必要性。很多人会认为:我的软件架构,设计结果和工程有关,为何要讲给工程师以外的人听?他们懂么?
其实pitch本身就是自我学习和自我修正的一部分。当着一个人或者几个人的面,在白板上画下脑海中的设计的那一刻,你就会有直觉哪个地方似乎有问题,这是很奇特的一种体验:你自己画给自己看并不会产生这种直觉。这大概是面对公众的焦灼产生的肾上腺素的效果。:)
此外,从听者的表情,或者他们提的听起来很傻很天真的问题,你会进一步知道哪些地方你以为你搞通了,其实自己是一知半解。太简单,太基础的问题,我们take it for granted,不屑去问自己,非要有人点出,自己才发现:啊,原来这里我也不懂哈。这就是破解 "you don’t know what you don’t know" 之法。
记得看过一个video,主讲人大谈企业文化,有个哥们傻乎乎发问:so what it culture literally? 主讲人愣了一下,拖拖拉拉讲了一堆自己都不能让自己信服的废话。估计回头他就去查韦氏词典了。
最后,总有人在某些领域的知识更丰富一些,他们会告诉你你一些你知道自己不懂的事情。填补了 "you know that you don’t know" 的空缺。
设计时的tradeoff
Rich hickey(clojure作者)在某个演讲中说:
everyone says design is about tradeoffs, but you need to enumerate at least two or more possible solutions, and the attributes and deficits of each, in order to make tradeoff.

所以,下回再腆着脸说:偶做了些tradeoff,先确保自己做足了功课再说。
设计的改变不可避免
设计不是一锤子买卖,改变不可避免。我之前的一个老板,喜欢把:change is your friend 挂在口头。软件开发的整个生命周期,变更是家常便饭,以至于变更管理都生出一门学问。软件的设计期更是如此。人总会犯错,设计总有缺陷,需求总会变化,老板总会指手画脚,PM总有一天会亮出獠牙,不再是贴心大哥,或者美萌小妹。。。所以,据理力争,然后接受必要的改变即可。连凯恩斯他老人家都说:

What do you do, sir?
(文章很长,感谢阅读至此,这文章3/30号开始写,拖拖拉拉算是凑合完成了)
如果您觉得这篇文章不错,请点赞。多谢!
欢迎订阅公众号『程序人生』(搜索微信号 programmer_life)。每篇文章都力求原汁原味,北京时间中午12点左右,美西时间下午8点左右与您相会。
1. Total Cost of Ownership
2. 要看具体的pub/sub系统的实现方式
原文链接:https://zhuanlan.zhihu.com/p/2 ... attle 查看全部

作者:陈天
链接:https://zhuanlan.zhihu.com/p/20005177
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

disclaimer: 本文所讲的设计,非UI/UE的设计,单单指软件代码/功能本身在技术上的设计。UI/UE的主题请出门右转找特赞(Tezign)。:)
在如今这个Lean/Agile横扫一切的年代,设计似乎有了被边缘化的倾向,做事的周期如此之快,似乎已容不下人们更多的思考。MVP(Minimal Viable Produce)在很多团队里演化成一个形而上的图腾,于是工程师们找到了一个完美的借口:我先做个MVP,设计的事,以后再说。
如果纯属个人玩票,有个点子,hack out还说得过去;但要严肃做一个项目,还是要下工夫设计一番,否则,没完没了的返工会让你无语泪千行。
设计首先得搞懂要解决的问题
工程师大多都是很聪明的人,聪明人有个最大的问题就是自负。很多人拿到一个需求,还没太搞明白其外延和内涵,代码就已经在脑袋里流转。这样做出来的系统,纵使再精妙,也免不了承受因需求理解不明确而导致的返工之苦。
搞懂需求这事,说起来简单,做起来难。需求有正确的但表达错误的需求,有正确的但没表达出来的需求,还有过度表达的需求。所以,拿到需求后,先不忙寻找解决方案,多问问自己,工作伙伴,客户follow up questions来澄清需求模糊不清之处。
搞懂需求,还需要了解需求对应的产品,公司,以及(潜在)竞争对手的现状,需求的上下文,以及需求的约束条件。人有二知二不知:
I know that I know

I know that I don’t know

I don’t know that I know

I don’t know that I don’t know

澄清需求的过程,就是不断驱逐无知,掌握现状,上下文和约束条件的过程。
这个主题讲起来很大,且非常重要,但毕竟不是本文的重点,所以就此带过。
寻找(多个)解决方案
如果对问题已经有不错的把握,接下来就是解决方案的发现之旅。这是个考察big picture的活计。同样是满足孩子想要个汽车的愿望,你可以:
去玩具店里买一个现成的

买乐高积木,然后组装

用纸糊一个,或者找块木头,刻一个

这对应软件工程问题的几种解决之道:
购买现成软件(acuquire or licensing),二次开发之(如果需要)

寻找building blocks,组装之(glue)

自己开发(build from scratch, or DIY)

大部分时候,如果a或b的TCO [1] 合理,那就不要选择c。做一个产品的目的是为客户提供某种服务,而不是证明自己能一行行码出出来这个产品。
a是个很重要的点,可惜大部分工程师脑袋里没有钱的概念,或者出于job security的私心,而忽略了。工程师现在越来越贵,能用合理的价格搞定的功能,就不该雇人去打理(自己打脸)。一个产品,最核心的部分不超过整个系统的20%,把人力资源铺在核心的部分,才是软件设计之道。
b我们稍后再讲。
对工程师而言,DIY出一个功能是个极大的诱惑。一种DIY是源自工程师的不满。任何开源软件,在处理某种特定业务逻辑的时候总会有一些不足,眼里如果把这些不足放在,却忽略了人家的好处,是大大的不妥。前两天我听到有人说 "consul sucks, …​, I’ll build our own service discovery framework…​",我就苦笑。我相信他能做出来一个简单的service discovery tool,这不是件特别困难的事情。问题是值不值得去做。如果连处于consul这个层次的基础组件都要自己去做,那要么是心太大,要么是没有定义好自己的软件系统的核心价值(除非系统的核心价值就在于此)。代码一旦写出来,无论是5000行还是50行,都是需要有人去维护的,在系统的生命周期里,每一行自己写的代码都是一笔债务,需要定期不定期地偿还利息。
另外一种DIY是出于工程师的无知。「无知者无畏」在某些场合的效果是正向的,有利于打破陈规。但在软件开发上,还是知识和眼界越丰富越开阔越好。一个无知的工程师在面对某个问题时(比如说service discovery),如果不知道这问题也许有现成的解决方案(consul),自己铆足了劲写一个,大半会有失偏颇(比如说没做上游服务的health check,或者自己本身的high availability),结果bug不断,辛辛苦苦一个个都啃下来,才发现,自己走了很多弯路,费了大半天劲,做了某个开源软件的功能的子集。当然,对工程师而言,这个练手的价值还是很大的,但对公司来说,这是一笔沉重的无意义的支出。
眼界定义了一个人的高度,如果你每天见同类的人,看同质的书籍/视频,(读)写隶属同一domain的代码,那多半眼界不够开阔。互联网的发展一日千里,变化太快,如果把自己禁锢在一方小天地里,很容易成为陶渊明笔下的桃花源中人:乃不知有汉,无论魏晋。
构建灵活且有韧性的系统
如果说之前说的都是废话,那么接下来的和真正的软件设计能扯上些关系。
分解和组合
软件设计是一个把大的问题不断分解,直至原子级的小问题,然后再不断组合的过程。这一点可以类比生物学:原子(keyword/macro)组合成分子(function),分子组合成细胞(module/class),细胞组合成组织(micro service),组织组合成器官(service),进而组合成生物(system)。
一个如此组合而成系统,是满足关注点分离(Separation of Concerns)的。大到一个器官,小到一个细胞,都各司其职,把自己要做的事情做到极致。心脏不必关心肾脏会干什么,它只需要做好自己的事情:把新鲜血液通过动脉排出,再把各个器官用过的血液从静脉回收。
分解和组合在软件设计中的作用如此重要,以至于一个系统如果合理分解,那么日后维护的代价就要小得多。同样讲关注点分离,不同的工程师,分离的方式可能完全不同。但究其根本,还有有一些规律可循。
总线(System Bus)
首先我们要把系统的总线定义出来。人体的总线,大的有几条:血管(动脉,静脉),神经网络,气管,输尿管。它们有的完全负责与外界的交互(气管,输尿管),有的完全是内部的信息中枢(血管),有的内外兼修(神经网络)。
总线把生产者和消费者分离,让彼此互不依赖。心脏往外供血时,把血压入动脉血管就是了。它并不需要知道谁是接收者。
同样的,回到我们熟悉的计算机系统,CPU访问内存也是如此:它发送一条消息给总线,总线通知RAM读取数据,然后RAM把数据返回给总线,CPU再获取之。整个过程中CPU只知道一个内存地址,毋须知道访问的具体是哪个内存槽的哪块内存 —— 总线将二者屏蔽开。
学过计算机系统的同学应该都知道,经典的PC结构有几种总线:数据总线,地址总线,控制总线,扩展总线等;做过网络设备的同学也都知道,一个经典的网络设备,其软件系统的总线分为:control plane和data plane。
路由(routing)
有了总线的概念,接下来必然要有路由。我们看人体的血管:
每一处分叉,就涉及到一次路由。
路由分为外部路由和内部路由。外部路由处理输入,把不同的输入dispatch到系统里不同的组件。做web app的,可能没有意识到,但其实每个web framework,最关键的组件之一就是url dispatch。HTTP的伟大之处就是每个request,都能通过url被dispatch到不同的handler处理。而url是目录式的,可以层层演进 —— 就像分形几何,一个大的系统,通过不断重复的模式,组合起来 —— 非常利于系统的扩展。遗憾的是,我们自己做系统,对于输入既没有总线的考量,又无路由的概念,if-else下去,久而久之,代码便绕成了意大利面条。
再举一例:DOM中的event bubble,在javascript处理起来已然隐含着路由的概念。你只需定义当某个事件(如onclick)发生时的callback函数就好,至于这事件怎么通过eventloop抵达回调函数,无需关心。好的路由系统剥茧抽丝,把繁杂的信息流正确送到处理者手中。
外部路由总还有「底层」为我们完成,内部路由则需工程师考虑。service级别的路由(数据流由哪个service处理)可以用consul等service discovery组件,service内部的路由(数据流到达后怎么处理)则需要自己完成。路由的具体方式有很多种,pattern matching最为常见。
无论用何种方式路由,数据抵达总线前为其定义Identity(ID)非常重要,你可以管这个过程叫data normalization,data encapsulation等,总之,一个消息能被路由,需要有个用于路由的ID。这ID可以是url,可以是一个message header,也可以是一个label(想象MPLS的情况)。当我们为数据赋予一个个合理的ID后,如何路由便清晰可见。
队列(Queue)
对于那些并非需要立即处理的数据,可以使用队列。队列也有把生产者和消费者分离的功效。队列有:
single producer single consumer(SPSC)

single producer multiple consumers(SPMC)

multiple producers single consumer(MPSC)

multiple producers multiple consumers(MPMC)

仔细想想,队列其实就是总线+路由(可选)+存储的一个特殊版本。一般而言,system bus之上是系统的各个service,每个service再用service bus(或者queue)把micro service chain起来,然后每个micro service内部的组件间,再用queue连接起来。
有了队列,有利于提高流水线的效率。一般而言,流水线的处理速度取决于最慢的组件。队列的存在,让慢速组件有机会运行多份,来弥补生产者和消费者速度上的差距。
Pub/Sub
存储在队列中的数据,除路由外,还有一种处理方式:pub/sub。和路由相似,pub/sub将生产者和消费者分离;但二者不同之处在于,路由的目的地由路由表中的表项控制,而pub/sub一般由publisher控制 [2]:任何subscribe某个数据的consumer,都会到publisher处注册,publisher由此可以定向发送消息。
协议(protocol)
一旦我们把系统分解成一个个service,service再分解成micro service,彼此之间互不依赖,仅仅通过总线或者队列来通讯,那么,我们就需要协议来定义彼此的行为。协议听起来很高大上,其实不然。我们写下的每个function(或者每个class),其实就是在定义一个不成文的协议:function的arity是什么,接受什么参数,返回什么结果。调用者需严格按照协议调用方能得到正确的结果。
service级别的协议是一份SLA:服务的endpoint是什么,版本是什么,接收什么格式的消息,返回什么格式的消息,消息在何种网络协议上承载,需要什么样的authorization,可以正常服务的最大吞吐量(throughput)是什么,在什么情况下会触发throttling等等。
头脑中有了总线,路由,队列,协议等这些在computer science 101中介绍的基础概念,系统的分解便有迹可寻:面对一个系统的设计,你要做的不再是一道作文题,而是一道填空题:在若干条system bus里填上其名称和流进流出的数据,在system bus之上的一个个方框里填上服务的名称和服务的功能。然后,每个服务再以此类推,直到感觉毋须再细化为止。
组成系统的必要服务
有些管理性质的服务,尽管和业务逻辑直接关系不大,但无论是任何系统,都需要考虑构建,这里罗列一二。
代谢(sweeping)
一个活着的生物时时刻刻都进行着新陈代谢:每时每刻新的细胞取代老的细胞,同时身体中的「垃圾」通过排泄系统排出体外。一个运转有序的城市也有新陈代谢:下水道,垃圾场,污水处理等维持城市的正常功能。没有了代谢功能,生物会凋零,城市会荒芜。
软件系统也是如此。日志会把硬盘写满,软件会失常,硬件会失效,网络会拥塞等等。一个好的软件系统需要一个好的代谢系统:出现异常的服务会被关闭,同样的服务会被重新启动,恢复运行。
代谢系统可以参考erlang的supervisor/child process结构,以及supervision tree。很多软件,都运行在简单的supervision tree模式下,如nginx。
高可用性(HA)
每个人都有两个肾。为了apple watch卖掉一个肾,另一个还能保证人体的正常工作。当然,人的两个肾是Active-Active工作模式,内部的肾元(micro service)是 N(active)+M(backup) clustering 工作的(看看人家这service的做的),少了一个,performance会一点点有折扣,但可以忽略不计。
大部分软件系统里的各种服务也需要高可用性:除非完全无状态的服务,且服务重启时间在ms级。服务的高可用性和路由是息息相关的:高可用性往往意味着同一服务的冗余,同时也意味着负载分担。好的路由系统(如consul)能够对路由至同一服务的数据在多个冗余服务间进行负载分担,同时在检测出某个失效服务后,将数据路只由至正常运作的服务。
高可用性还意味着非关键服务,即便不可恢复,也只会导致系统降级,而不会让整个系统无法访问。就像壁虎的尾巴断了不妨碍壁虎逃命,人摔伤了手臂还能吃饭一样,一个软件系统里统计模块的异常不该让用户无法访问他的个人页面。
安保(security)
安保服务分为主动安全和被动安全。authentication/authorization + TLS + 敏感信息加密 + 最小化输入输出接口可以算是主动安全,防火墙等安防系统则是被动安全。
继续拿你的肾来比拟 —— 肾脏起码有两大安全系统:
输入安全。肾器的厚厚的器官膜,保护器官的输入输出安全 —— 主要的输入输出只能是肾动脉,肾静脉和输尿管。

环境安全。肾器里有大量脂肪填充,避免在撞击时对核心功能的损伤。

除此之外,人体还提供了包括免疫系统,皮肤,骨骼,空腔等一系列安全系统,从各个维度最大程度保护一个器官的正常运作。如果我们仔细研究生物,就会发现,安保是个一揽子解决方案:小到细胞,大到整个人体,都有各自的安全措施。一个软件系统也需如此考虑系统中各个层次的安全。
透支保护(overdraft protection)
任何系统,任何服务都是有服务能力的 —— 当这能力被透支时,需要一定的应急计划。如果使用拥有auto scaling的云服务(如AWS),动态扩容是最好的解决之道,但受限于所用的解决方案,它并非万灵药,AWS的auto scaling依赖于load balancer,如Amazon自有的ELB,或者第三方的HAProxy,但ELB对某些业务,如websocket,支持不佳;而第三方的load balancer,则需要考虑部署,与Amazon的auto scaling结合(需要写点代码),避免单点故障,保证自身的capacity等一堆头疼事。
在无法auto scaling的场景最通用的做法是back pressure,把压力反馈到源头。就好像你不断熬夜,最后大脑受不了,逼着你睡觉一样。还有一种做法是服务降级,停掉非核心的service/micro-service,如analytical service,ad service,保证核心功能正常。
把设计的成果讲给别人听
完成了分解和组合,也严肃对待了诸多与业务没有直接关系,但又不得不做的必要功能后,接下来就是要把设计在白板上画下来,讲给任何一个利益相关者听。听他们的反馈。设计不是一个闭门造车的过程,全程都需要和各种利益相关者交流。然而,很多人都忽视了设计定型后,继续和外界交流的必要性。很多人会认为:我的软件架构,设计结果和工程有关,为何要讲给工程师以外的人听?他们懂么?
其实pitch本身就是自我学习和自我修正的一部分。当着一个人或者几个人的面,在白板上画下脑海中的设计的那一刻,你就会有直觉哪个地方似乎有问题,这是很奇特的一种体验:你自己画给自己看并不会产生这种直觉。这大概是面对公众的焦灼产生的肾上腺素的效果。:)
此外,从听者的表情,或者他们提的听起来很傻很天真的问题,你会进一步知道哪些地方你以为你搞通了,其实自己是一知半解。太简单,太基础的问题,我们take it for granted,不屑去问自己,非要有人点出,自己才发现:啊,原来这里我也不懂哈。这就是破解 "you don’t know what you don’t know" 之法。
记得看过一个video,主讲人大谈企业文化,有个哥们傻乎乎发问:so what it culture literally? 主讲人愣了一下,拖拖拉拉讲了一堆自己都不能让自己信服的废话。估计回头他就去查韦氏词典了。
最后,总有人在某些领域的知识更丰富一些,他们会告诉你你一些你知道自己不懂的事情。填补了 "you know that you don’t know" 的空缺。
设计时的tradeoff
Rich hickey(clojure作者)在某个演讲中说:
everyone says design is about tradeoffs, but you need to enumerate at least two or more possible solutions, and the attributes and deficits of each, in order to make tradeoff.

所以,下回再腆着脸说:偶做了些tradeoff,先确保自己做足了功课再说。
设计的改变不可避免
设计不是一锤子买卖,改变不可避免。我之前的一个老板,喜欢把:change is your friend 挂在口头。软件开发的整个生命周期,变更是家常便饭,以至于变更管理都生出一门学问。软件的设计期更是如此。人总会犯错,设计总有缺陷,需求总会变化,老板总会指手画脚,PM总有一天会亮出獠牙,不再是贴心大哥,或者美萌小妹。。。所以,据理力争,然后接受必要的改变即可。连凯恩斯他老人家都说:

What do you do, sir?
(文章很长,感谢阅读至此,这文章3/30号开始写,拖拖拉拉算是凑合完成了)
如果您觉得这篇文章不错,请点赞。多谢!
欢迎订阅公众号『程序人生』(搜索微信号 programmer_life)。每篇文章都力求原汁原味,北京时间中午12点左右,美西时间下午8点左右与您相会。
1. Total Cost of Ownership
2. 要看具体的pub/sub系统的实现方式
原文链接:https://zhuanlan.zhihu.com/p/2 ... attle

Apache Thrift设计概要

默认分类admin 发表了文章 • 0 个评论 • 167 次浏览 • 2017-01-17 16:58 • 来自相关话题

最近把Apache Thrift 的Java版代码翻了一遍,尝试理解做一个RPC框架所要考虑的方方面面。

网上关于Thrift设计的文章好像不多,于是把自己的笔记整理了一下发上来。

加插招聘广告:唯品会广州总部的基础架构部招人!! 如果你喜欢纯技术的工作,对大型互联网企业的服务化平台有兴趣,愿意在架构的成长期还可以大展拳脚的时候加盟,请电邮 calvin.xiao@vipshop.com内容大纲1. Overview
2. Transport层

2.1 Transport
2.2 WrapperTransport

3. Protocol层

3.1 IDL定义
3.2 TBinaryProtocol
3.3 TCompactProtocol
3.4 TTupleProtocol
3.5 TJSONProtocol

4. Processor层

4.1 基础接口

5. 服务端

5.1 Blocking Server
5.2 NonBlockingServer
5.3 TThreadedSelectorServer

6. 客户端

6.1 同步客户端
6.2 异步客户端

7. Http
8. Generator

1. Overview

Apache Thrift 的可赞之处是实现了跨超多语言(Java, C++, Go, Python,Ruby, PHP等等)的RPC框架,尽力为每种语言实现了相同抽象的RPC客户端与服务端。

简洁的四层接口抽象,每一层都可以独立的扩展增强或替换,是另一个可赞的地方。

最后,二进制的编解码方式和NIO的底层传输为它提供了不错的性能。

+-------------------------------------------+
| Server |
| (single-threaded, event-driven etc) |
+-------------------------------------------+
| Processor |
| (compiler generated) |
+-------------------------------------------+
| Protocol |
| (JSON, compact etc) |
+-------------------------------------------+
| Transport |
| (raw TCP, HTTP etc) |
+-------------------------------------------+

Transport层提供了一个简单的网络读写抽象层,有阻塞与非阻塞的TCP实现与HTTP的实现。

Protocol层定义了IDL中的数据结构与Transport层的传输数据格式之间的编解码机制。传输格式有二进制,压缩二进制,JSON等格式,IDL中的数据结构则包括Message,Struct,List,Map,Int,String,Bytes等。

Processor层由编译器编译IDL文件生成。
生成的代码会将传输层的数据解码为参数对象(比如商品对象有id与name两个属性,生成的代码会调用Protocol层的readInt与readString方法读出这两个属性值),然后调用由用户所实现的函数,并将结果编码送回。

在服务端, Server层创建并管理上面的三层,同时提供线程的调度管理。而对于NIO的实现,Server层可谓操碎了心。

在客户端, Client层由编译器直接生成,也由上面的三层代码组成。只要语言支持,客户端有同步与异步两种模式。

Facebook是Thrift的原作者,还开源有Nifty与Swift两个子项目, Cassandra是另一个著名用户,其跨语言的Client包就是基于Thrift的,这些在下一篇文章中展开讨论。

 

2. Transport层
2.1 Transport

TTransport除了open/close/flush,最重要的方法是int read(byte[] buf, int off, int len),void write(byte[] buf, int off, int len),读出、写入固定长度的内容。

TSocket使用经典的JDK Blocking IO的Transport实现。1K的BufferedStream, TCP配置项:KeepAlive=true,TCPNoDelay=true,SoLinger=false,SocketTimeout与ConnectionTimeout可配。

TNonblockingSocket,使用JDK NIO的Transport实现,读写的byte[]会每次被wrap成一个ByteBuffer。TCP配置项类似。
其实这个NonBlockingSocket并没有完全隔离传输层,后面异步Client或NIO的Server还有很多事情要做。

相应的,TServerSocket和TNonblockingServerSocket是ServerTransport的BIO、NIO实现,主要实现侦听端口,Accept后返回TSocket或TNonblockingSocket。其他TCP配置项:ReuseAddress=true,Backlog与SocketTimeout可配。

2.2 WrapperTransport

包裹一个底层的Transport,并利用自己的Buffer进行额外的操作。

1. TFramedTransport,按Frame读写数据。

每Frame的前4字节会记录Frame的长度(少于16M)。读的时候按长度预先将整Frame数据读入Buffer,再从Buffer慢慢读取。写的时候,每次flush将Buffer中的所有数据写成一个Frame。

有长度信息的TFramedTransport是后面NonBlockingServer粘包拆包的基础。

2. TFastFramedTransport 与TFramedTransport相比,始终使用相同的Buffer,提高了内存的使用率。

TFramedTransport的ReadBuffer每次读入Frame时都会创建新的byte[],WriteBuffer每次flush时如果大于初始1K也会重新创建byte[]。

而TFastFramedTransport始终使用相同的ReadBuffer和WriteBuffer,都是1K起步,不够时自动按1.5倍增长,和NIO的ByteBuffer一样,加上limit/pos这样的指针,每次重复使用时设置一下它们。

3. TZlibTransport ,读取时按1K为单位将数据读出并调用JDK的zip函数进行解压再放到Buffer,写入时,在flush时先zip再写入。

4. TSaslClientTransport与TSaslServerTransport, 提供SSL校验。

 

3. Protocol层
3.1 IDL定义

Thrift支持结构见 http://thrift.apache.org/docs/types

* 基本类型: i16,i32,i64, double, boolean,byte,byte[], String。
* 容器类型: List,Set,Map,TList/TSet/TMap类包含其元素的类型与元素的总个数。
* Struct类型,即面向对象的Class,继承于TBase。TStruct类有Name属性,还含有一系列的Field。TField类有自己的Name,类型,顺序id属性。
* Exception类型也是个Struct,继承于TException这个checked exception。
* Enum类型传输时是个i32。
* Message类型封装往返的RPC消息。TMessage类包含Name,类型(请求,返回,异常,ONEWAY)与seqId属性。

相应的,Protocol 层对上述数据结构有read与write的方法。
对基本类型是直接读写,对结构类型则是先调用readXXXBegin(),再调用其子元素的read()方法,再调用readXXXEnd()。
在所有函数中,Protocol层会直接调用Transport层读写特定长度的数据。

3.2 TBinaryProtocol

如前所述,i16,i32, double这些原始类型都是定长的,String,byte[]会在前4个字节说明自己的长度,容器类,Strutct类所对应的TMap,TStruct,TField,TMessage里有如前所述的属性 (不过Struct与Field里的name属性会被skip),所以其实现实可以简单想象的。

3.3 TCompactProtocol

比起TBinaryProtocol,会想方设法省点再省点。

1. 对整数类型使用了 ZigZag 压缩算法,数值越小压的越多。比如 i32 类型的整数本来是4个字节, 可以压缩成 1~5 字节不等。而 i64类型的整数本来是8个字节,可以压缩成 1~10 字节不等。 因此,值小的i32和i64,和小的Collection和短的String(因为它们都有定义长度的int属性) 越多,它就能省得越多。

2. 它还会尝试将Field的fieldId和type挤在一个byte里写。原本field是short,type也占一个byte,合共三个byte。 它将1个byte拆成两组4bit,前4bit放与前一个field的Id的delta值(不能相差超过15,如果中间太多没持久化的optional field),后4bit放type(目前刚好16种type,把byte[]和String合成一种了)

3.4 TTupleProtocol

继承于TCompactProtocol,Struct的编解码时使用更省地方但IDL间版本不兼容的TupleScheme,见Processor层。

3.5 TJSONProtocol

与我们平时的Restful JSON还是有点区别,具体的定义看cpp实现的TJSONProtool.h文件

对于容器类和Message,会用数组的方式,前几个元素是元信息,后面才是value,比如List是[type,size,[1,2,3]],

对于Struct,会是个Map,Key是fieldId而不是name(为了节省空间?),Value又是一个Map,只有一个Key-Value Pair,Key是type,Value才是真正value。

 

4. Processor层

建议使用一个最简单的IDL文件,用Windows版的Generator生成一个来进行观察。

4.1 基础接口

TBase是大部分生成的Struct,参数类,结果类的接口,最主要是实现从Protocol层读写自己的函数。

TProcessFunction是生成的服务方法类的基类,它的process函数会完成如下步骤:

1. 调用生成的args对象的read方法从protocol层读出自己
2. 调用子类生成的getResult()方法:拆分args对象得到参数,调用真正的用户实现得到结果,并组装成生成的result对象。
3. 写消息头,
4. 调用生成的result对象的write方法将自己写入protocol层
5 调用transport层的flush()。

TBaseProcessor 只有 boolean process(TProtocol in, TProtocol out) 这个简单接口,会先调readMessageBegin(),读出消息名,再从processMap里找出相应的TProcessFunction调用。

TMultiplexedProcessor,支持一个Server支持部署多个Service的情况,在Processor外面再加一层Map,消息名从“add”会变为“Caculator:add”,当然,这需要需要客户端发送时使用 TMultiplexedProtocol修饰原来的Protocol来实现。

4.2 代码生成


1. IFace接口

接口里的函数名不能重名,即使参数不一样也不行。
如果参数是个Struct,则会直接使用继承于TBase的生成类,对客户代码有一定侵入性。
默认抛出TException 这个checked exception,可自定义继承于TException的其他Exception类。

2. Processor类

继承于TBaseProcessor,简单构造出processMap,构造时需要传入用户实现的IFace实现类。

3. 接口里所有方法的方法类

继承于TProcessFunction,见前面TProcessFunction的描述。

4. Struct类,方法参数类和方法结果类

继承于Base。

读取Struct时,遇上fieldId为未知的,或者类型不同于期望类型的field,会被Skip掉。所谓skip,就是只按传过来的type,读取其内容推动数据流往前滚动,但不往field赋值,这也是为什么有了生成的代码,仍然要传输元数据的原因。

为了保持不同服务版本间的兼容性,永远对方法的参数与Struct的field只增不减不改就对了。

因为不同版本间,Struct的filed的数量未知,而StructEnd又无特殊标志,所以在Struct最后会放一个type=Stop的filed,读到则停止Struct的读入。

写入Struct时,Java对象(如String,Struct)或者设为optional的原始类型(如int)会先判断一下这个值被设置没有。Java对象只要判断其是否为null,原始类型就要额外增加一个bitset来记录该field是否已设置,根据field的数量,这个bitset是byte(8个)或short(16个)。

以上是StandardScheme的行为,每个Struct还会生成一种更节约空间但服务版本间不兼容的TupleScheme,它不再传输fieldId与type的元数据,只在Struct头写入一个bitset表明哪几个field有值,然后直接用生成的代码读取这些有值的field。所以如果新版idl中filed类型改动将出错;新的field也不会被读取数据流没有往前滚动,接下来也是错。

5. Thrift二进制与Restful JSON的编码效率对比

为了服务版本兼容,Thrift仍然需要传输数字型的fieldId,但比JSON的fieldName省地方。
int与byte[],当然比JSON的数字字符串和BASE64编码字符串省地方。
省掉了’:’和’,'
省掉了””,[], {}, 但作为代价,字符串,byte[],容器们都要有自己的长度定义,Struct也要有Stop Field。
但比起JSON,容器和Field又额外增加了类型定义的元数据。

 

5. 服务端

基类TServer相当于一个容器,拥有生产TProcessor、TTransport、TProtocol的工厂对象。改变这些工厂类,可以修饰包裹Transport与Protocol类,改变TProcessor的单例模式或与Spring集成等等。

https://github.com/m1ch1/mapke ... pared 这篇文章比较了各种Server

5.1 Blocking Server

TSimpleServer同时只能处理一个Client连接,只是个玩具。

TThreadPoolServer才是典型的多线程处理的Blocking Server实现。
线程池类似 Executors.newCachedThreadPool(),可设最小最大线程数(默认是5与无限)。
每条线程处理一个Client,如果所有线程都在忙,会等待一个random的时间重试直到设定的requestTimeout。
线程对于Client好像没有断开连接的机制,只靠捕获TTransportException来停止服务?

5.2 NonBlockingServer

TThreadedSelectorServer有一条线程专门负责accept,若干条Selector线程处理网络IO,一个Worker线程池处理消息。

THsHaServer只有一条AcceptSelect线程处理关于网络的一切,一个Worker线程池处理消息。

TNonblockingServer只有一条线程处理一切。

很明显TThreadedSelectorServer是被使用得最多的,因为在多核环境下多条Selector线程的表现会更好。所以只对它展开细读。

5.3 TThreadedSelectorServer

TThreadedSelectorServer属于 Half-Sync/Half-Async模式。

AcceptThread线程使用TNonblockingServerTransport执行accept操作,将accept到的Transport round-robin的交给其中一条SelectorThread。
因为SelectorThread自己也要处理IO,所以AcceptThread是先扔给SelectorThread里的Queue(默认长度只有4,满了就要阻塞等待)。

SelectorThread每个循环各执行一次如下动作
1. 注册Transport
2. select()处理IO
3. 处理FrameBuffer的状态变化

注册Transport时,在Selector中注册OP_READ,并创建一个带状态机(READING_FRAME_SIZE,READING_FRAME,READ_FRAME_COMPLETE, AWAITING_REGISTER_WRITE等)的FrameBuffer类与其绑定。

客户端必须使用FrameTransport(前4个字节记录Frame的长度)来发送数据以解决粘包拆包问题。

SelectorThread在每一轮的select()中,对有数据到达的Transport,其FrameBuffer先读取Frame的长度,然后创建这个长度的ByteBuffer继续读取数据,读满了就交给Worker线程中的Processor处理,没读够就继续下一轮循环。

当交给Processor处理时,Processor不像Blocking Server那样直接和当前的Transport打交道,而是实际将已读取到的Frame数据转存到一个MemoryTransport中,output时同样只是写到一个由内存中的ByteArray(初始大小为32)打底的OutputStream。

Worker线程池用newFixedThreadPool()创建,Processor会解包,调用用户实现,再把结果编码发送到前面传入的那个ByteArray中。FrameBuffer再把ByteArray转回ByteBuffer,状态转为AWAITING_REGISTER_WRITE,并在SelectorThread中注册该变化。

回到SelectorThread中,发现FrameBuffer的当前状态为AWAITING_REGISTER_WRITE,在Selector中注册OP_WRITE,等待写入的机会。在下一轮循环中就会开始写入数据,写完的话FrameBuffer又转到READING_FRAME_SIZE的状态,在Selector中重新注册OP_READ。

还有更多的状态机处理,略。

 

6. 客户端
6.1 同步客户端

同样通过生成器生成,其中Client类继承TClient基类实现服务的同步接口。

int add(int num1,int num2)会调用生成的send_add(num1, num2) 与 int receive_add()。
send_add()构造add_args()对象,调用父类的sendBase(“add",args),sendBase()调用protocol的writeMessage写入消息头,然后调用args自己的write(protocol),然后调用transport的flush()发送。
receive_add()构造add_result,调用父类的receive_base(add_result),receive_base()调用protocol层的readMessage读出消息头,如果类型是Exception则 用TApplicationException类来读取消息,否则调用result类的read()函数。

注意这里的seq_id并不支持并发访问,在发送时简单的+1,在接收时再进行比较,如果不对则会报错。因此Client好像是非线程安全的。

6.2 异步客户端

异步客户端,需要传入一个CallBack实现,在收到返回结果或错误时调用。

使用NonblockingSocket,每个客户端会起一条线程,在这条线程里忙活所有消息Non blocking发送,接收,编解码及调用Callback类,还有超时调用的处理。
其状态机的写法,TThreadedSelectorServer有相似之处,略。

 

7. Http

走Http协议,主要是利用了Thrift的二进制编解码机制,而放弃了它的底层NIO传输与服务线程模型。

THttpClient,使用Apache HttpClient或JDK的HttpURLConnection Post内容。

TServlet,作为Servlet,将request与response的stream交给Processor处理即可,不用处理线程模型与NIO,很简单。

 

8. Generator

Generator用C++编写,有Parser类分析thrift文件元数据模型,然后每种语言有自己的生成模型。

为Java生成代码的t_java_generator.cc有五千多行,不过很规整很容易看。

除了Java,一般还会生成Html的API描述文档。 查看全部
最近把Apache Thrift 的Java版代码翻了一遍,尝试理解做一个RPC框架所要考虑的方方面面。

网上关于Thrift设计的文章好像不多,于是把自己的笔记整理了一下发上来。

加插招聘广告:唯品会广州总部的基础架构部招人!! 如果你喜欢纯技术的工作,对大型互联网企业的服务化平台有兴趣,愿意在架构的成长期还可以大展拳脚的时候加盟,请电邮 calvin.xiao@vipshop.com内容大纲1. Overview
2. Transport层

2.1 Transport
2.2 WrapperTransport

3. Protocol层

3.1 IDL定义
3.2 TBinaryProtocol
3.3 TCompactProtocol
3.4 TTupleProtocol
3.5 TJSONProtocol

4. Processor层

4.1 基础接口

5. 服务端

5.1 Blocking Server
5.2 NonBlockingServer
5.3 TThreadedSelectorServer

6. 客户端

6.1 同步客户端
6.2 异步客户端

7. Http
8. Generator

1. Overview

Apache Thrift 的可赞之处是实现了跨超多语言(Java, C++, Go, Python,Ruby, PHP等等)的RPC框架,尽力为每种语言实现了相同抽象的RPC客户端与服务端。

简洁的四层接口抽象,每一层都可以独立的扩展增强或替换,是另一个可赞的地方。

最后,二进制的编解码方式和NIO的底层传输为它提供了不错的性能。

+-------------------------------------------+
| Server |
| (single-threaded, event-driven etc) |
+-------------------------------------------+
| Processor |
| (compiler generated) |
+-------------------------------------------+
| Protocol |
| (JSON, compact etc) |
+-------------------------------------------+
| Transport |
| (raw TCP, HTTP etc) |
+-------------------------------------------+

Transport层提供了一个简单的网络读写抽象层,有阻塞与非阻塞的TCP实现与HTTP的实现。

Protocol层定义了IDL中的数据结构与Transport层的传输数据格式之间的编解码机制。传输格式有二进制,压缩二进制,JSON等格式,IDL中的数据结构则包括Message,Struct,List,Map,Int,String,Bytes等。

Processor层由编译器编译IDL文件生成。
生成的代码会将传输层的数据解码为参数对象(比如商品对象有id与name两个属性,生成的代码会调用Protocol层的readInt与readString方法读出这两个属性值),然后调用由用户所实现的函数,并将结果编码送回。

在服务端, Server层创建并管理上面的三层,同时提供线程的调度管理。而对于NIO的实现,Server层可谓操碎了心。

在客户端, Client层由编译器直接生成,也由上面的三层代码组成。只要语言支持,客户端有同步与异步两种模式。

Facebook是Thrift的原作者,还开源有Nifty与Swift两个子项目, Cassandra是另一个著名用户,其跨语言的Client包就是基于Thrift的,这些在下一篇文章中展开讨论。

 

2. Transport层
2.1 Transport

TTransport除了open/close/flush,最重要的方法是int read(byte[] buf, int off, int len),void write(byte[] buf, int off, int len),读出、写入固定长度的内容。

TSocket使用经典的JDK Blocking IO的Transport实现。1K的BufferedStream, TCP配置项:KeepAlive=true,TCPNoDelay=true,SoLinger=false,SocketTimeout与ConnectionTimeout可配。

TNonblockingSocket,使用JDK NIO的Transport实现,读写的byte[]会每次被wrap成一个ByteBuffer。TCP配置项类似。
其实这个NonBlockingSocket并没有完全隔离传输层,后面异步Client或NIO的Server还有很多事情要做。

相应的,TServerSocket和TNonblockingServerSocket是ServerTransport的BIO、NIO实现,主要实现侦听端口,Accept后返回TSocket或TNonblockingSocket。其他TCP配置项:ReuseAddress=true,Backlog与SocketTimeout可配。

2.2 WrapperTransport

包裹一个底层的Transport,并利用自己的Buffer进行额外的操作。

1. TFramedTransport,按Frame读写数据。

每Frame的前4字节会记录Frame的长度(少于16M)。读的时候按长度预先将整Frame数据读入Buffer,再从Buffer慢慢读取。写的时候,每次flush将Buffer中的所有数据写成一个Frame。

有长度信息的TFramedTransport是后面NonBlockingServer粘包拆包的基础。

2. TFastFramedTransport 与TFramedTransport相比,始终使用相同的Buffer,提高了内存的使用率。

TFramedTransport的ReadBuffer每次读入Frame时都会创建新的byte[],WriteBuffer每次flush时如果大于初始1K也会重新创建byte[]。

而TFastFramedTransport始终使用相同的ReadBuffer和WriteBuffer,都是1K起步,不够时自动按1.5倍增长,和NIO的ByteBuffer一样,加上limit/pos这样的指针,每次重复使用时设置一下它们。

3. TZlibTransport ,读取时按1K为单位将数据读出并调用JDK的zip函数进行解压再放到Buffer,写入时,在flush时先zip再写入。

4. TSaslClientTransport与TSaslServerTransport, 提供SSL校验。

 

3. Protocol层
3.1 IDL定义

Thrift支持结构见 http://thrift.apache.org/docs/types

* 基本类型: i16,i32,i64, double, boolean,byte,byte[], String。
* 容器类型: List,Set,Map,TList/TSet/TMap类包含其元素的类型与元素的总个数。
* Struct类型,即面向对象的Class,继承于TBase。TStruct类有Name属性,还含有一系列的Field。TField类有自己的Name,类型,顺序id属性。
* Exception类型也是个Struct,继承于TException这个checked exception。
* Enum类型传输时是个i32。
* Message类型封装往返的RPC消息。TMessage类包含Name,类型(请求,返回,异常,ONEWAY)与seqId属性。

相应的,Protocol 层对上述数据结构有read与write的方法。
对基本类型是直接读写,对结构类型则是先调用readXXXBegin(),再调用其子元素的read()方法,再调用readXXXEnd()。
在所有函数中,Protocol层会直接调用Transport层读写特定长度的数据。

3.2 TBinaryProtocol

如前所述,i16,i32, double这些原始类型都是定长的,String,byte[]会在前4个字节说明自己的长度,容器类,Strutct类所对应的TMap,TStruct,TField,TMessage里有如前所述的属性 (不过Struct与Field里的name属性会被skip),所以其实现实可以简单想象的。

3.3 TCompactProtocol

比起TBinaryProtocol,会想方设法省点再省点。

1. 对整数类型使用了 ZigZag 压缩算法,数值越小压的越多。比如 i32 类型的整数本来是4个字节, 可以压缩成 1~5 字节不等。而 i64类型的整数本来是8个字节,可以压缩成 1~10 字节不等。 因此,值小的i32和i64,和小的Collection和短的String(因为它们都有定义长度的int属性) 越多,它就能省得越多。

2. 它还会尝试将Field的fieldId和type挤在一个byte里写。原本field是short,type也占一个byte,合共三个byte。 它将1个byte拆成两组4bit,前4bit放与前一个field的Id的delta值(不能相差超过15,如果中间太多没持久化的optional field),后4bit放type(目前刚好16种type,把byte[]和String合成一种了)

3.4 TTupleProtocol

继承于TCompactProtocol,Struct的编解码时使用更省地方但IDL间版本不兼容的TupleScheme,见Processor层。

3.5 TJSONProtocol

与我们平时的Restful JSON还是有点区别,具体的定义看cpp实现的TJSONProtool.h文件

对于容器类和Message,会用数组的方式,前几个元素是元信息,后面才是value,比如List是[type,size,[1,2,3]],

对于Struct,会是个Map,Key是fieldId而不是name(为了节省空间?),Value又是一个Map,只有一个Key-Value Pair,Key是type,Value才是真正value。

 

4. Processor层

建议使用一个最简单的IDL文件,用Windows版的Generator生成一个来进行观察。

4.1 基础接口

TBase是大部分生成的Struct,参数类,结果类的接口,最主要是实现从Protocol层读写自己的函数。

TProcessFunction是生成的服务方法类的基类,它的process函数会完成如下步骤:

1. 调用生成的args对象的read方法从protocol层读出自己
2. 调用子类生成的getResult()方法:拆分args对象得到参数,调用真正的用户实现得到结果,并组装成生成的result对象。
3. 写消息头,
4. 调用生成的result对象的write方法将自己写入protocol层
5 调用transport层的flush()。

TBaseProcessor 只有 boolean process(TProtocol in, TProtocol out) 这个简单接口,会先调readMessageBegin(),读出消息名,再从processMap里找出相应的TProcessFunction调用。

TMultiplexedProcessor,支持一个Server支持部署多个Service的情况,在Processor外面再加一层Map,消息名从“add”会变为“Caculator:add”,当然,这需要需要客户端发送时使用 TMultiplexedProtocol修饰原来的Protocol来实现。

4.2 代码生成


1. IFace接口

接口里的函数名不能重名,即使参数不一样也不行。
如果参数是个Struct,则会直接使用继承于TBase的生成类,对客户代码有一定侵入性。
默认抛出TException 这个checked exception,可自定义继承于TException的其他Exception类。

2. Processor类

继承于TBaseProcessor,简单构造出processMap,构造时需要传入用户实现的IFace实现类。

3. 接口里所有方法的方法类

继承于TProcessFunction,见前面TProcessFunction的描述。

4. Struct类,方法参数类和方法结果类

继承于Base。

读取Struct时,遇上fieldId为未知的,或者类型不同于期望类型的field,会被Skip掉。所谓skip,就是只按传过来的type,读取其内容推动数据流往前滚动,但不往field赋值,这也是为什么有了生成的代码,仍然要传输元数据的原因。

为了保持不同服务版本间的兼容性,永远对方法的参数与Struct的field只增不减不改就对了。

因为不同版本间,Struct的filed的数量未知,而StructEnd又无特殊标志,所以在Struct最后会放一个type=Stop的filed,读到则停止Struct的读入。

写入Struct时,Java对象(如String,Struct)或者设为optional的原始类型(如int)会先判断一下这个值被设置没有。Java对象只要判断其是否为null,原始类型就要额外增加一个bitset来记录该field是否已设置,根据field的数量,这个bitset是byte(8个)或short(16个)。

以上是StandardScheme的行为,每个Struct还会生成一种更节约空间但服务版本间不兼容的TupleScheme,它不再传输fieldId与type的元数据,只在Struct头写入一个bitset表明哪几个field有值,然后直接用生成的代码读取这些有值的field。所以如果新版idl中filed类型改动将出错;新的field也不会被读取数据流没有往前滚动,接下来也是错。

5. Thrift二进制与Restful JSON的编码效率对比

为了服务版本兼容,Thrift仍然需要传输数字型的fieldId,但比JSON的fieldName省地方。
int与byte[],当然比JSON的数字字符串和BASE64编码字符串省地方。
省掉了’:’和’,'
省掉了””,[], {}, 但作为代价,字符串,byte[],容器们都要有自己的长度定义,Struct也要有Stop Field。
但比起JSON,容器和Field又额外增加了类型定义的元数据。

 

5. 服务端

基类TServer相当于一个容器,拥有生产TProcessor、TTransport、TProtocol的工厂对象。改变这些工厂类,可以修饰包裹Transport与Protocol类,改变TProcessor的单例模式或与Spring集成等等。

https://github.com/m1ch1/mapke ... pared 这篇文章比较了各种Server

5.1 Blocking Server

TSimpleServer同时只能处理一个Client连接,只是个玩具。

TThreadPoolServer才是典型的多线程处理的Blocking Server实现。
线程池类似 Executors.newCachedThreadPool(),可设最小最大线程数(默认是5与无限)。
每条线程处理一个Client,如果所有线程都在忙,会等待一个random的时间重试直到设定的requestTimeout。
线程对于Client好像没有断开连接的机制,只靠捕获TTransportException来停止服务?

5.2 NonBlockingServer

TThreadedSelectorServer有一条线程专门负责accept,若干条Selector线程处理网络IO,一个Worker线程池处理消息。

THsHaServer只有一条AcceptSelect线程处理关于网络的一切,一个Worker线程池处理消息。

TNonblockingServer只有一条线程处理一切。

很明显TThreadedSelectorServer是被使用得最多的,因为在多核环境下多条Selector线程的表现会更好。所以只对它展开细读。

5.3 TThreadedSelectorServer

TThreadedSelectorServer属于 Half-Sync/Half-Async模式。

AcceptThread线程使用TNonblockingServerTransport执行accept操作,将accept到的Transport round-robin的交给其中一条SelectorThread。
因为SelectorThread自己也要处理IO,所以AcceptThread是先扔给SelectorThread里的Queue(默认长度只有4,满了就要阻塞等待)。

SelectorThread每个循环各执行一次如下动作
1. 注册Transport
2. select()处理IO
3. 处理FrameBuffer的状态变化

注册Transport时,在Selector中注册OP_READ,并创建一个带状态机(READING_FRAME_SIZE,READING_FRAME,READ_FRAME_COMPLETE, AWAITING_REGISTER_WRITE等)的FrameBuffer类与其绑定。

客户端必须使用FrameTransport(前4个字节记录Frame的长度)来发送数据以解决粘包拆包问题。

SelectorThread在每一轮的select()中,对有数据到达的Transport,其FrameBuffer先读取Frame的长度,然后创建这个长度的ByteBuffer继续读取数据,读满了就交给Worker线程中的Processor处理,没读够就继续下一轮循环。

当交给Processor处理时,Processor不像Blocking Server那样直接和当前的Transport打交道,而是实际将已读取到的Frame数据转存到一个MemoryTransport中,output时同样只是写到一个由内存中的ByteArray(初始大小为32)打底的OutputStream。

Worker线程池用newFixedThreadPool()创建,Processor会解包,调用用户实现,再把结果编码发送到前面传入的那个ByteArray中。FrameBuffer再把ByteArray转回ByteBuffer,状态转为AWAITING_REGISTER_WRITE,并在SelectorThread中注册该变化。

回到SelectorThread中,发现FrameBuffer的当前状态为AWAITING_REGISTER_WRITE,在Selector中注册OP_WRITE,等待写入的机会。在下一轮循环中就会开始写入数据,写完的话FrameBuffer又转到READING_FRAME_SIZE的状态,在Selector中重新注册OP_READ。

还有更多的状态机处理,略。

 

6. 客户端
6.1 同步客户端

同样通过生成器生成,其中Client类继承TClient基类实现服务的同步接口。

int add(int num1,int num2)会调用生成的send_add(num1, num2) 与 int receive_add()。
send_add()构造add_args()对象,调用父类的sendBase(“add",args),sendBase()调用protocol的writeMessage写入消息头,然后调用args自己的write(protocol),然后调用transport的flush()发送。
receive_add()构造add_result,调用父类的receive_base(add_result),receive_base()调用protocol层的readMessage读出消息头,如果类型是Exception则 用TApplicationException类来读取消息,否则调用result类的read()函数。

注意这里的seq_id并不支持并发访问,在发送时简单的+1,在接收时再进行比较,如果不对则会报错。因此Client好像是非线程安全的。

6.2 异步客户端

异步客户端,需要传入一个CallBack实现,在收到返回结果或错误时调用。

使用NonblockingSocket,每个客户端会起一条线程,在这条线程里忙活所有消息Non blocking发送,接收,编解码及调用Callback类,还有超时调用的处理。
其状态机的写法,TThreadedSelectorServer有相似之处,略。

 

7. Http

走Http协议,主要是利用了Thrift的二进制编解码机制,而放弃了它的底层NIO传输与服务线程模型。

THttpClient,使用Apache HttpClient或JDK的HttpURLConnection Post内容。

TServlet,作为Servlet,将request与response的stream交给Processor处理即可,不用处理线程模型与NIO,很简单。

 

8. Generator

Generator用C++编写,有Parser类分析thrift文件元数据模型,然后每种语言有自己的生成模型。

为Java生成代码的t_java_generator.cc有五千多行,不过很规整很容易看。

除了Java,一般还会生成Html的API描述文档。

ClassLoader, JavaAgent, Aspectj Weaving一站式扫盲帖

默认分类admin 发表了文章 • 0 个评论 • 155 次浏览 • 2017-01-17 16:56 • 来自相关话题

最近工作里复习的Class Loader基础知识集锦,写下来希望对别人有帮助,而且不止是为了撂倒面试官。

为了尽量简单明了容易背,有些部分写得比较干。

0. 参考资料:

书:《深入了解Java虚拟机》、《实战Java虚拟机》
规范:Java语言规范 第12章
源码: OpenJDK 7的Java及C代码( Class.c , Classloader.c,jvm.cpp)

 

1. Class装载的三个阶段
1.1 载入 (Load)

从Class文件或别的什么地方载入一段二进制流字节流,把它解释成永久代里的运行时数据结构,生成一个Class对象。
同时也会装载它的父类或父接口,并进行校验,比如编译版本不对会抛出UnsupportedClassError,依赖定义不对会抛 IncompatibleClassChangeError/ClassCircularityError等。

1.2 链接 (Resolve)

将之前载入的数据结构里的符号引用表,解析成直接引用。
中间如果遇到引用的类还没被加载,就会触发该类的加载。
可能JDK会很懒惰的在运行某个函数实际使用到该引用时才发生链接,也可能在类加载时就解析全部引用。

1.3 初始化 (Initniazle)

初始化静态变量,并执行静态初始化语句。

 

2. Class装载的时机

ClassLoader.loadClass()
前文所说的链接时触发的装载
Class.forName() 等java.lang.reflect反射包
new 构造对象
初始化子类时,会同时初始化父类
访问类的静态变量或静态方法(但static final的常量除外,此君在常量池里)

本质上,也是很懒惰的按需加载的,由于类装载的Lazy和前面解释引用的Lazy,所以Jar包里有时候有些类用到的了没在Class Path里的其他类,也能人品爆发的照跑不误。

除了时机1,其他几种方式默认都到达类装载的初始化阶段。

 

3. ClassLoader.loadClass() 与 Class.forName()

ClassLoader.loadClass(String name, boolean resolve),其中resolve默认为false,即只执行类装载的第一个阶段。

Class.forName(String name, boolean initialize, ClassLoader loader), 其中initialize默认为true,即执行到类装载的第三个阶段。

 

4. ClassNotFoundException 和 NoClassDefFoundError

ClassLoader.loadClass() 与 Class.forName() 找不到类定义的二进制流时抛出ClassNotFoundException。

链接阶段解释引用失败,找不到引用的类时抛出NoClassDefFoundError。

其他常见错误包括编译版本不对的

 

5. ClassLoader及双亲委派机制

ClassLoader.loadClass()的标准流程:

findLoadedClass() 查看类是否已加载
如果不存在,则调用parent loader的loadClass()
如果不存在,调用findClass() 在本ClassLoader的ClassPath里加载该类

所谓双亲委派机制,就是先从parent loader开始查找,找不到了才用自己的findClass()函数去查找,兼顾了效率:避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次,和安全:避免子类乱加载。

而OSGI或SPI或热替换方案,则需要破坏这个双亲委托,先调用自己的findClass()。

findClass() 是各个ClassLoader各自实现,各显神通的地方,从各种奇葩地方载入Class二进制字节流。

但最后都会调用defineClass(),传入二进制字节流,返回Class对象。留意此处,呆会AspectJ的时候会回到这里。

在JDK6,loadClass()很过分的定义了方法级的synchronized ,在JDK7改成一个以Class Name作Key的 parallelLockMap,增强了并行加载不同Class的能力。

 

6. System ClassLoader 与 Thread Context Classloader

有时候,看到错误日志说张三不是张三,包名类名一样但instanceof 死活返回 false,唯一原因是它们由两个不同的ClassLoader加载。

默认的Bootstrap(加载jdk的lib目录),Extension(加载jdk的lib/ext目录),Application(加载启动时定义的classpath)三层ClassLoader机制不再重复。

平时用ClassLoader.getSystemClassLoader()就可以得到sun.misc.Launcher$ApplicationClassLoader 这个Application ClassLoader。

在类A里加载类B,默认使用加载了类A的Loader。但,也有特殊情况,比如JDBC加载driver时的机制,需要在Bootstrap ClassLoader(JDBC属于JDK一部分)里根据配置反射创建jdbc driver的数据实现类,此时Bootstrap Classloader并没有某个jdbc driver实现类的包(在Application Classloaer中),因此Sun设计了一个特殊方案 --Thread Context Class Loader。

JAXB(也是JDK一部分,比如要在Jar包里找xsd schema文件的时候)也使用了它,所以用到它们时就要注意Thread Context ClassLoader的设置,将Application Classloader设进去。可以用代码随时设置current thread的loader,也可以用自定义的ThreadFactory在创建线程时设置,它默认是父线程的loader,如果都没设置就是 System ClassLoader这个Application ClassLoader。

 

7. Java Agent机制与AspectJ的LoadTime Weaving

在JDK5开始,在启动JVM时可增加-javaagent参数,在装载Class时对类进行动态的修改。

AspectJ的Load Time Weaving机制,需要配置 -javaagent: [path to aspectj-weaver.jar] 。

打开aspectj-weaver.jar,可以看到META-INF/MANIFEST里定义了 Premain-Class: org.aspectj.weaver.loadtime.Agent

再打开这个Agent类,简化后的代码大概这个样子:

 

ClassFileTransformer s_transformer = new ClassPreProcessorAgentAdapter();

 

public static void premain(String options, Instrumentation instrumentation) {

 

instrumentation.addTransformer(s_transformer);

 

}

可见它的主要作用是将自己的类转换器注册到JDK所传入的Instrumentation。

再看类转换器的定义:ClassLoader会在前面defineClass()的过程中,在把二进制字节流转换为Class对象之前,先把二进制流和当前ClassLoader传给转换器,由转换器加工为另一段二进制字节流返回。

AspectJ就是利用传入的ClassLoader,找出其Class Path里的META-INF/aop.xml,然后根据aop.xml里的配置进行代码植入。

测试显示,加了LoadTime Weaving,类加载的速度明显变慢,如果是100ms就调用超时的服务,需要做类的预加载。

 

8. Jar包的预加载

比如有个有趣的需求是加载某个Class A所在的Jar里的全部的Class (怎么好像一点都不有趣)

 

URL jarUrl = ClassA.getProtectionDomain().getCodeSource().getLocation();
JarFile jarfile = new JarFile(jarUrl.getPath());
Enumeration entries = jarfile.entries();

然后遍历JarEntry,过滤出后缀为.class的文件,按类名进行装载就可以了。

 

9.Class的二进制兼容性

如果Class A 依赖 spring-1.0.jar编译,当spring升级到spring-2.0.jar,Class A不需要修改代码也不需要重新编译,可以直接运行的,spring-2.0.jar就满足二进制兼容性。

在Java语言规范的第13章有详细的描述 ,不想直接睡着最好可以找个中文版来看,感谢那些翻译的同学。

虽然规范的这章看着比较长比较吓人,但其实二进制兼容性还是很容易做到的,只要你不做把接口改为抽象类之类或反过来这类奇怪的事情,其他一些看起来很大的改动,比如改throws定义,其实都没有问题。

真的遇到问题,设身处地想想自己是那段Class A的字节码,现在还能不能跑就行。

 

感谢你看到这里,希望你只在工作里用到这些知识,祝工作愉快。

文章持续修订,转载请保留原链接:
http://calvin1978.blogcn.com/a ... .html 查看全部
最近工作里复习的Class Loader基础知识集锦,写下来希望对别人有帮助,而且不止是为了撂倒面试官。

为了尽量简单明了容易背,有些部分写得比较干。

0. 参考资料:

书:《深入了解Java虚拟机》、《实战Java虚拟机》
规范:Java语言规范 第12章
源码: OpenJDK 7的Java及C代码( Class.c , Classloader.c,jvm.cpp)

 

1. Class装载的三个阶段
1.1 载入 (Load)

从Class文件或别的什么地方载入一段二进制流字节流,把它解释成永久代里的运行时数据结构,生成一个Class对象。
同时也会装载它的父类或父接口,并进行校验,比如编译版本不对会抛出UnsupportedClassError,依赖定义不对会抛 IncompatibleClassChangeError/ClassCircularityError等。

1.2 链接 (Resolve)

将之前载入的数据结构里的符号引用表,解析成直接引用。
中间如果遇到引用的类还没被加载,就会触发该类的加载。
可能JDK会很懒惰的在运行某个函数实际使用到该引用时才发生链接,也可能在类加载时就解析全部引用。

1.3 初始化 (Initniazle)

初始化静态变量,并执行静态初始化语句。

 

2. Class装载的时机

ClassLoader.loadClass()
前文所说的链接时触发的装载
Class.forName() 等java.lang.reflect反射包
new 构造对象
初始化子类时,会同时初始化父类
访问类的静态变量或静态方法(但static final的常量除外,此君在常量池里)

本质上,也是很懒惰的按需加载的,由于类装载的Lazy和前面解释引用的Lazy,所以Jar包里有时候有些类用到的了没在Class Path里的其他类,也能人品爆发的照跑不误。

除了时机1,其他几种方式默认都到达类装载的初始化阶段。

 

3. ClassLoader.loadClass() 与 Class.forName()

ClassLoader.loadClass(String name, boolean resolve),其中resolve默认为false,即只执行类装载的第一个阶段。

Class.forName(String name, boolean initialize, ClassLoader loader), 其中initialize默认为true,即执行到类装载的第三个阶段。

 

4. ClassNotFoundException 和 NoClassDefFoundError

ClassLoader.loadClass() 与 Class.forName() 找不到类定义的二进制流时抛出ClassNotFoundException。

链接阶段解释引用失败,找不到引用的类时抛出NoClassDefFoundError。

其他常见错误包括编译版本不对的

 

5. ClassLoader及双亲委派机制

ClassLoader.loadClass()的标准流程:

findLoadedClass() 查看类是否已加载
如果不存在,则调用parent loader的loadClass()
如果不存在,调用findClass() 在本ClassLoader的ClassPath里加载该类

所谓双亲委派机制,就是先从parent loader开始查找,找不到了才用自己的findClass()函数去查找,兼顾了效率:避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次,和安全:避免子类乱加载。

而OSGI或SPI或热替换方案,则需要破坏这个双亲委托,先调用自己的findClass()。

findClass() 是各个ClassLoader各自实现,各显神通的地方,从各种奇葩地方载入Class二进制字节流。

但最后都会调用defineClass(),传入二进制字节流,返回Class对象。留意此处,呆会AspectJ的时候会回到这里。

在JDK6,loadClass()很过分的定义了方法级的synchronized ,在JDK7改成一个以Class Name作Key的 parallelLockMap,增强了并行加载不同Class的能力。

 

6. System ClassLoader 与 Thread Context Classloader

有时候,看到错误日志说张三不是张三,包名类名一样但instanceof 死活返回 false,唯一原因是它们由两个不同的ClassLoader加载。

默认的Bootstrap(加载jdk的lib目录),Extension(加载jdk的lib/ext目录),Application(加载启动时定义的classpath)三层ClassLoader机制不再重复。

平时用ClassLoader.getSystemClassLoader()就可以得到sun.misc.Launcher$ApplicationClassLoader 这个Application ClassLoader。

在类A里加载类B,默认使用加载了类A的Loader。但,也有特殊情况,比如JDBC加载driver时的机制,需要在Bootstrap ClassLoader(JDBC属于JDK一部分)里根据配置反射创建jdbc driver的数据实现类,此时Bootstrap Classloader并没有某个jdbc driver实现类的包(在Application Classloaer中),因此Sun设计了一个特殊方案 --Thread Context Class Loader。

JAXB(也是JDK一部分,比如要在Jar包里找xsd schema文件的时候)也使用了它,所以用到它们时就要注意Thread Context ClassLoader的设置,将Application Classloader设进去。可以用代码随时设置current thread的loader,也可以用自定义的ThreadFactory在创建线程时设置,它默认是父线程的loader,如果都没设置就是 System ClassLoader这个Application ClassLoader。

 

7. Java Agent机制与AspectJ的LoadTime Weaving

在JDK5开始,在启动JVM时可增加-javaagent参数,在装载Class时对类进行动态的修改。

AspectJ的Load Time Weaving机制,需要配置 -javaagent: [path to aspectj-weaver.jar] 。

打开aspectj-weaver.jar,可以看到META-INF/MANIFEST里定义了 Premain-Class: org.aspectj.weaver.loadtime.Agent

再打开这个Agent类,简化后的代码大概这个样子:

 

ClassFileTransformer s_transformer = new ClassPreProcessorAgentAdapter();

 

public static void premain(String options, Instrumentation instrumentation) {

 

instrumentation.addTransformer(s_transformer);

 

}

可见它的主要作用是将自己的类转换器注册到JDK所传入的Instrumentation。

再看类转换器的定义:ClassLoader会在前面defineClass()的过程中,在把二进制字节流转换为Class对象之前,先把二进制流和当前ClassLoader传给转换器,由转换器加工为另一段二进制字节流返回。

AspectJ就是利用传入的ClassLoader,找出其Class Path里的META-INF/aop.xml,然后根据aop.xml里的配置进行代码植入。

测试显示,加了LoadTime Weaving,类加载的速度明显变慢,如果是100ms就调用超时的服务,需要做类的预加载。

 

8. Jar包的预加载

比如有个有趣的需求是加载某个Class A所在的Jar里的全部的Class (怎么好像一点都不有趣)

 

URL jarUrl = ClassA.getProtectionDomain().getCodeSource().getLocation();
JarFile jarfile = new JarFile(jarUrl.getPath());
Enumeration entries = jarfile.entries();

然后遍历JarEntry,过滤出后缀为.class的文件,按类名进行装载就可以了。

 

9.Class的二进制兼容性

如果Class A 依赖 spring-1.0.jar编译,当spring升级到spring-2.0.jar,Class A不需要修改代码也不需要重新编译,可以直接运行的,spring-2.0.jar就满足二进制兼容性。

在Java语言规范的第13章有详细的描述 ,不想直接睡着最好可以找个中文版来看,感谢那些翻译的同学。

虽然规范的这章看着比较长比较吓人,但其实二进制兼容性还是很容易做到的,只要你不做把接口改为抽象类之类或反过来这类奇怪的事情,其他一些看起来很大的改动,比如改throws定义,其实都没有问题。

真的遇到问题,设身处地想想自己是那段Class A的字节码,现在还能不能跑就行。

 

感谢你看到这里,希望你只在工作里用到这些知识,祝工作愉快。

文章持续修订,转载请保留原链接:
http://calvin1978.blogcn.com/a ... .html

Java ThreadPool的正确打开方式

默认分类admin 发表了文章 • 0 个评论 • 154 次浏览 • 2017-01-17 16:45 • 来自相关话题

原文链接:http://calvin1978.blogcn.com/a ... .html
 线程池应对于突然增大、来不及处理的请求,无非两种应对方式:

将未完成的请求放在队列里等待
临时增加处理线程,等高峰回落后再结束临时线程

JDK的Executors.newFixedPool() 和newCachedPool(),分别使用了这两种方式。

不过,这俩函数在方便之余,也屏蔽了ThreadPool原本多样的配置,对一些不求甚解的码农来说,就错过了一些更适合自己项目的选择。

 

1. ThreadPoolExecutor的原理

经典书《Java Concurrency in Pratice(Java并发编程实战)》的第8章,浓缩如下:

1. 每次提交任务时,如果线程数还没达到coreSize就创建新线程并绑定该任务。
所以第coreSize次提交任务后线程总数必达到coreSize,不会重用之前的空闲线程。
在生产环境,为了避免首次调用超时,可以调用executor.prestartCoreThread()预创建所有core线程,避免来一个创一个带来首次调用慢的问题。

2. 线程数达到coreSize后,新增的任务就放到工作队列里,而线程池里的线程则努力的使用take()阻塞地从工作队列里拉活来干。

3. 如果队列是个有界队列,又如果线程池里的线程不能及时将任务取走,工作队列可能会满掉,插入任务就会失败,此时线程池就会紧急的再创建新的临时线程来补救。

4. 临时线程使用poll(keepAliveTime,timeUnit)来从工作队列拉活,如果时候到了仍然两手空空没拉到活,表明它太闲了,就会被解雇掉。

5. 如果core线程数+临时线程数 >maxSize,则不能再创建新的临时线程了,转头执行RejectExecutionHanlder。默认的AbortPolicy抛RejectedExecutionException异常,其他选择包括静默放弃当前任务(Discard),放弃工作队列里最老的任务(DisacardOldest),或由主线程来直接执行(CallerRuns),或你自己发挥想象力写的一个。

 

2. FixedPool 与 CachedPool

FixedPool默认用了一条无界的工作队列 LinkedBlockingQueue, 所以只去到上面的第2步就不会继续往下走了,coreSize的线程做不完的任务不断堆积到无限长的Queue中。
所以只有coreSize一个参数,其他maxSize,keepAliveTime,RejectHandler的配置都不会实际生效。

CachedPool则把coreSize设成0,然后选用了一种特殊的Queue -- SynchronousQueue,只要当前没有空闲线程,Queue就会立刻报插入失败,让线程池增加新的临时线程,默认的KeepAliveTime是1分钟,而且maxSize是整型的最大值,也就是说只要有干不完的活,都会无限增增加线程数,直到高峰过去线程数才会回落。

 

3. 对FixedPool的进一步配置
3.1 设置QueueSize

如果不想搞一条无限长的Queue,避免任务无限等待显得像假死,同时占用太多内存,可能会把它换成一条有界的ArrayBlockingQueue,那就要同时关注一下这条队列满了之后的场景,选择正确的rejectHanlder。

此时,最好还是把maxSize设为coreSize一样的值,不把临时线程及其keepAlive时间拉进来,Queue+临时线程两者结合听是好听,但很难设置好。

3.2 有界队列选LinkedBlockingQueue 还是ArrayBlockingQueue?

按Executors的JavaDoc上说是ArrayBlockingQueue,起码ArrayBlockingQueue每插入一个Runnable就直接放到内部的数组里,而LinkedBlockingQueue则要 new Node(runnable),无疑会产生更多对象。而性能方面有兴趣的同学可以自己测一下。

allowCoreThreadTimeOut(true)

允许core线程也在完全没流量时收缩到0,但因为JDK的算法,只要当前线程数低于core,请求一来就会创建线程,不管现在有没有空闲的线程能服务这个请求,所以这个选项的作用有限,仅在完全没流量时有效。 但都完全没流量了,怎么滴其实也没所谓了。除非是同时有很多个线程池的情况。

 

4. 对CachedPool的进一步配置
4.1 设置coreSize

coreSize默认为0,但很多时候也希望是一个类似FixedPool的固定值,能处理大部分的情况,不要有太多加加减减的波动,等待和消耗的精力。

4.2 设置maxSize及rejectHandler

同理,maxSize默认是整形最大值,但太多的线程也很可能让系统崩溃,所以建议还是设一下maxSize和rejectHandler。

4.3 设置keepAliveTime

默认1分钟,可以根据项目再设置一把。

4.4 SynchronousQueue的性能?

高并发下,SynchronousQueue的性能绝对比LinkedBlockingQueue/ArrayBlockingQueue低一大截。虽然JDK6的实现号称比JDK5的改进很多,但还是慢,据文章说只在20线程并发下它才是快的。

 

5. SpringSide的ThreadPoolBuilder

广告时间,SpringSide的ThreadPoolBuilder能简化上述的配置。

此文太科普太水,主要就是为了帮SpringSide-Utils项目打广告:) 查看全部
原文链接:http://calvin1978.blogcn.com/a ... .html
 线程池应对于突然增大、来不及处理的请求,无非两种应对方式:

将未完成的请求放在队列里等待
临时增加处理线程,等高峰回落后再结束临时线程

JDK的Executors.newFixedPool() 和newCachedPool(),分别使用了这两种方式。

不过,这俩函数在方便之余,也屏蔽了ThreadPool原本多样的配置,对一些不求甚解的码农来说,就错过了一些更适合自己项目的选择。

 

1. ThreadPoolExecutor的原理

经典书《Java Concurrency in Pratice(Java并发编程实战)》的第8章,浓缩如下:

1. 每次提交任务时,如果线程数还没达到coreSize就创建新线程并绑定该任务。
所以第coreSize次提交任务后线程总数必达到coreSize,不会重用之前的空闲线程。
在生产环境,为了避免首次调用超时,可以调用executor.prestartCoreThread()预创建所有core线程,避免来一个创一个带来首次调用慢的问题。

2. 线程数达到coreSize后,新增的任务就放到工作队列里,而线程池里的线程则努力的使用take()阻塞地从工作队列里拉活来干。

3. 如果队列是个有界队列,又如果线程池里的线程不能及时将任务取走,工作队列可能会满掉,插入任务就会失败,此时线程池就会紧急的再创建新的临时线程来补救。

4. 临时线程使用poll(keepAliveTime,timeUnit)来从工作队列拉活,如果时候到了仍然两手空空没拉到活,表明它太闲了,就会被解雇掉。

5. 如果core线程数+临时线程数 >maxSize,则不能再创建新的临时线程了,转头执行RejectExecutionHanlder。默认的AbortPolicy抛RejectedExecutionException异常,其他选择包括静默放弃当前任务(Discard),放弃工作队列里最老的任务(DisacardOldest),或由主线程来直接执行(CallerRuns),或你自己发挥想象力写的一个。

 

2. FixedPool 与 CachedPool

FixedPool默认用了一条无界的工作队列 LinkedBlockingQueue, 所以只去到上面的第2步就不会继续往下走了,coreSize的线程做不完的任务不断堆积到无限长的Queue中。
所以只有coreSize一个参数,其他maxSize,keepAliveTime,RejectHandler的配置都不会实际生效。

CachedPool则把coreSize设成0,然后选用了一种特殊的Queue -- SynchronousQueue,只要当前没有空闲线程,Queue就会立刻报插入失败,让线程池增加新的临时线程,默认的KeepAliveTime是1分钟,而且maxSize是整型的最大值,也就是说只要有干不完的活,都会无限增增加线程数,直到高峰过去线程数才会回落。

 

3. 对FixedPool的进一步配置
3.1 设置QueueSize

如果不想搞一条无限长的Queue,避免任务无限等待显得像假死,同时占用太多内存,可能会把它换成一条有界的ArrayBlockingQueue,那就要同时关注一下这条队列满了之后的场景,选择正确的rejectHanlder。

此时,最好还是把maxSize设为coreSize一样的值,不把临时线程及其keepAlive时间拉进来,Queue+临时线程两者结合听是好听,但很难设置好。

3.2 有界队列选LinkedBlockingQueue 还是ArrayBlockingQueue?

按Executors的JavaDoc上说是ArrayBlockingQueue,起码ArrayBlockingQueue每插入一个Runnable就直接放到内部的数组里,而LinkedBlockingQueue则要 new Node(runnable),无疑会产生更多对象。而性能方面有兴趣的同学可以自己测一下。

allowCoreThreadTimeOut(true)

允许core线程也在完全没流量时收缩到0,但因为JDK的算法,只要当前线程数低于core,请求一来就会创建线程,不管现在有没有空闲的线程能服务这个请求,所以这个选项的作用有限,仅在完全没流量时有效。 但都完全没流量了,怎么滴其实也没所谓了。除非是同时有很多个线程池的情况。

 

4. 对CachedPool的进一步配置
4.1 设置coreSize

coreSize默认为0,但很多时候也希望是一个类似FixedPool的固定值,能处理大部分的情况,不要有太多加加减减的波动,等待和消耗的精力。

4.2 设置maxSize及rejectHandler

同理,maxSize默认是整形最大值,但太多的线程也很可能让系统崩溃,所以建议还是设一下maxSize和rejectHandler。

4.3 设置keepAliveTime

默认1分钟,可以根据项目再设置一把。

4.4 SynchronousQueue的性能?

高并发下,SynchronousQueue的性能绝对比LinkedBlockingQueue/ArrayBlockingQueue低一大截。虽然JDK6的实现号称比JDK5的改进很多,但还是慢,据文章说只在20线程并发下它才是快的。

 

5. SpringSide的ThreadPoolBuilder

广告时间,SpringSide的ThreadPoolBuilder能简化上述的配置。

此文太科普太水,主要就是为了帮SpringSide-Utils项目打广告:)

StringBuilder在高性能场景下的正确用法

默认分类admin 发表了文章 • 0 个评论 • 164 次浏览 • 2017-01-17 16:44 • 来自相关话题

原文链接:http://calvin1978.blogcn.com/a ... .html
 关于StringBuilder,一般同学只简单记住了,字符串拼接要用StringBuilder,不要用+,也不要用StringBuffer,然后性能就是最好的了,真的吗吗吗吗?

还有些同学,还听过三句似是而非的经验:

1. Java编译优化后+和StringBuilder的效果一样;

2. StringBuilder不是线程安全的,为了“安全”起见最好还是用StringBuffer;

3. 永远不要自己拼接日志信息的字符串,交给slf4j来。

 

1. 初始长度好重要,值得说四次。

StringBuilder的内部有一个char[], 不断的append()就是不断的往char[]里填东西的过程。

new StringBuilder() 时char[]的默认长度是16,然后,如果要append第17个字符,怎么办?

用System.arraycopy成倍复制扩容!!!!

这样一来有数组拷贝的成本,二来原来的char[]也白白浪费了要被GC掉。可以想见,一个129字符长度的字符串,经过了16,32,64, 128四次的复制和丢弃,合共申请了496字符的数组,在高性能场景下,这几乎不能忍。

所以,合理设置一个初始值多重要。

但如果我实在估算不好呢?多估一点点好了,只要字符串最后大于16,就算浪费一点点,也比成倍的扩容好。

 

2. Liferay的StringBundler类

Liferay的StringBundler类提供了另一个长度设置的思路,它在append()的时候,不急着往char[]里塞东西,而是先拿一个String[]把它们都存起来,到了最后才把所有String的length加起来,构造一个合理长度的StringBuilder。

 

3. 但,还是浪费了一倍的char[]

浪费发生在最后一步,StringBuilder.toString()

 

//创建拷贝, 不共享数组
return new String(value, 0, count);

String的构造函数会用 System.arraycopy()复制一把传入的char[]来保证安全性不可变性,如果故事就这样结束,StringBuilder里的char[]还是被白白牺牲了。

为了不浪费这些char[],一种方法是用Unsafe之类的各种黑科技,绕过构造函数直接给String的char[]属性赋值,但很少人这样做。

另一个靠谱一些的办法就是重用StringBuilder。而重用,还解决了前面的长度设置问题,因为即使一开始估算不准,多扩容几次之后也够了。

 

4. 重用StringBuilder

这个做法来源于JDK里的BigDecimal类(没事看看JDK代码多重要),后来发现Netty也同样使用。SpringSide里将代码提取成StringBuilderHolder,里面只有一个函数

 

public StringBuilder getStringBuilder() {
sb.setLength(0);
return sb;
}

StringBuilder.setLength()函数只重置它的count指针,而char[]则会继续重用,而toString()时会把当前的count指针也作为参数传给String的构造函数,所以不用担心把超过新内容大小的旧内容也传进去了。可见,StringBuilder是完全可以被重用的。

为了避免并发冲突,这个Holder一般设为ThreadLocal,标准写法见BigDecimal或StringBuilderHolder的注释。

不过,如果String的长度不大,那从ThreadLocal里取一次值的代价还更大的多,所以也不能把这个ThreadLocalStringBuilder搞出来后,见到StringBuilder就替换。。。
 

5. + 与 StringBuilder

 

String s = “hello ” + user.getName();

这一句经过javac编译后的效果,的确等价于使用StringBuilder,但没有设定长度。

 

String s = new StringBuilder().append(“hello”).append(user.getName());

但是,如果像下面这样:

 

String s = “hello ”;
// 隔了其他一些语句
s = s + user.getName();

每一条语句,都会生成一个新的StringBuilder,这里就有了两个StringBuilder,性能就完全不一样了。如果是在循环体里s+=i; 就更加多得没谱。

据R大说,努力的JVM工程师们在运行优化阶段, 根据+XX:+OptimizeStringConcat(JDK7u40后默认打开),把相邻的(中间没隔着控制语句) StringBuilder合成一个,也会努力的猜长度。

所以,保险起见还是继续自己用StringBuilder并设定长度好了。

 

6. StringBuffer 与 StringBuilder

StringBuffer与StringBuilder都是继承于AbstractStringBuilder,唯一的区别就是StringBuffer的函数上都有synchronized关键字。

那些说StringBuffer “安全”的同学,其实你几时看过几个线程轮流append一个StringBuffer的情况???

 

7. 永远把日志的字符串拼接交给slf4j??

 

logger.info("Hello {}", user.getName());

对于不知道要不要输出的日志,交给slf4j在真的需要输出时才去拼接的确能省节约成本。

但对于一定要输出的日志,直接自己用StringBuilder拼接更快。因为看看slf4j的实现,实际上就是不断的indexof("{}"), 不断的subString(),再不断的用StringBuilder拼起来而已,没有银弹。

PS. slf4j中的StringBuilder在原始Message之外预留了50个字符,如果可变参数加起来长过50字符还是得复制扩容......而且StringBuilder也没有重用。

 

8. 小结

StringBuilder默认的写法,会为129长度的字符串拼接,合共申请625字符的数组。所以高性能的场景下,永远要考虑用一个ThreadLocal 可重用的StringBuilder。而且重用之后,就不用再玩猜长度的游戏了。当然,如果字符串只有一百几十字节,也不一定要考虑重用,设好初始值就好。 查看全部
原文链接:http://calvin1978.blogcn.com/a ... .html
 关于StringBuilder,一般同学只简单记住了,字符串拼接要用StringBuilder,不要用+,也不要用StringBuffer,然后性能就是最好的了,真的吗吗吗吗?

还有些同学,还听过三句似是而非的经验:

1. Java编译优化后+和StringBuilder的效果一样;

2. StringBuilder不是线程安全的,为了“安全”起见最好还是用StringBuffer;

3. 永远不要自己拼接日志信息的字符串,交给slf4j来。

 

1. 初始长度好重要,值得说四次。

StringBuilder的内部有一个char[], 不断的append()就是不断的往char[]里填东西的过程。

new StringBuilder() 时char[]的默认长度是16,然后,如果要append第17个字符,怎么办?

用System.arraycopy成倍复制扩容!!!!

这样一来有数组拷贝的成本,二来原来的char[]也白白浪费了要被GC掉。可以想见,一个129字符长度的字符串,经过了16,32,64, 128四次的复制和丢弃,合共申请了496字符的数组,在高性能场景下,这几乎不能忍。

所以,合理设置一个初始值多重要。

但如果我实在估算不好呢?多估一点点好了,只要字符串最后大于16,就算浪费一点点,也比成倍的扩容好。

 

2. Liferay的StringBundler类

Liferay的StringBundler类提供了另一个长度设置的思路,它在append()的时候,不急着往char[]里塞东西,而是先拿一个String[]把它们都存起来,到了最后才把所有String的length加起来,构造一个合理长度的StringBuilder。

 

3. 但,还是浪费了一倍的char[]

浪费发生在最后一步,StringBuilder.toString()

 

//创建拷贝, 不共享数组
return new String(value, 0, count);

String的构造函数会用 System.arraycopy()复制一把传入的char[]来保证安全性不可变性,如果故事就这样结束,StringBuilder里的char[]还是被白白牺牲了。

为了不浪费这些char[],一种方法是用Unsafe之类的各种黑科技,绕过构造函数直接给String的char[]属性赋值,但很少人这样做。

另一个靠谱一些的办法就是重用StringBuilder。而重用,还解决了前面的长度设置问题,因为即使一开始估算不准,多扩容几次之后也够了。

 

4. 重用StringBuilder

这个做法来源于JDK里的BigDecimal类(没事看看JDK代码多重要),后来发现Netty也同样使用。SpringSide里将代码提取成StringBuilderHolder,里面只有一个函数

 

public StringBuilder getStringBuilder() {
sb.setLength(0);
return sb;
}

StringBuilder.setLength()函数只重置它的count指针,而char[]则会继续重用,而toString()时会把当前的count指针也作为参数传给String的构造函数,所以不用担心把超过新内容大小的旧内容也传进去了。可见,StringBuilder是完全可以被重用的。

为了避免并发冲突,这个Holder一般设为ThreadLocal,标准写法见BigDecimal或StringBuilderHolder的注释。

不过,如果String的长度不大,那从ThreadLocal里取一次值的代价还更大的多,所以也不能把这个ThreadLocalStringBuilder搞出来后,见到StringBuilder就替换。。。
 

5. + 与 StringBuilder

 

String s = “hello ” + user.getName();

这一句经过javac编译后的效果,的确等价于使用StringBuilder,但没有设定长度。

 

String s = new StringBuilder().append(“hello”).append(user.getName());

但是,如果像下面这样:

 

String s = “hello ”;
// 隔了其他一些语句
s = s + user.getName();

每一条语句,都会生成一个新的StringBuilder,这里就有了两个StringBuilder,性能就完全不一样了。如果是在循环体里s+=i; 就更加多得没谱。

据R大说,努力的JVM工程师们在运行优化阶段, 根据+XX:+OptimizeStringConcat(JDK7u40后默认打开),把相邻的(中间没隔着控制语句) StringBuilder合成一个,也会努力的猜长度。

所以,保险起见还是继续自己用StringBuilder并设定长度好了。

 

6. StringBuffer 与 StringBuilder

StringBuffer与StringBuilder都是继承于AbstractStringBuilder,唯一的区别就是StringBuffer的函数上都有synchronized关键字。

那些说StringBuffer “安全”的同学,其实你几时看过几个线程轮流append一个StringBuffer的情况???

 

7. 永远把日志的字符串拼接交给slf4j??

 

logger.info("Hello {}", user.getName());

对于不知道要不要输出的日志,交给slf4j在真的需要输出时才去拼接的确能省节约成本。

但对于一定要输出的日志,直接自己用StringBuilder拼接更快。因为看看slf4j的实现,实际上就是不断的indexof("{}"), 不断的subString(),再不断的用StringBuilder拼起来而已,没有银弹。

PS. slf4j中的StringBuilder在原始Message之外预留了50个字符,如果可变参数加起来长过50字符还是得复制扩容......而且StringBuilder也没有重用。

 

8. 小结

StringBuilder默认的写法,会为129长度的字符串拼接,合共申请625字符的数组。所以高性能的场景下,永远要考虑用一个ThreadLocal 可重用的StringBuilder。而且重用之后,就不用再玩猜长度的游戏了。当然,如果字符串只有一百几十字节,也不一定要考虑重用,设好初始值就好。

另一份Java应用调优指南之-前菜

默认分类admin 发表了文章 • 0 个评论 • 121 次浏览 • 2017-01-17 11:46 • 来自相关话题

原文链接:http://calvin1978.blogcn.com/articles/perf-tunning-1.html
 
每一次成功的调优,都会诞生又一份的调优指南。

一些必须写在前面的军规,虽然与Java应用的调优没直接关联,但是测试同学经常不留神的地方,导致应用的优化变成一场测试环境调优的戏码。
 

1 独占你的测试机器

包括跑JMeter的那些机器。

"top"或者"pidstat 1" 看一下,其他的路人甲乙丙丁的应用都关干净了没。

如果是云主机,确保更多的占有宿主机的资源,比如深夜大家下班了你在家连VPN回来跑。

 

2 了解你的测试机器

必须完完全全的了解你的机器,才知道有没卡在某个瓶颈,或者与线上环境、其他测试结果的比较。

还是那句, 包括跑JMeter的那些机器。

 

2.1 CPU

"cat /proc/cpuinfo", 看最后一条就好,比如

 

processor : 23
model name : Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz
physical id : 1
cpu cores : 6

所有数字都从零开始,physical id:1即两颗cpu, cpu core: 6即6核,processor : 23即24个处理器。

 

2 CPU * 6 Core * 2HT(Intel超线程技术) = 24 Processor

不过也有很多同事喜欢说24核,也懒得纠正了。

 

2.2 内存

"free -g" 没什么好说的。

 

2.3 硬盘

查看大小、分区、文件系统类型: "df -hT"
硬盘是否SCSI:/dev/sdX就是scsi的,hdX就是普通的。
硬盘是否SSD : "cat /sys/block/sda/queue/rotational", 0是SSD,1是传统硬盘,但也不一定灵

普通硬盘的写速度大概100M/s,RAID级别的查看不方便,SSD的速度也不定,所以用dd测一下最靠谱:

 

dd if=/dev/zero of=dd.file bs=8k count=128k conv=fdatasync
dd if=/dev/zero of=./dd.file bs=1G count=1 conv=fdatasync

上面命令测试了分别以每次8k和1g的大小,写入1g文件的速度。

if:输入文件名, /dev/zero 设备无穷尽地提供0
of:输出文件名
bs:块大小
count:次数
conv=fdatasync :实际写盘,而不是写入Page Cache

硬盘读速度的测试同理,不过要先清理缓存,否则直接从Page Cache读了。

 

sh -c "sync && echo 3 > /proc/sys/vm/drop_caches”
dd if=./dd.file of=/dev/null bs=8k

 

2.4 网卡

先用ifconfig看看有多少块网卡和bonding。bonding是个很棒的东西,可以把多块网卡绑起来,突破单块网卡的带宽限制。

然后检查每块网卡的速度,比如"ethtool eth0"。

再检查bonding,比如"cat /proc/net/bonding/bond0", 留意其Bonding Mode是负载均衡的,再留意其捆绑的网卡的速度。

最后检查测试客户机与服务机之间的带宽,先简单ping或traceroute 一下得到RTT时间,iperf之类的可稍后。

 

2.5 操作系统

Linux的内核版本,是否64位: "uname -a"
Redhat/CentOS版本 : "cat /etc/redhat-release"

 

3. 布置好你的机器状态采集工具

实时观察的,我喜欢dstat,详见《从dstat理解Linux性能监控体系》比vmstat,iostat, sar们都好用,起码对得够齐,单位能自动转换。不过dstat需要安装(yum install dstat,或者去它的网站下载解压即用版)

dstat -tamp:推荐,打印时间戳,比默认打印多一个memory信息,一个有否进程在block状态的信息
dstat -tamN bond0,lo: 如果有bonding,dstat会把bond0和eth0 有时会算双份,还有lo的也算到总量里,所以先用-N指定网卡检查下。

参考资料:后台性能测试不可不知的二三事

 

4. JMeter的调优顶一半的事

JMeter的版本越新越好。

 

4.1 JMeter的JVM参数

它默认连个垃圾收集算法都没有配,对延时要求高的,必须配上CMS或G1,内存也整大点降低GC的频率。其他的,给Server配的啥参数,给JMeter也来上一份。如果想动态改的,不写死在脚本里,可以配置环境变量$JVM_ARGS

 

4.2 测试计划的编写

什么if 语句,以及所有其实用动态语言来实现的都挺慢的。
xPath抽取结果集里的字段之类看着就慢的也别写了。
别加任何监听器和图形。
再配置输出日志的格式,能不要的列都别要了,最极端的其实就延时这列有用。

 

4.3 JMeter的运行

在Linux上用命令行跑,别偷懒用Window开着界面跑。
别开超过200条线程(虚拟机上更少)。
可以在不同机器上起多个JMeter,用集群汇总的模式。

 

4.4 结果的统计

初始连接,Server端热身,JVM编译热点方法等都需要时间,所以建议统计前删掉前面的一些日志。

要配置一下才能看到99.9%, 99.99% 分位数的延时,另外因为之前输出日志时省略了很多列,导入日志的时候配置也要如此。

但如果不能XWindows Forward,还要把日志下载回来再导入本地的JMeter,那还不如自己动动手,用sed, awk, sort配合一下自己写个分析延时的脚本直接在服务器上运行。

 

5. 其他被依赖的节点

比如所依赖的数据库够不够快,Restful API的模拟器够不够快,负载均衡器如HAProxy优化配置了没有。

  查看全部
原文链接:http://calvin1978.blogcn.com/articles/perf-tunning-1.html
 
每一次成功的调优,都会诞生又一份的调优指南。

一些必须写在前面的军规,虽然与Java应用的调优没直接关联,但是测试同学经常不留神的地方,导致应用的优化变成一场测试环境调优的戏码。
 

1 独占你的测试机器

包括跑JMeter的那些机器。

"top"或者"pidstat 1" 看一下,其他的路人甲乙丙丁的应用都关干净了没。

如果是云主机,确保更多的占有宿主机的资源,比如深夜大家下班了你在家连VPN回来跑。

 

2 了解你的测试机器

必须完完全全的了解你的机器,才知道有没卡在某个瓶颈,或者与线上环境、其他测试结果的比较。

还是那句, 包括跑JMeter的那些机器。

 

2.1 CPU

"cat /proc/cpuinfo", 看最后一条就好,比如

 

processor : 23
model name : Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz
physical id : 1
cpu cores : 6

所有数字都从零开始,physical id:1即两颗cpu, cpu core: 6即6核,processor : 23即24个处理器。

 

2 CPU * 6 Core * 2HT(Intel超线程技术) = 24 Processor

不过也有很多同事喜欢说24核,也懒得纠正了。

 

2.2 内存

"free -g" 没什么好说的。

 

2.3 硬盘

查看大小、分区、文件系统类型: "df -hT"
硬盘是否SCSI:/dev/sdX就是scsi的,hdX就是普通的。
硬盘是否SSD : "cat /sys/block/sda/queue/rotational", 0是SSD,1是传统硬盘,但也不一定灵

普通硬盘的写速度大概100M/s,RAID级别的查看不方便,SSD的速度也不定,所以用dd测一下最靠谱:

 

dd if=/dev/zero of=dd.file bs=8k count=128k conv=fdatasync
dd if=/dev/zero of=./dd.file bs=1G count=1 conv=fdatasync

上面命令测试了分别以每次8k和1g的大小,写入1g文件的速度。

if:输入文件名, /dev/zero 设备无穷尽地提供0
of:输出文件名
bs:块大小
count:次数
conv=fdatasync :实际写盘,而不是写入Page Cache

硬盘读速度的测试同理,不过要先清理缓存,否则直接从Page Cache读了。

 

sh -c "sync && echo 3 > /proc/sys/vm/drop_caches”
dd if=./dd.file of=/dev/null bs=8k

 

2.4 网卡

先用ifconfig看看有多少块网卡和bonding。bonding是个很棒的东西,可以把多块网卡绑起来,突破单块网卡的带宽限制。

然后检查每块网卡的速度,比如"ethtool eth0"。

再检查bonding,比如"cat /proc/net/bonding/bond0", 留意其Bonding Mode是负载均衡的,再留意其捆绑的网卡的速度。

最后检查测试客户机与服务机之间的带宽,先简单ping或traceroute 一下得到RTT时间,iperf之类的可稍后。

 

2.5 操作系统

Linux的内核版本,是否64位: "uname -a"
Redhat/CentOS版本 : "cat /etc/redhat-release"

 

3. 布置好你的机器状态采集工具

实时观察的,我喜欢dstat,详见《从dstat理解Linux性能监控体系》比vmstat,iostat, sar们都好用,起码对得够齐,单位能自动转换。不过dstat需要安装(yum install dstat,或者去它的网站下载解压即用版)

dstat -tamp:推荐,打印时间戳,比默认打印多一个memory信息,一个有否进程在block状态的信息
dstat -tamN bond0,lo: 如果有bonding,dstat会把bond0和eth0 有时会算双份,还有lo的也算到总量里,所以先用-N指定网卡检查下。

参考资料:后台性能测试不可不知的二三事

 

4. JMeter的调优顶一半的事

JMeter的版本越新越好。

 

4.1 JMeter的JVM参数

它默认连个垃圾收集算法都没有配,对延时要求高的,必须配上CMS或G1,内存也整大点降低GC的频率。其他的,给Server配的啥参数,给JMeter也来上一份。如果想动态改的,不写死在脚本里,可以配置环境变量$JVM_ARGS

 

4.2 测试计划的编写

什么if 语句,以及所有其实用动态语言来实现的都挺慢的。
xPath抽取结果集里的字段之类看着就慢的也别写了。
别加任何监听器和图形。
再配置输出日志的格式,能不要的列都别要了,最极端的其实就延时这列有用。

 

4.3 JMeter的运行

在Linux上用命令行跑,别偷懒用Window开着界面跑。
别开超过200条线程(虚拟机上更少)。
可以在不同机器上起多个JMeter,用集群汇总的模式。

 

4.4 结果的统计

初始连接,Server端热身,JVM编译热点方法等都需要时间,所以建议统计前删掉前面的一些日志。

要配置一下才能看到99.9%, 99.99% 分位数的延时,另外因为之前输出日志时省略了很多列,导入日志的时候配置也要如此。

但如果不能XWindows Forward,还要把日志下载回来再导入本地的JMeter,那还不如自己动动手,用sed, awk, sort配合一下自己写个分析延时的脚本直接在服务器上运行。

 

5. 其他被依赖的节点

比如所依赖的数据库够不够快,Restful API的模拟器够不够快,负载均衡器如HAProxy优化配置了没有。

 

另一份Java应用调优指南之-工具篇

默认分类admin 发表了文章 • 0 个评论 • 160 次浏览 • 2017-01-17 11:39 • 来自相关话题

Java应用的调优,再不写都要忘光了,先对付着写完,免费的JMC真的好用,大家越早用上越好。

前一篇是三个月前的 另一份Java应用调优指南 - 前菜

 

1. 土法调优两大件

先忆苦思甜,一般人在没有Profile工具的时候,调优的两大件,无非Heap Dump 与 Thread Dump。

 

1.1 Heap Dump

 

jmap -dump:live,format=b,file=heap.hprof pid

从安全点日志看,从Heap Dump开始,整个JVM都是停顿的,考虑到IO(虽是写到Page Cache,但或许会遇到background flush),几G的Heap可能产生几秒的停顿,在生产环境上执行时谨慎再谨慎。

live的选项,实际上是产生一次Full GC来保证只看还存活的对象。有时候也会故意不加live选项,看历史对象。

Dump出来的文件建议用JDK自带的VisualVM或Eclipse的MAT插件打开,对象的大小有两种统计方式:

本身大小(Shallow Size):对象本来的大小。
保留大小(Retained Size): 当前对象大小 + 当前对象直接或间接引用到的对象的大小总和。

看本身大小时,占大头的都是char[] ,byte[]之类的,没什么意思(用jmap -histo:live pid 看的也是本身大小)。所以需要关心的是保留大小比较大的对象,看谁在引用这些char[], byte[]。

(MAT能看的信息更多,但VisualVM胜在JVM自带,用法如下:命令行输入jvisualvm,文件->装入->堆Dump->检查 -> 查找20保留大小最大的对象,就会触发保留大小的计算,然后就可以类视图里浏览,按保留大小排序了)
 

1.2 Thread Dump

ThreadDump 同样会造成JVM停顿,在生产系统上执行要谨慎。

可以命令行方式执行它,"jstack pid” 或"jstack -l pid" ,其中-l 会同时打印各种lock,但会使得JVM停顿得长久得多(可能会差很多倍,比如普通的jstack可能几毫秒和一次GC没区别,加了-l 就是近一秒的时间),-l 建议不要用。

另一种是直接用代码来打印,比如线程池满了无法添加新任务,在开发或性能测试模式下,可以在代码里捕捉该异常后直接把当前线程池的情况打印出来。

 

ThreadMXBean threadMBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMBean.dumpAllThreads(false, false);

同样注意,这里threadMBean.dumpAllThreads(false,false)的参数为false,把参数改为true,则打印synchronizers与monitor,同样使得JVM停顿久很多。

线程状态:

RUNNABLE: 运行中状态,可能里面还能看到locked字样,表明它获得了某把锁。
BLOCKED:被某个锁(synchronizers)給block住了。
WAITING:等待某个condition或monitor发生,一般停留在park(), wait(), sleep(),join() 等语句里。
TIME_WAITING:和WAITING的区别是wait() 等语句加上了时间限制 wait(timeout)。

分析工具:

IBM Thread and Monitor Dump Analyze for Java 一个小巧的Jar包,能方便的按状态,线程名称,线程停留的函数排序,快速浏览。
http://spotify.github.io/threaddump-analyzer Spotify提供的Web版在线分析工具,可以将锁或条件相关联的线程聚合到一起。

 

2. 你真正要的Java Mission Control

如果你使用过JProfiler,Yourkit,VisualVM还有Eclipse的Profiler插件等一堆Profiler工具,或者用JavaSimion等在代码里打印过metrics,最后会发现免费的JMC才是你想要的。
 

2.1 优点

代替收费的JProfiler的好东西,以前Bea JRockit的宝贝,现在随着JDK7 up40以后的版本免费自带了,不过只在开发环境免费,就是说理论上不能拿它来连生产环境的机器。

另一个让人开心的事情就是JMC采用采样,而不是传统的代码植入的技术,对应用性能的影响非常非常小,完全可以开着JMC来做压测(唯一影响可能是full gc多了,减少一些监控项看看)。不会像以前,开了代码植入型的Profiler,出来的性能测试结果差了一个数量级不说,热点完全可能是错误的,这是一个真实的故事,具体细节就不说了。
 

2.2 功能

JMC里可以看的东西太多了,自己觉得最有用的如下:

内存Tab:分配Tab里的按类、按线程、对象的创建调用栈来归类的对象创建情况,让对象创建无处躲藏。
内存Tab:GC Tab的GC详细情况,以及TLAB外的分配情况(每条线程在Heap里分了一个Thread Local Area,在TLAB里的内存分配不需要线程竞争,所以TLAB之外的分配是不好的)
代码Tab:热点方法类及它的调用栈,超有用的功能。调用树是从线程角度看的方法调用,而按包名分类可以看3PP包的问题。
线程Tab:热点线程,再换个姿势来看热点方法和调用树。
线程Tab:争用,等待时间,锁定实例等。

 

2.3 使用方法简述

JDK7在启动服务时加上-XX:+UnlockCommercialFeatures -XX:+FlightRecorder 。
如果是远程服务器,要开JMX:

 

“-Dcom.sun.management.jmxremote.port=7001 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=your ip”

JDK自带的jmc命令,文件->连接->设定JMX连接,启动飞行纪录,固定时间选1分钟或更多,事件设置选为profiling,然后进一步修改,自己查看下都Profile了哪些信息,觉得不够的再添加些(下次就直接用上次设定就好了),比如:

加上对象数量的统计:Java Virtual Machine->GC->Detail->Object Count/Object Count after GC
方法调用采样的间隔从10ms改为1ms(但不能低于1ms,否则会影响性能了): Java Virtual Machine->Profiling下的两个选项
Socket与File采样, 10ms太久,但即使改为1ms也未必能抓住什么,可以干脆取消掉: Java Application->File Read/FileWrite/Socket Read/Socket Write

然后就开始Profile,到时间后Profile结束,会自动把记录下载回来,在JMC中展示。

其他资料:

java-performance.info上的介绍文章
JMC作者的博客
JMC官网

 

3. BTrace

神器,在生产环境上,动态监控某些方法的执行时长及其他信息,不再需要自己手工打日志,发版本,部署与重启服务。

据说淘宝就是经常开着BTrace在线上找问题的,我们最近也在生产上试了几把,太爽利了。

使用方法网上一搜大把,就不重复了。

原理就是自己写一个类似AspectJ的,希望监控哪个方法,监控后做什么动作的脚本,然后动态执行btrace命令将这个脚本attach到某个JVM上就行

 
这是一篇严肃的文章,就不配图了。 查看全部
Java应用的调优,再不写都要忘光了,先对付着写完,免费的JMC真的好用,大家越早用上越好。

前一篇是三个月前的 另一份Java应用调优指南 - 前菜

 

1. 土法调优两大件

先忆苦思甜,一般人在没有Profile工具的时候,调优的两大件,无非Heap Dump 与 Thread Dump。

 

1.1 Heap Dump

 

jmap -dump:live,format=b,file=heap.hprof pid

从安全点日志看,从Heap Dump开始,整个JVM都是停顿的,考虑到IO(虽是写到Page Cache,但或许会遇到background flush),几G的Heap可能产生几秒的停顿,在生产环境上执行时谨慎再谨慎。

live的选项,实际上是产生一次Full GC来保证只看还存活的对象。有时候也会故意不加live选项,看历史对象。

Dump出来的文件建议用JDK自带的VisualVM或Eclipse的MAT插件打开,对象的大小有两种统计方式:

本身大小(Shallow Size):对象本来的大小。
保留大小(Retained Size): 当前对象大小 + 当前对象直接或间接引用到的对象的大小总和。

看本身大小时,占大头的都是char[] ,byte[]之类的,没什么意思(用jmap -histo:live pid 看的也是本身大小)。所以需要关心的是保留大小比较大的对象,看谁在引用这些char[], byte[]。

(MAT能看的信息更多,但VisualVM胜在JVM自带,用法如下:命令行输入jvisualvm,文件->装入->堆Dump->检查 -> 查找20保留大小最大的对象,就会触发保留大小的计算,然后就可以类视图里浏览,按保留大小排序了)
 

1.2 Thread Dump

ThreadDump 同样会造成JVM停顿,在生产系统上执行要谨慎。

可以命令行方式执行它,"jstack pid” 或"jstack -l pid" ,其中-l 会同时打印各种lock,但会使得JVM停顿得长久得多(可能会差很多倍,比如普通的jstack可能几毫秒和一次GC没区别,加了-l 就是近一秒的时间),-l 建议不要用。

另一种是直接用代码来打印,比如线程池满了无法添加新任务,在开发或性能测试模式下,可以在代码里捕捉该异常后直接把当前线程池的情况打印出来。

 

ThreadMXBean threadMBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMBean.dumpAllThreads(false, false);

同样注意,这里threadMBean.dumpAllThreads(false,false)的参数为false,把参数改为true,则打印synchronizers与monitor,同样使得JVM停顿久很多。

线程状态:

RUNNABLE: 运行中状态,可能里面还能看到locked字样,表明它获得了某把锁。
BLOCKED:被某个锁(synchronizers)給block住了。
WAITING:等待某个condition或monitor发生,一般停留在park(), wait(), sleep(),join() 等语句里。
TIME_WAITING:和WAITING的区别是wait() 等语句加上了时间限制 wait(timeout)。

分析工具:

IBM Thread and Monitor Dump Analyze for Java 一个小巧的Jar包,能方便的按状态,线程名称,线程停留的函数排序,快速浏览。
http://spotify.github.io/threaddump-analyzer Spotify提供的Web版在线分析工具,可以将锁或条件相关联的线程聚合到一起。

 

2. 你真正要的Java Mission Control

如果你使用过JProfiler,Yourkit,VisualVM还有Eclipse的Profiler插件等一堆Profiler工具,或者用JavaSimion等在代码里打印过metrics,最后会发现免费的JMC才是你想要的。
 

2.1 优点

代替收费的JProfiler的好东西,以前Bea JRockit的宝贝,现在随着JDK7 up40以后的版本免费自带了,不过只在开发环境免费,就是说理论上不能拿它来连生产环境的机器。

另一个让人开心的事情就是JMC采用采样,而不是传统的代码植入的技术,对应用性能的影响非常非常小,完全可以开着JMC来做压测(唯一影响可能是full gc多了,减少一些监控项看看)。不会像以前,开了代码植入型的Profiler,出来的性能测试结果差了一个数量级不说,热点完全可能是错误的,这是一个真实的故事,具体细节就不说了。
 

2.2 功能

JMC里可以看的东西太多了,自己觉得最有用的如下:

内存Tab:分配Tab里的按类、按线程、对象的创建调用栈来归类的对象创建情况,让对象创建无处躲藏。
内存Tab:GC Tab的GC详细情况,以及TLAB外的分配情况(每条线程在Heap里分了一个Thread Local Area,在TLAB里的内存分配不需要线程竞争,所以TLAB之外的分配是不好的)
代码Tab:热点方法类及它的调用栈,超有用的功能。调用树是从线程角度看的方法调用,而按包名分类可以看3PP包的问题。
线程Tab:热点线程,再换个姿势来看热点方法和调用树。
线程Tab:争用,等待时间,锁定实例等。

 

2.3 使用方法简述

JDK7在启动服务时加上-XX:+UnlockCommercialFeatures -XX:+FlightRecorder 。
如果是远程服务器,要开JMX:

 

“-Dcom.sun.management.jmxremote.port=7001 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=your ip”

JDK自带的jmc命令,文件->连接->设定JMX连接,启动飞行纪录,固定时间选1分钟或更多,事件设置选为profiling,然后进一步修改,自己查看下都Profile了哪些信息,觉得不够的再添加些(下次就直接用上次设定就好了),比如:

加上对象数量的统计:Java Virtual Machine->GC->Detail->Object Count/Object Count after GC
方法调用采样的间隔从10ms改为1ms(但不能低于1ms,否则会影响性能了): Java Virtual Machine->Profiling下的两个选项
Socket与File采样, 10ms太久,但即使改为1ms也未必能抓住什么,可以干脆取消掉: Java Application->File Read/FileWrite/Socket Read/Socket Write

然后就开始Profile,到时间后Profile结束,会自动把记录下载回来,在JMC中展示。

其他资料:

java-performance.info上的介绍文章
JMC作者的博客
JMC官网

 

3. BTrace

神器,在生产环境上,动态监控某些方法的执行时长及其他信息,不再需要自己手工打日志,发版本,部署与重启服务。

据说淘宝就是经常开着BTrace在线上找问题的,我们最近也在生产上试了几把,太爽利了。

使用方法网上一搜大把,就不重复了。

原理就是自己写一个类似AspectJ的,希望监控哪个方法,监控后做什么动作的脚本,然后动态执行btrace命令将这个脚本attach到某个JVM上就行

 
这是一篇严肃的文章,就不配图了。

站在岸上看DEVOPS

默认分类admin 发表了文章 • 0 个评论 • 129 次浏览 • 2017-01-17 17:41 • 来自相关话题

今年项目要试水(X)AAS,尝试运维自己的产品,便离不开这几年最热的单词—DEVOPS。趁还在岸上,理解下DEVOPS对开发人员来说意味着什么。

因为没有实战,也就纸上谈兵一下,真正有经验的人, @连城404:为了分摊黑锅,运维发明了devops和full stack。

What is DEVOPS ?

十年前,敏捷出现,以反瀑布流程的方式模糊了需求、架构、开发、测试之间的界限,但对整个软件价值链的最后一环,交付与运维并没有太多的强调,造成一个有趣的现象是,产品以三周为迭代进行开发并交付给客户,但只充当与客户演示并收集反馈的媒介,而客户仍然会有自己独立的上线周期。

十年后,越来越多的互联网站与手机应用,需要频繁而持续的发布,每次发布需要部署到大量的机器集群上,需要不中断和破坏当前服务,在发布前能提供本次发布质量的证明,在发布后对系统有实时的监控,支持错误转移、服务降级,支持快速定位和修复故障。

显然,只靠传统运维人员的知识技能集(操作系统、网络、Shell脚本编写) 以及开发人员编写的的产品管理手册已不足以满足上述的需求了。好在,有敏捷的先例,解决方法是现成的——把开发拖下水吧:

1. 人员上,运维人员要更懂产品的架构与内在,而不只是那份管理员手册。开发人员要懂实际的运维,从实际部署、上线流程,到故障的定位与解决。当然,最重要是打通两个部门间的隔阂。

2. 产品特性上,运维所需的功能甚至运维的基础设施,成为了产品非功能性需求的重要一部分。

3. 流程上,产品交付与运维,接入整个软件生产周期构成一个完整的环。
 

What should developer do?

现在大家最关心的问题来了,DEVOPS的大旗下,开发人员到底要参与多少运维?

这幅图,又是零到一百之间的选择,各个组织根据自己的人员、技术,文化,找到自己最合适的那个平衡点。

理论上,开发人员在DEVOPS中可以有两种的参与:

一,为运维而开发。

比如,在代码中加入Metrics收集的代码,向Graphite吐出服务的调用次数与延时,通常是与运维协作的第一步——运维可以在CPU,内存占用与应用日志之外,不那么黑盒的看待应用了。

又比如,要和运维商量着,在代码端需要做些什么,才能实现监控,无中断升级,动态节点扩展,故障转移,服务降级,跨机房容灾。这些功能以前可能也是要的,但现在需求的采集更注重实效。甚至,运维的一些基础设施,也可以由开发代劳了。

又比如,环境与流程的统一,原来只在简版的集成测试环境上跑完功能测试就完事的开发闭环,现在需要考虑至少延伸到一台从服务器配置到数据库部署模型等都贴近生产环境的Staging服务器,又比如测试环境的安装部署,也会用和运维一样的基于Ansible 或 Saltstack。

二,开发直接参与运维。

也是最存在争议的地方,比如系统的上线升级,日常的运维操作,故障的即时修复。

开发人员参与运维,可以吃自己的狗粮,可以更深切的了解运维功能的需求,开发人员的故障修复更快,但是——

程序员是不是应该做更有创造性的事情?程序员做运维会不会感到厌烦?社会化分工还是不是一个无需争辩的现代社会真理?

其实类似的争论已发生过一次,在开发与测试的分工上。起码在我司,专门的测试人员仍然有不可动摇的作用,而开发人员,则承担了从持续的单元测试和功能测试,最后到手工的性能测试与稳定性测试,总体80%以上的测试工作量,很自然的合作。

所以,还是没有绝对,还是各个组织自己去通过不断的尝试,找到自己最合适的那个平衡点。 查看全部
今年项目要试水(X)AAS,尝试运维自己的产品,便离不开这几年最热的单词—DEVOPS。趁还在岸上,理解下DEVOPS对开发人员来说意味着什么。

因为没有实战,也就纸上谈兵一下,真正有经验的人, @连城404:为了分摊黑锅,运维发明了devops和full stack。

What is DEVOPS ?

十年前,敏捷出现,以反瀑布流程的方式模糊了需求、架构、开发、测试之间的界限,但对整个软件价值链的最后一环,交付与运维并没有太多的强调,造成一个有趣的现象是,产品以三周为迭代进行开发并交付给客户,但只充当与客户演示并收集反馈的媒介,而客户仍然会有自己独立的上线周期。

十年后,越来越多的互联网站与手机应用,需要频繁而持续的发布,每次发布需要部署到大量的机器集群上,需要不中断和破坏当前服务,在发布前能提供本次发布质量的证明,在发布后对系统有实时的监控,支持错误转移、服务降级,支持快速定位和修复故障。

显然,只靠传统运维人员的知识技能集(操作系统、网络、Shell脚本编写) 以及开发人员编写的的产品管理手册已不足以满足上述的需求了。好在,有敏捷的先例,解决方法是现成的——把开发拖下水吧:

1. 人员上,运维人员要更懂产品的架构与内在,而不只是那份管理员手册。开发人员要懂实际的运维,从实际部署、上线流程,到故障的定位与解决。当然,最重要是打通两个部门间的隔阂。

2. 产品特性上,运维所需的功能甚至运维的基础设施,成为了产品非功能性需求的重要一部分。

3. 流程上,产品交付与运维,接入整个软件生产周期构成一个完整的环。
 

What should developer do?

现在大家最关心的问题来了,DEVOPS的大旗下,开发人员到底要参与多少运维?

这幅图,又是零到一百之间的选择,各个组织根据自己的人员、技术,文化,找到自己最合适的那个平衡点。

理论上,开发人员在DEVOPS中可以有两种的参与:

一,为运维而开发。

比如,在代码中加入Metrics收集的代码,向Graphite吐出服务的调用次数与延时,通常是与运维协作的第一步——运维可以在CPU,内存占用与应用日志之外,不那么黑盒的看待应用了。

又比如,要和运维商量着,在代码端需要做些什么,才能实现监控,无中断升级,动态节点扩展,故障转移,服务降级,跨机房容灾。这些功能以前可能也是要的,但现在需求的采集更注重实效。甚至,运维的一些基础设施,也可以由开发代劳了。

又比如,环境与流程的统一,原来只在简版的集成测试环境上跑完功能测试就完事的开发闭环,现在需要考虑至少延伸到一台从服务器配置到数据库部署模型等都贴近生产环境的Staging服务器,又比如测试环境的安装部署,也会用和运维一样的基于Ansible 或 Saltstack。

二,开发直接参与运维。

也是最存在争议的地方,比如系统的上线升级,日常的运维操作,故障的即时修复。

开发人员参与运维,可以吃自己的狗粮,可以更深切的了解运维功能的需求,开发人员的故障修复更快,但是——

程序员是不是应该做更有创造性的事情?程序员做运维会不会感到厌烦?社会化分工还是不是一个无需争辩的现代社会真理?

其实类似的争论已发生过一次,在开发与测试的分工上。起码在我司,专门的测试人员仍然有不可动摇的作用,而开发人员,则承担了从持续的单元测试和功能测试,最后到手工的性能测试与稳定性测试,总体80%以上的测试工作量,很自然的合作。

所以,还是没有绝对,还是各个组织自己去通过不断的尝试,找到自己最合适的那个平衡点。

String.intern() 祛魅

默认分类admin 发表了文章 • 0 个评论 • 153 次浏览 • 2017-01-17 17:31 • 来自相关话题

String.intern(),写应用的同学不知道也没什么损失。但知道的同学,如果只觉得它很省内存,用==比较字符串很酷,或者期待JDK会做什么神奇的事情,然后恨不得所有地方都用上String.intern(),那很可能会误用,所以把上周在群里的讨论整理成此文。

在社会科学中,祛魅(Disenchantment)是指在现代社会中消去神秘主义(魅惑力)的表面并把文化合理化。这里乱用一下。

 

1. String.itern()的基本原理

详细可看占小狼同学的《浅谈Java String内幕(2)》

String.intern()是一个Native方法,底层调用C++的 StringTable::intern 方法,源码注释:当调用 intern 方法时,如果常量池中已经该字符串,则返回池中的字符串;否则将此字符串添加到常量池中,并返回字符串的引用。

所以明面上,它有两大好处,一是重复的字符串,会用同一个引用代替;二是字符串比较,不再需要逐个字符的equals()比较,而用==对比引用是否相同即可。

 

2. 省内存效果只对长期存在的字符串有效

String.intern()没有神奇的地方,只在字符串生成后,再去常量池里查找引用。所以字符串最初生成时所花的内存,是省不掉的。

String s = new String(bytes, “UTF-8”).intern();
String s = String.valueOf(i).intern();

只有大量对象放在长期存在的集合里,里面是大量重复的字符串,或者对象的属性是重复的字符串时,省内存的效果才显现出来。短生命周期的字符串,GC要干的活是一样的。

 

3. 执行路径上多次的==,才能抵消常量池HasHMap查找的代价

==当然比equals()快得多,但常量池其实是个HashMap,依然没有神奇的地方,依然要执行HashMap的get操作,所以,一次hashCode() 和至少一次的equals()已经预付了,如果hash冲突,那equals()次数更多。

 

4. 真的对性能影响甚微吗?

在我的服务化框架测试里,把几个Header字段intern了,性能立马从七万五调到七万一 QPS,原来从七万一升到七万五 ,曾做过多少效果甚微的优化加上一次Netty使用的优化而成,现在它掉下来倒是飞快。

PS. 七万五 20%CPU这个数字,这两周的博客里都没升过了: (

 

5. 小陷阱

来自R大的提醒, s.intern()是无效的,因为String是不变对象, String s1 = s.intern()后,这个s1才是个引用。

Java的常量池也是不省心的,要注意JDK版本,占小狼同学的《浅谈Java String内幕(2)》 查看全部
String.intern(),写应用的同学不知道也没什么损失。但知道的同学,如果只觉得它很省内存,用==比较字符串很酷,或者期待JDK会做什么神奇的事情,然后恨不得所有地方都用上String.intern(),那很可能会误用,所以把上周在群里的讨论整理成此文。

在社会科学中,祛魅(Disenchantment)是指在现代社会中消去神秘主义(魅惑力)的表面并把文化合理化。这里乱用一下。

 

1. String.itern()的基本原理

详细可看占小狼同学的《浅谈Java String内幕(2)》

String.intern()是一个Native方法,底层调用C++的 StringTable::intern 方法,源码注释:当调用 intern 方法时,如果常量池中已经该字符串,则返回池中的字符串;否则将此字符串添加到常量池中,并返回字符串的引用。

所以明面上,它有两大好处,一是重复的字符串,会用同一个引用代替;二是字符串比较,不再需要逐个字符的equals()比较,而用==对比引用是否相同即可。

 

2. 省内存效果只对长期存在的字符串有效

String.intern()没有神奇的地方,只在字符串生成后,再去常量池里查找引用。所以字符串最初生成时所花的内存,是省不掉的。

String s = new String(bytes, “UTF-8”).intern();
String s = String.valueOf(i).intern();

只有大量对象放在长期存在的集合里,里面是大量重复的字符串,或者对象的属性是重复的字符串时,省内存的效果才显现出来。短生命周期的字符串,GC要干的活是一样的。

 

3. 执行路径上多次的==,才能抵消常量池HasHMap查找的代价

==当然比equals()快得多,但常量池其实是个HashMap,依然没有神奇的地方,依然要执行HashMap的get操作,所以,一次hashCode() 和至少一次的equals()已经预付了,如果hash冲突,那equals()次数更多。

 

4. 真的对性能影响甚微吗?

在我的服务化框架测试里,把几个Header字段intern了,性能立马从七万五调到七万一 QPS,原来从七万一升到七万五 ,曾做过多少效果甚微的优化加上一次Netty使用的优化而成,现在它掉下来倒是飞快。

PS. 七万五 20%CPU这个数字,这两周的博客里都没升过了: (

 

5. 小陷阱

来自R大的提醒, s.intern()是无效的,因为String是不变对象, String s1 = s.intern()后,这个s1才是个引用。

Java的常量池也是不省心的,要注意JDK版本,占小狼同学的《浅谈Java String内幕(2)》

另一份Java应用调优指南之-前菜

默认分类admin 发表了文章 • 0 个评论 • 183 次浏览 • 2017-01-17 17:25 • 来自相关话题

每一次成功的调优,都会诞生又一份的调优指南。

一些必须写在前面的军规,虽然与Java应用的调优没直接关联,但是测试同学经常不留神的地方,导致应用的优化变成一场测试环境调优的戏码。
 

1 独占你的测试机器

包括跑JMeter的那些机器。

"top"或者"pidstat 1" 看一下,其他的路人甲乙丙丁的应用都关干净了没。

如果是云主机,确保更多的占有宿主机的资源,比如深夜大家下班了你在家连VPN回来跑。

 

2 了解你的测试机器

必须完完全全的了解你的机器,才知道有没卡在某个瓶颈,或者与线上环境、其他测试结果的比较。

还是那句, 包括跑JMeter的那些机器。

 

2.1 CPU

"cat /proc/cpuinfo", 看最后一条就好,比如

 

processor : 23
model name : Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz
physical id : 1
cpu cores : 6

所有数字都从零开始,physical id:1即两颗cpu, cpu core: 6即6核,processor : 23即24个处理器。

 

2 CPU * 6 Core * 2HT(Intel超线程技术) = 24 Processor

不过也有很多同事喜欢说24核,也懒得纠正了。

 

2.2 内存

"free -g" 没什么好说的。

 

2.3 硬盘

查看大小、分区、文件系统类型: "df -hT"
硬盘是否SCSI:/dev/sdX就是scsi的,hdX就是普通的。
硬盘是否SSD : "cat /sys/block/sda/queue/rotational", 0是SSD,1是传统硬盘,但也不一定灵

普通硬盘的写速度大概100M/s,RAID级别的查看不方便,SSD的速度也不定,所以用dd测一下最靠谱:

 

dd if=/dev/zero of=dd.file bs=8k count=128k conv=fdatasync
dd if=/dev/zero of=./dd.file bs=1G count=1 conv=fdatasync

上面命令测试了分别以每次8k和1g的大小,写入1g文件的速度。

if:输入文件名, /dev/zero 设备无穷尽地提供0
of:输出文件名
bs:块大小
count:次数
conv=fdatasync :实际写盘,而不是写入Page Cache

硬盘读速度的测试同理,不过要先清理缓存,否则直接从Page Cache读了。

 

sh -c "sync && echo 3 > /proc/sys/vm/drop_caches”
dd if=./dd.file of=/dev/null bs=8k

 

2.4 网卡

先用ifconfig看看有多少块网卡和bonding。bonding是个很棒的东西,可以把多块网卡绑起来,突破单块网卡的带宽限制。

然后检查每块网卡的速度,比如"ethtool eth0"。

再检查bonding,比如"cat /proc/net/bonding/bond0", 留意其Bonding Mode是负载均衡的,再留意其捆绑的网卡的速度。

最后检查测试客户机与服务机之间的带宽,先简单ping或traceroute 一下得到RTT时间,iperf之类的可稍后。

 

2.5 操作系统

Linux的内核版本,是否64位: "uname -a"
Redhat/CentOS版本 : "cat /etc/redhat-release"

 

3. 布置好你的机器状态采集工具

实时观察的,我喜欢dstat,详见《从dstat理解Linux性能监控体系》比vmstat,iostat, sar们都好用,起码对得够齐,单位能自动转换。不过dstat需要安装(yum install dstat,或者去它的网站下载解压即用版)

dstat -tamp:推荐,打印时间戳,比默认打印多一个memory信息,一个有否进程在block状态的信息
dstat -tamN bond0,lo: 如果有bonding,dstat会把bond0和eth0 有时会算双份,还有lo的也算到总量里,所以先用-N指定网卡检查下。

参考资料:后台性能测试不可不知的二三事

 

4. JMeter的调优顶一半的事

JMeter的版本越新越好。

 

4.1 JMeter的JVM参数

它默认连个垃圾收集算法都没有配,对延时要求高的,必须配上CMS或G1,内存也整大点降低GC的频率。其他的,给Server配的啥参数,给JMeter也来上一份。如果想动态改的,不写死在脚本里,可以配置环境变量$JVM_ARGS

 

4.2 测试计划的编写

什么if 语句,以及所有其实用动态语言来实现的都挺慢的。
xPath抽取结果集里的字段之类看着就慢的也别写了。
别加任何监听器和图形。
再配置输出日志的格式,能不要的列都别要了,最极端的其实就延时这列有用。

 

4.3 JMeter的运行

在Linux上用命令行跑,别偷懒用Window开着界面跑。
别开超过200条线程(虚拟机上更少)。
可以在不同机器上起多个JMeter,用集群汇总的模式。

 

4.4 结果的统计

初始连接,Server端热身,JVM编译热点方法等都需要时间,所以建议统计前删掉前面的一些日志。

要配置一下才能看到99.9%, 99.99% 分位数的延时,另外因为之前输出日志时省略了很多列,导入日志的时候配置也要如此。

但如果不能XWindows Forward,还要把日志下载回来再导入本地的JMeter,那还不如自己动动手,用sed, awk, sort配合一下自己写个分析延时的脚本直接在服务器上运行。

 

5. 其他被依赖的节点

比如所依赖的数据库够不够快,Restful API的模拟器够不够快,负载均衡器如HAProxy优化配置了没有。

 
唠叨完这些写给测试同学的话,下篇,就可以正式开始调优了。 查看全部
每一次成功的调优,都会诞生又一份的调优指南。

一些必须写在前面的军规,虽然与Java应用的调优没直接关联,但是测试同学经常不留神的地方,导致应用的优化变成一场测试环境调优的戏码。
 

1 独占你的测试机器

包括跑JMeter的那些机器。

"top"或者"pidstat 1" 看一下,其他的路人甲乙丙丁的应用都关干净了没。

如果是云主机,确保更多的占有宿主机的资源,比如深夜大家下班了你在家连VPN回来跑。

 

2 了解你的测试机器

必须完完全全的了解你的机器,才知道有没卡在某个瓶颈,或者与线上环境、其他测试结果的比较。

还是那句, 包括跑JMeter的那些机器。

 

2.1 CPU

"cat /proc/cpuinfo", 看最后一条就好,比如

 

processor : 23
model name : Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz
physical id : 1
cpu cores : 6

所有数字都从零开始,physical id:1即两颗cpu, cpu core: 6即6核,processor : 23即24个处理器。

 

2 CPU * 6 Core * 2HT(Intel超线程技术) = 24 Processor

不过也有很多同事喜欢说24核,也懒得纠正了。

 

2.2 内存

"free -g" 没什么好说的。

 

2.3 硬盘

查看大小、分区、文件系统类型: "df -hT"
硬盘是否SCSI:/dev/sdX就是scsi的,hdX就是普通的。
硬盘是否SSD : "cat /sys/block/sda/queue/rotational", 0是SSD,1是传统硬盘,但也不一定灵

普通硬盘的写速度大概100M/s,RAID级别的查看不方便,SSD的速度也不定,所以用dd测一下最靠谱:

 

dd if=/dev/zero of=dd.file bs=8k count=128k conv=fdatasync
dd if=/dev/zero of=./dd.file bs=1G count=1 conv=fdatasync

上面命令测试了分别以每次8k和1g的大小,写入1g文件的速度。

if:输入文件名, /dev/zero 设备无穷尽地提供0
of:输出文件名
bs:块大小
count:次数
conv=fdatasync :实际写盘,而不是写入Page Cache

硬盘读速度的测试同理,不过要先清理缓存,否则直接从Page Cache读了。

 

sh -c "sync && echo 3 > /proc/sys/vm/drop_caches”
dd if=./dd.file of=/dev/null bs=8k

 

2.4 网卡

先用ifconfig看看有多少块网卡和bonding。bonding是个很棒的东西,可以把多块网卡绑起来,突破单块网卡的带宽限制。

然后检查每块网卡的速度,比如"ethtool eth0"。

再检查bonding,比如"cat /proc/net/bonding/bond0", 留意其Bonding Mode是负载均衡的,再留意其捆绑的网卡的速度。

最后检查测试客户机与服务机之间的带宽,先简单ping或traceroute 一下得到RTT时间,iperf之类的可稍后。

 

2.5 操作系统

Linux的内核版本,是否64位: "uname -a"
Redhat/CentOS版本 : "cat /etc/redhat-release"

 

3. 布置好你的机器状态采集工具

实时观察的,我喜欢dstat,详见《从dstat理解Linux性能监控体系》比vmstat,iostat, sar们都好用,起码对得够齐,单位能自动转换。不过dstat需要安装(yum install dstat,或者去它的网站下载解压即用版)

dstat -tamp:推荐,打印时间戳,比默认打印多一个memory信息,一个有否进程在block状态的信息
dstat -tamN bond0,lo: 如果有bonding,dstat会把bond0和eth0 有时会算双份,还有lo的也算到总量里,所以先用-N指定网卡检查下。

参考资料:后台性能测试不可不知的二三事

 

4. JMeter的调优顶一半的事

JMeter的版本越新越好。

 

4.1 JMeter的JVM参数

它默认连个垃圾收集算法都没有配,对延时要求高的,必须配上CMS或G1,内存也整大点降低GC的频率。其他的,给Server配的啥参数,给JMeter也来上一份。如果想动态改的,不写死在脚本里,可以配置环境变量$JVM_ARGS

 

4.2 测试计划的编写

什么if 语句,以及所有其实用动态语言来实现的都挺慢的。
xPath抽取结果集里的字段之类看着就慢的也别写了。
别加任何监听器和图形。
再配置输出日志的格式,能不要的列都别要了,最极端的其实就延时这列有用。

 

4.3 JMeter的运行

在Linux上用命令行跑,别偷懒用Window开着界面跑。
别开超过200条线程(虚拟机上更少)。
可以在不同机器上起多个JMeter,用集群汇总的模式。

 

4.4 结果的统计

初始连接,Server端热身,JVM编译热点方法等都需要时间,所以建议统计前删掉前面的一些日志。

要配置一下才能看到99.9%, 99.99% 分位数的延时,另外因为之前输出日志时省略了很多列,导入日志的时候配置也要如此。

但如果不能XWindows Forward,还要把日志下载回来再导入本地的JMeter,那还不如自己动动手,用sed, awk, sort配合一下自己写个分析延时的脚本直接在服务器上运行。

 

5. 其他被依赖的节点

比如所依赖的数据库够不够快,Restful API的模拟器够不够快,负载均衡器如HAProxy优化配置了没有。

 
唠叨完这些写给测试同学的话,下篇,就可以正式开始调优了。

软件设计杂谈

默认分类admin 发表了文章 • 0 个评论 • 158 次浏览 • 2017-01-17 17:06 • 来自相关话题

作者:陈天
链接:https://zhuanlan.zhihu.com/p/20005177
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

disclaimer: 本文所讲的设计,非UI/UE的设计,单单指软件代码/功能本身在技术上的设计。UI/UE的主题请出门右转找特赞(Tezign)。:)
在如今这个Lean/Agile横扫一切的年代,设计似乎有了被边缘化的倾向,做事的周期如此之快,似乎已容不下人们更多的思考。MVP(Minimal Viable Produce)在很多团队里演化成一个形而上的图腾,于是工程师们找到了一个完美的借口:我先做个MVP,设计的事,以后再说。
如果纯属个人玩票,有个点子,hack out还说得过去;但要严肃做一个项目,还是要下工夫设计一番,否则,没完没了的返工会让你无语泪千行。
设计首先得搞懂要解决的问题
工程师大多都是很聪明的人,聪明人有个最大的问题就是自负。很多人拿到一个需求,还没太搞明白其外延和内涵,代码就已经在脑袋里流转。这样做出来的系统,纵使再精妙,也免不了承受因需求理解不明确而导致的返工之苦。
搞懂需求这事,说起来简单,做起来难。需求有正确的但表达错误的需求,有正确的但没表达出来的需求,还有过度表达的需求。所以,拿到需求后,先不忙寻找解决方案,多问问自己,工作伙伴,客户follow up questions来澄清需求模糊不清之处。
搞懂需求,还需要了解需求对应的产品,公司,以及(潜在)竞争对手的现状,需求的上下文,以及需求的约束条件。人有二知二不知:
I know that I know

I know that I don’t know

I don’t know that I know

I don’t know that I don’t know

澄清需求的过程,就是不断驱逐无知,掌握现状,上下文和约束条件的过程。
这个主题讲起来很大,且非常重要,但毕竟不是本文的重点,所以就此带过。
寻找(多个)解决方案
如果对问题已经有不错的把握,接下来就是解决方案的发现之旅。这是个考察big picture的活计。同样是满足孩子想要个汽车的愿望,你可以:
去玩具店里买一个现成的

买乐高积木,然后组装

用纸糊一个,或者找块木头,刻一个

这对应软件工程问题的几种解决之道:
购买现成软件(acuquire or licensing),二次开发之(如果需要)

寻找building blocks,组装之(glue)

自己开发(build from scratch, or DIY)

大部分时候,如果a或b的TCO [1] 合理,那就不要选择c。做一个产品的目的是为客户提供某种服务,而不是证明自己能一行行码出出来这个产品。
a是个很重要的点,可惜大部分工程师脑袋里没有钱的概念,或者出于job security的私心,而忽略了。工程师现在越来越贵,能用合理的价格搞定的功能,就不该雇人去打理(自己打脸)。一个产品,最核心的部分不超过整个系统的20%,把人力资源铺在核心的部分,才是软件设计之道。
b我们稍后再讲。
对工程师而言,DIY出一个功能是个极大的诱惑。一种DIY是源自工程师的不满。任何开源软件,在处理某种特定业务逻辑的时候总会有一些不足,眼里如果把这些不足放在,却忽略了人家的好处,是大大的不妥。前两天我听到有人说 "consul sucks, …​, I’ll build our own service discovery framework…​",我就苦笑。我相信他能做出来一个简单的service discovery tool,这不是件特别困难的事情。问题是值不值得去做。如果连处于consul这个层次的基础组件都要自己去做,那要么是心太大,要么是没有定义好自己的软件系统的核心价值(除非系统的核心价值就在于此)。代码一旦写出来,无论是5000行还是50行,都是需要有人去维护的,在系统的生命周期里,每一行自己写的代码都是一笔债务,需要定期不定期地偿还利息。
另外一种DIY是出于工程师的无知。「无知者无畏」在某些场合的效果是正向的,有利于打破陈规。但在软件开发上,还是知识和眼界越丰富越开阔越好。一个无知的工程师在面对某个问题时(比如说service discovery),如果不知道这问题也许有现成的解决方案(consul),自己铆足了劲写一个,大半会有失偏颇(比如说没做上游服务的health check,或者自己本身的high availability),结果bug不断,辛辛苦苦一个个都啃下来,才发现,自己走了很多弯路,费了大半天劲,做了某个开源软件的功能的子集。当然,对工程师而言,这个练手的价值还是很大的,但对公司来说,这是一笔沉重的无意义的支出。
眼界定义了一个人的高度,如果你每天见同类的人,看同质的书籍/视频,(读)写隶属同一domain的代码,那多半眼界不够开阔。互联网的发展一日千里,变化太快,如果把自己禁锢在一方小天地里,很容易成为陶渊明笔下的桃花源中人:乃不知有汉,无论魏晋。
构建灵活且有韧性的系统
如果说之前说的都是废话,那么接下来的和真正的软件设计能扯上些关系。
分解和组合
软件设计是一个把大的问题不断分解,直至原子级的小问题,然后再不断组合的过程。这一点可以类比生物学:原子(keyword/macro)组合成分子(function),分子组合成细胞(module/class),细胞组合成组织(micro service),组织组合成器官(service),进而组合成生物(system)。
一个如此组合而成系统,是满足关注点分离(Separation of Concerns)的。大到一个器官,小到一个细胞,都各司其职,把自己要做的事情做到极致。心脏不必关心肾脏会干什么,它只需要做好自己的事情:把新鲜血液通过动脉排出,再把各个器官用过的血液从静脉回收。
分解和组合在软件设计中的作用如此重要,以至于一个系统如果合理分解,那么日后维护的代价就要小得多。同样讲关注点分离,不同的工程师,分离的方式可能完全不同。但究其根本,还有有一些规律可循。
总线(System Bus)
首先我们要把系统的总线定义出来。人体的总线,大的有几条:血管(动脉,静脉),神经网络,气管,输尿管。它们有的完全负责与外界的交互(气管,输尿管),有的完全是内部的信息中枢(血管),有的内外兼修(神经网络)。
总线把生产者和消费者分离,让彼此互不依赖。心脏往外供血时,把血压入动脉血管就是了。它并不需要知道谁是接收者。
同样的,回到我们熟悉的计算机系统,CPU访问内存也是如此:它发送一条消息给总线,总线通知RAM读取数据,然后RAM把数据返回给总线,CPU再获取之。整个过程中CPU只知道一个内存地址,毋须知道访问的具体是哪个内存槽的哪块内存 —— 总线将二者屏蔽开。
学过计算机系统的同学应该都知道,经典的PC结构有几种总线:数据总线,地址总线,控制总线,扩展总线等;做过网络设备的同学也都知道,一个经典的网络设备,其软件系统的总线分为:control plane和data plane。
路由(routing)
有了总线的概念,接下来必然要有路由。我们看人体的血管:
每一处分叉,就涉及到一次路由。
路由分为外部路由和内部路由。外部路由处理输入,把不同的输入dispatch到系统里不同的组件。做web app的,可能没有意识到,但其实每个web framework,最关键的组件之一就是url dispatch。HTTP的伟大之处就是每个request,都能通过url被dispatch到不同的handler处理。而url是目录式的,可以层层演进 —— 就像分形几何,一个大的系统,通过不断重复的模式,组合起来 —— 非常利于系统的扩展。遗憾的是,我们自己做系统,对于输入既没有总线的考量,又无路由的概念,if-else下去,久而久之,代码便绕成了意大利面条。
再举一例:DOM中的event bubble,在javascript处理起来已然隐含着路由的概念。你只需定义当某个事件(如onclick)发生时的callback函数就好,至于这事件怎么通过eventloop抵达回调函数,无需关心。好的路由系统剥茧抽丝,把繁杂的信息流正确送到处理者手中。
外部路由总还有「底层」为我们完成,内部路由则需工程师考虑。service级别的路由(数据流由哪个service处理)可以用consul等service discovery组件,service内部的路由(数据流到达后怎么处理)则需要自己完成。路由的具体方式有很多种,pattern matching最为常见。
无论用何种方式路由,数据抵达总线前为其定义Identity(ID)非常重要,你可以管这个过程叫data normalization,data encapsulation等,总之,一个消息能被路由,需要有个用于路由的ID。这ID可以是url,可以是一个message header,也可以是一个label(想象MPLS的情况)。当我们为数据赋予一个个合理的ID后,如何路由便清晰可见。
队列(Queue)
对于那些并非需要立即处理的数据,可以使用队列。队列也有把生产者和消费者分离的功效。队列有:
single producer single consumer(SPSC)

single producer multiple consumers(SPMC)

multiple producers single consumer(MPSC)

multiple producers multiple consumers(MPMC)

仔细想想,队列其实就是总线+路由(可选)+存储的一个特殊版本。一般而言,system bus之上是系统的各个service,每个service再用service bus(或者queue)把micro service chain起来,然后每个micro service内部的组件间,再用queue连接起来。
有了队列,有利于提高流水线的效率。一般而言,流水线的处理速度取决于最慢的组件。队列的存在,让慢速组件有机会运行多份,来弥补生产者和消费者速度上的差距。
Pub/Sub
存储在队列中的数据,除路由外,还有一种处理方式:pub/sub。和路由相似,pub/sub将生产者和消费者分离;但二者不同之处在于,路由的目的地由路由表中的表项控制,而pub/sub一般由publisher控制 [2]:任何subscribe某个数据的consumer,都会到publisher处注册,publisher由此可以定向发送消息。
协议(protocol)
一旦我们把系统分解成一个个service,service再分解成micro service,彼此之间互不依赖,仅仅通过总线或者队列来通讯,那么,我们就需要协议来定义彼此的行为。协议听起来很高大上,其实不然。我们写下的每个function(或者每个class),其实就是在定义一个不成文的协议:function的arity是什么,接受什么参数,返回什么结果。调用者需严格按照协议调用方能得到正确的结果。
service级别的协议是一份SLA:服务的endpoint是什么,版本是什么,接收什么格式的消息,返回什么格式的消息,消息在何种网络协议上承载,需要什么样的authorization,可以正常服务的最大吞吐量(throughput)是什么,在什么情况下会触发throttling等等。
头脑中有了总线,路由,队列,协议等这些在computer science 101中介绍的基础概念,系统的分解便有迹可寻:面对一个系统的设计,你要做的不再是一道作文题,而是一道填空题:在若干条system bus里填上其名称和流进流出的数据,在system bus之上的一个个方框里填上服务的名称和服务的功能。然后,每个服务再以此类推,直到感觉毋须再细化为止。
组成系统的必要服务
有些管理性质的服务,尽管和业务逻辑直接关系不大,但无论是任何系统,都需要考虑构建,这里罗列一二。
代谢(sweeping)
一个活着的生物时时刻刻都进行着新陈代谢:每时每刻新的细胞取代老的细胞,同时身体中的「垃圾」通过排泄系统排出体外。一个运转有序的城市也有新陈代谢:下水道,垃圾场,污水处理等维持城市的正常功能。没有了代谢功能,生物会凋零,城市会荒芜。
软件系统也是如此。日志会把硬盘写满,软件会失常,硬件会失效,网络会拥塞等等。一个好的软件系统需要一个好的代谢系统:出现异常的服务会被关闭,同样的服务会被重新启动,恢复运行。
代谢系统可以参考erlang的supervisor/child process结构,以及supervision tree。很多软件,都运行在简单的supervision tree模式下,如nginx。
高可用性(HA)
每个人都有两个肾。为了apple watch卖掉一个肾,另一个还能保证人体的正常工作。当然,人的两个肾是Active-Active工作模式,内部的肾元(micro service)是 N(active)+M(backup) clustering 工作的(看看人家这service的做的),少了一个,performance会一点点有折扣,但可以忽略不计。
大部分软件系统里的各种服务也需要高可用性:除非完全无状态的服务,且服务重启时间在ms级。服务的高可用性和路由是息息相关的:高可用性往往意味着同一服务的冗余,同时也意味着负载分担。好的路由系统(如consul)能够对路由至同一服务的数据在多个冗余服务间进行负载分担,同时在检测出某个失效服务后,将数据路只由至正常运作的服务。
高可用性还意味着非关键服务,即便不可恢复,也只会导致系统降级,而不会让整个系统无法访问。就像壁虎的尾巴断了不妨碍壁虎逃命,人摔伤了手臂还能吃饭一样,一个软件系统里统计模块的异常不该让用户无法访问他的个人页面。
安保(security)
安保服务分为主动安全和被动安全。authentication/authorization + TLS + 敏感信息加密 + 最小化输入输出接口可以算是主动安全,防火墙等安防系统则是被动安全。
继续拿你的肾来比拟 —— 肾脏起码有两大安全系统:
输入安全。肾器的厚厚的器官膜,保护器官的输入输出安全 —— 主要的输入输出只能是肾动脉,肾静脉和输尿管。

环境安全。肾器里有大量脂肪填充,避免在撞击时对核心功能的损伤。

除此之外,人体还提供了包括免疫系统,皮肤,骨骼,空腔等一系列安全系统,从各个维度最大程度保护一个器官的正常运作。如果我们仔细研究生物,就会发现,安保是个一揽子解决方案:小到细胞,大到整个人体,都有各自的安全措施。一个软件系统也需如此考虑系统中各个层次的安全。
透支保护(overdraft protection)
任何系统,任何服务都是有服务能力的 —— 当这能力被透支时,需要一定的应急计划。如果使用拥有auto scaling的云服务(如AWS),动态扩容是最好的解决之道,但受限于所用的解决方案,它并非万灵药,AWS的auto scaling依赖于load balancer,如Amazon自有的ELB,或者第三方的HAProxy,但ELB对某些业务,如websocket,支持不佳;而第三方的load balancer,则需要考虑部署,与Amazon的auto scaling结合(需要写点代码),避免单点故障,保证自身的capacity等一堆头疼事。
在无法auto scaling的场景最通用的做法是back pressure,把压力反馈到源头。就好像你不断熬夜,最后大脑受不了,逼着你睡觉一样。还有一种做法是服务降级,停掉非核心的service/micro-service,如analytical service,ad service,保证核心功能正常。
把设计的成果讲给别人听
完成了分解和组合,也严肃对待了诸多与业务没有直接关系,但又不得不做的必要功能后,接下来就是要把设计在白板上画下来,讲给任何一个利益相关者听。听他们的反馈。设计不是一个闭门造车的过程,全程都需要和各种利益相关者交流。然而,很多人都忽视了设计定型后,继续和外界交流的必要性。很多人会认为:我的软件架构,设计结果和工程有关,为何要讲给工程师以外的人听?他们懂么?
其实pitch本身就是自我学习和自我修正的一部分。当着一个人或者几个人的面,在白板上画下脑海中的设计的那一刻,你就会有直觉哪个地方似乎有问题,这是很奇特的一种体验:你自己画给自己看并不会产生这种直觉。这大概是面对公众的焦灼产生的肾上腺素的效果。:)
此外,从听者的表情,或者他们提的听起来很傻很天真的问题,你会进一步知道哪些地方你以为你搞通了,其实自己是一知半解。太简单,太基础的问题,我们take it for granted,不屑去问自己,非要有人点出,自己才发现:啊,原来这里我也不懂哈。这就是破解 "you don’t know what you don’t know" 之法。
记得看过一个video,主讲人大谈企业文化,有个哥们傻乎乎发问:so what it culture literally? 主讲人愣了一下,拖拖拉拉讲了一堆自己都不能让自己信服的废话。估计回头他就去查韦氏词典了。
最后,总有人在某些领域的知识更丰富一些,他们会告诉你你一些你知道自己不懂的事情。填补了 "you know that you don’t know" 的空缺。
设计时的tradeoff
Rich hickey(clojure作者)在某个演讲中说:
everyone says design is about tradeoffs, but you need to enumerate at least two or more possible solutions, and the attributes and deficits of each, in order to make tradeoff.

所以,下回再腆着脸说:偶做了些tradeoff,先确保自己做足了功课再说。
设计的改变不可避免
设计不是一锤子买卖,改变不可避免。我之前的一个老板,喜欢把:change is your friend 挂在口头。软件开发的整个生命周期,变更是家常便饭,以至于变更管理都生出一门学问。软件的设计期更是如此。人总会犯错,设计总有缺陷,需求总会变化,老板总会指手画脚,PM总有一天会亮出獠牙,不再是贴心大哥,或者美萌小妹。。。所以,据理力争,然后接受必要的改变即可。连凯恩斯他老人家都说:

What do you do, sir?
(文章很长,感谢阅读至此,这文章3/30号开始写,拖拖拉拉算是凑合完成了)
如果您觉得这篇文章不错,请点赞。多谢!
欢迎订阅公众号『程序人生』(搜索微信号 programmer_life)。每篇文章都力求原汁原味,北京时间中午12点左右,美西时间下午8点左右与您相会。
1. Total Cost of Ownership
2. 要看具体的pub/sub系统的实现方式
原文链接:https://zhuanlan.zhihu.com/p/2 ... attle 查看全部

作者:陈天
链接:https://zhuanlan.zhihu.com/p/20005177
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

disclaimer: 本文所讲的设计,非UI/UE的设计,单单指软件代码/功能本身在技术上的设计。UI/UE的主题请出门右转找特赞(Tezign)。:)
在如今这个Lean/Agile横扫一切的年代,设计似乎有了被边缘化的倾向,做事的周期如此之快,似乎已容不下人们更多的思考。MVP(Minimal Viable Produce)在很多团队里演化成一个形而上的图腾,于是工程师们找到了一个完美的借口:我先做个MVP,设计的事,以后再说。
如果纯属个人玩票,有个点子,hack out还说得过去;但要严肃做一个项目,还是要下工夫设计一番,否则,没完没了的返工会让你无语泪千行。
设计首先得搞懂要解决的问题
工程师大多都是很聪明的人,聪明人有个最大的问题就是自负。很多人拿到一个需求,还没太搞明白其外延和内涵,代码就已经在脑袋里流转。这样做出来的系统,纵使再精妙,也免不了承受因需求理解不明确而导致的返工之苦。
搞懂需求这事,说起来简单,做起来难。需求有正确的但表达错误的需求,有正确的但没表达出来的需求,还有过度表达的需求。所以,拿到需求后,先不忙寻找解决方案,多问问自己,工作伙伴,客户follow up questions来澄清需求模糊不清之处。
搞懂需求,还需要了解需求对应的产品,公司,以及(潜在)竞争对手的现状,需求的上下文,以及需求的约束条件。人有二知二不知:
I know that I know

I know that I don’t know

I don’t know that I know

I don’t know that I don’t know

澄清需求的过程,就是不断驱逐无知,掌握现状,上下文和约束条件的过程。
这个主题讲起来很大,且非常重要,但毕竟不是本文的重点,所以就此带过。
寻找(多个)解决方案
如果对问题已经有不错的把握,接下来就是解决方案的发现之旅。这是个考察big picture的活计。同样是满足孩子想要个汽车的愿望,你可以:
去玩具店里买一个现成的

买乐高积木,然后组装

用纸糊一个,或者找块木头,刻一个

这对应软件工程问题的几种解决之道:
购买现成软件(acuquire or licensing),二次开发之(如果需要)

寻找building blocks,组装之(glue)

自己开发(build from scratch, or DIY)

大部分时候,如果a或b的TCO [1] 合理,那就不要选择c。做一个产品的目的是为客户提供某种服务,而不是证明自己能一行行码出出来这个产品。
a是个很重要的点,可惜大部分工程师脑袋里没有钱的概念,或者出于job security的私心,而忽略了。工程师现在越来越贵,能用合理的价格搞定的功能,就不该雇人去打理(自己打脸)。一个产品,最核心的部分不超过整个系统的20%,把人力资源铺在核心的部分,才是软件设计之道。
b我们稍后再讲。
对工程师而言,DIY出一个功能是个极大的诱惑。一种DIY是源自工程师的不满。任何开源软件,在处理某种特定业务逻辑的时候总会有一些不足,眼里如果把这些不足放在,却忽略了人家的好处,是大大的不妥。前两天我听到有人说 "consul sucks, …​, I’ll build our own service discovery framework…​",我就苦笑。我相信他能做出来一个简单的service discovery tool,这不是件特别困难的事情。问题是值不值得去做。如果连处于consul这个层次的基础组件都要自己去做,那要么是心太大,要么是没有定义好自己的软件系统的核心价值(除非系统的核心价值就在于此)。代码一旦写出来,无论是5000行还是50行,都是需要有人去维护的,在系统的生命周期里,每一行自己写的代码都是一笔债务,需要定期不定期地偿还利息。
另外一种DIY是出于工程师的无知。「无知者无畏」在某些场合的效果是正向的,有利于打破陈规。但在软件开发上,还是知识和眼界越丰富越开阔越好。一个无知的工程师在面对某个问题时(比如说service discovery),如果不知道这问题也许有现成的解决方案(consul),自己铆足了劲写一个,大半会有失偏颇(比如说没做上游服务的health check,或者自己本身的high availability),结果bug不断,辛辛苦苦一个个都啃下来,才发现,自己走了很多弯路,费了大半天劲,做了某个开源软件的功能的子集。当然,对工程师而言,这个练手的价值还是很大的,但对公司来说,这是一笔沉重的无意义的支出。
眼界定义了一个人的高度,如果你每天见同类的人,看同质的书籍/视频,(读)写隶属同一domain的代码,那多半眼界不够开阔。互联网的发展一日千里,变化太快,如果把自己禁锢在一方小天地里,很容易成为陶渊明笔下的桃花源中人:乃不知有汉,无论魏晋。
构建灵活且有韧性的系统
如果说之前说的都是废话,那么接下来的和真正的软件设计能扯上些关系。
分解和组合
软件设计是一个把大的问题不断分解,直至原子级的小问题,然后再不断组合的过程。这一点可以类比生物学:原子(keyword/macro)组合成分子(function),分子组合成细胞(module/class),细胞组合成组织(micro service),组织组合成器官(service),进而组合成生物(system)。
一个如此组合而成系统,是满足关注点分离(Separation of Concerns)的。大到一个器官,小到一个细胞,都各司其职,把自己要做的事情做到极致。心脏不必关心肾脏会干什么,它只需要做好自己的事情:把新鲜血液通过动脉排出,再把各个器官用过的血液从静脉回收。
分解和组合在软件设计中的作用如此重要,以至于一个系统如果合理分解,那么日后维护的代价就要小得多。同样讲关注点分离,不同的工程师,分离的方式可能完全不同。但究其根本,还有有一些规律可循。
总线(System Bus)
首先我们要把系统的总线定义出来。人体的总线,大的有几条:血管(动脉,静脉),神经网络,气管,输尿管。它们有的完全负责与外界的交互(气管,输尿管),有的完全是内部的信息中枢(血管),有的内外兼修(神经网络)。
总线把生产者和消费者分离,让彼此互不依赖。心脏往外供血时,把血压入动脉血管就是了。它并不需要知道谁是接收者。
同样的,回到我们熟悉的计算机系统,CPU访问内存也是如此:它发送一条消息给总线,总线通知RAM读取数据,然后RAM把数据返回给总线,CPU再获取之。整个过程中CPU只知道一个内存地址,毋须知道访问的具体是哪个内存槽的哪块内存 —— 总线将二者屏蔽开。
学过计算机系统的同学应该都知道,经典的PC结构有几种总线:数据总线,地址总线,控制总线,扩展总线等;做过网络设备的同学也都知道,一个经典的网络设备,其软件系统的总线分为:control plane和data plane。
路由(routing)
有了总线的概念,接下来必然要有路由。我们看人体的血管:
每一处分叉,就涉及到一次路由。
路由分为外部路由和内部路由。外部路由处理输入,把不同的输入dispatch到系统里不同的组件。做web app的,可能没有意识到,但其实每个web framework,最关键的组件之一就是url dispatch。HTTP的伟大之处就是每个request,都能通过url被dispatch到不同的handler处理。而url是目录式的,可以层层演进 —— 就像分形几何,一个大的系统,通过不断重复的模式,组合起来 —— 非常利于系统的扩展。遗憾的是,我们自己做系统,对于输入既没有总线的考量,又无路由的概念,if-else下去,久而久之,代码便绕成了意大利面条。
再举一例:DOM中的event bubble,在javascript处理起来已然隐含着路由的概念。你只需定义当某个事件(如onclick)发生时的callback函数就好,至于这事件怎么通过eventloop抵达回调函数,无需关心。好的路由系统剥茧抽丝,把繁杂的信息流正确送到处理者手中。
外部路由总还有「底层」为我们完成,内部路由则需工程师考虑。service级别的路由(数据流由哪个service处理)可以用consul等service discovery组件,service内部的路由(数据流到达后怎么处理)则需要自己完成。路由的具体方式有很多种,pattern matching最为常见。
无论用何种方式路由,数据抵达总线前为其定义Identity(ID)非常重要,你可以管这个过程叫data normalization,data encapsulation等,总之,一个消息能被路由,需要有个用于路由的ID。这ID可以是url,可以是一个message header,也可以是一个label(想象MPLS的情况)。当我们为数据赋予一个个合理的ID后,如何路由便清晰可见。
队列(Queue)
对于那些并非需要立即处理的数据,可以使用队列。队列也有把生产者和消费者分离的功效。队列有:
single producer single consumer(SPSC)

single producer multiple consumers(SPMC)

multiple producers single consumer(MPSC)

multiple producers multiple consumers(MPMC)

仔细想想,队列其实就是总线+路由(可选)+存储的一个特殊版本。一般而言,system bus之上是系统的各个service,每个service再用service bus(或者queue)把micro service chain起来,然后每个micro service内部的组件间,再用queue连接起来。
有了队列,有利于提高流水线的效率。一般而言,流水线的处理速度取决于最慢的组件。队列的存在,让慢速组件有机会运行多份,来弥补生产者和消费者速度上的差距。
Pub/Sub
存储在队列中的数据,除路由外,还有一种处理方式:pub/sub。和路由相似,pub/sub将生产者和消费者分离;但二者不同之处在于,路由的目的地由路由表中的表项控制,而pub/sub一般由publisher控制 [2]:任何subscribe某个数据的consumer,都会到publisher处注册,publisher由此可以定向发送消息。
协议(protocol)
一旦我们把系统分解成一个个service,service再分解成micro service,彼此之间互不依赖,仅仅通过总线或者队列来通讯,那么,我们就需要协议来定义彼此的行为。协议听起来很高大上,其实不然。我们写下的每个function(或者每个class),其实就是在定义一个不成文的协议:function的arity是什么,接受什么参数,返回什么结果。调用者需严格按照协议调用方能得到正确的结果。
service级别的协议是一份SLA:服务的endpoint是什么,版本是什么,接收什么格式的消息,返回什么格式的消息,消息在何种网络协议上承载,需要什么样的authorization,可以正常服务的最大吞吐量(throughput)是什么,在什么情况下会触发throttling等等。
头脑中有了总线,路由,队列,协议等这些在computer science 101中介绍的基础概念,系统的分解便有迹可寻:面对一个系统的设计,你要做的不再是一道作文题,而是一道填空题:在若干条system bus里填上其名称和流进流出的数据,在system bus之上的一个个方框里填上服务的名称和服务的功能。然后,每个服务再以此类推,直到感觉毋须再细化为止。
组成系统的必要服务
有些管理性质的服务,尽管和业务逻辑直接关系不大,但无论是任何系统,都需要考虑构建,这里罗列一二。
代谢(sweeping)
一个活着的生物时时刻刻都进行着新陈代谢:每时每刻新的细胞取代老的细胞,同时身体中的「垃圾」通过排泄系统排出体外。一个运转有序的城市也有新陈代谢:下水道,垃圾场,污水处理等维持城市的正常功能。没有了代谢功能,生物会凋零,城市会荒芜。
软件系统也是如此。日志会把硬盘写满,软件会失常,硬件会失效,网络会拥塞等等。一个好的软件系统需要一个好的代谢系统:出现异常的服务会被关闭,同样的服务会被重新启动,恢复运行。
代谢系统可以参考erlang的supervisor/child process结构,以及supervision tree。很多软件,都运行在简单的supervision tree模式下,如nginx。
高可用性(HA)
每个人都有两个肾。为了apple watch卖掉一个肾,另一个还能保证人体的正常工作。当然,人的两个肾是Active-Active工作模式,内部的肾元(micro service)是 N(active)+M(backup) clustering 工作的(看看人家这service的做的),少了一个,performance会一点点有折扣,但可以忽略不计。
大部分软件系统里的各种服务也需要高可用性:除非完全无状态的服务,且服务重启时间在ms级。服务的高可用性和路由是息息相关的:高可用性往往意味着同一服务的冗余,同时也意味着负载分担。好的路由系统(如consul)能够对路由至同一服务的数据在多个冗余服务间进行负载分担,同时在检测出某个失效服务后,将数据路只由至正常运作的服务。
高可用性还意味着非关键服务,即便不可恢复,也只会导致系统降级,而不会让整个系统无法访问。就像壁虎的尾巴断了不妨碍壁虎逃命,人摔伤了手臂还能吃饭一样,一个软件系统里统计模块的异常不该让用户无法访问他的个人页面。
安保(security)
安保服务分为主动安全和被动安全。authentication/authorization + TLS + 敏感信息加密 + 最小化输入输出接口可以算是主动安全,防火墙等安防系统则是被动安全。
继续拿你的肾来比拟 —— 肾脏起码有两大安全系统:
输入安全。肾器的厚厚的器官膜,保护器官的输入输出安全 —— 主要的输入输出只能是肾动脉,肾静脉和输尿管。

环境安全。肾器里有大量脂肪填充,避免在撞击时对核心功能的损伤。

除此之外,人体还提供了包括免疫系统,皮肤,骨骼,空腔等一系列安全系统,从各个维度最大程度保护一个器官的正常运作。如果我们仔细研究生物,就会发现,安保是个一揽子解决方案:小到细胞,大到整个人体,都有各自的安全措施。一个软件系统也需如此考虑系统中各个层次的安全。
透支保护(overdraft protection)
任何系统,任何服务都是有服务能力的 —— 当这能力被透支时,需要一定的应急计划。如果使用拥有auto scaling的云服务(如AWS),动态扩容是最好的解决之道,但受限于所用的解决方案,它并非万灵药,AWS的auto scaling依赖于load balancer,如Amazon自有的ELB,或者第三方的HAProxy,但ELB对某些业务,如websocket,支持不佳;而第三方的load balancer,则需要考虑部署,与Amazon的auto scaling结合(需要写点代码),避免单点故障,保证自身的capacity等一堆头疼事。
在无法auto scaling的场景最通用的做法是back pressure,把压力反馈到源头。就好像你不断熬夜,最后大脑受不了,逼着你睡觉一样。还有一种做法是服务降级,停掉非核心的service/micro-service,如analytical service,ad service,保证核心功能正常。
把设计的成果讲给别人听
完成了分解和组合,也严肃对待了诸多与业务没有直接关系,但又不得不做的必要功能后,接下来就是要把设计在白板上画下来,讲给任何一个利益相关者听。听他们的反馈。设计不是一个闭门造车的过程,全程都需要和各种利益相关者交流。然而,很多人都忽视了设计定型后,继续和外界交流的必要性。很多人会认为:我的软件架构,设计结果和工程有关,为何要讲给工程师以外的人听?他们懂么?
其实pitch本身就是自我学习和自我修正的一部分。当着一个人或者几个人的面,在白板上画下脑海中的设计的那一刻,你就会有直觉哪个地方似乎有问题,这是很奇特的一种体验:你自己画给自己看并不会产生这种直觉。这大概是面对公众的焦灼产生的肾上腺素的效果。:)
此外,从听者的表情,或者他们提的听起来很傻很天真的问题,你会进一步知道哪些地方你以为你搞通了,其实自己是一知半解。太简单,太基础的问题,我们take it for granted,不屑去问自己,非要有人点出,自己才发现:啊,原来这里我也不懂哈。这就是破解 "you don’t know what you don’t know" 之法。
记得看过一个video,主讲人大谈企业文化,有个哥们傻乎乎发问:so what it culture literally? 主讲人愣了一下,拖拖拉拉讲了一堆自己都不能让自己信服的废话。估计回头他就去查韦氏词典了。
最后,总有人在某些领域的知识更丰富一些,他们会告诉你你一些你知道自己不懂的事情。填补了 "you know that you don’t know" 的空缺。
设计时的tradeoff
Rich hickey(clojure作者)在某个演讲中说:
everyone says design is about tradeoffs, but you need to enumerate at least two or more possible solutions, and the attributes and deficits of each, in order to make tradeoff.

所以,下回再腆着脸说:偶做了些tradeoff,先确保自己做足了功课再说。
设计的改变不可避免
设计不是一锤子买卖,改变不可避免。我之前的一个老板,喜欢把:change is your friend 挂在口头。软件开发的整个生命周期,变更是家常便饭,以至于变更管理都生出一门学问。软件的设计期更是如此。人总会犯错,设计总有缺陷,需求总会变化,老板总会指手画脚,PM总有一天会亮出獠牙,不再是贴心大哥,或者美萌小妹。。。所以,据理力争,然后接受必要的改变即可。连凯恩斯他老人家都说:

What do you do, sir?
(文章很长,感谢阅读至此,这文章3/30号开始写,拖拖拉拉算是凑合完成了)
如果您觉得这篇文章不错,请点赞。多谢!
欢迎订阅公众号『程序人生』(搜索微信号 programmer_life)。每篇文章都力求原汁原味,北京时间中午12点左右,美西时间下午8点左右与您相会。
1. Total Cost of Ownership
2. 要看具体的pub/sub系统的实现方式
原文链接:https://zhuanlan.zhihu.com/p/2 ... attle

Apache Thrift设计概要

默认分类admin 发表了文章 • 0 个评论 • 167 次浏览 • 2017-01-17 16:58 • 来自相关话题

最近把Apache Thrift 的Java版代码翻了一遍,尝试理解做一个RPC框架所要考虑的方方面面。

网上关于Thrift设计的文章好像不多,于是把自己的笔记整理了一下发上来。

加插招聘广告:唯品会广州总部的基础架构部招人!! 如果你喜欢纯技术的工作,对大型互联网企业的服务化平台有兴趣,愿意在架构的成长期还可以大展拳脚的时候加盟,请电邮 calvin.xiao@vipshop.com内容大纲1. Overview
2. Transport层

2.1 Transport
2.2 WrapperTransport

3. Protocol层

3.1 IDL定义
3.2 TBinaryProtocol
3.3 TCompactProtocol
3.4 TTupleProtocol
3.5 TJSONProtocol

4. Processor层

4.1 基础接口

5. 服务端

5.1 Blocking Server
5.2 NonBlockingServer
5.3 TThreadedSelectorServer

6. 客户端

6.1 同步客户端
6.2 异步客户端

7. Http
8. Generator

1. Overview

Apache Thrift 的可赞之处是实现了跨超多语言(Java, C++, Go, Python,Ruby, PHP等等)的RPC框架,尽力为每种语言实现了相同抽象的RPC客户端与服务端。

简洁的四层接口抽象,每一层都可以独立的扩展增强或替换,是另一个可赞的地方。

最后,二进制的编解码方式和NIO的底层传输为它提供了不错的性能。

+-------------------------------------------+
| Server |
| (single-threaded, event-driven etc) |
+-------------------------------------------+
| Processor |
| (compiler generated) |
+-------------------------------------------+
| Protocol |
| (JSON, compact etc) |
+-------------------------------------------+
| Transport |
| (raw TCP, HTTP etc) |
+-------------------------------------------+

Transport层提供了一个简单的网络读写抽象层,有阻塞与非阻塞的TCP实现与HTTP的实现。

Protocol层定义了IDL中的数据结构与Transport层的传输数据格式之间的编解码机制。传输格式有二进制,压缩二进制,JSON等格式,IDL中的数据结构则包括Message,Struct,List,Map,Int,String,Bytes等。

Processor层由编译器编译IDL文件生成。
生成的代码会将传输层的数据解码为参数对象(比如商品对象有id与name两个属性,生成的代码会调用Protocol层的readInt与readString方法读出这两个属性值),然后调用由用户所实现的函数,并将结果编码送回。

在服务端, Server层创建并管理上面的三层,同时提供线程的调度管理。而对于NIO的实现,Server层可谓操碎了心。

在客户端, Client层由编译器直接生成,也由上面的三层代码组成。只要语言支持,客户端有同步与异步两种模式。

Facebook是Thrift的原作者,还开源有Nifty与Swift两个子项目, Cassandra是另一个著名用户,其跨语言的Client包就是基于Thrift的,这些在下一篇文章中展开讨论。

 

2. Transport层
2.1 Transport

TTransport除了open/close/flush,最重要的方法是int read(byte[] buf, int off, int len),void write(byte[] buf, int off, int len),读出、写入固定长度的内容。

TSocket使用经典的JDK Blocking IO的Transport实现。1K的BufferedStream, TCP配置项:KeepAlive=true,TCPNoDelay=true,SoLinger=false,SocketTimeout与ConnectionTimeout可配。

TNonblockingSocket,使用JDK NIO的Transport实现,读写的byte[]会每次被wrap成一个ByteBuffer。TCP配置项类似。
其实这个NonBlockingSocket并没有完全隔离传输层,后面异步Client或NIO的Server还有很多事情要做。

相应的,TServerSocket和TNonblockingServerSocket是ServerTransport的BIO、NIO实现,主要实现侦听端口,Accept后返回TSocket或TNonblockingSocket。其他TCP配置项:ReuseAddress=true,Backlog与SocketTimeout可配。

2.2 WrapperTransport

包裹一个底层的Transport,并利用自己的Buffer进行额外的操作。

1. TFramedTransport,按Frame读写数据。

每Frame的前4字节会记录Frame的长度(少于16M)。读的时候按长度预先将整Frame数据读入Buffer,再从Buffer慢慢读取。写的时候,每次flush将Buffer中的所有数据写成一个Frame。

有长度信息的TFramedTransport是后面NonBlockingServer粘包拆包的基础。

2. TFastFramedTransport 与TFramedTransport相比,始终使用相同的Buffer,提高了内存的使用率。

TFramedTransport的ReadBuffer每次读入Frame时都会创建新的byte[],WriteBuffer每次flush时如果大于初始1K也会重新创建byte[]。

而TFastFramedTransport始终使用相同的ReadBuffer和WriteBuffer,都是1K起步,不够时自动按1.5倍增长,和NIO的ByteBuffer一样,加上limit/pos这样的指针,每次重复使用时设置一下它们。

3. TZlibTransport ,读取时按1K为单位将数据读出并调用JDK的zip函数进行解压再放到Buffer,写入时,在flush时先zip再写入。

4. TSaslClientTransport与TSaslServerTransport, 提供SSL校验。

 

3. Protocol层
3.1 IDL定义

Thrift支持结构见 http://thrift.apache.org/docs/types

* 基本类型: i16,i32,i64, double, boolean,byte,byte[], String。
* 容器类型: List,Set,Map,TList/TSet/TMap类包含其元素的类型与元素的总个数。
* Struct类型,即面向对象的Class,继承于TBase。TStruct类有Name属性,还含有一系列的Field。TField类有自己的Name,类型,顺序id属性。
* Exception类型也是个Struct,继承于TException这个checked exception。
* Enum类型传输时是个i32。
* Message类型封装往返的RPC消息。TMessage类包含Name,类型(请求,返回,异常,ONEWAY)与seqId属性。

相应的,Protocol 层对上述数据结构有read与write的方法。
对基本类型是直接读写,对结构类型则是先调用readXXXBegin(),再调用其子元素的read()方法,再调用readXXXEnd()。
在所有函数中,Protocol层会直接调用Transport层读写特定长度的数据。

3.2 TBinaryProtocol

如前所述,i16,i32, double这些原始类型都是定长的,String,byte[]会在前4个字节说明自己的长度,容器类,Strutct类所对应的TMap,TStruct,TField,TMessage里有如前所述的属性 (不过Struct与Field里的name属性会被skip),所以其实现实可以简单想象的。

3.3 TCompactProtocol

比起TBinaryProtocol,会想方设法省点再省点。

1. 对整数类型使用了 ZigZag 压缩算法,数值越小压的越多。比如 i32 类型的整数本来是4个字节, 可以压缩成 1~5 字节不等。而 i64类型的整数本来是8个字节,可以压缩成 1~10 字节不等。 因此,值小的i32和i64,和小的Collection和短的String(因为它们都有定义长度的int属性) 越多,它就能省得越多。

2. 它还会尝试将Field的fieldId和type挤在一个byte里写。原本field是short,type也占一个byte,合共三个byte。 它将1个byte拆成两组4bit,前4bit放与前一个field的Id的delta值(不能相差超过15,如果中间太多没持久化的optional field),后4bit放type(目前刚好16种type,把byte[]和String合成一种了)

3.4 TTupleProtocol

继承于TCompactProtocol,Struct的编解码时使用更省地方但IDL间版本不兼容的TupleScheme,见Processor层。

3.5 TJSONProtocol

与我们平时的Restful JSON还是有点区别,具体的定义看cpp实现的TJSONProtool.h文件

对于容器类和Message,会用数组的方式,前几个元素是元信息,后面才是value,比如List是[type,size,[1,2,3]],

对于Struct,会是个Map,Key是fieldId而不是name(为了节省空间?),Value又是一个Map,只有一个Key-Value Pair,Key是type,Value才是真正value。

 

4. Processor层

建议使用一个最简单的IDL文件,用Windows版的Generator生成一个来进行观察。

4.1 基础接口

TBase是大部分生成的Struct,参数类,结果类的接口,最主要是实现从Protocol层读写自己的函数。

TProcessFunction是生成的服务方法类的基类,它的process函数会完成如下步骤:

1. 调用生成的args对象的read方法从protocol层读出自己
2. 调用子类生成的getResult()方法:拆分args对象得到参数,调用真正的用户实现得到结果,并组装成生成的result对象。
3. 写消息头,
4. 调用生成的result对象的write方法将自己写入protocol层
5 调用transport层的flush()。

TBaseProcessor 只有 boolean process(TProtocol in, TProtocol out) 这个简单接口,会先调readMessageBegin(),读出消息名,再从processMap里找出相应的TProcessFunction调用。

TMultiplexedProcessor,支持一个Server支持部署多个Service的情况,在Processor外面再加一层Map,消息名从“add”会变为“Caculator:add”,当然,这需要需要客户端发送时使用 TMultiplexedProtocol修饰原来的Protocol来实现。

4.2 代码生成


1. IFace接口

接口里的函数名不能重名,即使参数不一样也不行。
如果参数是个Struct,则会直接使用继承于TBase的生成类,对客户代码有一定侵入性。
默认抛出TException 这个checked exception,可自定义继承于TException的其他Exception类。

2. Processor类

继承于TBaseProcessor,简单构造出processMap,构造时需要传入用户实现的IFace实现类。

3. 接口里所有方法的方法类

继承于TProcessFunction,见前面TProcessFunction的描述。

4. Struct类,方法参数类和方法结果类

继承于Base。

读取Struct时,遇上fieldId为未知的,或者类型不同于期望类型的field,会被Skip掉。所谓skip,就是只按传过来的type,读取其内容推动数据流往前滚动,但不往field赋值,这也是为什么有了生成的代码,仍然要传输元数据的原因。

为了保持不同服务版本间的兼容性,永远对方法的参数与Struct的field只增不减不改就对了。

因为不同版本间,Struct的filed的数量未知,而StructEnd又无特殊标志,所以在Struct最后会放一个type=Stop的filed,读到则停止Struct的读入。

写入Struct时,Java对象(如String,Struct)或者设为optional的原始类型(如int)会先判断一下这个值被设置没有。Java对象只要判断其是否为null,原始类型就要额外增加一个bitset来记录该field是否已设置,根据field的数量,这个bitset是byte(8个)或short(16个)。

以上是StandardScheme的行为,每个Struct还会生成一种更节约空间但服务版本间不兼容的TupleScheme,它不再传输fieldId与type的元数据,只在Struct头写入一个bitset表明哪几个field有值,然后直接用生成的代码读取这些有值的field。所以如果新版idl中filed类型改动将出错;新的field也不会被读取数据流没有往前滚动,接下来也是错。

5. Thrift二进制与Restful JSON的编码效率对比

为了服务版本兼容,Thrift仍然需要传输数字型的fieldId,但比JSON的fieldName省地方。
int与byte[],当然比JSON的数字字符串和BASE64编码字符串省地方。
省掉了’:’和’,'
省掉了””,[], {}, 但作为代价,字符串,byte[],容器们都要有自己的长度定义,Struct也要有Stop Field。
但比起JSON,容器和Field又额外增加了类型定义的元数据。

 

5. 服务端

基类TServer相当于一个容器,拥有生产TProcessor、TTransport、TProtocol的工厂对象。改变这些工厂类,可以修饰包裹Transport与Protocol类,改变TProcessor的单例模式或与Spring集成等等。

https://github.com/m1ch1/mapke ... pared 这篇文章比较了各种Server

5.1 Blocking Server

TSimpleServer同时只能处理一个Client连接,只是个玩具。

TThreadPoolServer才是典型的多线程处理的Blocking Server实现。
线程池类似 Executors.newCachedThreadPool(),可设最小最大线程数(默认是5与无限)。
每条线程处理一个Client,如果所有线程都在忙,会等待一个random的时间重试直到设定的requestTimeout。
线程对于Client好像没有断开连接的机制,只靠捕获TTransportException来停止服务?

5.2 NonBlockingServer

TThreadedSelectorServer有一条线程专门负责accept,若干条Selector线程处理网络IO,一个Worker线程池处理消息。

THsHaServer只有一条AcceptSelect线程处理关于网络的一切,一个Worker线程池处理消息。

TNonblockingServer只有一条线程处理一切。

很明显TThreadedSelectorServer是被使用得最多的,因为在多核环境下多条Selector线程的表现会更好。所以只对它展开细读。

5.3 TThreadedSelectorServer

TThreadedSelectorServer属于 Half-Sync/Half-Async模式。

AcceptThread线程使用TNonblockingServerTransport执行accept操作,将accept到的Transport round-robin的交给其中一条SelectorThread。
因为SelectorThread自己也要处理IO,所以AcceptThread是先扔给SelectorThread里的Queue(默认长度只有4,满了就要阻塞等待)。

SelectorThread每个循环各执行一次如下动作
1. 注册Transport
2. select()处理IO
3. 处理FrameBuffer的状态变化

注册Transport时,在Selector中注册OP_READ,并创建一个带状态机(READING_FRAME_SIZE,READING_FRAME,READ_FRAME_COMPLETE, AWAITING_REGISTER_WRITE等)的FrameBuffer类与其绑定。

客户端必须使用FrameTransport(前4个字节记录Frame的长度)来发送数据以解决粘包拆包问题。

SelectorThread在每一轮的select()中,对有数据到达的Transport,其FrameBuffer先读取Frame的长度,然后创建这个长度的ByteBuffer继续读取数据,读满了就交给Worker线程中的Processor处理,没读够就继续下一轮循环。

当交给Processor处理时,Processor不像Blocking Server那样直接和当前的Transport打交道,而是实际将已读取到的Frame数据转存到一个MemoryTransport中,output时同样只是写到一个由内存中的ByteArray(初始大小为32)打底的OutputStream。

Worker线程池用newFixedThreadPool()创建,Processor会解包,调用用户实现,再把结果编码发送到前面传入的那个ByteArray中。FrameBuffer再把ByteArray转回ByteBuffer,状态转为AWAITING_REGISTER_WRITE,并在SelectorThread中注册该变化。

回到SelectorThread中,发现FrameBuffer的当前状态为AWAITING_REGISTER_WRITE,在Selector中注册OP_WRITE,等待写入的机会。在下一轮循环中就会开始写入数据,写完的话FrameBuffer又转到READING_FRAME_SIZE的状态,在Selector中重新注册OP_READ。

还有更多的状态机处理,略。

 

6. 客户端
6.1 同步客户端

同样通过生成器生成,其中Client类继承TClient基类实现服务的同步接口。

int add(int num1,int num2)会调用生成的send_add(num1, num2) 与 int receive_add()。
send_add()构造add_args()对象,调用父类的sendBase(“add",args),sendBase()调用protocol的writeMessage写入消息头,然后调用args自己的write(protocol),然后调用transport的flush()发送。
receive_add()构造add_result,调用父类的receive_base(add_result),receive_base()调用protocol层的readMessage读出消息头,如果类型是Exception则 用TApplicationException类来读取消息,否则调用result类的read()函数。

注意这里的seq_id并不支持并发访问,在发送时简单的+1,在接收时再进行比较,如果不对则会报错。因此Client好像是非线程安全的。

6.2 异步客户端

异步客户端,需要传入一个CallBack实现,在收到返回结果或错误时调用。

使用NonblockingSocket,每个客户端会起一条线程,在这条线程里忙活所有消息Non blocking发送,接收,编解码及调用Callback类,还有超时调用的处理。
其状态机的写法,TThreadedSelectorServer有相似之处,略。

 

7. Http

走Http协议,主要是利用了Thrift的二进制编解码机制,而放弃了它的底层NIO传输与服务线程模型。

THttpClient,使用Apache HttpClient或JDK的HttpURLConnection Post内容。

TServlet,作为Servlet,将request与response的stream交给Processor处理即可,不用处理线程模型与NIO,很简单。

 

8. Generator

Generator用C++编写,有Parser类分析thrift文件元数据模型,然后每种语言有自己的生成模型。

为Java生成代码的t_java_generator.cc有五千多行,不过很规整很容易看。

除了Java,一般还会生成Html的API描述文档。 查看全部
最近把Apache Thrift 的Java版代码翻了一遍,尝试理解做一个RPC框架所要考虑的方方面面。

网上关于Thrift设计的文章好像不多,于是把自己的笔记整理了一下发上来。

加插招聘广告:唯品会广州总部的基础架构部招人!! 如果你喜欢纯技术的工作,对大型互联网企业的服务化平台有兴趣,愿意在架构的成长期还可以大展拳脚的时候加盟,请电邮 calvin.xiao@vipshop.com内容大纲1. Overview
2. Transport层

2.1 Transport
2.2 WrapperTransport

3. Protocol层

3.1 IDL定义
3.2 TBinaryProtocol
3.3 TCompactProtocol
3.4 TTupleProtocol
3.5 TJSONProtocol

4. Processor层

4.1 基础接口

5. 服务端

5.1 Blocking Server
5.2 NonBlockingServer
5.3 TThreadedSelectorServer

6. 客户端

6.1 同步客户端
6.2 异步客户端

7. Http
8. Generator

1. Overview

Apache Thrift 的可赞之处是实现了跨超多语言(Java, C++, Go, Python,Ruby, PHP等等)的RPC框架,尽力为每种语言实现了相同抽象的RPC客户端与服务端。

简洁的四层接口抽象,每一层都可以独立的扩展增强或替换,是另一个可赞的地方。

最后,二进制的编解码方式和NIO的底层传输为它提供了不错的性能。

+-------------------------------------------+
| Server |
| (single-threaded, event-driven etc) |
+-------------------------------------------+
| Processor |
| (compiler generated) |
+-------------------------------------------+
| Protocol |
| (JSON, compact etc) |
+-------------------------------------------+
| Transport |
| (raw TCP, HTTP etc) |
+-------------------------------------------+

Transport层提供了一个简单的网络读写抽象层,有阻塞与非阻塞的TCP实现与HTTP的实现。

Protocol层定义了IDL中的数据结构与Transport层的传输数据格式之间的编解码机制。传输格式有二进制,压缩二进制,JSON等格式,IDL中的数据结构则包括Message,Struct,List,Map,Int,String,Bytes等。

Processor层由编译器编译IDL文件生成。
生成的代码会将传输层的数据解码为参数对象(比如商品对象有id与name两个属性,生成的代码会调用Protocol层的readInt与readString方法读出这两个属性值),然后调用由用户所实现的函数,并将结果编码送回。

在服务端, Server层创建并管理上面的三层,同时提供线程的调度管理。而对于NIO的实现,Server层可谓操碎了心。

在客户端, Client层由编译器直接生成,也由上面的三层代码组成。只要语言支持,客户端有同步与异步两种模式。

Facebook是Thrift的原作者,还开源有Nifty与Swift两个子项目, Cassandra是另一个著名用户,其跨语言的Client包就是基于Thrift的,这些在下一篇文章中展开讨论。

 

2. Transport层
2.1 Transport

TTransport除了open/close/flush,最重要的方法是int read(byte[] buf, int off, int len),void write(byte[] buf, int off, int len),读出、写入固定长度的内容。

TSocket使用经典的JDK Blocking IO的Transport实现。1K的BufferedStream, TCP配置项:KeepAlive=true,TCPNoDelay=true,SoLinger=false,SocketTimeout与ConnectionTimeout可配。

TNonblockingSocket,使用JDK NIO的Transport实现,读写的byte[]会每次被wrap成一个ByteBuffer。TCP配置项类似。
其实这个NonBlockingSocket并没有完全隔离传输层,后面异步Client或NIO的Server还有很多事情要做。

相应的,TServerSocket和TNonblockingServerSocket是ServerTransport的BIO、NIO实现,主要实现侦听端口,Accept后返回TSocket或TNonblockingSocket。其他TCP配置项:ReuseAddress=true,Backlog与SocketTimeout可配。

2.2 WrapperTransport

包裹一个底层的Transport,并利用自己的Buffer进行额外的操作。

1. TFramedTransport,按Frame读写数据。

每Frame的前4字节会记录Frame的长度(少于16M)。读的时候按长度预先将整Frame数据读入Buffer,再从Buffer慢慢读取。写的时候,每次flush将Buffer中的所有数据写成一个Frame。

有长度信息的TFramedTransport是后面NonBlockingServer粘包拆包的基础。

2. TFastFramedTransport 与TFramedTransport相比,始终使用相同的Buffer,提高了内存的使用率。

TFramedTransport的ReadBuffer每次读入Frame时都会创建新的byte[],WriteBuffer每次flush时如果大于初始1K也会重新创建byte[]。

而TFastFramedTransport始终使用相同的ReadBuffer和WriteBuffer,都是1K起步,不够时自动按1.5倍增长,和NIO的ByteBuffer一样,加上limit/pos这样的指针,每次重复使用时设置一下它们。

3. TZlibTransport ,读取时按1K为单位将数据读出并调用JDK的zip函数进行解压再放到Buffer,写入时,在flush时先zip再写入。

4. TSaslClientTransport与TSaslServerTransport, 提供SSL校验。

 

3. Protocol层
3.1 IDL定义

Thrift支持结构见 http://thrift.apache.org/docs/types

* 基本类型: i16,i32,i64, double, boolean,byte,byte[], String。
* 容器类型: List,Set,Map,TList/TSet/TMap类包含其元素的类型与元素的总个数。
* Struct类型,即面向对象的Class,继承于TBase。TStruct类有Name属性,还含有一系列的Field。TField类有自己的Name,类型,顺序id属性。
* Exception类型也是个Struct,继承于TException这个checked exception。
* Enum类型传输时是个i32。
* Message类型封装往返的RPC消息。TMessage类包含Name,类型(请求,返回,异常,ONEWAY)与seqId属性。

相应的,Protocol 层对上述数据结构有read与write的方法。
对基本类型是直接读写,对结构类型则是先调用readXXXBegin(),再调用其子元素的read()方法,再调用readXXXEnd()。
在所有函数中,Protocol层会直接调用Transport层读写特定长度的数据。

3.2 TBinaryProtocol

如前所述,i16,i32, double这些原始类型都是定长的,String,byte[]会在前4个字节说明自己的长度,容器类,Strutct类所对应的TMap,TStruct,TField,TMessage里有如前所述的属性 (不过Struct与Field里的name属性会被skip),所以其实现实可以简单想象的。

3.3 TCompactProtocol

比起TBinaryProtocol,会想方设法省点再省点。

1. 对整数类型使用了 ZigZag 压缩算法,数值越小压的越多。比如 i32 类型的整数本来是4个字节, 可以压缩成 1~5 字节不等。而 i64类型的整数本来是8个字节,可以压缩成 1~10 字节不等。 因此,值小的i32和i64,和小的Collection和短的String(因为它们都有定义长度的int属性) 越多,它就能省得越多。

2. 它还会尝试将Field的fieldId和type挤在一个byte里写。原本field是short,type也占一个byte,合共三个byte。 它将1个byte拆成两组4bit,前4bit放与前一个field的Id的delta值(不能相差超过15,如果中间太多没持久化的optional field),后4bit放type(目前刚好16种type,把byte[]和String合成一种了)

3.4 TTupleProtocol

继承于TCompactProtocol,Struct的编解码时使用更省地方但IDL间版本不兼容的TupleScheme,见Processor层。

3.5 TJSONProtocol

与我们平时的Restful JSON还是有点区别,具体的定义看cpp实现的TJSONProtool.h文件

对于容器类和Message,会用数组的方式,前几个元素是元信息,后面才是value,比如List是[type,size,[1,2,3]],

对于Struct,会是个Map,Key是fieldId而不是name(为了节省空间?),Value又是一个Map,只有一个Key-Value Pair,Key是type,Value才是真正value。

 

4. Processor层

建议使用一个最简单的IDL文件,用Windows版的Generator生成一个来进行观察。

4.1 基础接口

TBase是大部分生成的Struct,参数类,结果类的接口,最主要是实现从Protocol层读写自己的函数。

TProcessFunction是生成的服务方法类的基类,它的process函数会完成如下步骤:

1. 调用生成的args对象的read方法从protocol层读出自己
2. 调用子类生成的getResult()方法:拆分args对象得到参数,调用真正的用户实现得到结果,并组装成生成的result对象。
3. 写消息头,
4. 调用生成的result对象的write方法将自己写入protocol层
5 调用transport层的flush()。

TBaseProcessor 只有 boolean process(TProtocol in, TProtocol out) 这个简单接口,会先调readMessageBegin(),读出消息名,再从processMap里找出相应的TProcessFunction调用。

TMultiplexedProcessor,支持一个Server支持部署多个Service的情况,在Processor外面再加一层Map,消息名从“add”会变为“Caculator:add”,当然,这需要需要客户端发送时使用 TMultiplexedProtocol修饰原来的Protocol来实现。

4.2 代码生成


1. IFace接口

接口里的函数名不能重名,即使参数不一样也不行。
如果参数是个Struct,则会直接使用继承于TBase的生成类,对客户代码有一定侵入性。
默认抛出TException 这个checked exception,可自定义继承于TException的其他Exception类。

2. Processor类

继承于TBaseProcessor,简单构造出processMap,构造时需要传入用户实现的IFace实现类。

3. 接口里所有方法的方法类

继承于TProcessFunction,见前面TProcessFunction的描述。

4. Struct类,方法参数类和方法结果类

继承于Base。

读取Struct时,遇上fieldId为未知的,或者类型不同于期望类型的field,会被Skip掉。所谓skip,就是只按传过来的type,读取其内容推动数据流往前滚动,但不往field赋值,这也是为什么有了生成的代码,仍然要传输元数据的原因。

为了保持不同服务版本间的兼容性,永远对方法的参数与Struct的field只增不减不改就对了。

因为不同版本间,Struct的filed的数量未知,而StructEnd又无特殊标志,所以在Struct最后会放一个type=Stop的filed,读到则停止Struct的读入。

写入Struct时,Java对象(如String,Struct)或者设为optional的原始类型(如int)会先判断一下这个值被设置没有。Java对象只要判断其是否为null,原始类型就要额外增加一个bitset来记录该field是否已设置,根据field的数量,这个bitset是byte(8个)或short(16个)。

以上是StandardScheme的行为,每个Struct还会生成一种更节约空间但服务版本间不兼容的TupleScheme,它不再传输fieldId与type的元数据,只在Struct头写入一个bitset表明哪几个field有值,然后直接用生成的代码读取这些有值的field。所以如果新版idl中filed类型改动将出错;新的field也不会被读取数据流没有往前滚动,接下来也是错。

5. Thrift二进制与Restful JSON的编码效率对比

为了服务版本兼容,Thrift仍然需要传输数字型的fieldId,但比JSON的fieldName省地方。
int与byte[],当然比JSON的数字字符串和BASE64编码字符串省地方。
省掉了’:’和’,'
省掉了””,[], {}, 但作为代价,字符串,byte[],容器们都要有自己的长度定义,Struct也要有Stop Field。
但比起JSON,容器和Field又额外增加了类型定义的元数据。

 

5. 服务端

基类TServer相当于一个容器,拥有生产TProcessor、TTransport、TProtocol的工厂对象。改变这些工厂类,可以修饰包裹Transport与Protocol类,改变TProcessor的单例模式或与Spring集成等等。

https://github.com/m1ch1/mapke ... pared 这篇文章比较了各种Server

5.1 Blocking Server

TSimpleServer同时只能处理一个Client连接,只是个玩具。

TThreadPoolServer才是典型的多线程处理的Blocking Server实现。
线程池类似 Executors.newCachedThreadPool(),可设最小最大线程数(默认是5与无限)。
每条线程处理一个Client,如果所有线程都在忙,会等待一个random的时间重试直到设定的requestTimeout。
线程对于Client好像没有断开连接的机制,只靠捕获TTransportException来停止服务?

5.2 NonBlockingServer

TThreadedSelectorServer有一条线程专门负责accept,若干条Selector线程处理网络IO,一个Worker线程池处理消息。

THsHaServer只有一条AcceptSelect线程处理关于网络的一切,一个Worker线程池处理消息。

TNonblockingServer只有一条线程处理一切。

很明显TThreadedSelectorServer是被使用得最多的,因为在多核环境下多条Selector线程的表现会更好。所以只对它展开细读。

5.3 TThreadedSelectorServer

TThreadedSelectorServer属于 Half-Sync/Half-Async模式。

AcceptThread线程使用TNonblockingServerTransport执行accept操作,将accept到的Transport round-robin的交给其中一条SelectorThread。
因为SelectorThread自己也要处理IO,所以AcceptThread是先扔给SelectorThread里的Queue(默认长度只有4,满了就要阻塞等待)。

SelectorThread每个循环各执行一次如下动作
1. 注册Transport
2. select()处理IO
3. 处理FrameBuffer的状态变化

注册Transport时,在Selector中注册OP_READ,并创建一个带状态机(READING_FRAME_SIZE,READING_FRAME,READ_FRAME_COMPLETE, AWAITING_REGISTER_WRITE等)的FrameBuffer类与其绑定。

客户端必须使用FrameTransport(前4个字节记录Frame的长度)来发送数据以解决粘包拆包问题。

SelectorThread在每一轮的select()中,对有数据到达的Transport,其FrameBuffer先读取Frame的长度,然后创建这个长度的ByteBuffer继续读取数据,读满了就交给Worker线程中的Processor处理,没读够就继续下一轮循环。

当交给Processor处理时,Processor不像Blocking Server那样直接和当前的Transport打交道,而是实际将已读取到的Frame数据转存到一个MemoryTransport中,output时同样只是写到一个由内存中的ByteArray(初始大小为32)打底的OutputStream。

Worker线程池用newFixedThreadPool()创建,Processor会解包,调用用户实现,再把结果编码发送到前面传入的那个ByteArray中。FrameBuffer再把ByteArray转回ByteBuffer,状态转为AWAITING_REGISTER_WRITE,并在SelectorThread中注册该变化。

回到SelectorThread中,发现FrameBuffer的当前状态为AWAITING_REGISTER_WRITE,在Selector中注册OP_WRITE,等待写入的机会。在下一轮循环中就会开始写入数据,写完的话FrameBuffer又转到READING_FRAME_SIZE的状态,在Selector中重新注册OP_READ。

还有更多的状态机处理,略。

 

6. 客户端
6.1 同步客户端

同样通过生成器生成,其中Client类继承TClient基类实现服务的同步接口。

int add(int num1,int num2)会调用生成的send_add(num1, num2) 与 int receive_add()。
send_add()构造add_args()对象,调用父类的sendBase(“add",args),sendBase()调用protocol的writeMessage写入消息头,然后调用args自己的write(protocol),然后调用transport的flush()发送。
receive_add()构造add_result,调用父类的receive_base(add_result),receive_base()调用protocol层的readMessage读出消息头,如果类型是Exception则 用TApplicationException类来读取消息,否则调用result类的read()函数。

注意这里的seq_id并不支持并发访问,在发送时简单的+1,在接收时再进行比较,如果不对则会报错。因此Client好像是非线程安全的。

6.2 异步客户端

异步客户端,需要传入一个CallBack实现,在收到返回结果或错误时调用。

使用NonblockingSocket,每个客户端会起一条线程,在这条线程里忙活所有消息Non blocking发送,接收,编解码及调用Callback类,还有超时调用的处理。
其状态机的写法,TThreadedSelectorServer有相似之处,略。

 

7. Http

走Http协议,主要是利用了Thrift的二进制编解码机制,而放弃了它的底层NIO传输与服务线程模型。

THttpClient,使用Apache HttpClient或JDK的HttpURLConnection Post内容。

TServlet,作为Servlet,将request与response的stream交给Processor处理即可,不用处理线程模型与NIO,很简单。

 

8. Generator

Generator用C++编写,有Parser类分析thrift文件元数据模型,然后每种语言有自己的生成模型。

为Java生成代码的t_java_generator.cc有五千多行,不过很规整很容易看。

除了Java,一般还会生成Html的API描述文档。

ClassLoader, JavaAgent, Aspectj Weaving一站式扫盲帖

默认分类admin 发表了文章 • 0 个评论 • 155 次浏览 • 2017-01-17 16:56 • 来自相关话题

最近工作里复习的Class Loader基础知识集锦,写下来希望对别人有帮助,而且不止是为了撂倒面试官。

为了尽量简单明了容易背,有些部分写得比较干。

0. 参考资料:

书:《深入了解Java虚拟机》、《实战Java虚拟机》
规范:Java语言规范 第12章
源码: OpenJDK 7的Java及C代码( Class.c , Classloader.c,jvm.cpp)

 

1. Class装载的三个阶段
1.1 载入 (Load)

从Class文件或别的什么地方载入一段二进制流字节流,把它解释成永久代里的运行时数据结构,生成一个Class对象。
同时也会装载它的父类或父接口,并进行校验,比如编译版本不对会抛出UnsupportedClassError,依赖定义不对会抛 IncompatibleClassChangeError/ClassCircularityError等。

1.2 链接 (Resolve)

将之前载入的数据结构里的符号引用表,解析成直接引用。
中间如果遇到引用的类还没被加载,就会触发该类的加载。
可能JDK会很懒惰的在运行某个函数实际使用到该引用时才发生链接,也可能在类加载时就解析全部引用。

1.3 初始化 (Initniazle)

初始化静态变量,并执行静态初始化语句。

 

2. Class装载的时机

ClassLoader.loadClass()
前文所说的链接时触发的装载
Class.forName() 等java.lang.reflect反射包
new 构造对象
初始化子类时,会同时初始化父类
访问类的静态变量或静态方法(但static final的常量除外,此君在常量池里)

本质上,也是很懒惰的按需加载的,由于类装载的Lazy和前面解释引用的Lazy,所以Jar包里有时候有些类用到的了没在Class Path里的其他类,也能人品爆发的照跑不误。

除了时机1,其他几种方式默认都到达类装载的初始化阶段。

 

3. ClassLoader.loadClass() 与 Class.forName()

ClassLoader.loadClass(String name, boolean resolve),其中resolve默认为false,即只执行类装载的第一个阶段。

Class.forName(String name, boolean initialize, ClassLoader loader), 其中initialize默认为true,即执行到类装载的第三个阶段。

 

4. ClassNotFoundException 和 NoClassDefFoundError

ClassLoader.loadClass() 与 Class.forName() 找不到类定义的二进制流时抛出ClassNotFoundException。

链接阶段解释引用失败,找不到引用的类时抛出NoClassDefFoundError。

其他常见错误包括编译版本不对的

 

5. ClassLoader及双亲委派机制

ClassLoader.loadClass()的标准流程:

findLoadedClass() 查看类是否已加载
如果不存在,则调用parent loader的loadClass()
如果不存在,调用findClass() 在本ClassLoader的ClassPath里加载该类

所谓双亲委派机制,就是先从parent loader开始查找,找不到了才用自己的findClass()函数去查找,兼顾了效率:避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次,和安全:避免子类乱加载。

而OSGI或SPI或热替换方案,则需要破坏这个双亲委托,先调用自己的findClass()。

findClass() 是各个ClassLoader各自实现,各显神通的地方,从各种奇葩地方载入Class二进制字节流。

但最后都会调用defineClass(),传入二进制字节流,返回Class对象。留意此处,呆会AspectJ的时候会回到这里。

在JDK6,loadClass()很过分的定义了方法级的synchronized ,在JDK7改成一个以Class Name作Key的 parallelLockMap,增强了并行加载不同Class的能力。

 

6. System ClassLoader 与 Thread Context Classloader

有时候,看到错误日志说张三不是张三,包名类名一样但instanceof 死活返回 false,唯一原因是它们由两个不同的ClassLoader加载。

默认的Bootstrap(加载jdk的lib目录),Extension(加载jdk的lib/ext目录),Application(加载启动时定义的classpath)三层ClassLoader机制不再重复。

平时用ClassLoader.getSystemClassLoader()就可以得到sun.misc.Launcher$ApplicationClassLoader 这个Application ClassLoader。

在类A里加载类B,默认使用加载了类A的Loader。但,也有特殊情况,比如JDBC加载driver时的机制,需要在Bootstrap ClassLoader(JDBC属于JDK一部分)里根据配置反射创建jdbc driver的数据实现类,此时Bootstrap Classloader并没有某个jdbc driver实现类的包(在Application Classloaer中),因此Sun设计了一个特殊方案 --Thread Context Class Loader。

JAXB(也是JDK一部分,比如要在Jar包里找xsd schema文件的时候)也使用了它,所以用到它们时就要注意Thread Context ClassLoader的设置,将Application Classloader设进去。可以用代码随时设置current thread的loader,也可以用自定义的ThreadFactory在创建线程时设置,它默认是父线程的loader,如果都没设置就是 System ClassLoader这个Application ClassLoader。

 

7. Java Agent机制与AspectJ的LoadTime Weaving

在JDK5开始,在启动JVM时可增加-javaagent参数,在装载Class时对类进行动态的修改。

AspectJ的Load Time Weaving机制,需要配置 -javaagent: [path to aspectj-weaver.jar] 。

打开aspectj-weaver.jar,可以看到META-INF/MANIFEST里定义了 Premain-Class: org.aspectj.weaver.loadtime.Agent

再打开这个Agent类,简化后的代码大概这个样子:

 

ClassFileTransformer s_transformer = new ClassPreProcessorAgentAdapter();

 

public static void premain(String options, Instrumentation instrumentation) {

 

instrumentation.addTransformer(s_transformer);

 

}

可见它的主要作用是将自己的类转换器注册到JDK所传入的Instrumentation。

再看类转换器的定义:ClassLoader会在前面defineClass()的过程中,在把二进制字节流转换为Class对象之前,先把二进制流和当前ClassLoader传给转换器,由转换器加工为另一段二进制字节流返回。

AspectJ就是利用传入的ClassLoader,找出其Class Path里的META-INF/aop.xml,然后根据aop.xml里的配置进行代码植入。

测试显示,加了LoadTime Weaving,类加载的速度明显变慢,如果是100ms就调用超时的服务,需要做类的预加载。

 

8. Jar包的预加载

比如有个有趣的需求是加载某个Class A所在的Jar里的全部的Class (怎么好像一点都不有趣)

 

URL jarUrl = ClassA.getProtectionDomain().getCodeSource().getLocation();
JarFile jarfile = new JarFile(jarUrl.getPath());
Enumeration entries = jarfile.entries();

然后遍历JarEntry,过滤出后缀为.class的文件,按类名进行装载就可以了。

 

9.Class的二进制兼容性

如果Class A 依赖 spring-1.0.jar编译,当spring升级到spring-2.0.jar,Class A不需要修改代码也不需要重新编译,可以直接运行的,spring-2.0.jar就满足二进制兼容性。

在Java语言规范的第13章有详细的描述 ,不想直接睡着最好可以找个中文版来看,感谢那些翻译的同学。

虽然规范的这章看着比较长比较吓人,但其实二进制兼容性还是很容易做到的,只要你不做把接口改为抽象类之类或反过来这类奇怪的事情,其他一些看起来很大的改动,比如改throws定义,其实都没有问题。

真的遇到问题,设身处地想想自己是那段Class A的字节码,现在还能不能跑就行。

 

感谢你看到这里,希望你只在工作里用到这些知识,祝工作愉快。

文章持续修订,转载请保留原链接:
http://calvin1978.blogcn.com/a ... .html 查看全部
最近工作里复习的Class Loader基础知识集锦,写下来希望对别人有帮助,而且不止是为了撂倒面试官。

为了尽量简单明了容易背,有些部分写得比较干。

0. 参考资料:

书:《深入了解Java虚拟机》、《实战Java虚拟机》
规范:Java语言规范 第12章
源码: OpenJDK 7的Java及C代码( Class.c , Classloader.c,jvm.cpp)

 

1. Class装载的三个阶段
1.1 载入 (Load)

从Class文件或别的什么地方载入一段二进制流字节流,把它解释成永久代里的运行时数据结构,生成一个Class对象。
同时也会装载它的父类或父接口,并进行校验,比如编译版本不对会抛出UnsupportedClassError,依赖定义不对会抛 IncompatibleClassChangeError/ClassCircularityError等。

1.2 链接 (Resolve)

将之前载入的数据结构里的符号引用表,解析成直接引用。
中间如果遇到引用的类还没被加载,就会触发该类的加载。
可能JDK会很懒惰的在运行某个函数实际使用到该引用时才发生链接,也可能在类加载时就解析全部引用。

1.3 初始化 (Initniazle)

初始化静态变量,并执行静态初始化语句。

 

2. Class装载的时机

ClassLoader.loadClass()
前文所说的链接时触发的装载
Class.forName() 等java.lang.reflect反射包
new 构造对象
初始化子类时,会同时初始化父类
访问类的静态变量或静态方法(但static final的常量除外,此君在常量池里)

本质上,也是很懒惰的按需加载的,由于类装载的Lazy和前面解释引用的Lazy,所以Jar包里有时候有些类用到的了没在Class Path里的其他类,也能人品爆发的照跑不误。

除了时机1,其他几种方式默认都到达类装载的初始化阶段。

 

3. ClassLoader.loadClass() 与 Class.forName()

ClassLoader.loadClass(String name, boolean resolve),其中resolve默认为false,即只执行类装载的第一个阶段。

Class.forName(String name, boolean initialize, ClassLoader loader), 其中initialize默认为true,即执行到类装载的第三个阶段。

 

4. ClassNotFoundException 和 NoClassDefFoundError

ClassLoader.loadClass() 与 Class.forName() 找不到类定义的二进制流时抛出ClassNotFoundException。

链接阶段解释引用失败,找不到引用的类时抛出NoClassDefFoundError。

其他常见错误包括编译版本不对的

 

5. ClassLoader及双亲委派机制

ClassLoader.loadClass()的标准流程:

findLoadedClass() 查看类是否已加载
如果不存在,则调用parent loader的loadClass()
如果不存在,调用findClass() 在本ClassLoader的ClassPath里加载该类

所谓双亲委派机制,就是先从parent loader开始查找,找不到了才用自己的findClass()函数去查找,兼顾了效率:避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次,和安全:避免子类乱加载。

而OSGI或SPI或热替换方案,则需要破坏这个双亲委托,先调用自己的findClass()。

findClass() 是各个ClassLoader各自实现,各显神通的地方,从各种奇葩地方载入Class二进制字节流。

但最后都会调用defineClass(),传入二进制字节流,返回Class对象。留意此处,呆会AspectJ的时候会回到这里。

在JDK6,loadClass()很过分的定义了方法级的synchronized ,在JDK7改成一个以Class Name作Key的 parallelLockMap,增强了并行加载不同Class的能力。

 

6. System ClassLoader 与 Thread Context Classloader

有时候,看到错误日志说张三不是张三,包名类名一样但instanceof 死活返回 false,唯一原因是它们由两个不同的ClassLoader加载。

默认的Bootstrap(加载jdk的lib目录),Extension(加载jdk的lib/ext目录),Application(加载启动时定义的classpath)三层ClassLoader机制不再重复。

平时用ClassLoader.getSystemClassLoader()就可以得到sun.misc.Launcher$ApplicationClassLoader 这个Application ClassLoader。

在类A里加载类B,默认使用加载了类A的Loader。但,也有特殊情况,比如JDBC加载driver时的机制,需要在Bootstrap ClassLoader(JDBC属于JDK一部分)里根据配置反射创建jdbc driver的数据实现类,此时Bootstrap Classloader并没有某个jdbc driver实现类的包(在Application Classloaer中),因此Sun设计了一个特殊方案 --Thread Context Class Loader。

JAXB(也是JDK一部分,比如要在Jar包里找xsd schema文件的时候)也使用了它,所以用到它们时就要注意Thread Context ClassLoader的设置,将Application Classloader设进去。可以用代码随时设置current thread的loader,也可以用自定义的ThreadFactory在创建线程时设置,它默认是父线程的loader,如果都没设置就是 System ClassLoader这个Application ClassLoader。

 

7. Java Agent机制与AspectJ的LoadTime Weaving

在JDK5开始,在启动JVM时可增加-javaagent参数,在装载Class时对类进行动态的修改。

AspectJ的Load Time Weaving机制,需要配置 -javaagent: [path to aspectj-weaver.jar] 。

打开aspectj-weaver.jar,可以看到META-INF/MANIFEST里定义了 Premain-Class: org.aspectj.weaver.loadtime.Agent

再打开这个Agent类,简化后的代码大概这个样子:

 

ClassFileTransformer s_transformer = new ClassPreProcessorAgentAdapter();

 

public static void premain(String options, Instrumentation instrumentation) {

 

instrumentation.addTransformer(s_transformer);

 

}

可见它的主要作用是将自己的类转换器注册到JDK所传入的Instrumentation。

再看类转换器的定义:ClassLoader会在前面defineClass()的过程中,在把二进制字节流转换为Class对象之前,先把二进制流和当前ClassLoader传给转换器,由转换器加工为另一段二进制字节流返回。

AspectJ就是利用传入的ClassLoader,找出其Class Path里的META-INF/aop.xml,然后根据aop.xml里的配置进行代码植入。

测试显示,加了LoadTime Weaving,类加载的速度明显变慢,如果是100ms就调用超时的服务,需要做类的预加载。

 

8. Jar包的预加载

比如有个有趣的需求是加载某个Class A所在的Jar里的全部的Class (怎么好像一点都不有趣)

 

URL jarUrl = ClassA.getProtectionDomain().getCodeSource().getLocation();
JarFile jarfile = new JarFile(jarUrl.getPath());
Enumeration entries = jarfile.entries();

然后遍历JarEntry,过滤出后缀为.class的文件,按类名进行装载就可以了。

 

9.Class的二进制兼容性

如果Class A 依赖 spring-1.0.jar编译,当spring升级到spring-2.0.jar,Class A不需要修改代码也不需要重新编译,可以直接运行的,spring-2.0.jar就满足二进制兼容性。

在Java语言规范的第13章有详细的描述 ,不想直接睡着最好可以找个中文版来看,感谢那些翻译的同学。

虽然规范的这章看着比较长比较吓人,但其实二进制兼容性还是很容易做到的,只要你不做把接口改为抽象类之类或反过来这类奇怪的事情,其他一些看起来很大的改动,比如改throws定义,其实都没有问题。

真的遇到问题,设身处地想想自己是那段Class A的字节码,现在还能不能跑就行。

 

感谢你看到这里,希望你只在工作里用到这些知识,祝工作愉快。

文章持续修订,转载请保留原链接:
http://calvin1978.blogcn.com/a ... .html

Java ThreadPool的正确打开方式

默认分类admin 发表了文章 • 0 个评论 • 154 次浏览 • 2017-01-17 16:45 • 来自相关话题

原文链接:http://calvin1978.blogcn.com/a ... .html
 线程池应对于突然增大、来不及处理的请求,无非两种应对方式:

将未完成的请求放在队列里等待
临时增加处理线程,等高峰回落后再结束临时线程

JDK的Executors.newFixedPool() 和newCachedPool(),分别使用了这两种方式。

不过,这俩函数在方便之余,也屏蔽了ThreadPool原本多样的配置,对一些不求甚解的码农来说,就错过了一些更适合自己项目的选择。

 

1. ThreadPoolExecutor的原理

经典书《Java Concurrency in Pratice(Java并发编程实战)》的第8章,浓缩如下:

1. 每次提交任务时,如果线程数还没达到coreSize就创建新线程并绑定该任务。
所以第coreSize次提交任务后线程总数必达到coreSize,不会重用之前的空闲线程。
在生产环境,为了避免首次调用超时,可以调用executor.prestartCoreThread()预创建所有core线程,避免来一个创一个带来首次调用慢的问题。

2. 线程数达到coreSize后,新增的任务就放到工作队列里,而线程池里的线程则努力的使用take()阻塞地从工作队列里拉活来干。

3. 如果队列是个有界队列,又如果线程池里的线程不能及时将任务取走,工作队列可能会满掉,插入任务就会失败,此时线程池就会紧急的再创建新的临时线程来补救。

4. 临时线程使用poll(keepAliveTime,timeUnit)来从工作队列拉活,如果时候到了仍然两手空空没拉到活,表明它太闲了,就会被解雇掉。

5. 如果core线程数+临时线程数 >maxSize,则不能再创建新的临时线程了,转头执行RejectExecutionHanlder。默认的AbortPolicy抛RejectedExecutionException异常,其他选择包括静默放弃当前任务(Discard),放弃工作队列里最老的任务(DisacardOldest),或由主线程来直接执行(CallerRuns),或你自己发挥想象力写的一个。

 

2. FixedPool 与 CachedPool

FixedPool默认用了一条无界的工作队列 LinkedBlockingQueue, 所以只去到上面的第2步就不会继续往下走了,coreSize的线程做不完的任务不断堆积到无限长的Queue中。
所以只有coreSize一个参数,其他maxSize,keepAliveTime,RejectHandler的配置都不会实际生效。

CachedPool则把coreSize设成0,然后选用了一种特殊的Queue -- SynchronousQueue,只要当前没有空闲线程,Queue就会立刻报插入失败,让线程池增加新的临时线程,默认的KeepAliveTime是1分钟,而且maxSize是整型的最大值,也就是说只要有干不完的活,都会无限增增加线程数,直到高峰过去线程数才会回落。

 

3. 对FixedPool的进一步配置
3.1 设置QueueSize

如果不想搞一条无限长的Queue,避免任务无限等待显得像假死,同时占用太多内存,可能会把它换成一条有界的ArrayBlockingQueue,那就要同时关注一下这条队列满了之后的场景,选择正确的rejectHanlder。

此时,最好还是把maxSize设为coreSize一样的值,不把临时线程及其keepAlive时间拉进来,Queue+临时线程两者结合听是好听,但很难设置好。

3.2 有界队列选LinkedBlockingQueue 还是ArrayBlockingQueue?

按Executors的JavaDoc上说是ArrayBlockingQueue,起码ArrayBlockingQueue每插入一个Runnable就直接放到内部的数组里,而LinkedBlockingQueue则要 new Node(runnable),无疑会产生更多对象。而性能方面有兴趣的同学可以自己测一下。

allowCoreThreadTimeOut(true)

允许core线程也在完全没流量时收缩到0,但因为JDK的算法,只要当前线程数低于core,请求一来就会创建线程,不管现在有没有空闲的线程能服务这个请求,所以这个选项的作用有限,仅在完全没流量时有效。 但都完全没流量了,怎么滴其实也没所谓了。除非是同时有很多个线程池的情况。

 

4. 对CachedPool的进一步配置
4.1 设置coreSize

coreSize默认为0,但很多时候也希望是一个类似FixedPool的固定值,能处理大部分的情况,不要有太多加加减减的波动,等待和消耗的精力。

4.2 设置maxSize及rejectHandler

同理,maxSize默认是整形最大值,但太多的线程也很可能让系统崩溃,所以建议还是设一下maxSize和rejectHandler。

4.3 设置keepAliveTime

默认1分钟,可以根据项目再设置一把。

4.4 SynchronousQueue的性能?

高并发下,SynchronousQueue的性能绝对比LinkedBlockingQueue/ArrayBlockingQueue低一大截。虽然JDK6的实现号称比JDK5的改进很多,但还是慢,据文章说只在20线程并发下它才是快的。

 

5. SpringSide的ThreadPoolBuilder

广告时间,SpringSide的ThreadPoolBuilder能简化上述的配置。

此文太科普太水,主要就是为了帮SpringSide-Utils项目打广告:) 查看全部
原文链接:http://calvin1978.blogcn.com/a ... .html
 线程池应对于突然增大、来不及处理的请求,无非两种应对方式:

将未完成的请求放在队列里等待
临时增加处理线程,等高峰回落后再结束临时线程

JDK的Executors.newFixedPool() 和newCachedPool(),分别使用了这两种方式。

不过,这俩函数在方便之余,也屏蔽了ThreadPool原本多样的配置,对一些不求甚解的码农来说,就错过了一些更适合自己项目的选择。

 

1. ThreadPoolExecutor的原理

经典书《Java Concurrency in Pratice(Java并发编程实战)》的第8章,浓缩如下:

1. 每次提交任务时,如果线程数还没达到coreSize就创建新线程并绑定该任务。
所以第coreSize次提交任务后线程总数必达到coreSize,不会重用之前的空闲线程。
在生产环境,为了避免首次调用超时,可以调用executor.prestartCoreThread()预创建所有core线程,避免来一个创一个带来首次调用慢的问题。

2. 线程数达到coreSize后,新增的任务就放到工作队列里,而线程池里的线程则努力的使用take()阻塞地从工作队列里拉活来干。

3. 如果队列是个有界队列,又如果线程池里的线程不能及时将任务取走,工作队列可能会满掉,插入任务就会失败,此时线程池就会紧急的再创建新的临时线程来补救。

4. 临时线程使用poll(keepAliveTime,timeUnit)来从工作队列拉活,如果时候到了仍然两手空空没拉到活,表明它太闲了,就会被解雇掉。

5. 如果core线程数+临时线程数 >maxSize,则不能再创建新的临时线程了,转头执行RejectExecutionHanlder。默认的AbortPolicy抛RejectedExecutionException异常,其他选择包括静默放弃当前任务(Discard),放弃工作队列里最老的任务(DisacardOldest),或由主线程来直接执行(CallerRuns),或你自己发挥想象力写的一个。

 

2. FixedPool 与 CachedPool

FixedPool默认用了一条无界的工作队列 LinkedBlockingQueue, 所以只去到上面的第2步就不会继续往下走了,coreSize的线程做不完的任务不断堆积到无限长的Queue中。
所以只有coreSize一个参数,其他maxSize,keepAliveTime,RejectHandler的配置都不会实际生效。

CachedPool则把coreSize设成0,然后选用了一种特殊的Queue -- SynchronousQueue,只要当前没有空闲线程,Queue就会立刻报插入失败,让线程池增加新的临时线程,默认的KeepAliveTime是1分钟,而且maxSize是整型的最大值,也就是说只要有干不完的活,都会无限增增加线程数,直到高峰过去线程数才会回落。

 

3. 对FixedPool的进一步配置
3.1 设置QueueSize

如果不想搞一条无限长的Queue,避免任务无限等待显得像假死,同时占用太多内存,可能会把它换成一条有界的ArrayBlockingQueue,那就要同时关注一下这条队列满了之后的场景,选择正确的rejectHanlder。

此时,最好还是把maxSize设为coreSize一样的值,不把临时线程及其keepAlive时间拉进来,Queue+临时线程两者结合听是好听,但很难设置好。

3.2 有界队列选LinkedBlockingQueue 还是ArrayBlockingQueue?

按Executors的JavaDoc上说是ArrayBlockingQueue,起码ArrayBlockingQueue每插入一个Runnable就直接放到内部的数组里,而LinkedBlockingQueue则要 new Node(runnable),无疑会产生更多对象。而性能方面有兴趣的同学可以自己测一下。

allowCoreThreadTimeOut(true)

允许core线程也在完全没流量时收缩到0,但因为JDK的算法,只要当前线程数低于core,请求一来就会创建线程,不管现在有没有空闲的线程能服务这个请求,所以这个选项的作用有限,仅在完全没流量时有效。 但都完全没流量了,怎么滴其实也没所谓了。除非是同时有很多个线程池的情况。

 

4. 对CachedPool的进一步配置
4.1 设置coreSize

coreSize默认为0,但很多时候也希望是一个类似FixedPool的固定值,能处理大部分的情况,不要有太多加加减减的波动,等待和消耗的精力。

4.2 设置maxSize及rejectHandler

同理,maxSize默认是整形最大值,但太多的线程也很可能让系统崩溃,所以建议还是设一下maxSize和rejectHandler。

4.3 设置keepAliveTime

默认1分钟,可以根据项目再设置一把。

4.4 SynchronousQueue的性能?

高并发下,SynchronousQueue的性能绝对比LinkedBlockingQueue/ArrayBlockingQueue低一大截。虽然JDK6的实现号称比JDK5的改进很多,但还是慢,据文章说只在20线程并发下它才是快的。

 

5. SpringSide的ThreadPoolBuilder

广告时间,SpringSide的ThreadPoolBuilder能简化上述的配置。

此文太科普太水,主要就是为了帮SpringSide-Utils项目打广告:)

StringBuilder在高性能场景下的正确用法

默认分类admin 发表了文章 • 0 个评论 • 164 次浏览 • 2017-01-17 16:44 • 来自相关话题

原文链接:http://calvin1978.blogcn.com/a ... .html
 关于StringBuilder,一般同学只简单记住了,字符串拼接要用StringBuilder,不要用+,也不要用StringBuffer,然后性能就是最好的了,真的吗吗吗吗?

还有些同学,还听过三句似是而非的经验:

1. Java编译优化后+和StringBuilder的效果一样;

2. StringBuilder不是线程安全的,为了“安全”起见最好还是用StringBuffer;

3. 永远不要自己拼接日志信息的字符串,交给slf4j来。

 

1. 初始长度好重要,值得说四次。

StringBuilder的内部有一个char[], 不断的append()就是不断的往char[]里填东西的过程。

new StringBuilder() 时char[]的默认长度是16,然后,如果要append第17个字符,怎么办?

用System.arraycopy成倍复制扩容!!!!

这样一来有数组拷贝的成本,二来原来的char[]也白白浪费了要被GC掉。可以想见,一个129字符长度的字符串,经过了16,32,64, 128四次的复制和丢弃,合共申请了496字符的数组,在高性能场景下,这几乎不能忍。

所以,合理设置一个初始值多重要。

但如果我实在估算不好呢?多估一点点好了,只要字符串最后大于16,就算浪费一点点,也比成倍的扩容好。

 

2. Liferay的StringBundler类

Liferay的StringBundler类提供了另一个长度设置的思路,它在append()的时候,不急着往char[]里塞东西,而是先拿一个String[]把它们都存起来,到了最后才把所有String的length加起来,构造一个合理长度的StringBuilder。

 

3. 但,还是浪费了一倍的char[]

浪费发生在最后一步,StringBuilder.toString()

 

//创建拷贝, 不共享数组
return new String(value, 0, count);

String的构造函数会用 System.arraycopy()复制一把传入的char[]来保证安全性不可变性,如果故事就这样结束,StringBuilder里的char[]还是被白白牺牲了。

为了不浪费这些char[],一种方法是用Unsafe之类的各种黑科技,绕过构造函数直接给String的char[]属性赋值,但很少人这样做。

另一个靠谱一些的办法就是重用StringBuilder。而重用,还解决了前面的长度设置问题,因为即使一开始估算不准,多扩容几次之后也够了。

 

4. 重用StringBuilder

这个做法来源于JDK里的BigDecimal类(没事看看JDK代码多重要),后来发现Netty也同样使用。SpringSide里将代码提取成StringBuilderHolder,里面只有一个函数

 

public StringBuilder getStringBuilder() {
sb.setLength(0);
return sb;
}

StringBuilder.setLength()函数只重置它的count指针,而char[]则会继续重用,而toString()时会把当前的count指针也作为参数传给String的构造函数,所以不用担心把超过新内容大小的旧内容也传进去了。可见,StringBuilder是完全可以被重用的。

为了避免并发冲突,这个Holder一般设为ThreadLocal,标准写法见BigDecimal或StringBuilderHolder的注释。

不过,如果String的长度不大,那从ThreadLocal里取一次值的代价还更大的多,所以也不能把这个ThreadLocalStringBuilder搞出来后,见到StringBuilder就替换。。。
 

5. + 与 StringBuilder

 

String s = “hello ” + user.getName();

这一句经过javac编译后的效果,的确等价于使用StringBuilder,但没有设定长度。

 

String s = new StringBuilder().append(“hello”).append(user.getName());

但是,如果像下面这样:

 

String s = “hello ”;
// 隔了其他一些语句
s = s + user.getName();

每一条语句,都会生成一个新的StringBuilder,这里就有了两个StringBuilder,性能就完全不一样了。如果是在循环体里s+=i; 就更加多得没谱。

据R大说,努力的JVM工程师们在运行优化阶段, 根据+XX:+OptimizeStringConcat(JDK7u40后默认打开),把相邻的(中间没隔着控制语句) StringBuilder合成一个,也会努力的猜长度。

所以,保险起见还是继续自己用StringBuilder并设定长度好了。

 

6. StringBuffer 与 StringBuilder

StringBuffer与StringBuilder都是继承于AbstractStringBuilder,唯一的区别就是StringBuffer的函数上都有synchronized关键字。

那些说StringBuffer “安全”的同学,其实你几时看过几个线程轮流append一个StringBuffer的情况???

 

7. 永远把日志的字符串拼接交给slf4j??

 

logger.info("Hello {}", user.getName());

对于不知道要不要输出的日志,交给slf4j在真的需要输出时才去拼接的确能省节约成本。

但对于一定要输出的日志,直接自己用StringBuilder拼接更快。因为看看slf4j的实现,实际上就是不断的indexof("{}"), 不断的subString(),再不断的用StringBuilder拼起来而已,没有银弹。

PS. slf4j中的StringBuilder在原始Message之外预留了50个字符,如果可变参数加起来长过50字符还是得复制扩容......而且StringBuilder也没有重用。

 

8. 小结

StringBuilder默认的写法,会为129长度的字符串拼接,合共申请625字符的数组。所以高性能的场景下,永远要考虑用一个ThreadLocal 可重用的StringBuilder。而且重用之后,就不用再玩猜长度的游戏了。当然,如果字符串只有一百几十字节,也不一定要考虑重用,设好初始值就好。 查看全部
原文链接:http://calvin1978.blogcn.com/a ... .html
 关于StringBuilder,一般同学只简单记住了,字符串拼接要用StringBuilder,不要用+,也不要用StringBuffer,然后性能就是最好的了,真的吗吗吗吗?

还有些同学,还听过三句似是而非的经验:

1. Java编译优化后+和StringBuilder的效果一样;

2. StringBuilder不是线程安全的,为了“安全”起见最好还是用StringBuffer;

3. 永远不要自己拼接日志信息的字符串,交给slf4j来。

 

1. 初始长度好重要,值得说四次。

StringBuilder的内部有一个char[], 不断的append()就是不断的往char[]里填东西的过程。

new StringBuilder() 时char[]的默认长度是16,然后,如果要append第17个字符,怎么办?

用System.arraycopy成倍复制扩容!!!!

这样一来有数组拷贝的成本,二来原来的char[]也白白浪费了要被GC掉。可以想见,一个129字符长度的字符串,经过了16,32,64, 128四次的复制和丢弃,合共申请了496字符的数组,在高性能场景下,这几乎不能忍。

所以,合理设置一个初始值多重要。

但如果我实在估算不好呢?多估一点点好了,只要字符串最后大于16,就算浪费一点点,也比成倍的扩容好。

 

2. Liferay的StringBundler类

Liferay的StringBundler类提供了另一个长度设置的思路,它在append()的时候,不急着往char[]里塞东西,而是先拿一个String[]把它们都存起来,到了最后才把所有String的length加起来,构造一个合理长度的StringBuilder。

 

3. 但,还是浪费了一倍的char[]

浪费发生在最后一步,StringBuilder.toString()

 

//创建拷贝, 不共享数组
return new String(value, 0, count);

String的构造函数会用 System.arraycopy()复制一把传入的char[]来保证安全性不可变性,如果故事就这样结束,StringBuilder里的char[]还是被白白牺牲了。

为了不浪费这些char[],一种方法是用Unsafe之类的各种黑科技,绕过构造函数直接给String的char[]属性赋值,但很少人这样做。

另一个靠谱一些的办法就是重用StringBuilder。而重用,还解决了前面的长度设置问题,因为即使一开始估算不准,多扩容几次之后也够了。

 

4. 重用StringBuilder

这个做法来源于JDK里的BigDecimal类(没事看看JDK代码多重要),后来发现Netty也同样使用。SpringSide里将代码提取成StringBuilderHolder,里面只有一个函数

 

public StringBuilder getStringBuilder() {
sb.setLength(0);
return sb;
}

StringBuilder.setLength()函数只重置它的count指针,而char[]则会继续重用,而toString()时会把当前的count指针也作为参数传给String的构造函数,所以不用担心把超过新内容大小的旧内容也传进去了。可见,StringBuilder是完全可以被重用的。

为了避免并发冲突,这个Holder一般设为ThreadLocal,标准写法见BigDecimal或StringBuilderHolder的注释。

不过,如果String的长度不大,那从ThreadLocal里取一次值的代价还更大的多,所以也不能把这个ThreadLocalStringBuilder搞出来后,见到StringBuilder就替换。。。
 

5. + 与 StringBuilder

 

String s = “hello ” + user.getName();

这一句经过javac编译后的效果,的确等价于使用StringBuilder,但没有设定长度。

 

String s = new StringBuilder().append(“hello”).append(user.getName());

但是,如果像下面这样:

 

String s = “hello ”;
// 隔了其他一些语句
s = s + user.getName();

每一条语句,都会生成一个新的StringBuilder,这里就有了两个StringBuilder,性能就完全不一样了。如果是在循环体里s+=i; 就更加多得没谱。

据R大说,努力的JVM工程师们在运行优化阶段, 根据+XX:+OptimizeStringConcat(JDK7u40后默认打开),把相邻的(中间没隔着控制语句) StringBuilder合成一个,也会努力的猜长度。

所以,保险起见还是继续自己用StringBuilder并设定长度好了。

 

6. StringBuffer 与 StringBuilder

StringBuffer与StringBuilder都是继承于AbstractStringBuilder,唯一的区别就是StringBuffer的函数上都有synchronized关键字。

那些说StringBuffer “安全”的同学,其实你几时看过几个线程轮流append一个StringBuffer的情况???

 

7. 永远把日志的字符串拼接交给slf4j??

 

logger.info("Hello {}", user.getName());

对于不知道要不要输出的日志,交给slf4j在真的需要输出时才去拼接的确能省节约成本。

但对于一定要输出的日志,直接自己用StringBuilder拼接更快。因为看看slf4j的实现,实际上就是不断的indexof("{}"), 不断的subString(),再不断的用StringBuilder拼起来而已,没有银弹。

PS. slf4j中的StringBuilder在原始Message之外预留了50个字符,如果可变参数加起来长过50字符还是得复制扩容......而且StringBuilder也没有重用。

 

8. 小结

StringBuilder默认的写法,会为129长度的字符串拼接,合共申请625字符的数组。所以高性能的场景下,永远要考虑用一个ThreadLocal 可重用的StringBuilder。而且重用之后,就不用再玩猜长度的游戏了。当然,如果字符串只有一百几十字节,也不一定要考虑重用,设好初始值就好。

另一份Java应用调优指南之-前菜

默认分类admin 发表了文章 • 0 个评论 • 121 次浏览 • 2017-01-17 11:46 • 来自相关话题

原文链接:http://calvin1978.blogcn.com/articles/perf-tunning-1.html
 
每一次成功的调优,都会诞生又一份的调优指南。

一些必须写在前面的军规,虽然与Java应用的调优没直接关联,但是测试同学经常不留神的地方,导致应用的优化变成一场测试环境调优的戏码。
 

1 独占你的测试机器

包括跑JMeter的那些机器。

"top"或者"pidstat 1" 看一下,其他的路人甲乙丙丁的应用都关干净了没。

如果是云主机,确保更多的占有宿主机的资源,比如深夜大家下班了你在家连VPN回来跑。

 

2 了解你的测试机器

必须完完全全的了解你的机器,才知道有没卡在某个瓶颈,或者与线上环境、其他测试结果的比较。

还是那句, 包括跑JMeter的那些机器。

 

2.1 CPU

"cat /proc/cpuinfo", 看最后一条就好,比如

 

processor : 23
model name : Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz
physical id : 1
cpu cores : 6

所有数字都从零开始,physical id:1即两颗cpu, cpu core: 6即6核,processor : 23即24个处理器。

 

2 CPU * 6 Core * 2HT(Intel超线程技术) = 24 Processor

不过也有很多同事喜欢说24核,也懒得纠正了。

 

2.2 内存

"free -g" 没什么好说的。

 

2.3 硬盘

查看大小、分区、文件系统类型: "df -hT"
硬盘是否SCSI:/dev/sdX就是scsi的,hdX就是普通的。
硬盘是否SSD : "cat /sys/block/sda/queue/rotational", 0是SSD,1是传统硬盘,但也不一定灵

普通硬盘的写速度大概100M/s,RAID级别的查看不方便,SSD的速度也不定,所以用dd测一下最靠谱:

 

dd if=/dev/zero of=dd.file bs=8k count=128k conv=fdatasync
dd if=/dev/zero of=./dd.file bs=1G count=1 conv=fdatasync

上面命令测试了分别以每次8k和1g的大小,写入1g文件的速度。

if:输入文件名, /dev/zero 设备无穷尽地提供0
of:输出文件名
bs:块大小
count:次数
conv=fdatasync :实际写盘,而不是写入Page Cache

硬盘读速度的测试同理,不过要先清理缓存,否则直接从Page Cache读了。

 

sh -c "sync && echo 3 > /proc/sys/vm/drop_caches”
dd if=./dd.file of=/dev/null bs=8k

 

2.4 网卡

先用ifconfig看看有多少块网卡和bonding。bonding是个很棒的东西,可以把多块网卡绑起来,突破单块网卡的带宽限制。

然后检查每块网卡的速度,比如"ethtool eth0"。

再检查bonding,比如"cat /proc/net/bonding/bond0", 留意其Bonding Mode是负载均衡的,再留意其捆绑的网卡的速度。

最后检查测试客户机与服务机之间的带宽,先简单ping或traceroute 一下得到RTT时间,iperf之类的可稍后。

 

2.5 操作系统

Linux的内核版本,是否64位: "uname -a"
Redhat/CentOS版本 : "cat /etc/redhat-release"

 

3. 布置好你的机器状态采集工具

实时观察的,我喜欢dstat,详见《从dstat理解Linux性能监控体系》比vmstat,iostat, sar们都好用,起码对得够齐,单位能自动转换。不过dstat需要安装(yum install dstat,或者去它的网站下载解压即用版)

dstat -tamp:推荐,打印时间戳,比默认打印多一个memory信息,一个有否进程在block状态的信息
dstat -tamN bond0,lo: 如果有bonding,dstat会把bond0和eth0 有时会算双份,还有lo的也算到总量里,所以先用-N指定网卡检查下。

参考资料:后台性能测试不可不知的二三事

 

4. JMeter的调优顶一半的事

JMeter的版本越新越好。

 

4.1 JMeter的JVM参数

它默认连个垃圾收集算法都没有配,对延时要求高的,必须配上CMS或G1,内存也整大点降低GC的频率。其他的,给Server配的啥参数,给JMeter也来上一份。如果想动态改的,不写死在脚本里,可以配置环境变量$JVM_ARGS

 

4.2 测试计划的编写

什么if 语句,以及所有其实用动态语言来实现的都挺慢的。
xPath抽取结果集里的字段之类看着就慢的也别写了。
别加任何监听器和图形。
再配置输出日志的格式,能不要的列都别要了,最极端的其实就延时这列有用。

 

4.3 JMeter的运行

在Linux上用命令行跑,别偷懒用Window开着界面跑。
别开超过200条线程(虚拟机上更少)。
可以在不同机器上起多个JMeter,用集群汇总的模式。

 

4.4 结果的统计

初始连接,Server端热身,JVM编译热点方法等都需要时间,所以建议统计前删掉前面的一些日志。

要配置一下才能看到99.9%, 99.99% 分位数的延时,另外因为之前输出日志时省略了很多列,导入日志的时候配置也要如此。

但如果不能XWindows Forward,还要把日志下载回来再导入本地的JMeter,那还不如自己动动手,用sed, awk, sort配合一下自己写个分析延时的脚本直接在服务器上运行。

 

5. 其他被依赖的节点

比如所依赖的数据库够不够快,Restful API的模拟器够不够快,负载均衡器如HAProxy优化配置了没有。

  查看全部
原文链接:http://calvin1978.blogcn.com/articles/perf-tunning-1.html
 
每一次成功的调优,都会诞生又一份的调优指南。

一些必须写在前面的军规,虽然与Java应用的调优没直接关联,但是测试同学经常不留神的地方,导致应用的优化变成一场测试环境调优的戏码。
 

1 独占你的测试机器

包括跑JMeter的那些机器。

"top"或者"pidstat 1" 看一下,其他的路人甲乙丙丁的应用都关干净了没。

如果是云主机,确保更多的占有宿主机的资源,比如深夜大家下班了你在家连VPN回来跑。

 

2 了解你的测试机器

必须完完全全的了解你的机器,才知道有没卡在某个瓶颈,或者与线上环境、其他测试结果的比较。

还是那句, 包括跑JMeter的那些机器。

 

2.1 CPU

"cat /proc/cpuinfo", 看最后一条就好,比如

 

processor : 23
model name : Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz
physical id : 1
cpu cores : 6

所有数字都从零开始,physical id:1即两颗cpu, cpu core: 6即6核,processor : 23即24个处理器。

 

2 CPU * 6 Core * 2HT(Intel超线程技术) = 24 Processor

不过也有很多同事喜欢说24核,也懒得纠正了。

 

2.2 内存

"free -g" 没什么好说的。

 

2.3 硬盘

查看大小、分区、文件系统类型: "df -hT"
硬盘是否SCSI:/dev/sdX就是scsi的,hdX就是普通的。
硬盘是否SSD : "cat /sys/block/sda/queue/rotational", 0是SSD,1是传统硬盘,但也不一定灵

普通硬盘的写速度大概100M/s,RAID级别的查看不方便,SSD的速度也不定,所以用dd测一下最靠谱:

 

dd if=/dev/zero of=dd.file bs=8k count=128k conv=fdatasync
dd if=/dev/zero of=./dd.file bs=1G count=1 conv=fdatasync

上面命令测试了分别以每次8k和1g的大小,写入1g文件的速度。

if:输入文件名, /dev/zero 设备无穷尽地提供0
of:输出文件名
bs:块大小
count:次数
conv=fdatasync :实际写盘,而不是写入Page Cache

硬盘读速度的测试同理,不过要先清理缓存,否则直接从Page Cache读了。

 

sh -c "sync && echo 3 > /proc/sys/vm/drop_caches”
dd if=./dd.file of=/dev/null bs=8k

 

2.4 网卡

先用ifconfig看看有多少块网卡和bonding。bonding是个很棒的东西,可以把多块网卡绑起来,突破单块网卡的带宽限制。

然后检查每块网卡的速度,比如"ethtool eth0"。

再检查bonding,比如"cat /proc/net/bonding/bond0", 留意其Bonding Mode是负载均衡的,再留意其捆绑的网卡的速度。

最后检查测试客户机与服务机之间的带宽,先简单ping或traceroute 一下得到RTT时间,iperf之类的可稍后。

 

2.5 操作系统

Linux的内核版本,是否64位: "uname -a"
Redhat/CentOS版本 : "cat /etc/redhat-release"

 

3. 布置好你的机器状态采集工具

实时观察的,我喜欢dstat,详见《从dstat理解Linux性能监控体系》比vmstat,iostat, sar们都好用,起码对得够齐,单位能自动转换。不过dstat需要安装(yum install dstat,或者去它的网站下载解压即用版)

dstat -tamp:推荐,打印时间戳,比默认打印多一个memory信息,一个有否进程在block状态的信息
dstat -tamN bond0,lo: 如果有bonding,dstat会把bond0和eth0 有时会算双份,还有lo的也算到总量里,所以先用-N指定网卡检查下。

参考资料:后台性能测试不可不知的二三事

 

4. JMeter的调优顶一半的事

JMeter的版本越新越好。

 

4.1 JMeter的JVM参数

它默认连个垃圾收集算法都没有配,对延时要求高的,必须配上CMS或G1,内存也整大点降低GC的频率。其他的,给Server配的啥参数,给JMeter也来上一份。如果想动态改的,不写死在脚本里,可以配置环境变量$JVM_ARGS

 

4.2 测试计划的编写

什么if 语句,以及所有其实用动态语言来实现的都挺慢的。
xPath抽取结果集里的字段之类看着就慢的也别写了。
别加任何监听器和图形。
再配置输出日志的格式,能不要的列都别要了,最极端的其实就延时这列有用。

 

4.3 JMeter的运行

在Linux上用命令行跑,别偷懒用Window开着界面跑。
别开超过200条线程(虚拟机上更少)。
可以在不同机器上起多个JMeter,用集群汇总的模式。

 

4.4 结果的统计

初始连接,Server端热身,JVM编译热点方法等都需要时间,所以建议统计前删掉前面的一些日志。

要配置一下才能看到99.9%, 99.99% 分位数的延时,另外因为之前输出日志时省略了很多列,导入日志的时候配置也要如此。

但如果不能XWindows Forward,还要把日志下载回来再导入本地的JMeter,那还不如自己动动手,用sed, awk, sort配合一下自己写个分析延时的脚本直接在服务器上运行。

 

5. 其他被依赖的节点

比如所依赖的数据库够不够快,Restful API的模拟器够不够快,负载均衡器如HAProxy优化配置了没有。

 

另一份Java应用调优指南之-工具篇

默认分类admin 发表了文章 • 0 个评论 • 160 次浏览 • 2017-01-17 11:39 • 来自相关话题

Java应用的调优,再不写都要忘光了,先对付着写完,免费的JMC真的好用,大家越早用上越好。

前一篇是三个月前的 另一份Java应用调优指南 - 前菜

 

1. 土法调优两大件

先忆苦思甜,一般人在没有Profile工具的时候,调优的两大件,无非Heap Dump 与 Thread Dump。

 

1.1 Heap Dump

 

jmap -dump:live,format=b,file=heap.hprof pid

从安全点日志看,从Heap Dump开始,整个JVM都是停顿的,考虑到IO(虽是写到Page Cache,但或许会遇到background flush),几G的Heap可能产生几秒的停顿,在生产环境上执行时谨慎再谨慎。

live的选项,实际上是产生一次Full GC来保证只看还存活的对象。有时候也会故意不加live选项,看历史对象。

Dump出来的文件建议用JDK自带的VisualVM或Eclipse的MAT插件打开,对象的大小有两种统计方式:

本身大小(Shallow Size):对象本来的大小。
保留大小(Retained Size): 当前对象大小 + 当前对象直接或间接引用到的对象的大小总和。

看本身大小时,占大头的都是char[] ,byte[]之类的,没什么意思(用jmap -histo:live pid 看的也是本身大小)。所以需要关心的是保留大小比较大的对象,看谁在引用这些char[], byte[]。

(MAT能看的信息更多,但VisualVM胜在JVM自带,用法如下:命令行输入jvisualvm,文件->装入->堆Dump->检查 -> 查找20保留大小最大的对象,就会触发保留大小的计算,然后就可以类视图里浏览,按保留大小排序了)
 

1.2 Thread Dump

ThreadDump 同样会造成JVM停顿,在生产系统上执行要谨慎。

可以命令行方式执行它,"jstack pid” 或"jstack -l pid" ,其中-l 会同时打印各种lock,但会使得JVM停顿得长久得多(可能会差很多倍,比如普通的jstack可能几毫秒和一次GC没区别,加了-l 就是近一秒的时间),-l 建议不要用。

另一种是直接用代码来打印,比如线程池满了无法添加新任务,在开发或性能测试模式下,可以在代码里捕捉该异常后直接把当前线程池的情况打印出来。

 

ThreadMXBean threadMBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMBean.dumpAllThreads(false, false);

同样注意,这里threadMBean.dumpAllThreads(false,false)的参数为false,把参数改为true,则打印synchronizers与monitor,同样使得JVM停顿久很多。

线程状态:

RUNNABLE: 运行中状态,可能里面还能看到locked字样,表明它获得了某把锁。
BLOCKED:被某个锁(synchronizers)給block住了。
WAITING:等待某个condition或monitor发生,一般停留在park(), wait(), sleep(),join() 等语句里。
TIME_WAITING:和WAITING的区别是wait() 等语句加上了时间限制 wait(timeout)。

分析工具:

IBM Thread and Monitor Dump Analyze for Java 一个小巧的Jar包,能方便的按状态,线程名称,线程停留的函数排序,快速浏览。
http://spotify.github.io/threaddump-analyzer Spotify提供的Web版在线分析工具,可以将锁或条件相关联的线程聚合到一起。

 

2. 你真正要的Java Mission Control

如果你使用过JProfiler,Yourkit,VisualVM还有Eclipse的Profiler插件等一堆Profiler工具,或者用JavaSimion等在代码里打印过metrics,最后会发现免费的JMC才是你想要的。
 

2.1 优点

代替收费的JProfiler的好东西,以前Bea JRockit的宝贝,现在随着JDK7 up40以后的版本免费自带了,不过只在开发环境免费,就是说理论上不能拿它来连生产环境的机器。

另一个让人开心的事情就是JMC采用采样,而不是传统的代码植入的技术,对应用性能的影响非常非常小,完全可以开着JMC来做压测(唯一影响可能是full gc多了,减少一些监控项看看)。不会像以前,开了代码植入型的Profiler,出来的性能测试结果差了一个数量级不说,热点完全可能是错误的,这是一个真实的故事,具体细节就不说了。
 

2.2 功能

JMC里可以看的东西太多了,自己觉得最有用的如下:

内存Tab:分配Tab里的按类、按线程、对象的创建调用栈来归类的对象创建情况,让对象创建无处躲藏。
内存Tab:GC Tab的GC详细情况,以及TLAB外的分配情况(每条线程在Heap里分了一个Thread Local Area,在TLAB里的内存分配不需要线程竞争,所以TLAB之外的分配是不好的)
代码Tab:热点方法类及它的调用栈,超有用的功能。调用树是从线程角度看的方法调用,而按包名分类可以看3PP包的问题。
线程Tab:热点线程,再换个姿势来看热点方法和调用树。
线程Tab:争用,等待时间,锁定实例等。

 

2.3 使用方法简述

JDK7在启动服务时加上-XX:+UnlockCommercialFeatures -XX:+FlightRecorder 。
如果是远程服务器,要开JMX:

 

“-Dcom.sun.management.jmxremote.port=7001 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=your ip”

JDK自带的jmc命令,文件->连接->设定JMX连接,启动飞行纪录,固定时间选1分钟或更多,事件设置选为profiling,然后进一步修改,自己查看下都Profile了哪些信息,觉得不够的再添加些(下次就直接用上次设定就好了),比如:

加上对象数量的统计:Java Virtual Machine->GC->Detail->Object Count/Object Count after GC
方法调用采样的间隔从10ms改为1ms(但不能低于1ms,否则会影响性能了): Java Virtual Machine->Profiling下的两个选项
Socket与File采样, 10ms太久,但即使改为1ms也未必能抓住什么,可以干脆取消掉: Java Application->File Read/FileWrite/Socket Read/Socket Write

然后就开始Profile,到时间后Profile结束,会自动把记录下载回来,在JMC中展示。

其他资料:

java-performance.info上的介绍文章
JMC作者的博客
JMC官网

 

3. BTrace

神器,在生产环境上,动态监控某些方法的执行时长及其他信息,不再需要自己手工打日志,发版本,部署与重启服务。

据说淘宝就是经常开着BTrace在线上找问题的,我们最近也在生产上试了几把,太爽利了。

使用方法网上一搜大把,就不重复了。

原理就是自己写一个类似AspectJ的,希望监控哪个方法,监控后做什么动作的脚本,然后动态执行btrace命令将这个脚本attach到某个JVM上就行

 
这是一篇严肃的文章,就不配图了。 查看全部
Java应用的调优,再不写都要忘光了,先对付着写完,免费的JMC真的好用,大家越早用上越好。

前一篇是三个月前的 另一份Java应用调优指南 - 前菜

 

1. 土法调优两大件

先忆苦思甜,一般人在没有Profile工具的时候,调优的两大件,无非Heap Dump 与 Thread Dump。

 

1.1 Heap Dump

 

jmap -dump:live,format=b,file=heap.hprof pid

从安全点日志看,从Heap Dump开始,整个JVM都是停顿的,考虑到IO(虽是写到Page Cache,但或许会遇到background flush),几G的Heap可能产生几秒的停顿,在生产环境上执行时谨慎再谨慎。

live的选项,实际上是产生一次Full GC来保证只看还存活的对象。有时候也会故意不加live选项,看历史对象。

Dump出来的文件建议用JDK自带的VisualVM或Eclipse的MAT插件打开,对象的大小有两种统计方式:

本身大小(Shallow Size):对象本来的大小。
保留大小(Retained Size): 当前对象大小 + 当前对象直接或间接引用到的对象的大小总和。

看本身大小时,占大头的都是char[] ,byte[]之类的,没什么意思(用jmap -histo:live pid 看的也是本身大小)。所以需要关心的是保留大小比较大的对象,看谁在引用这些char[], byte[]。

(MAT能看的信息更多,但VisualVM胜在JVM自带,用法如下:命令行输入jvisualvm,文件->装入->堆Dump->检查 -> 查找20保留大小最大的对象,就会触发保留大小的计算,然后就可以类视图里浏览,按保留大小排序了)
 

1.2 Thread Dump

ThreadDump 同样会造成JVM停顿,在生产系统上执行要谨慎。

可以命令行方式执行它,"jstack pid” 或"jstack -l pid" ,其中-l 会同时打印各种lock,但会使得JVM停顿得长久得多(可能会差很多倍,比如普通的jstack可能几毫秒和一次GC没区别,加了-l 就是近一秒的时间),-l 建议不要用。

另一种是直接用代码来打印,比如线程池满了无法添加新任务,在开发或性能测试模式下,可以在代码里捕捉该异常后直接把当前线程池的情况打印出来。

 

ThreadMXBean threadMBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMBean.dumpAllThreads(false, false);

同样注意,这里threadMBean.dumpAllThreads(false,false)的参数为false,把参数改为true,则打印synchronizers与monitor,同样使得JVM停顿久很多。

线程状态:

RUNNABLE: 运行中状态,可能里面还能看到locked字样,表明它获得了某把锁。
BLOCKED:被某个锁(synchronizers)給block住了。
WAITING:等待某个condition或monitor发生,一般停留在park(), wait(), sleep(),join() 等语句里。
TIME_WAITING:和WAITING的区别是wait() 等语句加上了时间限制 wait(timeout)。

分析工具:

IBM Thread and Monitor Dump Analyze for Java 一个小巧的Jar包,能方便的按状态,线程名称,线程停留的函数排序,快速浏览。
http://spotify.github.io/threaddump-analyzer Spotify提供的Web版在线分析工具,可以将锁或条件相关联的线程聚合到一起。

 

2. 你真正要的Java Mission Control

如果你使用过JProfiler,Yourkit,VisualVM还有Eclipse的Profiler插件等一堆Profiler工具,或者用JavaSimion等在代码里打印过metrics,最后会发现免费的JMC才是你想要的。
 

2.1 优点

代替收费的JProfiler的好东西,以前Bea JRockit的宝贝,现在随着JDK7 up40以后的版本免费自带了,不过只在开发环境免费,就是说理论上不能拿它来连生产环境的机器。

另一个让人开心的事情就是JMC采用采样,而不是传统的代码植入的技术,对应用性能的影响非常非常小,完全可以开着JMC来做压测(唯一影响可能是full gc多了,减少一些监控项看看)。不会像以前,开了代码植入型的Profiler,出来的性能测试结果差了一个数量级不说,热点完全可能是错误的,这是一个真实的故事,具体细节就不说了。
 

2.2 功能

JMC里可以看的东西太多了,自己觉得最有用的如下:

内存Tab:分配Tab里的按类、按线程、对象的创建调用栈来归类的对象创建情况,让对象创建无处躲藏。
内存Tab:GC Tab的GC详细情况,以及TLAB外的分配情况(每条线程在Heap里分了一个Thread Local Area,在TLAB里的内存分配不需要线程竞争,所以TLAB之外的分配是不好的)
代码Tab:热点方法类及它的调用栈,超有用的功能。调用树是从线程角度看的方法调用,而按包名分类可以看3PP包的问题。
线程Tab:热点线程,再换个姿势来看热点方法和调用树。
线程Tab:争用,等待时间,锁定实例等。

 

2.3 使用方法简述

JDK7在启动服务时加上-XX:+UnlockCommercialFeatures -XX:+FlightRecorder 。
如果是远程服务器,要开JMX:

 

“-Dcom.sun.management.jmxremote.port=7001 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=your ip”

JDK自带的jmc命令,文件->连接->设定JMX连接,启动飞行纪录,固定时间选1分钟或更多,事件设置选为profiling,然后进一步修改,自己查看下都Profile了哪些信息,觉得不够的再添加些(下次就直接用上次设定就好了),比如:

加上对象数量的统计:Java Virtual Machine->GC->Detail->Object Count/Object Count after GC
方法调用采样的间隔从10ms改为1ms(但不能低于1ms,否则会影响性能了): Java Virtual Machine->Profiling下的两个选项
Socket与File采样, 10ms太久,但即使改为1ms也未必能抓住什么,可以干脆取消掉: Java Application->File Read/FileWrite/Socket Read/Socket Write

然后就开始Profile,到时间后Profile结束,会自动把记录下载回来,在JMC中展示。

其他资料:

java-performance.info上的介绍文章
JMC作者的博客
JMC官网

 

3. BTrace

神器,在生产环境上,动态监控某些方法的执行时长及其他信息,不再需要自己手工打日志,发版本,部署与重启服务。

据说淘宝就是经常开着BTrace在线上找问题的,我们最近也在生产上试了几把,太爽利了。

使用方法网上一搜大把,就不重复了。

原理就是自己写一个类似AspectJ的,希望监控哪个方法,监控后做什么动作的脚本,然后动态执行btrace命令将这个脚本attach到某个JVM上就行

 
这是一篇严肃的文章,就不配图了。