一、局域网
自从装上服务器之后,吃瓜群众我便是在其中跑了许多服务和应用,现今局域网如下:
从外网访问局域网,采用frp内网穿透,谷歌云是代理服务端;
服务器虚拟化技术采用kvm,管理UI是WebVirtMgr;
一、局域网
自从装上服务器之后,吃瓜群众我便是在其中跑了许多服务和应用,现今局域网如下:
从外网访问局域网,采用frp内网穿透,谷歌云是代理服务端;
服务器虚拟化技术采用kvm,管理UI是WebVirtMgr;
在上一篇文章里,我使用Travis来做自动部署,链接在此:Travis构建Google App Engine的CI/CD)。Travis使用起来挺方便,只不过它仅支持github。
不希望某些关键代码被人看到,而且,大部分代码托管在bitbucket上,挪移代码仓库不方便,考虑下来,只能找一个支持自定义代码仓库的CI平台了。
调研下来,有CodeShip/JenKins/Flow.Ci等几个工具。
CodeShip支持Bitbucket,只不过每个月只能构建100次。
JenKins是开源的,可以自己部署到服务器上,公司内部用的就是它,不过我不太喜欢JenKins,因为它的icon不甚好看…
Flow.Ci是国产开源的工具,现在(2018-02-13)还是Beta测试版本,类似Jenkins,自己部署,我选的就是它。
项目系统经常需要调用外部系统的接口,使用http的post请求方式来获得数据,陈达经常搞混post的请求方式,在这里记一下。
post请求,类似于get请求,拥有状态行、请求头header和请求体body。
状态行比较简单,一般都长下面的模样:1
<methon> <url> <version>
比如:1
POST www.iloveqyc.com HTTP/1.1
请求头则是用来说明解释本请求的上下文,比如请求头里可以指定cookie、UA等信息。在这里主要关注Content-Type这个信息,它指定了请求体body里面数据的编码方式。
编码方式是什么意思呢?陈达要给远方的服务器发消息,消息内容塞在body里面,服务器收到消息后怎么知道消息体里面是什么东西呢?把消息体当做key-value?当做json串?当做xml?服务器做不到啊~
所以在使用Post的时候,需要在Content-Type里指明消息体body的编码方式。
消息体主要就是用来承载该post请求要发送的内容,其编码方式可以用消息头header里的Content-Type指定。
这种方式很常见,一般页面上的表格都是用x-www-form-urlencoded的方式post,body里的提交值按照key1=value1&key2=value2进行排列,且进行url转码(比如转成unicode)。
这种方式也是目前项目用的post方式。
这种方式主要是用来上传文件,在body里面指定文件类型,和x-www-form-urlencoded一样,是2种最常用的方式。
使用这种方式,你可以在body里直接贴json字符串,而不用写键值对。(有的人不直接用application/json,而是使用x-www-form-urlencoded,在键值对的value里面写json,这倒也是一种方法)
顾名思义,这种方式提交的是xml,不过xml太臃肿,用的比较少
AQS(AbstractQueueSynchronizer),抽象队列同步器,是jdk提供的一种用来自定义实现同步组件的工具,java并发中的ReentrantLock/CountDownLatch/Semaphore也都是继承了它而实现。
现在陈达想要用它来实现一个自己的同步器,功能是限制同一时间内只有2个线程才能获取锁。
总的思路是同步组件QycLock实现Lock接口,实现加锁lock与解锁unlock。QycLock内部集成了一个继承自AQS的自定义同步器QycSync,主要的同步逻辑均在QynSync上,QycLcok仅是个包装。
QycSync继承自AQS,便是拥有了同步状态和同步队列,把初始状态值设为2,一个线程获取同步状态后,用CAS的方式让状态值-1,释放同步状态后,用CAS的方式让状态值+1。
QycSync代码如下: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
86package com.iloveqyc.service.test.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* Project: rcs-web
* PACKAGE_NAME: com.iloveqyc.service.test.lock
* Date: 2017/12/3
* Time: 下午2:20
*
* @author qiuyongchen David
* <p>
* Usage: 同步器
*/
public class QycLock implements Lock {
private QycSync qycSync = new QycSync(2);
@Override
public void lock() {
qycSync.acquireShared(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
qycSync.releaseShared(1);
}
@Override
public Condition newCondition() {
return null;
}
static class QycSync extends AbstractQueuedSynchronizer {
QycSync(int count) {
if (count <= 0) {
throw new RuntimeException();
}
setState(count);
}
@Override
protected int tryAcquireShared(int arg) {
for (;;) {
int currentState = getState();
int newState = currentState - 1;
if (newState < 0) {
System.out.println(Thread.currentThread().getName() + "到达时同步状态已经小于0,将会在同步队列中调用sun.misc.Unsafe.park,进入等待状态");
return newState;
}
boolean isSuccess = compareAndSetState(currentState, newState);
if (!isSuccess) {
System.out.println(Thread.currentThread().getName() + "尝试获取锁失败, 同步状态currentState预期为" + currentState + ", 实际不是");
continue;
}
return newState;
}
}
@Override
protected boolean tryReleaseShared(int arg) {
int currentState = getState();
int newState = currentState + 1;
return compareAndSetState(currentState, newState);
}
}
}
QycThread代码如下: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
34package com.iloveqyc.service.test.lock;
/**
* Project: rcs-web
* PACKAGE_NAME: com.iloveqyc.service.test.lock
* Date: 2017/12/3
* Time: 下午2:47
*
* @author qiuyongchen David
* <p>
* Usage: xxx
*/
public class QycThread implements Runnable {
private QycLock qycLock;
public QycThread(QycLock qycLock) {
this.qycLock = qycLock;
}
@Override
public void run() {
qycLock.lock();
System.out.println(Thread.currentThread().getName() + "成功获取了锁!即将带着锁睡眠60s");
try {
Thread.sleep(60000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "睡眠60s结束,即将释放锁");
qycLock.unlock();
}
}
main方法如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package com.iloveqyc.service.test.lock;
/**
* Project: rcs-web
* PACKAGE_NAME: com.iloveqyc.service.test.lock
* Date: 2017/12/3
* Time: 上午9:52
*
* @author qiuyongchen David
* <p>
* Usage: xxx
*/
public class QycTest {
public static void main(String[] args) {
QycLock lock = new QycLock();
for (int i = 0; i < 10; i++) {
Thread t = new Thread(new QycThread(lock));
t.setName(String.valueOf(i));
t.start();
}
}
}
『注:这是我一年前在知乎的回答,高赞,不知为何被知乎删了,故搬移至此』
假设你用一个全新的浏览器(第一次启动的那种),访问百度(http://www.baidu.com/),在你敲入网址并按下回车之后,将会发生以下神奇的事情:
浏览器先尝试从Host文件中获取www.baidu.com对应的IP地址,如果能取到当然万事大吉大家都能嗨,如果不能,就使用DNS协议来获取IP咯。
在DNS协议中,PC会向你的本地DNS服务器求助(一般是路由器),希望从本地DNS服务器那里得到百度的IP,得到就好,得不到还得向更高层次的DNS服务器求助,最终总能得到百度的IP。
得到百度的IP,下一步是使用TCP协议,建立TCP连接。
在TCP协议中,建立TCP需要与百度服务器握手三次,你先告诉服务器你要给服务器发东西(SYN),服务器应答你并告诉你它也要给你发东西(SYN、ACK),然后你应答服务器(ACK),总共来回了3次,称为3次握手。
不过,建立TCP连接有个前提(或者说给服务器发消息有个前提):你必须能成功地把消息发到服务器上。虽然已经知道IP,但并无啥用(比如说,你在广东,你知道北京的地理坐标经纬度就能到北京了?你得知道有哪些路通往北京吧你得准备盘缠吧你得花时间吧)。
为了将消息从你的PC上传到服务器上,需要用到IP协议、ARP协议和OSPF协议。
我们都知道,你的PC和百度服务器之间一般会有许多路由器之类的东西,IP协议指定了出发地(你的PC)和目的地(服务器);你的数据会经过一个又一个路由器,OSPF决定了会经过那些路由器(用一种叫路由算法的玩意,找出最佳路径);从一个路由器怎么传给下一个路由器?这是ARP协议的JOB,
ARP负责求下一个节点的地址(我们不止是要目的地,还要中间节点的地址)。IP协议使用的是IP地址,整个发送过程中只涉及出发地和目的地2个IP地址,
而ARP协议使用的是MAC地址,整个发送过程中涉及到每一个节点的MAP地址
现在,我们能和服务器通信,还建立了TCP连接,下一步干嘛,当然是用HTTP协议请求网页内容咯。
你发个HTTP请求报文给服务器,如果服务器禁止你访问它就给你回个”Forbidden”,如果它暂时挂掉了就给你回个“内部服务错误”,如果它正常才给你回个“OK“并将你要的数据传给你;如果你还需要其它的东西再去跟它要(它一般还会给你的-_-)。
你收到了服务器的回复,是一坨HTML形式的文本。浏览器必须要能够理解文本的内容,并快速地渲染到屏幕上(浏览器一般用有限自动机来理解文本内容,渲染的话就各看本事了,之所以微软IE卡成狗而谷歌浏览器很6,就是它们的渲染速度不同…)
渲染出来后,你就看到百度的首页了
在之前的『从0到1理解Java Web』的『日志框架』部分,我们知道了如何让项目打印日志在控制台上,方便我们调试观察代码的运行,这篇文章主要是介绍如何把日志打印输出到服务器文件上保存起来,上线后借助日志文件找到Bug。
这里分3部分,一个是流的概念,一个是log4j配置,另一个是打印spring自身的日志。
在linux中有流的概念,从一个地方流入(比如键盘,比如扫描仪,比如文件),流出到另一个地方(比如控制台,比如文件,比如屏幕),上一次我们实现了『从日志系统流出到控制台』,这次实现『从日志系统流出到文件』即可。
把日志的去向从控制台改到文件,需要改动哪里?这牵扯到第二部分,也就是log4j的配置。
log4j有3个组件,loggers/appenders/layouts,它们一起协同工作,指定了日志内容的格式,根据日志的类型与级别,将日志输出到指定的位置。
指定日志输出的最低级别,把级别信息传给appenders。
日志级别由低到高分别是:Trace < Debug < Info < Warn < Error < Fatal,只记录大于等于当前级别的信息。
指定日志的输出方式与保存位置。
主要有以下几类appender:
1.ConsoleAppender :将日志输出至控制台
2.DailyRollingFileAppender :将日志输出至文件,日志文件每天切割
3.FileAppender :将日志输出至文件
4.RollingFileAppender :将日志输出至文件,日志文件按指定的大小进行切割
5.WriterAppender :将日志以信息流的格式发送到指定位置
指定日志内容的格式,比如输出为Html等。
这3个组件的具体配置都在log4j.xml中。把lo4j4.xml修改为以下的内容: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<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="info" packages="com.dianping">
<Properties>
<!--日志输入路径-->
<Property name="log-path">/data/applogs/${sys:app.name}/logs</Property>
<!--日志输出到cat格式-->
<Property name="pattern">%d{yyyy-MM-dd HH:mm:ss.SSS} [${sys:app.name} ${hostName}}] [%-5level] [%t] [%c] - %msg%xEx%n</Property>
<!--日志进入存储中心的格式-->
<Property name="dw-pattern">%d{yyyy-MM-dd HH:mm:ss.SSS} ${hostName} ${sys:app.name} %p %t %c %msg%xEx%n</Property>
<Property name="scribeCategory">${sys:app.name}</Property> <!--日志需要和日志采集的一致-->
</Properties>
<Appenders>
<!-- 记录除用户行为日志外的所有信息 -->
<RollingFile name="appAppender" fileName="${log-path}/app.log" filePattern="${log-path}/app.log.%d{yyyy-MM-dd}">
<PatternLayout pattern="${pattern}"/>
<Policies>
<!-- 这里的interval由filePattern中的最小时间单位来决定,如果filePattern 中是天,这1就是1天-->
<TimeBasedTriggeringPolicy interval="1"/>
</Policies>
</RollingFile>
<!-- 独立的 error appender 便于排查问题 -->
<RollingFile name="errorAppender" fileName="${log-path}/error.log"
filePattern="${log-path}/error.log.%d{yyyy-MM-dd}">
<ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="${pattern}"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
</Policies>
</RollingFile>
<!--打印日志到控制台,方便调试-->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level [%c] - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="INFO">
<AppenderRef ref="appAppender"/>
<AppenderRef ref="errorAppender"/>
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
另外建立classpath:META-INF下建立文件app.properties(即mai/resources/METa-INF下),在里面指定项目的名称,内容如下:1
app.name=my-project-name
随后启动项目,进入/data/applogs/my-project-name/logs目录,即可看到日志文件。
到这里,我们已经成功地将代码里的日志输出到文件里。每天保存,可是,如果你仔细点观察,会发现日志文件里并没有spring和其它第三方库的日志,这可是大问题,很多时候Bug是spring的bean注入失败或找不到对应的类引起的,如果没有spring的日志,我们很难追查到真正原因。
监听web运行时的日志,那么我们就能监听到所有日志,包括spring和其它技术栈的日志。
在web项目配置文件web.xml中加入对应的监听器,内容如下: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<context-param>
<param-name>log4jContextName</param-name>
<param-value>poseidon-settlement-service</param-value>
</context-param>
<!--由Spring载入的Log4j配置文件位置 -->
<context-param>
<param-name>log4jConfiguration</param-name>
<param-value>classpath:log4j2.xml</param-value>
</context-param>
<listener>
<listener-class>org.apache.logging.log4j.web.Log4jServletContextListener</listener-class>
</listener>
<filter>
<filter-name>log4jServletFilter</filter-name>
<filter-class>org.apache.logging.log4j.web.Log4jServletFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>log4jServletFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
<dispatcher>INCLUDE</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
<!--Spring默认刷新Log4j配置文件的间隔,单位为毫秒 -->
<context-param>
<param-name>log4jRefreshInterval</param-name>
<param-value>60000</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
</listener>
同时引入log4j-web包,启动项目,即可。