你要如何衡量你的人生

坚持,努力,让好事发生

  Java多线程一般都会问到 ThreadLocal,既能考数据结构,又能考线程,还能考JVM。用起来很简单,但是理解需要花费时间。

ThreadLocal是什么

 ThreadLocal 线程本地变量/线程本地存储

此类提供线程局部变量。这些变量不同于普通的对应变量,因为每个访问一个(通过其get或set方法)的线程都有自己独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,它们希望将状态与线程相关联(例如,用户ID或事务ID)。
只要线程是活动的并且ThreadLocal实例是可访问的,则每个线程都对其线程局部变量的副本持有隐式引用。 线程消失后,其线程本地实例的所有副本都将进行垃圾回收(除非存在对这些副本的其他引用)。

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

阅读全文 »

为什么需要序列化?

平时编程时通常使用“对象”来进行数据

public class User {
    private Long id;
    private String name;
    private Integer age;
}

当需要对数据进行存储或者传输时,“对象”就不这么好用了,往往需要把数据转化成连续空间的“二进制字节流”。

一些典型的场景是:
(1) 数据库索引的磁盘存储:数据库的索引在内存里是b+树,但这个格式是不能够直接存储到磁盘上的,所以需要把b+树转化为连续空间的二进制字节流,才能存储到磁盘上;
(2) 缓存的KV存储:redis/memcache是KV类型的缓存,缓存存储的value必须是连续空间的二进制字节流,不能是对象;
(3) 数据的网络传输:socket发送的数据必须是连续空间的二进制字节流,不能是对象;

序列化(Serialization),就是将”对象”形态的数据转化为”连续空间二进制字节流”形态数据的过程。

有哪些常用的序列化?

文本类序列化方式:JSON、xml、csv
二进制类序列化方式:Hessian、Protobuf、thrift、Avro、

文本类序列化方式 优点就是可读性好,构造方便,调试也简单。不过缺点也明显,传输体积大,性能差。

实际上任何一种序列化框架,核心思想就是设计一种序列化协议,将对象的类型、属性类型、属性值一一按照固定的格式写到二进制字节流中来完成序列化,再按照固定的格式一一读出对象的类型、属性类型、属性值,通过这些信息重新创建出一个新的对象,来完成反序列化

文本类

JSON

二进制类序列化方式

Hessian

Hessian 是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架。
Hessian 协议要比 JDK、JSON 更加紧凑,性能上要比 JDK、JSON 序列化高效很多,而且生成的字节数也更小。

Hessian 本身也有问题,官方版本对 Java 里面一些常见对象的类型不支持,比如:
Linked 系列,LinkedHashMap、LinkedHashSet 等,但是可以通过扩展 CollectionDeserializer 类修复;
Locale 类,可以通过扩展 ContextSerializerFactory 类修复;
Byte/Short 反序列化的时候变成 Integer。

Protobuf

Protobuf 是 Google 公司内部的混合语言数据标准,是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列化,支持 Java、Python、C++、Go 等语言。

Protobuf 使用的时候需要定义 IDL(Interface description language),然后使用不同语言的 IDL 编译器,生成序列化工具类

优点是:
序列化后体积相比 JSON、Hessian 小很多;
IDL 能清晰地描述语义,所以足以帮助并保证应用程序之间的类型不会丢失,无需类似 XML 解析器;
序列化反序列化速度很快,不需要通过反射获取类型;
消息格式升级和兼容性不错,可以做到向后兼容。

thrift

thrift TProtocol

序列化协议要考虑什么因素?

安全性、通用性、兼容性、性能、效率、空间开销

不管使用成熟协议xml/json,还是自定义二进制协议来序列化对象,序列化协议设计时都需要考虑以下这些因素。
(1)解析效率:这个应该是序列化协议应该首要考虑的因素,像xml/json解析起来比较耗时,需要解析doom树,二进制自定义协议解析起来效率就很高;
(2)压缩率,传输有效性:同样一个对象,xml/json传输起来有大量的xml标签,信息有效性低,二进制自定义协议占用的空间相对来说就小多了;
(3)扩展性与兼容性:是否能够方便的增加字段,增加字段后旧版客户端是否需要强制升级,都是需要考虑的问题,xml/json和上面的二进制协议都能够方便的扩展;
(4)可读性与可调试性:这个很好理解,xml/json的可读性就比二进制协议好很多;
(5)跨语言:上面的两个协议都是跨语言的,有些序列化协议是与开发语言紧密相关的,例如dubbo的序列化协议就只能支持Java的RPC调用;
(6)通用性:xml/json非常通用,都有很好的第三方解析库,各个语言解析起来都十分方便,上面自定义的二进制协议虽然能够跨语言,但每个语言都要写一个简易的协议客户端;

RPC 框架在使用时要注意哪些问题?

对象构造得过于复杂

属性很多,并且存在多层的嵌套,比如 A 对象关联 B 对象,B 对象又聚合 C 对象,C 对象又关联聚合很多其他对象,对象依赖关系过于复杂。序列化框架在序列化与反序列化对象时,对象越复杂就越浪费性能,消耗 CPU,这会严重影响 RPC 框架整体的性能;另外,对象越复杂,在序列化与反序列化的过程中,出现问题的概率就越高。

对象过于庞大

我经常遇到业务过来咨询,为啥他们的 RPC 请求经常超时,排查后发现他们的入参对象非常得大,比如为一个大 List 或者大 Map,序列化之后字节长度达到了上兆字节。这种情况同样会严重地浪费了性能、CPU,并且序列化一个如此大的对象是很耗费时间的,这肯定会直接影响到请求的耗时。

使用序列化框架不支持的类作为入参类

比如 Hessian 框架,他天然是不支持 LinkedHashMap、LinkedHashSet 等,而且大多数情况下最好不要使用第三方集合类,如 Guava 中的集合类,很多开源的序列化框架都是优先支持编程语言原生的对象。因此如果入参是集合类,应尽量选用原生的、最为常用的集合类,如 HashMap、ArrayList。

对象有复杂的继承关系

大多数序列化框架在序列化对象时都会将对象的属性一一进行序列化,当有继承关系时,会不停地寻找父类,遍历属性。就像问题 1 一样,对象关系越复杂,就越浪费性能,同时又很容易出现序列化上的问题。

参考资料

[1] RPC 实战与核心原理 - 03 | 序列化:对象怎么在网络中传输?
[2] 必须知道的RPC内核细节(值得收藏)!!!
[3] 消息队列高手课 - 12 | 序列化与反序列化:如何通过网络传输结构化的数据?

一个人是否了解RPC的原理和技术细节,问2个问题就可以知道,
1、描述一下RPC的通信流程。
2、在通信时有哪些步骤,分别用到了什么技术?

这个题没有标准答案,这个需要自己理解以后才能描述。


(1) 什么是RPC

RPC 的全称是 Remote Procedure Call,即远程过程调用。

用户视角RPC调用

像调用本地函数一样,调用一个远端服务。
什么是“远程”,为什么“远”?
先来看下什么是,即本地函数调用

(1.1) 本地函数调用

下面是一个两数相加的本地函数

int result = Add(1, 2);
public static int Add(int x, int y) {
    return x + y;
}

本地函数调用

这行代码的时候,到底发生了什么?
(1)传递两个入参;
(2)调用了本地代码段中的函数,执行运算逻辑;
(3)返回一个出参;
这三个动作,都发生在同一个进程空间里,这是本地函数调用。

有办法,调用一个跨进程的函数呢?

阅读全文 »

 协议大家平时都会遇到,只是没有特别注意。
 像平时大家阅读文章的时候都是从上到下、从左往右 按行阅读,这可以看做一种阅读(通信)协议。 ( 备注: 古人在竹简上写的文字则是从上到下、从右往左 按列阅读。)
 阅读作文时第一行是标题,段首要空两格的是一个自然段。遇到一个句号是一句话。
这个就是一种阅读时的通信协议

在计算机远程方法调用时,传输的都是二进制的01,调用方(写数据)和被调用方(读数据)怎么约定通信协议的?

(1) 协议是什么

协议的作用就类似于文字中的符号,作为应用拆解请求消息的边界,保证二进制数据经过网络传输后,还能被正确地还原语义。

那么,服务端收到二进制数据后怎么根据协议解析出数据呢?

假如让你设计,你怎么设计一个协议

(1.1) 协议和序列化的区别

序列化后的二进制数据是协议的子集

RPC其实是把拦截到的方法参数,转成可以在网络中传输的二进制,并保证在服务提供方能正确地还原出语义,最终实现像调用本地一样地调用远程的目的。

那么客户端、服务端如何在二进制流里区分出想要的数据?


(2) 传输协议的作用(职责)

首先要有协议长度,比如用int类型表示,放在二进制数据的前4个字节
需要标识是什么协议
协议可能会有多个版本,需要表示协议版本,用byte类型表示
需要消息Id,用来标识唯一,调用方根据消息Id来区分同一个方法的不同请求,用int类型标识,占4字节
需要消息类型,用来标识是 调用、响应、异常,用byte类型标识,占1字节
需要序列化方式 用byte类型,占用1字节
还需要预留协议扩展字段,不定长,约定协议扩展字段的前2个字节标识扩展字段的长度
协议头长度,除协议体的部分的长度,用2字节表示
剩下的是协议体内容,存放序列化后的二进制数据 (协议体的长度可以根据协议长度-协议头长度获得)

问题一:如何规定远程调用的语法?

客户端如何告诉服务端,我是一个加法,而另一个是乘法。 是用字符串“add”传给你,还是传给你一个整数,比如 1 表示加法,2 表示乘法? (方法描述)
服务端该如何告诉客户端,这个加法,目前只能加整数,不能加小数,不能加字符串; (类型描述)
而另一个加法“add1”,它能实现小数和整数的混合加法。 (类型区分)
返回值是什么?正确的时候返回什么,错误的时候又返回什么? (返回值描述)

问题二:如果传递参数?

是先传两个整数,后传一个操作符“add”,还是先传操作符,再传两个整数?
如果都是 UDP,想要实现一个逆波兰表达式,放在一个报文里面还好,如果是 TCP,是一个流,在这个流里面,如何将两次调用进行分界?什么时候是头,什么时候是尾?
把这次的参数和上次的参数混了起来,TCP 一端发送出去的数据,另外一端不一定能一下子全部读取出来。所以,怎么才算读完呢?

问题三:如何表示数据?

1、如果是变长的类型,是一个结构体,甚至是一个类,应该怎么处理呢?
2、如果是 int,不同的平台上长度也不同,该怎么处理呢?
3、在网络上传输超过一个 Byte 的类型,还有大端 Big Endian 和小端 Little Endian 的问题。假设我们要在 32 位四个 Byte 的一个空间存放整数 1,很显然只要一个 Byte 放 1,其他三个 Byte 放 0 就可以了。那问题是,最后一个 Byte 放 1 呢,还是第一个 Byte 放 1 呢?或者说 1 作为最低位,应该是放在 32 位的最后一个位置呢,还是放在第一个位置呢?最低位放在最后一个位置,叫作 Little Endian,最低位放在第一个位置,叫作 Big Endian。
TCP/IP 协议栈是按照 Big Endian 来设计的,而 X86 机器多按照 Little Endian 来设计的,因而发出去的时候需要做一个转换。

问题四:如何知道一个服务端都实现了哪些远程调用?

从哪个端口可以访问这个远程调用?
假设服务端实现了多个远程调用,每个可能实现在不同的进程中,监听的端口也不一样,而且由于服务端都是自己实现的,不可能使用一个大家都公认的端口,而且有可能多个进程部署在一台机器上,大家需要抢占端口,为了防止冲突,往往使用随机端口,那客户端如何找到这些监听的端口呢?

问题五:发生了错误、重传、丢包、性能等问题怎么办?

本地调用没有这个问题,但是一旦到网络上,这些问题都需要处理,因为网络是不可靠的,虽然在同一个连接中,我们还可通过 TCP 协议保证丢包、重传的问题,但是如果服务器崩溃了又重启,当前连接断开了,TCP 就保证不了了,需要应用自己进行重新调用,重新传输会不会同样的操作做两遍,远程调用性能会不会受影响呢?

协议体里面的内容都是经过序列化出来的,也就是说你要获取到你参数的值,就必须把整个协议体里面的数据经过反序列化出来。但在某些场景下,这样做的代价有点高啊!

(3) 协议核心要素

XID 唯一标识一对请求和回复。请求为 0,回复为 1。
RPC 有版本号,两端要匹配 RPC 协议的版本号。如果不匹配,就会返回 Deny,原因就是 RPC_MISMATCH。
程序有编号。如果服务端找不到这个程序,就会返回 PROG_UNAVAIL。
程序有版本号。如果程序的版本号不匹配,就会返回 PROG_MISMATCH。
一个程序可以有多个方法,方法也有编号,如果找不到方法,就会返回 PROC_UNAVAIL。
调用需要认证鉴权,如果不通过,则 Deny。
参数列表,如果参数无法解析,则返回 GABAGE_ARGS。

协议长度 RPC每次发请求发的大小都是不固定的,所以我们的协议必须能让接收方正确地读出不定长的内容。
序列化方式
协议标示
消息 ID
消息类型

断句,双工通信,配合专用的序列化方法,可以实现一套高性能的网络通信协议。

参考资料

[1] 趣谈网络协议 - 第32讲 | RPC协议综述:远在天边,近在眼前
[2] RPC 实战与核心原理 - 02 | 协议:怎么设计可扩展且向后兼容的协议?
[3] 消息队列高手课 - 13 | 传输协议:应用程序之间对话的语言

服务发现,即消费端自动发现服务地址列表的能力,是微服务框架需要具备的关键能力,借助于自动化的服务发现,微服务之间可以在无需感知对端部署位置与 IP 地址的情况下实现通信。

什么是发现

服务注册,就是将提供某个服务的模块信息(通常是这个服务的ip和端口)注册到1个公共的组件上去(比如: zookeeper\consul\etcd)。

服务发现,就是新注册的这个服务模块能够及时的被其他调用者发现。不管是服务新增和服务删减都能实现自动发现。

实现服务发现的方式有很多种

为什么做服务发现

单体 -> 微服务

在不用服务注册之前,我们可以想象一下,怎么去维护这种复制的关系网络呢?答案就是:写死!。
靠人手的去写配置和变更配置,对于运维和开发同学来说简直就是灾难。

健康检查

服务注册和服务发现的难点及业界的解决方案

服务注册和服务发现的难点

  1. 集群: 得组成集群,这样单台出现故障,不至于服务宕机
  2. 数据同步: 组成了集群,得要数据同步,注册的信息,在1台注册了,在其他机器上也能看到,不然的话,1台挂了,他这台的数据都丢失了。
  3. 强一致性: 数据同步,在多台要有一致性的要求,保证数据不会出现不一致的情况。
  4. 高并发高可用: 要能保证请求量比较大的情况下,服务还能保持高可用。
  5. 选举机制: 在有集群和数据同步以及一致性要求的情况下,得有一个master来主持整个运作,那就要有选取机制,确保选举公平和稳定。
  6. 分布式: 随着微服务上云,各个机器可能近在眼前,却远在天边,如何支撑分布式上的不同环境的机器互联,这也是一个很大的问题。
  7. 安装简单: 一个软件好不好用,是否亲民,安装的易用性是一个很大的因素,如果一个软件安装简单,调试方便,那么就会很受欢迎。

业界的解决方案

目前市面上已经有了服务注册和服务发现的解决方案,代表作是:zookeeperconsul以及etcd,他们功能强大,安全稳定,高并发高可用,强一致性,目前市面上都是用这几个来实现自己的服务注册和发现的。

参考资料

[1] 4、服务发现
[2] 深入了解服务注册与发现
[3] 服务发现

Dubbo是什么

Dubbo 是一款高性能、轻量级的开源RPC框架,提供服务自动注册、自动发现等高效服务治理方案, 可以和 Spring 框架无缝集成。

Dubbo 核心功能有哪些

1、Remoting:网络通信框架,提供对多种NIO框架抽象封装,包括“同步转异步”和“请求-响应”模式的信息交换方式。
2、Cluster:服务框架,提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。
3、Registry:服务注册,基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。

Dubbo能做什么

1、 透明化的远程方法调用,就像调用本地方法一样调用远程方法,只需简单配置,没有任何API侵入。
2、 软负载均衡及容错机制,可在内网替代F5等硬件负载均衡器,降低成本,减少单点。
3、 服务自动注册与发现,不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的IP地址,并且能够平滑添加或删除服务提供者。

主要核心部件

Remoting: 网络通信框架,实现了 sync-over-async 和request-response 消息机制
RPC: 一个远程过程调用的抽象,支持负载均衡、容灾和集群功能
Registry: 服务目录框架用于服务的注册和服务事件发布和订阅

Dubbo快速上手

快速开始使用 Dubbo

参考资料

[1] Dubbo入门
[2] Dubbo3 简介
[3] Dubbo 一篇文章就够了:从入门到实战
[4] Dubbo面试题
[5] Dubbo 源代码分析
[6] 【芋艿】精尽 Dubbo 原理与源码专栏

今天和同事讨论的时候遇到一个问题,API网关是如何把http协议转成rpc协议的?
在使用API网关时,对外提供的http接口,对内使用的公司的rpc协议,在调用方使用http发起请求后,是如何调用到内部服务的rpc接口的?

泛化调用

一般情况下我们通过RPC调用接口提供方的服务,首先在消费端嵌入提供方的Jar包,从而使用Jar包中的类和方法。

在通用的API网关系统中,考虑到扩展性和维护成本,不会使用服务提供方客户端的JAR包,而是通过另外一种方式实现调用,就是泛化调用

其中的原理跟普通的RPC调用时一致的,网络序列化反射这些底层的技术原理一致。区别在于参数和返回值都用Map来表示。任何一个成熟的RPC框架都会支持泛化调用,比如Dubbo提供的泛化。

Dubbo泛化调用

泛化接口调用方式主要用于客户端没有API接口及模型类元的情况,参数及返回值中的所有入参出参均用Map表示,通常用于框架集成,比如:实现一个通用的API网关或者服务测试框架。

可通过 GenericService 调用所有服务实现

import org.apache.dubbo.rpc.service.GenericService; 
... 
 
// 引用远程服务 
// 该实例很重量,里面封装了所有与注册中心及服务提供方连接,请缓存
ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>(); 
// 弱类型接口名
reference.setInterface("com.xxx.XxxService");  
reference.setVersion("1.0.0");
// 声明为泛化接口 
reference.setGeneric(true);  

// 用org.apache.dubbo.rpc.service.GenericService可以替代所有接口引用  
GenericService genericService = reference.get(); 
 
// 基本类型以及Date,List,Map等不需要转换,直接调用 
Object result = genericService.$invoke("sayHello", new String[] {"java.lang.String"}, new Object[] {"world"}); 
 
// 用Map表示POJO参数,如果返回值为POJO也将自动转成Map 
Map<String, Object> person = new HashMap<String, Object>(); 
person.put("name", "xxx"); 
person.put("password", "yyy"); 
// 如果返回POJO将自动转成Map 
Object result = genericService.$invoke("findPerson", new String[]
{"com.xxx.Person"}, new Object[]{person}); 
 
...

API网关

网关系统与RPC环境起初是两个环境的系统,网关系统不需要依赖RPC的存在,各有各的生命周期
api-gateway-and-rpc

在泛化调用这种,网关系统需要知道服务的类名和方法名。还需要做的就是将API通过一种方式存储到网关系统能够访问的一种存储中。比如存储到Redis。
网关系统可以提供一个API发布平台入口,让API发布者将RPC环境下的API数据录入到API发布平台。

API信息 包括所有的方法、入参、出参、注释、描述、接口负责人信息等。

api-gateway-simple-design
如上图
API网关负责API接口获取、协议转换、泛化调用
RPC框架负责 编解码、序列化、反序列化、长连接等。

泛化调用源码解读

参考资料

[1] API网关基石:泛化调用
[2] 微服务中台技术解析之网关 (dubbo-rest) 实践
[3] 使用泛化调用
[4] Dubbo高性能网关–Flurry介绍
[5] dubbo网关演进之路

nginx 可以用于 反向代理、负载均衡、缓存加速、解决跨域 等场景。
详细描述见 官网 http://nginx.org/en/

(1) 下载安装

下载地址 http://nginx.org/en/download.html

(1.1) nginx依赖环境

nginx依赖以下模块:
gzip模块需要 zlib 库 及其开发环境
rewrite模块需要 pcre 库及开发环境
ssl 功能需要openssl库及开发环境以及 yum install -y gcc-c++ 环境。

(1.2) nginx安装环境检查

ZBMAC-C02PGMT0F:nginx-1.18.0 weikeqin1$ ./configure
ZBMAC-C02PGMT0F:nginx-1.18.0 weikeqin1$ make
ZBMAC-C02PGMT0F:nginx-1.18.0 weikeqin1$ make install

./configure 是用来检查安装环境。在configure阶段结束以后,将会出现如下信息:

阅读全文 »

即时编译是一项用来提升应用程序运行效率的技术。通常而言,代码会先被 Java 虚拟机解释执行,之后反复执行的热点代码则会被即时编译成为机器码,直接运行在底层硬件之上。

HotSpot 虚拟机包含多个即时编译器 C1、C2 和 Graal。

Graal 是一个实验性质的即时编译器,可以通过参数 -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler 启用,并且替换 C2

在 Java 7 以前,我们需要根据程序的特性选择对应的即时编译器。
对于执行时间较短的,或者对启动性能有要求的程序,我们采用编译效率较快的 C1,对应参数 -client。
对于执行时间较长的,或者对峰值性能有要求的程序,我们采用生成代码执行效率较快的 C2,对应参数 -server。

阅读全文 »

在 Java 虚拟机的语境下,垃圾指的是死亡的对象所占据的堆空间。这里便涉及了一个关键的问题:如何辨别一个对象是存是亡?

如何判断对象已死

引用计数法(reference counting)

为每个对象添加一个引用计数器,用来统计指向该对象的引用个数。一旦某个对象的引用计数器为 0,则说明该对象已经死亡,便可以被回收了。

具体实现是这样子的:如果有一个引用,被赋值为某一对象,那么将该对象的引用计数器 +1。如果一个指向某一对象的引用,被赋值为其他值,那么将该对象的引用计数器 -1。也就是说,我们需要截获所有的引用更新操作,并且相应地增减目标对象的引用计数器。

缺点:

  1. 需要额外的空间来存储计数器,以及繁琐的更新操作。
  2. 有一个重大的漏洞,那便是无法处理循环引用对象。从而造成了内存泄露。

可达性分析

将一系列 GC Roots 作为初始的存活对象合集(live set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的。

阅读全文 »
0%