0%

一、前言

很多开发同学,只关心功能开发和上线,上线后的运维工作,作为软件产品的生命周期中非常重要的一个环节,常常被忽视。即使一些同学可能有一些意识知道要关心产品上线后的情况,但是不知道如何去下手。

在这里,我们主要从开发的角度,聊聊如何做好软件上线后的监控,以及异常情况下的告警。

(这里,不会去关心业务层面上的一些运行数据,比如GMV,PV等等,虽然这些数据的异常变化,可能也跟系统稳定性也存在一些关系)

1.1 为什么关心监控

真实世界中,我们不会构建一个100%稳定可靠的服务,一个是因为各种软件上的bug或者硬件设备的故障,再或者各种人为操作的配置问题或者网络问题等等;二是因为如果我们保证极端可靠性,解决前面的问题,其超高成本带来的收益非常低,低到所有公司,哪怕是Google也不会去接受。

阅读全文 »

一、通用DDD架构

首先,看下图:
系统分层结构对比

左图、是经典的DDD架构分层图。右图、在使用依赖倒置原则下的分层图。

两个结构图对比,最典型的区别就是基础设施层,在最上层还是最底层。由于,我们项目都在使用spring或者guice等ioc工具,所以推荐使用右图的分层架构。

阅读全文 »

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

其实,在官网 https://netty.io/ 中特别指出,netty是基于NIO的网络通信框架。因此,在学习Netty之前,先熟悉java NIO组件。一般,我们说NIO,指的是非阻塞IO (Non-blocking IO)。

1. I/O模型


在计算机开发领域中,一个服务的性能受制于两方面:CPU处理能力和I/O处理能力。也就是说,我们希望我们的服务,可以消耗更少的CPU计算单元,消耗更少的I/O处理等待时长。

因此,由于I/O性能对整个系统的影响如此重要,在很多系统里,尽量来避免IO问题。例如,Redis通过使用内存,异步刷磁盘来提高IO性能,保证系统的处理能力。

当然,在很多时候,我们是避免不了I/O性能的制约的。比如,网络通信,就是一个典型的I/O问题。在优化I/O问题之前,先来聊聊I/O模型。

在《UNIX网络编程卷1》中,将I/O模型分为5种:

  • 阻塞式I/O;
  • 非租塞式I/O;
  • I/O复用(select,poll,epoll等);
  • 信号驱动式I/O(SIGIO);
  • 异步I/O(POSIX的aio_系列函数)

在区分阻塞和非租塞,异步和同步之前,先介绍下,一个输入操作主要包括两个阶段:

  1. 等待数据准备好;
  2. 从内核向进程复制数据。

对于一个网络I/O请求来说,第一步就是等待网络通信的数据传输到服务器上,系统内核会将所有传输过来的数据保存在对应位置的缓冲区中;第二步就是将网络的数据从内核区复制到用户进程的缓冲区中,然后应用就可以处理。

阅读全文 »

本设计原则,基于dubbo.io页面上整理而来。原文地址:http://dubbo.io/books/dubbo-dev-book/principals/introduction.html

1.代码基本要求

1.1 避免NPE和越界异常

在java中,虽然可能出现各种异常,甚至我们使用异常来驱动非法业务逻辑执行流程,但是如果在执行过程中出现 空指针异常和越界异常,是非常不允许的。这说明代码不够健壮,开发同学意识不够严谨。推荐的做法,对数据进行判断,打印详细的错误信息,以及可能的告知上次参数不合法等。

1.2 尽早失败FailFast

系统设计中,有一个尽早失败说法,表示,比如我们请求失败了,不进行重试,直接返回错误。代码中,也存在尽早失败的原则。比如,一个不合法的参数值或者记录状态,在请求的时候,应该进行参数检查,然后报错。在,开发过程中,有些同学,喜欢在使用参数的时候,才进行判断和错误处理,这样会导致耗费不必要的执行时间,甚至导致需要回滚之前的操作。

一般情况下,大家基本上会进行所有参数的前置校验;但是存在,一个请求,需要查询数据库判断是否状态合法时,可能有的时候,为了避免查表耗时,从而到用的时候才进行校验的情况。在正常业务下,为了这一点的RT提升,而可能让后续逻辑变得复杂,不太推荐。

阅读全文 »

1. 背景

当前,存在各种API网关,这些网关的定位之一就是管理域内各个服务需要对外的接口,统一做降级限流,权限控制,安全校验等工作。内部一般使用各种RPC服务,因此,基于微服务的不断扩展和升级,一般网关都是使用RPC 泛化机制来完成对内部服务的调用。

泛化机制,和普通的rpc调用有一些不同,它不需要引入各个依赖服务的jar包,调用方如果知道接口方法的具体信息:名称,版本,参数类型等,就可以完成rpc调用。

在开发过程中,使用泛化机制,发现一个”问题”,就是业务方调用我们内部的一个接口,接口中有一个参数是long类型,但是业务方使用string类型的参数(数字字符串,例如”12345678”)请求过来,但是并没有出现错误。这个问题,在于后面另一个业务方,也按照string类型请求(非纯数字,例如”E12345678”),这个时候报错了。

阅读全文 »

上篇文章介绍了深入浅出RPC一些原理知识,这里将继续深入讲讲关于RPC网络服务技术。本文的目标,是让对Java网络服务端开发感兴趣的新人,可以一步步深入了解一个高性能服务所需要的相关知识体系。

当然,高性能高并发的服务端开发,涉及的知识点和手段众多,比如linux网络和内核参数调优等。本文只关注代码开发层级的介绍和深入。

前言

本文中,所谓 服务端指的是,基于client/server服务组件中的server端。在网络世界里,基本上任何一个用户交互都涉及客户端和服务器端网络通信;尤其是,当存在对服务端大量请求时,如果让服务器持续对外服务过程中,抗住更多的并发请求,是一个互联网开发同学十分关心和必备的技能。

本文中,首先会基于Java网络API开发一个HelloWorld版本的server端版本。
接下来,会不断的在前一个server版本上进行优化和升级,使得其可以响应更多的网络连接请求。
在此期间,会引入一些概念,比如IO同步异步、阻塞非阻塞。
最后,我们要站在巨人的肩膀上,基于Netty来开发一个生产级别的server端应用。

阅读全文 »

远程过程调用(Remote Procedure Call,简称RPC),在微服务大行其道的今天,得到了广泛的应用。因此,在分布式系统服务群中开发应用,了解RPC一些原理和实现架构,还是很有必要的。本文,将从大的框架层面来聊聊RPC原理和实现。

前言

远程过程调用RPC,就是客户端基于某种传输协议通过网络向服务提供端请求服务处理,然后获取返回数据(对于ONE WAY模式则不返还响应结果);而这种调用对于客户端而言,和调用本地服务一样方便,开发人员不需要了解具体底层网络传输协议。简单讲,就是本地调用的逻辑处理的过程放在的远程的机器上,而不是本地服务代理来处理。

目前,Java界的RPC中间件百家争鸣,国内开源的就有阿里的Dubbo(当当二次开发的DubboX),新浪Motan;国外跨语言的有Facebook的Thrift, Google的gRpc等。

阅读全文 »

GC 是每一个Java程序员不可绕过的话题。GC 是在某些时候内存的垃圾对象数据进行搜寻定位,然后进行内存空间回收。根据这个定义,则学习GC相关知识,需要关注:对JVM整个内存结构中哪些区域进行垃圾回收;在这些内存区域中的类数据或者实例数据等数据结构是什么样子的;然后想想如何在JVM内存空间中分配内存给这些实例数据;在所有已分配了的实例里,怎么找出需要回收的数据。

综上,对于JVM GC知识体系来说,就是弄清楚JVM在什么时候,对什么对象,进行什么操作来回收内存空间。

JVM 内存结构

既然是对内存进行GC操作,那么首先需要了解 JVM 的内存结构了。

在Oracle的官方文档【Java Garbage Collection Basics】中,给出了JVM的整体架构图,如下所示:

JVM架构图

也就是说,对一个JVM来说,其主要由 ClassLoaderRuntime Data 区域 ,执行引擎以及本地方法四大部分组成。(关键的性能优化集中在Heap,JIT编译GC三大块)。

阅读全文 »

框架背景

Apache Thrift是Facebook实现的一种高效的,支持多种编程语言的远程服务调用的框架.在多语言并行于业务之中的公司,其是一个很好的RPC框架选择,但是由于缺少服务发现管理功能,在使用的时候,需要告知业务方现有业务部署的地址,并且调用方需要自己实现服务状态的感知和重试机制.此外,对于互联网公司而言,业务快速变化必然导致机器的增减,这些变化,需要通知到所有调用方来更改调用机器的配置,是非常麻烦的.

显然,对于Thrift来说,一个服务发现管理框架是多么的重要。

那么,服务发现管理框架其实可以做的很重,也可以做的很轻;对于我们,需要满足什么需求:

  • 服务调用方自动获取服务提供方地址;
  • 服务提供方服务分组;
  • 服务调用方负载均衡策略;
  • 服务非兼容升级;

具体的需求分析和实现,将在 Ourea服务发现实现原理介绍。

Thrift服务原生使用

Thrift 接口使用还是比较简单地,对外提供的server和client接口封装了所有的内部实现细节,所以,一般我们只需要告诉Thrift地址端口信息,然后就可以完成简单地RPC调用。

下面,给出一个简单地示例:

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

// 服务端示例
public class SimpleThriftServer {
private static final Logger logger = LoggerFactory.getLogger(SimpleThriftServer.class);
private static final int port = 9999;

public void simple(int port){

try {
TServerSocket tServerSocket = new TServerSocket(port);
Hello.Processor processor = new Hello.Processor(new HelloService());

TServer server = new TSimpleServer(new TServer.Args(tServerSocket).processor(processor) );
server.serve();

}catch (Exception e){
logger.error("server start error........",e);
}
}

public static void main(String[] args) throws InterruptedException {
SimpleThriftServer server = new SimpleThriftServer();
server.simple(port);
}
}

// client 端示例
public class SimpleThriftClient {

private static final Logger logger = LoggerFactory.getLogger(SimpleThriftClient.class);
private static final int port = 9999;
private static final String ip = "127.0.0.1";

public static void main() {
TTransport transport = null;
try {
transport = new TSocket(ip, port);
TProtocol protocol = new TBinaryProtocol(transport);
Hello.Client client = new Hello.Client(protocol);
transport.open();
HelloResult result = client.sayHello("hello world");
} catch (Exception e) {
logger.error("client invoke fail. ", e);
} finally {
if (transport != null) {
transport.close();
}
}
}
}

Note:

  • TProtocol 协议和编解码组件
  • TTransport 传输通信组件
  • TProcessor 服务处理相关组件
  • TServer 服务提供组件
  • Client 服务调用客户端

Thrift 原生的对外接口已经很简单了,但是为什么还需要去封装呢?上文的代码虽然简单,但是有几个点需要去注意:

  1. 对于生产环境的服务,在发布新功能,出现故障down机,都会导致服务出现不可用的情况;此外,对外的服务一般都是集群部署,集群机器的增减也是很可能会出现的事情,因此,就会出现最初对外提供的服务IP地址会出现新增(新建服务),减少(缩减服务),暂时停服(机器故障),这些所有变更通知所有业务服务调用方去更改是很难处理的事情。此外,由于服务可能存在大量的机器列表,这些配置在业务代码中,本身也是不可取的。
  2. 服务调用的时候,可能存在某些服务当时负载过高,或者服务网络问题等导致服务调用策略需要调整。也就是在选择调用集群中某台机器的时候,每个业务都要自己去实现策略,这是不可取的。此外,对于服务的负载情况无法感知,即使是静态的服务提供权重都无法获取,导致了即使客户端自己实现均衡策略,由于缺少必要的数据支持,导致只能采用轮询和随机。
  3. 业务上,服务调用之间隔离,服务接口的灰度升级等,是比较常见的技术需求。Thrift 对外发布的服务的所有IP,对于调用方来说都是平等的,也就是,如果我需要将集群中某些机器进行接口的非兼容的灰度升级,或者某些机器独立出来给一些非常重要的业务使用。目前,这种场景,只能新加机器来解决了。
  4. 对于调用方Client的调用,每次都需要去创建连接,然后和server端交互,对于大请求场景下的应用,对性能的影响是很大的。创建connection对象,是很重的,需要进行池化。
  5. ……

基于以上的一些原因,开发了基于Zookeeper的Thrift服务发现机制框架。

阅读全文 »

前言

对于一个合格的项目来说,单元测试是必不可少的一部分。尤其是,如果对于TDD思想来说,单元测试则是整个项目开发的基石。对于Javaer来说,Junit 是一个基础的单元测试框架.对于Spring框架来说, 其实现了JUnit的接口来直接支持单元测试, 但是对于Guice来说, 其定位为一个轻量级的依赖注入框架, 所以这些就需要自己来实现. 此外, 对于依赖外部的接口服务的应用, 我们在测试的时候, 是不希望其服务的不稳定导致我们单测失败; 此外, 对外部接口进行屏蔽, 也可以达到对每一个外部服务返回逻辑分支的覆盖.

因此,本文基于JUnit + Mockito + Guice框架说,然后基于简单地实例来说明JUnit的运行机制。

单元测试实践

Guice 作为一个注入框架,和Spring相比,并没有什么特别的。使用Guice介绍单元测试,其一是项目开发中使用Guice,其二由于我们需要去自己实现JUnit接口来支持Guice,能够更深入地了解JUnit结构。Mockito 是java中我比较喜欢的mock工具,当然,我也没有用过其他的。O(∩_∩)O~

首先,需要引入两个实现:

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

/**
* @author tao.ke Date: 16/3/9 Time: 下午4:24
*/
public class GuiceJUnitRunner extends BlockJUnit4ClassRunner {

private Injector injector;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface GuiceModules {
Class<?>[] value();
}

@Override
public Object createTest() throws Exception {
Object obj = super.createTest();
injector.injectMembers(obj);
return obj;
}

public GuiceJUnitRunner(Class<?> klass) throws InitializationError {
super(klass);
Class<?>[] classes = getModulesFor(klass);
injector = createInjectorFor(classes);
}

private Injector createInjectorFor(Class<?>[] classes) throws InitializationError {
Module[] modules = new Module[classes.length];
for (int i = 0; i < classes.length; i++) {
try {
modules[i] = (Module) (classes[i]).newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new InitializationError(e);
}
}
return Guice.createInjector(modules);
}

private Class<?>[] getModulesFor(Class<?> klass) throws InitializationError {
GuiceModules annotation = klass.getAnnotation(GuiceModules.class);
if (annotation == null) {
return new Class[0];
}
return annotation.value();
}
}

GuiceJUnitRunner 实现了JUnit框架BlockJUnit4ClassRunner接口,Runner是JUnit的核心,所有测试的运行,最后都是Runner来衔接运作起来的。在JuiceJUnitRunner中,我们根据annotation来初始化Guice环境需要的一些初始化配置,拦截器等;此外,熟悉Guice的同学知道,其有一个非常不好的体验,就是需要在最外层手动注入实例调用服务,这里我们希望可以直接注入service实例,因此在Runner初始化时完成这一动作。

阅读全文 »