u
@@ -12,7 +12,9 @@
|
||||
<!-- theme css & js -->
|
||||
<meta name="generator" content="Hexo 4.2.0">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="book-container">
|
||||
<div class="book-sidebar">
|
||||
<div class="book-brand">
|
||||
@@ -25,61 +27,86 @@
|
||||
<ul class="uncollapsible">
|
||||
<li><a href="/" class="current-tab">首页</a></li>
|
||||
</ul>
|
||||
|
||||
<ul class="uncollapsible">
|
||||
<li><a href="../">上一级</a></li>
|
||||
</ul>
|
||||
|
||||
<ul class="uncollapsible">
|
||||
<li>
|
||||
|
||||
<a class="current-tab" href="/极客时间/Java基础36讲.md.html">Java基础36讲.md.html</a>
|
||||
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/Java错误示例100讲.md.html">Java错误示例100讲.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/Linux性能优化.md.html">Linux性能优化.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/MySQL实战45讲.md.html">MySQL实战45讲.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/从0开始学微服务.md.html">从0开始学微服务.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/代码精进之路.md.html">代码精进之路.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/持续交付36讲.md.html">持续交付36讲.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/程序员进阶攻略.md.html">程序员进阶攻略.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/趣谈网络协议.md.html">趣谈网络协议.md.html</a>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||||
<div class="sidebar-toggle-inner"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function add_inner() {
|
||||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||||
inner.classList.add('show')
|
||||
}
|
||||
|
||||
function remove_inner() {
|
||||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||||
inner.classList.remove('show')
|
||||
}
|
||||
|
||||
function sidebar_toggle() {
|
||||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||||
let sidebar = document.querySelector('.book-sidebar')
|
||||
@@ -94,6 +121,8 @@
|
||||
content.classList.add('extend')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function open_sidebar() {
|
||||
let sidebar = document.querySelector('.book-sidebar')
|
||||
let overlay = document.querySelector('.off-canvas-overlay')
|
||||
@@ -106,7 +135,9 @@ function hide_canvas() {
|
||||
sidebar.classList.remove('show')
|
||||
overlay.classList.remove('show')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="off-canvas-content">
|
||||
<div class="columns">
|
||||
<div class="column col-12 col-lg-12">
|
||||
@@ -186,6 +217,7 @@ jaotc --output libjava.base.so --module java.base
|
||||
</code></pre>
|
||||
<p>然后,在启动时直接指定就可以了。</p>
|
||||
<pre><code>java -XX:AOTLibrary=./libHelloWorld.so,./libjava.base.so HelloWorld
|
||||
|
||||
</code></pre>
|
||||
<p>而且,Oracle JDK 支持分层编译和 AOT 协作使用,这两者并不是二选一的关系。如果你有兴趣,可以参考相关文档:<a href="https://openjdk.java.net/jeps/295">http://openjdk.java.net/jeps/295</a>。AOT 也不仅仅是只有这一种方式,业界早就有第三方工具(如 GCJ、Excelsior JET)提供相关功能。</p>
|
||||
<p>另外,JVM 作为一个强大的平台,不仅仅只有 Java 语言可以运行在 JVM 上,本质上合规的字节码都可以运行,Java 语言自身也为此提供了便利,我们可以看到类似 Clojure、Scala、Groovy、JRuby、Jython 等大量 JVM 语言,活跃在不同的场景。</p>
|
||||
@@ -569,9 +601,11 @@ ${JAVA_HOME}/bin/javap -v StringConcat.class
|
||||
<p>看起来很不错是吧?但实际情况估计会让你大跌眼镜。一般使用 Java 6 这种历史版本,并不推荐大量使用 intern,为什么呢?魔鬼存在于细节中,被缓存的字符串是存在所谓 PermGen 里的,也就是臭名昭著的“永久代”,这个空间是很有限的,也基本不会被 FullGC 之外的垃圾收集照顾到。所以,如果使用不当,OOM 就会光顾。</p>
|
||||
<p>在后续版本中,这个缓存被放置在堆中,这样就极大避免了永久代占满的问题,甚至永久代在 JDK 8 中被 MetaSpace(元数据区)替代了。而且,默认缓存大小也在不断地扩大中,从最初的 1009,到 7u40 以后被修改为 60013。你可以使用下面的参数直接打印具体数字,可以拿自己的 JDK 立刻试验一下。</p>
|
||||
<pre><code>-XX:+PrintStringTableStatistics
|
||||
|
||||
</code></pre>
|
||||
<p>你也可以使用下面的 JVM 参数手动调整大小,但是绝大部分情况下并不需要调整,除非你确定它的大小已经影响了操作效率。</p>
|
||||
<pre><code>-XX:StringTableSize=N
|
||||
|
||||
</code></pre>
|
||||
<p>Intern 是一种<strong>显式地排重机制</strong>,但是它也有一定的副作用,因为需要开发者写代码时明确调用,一是不方便,每一个都显式调用是非常麻烦的;另外就是我们很难保证效率,应用开发阶段很难清楚地预计字符串的重复情况,有人认为这是一种污染代码的实践。</p>
|
||||
<p>幸好在 Oracle JDK 8u20 之后,推出了一个新的特性,也就是 G1 GC 下的字符串排重。它是通过将相同数据的字符串指向同一份数据来做到的,是 JVM 底层的改变,并不需要 Java 类库做什么修改。</p>
|
||||
@@ -630,6 +664,7 @@ ${JAVA_HOME}/bin/javap -v StringConcat.class
|
||||
</code></pre>
|
||||
<p>因为反射机制使用广泛,根据社区讨论,目前,Java 9 仍然保留了兼容 Java 8 的行为,但是很有可能在未来版本,完全启用前面提到的针对 setAccessible 的限制,即只有当被反射操作的模块和指定的包对反射调用者模块 Open,才能使用 setAccessible,我们可以使用下面参数显式设置。</p>
|
||||
<pre><code>--illegal-access={ permit | warn | deny }
|
||||
|
||||
</code></pre>
|
||||
<p>\2. 动态代理</p>
|
||||
<p>前面的问题问到了动态代理,我们一起看看,它到底是解决什么问题?</p>
|
||||
@@ -759,6 +794,7 @@ java/lang/Integer.intValue:()I
|
||||
<p>首先,继续深挖缓存,Integer 的缓存范围虽然默认是 -128 到 127,但是在特别的应用场景,比如我们明确知道应用会频繁使用更大的数值,这时候应该怎么办呢?</p>
|
||||
<p>缓存上限值实际是可以根据需要调整的,JVM 提供了参数设置:</p>
|
||||
<pre><code>-XX:AutoBoxCacheMax=N
|
||||
|
||||
</code></pre>
|
||||
<p>这些实现,都体现在<a href="http://hg.openjdk.java.net/jdk/jdk/file/26ac622a4cab/src/java.base/share/classes/java/lang/Integer.java">java.lang.Integer</a>源码之中,并实现在 IntegerCache 的静态初始化块里。</p>
|
||||
<pre><code>private static class IntegerCache {
|
||||
@@ -855,9 +891,11 @@ java/lang/Integer.intValue:()I
|
||||
</ul>
|
||||
<p>我今天介绍的这些集合类,都不是线程安全的,对于 java.util.concurrent 里面的线程安全容器,我在专栏后面会去介绍。但是,并不代表这些集合完全不能支持并发编程的场景,在 Collections 工具类中,提供了一系列的 synchronized 方法,比如</p>
|
||||
<pre><code>static <T> List<T> synchronizedList(List<T> list)
|
||||
|
||||
</code></pre>
|
||||
<p>我们完全可以利用类似方法来实现基本的线程安全集合:</p>
|
||||
<pre><code>List list = Collections.synchronizedList(new ArrayList());
|
||||
|
||||
</code></pre>
|
||||
<p>它的实现,基本就是将每个基本方法,比如 get、set、add 之类,都通过 synchronizd 添加基本的同步支持,非常简单粗暴,但也非常实用。注意这些方法创建的线程安全集合,都符合迭代时 fail-fast 行为,当发生意外的并发修改时,尽早抛出 ConcurrentModificationException 异常,以避免不可预计的行为。</p>
|
||||
<p>另外一个经常会被考察到的问题,就是理解 Java 提供的默认排序算法,具体是什么排序方式以及设计思路等。</p>
|
||||
@@ -877,6 +915,7 @@ list.add("World");
|
||||
</code></pre>
|
||||
<p>而利用新的容器静态工厂方法,一句代码就够了,并且保证了不可变性。</p>
|
||||
<pre><code>List<String> simpleList = List.of("Hello","world");
|
||||
|
||||
</code></pre>
|
||||
<p>更进一步,通过各种 of 静态工厂方法创建的实例,还应用了一些我们所谓的最佳实践,比如,它是不可变的,符合我们对线程安全的需求;它因为不需要考虑扩容,所以空间上更加紧凑等。</p>
|
||||
<p>如果我们去看 of 方法的源码,你还会发现一个特别有意思的地方:我们知道 Java 已经支持所谓的可变参数(varargs),但是官方类库还是提供了一系列特定参数长度的方法,看起来似乎非常不优雅,为什么呢?这其实是为了最优的性能,JVM 在处理变长参数的时候会有明显的额外开销,如果你需要实现性能敏感的 API,也可以进行参考。</p>
|
||||
@@ -1023,6 +1062,7 @@ public class LinkedHashMapSample {
|
||||
<li>具体键值对在哈希表中的位置(数组 index)取决于下面的位运算:</li>
|
||||
</ul>
|
||||
<pre><code>i = (n - 1) & hash
|
||||
|
||||
</code></pre>
|
||||
<p>仔细观察哈希值的源头,我们会发现,它并不是 key 本身的 hashCode,而是来自于 HashMap 内部的另外一个 hash 方法。注意,为什么这里需要将高位数据移位到低位进行异或运算呢?<strong>这是因为有些数据计算出的哈希值差异主要在高位,而 HashMap 里的哈希寻址是忽略容量以上的高位的,那么这种处理就可以有效避免类似情况下的哈希碰撞。</strong></p>
|
||||
<pre><code>static final int hash(Object kye) {
|
||||
@@ -1385,11 +1425,13 @@ public class LinkedHashMapSample {
|
||||
<p>Selector 同样是基于底层操作系统机制,不同模式、不同版本都存在区别,例如,在最新的代码库里,相关实现如下:</p>
|
||||
<p>Linux 上依赖于 epoll(<a href="http://hg.openjdk.java.net/jdk/jdk/file/d8327f838b88/src/java.base/linux/classes/sun/nio/ch/EPollSelectorImpl.java">http://hg.openjdk.java.net/jdk/jdk/file/d8327f838b88/src/java.base/linux/classes/sun/nio/ch/EPollSelectorImpl.java</a>)。</p>
|
||||
<pre><code>Windows 上 NIO2(AIO)模式则是依赖于 iocp(http://hg.openjdk.java.net/jdk/jdk/file/d8327f838b88/src/java.base/windows/classes/sun/nio/ch/Iocp.java)。
|
||||
|
||||
</code></pre>
|
||||
<ul>
|
||||
<li>Chartset,提供 Unicode 字符串定义,NIO 也提供了相应的编解码器等,例如,通过下面的方式进行字符串到 ByteBuffer 的转换:</li>
|
||||
</ul>
|
||||
<pre><code>Charset.defaultCharset().encode("Hello world!"));
|
||||
|
||||
</code></pre>
|
||||
<p>2.NIO 能解决什么问题?</p>
|
||||
<p>下面我通过一个典型场景,来分析为什么需要 NIO,为什么需要多路复用。设想,我们需要实现一个服务器应用,只简单要求能够同时服务多个客户端请求即可。</p>
|
||||
@@ -1669,6 +1711,7 @@ throws IOException
|
||||
<p>但是请注意,Direct Buffer 创建和销毁过程中,都会比一般的堆内 Buffer 增加部分开销,所以通常都建议用于长期使用、数据较大的场景。</p>
|
||||
<p>使用 Direct Buffer,我们需要清楚它对内存和 JVM 参数的影响。首先,因为它不在堆上,所以 Xmx 之类参数,其实并不能影响 Direct Buffer 等堆外成员所使用的内存额度,我们可以使用下面参数设置大小:</p>
|
||||
<pre><code>-XX:MaxDirectMemorySize=512M
|
||||
|
||||
</code></pre>
|
||||
<p>从参数设置和内存问题排查角度来看,这意味着我们在计算 Java 可以使用的内存大小的时候,不能只考虑堆的需要,还有 Direct Buffer 等一系列堆外因素。如果出现内存不足,堆外内存占用也是一种可能性。</p>
|
||||
<p>另外,大多数垃圾收集过程中,都不会主动收集 Direct Buffer,它的垃圾收集过程,就是基于我在专栏前面所介绍的 Cleaner(一个内部实现)和幻象引用(PhantomReference)机制,其本身不是 public 类型,内部实现了一个 Deallocator 负责销毁的逻辑。对它的销毁往往要拖到 full GC 的时候,所以使用不当很容易导致 OutOfMemoryError。</p>
|
||||
@@ -1681,6 +1724,7 @@ throws IOException
|
||||
<p>\5. 跟踪和诊断 Direct Buffer 内存占用?</p>
|
||||
<p>因为通常的垃圾收集日志等记录,并不包含 Direct Buffer 等信息,所以 Direct Buffer 内存诊断也是个比较头疼的事情。幸好,在 JDK 8 之后的版本,我们可以方便地使用 Native Memory Tracking(NMT)特性来进行诊断,你可以在程序启动时加上下面参数:</p>
|
||||
<pre><code>-XX:NativeMemoryTracking={summary|detail}
|
||||
|
||||
</code></pre>
|
||||
<p>注意,激活 NMT 通常都会导致 JVM 出现 5%~10% 的性能下降,请谨慎考虑。</p>
|
||||
<p>运行时,可以采用下面命令进行交互式对比:</p>
|
||||
@@ -1774,12 +1818,14 @@ public short doSomething() {
|
||||
<p><strong>OOP 原则实践中的取舍</strong></p>
|
||||
<p>值得注意的是,现代语言的发展,很多时候并不是完全遵守前面的原则的,比如,Java 10 中引入了本地方法类型推断和 var 类型。按照,里氏替换原则,我们通常这样定义变量:</p>
|
||||
<pre><code>List<String> list = new ArrayList<>();
|
||||
|
||||
</code></pre>
|
||||
<p>如果使用 var 类型,可以简化为</p>
|
||||
<pre><code>var list = new ArrayList<String>();
|
||||
</code></pre>
|
||||
<p>但是,list 实际会被推断为“ArrayList < String >”</p>
|
||||
<pre><code>ArrayList<String> list = new ArrayList<String>();
|
||||
|
||||
</code></pre>
|
||||
<p>理论上,这种语法上的便利,其实是增强了程序对实现的依赖,但是微小的类型泄漏却带来了书写的便利和代码可读性的提高,所以,实践中我们还是要按照得失利弊进行选择,而不是一味得遵循原则。</p>
|
||||
<p><strong>OOP 原则在面试题目中的分析</strong></p>
|
||||
@@ -1839,6 +1885,7 @@ public short doSomething() {
|
||||
<p>因为装饰器模式本质上是包装同类型实例,我们对目标对象的调用,往往会通过包装类覆盖过的方法,迂回调用被包装的实例,这就可以很自然地实现增加额外逻辑的目的,也就是所谓的“装饰”。</p>
|
||||
<p>例如,BufferedInputStream 经过包装,为输入流过程增加缓存,类似这种装饰器还可以多次嵌套,不断地增加不同层次的功能。</p>
|
||||
<pre><code>public BufferedInputStream(InputStream in)
|
||||
|
||||
</code></pre>
|
||||
<p>我在下面的类图里,简单总结了 InputStream 的装饰模式实践。</p>
|
||||
<p><img src="assets/77ad2dc2513da8155a3781e8291fac33.png" alt="img" /></p>
|
||||
@@ -2038,10 +2085,12 @@ Observed data race, former is 13097, latter is 13099
|
||||
<p>我会在下一讲,对 synchronized 和其他锁实现的更多底层细节进行深入分析。</p>
|
||||
<p>代码中使用 synchronized 非常便利,如果用来修饰静态方法,其等同于利用下面代码将方法体囊括进来:</p>
|
||||
<pre><code>synchronized (ClassName.class) {}
|
||||
|
||||
</code></pre>
|
||||
<p>再来看看 ReentrantLock。你可能好奇什么是再入?它是表示当一个线程试图获取一个它已经获取的锁时,这个获取动作就自动成功,这是对锁获取粒度的一个概念,也就是锁的持有是以线程为单位而不是基于调用次数。Java 锁实现强调再入性是为了和 pthread 的行为进行区分。</p>
|
||||
<p>再入锁可以设置公平性(fairness),我们可在创建再入锁时选择是否是公平的。</p>
|
||||
<pre><code>ReentrantLock fairLock = new ReentrantLock(true);
|
||||
|
||||
</code></pre>
|
||||
<p>这里所谓的公平性是指在竞争场景中,当公平性为真时,会倾向于将锁赋予等待时间最久的线程。公平性是减少线程“饥饿”(个别线程长期等待锁,但始终无法获取)情况发生的一个办法。</p>
|
||||
<p>如果使用 synchronized,我们根本<strong>无法进行</strong>公平性的选择,其永远是不公平的,这也是主流操作系统线程调度的选择。通用场景中,公平性未必有想象中的那么重要,Java 默认的调度策略很少会导致 “饥饿”发生。与此同时,若要保证公平性则会引入额外开销,自然会导致一定的吞吐量下降。所以,我建议<strong>只有</strong>当你的程序确实有公平性需要的时候,才有必要指定它。</p>
|
||||
@@ -2305,6 +2354,7 @@ public ArrayBlockingQueue(int capacity, boolean fair) {
|
||||
<li>计时等待(TIMED_WAIT),其进入条件和等待状态类似,但是调用的是存在超时条件的方法,比如 wait 或 join 等方法的指定超时版本,如下面示例:</li>
|
||||
</ul>
|
||||
<pre><code>public final native void wait(long timeout) throws InterruptedException;
|
||||
|
||||
</code></pre>
|
||||
<ul>
|
||||
<li>终止(TERMINATED),不管是意外退出还是正常执行结束,线程已经完成使命,终止运行,也有人把这个状态叫作死亡。</li>
|
||||
@@ -2467,6 +2517,7 @@ waitForAConfition(...);
|
||||
<p>首先,可以使用 jps 或者系统的 ps 命令、任务管理器等工具,确定进程 ID。</p>
|
||||
<p>其次,调用 jstack 获取线程栈:</p>
|
||||
<pre><code>${JAVA_HOME}\bin\jstack your_pid
|
||||
|
||||
</code></pre>
|
||||
<p>然后,分析得到的输出,具体片段如下:</p>
|
||||
<p><img src="assets/1fcc1a521b801a5f7428d5229525a38b.png" alt="img" /></p>
|
||||
@@ -2842,6 +2893,7 @@ void put(E e) throws InterruptedException;
|
||||
<li>ArrayBlockingQueue 是最典型的的有界队列,其内部以 final 的数组保存数据,数组的大小就决定了队列的边界,所以我们在创建 ArrayBlockingQueue 时,都要指定容量,如</li>
|
||||
</ul>
|
||||
<pre><code>public ArrayBlockingQueue(int capacity, boolean fair)
|
||||
|
||||
</code></pre>
|
||||
<ul>
|
||||
<li>LinkedBlockingQueue,容易被误解为无边界,但其实其行为和内部代码都是基于有界的逻辑实现的,只不过如果我们没有在创建队列时就指定容量,那么其容量限制就自动被设置为 Integer.MAX_VALUE,成为了无界队列。</li>
|
||||
@@ -3133,6 +3185,7 @@ private volatile int value;
|
||||
</code></pre>
|
||||
<p>而类似 compareAndSet 这种返回 boolean 类型的函数,因为其返回值表现的就是成功与否,所以不需要重试。</p>
|
||||
<pre><code>public final boolean compareAndSet(int expectedValue, int newValue)
|
||||
|
||||
</code></pre>
|
||||
<p>CAS 是 Java 并发中所谓 lock-free 机制的基础。</p>
|
||||
<h2>考点分析</h2>
|
||||
@@ -3191,6 +3244,7 @@ private void acquireLock(){
|
||||
<li>一个 volatile 的整数成员表征状态,同时提供了 setState 和 getState 方法</li>
|
||||
</ul>
|
||||
<pre><code>private volatile int state;
|
||||
|
||||
</code></pre>
|
||||
<ul>
|
||||
<li>一个先入先出(FIFO)的等待线程队列,以实现多线程间竞争和等待,这是 AQS 机制的核心之一。</li>
|
||||
@@ -3361,6 +3415,7 @@ java -Xbootclasspath/p:<your_dir> your_App
|
||||
</ul>
|
||||
<p>首先,确认要修改的类文件已经编译好,并按照对应模块(假设是 java.base)结构存放, 然后,给模块打补丁:</p>
|
||||
<pre><code>java --patch-module java.base=your_patch yourApp
|
||||
|
||||
</code></pre>
|
||||
<ul>
|
||||
<li>扩展类加载器被重命名为平台类加载器(Platform Class-Loader),而且 extension 机制则被移除。也就意味着,如果我们指定 java.ext.dirs 环境变量,或者 lib/ext 目录存在,JVM 将直接返回<strong>错误</strong>!建议解决办法就是将其放入 classpath 里。</li>
|
||||
@@ -3574,6 +3629,7 @@ cw.visitEnd(); // 结束类字节码生成
|
||||
<li>在<a href="http://hg.openjdk.java.net/jdk/jdk/file/9f62267e79df/src/java.base/share/classes/java/nio/Bits.java">java.nio.BIts.reserveMemory()</a> 方法中,我们能清楚的看到,System.gc() 会被调用,以清理空间,这也是为什么在大量使用 NIO 的 Direct Buffer 之类时,通常建议不要加下面的参数,毕竟是个最后的尝试,有可能避免一定的内存不足问题。</li>
|
||||
</ul>
|
||||
<pre><code>-XX:+DisableExplictGC
|
||||
|
||||
</code></pre>
|
||||
<p>当然,也不是在任何情况下垃圾收集器都会被触发的,比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM 可以判断出垃圾收集并不能解决这个问题,所以直接抛出 OutOfMemoryError。</p>
|
||||
<p>从我前面分析的数据区的角度,除了程序计数器,其他区域都有可能会因为可能的空间不足发生 OutOfMemoryError,简单总结如下:</p>
|
||||
@@ -3637,27 +3693,32 @@ cw.visitEnd(); // 结束类字节码生成
|
||||
<li>最大堆体积</li>
|
||||
</ul>
|
||||
<pre><code>-Xmx value
|
||||
|
||||
</code></pre>
|
||||
<ul>
|
||||
<li>初始的最小堆体积</li>
|
||||
</ul>
|
||||
<pre><code>-Xms value
|
||||
|
||||
</code></pre>
|
||||
<ul>
|
||||
<li>老年代和新生代的比例</li>
|
||||
</ul>
|
||||
<pre><code>-XX:NewRatio=value
|
||||
|
||||
</code></pre>
|
||||
<p>默认情况下,这个数值是 2,意味着老年代是新生代的 2 倍大;换句话说,新生代是堆大小的 1/3。</p>
|
||||
<ul>
|
||||
<li>当然,也可以不用比例的方式调整新生代的大小,直接指定下面的参数,设定具体的内存大小数值。</li>
|
||||
</ul>
|
||||
<pre><code>-XX:NewSize=value
|
||||
|
||||
</code></pre>
|
||||
<ul>
|
||||
<li>Eden 和 Survivor 的大小是按照比例设置的,如果 SurvivorRatio 是 8,那么 Survivor 区域就是 Eden 的 1/8 大小,也就是新生代的 1/10,因为 YoungGen=Eden + 2*Survivor,JVM 参数格式是</li>
|
||||
</ul>
|
||||
<pre><code>-XX:SurvivorRatio=value
|
||||
|
||||
</code></pre>
|
||||
<ul>
|
||||
<li>TLAB 当然也可以调整,JVM 实现了复杂的适应策略,如果你有兴趣可以参考这篇<a href="https://blogs.oracle.com/jonthecollector/the-real-thing">说明</a>。</li>
|
||||
@@ -3670,9 +3731,11 @@ cw.visitEnd(); // 结束类字节码生成
|
||||
<p>接下来我会依赖 NMT 特性对 JVM 进行分析,它所提供的详细分类信息,非常有助于理解 JVM 内部实现。</p>
|
||||
<p>首先来做些准备工作,开启 NMT 并选择 summary 模式,</p>
|
||||
<pre><code>-XX:NativeMemoryTracking=summary
|
||||
|
||||
</code></pre>
|
||||
<p>为了方便获取和对比 NMT 输出,选择在应用退出时打印 NMT 统计信息</p>
|
||||
<pre><code>-XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics
|
||||
|
||||
</code></pre>
|
||||
<p>然后,执行一个简单的在标准输出打印 HelloWorld 的程序,就可以得到下面的输出<img src="assets/55f1c7f0550adbbcc885c97a4dd426bb.png" alt="img" /></p>
|
||||
<p>我来仔细分析一下,NMT 所表征的 JVM 本地内存使用:</p>
|
||||
@@ -3681,9 +3744,11 @@ cw.visitEnd(); // 结束类字节码生成
|
||||
<li>第二部分是 Class 内存占用,它所统计的就是 Java 类元数据所占用的空间,JVM 可以通过类似下面的参数调整其大小:</li>
|
||||
</ul>
|
||||
<pre><code>-XX:MaxMetaspaceSize=value
|
||||
|
||||
</code></pre>
|
||||
<p>对于本例,因为 HelloWorld 没有什么用户类库,所以其内存占用主要是启动类加载器(Bootstrap)加载的核心类库。你可以使用下面的小技巧,调整启动类加载器元数据区,这主要是为了对比以加深理解,也许只有在 hack JDK 时才有实际意义。</p>
|
||||
<pre><code>-XX:InitialBootClassLoaderMetaspaceSize=30720
|
||||
|
||||
</code></pre>
|
||||
<ul>
|
||||
<li>下面是 Thread,这里既包括 Java 线程,如程序主线程、Cleaner 线程等,也包括 GC 等本地线程。你有没有注意到,即使是一个 HelloWorld 程序,这个线程数量竟然还有 25。似乎有很多浪费,设想我们要用 Java 作为 Serverless 运行时,每个 function 是非常短暂的,如何降低线程数量呢? 如果你充分理解了专栏讲解的内容,对 JVM 内部有了充分理解,思路就很清晰了: JDK 9 的默认 GC 是 G1,虽然它在较大堆场景表现良好,但本身就会比传统的 Parallel GC 或者 Serial GC 之类复杂太多,所以要么降低其并行线程数目,要么直接切换 GC 类型; JIT 编译默认是开启了 TieredCompilation 的,将其关闭,那么 JIT 也会变得简单,相应本地线程也会减少。 我们来对比一下,这是默认参数情况的输出:<img src="assets/97d060b306e44af3a8443f932a0a4d42.png" alt="img" /></li>
|
||||
@@ -3694,7 +3759,9 @@ cw.visitEnd(); // 结束类字节码生成
|
||||
<li>接下来是 Code 统计信息,显然这是 CodeCache 相关内存,也就是 JIT compiler 存储编译热点方法等信息的地方,JVM 提供了一系列参数可以限制其初始值和最大值等,例如:</li>
|
||||
</ul>
|
||||
<pre><code>-XX:InitialCodeCacheSize=value
|
||||
|
||||
-XX:ReservedCodeCacheSize=value
|
||||
|
||||
</code></pre>
|
||||
<p>你可以设置下列 JVM 参数,也可以只设置其中一个,进一步判断不同参数对 CodeCache 大小的影响。<img src="assets/945740c37433f783d2d877c67dcc1170.png" alt="img" /><img src="http://192.168.73.85:8080/%E6%9E%81%E5%AE%A2%E6%97%B6%E9%97%B4/Java%E5%9F%BA%E7%A1%8036%E8%AE%B2/assets/82d1fbc9ca09698c01ccff18fb97c8cd.png" alt="img" /></p>
|
||||
<p>很明显,CodeCache 空间下降非常大,这是因为我们关闭了复杂的 TieredCompilation,而且还限制了其初始大小。</p>
|
||||
@@ -3703,6 +3770,7 @@ cw.visitEnd(); // 结束类字节码生成
|
||||
</ul>
|
||||
<p>使用命令:</p>
|
||||
<pre><code>-XX:+UseSerialGC
|
||||
|
||||
</code></pre>
|
||||
<p><img src="assets/6eeee6624c7dc6be54bfce5e93064233.png" alt="img" /></p>
|
||||
<p>可见,不仅总线程数大大降低(25 → 13),而且 GC 设施本身的内存开销就少了非常多。据我所知,AWS Lambda 中 Java 运行时就是使用的 Serial GC,可以大大降低单个 function 的启动和运行开销。</p>
|
||||
@@ -3723,17 +3791,20 @@ cw.visitEnd(); // 结束类字节码生成
|
||||
<li>Serial GC,它是最古老的垃圾收集器,“Serial”体现在其收集工作是单线程的,并且在进行垃圾收集过程中,会进入臭名昭著的“Stop-The-World”状态。当然,其单线程设计也意味着精简的 GC 实现,无需维护复杂的数据结构,初始化也简单,所以一直是 Client 模式下 JVM 的默认选项。 从年代的角度,通常将其老年代实现单独称作 Serial Old,它采用了标记 - 整理(Mark-Compact)算法,区别于新生代的复制算法。 Serial GC 的对应 JVM 参数是:</li>
|
||||
</ul>
|
||||
<pre><code>-XX:+UseSerialGC
|
||||
|
||||
</code></pre>
|
||||
<ul>
|
||||
<li>ParNew GC,很明显是个新生代 GC 实现,它实际是 Serial GC 的多线程版本,最常见的应用场景是配合老年代的 CMS GC 工作,下面是对应参数</li>
|
||||
</ul>
|
||||
<pre><code>-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
|
||||
|
||||
</code></pre>
|
||||
<ul>
|
||||
<li>CMS(Concurrent Mark Sweep) GC,基于标记 - 清除(Mark-Sweep)算法,设计目标是尽量减少停顿时间,这一点对于 Web 等反应时间敏感的应用非常重要,一直到今天,仍然有很多系统使用 CMS GC。但是,CMS 采用的标记 - 清除算法,存在着内存碎片化问题,所以难以避免在长时间运行等情况下发生 full GC,导致恶劣的停顿。另外,既然强调了并发(Concurrent),CMS 会占用更多 CPU 资源,并和用户线程争抢。</li>
|
||||
<li>Parrallel GC,在早期 JDK 8 等版本中,它是 server 模式 JVM 的默认 GC 选择,也被称作是吞吐量优先的 GC。它的算法和 Serial GC 比较相似,尽管实现要复杂的多,其特点是新生代和老年代 GC 都是并行进行的,在常见的服务器环境中更加高效。 开启选项是:</li>
|
||||
</ul>
|
||||
<pre><code>-XX:+UseParallelGC
|
||||
|
||||
</code></pre>
|
||||
<p>另外,Parallel GC 引入了开发者友好的配置项,我们可以直接设置暂停时间或吞吐量等目标,JVM 会自动进行适应性调整,例如下面参数:</p>
|
||||
<pre><code>-XX:MaxGCPauseMillis=value
|
||||
@@ -3761,6 +3832,7 @@ cw.visitEnd(); // 结束类字节码生成
|
||||
</ul>
|
||||
<p>方法区无用元数据的回收比较复杂,我简单梳理一下。还记得我对类加载器的分类吧,一般来说初始化类加载器加载的类型是不会进行类卸载(unload)的;而普通的类型的卸载,往往是要求相应自定义类加载器本身被回收,所以大量使用动态类型的场合,需要防止元数据区(或者早期的永久代)不会 OOM。在 8u40 以后的 JDK 中,下面参数已经是默认的:</p>
|
||||
<pre><code>-XX:+ClassUnloadingWithConcurrentMark
|
||||
|
||||
</code></pre>
|
||||
<p>第二,常见的垃圾收集算法,我认为总体上有个了解,理解相应的原理和优缺点,就已经足够了,其主要分为三类:</p>
|
||||
<ul>
|
||||
@@ -3777,6 +3849,7 @@ cw.visitEnd(); // 结束类字节码生成
|
||||
<p>第二, 经过一次 Minor GC,Eden 就会空闲下来,直到再次达到 Minor GC 触发条件,这时候,另外一个 Survivor 区域则会成为 to 区域,Eden 区域的存活对象和 From 区域对象,都会被复制到 to 区域,并且存活的年龄计数会被加 1。<img src="assets/3be4ac4834e2790a8211252f2bebfd48.png" alt="img" /></p>
|
||||
<p>第三, 类似第二步的过程会发生很多次,直到有对象年龄计数达到阈值,这时候就会发生所谓的晋升(Promotion)过程,如下图所示,超过阈值的对象会被晋升到老年代。这个阈值是可以通过参数指定:</p>
|
||||
<pre><code>-XX:MaxTenuringThreshold=<N>
|
||||
|
||||
</code></pre>
|
||||
<p><img src="assets/dbcb15c99b368773145b358734e10e8d.png" alt="img" /></p>
|
||||
<p>后面就是老年代 GC,具体取决于选择的 GC 选项,对应不同的算法。下面是一个简单标记 - 整理算法过程示意图,老年代中的无用对象被清除后, GC 会将对象进行整理,以防止内存碎片化。</p>
|
||||
@@ -4096,16 +4169,20 @@ while (!initialized)
|
||||
<p>首先,就是最常见的 SQL 注入攻击。一个典型的场景就是 Web 系统的用户登录功能,根据用户输入的用户名和密码,我们需要去后端数据库核实信息。</p>
|
||||
<p>假设应用逻辑是,后端程序利用界面输入动态生成类似下面的 SQL,然后让 JDBC 执行。</p>
|
||||
<pre><code>Select * from use_info where username = “input_usr_name” and password = “input_pwd”
|
||||
|
||||
</code></pre>
|
||||
<p>但是,如果我输入的 input_pwd 是类似下面的文本,</p>
|
||||
<pre><code>“ or “”=”
|
||||
|
||||
</code></pre>
|
||||
<p>那么,拼接出的 SQL 字符串就变成了下面的条件,OR 的存在导致输入什么名字都是复合条件的。</p>
|
||||
<pre><code>Select * from use_info where username = “input_usr_name” and password = “” or “” = “”
|
||||
|
||||
</code></pre>
|
||||
<p>这里只是举个简单的例子,它是利用了期望输入和可能输入之间的偏差。上面例子中,期望用户输入一个数值,但实际输入的则是 SQL 语句片段。类似场景可以利用注入的不同 SQL 语句,进行各种不同目的的攻击,甚至还可以加上“;delete xxx”之类语句,如果数据库权限控制不合理,攻击效果就可能是灾难性的。</p>
|
||||
<p>第二,操作系统命令注入。Java 语言提供了类似 Runtime.exec(…) 的 API,可以用来执行特定命令,假设我们构建了一个应用,以输入文本作为参数,执行下面的命令:</p>
|
||||
<pre><code>ls –la input_file_name
|
||||
|
||||
</code></pre>
|
||||
<p>但是如果用户输入是 “input_file_name;rm –rf /*”,这就有可能出现问题了。当然,这只是个举例,Java 标准类库本身进行了非常多的改进,所以类似这种编程错误,未必可以真的完成攻击,但其反映的一类场景是真实存在的。</p>
|
||||
<p>第三,XML 注入攻击。Java 核心类库提供了全面的 XML 处理、转换等各种 API,而 XML 自身是可以包含动态内容的,例如 XPATH,如果使用不当,可能导致访问恶意内容。</p>
|
||||
@@ -4144,6 +4221,7 @@ while (!initialized)
|
||||
</ul>
|
||||
<p>在应用实践中,如果对安全要求非常高,建议打开 SecurityManager,</p>
|
||||
<pre><code>-Djava.security.manager
|
||||
|
||||
</code></pre>
|
||||
<p>请注意其开销,通常只要开启 SecurityManager,就会导致 10% ~ 15% 的性能下降,在 JDK 9 以后,这个开销有所改善。</p>
|
||||
<p>理解了基础 Java 安全机制,接下来我们来一起探讨安全漏洞(<a href="https://en.wikipedia.org/wiki/Vulnerability_(computing)">Vulnerability</a>)。</p>
|
||||
@@ -4195,6 +4273,7 @@ if (a + b < c) {
|
||||
<p>从语言特性来说,Java 和 JVM 提供了很多基础性的改进,相比于传统的 C、C++ 等语言,对于数组越界等处理要完善的多,原生的避免了<a href="https://en.wikipedia.org/wiki/Buffer_overflow">缓冲区溢出</a>等攻击方式,提高了软件的安全性。但这并不代表完全杜绝了问题,Java 程序可能调用本地代码,也就是 JNI 技术,错误的数值可能导致 C/C++ 层面的数据越界等问题,这是很危险的。</p>
|
||||
<p>所以,上面的条件判断,需要判断其数值范围,例如,写成类似下面结构。</p>
|
||||
<pre><code>if (a < c – b)
|
||||
|
||||
</code></pre>
|
||||
<p>再来看一个例子,请看下面的一段异常处理代码:</p>
|
||||
<pre><code>try {
|
||||
@@ -4283,17 +4362,20 @@ throw new RuntimeException(hostname + port + “ doesn’t response”);
|
||||
<li>利用 top 命令获取相应 pid,“-H”代表 thread 模式,你可以配合 grep 命令更精准定位。</li>
|
||||
</ul>
|
||||
<pre><code>top –H
|
||||
|
||||
</code></pre>
|
||||
<ul>
|
||||
<li>然后转换成为 16 进制。</li>
|
||||
</ul>
|
||||
<pre><code>printf "%x" your_pid
|
||||
|
||||
</code></pre>
|
||||
<ul>
|
||||
<li>最后利用 jstack 获取的线程栈,对比相应的 ID 即可。</li>
|
||||
</ul>
|
||||
<p>当然,还有更加通用的诊断方向,利用 vmstat 之类,查看上下文切换的数量,比如下面就是指定时间间隔为 1,收集 10 次。</p>
|
||||
<pre><code>vmstat -1 -10
|
||||
|
||||
</code></pre>
|
||||
<p>输出如下:<img src="assets/abd28cb4a771365211e1a01d628213a0.png" alt="img" /></p>
|
||||
<p>如果每秒上下文(cs,<a href="https://en.wikipedia.org/wiki/Context_switch">context switch</a>)切换很高,并且比系统中断高很多(in,system <a href="https://en.wikipedia.org/wiki/Interrupt">interrupt</a>),就表明很有可能是因为不合理的多线程调度所导致。当然还需要利用<a href="https://linux.die.net/man/1/pidstat">pidstat</a>等手段,进行更加具体的定位,我就不再进一步展开了。</p>
|
||||
@@ -4316,6 +4398,7 @@ throw new RuntimeException(hostname + port + “ doesn’t response”);
|
||||
<p>所以,JFR/JMC 完全具备了生产系统 Profiling 的能力,目前也确实在真正大规模部署的云产品上使用过相关技术,快速地定位了问题。</p>
|
||||
<p>它的使用也非常方便,你不需要重新启动系统或者提前增加配置。例如,你可以在运行时启动 JFR 记录,并将这段时间的信息写入文件:</p>
|
||||
<pre><code>Jcmd <pid> JFR.start duration=120s filename=myrecording.jfr
|
||||
|
||||
</code></pre>
|
||||
<p>然后,使用 JMC 打开“.jfr 文件”就可以进行分析了,方法、异常、线程、IO 等应有尽有,其功能非常强大。如果你想了解更多细节,可以参考相关<a href="https://blog.takipi.com/oracle-java-mission-control-the-ultimate-guide/">指南</a>。</p>
|
||||
<p>今天我从一个典型性能问题出发,从症状表现到具体的系统分析、JVM 分析,系统性地整理了常见性能分析的思路;并且在知识扩展部分,从方法论和实际操作的角度,让你将理论和实际结合,相信一定可以对你有所帮助。</p>
|
||||
@@ -4390,9 +4473,11 @@ public void testMethod() {
|
||||
</code></pre>
|
||||
<p>当我们实现了具体的测试后,就可以利用下面的 Maven 命令构建。</p>
|
||||
<pre><code>mvn clean install
|
||||
|
||||
</code></pre>
|
||||
<p>运行基准测试则与运行不同的 Java 应用没有明显区别。</p>
|
||||
<pre><code>java -jar target/benchmarks.jar
|
||||
|
||||
</code></pre>
|
||||
<p>更加具体的上手步骤,请参考相关<a href="http://www.baeldung.com/java-microbenchmark-harness">指南</a>。JMH 处处透着浓浓的工程师味道,并没有纠结于完善的文档,而是提供了非常棒的<a href="http://hg.openjdk.java.net/code-tools/jmh/file/3769055ad883/jmh-samples/src/main/java/org/openjdk/jmh/samples">样例代码</a>,所以你需要习惯于直接从代码中学习。</p>
|
||||
<p><strong>如何保证微基准测试的正确性,有哪些坑需要规避?</strong></p>
|
||||
@@ -4403,9 +4488,11 @@ public void testMethod() {
|
||||
<li>保证代码经过了足够并且合适的预热。我在<a href="http://time.geekbang.org/column/article/6845">专栏第 1 讲</a>中提到过,默认情况,在 server 模式下,JIT 会在一段代码执行 10000 次后,将其编译为本地代码,client 模式则是 1500 次以后。我们需要排除代码执行初期的噪音,保证真正采样到的统计数据符合其稳定运行状态。 通常建议使用下面的参数来判断预热工作到底是经过了多久。</li>
|
||||
</ul>
|
||||
<pre><code>-XX:+PrintCompilation
|
||||
|
||||
</code></pre>
|
||||
<p>我这里建议考虑另外加上一个参数,否则 JVM 将默认开启后台编译,也就是在其他线程进行,可能导致输出的信息有些混淆。</p>
|
||||
<pre><code>-Xbatch
|
||||
|
||||
</code></pre>
|
||||
<p>与此同时,也要保证预热阶段的代码路径和采集阶段的代码路径是一致的,并且可以观察 PrintCompilation 输出是否在后期运行中仍然有零星的编译语句出现。</p>
|
||||
<ul>
|
||||
@@ -4444,6 +4531,7 @@ public void testMethod(MyState state, Blackhole blackhole) {
|
||||
<li>如果你希望确定方法内联(Inlining)对性能的影响,可以考虑打开下面的选项。</li>
|
||||
</ul>
|
||||
<pre><code>-XX:+PrintInlining
|
||||
|
||||
</code></pre>
|
||||
<p>从上面的总结,可以看出来微基准测试是一个需要高度了解 Java、JVM 底层机制的技术,是个非常好的深入理解程序背后效果的工具,但是也反映了我们需要审慎对待微基准测试,不被可能的假象蒙蔽。</p>
|
||||
<p>我今天介绍的内容是相对常见并易于把握的,对于微基准测试,GC 等基层机制同样会影响其统计数据。我在前面提到,微基准测试通常希望执行时间和内存分配速率都控制在有限范围内,而在这个过程中发生 GC,很可能导致数据出现偏差,所以 Serial GC 是个值得考虑的选项。另外,JDK 11 引入了<a href="https://openjdk.java.net/jeps/318">Epsilon GC</a>,可以考虑使用这种什么也不做的 GC 方式,从最大可能性去排除相关影响。</p>
|
||||
@@ -4487,14 +4575,17 @@ public void testMethod(MyState state, Blackhole blackhole) {
|
||||
<li>打印编译发生的细节。</li>
|
||||
</ul>
|
||||
<pre><code>-XX:+PrintCompilation
|
||||
|
||||
</code></pre>
|
||||
<ul>
|
||||
<li>输出更多编译的细节。</li>
|
||||
</ul>
|
||||
<pre><code>-XX:UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:LogFile=<your_file_path>
|
||||
|
||||
</code></pre>
|
||||
<p>JVM 会生成一个 xml 形式的文件,另外, LogFile 选项是可选的,不指定则会输出到</p>
|
||||
<pre><code>hotspot_pid<pid>.log
|
||||
|
||||
</code></pre>
|
||||
<p>具体格式可以参考 Ben Evans 提供的<a href="https://github.com/AdoptOpenJDK/jitwatch/">JitWatch</a>工具和<a href="http://www.oracle.com/technetwork/articles/java/architect-evans-pt1-2266278.html">分析指南</a>。</p>
|
||||
<p><img src="assets/07b00499b0ca857fc3ccd51f7046d946.png" alt="img" /></p>
|
||||
@@ -4502,6 +4593,7 @@ public void testMethod(MyState state, Blackhole blackhole) {
|
||||
<li>打印内联的发生,可利用下面的诊断选项,也需要明确解锁。</li>
|
||||
</ul>
|
||||
<pre><code>-XX:+PrintInlining
|
||||
|
||||
</code></pre>
|
||||
<ul>
|
||||
<li>如何知晓 Code Cache 的使用状态呢?</li>
|
||||
@@ -4513,21 +4605,26 @@ public void testMethod(MyState state, Blackhole blackhole) {
|
||||
</ul>
|
||||
<p>我曾经介绍过 JIT 的默认门限,server 模式默认 10000 次,client 是 1500 次。门限大小也存在着调优的可能,可以使用下面的参数调整;与此同时,该参数还可以变相起到降低预热时间的作用。</p>
|
||||
<pre><code>-XX:CompileThreshold=N
|
||||
|
||||
</code></pre>
|
||||
<p>很多人可能会产生疑问,既然是热点,不是早晚会达到门限次数吗?这个还真未必,因为 JVM 会周期性的对计数的数值进行衰减操作,导致调用计数器永远不能达到门限值,除了可以利用 CompileThreshold 适当调整大小,还有一个办法就是关闭计数器衰减。</p>
|
||||
<pre><code>-XX:-UseCounterDecay
|
||||
|
||||
</code></pre>
|
||||
<p>如果你是利用 debug 版本的 JDK,还可以利用下面的参数进行试验,但是生产版本是不支持这个选项的。</p>
|
||||
<pre><code>-XX:CounterHalfLifeTime
|
||||
|
||||
</code></pre>
|
||||
<ul>
|
||||
<li>调整 Code Cache 大小</li>
|
||||
</ul>
|
||||
<p>我们知道 JIT 编译的代码是存储在 Code Cache 中的,需要注意的是 Code Cache 是存在大小限制的,而且不会动态调整。这意味着,如果 Code Cache 太小,可能只有一小部分代码可以被 JIT 编译,其他的代码则没有选择,只能解释执行。所以,一个潜在的调优点就是调整其大小限制。</p>
|
||||
<pre><code>-XX:ReservedCodeCacheSize=<SIZE>
|
||||
|
||||
</code></pre>
|
||||
<p>当然,也可以调整其初始大小。</p>
|
||||
<pre><code>-XX:InitialCodeCacheSize=<SIZE>
|
||||
|
||||
</code></pre>
|
||||
<p>注意,在相对较新版本的 Java 中,由于分层编译(Tiered-Compilation)的存在,Code Cache 的空间需求大大增加,其本身默认大小也被提高了。</p>
|
||||
<ul>
|
||||
@@ -4535,6 +4632,7 @@ public void testMethod(MyState state, Blackhole blackhole) {
|
||||
</ul>
|
||||
<p>JVM 的编译器线程数目与我们选择的模式有关,选择 client 模式默认只有一个编译线程,而 server 模式则默认是两个,如果是当前最普遍的分层编译模式,则会根据 CPU 内核数目计算 C1 和 C2 的数值,你可以通过下面的参数指定的编译线程数。</p>
|
||||
<pre><code>-XX:CICompilerCount=N
|
||||
|
||||
</code></pre>
|
||||
<p>在强劲的多处理器环境中,增大编译线程数,可能更加充分的利用 CPU 资源,让预热等过程更加快速;但是,反之也可能导致编译线程争抢过多资源,尤其是当系统非常繁忙时。例如,系统部署了多个 Java 应用实例的时候,那么减小编译线程数目,则是可以考虑的。</p>
|
||||
<p>生产实践中,也有人推荐在服务器上关闭分层编译,直接使用 server 编译器,虽然会导致稍慢的预热速度,但是可能在特定工作负载上会有微小的吞吐量提高。</p>
|
||||
@@ -4543,6 +4641,7 @@ public void testMethod(MyState state, Blackhole blackhole) {
|
||||
</ul>
|
||||
<p>比如,减少进入安全点。严格说,它远远不只是发生在动态编译的时候,GC 阶段发生的更加频繁,你可以利用下面选项诊断安全点的影响。</p>
|
||||
<pre><code>-XX:+PrintSafepointStatistics ‑XX:+PrintGCApplicationStoppedTime
|
||||
|
||||
</code></pre>
|
||||
<p>注意,在 JDK 9 之后,PrintGCApplicationStoppedTime 已经被移除了,你需要使用“-Xlog:safepoint”之类方式来指定。</p>
|
||||
<p>很多优化阶段都可能和安全点相关,例如:</p>
|
||||
@@ -4551,6 +4650,7 @@ public void testMethod(MyState state, Blackhole blackhole) {
|
||||
<li>常规的锁优化阶段也可能发生,比如,偏斜锁的设计目的是为了避免无竞争时的同步开销,但是当真的发生竞争时,撤销偏斜锁会触发安全点,是很重的操作。所以,在并发场景中偏斜锁的价值其实是被质疑的,经常会明确建议关闭偏斜锁。</li>
|
||||
</ul>
|
||||
<pre><code>-XX:-UseBiasedLocking
|
||||
|
||||
</code></pre>
|
||||
<p>主要的优化手段就介绍到这里,这些方法都是普通 Java 开发者就可以利用的。如果你想对 JVM 优化手段有更深入的了解,建议你订阅 JVM 专家郑雨迪博士的专栏。</p>
|
||||
<h2>一课一练</h2>
|
||||
@@ -4838,21 +4938,25 @@ public void testMethod(MyState state, Blackhole blackhole) {
|
||||
<a href="/极客时间/Java错误示例100讲.md.html">下一页</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||||
</div>
|
||||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70998098df1d8b66","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709ba3a57eaefbdc","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||||
</body>
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-NPSEEVD756');
|
||||
var path = window.location.pathname
|
||||
@@ -4866,12 +4970,14 @@ public void testMethod(MyState state, Blackhole blackhole) {
|
||||
} else {
|
||||
setCookie("lastPath", path)
|
||||
}
|
||||
|
||||
function setCookie(cname, cvalue) {
|
||||
var d = new Date();
|
||||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||||
var expires = "expires=" + d.toGMTString();
|
||||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||||
}
|
||||
|
||||
function getCookie(cname) {
|
||||
var name = cname + "=";
|
||||
var ca = document.cookie.split(';');
|
||||
@@ -4881,5 +4987,7 @@ public void testMethod(MyState state, Blackhole blackhole) {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
<!-- theme css & js -->
|
||||
<meta name="generator" content="Hexo 4.2.0">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="book-container">
|
||||
<div class="book-sidebar">
|
||||
<div class="book-brand">
|
||||
@@ -25,61 +27,86 @@
|
||||
<ul class="uncollapsible">
|
||||
<li><a href="/" class="current-tab">首页</a></li>
|
||||
</ul>
|
||||
|
||||
<ul class="uncollapsible">
|
||||
<li><a href="../">上一级</a></li>
|
||||
</ul>
|
||||
|
||||
<ul class="uncollapsible">
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/Java基础36讲.md.html">Java基础36讲.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
<a class="current-tab" href="/极客时间/Java错误示例100讲.md.html">Java错误示例100讲.md.html</a>
|
||||
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/Linux性能优化.md.html">Linux性能优化.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/MySQL实战45讲.md.html">MySQL实战45讲.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/从0开始学微服务.md.html">从0开始学微服务.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/代码精进之路.md.html">代码精进之路.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/持续交付36讲.md.html">持续交付36讲.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/程序员进阶攻略.md.html">程序员进阶攻略.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/趣谈网络协议.md.html">趣谈网络协议.md.html</a>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||||
<div class="sidebar-toggle-inner"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function add_inner() {
|
||||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||||
inner.classList.add('show')
|
||||
}
|
||||
|
||||
function remove_inner() {
|
||||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||||
inner.classList.remove('show')
|
||||
}
|
||||
|
||||
function sidebar_toggle() {
|
||||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||||
let sidebar = document.querySelector('.book-sidebar')
|
||||
@@ -95,6 +122,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function open_sidebar() {
|
||||
let sidebar = document.querySelector('.book-sidebar')
|
||||
let overlay = document.querySelector('.off-canvas-overlay')
|
||||
@@ -107,7 +135,9 @@ function hide_canvas() {
|
||||
sidebar.classList.remove('show')
|
||||
overlay.classList.remove('show')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="off-canvas-content">
|
||||
<div class="columns">
|
||||
<div class="column col-12 col-lg-12">
|
||||
@@ -139,21 +169,25 @@ function hide_canvas() {
|
||||
<a href="/极客时间/Linux性能优化.md.html">下一页</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||||
</div>
|
||||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099809e5b658b66","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709ba3a9fabffbdc","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||||
</body>
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-NPSEEVD756');
|
||||
var path = window.location.pathname
|
||||
@@ -167,12 +201,14 @@ function hide_canvas() {
|
||||
} else {
|
||||
setCookie("lastPath", path)
|
||||
}
|
||||
|
||||
function setCookie(cname, cvalue) {
|
||||
var d = new Date();
|
||||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||||
var expires = "expires=" + d.toGMTString();
|
||||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||||
}
|
||||
|
||||
function getCookie(cname) {
|
||||
var name = cname + "=";
|
||||
var ca = document.cookie.split(';');
|
||||
@@ -182,5 +218,7 @@ function hide_canvas() {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
<!-- theme css & js -->
|
||||
<meta name="generator" content="Hexo 4.2.0">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="book-container">
|
||||
<div class="book-sidebar">
|
||||
<div class="book-brand">
|
||||
@@ -25,61 +27,86 @@
|
||||
<ul class="uncollapsible">
|
||||
<li><a href="/" class="current-tab">首页</a></li>
|
||||
</ul>
|
||||
|
||||
<ul class="uncollapsible">
|
||||
<li><a href="../">上一级</a></li>
|
||||
</ul>
|
||||
|
||||
<ul class="uncollapsible">
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/Java基础36讲.md.html">Java基础36讲.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/Java错误示例100讲.md.html">Java错误示例100讲.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
<a class="current-tab" href="/极客时间/Linux性能优化.md.html">Linux性能优化.md.html</a>
|
||||
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/MySQL实战45讲.md.html">MySQL实战45讲.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/从0开始学微服务.md.html">从0开始学微服务.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/代码精进之路.md.html">代码精进之路.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/持续交付36讲.md.html">持续交付36讲.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/程序员进阶攻略.md.html">程序员进阶攻略.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/趣谈网络协议.md.html">趣谈网络协议.md.html</a>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||||
<div class="sidebar-toggle-inner"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function add_inner() {
|
||||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||||
inner.classList.add('show')
|
||||
}
|
||||
|
||||
function remove_inner() {
|
||||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||||
inner.classList.remove('show')
|
||||
}
|
||||
|
||||
function sidebar_toggle() {
|
||||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||||
let sidebar = document.querySelector('.book-sidebar')
|
||||
@@ -94,6 +121,8 @@
|
||||
content.classList.add('extend')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function open_sidebar() {
|
||||
let sidebar = document.querySelector('.book-sidebar')
|
||||
let overlay = document.querySelector('.off-canvas-overlay')
|
||||
@@ -106,7 +135,9 @@ function hide_canvas() {
|
||||
sidebar.classList.remove('show')
|
||||
overlay.classList.remove('show')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="off-canvas-content">
|
||||
<div class="columns">
|
||||
<div class="column col-12 col-lg-12">
|
||||
@@ -320,6 +351,7 @@ $ grep 'model name' /proc/cpuinfo | wc -l
|
||||
<h3>场景一:CPU 密集型进程</h3>
|
||||
<p>首先,我们在第一个终端运行 stress 命令,模拟一个 CPU 使用率 100% 的场景:</p>
|
||||
<pre><code>$ stress --cpu 1 --timeout 600
|
||||
|
||||
</code></pre>
|
||||
<p>接着,在第二个终端运行 uptime 查看平均负载的变化情况:</p>
|
||||
<pre><code># -d 参数表示高亮显示变化的区域
|
||||
@@ -346,6 +378,7 @@ $ pidstat -u 5 1
|
||||
<h3>场景二:I/O 密集型进程</h3>
|
||||
<p>首先还是运行 stress 命令,但这次模拟 I/O 压力,即不停地执行 sync:</p>
|
||||
<pre><code>$ stress -i 1 --timeout 600
|
||||
|
||||
</code></pre>
|
||||
<p>还是在第二个终端运行 uptime 查看平均负载的变化情况:</p>
|
||||
<pre><code>$ watch -d uptime
|
||||
@@ -376,6 +409,7 @@ Linux 4.15.0 (ubuntu) 09/22/18 _x86_64_ (2 CPU)
|
||||
<p>当系统中运行进程超出 CPU 运行能力时,就会出现等待 CPU 的进程。</p>
|
||||
<p>比如,我们还是使用 stress,但这次模拟的是 8 个进程:</p>
|
||||
<pre><code>$ stress -c 8 --timeout 600
|
||||
|
||||
</code></pre>
|
||||
<p>由于系统只有 2 个 CPU,明显比 8 个进程要少得多,因而,系统的 CPU 处于严重过载状态,平均负载高达 7.97:</p>
|
||||
<pre><code>$ uptime
|
||||
@@ -789,6 +823,7 @@ Time per request: 859.942 [ms] (mean)
|
||||
<p>这次,我们在第二个终端,将测试的请求总数增加到 10000。这样当你在第一个终端使用性能分析工具时, Nginx 的压力还是继续。</p>
|
||||
<p>继续在第二个终端,运行 ab 命令:</p>
|
||||
<pre><code>$ ab -c 10 -n 10000 http://10.240.0.5:10000/
|
||||
|
||||
</code></pre>
|
||||
<p>接着,回到第一个终端运行 top 命令,并按下数字 1 ,切换到每个 CPU 的使用率:</p>
|
||||
<pre><code>$ top
|
||||
@@ -910,6 +945,7 @@ Time per request: 1138.229 [ms] (mean)
|
||||
<p>这次,我们在第二个终端,将测试的并发请求数改成 5,同时把请求时长设置为 10 分钟(-t 600)。这样,当你在第一个终端使用性能分析工具时, Nginx 的压力还是继续的。</p>
|
||||
<p>继续在第二个终端运行 ab 命令:</p>
|
||||
<pre><code>$ ab -c 5 -t 600 http://192.168.0.10:10000/
|
||||
|
||||
</code></pre>
|
||||
<p>然后,我们在第一个终端运行 top 命令,观察系统的 CPU 使用情况:</p>
|
||||
<pre><code>$ top
|
||||
@@ -1072,6 +1108,7 @@ $ perf report
|
||||
<p>这时,你就得继续排查,为什么被调用的命令,会导致 CPU 使用率升高或 I/O 升高等问题。这些复杂场景的案例,我会在后面的综合实战里详细分析。</p>
|
||||
<p>最后,在案例结束时,不要忘了清理环境,执行下面的 Docker 命令,停止案例中用到的 Nginx 进程:</p>
|
||||
<pre><code>$ docker rm -f nginx phpfpm
|
||||
|
||||
</code></pre>
|
||||
<h2>execsnoop</h2>
|
||||
<p>在这个案例中,我们使用了 top、pidstat、pstree 等工具分析了系统 CPU 使用率高的问题,并发现 CPU 升高是短时进程 stress 导致的,但是整个分析过程还是比较复杂的。对于这类问题,有没有更好的方法监控呢?</p>
|
||||
@@ -1159,6 +1196,7 @@ stress 30407 30405 0 /usr/local/bin/stress -t 1 -d 1
|
||||
<h3>操作和分析</h3>
|
||||
<p>安装完成后,我们首先执行下面的命令运行案例应用:</p>
|
||||
<pre><code>$ docker run --privileged --name=app -itd feisky/app:iowait
|
||||
|
||||
</code></pre>
|
||||
<p>然后,输入 ps 命令,确认案例应用已正常启动。如果一切正常,你应该可以看到如下所示的输出:</p>
|
||||
<pre><code>$ ps aux | grep /app
|
||||
@@ -1325,6 +1363,7 @@ $ perf report
|
||||
<p>看来,罪魁祸首是 app 内部进行了磁盘的直接 I/O 啊!</p>
|
||||
<p>下面的问题就容易解决了。我们接下来应该从代码层面分析,究竟是哪里出现了直接读请求。查看源码文件 <a href="https://github.com/feiskyer/linux-perf-examples/blob/master/high-iowait-process/app.c">app.c</a>,你会发现它果然使用了 O_DIRECT 选项打开磁盘,于是绕过了系统缓存,直接对磁盘进行读写。</p>
|
||||
<pre><code>open(disk, O_RDONLY|O_DIRECT|O_LARGEFILE, 0755)
|
||||
|
||||
</code></pre>
|
||||
<p>直接读写磁盘,对 I/O 敏感型应用(比如数据库系统)是很友好的,因为你可以在应用中,直接控制磁盘的读写。但在大部分情况下,我们最好还是通过系统缓存来优化磁盘 I/O,换句话说,删除 O_DIRECT 这个选项就是了。</p>
|
||||
<p><a href="https://github.com/feiskyer/linux-perf-examples/blob/master/high-iowait-process/app-fix1.c">app-fix1.c</a> 就是修改后的文件,我也打包成了一个镜像文件,运行下面的命令,你就可以启动它了:</p>
|
||||
@@ -1883,9 +1922,11 @@ $ stress-ng -i 1 --hdd 1 --timeout 600
|
||||
<p>回忆一下我们学过的进程状态,你应该记得,等待 CPU 的进程已经在 CPU 的就绪队列中,处于运行状态;而等待 I/O 的进程则处于不可中断状态。</p>
|
||||
<p>另外,不同版本的 sysbench 运行参数也不是完全一样的。比如,在案例 Ubuntu 18.04 中,运行 sysbench 的格式为:</p>
|
||||
<pre><code>$ sysbench --threads=10 --max-time=300 threads run
|
||||
|
||||
</code></pre>
|
||||
<p>而在 Ubuntu 16.04 中,运行格式则为(感谢 Haku 留言分享的执行命令):</p>
|
||||
<pre><code>$ sysbench --num-threads=10 --max-time=300 --test=threads run
|
||||
|
||||
</code></pre>
|
||||
<h2>问题 4:无法模拟出 I/O 性能瓶颈,以及 I/O 压力过大的问题</h2>
|
||||
<p><img src="assets/9e235aca4e92b68e84dba03881c591d8.png" alt="img" /></p>
|
||||
@@ -1905,6 +1946,7 @@ $ stress-ng -i 1 --hdd 1 --timeout 600
|
||||
</ul>
|
||||
<p>你可以点击 <a href="https://github.com/feiskyer/linux-perf-examples/tree/master/high-iowait-process">Github</a> 查看它的源码,使用方法我写在了这里:</p>
|
||||
<pre><code>$ docker run --privileged --name=app -itd feisky/app:iowait /app -d /dev/sdb -s 67108864 -c 20
|
||||
|
||||
</code></pre>
|
||||
<p>案例运行后,你可以执行 docker logs 查看它的日志。正常情况下,你可以看到下面的输出:</p>
|
||||
<pre><code>$ docker logs app
|
||||
@@ -1934,6 +1976,7 @@ pling period of length delay. The process and memory reports are instantaneous
|
||||
<p>这也是留言比较多的一个问题,在 CentOS 系统中,使用 perf 工具看不到函数名,只能看到一些 16 进制格式的函数地址。</p>
|
||||
<p>其实,只要你观察一下 perf 界面最下面的那一行,就会发现一个警告信息:</p>
|
||||
<pre><code>Failed to open /opt/bitnami/php/lib/php/extensions/opcache.so, continuing without symbols
|
||||
|
||||
</code></pre>
|
||||
<p>这说明,perf 找不到待分析进程依赖的库。当然,实际上这个案例中有很多依赖库都找不到,只不过,perf 工具本身只在最后一行显示警告信息,所以你只能看到这一条警告。</p>
|
||||
<p>这个问题,其实也是在分析 Docker 容器应用时,我们经常碰到的一个问题,因为容器应用依赖的库都在镜像里面。</p>
|
||||
@@ -1972,9 +2015,11 @@ $ perf_4.9 report
|
||||
<p>首先是 perf 工具的版本问题。在最后一步中,我们运行的工具是容器内部安装的版本 perf_4.9,而不是普通的 perf 命令。这是因为, perf 命令实际上是一个软连接,会跟内核的版本进行匹配,但镜像里安装的 perf 版本跟虚拟机的内核版本有可能并不一致。</p>
|
||||
<p>另外,php-fpm 镜像是基于 Debian 系统的,所以安装 perf 工具的命令,跟 Ubuntu 也并不完全一样。比如, Ubuntu 上的安装方法是下面这样:</p>
|
||||
<pre><code>$ apt-get install -y linux-tools-common linux-tools-generic linux-tools-$(uname -r))
|
||||
|
||||
</code></pre>
|
||||
<p>而在 php-fpm 容器里,你应该执行下面的命令来安装 perf:</p>
|
||||
<pre><code>$ apt-get install -y linux-perf
|
||||
|
||||
</code></pre>
|
||||
<p>当你按照前面这几种方法操作后,你就可以在容器内部看到 sqrt 的堆栈:</p>
|
||||
<p><img src="assets/76f8d0f36210001e750b0a82026dedaf.png" alt="img" /></p>
|
||||
@@ -2034,6 +2079,7 @@ $ perf_4.9 report
|
||||
<p>threshold 的默认值为 0.5%,也就是说,事件比例超过 0.5% 时,调用栈才能被显示。再观察我们案例应用 app 的事件比例,只有 0.34%,低于 0.5%,所以看不到 app 的调用栈就很正常了。</p>
|
||||
<p>这种情况下,你只需要给 perf report 设置一个小于 0.34% 的阈值,就可以显示我们想看到的调用图了。比如执行下面的命令:</p>
|
||||
<pre><code>$ perf report -g graph,0.3
|
||||
|
||||
</code></pre>
|
||||
<p>你就可以得到下面这个新的输出界面,展开 app 后,就可以看到它的调用栈了。</p>
|
||||
<p><img src="assets/b34f95617d13088671f4d9c2b9134693.png" alt="img" /></p>
|
||||
@@ -2144,6 +2190,7 @@ $ perf_4.9 report
|
||||
<p>oom_adj 的范围是 [-17, 15],数值越大,表示进程越容易被 OOM 杀死;数值越小,表示进程越不容易被 OOM 杀死,其中 -17 表示禁止 OOM。</p>
|
||||
<p>比如用下面的命令,你就可以把 sshd 进程的 oom_adj 调小为 -16,这样, sshd 进程就不容易被 OOM 杀死。</p>
|
||||
<pre><code>echo -16 > /proc/$(pidof sshd)/oom_adj
|
||||
|
||||
</code></pre>
|
||||
<h2>如何查看内存使用情况</h2>
|
||||
<p>通过了解内存空间的分布,以及内存的分配和回收,我想你对内存的工作原理应该有了大概的认识。当然,系统的实际工作原理更加复杂,也会涉及其他一些机制,这里我只讲了最主要的原理。掌握了这些,你可以对内存的运作有一条主线认识,不至于脑海里只有术语名词的堆砌。</p>
|
||||
@@ -2292,6 +2339,7 @@ r b swpd free buff cache si so bi bo in cs us sy id wa st
|
||||
<p>正常情况下,空闲系统中,你应该看到的是,这几个值在多次结果中一直保持不变。</p>
|
||||
<p>接下来,到第二个终端执行 dd 命令,通过读取随机设备,生成一个 500MB 大小的文件:</p>
|
||||
<pre><code>$ dd if=/dev/urandom of=/tmp/file bs=1M count=500
|
||||
|
||||
</code></pre>
|
||||
<p>然后再回到第一个终端,观察 Buffer 和 Cache 的变化情况:</p>
|
||||
<pre><code>procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
|
||||
@@ -2426,6 +2474,7 @@ sudo apt-get install -y bcc-tools libbcc-examples linux-headers-$(uname -r)
|
||||
</blockquote>
|
||||
<p>操作完这些步骤,bcc 提供的所有工具就都安装到 /usr/share/bcc/tools 这个目录中了。不过这里提醒你,bcc 软件包默认不会把这些工具配置到系统的 PATH 路径中,所以你得自己手动配置:</p>
|
||||
<pre><code>$ export PATH=$PATH:/usr/share/bcc/tools
|
||||
|
||||
</code></pre>
|
||||
<p>配置完,你就可以运行 cachestat 和 cachetop 命令了。比如,下面就是一个 cachestat 的运行界面,它以 1 秒的时间间隔,输出了 3 组缓存统计数据:</p>
|
||||
<pre><code>$ cachestat 1 3
|
||||
@@ -2559,6 +2608,7 @@ $ cachetop 5
|
||||
</code></pre>
|
||||
<p>接着,再到第二个终端,执行下面的命令运行案例:</p>
|
||||
<pre><code>$ docker run --privileged --name=app -itd feisky/app:io-direct
|
||||
|
||||
</code></pre>
|
||||
<p>案例运行后,我们还需要运行下面这个命令,来确认案例已经正常启动。如果一切正常,你应该可以看到类似下面的输出:</p>
|
||||
<pre><code>$ docker logs app
|
||||
@@ -2584,7 +2634,7 @@ strace: Process 4988 attached
|
||||
restart_syscall(<\.\.\. resuming interrupted nanosleep \.\.\.>) = 0
|
||||
openat(AT_FDCWD, "/dev/sdb1", O_RDONLY|O_DIRECT) = 4
|
||||
mmap(NULL, 33558528, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f448d240000
|
||||
read(4, "8vq\213\314\264u\373\4\336K\224\<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="53616613">[email protected]</a>\371\1\252\2\262\252q\221\n0\30\225bD\252\<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="85b7b3b3c5cf">[email protected]</a>"\.\.\., 33554432) = 33554432
|
||||
read(4, "8vq\213\314\264u\373\4\336K\224\<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="1b292e5b">[email protected]</a>\371\1\252\2\262\252q\221\n0\30\225bD\252\<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="a3919595e3e9">[email protected]</a>"\.\.\., 33554432) = 33554432
|
||||
write(1, "Time used: 0.948897 s to read 33"\.\.\., 45) = 45
|
||||
close(4) = 0
|
||||
</code></pre>
|
||||
@@ -2690,6 +2740,7 @@ sudo apt-get install -y bcc-tools libbcc-examples linux-headers-$(uname -r)
|
||||
<p>如果安装过程中有什么问题,同样鼓励你先自己搜索解决,解决不了的,可以在留言区向我提问。如果你以前已经安装过了,就可以忽略这一点了。</p>
|
||||
<p>安装完成后,再执行下面的命令来运行案例:</p>
|
||||
<pre><code>$ docker run --name=app -itd feisky/app:mem-leak
|
||||
|
||||
</code></pre>
|
||||
<p>案例成功运行后,你需要输入下面的命令,确认案例应用已经正常启动。如果一切正常,你应该可以看到下面这个界面:</p>
|
||||
<pre><code>$ docker logs app
|
||||
@@ -3089,9 +3140,11 @@ polkitd 1004 44 kB
|
||||
<p>这也说明了一点,虽然缓存属于可回收内存,但在类似大文件拷贝这类场景下,系统还是会用 Swap 机制来回收匿名内存,而不仅仅是回收占用绝大部分内存的文件页。</p>
|
||||
<p>最后,如果你在一开始配置了 Swap,不要忘记在案例结束后关闭。你可以运行下面的命令,关闭 Swap:</p>
|
||||
<pre><code>$ swapoff -a
|
||||
|
||||
</code></pre>
|
||||
<p>实际上,关闭 Swap 后再重新打开,也是一种常用的 Swap 空间清理方法,比如:</p>
|
||||
<pre><code>$ swapoff -a && swapon -a
|
||||
|
||||
</code></pre>
|
||||
<h2>小结</h2>
|
||||
<p>在内存资源紧张时,Linux 会通过 Swap ,把不常访问的匿名页换出到磁盘中,下次访问的时候再从磁盘换入到内存中来。你可以设置 /proc/sys/vm/min_free_kbytes,来调整系统定期回收内存的阈值;也可以设置 /proc/sys/vm/swappiness,来调整文件页和匿名页的回收倾向。</p>
|
||||
@@ -3494,6 +3547,7 @@ Minimum / Average / Maximum Object : 0.01K / 0.20K / 22.88K
|
||||
<h2>思考</h2>
|
||||
<p>最后,给你留一个思考题。在实际工作中,我们经常会根据文件名字,查找它所在路径,比如:</p>
|
||||
<pre><code>$ find / -name file-name
|
||||
|
||||
</code></pre>
|
||||
<p>今天的问题就是,这个命令,会不会导致系统的缓存升高呢?如果有影响,又会导致哪种类型的缓存升高呢?你可以结合今天内容,自己先去操作和分析,看看观察到的结果跟你分析的是否一样。</p>
|
||||
<p>欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。</p>
|
||||
@@ -3681,6 +3735,7 @@ Actual DISK READ: 0.00 B/s | Actual DISK WRITE: 0.00 B/s
|
||||
<h2>案例分析</h2>
|
||||
<p>首先,我们在终端中执行下面的命令,运行今天的目标应用:</p>
|
||||
<pre><code>$ docker run -v /tmp:/tmp --name=app -itd feisky/logapp
|
||||
|
||||
</code></pre>
|
||||
<p>然后,在终端中运行 ps 命令,确认案例应用正常启动。如果操作无误,你应该可以在 ps 的输出中,看到一个 app.py 的进程:</p>
|
||||
<pre><code>$ ps -ef | grep /app.py
|
||||
@@ -3799,6 +3854,7 @@ signal.signal(signal.SIGUSR2, set_logging_warning)
|
||||
<p>根据源码中的日志调用 logger. info(message) ,我们知道,它的日志是 INFO 级,这也正是它的默认级别。那么,只要把默认级别调高到 WARNING 级,日志问题应该就解决了。</p>
|
||||
<p>接下来,我们就来检查一下,刚刚的分析对不对。在终端中运行下面的 kill 命令,给进程 18940 发送 SIGUSR2 信号:</p>
|
||||
<pre><code>$ kill -SIGUSR2 18940
|
||||
|
||||
</code></pre>
|
||||
<p>然后,再执行 top 和 iostat 观察一下:</p>
|
||||
<pre><code>$ top
|
||||
@@ -3814,6 +3870,7 @@ sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.0
|
||||
<p>到这里,我们不仅定位了狂打日志的应用程序,并通过调高日志级别的方法,完美解决了 I/O 的性能瓶颈。</p>
|
||||
<p>案例最后,当然不要忘了运行下面的命令,停止案例应用:</p>
|
||||
<pre><code>$ docker rm -f app
|
||||
|
||||
</code></pre>
|
||||
<h2>小结</h2>
|
||||
<p>日志,是了解应用程序内部运行情况,最常用、也最有效的工具。无论是操作系统,还是应用程序,都会记录大量的运行日志,以便事后查看历史记录。这些日志一般按照不同级别来开启,比如,开发环境通常打开调试级别的日志,而线上环境则只记录警告和错误日志。</p>
|
||||
@@ -3852,6 +3909,7 @@ sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.0
|
||||
<h2><strong>案例分析</strong></h2>
|
||||
<p>首先,我们在第一个终端中执行下面的命令,运行本次案例要分析的目标应用:</p>
|
||||
<pre><code>$ docker run --name=app -p 10000:80 -itd feisky/word-pop
|
||||
|
||||
</code></pre>
|
||||
<p>然后,在第二个终端中运行 curl 命令,访问 <a href="http://192.168.0.10:1000/">http://192.168.0.10:1000/</a>,确认案例正常启动。你应该可以在 curl 的输出界面里,看到一个 hello world 的输出:</p>
|
||||
<pre><code>$ curl http://192.168.0.10:10000/
|
||||
@@ -3859,6 +3917,7 @@ hello world
|
||||
</code></pre>
|
||||
<p>接下来,在第二个终端中,访问案例应用的单词热度接口,也就是 <a href="http://192.168.0.10:1000/popularity/word">http://192.168.0.10:1000/popularity/word</a>。</p>
|
||||
<pre><code>$ curl http://192.168.0.10:1000/popularity/word
|
||||
|
||||
</code></pre>
|
||||
<p>稍等一会儿,你会发现,这个接口居然这么长时间都没响应,究竟是怎么回事呢?我们先回到终端一来分析一下。</p>
|
||||
<p>我们试试在第一个终端里,随便执行一个命令,比如执行 df 命令,查看一下文件系统的使用情况。奇怪的是,这么简单的命令,居然也要等好久才有输出。</p>
|
||||
@@ -3872,6 +3931,7 @@ tmpfs 816932 1188 815744 1% /run
|
||||
<p>这里的思路其实跟上一个案例比较类似,我们可以先用 top 来观察 CPU 和内存的使用情况,然后再用 iostat 来观察磁盘的 I/O 情况。</p>
|
||||
<p>为了避免分析过程中 curl 请求突然结束,我们回到终端二,按 Ctrl+C 停止刚才的应用程序;然后,把 curl 命令放到一个循环里执行;这次我们还要加一个 time 命令,观察每次的执行时间:</p>
|
||||
<pre><code>$ while true; do time curl http://192.168.0.10:10000/popularity/word; sleep 1; done
|
||||
|
||||
</code></pre>
|
||||
<p>继续回到终端一来分析性能。我们在终端一中运行 top 命令,观察 CPU 和内存的使用情况:</p>
|
||||
<pre><code>$ top
|
||||
@@ -3925,6 +3985,7 @@ stat("/usr/local/lib/python3.7/importlib/_bootstrap.py", {st_mode=S_IF
|
||||
<p>从 strace 中,你可以看到大量的 stat 系统调用,并且大都为 python 的文件,但是,请注意,这里并没有任何 write 系统调用。</p>
|
||||
<p>由于 strace 的输出比较多,我们可以用 grep ,来过滤一下 write,比如:</p>
|
||||
<pre><code>$ strace -p 12280 2>&1 | grep write
|
||||
|
||||
</code></pre>
|
||||
<p>遗憾的是,这里仍然没有任何输出。</p>
|
||||
<p>难道此时已经没有性能问题了吗?重新执行刚才的 top 和 iostat 命令,你会不幸地发现,性能问题仍然存在。</p>
|
||||
@@ -4128,6 +4189,7 @@ Got data: () in 15.364538192749023 sec
|
||||
<p>不过别急,在具体分析前,为了避免在分析过程中客户端的请求结束,我们把 curl 命令放到一个循环里执行。同时,为了避免给系统过大压力,我们设置在每次查询后,都先等待 5 秒,然后再开始新的请求。</p>
|
||||
<p>所以,你可以在终端二中,继续执行下面的命令:</p>
|
||||
<pre><code>$ while true; do curl http://192.168.0.10:10000/products/geektime; sleep 5; done
|
||||
|
||||
</code></pre>
|
||||
<p>接下来,重新回到终端一中,分析接口响应速度慢的原因。不过,重回终端一后,你会发现系统响应也明显变慢了,随便执行一个命令,都得停顿一会儿才能看到输出。</p>
|
||||
<p>这跟上一节的现象很类似,看来,我们还是得观察一下系统的资源使用情况,比如 CPU、内存和磁盘 I/O 等的情况。</p>
|
||||
@@ -4176,6 +4238,7 @@ $ pidstat -d 1
|
||||
</code></pre>
|
||||
<p>观察一会,你会发现,线程 28014 正在读取大量数据,且读取文件的描述符编号为 38。这儿的 38 又对应着哪个文件呢?我们可以执行下面的 lsof 命令,并且指定线程号 28014 ,具体查看这个可疑线程和可疑文件:</p>
|
||||
<pre><code>$ lsof -p 28014
|
||||
|
||||
</code></pre>
|
||||
<p>奇怪的是,lsof 并没有给出任何输出。实际上,如果你查看 lsof 命令的返回值,就会发现,这个命令的执行失败了。</p>
|
||||
<p>我们知道,在 SHELL 中,特殊标量 $? 表示上一条命令退出时的返回值。查看这个特殊标量,你会发现它的返回值是 1。可是别忘了,在 Linux 中,返回值为 0 ,才表示命令执行成功。返回值为 1,显然表明执行失败。</p>
|
||||
@@ -4234,7 +4297,9 @@ db.opt products.MYD products.MYI products.frm
|
||||
<p>既然已经找出了数据库和表,接下来要做的,就是弄清楚数据库中正在执行什么样的 SQL 了。我们继续在终端一中,运行下面的 docker exec 命令,进入 MySQL 的命令行界面:</p>
|
||||
<pre><code>$ docker exec -i -t mysql mysql
|
||||
...
|
||||
|
||||
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
|
||||
|
||||
mysql>
|
||||
</code></pre>
|
||||
<p>下一步你应该可以想到,那就是在 MySQL 命令行界面中,执行 show processlist 命令,来查看当前正在执行的 SQL 语句。</p>
|
||||
@@ -4320,6 +4385,7 @@ Got data: ()in 0.003951072692871094 sec
|
||||
<p>首先,我们在终端二中停止 curl 命令,然后回到终端一中,执行下面的命令删除索引:</p>
|
||||
<pre><code># 删除索引
|
||||
$ docker exec -i -t mysql mysql
|
||||
|
||||
mysql> use test;
|
||||
mysql> DROP INDEX products_index ON products;
|
||||
</code></pre>
|
||||
@@ -4426,6 +4492,7 @@ $ curl http://192.168.0.10:10000/init/5000
|
||||
<p>到底出了什么问题呢?我们还是要用前面学过的性能工具和原理,来找到这个瓶颈。</p>
|
||||
<p>不过别急,同样为了避免分析过程中客户端的请求结束,在进行性能分析前,我们先要把 curl 命令放到一个循环里来执行。你可以在终端二中,继续执行下面的命令:</p>
|
||||
<pre><code>$ while true; do curl http://192.168.0.10:10000/get_cache; done
|
||||
|
||||
</code></pre>
|
||||
<p>接下来,再重新回到终端一,查找接口响应慢的“病因”。</p>
|
||||
<p>最近几个案例的现象都是响应很慢,这种情况下,我们自然先会怀疑,是不是系统资源出现了瓶颈。所以,先观察 CPU、内存和磁盘 I/O 等的使用情况肯定不会错。</p>
|
||||
@@ -4571,6 +4638,7 @@ OK
|
||||
</code></pre>
|
||||
<p>改完后,切换到终端二中查看,你会发现,现在的请求时间,已经缩短到了 0.9s:</p>
|
||||
<pre><code>{..., "elapsed_seconds":0.9368953704833984,"type":"good"}
|
||||
|
||||
</code></pre>
|
||||
<p>而第二个问题,就要查看应用的源码了。点击 <a href="https://github.com/feiskyer/linux-perf-examples/blob/master/redis-slow/app.py">Github</a> ,你就可以查看案例应用的源代码:</p>
|
||||
<pre><code>def get_cache(type_name):
|
||||
@@ -4592,6 +4660,7 @@ OK
|
||||
<p>你可以发现,解决第二个问题后,新接口的性能又有了进一步的提升,从刚才的 0.9s ,再次缩短成了不到 0.2s。</p>
|
||||
<p>当然,案例最后,不要忘记清理案例应用。你可以切换到终端一中,执行下面的命令进行清理:</p>
|
||||
<pre><code>$ docker rm -f app redis
|
||||
|
||||
</code></pre>
|
||||
<h2>小结</h2>
|
||||
<p>今天我带你一起分析了一个 Redis 缓存的案例。</p>
|
||||
@@ -5038,6 +5107,7 @@ eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
|
||||
RX errors 0 dropped 0 overruns 0 frame 0
|
||||
TX packets 32637401 bytes 4815573306 (4.8 GB)
|
||||
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
|
||||
|
||||
$ ip -s addr show dev eth0
|
||||
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
|
||||
link/ether 78:0d:3a:07:cf:3a brd ff:ff:ff:ff:ff:ff
|
||||
@@ -5419,6 +5489,7 @@ $ yum install -y httpd-tools
|
||||
</code></pre>
|
||||
<p>接下来,在目标机器上,使用 Docker 启动一个 Nginx 服务,然后用 ab 来测试它的性能。首先,在目标机器上运行下面的命令:</p>
|
||||
<pre><code>$ docker run -p 80:80 -itd nginx
|
||||
|
||||
</code></pre>
|
||||
<p>而在另一台机器上,运行 ab 命令,测试 Nginx 的性能:</p>
|
||||
<pre><code># -c 表示并发请求数为 1000,-n 表示总的请求数为 10000
|
||||
@@ -5507,6 +5578,7 @@ end
|
||||
</code></pre>
|
||||
<p>而在执行测试时,通过 -s 选项,执行脚本的路径:</p>
|
||||
<pre><code>$ wrk -c 1000 -t 2 -s auth.lua http://192.168.0.30/
|
||||
|
||||
</code></pre>
|
||||
<p>wrk 需要你用 Lua 脚本,来构造请求负载。这对于大部分场景来说,可能已经足够了 。不过,它的缺点也正是,所有东西都需要代码来构造,并且工具本身不提供 GUI 环境。</p>
|
||||
<p>像 Jmeter 或者 LoadRunner(商业产品),则针对复杂场景提供了脚本录制、回放、GUI 等更丰富的功能,使用起来也更加方便。</p>
|
||||
@@ -5650,7 +5722,7 @@ nameserver 114.114.114.114
|
||||
<p>首先,执行下面的命令,进入今天的第一个案例。如果一切正常,你将可以看到下面这个输出:</p>
|
||||
<pre><code># 进入案例环境的 SHELL 终端中
|
||||
$ docker run -it --rm -v $(mktemp):/etc/resolv.conf feisky/dnsutils bash
|
||||
<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="98eaf7f7ecd8affda1fdfcaefdfcaca1afac">[email protected]</a>:/#
|
||||
<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="84f6ebebf0c4b3e1bde1e0b2e1e0b0bdb3b0">[email protected]</a>:/#
|
||||
</code></pre>
|
||||
<p>注意,这儿 root 后面的 7e9ed6ed4974,是 Docker 生成容器的 ID 前缀,你的环境中很可能是不同的 ID,所以直接忽略这一项就可以了。</p>
|
||||
<blockquote>
|
||||
@@ -5680,6 +5752,7 @@ round-trip min/avg/max/stddev = 31.116/31.163/31.245/0.058 ms
|
||||
<p>从这次的输出可以看到,nslookup 连接环回地址(127.0.0.1 和 ::1)的 53 端口失败。这里就有问题了,为什么会去连接环回地址,而不是我们的先前看到的 114.114.114.114 呢?</p>
|
||||
<p>你可能已经想到了症结所在——有可能是因为容器中没有配置 DNS 服务器。那我们就执行下面的命令确认一下:</p>
|
||||
<pre><code>/# cat /etc/resolv.conf
|
||||
|
||||
</code></pre>
|
||||
<p>果然,这个命令没有任何输出,说明容器里的确没有配置 DNS 服务器。到这一步,很自然的,我们就知道了解决方法。在 /etc/resolv.conf 文件中,配置上 DNS 服务器就可以了。</p>
|
||||
<p>你可以执行下面的命令,在配置好 DNS 服务器后,重新执行 nslookup 命令。自然,我们现在发现,这次可以正常解析了:</p>
|
||||
@@ -5695,7 +5768,7 @@ Address: 39.106.233.176
|
||||
<h3>案例 2:DNS 解析不稳定</h3>
|
||||
<p>接下来,我们再来看第二个案例。执行下面的命令,启动一个新的容器,并进入它的终端中:</p>
|
||||
<pre><code>$ docker run -it --rm --cap-add=NET_ADMIN --dns 8.8.8.8 feisky/dnsutils bash
|
||||
<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="dba9b4b4af9bebb8bfe8bebeebb8e3beb8b9">[email protected]</a>:/#
|
||||
<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="fa8895958ebaca999ec99f9fca99c29f9998">[email protected]</a>:/#
|
||||
</code></pre>
|
||||
<p>然后,跟上一个案例一样,还是运行 nslookup 命令,解析 time.geekbang.org 的 IP 地址。不过,这次要加一个 time 命令,输出解析所用时间。如果一切正常,你可能会看到如下输出:</p>
|
||||
<pre><code>/# time nslookup time.geekbang.org
|
||||
@@ -5894,6 +5967,7 @@ sys 0m0.003s
|
||||
<p>到这里,再往后该怎么分析呢?其实,这时候就可以用 tcpdump 抓包,查看 ping 在收发哪些网络包。</p>
|
||||
<p>我们再打开另一个终端(终端二),SSH 登录案例机器后,执行下面的命令:</p>
|
||||
<pre><code>$ tcpdump -nn udp port 53 or host 35.190.27.188
|
||||
|
||||
</code></pre>
|
||||
<p>当然,你可以直接用 tcpdump 不加任何参数来抓包,但那样的话,就可能抓取到很多不相干的包。由于我们已经执行过 ping 命令,知道了 geekbang.org 的 IP 地址是 35.190.27.188,也知道 ping 命令会执行 DNS 查询。所以,上面这条命令,就是基于这个规则进行过滤。</p>
|
||||
<p>我来具体解释一下这条命令。</p>
|
||||
@@ -5954,6 +6028,7 @@ rtt min/avg/max/mdev = 32.879/35.160/39.030/2.755 ms
|
||||
<p>到这里, 我就带你一起使用 tcpdump ,解决了一个最常见的 ping 工作缓慢的问题。</p>
|
||||
<p>案例最后,如果你在开始时,执行了 iptables 命令,那也不要忘了删掉它:</p>
|
||||
<pre><code>$ iptables -D INPUT -p udp --sport 53 -m string --string googleusercontent --algo bm -j DROP
|
||||
|
||||
</code></pre>
|
||||
<p>不过,删除后你肯定还有疑问,明明我们的案例跟 Google 没啥关系,为什么要根据 googleusercontent ,这个毫不相关的字符串来过滤包呢?</p>
|
||||
<p>实际上,如果换一个 DNS 服务器,就可以用 PTR 反查到 35.190.27.188 所对应的域名:</p>
|
||||
@@ -5983,6 +6058,7 @@ Authoritative answers can be found from:
|
||||
<p><img src="assets/4870a28c032bdd2a26561604ae2f7cb3.png" alt="img" /></p>
|
||||
<p>最后,再次强调 tcpdump 的输出格式,我在前面已经介绍了它的基本格式:</p>
|
||||
<pre><code>时间戳 协议 源地址. 源端口 > 目的地址. 目的端口 网络包详细信息
|
||||
|
||||
</code></pre>
|
||||
<p>其中,网络包的详细信息取决于协议,不同协议展示的格式也不同。所以,更详细的使用方法,还是需要你去查询 tcpdump 的 <a href="https://www.tcpdump.org/manpages/tcpdump.1.html">man</a> 手册(执行 man tcpdump 也可以得到)。</p>
|
||||
<p>不过,讲了这么多,你应该也发现了。tcpdump 虽然功能强大,可是输出格式却并不直观。特别是,当系统中网络包数比较多(比如 PPS 超过几千)的时候,你想从 tcpdump 抓取的网络包中分析问题,实在不容易。</p>
|
||||
@@ -5991,9 +6067,11 @@ Authoritative answers can be found from:
|
||||
<p>Wireshark 也是最流行的一个网络分析工具,它最大的好处就是提供了跨平台的图形界面。跟 tcpdump 类似,Wireshark 也提供了强大的过滤规则表达式,同时,还内置了一系列的汇总分析工具。</p>
|
||||
<p>比如,拿刚刚的 ping 案例来说,你可以执行下面的命令,把抓取的网络包保存到 ping.pcap 文件中:</p>
|
||||
<pre><code>$ tcpdump -nn udp port 53 or host 35.190.27.188 -w ping.pcap
|
||||
|
||||
</code></pre>
|
||||
<p>接着,把它拷贝到你安装有 Wireshark 的机器中,比如你可以用 scp 把它拷贝到本地来:</p>
|
||||
<pre><code>$ scp host-ip/path/ping.pcap .
|
||||
|
||||
</code></pre>
|
||||
<p>然后,再用 Wireshark 打开它。打开后,你就可以看到下面这个界面:</p>
|
||||
<p><img src="assets/6b854703dcfcccf64c0a69adecf2f42c.png" alt="img" /></p>
|
||||
@@ -6013,6 +6091,7 @@ $ tcpdump -nn host 93.184.216.34 -w web.pcap
|
||||
</blockquote>
|
||||
<p>接下来,切换到终端二,执行下面的 curl 命令,访问 <a href="http://example.com/">http://example.com</a>:</p>
|
||||
<pre><code>$ curl http://example.com
|
||||
|
||||
</code></pre>
|
||||
<p>最后,再回到终端一,按下 Ctrl+C 停止 tcpdump,并把得到的 web.pcap 拷贝出来。</p>
|
||||
<p>使用 Wireshark 打开 web.pcap 后,你就可以在 Wireshark 中,看到如下的界面:</p>
|
||||
@@ -6085,21 +6164,25 @@ $ tcpdump -nn host 93.184.216.34 -w web.pcap
|
||||
<a href="/极客时间/MySQL实战45讲.md.html">下一页</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||||
</div>
|
||||
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709980a0a8ba8b66","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||||
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709ba3ab1c04fbdc","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||||
</body>
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-NPSEEVD756');
|
||||
var path = window.location.pathname
|
||||
@@ -6113,12 +6196,14 @@ $ tcpdump -nn host 93.184.216.34 -w web.pcap
|
||||
} else {
|
||||
setCookie("lastPath", path)
|
||||
}
|
||||
|
||||
function setCookie(cname, cvalue) {
|
||||
var d = new Date();
|
||||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||||
var expires = "expires=" + d.toGMTString();
|
||||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||||
}
|
||||
|
||||
function getCookie(cname) {
|
||||
var name = cname + "=";
|
||||
var ca = document.cookie.split(';');
|
||||
@@ -6128,5 +6213,7 @@ $ tcpdump -nn host 93.184.216.34 -w web.pcap
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
<!-- theme css & js -->
|
||||
<meta name="generator" content="Hexo 4.2.0">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="book-container">
|
||||
<div class="book-sidebar">
|
||||
<div class="book-brand">
|
||||
@@ -25,61 +27,86 @@
|
||||
<ul class="uncollapsible">
|
||||
<li><a href="/" class="current-tab">首页</a></li>
|
||||
</ul>
|
||||
|
||||
<ul class="uncollapsible">
|
||||
<li><a href="../">上一级</a></li>
|
||||
</ul>
|
||||
|
||||
<ul class="uncollapsible">
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/Java基础36讲.md.html">Java基础36讲.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/Java错误示例100讲.md.html">Java错误示例100讲.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/Linux性能优化.md.html">Linux性能优化.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
<a class="current-tab" href="/极客时间/MySQL实战45讲.md.html">MySQL实战45讲.md.html</a>
|
||||
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/从0开始学微服务.md.html">从0开始学微服务.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/代码精进之路.md.html">代码精进之路.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/持续交付36讲.md.html">持续交付36讲.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/程序员进阶攻略.md.html">程序员进阶攻略.md.html</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
|
||||
<a href="/极客时间/趣谈网络协议.md.html">趣谈网络协议.md.html</a>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||||
<div class="sidebar-toggle-inner"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function add_inner() {
|
||||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||||
inner.classList.add('show')
|
||||
}
|
||||
|
||||
function remove_inner() {
|
||||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||||
inner.classList.remove('show')
|
||||
}
|
||||
|
||||
function sidebar_toggle() {
|
||||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||||
let sidebar = document.querySelector('.book-sidebar')
|
||||
@@ -94,6 +121,8 @@
|
||||
content.classList.add('extend')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function open_sidebar() {
|
||||
let sidebar = document.querySelector('.book-sidebar')
|
||||
let overlay = document.querySelector('.off-canvas-overlay')
|
||||
@@ -106,7 +135,9 @@ function hide_canvas() {
|
||||
sidebar.classList.remove('show')
|
||||
overlay.classList.remove('show')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="off-canvas-content">
|
||||
<div class="columns">
|
||||
<div class="column col-12 col-lg-12">
|
||||
@@ -1287,19 +1318,19 @@ mysql> alter table SUser add index index2(email(6));
|
||||
<p>从图中你可以看到,由于 email(6) 这个索引结构中每个邮箱字段都只取前 6 个字节(即:zhangs),所以占用的空间会更小,这就是使用前缀索引的优势。</p>
|
||||
<p>但,这同时带来的损失是,可能会增加额外的记录扫描次数。</p>
|
||||
<p>接下来,我们再看看下面这个语句,在这两个索引定义下分别是怎么执行的。</p>
|
||||
<pre><code>select id,name,email from SUser where email='<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="dfa5b7beb1b8acaca7a6a59fa7a7a7f1bcb0b2">[email protected]</a>';
|
||||
<pre><code>select id,name,email from SUser where email='<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="b8c2d0d9d6dfcbcbc0c1c2f8c0c0c096dbd7d5">[email protected]</a>';
|
||||
</code></pre>
|
||||
<p><strong>如果使用的是 index1</strong>(即 email 整个字符串的索引结构),执行顺序是这样的:</p>
|
||||
<ol>
|
||||
<li>从 index1 索引树找到满足索引值是’<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="8df7e5ece3eafefef5f4f7cdf5f5f5a3eee2e0">[email protected]</a>’的这条记录,取得 ID2 的值;</li>
|
||||
<li>从 index1 索引树找到满足索引值是’<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="473d2f26292034343f3e3d073f3f3f6924282a">[email protected]</a>’的这条记录,取得 ID2 的值;</li>
|
||||
<li>到主键上查到主键值是 ID2 的行,判断 email 的值是正确的,将这行记录加入结果集;</li>
|
||||
<li>取 index1 索引树上刚刚查到的位置的下一条记录,发现已经不满足 email='<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="7c06141d121b0f0f0405063c040404521f1311">[email protected]</a>’的条件了,循环结束。</li>
|
||||
<li>取 index1 索引树上刚刚查到的位置的下一条记录,发现已经不满足 email='<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="a6dccec7c8c1d5d5dedfdce6dedede88c5c9cb">[email protected]</a>’的条件了,循环结束。</li>
|
||||
</ol>
|
||||
<p>这个过程中,只需要回主键索引取一次数据,所以系统认为只扫描了一行。</p>
|
||||
<p><strong>如果使用的是 index2</strong>(即 email(6) 索引结构),执行顺序是这样的:</p>
|
||||
<ol>
|
||||
<li>从 index2 索引树找到满足索引值是’zhangs’的记录,找到的第一个是 ID1;</li>
|
||||
<li>到主键上查到主键值是 ID1 的行,判断出 email 的值不是’<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="8ef4e6efe0e9fdfdf6f7f4cef6f6f6a0ede1e3">[email protected]</a>’,这行记录丢弃;</li>
|
||||
<li>到主键上查到主键值是 ID1 的行,判断出 email 的值不是’<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="c8b2a0a9a6afbbbbb0b1b288b0b0b0e6aba7a5">[email protected]</a>’,这行记录丢弃;</li>
|
||||
<li>取 index2 上刚刚查到的位置的下一条记录,发现仍然是’zhangs’,取出 ID2,再到 ID 索引上取整行然后判断,这次值对了,将这行记录加入结果集;</li>
|
||||
<li>重复上一步,直到在 idxe2 上取到的值不是’zhangs’时,循环结束。</li>
|
||||
</ol>
|
||||
@@ -1324,10 +1355,10 @@ from SUser;
|
||||
<h2>前缀索引对覆盖索引的影响</h2>
|
||||
<p>前面我们说了使用前缀索引可能会增加扫描行数,这会影响到性能。其实,前缀索引的影响不止如此,我们再看一下另外一个场景。</p>
|
||||
<p>你先来看看这个 SQL 语句:</p>
|
||||
<pre><code>select id,email from SUser where email='<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="e59f8d848b8296969d9c9fa59d9d9dcb868a88">[email protected]</a>';
|
||||
<pre><code>select id,email from SUser where email='<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="fc86949d929b8f8f848586bc848484d29f9391">[email protected]</a>';
|
||||
</code></pre>
|
||||
<p>与前面例子中的 SQL 语句</p>
|
||||
<pre><code>select id,name,email from SUser where email='<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="9ce6f4fdf2fbefefe4e5e6dce4e4e4b2fff3f1">[email protected]</a>';
|
||||
<pre><code>select id,name,email from SUser where email='<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="1e64767f70796d6d6667645e666666307d7173">[email protected]</a>';
|
||||
</code></pre>
|
||||
<p>相比,这个语句只要求返回 id 和 email 字段。</p>
|
||||
<p>所以,如果使用 index1(即 email 整个字符串的索引结构)的话,可以利用覆盖索引,从 index1 查到结果后直接就返回了,不需要回到 ID 索引再去查一次。而如果使用 index2(即 email(6) 索引结构)的话,就不得不回到 ID 索引再去判断 email 字段的值。</p>
|
||||
@@ -2014,7 +2045,7 @@ SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G
|
||||
/* @b 保存 Innodb_rows_read 的当前值 */
|
||||
select VARIABLE_VALUE into @b from performance_schema.session_status where variable_name = 'Innodb_rows_read';
|
||||
/* 计算 Innodb_rows_read 差值 */
|
||||
select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="fb99d6bb9a">[email protected]</a>;
|
||||
select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="9efcb3deff">[email protected]</a>;
|
||||
</code></pre>
|
||||
<p>这个方法是通过查看 OPTIMIZER_TRACE 的结果来确认的,你可以从 number_of_tmp_files 中看到是否使用了临时文件。</p>
|
||||
<p><img src="assets/89baf99cdeefe90a22370e1d6f5e6495-1584367392864.png" alt="img" /></p>
|
||||
@@ -2025,8 +2056,8 @@ select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail=
|
||||
<p>接下来,我再和你解释一下图 4 中其他两个值的意思。</p>
|
||||
<p>我们的示例表中有 4000 条满足 city='杭州’的记录,所以你可以看到 examined_rows=4000,表示参与排序的行数是 4000 行。</p>
|
||||
<p>sort_mode 里面的 packed_additional_fields 的意思是,排序过程对字符串做了“紧凑”处理。即使 name 字段的定义是 varchar(16),在排序过程中还是要按照实际长度来分配空间的。</p>
|
||||
<p>同时,最后一个查询语句 select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="98fab5d8f9">[email protected]</a> 的返回结果是 4000,表示整个执行过程只扫描了 4000 行。</p>
|
||||
<p>这里需要注意的是,为了避免对结论造成干扰,我把 internal_tmp_disk_storage_engine 设置成 MyISAM。否则,select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="67054a2706">[email protected]</a> 的结果会显示为 4001。</p>
|
||||
<p>同时,最后一个查询语句 select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="dbb9f69bba">[email protected]</a> 的返回结果是 4000,表示整个执行过程只扫描了 4000 行。</p>
|
||||
<p>这里需要注意的是,为了避免对结论造成干扰,我把 internal_tmp_disk_storage_engine 设置成 MyISAM。否则,select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="2d4f006d4c">[email protected]</a> 的结果会显示为 4001。</p>
|
||||
<p>这是因为查询 OPTIMIZER_TRACE 这个表时,需要用到临时表,而 internal_tmp_disk_storage_engine 的默认值是 InnoDB。如果使用的是 InnoDB 引擎的话,把数据从临时表取出来的时候,会让 Innodb_rows_read 的值加 1。</p>
|
||||
<h2>rowid 排序</h2>
|
||||
<p>在上面这个算法过程里面,只对原表的数据读了一遍,剩下的操作都是在 sort_buffer 和临时文件中执行的。但这个算法有一个问题,就是如果查询要返回的字段很多的话,那么 sort_buffer 里面要放的字段数太多,这样内存里能够同时放下的行数很少,要分成很多个临时文件,排序的性能会很差。</p>
|
||||
@@ -2053,9 +2084,9 @@ select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail=
|
||||
<p>图 5 rowid 排序</p>
|
||||
<p>对比图 3 的全字段排序流程图你会发现,rowid 排序多访问了一次表 t 的主键索引,就是步骤 7。</p>
|
||||
<p>需要说明的是,最后的“结果集”是一个逻辑概念,实际上 MySQL 服务端从排序后的 sort_buffer 中依次取出 id,然后到原表查到 city、name 和 age 这三个字段的结果,不需要在服务端再耗费内存存储结果,是直接返回给客户端的。</p>
|
||||
<p>根据这个说明过程和图示,你可以想一下,这个时候执行 select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="5e3c731e3f">[email protected]</a>,结果会是多少呢?</p>
|
||||
<p>根据这个说明过程和图示,你可以想一下,这个时候执行 select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="81e3acc1e0">[email protected]</a>,结果会是多少呢?</p>
|
||||
<p>现在,我们就来看看结果有什么不同。</p>
|
||||
<p>首先,图中的 examined_rows 的值还是 4000,表示用于排序的数据是 4000 行。但是 select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="ee8cc3ae8f">[email protected]</a> 这个语句的值变成 5000 了。</p>
|
||||
<p>首先,图中的 examined_rows 的值还是 4000,表示用于排序的数据是 4000 行。但是 select @<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="5e3c731e3f">[email protected]</a> 这个语句的值变成 5000 了。</p>
|
||||
<p>因为这时候除了排序过程外,在排序完成后,还要根据 id 去原表取值。由于语句是 limit 1000,因此会多读 1000 行。</p>
|
||||
<p><img src="assets/27f164804d1a4689718291be5d10f89b-1584367392865.png" alt="img" /></p>
|
||||
<p>图 6 rowid 排序的 OPTIMIZER_TRACE 部分输出</p>
|
||||
@@ -2286,7 +2317,7 @@ SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G
|
||||
</ol>
|
||||
<p>我们把这个算法,暂时称作随机算法 1。这里,我直接给你贴一下执行语句的序列:</p>
|
||||
<pre><code>mysql> select max(id),min(id) into @M,@N from t ;
|
||||
set @X= floor((@<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="f2bfdfb2bc">[email protected]</a>+1)*rand() + @N);
|
||||
set @X= floor((@<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="216c0c616f">[email protected]</a>+1)*rand() + @N);
|
||||
select * from t where id >= @X limit 1;
|
||||
</code></pre>
|
||||
<p>这个方法效率很高,因为取 max(id) 和 min(id) 都是不需要扫描索引的,而第三步的 select 也可以用索引快速定位,可以认为就只扫描了 3 行。但实际上,这个算法本身并不严格满足题目的随机要求,因为 ID 中间可能有空洞,因此选择不同行的概率不一样,不是真正的随机。</p>
|
||||
@@ -6374,7 +6405,7 @@ create table db2.t like db1.t
|
||||
<p>为了便于说明,我先创建一个用户:</p>
|
||||
<pre><code>create user 'ua'@'%' identified by 'pa';
|
||||
</code></pre>
|
||||
<p>这条语句的逻辑是创建一个用户’ua’@’%’,密码是 pa。注意,在 MySQL 里面,用户名 (user)+ 地址 (host) 才表示一个用户,因此 <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="96e3f7d6ffe6a7">[email protected]</a> 和 <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="6f1a0e2f061f5d">[email protected]</a> 代表的是两个不同的用户。</p>
|
||||
<p>这条语句的逻辑是创建一个用户’ua’@’%’,密码是 pa。注意,在 MySQL 里面,用户名 (user)+ 地址 (host) 才表示一个用户,因此 <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="d8adb998b1a8e9">[email protected]</a> 和 <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="047165446d7436">[email protected]</a> 代表的是两个不同的用户。</p>
|
||||
<p>这条命令做了两个动作:</p>
|
||||
<ol>
|
||||
<li>磁盘上,往 mysql.user 表里插入一行,由于没有指定权限,所以这行数据上所有表示权限的字段的值都是 N;</li>
|
||||
@@ -6580,21 +6611,25 @@ GRANT SELECT(id), INSERT (id,a) ON mydb.mytbl TO 'ua'@'%' with grant option;
|
||||
<a href="/极客时间/从0开始学微服务.md.html">下一页</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||||
</div>
|
||||
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709980a94b828b66","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||||
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709ba3afc855fbdc","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||||
</body>
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-NPSEEVD756');
|
||||
var path = window.location.pathname
|
||||
@@ -6608,12 +6643,14 @@ GRANT SELECT(id), INSERT (id,a) ON mydb.mytbl TO 'ua'@'%' with grant option;
|
||||
} else {
|
||||
setCookie("lastPath", path)
|
||||
}
|
||||
|
||||
function setCookie(cname, cvalue) {
|
||||
var d = new Date();
|
||||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||||
var expires = "expires=" + d.toGMTString();
|
||||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||||
}
|
||||
|
||||
function getCookie(cname) {
|
||||
var name = cname + "=";
|
||||
var ca = document.cookie.split(';');
|
||||
@@ -6623,5 +6660,7 @@ GRANT SELECT(id), INSERT (id,a) ON mydb.mytbl TO 'ua'@'%' with grant option;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
</html>
|
||||
|
||||
BIN
极客时间/assets/00110923007513e865d7f43a124887c1-1584367399854.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
极客时间/assets/0014f97423bd75235a9187f492fb2453-1584367399531.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
极客时间/assets/00613758a46fe1341089ce11ef8a0f84.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
极客时间/assets/006cc8a4bf7a13fea0f456905c263afe.jpg
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
极客时间/assets/006d004b8f4c2fa2d12451ff2de76524.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
极客时间/assets/009586150d3d895bb129ab2d0e8ee9d6.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
极客时间/assets/00a1d5aa42c53d4d355e297ca5f221f0.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
极客时间/assets/0150301698979255a6f27711c35e9eef-1584367389377.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
极客时间/assets/016120f1bf46100812f1d1ccec1e517f.jpg
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
极客时间/assets/01ee306698c7dd6207e80fea0a8238c8.jpg
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
极客时间/assets/022198aa7ac5584330aae0cb35a82f29.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
极客时间/assets/0258775aac1126735504c9a6399745f5.jpg
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
极客时间/assets/02de374239ba3b0ea10cc9192821552a.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
极客时间/assets/02e083adaec6e1191f54992f7bc13dcd-1584367390483.png
Normal file
|
After Width: | Height: | Size: 312 KiB |
BIN
极客时间/assets/036634e53276eaf8535c3442805dfaeb-1584367406944.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
极客时间/assets/036cde548f2455e3d80b6b1c50e33c91.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
极客时间/assets/0397a72d0c5d76d4e3591cbe61eef729.jpg
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
极客时间/assets/0399382169faf50fc1b354099af71954-1584367402944.jpg
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
极客时间/assets/03d4a216c024a9e761ed43c6787bf7dd-1584287099474.jpg
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
极客时间/assets/03d4a216c024a9e761ed43c6787bf7dd.jpg
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
极客时间/assets/03df39f76b60ac2c0a61b75a4dc25869.jpg
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
极客时间/assets/04fb9d24065635a6a637c25ba9ddde68-1584367388076.png
Normal file
|
After Width: | Height: | Size: 235 KiB |
BIN
极客时间/assets/058efd9587ff02ebdaecc92af8184236.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
极客时间/assets/063d34003341274dc91a67561af1eee2.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
极客时间/assets/065ef246c59019effc8384967d774318-1584367399845.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
极客时间/assets/067ef9df4212cd4ede3cffcdac7001be.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
极客时间/assets/06b355394f525c54f200d8a1af63ddea.jpg
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
极客时间/assets/06ba300a78aef37b9d190aba61c37865-1584286945413.jpg
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
极客时间/assets/06ba300a78aef37b9d190aba61c37865.jpg
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
极客时间/assets/06cc25118730fbf611eb315705420ed2-1584286441684.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
极客时间/assets/077720a9965c6daf354a3bc2518e4843.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
极客时间/assets/078d10be60b8a6a804a709c7e896167b.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
极客时间/assets/07b00499b0ca857fc3ccd51f7046d946.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
极客时间/assets/07bcdba5b563ebae36f5b5b453aacd9d.png
Normal file
|
After Width: | Height: | Size: 214 KiB |
BIN
极客时间/assets/08044dcbdbaaedb30222695be29bc119.jpg
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
极客时间/assets/080ce3bbe673de38e196b5b741a86313.jpg
Normal file
|
After Width: | Height: | Size: 501 KiB |
BIN
极客时间/assets/08ef911dcf7311485b8b7831c422e43f.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
极客时间/assets/095d2a687f311d22481b51d97d9a9141.jpg
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
极客时间/assets/0a85d5cda10eef1a24d84fe0100b9917.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
极客时间/assets/0b2688db42e3c9b29f19d46eed0aae84.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
极客时间/assets/0b32d6e35ff0bbc5d46cfb87f6669d9e.jpg
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
极客时间/assets/0bc51f8f887aae04ef89a1a88cb5a17a.jpg
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
极客时间/assets/0c3e56272b08e58461e38bbbfd6c796f.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
极客时间/assets/0c62b601afda86fe5d0fe57346ace957-1584367388037.png
Normal file
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 609 KiB |
BIN
极客时间/assets/0d2070e8f84c4801adbfa03bda1f98d9-1584367388001.png
Normal file
|
After Width: | Height: | Size: 609 KiB |
BIN
极客时间/assets/0d686918ad9f7ea29791422d6eb41f36-1584286230034.jpg
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
极客时间/assets/0d796060073668ca169166a8903fbf3d-1584367394597.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
极客时间/assets/0dd290f8842959cb02d6c3a434a58e68.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
极客时间/assets/0ecb6d11e5e7725107c0291c45aa7e99.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
极客时间/assets/0faf56cd9521e665f739b03dd04470ba.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
极客时间/assets/101e6beb60f0bc482ef6cb0e793d5864.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
极客时间/assets/1028f169ec53723efa71acc680e718f2.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
极客时间/assets/1070a53f237a8ef75845f49b71961292.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
极客时间/assets/10aa7eac3fd38dfc2a09d6475ff4d93a-1584286228468.jpg
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
极客时间/assets/10e14e8b9691ac6337d457172b641a3d-1584367401994.jpg
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
极客时间/assets/10ff27d1032bf32393195f23ef2f9874.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
极客时间/assets/116a168c0eb55fabd7786fca728bd850-1584286230034.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
极客时间/assets/121dcefacba1b554accd0a90ef349fbd.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
极客时间/assets/12526a857a7685af0d7c2ee389c0ca9d.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
极客时间/assets/12eb6a38c347203f60df72ecaea95565-1584367400318.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
极客时间/assets/1334b9c08b8fd837832fdb2d82e6b0aa-1584367399834.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
极客时间/assets/1340fd4a4f05a977669aff367fc2697c.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
极客时间/assets/134583875561de914991fc2e192cf842-1584367389802.jpg
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
极客时间/assets/139a061b33fd0d711136c2846ff90c37.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
极客时间/assets/13ea5d45ff39006f14368f44169e5813.png
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
极客时间/assets/14221e482876b0b243f5213c7a1cc62e.jpg
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
极客时间/assets/14362fab592dee5226bb498e3e46e994.jpg
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
极客时间/assets/144679d37d552e4d5c436cab88582f04.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
极客时间/assets/14bc3d26efe093d3eada173f869146b1.png
Normal file
|
After Width: | Height: | Size: 763 KiB |
BIN
极客时间/assets/14cd598e52a2b72dd334a42603e5b894-1584367389796.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
极客时间/assets/14d88076dad6db573f0b66f2c17df916-1584367406948.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
极客时间/assets/150f28d9e745952f5968eff05e3f0ad2.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
极客时间/assets/15138305829ed15f45dd53ec38bd8379.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
极客时间/assets/15ae4f17c46bf71e8349a8f2ef70d573-1584367400733.jpg
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
极客时间/assets/15e254a8e92e031b20feb6ebdcc32402.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
极客时间/assets/16dbf8124ad529fec0066950446079d4-1584367389624.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
极客时间/assets/16dcd6fb8105a1caa75887b5ffa0bd7b.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
极客时间/assets/172a61261b64a6847a625afd17131c41.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
极客时间/assets/1761edbd7734276ae0a213af3cdd3311-1584367401565.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
极客时间/assets/1788deca56cb83c114d8353c92e3bde3-1584367406946.jpg
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
极客时间/assets/17ac2f46ef531e2b4380300f10267e3d.jpg
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
极客时间/assets/17f88dc70c3fbe06a7738a0ac01db4d0-1584367400329.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
极客时间/assets/1802a35475ee2755fb45aec55ed2d98a.png
Normal file
|
After Width: | Height: | Size: 340 KiB |
BIN
极客时间/assets/183a704d4495bebbc13c524695b5b6c3-1584367400345.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
极客时间/assets/184a02e0ee2404b46409cbf3d34837cb.jpg
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
极客时间/assets/18b64aee22c67f488171a73133e4d465.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
极客时间/assets/19084718d4682168fea4bb6cb27c4fba.png
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
极客时间/assets/1910bc1a0048d4de7b2128eb0f5dbcd2-1584286484075.jpg
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
极客时间/assets/19cd98970ceceaed8247a586ba3895b1.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
极客时间/assets/1a0ba797b9a0f0e32c9e561b97955917-1584286910692.jpg
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
极客时间/assets/1a0ba797b9a0f0e32c9e561b97955917.jpg
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
极客时间/assets/1a5d299c2eb5480eda93a8f8e3b3ca1a-1584286829605.jpg
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
极客时间/assets/1a5d299c2eb5480eda93a8f8e3b3ca1a.jpg
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
极客时间/assets/1a85a3bac30a32438bfd8862e5a34eef-1584367398753.png
Normal file
|
After Width: | Height: | Size: 237 KiB |
BIN
极客时间/assets/1a97a0b90c2304cbdf22a2bc8a8ce94b.jpg
Normal file
|
After Width: | Height: | Size: 29 KiB |