开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Netty入门看这一篇就够了

发表于 2021-02-02

一、简介

Netty是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器和客户端。

二、JDK原生NIO程序的问题

JDK原生也有一套网络应用程序API,但是存在一系列问题,主要如下:

  • NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等;
  • 需要具备其它的额外技能做铺垫,例如熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序;
  • 可靠性能力补齐,开发工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大;
  • JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题,但是直到JDK1.7版本该问题仍旧存在,只不过该bug发生概率降低了一些而已,它并没有被根本解决。

三、Netty的特点

Netty的对JDK自带的NIO的API进行封装,解决上述问题,主要特点有:

  • 设计优雅 适用于各种传输类型的统一API - 阻塞和非阻塞Socket 基于灵活且可扩展的事件模型,可以清晰地分离关注点 高度可定制的线程模型 - 单线程,一个或多个线程池 真正的无连接数据报套接字支持(自3.1起);
  • 使用方便 详细记录的Javadoc,用户指南和示例 没有其他依赖项,JDK 5(Netty 3.x)或6(Netty 4.x)就足够了;
  • 高性能 吞吐量更高,延迟更低 减少资源消耗 最小化不必要的内存复制;
  • 安全 完整的SSL / TLS和StartTLS支持;
  • 社区活跃,不断更新 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会被加入。

四、Netty常见使用

Netty常见的使用场景如下:

  • 互联网行业 在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,Netty作为异步高新能的通信框架,往往作为基础通信组件被这些RPC框架使用。 典型的应用有:阿里分布式服务框架Dubbo的RPC框架使用Dubbo协议进行节点间通信,Dubbo协议默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信。
  • 游戏行业 无论是手游服务端还是大型的网络游戏,Java语言得到了越来越广泛的应用。Netty作为高性能的基础通信组件,它本身提供了TCP/UDP和HTTP协议栈。 非常方便定制和开发私有协议栈,账号登录服务器,地图服务器之间可以方便的通过Netty进行高性能的通信。
  • 大数据领域 经典的Hadoop的高性能通信和序列化组件Avro的RPC框架,默认采用Netty进行跨界点通信,它的Netty Service基于Netty框架二次封装实现。

五、Netty高性能设计

Netty作为异步事件驱动的网络,高性能之处主要来自于其I/O模型和线程处理模型,前者决定如何收发数据,后者决定如何处理数据

I/O模型

用什么样的通道将数据发送给对方,BIO、NIO或者AIO,I/O模型在很大程度上决定了框架的性能

阻塞I/O

传统阻塞型I/O(BIO)可以用下图表示:

Blocking I/O

特点

  • 每个请求都需要独立的线程完成数据read,业务处理,数据write的完整操作

问题

  • 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大
  • 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在read操作上,造成线程资源浪费

I/O复用模型

img

在I/O复用模型中,会用到select,这个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作,而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数

Netty的非阻塞I/O的实现关键是基于I/O复用模型,这里用Selector对象表示:

Nonblocking I/O

Netty的IO线程NioEventLoop由于聚合了多路复用器Selector,可以同时并发处理成百上千个客户端连接。当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。

由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致的线程挂起,一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。

基于buffer

  • 传统的I/O是面向字节流或字符流的,以流式的方式顺序地从一个Stream 中读取一个或多个字节, 因此也就不能随意改变读取指针的位置。
  • 在NIO中, 抛弃了传统的 I/O流, 而是引入了Channel和Buffer的概念. 在NIO中, 只能从Channel中读取数据到Buffer中或将数据 Buffer 中写入到 Channel。
  • 基于buffer操作不像传统IO的顺序操作, NIO 中可以随意地读取任意位置的数据

线程模型

数据报如何读取?读取之后的编解码在哪个线程进行,编解码后的消息如何派发,线程模型的不同,对性能的影响也非常大。

事件驱动模型

通常,我们设计一个事件处理模型的程序有两种思路

  • 轮询方式 线程不断轮询访问相关事件发生源有没有发生事件,有发生事件就调用事件处理逻辑。
  • 事件驱动方式 发生事件,主线程把事件放入事件队列,在另外线程不断循环消费事件列表中的事件,调用事件对应的处理逻辑处理事件。事件驱动方式也被称为消息通知方式,其实是设计模式中观察者模式的思路。

以GUI的逻辑处理为例,说明两种逻辑的不同:

  • 轮询方式 线程不断轮询是否发生按钮点击事件,如果发生,调用处理逻辑
  • 事件驱动方式 发生点击事件把事件放入事件队列,在另外线程消费的事件列表中的事件,根据事件类型调用相关事件处理逻辑

事件驱动模型

主要包括4个基本组件:

  • 事件队列(event queue):接收事件的入口,存储待处理事件
  • 分发器(event mediator):将不同的事件分发到不同的业务逻辑单元
  • 事件通道(event channel):分发器与处理器之间的联系渠道
  • 事件处理器(event processor):实现业务逻辑,处理完成后会发出事件,触发下一步操作可以看出,相对传统轮询模式,事件驱动有如下优点:
  • 可扩展性好,分布式的异步架构,事件处理器之间高度解耦,可以方便扩展事件处理逻辑
  • 高性能,基于队列暂存事件,能方便并行异步处理事件

Reactor线程模型

Reactor是反应堆的意思,Reactor模型,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。 服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor模式也叫Dispatcher模式,即I/O多了复用统一监听事件,收到事件后分发(Dispatch给某进程),是编写高性能网络服务器的必备技术之一。

Reactor模型中有2个关键组成:

  • Reactor Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人
  • Handlers 处理程序执行I/O事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor通过调度适当的处理程序来响应I/O事件,处理程序执行非阻塞操作

Reactor模型

取决于Reactor的数量和Hanndler线程数量的不同,Reactor模型有3个变种

  • 单Reactor单线程
  • 单Reactor多线程
  • 主从Reactor多线程

可以这样理解,Reactor就是一个执行while (true) { selector.select(); …}循环的线程,会源源不断的产生新的事件,称作反应堆很贴切。

Netty线程模型

Netty主要基于主从Reactors多线程模型(如下图)做了一定的修改,其中主从Reactor多线程模型有多个Reactor:MainReactor和SubReactor:

  • MainReactor负责客户端的连接请求,并将请求转交给SubReactor
  • SubReactor负责相应通道的IO读写请求
  • 非IO请求(具体逻辑处理)的任务则会直接写入队列,等待worker threads进行处理

主从Rreactor多线程模型

特别说明的是: 虽然Netty的线程模型基于主从Reactor多线程,借用了MainReactor和SubReactor的结构,但是实际实现上,SubReactor和Worker线程在同一个线程池中:

1
2
3
4
java复制代码EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)

上面代码中的bossGroup 和workerGroup是Bootstrap构造方法中传入的两个对象,这两个group均是线程池

  • bossGroup线程池则只是在bind某个端口后,获得其中一个线程作为MainReactor,专门处理端口的accept事件,每个端口对应一个boss线程
  • workerGroup线程池会被各个SubReactor和worker线程充分利用

异步处理

异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

Netty中的I/O操作是异步的,包括bind、write、connect等操作会简单的返回一个ChannelFuture,调用者并不能立刻获得结果,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。

当future对象刚刚创建时,处于非完成状态,调用者可以通过返回的ChannelFuture来获取操作执行的状态,注册监听函数来执行完成后的操,常见有如下操作:

  • 通过isDone方法来判断当前操作是否完成
  • 通过isSuccess方法来判断已完成的当前操作是否成功
  • 通过getCause方法来获取已完成的当前操作失败的原因
  • 通过isCancelled方法来判断已完成的当前操作是否被取消
  • 通过addListener方法来注册监听器,当操作已完成(isDone方法返回完成),将会通知指定的监听器;如果future对象已完成,则理解通知指定的监听器

例如下面的的代码中绑定端口是异步操作,当绑定操作处理完,将会调用相应的监听器处理逻辑

1
2
3
4
5
6
7
java复制代码    serverBootstrap.bind(port).addListener(future -> {
if (future.isSuccess()) {
System.out.println(new Date() + ": 端口[" + port + "]绑定成功!");
} else {
System.err.println("端口[" + port + "]绑定失败!");
}
});

相比传统阻塞I/O,执行I/O操作后线程会被阻塞住, 直到操作完成;异步处理的好处是不会造成线程阻塞,线程在I/O操作期间可以执行别的程序,在高并发情形下会更稳定和更高的吞吐量。

六、Netty架构设计

功能特性

Netty功能特性图

  • 传输服务 支持BIO和NIO
  • 容器集成 支持OSGI、JBossMC、Spring、Guice容器
  • 协议支持 HTTP、Protobuf、二进制、文本、WebSocket等一系列常见协议都支持。 还支持通过实行编码解码逻辑来实现自定义协议
  • Core核心 可扩展事件模型、通用通信API、支持零拷贝的ByteBuf缓冲对象

模块组件

Bootstrap、ServerBootstrap

Bootstrap意思是引导,一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类。

Future、ChannelFuture

正如前面介绍,在Netty中所有的IO操作都是异步的,不能立刻得知消息是否被正确处理,但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过Future和ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。

Channel

Netty网络通信的组件,能够用于执行网络I/O操作。 Channel为用户提供:

  • 当前网络连接的通道的状态(例如是否打开?是否已连接?)
  • 网络连接的配置参数 (例如接收缓冲区大小)
  • 提供异步的网络I/O操作(如建立连接,读写,绑定端口),异步调用意味着任何I / O调用都将立即返回,并且不保证在调用结束时所请求的I / O操作已完成。调用立即返回一个ChannelFuture实例,通过注册监听器到ChannelFuture上,可以I / O操作成功、失败或取消时回调通知调用方。
  • 支持关联I/O操作与对应的处理程序

不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应,下面是一些常用的 Channel 类型

  • NioSocketChannel,异步的客户端 TCP Socket 连接
  • NioServerSocketChannel,异步的服务器端 TCP Socket 连接
  • NioDatagramChannel,异步的 UDP 连接
  • NioSctpChannel,异步的客户端 Sctp 连接
  • NioSctpServerChannel,异步的 Sctp 服务器端连接 这些通道涵盖了 UDP 和 TCP网络 IO以及文件 IO.

Selector

Netty基于Selector对象实现I/O多路复用,通过 Selector, 一个线程可以监听多个连接的Channel事件, 当向一个Selector中注册Channel 后,Selector 内部的机制就可以自动不断地查询(select) 这些注册的Channel是否有已就绪的I/O事件(例如可读, 可写, 网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel 。

NioEventLoop

NioEventLoop中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用NioEventLoop的run方法,执行I/O任务和非I/O任务:

  • I/O任务 即selectionKey中ready的事件,如accept、connect、read、write等,由processSelectedKeys方法触发。
  • 非IO任务 添加到taskQueue中的任务,如register0、bind0等任务,由runAllTasks方法触发。

两种任务的执行时间比由变量ioRatio控制,默认为50,则表示允许非IO任务执行的时间与IO任务的执行时间相等。

NioEventLoopGroup

NioEventLoopGroup,主要管理eventLoop的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个Channel上的事件,而一个Channel只对应于一个线程。

ChannelHandler

ChannelHandler是一个接口,处理I / O事件或拦截I / O操作,并将其转发到其ChannelPipeline(业务处理链)中的下一个处理程序。

ChannelHandler本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类:

  • ChannelInboundHandler用于处理入站I / O事件
  • ChannelOutboundHandler用于处理出站I / O操作

或者使用以下适配器类:

  • ChannelInboundHandlerAdapter用于处理入站I / O事件
  • ChannelOutboundHandlerAdapter用于处理出站I / O操作
  • ChannelDuplexHandler用于处理入站和出站事件

ChannelHandlerContext

保存Channel相关的所有上下文信息,同时关联一个ChannelHandler对象

ChannelPipline

保存ChannelHandler的List,用于处理或拦截Channel的入站事件和出站操作。 ChannelPipeline实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及Channel中各个的ChannelHandler如何相互交互。

在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应, 它们的组成关系如下:

img

一个 Channel 包含了一个 ChannelPipeline, 而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表, 并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。入站事件和出站事件在一个双向链表中,入站事件会从链表head往后传递到最后一个入站的handler,出站事件会从链表tail往前传递到最前一个出站的handler,两种类型的handler互不干扰。

工作原理架构

初始化并启动Netty服务端过程如下:

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
java复制代码    public static void main(String[] args) {
// 创建mainReactor
NioEventLoopGroup boosGroup = new NioEventLoopGroup();
// 创建工作线程组
NioEventLoopGroup workerGroup = new NioEventLoopGroup();

final ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
// 组装NioEventLoopGroup
.group(boosGroup, workerGroup)
// 设置channel类型为NIO类型
.channel(NioServerSocketChannel.class)
// 设置连接配置参数
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
// 配置入站、出站事件handler
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
// 配置入站、出站事件channel
ch.pipeline().addLast(...);
ch.pipeline().addLast(...);
}
});

// 绑定端口
int port = 8080;
serverBootstrap.bind(port).addListener(future -> {
if (future.isSuccess()) {
System.out.println(new Date() + ": 端口[" + port + "]绑定成功!");
} else {
System.err.println("端口[" + port + "]绑定失败!");
}
});
}
  • 基本过程如下:
  1. 初始化创建2个NioEventLoopGroup,其中boosGroup用于Accetpt连接建立事件并分发请求, workerGroup用于处理I/O读写事件和业务逻辑
  2. 基于ServerBootstrap(服务端启动引导类),配置EventLoopGroup、Channel类型,连接参数、配置入站、出站事件handler
  3. 绑定端口,开始工作

结合上面的介绍的Netty Reactor模型,介绍服务端Netty的工作架构图:

服务端Netty Reactor工作架构图

server端包含1个Boss NioEventLoopGroup和1个Worker NioEventLoopGroup,NioEventLoopGroup相当于1个事件循环组,这个组里包含多个事件循环NioEventLoop,每个NioEventLoop包含1个selector和1个事件循环线程。

每个Boss NioEventLoop循环执行的任务包含3步:

  • 1 轮询accept事件
  • 2 处理accept I/O事件,与Client建立连接,生成NioSocketChannel,并将NioSocketChannel注册到某个Worker NioEventLoop的Selector上 *3 处理任务队列中的任务,runAllTasks。任务队列中的任务包括用户调用eventloop.execute或schedule执行的任务,或者其它线程提交到该eventloop的任务。

每个Worker NioEventLoop循环执行的任务包含3步:

  • 1 轮询read、write事件;
  • 2 处I/O事件,即read、write事件,在NioSocketChannel可读、可写事件发生时进行处理
  • 3 处理任务队列中的任务,runAllTasks。

其中任务队列中的task有3种典型使用场景

  • 1 用户程序自定义的普通任务
1
2
3
4
5
6
java复制代码ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
//...
}
});
  • 2 非当前reactor线程调用channel的各种方法 例如在推送系统的业务线程里面,根据用户的标识,找到对应的channel引用,然后调用write类方法向该用户推送消息,就会进入到这种场景。最终的write会提交到任务队列中后被异步消费。
  • 3 用户自定义定时任务
1
2
3
4
5
6
java复制代码ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {

}
}, 60, TimeUnit.SECONDS);

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

我把SpringBoot应用部署到了K8S上,怎么感觉用起来

发表于 2021-02-02

SpringBoot实战电商项目mall(40k+star)地址:github.com/macrozheng/…

摘要

想要把一个复杂的微服务项目部署到K8S上去,首先我们得学会把单个SpringBoot应用部署上去。今天我们来讲下如何把SpringBoot应用部署到K8S上去,和使用Docker Compose部署非常类似,希望对大家有所帮助!

学前准备

学习本文需要有一些K8S基础,对K8S还不了解的朋友可以参考如下的文章。

  • 《K8S太火了!花10分钟玩转它不香么?》
  • 《自从上了K8S,项目更新都不带停机的!》

推送镜像到Docker Hub

之前我们都是自建的镜像仓库,这次我们换种方式,把镜像上传到Docker Hub中去。

  • 首先我们得注册个Docker Hub的账号,Docker Hub地址:hub.docker.com/

  • 部署应用使用之前的mall-tiny-fabric项目,先修改pom.xml文件,主要是添加Docker Hub的认证信息和修改下镜像前缀,具体内容如下;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码<configuration>
<!-- Docker 远程管理地址-->
<dockerHost>http://192.168.5.94:2375</dockerHost>
<!-- 添加认证信息-->
<authConfig>
<push>
<!--Docker Hub 用户名-->
<username>macrodocker</username>
<!--Docker Hub 密码-->
<password>xxx</password>
</push>
</authConfig>
<images>
<image>
<!--修改镜像前缀为Docker Hub 用户名-->
<name>macrodocker/${project.name}:${project.version}</name>
</image>
</images>
</configuration>
  • 修改完成后使用package命令先把镜像打包到Linux服务器,再使用docker:push命令把镜像推送到Docker Hub中去:

  • 推送成功以后就可以在Docker Hub中看到镜像了。

应用部署

接下来我们将把应用部署到K8S上去,包含SpringBoot应用的部署和MySQL的部署。

部署MySQL

  • 首先添加配置文件mysql-deployment.yaml用于创建Deployment,具体说明参考注释即可;
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
yaml复制代码apiVersion: apps/v1
kind: Deployment
metadata:
# 指定Deployment的名称
name: mysql-deployment
# 指定Deployment的标签
labels:
app: mysql
spec:
# 指定创建的Pod副本数量
replicas: 1
# 定义如何查找要管理的Pod
selector:
# 管理标签app为mysql的Pod
matchLabels:
app: mysql
# 指定创建Pod的模板
template:
metadata:
# 给Pod打上app:mysql标签
labels:
app: mysql
# Pod的模板规约
spec:
containers:
- name: mysql
# 指定容器镜像
image: mysql:5.7
# 指定开放的端口
ports:
- containerPort: 3306
# 设置环境变量
env:
- name: MYSQL_ROOT_PASSWORD
value: root
# 使用存储卷
volumeMounts:
# 将存储卷挂载到容器内部路径
- mountPath: /var/log/mysql
name: log-volume
- mountPath: /var/lib/mysql
name: data-volume
- mountPath: /etc/mysql
name: conf-volume
# 定义存储卷
volumes:
- name: log-volume
# hostPath类型存储卷在宿主机上的路径
hostPath:
path: /home/docker/mydata/mysql/log
# 当目录不存在时创建
type: DirectoryOrCreate
- name: data-volume
hostPath:
path: /home/docker/mydata/mysql/data
type: DirectoryOrCreate
- name: conf-volume
hostPath:
path: /home/docker/mydata/mysql/conf
type: DirectoryOrCreate
  • 通过应用配置文件来创建Deployment;
1
bash复制代码kubectl apply -f mysql-deployment.yaml
  • 运行成功后查询Deployment,发现mysql-deployment已经就绪;
1
2
3
4
bash复制代码[macro@linux-local k8s]$ kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
mysql-deployment 1/1 1 1 38s
nginx-volume-deployment 2/2 2 2 6d5h
  • 想要其他Pod可以通过服务名称访问MySQL,需要创建Service,添加配置文件mysql-service.yaml用于创建Service;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yaml复制代码apiVersion: v1
kind: Service
metadata:
# 定义服务名称,其他Pod可以通过服务名称作为域名进行访问
name: mysql-service
spec:
# 指定服务类型,通过Node上的静态端口暴露服务
type: NodePort
# 管理标签app为mysql的Pod
selector:
app: mysql
ports:
- name: http
protocol: TCP
port: 3306
targetPort: 3306
# Node上的静态端口
nodePort: 30306
  • 通过应用配置文件来创建Service;
1
bash复制代码kubectl apply -f mysql-service.yaml
  • 运行成功后查询Service,发现mysql-service已经暴露在Node的30306端口上了;
1
2
3
4
5
bash复制代码[macro@linux-local k8s]$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 7d23h
mysql-service NodePort 10.107.189.51 <none> 3306:30306/TCP 7s
nginx-service NodePort 10.101.171.181 <none> 80:30080/TCP 6d2h
  • 部署完成后需要新建mall数据库,并导入相关表,表地址:github.com/macrozheng/…
  • 这里有个比较简单的方法来导入数据库,通过Navicat创建连接,先配置一个SSH通道;

  • 之后我们就可以像在Linux服务器上访问数据库一样访问Minikube中的数据库了,直接添加Minikube中数据库IP和端口即可。

部署SpringBoot应用

  • 首先添加配置文件mall-tiny-fabric-deployment.yaml用于创建Deployment,这里我们可以通过环境变量来覆盖SpringBoot中的默认配置;
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
yaml复制代码apiVersion: apps/v1
kind: Deployment
metadata:
name: mall-tiny-fabric-deployment
labels:
app: mall-tiny-fabric
spec:
replicas: 1
selector:
matchLabels:
app: mall-tiny-fabric
template:
metadata:
labels:
app: mall-tiny-fabric
spec:
containers:
- name: mall-tiny-fabric
# 指定Docker Hub中的镜像地址
image: macrodocker/mall-tiny-fabric:0.0.1-SNAPSHOT
ports:
- containerPort: 8080
env:
# 指定数据库连接地址
- name: spring.datasource.url
value: jdbc:mysql://mysql-service:3306/mall?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
# 指定日志文件路径
- name: logging.path
value: /var/logs
volumeMounts:
- mountPath: /var/logs
name: log-volume
volumes:
- name: log-volume
hostPath:
path: /home/docker/mydata/app/mall-tiny-fabric/logs
type: DirectoryOrCreate
  • 通过应用配置文件来创建Deployment;
1
bash复制代码kubectl apply -f mall-tiny-fabric-deployment.yaml
  • 我们可以通过kubectl logs命令来查看应用的启动日志;
1
2
3
4
5
6
7
bash复制代码[macro@linux-local k8s]$ kubectl get pods
NAME READY STATUS RESTARTS AGE
mall-tiny-fabric-deployment-8684857dff-pnz2t 1/1 Running 0 47s
mysql-deployment-5dccc96ccf-sfxvg 1/1 Running 0 25m
nginx-volume-deployment-6f6c89976d-nv2rn 1/1 Running 4 6d6h
nginx-volume-deployment-6f6c89976d-tmhc5 1/1 Running 4 6d5h
[macro@linux-local k8s]$ kubectl logs -f mall-tiny-fabric-deployment-8684857dff-pnz2t
  • 如果想要从外部访问SpringBoot应用,需要创建Service,添加配置文件mall-tiny-fabric-service.yaml用于创建Service;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yaml复制代码apiVersion: v1
kind: Service
metadata:
name: mall-tiny-fabric-service
spec:
type: NodePort
selector:
app: mall-tiny-fabric
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 8080
# Node上的静态端口
nodePort: 30180
  • 通过应用配置文件来创建Service;
1
bash复制代码kubectl apply -f mall-tiny-fabric-service.yaml
  • 此时服务已经暴露到了Node的30180端口上了;
1
2
3
4
5
6
bash复制代码[macro@linux-local k8s]$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 7d23h
mall-tiny-fabric-service NodePort 10.100.112.84 <none> 8080:30180/TCP 5s
mysql-service NodePort 10.107.189.51 <none> 3306:30306/TCP 13m
nginx-service NodePort 10.101.171.181 <none> 80:30080/TCP 6d2h
  • 在Linux服务器上,我们可以通过curl命令来访问下项目的Swagger页面,不过只能查看到返回的一串HTML代码。
1
bash复制代码curl $(minikube ip):30180/swagger-ui.html

外部访问应用

由于使用Minikube安装的K8S Node处于Linux服务器的内网环境,无法直接从外部访问,所以我们需要安装一个Nginx反向代理下才能访问。

  • 首先我们需要安装Nginx,对Nginx不熟悉的朋友直接参考该文章即可:《Nginx的这些妙用,你肯定有不知道的!》
  • 安装完成后添加一个Nginx的配置文件,这里我的配置路径为/mydata/nginx/conf/conf.d/,用于将mall-tiny.macrozheng.com域名的访问代理到K8S中的SpringBoot应用中去,proxy_pass为上面curl使用的路径;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码server {
listen 80;
server_name mall-tiny.macrozheng.com; #修改域名

location / {
proxy_set_header Host $host:$server_port;
proxy_pass http://192.168.49.2:30180; #修改为代理服务地址
index index.html index.htm;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}

}
  • 重启Nginx服务,再修改访问Linux服务器的本机host文件,添加如下记录;
1
复制代码192.168.5.94 mall-tiny.macrozheng.com
  • 之后即可直接在本机上访问K8S上的SpringBoot应用了,访问地址:mall-tiny.macrozheng.com/swagger-ui.…

总结

通过把SpringBoot应用部署到K8S上的一顿操作,我们可以发现在K8S上部署和在Docker上部署有很多相似之处。K8S上很多部署用的脚本,直接翻译之前使用Docker Compose的脚本即可,非常类似。如果你之前用过Docker,那么你就可以轻松上手K8S!

项目源码地址

github.com/macrozheng/…

本文 GitHub github.com/macrozheng/… 已经收录,欢迎大家Star!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python干掉了97%的办公软件?

发表于 2021-02-01

“21世纪,不会Python等于文盲。”
这句流行语并非夸张,《2020年职场学习趋势报告》显示,在2020年最受欢迎的技能排行榜,Python排在第一。

除职场外,Python也开始走入课堂。山东等地已经在小学教材中加入了Python、北京和浙江甚至已经把Python纳入了高考范围。

这也意味着,Python不再是程序员的专属,而是彻底破圈,呈现出全民学习的趋势。“全球化时代学英语,大数据时代学Python”或将成为现实。
如果你要问我,为什么Python会这么火爆,在我来看更多是时代的契机。

在这个大数据时代,从来没有哪一种语言可以像Python一样,在自动化办公、爬虫、数据分析等领域都有众多应用。
更没有哪一种语言,语法如此简洁易读,消除了普通人对于“编程”这一行为的恐惧,从小学生到老奶奶都可以学会。
Python到底有多神奇呢?接下来的几个案例,或许可以回答这个问题。
1 1 1
自动化办公
江湖上流传着“Python杀死了Excel”的说法。
举个例子,处理一张Excel表格过程:定位空值-删除空值-修改数据格式-去除异常值……
繁琐的每一步都是来自鼠标点击, 中间如果一步有误,很多步骤都需要重新调整 ,浪费大量时间。
但使用Python就非常方便 ,输入简短的代码,就可以自动处理上百份表格,跨表取数也不是问题。

而且程序是可以复用的。下次做表,只需要调整设定好的参数就可以,不需要再手动重来。
Python还能帮你实现数据可视化。如果想做出各种好看的图表,几行代码就能自动生成,省时省力,还具有交互功能。
1 2 1
一键爬取全网信息
Python网络爬虫功能很强大。网上的公开信息,无论是论文、报表,还是电影、音乐、优惠券,都可以用Python写个小程序,通通抓取下来自动保存,再也不用千辛万苦地搜索。
用爬虫爬电影
“爬虫”这个词很形象。Python就像一张大网,所有的资料就像虫子,等着被“一网打尽”。
1 3 1
进行数据分析
现在越来越多岗位要求具备数据分析能力,而Python就是数据分析的利器。
它可以快速处理十几个G的大量数据,自动清洗、去重、分类,帮助你得到想要的结果。

像百度、腾讯、Google、美团等大公司的数据分析岗位,都要求必须会使用Python。
今年腾讯笔试题便出现了数据分析编程题,让应聘者惊呼:措手不及。

Python如此强大,已经有不少人已经悄悄运用在工作上了。当然成果也是很惊艳的!
面对这样强大的能力,会Python的人名副其实地成为了就业领域的“爆款抢手货”!
目前,Python人才需求增速高达174%,人才缺口高达50万,部分领域如人工智能、大数据开发,年薪30万都招不到人!

问题来了,Python这么厉害,一定很难学吧?
恰恰相反!Python是最简单易学的编程语言, 对小白学习者非常友好。
举个例子,用三种编程语言做同一件事时:

Python的简洁一目了然。
所以,看到这里,你是不是对学习Python也有了一些兴趣呢?
如果你想学习Python,可以关注同名公号/Python小白集训营/,领取免费的自学视频教程,还有精美编程电子书等着你,助你自学路上畅通无阻。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java 微信支付

发表于 2021-02-01

微信支付开发文档 (V2版)

详细请求参数与返回参数请参考:
微信支付接口文档

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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
java复制代码/**
* <p>
* 便于使用,将所有的工具方法都集中在此,包含:
* 1. 执行 HTTP POST 请求,返回执行结果的 String
* 2. 创建签名(为下单数据创建)
* 3. 创建签名(为 APP 创建)
* 4. 检验签名
* 5. 读取 HTTP Request 内容
* 6. 读取 HTTP Response 内容
* 7. 将 Map 转化为 Xml
* 8. 将 Xml 转化为 Map
* 9. 生成 32 位随机字符串
* 10. MD5 签名
*/
public class WxUtils {

//APPID 微信开放平台 appid
public static final String APPID = "xxx";

//MCH_ID 微信商户平台 商户 mch_id
public static final String MCH_ID = "xxx";

//NOTIFY_URL 回调通知地址(接收微信支付异步通知回调地址,通知url必须为直接可访问的url,不能携带参数。)
public static final String NOTIFY_URL = "xxx";

//API_KEY 微信商户平台密钥
public static String API_KEY = "xxx";

// 下单 API 地址
public static final String PLACE_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";


/**
* 1.请求方式
* @param requestUrl
* @param outputStr
* @return
*/
public static String httpsRequest(String requestUrl,String outputStr) {
try {
URL url = new URL(requestUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();

conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
// 设置请求方式(GET/POST)
conn.setRequestMethod("POST");
//conn.setRequestProperty("content-type", "application/x-www-form-urlencoded");
conn.setRequestProperty("content-type", "text/xml;charset=utf-8");
// 当outputStr不为null时向输出流写数据
if (null != outputStr) {
OutputStream outputStream = conn.getOutputStream();
// 注意编码格式
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
// 从输入流读取返回内容
InputStream inputStream = conn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuilder buffer = new StringBuilder();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
// 释放资源
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
conn.disconnect();
return buffer.toString();
} catch (ConnectException ce) {
System.out.println("连接超时:{}"+ ce);
} catch (Exception e) {
System.out.println("https请求异常:{}"+ e);
}
return null;
}

/** 2
* 第一次签名
*
* @param parameters 数据为服务器生成,下单时必须的字段排序签名
* @param key
* @return
*/
public static String createSign(SortedMap<String, Object> parameters, String key) {
StringBuffer sb = new StringBuffer();
Set es = parameters.entrySet();//所有参与传参的参数按照accsii排序(升序)
Iterator it = es.iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
Object v = entry.getValue();
if (null != v && !"".equals(v)
&& !"sign".equals(k) && !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + key);
return encodeMD5(sb.toString());
}

/** 3
* 第二次签名
*
* @param result 数据为微信返回给服务器的数据(XML 的 String),再次签名后传回给客户端(APP)使用
* @param key 密钥
* @return
* @throws IOException
*/
public static Map createSign2(String result, String key) throws IOException {
SortedMap<String, Object> map = new TreeMap<>(transferXmlToMap(result));
String returnCode = (String)map.get("return_code");
if(StringUtils.equalsIgnoreCase("success",returnCode)){
String resultCode = (String)map.get("result_code");
Map app = new HashMap();
app.put("appid", map.get("appid"));//应用ID
app.put("partnerid", map.get("mch_id"));//商户号
app.put("prepayid", map.get("prepay_id"));//预支付交易会话ID
app.put("package", "Sign=WXPay");// 固定字段,保留,不可修改
app.put("noncestr", map.get("nonce_str"));//随机字符串
app.put("timestamp", new Date().getTime() / 1000); //时间戳 时间为秒,JDK 生成的是毫秒,故除以 1000
app.put("sign", createSign(new TreeMap<>(app), key));//签名
if(StringUtils.equalsIgnoreCase("success",resultCode)){
app.put("tradestate",map.get("trade_state"));
}
System.out.println(app+"-------------");
return app;
}
return null;
}

/** 4
* 验证签名是否正确
*
* @return boolean
* @throws Exception
*/
public static boolean checkSign(SortedMap<String, Object> parameters, String key) throws Exception {
String signWx = parameters.get("sign").toString();
if (signWx == null) return false;
parameters.remove("sign"); // 需要去掉原 map 中包含的 sign 字段再进行签名
String signMe = createSign(parameters, key);
return signWx.equals(signMe);
}

/** 5
* 读取 request body 内容作为字符串
*
* @param request
* @return
* @throws IOException
*/
public static String readRequest(HttpServletRequest request) throws IOException {
InputStream inputStream;
StringBuffer sb = new StringBuffer();
inputStream = request.getInputStream();
String str;
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
while ((str = in.readLine()) != null) {
sb.append(str);
}
in.close();
inputStream.close();
return sb.toString();
}

/** 6
* 读取 response body 内容为字符串
*/
public static String readResponse(HttpResponse response) throws IOException {
BufferedReader in = new BufferedReader(
new InputStreamReader(response.getEntity().getContent()));
String result = new String();
String line;
while ((line = in.readLine()) != null) {
result += line;
}
return result;
}

/** 7
* 将 Map 转化为 XML
*
* @param map
* @return
*/
public static String transferMapToXml(SortedMap<String, Object> map) {
StringBuffer sb = new StringBuffer();
sb.append("<xml>");
for (String key : map.keySet()) {
sb.append("<").append(key).append(">")
.append(map.get(key))
.append("</").append(key).append(">");
}
return sb.append("</xml>").toString();
}


/** 8
* 将 XML 转化为 map
*
* @param strxml
* @return
* @throws JDOMException
* @throws IOException
*/
public static Map transferXmlToMap(String strxml) throws IOException {
strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
if (null == strxml || "".equals(strxml)) {
return null;
}
Map m = new HashMap();
InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
SAXBuilder builder = new SAXBuilder();
Document doc = null;
try {
doc = builder.build(in);
} catch (JDOMException e) {
throw new IOException(e.getMessage()); // 统一转化为 IO 异常输出
}
// 解析 DOM
Element root = doc.getRootElement();
List list = root.getChildren();
Iterator it = list.iterator();
while (it.hasNext()) {
Element e = (Element) it.next();
String k = e.getName();
String v = "";
List children = e.getChildren();
if (children.isEmpty()) {
v = e.getTextNormalize();
} else {
v = getChildrenText(children);
}
m.put(k, v);
}
//关闭流
in.close();
return m;
}

/** 9
* 辅助 transferXmlToMap 方法递归提取子节点数据
* @param children
* @return
*/
private static String getChildrenText(List<Element> children) {
StringBuffer sb = new StringBuffer();
if (!children.isEmpty()) {
Iterator<Element> it = children.iterator();
while (it.hasNext()) {
Element e = (Element) it.next();
String name = e.getName();
String value = e.getTextNormalize();
List<Element> list = e.getChildren();
sb.append("<" + name + ">");
if (!list.isEmpty()) {
sb.append(getChildrenText(list));
}
sb.append(value);
sb.append("</" + name + ">");
}
}
return sb.toString();
}


/** 10
* 生成 32 位随机字符串,包含:数字、字母大小写
*
* @return
*/
public static String gen32RandomString() {
char[] dict = {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
StringBuffer sb = new StringBuffer();
Random random = new Random();
for (int i = 0; i < 32; i++) {
sb.append(String.valueOf(dict[(int) (Math.random() * 62)]));
}
return sb.toString().toUpperCase();
}


/** 11
* MD5 签名
*
* @param str
* @return 签名后的字符串信息
*/
public static String encodeMD5(String str) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] inputByteArray = (str).getBytes();
messageDigest.update(inputByteArray);
byte[] resultByteArray = messageDigest.digest();
return byteArrayToHex(resultByteArray);
} catch (NoSuchAlgorithmException e) {
return null;
}
}

/** 12
* 辅助 encodeMD5 方法实现
* @param byteArray
* @return
*/
private static String byteArrayToHex(byte[] byteArray) {
char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
char[] resultCharArray = new char[byteArray.length * 2];
int index = 0;
for (byte b : byteArray) {
resultCharArray[index++] = hexDigits[b >>> 4 & 0xf];
resultCharArray[index++] = hexDigits[b & 0xf];
}
// 字符数组组合成字符串返回
return new String(resultCharArray);
}
}
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
typescript复制代码package com.xp.service.order.impl;

@Service
public class WXPayServiceImpl implements WXPayService {

@Override
public Map pay(List<OrderInfo> list, Integer userId, List<UserCoupon>couponList,String ipAddress){
try {
SortedMap<String, Object> parameters = new TreeMap<>();
parameters.put("appid", WxUtils.APPID);//应用ID
parameters.put("body", "微信支付");//商品描述
parameters.put("mch_id", WxUtils.MCH_ID);//商户号
parameters.put("nonce_str", WxUtils.gen32RandomString());//随机字符串 不长于32位
parameters.put("notify_url", WxUtils.NOTIFY_URL);//通知地址
//parameters.put("device_info", "WEB"); //设备号 默认"WEB"
parameters.put("out_trade_no", "唯一订单号");//商户订单号
parameters.put("spbill_create_ip", ipAddress);//终端IP
parameters.put("total_fee", "0.01"); // 总金额
parameters.put("trade_type", "APP");//交易类型
parameters.put("sign", WxUtils.createSign(parameters,WxUtils. API_KEY)); //签名 sign 必须在最后
String requestXML = WxUtils.transferMapToXml(parameters);
String result = WxUtils.httpsRequest(WxUtils.PLACE_URL, requestXML); // 执行 HTTP 请求,获取接收的字
return WxUtils.createSign2(result, WxUtils.API_KEY);
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

@Override
public String notify(HttpServletRequest request, HttpServletResponse response){
try {
// 预先设定返回的 response 类型为 xml
response.setHeader("Content-type", "application/xml");
// 读取参数,解析Xml为map
Map<String, String> map = WxUtils.transferXmlToMap(WxUtils.readRequest(request));
// 转换为有序 map,判断签名是否正确
boolean isSignSuccess = WxUtils.checkSign(new TreeMap<String, Object>(map), WxUtils.API_KEY);
if (isSignSuccess) {
// 签名校验成功,说明是微信服务器发出的数据
//String orderId = map.get("out_trade_no");
//if (tradeService.hasProcessed(orderId)) // 判断该订单是否已经被接收处理过
//return success();
// 可在此持久化微信传回的该 map 数据
if (map.get("return_code").equals("SUCCESS")) {
if (map.get("result_code").equals("SUCCESS")) {
/**
*回调成功之后处理业务逻辑(比如,更新订单状态,增加积分,通知仓库发货等等。)
*/

return "SUCCESS";
} else {
return "FAIL";
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return "FAIL";
}
}

现在微信支付已经出现了 3.0 的文档版本,实现起来更加简单方便。微信支付开发文档 3.0 全新发布

但是,以上 v2 版的依然可用,已经亲测试过,完全 OK。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

技术方案设计的方法论及案例分享 技术方案体现广度和深度 技术

发表于 2021-02-01

头图.png

作者 | 高福来(不拔)
来源|阿里巴巴云原生公众号

怎么去体现技术方案设计的深度是大家普遍关心的一个问题,这个问题不是个例问题,因此本文主要分享下作者个人的一些观点和看法。

文章主要分为三个部分:

  • 第一部分主要分析为什么技术方案没有体现出深度,找到问题后就好解决,并提出技术方案的广度和深度特征。
  • 第二部分是技术方案设计的方法论,主要包括了本质论、矛盾论、系统论、演进论四个方法论,构成一个闭环反馈链路。
  • 第三部分是通过具体的案例,反复运用第二部分的方法论阐述在实例的案例中如何去应用,加深对方法论的理解。

技术方案体现广度和深度

  1. 方案设计常见的反馈

我们都希望自己设计的技术方案能够让人眼前一亮、叹为观止、拍案叫绝……,然而在实际情况下,却并不是这样的,我们经常听到如下的说法:

  • 场景简单:业务场景很简单,怎么也设计不出花儿来。
  • 复杂度低:业务复杂度低,很难讲得出挑战来。
  • 亮点少:运用的技术亮点少,基本上都是现有的中间件或框架来完成。
  • 设计普通:方案缺乏新颖,业内也是这么做的,没有体现出自己的设计能力。
  • ……

的确,上面反而是经常遇到的场景,那么需要思考下背后的问题和原因,为什么会有这样的感受,如果这个事情交给另外一个人去做,为什么他能设计出更好的方法,而当时你却没有想到呢?

  1. 原因探究

个人觉得这个问题最为核心的一点是就事论事,因为只是看到这个事,需要完成某个具体的功能点,而没有跳去这个事情的表象,去思考到底要什么、解决了什么问题、价值是什么,这样思考很有可能你现在的解决方案只是其中一个很小的点,没有站在全局去思考问题。曾经我的老师讲过一个观点:把手掌放在眼前,你只能看到这个手掌,如果把手掌放在远处,你的视野就更广了。因此视野更关键,不要只关注事情的本身,可以跳出来看看,或者你能想到的更多。

就事论事只是一个表象,背后还是深层次的原因,个人觉得是缺乏体系化的思考,”只见树木、不见森林”,没有从不同的维度上去思考问题,只是线性的思考,直接的表现就是【就事论事】,只把手头上的事情完成即可。讲体系化思考的书籍很多,大家有兴趣可以去了解下,帮助自己更好地思考问题。

到这里其实还没有结束,还有一个重要的原因是缺乏方法论引导,就是没有形成自己的一套方法去思考问题、解决问题,不同的人会有自己的方法,有了方法论的引导,拿到一个问题,知道怎么去分析、思考、解决,远比只是被动地接受一种具体的方案要好,下次场景变了,很有可能现有的方案是不能支撑的,因此需要建立一套适合自己的方法论,具体在第二部分会分享自己的方法论。

  1. 技术广度和深度

广度和深度对于我们来讲并不陌生,大家都知道要体现出广度和深度,却不知道怎么去做。广度觉得从数量和类型两个维度去分析(应该还有其它的维度,大家可以自行补充),是让事物更加地丰富,比如动物园里有不同的动物,种类比较多,就能更加满足不同人的观赏需求;深度主要体现出问题的识别和创新解决上,一个问题大家没有发现,而你从中发现了,这就是深度,比如网上购物,站在今天来看,再平常不过了,但在 20 年前,并不是每个人能想到的。现如今,同样是做电商,每个公司的打法、策略是不一样的,这就体现在深度上,深耕于某一个领域。

这里拿自己的经历来说明:之前本人在滴滴是做优惠券业务(当时营销比较简单,就是单一券业务),优惠券只是一种营销的具体手段,行业内有卡、券、分、金,那么对于技术来讲就是丰富营销基础能力,从单一券能力发展至卡、券、分、金的营销行业标配能力,这个就体现了广度,从数量、类型上丰富了。而怎么体现深度呢?营销中有一个重要问题是如何防控资损,一旦有资损,问题就比较大,因此需要去好好思考和设计方案,当时借鉴稳定性方案,分成事前、事中、事后三个阶段去防控资损,每一个阶段里又包含了不同的方案,深度主要体现对问题的识别,以及怎样创新地去解决,重点是创新,做到人无我有、人有我优。

  1. 怎样证明技术方案是好的

大家在和别人分享、交流技术方案时,有人会提出一些尖锐的问题,比如:为什么说你的技术方案是好的?其实这个问题非常好,值得大家去思考。

有一个很常见的情况,大家去讲一个技术方案时,把背景、目标讲完之后,直接给出了技术方案,其实技术方案本身并不重要,重要的是你是怎么思考的,思考的过程非常重要,强调的是 WHY,HOW 很重要,但 WHY 更重要。这里有两个原则:

  • 三段论:大提前、小提前、结论。一定要先讲大提前,它是一个有力的支撑,比如写议论文时,平时常写”鲁迅说过 xxxxx”,这个就是大提前;在技术方案设计上,就是要看业内的方案、业界的标杆在哪里,和它有什么不一样、创新了什么,一目了然,往往大家忽略了这个大提前,直接讲自己的方案,怎么证明你的就是好的呢?没有对比就没有感觉。
  • 环境论:有时业内还没有具体的方案,或者是当下你的公司不适合业内顶配的方案,比如”中国特色社会主义”,它就是强调当前的环境,结合了具体的业务场景来权衡考虑的,并不是行业内的最优方案就是适合你的,方案的设计一定要有权衡、选择,设计出最适合当前环境的方案。

1.png

技术方案设计的方法论

  1. 方法论到底是什么

经常有人讲方法论,方法论也让人感觉比较玄乎,感觉是一种虚无缥缈的东西,方法论在百科中的解释是:“方法论是关于人们认识世界、改造世界的方法的理论”,看了这个定义,大家还是不清楚它到底是什么,只知道它挺厉害的,但不知道方法论到底是什么、有哪些方法论、应该如何去运用方法论,所以这里谈下自己的理解。

个人对方法论的理解是方法论是让方法变成更方法的方法,方法论拆分成两个词方法和论。因此它首先是一种方法,方法是为了解决具体的问题,比如大家熟知的稳定性建设,全链路压测、异常监控等都是具体的方法,但这些方法都是一个个散的点,并不是最好的方法,方法论强调的是好的方法;然后再看”论”,论是议论、分析、思考的过程,它最大的好处是让方法更好,还是拿稳定性建设来讲,现在有成熟的方法论,分成事前、事中、事后三个阶段,事前包括容量评估、全链路压测、强弱依赖……,这样讲就比较成体系,将它划分成事前、事中、事后,覆盖了整个过程,你基本上挑不出什么毛病出来。因此方法论是对解决方法进一步的升华和提炼,形成更通用、成体系的方法,它并不是虚无缥缈的东西。

方法论是通过不完全归纳法总结出来的,方法论并不是万能的,比如你看到的天鹅都是白色的,万一哪天出现了一只黑天鹅,就说明当时的归纳是不完全归纳的。

  1. 技术方案设计方法论

下面所说的方法论都是存在的,自己只是组合运用了这些方法论而已,下面总结下自己工作中使用的一些受益比较大的方法论。

本质论是我第一个受益的方法论,本质论强调的是透过现象看本质,这句话听起来是比较简单的,但要做到却是非常难的。看透本质至关重要,能让你真正把控事物的核心,我个人的一个方法是使用不超过 15 个字概括出事物的本质,因为本质的东西是简单的、美的、直揭主旨的,所以判断是否抓住了事物本质的一个标准就是用简单的话能否概括出事物的主旨。比如高并发,现在不再是一个新鲜的词汇,甚至大学生都知道怎么去做,缓存、异步操作、并行……,这些都是具体的措施,问高并发到底是什么,大家都能回答一些,比如流量大、系统压力大、用户多……,这些都是具体的特征,用一句话概括高并发:有限的资源应对大量的请求,概括出了高并发的根本特性,抓住了本质的东西就比较解决问题。带应届生的时候,我提到一个观点:工作三年以后,要能说得出 10 句对技术本质理解的话,提早给自己定下目标,在平时中积累一些思考和沉淀。

矛盾论揭示的是事物之间的矛盾,矛盾是推动事物不断发展的动力,一般从事物本质中,可以看到一些矛盾出来,比如上面高并发的本质是有限的资源应对大量的请求,有限对大量本身就是一对矛盾,找到了矛盾就去解决矛盾,解决的一个方向就是平衡矛盾,矛盾解决了,问题自然就解决了,比如现在资源是大量的,完全可以应对大量的请求,这样高并发的场景对于你来讲就不是一个问题。

系统论是从系统各个要素出发,多维度思考问题,最为简单的是从矛盾双方出发思考问题,比如有限的资源,能不能让资源的数量变多呢?能不能提升资源的处理能力呢?……,从这些方向去思考,思路就一下子打开了,所谓的缓存等常说的方法只是一个个具体的解决手段,我们需要更加立体、多维的解决思路,再结合具体的场景、现状组合一些解决方法。

演进论强调事物是进化的,符合事物的发展规律和人的认识,有可能我们想得非常完善,不可能等所有的事情都做好了再上线,得有计划、分阶段地解决问题,优先解决主要矛盾、核心诉求。也有可能经过一段时间之后,事物的主要矛盾发生了变化,我们的方案也得演进式设计。

2.png

技术方案设计案例

下面拿三个具体的案例来讲怎么将方法论落地于实际的技术方案设计,让大家能够感觉到方法论的真正作用,不再是一种虚的感觉。

  1. 高并发技术方案

高并发在之前是非常火的,大家也都能说出一些解决措施,如使用缓存、MQ、并行……,下面谈下自己的一些思路。

问题定义:高并发的本质是有限的资源应对大量的请求,它的核心问题就是现状不足已支撑那么大量的请求,系统的负载太高,很可能出现网站打不开、用户下不了单等现象。

问题分析:高并发的矛盾就是有限的资源对大量的请求,解决了这个矛盾就解决了高并发的问题。接下来就是平衡这对矛盾,一般是采用”中和”的思想,就像中医治病:寒病用热药、热病用寒药,因此就会站在资源和请求两个维度去思考。资源能不能变多:常见的有水平扩展;资源能不能变强:常见的是性能优化,性能优化又会分成前端优化、网络优化、计算优化、存储优化、程序优化……。请求能不能减少呢?比如通过答题错峰,合并请求等等,这样解决问题的思路就一下打开了。

解决方案是重要的,但设计的过程更为重要,清楚了问题是什么、怎么去分析,解决方案自然而然就出来了,重要的还是分析的过程。

3.png

  1. 异步处理技术方案

说到异步处理,大家最容易想到的方案就是 MQ。MQ 是常见解决的技术方案,如下图所示:贷款端系统向放款端系统发送标的信息,一天的量大约有 4000 多笔,每天偶尔有几个是超时的,影响放款。怎么去解决这个问题呢?用 MQ 是最容易想到的,当时公司还不有用到 MQ 中间件,去搭建一个不现实,怎么办呢。

4.png

问题定义:现有的系统能力无法支撑实时处理,同步调用对系统的压力很大,很有可能某个时间点系统的负载比较大,处理慢了接口调用就超时了。

问题分析:借鉴 MQ 的设计原理,发送方将消息先发送至 Broker 上,消费方从 Broker 上拉取消息消费,抽象出异步处理的本质就是数据暂存 + 择机处理,那么问题来了,数据暂存在哪里呢,内存?文件?数据库?……,择机处理的方式是拉还是推,定时还是随机……,这样一思考,发现除了 MQ 还有很多其它的解决方法,总结出通用的解决方案后,可以在不同具体的环境中演绎出不同的方案。当时设计的方案就是将数据存储到 ftp 服务器上,实现也比较简单,方案没有最好,只有适不适合,难道公司没有 MQ 中间件,这个事情就不能解决了吗?

  1. 可扩展性技术方案

可扩展性设计是现在一个非常典型的场景,当时遇到的场景是实时人群计算场景,每当业务方提一个需求过来,就要进行对数据口径,然后熟悉业务方的一些业务,接下来就是编写 Flink 任务,测试、核对,最后上线,整个流程下来至少 2 周,需求提一个简单需求,很疑惑为什么要 2 周才能上线。

问题定义:业务方希望快速上线而实际开发要 2 周的矛盾,究其主要原因是不懂业务,需要有熟悉的阶段,这个阶段耗时比较多,真正开发的时间不多,怎么去解决这个问题呢?

问题分析:虽然主要的矛盾找到了,很明显的一个方向是让业务方的开发参与进来,平台只做一些支撑、答疑的作用,但是让业务方的同学进来,就有一个挑战:别人没有学过 Flink,你让他来开发,业务方愿意吗?对整个业务进一步的抽象,发现我们的需求场景是变化的,实时指标也是变化的,但整个流程却是不变的,用 y = f(x) 来表示,就是来一个 x 经过计算、变换成结果 y,所以当时就梳理出了哪些是变化的、哪些是不变的,从多变中找不变的东西。这里还需要一种能力是抽象分层,如果把 f() 只当作一层,就只有一个抽象分层,如果里面它还有复合函数,那么就有多个抽象层,这取决于对问题的思考,不同的人设计出的抽象层次是不一样的。当时借鉴了 Flink 的一些设计思想,将整个过程产品化了,业务方只要选择、勾选一些信息,就会自动生成 Flink SQL,然后点击运行即可。SQL 对于大家来讲,入门比较简单,基本上能看得懂,没太大的难度。平台侧不需要像之前那样完全投入人力去学习业务知识、开发、测试上线。

5.png

总结

本要分享了技术方案设计的一些思路,整个方法论包括本质论、矛盾论、系统论、演进论,通过三个具体的案例阐述怎么去运用方法论。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

超全MyBatis动态代理详解!(绝对干货)

发表于 2021-02-01

前言

假如有人问你这么几个问题,看能不能答上来

  1. Mybatis Mapper 接口没有实现类,怎么实现的 SQL 查询
  2. JDK 动态代理为什么不能对类进行代理(充话费送的问题)
  3. 抽象类可不可以进行 JDK 动态代理(附加问题)

答不上来的铁汁,证明 Proxy、Mybatis 源码还没看到位。不过没有关系,继续往下看就明白了

动态代理实战

众所周知哈,Mybatis 底层封装使用的 JDK 动态代理。说 Mybatis 动态代理之前,先来看一下平常我们写的动态代理 Demo,抛砖引玉

一般来说定义 JDK 动态代理分为三个步骤,如下所示

  1. 定义代理接口
  2. 定义代理接口实现类
  3. 定义动态代理调用处理器

三步代码如下所示,玩过动态代理的小伙伴看过就能明白

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
java复制代码public interface Subject { // 定义代理接口
String sayHello();
}

public class SubjectImpl implements Subject { // 定义代理接口实现类
@Override
public String sayHello() {
System.out.println(" Hello World");
return "success";
}
}

public class ProxyInvocationHandler implements InvocationHandler { // 定义动态代理调用处理器
private Object target;

public ProxyInvocationHandler(Object target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(" 🧱 🧱 🧱 进入代理调用处理器 ");
return method.invoke(target, args);
}
}

写个测试程序,运行一下看看效果,同样是分三步

  1. 创建被代理接口的实现类
  2. 创建动态代理类,说一下三个参数
    • 类加载器
    • 被代理类所实现的接口数组
    • 调用处理器(调用被代理类方法,每次都经过它)
  3. 被代理实现类调用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class ProxyTest {
public static void main(String[] args) {
Subject subject = new SubjectImpl();
Subject proxy = (Subject) Proxy
.newProxyInstance(
subject.getClass().getClassLoader(),
subject.getClass().getInterfaces(),
new ProxyInvocationHandler(subject));

proxy.sayHello();
/**
* 打印输出如下
* 调用处理器:🧱 🧱 🧱 进入代理调用处理器
* 被代理实现类:Hello World
*/
}
}

Demo 功能实现了,大致运行流程也清楚了,下面要针对原理实现展开分析

动态代理原理分析

从原理的角度上解析一下,上面动态代理测试程序是如何执行的

第一步简单明了,创建了 Subject 接口的实现类,也是我们常规的实现

第二步是创建被代理对象的动态代理对象。这里有朋友就问了,怎么证明这是个动态代理对象?如图所示

JDK 动态代理对象名称是有规则的,凡是经过 Proxy 类生成的动态代理对象,前缀必然是 $Proxy,后面的数字也是名称组成部分

如果有小伙伴想要一探究竟,关注 Proxy 内部类 ProxyClassFactory,这里会有想要的答案

回归正题,继续看一下 ProxyInvocationHandler,内部保持了被代理接口实现类的引用,invoke 方法内部使用反射调用被代理接口实现类方法

可以看出生成的动态代理类,继承了 Proxy 类,然后对 Subject 接口进行了实现,而实现方法 sayHello 中实际调用的是 ProxyInvocationHandler 的 invoke 方法

一不小心发现了 JDK 动态代理不能对类进行代理的原因 ^ ^

也就是说,当我们调用 Subject#sayHello 时,方法调用链是这样的

但是,Demo 里有被代理接口的实现类,Mybatis Mapper 没有,这要怎么玩

不知道不要紧,知道了估计也看不到这了,一起看下 mybatis 源码是怎么玩的

mybatis version:3.4.x

Mybatis 源码实现

不知道大家考没考虑过这么一个问题,Mapper Mapper 为什么不需要实现类?

假如说,我们项目使用的三层设计,Controller 控制请求接收,Service 负责业务处理,Mapper 负责数据库交互

Mapper 层也就是我们常说的数据库映射层,负责对数据库的操作,比如对数据的查询或者新增、删除等

大胆设想下,项目没有使用 Mybatis,需要在 Mapper 实现层写数据库交互,会写一些什么内容?

会写一些常规的 JDBC 操作,比如:

1
2
3
4
5
6
7
8
9
10
11
java复制代码// 装载Mysql驱动
Class.forName(driveName);
// 获取连接
con = DriverManager.getConnection(url, user, pass);
// 创建Statement
Statement state = con.createStatement();
// 构建SQL语句
String stuQuerySqlStr = "SELECT * FROM student";
// 执行SQL返回结果
ResultSet result = state.executeQuery(stuQuerySqlStr);
...

如果项目中所有 Mapper 实现层都要这么玩,那岂不是很想打人…

所以 Mybatis 结合项目痛点,应运而生,怎么做的呢

  1. 将所有和 JDBC 交互的操作,底层采用 JDK 动态代理封装,使用者只需要自定义 Mapper 和 .xml 文件
  2. SQL 语句定义在 .xml 文件或者 Mapper 中,项目启动时通过解析器解析 SQL 语句组装为 Java 中的对象

解析器分为多种,因为 Mybatis 中不仅有静态语句,同时也包含动态 SQL 语句

这也就是为什么 Mapper 接口不需要实现类,因为都已经被 Mybatis 通过动态代理封装了,如果每个 Mapper 都来一个实现类,臃肿且无用。经过这一顿操作,展示给我们的就是项目里用到的 Mybatis 框架

上面铺垫这么久,终于要到主角了,为什么 Mybatis Mapper 接口没有实现类也可以实现动态代理

想要严格按照先后顺序介绍 Mybatis 动态代理流程,而不超前引用未介绍过的术语,这几乎是不可能的,笔者尽量说的通俗易懂

无实现类完成动态代理

核心点来了,拿起小本本坐板正了

我们先来看下普通动态代理有没有可能不用实现类,仅靠接口完成

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public interface Subject {
String sayHello();
}

public class ProxyInvocationHandler implements InvocationHandler {

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(" 🧱 🧱 🧱 进入代理调用处理器 ");
return "success";
}
}

根据代码可以看到,我们并没有实现接口 Subject,继续看一下怎么实现动态代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class ProxyTest {
public static void main(String[] args) {
Subject proxy = (Subject) Proxy
.newProxyInstance(
subject.getClass().getClassLoader(),
new Class[]{Subject.class},
new ProxyInvocationHandler());

proxy.sayHello();
/**
* 打印输出如下
* 调用处理器:🧱 🧱 🧱 进入代理调用处理器
*/
}
}

可以看到,对比文初的 Demo,这里对 Proxy.newProxyInstance 方法的参数作出了变化

之前是通过实现类获取所实现接口的 Class 数组,而这里是把接口本身放到 Class 数组中,殊归同途

有实现接口和无实现接口产生的动态代理类有什么区别

  1. 有实现接口是对 InvocationHandler#invoke 方法调用,invoke 方法通过反射调用被代理对象(SubjectImpl)方法(sayHello)
  2. 无实现接口则是仅对 InvocationHandler#invoke 产生调用。所以有接口实现返回的是被代理对象接口返回值,而无实现接口返回的仅是 invoke 方法返回值

InvocationHandler#invoke 方法返回值是 success 字符串,定义个字符串变量,是否能成功返回

现在第一个问题答案已经浮现,Mapper 没有实现类,所有调用 JDBC 等操作都是在 Mybatis InvocationHandler 实现的

问题既然已经得到了解决,给人一种感觉,好像没那么难,但是你不好奇,Mybatis 底层怎么做的么?

先抛出一个问题,然后带着问题去看源码,可能让你记忆 Double 倍深刻

咱们 Demo 里的接口是固定的,Mybatis Mapper 可是不固定的,怎么搞?

Mybatis 是这么说的

看看 Mybatis 底层它怎么实现的动态接口代理,小伙伴只需要关注标记处的代码即可

和我们的 Demo 代码很像,核心点在于 mapperInterface 它是怎么赋值的

先来说一下 Mybatis 代理工厂中具体生成动态代理类具体逻辑

  1. 根据 .xml 上关联的 namespace, 通过 Class#forName 反射的方式返回 Class 对象(不止 .xml namespace 一种方式)
  2. 将得到的 Class 对象(实际就是接口对象)传递给 Mybatis 代理工厂生成代理对象,也就是刚才 mapperInterface 属性

谜底揭晓,Mybatis 使用接口全限定名通过 Class#forName 生成 Class 对象,这个 Class 对象类型就是接口

为了方便大家理解,通过 Mybatis 源码提供的测试类举例。假设已有接口 AutoConstructorMapper 以及对应的 .xml 如下

执行第一步,根据 .xml namespace 得到 Class 对象

  1. 首先第一步获取 .xml 上 mapper 标签 namespace 属性,得到 mapper 接口全限定信息
  2. 根据 mapper 全限定信息获取 Class 对象
  3. 添加到对应的映射器容器中,等待生成动态代理对象

如果此时调用生成动态代理对象,代理工厂 newInstance 方法如下:

至此,文初提的 Proxy、Mybatis 动态代理相关问题已全部答疑

抽象类能否 JDK 动态代理

说代码前结论先行,不能!

1
2
3
4
5
6
7
8
9
10
java复制代码public abstract class AbstractProxy {
abstract void sayHello();
}

AbstractProxy proxyInterface = (AbstractProxy) Proxy
.newProxyInstance(
ProxyTest.class.getClassLoader(),
new Class[]{AbstractProxy.class},
new ProxyInvocationHandler());
proxyInterface.sayHello();

毫无疑问,报错是必然的,JDK 是不能对类进行代理的

带着小疑惑我们看一下 Proxy 源码报错位置,JDK 动态代理在生成代理类的过程代码中,会有是否接口验证

抽象类终归是类,加个 abstract 也成不了接口(就像我,虽然胖了 60 斤,但依然是帅哥)

下次面试官如果有问这问题的,斩钉截铁一点,就是不能

结言

结合 Mybatis 使用 JDK 动态代理相关的问题,展开了文章的讲述,这里总结下

Q:JDK 动态代理能否对类代理?

因为 JDK 动态代理生成的代理类,会继承 Proxy 类,由于 Java 无法多继承,所以无法对类进行代理

Q:抽象类是否可以 JDK 动态代理?

不可以,抽象类本质上也是类,Proxy 生成代理类过程中,会校验传入 Class 是否接口

Q:Mybatis Mapper 接口没有实现类,怎么实现的动态代理?

Mybatis 会通过 Class#forname 得到 Mapper 接口 Class 对象,生成对应的动态代理对象,核心业务处理都会在 InvocationHandler#invoke 进行处理


希望读过的小伙伴都能有所收获,如果对于文章内容有所疑惑,可以通过留言或者添加作者好友的方式沟通,祝好!

微信搜索【源码兴趣圈】,关注公众号后回复 123 领取内容涵盖 GO、Netty、Seata、SpringCloud Alibaba、开发规范、面试宝典、数据结构等学习资料!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

太强了,Istio竟然有这么多功能,拜拜了 SpringCl

发表于 2021-01-31

1 简介

Istio,希腊语,意扬帆起航。

一个完全开源的服务网格产品,对分布式应用是透明的。Istio 管理服务之间的流量,实施访问政策并汇总遥测数据,而不需要更改应用代码。Istio 以透明的方式对现有分布式应用进行分层,从而简化了部署复杂性。

也是一个平台,可与任何日志、遥测和策略系统集成。
服务于微服务架构,并提供保护、连接和监控微服务的统一方法。

在原有的数据平面的基础上,增加了控制平面。

为什么会火

  • 发布及时(2017年5月发布0.1版本)
  • 巨头厂商buff 加持
  • 第二代Service Mesh
  • Envoy的加入让Istio如虎添翼
  • 功能强大

优点

  • 轻松构建服务网格
  • 应用代码无需更改
  • 功能强大

2 核心功能

2.1 流量控制

路由、流量转移
流量进出
网络弹性能力
测试相关

2.1.1 核心资源(CRD)

虚拟服务(Virtual Service) 和目标规则(Destination Rule) 是 Istio 流量路由功能的关键拼图。

2.1.1.1 虚拟服务( Virtual Service )

虚拟服务让你配置如何在服务网格内将请求路由到服务,这基于 Istio 和平台提供的基本的连通性和服务发现能力。每个虚拟服务包含一组路由规则,Istio 按顺序评估它们,Istio 将每个给定的请求匹配到虚拟服务指定的实际目标地址。您的网格可以有多个虚拟服务,也可以没有,取决于使用场景。

  • 将流量路由到给定目标地址
  • 请求地址与真实的工作负载解耦
  • 包含一组路由规则
  • 通常和目标规则( Destination Rule)成对出现
  • 丰富的路由匹配规则

2.1.1.2 目标规则( Destination Rule)

定义虚拟服务路由目标地址的真实地址,即子集。
设置负载均衡的方式

  • 随机
  • 权重
  • 最少请求数

2.1.1.3 网关(Gateway)

Egress 不一定使用。

服务入口 (Service Entry)

  • 使用服务入口(Service Entry) 来添加一个入口到 Istio 内部维护的服务注册中心,即把外部服务注册到网格中。

    添加了服务入口后,Envoy 代理可以向服务发送流量,就好像它是网格内部的服务一样。

配置服务入口允许您管理运行在网格外的服务的流量,它包括以下几种能力:

  • 为外部目标 redirect 和转发请求,例如来自 web 端的 API 调用,或者流向遗留老系统的服务。
  • 为外部目标定义重试、超时和故障注入策略。
  • 添加一个运行在虚拟机的服务来扩展您的网格。
  • 从逻辑上添加来自不同集群的服务到网格,在 Kubernetes 上实现一个多集群 Istio 网格。

你不需要为网格服务要使用的每个外部服务都添加服务入口。默认情况下,Istio 配置 Envoy 代理将请求传递给未知服务。但是,您不能使用 Istio 的特性来控制没有在网格中注册的目标流量。

Sidecar


默认情况下,Istio 让每个 Envoy 代理都可以访问来自和它关联的工作负载的所有端口的请求,然后转发到对应的工作负载。您可以使用 sidecar 配置去做下面的事情:

  • 微调 Envoy 代理接受的端口和协议集。
  • 限制 Envoy 代理可以访问的服务集合。

你可能希望在较庞大的应用程序中限制这样的 sidecar 可达性,配置每个代理能访问网格中的任意服务可能会因为高内存使用量而影响网格的性能。

您可以指定将 sidecar 配置应用于特定命名空间中的所有工作负载,或者使用 workloadSelector 选择特定的工作负载。

2.1.2 网络弹性和测试

除了为你的网格导流,Istio 还提供了可选的故障恢复和故障注入功能,使你可以在运行时动态配置这些功能。使用这些特性可以让应用程序运行稳定,确保服务网格能够容忍故障节点,并防止局部故障级联影响到其他节点。

超时

超时是 Envoy 代理等待来自给定服务的答复的时间量,以确保服务不会因为等待答复而无限期的挂起,并在可预测的时间范围内调用成功或失败。HTTP 请求的默认超时时间是 15 秒,这意味着如果服务在 15 秒内没有响应,调用将失败。

对于某些应用程序和服务,Istio 的缺省超时可能不合适。例如,超时太长可能会由于等待失败服务的回复而导致过度的延迟;而超时过短则可能在等待涉及多个服务返回的操作时触发不必要地失败。为了找到并使用最佳超时设置,Istio 允许您使用虚拟服务按服务轻松地动态调整超时,而不必修改您的业务代码。

重试

重试设置指定如果初始调用失败,Envoy 代理尝试连接服务的最大次数。通过确保调用不会因为临时过载的服务或网络等问题而永久失败,重试可以提高服务可用性和应用程序的性能。重试之间的间隔(25ms+)是可变的,并由 Istio 自动确定,从而防止被调用服务被请求淹没。HTTP 请求的默认重试行为是在返回错误之前重试两次。

与超时一样,Istio 默认的重试行为在延迟方面可能不适合您的应用程序需求(对失败的服务进行过多的重试会降低速度)或可用性。您可以在虚拟服务中按服务调整重试设置,而不必修改业务代码。您还可以通过添加每次重试的超时来进一步细化重试行为,并指定每次重试都试图成功连接到服务所等待的时间量。

熔断器

熔断器是 Istio 为创建具有弹性的微服务应用提供的另一个有用的机制。在熔断器中,设置一个对服务中的单个主机调用的限制,例如并发连接的数量或对该主机调用失败的次数。一旦限制被触发,熔断器就会“跳闸”并停止连接到该主机。使用熔断模式可以快速失败而不必让客户端尝试连接到过载或有故障的主机。

熔断适用于在负载均衡池中的“真实”网格目标地址,您可以在目标规则中配置熔断器阈值,让配置适用于服务中的每个主机

故障注入

在配置了网络,包括故障恢复策略之后,可使用 Istio 的故障注入机制来为整个应用程序测试故障恢复能力。故障注入是一种将错误引入系统以确保系统能够承受并从错误条件中恢复的测试方法。使用故障注入特别有用,能确保故障恢复策略不至于不兼容或者太严格,这会导致关键服务不可用。

与其他错误注入机制(如延迟数据包或在网络层杀掉 Pod)不同,Istio 允许在应用层注入错误。这使您可以注入更多相关的故障,例如 HTTP 错误码,以获得更多相关的结果。

可以注入两种故障,它们都使用虚拟服务配置:

  • 延迟
    延迟是时间故障。它们模拟增加的网络延迟或一个超载的上游服务。
  • 终止
    终止是崩溃失败。他们模仿上游服务的失败。终止通常以 HTTP 错误码或 TCP 连接失败的形式出现。

流量镜像

流量镜像,也称为影子流量,是一个以尽可能低的风险为生产带来变化的强大的功能。镜像会将实时流量的副本发送到镜像服务。镜像流量发生在主服务的关键请求路径之外。

在此任务中,首先把流量全部路由到 v1 版本的测试服务。然后,执行规则将一部分流量镜像到 v2 版本。

2.2 可观察性

可观察性≠监控

监控是指从运维角度,被动地审视系统行为和状态,是在系统之外探查系统的运行时状态。
而可观察性是从开发者的角度主动地探究系统的状态,开发过程中去考虑把哪些系统指标暴露出去。而在原始时期的我们都是通过日志查看系统运行时状态,所以这也是一种理念创新。

组成

指标(Metrics)

通过聚合的数据来监测你的应用运行情况。为了监控服务行为,Istio 为服务网格中所有出入的服务流量都生成了指标。这些指标提供了关于行为的信息,例如总流量数、错误率和请求响应时间。

除了监控网格中服务的行为外,监控网格本身的行为也很重要。Istio 组件可以导出自身内部行为的指标,以提供对网格控制平面的功能和健康情况的洞察能力。

Istio 指标收集由运维人员配置来驱动。运维人员决定如何以及何时收集指标,以及指标本身的详细程度。这使得它能够灵活地调整指标收集来满足个性化需求。
Istio中的指标分类:

代理级别的指标( Proxy-level)

Istio 指标收集从 sidecar 代理(Envoy) 开始。每个代理为通过它的所有流量(入站和出站)生成一组丰富的指标。代理还提供关于它本身管理功能的详细统计信息,包括配置信息和健康信息。

Envoy 生成的指标提供了资源(例如监听器和集群)粒度上的网格监控。因此,为了监控 Envoy 指标,需要了解网格服务和 Envoy 资源之间的连接。

Istio 允许运维在每个工作负载实例上选择生成和收集哪个 Envoy 指标。默认情况下,Istio 只支持 Envoy 生成的统计数据的一小部分,以避免依赖过多的后端服务,还可以减少与指标收集相关的 CPU 开销。然而,运维可以在需要时轻松地扩展收集到的代理指标集。这支持有针对性地调试网络行为,同时降低了跨网格监控的总体成本。

Envoy 文档包括了 Envoy 统计信息收集的详细说明。Envoy 统计里的操作手册提供了有关控制代理级别指标生成的更多信息。

代理级别指标的例子:

1
2
3
4
5
6
7
8
bash复制代码# 当前集群中来自于上游服务的总的请求数
envoy_cluster_internal_upstream_rq{response_code_class="2xx",
cluster_name="xds-grpc"} 7163

# 上游服务完成的请求数量
envoy_cluster_upstream_rq_completed{cluster_name="xds-grpc"} 7164
# 是SSL连接出错的数量
envoy_cluster_ssl_connection_error{cluster_name="xds-grpc"} 0
服务级别的指标( Service-level)

监控服务通信的面向服务的指标。

  • 四个基本的服务监控需求:延迟、流量、错误和饱和情况。Istio 带有一组默认的仪表板,用于监控基于这些指标的服务行为。
  • 默认的 Istio 指标由 Istio 提供的配置集定义并默认导出到 Prometheus。运维人员可以自由地修改这些指标的形态和内容,更改它们的收集机制,以满足各自的监控需求。
    收集指标任务为定制 Istio 指标生成提供了更详细的信息。
  • 服务级别指标的使用完全是可选的。运维人员可以选择关闭指标的生成和收集来满足自身需要。

服务级别指标的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bash复制代码istio_requests_total{
connection_security_policy="mutual_tls",
destination_app="details",
destination_principal="cluster.local/ns/default/sa/default",
destination_service="details.default.svc.cluster.local",
destination_service_name="details",
destination_service_namespace="default",
destination_version="v1",
destination_workload="details-v1",
destination_workload_namespace="default",
reporter="destination",
request_protocol="http",
response_code="200",
response_flags="-",
source_app="productpage",
source_principal="cluster.local/ns/default/sa/default",
source_version="v1",
source_workload="productpage-v1",
source_workload_namespace="default"
} 214
控制平面指标(Control plane )

每一个 Istio 的组件(Pilot、Galley、Mixer)都提供了对自身监控指标的集合。这些指标容许监控 Istio 自己的行为(这与网格内的服务有所不同)。

访问日志

通过应用产生的事件来监控你的应用。
访问日志提供了一种从单个工作负载实例的角度监控和理解行为的方法。
Istio 可以从一组可配置的格式集生成服务流量的访问日志,为运维人员提供日志记录的方式、内容、时间和位置的完全控制。Istio 向访问日志机制暴露了完整的源和目标元数据,允许对网络通信进行详细的审查。

  • 生成位置可选
    访问日志可以在本地生成,或者导出到自定义的后端基础设施,包括 Fluentd。
  • 日志内容
    应用日志
    Envoy 服务日志:kubectl logs -l app=demo -C istio-proxy

Istio 访问日志例子(JSON 格式):

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
bash复制代码{
"level": "info",
"time": "2019-06-11T20:57:35.424310Z",
"instance": "accesslog.instance.istio-control",
"connection_security_policy": "mutual_tls",
"destinationApp": "productpage",
"destinationIp": "10.44.2.15",
"destinationName": "productpage-v1-6db7564db8-pvsnd",
"destinationNamespace": "default",
"destinationOwner": "kubernetes://apis/apps/v1/namespaces/default/deployments/productpage-v1",
"destinationPrincipal": "cluster.local/ns/default/sa/default",
"destinationServiceHost": "productpage.default.svc.cluster.local",
"destinationWorkload": "productpage-v1",
"httpAuthority": "35.202.6.119",
"latency": "35.076236ms",
"method": "GET",
"protocol": "http",
"receivedBytes": 917,
"referer": "",
"reporter": "destination",
"requestId": "e3f7cffb-5642-434d-ae75-233a05b06158",
"requestSize": 0,
"requestedServerName": "outbound_.9080_._.productpage.default.svc.cluster.local",
"responseCode": 200,
"responseFlags": "-",
"responseSize": 4183,
"responseTimestamp": "2019-06-11T20:57:35.459150Z",
"sentBytes": 4328,
"sourceApp": "istio-ingressgateway",
"sourceIp": "10.44.0.8",
"sourceName": "ingressgateway-7748774cbf-bvf4j",
"sourceNamespace": "istio-control",
"sourceOwner": "kubernetes://apis/apps/v1/namespaces/istio-control/deployments/ingressgateway",
"sourcePrincipal": "cluster.local/ns/istio-control/sa/default",
"sourceWorkload": "ingressgateway",
"url": "/productpage",
"userAgent": "curl/7.54.0",
"xForwardedFor": "10.128.0.35"
}

分布式追踪

通过追踪请求来了解服务之间的调用关系,用于问题的排查以及性能分析。
分布式追踪通过监控流经网格的单个请求,提供了一种监控和理解行为的方法。追踪使网格的运维人员能够理解服务的依赖关系以及在服务网格中的延迟源。

Istio 支持通过 Envoy 代理进行分布式追踪。代理自动为其应用程序生成追踪 span,只需要应用程序转发适当的请求上下文即可。

Istio 支持很多追踪系统,包括 Zipkin、Jaeger、LightStep、Datadog。运维人员控制生成追踪的采样率(每个请求生成跟踪数据的速率)。这允许运维人员控制网格生成追踪数据的数量和速率。

更多关于 Istio 分布式追踪的信息可以在分布式追踪 FAQ 中找到。

Istio 为一个请求生成的分布式追踪数据:

网络安全

授权及身份认证

策略

限流
黑白名单

参考

  • istio.io/latest/zh/d…
  • istio.io/latest/zh/d…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

毕业设计-分布式爬虫系统(干货)

发表于 2021-01-31

前言

​ 很多同学会问:“为什么我的毕业设计总是过不了?为什么我的毕设分数很低?”这种情况要么就是你的毕设做得过于粗糙,要么就是功能过于简单,给导师的感觉就是很容易就能实现,你小子压根没花时间去做。你们说是不是这个理儿?

​ 本期案例分享,学长给大家上点干货,手把手带你开发一个分布式爬虫系统。通过这个项目,你将学习到下面几点:

  1. 架构设计。如果设计一个通用的爬虫系统?一个系统支持爬取所有的网站。
  2. 分布式开发经验。分布式系统开发考虑的点会更多,如何保证代码在多节点部署时还能正确的运行?
  3. 多线程开发经验。大量使用了concurrent包中的多线程类,多线程、线程池、锁。结合真实的业务场景教你怎么玩转多线程,跟你平时写的多线程demo是完全不同的。

郑重声明:本项目的出发点是学习和技术分享,项目中出现的爬虫案例也都是互联网上可以公开访问的网站,爬取时严格控制了爬取频率以及爬取速度(单线程去爬取,每爬取一个页面休眠1秒,最多爬取100个页面),绝不会影响目标网站的正常运行。

​ (想要源码、文档、视频教程的同学请扫码加我微信。这里打个广告,学长多年bat工作经验,常年负责校招和社招面试,有兴趣的同学可以私信我,我可以提供简历辅导和内推

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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264


![image-20210131195045721](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/41517b329119eb3bcf344d700ec01bdb3253f9fabf5c8ec60791fd000c23f68b)


项目架构
----


### 爬虫组件


![img](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/36450976cd14c5575d6cd68b70ad7314696fbba4277cb41a141553d0e6066198)


上面是爬虫系统的经典架构图,简单说下每个组件的职责:


* Spiders:每个spider负责处理一个特殊的网站,负责抽取目标网站的数据
* Scheduler:调度器从引擎接受request并将他们入队,以便之后引擎请求他们时提供给引擎
* Downloader:根据request对象去执行网络下载
* Pipeline:负责将spider抽取出来的item数据进行存储,这里我们存储在MySQL


最后我们合起来说明下爬虫流程:


1. 首先创建一个spider爬虫任务,这时候会有一个入口URL,spider从这个入口开始爬取
2. 调用Downloader组件去执行http请求下载整个页面
3. spider解析页面中的内容,将需要的内容放入item里面,同时将页面中的子URL放入Scheduler组件
4. Pipeline负责将item中的数据就行持久化存储
5. URL放入Scheduler组件,Scheduler组件会对URL进行去重,避免重复爬取
6. spider爬完当前页面后就继续从Scheduler拿URL,如果有URL则继续爬取,没有则说明所有页面都爬完了,spider任务结束


### 分布式爬虫


​ 什么是分布式爬虫?用大白话来说就是:我部署了多个爬虫模块,这几个模块可以一起来爬虫。从上面架构图的分析,只需要将Scheduler模块基于redis实现,那么所有的模块的spider只需要从redis获取URL,然后爬到新的子URL时也放入redis中,此时我们的架构已经是支持分布式爬虫了。(代码细节较多,文章篇幅有限,不展开细说了)


### 定制化爬虫


​ 对于一些页面静态化的网站,做了SEO可以直接被搜索引擎爬取的网站、没有做反爬的网站,这些网站我们是可以定制一个通用的爬虫策略来爬取,直接http请求,然后解析内容和图片等资源。而对于一些做了反爬策略的,例如分页的数据、动态渲染的网页、请求头拦截、ip高频拦截等等。对于这类网站的爬虫需要做一些定制化的逻辑,所以在架构设计上,学长提供了一个爬虫订制模板的入口,通过在代码中开发针对具体网站的定制化爬虫策略,这样就可以避开大多数的反爬规则,从而实现一个爬虫系统可以支持绝大多数的网站爬取。


### 领域模型


* DO(DataObject):与数据库表结构一一对应,通过DAO层向上传输数据源对象
* BO(BusinessObject):业务对象。由Service层输出的封装业务逻辑的对象
* VO(View Object):显示层对象,通常是Web向模板渲染引擎层传输的对象


BO和VO领域模型又分为BoRequest(输入模型)、BoResponse(输出模型)、VoRequest(输入模型)、VoResponse(输出模型)


技术栈
---


前端:vue + element


后端:jdk1.8 + springboot + redis + mysql + jsoup + httpClient


权限:security+spring-session


接口设计
----


​ 整个项目接口采用的目前互联网比较流行的restful风格设计,每个接口、每个参数都有详细的文档说明。因为企业中开发必然是团队协作,必然前后端分离的开发模式,你得先把接口定义出来,然后前端可以和后端同步开发。还有一种就是对外提供接口,比如你们隔壁团队也想调用你这个服务的接口,但是你两排期是同一周,这时候你得先把接口定义出来给人家,然后大家同步开发,开发完了之后再进行联调。


![image-20210131185438111](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/5f2154335758c22f4f559fe4f70d3ab7e4ab3f80cf68046614eb1cba40ee147a)


### 运行效果


**系统登录**


![image-login](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/9298abb5ad4bd2b0fea7aeeb5f1f54e8016f1e676d4325688f78f1f78c576170)


**dashboard**


实时统计系统数据


![image-20210131185722879](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/9f8ffe78c4c02ee6a29c185327e156371ec7fd88b5df34360d1e71e2c2a99b67)


**任务管理**


页面菜单、“查询”、“创建”、“编辑”、“删除”按钮都支持单独的权限分配,这里列举了爬虫案例,“爬取百度新闻”、“爬取必应壁纸”、“爬取当当网书籍信息”、“爬取新浪新闻”


创建爬虫任务


![image-20210131190101510](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/d34d17fdffdd92e4f1dc2672f67e1f675c5987b5ef8408b4adc89329836a07bc)


![image-20210131185845528](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/3a3322373baa5fcf076c4ddff583f496e83ec64496f585c44004a125f05de6c0)


**爬取必应壁纸**


​ 很多人用必应搜索是因为喜欢必应的高清壁纸(学长就是这样的),这里演示必应壁纸的爬虫。因为必应壁纸涉及分页,这里刚好用到我们的订制模板功能,通过写一个BingTemplate模板,我们可以轻松地搞定分页数据爬虫。


​ **文明爬虫**,我们只爬了100页数据,每页的图片都在资源详情里面,非常漂亮,可以点击放大图片并下载


![bing](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/faf10b2ba99e0e4084962905677cc6e548d899bf3d8d7aee7bff8852a6bdc584)


![image (1)](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/2a02580c27e2b41301c66450745a5c06e1b1cc48e74386ff7af49f47a860ce7f)


**爬取当当网**


​ 之前学长在专栏分享过《图书管理系统》,当时为了让内容更真实,从当当网爬取了一些书籍信息,比如书名、作者、出版社、价格、简介等信息。(感兴趣的可以去专栏回顾下图书管理系统的设计和实现)


​ 订制一个当当网的爬虫模板DangDangTemplate,只须一个URL,开始我们的订制爬虫之旅。


![image-20210131192319379](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/322fd59e51103c9773142d55916b2c175df3e57e18806bd714db349369474848)


**爬取百度和新浪新闻**


​ 百度新闻和新浪新闻都比较好爬,不需要模板,直接新建任务,只需要填入一个URL,立即开始爬虫。


![image-20210131191805320](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/5e280d756fe1bbf9306725e2c1580b59aa9694695880d9de300b652d2a40badd)


![image-20210131191558471](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/02e812efb81a1345d2d28a13ecdafc700ab67220406376ef82f8ea026c9fb7f6)


**资源管理**


所有爬取到的数据都可以在资源管理界面查询到,点击“资源详情”可以看到具体的文本、图片内容,图片支持放大、下载、以及幻灯片播放


![image-20210131192636062](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/95b14029e79fc236c6995623771c842a8865a3b3befdfe77f9e515f662cc455c)


**模板管理**


​ 前面已经说过,对于有反爬策略的网站和定制化的爬虫都可以通过开发一个爬虫模板来实现,这样的设计对系统扩展性是非常好的,等于说一个爬虫系统可以爬取所有的内容。


![image-20210131192937284](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/f4f4ab0159a5669a068132019acd9c10b19d9d00a9cadb88815e935f7edc8db5)


**日志管理**


​ 日志管理默认是开给管理员的,在系统中的所有操作都会被记录,在系统出现异常时也便于管理员进行问题排查。


![image-20210131193355572](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/8be9cba1db04acdddd3c0329af4f5f30012e3b59cfaf3f59256f93a2049bbd65)


**用户管理**


​ 默认也是只有管理员拥有用户管理菜单的权限,可以新建/编辑用户、分配用户角色、禁用/启用等操作


![image-20210131193425813](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/5d8e92eab58eef36563c1f6e37e34939a95f27b2c39a39582bcca41d260b73e5)


**编辑用户信息**


​ 拥有账号编辑权限的用户可以进行编辑操作


![image-20210131193554735](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/c414f0240503f4a0a865a216ee76e8f0ce1872cb0c42c19935a85b384a21174a)


**角色管理**


​ 默认也是只有管理员拥有角色管理菜单的权限,这里的权限是细粒度到按钮权限的,每个按钮都可以进行权限管理,假如给用户只分配了任务的“查询”权限,但是这个用户是个程序员,他想通过接口请求直接访问任务修改接口,这时候后端是会权限校验的,返回“未授权”的错误码,然后前端根据“未授权”错误码会重定向到一个403页面(这也是为什么说只有前端校验是不安全的,后端也必须得校验,这在实际企业里开发也是这样的,还没有实际开发经验的学弟学妹拿个小本本记一记,哈哈哈)


![image-20210131193716723](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/e51a0488ea97141519b7c38bd48ee0705f7684d115da46dd9cabddf795dcad76)


![image-20210131193755185](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/03b1764b60696e37dccc48c0bee236cb4bc15ecc010b6a83c16960f9c08be3ba)


![image-20210109205945188](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/53ebab3e5d477ae22e8ab3a673dfa3cec17d80fa4a71e320dea82aeb36e9fc6e)


### 权限设计


​ 权限基于security和spring-session实现。权限可以分为认证和授权,认证其实就是登录,用户登录时会进行账号密码的校验,校验成功后会,会把session存入redis中。授权指的是用户是否拥有访问后端资源的权限,每个新用户在创建后都会分配角色,角色其实就是一个权限集合,这里的权限可以理解为访问后端一个个接口(资源)的权限。


​ 这里权限设计的非常灵活,细粒度到按钮级别,比如课程菜单的新增、删除、修改、查询动作,学生可能只有课程的查询权限,无法新增和修改课程,即使通过接口直接访问后端的修改或者新增接口,后端也会返回授权失败错误,因为后端每个需要权限的接口都打了权限标识,只有拥有资源权限用户才能访问。


### 日志方案


​ 日志采用lombok注解+slf4j+log4j2的实现方案,基于profile实现了多环境的日志配置,因为不同环境的日志打印策略是不一样,比如开发环境我可能需要打印到console控制台,需要debug级别的日志以便于本地开发调试,测试环境可能就需要打印到日志文件里,线上环境可能需要打印到文件的同时将日志发送到kafka然后收集到es中,这样当线上部署了多台机器后我们查日志不用一台一台机器去查日志了,因为都收集到es了,我们只需要登录kibana去搜索,这样就非常方便。这里说到的kafka+es+kibana这样一套日志解决方案也是目前互联网公司比较常用的一套解决方案。如果你动手能力够强,你可以本地搭一套kafka、es、kibana,然后只需要在配置文件中加入几行配置就实现了这么一套企业级的日志解决方案(默认是输出到日志文件)。


下面是部分关键配置,如果要配置kafka,只需要在标签中配置配置即可



```
xml复制代码 <?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" xmlns:xi="http://www.w3.org/2001/XInclude">
<Properties>
<Property name="LOG_FILE">system.log</Property>
<Property name="LOG_PATH">./logs</Property>
<Property name="PID">????</Property>
<Property name="LOG_EXCEPTION_CONVERSION_WORD">%xwEx</Property>
<Property name="LOG_LEVEL_PATTERN">%5p</Property>
<Property name="LOG_DATE_FORMAT_PATTERN">yyyy-MM-dd HH:mm:ss.SSS</Property>
<Property name="CONSOLE_LOG_PATTERN">%clr{%d{${LOG_DATE_FORMAT_PATTERN}}}{faint} %clr{${LOG_LEVEL_PATTERN}} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}
</Property>
<Property name="FILE_LOG_PATTERN">%d{${LOG_DATE_FORMAT_PATTERN}} ${LOG_LEVEL_PATTERN} ${sys:PID} --- [%t] %-40.40c{1.}:%L : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}
</Property>
</Properties>
<Appenders>
<xi:include href="log4j2/file-appender.xml"/>
</Appenders>
<Loggers>
<logger name="com.senior.book" level="info"/>
<Root level="info">
<AppenderRef ref="FileAppender"/>
</Root>
</Loggers>
</Configuration>

```

### 服务监控


​ 服务监控基于 Actuator + Prometheus + Grafana 实现,代码侵入很小,只需要在pom中加入依赖。数据大盘Dashboard可以自己设置,也可以去Dashboard市场下载你想要的模板,总之,这块完全是看动手能力,大家自己玩吧

1
2
3
4
5
6
7
8
9
xml复制代码		<!--服务监控-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

image-20201226110228490

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

在nodejs中创建cluster 简介 cluster集群

发表于 2021-01-31

简介

在前面的文章中,我们讲到了可以通过worker_threads来创建新的线程,可以使用child_process来创建新的子进程。本文将会介绍如何创建nodejs的集群cluster。

cluster集群

我们知道,nodejs的event loop或者说事件响应处理器是单线程的,但是现在的CPU基本上都是多核的,为了充分利用现代CPU多核的特性,我们可以创建cluster,从而使多个子进程来共享同一个服务器端口。

也就是说,通过cluster,我们可以使用多个子进程来服务处理同一个端口的请求。

先看一个简单的http server中使用cluster的例子:

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
js复制代码const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
console.log(`主进程 ${process.pid} 正在运行`);

// 衍生工作进程。
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}

cluster.on('exit', (worker, code, signal) => {
console.log(`工作进程 ${worker.process.pid} 已退出`);
});
} else {
// 工作进程可以共享任何 TCP 连接。
// 在本例子中,共享的是 HTTP 服务器。
http.createServer((req, res) => {
res.writeHead(200);
res.end('你好世界\n');
}).listen(8000);

console.log(`工作进程 ${process.pid} 已启动`);
}

cluster详解

cluster模块源自于lib/cluster.js,我们可以通过cluster.fork()来创建子工作进程,用来处理主进程的请求。

cluster中的event

cluster继承自events.EventEmitter,所以cluster可以发送和接收event。

cluster支持7中event,分别是disconnect,exit,fork,listening,message,online和setup。

在讲解disconnect之前,我们先介绍一个概念叫做IPC,IPC的全称是Inter-Process Communication,也就是进程间通信。

IPC主要用来进行主进程和子进程之间的通信。一个工作进程在创建后会自动连接到它的主进程。 当 ‘disconnect’ 事件被触发时才会断开连接。

触发disconnect事情的原因有很多,可以是主动调用worker.disconnect(),也可以是工作进程退出或者被kill掉。

1
2
3
js复制代码cluster.on('disconnect', (worker) => {
console.log(`工作进程 #${worker.id} 已断开连接`);
});

exit事件会在任何一个工作进程关闭的时候触发。一般用来监测cluster中某一个进程是否异常退出,如果退出的话使用cluster.fork创建新的进程,以保证有足够多的进程来处理请求。

1
2
3
4
5
js复制代码cluster.on('exit', (worker, code, signal) => {
console.log('工作进程 %d 关闭 (%s). 重启中...',
worker.process.pid, signal || code);
cluster.fork();
});

fork事件会在调用cluster.fork方法的时候被触发。

1
2
3
4
5
6
7
8
js复制代码const timeouts = [];
function errorMsg() {
console.error('连接出错');
}

cluster.on('fork', (worker) => {
timeouts[worker.id] = setTimeout(errorMsg, 2000);
});

主进程和工作进程的listening事件都会在工作进程调用listen方法的时候触发。

1
2
3
4
js复制代码cluster.on('listening', (worker, address) => {
console.log(
`工作进程已连接到 ${address.address}:${address.port}`);
});

其中worker代表的是工作线程,而address中包含三个属性:address、 port 和 addressType。 其中addressType有四个可选值:

  • 4 (TCPv4)
  • 6 (TCPv6)
  • -1 (Unix 域 socket)
  • ‘udp4’ or ‘udp6’ (UDP v4 或 v6)

message事件会在主进程收到子进程发送的消息时候触发。

当主进程生成工作进程时会触发fork,当工作进程运行时会触发online。

setupMaster方法被调用的时候,会触发setup事件。

cluster中的方法

cluster中三个方法,分别是disconnect,fork和setupMaster。

1
js复制代码cluster.disconnect([callback])

调用cluster的disconnect方法,实际上会在cluster中的每个worker中调用disconnect方法。从而断开worker和主进程的连接。

当所有的worker都断开连接之后,会执行callback。

1
js复制代码cluster.fork([env])

fork方法,会从主进程中创建新的子进程。其中env是要添加到进程环境变量的键值对。

fork将会返回一个cluster.Worker对象,代表工作进程。

最后一个方法是setupMaster:

1
js复制代码cluster.setupMaster([settings])

默认情况下,cluster通过fork方法来创建子进程,但是我们可以通过setupMaster来改变这个行为。通过设置settings变量,我们可以改变后面fork子进程的行为。

我们看一个setupMaster的例子:

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码const cluster = require('cluster');
cluster.setupMaster({
exec: 'worker.js',
args: ['--use', 'https'],
silent: true
});
cluster.fork(); // https 工作进程
cluster.setupMaster({
exec: 'worker.js',
args: ['--use', 'http']
});
cluster.fork(); // http 工作进程

cluster中的属性

通过cluster对象,我们可以通过isMaster和isWorker来判断进程是否主进程。

可以通过worker来获取当前工作进程对象的引用:

1
2
3
4
5
6
7
8
9
js复制代码const cluster = require('cluster');

if (cluster.isMaster) {
console.log('这是主进程');
cluster.fork();
cluster.fork();
} else if (cluster.isWorker) {
console.log(`这是工作进程 #${cluster.worker.id}`);
}

可以通过workers来遍历活跃的工作进程对象:

1
2
3
4
5
6
7
8
9
js复制代码// 遍历所有工作进程。
function eachWorker(callback) {
for (const id in cluster.workers) {
callback(cluster.workers[id]);
}
}
eachWorker((worker) => {
worker.send('通知所有工作进程');
});

每个worker都有一个id编号,用来定位该worker。

cluster中的worker

worker类中包含了关于工作进程的所有的公共的信息和方法。cluster.fork出来的就是worker对象。

worker的事件和cluster的很类似,支持6个事件:disconnect,error,exit,listening,message和online。

worker中包含3个属性,分别是:id,process和exitedAfterDisconnect。

其中id是worker的唯一标记。

worker中的process,实际上是ChildProcess对象,是通过child_process.fork()来创建出来的。

因为在worker中,process属于全局变量,所以我们可以直接在worker中使用process来进行发送消息。

exitedAfterDisconnect表示如果工作进程由于 .kill() 或 .disconnect() 而退出的话,值就是true。如果是以其他方式退出的话,返回值就是false。如果工作进程尚未退出,则为 undefined。

我们可以通过worker.exitedAfterDisconnect 来区分是主动退出还是被动退出,主进程可以根据这个值决定是否重新生成工作进程。

1
2
3
4
5
6
7
8
js复制代码cluster.on('exit', (worker, code, signal) => {
if (worker.exitedAfterDisconnect === true) {
console.log('这是自发退出,无需担心');
}
});

// 杀死工作进程。
worker.kill();

worker还支持6个方法,分别是:send,kill,destroy,disconnect,isConnected,isDead。

这里我们主要讲解一下send方法来发送消息:

1
js复制代码worker.send(message[, sendHandle[, options]][, callback])

可以看到send方法和child_process中的send方法参数其实是很类似的。而本质上,worker.send在主进程中,这会发送消息给特定的工作进程。 相当于 ChildProcess.send()。在工作进程中,这会发送消息给主进程。 相当于 process.send()。

1
2
3
4
5
6
7
8
9
js复制代码if (cluster.isMaster) {
const worker = cluster.fork();
worker.send('你好');

} else if (cluster.isWorker) {
process.on('message', (msg) => {
process.send(msg);
});
}

在上面的例子中,如果是在主进程中,那么可以使用worker.send来发送消息。而在子进程中,则可以使用worker中的全局变量process来发送消息。

总结

使用cluster可以充分使用多核CPU的优势,希望大家在实际的项目中应用起来。

本文作者:flydean程序那些事

本文链接:www.flydean.com/nodejs-clus…

本文来源:flydean的博客

欢迎关注我的公众号:「程序那些事」最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

手写一个简单版的线程池 为什么要使用线程 实战:手写简易线程

发表于 2021-01-31

有些人可能对线程池比较陌生,并且更不熟悉线程池的工作原理。所以他们在使用线程的时候,多数情况下都是new Thread来实现多线程。但是,往往良好的多线程设计大多都是使用线程池来实现的。

为什么要使用线程

  1. 降低资源的消耗。降低线程创建和销毁的资源消耗。
  2. 提高响应速度:线程的创建时间为T1,执行时间T2,销毁时间T3,免去T1和T3的时间
  3. 提高线程的可管理性

下图所示为线程池的实现原理:调用方不断向线程池中提交任务;线程池中有一组线程,不断地从队列中取任务,这是一个典型的生产者-消费者模型。

要实现一个线程池,有几个问题需要考虑:

  1. 队列设置多长?如果是无界的,调用方不断往队列中方任务,可能导致内存耗尽。如果是有界的,当队列满了之后,调用方如何处理?
  2. 线程池中的线程个数是固定的,还是动态变化的?
  3. 每次提交新任务,是放入队列?还是开新线程
  4. 当没有任务的时候,线程是睡眠一小段时间?还是进入阻塞?如果进入阻塞,如何唤醒?

针对问题4,有3种做法:

  1. 不使用阻塞队列,只使用一般的线程安全的队列,也无阻塞/唤醒机制。当队列为空时,线程池中的线程只能睡眠一会儿,然后醒来去看队列中有没有新任务到来,如此不断轮询。
  2. 不使用阻塞队列,但在队列外部,线程池内部实现了阻塞/唤醒机制
  3. 使用阻塞队列

很显然,做法3最完善,既避免了线程池内部自己实现阻塞/唤醒机制的麻烦,也避免了做法1的睡眠/轮询带来的资源消耗和延迟。

现在来带大家手写一个简单的线程池,让大家更加理解线程池的工作原理

实战:手写简易线程池

根据上图可以知道,实现线程池需要一个阻塞队列+存放线程的容器

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
java复制代码/**
* Five在努力
* 自定义线程池
*/
public class ThreadPool {

/** 默认线程池中的线程的数量 */
private static final int WORK_NUM = 5;

/** 默认处理任务的数量 */
private static final int TASK_NUM = 100;

/** 存放任务 */
private final BlockingQueue<Runnable> taskQueue;

private final Set<WorkThread> workThreads;//保存线程的集合

private int workNumber;//线程数量

private int taskNumber;//任务数量

public ThreadPool(){
this(WORK_NUM , TASK_NUM);
}

public ThreadPool(int workNumber , int taskNumber) {
if (taskNumber<=0){
taskNumber = TASK_NUM;
}
if (workNumber<=0){
workNumber = WORK_NUM;
}
this.taskQueue = new ArrayBlockingQueue<Runnable>(taskNumber);
this.workNumber = workNumber;
this.taskNumber = taskNumber;

workThreads = new HashSet<>();

//工作线程准备好了
//启动一定数量的线程数,从队列中获取任务处理
for (int i=0;i<workNumber;i++) {
WorkThread workThread = new WorkThread("thead_"+i);
workThread.start();
workThreads.add(workThread);
}
}

/**
* 线程池执行任务的方法,其实就是往BlockingQueue中添加元素
* @param task
*/
public void execute(Runnable task) {
try {
taskQueue.put(task);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}


/**
* 销毁线程池
*/
public void destroy(){
System.out.println("ready close pool...");
for (WorkThread workThread : workThreads) {
workThread.stopWorker();
workThread = null;//help gc
}
workThreads.clear();
}

/** 内部类,工作线程的实现 */
private class WorkThread extends Thread{
public WorkThread(String name){
super();
setName(name);
}
@Override
public void run() {
while (!interrupted()) {
try {
Runnable runnable = taskQueue.take();//获取任务
if (runnable !=null) {
System.out.println(getName()+" ready execute:"+runnable.toString());
runnable.run();//执行任务
}
runnable = null;//help gc
} catch (Exception e) {
interrupt();
e.printStackTrace();
}
}
}

public void stopWorker(){
interrupt();
}
}
}

上面代码定义了默认的线程数量和默认处理任务数量,同时用户也可以自定义线程数量和处理任务数量。用BlockingQueue阻塞队列来存放任务。用set来存放工作线程,set的好处就不用多说了。懂的都懂

构造方法中new对象的时候,循环启动线程,并把线程放入set中。WorkThread实现Thread,run方法实现也很简单,因为有一个stop方法,所以这里需要while判断,之后从taskQueue队列中,获取任务。如何获取不到就阻塞,获取到的话runnable.run();就执行任务,之后把任务变成null

销毁线程只需要遍历set,把每个线程停止,并且变为null就行了

执行线程任务execute,只需要从往阻塞队列中添加任务就行了

测试一下:

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
java复制代码public class TestMySelfThreadPool {

private static final int TASK_NUM = 50;//任务的个数

public static void main(String[] args) {
ThreadPool myPool = new ThreadPool(3,50);
for (int i=0;i<TASK_NUM;i++) {
myPool.execute(new MyTask("task_"+i));
}

}

static class MyTask implements Runnable{

private String name;
public MyTask(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}


@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("task :"+name+" end...");

}

@Override
public String toString() {
// TODO Auto-generated method stub
return "name = "+name;
}
}
}


结果ok。没什么问题

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…725726727…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%