I. 前言

本文档是Janus Reference Documentation

总之,Janus是一个基于Netty自主研发的高性能网关

Release Note

关于Janus的Release Note可以访问[Janus-Release-Note]

II. Janus 概述

1. Janus网关介绍

Janus(汉译作“杰纳斯”或“雅努斯”)是天门神,早晨打开天门,让阳光普照人间,晚上又把天门关上,使黑暗降临大地。他的头部前后各有一副面孔,同时看着两个不同方向,一副看着过去,一副看着未来,因此也称两面神

Janus掌握所有对外接口的进出.

服务网关Janus是Halo体系的一个组成模块,为各业务开发部门开发的业务服务(RESTful、RPC)提供对外统一的,高性能的HTTP网关。
Janus致力于优化API入口的七层协议接入,主要从下面几个方面进行优化:

  • 协议支持:外网HTTPS、HTTP2、HTTP、自定义协议等,自动适配到内网RPC协议和HTTP协议。

  • 接入优化:SSL session复用、SSL 加解密异步化、异地多活、流量管控

  • 安全: 认证验签、验签加固、设备指纹、WAF、防刷、限流、风控

  • 其他: 单元化、轻量数据聚合、A/B test, mock api, 多租户等

2. Janus 基本功能和定位

3. Janus 项目

3.1. Janus Team

Janus Team开发团队信息如下,感谢所有对Janus有贡献的所有人!

github id 角色 github地址 备注

SoftwareKing

PPMC

https://github.com/SoftwareKing

wolfgump

PPMC

https://github.com/wolfgump

tbkk

PPMC

https://github.com/tbkk

3.2. Janus工程

Janus工程主要为3个项目,如下所示:

工程名 说明 github地址

janus

janus网关Server

https://github.com/JanusTeam/Janus

janus-ui

janus管控平台前端

https://github.com/JanusTeam/janus-ui

janus-admin

Janus网关的管控平台后端

暂无

III. janus设计

4. janus filter设计

4.1. Filter设计

责任链模式是实现了类似「流水线」结构的逐级处理,通常是一条链式结构,将「抽象处理者」的不同实现串联起来: 如果当前节点能够处理任务则直接处理掉,如果无法处理则委托给责任链的下一个节点,如此往复直到有节点可以处理这个任务。

Filter链的设计正式采用了这种责任链的设计方式

5. Janus 路由设计

5.2. 示例路由配置

routes:
  - id: 1 #演示Spring Cloud应用模糊匹配并走LB  # http://localhost:8081/janus-admin/admin/menu/manage/allMenu
    filters:
      - name: StripPrefix  #去前缀
        args:
          parts: 1
      - name: AddRequestHeader  #添加Header
        args:
          X-Request-Blue: Blue
          X-Request-Red: red
    protocol: lb://sc
    serviceName: janus-admin
    loadBalancerName: round-robin #random、hash; default is round-robin
    predicates: # and ; if need or add a new route
      - name: PathRegex
        args:
          pattern: ^/janus-admin/.*

  - id: 2 #演示 URL1->URL2 并走LB  #http://localhost:8081/api/getAllMenu
    protocol: lb://sc
    serviceName: janus-admin
    filters:
      - name: PathMapping
        args:
          /api/getAllMenu: "/admin/menu/manage/allMenu"
    predicates: # and ; if need or add a new route
      - name: PathPrecise
        args:
          pattern: /api/getAllMenu

  - id: 3 #用于演示精准匹配,不走LB  http://localhost:8081/admin/menu/manage/allMenu
    protocol: http
    hosts:
      - 127.0.0.1:8084
    predicates: # and ; if need or add a new route
      - name: PathPrecise
        args:
          paths: /admin/menu/manage/allMenu

  - id: 4 #用于模糊匹配,不走LB
    protocol: http
    hosts:
      - 127.0.0.1:8084
    predicates: # and ; if need or add a new route
      - name: PathRegex
        args:
          pattern: ^/janus-admin-test/.*


  - id: 5 #用于演示精准匹配,不走LB  http://localhost:8081/admin1/menu/manage/allMenu3
    filters:
      - name: MockResponse
        args:
          /admin1/menu/manage/allMenu3: "hello word!"
    protocol: http
    hosts:
      - 127.0.0.1:8084
    predicates: # and ; if need or add a new route
      - name: PathPrecise
        args:
          paths: /admin1/menu/manage/allMenu3


  - id: 6 #用于演示精准匹配,不走LB,直接转发到指定的IP和端口   http://localhost:8081/admin2/menu/manage/allMenu2
    filters:
      - name: PathMapping
        args:
          /admin2/menu/manage/allMenu2: "/admin/menu/manage/allMenu"
    protocol: http
    hosts:
      - 127.0.0.1:8084
    predicates: # and ; if need or add a new route
      - name: PathPrecise
        args:
          paths: /admin2/menu/manage/allMenu2

  - id: 7 #根据配置的多个Hosts不通过注册中心进行LB  #http://localhost:8081/admin7/menu/manage/allMenu2
    filters:
      - name: PathMapping
        args:
          /admin7/menu/manage/allMenu2: "/admin/menu/manage/allMenu"
    protocol: lb://hosts
    hosts:
      - 127.0.0.1:8084
      - admin.qingcloud.net:80
    predicates: # and ; if need or add a new route
      - name: PathPrecise
        args:
          paths: /admin7/menu/manage/allMenu2

6. Janus Predicates(断言设计)

7. Janus Netty中的设计

org.xujin.janus.core.netty.client.NettyConnectClient

org.xujin.janus.core.netty.server.NettyServer-→NettyServerHandler

7.1. Janus Server的设计

7.1.1. 网关接受请求的Netty Server

org.xujin.janus.core.netty.NettyServer.java

/**
  * 初始化EventPool 参数
*/
 private void initEventPool() {
        bossGroup = new NioEventLoopGroup(serverConfig.getBossThread(), new ThreadFactory() {
            private AtomicInteger index = new AtomicInteger(0);

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "Janus_Boss_" + index.incrementAndGet());
            }
        });
        workGroup = new NioEventLoopGroup(serverConfig.getWorkThread(), new ThreadFactory() {
            private AtomicInteger index = new AtomicInteger(0);

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "Janus_Work_" + index.incrementAndGet());
            }
        });

 }

 /**
     * 服务开启
     */
 public void start(int port) {
     //TODO 上生产时去掉
     ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.ADVANCED);
     initEventPool();
     ServerBootstrap bootstrap = new ServerBootstrap();
     //绑定线程池
     bootstrap.group(bossGroup, workGroup)
                .channel(NioServerSocketChannel.class) //指定使用的channel
                .option(ChannelOption.SO_REUSEADDR, serverConfig.isReUseAddr())
                .option(ChannelOption.SO_BACKLOG, serverConfig.getBackLog())
                .childOption(ChannelOption.SO_RCVBUF, serverConfig.getRevBuf())
                .childOption(ChannelOption.SO_SNDBUF, serverConfig.getSndBuf())
                .childOption(ChannelOption.TCP_NODELAY, serverConfig.isTcpNoDelay())
                .childOption(ChannelOption.SO_KEEPALIVE, serverConfig.isKeepalive())
                .childHandler(initChildHandler());

      bootstrap.bind(port).addListener((ChannelFutureListener) channelFuture -> {
            if (channelFuture.isSuccess()) {
                changeOnLine(true);
                log.info("服务端启动成功【" + IpUtils.getHost() + ":" + port + "");
            } else {
                log.error("服务端启动失败【" + IpUtils.getHost() + ":" + port + "】,cause:"
                        + channelFuture.cause().getMessage());
                shutdown(port);
            }
        });
  }
  • Netty的Server端,不管是Netty Server端和Client端都有NioEventLoopGroup

  • 因为是Netty Server端,所以Channel类型是NioServerSocketChannel.

  • childHandler(initChildHandler())是基于Pipeline的自定义Handler机制。

  • EventLoopGroup:无论是服务端还是客户端,都必须指定EventLoopGroup。 在上面的代码中, 指定了NioEventLoopGroup,表示一个NIO的EventLoopGroup,不过服务端需要指定两个EventLoopGroup,一个是bossGroup, 用于处理客户端的连接请求;另一个是workerGroup,用于处理与各个客户端连接的I/O操作。

(2)指定Channel的类型。这里是服务端,所以使用了NioServerSocketChannel。

(3)配置自定义的业务处理器Handler。

@SuppressWarnings("rawtypes")
private ChannelInitializer initChildHandler() {
return new ChannelInitializer<SocketChannel>() {
final ThreadPoolExecutor serverHandlerPool = ThreadPoolUtils.makeServerThreadPool(NettyServer.class.getSimpleName());
//网关Server的Http请求入口Handler
final NettyServerHandler nettyServerHandler = new NettyServerHandler(serverHandlerPool);

            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                //initHandler(ch.pipeline(), serverConfig);
                ch.pipeline().addLast(new IdleStateHandler(180, 0, 0));
                ch.pipeline().addLast("logging", new LoggingHandler(LogLevel.DEBUG));
                // We want to allow longer request lines, headers, and chunks
                // respectively.
                ch.pipeline().addLast("decoder", new HttpRequestDecoder(
                        serverConfig.getMaxInitialLineLength(),
                        serverConfig.getMaxHeaderSize(),
                        serverConfig.getMaxChunkSize()));
                //ch.pipeline().addLast("inflater", new HttpContentDecompressor());
                ch.pipeline().addLast("aggregator", new HttpObjectAggregator(serverConfig.getMaxHttpLength()));
                //响应解码器
                ch.pipeline().addLast("http-encoder", new HttpResponseEncoder());
                //目的是支持异步大文件传输()
                ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
                //网关Server的Netty HTTP连接处理类
                ch.pipeline().addLast("process", nettyServerHandler);
            }
        };
    }
7.1.2. 网关转发调用内部源服务的Client
  1. 网关内部转发的客户端Java类 org.xujin.janus.core.netty.client.NettyConnectClient.java

@Slf4j
public class NettyConnectClient extends ConnectClient {

    public  NioEventLoopGroup clientGroup = new NioEventLoopGroup(0,
            new DefaultThreadFactory("Janus-client-Worker", true));

    private Channel channel;

    @Override
    public void init(String address) throws Exception {

        Object[] array = IpUtils.parseIpPort(address);
        String host = (String) array[0];
        int port = (int) array[1];

        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(clientGroup)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel channel) throws Exception {
                        channel.pipeline()
                                .addLast(new IdleStateHandler(0, 0, 60, TimeUnit.SECONDS))
//                                .addLast(new HttpClientCodec())
//                                .addLast(new HttpObjectAggregator(65536))
                                //解压
                                //.addLast(new HttpContentDecompressor())
                                .addLast(new HttpClientCodec(NettyConstants.MAX_INITIAL_LINE_LENGTH,NettyConstants.MAX_HEADER_SIZE, NettyConstants.MAX_CHUNK_SIZE),
                                new HttpObjectAggregator(NettyConstants.MAX_RESPONSE_SIZE))
                                //Netty Client把请求转发给内网源服务的Handler
                                .addLast("handler", new NettyClientHandler());
                    }
                })
                .option(ChannelOption.TCP_NODELAY, true)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 60*1000);
        this.channel = bootstrap.connect(host, port).sync().channel();
        // valid
        if (!isValidate()) {
            close();
            return;
        }

        log.debug("netty client proxy, connect to server success at host:{}, port:{}", host, port);
    }
}
  • Netty客户端必须有NioEventLoopGroup

  • ChannelType:指定Channel的类型。因为是客户端,所以使用了NioSocketChannel。

  • new NioEventLoopGroup()核心线程数是CPU*2,如果我们传入的线程数nThreads是0,那么Netty会设置默认的线程数DEFAULT_EVENT_LOOP_THREADS,而这个默认的

Netty的客户端如何发起TCP长连接

bootstrap.connect(host, port).sync().channel()

客户端通过调用Bootstrap的connect()方法进行连接。在connect()方法中进行一些参数检查,并调用doConnect()方法,

static {
    DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
                "io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));

    if (logger.isDebugEnabled()) {
            logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
        }
}

/**
* @see MultithreadEventExecutorGroup#MultithreadEventExecutorGroup(int, ThreadFactory, Object...)
*/
protected MultithreadEventLoopGroup(int nThreads, ThreadFactory threadFactory, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, threadFactory, args);
}
7.1.3. 网关转发调用源服务的连接池

网关Server内置的Netty客户端不是每次转发都立即创建一个连接,而是从连接池中获取一个连接。

8. RPC数据传输

9. http数据处理

10. Janus监控告警系统设计

10.1. 背景

  1. 需要统一的流量统计、监控告警指标,以便对Janus系统性能、运行状态进行分析。

  2. Janus网关需要对进来的业务流量进行监控、统计和实时告警,以让业务方了解他们的服务经过Janus的实时状态。

  3. 对Janus本身运行情况进行监控和告警,以便发现Janus系统的问题,对产品持续做改进。

  4. 通过监控告警,直接定位到相应的处理人员,提高故障处理效率。

10.2. 应用场景

  1. 业务方人员。了解服务调用统计信息、调用耗时、出错率、异常等信息,方便排查问题。

  2. Janus开发人员。评估系统性能、流量负载情况,发现系统运行问题。

  3. 系统运维人员。了解Janus机器和进程运行状态,机器负载,以发现集群运行问题。

11. WAF2.0

12. Janus限流方案设计

12.1. 需求

  1. 保证Janus服务器在 瞬间 极限流量的冲击下,稳定提供服务。

IV. janus Admin设计

13. 数据库设计

13.2. API的模型设计

- id: 7 #根据配置的多个Hosts不通过注册中心进行LB  #http://localhost:8081/admin7/menu/manage/allMenu2
    filters:
      - name: PathMapping
        args:
          /admin7/menu/manage/allMenu2: "/admin/menu/manage/allMenu"
    protocol: lb://hosts
    hosts:
      - 127.0.0.1:8084
      - admin.qingcloud.net:80
    predicates: # and ; if need or add a new route
      - name: PathPrecise
        args:
          paths: /admin7/menu/manage/allMenu2

14. Janus Admin使用

14.1. API管理

14.1.1. 新建API

  • 新建路由进行负载均衡

- id: 7 #根据配置的多个Hosts不通过注册中心进行LB  #http://localhost:8081/admin7/menu/manage/allMenu2
    filters:
      - name: PathMapping
        args:
          /admin7/menu/manage/allMenu2: "/admin/menu/manage/allMenu"
    protocol: lb://hosts
    hosts:
      - 127.0.0.1:8084
      - admin.qingcloud.net:80
    predicates: # and ; if need or add a new route
      - name: PathPrecise
        args:
          paths: /admin7/menu/manage/allMenu2

Janus Admin的UI操作

lb hosts 1
lb hosts 2

15. 开发者手册

15.1. 启动Janus Server

在IDEA中运行JanusServerApplication.java,设置启动参数即可。 eg:-Dcluster=Janus_Test -Dlocal=true,如下图所示:

1
Note
-Dcluster=Janus_Test是网关集群编码Janus_Test, -Dlocal=true表示启动的时候读取本地配置文件,而不是远端配置文件