This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
<audio id="audio" title="31 | Logger组件Tomcat的日志框架及实战" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/06/7b/06f1a6e8d3084c0929d5d807504b9c7b.mp3"></audio>
每一个系统都有一些通用的模块比如日志模块、异常处理模块、工具类等对于Tomcat来说比较重要的通用模块有日志、Session管理和集群管理。从今天开始我会分三期来介绍通用模块今天这一期先来讲日志模块。
日志模块作为一个通用的功能在系统里通常会使用第三方的日志框架。Java的日志框架有很多比如JULJava Util Logging、Log4j、Logback、Log4j2、Tinylog等。除此之外还有JCLApache Commons Logging和SLF4J这样的“门面日志”。下面是SLF4J与日志框架Logback、Log4j的关系图
<img src="https://static001.geekbang.org/resource/image/a2/2b/a263790cd53456273d50ab754063a72b.png" alt="">
我先来解释一下什么是“门面日志”。“门面日志”利用了设计模式中的门面模式思想对外提供一套通用的日志记录的API而不提供具体的日志输出服务如果要实现日志输出需要集成其他的日志框架比如Log4j、Logback、Log4j2等。
这种门面模式的好处在于记录日志的API和日志输出的服务分离开代码里面只需要关注记录日志的API通过SLF4J指定的接口记录日志而日志输出通过引入JAR包的方式即可指定其他的日志框架。当我们需要改变系统的日志输出服务时不用修改代码只需要改变引入日志输出框架JAR包。
今天我们就来看看Tomcat的日志模块是如何实现的。默认情况下Tomcat使用自身的JULI作为Tomcat内部的日志处理系统。JULI的日志门面采用了JCL而JULI的具体实现是构建在Java原生的日志系统`java.util.logging`之上的所以在看JULI的日志系统之前我先简单介绍一下Java的日志系统。
## Java日志系统
Java的日志包在`java.util.logging`路径下,包含了几个比较重要的组件,我们通过一张图来理解一下:
<img src="https://static001.geekbang.org/resource/image/5a/1d/5aae1574eb4e0c33da06484011bb121d.png" alt="">
从图上我们看到这样几个重要的组件:
- Logger用来记录日志的类。
- Handler规定了日志的输出方式如控制台输出、写入文件。
- Level定义了日志的不同等级。
- Formatter将日志信息格式化比如纯文本、XML。
我们可以通过下面的代码来使用这些组件:
```
public static void main(String[] args) {
Logger logger = Logger.getLogger(&quot;com.mycompany.myapp&quot;);
logger.setLevel(Level.FINE);
logger.setUseParentHandlers(false);
Handler hd = new ConsoleHandler();
hd.setLevel(Level.FINE);
logger.addHandler(hd);
logger.info(&quot;start log&quot;);
}
```
## JULI
JULI对日志的处理方式与Java自带的基本一致但是Tomcat中可以包含多个应用而每个应用的日志系统应该相互独立。Java的原生日志系统是每个JVM有一份日志的配置文件这不符合Tomcat多应用的场景所以JULI重新实现了一些日志接口。
**DirectJDKLog**
Log的基础实现类是DirectJDKLog这个类相对简单就包装了一下Java的Logger类。但是它也在原来的基础上进行了一些修改比如修改默认的格式化方式。
**LogFactory**
Log使用了工厂模式来向外提供实例LogFactory是一个单例可以通过SeviceLoader为Log提供自定义的实现版本如果没有配置就默认使用DirectJDKLog。
```
private LogFactory() {
// 通过ServiceLoader尝试加载Log的实现类
ServiceLoader&lt;Log&gt; logLoader = ServiceLoader.load(Log.class);
Constructor&lt;? extends Log&gt; m=null;
for (Log log: logLoader) {
Class&lt;? extends Log&gt; c=log.getClass();
try {
m=c.getConstructor(String.class);
break;
}
catch (NoSuchMethodException | SecurityException e) {
throw new Error(e);
}
}
//如何没有定义Log的实现类discoveredLogConstructor为null
discoveredLogConstructor = m;
}
```
下面的代码是LogFactory的getInstance方法
```
public Log getInstance(String name) throws LogConfigurationException {
//如果discoveredLogConstructor为null也就没有定义Log类默认用DirectJDKLog
if (discoveredLogConstructor == null) {
return DirectJDKLog.getInstance(name);
}
try {
return discoveredLogConstructor.newInstance(name);
} catch (ReflectiveOperationException | IllegalArgumentException e) {
throw new LogConfigurationException(e);
}
}
```
**Handler**
在JULI中就自定义了两个HandlerFileHandler和AsyncFileHandler。FileHandler可以简单地理解为一个在特定位置写文件的工具类有一些写操作常用的方法如open、write(publish)、close、flush等使用了读写锁。其中的日志信息通过Formatter来格式化。
AsyncFileHandler继承自FileHandler实现了异步的写操作。其中缓存存储是通过阻塞双端队列LinkedBlockingDeque来实现的。当应用要通过这个Handler来记录一条消息时消息会先被存储到队列中而在后台会有一个专门的线程来处理队列中的消息取出的消息会通过父类的publish方法写入相应文件内。这样就可以在大量日志需要写入的时候起到缓冲作用防止都阻塞在写日志这个动作上。需要注意的是我们可以为阻塞双端队列设置不同的模式在不同模式下对新进入的消息有不同的处理方式有些模式下会直接丢弃一些日志
```
OVERFLOW_DROP_LAST丢弃栈顶的元素
OVERFLOW_DROP_FIRSH丢弃栈底的元素
OVERFLOW_DROP_FLUSH等待一定时间并重试不会丢失元素
OVERFLOW_DROP_CURRENT丢弃放入的元素
```
**Formatter**
Formatter通过一个format方法将日志记录LogRecord转化成格式化的字符串JULI提供了三个新的Formatter。
- OnlineFormatter基本与Java自带的SimpleFormatter格式相同不过把所有内容都写到了一行中。
- VerbatimFormatter只记录了日志信息没有任何额外的信息。
- JdkLoggerFormatter格式化了一个轻量级的日志信息。
**日志配置**
Tomcat的日志配置文件为Tomcat文件夹下`conf/logging.properties`。我来拆解一下这个配置文件首先可以看到各种Handler的配置
```
handlers = 1catalina.org.apache.juli.AsyncFileHandler, 2localhost.org.apache.juli.AsyncFileHandler, 3manager.org.apache.juli.AsyncFileHandler, 4host-manager.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler
.handlers = 1catalina.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler
```
`1catalina.org.apache.juli.AsyncFileHandler`为例数字是为了区分同一个类的不同实例catalina、localhost、manager和host-manager是Tomcat用来区分不同系统日志的标志后面的字符串表示了Handler具体类型如果要添加Tomcat服务器的自定义Handler需要在字符串里添加。
接下来是每个Handler设置日志等级、目录和文件前缀自定义的Handler也要在这里配置详细信息:
```
1catalina.org.apache.juli.AsyncFileHandler.level = FINE
1catalina.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
1catalina.org.apache.juli.AsyncFileHandler.prefix = catalina.
1catalina.org.apache.juli.AsyncFileHandler.maxDays = 90
1catalina.org.apache.juli.AsyncFileHandler.encoding = UTF-8
```
## Tomcat + SLF4J + Logback
在今天文章开头我提到SLF4J和JCL都是日志门面那它们有什么区别呢它们的区别主要体现在日志服务类的绑定机制上。JCL采用运行时动态绑定的机制在运行时动态寻找和加载日志框架实现。
SLF4J日志输出服务绑定则相对简单很多在编译时就静态绑定日志框架只需要提前引入需要的日志框架。另外Logback可以说Log4j的进化版在性能和可用性方面都有所提升。你可以参考官网上这篇[文章](https://logback.qos.ch/reasonsToSwitch.html)来了解Logback的优势。
基于此我们来实战一下如何将Tomcat默认的日志框架切换成为“SLF4J + Logback”。具体的步骤是
1.根据你的Tomcat版本从[这里](https://github.com/tomcat-slf4j-logback/tomcat-slf4j-logback/releases)下载所需要文件。解压后你会看到一个类似于Tomcat目录结构的文件夹。<br>
2.替换或拷贝下列这些文件到Tomcat的安装目录
<img src="https://static001.geekbang.org/resource/image/09/17/09c024a49b055a86c961ea4a3beb6717.jpg" alt="">
3.删除`&lt;Tomcat&gt;/conf/logging.properties`<br>
4.启动Tomcat
## 本期精华
今天我们谈了日志框架与日志门面的区别以及Tomcat的日志模块是如何实现的。默认情况下Tomcat的日志模板叫作JULIJULI的日志门面采用了JCL而具体实现是基于Java默认的日志框架Java Util LoggingTomcat在Java Util Logging基础上进行了改造使得它自身的日志框架不会影响Web应用并且可以分模板配置日志的输出文件和格式。最后我分享了如何将Tomcat的日志模块切换到时下流行的“SLF4J + Logback”希望对你有所帮助。
## 课后思考
Tomcat独立部署时各种日志都输出到了相应的日志文件假如Spring Boot以内嵌式的方式运行Tomcat这种情况下Tomcat的日志都输出到哪里去了
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,299 @@
<audio id="audio" title="32 | Manager组件Tomcat的Session管理机制解析" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/80/7e/80ea367a5d5d2b6f99bac315a3d1147e.mp3"></audio>
我们可以通过Request对象的getSession方法来获取Session并通过Session对象来读取和写入属性值。而Session的管理是由Web容器来完成的主要是对Session的创建和销毁除此之外Web容器还需要将Session状态的变化通知给监听者。
当然Session管理还可以交给Spring来做好处是与特定的Web容器解耦Spring Session的核心原理是通过Filter拦截Servlet请求将标准的ServletRequest包装一下换成Spring的Request对象这样当我们调用Request对象的getSession方法时Spring在背后为我们创建和管理Session。
那么Tomcat的Session管理机制我们还需要了解吗我觉得还是有必要因为只有了解这些原理我们才能更好的理解Spring Session以及Spring Session为什么设计成这样。今天我们就从Session的创建、Session的清理以及Session的事件通知这几个方面来了解Tomcat的Session管理机制。
## Session的创建
Tomcat中主要由每个Context容器内的一个Manager对象来管理Session。默认实现类为StandardManager。下面我们通过它的接口来了解一下StandardManager的功能
```
public interface Manager {
public Context getContext();
public void setContext(Context context);
public SessionIdGenerator getSessionIdGenerator();
public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator);
public long getSessionCounter();
public void setSessionCounter(long sessionCounter);
public int getMaxActive();
public void setMaxActive(int maxActive);
public int getActiveSessions();
public long getExpiredSessions();
public void setExpiredSessions(long expiredSessions);
public int getRejectedSessions();
public int getSessionMaxAliveTime();
public void setSessionMaxAliveTime(int sessionMaxAliveTime);
public int getSessionAverageAliveTime();
public int getSessionCreateRate();
public int getSessionExpireRate();
public void add(Session session);
public void changeSessionId(Session session);
public void changeSessionId(Session session, String newId);
public Session createEmptySession();
public Session createSession(String sessionId);
public Session findSession(String id) throws IOException;
public Session[] findSessions();
public void load() throws ClassNotFoundException, IOException;
public void remove(Session session);
public void remove(Session session, boolean update);
public void addPropertyChangeListener(PropertyChangeListener listener)
public void removePropertyChangeListener(PropertyChangeListener listener);
public void unload() throws IOException;
public void backgroundProcess();
public boolean willAttributeDistribute(String name, Object value);
}
```
不出意外我们在接口中看到了添加和删除Session的方法另外还有load和unload方法它们的作用是分别是将Session持久化到存储介质和从存储介质加载Session。
当我们调用`HttpServletRequest.getSession(true)`这个参数true的意思是“如果当前请求还没有Session就创建一个新的”。那Tomcat在背后为我们做了些什么呢
HttpServletRequest是一个接口Tomcat实现了这个接口具体实现类是`org.apache.catalina.connector.Request`
但这并不是我们拿到的RequestTomcat为了避免把一些实现细节暴露出来还有基于安全上的考虑定义了Request的包装类叫作RequestFacade我们可以通过代码来理解一下
```
public class Request implements HttpServletRequest {}
```
```
public class RequestFacade implements HttpServletRequest {
protected Request request = null;
public HttpSession getSession(boolean create) {
return request.getSession(create);
}
}
```
因此我们拿到的Request类其实是RequestFacadeRequestFacade的getSession方法调用的是Request类的getSession方法我们继续来看Session具体是如何创建的
```
Context context = getContext();
if (context == null) {
return null;
}
Manager manager = context.getManager();
if (manager == null) {
return null;
}
session = manager.createSession(sessionId);
session.access();
```
从上面的代码可以看出Request对象中持有Context容器对象而Context容器持有Session管理器Manager这样通过Context组件就能拿到Manager组件最后由Manager组件来创建Session。
因此最后还是到了StandardManagerStandardManager的父类叫ManagerBase这个createSession方法定义在ManagerBase中StandardManager直接重用这个方法。
接着我们来看ManagerBase的createSession是如何实现的
```
@Override
public Session createSession(String sessionId) {
//首先判断Session数量是不是到了最大值最大Session数可以通过参数设置
if ((maxActiveSessions &gt;= 0) &amp;&amp;
(getActiveSessions() &gt;= maxActiveSessions)) {
rejectedSessions++;
throw new TooManyActiveSessionsException(
sm.getString(&quot;managerBase.createSession.ise&quot;),
maxActiveSessions);
}
// 重用或者创建一个新的Session对象请注意在Tomcat中就是StandardSession
// 它是HttpSession的具体实现类而HttpSession是Servlet规范中定义的接口
Session session = createEmptySession();
// 初始化新Session的值
session.setNew(true);
session.setValid(true);
session.setCreationTime(System.currentTimeMillis());
session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
String id = sessionId;
if (id == null) {
id = generateSessionId();
}
session.setId(id);// 这里会将Session添加到ConcurrentHashMap中
sessionCounter++;
//将创建时间添加到LinkedList中并且把最先添加的时间移除
//主要还是方便清理过期Session
SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
synchronized (sessionCreationTiming) {
sessionCreationTiming.add(timing);
sessionCreationTiming.poll();
}
return session
}
```
到此我们明白了Session是如何创建出来的创建出来后Session会被保存到一个ConcurrentHashMap中
```
protected Map&lt;String, Session&gt; sessions = new ConcurrentHashMap&lt;&gt;();
```
请注意Session的具体实现类是StandardSessionStandardSession同时实现了`javax.servlet.http.HttpSession``org.apache.catalina.Session`接口并且对程序员暴露的是StandardSessionFacade外观类保证了StandardSession的安全避免了程序员调用其内部方法进行不当操作。StandardSession的核心成员变量如下
```
public class StandardSession implements HttpSession, Session, Serializable {
protected ConcurrentMap&lt;String, Object&gt; attributes = new ConcurrentHashMap&lt;&gt;();
protected long creationTime = 0L;
protected transient volatile boolean expiring = false;
protected transient StandardSessionFacade facade = null;
protected String id = null;
protected volatile long lastAccessedTime = creationTime;
protected transient ArrayList&lt;SessionListener&gt; listeners = new ArrayList&lt;&gt;();
protected transient Manager manager = null;
protected volatile int maxInactiveInterval = -1;
protected volatile boolean isNew = false;
protected volatile boolean isValid = false;
protected transient Map&lt;String, Object&gt; notes = new Hashtable&lt;&gt;();
protected transient Principal principal = null;
}
```
## Session的清理
我们再来看看Tomcat是如何清理过期的Session。在Tomcat[热加载和热部署](https://time.geekbang.org/column/article/104423)的文章里我讲到容器组件会开启一个ContainerBackgroundProcessor后台线程调用自己以及子容器的backgroundProcess进行一些后台逻辑的处理和Lifecycle一样这个动作也是具有传递性的也就是说子容器还会把这个动作传递给自己的子容器。你可以参考下图来理解这个过程。
<img src="https://static001.geekbang.org/resource/image/3b/eb/3b2dfa635469c0fe7e3a17e2517c53eb.jpg" alt="">
其中父容器会遍历所有的子容器并调用其backgroundProcess方法而StandardContext重写了该方法它会调用StandardManager的backgroundProcess进而完成Session的清理工作下面是StandardManager的backgroundProcess方法的代码
```
public void backgroundProcess() {
// processExpiresFrequency 默认值为6而backgroundProcess默认每隔10s调用一次也就是说除了任务执行的耗时每隔 60s 执行一次
count = (count + 1) % processExpiresFrequency;
if (count == 0) // 默认每隔 60s 执行一次 Session 清理
processExpires();
}
/**
* 单线程处理,不存在线程安全问题
*/
public void processExpires() {
// 获取所有的 Session
Session sessions[] = findSessions();
int expireHere = 0 ;
for (int i = 0; i &lt; sessions.length; i++) {
// Session 的过期是在isValid()方法里处理的
if (sessions[i]!=null &amp;&amp; !sessions[i].isValid()) {
expireHere++;
}
}
}
```
backgroundProcess由Tomcat后台线程调用默认是每隔10秒调用一次但是Session的清理动作不能太频繁因为需要遍历Session列表会耗费CPU资源所以在backgroundProcess方法中做了取模处理backgroundProcess调用6次才执行一次Session清理也就是说Session清理每60秒执行一次。
## Session事件通知
按照Servlet规范在Session的生命周期过程中要将事件通知监听者Servlet规范定义了Session的监听器接口
```
public interface HttpSessionListener extends EventListener {
//Session创建时调用
public default void sessionCreated(HttpSessionEvent se) {
}
//Session销毁时调用
public default void sessionDestroyed(HttpSessionEvent se) {
}
}
```
注意到这两个方法的参数都是HttpSessionEvent所以Tomcat需要先创建HttpSessionEvent对象然后遍历Context内部的LifecycleListener并且判断是否为HttpSessionListener实例如果是的话则调用HttpSessionListener的sessionCreated方法进行事件通知。这些事情都是在Session的setId方法中完成的
```
session.setId(id);
@Override
public void setId(String id, boolean notify) {
//如果这个id已经存在先从Manager中删除
if ((this.id != null) &amp;&amp; (manager != null))
manager.remove(this);
this.id = id;
//添加新的Session
if (manager != null)
manager.add(this);
//这里面完成了HttpSessionListener事件通知
if (notify) {
tellNew();
}
}
```
从代码我们看到setId方法调用了tellNew方法那tellNew又是如何实现的呢
```
public void tellNew() {
// 通知org.apache.catalina.SessionListener
fireSessionEvent(Session.SESSION_CREATED_EVENT, null);
// 获取Context内部的LifecycleListener并判断是否为HttpSessionListener
Context context = manager.getContext();
Object listeners[] = context.getApplicationLifecycleListeners();
if (listeners != null &amp;&amp; listeners.length &gt; 0) {
//创建HttpSessionEvent
HttpSessionEvent event = new HttpSessionEvent(getSession());
for (int i = 0; i &lt; listeners.length; i++) {
//判断是否是HttpSessionListener
if (!(listeners[i] instanceof HttpSessionListener))
continue;
HttpSessionListener listener = (HttpSessionListener) listeners[i];
//注意这是容器内部事件
context.fireContainerEvent(&quot;beforeSessionCreated&quot;, listener);
//触发Session Created 事件
listener.sessionCreated(event);
//注意这也是容器内部事件
context.fireContainerEvent(&quot;afterSessionCreated&quot;, listener);
}
}
}
```
上面代码的逻辑是先通过StandardContext将HttpSessionListener类型的Listener取出然后依次调用它们的sessionCreated方法。
## 本期精华
今天我们从Request谈到了Session的创建、销毁和事件通知里面涉及不少相关的类下面我画了一张图帮你理解和消化一下这些类的关系
<img src="https://static001.geekbang.org/resource/image/11/cf/11493762a465c27152dbb4aa4b563ecf.jpg" alt="">
Servlet规范中定义了HttpServletRequest和HttpSession接口Tomcat实现了这些接口但具体实现细节并没有暴露给开发者因此定义了两个包装类RequestFacade和StandardSessionFacade。
Tomcat是通过Manager来管理Session的默认实现是StandardManager。StandardContext持有StandardManager的实例并存放了HttpSessionListener集合Session在创建和销毁时会通知监听器。
## 课后思考
TCP连接的过期时间和Session的过期时间有什么区别
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,297 @@
<audio id="audio" title="33 | Cluster组件Tomcat的集群通信原理" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4c/ad/4cc3977b530ed4e0f67c38e639cc38ad.mp3"></audio>
为了支持水平扩展和高可用Tomcat提供了集群部署的能力但与此同时也带来了分布式系统的一个通用问题那就是如何在集群中的多个节点之间保持数据的一致性比如会话Session信息。
要实现这一点基本上有两种方式一种是把所有Session数据放到一台服务器或者一个数据库中集群中的所有节点通过访问这台Session服务器来获取数据。另一种方式就是在集群中的节点间进行Session数据的同步拷贝这里又分为两种策略第一种是将一个节点的Session拷贝到集群中其他所有节点第二种是只将一个节点上的Session数据拷贝到另一个备份节点。
对于Tomcat的Session管理来说这两种方式都支持。今天我们就来看看第二种方式的实现原理也就是Tomcat集群通信的原理和配置方法最后通过官网上的一个例子来了解下Tomcat集群到底是如何工作的。
## 集群通信原理
要实现集群通信首先要知道集群中都有哪些成员。Tomcat是通过**组播**Multicast来实现的。那什么是组播呢为了理解组播我先来说说什么是“单播”。网络节点之间的通信就好像是人们之间的对话一样一个人对另外一个人说话此时信息的接收和传递只在两个节点之间进行比如你在收发电子邮件、浏览网页时使用的就是单播也就是我们熟悉的“点对点通信”。
如果一台主机需要将同一个消息发送多个主机逐个传输,效率就会比较低,于是就出现组播技术。组播是**一台主机向指定的一组主机发送数据报包**组播通信的过程是这样的每一个Tomcat节点在启动时和运行时都会周期性默认500毫秒发送组播心跳包同一个集群内的节点都在相同的**组播地址**和**端口**监听这些信息在一定的时间内默认3秒不发送**组播报文**的节点就会被认为已经崩溃了,会从集群中删去。因此通过组播,集群中每个成员都能维护一个集群成员列表。
## 集群通信配置
有了集群成员的列表集群中的节点就能通过TCP连接向其他节点传输Session数据。Tomcat通过SimpleTcpCluster类来进行会话复制In-Memory Replication。要开启集群功能只需要将`server.xml`里的这一行的注释去掉就行:
<img src="https://static001.geekbang.org/resource/image/45/89/45760766a99cad8a1e7001beae6d5589.png" alt="">
变成这样:
<img src="https://static001.geekbang.org/resource/image/99/bb/99d7bd2cebfd19dfd4d702351a4450bb.png" alt="">
虽然只是简单的一行配置但这一行配置等同于下面这样的配置也就是说Tomcat给我们设置了很多默认参数这些参数都跟集群通信有关。
```
&lt;!--
SimpleTcpCluster是用来复制Session的组件。复制Session有同步和异步两种方式
同步模式下向浏览器的发送响应数据前需要先将Session拷贝到其他节点完
异步模式下无需等待Session拷贝完成就可响应。异步模式更高效但是同步模式
可靠性更高。
同步异步模式由channelSendOptions参数控制默认值是8为异步模式4是同步模式。
在异步模式下,可以通过加上&quot;拷贝确认&quot;Acknowledge来提高可靠性此时
channelSendOptions设为10
--&gt;
&lt;Cluster className=&quot;org.apache.catalina.ha.tcp.SimpleTcpCluster&quot;
channelSendOptions=&quot;8&quot;&gt;
&lt;!--
Manager决定如何管理集群的Session信息。
Tomcat提供了两种ManagerBackupManager和DeltaManager。
BackupManager集群下的某一节点的Session将复制到一个备份节点。
DeltaManager 集群下某一节点的Session将复制到所有其他节点。
DeltaManager是Tomcat默认的集群Manager。
expireSessionsOnShutdown设置为true时一个节点关闭时
将导致集群下的所有Session失效
notifyListenersOnReplication集群下节点间的Session复制、
删除操作是否通知session listeners
maxInactiveInterval集群下Session的有效时间(单位:s)。
maxInactiveInterval内未活动的Session将被Tomcat回收。
默认值为1800(30min)
--&gt;
&lt;Manager className=&quot;org.apache.catalina.ha.session.DeltaManager&quot;
expireSessionsOnShutdown=&quot;false&quot;
notifyListenersOnReplication=&quot;true&quot;/&gt;
&lt;!--
Channel是Tomcat节点之间进行通讯的工具。
Channel包括5个组件Membership、Receiver、Sender、
Transport、Interceptor
--&gt;
&lt;Channel className=&quot;org.apache.catalina.tribes.group.GroupChannel&quot;&gt;
&lt;!--
Membership维护集群的可用节点列表。它可以检查到新增的节点
也可以检查没有心跳的节点
className指定Membership使用的类
address组播地址
port组播端口
frequency发送心跳(向组播地址发送UDP数据包)的时间间隔(单位:ms)。
dropTimeMembership在dropTime(单位:ms)内未收到某一节点的心跳,
则将该节点从可用节点列表删除。默认值为3000。
--&gt;
&lt;Membership className=&quot;org.apache.catalina.tribes.membership.
McastService&quot;
address=&quot;228.0.0.4&quot;
port=&quot;45564&quot;
frequency=&quot;500&quot;
dropTime=&quot;3000&quot;/&gt;
&lt;!--
Receiver用于各个节点接收其他节点发送的数据。
接收器分为两种BioReceiver(阻塞式)、NioReceiver(非阻塞式)
className指定Receiver使用的类
address接收消息的地址
port接收消息的端口
autoBind端口的变化区间如果port为4000autoBind为100
接收器将在4000-4099间取一个端口进行监听。
selectorTimeoutNioReceiver内Selector轮询的超时时间
maxThreads线程池的最大线程数
--&gt;
&lt;Receiver className=&quot;org.apache.catalina.tribes.transport.nio.
NioReceiver&quot;
address=&quot;auto&quot;
port=&quot;4000&quot;
autoBind=&quot;100&quot;
selectorTimeout=&quot;5000&quot;
maxThreads=&quot;6&quot;/&gt;
&lt;!--
Sender用于向其他节点发送数据Sender内嵌了Transport组件
Transport真正负责发送消息。
--&gt;
&lt;Sender className=&quot;org.apache.catalina.tribes.transport.
ReplicationTransmitter&quot;&gt;
&lt;!--
Transport分为两种bio.PooledMultiSender(阻塞式)
和nio.PooledParallelSender(非阻塞式)PooledParallelSender
是从tcp连接池中获取连接可以实现并行发送即集群中的节点可以
同时向其他所有节点发送数据而互不影响。
--&gt;
&lt;Transport className=&quot;org.apache.catalina.tribes.
transport.nio.PooledParallelSender&quot;/&gt;
&lt;/Sender&gt;
&lt;!--
Interceptor : Cluster的拦截器
TcpFailureDetectorTcpFailureDetector可以拦截到某个节点关闭
的信息并尝试通过TCP连接到此节点以确保此节点真正关闭从而更新集
群可用节点列表
--&gt;
&lt;Interceptor className=&quot;org.apache.catalina.tribes.group.
interceptors.TcpFailureDetector&quot;/&gt;
&lt;!--
MessageDispatchInterceptor查看Cluster组件发送消息的
方式是否设置为Channel.SEND_OPTIONS_ASYNCHRONOUS如果是
MessageDispatchInterceptor先将等待发送的消息进行排队
然后将排好队的消息转给Sender。
--&gt;
&lt;Interceptor className=&quot;org.apache.catalina.tribes.group.
interceptors.MessageDispatchInterceptor&quot;/&gt;
&lt;/Channel&gt;
&lt;!--
Valve : Tomcat的拦截器
ReplicationValve在处理请求前后打日志过滤不涉及Session变化的请求。
--&gt;
&lt;Valve className=&quot;org.apache.catalina.ha.tcp.ReplicationValve&quot;
filter=&quot;&quot;/&gt;
&lt;Valve className=&quot;org.apache.catalina.ha.session.
JvmRouteBinderValve&quot;/&gt;
&lt;!--
Deployer用于集群的farm功能监控应用中文件的更新以保证集群中所有节点
应用的一致性如某个用户上传文件到集群中某个节点的应用程序目录下Deployer
会监测到这一操作并把文件拷贝到集群中其他节点相同应用的对应目录下以保持
所有应用的一致,这是一个相当强大的功能。
--&gt;
&lt;Deployer className=&quot;org.apache.catalina.ha.deploy.FarmWarDeployer&quot;
tempDir=&quot;/tmp/war-temp/&quot;
deployDir=&quot;/tmp/war-deploy/&quot;
watchDir=&quot;/tmp/war-listen/&quot;
watchEnabled=&quot;false&quot;/&gt;
&lt;!--
ClusterListener : 监听器监听Cluster组件接收的消息
使用DeltaManager时Cluster接收的信息通过ClusterSessionListener
传递给DeltaManager从而更新自己的Session列表。
--&gt;
&lt;ClusterListener className=&quot;org.apache.catalina.ha.session.
ClusterSessionListener&quot;/&gt;
&lt;/Cluster&gt;
```
从上面的的参数列表可以看到默认情况下Session管理组件DeltaManager会在节点之间拷贝SessionDeltaManager采用的一种all-to-all的工作方式即集群中的节点会把Session数据向所有其他节点拷贝而不管其他节点是否部署了当前应用。当集群节点数比较少时比如少于4个这种all-to-all的方式是不错的选择但是当集群中的节点数量比较多时数据拷贝的开销成指数级增长这种情况下可以考虑BackupManagerBackupManager只向一个备份节点拷贝数据。
在大体了解了Tomcat集群实现模型后就可以对集群作出更优化的配置了。Tomcat推荐了一套配置使用了比DeltaManager更高效的BackupManager并且通过ReplicationValve设置了请求过滤。
这里还请注意在一台服务器部署多个节点时需要修改Receiver的侦听端口另外为了在节点间高效地拷贝数据所有Tomcat节点最好采用相同的配置具体配置如下
```
&lt;Cluster className=&quot;org.apache.catalina.ha.tcp.SimpleTcpCluster&quot;
channelSendOptions=&quot;6&quot;&gt;
&lt;Manager className=&quot;org.apache.catalina.ha.session.BackupManager&quot;
expireSessionsOnShutdown=&quot;false&quot;
notifyListenersOnReplication=&quot;true&quot;
mapSendOptions=&quot;6&quot;/&gt;
&lt;Channel className=&quot;org.apache.catalina.tribes.group.
GroupChannel&quot;&gt;
&lt;Membership className=&quot;org.apache.catalina.tribes.membership.
McastService&quot;
address=&quot;228.0.0.4&quot;
port=&quot;45564&quot;
frequency=&quot;500&quot;
dropTime=&quot;3000&quot;/&gt;
&lt;Receiver className=&quot;org.apache.catalina.tribes.transport.nio.
NioReceiver&quot;
address=&quot;auto&quot;
port=&quot;5000&quot;
selectorTimeout=&quot;100&quot;
maxThreads=&quot;6&quot;/&gt;
&lt;Sender className=&quot;org.apache.catalina.tribes.transport.
ReplicationTransmitter&quot;&gt;
&lt;Transport className=&quot;org.apache.catalina.tribes.transport.
nio.PooledParallelSender&quot;/&gt;
&lt;/Sender&gt;
&lt;Interceptor className=&quot;org.apache.catalina.tribes.group.
interceptors.TcpFailureDetector&quot;/&gt;
&lt;Interceptor className=&quot;org.apache.catalina.tribes.group.
interceptors.MessageDispatchInterceptor&quot;/&gt;
&lt;Interceptor className=&quot;org.apache.catalina.tribes.group.
interceptors.ThroughputInterceptor&quot;/&gt;
&lt;/Channel&gt;
&lt;Valve className=&quot;org.apache.catalina.ha.tcp.ReplicationValve&quot;
filter=&quot;.*\.gif|.*\.js|.*\.jpeg|.*\.jpg|.*\.png|.*\
.htm|.*\.html|.*\.css|.*\.txt&quot;/&gt;
&lt;Deployer className=&quot;org.apache.catalina.ha.deploy.FarmWarDeployer&quot;
tempDir=&quot;/tmp/war-temp/&quot;
deployDir=&quot;/tmp/war-deploy/&quot;
watchDir=&quot;/tmp/war-listen/&quot;
watchEnabled=&quot;false&quot;/&gt;
&lt;ClusterListener className=&quot;org.apache.catalina.ha.session.
ClusterSessionListener&quot;/&gt;
&lt;/Cluster&gt;
```
## 集群工作过程
Tomcat的官网给出了一个例子来说明Tomcat集群模式下是如何工作的以及Tomcat集群是如何实现高可用的。比如集群由Tomcat A和Tomcat B两个Tomcat实例组成按照时间先后顺序发生了如下事件
**1. Tomcat A启动**
Tomcat A启动过程中当Host对象被创建时一个Cluster组件默认是SimpleTcpCluster被关联到这个Host对象。当某个应用在`web.xml`中设置了Distributable时Tomcat将为此应用的上下文环境创建一个DeltaManager。SimpleTcpCluster启动Membership服务和Replication服务。
**2. Tomcat B启动在Tomcat A之后启动**
首先Tomcat B会执行和Tomcat A一样的操作然后SimpleTcpCluster会建立一个由Tomcat A和Tomcat B组成的Membership。接着Tomcat B向集群中的Tomcat A请求Session数据如果Tomcat A没有响应Tomcat B的拷贝请求Tomcat B会在60秒后time out。在Session数据拷贝完成之前Tomcat B不会接收浏览器的请求。
**3. Tomcat A接收HTTP请求创建Session 1**
Tomcat A响应客户请求在把结果发送回客户端之前ReplicationValve会拦截当前请求如果Filter中配置了不需拦截的请求类型这一步就不会进行默认配置下拦截所有请求如果发现当前请求更新了Session就调用Replication服务建立TCP连接将Session拷贝到Membership列表中的其他节点即Tomcat B。在拷贝时所有保存在当前Session中的可序列化的对象都会被拷贝而不仅仅是发生更新的部分。
**4. Tomcat A崩溃**
当Tomcat A崩溃时Tomcat B会被告知Tomcat A已从集群中退出然后Tomcat B就会把Tomcat A从自己的Membership列表中删除。并且Tomcat B的Session更新时不再往Tomcat A拷贝同时负载均衡器会把后续的HTTP请求全部转发给Tomcat B。在此过程中所有的Session数据不会丢失。
**5. Tomcat B接收Tomcat A的请求**
Tomcat B正常响应本应该发往Tomcat A的请求因为Tomcat B保存了Tomcat A的所有Session数据。
**6. Tomcat A重新启动**
Tomcat A按步骤1、2操作启动加入集群并从Tomcat B拷贝所有Session数据拷贝完成后开始接收请求。
**7. Tomcat A接收请求Session 1被用户注销**
Tomcat继续接收发往Tomcat A的请求Session 1设置为失效。请注意这里的失效并非因为Tomcat A处于非活动状态超过设置的时间而是应用程序执行了注销的操作比如用户登出而引起的Session失效。这时Tomcat A向Tomcat B发送一个Session 1 Expired的消息Tomcat B收到消息后也会把Session 1设置为失效。
**8. Tomcat B接收到一个新请求创建Session 2**
同理这个新的Session也会被拷贝到Tomcat A。
**9. Tomcat A上的Session 2过期**
因超时原因引起的Session失效Tomcat A无需通知Tomcat BTomcat B同样知道Session 2已经超时。因此对于Tomcat集群有一点非常重要**所有节点的操作系统时间必须一致**。不然会出现某个节点Session已过期而在另一节点此Session仍处于活动状态的现象。
## 本期精华
今天我谈了Tomcat的集群工作原理和配置方式还通过官网上的一个例子说明了Tomcat集群的工作过程。Tomcat集群对Session的拷贝支持两种方式DeltaManager和BackupManager。
当集群中节点比较少时可以采用DeltaManager因为Session数据在集群中各个节点都有备份任何一个节点崩溃都不会对整体造成影响可靠性比较高。
当集群中节点数比较多时可以采用BackupManager这是因为一个节点的Session只会拷贝到另一个节点数据拷贝的开销比较少同时只要这两个节点不同时崩溃Session数据就不会丢失。
## 课后思考
在Tomcat官方推荐的配置里ReplicationValve被配置成下面这样
```
&lt;Valve className=&quot;org.apache.catalina.ha.tcp.ReplicationValve&quot;
filter=&quot;.*\.gif|.*\.js|.*\.jpeg|.*\.jpg|.*\.png|.*\
.htm|.*\.html|.*\.css|.*\.txt&quot;/&gt;
```
你是否注意到filter的值是一些JS文件或者图片等这是为什么呢
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,29 @@
<audio id="audio" title="特别放送 | 如何持续保持对学习的兴趣?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e9/ef/e9eba83a465fad6c5b225273c26228ef.mp3"></audio>
你好我是李号双。今天我们抛开技术本身的内容来聊聊专栏或者一门新技术的学习方法我也分享一下自己是如何啃下Tomcat和Jetty源码的。
专栏如今已经更新完了五个模块我们学习了Tomcat和Jetty的整体架构、连接器、容器和通用组件这些内容可以说是Tomcat和Jetty的设计核心。在日常工作的使用中我们使用到了Tomcat和Jetty提供的功能我希望通过学习专栏还能帮你了解这些功能是如何实现的以及Tomcat和Jetty在设计时都考虑了哪些地方。
所以在学习专栏时你不妨思考这样一个问题假如让你来设计并实现一个Web容器你会怎么做呢如何合理设计顶层模块如何考虑方方面面的需求比如最基本的功能需求是加载和运行Web程序最重要的非功能需求是高性能、高并发。你可以顺着这两条线先思考下你会怎么做然后再回过头来看看Tomcat和Jetty是如何做到的。这样的学习方法其实就在有意识地训练自己独立设计一个系统的能力不管是对于学习这个专栏还是其他技术带着问题再去学习都会有所帮助。
说完关于专栏的学习方法下面我必须要鼓励一下坚持学习到现在的你。专栏从第三模块开始开始讲解连接器、容器和通用组件的设计和原理有些内容可能比较偏向底层确实难度比较大如果对底层源码不熟悉或者不感兴趣学习起来会有些痛苦。但是我之所以设计了这部分内容就是希望能够揭开Tomcat和Jetty的内部细节因为任何一个优秀的中间件之所以可以让用户使用比较容易其内部一定都是很复杂的。这也从侧面传递出一个信号美好的东西都是有代价的需要也值得我们去付出时间和精力。
我和你一样我们都身处IT行业这个行业技术更新迭代非常快因此我们需要以一个开放的心态持续学习。而学习恰恰又是一个反人性的过程甚至是比较痛苦的尤其是有些技术框架本身比较庞大设计得非常复杂我们在学习初期很容易遇到“挫折感”一些技术点怎么想也想不明白往往也会有放弃的想法。我同样经历过这个过程**我的经验是找到适合自己的学习方法非常重要,同样关键的是要保持学习的兴趣和动力**。
举个我学习Spring框架的例子记得当时我在接触Spring框架的时候一开始就钻进一个模块开始啃起了源代码。由于Spring框架本身比较庞杂分很多模块当时给我最直观的感受就是看不懂我不明白代码为什么要这么写为什么设计得这么“绕”。这里面的问题是首先**我还没弄清楚森林长什么样子,就盯着树叶看**很可能是盲人摸象看不到全貌和整体的设计思路。第二个问题是我还没学会用Spring就开始研究它是如何设计的结果可想而知也遇到了挫折。后来我逐渐总结出一些学习新技术的小经验在学习一门技术的时候一定要先看清它的全貌我推荐先看官方文档看看都有哪些模块、整体上是如何设计的。接着我们先不要直接看源码而是要动手跑一跑官网上的例子或者用这个框架实现一个小系统关键是要学会怎么使用。只有在这个基础上才能深入到特定模块去研究设计思路或者深入到某一模块源码之中。这样在学习的过程中按照一定的顺序一步一步来就能够即时获得**成就感**,有了成就感你才会更加专注,才会愿意花更多时间和精力去深入研究。因此要保持学习的兴趣,我觉得有两个方面比较重要:
第一个是我们需要**带着明确的目标去学习**。比如某些知识点是面试的热点那学习目标就是彻底理解和掌握它当被问到相关问题时你的回答能够使得面试官对你刮目相看有时候往往凭着某一个亮点就能影响最后的录用结果。再比如你想掌握一门新技术来解决工作上的问题那你的学习目标应该是不但要掌握清楚原理还要能真正的将新技术合理运用到实际工作中解决实际问题产生实际效果。我们学习了Tomcat和Jetty的责任链模式是不是在实际项目中的一些场景下就可以用到这种设计呢再比如学习了调优方法是不是可以在生产环境里解决性能问题呢总之技术需要变现才有学习动力。
第二个是**一定要动手实践**。只有动手实践才会让我们对技术有最直观的感受。有时候我们听别人讲经验和理论感觉似乎懂了但是过一段时间便又忘记了。如果我们动手实践了特别是在这个过程中碰到了一些问题通过网上查找资料或者跟同事讨论解决了问题这便是你积累的宝贵经验想忘记都难。另外适当的动手实践能够树立起信心培养起兴趣这跟玩游戏上瘾有点类似通过打怪升级一点点积累起成就感。比如学习了Tomcat的线程池实现我们就可以自己写一个定制版的线程池学习了Tomcat的类加载器我们也可以自己动手写一个类加载器。
专栏更新到现在,内容最难的部分已经结束,在后面的实战调优模块,我在设计内容时都安排了实战环节。毕竟调优本身就是一个很贴近实际场景的话题,应该基于特定场景,去解决某个性能问题,而不是为了调优而调优。所以这部分内容也更贴近实际工作场景,你可以尝试用我前面讲的方法,带着问题学习后面的专栏。
调优的过程中需要一些知识储备比如我们需要掌握操作系统、JVM以及网络通信的原理这些原理在专栏前面的文章也讲到过。虽然涉及很多原理也很复杂并不是说要面面俱到我们也不太容易深入到每个细节所以最关键的是要弄懂相关参数的含义比如JVM内存的参数、GC的参数、Linux内核的相关参数等。
除此之外,调优的过程还需要借助大量的工具,包括性能监控工具、日志分析工具、网络抓包工具和流量压测工具等,熟练使用这些工具也是每一个后端程序员必须掌握的看家本领,因此在实战环节,我也设计了一些场景来带你熟悉这些工具。
说了那么多,就是希望你保持对学习的热情,树立明确的目标,再加上亲自动手实践。专栏学习到现在这个阶段,是时候开始动手实践了,希望你每天都能积累一点,每天都能有所进步。
最后欢迎你在留言区分享一下你学习一门新技术的方法和心得,与我和其他同学一起讨论。