0%

微服务的通信模式

概念

  • 微服务(Microservices)是一种架构风格,一个大型复杂应用服务由一个或多个微服务组成。系统中的各个微服务可被独立部署,各个微服务之间是松耦合的。每个微服务仅关注于完成一件任务,每个任务代表着一个小的业务能力。

    优势 成本
    降低复杂度、可独立部署、容错、扩展 资源增加、沟通成本、网络通信开销、网络安全、服务监控、运维成本
  • RPC:请求/响应式的通信技术,它使得我们可以像调用本地函数一样调用一个远程服务,通常能够自动生成客户端和服务端的代码,常用于服务间的访问。

  • REST:是基于HTTP(s)的RESTful风格的通信技术,架构模型倾向于遵循 HTTP 协议,浏览器对其兼容性更好。数据结构默认使用Json/Xml。

  • gRPC:是基于 RCP 架构的变体,该技术遵循一个使用 HTTP 2.0 协议的 RPC API 实现。数据结构默认使用Protobuf。

    比较 gRPC 服务和 HTTP API

问题背景

  1. 现有微服务架构,涵盖了dubbo、SpringCloud、K8s多个框架,有些冗余不统一。

  2. 现有微服务架构,有安全隐患,在服务与服务之间的内部访问采用无权限校验时,存在接口外露的安全问题。

    例如:使用feign的时候,你要给别的服务提供接口,就要对feign的接口不做登录校验,然后feign的服务提供不同于dubbo是单独的一个端口,因此对服务的端口进行nginx对外配置时,也让内部服务队外进行暴露了,如果不在nginx或网关做特殊处理,外网随时可以访问内部接口,且没有安全认知机制。

  3. 由于服务网格的风生水起,作为一个研究方向还是值得去研究的。

微服务通信主要解决的问题?

服务之间访问API的便利性

  • DUBBO RPC: 有dubbo api jar支持,使用函数式访问调用,只用为对方提供一个api jar包即可,文档要求低,不易出错,采用RPC封装,可以使用Protobuf压缩网络资源的消耗。

  • Spring feign: 也能生成feign的接口 jar为其他服务提供调用,而且支持文档自动生成,不易出错。

  • Http:http直接访问的模式很多,可以用很多工具例如hutool等,但是文档要求高、而且需要知道提供者服务的ip、域名、在k8s模式下访问其他服务,需要知道其他服务在k8s中的服务名。

复原能力

  • 重试
  • 熔断

负载均衡

服务A调用服务B,服务B为一组Pod,存在多个ip地址,默认情况通过负载均衡实现随机访问一个pod。

分布式追踪

链路追踪,快速排除服务之间访问的问题。

服务版本控制

解决服务之前版本依赖问题。

微服务通信设计

服务间通信越少,性能越好。

交互类型:

查询:一个服务向另一个服务查询数据。
  • 请求/响应消息传送:简单的微服务之间调用,层级比较少时适用

    上图就是层级较多时,调用链复杂,存在大量耦合。

  • 具体化视图模式

  • 聚合器微服务:减少服务依赖。

    此模式隔离了对多个后端微服务进行调用的操作,将其逻辑集中到专门的微服务中。 上图中的紫色签出聚合器微服务协调签出操作的工作流。 它包括按顺序调用多个后端微服务。 工作流中的数据将聚合并返回给调用方。 尽管它仍实现直接 HTTP 调用,但聚合器微服务减少了后端微服务之间的直接依赖关系。

  • 请求/答复模式:利用队列进行设计。

命令:一个服务让另一个服务执行一些操作。
事件:订阅通知模式。

K8S服务与服务之间访问

Service的类型

  1. clusterIP:通过集群内部IP地址暴露服务,但该地址仅在集群内部可见、可达,它无法被集群外部的客户端访问;默认类型;建议由K8S动态指定一个,也支持用户手动明确指定;

    特点:安全性高,内部访问速度比较快,不支持跨集群访问。

  2. NodePort: NodePort是ClusterIP的增强类型,它会于ClusterIP的功能之外,在每个节点上使用一个相同的端口号将外部流量引入到该Service上来。

    特点:支持跨集群访问,方便外部访问调试、安全性低(接口暴露)

  3. LoadBalancer: 是NodePort的增强类型,为各节点上的NodePort提供一个外部负载均衡器;需要公有云支持

  4. ExternalName:外部流程引入到K8S内部,借助集群上KubeDNS来实现,服务的名称会被解析为一个CNAME记录,而CNAME名称会被DNS解析为集群外部的服务的TP地址,实现内部服务与外部服务的数据交互 ExternallP 可以与ClusterIP、NodePort一起使用 使用其中一个IP做出口IP

pSDSCcR.png

问题:

  1. feign访问如果没有网关,内部服务之间透明访问,存在安全隐患
  2. nodePort需要固定,随机分配,迁移复制能力弱,nodeport资源占用大
  3. eureka注册和dubbo注册需要使用节点地址和端口

Spring Cloud

Spring家族产品,包含一系列组件,其实就是整个Spring生态组件框架的统称。也是基于SpringBoot框架的基础上与众多组件框架相结合从而形成一个整的微服务架构生态,SpringCloud提供配置管理,服务智能,断路器,智能路由,微代理,控制总线,全局锁,决策竞选分布式会话和集群状态管理一系列解决方案。

spring Cloud架构

spring Cloud架构

优缺点

优势 缺点
在spring开发体系下,原生支持SpringCloud 强依赖java语言
基于HTTP通信协议门槛低
提供大量组件(生态完善)

Dubbo

Dubbo是阿里巴巴公司自主研发并开源的一款高性能、轻量级的开源RPC分布式服务框架,服务消费方引用服务提供方二方库本地化调用远程接口,通过注册中心zookeeper做服务提供者、服务消费者服务的注册与发现来实现远程服务间的数据交互,支持服务智能容错、智能均衡、灰度,可与Spring集成。

Dubbo 服务架构

Dubbo 服务架构

优缺点

优势 缺点
灵活的通信协议(HTTP、gRPC)

Istio mesh

主要解决:日志记录、监控、流量控制、服务注册、服务发现、服务限流、服务熔断、鉴权、访问控制和服务调用可视化等。

服务网格是处理服务间通信的软件层。 服务网格旨在解决上一部分中列出的许多问题,并将这些问题的责任从微服务本身转移到共享层。 服务网格充当代理,可截获群集中微服务之间的网络通信。 目前,服务网格概念主要适用于容器业务流程协调程序,而不是无服务器体系结构。

目前,Kubernetes 中服务网格的主要选项包括 LinkerdIstio。 这两种技术正在快速演进。 但是,Linkerd 和 Istio 具有一些共同的功能,包括:

  • 根据观测到的延迟或未完成的请求数,在会话级别进行负载均衡。 这样,便可以基于 Kubernetes 提供的第 4 层负载均衡来提高性能。
  • 基于 URL 路径、主机标头、API 版本或其他应用程序级规则进行第 7 层路由。
  • 失败请求重试。 服务网格可识别 HTTP 错误代码,并可以自动重试失败的请求。 可以配置最大重试次数和超时期限,以限制最大延迟。
  • 断路。 如果某个实例一直无法完成请求,则服务网格会暂时性地将它标记为不可用。 在回退期过后,服务将重试该实例。 可以根据多个条件(例如连续失败次数)来配置断路器。
  • 服务网格会捕获有关服务间调用的指标,例如请求量、延迟、错误和成功率,以及响应大小。 此外,服务网格可以通过添加请求中每个跃点的关联信息,来启用分布式跟踪。
  • 服务间调用的相互 TLS 身份验证。

是否需要服务网格? 视情况而定。 如果不使用服务网格,则需要考虑本文开头所述的难题。 不使用服务网格也可以解决重试、断路器和分布式跟踪等方面的问题,但是,服务网格可将这些问题从单个服务转移到专用的层。 另一方面,服务网格会增大群集设置和配置的复杂性。 另外,它可能造成性能影响,因为请求现在是通过服务网格代理路由的,并且附加的服务在群集中的每个节点上运行。 在生产环境中部署服务网格之前,应该执行全面的性能和负载测试。

Istio 服务网格架构

 Istio架构

优缺点

优势 缺点
语言无关(兼容多种平台语言) 增加服务间延迟
减少应用的连接数(例如:多个微服务连接数据库) 增加运维复杂
容器友好,和k8s集成度高 强依赖k8s平台
可视化程度高(方便监控服务间调用情况/apm链路追踪) 增加硬件资源
支持动态调整流量、负载均衡等

注册中心兼容比较

Nacos Eureka Consul CoreDNS Zookeeper etcd(k8s)
一致性协议 CP+AP AP(可用性高) CP(一致性高) CP CP
一致性算法 Raft Raft Paxos Raft
健康检查 TCP/HTTP/
MYSQL/Client Beat
Client Beat TCP/HTTP/
gRPC/Cmd
Keep Alive 连接心条
负载均衡策略 权重/
metadata/Selector
Ribbon Fabio RoundRobin Envoy
雪崩保护
自动注销实例 支持 支持 不支持 不支持 支持
访问协议 HTTP/DNS HTTP HTTP/DNS DNS TCP HTTP/GRPC
监听支持 支持 支持 支持 不支持 支持 支持
多数据中心 支持 支持 支持 不支持 不支持
跨注册中心同步 支持 不支持 支持 不支持 不支持
SpringCloud集成 支持 支持 支持 不支持 支持 支持
Dubbo集成 支持 不支持 不支持 不支持 支持
K8S集成 支持 不支持 支持 支持 不支持 支持

参考

为微服务设计服务间通信

zookeeper安装

1
2
3
4
5
6
7
brew install zookeeper #安装
zkServer start #启动
zkcli -server localhost:2181 #客户端连接zk服务
[zk: localhost:2181(CONNECTED)] ls / # ls进行查看连接资源
[zk: localhost:2181(CONNECTED) 13] ls /dubbo/com.api.ITestService/providers #查看连接的提供者
#docker方式启动
docker run --name some-zookeeper -p 2181:2181 --restart always -d zookeeper

springboot 引入dubbo

消费者和提供者公共配置

  1. 添加依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <dependency>
    <groupId>com.alibaba.boot</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>0.2.0</version>
    </dependency>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>dubbo</artifactId>
    <version>2.6.6</version>
    </dependency>
    <dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.33.Final</version>
    </dependency>
  2. 启用dubbo,在启动类添加注解@EnableDubbo

  3. 配置文件添加zk,spring.dubbo.registry.address = zookeeper://localhost:2181

  4. 添加接口

    1
    2
    3
    public interface ITestService {
    public String hello();
    }

提供者代码

1
2
3
4
5
6
7
8
9
import com.alibaba.dubbo.config.annotation.Service;

@Service
public class TestService implements ITestService {
@Override
public String hello() {
return "hello world!";
}
}

消费者代码

1
2
3
4
5
6
7
8
9
10
11
12
import com.alibaba.dubbo.config.annotation.Reference;

@RestController
public class TestController {

@Reference(check = false)
ITestService iTestService;

@GetMapping("hello")
public String hello(){
return iTestService.hello();
}

常见问题

  1. 异常信息

    1
    2
    cause: io/netty/bootstrap/ServerBootstrap
    at com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol.createServer(DubboProtocol.java:287)[dubbo-2.6.6.jar:2.6.6]

    解决:添加netty依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.33.Final</version>
    </dependency>
  2. 异常信息

    1
    2
    java.lang.IllegalArgumentException: Specified invalid port from env value:0
    at com.alibaba.dubbo.config.ServiceConfig.parsePort(ServiceConfig.java:691) ~[dubbo-2.6.6.jar:2.6.6]

    解决:环境变量设置有问题,在vm上覆写参数-DDUBBO_PORT_TO_BIND=20880

  3. 异常信息

    1
    2
    failed to connect to server /10.0.0.54:20880, error message is:connection timed out: /10.0.0.54:20880
    at com.alibaba.dubbo.remoting.transport.netty4.NettyClient.doConnect(NettyClient.java:127) ~[dubbo-2.6.6.jar:2.6.6]

    解决:多网卡下ip绑定错误,导致连接超时,在vm添加参数-DDUBBO_IP_TO_BIND=10.9.98.133绑定ip

  4. 异常信息

    1
    2
    3
    c.c.b.s.w.a.GlobalExceptionHandler : application error
    com.alibaba.dubbo.rpc.RpcException: No provider available from registry 192.168.240.15:2181 for service cn.com.api.dubbo.service.TestDubboService:1.0.0 on consumer 10.233.73.69 use dubbo version 2.6.6, please check status of providers(disabled, not registered or in blacklist).
    at com.alibaba.dubbo.registry.integration.RegistryDirectory.doList(RegistryDirectory.java:590)

    解决:如果zookeeper都显示提供者和消费者都注册通知正常,但是消费者缓存里面拿不到提供者的服务地址,检查spring.dubbo.invoke.invalid.ips 这个配置是否把你的ip拦截了

Keepalived介绍

大白话:就是虚拟一个ip,然后虚拟机绑定该ip,如果该虚拟机挂掉,该另一个虚拟机就绑定虚拟ip。

keepalived是基于VRRP协议为LVS服务提供高可用方案。主要提供了负载均衡和高可用功能,用来避免单点故障。负载均衡是通过linux的IPVS(ip虚拟服务器)实现,高可用通过VRRP实现多机故障转移。
  keepalived一般是2个节点运行keepalived,一台是主节点(MASTER),一台是备节点(BACKUP)对外表现都是一个虚拟IP,主节点会通过组播方式发送特定的消息给备节点,如果备节点收不到这个特定消息时,说明主节点就宕机了,此时备节点就会接管VIP进行服务提供,这就实现了高可用。

  • VRRP虚拟路由器冗余协议(英语:Virtual Router Redundancy Protocol,缩写为 VRRP)是一种网络协议,可以为参与的路由器自动分配可用的IP地址。这个协议通过在子网中,自动选取默认网关,来增加路由的可用性和可靠性。

    VRRP原理:是一种容错协议,保证当主机的下一条路由器出现故障时,由另一台路由器来代替出现故障的路由器进行工作,从而保持网络通信的连续性和可靠性。

    VRRP相关术语:

    1. 虚拟路由器:由一个Master路由器和一个或多个Backup路由器组成。
    2. VRID:虚拟路由器的标识。同一虚拟路由器内的路由器有着相同的VRID。
    3. VIP(虚拟IP地址):路由器组(虚拟路由器)的IP地址。
    4. 抢占模式与非抢占模式:Master会以组播方式不断的向虚拟路由器组内发送自己的心跳报文,一旦Backup在设定时间内没有收到心跳信息的次数超过了设定次数,则会将Master的所有权转移到优先级最高的Backup,则就是抢占模式。非抢占模式是指只有在主节点完全故障时才能将backup变为master。
  • 组播:网卡支持并开启组播功能,组播通信方式是一种很节省资源的通信方式。其余还有单播、广播。

  • LVSLinux虚拟服务器(Linux Virtual Server)是一个虚拟的服务器集群系统,用于实现负载平衡

  • VIP虚拟IP地址(Virtual IP Address),主要是用来进行不同主机之间的切换,主要用在服务器的主从切换。

L2ohL9.png

实战

常用命令

1
2
3
4
#安装
yum install -y keepalived
# 启动keepalived
systemctl start keepalived

10.0.0.10双主热备配置文件 /etc/keepalived/keepalived.conf10.0.0.11修改相反即可

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
! Configuration File for keepalived

global_defs {
router_id NGINX_DEVEL1 # 标识节点的字符串,一般设置为hostname
}

vrrp_script chk_nginx { #检测业务进程(nginx)来调整keepalived权重
script "/etc/keepalived/check_nginx.sh"
interval 2 #每2s检测一次
#weight -5 #检测失败(脚本返回非0)则优先级 -5
weight 2 #检测失败(脚本返回非0)则优先级 +2?
fall 3 #检测连续3次失败才算确定是真失败。
rise 2 #检测 2 次成功就算成功。但不修改优先级
}

vrrp_instance VI_1 {
state MASTER # 标识主节点服务(只有MASTER和BACKUP两种,大写)
interface eth0 # 当前ip的网卡接口
mcast_src_ip 10.0.0.10 # 本机IP地址
virtual_router_id 51 # 虚拟路由 ID(0-255),在一个 VRRP 实例中主备服务器 ID 必须一样
priority 200 # 优先级值设定:MASTER 要比 BACKUP 的值大
advert_int 1 # MASTER和BACKUP节点之间的同步检查时间间隔,单位为秒,主被设置一致

track_interface { # 设置额外的监控,eth0网卡出现问题也会切换
eth0
}

authentication { # 认证机制,主从节点保持一致即可
auth_type PASS
auth_pass password123 # MASTER和BACKUP使用相同明文才可以互通
}

virtual_ipaddress { # 虚拟IP地址池,可以多个IP
10.0.0.20/24 # 虚拟IP N个(VIP)
}

track_script {
chk_nginx
}
}

vrrp_instance VI_2 {
state BACKUP
interface eth0
mcast_src_ip 10.0.0.11
virtual_router_id 52
priority 180
advert_int 1

track_interface {
eth0
}

authentication {
auth_type PASS
auth_pass password123
}

virtual_ipaddress {
10.0.0.21/24
}

track_script {
chk_nginx
}
}

后面就可以通过vip进行访问了。

ip a s查看vip

1
2
3
4
5
6
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 52:54:99:e5:7e:13 brd ff:ff:ff:ff:ff:ff
inet 10.0.0.10/24 brd 10.255.243.255 scope global noprefixroute dynamic eth0
valid_lft 58597sec preferred_lft 58597sec
inet 10.0.0.20/24 scope global secondary eth0
valid_lft forever preferred_lft forever

/etc/keepalived/check_nginx.sh脚本内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash

function check_nginx() {
for ((i=0;i<5;i++));do
nginx_job_id=$(pgrep nginx)
if [[ ! -z $nginx_job_id ]];then
return
else
sleep 2
fi
nginx_job_id=0
done
}

# 1: running 0: stopped
check_nginx
if [[ $nginx_job_id -eq 0 ]]; then
/usr/bin/systemctl stop keepalived
exit 1
else
exit 0
fi

百度经验助手

名称:百度经验助手

匹配规则:https://jingyan.baidu.com/edit/content*

脚本:

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
'use strict';
//修改标题和图标
var link = document.querySelector("link[rel*='icon']") || document.createElement('link');
link.type = 'image/x-icon';
link.rel = 'shortcut icon';
link.href = 'https://blog.iexxk.com/images/favicon-32x32-next.png';
document.getElementsByTagName('head')[0].appendChild(link);
document.title = '经验笔记编辑';
//隐藏不用的元素
document.getElementById("js_float_high_quality_wrap").style.visibility = "hidden";
document.getElementById("header").style.display = "none";
document.querySelector("div.wgt-benefit").style.display = "none";
document.querySelector("div.video-guide-left").style.visibility = "hidden";
//检测敏感词汇
var eb=new Array("女人","代理");
var bz = document.querySelector(".ca-step-item");
bz.onclick = function(){
for (const v of eb) {
var dom = $(':contains('+v+')').filter(function(){
return $(this).text()==v;
});
dom.css("color","red");
}
}
// 格式化输入内容,加租加句号
var ed= document.querySelectorAll('.edui-body-container');
var submit = document.querySelector(".release-btn");
var catalogListUl =document.getElementById("catalogList"); //在左侧悬浮窗生成发布
var li =document.createElement("li"); //createElement生成button对象
var input =document.createElement("input"); //createElement生成input对象
input.style="width:96px;";
li.innerHTML = '发布';
li.onclick = function(){
var ed= document.querySelectorAll('.edui-body-container');
for(var inp of ed){
var lp= inp.lastElementChild;
var fp= inp.firstElementChild;
if(lp.textContent.indexOf(",") == -1){ //不包含逗号,进行格式话
lp.textContent="在“"+lp.textContent+"”界面,点击“"+fp.textContent+"”按钮。";
fp.textContent="点击“"+fp.textContent+"”";
}
if(!lp.textContent.endsWith('。')){ //不包含句话,添加句号
lp.textContent=lp.textContent+"。";
}
if(inp.id != 'editor-brief'){ //加粗
var b=document.createElement("strong");
b.textContent=fp.textContent;
fp.textContent="";
fp.appendChild(b);
}
inp.focus();
}

input.focus();
submit.click();
}
catalogListUl.appendChild(li);
catalogListUl.appendChild(input);
//添加下拉软件选择
function soft(input){
var s=input[4];
s.setAttribute("list","soft");
var l=document.createElement("datalist");
l.setAttribute("id","soft");
var opvs=new Map();
opvs.set("微信","8.0.20");
opvs.set("QQ","v8.8.85.636");
opvs.set("kubesphere","v3.1.0");
opvs.set("国家医保服务平台","v1.3.7");
opvs.set("Zepp Life","6.0.3");
opvs.set("米家","v7.4.203");
opvs.set("支付宝","10.2.60");
opvs.set("访达","12.3");
opvs.set("Confluence","6.14.1");
opvs.set("IDEA","2022.1.1");
opvs.set("热血航线","1.7.1");
opvs.set("铁路12306","5.5.0.18");
opvs.set("网易云音乐","8.7.35");
opvs.set("保卫萝卜3","2.2.4");
opvs.set("微信读书","6.2.3");
opvs.set("闲鱼","7.4.80");
opvs.set("哔哩哔哩","6.71.0");
opvs.set("Apifox","2.0.2");
opvs.set("DaVinci Resolve studio 17","17.2.2");
opvs.set("中国银行","7.3.2");
opvs.set("炉石传说","23.2.138759");
opvs.set("云闪付","9.1.3");
opvs.set("必剪","2.13.0");
opvs.set("自动操作","2.10");
for (const v of opvs.keys()) {
var op= document.createElement("option");
op.setAttribute("value",v);
l.appendChild(op);
}
s.appendChild(l);
s.onchange = function() {
console.log(s.value);
input[5].value=opvs.get(s.value);
input[5].focus();
}
}
// 添加类型选择事件
var ss = document.getElementById("category").querySelectorAll("select")[2]; //获取第二个下拉选择框
ss.onchange = function() { //监听下拉框选择事件
console.log(ss.value);
if (ss.value == 16) { //电脑
var pcInput= document.getElementById("js-software-list").querySelectorAll("input");
pcInput[0].focus();
pcInput[0].value = "macOS Monterey";
pcInput[1].focus();
pcInput[1].value = "12.2.1";
pcInput[2].focus();
pcInput[2].value = "MacBook Pro";
pcInput[3].focus();
pcInput[3].value = "2017";
//添加下拉软件选择
soft(pcInput);
} else if (ss.value == 20||ss.value == 32) { //手机
var phoneInput= document.getElementById("js-software-list").querySelectorAll("input");
phoneInput[0].focus();
phoneInput[0].value = "iOS";
phoneInput[1].focus();
phoneInput[1].value = "15.4.1";
phoneInput[2].focus();
phoneInput[2].value = "iPhone";
phoneInput[3].focus();
phoneInput[3].value = "13";
//添加下拉软件选择
soft(phoneInput);
}
document.getElementById("is-origin").click();//自动勾选原创
document.querySelector("input[name='title']").focus();
};

轻量K8s集群

对比

  • MicroK8s:集成度高,配置部署简单,Kubernetes 发行版,旨在运行快速、自我修复和高度可用的 Kubernetes 集群,适合在云、本地开发环境以及边缘和物联网设备。
  • K3s:插件独立,更轻量,部署配置复杂,用于物联网边缘设备生产级Kubernetes工作负载。
  • MINIKUBE:本地Kubernetes安装程序,常用于本地运行。(独立安装docker)
  • colima:工具可以单独使用docker,k8s,但是docker k8s需要自己装

MicroK8s(不采用)

multipass使用hyperkit创建了一个microk8s-vm虚拟机,发布的 v1.14 Kubernetes 将标志着 MicroK8s 切换到 Containerd 并增强安全性

1
2
3
4
5
6
7
8
9
10
11
12
13
brew install ubuntu/microk8s/microk8s
microk8s install #会提示是否安装multipass,点击是
microk8s status --wait-ready
microk8s enable dashboard dns registry istio
microk8s dashboard-proxy
microk8s start/stop
#查看虚拟机,
multipass list
#进入ubuntu虚拟机
multipass shell microk8s-vm
#卸载
microk8s uninstall

Multipass:Multipass 是一款可运行于 Linux、Windows 和 MacOS 的轻量级虚拟机管理器,它专为希望使用单个命令即可启动全新 Ubuntu 环境的开发人员而设计。它在 Linux 上使用 KVM、在 Windows 上使用 Hyper-V、在 MacOS 上使用 HyperKit,以便以最小的开销运行虚拟机。它还可以在 Windows 和 MacOS 上使用 VirtualBox。

最终还是采用docker desktop

基础服务

名称 说明
kubernetes(k8s) k8s集群
kubeadm k8s集群部署工具
kubesphere(青云) 容器管理平台
Prometheus-Operator k8s监控平台
Grafana Loki 日志收集平台
jaeger 分布式链路追踪
skywalking 分布式链路追踪
KubeKey kk一键安装k8s

k8s组件概览

k8s

控制平面组件

  • kube-apiserver:API 服务器是 Kubernetes 控制面的前端,各组件通讯中转站,接收外部请求。
  • etcd:兼具一致性和高可用性的键值数据库,可以作为保存 Kubernetes 所有集群数据的后台数据库。
  • kubu-scheduler:节点资源调度(调度决策考虑的因素包括单个 Pod 和 Pod 集合的资源需求、硬件/软件/策略约束、亲和性和反亲和性规范、数据位置、工作负载间的干扰和最后时限。)
  • kube-controller-manager:控制器进程组件(节点、任务、端点、账户和令牌控制器)
  • cloud-controller-manager:云平台交互控制器(节点、路由、服务控制器)非必需

Node组件(每个节点上运行)

  • kubelet:维护节点状态等
  • kube-proxy:负责节点网络代理

容器/插件(DNS必须)/Web(管理界面)/监控/日志

架构模式

  • master节点:包含kube-apiserver、kubu-scheduler、kube-controller-manager组件,主要用于管理集群。
  • etcd节点:包含etcd组件,存储重要的集群有状态信息
  • worker节点:工作节点,主要用于运行业务服务容器等

etcd和master分开部署和部署到一台区别?

高可用

  • Master的kube-apiserver、kube-controller-mansger和kube-scheduler服务至少三个结点的多实例方式部署。
  • ETCD至少以3个结点的集群模式部署。
  • 负载均衡 Load Balance使用双机热备,向Node暴露虚拟IP作为入口地址,供客户端访问。

高安全

  • Master、ETCD集群启用基于CA认证的HTTPS安全机制,Master启用RBAC授信机制。

快速部署k8s青云集群

部署步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
#-- 所有机器执行
yum update -y
# kk安装前所需依赖socat conntrack是必须安装的,ebtables ipset是建议安装的
yum install -y socat conntrack ebtables ipset
# 检查时间,还需检查是否安装sudo/curl/openssl软件
date
#-- 单机执行,如果执行没反应可以单独下载,然后重命名文件,然后执行export VERSION=v2.2.1
export KKZONE=cn
curl -sfL https://get-kk.kubesphere.io | VERSION=v1.1.0 sh -
# 创建带kubernetes和kubesphere的配置文件,也可以只安装kubernetes,安装kubesphere必会安装kubernetes
./kk create config --with-kubernetes v1.18.6 --with-kubesphere v3.1.0
export KKZONE=cn
./kk create cluster -f config-sample.yaml | tee kk.log

all in one部署

all in one部署属于单机部署,是所有东西部署到一台机器,适合实验环境。

注意事项:

  • 虚拟机需要设置内存8G以上,cpu设置8个虚拟cpu。
  • 每个kk命令执行前都执行下export KKZONE=cn命令,解决网络问题,该命令只对下个命令生效。
  • 安装最好不要选择最新的版本,选择最新的前一个版本,因为国内资源也许没有更新这么快,导致一些资源下载失败。

组件介绍

组件安装日志查看命令kubectl logs -n kubesphere-system $(kubectl get pod -n kubesphere-system -l app=ks-installer -o jsonpath='{.items[0].metadata.name}') -f

或者

kubectl logs -n kubesphere-system $(kubectl get pod -n kubesphere-system -l 'app in (ks-install, ks-installer)' -o jsonpath='{.items[0].metadata.name}') -f

组件启用分安装时启用和安装后启用

  • 安装时启用

    1
    2
    3
    #下载https://github.com/kubesphere/ks-installer/releases/download/v3.2.1/kubesphere-installer.yaml
    #修改kubesphere-installer.yaml然后在里面启用组件
    kubectl apply -f cluster-configuration.yaml
  • 安装后启用

    命令模式:执行kubectl -n kubesphere-system edit clusterconfiguration ks-installer

      graph LR
    A[集群管理] --> B[自定义资源CRD] --> C[clusterconfiguration] --> D[ks-installer] --> E[编辑配置启用组件] --> F[服务组件查看组件状态]

devops

  • 流水线队列中,检查是否设置ci节点,集群设置标签node-role.kubernetes.io/worker设置ci
  • 工作台–>企业空间–>DevOps工程
  • 自定义 Jenkins Agent,在集群管理->集群->配置中心->配置->jenkins-casc-config添加自定义镜像的配置,然后登陆 Jenkins 重新加载(新版本也许可以不用),最后在jenkins部署脚本就可以使用对应的agent.node.lable使用自定义镜像编译了。

logging(monitoring)

  • 日志监测

Istio(servicemesh)

  • 服务网格

metrics_server

  • 资源监控

    1
    2
    #开启才有的命令
    kubectl top node

常见错误

  1. 安装kubesphere提示如下错误

    1
    2
    ERRO[16:03:26 CST] Failed to exec command: sudo -E /bin/sh -c "/usr/local/bin/kubectl -n kubesphere-monitoring-system create secret generic kube-etcd-client-certs --from-file=etcd-client-ca.crt=/etc/ssl/etcd/ssl/ca.pem --from-file=etcd-client.crt=/etc/ssl/etcd/ssl/node-k8s-etcd-242.14.pem --from-file=etcd-client.key=/etc/ssl/etcd/ssl/node-k8s-etcd-242.14-key.pem"
    error: error reading /etc/ssl/etcd/ssl/node-k8s-etcd-242.14.pem: no such file or directory: Process exited with status 1 node=10.255.242.10

    解决:

    1
    2
    3
    4
    # 修改所有member开头的为node
    cp /etc/ssl/etcd/ssl/member-k8s-etcd-242.14-key.pem /etc/ssl/etcd/ssl/node-k8s-etcd-242.14-key.pem
    # 修改完再次执行部署脚本
    ./kk create cluster -f config-sample.yaml | tee kk.log
  2. 在worker节点执行KK提示如下错误

    1
    2
    3
    4
    [upload-certs] Storing the certificates in Secret "kubeadm-certs" in the "kube-system" Namespace
    error execution phase upload-certs: error uploading certs: error creating token: timed out waiting for the condition
    ....
    Failed to get cluster status: Failed to upload kubeadm certs: Failed to exec command: sudo -E /bin/sh -c "/usr/local/bin/kubeadm init phase upload-certs --upload-certs"

    解决:检查 controlPlaneEndpoint配置的负载均衡服务是否正常,lb正常还是不成功,先将负载设置成master节点的ip,后面部署好了再添加负载,修改负载节点过后执行./kk delete cluster -f config-sample.yaml 在执行安装命令,不然之前安装存的还是旧的负载均衡。

  3. 流水线添加节点报错

    1
    java.net.ProtocolException: Expected HTTP 101 response but was '400 Bad Request'

    解决:在jenkins-casc-config添加的新节点配置错误,最好的解决方案是复制之前的模版,修改所有名称,例如替换所有nodejsnodejs1415,不要采用继承模式,可能新版本才支持。

  4. 访问某个页面提示Internal Server Error

    1
    Internal Server Error: "/apis/clusters/sim-1/apiextensions.k8s.io/v1beta1/customresourcedefinitions/clusterconfigurations.installer.kubesphere.io": http2: invalid Connection request header: ["upgrade"]

    解决:在负载均衡服务器(keepalived)的nginx配置添加如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # /apis/clusters/sim-1/apiextensions.k8s.io和错误异常的地址对应
    location /apis/clusters/sim-1/apiextensions.k8s.io {
    proxy_http_version 1.1;
    proxy_redirect off;
    proxy_pass http://实际域名地址;
    proxy_set_header Host $host:$server_port;
    #proxy_set_header Upgrade $http_upgrade;
    proxy_set_header X-Forwarded-Proto $scheme;
    #proxy_set_header Connection "upgrade";
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
  5. helm部署的时候push报如下错误Error: UPGRADE FAILED: pre-upgrade hooks failed: unable to build kubernetes object for deleting hook appchart-uat/templates/alertReceiver.yaml: unable to recognize "": no matches for kind "Receiver" in version "notification.kubesphere.io/v2beta2"

    解决:安装kubesphere/notification-manager

    分析:根据错误发现是webhook的问题,检查组件Monitoring发现只有10个服务,少了一个[notification-manager-webhook](https://host-kslb.mh.xxx.com.cn/clusters/innovation-lab/components/kubesphere-monitoring-system/notification-manager-webhook),因此根据官方文档重新安装下

    1
    2
    3
    4
    5
    6
    # Deploy CRDs and the Notification Manager Operator:
    kubectl apply -f https://github.com/kubesphere/notification-manager/releases/download/v2.0.1/bundle.yaml
    # Deploy default template:
    kubectl apply -f https://github.com/kubesphere/notification-manager/releases/download/v2.0.1/template.yaml
    # Deploy built-in language packs.
    kubectl apply -f https://github.com/kubesphere/notification-manager/releases/download/v2.0.1/zh-cn.yaml
  6. 删除log组件之后,提示no endpoints available for service ":ks-apiserver:"具体错误找不到了,大概是9443端口连接不上

    解决:同第五个问题

  7. 流水线错误:Error: UPGRADE FAILED: pre-upgrade hooks failed: receivers.notification.kubesphere.io "-webhook-receiver" is forbidden: User "xxx" cannot delete resource "receivers" in API group

    解决:xxx用户没有集群的权限,修改为有权限的用户kubeconfig

    原因:会根据xxx用户的集群凭证去创建CRD,但是被拒绝了。

  8. ks-controller-manager部署启动报错,不断重启,错误信息如下:

    关键错误信息:E0425 16:45:32.489641 1 runtime.go:78] Observed a panic: "invalid memory address or nil pointer dereference" (runtime error: invalid memory address or nil pointer dereference)

    错误定位:可在日志中看到是(*VirtualServiceController).getSubsets该方法报错,去kubesphere/pkg/controller/virtualservice/virtualservice_controller.go的代码里面找到getSubsets方法

    可以看到是strategy应用出了问题。

    解决:在自定义资源CRD,搜索Strategy点击进去,删除可能又问题的crd资源,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    apiVersion: servicemesh.kubesphere.io/v1alpha2
    kind: Strategy
    metadata:
    annotations:
    app.kubernetes.io/icon: /assets/bookinfo.svg
    kubesphere.io/creator: '81006638'
    servicemesh.kubesphere.io/newWorkloadName: reviews-v2
    servicemesh.kubesphere.io/oldWorkloadName: reviews-v1
    servicemesh.kubesphere.io/workloadReplicas: '1'
    servicemesh.kubesphere.io/workloadType: deployments
    labels:
    app: reviews
    app.kubernetes.io/name: bookinfo
    app.kubernetes.io/version: v1
    name: v2
    namespace: dairk-test
    spec:
    principal: v1
    selector:
    matchLabels:
    app: reviews
    app.kubernetes.io/name: bookinfo
    app.kubernetes.io/version: v1

部署配置config-sample.yaml

主要修改hosts

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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
apiVersion: kubekey.kubesphere.io/v1alpha1
kind: Cluster
metadata:
name: sample
spec:
hosts:
## 添加虚拟机
- {name: k8s-master-240.23, address: 10.5.240.23, internalAddress: 10.5.240.23, user: root, password: pwd}
- {name: k8s-master-240.24, address: 10.5.240.24, internalAddress: 10.5.240.24, user: root, password: pwd}
- {name: k8s-master-240.25, address: 10.5.240.25, internalAddress: 10.5.240.25, user: root, password: pwd}
- {name: k8s-etcd-240.26, address: 10.5.240.26, internalAddress: 10.5.240.26, user: root, password: pwd}
- {name: k8s-etcd-240.27, address: 10.5.240.27, internalAddress: 10.5.240.27, user: root, password: pwd}
- {name: k8s-etcd-240.28, address: 10.5.240.28, internalAddress: 10.5.240.28, user: root, password: pwd}
- {name: k8s-worker-240.29, address: 10.5.240.29, internalAddress: 10.5.240.29, user: root, password: pwd}
- {name: k8s-worker-240.30, address: 10.5.240.30, internalAddress: 10.5.240.30, user: root, password: pwd}
roleGroups:
## 分配虚拟机角色
etcd:
- k8s-etcd-240.26
- k8s-etcd-240.27
- k8s-etcd-240.28
master:
- k8s-master-240.23
- k8s-master-240.24
- k8s-master-240.25
worker:
- k8s-worker-240.29
- k8s-worker-240.30
controlPlaneEndpoint:
## 高可用
internalLoadbalancer: haproxy
domain: lb.kubesphere.local
## 地址需要修改
address: "10.255.240.23"
port: 6443
kubernetes:
version: v1.18.6
imageRepo: kubesphere
clusterName: cluster.local
network:
plugin: calico
kubePodsCIDR: 10.233.64.0/18
kubeServiceCIDR: 10.233.0.0/18
registry:
registryMirrors: []
insecureRegistries: []
addons: []
---
apiVersion: installer.kubesphere.io/v1alpha1
kind: ClusterConfiguration
metadata:
name: ks-installer
namespace: kubesphere-system
labels:
version: v3.1.0
spec:
persistence:
storageClass: ""
authentication:
jwtSecret: ""
zone: ""
local_registry: ""
etcd:
monitoring: false
endpointIps: localhost
port: 2379
tlsEnable: true
common:
redis:
enabled: false
redisVolumSize: 2Gi
openldap:
enabled: false
openldapVolumeSize: 2Gi
minioVolumeSize: 20Gi
monitoring:
endpoint: http://prometheus-operated.kubesphere-monitoring-system.svc:9090
es:
elasticsearchMasterVolumeSize: 4Gi
elasticsearchDataVolumeSize: 20Gi
logMaxAge: 7
elkPrefix: logstash
basicAuth:
enabled: false
username: ""
password: ""
externalElasticsearchUrl: ""
externalElasticsearchPort: ""
console:
enableMultiLogin: true
port: 30880
alerting:
enabled: false
# thanosruler:
# replicas: 1
# resources: {}
auditing:
enabled: false
devops:
enabled: false
jenkinsMemoryLim: 2Gi
jenkinsMemoryReq: 1500Mi
jenkinsVolumeSize: 8Gi
jenkinsJavaOpts_Xms: 512m
jenkinsJavaOpts_Xmx: 512m
jenkinsJavaOpts_MaxRAM: 2g
events:
enabled: false
ruler:
enabled: true
replicas: 2
logging:
enabled: false
logsidecar:
enabled: true
replicas: 2
metrics_server:
enabled: false
monitoring:
storageClass: ""
prometheusMemoryRequest: 400Mi
prometheusVolumeSize: 20Gi
multicluster:
clusterRole: none
network:
networkpolicy:
enabled: false
ippool:
type: none
topology:
type: none
notification:
enabled: false
openpitrix:
store:
enabled: false
servicemesh:
enabled: false
kubeedge:
enabled: false
cloudCore:
nodeSelector: {"node-role.kubernetes.io/worker": ""}
tolerations: []
cloudhubPort: "10000"
cloudhubQuicPort: "10001"
cloudhubHttpsPort: "10002"
cloudstreamPort: "10003"
tunnelPort: "10004"
cloudHub:
advertiseAddress:
- ""
nodeLimit: "100"
service:
cloudhubNodePort: "30000"
cloudhubQuicNodePort: "30001"
cloudhubHttpsNodePort: "30002"
cloudstreamNodePort: "30003"
tunnelNodePort: "30004"
edgeWatcher:
nodeSelector: {"node-role.kubernetes.io/worker": ""}
tolerations: []
edgeWatcherAgent:
nodeSelector: {"node-role.kubernetes.io/worker": ""}
tolerations: []

参考

Kubernetes核心架构与高可用集群详解(含100%部署成功的方案)

kk多节点安装

k8s-快速入门

相关参数

QPS(Queries Per Second/吞吐量):每秒能够响应的查询次数,也即是最大吞吐能力(吞吐量)。

TPS(Transactions Per Second):每秒处理的事务数目。一个事务是指一个客户端向服务器发送请求然后服务器做出反应的过程。TPS 的过程包括:客户端请求服务端、服务端内部处理、服务端返回客户端。

例如:访问一个页面会请求服务器 3 次,那么访问这一个页面就会产生一个TPS,三个QPS。

jester dubbo测试插件

参考

JMeter之Ramp-up Period(in seconds)说明

基础名词

  • 节点/node:一个节点是一个运行Kubernetes 中的主机。
  • 容器组/pod:一个 Pod 对应于由若干容器组成的一个容器组,同个组内的容器共享一个存储卷(volume)。
  • 服务/services:一个 Kubernetes 服务是容器组逻辑的高级抽象,同时也对外提供访问容器组的策略。
  • 容器组生命周期/pos-states:包含所有容器状态集合,包括容器组状态类型,容器组生命周期,事件,重启策略,以及 replication controllers。

基础命令

1
2
3
4
5
6
7
8
9
10
#创建部署
kubectl create deployment test --image=nginx:latest
#获取部署信息
kubectl get deployments
#获取集群节点
kubectl get nodes
#获取容器组
kubectl get pods --all-namespaces
#获取服务
kubectl get service --all-namespaces

kubeconfig

是远程连接集群的授权证书配置文件,不管集群管理,还是流水线发布都会用到,集群下面的是默认管理员的账号,最好是用个人自己的集群kubeConfig。

安装成功后位于~/..kube/config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#----集群参数配置---
apiVersion: v1 #标识客户端解析器的版本,不应手动编辑
clusters: #可以配置多个集群
- cluster: #通过kubectl config指令生成
certificate-authority-data: LS....tLQo=
server: https://ip:6443 #集群访问方式
name: cluster.local #集群名字
#----上下文参数配置---
contexts: #多个上下文
- context: #关联集群clusters和用户users
cluster: cluster.local #集群名字
user: kubernetes-admin #客户端用户
name: kubernetes-admin@cluster.local #上下文名字
current-context: kubernetes-admin@cluster.local #当前使用那个上下文,就可以确定使用那个集群环境了
kind: Config #标识客户端解析器的模式,不应手动编辑
preferences: {} #指定可选(和当前未使用)的 kubectl 首选项
#----客户端认证参数配置---
users:
- name: kubernetes-admin
user:
client-certificate-data: LS0tLS1....LS0tLQo=
client-key-data: LS0tL...S0tLQo=
创建用户认证授权的 kubeconfig 文件(待验证)

参考创建用户认证授权的 kubeconfig 文件

  1. /etc/kubernetes/ssl目录创建devuser-csr.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    {
    "CN": "devuser",
    "hosts": [],
    "key": {
    "algo": "rsa",
    "size": 2048
    },
    "names": [
    {
    "C": "CN",
    "ST": "BeiJing",
    "L": "BeiJing",
    "O": "k8s",
    "OU": "System"
    }
    ]
    }
  2. 生成CA证书和密钥cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes devuser-csr.json | cfssljson -bare devuser

  3. 创建用户的kubeconfig 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    # 设置集群参数
    export KUBE_APISERVER="https://172.20.0.113:6443"
    kubectl config set-cluster kubernetes \
    --certificate-authority=/etc/kubernetes/ssl/ca.pem \
    --embed-certs=true \
    --server=${KUBE_APISERVER} \
    --kubeconfig=devuser.kubeconfig

    # 设置客户端认证参数
    kubectl config set-credentials devuser \
    --client-certificate=/etc/kubernetes/ssl/devuser.pem \
    --client-key=/etc/kubernetes/ssl/devuser-key.pem \
    --embed-certs=true \
    --kubeconfig=devuser.kubeconfig

    # 设置上下文参数
    kubectl config set-context kubernetes \
    --cluster=kubernetes \
    --user=devuser \
    --namespace=dev \
    --kubeconfig=devuser.kubeconfig

    # 设置默认上下文
    kubectl config use-context kubernetes --kubeconfig=devuser.kubeconfig
  4. 查看上下文,执行kubectl config get-contexts,会显示~/.kube/config的账号

  5. 替换后,执行cp -f ./devuser.kubeconfig ~/.kube/config可以显示新的账号

  6. 绑定角色,限制用户可以访问那几个空间(namespace),例如

    1
    2
    3
    #---这样 devuser 用户对 dev 和 test 两个 namespace 具有完全访问权限。
    kubectl create rolebinding devuser-admin-binding --clusterrole=admin --user=devuser --namespace=dev
    kubectl create rolebinding devuser-admin-binding --clusterrole=admin --user=devuser --namespace=test
  7. 测试,执行kubectl get pods --namespace default会返回Forbidden无权限

后端技术

  • SpringBoot框架: 开箱即用 编码(注解)/配置/部署/监控/集成/开发简单
  • SpringCloud框架:简化了分布式系统开发,服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控

spring

  • springBoot Actuator(接口)/Admin(服务) 服务监控中心

  • Sentinel 熔断与降级

  • Ribbon 负载均衡

  • Spring Security 安全控制

  • feign 服务调用

  • swagger+ knife4j

  • minio 分布式文件系统

  • 分布式日志elk

  • docker容器部署

  • mongodb

  • redis

  • Seata 分布式事务

  • Sleuth 链路追踪

  • Quartz

  • 缓存穿透:很多不存在的key直接请求到数据库

    解决:参数校验、布隆过滤器、缓存时间、内存缓存

  • 缓存雪崩:大量的数据直接请求到数据库,例如redis缓存key大面积过期

    解决:集群、限流、过期时间

    • Eureka 服务发现框架
    • Ribbon 进程内负载均衡器
    • Open Feign 服务调用映射
    • Hystrix 服务降级熔断器
    • Zuul 微服务网关
    • Config 微服务统一配置中心
    • Bus 消息总线

基础概念

sidecar: 微服务异构,就是指可以让其他第三方(语言)服务,接入springcloud(nacos)里面进行管理等

框架源码:alibaba/spring-cloud-alibaba:Sidecar

需求

  1. 需要接入第三方服务,第三方服务以接口方式提供

  2. 第三方服务可以被其他第三方服务替换

  3. 第三方服务可能不支持集群部署,就是部署多个相同的实例,数据不共享

  4. 需要支持集群部署

  5. 需要监控第三方服务

  6. 集成到alibaba springcloud框架

  7. 接入方式feign

设计

项目框架采用边车模式(sidecar),但是不集成alibaba-sidecar,手动进行实现,因为需要支持多同类型第三方服务,需要对数据进行包装,

备选方案:集成alibaba-sidecar,因为异构只能直接代理,因此数据的包装可以采用过滤器和解码器进行处理

支持同类型第三方服务扩展替换

采用工厂设计模式进行搭建工程

支持集群部署

采用边车系统部署模式,一个第三方服务一个该服务

支持第三方服务监控

采用重写心跳,在心跳里面对第三方服务进行监控并绑定为自己的服务状态。

测试发现心跳是down的状态不熔断,只是降级。

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
@Component
public class SidecarHealthIndicator extends AbstractHealthIndicator {

@Autowired
AiConfig aiConfig;

@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
try {
String result;
if (aiConfig.aiFaceType.equals(FaceType.NT.name())) {
result = HttpUtil.get(aiConfig.aiFaceUrl + "/version", aiConfig.aiFaceUrlTimeout);
builder.withDetail("version", result);
} else if (aiConfig.aiFaceType.equals(FaceType.KS.name())) {
result = HttpUtil.get(aiConfig.aiFaceUrl + "/version", aiConfig.aiFaceUrlTimeout);
JSONObject r = JSONUtil.parseObj(result);
builder.withDetail("version", r.getStr("platform_version"));
} else {
result = HttpUtil.get(aiConfig.aiFaceUrl + "/version", aiConfig.aiFaceUrlTimeout);
builder.withDetail("version", result);
}
builder.up();
} catch (Exception e) {
builder.down(e);
}
}
}

第三方服务不支持集群,数据不共享(不考虑异常情况)

方案1: 在业务包装接口里面实现向其他实例进行数据同步

在数据存储类型的接口里面查询该服务的其他实例,然后发同样的数据到该服务的其他实例。

注意事项:由于该服务也部署了复数个实例,因此估计需要采用redis等中间件实现那些服务已经发送过,不然会形成服务间的死循环

方案2: 利用feign的重试机制

在接口里面返回指定错误码,然后根据错误码进行重试,然后计数重试次数(可采用redis进行计数),当重试次数达到了实例的个数,就说明每个实例都请求了一次了,数据都存在于每个实例了。

缺点:如果10个实例,每个实例处理请求时间2s,10个就需要20s,因为是按顺序进行请求的

方案3: 利用feign拦截器异步请求其他实例(目前采用)

可以在拦截器里面设置header标志,标志其他服务不需要拦截,向其他服务请求,不然也会形成服务间的死循环

拦截器两种实现方式

  • 在feign指定配置类@FeignClient(...,configuration = MyConfiguration.class)
  • 实现1⃣️feign.RequestInterceptor/2⃣️HandlerInterceptor/3⃣️ClientHttpRequestInterceptor接口,进行全局拦截

这里采用接口拦截模式,配置模式会在其他项目里面引入

拦截器用2⃣️HandlerInterceptor,因为1⃣️feign.RequestInterceptor不知道为什么拦截不生效

具体实现见附录一:spring HandlerInterceptor器的实现并读取body

步骤:
  1. 继承HttpServletRequestWrapper实现一个读取并保存requestBody的类BodyReaderHttpServletRequestWrapper.java

  2. 新建一个过滤器BodyReadFilter.java用于调用BodyReaderHttpServletRequestWrapper进行保存body

  3. 新建一个拦截器StatefulFeignInterceptor.java实现HandlerInterceptor中的preHandle

  4. 新建一个配置StatefulConfig.java用于启用拦截器StatefulFeignInterceptor

注意:如果要在拦截器里面使用@Autowired功能,就必须使用bean注入该类,不能用注解@Component等进行注入

向其他服务发送请求的逻辑,在StatefulFeignInterceptor里面的preHandle进行实现就可以了,代码如下

sub的作用时为了防止死循环,子服务不进行转发

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
if ("true".equals(request.getHeader("sub"))) {
log.info("sub request " + request.getRequestURI());
} else {
ThreadUtil.execAsync(() -> {
String uri = request.getRequestURI();
log.info("main request " + uri);
List<String> urls = aiConfig.aiFaceStatefulUrls;
if (urls.contains(uri)) {
BodyReaderHttpServletRequestWrapper requestWrapper = null;
try {
requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
} catch (IOException e) {
log.error("read body error: {}", e.getMessage());
}
String body = IoUtil.read(requestWrapper.getInputStream(), requestWrapper.getCharacterEncoding());
log.debug("请求体:{}", body);
String ip = discoveryProperties.getIp();
List<ServiceInstance> instanceList = discoveryClient.getInstances("xkiot-ai");
for (ServiceInstance serviceInstance : instanceList) {
if (!ip.equals(serviceInstance.getHost())) {
String url = serviceInstance.getUri().toString() + uri;
HttpRequest.post(url).header("sub", "true").body(body).execute(true).body();
}
}
}
});
}
return true;

注意事项:如果服务里面需要创建一个用户id,然后每台服务的用户id要一致,只能通过接口传入用户id,或者把用户id共享到redis内存里面(比较麻烦)

方案4: 利用feign解码器异步请求其他实例

解码器是对请求结果进行处理,因此如果使用该模式,估计需要用中间件redis来解决服务间的死循环

方案5: 幻想方案,在某个地方设置或重写,可以让feign支持向所有实例发送请求
方案6: 幻想方案,利用事务或异步请求合并处理结果,该模式可以解决异常情况
方案7: 解决第三方有状态服务的部署,第三方服务实现数据共享

附录一:spring HandlerInterceptor器的实现并读取body

BodyReaderHttpServletRequestWrapper.java

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
import org.springframework.util.StreamUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {

private byte[] requestBody = null;//用于将流保存下来

public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
requestBody = StreamUtils.copyToByteArray(request.getInputStream());
}

@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream bodyStream = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
@Override
public int read() {
return bodyStream.read(); // 读取 requestBody 中的数据
}

@Override
public boolean isFinished() {
return false;
}

@Override
public boolean isReady() {
return false;
}

@Override
public void setReadListener(ReadListener readListener) {
}
};
}

@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}

}

BodyReadFilter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@Component
@WebFilter(urlPatterns = "/**", filterName = "BodyReadFilter")
public class BodyReadFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
if (servletRequest instanceof HttpServletRequest) {
requestWrapper = new BodyReaderHttpServletRequestWrapper((HttpServletRequest) servletRequest);
}
if (requestWrapper == null) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
filterChain.doFilter(requestWrapper, servletResponse);
}
}
}

StatefulFeignInterceptor.java

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
import cn.hutool.core.io.IoUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


@Slf4j
public class StatefulFeignInterceptor implements HandlerInterceptor {

@Autowired
AiConfig aiConfig;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

if (aiConfig.aiFaceStatefulUrls.contains(request.getRequestURI())) {
BodyReaderHttpServletRequestWrapper requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
String body = IoUtil.read(requestWrapper.getInputStream(), requestWrapper.getCharacterEncoding());
log.debug("请求体:{}", body);
}
return true;
}

}

StatefulConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class StatefulConfig implements WebMvcConfigurer {

/**
* 解决StatefulFeignInterceptor里面的使用Autowired注入为null的问题
*
* @return
*/
@Bean
public StatefulFeignInterceptor statefulFeignInterceptor() {
return new StatefulFeignInterceptor();
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(statefulFeignInterceptor()).addPathPatterns("/**");
}
}

额外

Nacos 的cp/ap模式

AP模式(nacos默认模式)不支持数据一致性,所以只支持服务注册的临时实例

CP模式支持服务注册的永久实例,满足数据的一致性

这里的数据一致性,让我一度认为是指服务的所有实例数据一致,让我以为可以设置过后,每个实例都会发请求

参考

SpringBoot常用拦截器(HandlerInterceptor,ClientHttpRequestInterceptor,RequestInterceptor)