记一次 Redis 连接问题排查

问题发现

客户端:业务应用使用 lettuce 客户端

服务端:Redis server 部署架构采用 1 主 + 1 从 + 3 哨兵

Redis 和业务应用部署在同一个 K8s 集群中,Redis Server 暴露了一个 redis-service,指向到 master 节点,业务应用通过 redis-service 连接 Redis。

某个时刻起,开始发现业务报错,稍加定位,发现是 Redis 访问出了问题,搜索业务应用日志,发现关键信息:

1
org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: READONLY You can't write against a read only replica.

这是一个 Redis 访问的报错,看起来跟 Redis 的读写配置有关。


聊聊服务发现的推拉模型

前言

过去一年,我的工作重心投入到了 API 网关(阿里云 CSB)中,这对于我来说是一个新的领域,但和之前接触的微服务治理方向又密不可分。API 网关适配微服务场景需要完成一些基础能力的建设,其一便是对接注册中心,从而作为微服务的入口流量,例如 Zuul、SpringCloud Gateway 都实现了这样的功能。实际上很多开源网关在这一特性上均存在较大的局限性,本文暂不讨论这些局限性,而是针对服务发现这一通用的场景,分享我对它的一些思考。


聊聊服务治理中的路由设计

前言

路由(Route)的设计广泛存在于众多领域,以 RPC 框架 Dubbo 为例,就有标签路由、脚本路由、权重路由、同机房路由等实现。

在框架设计层面,路由层往往位于负载均衡层之前,在进行选址时,路由完成的是 N 选 M(M <= N),而负载均衡完成的是 M 选一,共同影响选址逻辑,最后触发调用。

在业务层面,路由往往是为了实现一定的业务语义,对流量进行调度,所以服务治理框架通常提供的都是基础的路由扩展能力,使用者根据业务场景进行扩展。

路由过程

今天这篇文章将会围绕路由层该如何设计展开。


Dubbo中的连接控制,你真的理解吗?

前言

这是一篇很久之前就想动笔写的文章,最近正好看到群里有小伙伴分享了 Dubbo 连接相关的文章,才又让我想起了这个话题。今天想跟大家聊的便是 Dubbo 中的连接控制这一话题。说到“连接控制”,可能有读者还没反应过来,但你对下面的配置可能不会感到陌生:

1
<dubbo:reference interface="com.foo.BarService" connections="10" />

如果你还不了解 Dubbo 中连接控制的用法,可以参考官方文档:https://dubbo.apache.org/zh/docs/advanced/config-connections/ ,话说最近 Dubbo 官方文档来了一次大换血,好多熟悉的文档差点都没找到在哪儿 Orz。

众所周知,dubbo 协议通信默认是长连接,连接配置功能用于决定消费者与提供者建立的长连接数。但官方文档只给出了该功能的使用方法,却并没有说明什么时候应该配置连接控制,本文将主要围绕该话题进行探讨。

本文也会涉及长连接相关的一些知识点。


Dubbo 支持的几个主流序列化框架评测

前言

今天要聊的技术是序列化,这不是我第一次写序列化相关的文章了,今天动笔之前,我还特地去博客翻了下我博客早期的一篇序列化文章(如下图),竟然都过去 4 年了。

历史记录

为什么又想聊序列化了呢?因为最近的工作用到了序列化相关的内容,其次,这几年 Dubbo 也发生了翻天覆地的变化,其中 Dubbo 3.0 主推的 Tripple 协议,更是打着下一代 RPC 通信协议的旗号,有取代 Dubbo 协议的势头。而 Tripple 协议使用的便是 Protobuf 序列化方案。

另外,Dubbo 社区也专门搞了一个序列化压测的项目:https://github.com/apache/dubbo-benchmark.git ,本文也将围绕这个项目,从性能维度展开对 Dubbo 支持的各个序列化框架的讨论。

当我们聊序列化的时候,我们关注什么?

最近几年,各种新的高效序列化方式层出不穷,最典型的包括:

  • 专门针对 Java 语言的:JDK 序列化、Kryo、FST
  • 跨语言的:Protostuff,ProtoBuf,Thrift,Avro,MsgPack 等等

为什么开源社区涌现了这么多的序列化框架,Dubbo 也扩展了这么多的序列化实现呢?主要还是为了满足不同的需求。

序列化框架的选择主要有以下几个方面:

  1. 跨语言。是否只能用于 java 间序列化 / 反序列化,是否跨语言,跨平台。
  2. 性能。分为空间开销和时间开销。序列化后的数据一般用于存储或网络传输,其大小是很重要的一个参数;解析的时间也影响了序列化协议的选择,如今的系统都在追求极致的性能。
  3. 兼容性。系统升级不可避免,某一实体的属性变更,会不会导致反序列化异常,也应该纳入序列化协议的考量范围。

和 CAP 理论有点类似,目前市面上很少有一款序列化框架能够同时在三个方面做到突出,例如 Hessian2 在兼容性方面的表现十分优秀,性能也尚可,Dubbo 便使用了其作为默认序列化实现,而性能方面它其实是不如 Kryo 和 FST 的,在跨语言这一层面,它表现的也远不如 ProtoBuf,JSON。

其实反过来想想,要是有一个序列化方案既是跨语言的,又有超高的性能,又有很好的兼容性,那不早就成为分布式领域的标准了?其他框架早就被干趴了。

大多数时候,我们是挑选自己关注的点,找到合适的框架,满足我们的诉求,这才导致了序列化框架百花齐放的局面。

性能测试

很多序列化框架都宣称自己是“高性能”的,光他们说不行呀,我还是比较笃信“benchmark everything”的箴言,这样得出的结论,更能让我对各个技术有自己的认知,避免人云亦云,避免被不是很权威的博文误导。

怎么做性能测试呢?例如像这样?

1
2
3
long start = System.currentTimeMillis();
measure();
System.out.println(System.currentTimeMillis()-start);

貌似不太高大上,但又说不上有什么问题。如果你这么想,那我推荐你了解下 JMH 基准测试框架,我之前写过的一篇文章《JAVA 拾遗 — JMH 与 8 个测试陷阱》推荐你先阅读以下。

事实上,Dubbo 社区的贡献者们早就搭建了一个比较完备的 Dubbo 序列化基础测试工程:https://github.com/apache/dubbo-benchmark.git。

dubbo-benchmark

你只要具备基本的 JMH 和 Dubbo 的知识,就可以测试出在 Dubbo 场景下各个序列化框架的表现。

我这里也准备了一份我测试的报告,供读者们参考。如果大家准备自行测试,不建议在个人 windows/mac 上 benchmark,结论可能会不准确。我使用了两台阿里云的 ECS 来进行测试,测试环境:Aliyun Linux,4c8g,启动脚本:

1
java -server -Xmx2g -Xms2g -XX:MaxDirectMemorySize=1g -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/home/admin/

为啥选择这个配置?我手上正好有两台这样的资源,没有特殊的设置~,况且从启动脚本就可以看出来,压测程序不会占用太多资源,我都没用满。

测试工程介绍:

1
2
3
4
5
6
7
8
9
public interface UserService {
public boolean existUser(String email);

public boolean createUser(User user);

public User getUser(long id);

public Page<User> listUser(int pageNo);
}

一个 UserService 接口对业务应用中的 CRUD 操作。server 端以不同的序列化方案提供该服务,client 使用 JMH 进行多轮压测。

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
@Benchmark
@BenchmarkMode({Mode.Throughput })
@OutputTimeUnit(TimeUnit.SECONDS)
@Override
public boolean existUser() throws Exception {
// ...
}

@Benchmark
@BenchmarkMode({Mode.Throughput})
@OutputTimeUnit(TimeUnit.SECONDS)
@Override
public boolean createUser() throws Exception {
// ...
}

@Benchmark
@BenchmarkMode({Mode.Throughput})
@OutputTimeUnit(TimeUnit.SECONDS)
@Override
public User getUser() throws Exception {
// ...
}

@Benchmark
@BenchmarkMode({Mode.Throughput})
@OutputTimeUnit(TimeUnit.SECONDS)
@Override
public Page<User> listUser() throws Exception {
// ...
}

整体的 benchmark 框架结构如上,详细的实现,可以参考源码。我这里只选择的一个评测指标 Throughput,即吞吐量。

省略一系列压测过程,直接给出结果:

Kryo

1
2
3
4
5
Benchmark           Mode  Cnt      Score      Error  Units
Client.createUser thrpt 3 20913.339 ± 3948.207 ops/s
Client.existUser thrpt 3 31669.871 ± 1582.723 ops/s
Client.getUser thrpt 3 29706.647 ± 3278.029 ops/s
Client.listUser thrpt 3 17234.979 ± 1818.964 ops/s

Fst

1
2
3
4
5
Benchmark           Mode  Cnt      Score       Error  Units
Client.createUser thrpt 3 15438.865 ± 4396.911 ops/s
Client.existUser thrpt 3 25197.331 ± 12116.109 ops/s
Client.getUser thrpt 3 21723.626 ± 7441.582 ops/s
Client.listUser thrpt 3 15768.321 ± 11684.183 ops/s

Hessian2

1
2
3
4
5
Benchmark           Mode  Cnt      Score      Error  Units
Client.createUser thrpt 3 22948.875 ± 2005.721 ops/s
Client.existUser thrpt 3 34735.122 ± 1477.339 ops/s
Client.getUser thrpt 3 20679.921 ± 999.129 ops/s
Client.listUser thrpt 3 3590.129 ± 673.889 ops/s

FastJson

1
2
3
4
5
Benchmark           Mode  Cnt      Score      Error  Units
Client.createUser thrpt 3 26269.487 ± 1667.895 ops/s
Client.existUser thrpt 3 29468.687 ± 5152.944 ops/s
Client.getUser thrpt 3 25204.239 ± 4326.485 ops/s
Client.listUser thrpt 3 9823.574 ± 2087.110 ops/s

Tripple

1
2
3
4
5
Benchmark           Mode  Cnt      Score       Error  Units
Client.createUser thrpt 3 19721.871 ± 5121.444 ops/s
Client.existUser thrpt 3 35350.031 ± 20801.169 ops/s
Client.getUser thrpt 3 20841.078 ± 8583.225 ops/s
Client.listUser thrpt 3 4655.687 ± 207.503 ops/s

怎么看到这个测试结果呢?createUser、existUser、getUser 这几个方法测试下来,效果是参差不齐的,不能完全得出哪个框架性能最优,我的推测是因为序列化的数据量比较简单,量也不大,就是一个简单的 User 对象;而 listUser 的实现是返回了一个较大的 List<User> ,可以发现,Kryo 和 Fst 序列化的确表现优秀,处于第一梯队;令我意外的是 FastJson 竟然比 Hessian 还要优秀,位列第二梯队;Tripple(背后是 ProtoBuf)和 Hessian2 位列第三梯队。

当然,这样的结论一定受限于 benchmark 的模型,测试用例中模拟的 CRUD 也不一定完全贴近业务场景,毕竟业务是复杂的。

怎么样,这样的结果是不是也符合你的预期呢?

Dubbo 序列化二三事

最后,聊聊你可能知道也可能不知道的一些序列化知识。

hession-lite

Dubbo 使用的 Hessian2 其实并不是原生的 Hessian2 方案。注意看源码中的依赖:

1
2
3
4
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>hessian-lite</artifactId>
</dependency>

最早是阿里开源的 hessian-lite,后来随着 Dubbo 贡献给了 Apache,该项目也一并进入了 Apache,github 地址:https://github.com/apache/dubbo-hessian-lite。相比原生 Hessian2,Dubbo 独立了一个仓库致力于在 RPC 场景下,发挥出更高的性能以及满足一些定制化的需求。

在 IO 线程中进行序列化

Dubbo 客户端在高版本中默认是在业务线程中进行序列化的,而不是 IO 线程,你可以通过 decode.in.io 控制序列化与哪个线程绑定

1
2
3
4
5
<dubbo:reference id="userService" check="false"
interface="org.apache.dubbo.benchmark.service.UserService"
url="dubbo://${server.host}:${server.port}">
<dubbo:parameter key="decode.in.io" value="true" />
</dubbo:reference>

在 benchmark 时,我发现 IO 线程中进行序列化,性能会更好,这可能和序列化本身是一个耗费 CPU 的操作,多线程无法加速反而会导致更多的竞争有关。

SerializationOptimizer

某些序列化实现,例如 Kryo 和 Fst 可以通过显示注册序列化的类来进行加速,如果想利用该特性来提升序列化性能,可以实现 org.apache.dubbo.common.serialize.support.SerializationOptimizer 接口。一个示例:

1
2
3
4
5
6
public class SerializationOptimizerImpl implements SerializationOptimizer {
@Override
public Collection<Class<?>> getSerializableClasses() {
return Arrays.asList(User.class, Page.class, UserService.class);
}
}

按照大多数人的习惯,可能会觉得这很麻烦,估计很少有用户这么用。注意客户端和服务端需要同时开启这一优化。

别忘了在 protocol 上配置指定这一优化器:

1
<dubbo:protocol name="dubbo" host="${server.host}" server="netty4" port="${server.port}" serialization="kryo" optimizer="org.apache.dubbo.benchmark.serialize.SerializationOptimizerImpl"/>

序列化方式由服务端指定

一般而言,Dubbo 框架使用的协议(默认是 dubbo)和序列化方式(默认是 hessian2)是由服务端指定的,不需要在消费端指定。因为服务端是服务的提供者,拥有对服务的定义权,消费者在订阅服务收到服务地址通知时,服务地址会包含序列化的实现方式,Dubbo 以这样的契约方式从而实现 consumer 和 provider 的协同通信。

在大多数业务应用,应用可能既是服务 A 的提供者,同时也是服务 B 的消费者,所以建议在架构决策者层面协商固定出统一的协议,如果没有特殊需求,保持默认值即可。

但如果应用仅仅作为消费者,而又想指定序列化协议或者优化器(某些特殊场景),注意这时候配置 protolcol 是不生效的,因为没有服务提供者是不会触发 protocol 的配置流程的。可以像下面这样指定消费者的配置:

1
2
3
4
5
<dubbo:reference id="userService" check="false"
interface="org.apache.dubbo.benchmark.service.UserService"
url="dubbo://${server.host}:${server.port}?optimizer=org.apache.dubbo.benchmark.serialize.SerializationOptimizerImpl&amp;serialization=kryo">
<dubbo:parameter key="decode.in.io" value="true" />
</dubbo:reference>

&amp; 代表 &,避免 xml 中的转义问题

总结

借 Dubbo 中各个序列化框架的实现,本文探讨了选择序列化框架时我们的关注点,并探讨了各个序列化实现在 Dubbo 中具体的性能表现, 给出了详细的测试报告,同时,也给出了一些序列化的小技巧,如果在 Dubbo 中修改默认的序列化行为,你可能需要关注这些细节。

最后再借 Dubbo3 支持的 Tripple 协议来聊一下技术发展趋势的问题。我们知道 json 能替代 xml 作为众多前后端开发者耳熟能详的一个技术,并不是因为其性能如何如何,而是在于其恰如其分的解决了大家的问题。一个技术能否流行,也是如此,一定在于其帮助用户解决了痛点。至于解决了什么问题,在各个历史发展阶段又是不同的,曾经,Dubbo2.x 凭借着其丰富的扩展能力,强大的性能,活跃度高的社区等优势帮助用户解决一系列的难题,也获得了非常多用户的亲来;现在,Dubbo3.x 提出的应用级服务发现、统一治理规则、Tripple 协议,也是在尝试解决云原生时代下的难题,如多语言,适配云原生基础设施等,追赶时代,帮助用户。


浅析 SpringMVC 中返回对象的循环引用问题

问题发现

@RestController、@ResponseBody 等注解是我们在写 Web 应用时打交道最多的注解了,我们经常有这样的需求:返回一个对象给前端,SpringMVC 帮助我们序列化成 JSON 对象。而今天我要分享的话题也不是什么高深的内容,可能大家多多少少也都遇到过,那就是返回对象中存在循环引用时的问题,分享我的一些思考。

该问题非常简单容易复现,直接上代码。

准备两个循环引用的对象:

1
2
3
4
5
6
7
8
9
10
11
@Data
public class Person {
private String name;
private IdCard idCard;
}

@Data
public class IdCard {
private String id;
private Person person;
}

Nacos Client 1.4.1 版本踩坑记录

问题发现

就在这周,我接到 MSE Nacos 用户的反馈,说线上 Nacos 不可用,服务都下线了,日志里面也是一堆报错,我下意识以为线上炸了,赶紧上线排查。本文主要记录这次问题的排查过程,以及解决方案。

首先看用户反馈的报错,日志如下:

并且用户反馈业务日志也出现了大量的服务地址找不到的报错,说明 Nacos 服务都下线了。

我立刻查看了服务端的监控,发现用户的 MSE Nacos 集群并无异常,cpu/内存等指标有下降,并没有异常行为,排除了服务端异常的可能性。

随即将视线聚焦在了客户端。老实说,这个报错我第一次见,看异常堆栈,字面意思便是域名解析出问题了。这个报错大概持续了 10 分钟,立刻让用户在业务节点上使用 ping、dig 等工具确认域名解析是否正常,测试发现均无异常。继续让用户 telnet mse-xx.com 8848,发现也能够 telnet 通。

根据这些现象,大概能得出结论:用户的机器上出现了短暂的域名解析问题,导致短时间访问不通 MSE Nacos。但用户继续反馈说,一部分重启以后的机器已经恢复了,但没有重启的机器,竟然还会出现调用报错。不然怎么说重启大法好呢,但也加深了问题的诡异性。

正当一筹莫展时,另一用户也找上来了,竟然也是一样的问题,并且由于第二个用户还同时使用了 redis,报错日志中除了出现 nacos 的域名解析问题,还报了 redis 的域名解析报错。至此,更加坚定了我之前推测,根因肯定是域名解析出现了故障,导致这两个用户收到了影响。但问题在于,为什么短暂的域名解析失败(大概 10 分钟),会导致持续性的 Nacos 问题呢?并且只有重启才能恢复。

分析两个用户的共性,最终我和同事将可疑点锁定在了 Nacos 客户端版本上,对比发现,用户都是同一个报错,并且竟然都是 nacos-client 1.4.1 版本。

##Nacos 1.4.1 版本引入的 bug

在问题发生时,Nacos 1.x 最新的版本已经是 Nacos 1.4.2 了,将源码 checkout 到 1.4.1 版本,追踪堆栈附近的问题,

上述这段代码是 Nacos 访问服务端的一段代码,进入 595 行,一探究竟。

我们成功找到了堆栈中的直接报错,就是这段 IsIPv4 的判断触发。splitIPPortStr 这个方法的主要逻辑是从 Nacos 的连接串筛选出连接地址,主要是为了做默认端口号的判断,如果用户没有携带 8848,会默认带上 8848。

但问题恰恰便是出现在这儿:

InetAddress.getByName(addr) 是一个内置的方法,描述如下:

1
Given the name of a host, returns an array of its IP addresses, based on the configured name service on the system.

意思是把一个域名传给操作系统,返回一串 IP,这不就是域名解析吗!我当时就很好奇,你说你判断 IPv4 格式,为啥要这么判断呢?直接判断 IPv4 的 pattern 不行吗?而这段代码,恰恰是导致问题的凶手之一。

我们看看 1.4.2,已经修复了这个逻辑了,直接改成了正则判断。

但疑问还是存在的,域名解析短暂失败了,为啥会导致服务全都下线了,并且解析恢复后,服务依旧没有上线呢?

继续追踪这段代码,发现 callServer 这段代码会被 com.alibaba.nacos.client.naming.beat.BeatReactor 持有,用于维持自身和 Nacos 的心跳。

而由于上述域名解析失败,抛出的异常是 IllegalArgumentException,并没有被里层方法转换成 NacosException,从而导致心跳线程没有 catch 住异常,彻底停止发送心跳了!

这也就成功解释了,为什么短暂的域名解析失败,会导致服务全部下线了。(Nacos 是利用心跳维护和 server 端的存活状态的)

改进建议

  1. 修改 isIPv6 和 isIPv4 的判断方式,改为正则匹配。上文提及,这点已经在 1.4.2 修复了。
  2. 心跳线程要保证不被异常中断下一次心跳的提交。

第二点,也已经被修复了。

总结

nacos-client 1.4.1 存在严重的 bug,客户端与 Nacos Server 如果发生短暂的域名解析问题,会导致心跳永久丢失,进而引发服务全量下线,即使网络恢复,也不会自动恢复心跳。

域名解析失败常见于网络抖动或者 K8s 环境下的 coreDNS 访问超时等场景,为避免域名解析对 Nacos 造成的重大影响,请务必自查应用代码中使用的 nacos-client 的版本。

该问题仅存在于 1.4.1 版本,低于此版本不受此问题的影响,使用 1.4.1 的用户建议升级至 1.4.2 以避免此问题。

使用 SpringCloud/Dubbo 的用户,需要确认实际框架使用的 nacos-client 版本,可以通过显式指定 nacos-client 的版本以覆盖框架默认的版本。其中 Dubbo 用户要格外小心,Dubbo 的 2.7.11 版本默认使用了 nacos-client 1.4.1,务必显式指定 nacos-client 的版本到 1.4.2,Dubbo 也将在下个 release 版本替换 Nacos 的默认版本。


警惕 Spring Boot Actuator 引发的安全漏洞

前言

一年一度的 HW 行动开始了,最近也是被各种安全漏洞搞的特别闹心,一周能收到几十封安全团队扫描出来的漏洞邮件,这其中有一类漏洞很容易被人忽视,但影响面却极广,危害也极大,我说出它的名字你应该也不会感到陌生,正是 Spring Boot Actuator

写这篇文章前,我跟我的朋友做了一个小调查,问他们对 Spring Boot Actuator 的了解,结果惊人的一致,大家都知道 Spring Boot 提供了 spring-boot-starter-actuator 的自动配置,但却很少有人真正用到它相关的特性。在继续往下面看这篇文章时,大家也可以先思考下几个问题:

  1. 检查下你开发的项目中有引入 spring-boot-starter-actuator 依赖吗?
  2. 你在项目中有真正用到 spring-boot-starter-actuator 的有关功能吗?
  3. 你知道 spring-boot-starter-actuator 的安全风险和正确配置方式吗?

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

开头先废话几句,有段时间没有更新博客了,除了公司项目比较忙之外,还有个原因就是开始思考如何更好地写作。远的来说,我从大一便开始在 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 指令,可以一览所有的信号编号。


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

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

注册中心的地位

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

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

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


Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×