【如何实现一个简单的RPC框架】系列文章:
【远程调用框架】如何实现一个简单的RPC框架(一)想法与设计
【远程调用框架】如何实现一个简单的RPC框架(二)实现与使用
【远程调用框架】如何实现一个简单的RPC框架(三)优化一:利用动态代理改变用户服务调用方式
【远程调用框架】如何实现一个简单的RPC框架(四)优化二:改变底层通信框架
【远程调用框架】如何实现一个简单的RPC框架(五)优化三:软负载中心设计与实现
第一个优化以及第二个优化修改后的工程代码可下载资源 如何实现一个简单的RPC框架
2、优化二: 改变底层通信框架
简单socket通信BIO方式-》-》NIO方式-》使用netty服务框架
关于这部分,可以提前阅读下博客《Java NIO BIO AIO总结》
2.1 目的
问题描述:在目前的服务框架版本中,服务发布端和服务调用端采用的IO通信模式为BIO,即使用最基础的Java Socket编程的方式。看过我们之前实现介绍部分的读者都知道,服务端一直在监听请求,每当有一个请求发来,则会创建一个新的线程去处理该请求,如下代码:
while (true){
Socket socket = serverSocket.accept();
new Thread(new ServerProcessThread(socket)).start();//开启新的线程进行连接请求的处理
}
而ServerProcessThread线程完成了服务的调用及结果的返回工作,这样的方法,有以下两个弊端:
- (1)对于每一个socket连接都创建一个线程去维护,当连接逐渐增多的情况下,创建的线程数随之增加,对虚拟机造成一定压力。
(2)这种方式为阻塞式IO,即数据的读写是阻塞的,在没有有效可读/可写数据的情况下,线程会一直阻塞,造成资源的浪费。
因此为了解决上述两个弊端,我们改变这种IO模式。step1.使用selector+channel+buffer实现NIO模式(参考博客《Java NIO BIO AIO总结》)
NIO的模式有两个特点:
(1)不用对所有连接都创建新的线程去维护,selector线程可以管理多个数据通道;
(2)IO数据读写是非阻塞的,只有当出现有效读写数据时才会出发相应的事件进行读写,节约资源。- step2.使用netty/mina的框架来实现。
2.2 实现
2.2.1 NIO模式
关于NIO模式的基本客户端与服务端的实现代码在博客《Java NIO BIO AIO总结》中已经进行了介绍。这里我对LCRPC框架代码的改造即利用该博客中的代码。仅作为NIO通信模式的使用示例,因为还有好多可以修改的地方。
- (1)服务发布端的代码修改
这里我们为接口IProviderService添加一个方法:startListenByNIO,该方法实现采用NIO的模式进行服务的监听,与之对应的是该接口中的startListen方法使用BIO的模式进行服务的监听,即我们第一版本中的内容。startListenByNIO方法的实现代码如下:
@Override
public boolean startLisetenByNIO() {
new Thread(new NIOServerThread()).start();
return true;
}
该方法开启新的线程,采用NIO的模式进行服务的监听。线程类NIOServerThread的代码与博客《Java NIO BIO AIO总结》介绍的一致,只是read事件的触发方法代码有所改动。该类的代码如下:
public class NIOServerThread extends NIOBase implements Runnable{
@Override
public void run() {
try {
initSelector();//初始化通道管理器Selector
initServer(Constant.IP,Constant.PORT);//初始化ServerSocketChannel,开启监听
listen();//轮询处理Selector选中的事件
} catch (IOException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 初始化 该线程中的通道管理器Selector
*/
public void initSelector() throws IOException {
this.selector = Selector.open();
}
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则循环处理
* 这里主要监听连接事件以及读事件
*/
public void listen() throws IOException, ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
System.out.println("监听成功,可开始进行服务注册!");
//轮询访问select
while(true){
//当注册的事件到达时,方法返回;否则将一直阻塞
selector.select();
//获得selector中选中的项的迭代器,选中的项为注册的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
//循环处理注册事件
/**
* 一共有四种事件:
* 1. 服务端接收客户端连接事件: SelectionKey.OP_ACCEPT
* 2. 客户端连接服务端事件: SelectionKey.OP_CONNECT
* 3. 读事件: SelectionKey.OP_READ
* 4. 写事件: SelectionKey.OP_WRITE
*/
while(iterator.hasNext()){
SelectionKey key = iterator.next();
//手动删除已选的key,以防重复处理
iterator.remove();
//判断事件性质
if (key.isAcceptable()){//服务端接收客户端连接事件
accept(key);
}else if (key.isReadable()){//读事件
read(key);
}
}
}
}
/**
* 获得一个ServerSocket通道,并通过port对其进行初始化
* @param port 监听的端口号
*/
private void initServer(String ip,int port) throws IOException {
//step1. 获得一个ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//step2. 初始化工作
serverSocketChannel.configureBlocking(false);//设置通道为非阻塞
serverSocketChannel.socket().bind(new InetSocketAddress(ip,port));
//step3. 将该channel注册到Selector上,并为该通道注册SelectionKey.OP_ACCEPT事件
//这样一来,当有"服务端接收客户端连接"事件到达时,selector.select()方法会返回,否则将一直阻塞
serverSocketChannel.register(this.selector,SelectionKey.OP_ACCEPT);
}
/**
* 当监听到服务端接收客户端连接事件后的处理函数
* @param key 事件key,可以从key中获取channel,完成事件的处理
*/
public void accept(SelectionKey key) throws IOException {
//step1. 获取serverSocketChannel
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
//step2. 获得和客户端连接的socketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);//设置为非阻塞
//step3. 注册该socketChannel
socketChannel.register(selector,SelectionKey.OP_READ);//为了接收客户端的消息,注册读事件
}
public void read(SelectionKey key) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
byte[] result = getReadData(key);
if (result == null) return;
SocketChannel socketChannel = (SocketChannel) key.channel();
LCRPCRequestDO requestDO = (LCRPCRequestDO) ObjectAndByteUtil.toObject(result);
IProviderService providerService = new ProviderServiceImpl();
//将结果写回
socketChannel.write(ByteBuffer.wrap(ObjectAndByteUtil.toByteArray(providerService.getFuncCalldata(requestDO))));
// socketChannel.close();//关闭
}
}
类NIOBase为基类。代码如下:
public class NIOBase {
// 线程中的通道管理器
public Selector selector;
/**
* 初始化 该线程中的通道管理器Selector
*/
public void initSelector() throws IOException {
this.selector = Selector.open();
}
public byte[] getReadData(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
int len = socketChannel.read(byteBuffer);
if (len == -1){
socketChannel.close();
return null;//说明连接已经断开
}
int lenth = 0;
List<byte[]> list = new ArrayList<>();
while (len > 0){
lenth += len;
byteBuffer.flip();
byte[] arr = new byte[len];
byteBuffer.get(arr,0,len);
list.add(arr);
byteBuffer.clear();
len = socketChannel.read(byteBuffer);
}
byte[] result = new byte[lenth];
int l = 0;
for (int i = 0;i<list.size();i++){
for (int j = 0;j<list.get(i).length;j++){
result[l + j] = list.get(i)[j];
}
l += list.get(i).length;
}
return result;
}
}
getReadData方法读取客户端发送的全部数据。利用帮助类ObjectAndByteUtil对客户端发送的数据进行序列化为reqeust对象。同时为接口IProviderService添加方法getFuncCallData,利用request对象调用相应服务方法,得到方法的返回值,反序列化后发送给客户端,该方法的代码与第一版本一致。
帮助类ObjectAndByteUtil负责利用反/序列化技术进行字节数组与对象之间的转化,代码如下:
package whu.edu.lcrpc.util;
import java.io.*;
/**
* Created by apple on 17/3/30.
*/
public class ObjectAndByteUtil {
/**
* 对象转数组
* @param obj
* @return
*/
public static byte[] toByteArray (Object obj) {
byte[] bytes = null;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
oos.flush();
bytes = bos.toByteArray ();
oos.close();
bos.close();
} catch (IOException ex) {
ex.printStackTrace();
}
return bytes;
}
/**
* 数组转对象
* @param bytes
* @return
*/
public static Object toObject (byte[] bytes) {
Object obj = null;
try {
ByteArrayInputStream bis = new ByteArrayInputStream (bytes);
ObjectInputStream ois = new ObjectInputStream (bis);
obj = ois.readObject();
ois.close();
bis.close();
} catch (IOException ex) {
ex.printStackTrace();
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
return obj;
}
}
为了采用NIO的方法开启服务发布端的服务监听,我们修改LCRPCProviderImpl类中对startListen函数的调用改为方法startListenByNIO,使得服务端采用NIO的方式发布服务。
- (2)客户端代码修改
客户端的代码大部分与博客《》中的一致,只不过还是在read事件出发函数中,有所修改,主要流程就是读取到服务端返回的数据后进行序列化,代码如下:
public Object read(SelectionKey key) throws IOException {
//step1. 得到事件发生的通道
byte[] result = getReadData(key);
if (result == null) return null;
Object object = ObjectAndByteUtil.toObject(result);
return object;
}
我们为接口IConsumerService添加方法sendDataByNIO,采用NIO的方式将服务调用端的请求信息序列化后发送给服务端,该函数代码如下:
public Object sendDataByNIO(String ip, LCRPCRequestDO requestDO) throws IOException, ClassNotFoundException {
NIOClient nioClient = new NIOClient(requestDO,ip);
return nioClient.run();
}
类NIOClient代码如下,其中run函数开启轮询,当所注册事件发生时,触发相应的方法。并在read事件触发后结束轮训。
public class NIOClient extends NIOBase{
private LCRPCRequestDO requestDO;//客户端对应的请求DO,发送给服务端
private String ip;
public NIOClient(LCRPCRequestDO requestDO,String ip){
this.requestDO = requestDO;
this.ip = ip;
}
public Object run() {
try {
initSelector();//初始化通道管理器
initClient(ip,Constant.PORT);//初始化客户端连接scoketChannel
return listen();//开始轮询处理事件
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public Object listen() throws IOException {
//轮询访问select
boolean flag = true;
Object result = null;
while(flag){
//当注册的事件到达时,方法返回;否则将一直阻塞
selector.select();
//获得selector中选中的项的迭代器,选中的项为注册的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
//循环处理注册事件
/**
* 一共有四种事件:
* 1. 服务端接收客户端连接事件: SelectionKey.OP_ACCEPT
* 2. 客户端连接服务端事件: SelectionKey.OP_CONNECT
* 3. 读事件: SelectionKey.OP_READ
* 4. 写事件: SelectionKey.OP_WRITE
*/
while(iterator.hasNext()){
SelectionKey key = iterator.next();
//手动删除已选的key,以防重复处理
iterator.remove();
//判断事件性质
if (key.isReadable()){//读事件
result = read(key);
flag = false;
break;
}else if (key.isConnectable()) {//客户端连接事件
connect(key);
}
}
}
return result;
}
/**
* 获得一个SocketChannel,并对该channel做一些初始化工作,并注册到
* @param ip
* @param port
*/
public void initClient(String ip,int port) throws IOException {
//step1. 获得一个SocketChannel
SocketChannel socketChannel = SocketChannel.open();
//step2. 初始化该channel
socketChannel.configureBlocking(false);//设置通道为非阻塞
//step3. 客户端连接服务器,其实方法执行并没有实现连接,需要再listen()方法中调用channel.finishConnect()方法才能完成连接
socketChannel.connect(new InetSocketAddress(ip,port));
//step4. 注册该channel到selector中,并为该通道注册SelectionKey.OP_CONNECT事件和SelectionKey.OP_READ事件
socketChannel.register(this.selector,SelectionKey.OP_CONNECT|SelectionKey.OP_READ);
}
/**
* 当监听到客户端连接事件后的处理函数
* @param key 事件key,可以从key中获取channel,完成事件的处理
*/
public void connect(SelectionKey key) throws IOException {
//step1. 获取事件中的channel
SocketChannel socketChannel = (SocketChannel) key.channel();
//step2. 如果正在连接,则完成连接
if (socketChannel.isConnectionPending()){
socketChannel.finishConnect();
}
socketChannel.configureBlocking(false);//将连接设置为非阻塞
//step3. 连接后,可以给服务端发送消息
socketChannel.write(ByteBuffer.wrap(ObjectAndByteUtil.toByteArray(requestDO)));
}
public Object read(SelectionKey key) throws IOException {
//step1. 得到事件发生的通道
byte[] result = getReadData(key);
if (result == null) return null;
Object object = ObjectAndByteUtil.toObject(result);
return object;
}
}
为了使得客户端采用NIO方式进行通讯,我们修改MyInvokeHandler类:
// result = consumerService.sendData(serviceAddress,requestDO);//采用BIO的方式
result = consumerService.sendDataByNIO(serviceAddress,requestDO);//采用NIO的方式
至此,NIO通信模式代码修改完毕。在测试的过程中,遇到了一个问题,就是在服务调用端发出一个服务调用请求后,服务发布端一直在触发read事件,查阅资料后,了解到这种NIO的实现方式中,客户端或者服务端其中一方将连接关闭后,会一直触发另一方的read事件,这时read会回传-1,若没有即使正确处理断线(关闭channel),read事件会一直触发,因此在getData函数读取数据时,添加如下代码:
if (len == -1){
socketChannel.close();
return null;//说明连接已经断开
}
至此,问题得以解决。
服务注册查找中心以及服务端客户端的代码都不需要改变,分别运行后,得到与第一版相同的结果。(由于服务端我们采用一个selector管理所有channel,并且没有开启新的线程去处理数据,因此客户端会以同步的方式得到四次服务调用结果)
2.2.2 netty/mina
目前为止我们的代码中,通信部分采用了NIO和BIO两种模式。BIO模式采用socket编程实现,NIO部分采用selector channel buffer编程实现。但是无论哪一种,都只是简单的帮助我们了解两种通信模式的基本概念,以及如何用最简单得编程方式实现。我们在代码中,也有非常多的异常,网络等情况没有考虑,在实际生产中,也绝不会使用这种最基本最底层的编程方式来完成远程得通信。因此,我们这里引入Netty开源框架来实现通信。他帮助我们考虑了多种状况,使得我们以简单的代码完成高质量的远程通信,专注于其他业务逻辑等的实现。
在分布式应用系统开发中,服务化的应用之间进行远程通信时使用。Netty是在Java NIO的基础上封装的用于客户端服务端网络应用程序开发的框架,帮助用户考虑在分布式、高并发、高性能开发中遇到的多种状况,使得用户使用更容易的网络编程接口完成网络通信,专注于其他业务逻辑的开发。
(1)关于Netty
(以下内容摘自知乎的帖子《通俗地讲,Netty 能做什么?》)
netty是一套在java NIO的基础上封装的便于用户开发网络应用程序的api.
Netty是什么?
1)本质:JBoss做的一个Jar包
2)目的:快速开发高性能、高可靠性的网络服务器和客户端程序
3)优点:提供异步的、事件驱动的网络应用程序框架和工具
通俗的说:一个好使的处理Socket的东东
(2)为什么选择netty
以下内容摘抄自《Netty权威指南》
在上述优化中,我们使用JDK为我们提供的NIO的类库来修改LCRPC框架的远程通信方式。以下总结了不选择Java原声NIO编程的原因:
由于上述原因,在大多数场景下,不建议大家直接食用JDK的NIO类库,除非精通NIO编程或者有特殊的需求。在绝大多数的业务场景中,我们可以使用NIO框架Netty来进行NIO编程,他既可以作为客户端也可以作为服务端,同时也支持UDP和异步文件传输,功能非常强大。
以下总结了为什么选择Netty作为基础通信框架:
(3)LCRPC服务框架优化:使用netty替换底层网络通讯
与NIO的修改方式大致相同
增加四个netty服务端与客户端的类;
netty服务端开启监听的类NettyServer:
package whu.edu.lcrpc.io.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import whu.edu.lcrpc.util.Constant;
/**
* Created by apple on 17/4/10.
*/
public class NettyServer {
public void bind() throws InterruptedException {
//配置服务端的NIO的线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup wokerGroup = new NioEventLoopGroup();
//创建服务启动的辅助类
try{
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup,wokerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,1024)
.childHandler(new ChildChannelHandler());
//绑定端口,同步等待成功
ChannelFuture f = b.bind(Constant.PORT).sync();
System.out.println("已经开始监听,可以注册服务了");
//等待服务端监听端口关闭
f.channel().closeFuture().sync();
}finally {
//优雅退出,释放线程池资源
bossGroup.shutdownGracefully();
wokerGroup.shutdownGracefully();
}
}
private class ChildChannelHandler extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new NettyServerHandler());
}
}
}
netty服务端hanlder类NettyServerhandler:
package whu.edu.lcrpc.io.netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import whu.edu.lcrpc.entity.LCRPCRequestDO;
import whu.edu.lcrpc.service.IProviderService;
import whu.edu.lcrpc.service.impl.ProviderServiceImpl;
import whu.edu.lcrpc.util.ObjectAndByteUtil;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Date;
/**
* Created by apple on 17/4/10.
*/
public class NettyServerHandler extends ChannelHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf)msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
if (req == null) return;
LCRPCRequestDO requestDO = (LCRPCRequestDO) ObjectAndByteUtil.toObject(req);
IProviderService providerService = new ProviderServiceImpl();
ByteBuf resp = Unpooled.copiedBuffer(ObjectAndByteUtil.toByteArray(providerService.getFuncCalldata(requestDO)));
ctx.write(resp);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
ctx.close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
netty客户端连接类NettyClient:
package whu.edu.lcrpc.io.netty;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import whu.edu.lcrpc.util.Constant;
import whu.edu.lcrpc.util.ObjectAndByteUtil;
import java.io.UnsupportedEncodingException;
/**
* Created by apple on 17/4/10.
*/
public class NettyClient {
private Object reqObj;
private String ip;
public NettyClient(Object reqObj, String ip){
this.reqObj = reqObj;
this.ip = ip;
}
public Object connect() throws InterruptedException, UnsupportedEncodingException {
//配置客户端NIO线程组
EventLoopGroup group = new NioEventLoopGroup();
try{
Bootstrap b = new Bootstrap();
byte[] req = ObjectAndByteUtil.toByteArray(reqObj);
NettyClientHandler nettyClientHandler = new NettyClientHandler(req);
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(nettyClientHandler);
}
});
//发起异步连接操作
ChannelFuture f = b.connect(ip, Constant.PORT).sync();
//等待客户端链路关闭
f.channel().closeFuture().sync();
//拿到异步请求结果,返回
Object responseObj = ObjectAndByteUtil.toObject(nettyClientHandler.response);
return responseObj;
}finally {
//优雅退出,释放NIO线程组
group.shutdownGracefully();
}
}
}
netty客户端handler类NettyClientHandler:
package whu.edu.lcrpc.io.netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
/**
* Created by apple on 17/4/11.
*/
public class NettyClientHandler extends ChannelHandlerAdapter {
private ByteBuf firstMessage;
public byte[] response;
public NettyClientHandler(byte[] req){
//将请求写入缓冲区
firstMessage = Unpooled.buffer(req.length);
firstMessage.writeBytes(req);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(firstMessage);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
response = new byte[buf.readableBytes()];
buf.readBytes(response);
ctx.close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
而后修改LCRPC中原本的代码,采用netty来进行远程通信。
首先在接口IConsumerService中增加函数sendDataByNetty,该函数采用netty的方式向服务发布端发送数据。函数实现如下:
@Override
public Object sendDataByNetty(String ip, LCRPCRequestDO requestDO) throws IOException, ClassNotFoundException, InterruptedException {
NettyClient nettyClient = new NettyClient(requestDO,ip);
return nettyClient.connect();
}
而后在接口IProviderService增加函数startListenByNetty,该函数采用netty的方式开启服务监听。
@Override
public boolean startListenByNetty() {
new Thread(()->{
NettyServer nettyServer = new NettyServer();
try {
nettyServer.bind();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
return true;
}
然后在代理handler类MyInvocationHandler中,
修改
result = consumerService.sendDataByNIO(serviceAddress,requestDO);//采用NIO的方式
为
result = consumerService.sendDataByNetty(serviceAddress,requestDO); //采用netty的方式
采用netty的方式调用服务。
并且在类LCRPCProviderImpl中使用方法startListenByNetty开启服务的监听。
客户端以及服务端的测试工程代码均不需要改变,进行测试后,输出结果不变。
需要注意的是:上述关于Netty的使用没有考虑到TCL粘包/拆包的问题!
3、优化三:服务框架工作日志
这个优化未完待续~