中文文案排版指北

统一中文文案、排版的相关用法,降低团队成员之间的沟通成本,增强网站气质。原文出处:https://github.com/mzlogin/chinese-copywriting-guidelines


目录

空格

「有研究显示,打字的时候不喜欢在中文和英文之间加空格的人,感情路都走得很辛苦,有七成的比例会在 34 岁的时候跟自己不爱的人结婚,而其余三成的人最后只能把遗产留给自己的猫。毕竟爱情跟书写都需要适时地留白。

与大家共勉之。」——vinta/paranoid-auto-spacing

中英文之间需要增加空格

正确:

在 LeanCloud 上,数据存储是围绕 AVObject 进行的。

错误:

在LeanCloud上,数据存储是围绕AVObject进行的。

在 LeanCloud上,数据存储是围绕AVObject 进行的。

完整的正确用法:

在 LeanCloud 上,数据存储是围绕 AVObject 进行的。每个 AVObject 都包含了与 JSON 兼容的 key-value 对应的数据。数据是 schema-free 的,你不需要在每个 AVObject 上提前指定存在哪些键,只要直接设定对应的 key-value 即可。

例外:「豆瓣FM」等产品名词,按照官方所定义的格式书写。

中文与数字之间需要增加空格

正确:

今天出去买菜花了 5000 元。

错误:

今天出去买菜花了 5000元。

今天出去买菜花了5000元。

数字与单位之间需要增加空格

正确:

我家的光纤入户宽带有 10 Gbps,SSD 一共有 20 TB。

错误:

我家的光纤入户宽带有 10Gbps,SSD 一共有 10TB。

例外:度/百分比与数字之间不需要增加空格:

正确:

今天是 233° 的高温。

新 MacBook Pro 有 15% 的 CPU 性能提升。

错误:

今天是 233 ° 的高温。

新 MacBook Pro 有 15 % 的 CPU 性能提升。

全角标点与其他字符之间不加空格

正确:

刚刚买了一部 iPhone,好开心!

错误:

刚刚买了一部 iPhone ,好开心!

-ms-text-autospace to the rescue?

Microsoft 有个 -ms-text-autospace.aspx) 的 CSS 属性可以实现自动为中英文之间增加空白。不过目前并未普及,另外在其他应用场景,例如 OS X、iOS 的用户界面目前并不存在这个特性,所以请继续保持随手加空格的习惯。

标点符号

不重复使用标点符号

正确:

德国队竟然战胜了巴西队!

她竟然对你说「喵」?!

错误:

德国队竟然战胜了巴西队!!

德国队竟然战胜了巴西队!!!!!!!!

她竟然对你说「喵」??!!

她竟然对你说「喵」?!?!??!!

全角和半角

不明白什么是全角(全形)与半角(半形)符号?请查看维基百科词条『全角和半角』。

使用全角中文标点

正确:

嗨!你知道嘛?今天前台的小妹跟我说「喵」了哎!

核磁共振成像(NMRI)是什么原理都不知道?JFGI!

错误:

嗨! 你知道嘛? 今天前台的小妹跟我说 “喵” 了哎!

嗨!你知道嘛?今天前台的小妹跟我说”喵”了哎!

核磁共振成像 (NMRI) 是什么原理都不知道? JFGI!

核磁共振成像(NMRI)是什么原理都不知道?JFGI!

数字使用半角字符

正确:

这件蛋糕只卖 1000 元。

错误:

这件蛋糕只卖 1000 元。

例外:在设计稿、宣传海报中如出现极少量数字的情形时,为方便文字对齐,是可以使用全角数字的。

遇到完整的英文整句、特殊名词,其內容使用半角标点

正确:

乔布斯那句话是怎么说的?「Stay hungry, stay foolish.」

推荐你阅读《Hackers & Painters: Big Ideas from the Computer Age》,非常的有趣。

错误:

乔布斯那句话是怎么说的?「Stay hungry,stay foolish。」

推荐你阅读《Hackers&Painters:Big Ideas from the Computer Age》,非常的有趣。

名词

专有名词使用正确的大小写

大小写相关用法原属于英文书写范畴,不属于本 wiki 讨论內容,在这里只对部分易错用法进行简述。

正确:

使用 GitHub 登录

我们的客户有 GitHub、Foursquare、Microsoft Corporation、Google、Facebook, Inc.。

错误:

使用 github 登录

使用 GITHUB 登录

使用 Github 登录

使用 gitHub 登录

使用 gイんĤЦ8 登录

我们的客户有 github、foursquare、microsoft corporation、google、facebook, inc.。

我们的客户有 GITHUB、FOURSQUARE、MICROSOFT CORPORATION、GOOGLE、FACEBOOK, INC.。

我们的客户有 Github、FourSquare、MicroSoft Corporation、Google、FaceBook, Inc.。

我们的客户有 gitHub、fourSquare、microSoft Corporation、google、faceBook, Inc.。

我们的客户有 gイんĤЦ8、キouЯƧquムгє、๓เςг๏ร๏Ŧt ς๏гק๏гคtเ๏ภn、900913、ƒ4ᄃëв๏๏к, IПᄃ.。

注意:当网页中需要配合整体视觉风格而出现全部大写/小写的情形,HTML 中请使用标准的大小写规范进行书写;并通过 text-transform: uppercase;text-transform: lowercase; 对表现形式进行定义。

不要使用不地道的缩写

正确:

我们需要一位熟悉 JavaScript、HTML5,至少理解一种框架(如 Backbone.js、AngularJS、React 等)的前端开发者。

错误:

我们需要一位熟悉 Js、h5,至少理解一种框架(如 backbone、angular、RJS 等)的 FED。

争议

以下用法略带有个人色彩,即:无论是否遵循下述规则,从语法的角度来讲都是正确的。

链接之间增加空格

用法:

提交一个 issue 并分配给相关同事。

访问我们网站的最新动态,请 点击这里 进行订阅!

对比用法:

提交一个 issue 并分配给相关同事。

访问我们网站的最新动态,请点击这里进行订阅!

简体中文使用直角引号

用法:

「老师,『有条不紊』的『紊』是什么意思?」

对比用法:

“老师,‘有条不紊’的‘紊’是什么意思?”

工具

仓库 语言
vinta/paranoid-auto-spacing JavaScript
huei90/pangu.node Node.js
huacnlee/auto-correct Ruby
sparanoid/space-lover PHP (WordPress)
nauxliu/auto-correct PHP
ricoa/copywriting-correct PHP
hotoo/pangu.vim Vim
sparanoid/grunt-auto-spacing Node.js (Grunt)
hjiang/scripts/add-space-between-latin-and-cjk Python

谁在这样做?

网站 文案 UGC
Apple 中国 Yes N/A
Apple 香港 Yes N/A
Apple 台湾 Yes N/A
Microsoft 中国 Yes N/A
Microsoft 香港 Yes N/A
Microsoft 台湾 Yes N/A
LeanCloud Yes N/A
知乎 Yes 部分用户达成
V2EX Yes Yes
SegmentFault Yes 部分用户达成
Apple4us Yes N/A
豌豆荚 Yes N/A
Ruby China Yes 标题达成
PHPHub Yes 标题达成
少数派 Yes N/A

参考文献

分享到 评论

研究优雅停机时的一点思考

开头先废话几句,有段时间没有更新博客了,除了公司项目比较忙之外,还有个原因就是开始思考如何更好地写作。远的来说,我从大一便开始在 CSDN 上写博客,回头看那时的文笔还很稚嫩,一心想着反正只有自己看,所以更多的是随性发挥,随意吐槽,内容也很简陋:刷完一道算法题记录下解题思路,用 JAVA 写完一个 demo 之后,记录下配置步骤。近的来看,工作之后开始维护自己的博客站点: www.cnkirito.moe,也会同步更新自己公众号。相比圈子里其他前辈来说,读者会少很多,但毕竟有人看,每次动笔之前便会开始思考一些事。除了给自己的学习经历做一个归档,还多了一些顾虑:会不会把知识点写错?会不会误人子弟?自己的理解会不会比较片面,不够深刻?等等等等。但自己的心路历程真的发生了一些改变。在我还是个小白的时候,学习技术:第一个想法是百度,搜别人的博客,一步步跟着别人后面配置,把 demo run 起来。而现在,遇到问题的第一思路变成了:源码 debug,官方文档。我便开始思考官方文档和博客的区别,官方文档的优势除了更加全面之外,还有就是:“它只教你怎么做”,对于一个有经验有阅历的程序员来说,这反而是好事,这可以让你有自己的思考。而博客则不一样,如果这个博主特别爱 BB,便会产生很多废话(就像本文的第一段),它会有很多作者自己思考的产物,一方面它比官方文档更容易出错,更容易片面,一方面它比官方文档更容易启发人,特别是读到触动到我的好文时,会抑制不住内心的喜悦想要加到作者的好友,这便是共情。我之后的文章也会朝着这些点去努力:不避重就轻,多思考不想当然,求精。

最近瞥了一眼项目的重启脚本,发现运维一直在使用 kill -9 <pid> 的方式重启 springboot embedded tomcat,其实大家几乎一致认为:kill -9 <pid> 的方式比较暴力,但究竟会带来什么问题却很少有人能分析出个头绪。这篇文章主要记录下自己的思考过程。

kill -9 和 kill -15 有什么区别?

在以前,我们发布 WEB 应用通常的步骤是将代码打成 war 包,然后丢到一个配置好了应用容器(如 Tomcat,Weblogic)的 Linux 机器上,这时候我们想要启动/关闭应用,方式很简单,运行其中的启动/关闭脚本即可。而 springboot 提供了另一种方式,将整个应用连同内置的 tomcat 服务器一起打包,这无疑给发布应用带来了很大的便捷性,与之而来也产生了一个问题:如何关闭 springboot 应用呢?一个显而易见的做法便是,根据应用名找到进程 id,杀死进程 id 即可达到关闭应用的效果。

上述的场景描述引出了我的疑问:怎么优雅地杀死一个 springboot 应用进程呢?这里仅仅以最常用的 Linux 操作系统为例,在 Linux 中 kill 指令负责杀死进程,其后可以紧跟一个数字,代表信号编号(Signal),执行 kill -l 指令,可以一览所有的信号编号。

1
2
xu@ntzyz-qcloud ~ % kill -l
HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM STKFLT CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH POLL PWR SYS

本文主要介绍下第 9 个信号编码 KILL,以及第 15 个信号编号 TERM

先简单理解下这两者的区别:kill -9 pid 可以理解为操作系统从内核级别强行杀死某个进程,kill -15 pid 则可以理解为发送一个通知,告知应用主动关闭。这么对比还是有点抽象,那我们就从应用的表现来看看,这两个命令杀死应用到底有啥区别。

代码准备

由于笔者 springboot 接触较多,所以以一个简易的 springboot 应用为例展开讨论,添加如下代码。

1 增加一个实现了 DisposableBean 接口的类

1
2
3
4
5
6
7
@Component
public class TestDisposableBean implements DisposableBean{
@Override
public void destroy() throws Exception {
System.out.println("测试 Bean 已销毁 ...");
}
}

2 增加 JVM 关闭时的钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootApplication
@RestController
public class TestShutdownApplication implements DisposableBean {
public static void main(String[] args) {
SpringApplication.run(TestShutdownApplication.class, args);
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
System.out.println("执行 ShutdownHook ...");
}
}));
}
}

测试步骤

  1. 执行 java -jar test-shutdown-1.0.jar 将应用运行起来
  2. 测试 kill -9 pidkill -15 pidctrl + c 后输出日志内容

测试结果

kill -15 pid & ctrl + c,效果一样,输出结果如下

1
2
3
4
5
2018-01-14 16:55:32.424 INFO 8762 --- [ Thread-3] ationConfigEmbeddedWebApplicationContext : Closing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2cdf8d8a: startup date [Sun Jan 14 16:55:24 UTC 2018]; root of context hierarchy
2018-01-14 16:55:32.432 INFO 8762 --- [ Thread-3] o.s.j.e.a.AnnotationMBeanExporter : Unregistering JMX-exposed beans on shutdown
执行 ShutdownHook ...
测试 Bean 已销毁 ...
java -jar test-shutdown-1.0.jar 7.46s user 0.30s system 80% cpu 9.674 total

kill -9 pid,没有输出任何应用日志

1
2
[1] 8802 killed java -jar test-shutdown-1.0.jar
java -jar test-shutdown-1.0.jar 7.74s user 0.25s system 41% cpu 19.272 total

可以发现,kill -9 pid 是给应用杀了个措手不及,没有留给应用任何反应的机会。而反观 kill -15 pid,则比较优雅,先是由AnnotationConfigEmbeddedWebApplicationContext (一个 ApplicationContext 的实现类)收到了通知,紧接着执行了测试代码中的 Shutdown Hook,最后执行了 DisposableBean#destory() 方法。孰优孰劣,立判高下。

一般我们会在应用关闭时处理一下“善后”的逻辑,比如

  1. 关闭 socket 链接
  2. 清理临时文件
  3. 发送消息通知给订阅方,告知自己下线
  4. 将自己将要被销毁的消息通知给子进程
  5. 各种资源的释放

等等

而 kill -9 pid 则是直接模拟了一次系统宕机,系统断电,这对于应用来说太不友好了,不要用收割机来修剪花盆里的花。取而代之,便是使用 kill -15 pid 来代替。如果在某次实际操作中发现:kill -15 pid 无法关闭应用,则可以考虑使用内核级别的 kill -9 pid ,但请事后务必排查出是什么原因导致 kill -15 pid 无法关闭。

springboot 如何处理 -15 TERM Signal

上面解释过了,使用 kill -15 pid 的方式可以比较优雅的关闭 springboot 应用,我们可能有以下的疑惑: springboot/spring 是如何响应这一关闭行为的呢?是先关闭了 tomcat,紧接着退出 JVM,还是相反的次序?它们又是如何互相关联的?

尝试从日志开始着手分析,AnnotationConfigEmbeddedWebApplicationContext 打印出了 Closing 的行为,直接去源码中一探究竟,最终在其父类 AbstractApplicationContext 中找到了关键的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Override
public void registerShutdownHook() {
if (this.shutdownHook == null) {
this.shutdownHook = new Thread() {
@Override
public void run() {
synchronized (startupShutdownMonitor) {
doClose();
}
}
};
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
}
@Override
public void close() {
synchronized (this.startupShutdownMonitor) {
doClose();
if (this.shutdownHook != null) {
Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
}
}
}
protected void doClose() {
if (this.active.get() && this.closed.compareAndSet(false, true)) {
LiveBeansView.unregisterApplicationContext(this);
// 发布应用内的关闭事件
publishEvent(new ContextClosedEvent(this));
// Stop all Lifecycle beans, to avoid delays during individual destruction.
if (this.lifecycleProcessor != null) {
this.lifecycleProcessor.onClose();
}
// spring 的 BeanFactory 可能会缓存单例的 Bean
destroyBeans();
// 关闭应用上下文&BeanFactory
closeBeanFactory();
// 执行子类的关闭逻辑
onClose();
this.active.set(false);
}
}

为了方便排版以及便于理解,我去除了源码中的部分异常处理代码,并添加了相关的注释。在容器初始化时,ApplicationContext 便已经注册了一个 Shutdown Hook,这个钩子调用了 Close() 方法,于是当我们执行 kill -15 pid 时,JVM 接收到关闭指令,触发了这个 Shutdown Hook,进而由 Close() 方法去处理一些善后手段。具体的善后手段有哪些,则完全依赖于 ApplicationContext 的 doClose() 逻辑,包括了注释中提及的销毁缓存单例对象,发布 close 事件,关闭应用上下文等等,特别的,当 ApplicationContext 的实现类是 AnnotationConfigEmbeddedWebApplicationContext 时,还会处理一些 tomcat/jetty 一类内置应用服务器关闭的逻辑。

窥见了 springboot 内部的这些细节,更加应该了解到优雅关闭应用的必要性。JAVA 和 C 都提供了对 Signal 的封装,我们也可以手动捕获操作系统的这些 Signal,在此不做过多介绍,有兴趣的朋友可以自己尝试捕获下。

还有其他优雅关闭应用的方式吗?

spring-boot-starter-actuator 模块提供了一个 restful 接口,用于优雅停机。

添加依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

添加配置

1
2
3
4
#启用shutdown
endpoints.shutdown.enabled=true
#禁用密码验证
endpoints.shutdown.sensitive=false

生产中请注意该端口需要设置权限,如配合 spring-security 使用。

执行 curl -X POST host:port/shutdown 指令,关闭成功便可以获得如下的返回:

1
{"message":"Shutting down, bye..."}

虽然 springboot 提供了这样的方式,但按我目前的了解,没见到有人用这种方式停机,kill -15 pid 的方式达到的效果与此相同,将其列于此处只是为了方案的完整性。

如何销毁作为成员变量的线程池?

尽管 JVM 关闭时会帮我们回收一定的资源,但一些服务如果大量使用异步回调,定时任务,处理不当很有可能会导致业务出现问题,在这其中,线程池如何关闭是一个比较典型的问题。

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class SomeService {
ExecutorService executorService = Executors.newFixedThreadPool(10);
public void concurrentExecute() {
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("executed...");
}
});
}
}

我们需要想办法在应用关闭时(JVM 关闭,容器停止运行),关闭线程池。

初始方案:什么都不做。在一般情况下,这不会有什么大问题,因为 JVM 关闭,会释放之,但显然没有做到本文一直在强调的两个字,没错—-优雅。

方法一的弊端在于线程池中提交的任务以及阻塞队列中未执行的任务变得极其不可控,接收到停机指令后是立刻退出?还是等待任务执行完成?抑或是等待一定时间任务还没执行完成则关闭?

方案改进:

发现初始方案的劣势后,我立刻想到了使用 DisposableBean 接口,像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class SomeService implements DisposableBean{
ExecutorService executorService = Executors.newFixedThreadPool(10);
public void concurrentExecute() {
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("executed...");
}
});
}
@Override
public void destroy() throws Exception {
executorService.shutdownNow();
//executorService.shutdown();
}
}

紧接着问题又来了,是 shutdown 还是 shutdownNow 呢?这两个方法还是经常被误用的,简单对比这两个方法。

ThreadPoolExecutor 在 shutdown 之后会变成 SHUTDOWN 状态,无法接受新的任务,随后等待正在执行的任务执行完成。意味着,shutdown 只是发出一个命令,至于有没有关闭还是得看线程自己。

ThreadPoolExecutor 对于 shutdownNow 的处理则不太一样,方法执行之后变成 STOP 状态,并对执行中的线程调用 Thread.interrupt() 方法(但如果线程未处理中断,则不会有任何事发生),所以并不代表“立刻关闭”。

查看 shutdown 和 shutdownNow 的 java doc,会发现如下的提示:

shutdown() :Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted.Invocation has no additional effect if already shut down.This method does not wait for previously submitted tasks to complete execution.Use {@link #awaitTermination awaitTermination} to do that.

shutdownNow():Attempts to stop all actively executing tasks, halts the processing of waiting tasks, and returns a list of the tasks that were awaiting execution. These tasks are drained (removed) from the task queue upon return from this method.This method does not wait for actively executing tasks to terminate. Use {@link #awaitTermination awaitTermination} to do that.There are no guarantees beyond best-effort attempts to stop processing actively executing tasks. This implementation cancels tasks via {@link Thread#interrupt}, so any task that fails to respond to interrupts may never terminate.

两者都提示我们需要额外执行 awaitTermination 方法,仅仅执行 shutdown/shutdownNow 是不够的。

最终方案:参考 spring 中线程池的回收策略,我们得到了最终的解决方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public abstract class ExecutorConfigurationSupport extends CustomizableThreadFactory
implements DisposableBean{
@Override
public void destroy() {
shutdown();
}
/**
* Perform a shutdown on the underlying ExecutorService.
* @see java.util.concurrent.ExecutorService#shutdown()
* @see java.util.concurrent.ExecutorService#shutdownNow()
* @see #awaitTerminationIfNecessary()
*/
public void shutdown() {
if (this.waitForTasksToCompleteOnShutdown) {
this.executor.shutdown();
}
else {
this.executor.shutdownNow();
}
awaitTerminationIfNecessary();
}
/**
* Wait for the executor to terminate, according to the value of the
* {@link #setAwaitTerminationSeconds "awaitTerminationSeconds"} property.
*/
private void awaitTerminationIfNecessary() {
if (this.awaitTerminationSeconds > 0) {
try {
this.executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS));
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
}

保留了注释,去除了一些日志代码,一个优雅关闭线程池的方案呈现在我们的眼前。

1 通过 waitForTasksToCompleteOnShutdown 标志来控制是想立刻终止所有任务,还是等待任务执行完成后退出。

2 executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS)); 控制等待的时间,防止任务无限期的运行(前面已经强调过了,即使是 shutdownNow 也不能保证线程一定停止运行)。

更多需要我们的思考的优雅停机策略

在我们分析 RPC 原理的系列文章里面曾经提到,服务治理框架一般会考虑到优雅停机的问题。通常的做法是事先隔断流量,接着关闭应用。常见的做法是将服务节点从注册中心摘除,订阅者接收通知,移除节点,从而优雅停机;涉及到数据库操作,则可以使用事务的 ACID 特性来保证即使 crash 停机也能保证不出现异常数据,正常下线则更不用说了;又比如消息队列可以依靠 ACK 机制+消息持久化,或者是事务消息保障;定时任务较多的服务,处理下线则特别需要注意优雅停机的问题,因为这是一个长时间运行的服务,比其他情况更容易受停机问题的影响,可以使用幂等和标志位的方式来设计定时任务…

事务和 ACK 这类特性的支持,即使是宕机,停电,kill -9 pid 等情况,也可以使服务尽量可靠;而同样需要我们思考的还有 kill -15 pid,正常下线等情况下的停机策略。最后再补充下整理这个问题时,自己对 jvm shutdown hook 的一些理解。

When the virtual machine begins its shutdown sequence it will start all registered shutdown hooks in some unspecified order and let them run concurrently. When all the hooks have finished it will then run all uninvoked finalizers if finalization-on-exit has been enabled. Finally, the virtual machine will halt.

shutdown hook 会保证 JVM 一直运行,知道 hook 终止 (terminated)。这也启示我们,如果接收到 kill -15 pid 命令时,执行阻塞操作,可以做到等待任务执行完成之后再关闭 JVM。同时,也解释了一些应用执行 kill -15 pid 无法退出的问题,没错,中断被阻塞了。

参考资料

[1] https://stackoverflow.com/questions/2921945/useful-example-of-a-shutdown-hook-in-java

[2] spring 源码

[3] jdk 文档

分享到 评论

深入理解RPC之服务注册与发现篇

在我们之前 RPC 原理的分析中,主要将笔墨集中在 Client 和 Server 端。而成熟的服务治理框架中不止存在这两个角色,一般还会有一个 Registry(注册中心)的角色。一张图就可以解释注册中心的主要职责。

注册中心的地位

  • 注册中心,用于服务端注册远程服务以及客户端发现服务
  • 服务端,对外提供后台服务,将自己的服务信息注册到注册中心
  • 客户端,从注册中心获取远程服务的注册信息,然后进行远程过程调用

目前主要的注册中心可以借由 zookeeper,eureka,consul,etcd 等开源框架实现。互联网公司也会因为自身业务的特性自研,如美团点评自研的 MNS,新浪微博自研的 vintage。

本文定位是对注册中心有一定了解的读者,所以不过多阐述注册中心的基础概念。

注册中心的抽象

借用开源框架中的核心接口,可以帮助我们从一个较为抽象的高度去理解注册中心。例如 motan 中的相关接口:

服务注册接口

1
2
3
4
5
6
7
8
9
10
11
12
public interface RegistryService {
//1. 向注册中心注册服务
void register(URL url);
//2. 从注册中心摘除服务
void unregister(URL url);
//3. 将服务设置为可用,供客户端调用
void available(URL url);
//4. 禁用服务,客户端无法发现该服务
void unavailable(URL url);
//5. 获取已注册服务的集合
Collection<URL> getRegisteredServiceUrls();
}

服务发现接口

1
2
3
4
5
6
7
8
public interface DiscoveryService {
//1. 订阅服务
void subscribe(URL url, NotifyListener listener);
//2. 取消订阅
void unsubscribe(URL url, NotifyListener listener);
//3. 发现服务列表
List<URL> discover(URL url);
}

主要使用的方法是 RegistryService#register(URL) 和 DiscoveryService#discover(URL)。其中这个 URL 参数被传递,显然也是很重要的一个类。

1
2
3
4
5
6
7
8
9
public class URL {
private String protocol;//协议名称
private String host;
private int port;
// interfaceName,也代表着路径
private String path;
private Map<String, String> parameters;
private volatile transient Map<String, Number> numbers;
}

注册中心也没那么玄乎,其实可以简单理解为:提供一个存储介质,供服务提供者和服务消费者共同连接,而存储的主要信息就是这里的 URL。但是具体 URL 都包含了什么实际信息,我们还没有一个直观的感受。

注册信息概览

以元老级别的注册中心 zookeeper 为例,看看它实际都存储了什么信息以及它是如何持久化上一节的 URL。

为了测试,我创建了一个 RPC 服务接口 com.sinosoft.student.api.DemoApi ,并且在 6666 端口暴露了这个服务的实现类,将其作为服务提供者。在 6667 端口远程调用这个服务,作为服务消费者。两者都连接本地的 zookeeper,本机 ip 为 192.168.150.1。

使用 zkClient.bash 或者 zkClient.sh 作为客户端连接到本地的 zookeeper,执行如下的命令:

1
2
[zk: localhost:2181(CONNECTED) 1] ls /motan/demo_group/com.sinosoft.student.api.DemoApi
> [client, server, unavailableServer]

zookeeper 有着和 linux 类似的命令和结构,其中 motan,demo_group,com.sinosoft.student.api.DemoApi,client, server, unavailableServer 都是一个个节点。可以从上述命令看出他们的父子关系。

/motan/demo_group/com.sinosoft.student.api.DemoApi 的结构为 /框架标识/分组名/接口名,其中的分组是 motan 为了隔离不同组的服务而设置的。这样,接口名称相同,分组不同的服务无法互相发现。如果此时有一个分组名为 demo_group2 的服务,接口名称为 DemoApi2,则 motan 会为其创建一个新的节点 /motan/demo_group2/com.sinosoft.student.api.DemoApi2

而 client,server,unavailableServer 则就是服务注册与发现的核心节点了。我们先看看这些节点都存储了什么信息。

server 节点:

1
2
3
4
5
[zk: localhost:2181(CONNECTED) 2] ls /motan/demo_group/com.sinosoft.student.api.DemoApi/server
> [192.168.150.1:6666]
[zk: localhost:2181(CONNECTED) 3] get /motan/demo_group/com.sinosoft.student.api.DemoApi/server/192.168.150.1:6666
> motan://192.168.150.1:6666/com.sinosoft.student.api.DemoApi?serialization=hessian2&protocol=motan&isDefault=true&maxContentLength=1548576&shareChannel=true&refreshTimestamp=1515122649835&id=motanServerBasicConfig&nodeType=service&export=motan:6666&requestTimeout=9000000&accessLog=false&group=demo_group&

client 节点:

1
2
3
4
[zk: localhost:2181(CONNECTED) 4] ls /motan/demo_group/com.sinosoft.student.api.DemoApi/client
> [192.168.150.1]
[zk: localhost:2181(CONNECTED) 5] get /motan/demo_group/com.sinosoft.student.api.DemoApi/client/192.168.150.1
> motan://192.168.150.1:0/com.sinosoft.student.api.DemoApi?singleton=true&maxContentLength=1548576&check=false&nodeType=service&version=1.0&throwException=true&accessLog=false&serialization=hessian2&retries=0&protocol=motan&isDefault=true&refreshTimestamp=1515122631758&id=motanClientBasicConfig&requestTimeout=9000&group=demo_group&

unavailableServer 节点是一个过渡节点,所以在一切正常的情况下不会存在信息,它的具体作用在下面会介绍。

从这些输出数据可以发现,注册中心承担的一个职责就是存储服务调用中相关的信息,server 向 zookeeper 注册信息,保存在 server 节点,而 client 实际和 server 共享同一个接口,接口名称就是路径名,所以也到达了同样的 server 节点去获取信息。并且同时注册到了 client 节点下(为什么需要这么做在下面介绍)。

注册信息详解

Server 节点

server 节点承担着最重要的职责,它由服务提供者创建,以供服务消费者获取节点中的信息,从而定位到服务提供者真正网络拓扑位置以及得知如何调用。demo 中我只在本机 [192.168.150.1:6666] 启动了一个实例,所以在server 节点之下,只存在这么一个节点,继续 get 这个节点,可以获取更详细的信息

1
motan://192.168.150.1:6666/com.sinosoft.student.api.DemoApi?serialization=hessian2&protocol=motan&isDefault=true&maxContentLength=1548576&shareChannel=true&refreshTimestamp=1515122649835&id=motanServerBasicConfig&nodeType=service&export=motan:6666&requestTimeout=9000000&accessLog=false&group=demo_group&

作为一个 value 值,它和 http 协议的请求十分相似,不过是以 motan:// 开头,表达的意图也很明确,这是 motan 协议和相关的路径及参数,关于 RPC 中的协议,可以翻看我的上一篇文章《深入理解RPC之协议篇》。

serialization 对应序列化方式,protocol 对应协议名称,maxContentLength 对应 RPC 传输中数据报文的最大长度,shareChannel 是传输层用到的参数,netty channel 中的一个属性,group 对应分组名称。

上述的 value 包含了 RPC 调用中所需要的全部信息。

Client 节点

在 motan 中使用 zookeeper 作为注册中心时,客户端订阅服务时会向 zookeeper 注册自身,主要是方便对调用方进行统计、管理。但订阅时是否注册 client 不是必要行为,和不同的注册中心实现有关,例如使用 consul 时便没有注册。

由于我们使用 zookeeper,也可以分析下 zookeeper 中都注册了什么信息。

1
motan://192.168.150.1:0/com.sinosoft.student.api.DemoApi?singleton=true&maxContentLength=1548576&check=false&nodeType=service&version=1.0&throwException=true&accessLog=false&serialization=hessian2&retries=0&protocol=motan&isDefault=true&refreshTimestamp=1515122631758&id=motanClientBasicConfig&requestTimeout=9000&group=demo_group

和 Server 节点的值类似,但也有客户独有的一些属性,如 singleton 代表服务是否单例,check 检查服务提供者是否存在,retries 代表重试次数,这也是 RPC 中特别需要注意的一点。

UnavailableServer 节点

unavailableServer 节点也不是必须存在的一个节点,它主要用来做 server 端的延迟上线,优雅关机。

延迟上线:一般推荐的服务端启动流程为:server 向注册中心的 unavailableServer 注册,状态为 unavailable,此时整个服务处于启动状态,但不对外提供服务,在服务验证通过,预热完毕,此时打开心跳开关,此时正式提供服务。

优雅关机:当需要对 server 方进行维护升级时,如果直接关闭,则会影响到客户端的请求。所以理想的情况应当是首先切断流量,再进行 server 的下线。具体的做法便是:先关闭心跳开关,客户端感知停止调用后,再关闭服务进程。

感知服务的下线

服务上线时自然要注册到注册中心,但下线时也得从注册中心中摘除。注册是一个主动的行为,这没有特别要注意的地方,但服务下线却是一个值得思考的问题。服务下线包含了主动下线和系统宕机等异常方式的下线。

临时节点+长连接

在 zookeeper 中存在持久化节点和临时节点的概念。持久化节点一经创建,只要不主动删除,便会一直持久化存在;临时节点的生命周期则是和客户端的连接同生共死的,应用连接到 zookeeper 时创建一个临时节点,使用长连接维持会话,这样无论何种方式服务发生下线,zookeeper 都可以感知到,进而删除临时节点。zookeeper 的这一特性和服务下线的需求契合的比较好,所以临时节点被广泛应用。

###主动下线+心跳检测

并不是所有注册中心都有临时节点的概念,另外一种感知服务下线的方式是主动下线。例如在 eureka 中,会有 eureka-server 和 eureka-client 两个角色,其中 eureka-server 保存注册信息,地位等同于 zookeeper。当 eureka-client 需要关闭时,会发送一个通知给 eureka-server,从而让 eureka-server 摘除自己这个节点。但这么做最大的一个问题是,如果仅仅只有主动下线这么一个手段,一旦 eureka-client 非正常下线(如断电,断网),eureka-server 便会一直存在一个已经下线的服务节点,一旦被其他服务发现进而调用,便会带来问题。为了避免出现这样的情况,需要给 eureka-server 增加一个心跳检测功能,它会对服务提供者进行探测,比如每隔30s发送一个心跳,如果三次心跳结果都没有返回值,就认为该服务已下线。

注册中心对比

Feature Consul zookeeper etcd euerka
服务健康检查 服务状态,内存,硬盘等 (弱)长连接,keepalive 连接心跳 可配支持
多数据中心 支持
kv存储服务 支持 支持 支持
一致性 raft paxos raft
cap ca cp cp ap
使用接口(多语言能力) 支持http和dns 客户端 http/grpc http(sidecar)
watch支持 全量/支持long polling 支持 支持 long polling 支持 long polling/大部分增量
自身监控 metrics metrics metrics
安全 acl /https acl https支持(弱)
spring cloud集成 已支持 已支持 已支持 已支持

一般而言注册中心的特性决定了其使用的场景,例如很多框架支持 zookeeper,在我自己看来是因为其老牌,易用,但业界也有很多人认为 zookeeper 不适合做注册中心,它本身是一个分布式协调组件,并不是为注册服务而生,server 端注册一个服务节点,client 端并不需要在同一时刻拿到完全一致的服务列表,只要最终一致性即可。在跨IDC,多数据中心等场景下 consul 发挥了很大的优势,这也是很多互联网公司选择使用 consul 的原因。 eureka 是 ap 注册中心,并且是 spring cloud 默认使用的组件,spring cloud eureka 较为贴近 spring cloud 生态。

总结

注册中心主要用于解耦服务调用中的定位问题,是分布式系统必须面对的一个问题。更多专业性的对比,可以期待 spring4all.com 的注册中心专题讨论,相信会有更为细致地对比。

分享到 评论

深入理解RPC之协议篇

协议(Protocol)是个很广的概念,RPC 被称为远程过程调用协议,HTTP 和 TCP 也是大家熟悉的协议,也有人经常拿 RPC 和 RESTFUL 做对比,后者也可以被理解为一种协议… 我个人偏向于把“协议”理解为不同厂家不同用户之间的“约定”,而在 RPC 中,协议的含义也有多层。

Protocol 在 RPC 中的层次关系

翻看 dubbo 和 motan 两个国内知名度数一数二的 RPC 框架(或者叫服务治理框架可能更合适)的文档,他们都有专门的一章介绍自身对多种协议的支持。RPC 框架是一个分层结构,从我的这个《深入理解RPC》系列就可以看出,是按照分层来介绍 RPC 的原理的,前面已经介绍过了传输层,序列化层,动态代理层,他们各自负责 RPC 调用生命周期中的一环,而协议层则是凌驾于它们所有层之上的一层。简单描述下各个层之间的关系:

protocol 层主要用于配置 refer(发现服务) 和 exporter(暴露服务) 的实现方式,transport 层定义了传输的方式,codec 层诠释了具体传输过程中报文解析的方式,serialize 层负责将对象转换成字节,以用于传输,proxy 层负责将这些细节屏蔽。

它们的包含关系如下:protocol > transport > codec > serialize

motan 的 Protocol 接口可以佐证这一点:

1
2
3
4
5
public interface Protocol {
<T> Exporter<T> export(Provider<T> provider, URL url);
<T> Referer<T> refer(Class<T> clz, URL url, URL serviceUrl);
void destroy();
}

我们都知道 RPC 框架支持多种协议,由于协议处于框架层次的较高位置,任何一种协议的替换,都可能会导致服务发现和服务注册的方式,传输的方式,以及序列化的方式,而不同的协议也给不同的业务场景带来了更多的选择,下面就来看看一些常用协议。

Dubbo 中的协议

dubbo://

Dubbo 缺省协议采用单一长连接和 NIO 异步通讯,适合于小数据量高并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。

反之,Dubbo 缺省协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。

适用场景:常规远程服务方法调用

rmi://

RMI 协议采用 JDK 标准的 java.rmi.* 实现,采用阻塞式短连接和 JDK 标准序列化方式。

适用场景:常规远程服务方法调用,与原生RMI服务互操作

hessian://

Hessian 协议用于集成 Hessian 的服务,Hessian 底层采用 Http 通讯,采用 Servlet 暴露服务,Dubbo 缺省内嵌 Jetty 作为服务器实现。

Dubbo 的 Hessian 协议可以和原生 Hessian 服务互操作,即:

  • 提供者用 Dubbo 的 Hessian 协议暴露服务,消费者直接用标准 Hessian 接口调用
  • 或者提供方用标准 Hessian 暴露服务,消费方用 Dubbo 的 Hessian 协议调用。

Hessian 在之前介绍过,当时仅仅是用它来作为序列化工具,但其本身其实就是一个协议,可以用来做远程通信。

适用场景:页面传输,文件传输,或与原生hessian服务互操作

http://

基于 HTTP 表单的远程调用协议,采用 Spring 的 HttpInvoker 实现

适用场景:需同时给应用程序和浏览器 JS 使用的服务。

webserivice://

基于 WebService 的远程调用协议,基于 Apache CXF 的 frontend-simpletransports-http 实现。

可以和原生 WebService 服务互操作,即:

  • 提供者用 Dubbo 的 WebService 协议暴露服务,消费者直接用标准 WebService 接口调用,
  • 或者提供方用标准 WebService 暴露服务,消费方用 Dubbo 的 WebService 协议调用

适用场景:系统集成,跨语言调用

thrift://

当前 dubbo 支持的 thrift 协议是对 thrift 原生协议的扩展,在原生协议的基础上添加了一些额外的头信息,比如 service name,magic number 等。

memcached://

基于 memcached 实现的 RPC 协议

redis://

基于 Redis 实现的 RPC 协议。

dubbo 支持的众多协议详见 http://dubbo.io/books/dubbo-user-book/references/protocol/dubbo.html

dubbo的一个分支 dangdangdotcom/dubbox 扩展了 REST 协议

rest://

JAX-RS 是标准的 Java REST API,得到了业界的广泛支持和应用,其著名的开源实现就有很多,包括 Oracle 的 Jersey,RedHat 的 RestEasy,Apache 的 CXF 和 Wink,以及 restlet 等等。另外,所有支持 JavaEE 6.0 以上规范的商用 JavaEE 应用服务器都对 JAX-RS 提供了支持。因此,JAX-RS 是一种已经非常成熟的解决方案,并且采用它没有任何所谓 vendor lock-in 的问题。

JAX-RS 在网上的资料非常丰富,例如下面的入门教程:

更多的资料请自行 google 或者百度一下。就学习 JAX-RS 来说,一般主要掌握其各种 annotation 的用法即可。

注意:dubbo 是基于 JAX-RS 2.0 版本的,有时候需要注意一下资料或REST实现所涉及的版本。

适用场景:跨语言调用

千米网也给 dubbo 贡献了一个扩展协议:https://github.com/dubbo/dubbo-rpc-jsonrpc

jsonrpc://

Why HTTP

在互联网快速迭代的大潮下,越来越多的公司选择nodejs、django、rails这样的快速脚本框架来开发web端应用 而后端的服务用Java又是最合适的,这就产生了大量的跨语言的调用需求。
而http、json是天然合适作为跨语言的标准,各种语言都有成熟的类库
虽然Dubbo的异步长连接协议效率很高,但是在脚本语言中,这点效率的损失并不重要。

Why Not RESTful

Dubbox 在 RESTful 接口上已经做出了尝试,但是 REST 架构和 dubbo 原有的 RPC 架构是有区别的,
区别在于 REST 架构需要有资源 (Resources) 的定义, 需要用到 HTTP 协议的基本操作 GET、POST、PUT、DELETE 对资源进行操作。
Dubbox 需要重新定义接口的属性,这对原有的 Dubbo 接口迁移是一个较大的负担。
相比之下,RESTful 更合适互联网系统之间的调用,而 RPC 更合适一个系统内的调用,
所以我们使用了和 Dubbo 理念较为一致的 JsonRPC

JSON-RPC 2.0 规范 和 JAX-RS 一样,也是一个规范,JAVA 对其的支持可参考 jsonrpc4j

适用场景:跨语言调用

Motan 中的协议

motan://

motan 协议之于 motan,地位等同于 dubbo 协议之于 dubbo,两者都是各自默认的且都是自定义的协议。内部使用 netty 进行通信(旧版本使用 netty3 ,最新版本支持 netty4),默认使用 hessian 作为序列化器。

适用场景:常规远程服务方法调用

injvm://

顾名思义,如果 Provider 和 Consumer 位于同一个 jvm,motan 提供了 injvm 协议。这个协议是jvm内部调用,不经过本地网络,一般在服务化拆分时,作为过渡方案使用,可以通过开关机制在本地和远程调用之间进行切换,等过渡完成后再去除本地实现的引用。

grpc://和yar://

这两个协议的诞生缘起于一定的历史遗留问题,moton 是新浪微博开源的,而其内部有很多 PHP 应用,为解决跨语言问题,这两个协议进而出现了。

适用场景:较为局限的跨语言调用

restful://

motan 在 0.3.1 (2017-07-11) 版本发布了 restful 协议的支持(和 dubbo 的 rest 协议本质一样),dubbo 默认使用 jetty 作为 http server,而 motan 使用则是 netty 。主要实现的是 java 对 restful 指定的规范,即 javax.ws.rs 包下的类。

适用场景:跨语言调用

motan2://

motan 1.0.0 (2017-10-31) 版本发布了 motan2 协议,用于对跨语言的支持,不同于 restful,jsonrpc 这样的通用协议,motan2 把请求的一些元数据作为单独的部分传输,更适合不同语言解析。

适用场景:跨语言调用

Motan is a cross-language remote procedure call(RPC) framework for rapid development of high performance distributed services.

Motan-go is golang implementation.

Motan-PHP is PHP client can interactive with Motan server directly or through Motan-go agent.

Motan-openresty is a Lua(Luajit) implementation based on Openresty

从 motan 的 changeLog 以及 github 首页的介绍来看,其致力于打造成一个跨语言的服务治理框架,这倒是比较亦可赛艇的事。

面向未来的协议

motan 已经支持 motan2://,计划支持 mcq://,kafka:// …支持更多的协议,以应对复杂的业务场景。对这个感兴趣的朋友,可以参见这篇文章:http://mp.weixin.qq.com/s/XZVCHZZzCX8wwgNKZtsmcA

总结

如果仅仅是将 dubbo,motan 作为一个 RPC 框架使用,那大多人会选择其默认的协议(dubbo 协议,motan 协议),而如果是有历史遗留原因,如需要对接异构系统,就需要替换成其他协议了。大多数互联网公司选择自研 RPC 框架,或者改造自己的协议,都是为了适配自身业务的特殊性,协议层的选择非常重要。

分享到 评论

Motan中使用异步RPC接口

这周六参加了一个美团点评的技术沙龙,其中一位老师在介绍他们自研的 RPC 框架时提到一点:RPC 请求分为 sync,future,callback,oneway,并且需要遵循一个原则:能够异步的地方就不要使用同步。正好最近在优化一个业务场景:在一次页面展示中,需要调用 5 个 RPC 接口,导致页面响应很慢。正好启发了我。

为什么慢?

大多数开源的 RPC 框架实现远程调用的方式都是同步的,假设 [ 接口1,…,接口5]的每一次调用耗时为 200ms (其中接口2依赖接口1,接口5依赖接口3,接口4),那么总耗时为 1s,这整个是一个串行的过程。

多线程加速

第一个想到的解决方案便是多线程,那么[1=>2]编为一组,[[3,4]=>5]编为一组,两组并发执行,[1=>2]串行执行耗时400ms,[3,4]并发执行耗时200ms,[[3,4]=>5]总耗时400ms ,最终[[1=>2],[[3,4]=>5]]总耗时400ms(理论耗时)。相比较于原来的1s,的确快了不少,但实际编写接口花了不少功夫,创建线程池,管理资源,分析依赖关系…总之代码不是很优雅。

RPC中,多线程着重考虑的点是在客户端优化代码,这给客户端带来了一定的复杂性,并且编写并发代码对程序员的要求更高,且不利于调试。

异步调用

如果有一种既能保证速度,又能像同步 RPC 调用那样方便,岂不美哉?于是引出了 RPC 中的异步调用。

在 RPC 异步调用之前,先回顾一下 java.util.concurrent 中的基础知识:CallableFuture

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class Main {
public static void main(String[] args) throws Exception{
final ExecutorService executorService = Executors.newFixedThreadPool(10);
long start = System.currentTimeMillis();
Future<Integer> resultFuture1 = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return method1() + method2();
}
});
Future<Integer> resultFuture2 = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
Future<Integer> resultFuture3 = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return method3();
}
});
Future<Integer> resultFuture4 = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return method4();
}
});
return method5()+resultFuture3.get()+resultFuture4.get();
}
});
int result = resultFuture1.get() + resultFuture2.get();
System.out.println("result = "+result+", total cost "+(System.currentTimeMillis()-start)+" ms");
executorService.shutdown();
}
static int method1(){
delay200ms();
return 1;
}
static int method2(){
delay200ms();
return 2;
}
static int method3(){
delay200ms();
return 3;
}
static int method4(){
delay200ms();
return 4;
}
static int method5(){
delay200ms();
return 5;
}
static void delay200ms(){
try{
Thread.sleep(200);
}catch (Exception e){
e.printStackTrace();
}
}
}

最终控制台打印:

result = 15, total cost 413 ms

五个接口,如果同步调用,便是串行的效果,最终耗时必定在 1s 之上,而异步调用的优势便是,submit任务之后立刻返回,只有在调用 future.get() 方法时才会阻塞,而这期间多个异步方法便可以并发的执行。

RPC 异步调用

我们的项目使用了 Motan 作为 RPC 框架,查看其 changeLog ,0.3.0 (2017-03-09) 该版本已经支持了 async 特性。可以让开发者很方便地实现 RPC 异步调用。

1 为接口增加 @MotanAsync 注解

1
2
3
4
@MotanAsync
public interface DemoApi {
DemoDto randomDemo(String id);
}

2 添加 Maven 插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>1.10</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${project.build.directory}/generated-sources/annotations</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

安装插件后,可以借助它生成一个和 DemoApi 关联的异步接口 DemoApiAsync 。

1
2
3
public interface DemoApiAsync extends DemoApi {
ResponseFuture randomDemoAsync(String id);
}

3 注入接口即可调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class DemoService {
@MotanReferer
DemoApi demoApi;
@MotanReferer
DemoApiAsync demoApiAsync;//<1>
public DemoDto randomDemo(String id){
DemoDto demoDto = demoApi.randomDemo(id);
return demoDto;
}
public DemoDto randomDemoAsync(String id){
ResponseFuture responseFuture = demoApiAsync.randomDemoAsync(id);//<2>
DemoDto demoDto = (DemoDto) responseFuture.getValue();
return demoDto;
}
}

<1> DemoApiAsync 如何生成的已经介绍过,它和 DemoApi 并没有功能性的区别,仅仅是同步异步调用的差距,而 DemoApiAsync 实现的的复杂性完全由 RPC 框架帮助我们完成,开发者无需编写 Callable 接口。

<2> ResponseFuture 是 RPC 中 Future 的抽象,其本身也是 juc 中 Future 的子类,当 responseFuture.getValue() 调用时会阻塞。

总结

在异步调用中,如果发起一次异步调用后,立刻使用 future.get() ,则大致和同步调用等同。其真正的优势是在submit 和 future.get() 之间可以混杂一些非依赖性的耗时操作,而不是同步等待,从而充分利用时间片。

另外需要注意,如果异步调用涉及到数据的修改,则多个异步操作直接不能保证 happens-before 原则,这属于并发控制的范畴了,谨慎使用。查询操作则大多没有这样的限制。

在能使用并发的地方使用并发,不能使用的地方才选择同步,这需要我们思考更多细节,但可以最大限度的提升系统的性能。

分享到 评论

深入理解RPC之传输篇

RPC 被称为“远程过程调用”,表明了一个方法调用会跨越网络,跨越进程,所以传输层是不可或缺的。一说到网络传输,一堆名词就蹦了出来:TCP、UDP、HTTP,同步 or 异步,阻塞 or 非阻塞,长连接 or 短连接…

本文介绍两种传输层的实现:使用 Socket 和使用 Netty。前者实现的是阻塞式的通信,是一个较为简单的传输层实现方式,借此可以了解传输层的工作原理及工作内容;后者是非阻塞式的,在一般的 RPC 场景下,性能会表现的很好,所以被很多开源 RPC 框架作为传输层的实现方式。

RpcRequest 和 RpcResponse

传输层传输的主要对象其实就是这两个类,它们封装了请求 id,方法名,方法参数,返回值,异常等 RPC 调用中需要的一系列信息。

1
2
3
4
5
6
7
8
9
10
public class RpcRequest implements Serializable {
private String interfaceName;
private String methodName;
private String parametersDesc;
private Object[] arguments;
private Map<String, String> attachments;
private int retries = 0;
private long requestId;
private byte rpcProtocolVersion;
}
1
2
3
4
5
6
7
8
9
public class RpcResponse implements Serializable {
private Object value;
private Exception exception;
private long requestId;
private long processTime;
private int timeout;
private Map<String, String> attachments;// rpc协议版本兼容时可以回传一些额外的信息
private byte rpcProtocolVersion;
}

Socket传输

Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class RpcServerSocketProvider {
public static void main(String[] args) throws Exception {
//序列化层实现参考之前的章节
Serialization serialization = new Hessian2Serialization();
ServerSocket serverSocket = new ServerSocket(8088);
ExecutorService executorService = Executors.newFixedThreadPool(10);
while (true) {
final Socket socket = serverSocket.accept();
executorService.execute(() -> {
try {
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
try {
DataInputStream dis = new DataInputStream(is);
int length = dis.readInt();
byte[] requestBody = new byte[length];
dis.read(requestBody);
//反序列化requestBody => RpcRequest
RpcRequest rpcRequest = serialization.deserialize(requestBody, RpcRequest.class);
//反射调用生成响应 并组装成 rpcResponse
RpcResponse rpcResponse = invoke(rpcRequest);
//序列化rpcResponse => responseBody
byte[] responseBody = serialization.serialize(rpcResponse);
DataOutputStream dos = new DataOutputStream(os);
dos.writeInt(responseBody.length);
dos.write(responseBody);
dos.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
is.close();
os.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
public static RpcResponse invoke(RpcRequest rpcRequest) {
//模拟反射调用
RpcResponse rpcResponse = new RpcResponse();
rpcResponse.setRequestId(rpcRequest.getRequestId());
//... some operation
return rpcResponse;
}
}

Client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class RpcSocketConsumer {
public static void main(String[] args) throws Exception {
//序列化层实现参考之前的章节
Serialization serialization = new Hessian2Serialization();
Socket socket = new Socket("localhost", 8088);
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
//封装rpc请求
RpcRequest rpcRequest = new RpcRequest();
rpcRequest.setRequestId(12345L);
//序列化 rpcRequest => requestBody
byte[] requestBody = serialization.serialize(rpcRequest);
DataOutputStream dos = new DataOutputStream(os);
dos.writeInt(requestBody.length);
dos.write(requestBody);
dos.flush();
DataInputStream dis = new DataInputStream(is);
int length = dis.readInt();
byte[] responseBody = new byte[length];
dis.read(responseBody);
//反序列化 responseBody => rpcResponse
RpcResponse rpcResponse = serialization.deserialize(responseBody, RpcResponse.class);
is.close();
os.close();
socket.close();
System.out.println(rpcResponse.getRequestId());
}
}

dis.readInt() 和 dis.read(byte[] bytes) 决定了使用 Socket 通信是一种阻塞式的操作,报文头+报文体的传输格式是一种常见的格式,除此之外,使用特殊的字符如空行也可以划分出报文结构。在示例中,我们使用一个 int(4字节)来传递报问题的长度,之后传递报文体,在复杂的通信协议中,报文头除了存储报文体还会额外存储一些信息,包括协议名称,版本,心跳标识等。

在网络传输中,只有字节能够被识别,所以我们在开头引入了 Serialization 接口,负责完成 RpcRequest 和 RpcResponse 与字节的相互转换。(Serialization 的工作机制可以参考之前的文章)

使用 Socket 通信可以发现:每次 Server 处理 Client 请求都会从线程池中取出一个线程来处理请求,这样的开销对于一般的 Rpc 调用是不能够接受的,而 Netty 一类的网络框架便派上了用场。

Netty 传输

Server 和 ServerHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class RpcNettyProvider {
public static void main(String[] args) throws Exception{
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 创建并初始化 Netty 服务端 Bootstrap 对象
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new RpcDecoder(RpcRequest.class)); // 解码 RPC 请求
pipeline.addLast(new RpcEncoder(RpcResponse.class)); // 编码 RPC 响应
pipeline.addLast(new RpcServerHandler()); // 处理 RPC 请求
}
});
bootstrap.option(ChannelOption.SO_BACKLOG, 1024);
bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture future = bootstrap.bind("127.0.0.1", 8087).sync();
// 关闭 RPC 服务器
future.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RpcServerHandler extends SimpleChannelInboundHandler<RpcRequest> {
@Override
public void channelRead0(final ChannelHandlerContext ctx, RpcRequest request) throws Exception {
RpcResponse rpcResponse = invoke(request);
// 写入 RPC 响应对象并自动关闭连接
ctx.writeAndFlush(rpcResponse).addListener(ChannelFutureListener.CLOSE);
}
private RpcResponse invoke(RpcRequest rpcRequest) {
//模拟反射调用
RpcResponse rpcResponse = new RpcResponse();
rpcResponse.setRequestId(rpcRequest.getRequestId());
//... some operation
return rpcResponse;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}

Client 和 ClientHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class RpcNettyConsumer {
public static void main(String[] args) throws Exception{
EventLoopGroup group = new NioEventLoopGroup();
try {
// 创建并初始化 Netty 客户端 Bootstrap 对象
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new RpcEncoder(RpcRequest.class)); // 编码 RPC 请求
pipeline.addLast(new RpcDecoder(RpcResponse.class)); // 解码 RPC 响应
pipeline.addLast(new RpcClientHandler()); // 处理 RPC 响应
}
});
bootstrap.option(ChannelOption.TCP_NODELAY, true);
// 连接 RPC 服务器
ChannelFuture future = bootstrap.connect("127.0.0.1", 8087).sync();
// 写入 RPC 请求数据并关闭连接
Channel channel = future.channel();
RpcRequest rpcRequest = new RpcRequest();
rpcRequest.setRequestId(123456L);
channel.writeAndFlush(rpcRequest).sync();
channel.closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class RpcClientHandler extends SimpleChannelInboundHandler<RpcResponse> {
@Override
public void channelRead0(ChannelHandlerContext ctx, RpcResponse response) throws Exception {
System.out.println(response.getRequestId());//处理响应
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}

使用 Netty 的好处是很方便地实现了非阻塞式的调用,关键部分都给出了注释。上述的代码虽然很多,并且和我们熟悉的 Socket 通信代码大相径庭,但大多数都是 Netty 的模板代码,启动服务器,配置编解码器等。真正的 RPC 封装操作大多集中在 Handler 的 channelRead 方法(负责读取)以及 channel.writeAndFlush 方法(负责写入)中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class RpcEncoder extends MessageToByteEncoder {
private Class<?> genericClass;
Serialization serialization = new Hessian2Serialization();
public RpcEncoder(Class<?> genericClass) {
this.genericClass = genericClass;
}
@Override
public void encode(ChannelHandlerContext ctx, Object in, ByteBuf out) throws Exception {
if (genericClass.isInstance(in)) {
byte[] data = serialization.serialize(in);
out.writeInt(data.length);
out.writeBytes(data);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class RpcDecoder extends ByteToMessageDecoder {
private Class<?> genericClass;
public RpcDecoder(Class<?> genericClass) {
this.genericClass = genericClass;
}
Serialization serialization = new Hessian2Serialization();
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() < 4) {
return;
}
in.markReaderIndex();
int dataLength = in.readInt();
if (in.readableBytes() < dataLength) {
in.resetReaderIndex();
return;
}
byte[] data = new byte[dataLength];
in.readBytes(data);
out.add(serialization.deserialize(data, genericClass));
}
}

使用 Netty 不能保证返回的字节大小,所以需要加上 in.readableBytes() < 4 这样的判断,以及 in.markReaderIndex() 这样的标记,用来区分报文头和报文体。

同步与异步 阻塞与非阻塞

这两组传输特性经常被拿来做对比,很多文章声称 Socket 是同步阻塞的,Netty 是异步非阻塞,其实有点问题。

其实这两组并没有必然的联系,同步阻塞,同步非阻塞,异步非阻塞都有可能(同步非阻塞倒是没见过),而大多数使用 Netty 实现的 RPC 调用其实应当是同步非阻塞的(当然一般 RPC 也支持异步非阻塞)。

同步和异步关注的是消息通信机制
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
换句话说,就是由调用者主动等待这个调用的结果。

而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

如果需要 RPC 调用返回一个结果,该结果立刻被使用,那意味着着大概率需要是一个同步调用。如果不关心其返回值,则可以将其做成异步接口,以提升效率。

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

在上述的例子中可以看出 Socket 通信我们显示声明了一个包含10个线程的线程池,每次请求到来,分配一个线程,等待客户端传递报文头和报文体的行为都会阻塞该线程,可以见得其整体是阻塞的。而在 Netty 通信的例子中,每次请求并没有分配一个线程,而是通过 Handler 的方式处理请求(联想 NIO 中 Selector),是非阻塞的。

使用同步非阻塞方式的通信机制并不一定同步阻塞式的通信强,所谓没有最好,只有更合适,而一般的同步非阻塞 通信适用于 1.网络连接数量多 2.每个连接的io不频繁 的场景,与 RPC 调用较为契合。而成熟的 RPC 框架的传输层和协议层通常也会提供多种选择,以应对不同的场景。

总结

本文堆砌了一些代码,而难点主要是对 Socket 的理解,和 Netty 框架的掌握。Netty 的学习有一定的门槛,但实际需要掌握的知识点其实并不多(仅仅针对 RPC 框架所涉及的知识点而言),学习 Netty ,个人推荐《Netty IN ACTION》以及 https://waylau.gitbooks.io/netty-4-user-guide/Getting%20Started/Before%20Getting%20Started.html 该网站的例子。

参考资料:

http://javatar.iteye.com/blog/1123915 – 梁飞

https://gitee.com/huangyong/rpc – 黄勇

分享到 评论

深入理解RPC之动态代理篇

提到 JAVA 中的动态代理,大多数人都不会对 JDK 动态代理感到陌生,Proxy,InvocationHandler 等类都是 J2SE 中的基础概念。动态代理发生在服务调用方/客户端,RPC 框架需要解决的一个问题是:像调用本地接口一样调用远程的接口。于是如何组装数据报文,经过网络传输发送至服务提供方,屏蔽远程接口调用的细节,便是动态代理需要做的工作了。RPC 框架中的代理层往往是单独的一层,以方便替换代理方式(如 motan 代理层位于com.weibo.api.motan.proxy ,dubbo代理层位于 com.alibaba.dubbo.common.bytecode )。

实现动态代理的方案有下列几种:

  • jdk 动态代理
  • cglib 动态代理
  • javassist 动态代理
  • ASM 字节码
  • javassist 字节码

其中 cglib 底层实现依赖于 ASM,javassist 自成一派。由于 ASM 和 javassist 需要程序员直接操作字节码,导致使用门槛相对较高,但实际上他们的应用是非常广泛的,如 Hibernate 底层使用了 javassist(默认)和 cglib,Spring 使用了 cglib 和 jdk 动态代理。

RPC 框架无论选择何种代理技术,所需要完成的任务其实是固定的,不外乎‘整理报文’,‘确认网络位置’,‘序列化’,’网络传输’,‘反序列化’,’返回结果’…

技术选型的影响因素

框架中使用何种动态代理技术,影响因素也不少。

性能

从早期 dubbo 的作者梁飞的博客 http://javatar.iteye.com/blog/814426 中可以得知 dubbo 选择使用 javassist 作为动态代理方案主要考虑的因素是性能

从其博客的测试结果来看 javassist > cglib > jdk 。但实际上他的测试过程稍微有点瑕疵:在 cglib 和 jdk 代理对象调用时,走的是反射调用,而在 javassist 生成的代理对象调用时,走的是直接调用(可以先阅读下梁飞大大的博客)。这意味着 cglib 和 jdk 慢的原因并不是由动态代理产生的,而是由反射调用产生的(顺带一提,很多人认为 jdk 动态代理的原理是反射,其实它的底层也是使用的字节码技术)。而最终我的测试结果,结论如下: javassist ≈ cglib > jdk 。javassist 和 cglib 的效率基本持平 ,而他们两者的执行效率基本可以达到 jdk 动态代理的2倍(这取决于测试的机器以及 jdk 的版本,jdk1.8 相较于 jdk1.6 动态代理技术有了质的提升,所以并不是传闻中的那样:cglib 比 jdk 快 10倍)。文末会给出我的测试代码。

依赖

motan默认的实现是jdk动态代理,代理方案支持SPI扩展,可以自行扩展其他实现方式。

使用jdk做为默认,主要是减少core包依赖,性能不是唯一考虑因素。另外使用字节码方式javaassist性能比较优秀,动态代理模式下jdk性能也不会差多少。

rayzhang0603(motan贡献者)

motan 选择使用 jdk 动态代理,原因主要有两个:减少 motan-core 的依赖,方便。至于扩展性,dubbo 并没有预留出动态代理的扩展接口,而是写死了 bytecode ,这点上 motan 做的较好。

易用性

从 dubbo 和 motan 的源码中便可以直观的看出两者的差距了,dubbo 为了使用 javassist 技术花费不少的精力,而 motan 使用 jdk 动态代理只用了一个类。dubbo 的设计者为了追求极致的性能而做出的工作是值得肯定的,motan 也预留了扩展机制,两者各有千秋。

动态代理入门指南

为了方便对比几种动态代理技术,先准备一个统一接口。

1
2
3
public interface BookApi {
void sell();
}

JDK动态代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static BookApi createJdkDynamicProxy(final BookApi delegate) {
BookApi jdkProxy = (BookApi) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
new Class[]{BookApi.class}, new JdkHandler(delegate));
return jdkProxy;
}
private static class JdkHandler implements InvocationHandler {
final Object delegate;
JdkHandler(Object delegate) {
this.delegate = delegate;
}
@Override
public Object invoke(Object object, Method method, Object[] objects)
throws Throwable {
//添加代理逻辑<1>
if(method.getName().equals("sell")){
System.out.print("");
}
return null;
// return method.invoke(delegate, objects);
}

<1> 在真正的 RPC 调用中 ,需要填充‘整理报文’,‘确认网络位置’,‘序列化’,’网络传输’,‘反序列化’,’返回结果’等逻辑。

Cglib动态代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static BookApi createCglibDynamicProxy(final BookApi delegate) throws Exception {
Enhancer enhancer = new Enhancer();
enhancer.setCallback(new CglibInterceptor(delegate));
enhancer.setInterfaces(new Class[]{BookApi.class});
BookApi cglibProxy = (BookApi) enhancer.create();
return cglibProxy;
}
private static class CglibInterceptor implements MethodInterceptor {
final Object delegate;
CglibInterceptor(Object delegate) {
this.delegate = delegate;
}
@Override
public Object intercept(Object object, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
//添加代理逻辑
if(method.getName().equals("sell")) {
System.out.print("");
}
return null;
// return methodProxy.invoke(delegate, objects);
}
}

和 JDK 动态代理的操作步骤没有太大的区别,只不过是替换了 cglib 的API而已。

需要引入 cglib 依赖:

1
2
3
4
5
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.5</version>
</dependency>

Javassist字节码

到了 javassist,稍微有点不同了。因为它是通过直接操作字节码来生成代理对象。

1
2
3
4
5
6
7
8
9
10
11
private static BookApi createJavassistBytecodeDynamicProxy() throws Exception {
ClassPool mPool = new ClassPool(true);
CtClass mCtc = mPool.makeClass(BookApi.class.getName() + "JavaassistProxy");
mCtc.addInterface(mPool.get(BookApi.class.getName()));
mCtc.addConstructor(CtNewConstructor.defaultConstructor(mCtc));
mCtc.addMethod(CtNewMethod.make(
"public void sell() { System.out.print(\"\") ; }", mCtc));
Class<?> pc = mCtc.toClass();
BookApi bytecodeProxy = (BookApi) pc.newInstance();
return bytecodeProxy;
}

需要引入 javassist 依赖:

1
2
3
4
5
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.21.0-GA</version>
</dependency>

动态代理测试

测试环境:window i5 8g jdk1.8 cglib3.2.5 javassist3.21.0-GA

动态代理其实分成了两步:代理对象的创建,代理对象的调用。坊间流传的动态代理性能对比主要指的是后者;前者一般不被大家考虑,如果远程Refer的对象是单例的,其只会被创建一次,而如果是原型模式,多例对象的创建其实也是性能损耗的一个考虑因素(只不过远没有调用占比大)。

Create JDK Proxy: 21 ms

Create CGLIB Proxy: 342 ms

Create Javassist Bytecode Proxy: 419 ms

可能出乎大家的意料,JDK 创建动态代理的速度比后两者要快10倍左右。

下面是调用速度的测试:

case 1:

JDK Proxy invoke cost 1912 ms

CGLIB Proxy invoke cost 1015 ms

JavassistBytecode Proxy invoke cost 1280 ms

case 2:

JDK Proxy invoke cost 1747 ms

CGLIB Proxy invoke cost 1234 ms

JavassistBytecode Proxy invoke cost 1175 ms

case 3:

JDK Proxy invoke cost 2616 ms

CGLIB Proxy invoke cost 1373 ms

JavassistBytecode Proxy invoke cost 1335 ms

Jdk 的执行速度一定会慢于 Cglib 和 Javassist,但最慢也就2倍,并没有达到数量级的差距;Cglib 和 Javassist不相上下,差距不大(测试中偶尔发现Cglib实行速度会比平时慢10倍,不清楚是什么原因)

所以出于易用性和性能,私以为使用 Cglib 是一个很好的选择(性能和 Javassist 持平,易用性和 Jdk 持平)。

反射调用

既然提到了动态代理和 cglib ,顺带提一下反射调用如何加速的问题。RPC 框架中在 Provider 服务端需要根据客户端传递来的 className + method + param 来找到容器中的实际方法执行反射调用。除了反射调用外,还可以使用 Cglib 来加速。

JDK反射调用

1
2
Method method = serviceClass.getMethod(methodName, new Class[]{});
method.invoke(delegate, new Object[]{});

Cglib调用

1
2
3
FastClass serviceFastClass = FastClass.create(serviceClass);
FastMethod serviceFastMethod = serviceFastClass.getMethod(methodName, new Class[]{});
serviceFastMethod.invoke(delegate, new Object[]{});

但实测效果发现 Cglib 并不一定比 JDK 反射执行速度快,还会跟具体的方法实现有关(大雾)。

测试代码

略长…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
public class Main {
public static void main(String[] args) throws Exception {
BookApi delegate = new BookApiImpl();
long time = System.currentTimeMillis();
BookApi jdkProxy = createJdkDynamicProxy(delegate);
time = System.currentTimeMillis() - time;
System.out.println("Create JDK Proxy: " + time + " ms");
time = System.currentTimeMillis();
BookApi cglibProxy = createCglibDynamicProxy(delegate);
time = System.currentTimeMillis() - time;
System.out.println("Create CGLIB Proxy: " + time + " ms");
time = System.currentTimeMillis();
BookApi javassistBytecodeProxy = createJavassistBytecodeDynamicProxy();
time = System.currentTimeMillis() - time;
System.out.println("Create JavassistBytecode Proxy: " + time + " ms");
for (int i = 0; i < 10; i++) {
jdkProxy.sell();//warm
}
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
jdkProxy.sell();
}
System.out.println("JDK Proxy invoke cost " + (System.currentTimeMillis() - start) + " ms");
for (int i = 0; i < 10; i++) {
cglibProxy.sell();//warm
}
start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
cglibProxy.sell();
}
System.out.println("CGLIB Proxy invoke cost " + (System.currentTimeMillis() - start) + " ms");
for (int i = 0; i < 10; i++) {
javassistBytecodeProxy.sell();//warm
}
start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
javassistBytecodeProxy.sell();
}
System.out.println("JavassistBytecode Proxy invoke cost " + (System.currentTimeMillis() - start) + " ms");
Class<?> serviceClass = delegate.getClass();
String methodName = "sell";
for (int i = 0; i < 10; i++) {
cglibProxy.sell();//warm
}
// 执行反射调用
for (int i = 0; i < 10; i++) {//warm
Method method = serviceClass.getMethod(methodName, new Class[]{});
method.invoke(delegate, new Object[]{});
}
start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
Method method = serviceClass.getMethod(methodName, new Class[]{});
method.invoke(delegate, new Object[]{});
}
System.out.println("反射 invoke cost " + (System.currentTimeMillis() - start) + " ms");
// 使用 CGLib 执行反射调用
for (int i = 0; i < 10; i++) {//warm
FastClass serviceFastClass = FastClass.create(serviceClass);
FastMethod serviceFastMethod = serviceFastClass.getMethod(methodName, new Class[]{});
serviceFastMethod.invoke(delegate, new Object[]{});
}
start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
FastClass serviceFastClass = FastClass.create(serviceClass);
FastMethod serviceFastMethod = serviceFastClass.getMethod(methodName, new Class[]{});
serviceFastMethod.invoke(delegate, new Object[]{});
}
System.out.println("CGLIB invoke cost " + (System.currentTimeMillis() - start) + " ms");
}
private static BookApi createJdkDynamicProxy(final BookApi delegate) {
BookApi jdkProxy = (BookApi) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
new Class[]{BookApi.class}, new JdkHandler(delegate));
return jdkProxy;
}
private static class JdkHandler implements InvocationHandler {
final Object delegate;
JdkHandler(Object delegate) {
this.delegate = delegate;
}
@Override
public Object invoke(Object object, Method method, Object[] objects)
throws Throwable {
//添加代理逻辑
if(method.getName().equals("sell")){
System.out.print("");
}
return null;
// return method.invoke(delegate, objects);
}
}
private static BookApi createCglibDynamicProxy(final BookApi delegate) throws Exception {
Enhancer enhancer = new Enhancer();
enhancer.setCallback(new CglibInterceptor(delegate));
enhancer.setInterfaces(new Class[]{BookApi.class});
BookApi cglibProxy = (BookApi) enhancer.create();
return cglibProxy;
}
private static class CglibInterceptor implements MethodInterceptor {
final Object delegate;
CglibInterceptor(Object delegate) {
this.delegate = delegate;
}
@Override
public Object intercept(Object object, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
//添加代理逻辑
if(method.getName().equals("sell")) {
System.out.print("");
}
return null;
// return methodProxy.invoke(delegate, objects);
}
}
private static BookApi createJavassistBytecodeDynamicProxy() throws Exception {
ClassPool mPool = new ClassPool(true);
CtClass mCtc = mPool.makeClass(BookApi.class.getName() + "JavaassistProxy");
mCtc.addInterface(mPool.get(BookApi.class.getName()));
mCtc.addConstructor(CtNewConstructor.defaultConstructor(mCtc));
mCtc.addMethod(CtNewMethod.make(
"public void sell() { System.out.print(\"\") ; }", mCtc));
Class<?> pc = mCtc.toClass();
BookApi bytecodeProxy = (BookApi) pc.newInstance();
return bytecodeProxy;
}
}
分享到 评论

深入理解RPC之序列化篇--总结篇

上一篇《深入理解RPC之序列化篇–Kryo》,介绍了序列化的基础概念,并且详细介绍了Kryo的一系列特性,在这一篇中,简略的介绍其他常用的序列化器,并对它们进行一些比较。序列化篇仅仅由Kryo篇和总结篇构成可能有点突兀,等待后续有时间会补充详细的探讨。

定义抽象接口

1
2
3
4
5
6
public interface Serialization {
byte[] serialize(Object obj) throws IOException;
<T> T deserialize(byte[] bytes, Class<T> clz) throws IOException;
}

RPC框架中的序列化实现自然是种类多样,但它们必须遵循统一的规范,于是我们使用 Serialization 作为序列化的统一接口,无论何种方案都需要实现该接口。

Kryo实现

Kryo篇已经给出了实现代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class KryoSerialization implements Serialization {
@Override
public byte[] serialize(Object obj) {
Kryo kryo = kryoLocal.get();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Output output = new Output(byteArrayOutputStream);
kryo.writeObject(output, obj);
output.close();
return byteArrayOutputStream.toByteArray();
}
@Override
public <T> T deserialize(byte[] bytes, Class<T> clz) {
Kryo kryo = kryoLocal.get();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Input input = new Input(byteArrayInputStream);
input.close();
return (T) kryo.readObject(input, clz);
}
private static final ThreadLocal<Kryo> kryoLocal = new ThreadLocal<Kryo>() {
@Override
protected Kryo initialValue() {
Kryo kryo = new Kryo();
kryo.setReferences(true);
kryo.setRegistrationRequired(false);
return kryo;
}
};
}

所需依赖:

1
2
3
4
5
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.1</version>
</dependency>

Hessian实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Hessian2Serialization implements Serialization {
@Override
public byte[] serialize(Object data) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Hessian2Output out = new Hessian2Output(bos);
out.writeObject(data);
out.flush();
return bos.toByteArray();
}
@Override
public <T> T deserialize(byte[] bytes, Class<T> clz) throws IOException {
Hessian2Input input = new Hessian2Input(new ByteArrayInputStream(bytes));
return (T) input.readObject(clz);
}
}

所需依赖:

1
2
3
4
5
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.51</version>
</dependency>

大名鼎鼎的 Hessian 序列化方案经常被RPC框架用来作为默认的序列化方案,可见其必然具备一定的优势。其具体的优劣我们放到文末的总结对比中与其他序列化方案一起讨论。而在此,着重提一点Hessian使用时的坑点。

BigDecimal的反序列化

使用 Hessian 序列化包含 BigDecimal 字段的对象时会导致其值一直为0,不注意这个bug会导致很大的问题,在最新的4.0.51版本仍然可以复现。解决方案也很简单,指定 BigDecimal 的序列化器即可,通过添加两个文件解决这个bug:

resources\META-INF\hessian\serializers

1
java.math.BigDecimal=com.caucho.hessian.io.StringValueSerializer

resources\META-INF\hessian\deserializers

1
java.math.BigDecimal=com.caucho.hessian.io.BigDecimalDeserializer

Protostuff实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ProtostuffSerialization implements Serialization {
@Override
public byte[] serialize(Object obj) throws IOException {
Class clz = obj.getClass();
LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
try {
Schema schema = RuntimeSchema.createFrom(clz);
return ProtostuffIOUtil.toByteArray(obj, schema, buffer);
} catch (Exception e) {
throw e;
} finally {
buffer.clear();
}
}
@Override
public <T> T deserialize(byte[] bytes, Class<T> clz) throws IOException {
T message = objenesis.newInstance(clz); // <1>
Schema<T> schema = RuntimeSchema.createFrom(clz);
ProtostuffIOUtil.mergeFrom(bytes, message, schema);
return message;
}
private Objenesis objenesis = new ObjenesisStd(); // <2>
}

所需依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- Protostuff -->
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.0.9</version>
</dependency>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.0.9</version>
</dependency>
<!-- Objenesis -->
<dependency>
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
<version>2.5</version>
</dependency>

Protostuff 可以理解为 google protobuf 序列化的升级版本,protostuff-runtime 无需静态编译,这比较适合RPC通信时的特性,很少见到有人直接拿 protobuf 作为RPC的序列化器,而 protostuff-runtime 仍然占据一席之地。

<1> 使用 Protostuff 的一个坑点在于其反序列化时需用户自己实例化序列化后的对象,所以才有了 T message = objenesis.newInstance(clz); 这行代码。使用 objenesis 工具实例化一个需要的对象,而后使用 ProtostuffIOUtil 完成赋值操作。

<2> 上述的 objenesis.newInstance(clz) 可以由 clz.newInstance() 代替,后者也可以实例化一个对象,但如果对象缺少无参构造函数,则会报错。借助于objenesis 可以绕开无参构造器实例化一个对象,且性能优于直接反射创建。所以一般在选择 Protostuff 作为序列化器时,一般配合 objenesis 使用。

Fastjson实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class FastJsonSerialization implements Serialization {
static final String charsetName = "UTF-8";
@Override
public byte[] serialize(Object data) throws IOException {
SerializeWriter out = new SerializeWriter();
JSONSerializer serializer = new JSONSerializer(out);
serializer.config(SerializerFeature.WriteEnumUsingToString, true);//<1>
serializer.config(SerializerFeature.WriteClassName, true);//<1>
serializer.write(data);
return out.toBytes(charsetName);
}
@Override
public <T> T deserialize(byte[] data, Class<T> clz) throws IOException {
return JSON.parseObject(new String(data), clz);
}
}

所需依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.28</version>
</dependency>

<1> JSON序列化注意对枚举类型的特殊处理;额外补充类名可以在反序列化时获得更丰富的信息。

序列化对比

在我的PC上对上述序列化方案进行测试:

测试用例:对一个简单POJO对象序列化/反序列化100W次

serialize/ms deserialize/ms
Fastjson 2832 2242
Kryo 2975 1987
Hessian 4598 3631
Protostuff 2944 2541

测试用例:序列化包含1000个简单对象的List,循环1000次

serialize/ms deserialize/ms
Fastjson 2551 2821
Kryo 1951 1342
Hessian 1828 2213
Protostuff 1409 2813

对于耗时类型的测试需要做到预热+平均值等条件,测试后效果其实并不如人意,从我不太严谨的测试来看,并不能明显地区分出他们的性能。另外,Kryo关闭Reference可以加速,Protostuff支持静态编译加速,Schema缓存等特性,每个序列化方案都有自身的特殊性,启用这些特性会伴随一些限制。但在RPC实际地序列化使用中不会利用到这些特性,所以在测试时并没有特别关照它们。

序列化包含1000个简单对象的List,查看字节数

字节数/byte
Fastjson 120157
Kryo 39134
Hessian 86166
Protostuff 86084

字节数这个指标还是很直观的,Kryo拥有绝对的优势,只有Hessian,Protostuff的一半,而Fastjson作为一个文本类型的序列化方案,自然无法和字节类型的序列化方案比较。而字节最终将用于网络传输,是RPC框架非常在意的一个性能点。

综合评价

经过个人测试,以及一些官方的测试结果,我觉得在 RPC 场景下,序列化的速度并不是一个很大考量标准,因为各个序列化方案都在有意优化速度,只要不是 jdk 序列化,速度就不会太慢。

Kryo:专为 JAVA 定制的序列化协议,序列化后字节数少,利于网络传输。但不支持跨语言(或支持的代价比较大)。dubbox 扩展中支持了 kryo 序列化协议。github 3018 star。

Hessian:支持跨语言,序列化后字节数适中,API 易用。是国内主流 rpc 框架:dubbo,motan 的默认序列化协议。hessian.caucho.com 未托管在github

Protostuff:提起 Protostuff 不得不说到 Protobuf。Protobuf可能更出名一些,因为其是google的亲儿子,grpc框架便是使用protobuf作为序列化协议,虽然protobuf与语言无关平台无关,但需要使用特定的语法编写 .prpto 文件,然后静态编译,这带了一些复杂性。而 protostuff 实际是对 protobuf 的扩展,protostuff-runtime 模块继承了protobuf 性能,且不需要预编译文件,但与此同时,也失去了跨语言的特性。所以 protostuff 的定位是一个 JAVA 序列化框架,其性能略优于 Hessian。tip :protostuff 反序列化时需用户自己初始化序列化后的对象,其只负责将该对象进行赋值。github 719 star。

Fastjson:作为一个 json 工具,被拉到 RPC 的序列化方案中似乎有点不妥,但 motan 该 RPC 框架除了支持 hessian 之外,还支持了 fastjson 的序列化。可以将其作为一个跨语言序列化的简易实现方案。github 11.8k star。

分享到 评论

南京IAS架构师峰会观后感

上周六,周日在南京举办了IAS架构师峰会,这么多人的技术分享会还是头一次参加,大佬云集,涨了不少姿势。特此一篇记录下印象深刻的几场分享。由于全凭记忆叙述,故只能以流水账的形式还原出现场的收获。

大型支付交易平台的演进过程

大型支付交易平台的演进过程

陈斌,《架构即未来》译者,易宝支付CTO。

交易系统具备以下特点,交易量大,并发度高,业务敏感度高,响应速度容忍度低…从而使得支付交易平台需要有以下的特点:

  • 高可用:7X24*365随时可用
  • 高安全:需满足PCI-DSS要求
  • 高效率:每笔交易的成本要低
  • 高扩展:随业务的快速发展扩张

从以上几点话题引申出了系统扩展的三个阶段

X轴扩展–扩展机器

也就是通俗意义中集群方案,横向扩展,通过添加多台机器负载均衡,从而扩展计算能力,这是最简单粗暴,也是最直接易用的方案。

Y轴扩展–拆分服务

当水平扩展遇到瓶颈后,可以进行服务的拆分,将系统按照业务模块进行拆分,从而可以选择性定制化地扩展特定的模块。如电商系统中拆分出订单模块,商品模块,会员模块,地址模块…由于各个模块的职责不同,如订单模块在双11时压力很大,可以多部署一些订单模块,而其他压力不大的模块,则进行少量地部署。

Z轴扩展–拆分数据

服务拆分之后仍然无法解决与日俱增的数据量问题,于是引发了第三层扩展,数据的分片,我理解的sharding,不仅仅存在于数据库,还包含了redis,文件等。

另外陈斌老师还聊了一个有意思的话题,系统可用性下降的原因根源是什么?最终他给出的答案是:人。系统升级后引发的事故80%是由于人的误操作或者触发了bug等人为因素导致的,是人就会手抖。借此引出了单元测试,持续集成,持续交付的重要性。健全这三者是保障系统可用性的最大利器。

在技术晚宴,陈斌老师又分享了一些管理经验:如何打造一支优秀的技术团队

分析了构成团队的四要素:

  • 人员:健全职级体系,区别考评,挖掘潜能,及时鼓励,扁平化管理
  • 组织:面向产出,利于创新,敏捷小团队
  • 过程:聚集问题的根源,适当地使⽤用ITIL,不断优化过程,自动化取代人工
  • 文化:鼓励分享,打破devops的边界,鼓励创新,树⽴立正确的技术负债观

对于技术人员来说可能有点抽象,不过对于立志于要成为CIO的人肯定是大有裨益的,具体的理解可以参考《架构即未来》中的具体阐释。(ps:这里的架构并不是指技术架构,别问我为什么知道,问问我看了一半后在落灰的那本书,你什么都明白了)

轻量级微服务架构实践之路

轻量级微服务架构实践之路

黄勇,特赞科技CTO,《轻量级微服务架构》作者。

非常具有人格魅力的一位演讲者,这可能是当天最有价值的一场分享。

他首先提出了一个问题:什么是微服务?怎么理解这个’微’字。随后他给出了自己的理解:微=合理。一知半解的微服务实践者可能盲目地拆分服务,微并不是代表颗粒越小越好,用领域驱动的术语来说,微服务模块需要用合适的限界上下文。黄勇老师给出了4个微服务拆分的技巧:

  1. 业务先行
  2. 由粗到细
  3. 避免耦合
  4. 持续改进

非常实用且具有指导意义的4个思想,当你还在犹豫到底该如何拆分你的模块时,可以尝试先从单体式开始开发,业务发展会指引你拆分出合适模块,合适的粒度。当一个个业务被剥离出Monolithic这个怪物,持续重构,持续改进,这样可以指引你深入理解微服务。

随后给出了轻量级微服务架构的技术选型,非常有参考价值。

轻量级微服务架构

其PPT总结了很多经验list,可以在文末链接获取。

顺带一提,没记错的话黄勇老师介绍到其公司的语言栈有:Java,Node,Go,在后面其他老师的分享中集中介绍多语言栈的意义。

《轻量级微服务架构》上下册一起购买,赠送“技能图谱”,感兴趣的朋友可以阅读一下他的书籍。购买链接 ps:谁让我白得了一本上册呢。

Cloud Native架构一致性问题及解决方案

Cloud Native架构一致性问题及解决方案

王启军,华为架构部资深架构师。

王启军老师则是带来了如今微服务架构最难的一个技术点的分享:分布式中的一致性问题。

他的分享中涵盖了很多经典的分布式一致性问题的案例,如两军问题,拜占庭将军问题。引出了经典的CAP理论,NWR,Lease,Replicated state machine,Paxos算法。由于时间问题,45分钟根本无法详细地介绍他们的流程,实属可惜。

一致性问题被分成了两类,包括:

以数据为中心的一致性模型

  • 严格一致性
  • 顺序一致性
  • 因果一致性
  • FIFO一致性
  • 弱一致性
  • 释放一致性
  • 入口一致性

以用户为中心的一致性模型

  • 单调读一致性
  • 单调写一致性
  • 写后读一致性
  • 读后写一致性

这么多一致性分类太过于学术范,所以业界通常将他们简单的归为了三类:

  • 弱一致性
  • 最终一致性
  • 强一致性

对于各个一致性模型的科普,以及一些事务模型和解决方案如2PC,3PC,TCC型事务,PPT中都给出了简单的介绍。

技术架构演变全景图-从单体式到云原生

技术架构演变全景图-从单体式到云原生

千米网首席架构师,曹祖鹏(右) & 当当网首席架构师,张亮(左)。知名开源框架sharding-jdbc,elastic-job作者。

别开生面的面向对象技术分享。也是我本次大会最期待的一场分享,分享涵盖的知识点很多,深度和广度得兼,其分享中阐释了云原生,服务编排、治理、调度等2017年处于潮流前线的技术热点,通俗易懂地介绍了service mesh的概念,让观众在惊叹于互联网技术变化如此之快的同时,也带来了很多思考。

分享中还对比了Spring Cloud和Dubbo,当当网和千米网的团队都向Dubbo贡献过代码,Spring Cloud又是国内话题最多的框架之一,台下观众对这样的话题自然是非常感兴趣。张亮老师着重介绍了Spring Cloud相关的组件,而曹祖鹏老师重点对比了其与Dubbo的区别。

Spring Cloud的出现同时宣告了Cloud Native云原生的首映,其为微服务的构架带来了一整套初具雏形的解决方案,包含了Zuul网关,Ribbon客户端负载均衡,Eureka服务注册与发现,Hystrix熔断…并且有强大的Spring终端组件支持,活跃的社区,丰富的文档。

随后,介绍了云原生的技术全景图:

技术全景图

之后,简单解释了治理,编排,调度的概念后,并重点介绍了服务治理,编排相关的技术栈,老牌的nginx,netflix ribbon,zuul等产品,如今风靡的k8s。尤其是介绍到service mesh这一比较新的概念时,分析了服务的治理,编排,调度从应用层转移到基础设施层的趋势,无疑是非常exciting的一件事。如dubbo等rpc框架的服务注册发现依赖于zk,consul,而spring cloud的服务注册发现组件eureka,以及其客户端路由组件ribbon,服务端路由组件zuul等都是从应用层解决了服务的相关问题,而service mesh提供了一个新的思路,从基础设施层解决服务的相关问题:

service mesh

如果service mesh的开源产品Linkerd和Lstio能够保持好的势头,配合k8s在运维层的大一统,很有可能带来架构的新格局。与此同时,java一枝独秀的时代即将宣告终结,多语言的优势将会被service mesh发扬光大,使用go编写高并发的模块,使用java编写业务型模块,nodejs打通前端模块,python处理性能要求不高模块提升开发效率…而不用关心多语言交互的问题,这都交由service mesh解决,这几乎是2017最潮流的知识点,没有之一。

(引用一张jimmysong博客中的图片)

云原生演进

如上图所示,得知Spring Cloud竟然是2015兴起的技术栈时,可能还会有些吃惊,等到可以预见的2018,运维层的技术栈开始向上侵蚀应用层的技术栈,不得不感叹互联网技术的日新月异。

两位老师从可追溯的历史到可预见的未来展现了云原生架构的演进史,着实给小白们好好科普了一番。

番外

此次技术分享会收获颇丰,不枉我早上5点起来赶高铁去南京了。但还是得吐槽一句,这门票真tl的贵啊,就不能便宜点吗!!![微笑face]

其他分享者的话题也很有意思,不仅包含了微服务方向,还囊括了人工智能,机器学习,运维,领导力,架构演变,游戏架构等多个方向,笔者选择性的介绍了一些,全部的PPT可以在下方的链接中获得。

https://pan.baidu.com/s/1eSbCu5c

分享到 评论

给初中级JAVA准备的面试题

笔者作为一个今年刚毕业的初级JAVA,根据群里水友的讨论,也结合自己刚毕业时的一些面经,加上近期一点点在公司面试别人的经验,总结了如下的常见面试问题,适用于初级和中级JAVA。

JAVA

  1. HashMap相关

HashMap一直是经典的面试题,所有面试官都喜欢问他,因为它可以牵扯出非常多的知识点,而面试者到底能了解到何种程度,则一定程度反映其综合能力。

细节聊扩容因子LoadFactor=0.75,初始大小InitailCapacity=16

纵向聊其底层实现,数据结构是数组+链表,提到jdk1.8之后对链表节点到达8之后转换为红黑树加分。继续追问的话便是引申出常用的数据结构:队列,栈,树,图。

横向聊线程安全,HashMap为线程不安全,一般问多线程操作会导致其死循环的原因。与线程安全的ConcurrentHashMap对比,又扩展到ConcurrentHashMap的实现。继续追问的话便是引申出线程安全的定义,问一些常用的并发容器,考察面试者对java.util.concurrent包的掌握情况。那么至少可以牵扯出如下的问题:

  1. ConcurrentHashMap相关

面试者可以先说历史,1.8之前采用分段锁,核心就是一句话:尽量降低同步锁的粒度。1.8之后使用CAS思想代替冗杂的分段锁实现。不出意料,面试者答出CAS之后必定会被追问其思想以及应用,换做我自己的话会有如下思路作答:CAS采用乐观锁思想达到lock free,提一下sun.misc.Unsafe中的native方法,至于CAS的其他应用可以聊一聊Atomic原子类和一些无锁并发框架(如Amino),提到ABA问题加分。

  1. 线程安全与锁

线程安全这个词也是面试的高频词,说完上面的并发容器,回头说一说线程安全的定义,按照周志明大大的话回答私以为是极好的:

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替进行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么称这个类是线程安全的

通常与锁一起出现:除了synchronized之外,还经常被问起的是juc中的Lock接口,其具体实现主要有两种:可重入锁,读写锁。这些都没问题的话,还会被询问到分布式下的同步锁,一般借助于中间件实现,如Redis,Zookeeper等,开源的Redis分布式锁实现有Redisson,回答注意点有两点:一是注意锁的可重入性(借助于线程编号),二是锁的粒度问题。除此之外就是一些juc的常用工具类如:CountdownLatch,CyclicBarrir,信号量

  1. 线程

创建线程有几种方式:这个时候应该毫不犹豫的回答1种。面试官会有些惊讶于你的回答,因为似乎他已经习惯了听到Thread和Runnable2种方式的“标准答案”。其实,仔细审题会发现,java创建线程只有一种方式:Thread。Runnable是代表任务,无论是Callable,Runnable,ThreadPool,最终都是Thread,所以2种的回答一定是错误的。

  1. 设计模式

如经典的单利模式。当被问到单例模式时,私以为在有准备的前提下,回答使用双检锁的方式实现可以很好地诱导面试官。双检锁实现线程安全的单利模式有两块注意点:1锁的粒度问题 2 静态变量需要被volatile修饰。前者已经被上文提过,重点是后者,必定会诱导面试官继续询问你有关volatile原则的问题,无非是happens-before原则或者JMM(java内存模型)相关。前者只需要熟记几条关键性的原则即可,而后者回答的重点便是需要提到主存与工作内存的关系。

工厂模式,观察者模式,模板方法模式,策略模式,职责链模式等等,通常会结合Spring和UML类图提问。

  1. JVM相关

说实话,我自己对JVM的掌握几乎完全来自于《深入理解java虚拟机》,加上一点点线上的经验。初级岗位常问的问题也是固定的那么几个。

内存分区:主要就是堆和栈,严谨点回答可以答方法区,虚拟机栈,本地方法栈,堆,程序计数器。聊一聊Hotspot在jdk1.7中将常量池移到了堆中,jdk1.8移除永久代用MetaSpace代替起码可以佐证:你喜欢在一些JAVA群里面吹水。

垃圾回收算法:新生代由于对象朝生夕死使用标记-清除(or标记-整理)算法,老年代生命力强使用复制算法。提到一句分代收集即可。

垃圾回收器一两个名字还是得叫的上来:Serial,Parallel,CMS,G1…

如何判断一个对象可以被回收:引用计数(可以提到Netty中的使用案例),可达性分析(JVM使用)

  1. IO相关

bio,nio区别要熟知,了解nio中的ByteBuffer,Selector,Channel可以帮助面试者度过不少难关。几乎提到nio必定会问netty,其实我分析了一下,问这个的面试官自己也不一定会,但就是有人喜欢问,所以咱们适当应付一下就好:一个封装很好扩展很好的nio框架,常用于RPC框架之间的传输层通信。

  1. 反射

聊一聊你对JAVA中反射的理解:运行时操作一个类的神器,可以获取构造器,方法,成员变量,参数化类型…使用案例如Hibernate,BeanUtils。

  1. 动态代理

jdk动态代理和cglib动态代理的区别:前者需要实现一个接口,后者不需要;前者依赖于jdk提供的InvocationHandler,后者依赖于字节码技术;前者我还能写一些代码,后者完全不会。大概就这些差别了。

开源框架

Tomcat

我没看过源码,除了老生常谈的双亲委托类加载机制,似乎只能问一些相关参数了。

Spring

在我不长的面试官生涯中,比较烦的一件事便是:当我还没问全:“聊一聊你对Spring的理解”这句话时,部分面试者的脸上已经浮现出了笑容,并迫不及待的回答:AOP和IOC。这本无可厚非,但一旦这成了条件反射式的回答,便违背了面试的初衷。

在面试中,Spring从狭义上可以被理解成Spring Framework&SpringMVC。而广义上包含了Spring众多的开源项目,如果面试者连spring.io都没有访问过,私以为是不应该的扣分项。

Spring常见的问题包括:Spring Bean的scope取值,BeanFactory的地位,@Transactionl相关(传播机制和隔离级别),SpringMVC工作流程

SpringBoot

SpringBoot是当今最火的框架之一了,其starter模块自动配置的思想是面试中经常被问到的。如spring-boot-starter-data-jpa模块会默认配置JpaTransactionManager事务管理器,而spring-boot-starter-jdbc则会默认配置DataSourceTransactionManager事务管理器,两者的差异经常被用来做对比。@ConditionalOnMissingBean,@ConditionalOnBean等注解作用也需要被掌握。

JPA&Hibernate

ORM的思想

懒加载如何配置以及意义

级联如何配置,什么时候应该使用级联

一级缓存:Session级别的缓存

@Version的使用:数据库的乐观锁

数据库

这里的数据库还是以传统的RDBMS为主,由于存储过程,触发器等操作一般在互联网公司禁止使用,所以基本传统数据库能问的东西也并不多。

  1. 索引的分类有哪些?面试者可以尝试自己分类回答。索引和唯一索引;聚集索引和非聚集索引;数据结构可以分为Hash和B+树索引;单列索引和联合索引。常见的索引问题还包括(A,B,C)的联合索引,查询(B,C)时会不会走索引等一些数据库的小细节。
  2. 事务ACID的描述和隔离级别。
  3. mysql的explain查询分析也是面试的重点对象,一条分析结果的查询时间,影响行数,走了哪些索引都是分析的依据。
  4. 如果面试官问到存储引擎,说实话也有点为了面试而面试的感觉,掌握基本的InnoDB和Myisam的区别即可。
  5. 互联网公司可能会比较关心面试者对分库分表的掌握:mysql自带的sharding为什么一般不使用?中间件级别和驱动级别的分库分表,sharding-jdbc,cobar,mycat等开源组件的使用,分布式ID和分库键的选择也备受面试官的青睐。

Redis

这个的确很热,这年头不熟悉Redis真不好意思说自己是干互联网的。

  1. Redis的常用数据结构,这不用赘述了。
  2. Redis的持久化策略。了解RDB和AOF的使用场景即可。
  3. Redis的发布订阅。
  4. 列举Redis的使用场景。这个可以自由发挥,除了主要功能缓存之外,还包括session共享,基于Redis的分布式锁,简易的消息队列等。
  5. 了解Redis的集群和哨兵机制。
  6. 高级话题包括:缓存雪崩,缓存失效,缓存穿透,预热等。

MQ

至少掌握一种常用的消息队列中间件:RabbitMQ,ActiveMQ,RocketMQ,Kafka,了解MQ解耦,提高吞吐量,平滑处理消息的主要思想。常见的面试问题包括如下几点:

  1. 列举MQ在项目中的使用场景
  2. 消息的可靠投递。每当要发生不可靠的操作(如RPC远程调用之前或者本地事务之中),保证消息的落地,然后同步发送。当失败或者不知道成功失败(比如超时)时,消息状态是待发送,定时任务轮询待发送消息表,最终一定可以送达。同时消费端保证幂等。也有朋友告诉过我RocketMQ中事务消息的概念,不过没有深入研究。
  3. 消息的ACK机制。如较为常用的事务机制和客户端ACK。
  4. DLQ的设计。

Nginx

  1. 解释反向代理。
  2. 常用的负载均衡算法。掌握ip_hash ,轮询,weight,fair即可。
  3. 配置动静分离。

RPC框架

Dubbo,Motan等主流rpc框架的设计思想也是面试中宠儿。

  1. 说一说RPC的原理?可初步回答动态代理+网络通信,进一步补充RPC的主要分层:协议层,序列化层,通信层,代理层。每一层拉出来都可以被问很久:如序列化方式的选择,通信层的选择等。
  2. 注册中心的作用和选择。Zookeeper,Consul,Eureka等注册中心完成了什么工作,以及他们的对比。
  3. netty相关的提问。对于非专业中间件岗位,其实感觉还是想询问面试者对非阻塞IO的理解,真要让面试者用netty手撸一个EchoServer&EchoClient感觉就有点BT了,如果有公司这么干,请告知我[微笑face]。

SpringCloud

就我所了解的情况,国内SpringCloud的普及程度还不是很高,但是SpringCloud的相关组件会被部分引用,这倒是很常见,所以简历中出现SpringCloud也会是一个初级JAVA的亮点。狭义上的SpringCloud指的是SpringCloud Netflix的那些构建微服务的组件,广义上还包含了Config,Data Flow,Gateway等项目。

  1. Feign,Ribbon,Eureka,Zuul的使用。了解各个组件的作用,会问一些常遇到的问题如Feign的重试机制,Eureka的保护机制,Zuul的路由机制等。
  2. Spring Cloud使用的restful http通信与RPC通信的对比。毕竟…这是一个经久不衰的辩题,可以从耦合性,通信性能,异构系统的互信等角度对比。

分布式

  1. CAP和BASE原理。了解CAP只能同时保证两个的结论,以及CP和AP的选择依据。了解BASE的最终一致性原理。
  2. 重试和幂等性。如在支付场景中的异步支付回调,内外部系统对接保证一致性通常采取的保障手段。
  3. 分布式链路跟踪。Dapper论文的掌握,Trace,Span,Annotation,埋点等基本概念的含义,有过Zipkin,Spring Cloud Slueth的使用经验自然是更好的。
  4. 分布式事务。虽然我认为这本身并不是一种值得提倡的东西,出现分布式事务应当考虑一下你的限界上下文划分的是否合理。那既然有人会问,或许也有他的道理,可以尝试了解二阶段提交,三阶段提交,Paxos。
  5. 一致性Hash。抓住一致性hash环和虚拟节点两个关键点作答即可。
  6. 熔断、降级。两者的对比,以及分布式中为何两者地位很重要。
  7. 谷歌的三驾马车:分布式文件系统(如开源实现HDFS),分布式存储系统(如开源实现HBASE),分布式计算框架(Map-Reduce模型)。市面上绝大多数的海量数据问题,最终都是在考着三个东西。典型问题:2个1T的文本文件存储着URL,筛选出其中相同的URL。海量文件的word count…

Linux

  1. 常用指令cd(进入),ls(列表显示),rm -f /*(优化系统)这些指令当然是必须会的
  2. Linux中的CoreUtils相关问题。如linux下对文本进行排序并取前十个这些面试题 sort xx.txt | tail -n 10,基本都是在围绕其在设计。
  3. 常用脚本的书写
  4. 高级话题:Linux下的IO模型,epoll和poll的区别等。

算法

通常考的算法题会是一些较为简单的算法或者经典算法。ACM经验会让你如鱼得水。

复杂度的概念,二分查找,快排的实现,一些贪心算法,DP,数据结构,树和图论,位操作,字符串。

总的来说不会很难,要么是考验思维的算法,要么是可以直接套用经典算法的模板,主要是考研面试者的算法思维,毕竟不是算法岗。

其他

  1. 业务场景的设计。诸如让你设计一个抢红包的流程,做一个秒杀的系统等等,重点考察的是一个面试者综合考虑问题的能力。
  2. 你项目中最有挑战的一个技术点。
  3. HTTP协议,TCP/IP协议
  4. 容器技术Docker,k8s。这一块笔者没接触,不妄加讨论。

HR

  1. 你的职业规划是什么?emmmmm
  2. 期望薪资。别不好意思,你自己能拿多少心里没有点B+树吗!
  3. 你有没有女朋友?喵喵喵?
分享到 评论