声明

翻译自Timing With Curl


当一个http请求花费了太长时间的时候,我经常用下面的命令来查找原因。

1
2
3
4
5
6
7
8
curl -L -w "time_namelookup: %{time_namelookup}
time_connect: %{time_connect}
time_appconnect: %{time_appconnect}
time_pretransfer: %{time_pretransfer}
time_redirect: %{time_redirect}
time_starttransfer: %{time_starttransfer}
time_total: %{time_total}
" https://example.com/

下面是用一条指令来实现的同样的命令,我可以在未来用到的时候,在这个页面通过三次点击快速复制。

1
curl -L -w "time_namelookup: %{time_namelookup}\ntime_connect: %{time_connect}\ntime_appconnect: %{time_appconnect}\ntime_pretransfer: %{time_pretransfer}\ntime_redirect: %{time_redirect}\ntime_starttransfer: %{time_starttransfer}\ntime_total: %{time_total}\n" https://example.com/

这个命令输入之后,一般能看到以下输出:

1
2
3
4
5
6
7
8
9
10
11
12
$ curl -L -w "namelookup: %{time_namelookup}\nconnect: %{time_connect}\nappconnect: %{time_appconnect}\npretransfer: %{time_pretransfer}\nstarttransfer: %{time_starttransfer}\ntotal: %{time_total}\n" https://example.com/
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
...
</html>
time_namelookup: 0.001403
time_connect: 0.245464
time_appconnect: 0.757656
time_pretransfer: 0.757823
time_redirect: 0.000000
time_starttransfer: 0.982111
time_total: 0.982326

为了简洁起见,在上面的输出中,我用省略号忽略了大部分的html部分的输出。
下面的列表描述了输出中每一个数字的含义。这些信息直接取自curl 7.20.0版本的手册。以下是详情:

  • time_namelookup: 以秒为单位,表示从命令开始到域名被解析完成所需时间。
  • time_connect: 以秒为单位,表示从命令开始到TCP请求完成连接到远程端口(或代理)所需时间。
  • time_appconnect: 以秒为单位,表示从命令开始到SSL/SSH等请求完成到远程端口的连接/握手所需时间。(7.19.0版本后加入)
  • time_pretransfer: 以秒为单位,表示从命令开始到文件传输开始所需时间。这包括所有的预传输命令和协议。包含所有涉及到特定协议的预传输指令和谈判(?)。
  • time_redirect: 以秒为单位,包括所有的重定向步骤,从域名检索,连接,预传输以及在最后交换数据开始之前的传输所需时间。time_redirect展示了多个重定向请求的完整执行时间。(7.12.3版本后加入)
  • time_starttransfer: 以秒为单位,表示从命令开始到第一个字节被传输所属时间。这包含了time_pretransfer,以及服务器计算结果所属的时间。
  • time_total: 以秒为单位,表示整个操作持续到最后所需的总时间。这个时间会精确到毫秒进行展现。

一个没啥用的关键细节是,time_appconnecttime_connect之间的时间差可以告诉我们SSL/TLS握手所需时间。对于一个没有SSL/TLS的干净连接,这个时间会显示为0。下面的输出证明了这件事:

1
2
3
4
5
6
7
8
9
10
11
12
$ curl -L -w "time_namelookup: %{time_namelookup}\ntime_connect: %{time_connect}\ntime_appconnect: %{time_appconnect}\ntime_pretransfer: %{time_pretransfer}\ntime_redirect: %{time_redirect}\ntime_starttransfer: %{time_starttransfer}\ntime_total: %{time_total}\n" http://example.com/
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
...
</html>
time_namelookup: 0.001507
time_connect: 0.247032
time_appconnect: 0.000000
time_pretransfer: 0.247122
time_redirect: 0.000000
time_starttransfer: 0.512645
time_total: 0.512853

同样注意time_redirect在上面的输出中也是0。这是因为访问example.com的时候,没有重定向发生。以下是另一个例子,展示了当重定向发生时输出会是什么样子:

1
2
3
4
5
6
7
8
9
10
11
12
$ curl -L -w "time_namelookup: %{time_namelookup}\ntime_connect: %{time_connect}\ntime_appconnect: %{time_appconnect}\ntime_pretransfer: %{time_pretransfer}\ntime_redirect: %{time_redirect}\ntime_starttransfer: %{time_starttransfer}\ntime_total: %{time_total}\n" https://susam.in/blog
<!DOCTYPE HTML>
<html>
...
</html>
time_namelookup: 0.001886
time_connect: 0.152445
time_appconnect: 0.465326
time_pretransfer: 0.465413
time_redirect: 0.614289
time_starttransfer: 0.763997
time_total: 0.765413

当网络服务遭遇一个潜在的延迟事件时,我会在多个客户端优先使用这条命令,因为这个命令返回的结果帮助我快速找到网络延迟的原因。

之所以有这个文章,是因为遇到在nodejs中使用express框架直接接受post请求时,无法获取到body中的内容,于是google了一篇英文文章解决了这个问题。
翻译自codeforgeek


GET和POST是两种用于构建REST接口的普通HTTP请求。两者的命名包含着不同的使用目的。
根据定义,GET请求从指定的地址抓取数据,POST请求则是发送数据到指定的地址。

Express框架提供了router()方法用于构建HTTP终端。下面让我们看下怎么使用Express来实现GET和POST请求。

GET请求

在Express中使用GET请求非常直接。你首先必须创建一个express和router的实例。下面是实现的一小段代码。

1
2
3
4
5
6
7
8
9
10
11
const express = require("express");
const router = express.Router();
const app = express();

router.get(‘/handle’,(request,response) => {
//code to perform particular action.
//To access GET variable use req.query() and req.params() methods.
});

// add router in the Express app.
app.use("/", router);

GET请求会缓存在浏览器历史中。这就是为什么不推荐使用GET请求发送敏感数据(密码,ATM取款码,等等)。你应该只使用GET请求从服务器获取数据。

POST请求

Express需要一个额外的中间件模块提取POST请求中的数据。这个中间件叫做”body-parser”。我们需要在Express的实例中安装和配置它。
你可以使用如下命令安装body-parser。

1
sudo npm install --save body-parser

你需要在你的项目中导入这个包并告知Express去使用这个中间件。参考如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const express = require("express");
const bodyParser = require("body-parser");
const router = express.Router();
const app = express();

//Here we are configuring express to use body-parser as middle-ware.
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

router.post(‘/handle’,(request,response) => {
//code to perform particular action.
//To access POST variable use req.body()methods.
console.log(request.body);
});

// add router in the Express app.
app.use("/", router);

通过这种方式,你可以在Express框架中使用GET和POST请求。(译者注:注意先后顺序,body-parser的注入一定要在router的注入之前,否则也会不起作用)


后面的内容是博主写的demo,感觉也没必要发上来,就略过了。

问题起源

这两天刚好一年交一次的保险开始催我交年费,最近由于基金太绿,手头实在有点紧,因此这个年费一直拖着还没交,于是我动了是不是要退保的想法。

而销售则继续去年的话术,说我这个交满20年就可以不交了,现在交到12年退保太可惜了之类的。

今年我没有轻易把这话听进去,于是我又去搜了下这个保险产品的条例,这个产品的介绍如下图。

按照我当前的情况,排除意外身故的情况,简单概括下就是:

  • 我每年交7600,交20年
  • 到66岁每两年领一次总保额(总保额是50000)的8%,我这份产品从19岁开始买,到66岁总共可以领23次
  • 66岁一次性给100%全保额
  • 66岁开始每年领一次总保额的8%,我假设我能活到88岁,那总共领22次
    阅读全文 »

下载和安装jenkins

相对简单的办法,依次执行以下语句(前提,需要先安装rpm)

1
2
3
4
#获取jenkins的rpm安装文件
wget https://pkg.jenkins.io/redhat-stable/jenkins-2.222.3-1.1.noarch.rpm
#使用rpm安装
rpm -ivh jenkins-2.222.3-1.1.noarch.rpm

启动jenkins

jenkins启动需要先安装java,java的安装不再赘述,java安装完成后,修改jenkins配置文件,指向java的安装目录

1
vi /etc/init.d/jenkins

在”candidates=”这一行加入java安装目录,如果默认配置里面已经指向了你的java安装目录,则不需要再次配置。

1
2
3
4
5
6
7
8
9
10
11
12
candidates="
/etc/alternatives/java
/usr/lib/jvm/java-1.8.0/bin/java
/usr/lib/jvm/jre-1.8.0/bin/java
/usr/lib/jvm/java-1.7.0/bin/java
/usr/lib/jvm/jre-1.7.0/bin/java
/usr/lib/jvm/java-11.0/bin/java
/usr/lib/jvm/jre-11.0/bin/java
/usr/lib/jvm/java-11-openjdk-amd64
/usr/bin/java
/usr/java/jdk1.8.0_121/jre/bin/java
"
阅读全文 »

前言:
azkaban的用处和好处不再赘述,这里只讲怎么将azkaban在项目中使用起来。
另外非常推荐直接看官方的英文文档进行学习,那将是最新最可靠的教程。

下载和安装azkaban

下载azkaban

方法1:直接到azkaban的github网页下载tar.gz包。截止2020.4.29日最新版本的包地址为(https://github.com/azkaban/azkaban/archive/3.84.10.tar.gz)
方法2:在linux执行

1
wget https://github.com/azkaban/azkaban/archive/3.84.10.tar.gz

安装azkaban

将包进行解压,执行

1
tar -zxvf azkaban-3.81.10.tar.gz

解压完成后目录结构如下图

阅读全文 »

数据来源

历年周最佳的数据来自于NBA数据官网,链接为 http://www.stat-nba.com/award/item18.html

这里可以看到每个赛季的周最佳球员。然后每个球员旁边有个数据的链接,表示的就是这名球员当周的平均数据。比如11月11日-11月17日的东部周最佳是武切维奇,其数据链接为http://www.stat-nba.com/player/3672.html

数据处理

爬取周最佳球员

首先看周最佳的页面,链接是http://www.stat-nba.com/award/item18.html,选中武切维奇的名字,右键打开右键菜单,选择“查看页面元素”。出现如下页面:

注意,我们需要获取到的是所有的周最佳数据,而不是某个人的,因此我们需要找到这个页面元素的父元素,因此才能找到这个父元素下面的所有子元素-即所有的周最佳数据。

1
2
3
如图所示,周最佳数据的节点来自于<td class='current'> -> <table class='stat-box'  style='width:170px;border:0'>  -> <td>

根据网页上下文的查看,可以知道"<td class="current">是赛季的节点,<table class="stat-box" style="width:170px;border:0">是每个赛季下每周的节点,而<td>就是每周下面每个球员的节点。
阅读全文 »

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
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

<!-- 常量定义start -->
<!-- 默认日志输出格式-->
<property name="HOST_NAME" value="Project_Name"/>
<property name="DEFAULT_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} ** ${HOST_NAME} ** [%thread] ** %level ** [%logger] ** - %file:%line ** [%method] - ** %msg%n"/>
<property name="MAX_HISTORY" value="30"/>
<property name="ASYNC_QUEUE_SIZE" value="1024"/>
<property name="DEFAULT_CHARSET" value="UTF-8"/>
<!-- 常量定义end -->

<!-- 默认日志的console appender,本地使用的日志 -->
<appender class="ch.qos.logback.core.ConsoleAppender" name="STDOUT">
<encoder>
<pattern>${DEFAULT_PATTERN}</pattern>
<charset>${DEFAULT_CHARSET}</charset>
</encoder>
</appender>
<appender class="ch.qos.logback.core.rolling.RollingFileAppender" name="ROLLING_FILE_DEFAULT">
<append>true</append>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>E:/logs/${HOST_NAME}/${HOST_NAME}-%d{yyyyMMdd}.log</fileNamePattern>
<maxHistory>${MAX_HISTORY}</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${DEFAULT_PATTERN}</pattern>
<charset>${DEFAULT_CHARSET}</charset>
</encoder>
</appender>
<springProfile name="!local">
<!-- 服务器上使用的appender start -->
<!-- 默认的file appender,按天切分日志 -->
<appender class="ch.qos.logback.core.rolling.RollingFileAppender" name="ROLLING_FILE_DEFAULT">
<append>true</append>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/home/logs/${HOSTNAME}/${HOSTNAME}-%d{yyyyMMdd}.log</fileNamePattern>
<maxHistory>${MAX_HISTORY}</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${DEFAULT_PATTERN}</pattern>
<charset>${DEFAULT_CHARSET}</charset>
</encoder>
</appender>

<!-- 错误日志,按天切分 -->
<appender class="ch.qos.logback.core.rolling.RollingFileAppender" name="ROLLING_FILE_ERROR">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<append>true</append>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/home/logs/${HOSTNAME}/${HOSTNAME}_error-%d{yyyyMMdd}.log</fileNamePattern>
<maxHistory>${MAX_HISTORY}</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${DEFAULT_PATTERN}</pattern>
<charset>${DEFAULT_CHARSET}</charset>
</encoder>
</appender>

<appender class="ch.qos.logback.classic.AsyncAppender" name="ASYNC_ROLLING_FILE_DEFAULT">
<!-- 如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>-1</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>2048</queueSize>
<includeCallerData>true</includeCallerData>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="ROLLING_FILE_DEFAULT"/>
</appender>

<appender class="ch.qos.logback.classic.AsyncAppender" name="ASYNC_ROLLING_FILE_ERROR">
<!-- 如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>-1</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>2048</queueSize>
<includeCallerData>true</includeCallerData>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="ROLLING_FILE_ERROR"/>
</appender>

</springProfile>

<root level="info">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ROLLING_FILE_DEFAULT"/>
</root>

</configuration>

问题描述

Dubbo有一个比较奇怪的问题,目前不知道Apache和Alibaba公司出于什么样的考虑,貌似一直都没有一个比较合适的解决方案,问题如下:

  • 项目搭建中你需要自定义一个本地的Exception,命名为比如BusinessException。比较一般的书写代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /**
    * @Author linqiang
    * @Date 2019/10/24 16:20
    * @Version 1.0
    * @Description 业务异常类
    **/
    public class BusinessException extends RuntimeException {
    private Integer code;
    private String msg;

    public BusinessException(Integer code, String msg) {
    this.code = code;
    this.msg = msg;
    }

    public Integer getCode() {
    return code;
    }

    public String getMsg() {
    return msg;
    }
    }
  • 通常这个BusinessException是要能够跨模块使用的,一般放在commons或者core模块中,同时别的模块的pom.xml文件引入这些模块,使得整个项目都可以使用这个BusinessException。

  • 问题来了,如果在A模块调用了B模块,B模块抛出了一个BusinessException,这时A模块接收到的不是BusinessException,而是一个RuntimeException,而且关于BusinessException的细节已经完全丢失,只会剩下一个类名的描述。

问题原因

关于该问题出现的原因,参考这篇文章,归纳一下,就是在Dubbo的传输信息过程中,类ExceptionFilter.java会对Exception做一个过滤,其过滤器的关键代码如下:

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
// directly throw if it's checked exception
if (!(exception instanceof RuntimeException) && (exception instanceof Exception)) {
return;
}
// directly throw if the exception appears in the signature
try {
Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
Class<?>[] exceptionClassses = method.getExceptionTypes();
for (Class<?> exceptionClass : exceptionClassses) {
if (exception.getClass().equals(exceptionClass)) {
return;
}
}
} catch (NoSuchMethodException e) {
return;
}
<!-- more -->
// for the exception not found in method's signature, print ERROR message in server's log.
logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + exception.getClass().getName() + ": " + exception.getMessage(), exception);

// directly throw if exception class and interface class are in the same jar file.
String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
return;
}
// directly throw if it's JDK exception
String className = exception.getClass().getName();
if (className.startsWith("java.") || className.startsWith("javax.")) {
return;
}

// directly throw if it's dubbo exception
if (exception instanceof RpcException) {
return;
}

// otherwise, wrap with RuntimeException and throw back to the client
appResponse.setException(new RuntimeException(StringUtils.toString(exception)));
return;

即Dubbo在遇到异常时会这样处理:

  • 非RuntimeException不处理,直接返回
  • 抛出的是方法上注明的异常,直接返回
  • 如果异常类和接口类在同一jar包,直接返回
  • java或者javax目录下的异常类,直接返回
  • Dubbo自带的RpcException,直接返回
  • 其他的异常,会被封装为RuntimeException返回

解决方式

根据以上的分析,那么很显然,自定义异常是被直接封装为RuntimeException返回了,而且只带了自定义异常的类名信息,丢失了别的细节。

那么我们想要自定义异常进行正常返回,那只有满足这个FIlter所写的上述条件。我们可以分析一下:

  • 不继承RuntimeException,以检查时异常抛出。不推荐,正常的业务异常应该是运行时异常。

  • 在接口方法上要写上throws BusinessException,如下:

    1
    2
    3
    4
    5
    public interface DemoService {

    DemoUser getUserInfo(Long userID) throws BusinessException;

    }

    不推荐,不符合接口设计原则,且这样是把运行时异常作为检查时异常处理。

  • 把自定义异常类和接口放在同一个包目录下。不推荐,毕竟这样相当于绑定了异常类的目录,耦合性变高。

  • 改包名,以“java.”或者“javax.”来开头。不推荐,违反了类命名原则。

  • 继承Dubbo的RpcException。RpcException也是继承了RuntimeException,因此能够以RuntimeException的方式进行处理。不推荐,相当于自定义异常属于Dubbo的RpcException,这在程序设计上不合理。

我们发现,想要满足Dubbo的过滤器直接返回异常的条件,我们就必须做出一些违反程序设计的操作,如果一定要从这些方法中选择一种的话,相对来说,自定义异常类和接口放在同一目录下,以及继承RpcException是对于程序侵入性更小的方式。

其他解决方式

参考 这篇文章,提供了两种解决方式:

1.在配置文件中配置如下,效果是:关闭ExceptionFIlter,使所有异常绕过该过滤器直接返回。不推荐,Dubbo既然设置了这个异常过滤类,一定是出于安全和功能上的考虑,直接禁用可能会引发别的问题。

1
2
3
dubbo:
provider:
filter: -exception

2.修改Dubbo源文件ExceptionFilter,使其遇到BusinessException也能直接返回。不推荐,相当于定制了本地的Dubbo包,是一个后续很容易被人忽略的大坑。

总结

Dubbo在处理自定义异常时,会直接返回RuntimeException,且抹去自定义异常的所有细节,导致无法处理。

本文写下的时候,Dubbo版本为2.7.3,该问题还没有非常完美的解决方案,相对来说,把自定义异常和接口类放在同一目录下是侵入性最小的方案。

1.在maven库中直接引入oracle jdbc driver是不能下载到本地的,原因是oracle的jdbc工具包不是开放给公众下载的,需要用户是oracle的注册用户才能在oracle官网下载到。

2.解决方式:目前没有发现更简便的方法,只能从oracle官网上下载jdbc driver的包,然后从本地upload到本地仓库。

  • 进入JDBC下载网站,选择自己需要的JDBC driver版本,点击进入。

  • 下载需要的JDBC driver的jar包

  • 如果还没有登陆oracle账号,需要你登陆以后才能下载,没有注册过的注册一个账号,这里不再赘述。

  • 下载完成后,进入下载完成的文件夹。在这里需要手动执行命令,在该文件夹打开git窗口,如何在windows安装git请自行百度。执行如下命令。

    阅读全文 »

  • 基础架构:Spring Boot / Spring Cloud
  • 数据库:MySQL / Oracle
  • 数据库层连接:Hibernate / Mybatis / JPA
  • 分页
  • 日志:Log4j / Slf4J
  • 数据库连接池:Druid
  • 消息中间件:Dubbo / Grpc
  • 缓存:Redis
  • 权限:Spring Security / Shiro
  • 包管理仓库:Maven / Gradle
  • 统一日志记录-数据库层面
  • JSON处理:FastJSON
  • 统一返回结果处理
  • Zookeeper
  • MQ组件:RabbitMQ / RocketMQ / Kafka
  • 统一Exception处理
  • 公用Util类
  • 测试用例:JUnit
  • 集群
0%