分布式调用
负载均衡
负载均衡:高性能集群的设计主要体现在请求分配上,将请求按照一定的规则分配到不同的服务器上执行的过程。
负载均衡器:完成负载均衡的组件或者应用。
负载均衡分类
DNS 负载均衡:DNS 服务器根据用户所处的网络区域选择最近的机房为其提供服务
就近访问所在区域附件的 DNS 服务器
依赖于 DNS 服务器的缓存,而 DNS 服务器的缓存刷新相对较慢
DNS 负载均衡算法一般比较简单
硬件负载均衡:网络区域的负载均衡器负责将请求分配到具体的应用服务器集群
通过硬件设备来实现负载均衡的功能,需要一个专用的网络设备,一般软件用不上
支持超高并发,稳定,防火墙,防 DoS 攻击,……
流行的产品:NetScaler, F5, Radware, Array, ...
软件负载均衡:在集群内通过 Nginx 这样的软件将请求分配到对应的应用服务器
基于特定环境,配置简单,使用灵活,成本低廉
功能:反向代理,负载均衡,动态缓存与过滤
流行的软件:Nginx, LVS, HAProxy, ...
硬件负载均衡产品:F5 BIG-IP
多链路:INBOUND 来自网络的请求信息;OUTBOUND 返回给请求者的应答信息。
“三明治”结构的多防火墙:交换机夹防火墙,交换机确保同一个会话的双向数据流向同一个防火墙
服务器负载均衡
软件负载均衡器:Nginx
反向代理服务器的两个重要功能:
反向代理:对客户端与服务端进行隔离,让客户端的请求找到对应的服务
负载均衡:针对水平扩展的多个服务进行负载均衡,将客户端的请求按策略分配到多个承载相同服务的服务器上
Nginx 最大能处理“万级别”的并发量,“百万级别”的并发量需要在代理层之上加接入层(硬件负载均衡器)。
配置
worker_processes
最大可用进程数(不超过CPU核数)events.worker_connections
每个进程最多能处理的连接数,乘以worker_processes
等于并发量
代理层的动态缓存
Nginx 本身具有提供缓存功能
Nginx 可以和 Redis 直接通信,提取缓存数据
适合缓存不怎么变化的数据,和非常简单的业务(不需要应用服务器处理,集合 Lua 脚本)
缓存数据需要专门的进程对其进行刷新
负载均衡算法
round-robin 轮询算法:均匀分配,Nginx 默认算法
http {
upstream sampleservers {
server 192.168.1.1:8001;
server 192.168.1.2:8002;
}
server {
listen 80;
location / {
proxy_pass http://sampleservers;
}
}
}
weight 权重算法:让资源更好的服务器承担更多的访问量
http {
upstream sampleservers {
server 192.168.1.1:8001 weight 2;
server 192.168.1.2:8002 weight 1;
}
server {
listen 80;
location / {
proxy_pass http://sampleservers;
}
}
}
IP-hash: 来自同一 IP 的用户请求会转发给相同的服务器,可以使同一客户端的 Session 保持一致。优先级高于加权平均。
http {
upstream sampleservers {
ip_hash;
server 192.168.1.1:8001 weight 2;
server 192.168.1.2:8002 weight 1;
}
server {
listen 80;
location / {
proxy_pass http://sampleservers;
}
}
}
hash: 比 IP-hash 更强大,自定义请求的哈希值,哈希值相同的请求会被转发到同一个服务器上
http {
upstream backend {
hash $remote_addr consistent;
server backend1.example.com;
server backend2.example.com;
server backend3.example.com;
}
server {
listen 80;
location / {
proxy_pass http://backend;
}
}
}
hash ...
指定哈希算法$remote_addr
使用客户端的 IP 地址作为哈希的键consistent
使用一致性哈希,服务器增减时只重新分配受影响的请求,否则可能重新分配所有请求
least_conn: 将请求转发给连接数较少的服务器。比默认的轮询要好。
API 网关
API 网关的意义
从业务层面上看,完成某个业务需要多个微服务配合时,不应该让客户端去调用这些微服务,而是应该由服务端将这些微服务打包,提供一个单一的接口给客户端使用
从系统层面上看,外部调用微服务,微服务之间的调用,都需要有一个统一的入口方便管理
从客户端层面上看,API 网关可以解决为不同类型的客户端分配不同微服务的问题
API 网关的服务定位
面向 WebAPP:前后端分离模式的 Web/H5 应用
面向MobileAPP:需要做额外的设备生命周期管理工作
OpenAPI:对外开放,需要注意流量和安全问题
内部服务:着重考虑功能边界、认证和授权的问题
API 网关的技术原理
实现时需要考虑的问题:
协议转换:定义通用协议转换各种协议的微服务(HTTP, HTTPS, HTTP 1.1/1, Dubbo, Thrift, gRPC, ...)
链式处理:Netflix 开源网关 Zuul, 责任链模式,分步骤按顺序处理请求
异步请求:API 网关维护自己的事件循环
服务注册与发现
Nginx 可以简单实现服务A对经过水平扩展的服务B的调用(利用其负载均衡功能):
http {
upstream servers {
server 192.168.1.1:8001 weight 2;
server 192.168.1.2:8002 weight 1;
}
server {
listen 80;
location /service_b {
proxy_pass http://servers;
}
}
}
问题是需要在Nginx配置文件中手动管理各个服务的调用地址,不优雅。
所以需要一个专门的服务,来管理各个服务调用地址及其他元数据,即负责服务注册发现的服务。
服务注册与发现的概念和原理
服务提供者:在启动服务时向服务注册中心注册自己
服务消费者:在启动时在服务注册中心订阅自己需要的服务
一个服务既可能是提供者,也可能是消费者。
简单情形下,服务消费者可以直接向服务注册中心查询提供者地址;
复杂情形下,消费者需要维护一张本地路由表,当服务注册中心数据变更时主动更新消费者本地的路由表,消费者基于本地路由表查询提供者的地址。
服务注册中心的可用性
服务之间的互相发现强依赖于服务注册中心的可用性——支持对等集群。
数据复制的两种模式:
主从复制(Master-Slave):主库负责读写,从库负责读
对等复制(Peer to Peer):服务节点地位相同,客户端只需连接一个即可;对等集群内部需要定期进行数据同步
服务注册中心的服务保存
其他服务主动向服务注册中心发送“续约”心跳包,以维持自己在服务注册中心的可用性。
服务注册中心定时更新各服务本地的路由表。
服务间的远程调用
微服务的调用场景示例:一个服务依赖于多个其他服务时,会导致网络调用(不同服务器间的调用)次数的增加
RPC 调用过程
RPC: Remote Procedure Call, 远程过程调用,远程函数:让调用远程服务看起来像调用本地服务一样简单!
RPC 动态代理
关于 Java 的动态代理
InvocationHandler
是 Java 反射机制中的一个接口,用于定义代理对象在处理方法调用时的行为,是实现动态代理的关键部件之一。该接口位于java.lang.reflect
包中,定义如下:public interface InvocationHandler { // 代理对象实例,被调用的方法对象,方法调用的参数数组 Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }
invoke
方法在代理对象上的每个方法调用时都会被调用,因为可以在其中实现任何自定义逻辑,例如日志记录、性能监控、权限检查、缓存处理等。下面是一个简单的自定义调用处理器:
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; class MyInvocationHandler implements InvocationHandler { private Object target; public MyInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Before method invocation"); Object result = method.invoke(target, args); System.out.println("After method invocation"); return result; } }
使用自定义调用处理器动态代理目标对象:
import java.lang.reflect.Proxy; public class ProxyExample { public static void main(String[] args) { // 创建目标对象 HelloImpl target = new HelloImpl(); // 创建InvocationHandler MyInvocationHandler handler = new MyInvocationHandler(target); // 创建代理对象 Hello proxyInstance = (Hello) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler ); // 调用代理对象的方法 proxyInstance.sayHello(); } }
我们不使用
taget.sayHello()
而是使用proxyInstance.sayHello()
!
RPC 通过动态代理机制,为客户端生成服务类的代理类:真实服务对象运行在服务端,代理对象运行在客户端。
在客户端调用服务的方法时,会被代理对象拦截,注入远程调用逻辑即可模拟本地调用。
// 定义服务接口
public interface ServerProvider {
public void sayHello(String str);
}
// 实现服务接口
public class ServerProviderImpl implements ServerProvider {
@Override
public void sayHello(String str) {
System.out.println("Hello " + str);
}
}
实现一个代理类,客户端将调用代理对象的方法而不是真实对象的方法。
public class DydamicProxy implements InvocationHandler {
private Object realObject;
public DynamicProxy(Object object) {
// 此处使用实例变量存储代理类接收到的真实的服务对象(动态代理)
this.realObject = object;
}
public Object invoke(Object object, Method method, Object[] args) {
// 在这里实现远程调用逻辑
method.invoke(realObject, args);
return null;
}
}
调用代理对象的服务方法时,invoke 方法总是会被调用!我们在 invoke 方法中实现远程调用逻辑,将在客户端上调用代理对象方法,等价成在服务端调用真实对象的方法。
public class Client {
public static void main(String[] args) {
ServerProvider realServerProvider = new ServerProviderImpl();
InvocationHandler handler = new DynamicProxy(realServerProvider);
ServerProvider serverProvider = (ServerProvider) Proxy.newProxyInstance(
// ClassLoader loader: 用于加载代理类的类加载器
handler.getClass().getClassLoader(),
// Class<?>[] interfaces: 代理类要实现的接口数组,代理类会代理这些接口中定义的所有方法
realServerProvider.getClass().getInterfaces(),
// InvocationHanler h: 负责处理代理实例上的方法调用,所有代理实例上的方法调用都会被转发到这个对象的 invoke 方法上
handler);
serverProvider.sayHello("world");
}
}
客户端使用 serverProvider.sayHello("world")
而不是 realServerProvider.sayHello("world")
!
几个理解要点:
动态代理是 RPC 实现本地调用体验的关键
服务对象运行在服务端,代理对象运行在客户端,它们一一对应
虽然业务逻辑是服务对象提供的,但是我们在客户端上不直接调用服务对象的方法(客户端调用了也没法儿执行),而是调用其代理对象的对应方法(代理对象需要实现远程调用逻辑)
代理对象实现的远程调用逻辑指的就是,代理对象收到数据=>将数据发送到服务端=>调用服务对象方法=>将执行结果返回给代理对象=>代理对象返回执行结果这个过程,对于调用者来说,与调用本地函数体验一样
动态代理就是在运行时基于目标对象,动态创建代理对象的过程
Java 实现动态代理的关键步骤:创建代理类(实现
InvocationHandler
接口)、创建代理类实例(调用Proxy.newProxyInstance
方法)
RPC 序列化
代理对象 invoke 方法只的远程调用逻辑涉及到网络数据传输:
代理对象的关键信息(类型、属性、属性值、方法名、方法传入参数等)转换成字节流发送到服务端
服务端将字节流数据转换成服务端对象,然后完成调用
服务端将调用结果转换成字节流发送到客户端
客户端将收到的字节流转换成语言内置对象
常见的序列化/反序列化方式:
JSON:文本序列化,类型丢失,适合小数据
Hessian:动态类型、二进制、紧凑,被广泛用于 RPC 序列化协议
Protobuf:更小,IDL 优势
Thrift:Thrift 框架专用
协议编码
这部分是 RPC 协议的内容,开发时一般不涉及。
RPC 是应用层协议(一般基于 TCP),与 HTTP 处于同一层。
传输层协议:TCP,UDP。
网络传输
RPC的网络传输本质上是服务调用方和服务提供方的一次网络信息交换过程。
网络 IO 传输的结果是将数据包放到内核缓冲区中,数据从内核缓冲区复制到应用缓冲区后就可以进行数据计算。
阻塞式调用时,所有响应数据全部写入内核缓冲区后才会一起复制到应用缓冲区,然后应用开始处理数据(同步阻塞IO,Blocking IO)。流式调用时,每接收一块响应数据,内核都会将数据从内核缓冲区复制到应用缓冲区,触发应用的数据处理动作。
同步非阻塞IO(non-blocking IO)通过轮询实现:应用周期性的询问内核是否准备好数据。
IO 多路复用指的是单独开一个数据进程,由该进程主动通知应用处理数据。
Netty 实现 RPC
Java 的 RPC 异步通信框架,基于事件驱动。
总结
客户端的调用请求需要经过负载均衡器进入应用服务器:DNS负载均衡、硬件负载均衡、软件负载均衡
负载均衡算法:平均、加权平均、基于IP地址的哈希、自定义键的哈希等
API 网关负责将服务进行聚合提供简单明了的调用接口给客户端:协议转换、链式处理、异步请求
应用内部的各个服务之间怎么互相发现?服务注册与发现机制
服务之间的通信——RPC调用:动态代理-序列化-协议编码-网络传输
Java 的异步 RPC 框架——Netty
评论区